[ruby/prism] Add the ability to convert nodes to dot

https://github.com/ruby/prism/commit/3e4b4fb947
This commit is contained in:
Kevin Newton 2023-11-03 13:36:28 -04:00 коммит произвёл git
Родитель 2fb1d37439
Коммит 94f82a65f7
5 изменённых файлов: 190 добавлений и 0 удалений

Просмотреть файл

@ -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"

Просмотреть файл

@ -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",

Просмотреть файл

@ -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

Просмотреть файл

@ -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| -%>

Просмотреть файл

@ -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",