summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKevin Newton <kddnewton@gmail.com>2023-11-03 13:36:28 -0400
committergit <svn-admin@ruby-lang.org>2023-11-12 02:53:33 +0000
commit94f82a65f7b0b896c8cd44831c35c18661d0ecf2 (patch)
tree727df9d3c234a29c4d81136371ff51e32aec9532
parent2fb1d374393da45f4931cbbc7e573e37ca97e00a (diff)
[ruby/prism] Add the ability to convert nodes to dot
https://github.com/ruby/prism/commit/3e4b4fb947
-rw-r--r--lib/prism.rb1
-rw-r--r--lib/prism/prism.gemspec1
-rw-r--r--prism/templates/lib/prism/dot_visitor.rb.erb182
-rw-r--r--prism/templates/lib/prism/node.rb.erb5
-rwxr-xr-xprism/templates/template.rb1
5 files changed, 190 insertions, 0 deletions
diff --git a/lib/prism.rb b/lib/prism.rb
index 350febcaa8..909b71d66d 100644
--- a/lib/prism.rb
+++ b/lib/prism.rb
@@ -16,6 +16,7 @@ module Prism
autoload :Debug, "prism/debug"
autoload :DesugarCompiler, "prism/desugar_compiler"
autoload :Dispatcher, "prism/dispatcher"
+ autoload :DotVisitor, "prism/dot_visitor"
autoload :DSL, "prism/dsl"
autoload :LexCompat, "prism/lex_compat"
autoload :LexRipper, "prism/lex_compat"
diff --git a/lib/prism/prism.gemspec b/lib/prism/prism.gemspec
index 23e7b3833b..65cc61a825 100644
--- a/lib/prism/prism.gemspec
+++ b/lib/prism/prism.gemspec
@@ -67,6 +67,7 @@ Gem::Specification.new do |spec|
"lib/prism/debug.rb",
"lib/prism/desugar_compiler.rb",
"lib/prism/dispatcher.rb",
+ "lib/prism/dot_visitor.rb",
"lib/prism/dsl.rb",
"lib/prism/ffi.rb",
"lib/prism/lex_compat.rb",
diff --git a/prism/templates/lib/prism/dot_visitor.rb.erb b/prism/templates/lib/prism/dot_visitor.rb.erb
new file mode 100644
index 0000000000..45050935cc
--- /dev/null
+++ b/prism/templates/lib/prism/dot_visitor.rb.erb
@@ -0,0 +1,182 @@
+require "cgi"
+
+module Prism
+ # This visitor provides the ability to call Node#to_dot, which converts a
+ # subtree into a graphviz dot graph.
+ class DotVisitor < Visitor
+ class Field # :nodoc:
+ attr_reader :name, :value, :port
+
+ def initialize(name, value, port)
+ @name = name
+ @value = value
+ @port = port
+ end
+
+ def to_dot
+ if port
+ "<tr><td align=\"left\" colspan=\"2\" port=\"#{name}\">#{name}</td></tr>"
+ else
+ "<tr><td align=\"left\">#{name}</td><td>#{CGI.escapeHTML(value)}</td></tr>"
+ end
+ end
+ end
+
+ class Table # :nodoc:
+ attr_reader :name, :fields
+
+ def initialize(name)
+ @name = name
+ @fields = []
+ end
+
+ def field(name, value = nil, port: false)
+ fields << Field.new(name, value, port)
+ end
+
+ def to_dot
+ dot = <<~DOT
+ <table border="0" cellborder="1" cellspacing="0" cellpadding="4">
+ <tr><td colspan="2"><b>#{name}</b></td></tr>
+ DOT
+
+ if fields.any?
+ "#{dot} #{fields.map(&:to_dot).join("\n ")}\n</table>"
+ else
+ "#{dot}</table>"
+ end
+ end
+ end
+
+ class Digraph # :nodoc:
+ attr_reader :nodes, :waypoints, :edges
+
+ def initialize
+ @nodes = []
+ @waypoints = []
+ @edges = []
+ end
+
+ def node(value)
+ nodes << value
+ end
+
+ def waypoint(value)
+ waypoints << value
+ end
+
+ def edge(value)
+ edges << value
+ end
+
+ def to_dot
+ <<~DOT
+ digraph "Prism" {
+ node [
+ fontname=\"Courier New\"
+ shape=plain
+ style=filled
+ fillcolor=gray95
+ ];
+
+ #{nodes.map { |node| node.gsub(/\n/, "\n ") }.join("\n ")}
+ node [shape=point];
+ #{waypoints.join("\n ")}
+
+ #{edges.join("\n ")}
+ }
+ DOT
+ end
+ end
+
+ private_constant :Field, :Table, :Digraph
+
+ # The digraph that is being built.
+ attr_reader :digraph
+
+ # Initialize a new dot visitor.
+ def initialize
+ @digraph = Digraph.new
+ end
+
+ # Convert this visitor into a graphviz dot graph string.
+ def to_dot
+ digraph.to_dot
+ end
+ <%- nodes.each do |node| -%>
+
+ # Visit a <%= node.name %> node.
+ def visit_<%= node.human %>(node)
+ table = Table.new("<%= node.name %>")
+ id = node_id(node)
+ <%- node.fields.each do |field| -%>
+
+ # <%= field.name %>
+ <%- case field -%>
+ <%- when Prism::NodeField -%>
+ table.field("<%= field.name %>", port: true)
+ digraph.edge("#{id}:<%= field.name %> -> #{node_id(node.<%= field.name %>)};")
+ <%- when Prism::OptionalNodeField -%>
+ unless (<%= field.name %> = node.<%= field.name %>).nil?
+ table.field("<%= field.name %>", port: true)
+ digraph.edge("#{id}:<%= field.name %> -> #{node_id(<%= field.name %>)};")
+ end
+ <%- when Prism::NodeListField -%>
+ table.field("<%= field.name %>", port: true)
+
+ waypoint = "#{id}_<%= field.name %>"
+ digraph.waypoint("#{waypoint};")
+
+ digraph.edge("#{id}:<%= field.name %> -> #{waypoint};")
+ node.<%= field.name %>.each { |child| digraph.edge("#{waypoint} -> #{node_id(child)};") }
+ <%- when Prism::StringField, Prism::ConstantField, Prism::OptionalConstantField, Prism::UInt32Field, Prism::ConstantListField -%>
+ table.field("<%= field.name %>", node.<%= field.name %>.inspect)
+ <%- when Prism::LocationField -%>
+ table.field("<%= field.name %>", location_inspect(node.<%= field.name %>))
+ <%- when Prism::OptionalLocationField -%>
+ unless (<%= field.name %> = node.<%= field.name %>).nil?
+ table.field("<%= field.name %>", location_inspect(<%= field.name %>))
+ end
+ <%- when Prism::FlagsField -%>
+ <%- flag = flags.find { |flag| flag.name == field.kind }.tap { |flag| raise "Expected to find #{field.kind}" unless flag } -%>
+ table.field("<%= field.name %>", <%= flag.human %>_inspect(node))
+ <%- else -%>
+ <%- raise -%>
+ <%- end -%>
+ <%- end -%>
+
+ digraph.nodes << <<~DOT
+ #{id} [
+ label=<#{table.to_dot.gsub(/\n/, "\n ")}>
+ ];
+ DOT
+
+ super
+ end
+ <%- end -%>
+
+ private
+
+ # Generate a unique node ID for a node throughout the digraph.
+ def node_id(node)
+ "Node_#{node.object_id}"
+ end
+
+ # Inspect a location to display the start and end line and column numbers.
+ def location_inspect(location)
+ "(#{location.start_line},#{location.start_column})-(#{location.end_line},#{location.end_column})"
+ end
+ <%- flags.each do |flag| -%>
+
+ # Inspect a node that has <%= flag.human %> flags to display the flags as a
+ # comma-separated list.
+ def <%= flag.human %>_inspect(node)
+ flags = []
+ <%- flag.values.each do |value| -%>
+ flags << "<%= value.name.downcase %>" if node.<%= value.name.downcase %>?
+ <%- end -%>
+ flags.join(", ")
+ end
+ <%- end -%>
+ end
+end
diff --git a/prism/templates/lib/prism/node.rb.erb b/prism/templates/lib/prism/node.rb.erb
index e41383a79b..5b89b3ab36 100644
--- a/prism/templates/lib/prism/node.rb.erb
+++ b/prism/templates/lib/prism/node.rb.erb
@@ -31,6 +31,11 @@ module Prism
end
q.current_group.break
end
+
+ # Convert this node into a graphviz dot graph string.
+ def to_dot
+ DotVisitor.new.tap { |visitor| accept(visitor) }.to_dot
+ end
end
<%- nodes.each do |node| -%>
diff --git a/prism/templates/template.rb b/prism/templates/template.rb
index 8873d562cc..38459311f7 100755
--- a/prism/templates/template.rb
+++ b/prism/templates/template.rb
@@ -426,6 +426,7 @@ module Prism
"java/org/prism/AbstractNodeVisitor.java",
"lib/prism/compiler.rb",
"lib/prism/dispatcher.rb",
+ "lib/prism/dot_visitor.rb",
"lib/prism/dsl.rb",
"lib/prism/mutation_compiler.rb",
"lib/prism/node.rb",