From 7f4fe77b58e930106d391e4e91f055e7e0bf0e74 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Fri, 10 Feb 2023 10:11:40 -0500 Subject: [PATCH 01/11] Move mermaid rendering into its own file --- lib/syntax_tree.rb | 60 ++++++++------- lib/syntax_tree/mermaid.rb | 85 ++++++++++++++++++++++ lib/syntax_tree/visitor/mermaid_visitor.rb | 37 ++++------ 3 files changed, 130 insertions(+), 52 deletions(-) create mode 100644 lib/syntax_tree/mermaid.rb diff --git a/lib/syntax_tree.rb b/lib/syntax_tree.rb index e5bc5ab5..edf7688e 100644 --- a/lib/syntax_tree.rb +++ b/lib/syntax_tree.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -require "cgi" require "etc" require "json" require "pp" @@ -71,19 +70,6 @@ module SyntaxTree # that Syntax Tree can format arbitrary parts of a document. DEFAULT_INDENTATION = 0 - # This is a hook provided so that plugins can register themselves as the - # handler for a particular file type. - def self.register_handler(extension, handler) - HANDLERS[extension] = handler - end - - # Parses the given source and returns the syntax tree. - def self.parse(source) - parser = Parser.new(source) - response = parser.parse - response unless parser.error? - end - # Parses the given source and returns the formatted source. def self.format( source, @@ -98,6 +84,20 @@ def self.format( formatter.output.join end + # Indexes the given source code to return a list of all class, module, and + # method definitions. Used to quickly provide indexing capability for IDEs or + # documentation generation. + def self.index(source) + Index.index(source) + end + + # Indexes the given file to return a list of all class, module, and method + # definitions. Used to quickly provide indexing capability for IDEs or + # documentation generation. + def self.index_file(filepath) + Index.index_file(filepath) + end + # A convenience method for creating a new mutation visitor. def self.mutation visitor = Visitor::MutationVisitor.new @@ -105,6 +105,18 @@ def self.mutation visitor end + # Parses the given source and returns the syntax tree. + def self.parse(source) + parser = Parser.new(source) + response = parser.parse + response unless parser.error? + end + + # Parses the given file and returns the syntax tree. + def self.parse_file(filepath) + parse(read(filepath)) + end + # Returns the source from the given filepath taking into account any potential # magic encoding comments. def self.read(filepath) @@ -120,23 +132,15 @@ def self.read(filepath) File.read(filepath, encoding: encoding) end + # This is a hook provided so that plugins can register themselves as the + # handler for a particular file type. + def self.register_handler(extension, handler) + HANDLERS[extension] = handler + end + # Searches through the given source using the given pattern and yields each # node in the tree that matches the pattern to the given block. def self.search(source, query, &block) Search.new(Pattern.new(query).compile).scan(parse(source), &block) end - - # Indexes the given source code to return a list of all class, module, and - # method definitions. Used to quickly provide indexing capability for IDEs or - # documentation generation. - def self.index(source) - Index.index(source) - end - - # Indexes the given file to return a list of all class, module, and method - # definitions. Used to quickly provide indexing capability for IDEs or - # documentation generation. - def self.index_file(filepath) - Index.index_file(filepath) - end end diff --git a/lib/syntax_tree/mermaid.rb b/lib/syntax_tree/mermaid.rb new file mode 100644 index 00000000..fa923876 --- /dev/null +++ b/lib/syntax_tree/mermaid.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +require "cgi" + +module SyntaxTree + # This module is responsible for rendering mermaid flow charts. + module Mermaid + class Node + SHAPES = %i[circle rectangle stadium].freeze + + attr_reader :id, :label, :shape + + def initialize(id, label, shape) + raise unless SHAPES.include?(shape) + + @id = id + @label = label + @shape = shape + end + + def render + left_bound, right_bound = + case shape + when :circle + ["((", "))"] + when :rectangle + ["[", "]"] + when :stadium + ["([", "])"] + end + + " #{id}#{left_bound}\"#{CGI.escapeHTML(label)}\"#{right_bound}" + end + end + + class Edge + TYPES = %i[directed].freeze + + attr_reader :from, :to, :label, :type + + def initialize(from, to, label, type) + raise unless TYPES.include?(type) + + @from = from + @to = to + @label = label + @type = type + end + + def render + case type + when :directed + " #{from.id} -- \"#{CGI.escapeHTML(label)}\" --> #{to.id}" + end + end + end + + class FlowChart + attr_reader :nodes, :edges + + def initialize + @nodes = {} + @edges = [] + end + + def edge(from, to, label, type = :directed) + edges << Edge.new(from, to, label, type) + end + + def node(id, label, shape = :rectangle) + nodes[id] = Node.new(id, label, shape) + end + + def render + output = StringIO.new + output.puts("flowchart TD") + + nodes.each_value { |node| output.puts(node.render) } + edges.each { |edge| output.puts(edge.render) } + + output.string + end + end + end +end diff --git a/lib/syntax_tree/visitor/mermaid_visitor.rb b/lib/syntax_tree/visitor/mermaid_visitor.rb index 2b06049a..e63ee2a6 100644 --- a/lib/syntax_tree/visitor/mermaid_visitor.rb +++ b/lib/syntax_tree/visitor/mermaid_visitor.rb @@ -4,18 +4,16 @@ module SyntaxTree class Visitor # This visitor transforms the AST into a mermaid flow chart. class MermaidVisitor < FieldVisitor - attr_reader :output, :target + attr_reader :flowchart, :target def initialize - @output = StringIO.new - @output.puts("flowchart TD") - + @flowchart = Mermaid::FlowChart.new @target = nil end def visit_program(node) super - output.string + flowchart.render end private @@ -26,19 +24,13 @@ def comments(node) def field(name, value) case value - when Node - node_id = visit(value) - output.puts(" #{target} -- \"#{name}\" --> #{node_id}") - when String - node_id = "#{target}_#{name}" - output.puts(" #{node_id}([#{CGI.escapeHTML(value.inspect)}])") - output.puts(" #{target} -- \"#{name}\" --> #{node_id}") when nil # skip + when Node + flowchart.edge(target, visit(value), name) else - node_id = "#{target}_#{name}" - output.puts(" #{node_id}([\"#{CGI.escapeHTML(value.inspect)}\"])") - output.puts(" #{target} -- \"#{name}\" --> #{node_id}") + to = flowchart.node("#{target.id}_#{name}", value.inspect, :stadium) + flowchart.edge(target, to, name) end end @@ -52,11 +44,8 @@ def node(node, type) previous_target = target begin - @target = "node_#{node.object_id}" - + @target = flowchart.node("node_#{node.object_id}", type) yield - - output.puts(" #{@target}[\"#{type}\"]") @target ensure @target = previous_target @@ -65,11 +54,11 @@ def node(node, type) def pairs(name, values) values.each_with_index do |(key, value), index| - node_id = "#{target}_#{name}_#{index}" - output.puts(" #{node_id}((\" \"))") - output.puts(" #{target} -- \"#{name}[#{index}]\" --> #{node_id}") - output.puts(" #{node_id} -- \"[0]\" --> #{visit(key)}") - output.puts(" #{node_id} -- \"[1]\" --> #{visit(value)}") if value + to = flowchart.node("#{target.id}_#{name}_#{index}", " ", :circle) + + flowchart.edge(target, to, "#{name}[#{index}]") + flowchart.edge(to, visit(key), "[0]") + flowchart.edge(to, visit(value), "[1]") if value end end From 103236bb822f7cb7a449a559321e82f0bef75e4c Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Fri, 10 Feb 2023 10:26:34 -0500 Subject: [PATCH 02/11] Render CFG using new mermaid code --- lib/syntax_tree.rb | 1 + lib/syntax_tree/mermaid.rb | 75 +++++++++++++++------- lib/syntax_tree/visitor/mermaid_visitor.rb | 4 +- lib/syntax_tree/yarv/control_flow_graph.rb | 37 +++++------ 4 files changed, 74 insertions(+), 43 deletions(-) diff --git a/lib/syntax_tree.rb b/lib/syntax_tree.rb index edf7688e..9cbd49c7 100644 --- a/lib/syntax_tree.rb +++ b/lib/syntax_tree.rb @@ -23,6 +23,7 @@ require_relative "syntax_tree/visitor/environment" require_relative "syntax_tree/visitor/with_environment" +require_relative "syntax_tree/mermaid" require_relative "syntax_tree/parser" require_relative "syntax_tree/pattern" require_relative "syntax_tree/search" diff --git a/lib/syntax_tree/mermaid.rb b/lib/syntax_tree/mermaid.rb index fa923876..f5c85f2f 100644 --- a/lib/syntax_tree/mermaid.rb +++ b/lib/syntax_tree/mermaid.rb @@ -6,7 +6,7 @@ module SyntaxTree # This module is responsible for rendering mermaid flow charts. module Mermaid class Node - SHAPES = %i[circle rectangle stadium].freeze + SHAPES = %i[circle rectangle rounded stadium].freeze attr_reader :id, :label, :shape @@ -19,17 +19,23 @@ def initialize(id, label, shape) end def render - left_bound, right_bound = - case shape - when :circle - ["((", "))"] - when :rectangle - ["[", "]"] - when :stadium - ["([", "])"] - end + left_bound, right_bound = bounds + "#{id}#{left_bound}\"#{CGI.escapeHTML(label)}\"#{right_bound}" + end - " #{id}#{left_bound}\"#{CGI.escapeHTML(label)}\"#{right_bound}" + private + + def bounds + case shape + when :circle + ["((", "))"] + when :rectangle + ["[", "]"] + when :rounded + ["(", ")"] + when :stadium + ["([", "])"] + end end end @@ -50,34 +56,57 @@ def initialize(from, to, label, type) def render case type when :directed - " #{from.id} -- \"#{CGI.escapeHTML(label)}\" --> #{to.id}" + if label + "#{from.id} -- \"#{CGI.escapeHTML(label)}\" --> #{to.id}" + else + "#{from.id} --> #{to.id}" + end end end end class FlowChart - attr_reader :nodes, :edges + attr_reader :output, :prefix, :nodes def initialize + @output = StringIO.new + @output.puts("flowchart TD") + @prefix = " " @nodes = {} - @edges = [] end - def edge(from, to, label, type = :directed) - edges << Edge.new(from, to, label, type) + def edge(from, to, label = nil, type: :directed) + edge = Edge.new(from, to, label, type) + output.puts("#{prefix}#{edge.render}") end - def node(id, label, shape = :rectangle) - nodes[id] = Node.new(id, label, shape) + def fetch(id) + nodes.fetch(id) end - def render - output = StringIO.new - output.puts("flowchart TD") + def node(id, label, shape: :rectangle) + node = Node.new(id, label, shape) + nodes[id] = node + + output.puts("#{prefix}#{nodes[id].render}") + node + end + + def subgraph(id) + output.puts("#{prefix}subgraph #{id}") + + previous = prefix + @prefix = "#{prefix} " - nodes.each_value { |node| output.puts(node.render) } - edges.each { |edge| output.puts(edge.render) } + begin + yield + ensure + @prefix = previous + output.puts("#{prefix}end") + end + end + def render output.string end end diff --git a/lib/syntax_tree/visitor/mermaid_visitor.rb b/lib/syntax_tree/visitor/mermaid_visitor.rb index e63ee2a6..1694952d 100644 --- a/lib/syntax_tree/visitor/mermaid_visitor.rb +++ b/lib/syntax_tree/visitor/mermaid_visitor.rb @@ -29,7 +29,7 @@ def field(name, value) when Node flowchart.edge(target, visit(value), name) else - to = flowchart.node("#{target.id}_#{name}", value.inspect, :stadium) + to = flowchart.node("#{target.id}_#{name}", value.inspect, shape: :stadium) flowchart.edge(target, to, name) end end @@ -54,7 +54,7 @@ def node(node, type) def pairs(name, values) values.each_with_index do |(key, value), index| - to = flowchart.node("#{target.id}_#{name}_#{index}", " ", :circle) + to = flowchart.node("#{target.id}_#{name}_#{index}", " ", shape: :circle) flowchart.edge(target, to, "#{name}[#{index}]") flowchart.edge(to, visit(key), "[0]") diff --git a/lib/syntax_tree/yarv/control_flow_graph.rb b/lib/syntax_tree/yarv/control_flow_graph.rb index 73d30208..927f535a 100644 --- a/lib/syntax_tree/yarv/control_flow_graph.rb +++ b/lib/syntax_tree/yarv/control_flow_graph.rb @@ -208,25 +208,24 @@ def to_son end def to_mermaid - output = StringIO.new - output.puts("flowchart TD") + flowchart = Mermaid::FlowChart.new + disasm = Disassembler::Mermaid.new - fmt = Disassembler::Mermaid.new blocks.each do |block| - output.puts(" subgraph #{block.id}") - previous = nil - - block.each_with_length do |insn, length| - node_id = "node_#{length}" - label = "%04d %s" % [length, insn.disasm(fmt)] - - output.puts(" #{node_id}(\"#{CGI.escapeHTML(label)}\")") - output.puts(" #{previous} --> #{node_id}") if previous - - previous = node_id + flowchart.subgraph(block.id) do + previous = nil + + block.each_with_length do |insn, length| + node = + flowchart.node( + "node_#{length}", + "%04d %s" % [length, insn.disasm(disasm)] + ) + + flowchart.edge(previous, node) if previous + previous = node + end end - - output.puts(" end") end blocks.each do |block| @@ -235,11 +234,13 @@ def to_mermaid block.block_start + block.insns.sum(&:length) - block.insns.last.length - output.puts(" node_#{offset} --> node_#{outgoing.block_start}") + from = flowchart.fetch("node_#{offset}") + to = flowchart.fetch("node_#{outgoing.block_start}") + flowchart.edge(from, to) end end - output.string + flowchart.render end # This method is used to verify that the control flow graph is well From 6dbe713baf4dd6fd87183d77dfc38340d7bbbf6f Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Fri, 10 Feb 2023 10:29:21 -0500 Subject: [PATCH 03/11] Fix up data flow mermaid rendering --- lib/syntax_tree/yarv/data_flow_graph.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/syntax_tree/yarv/data_flow_graph.rb b/lib/syntax_tree/yarv/data_flow_graph.rb index ace40296..185eeee5 100644 --- a/lib/syntax_tree/yarv/data_flow_graph.rb +++ b/lib/syntax_tree/yarv/data_flow_graph.rb @@ -155,8 +155,8 @@ def to_mermaid end insn_flows[length].in.each do |input| - if input.is_a?(Integer) - output.puts(" node_#{input} --> #{node_id}") + if input.is_a?(LocalArgument) + output.puts(" node_#{input.length} --> #{node_id}") links << "green" end end From 72619fb4469786b62a3e97d63c30d62c404f31b3 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Fri, 10 Feb 2023 10:38:17 -0500 Subject: [PATCH 04/11] Render DFG with new mermaid renderer --- lib/syntax_tree/mermaid.rb | 88 +++++++++++++--------- lib/syntax_tree/visitor/mermaid_visitor.rb | 10 +-- lib/syntax_tree/yarv/control_flow_graph.rb | 4 +- lib/syntax_tree/yarv/data_flow_graph.rb | 57 ++++++-------- 4 files changed, 84 insertions(+), 75 deletions(-) diff --git a/lib/syntax_tree/mermaid.rb b/lib/syntax_tree/mermaid.rb index f5c85f2f..28cc095a 100644 --- a/lib/syntax_tree/mermaid.rb +++ b/lib/syntax_tree/mermaid.rb @@ -5,6 +5,39 @@ module SyntaxTree # This module is responsible for rendering mermaid flow charts. module Mermaid + def self.escape(label) + "\"#{CGI.escapeHTML(label)}\"" + end + + class Link + TYPES = %i[directed].freeze + COLORS = %i[green red].freeze + + attr_reader :from, :to, :label, :type, :color + + def initialize(from, to, label, type, color) + raise if !TYPES.include?(type) + raise if color && !COLORS.include?(color) + + @from = from + @to = to + @label = label + @type = type + @color = color + end + + def render + case type + when :directed + if label + "#{from.id} -- #{Mermaid.escape(label)} --> #{to.id}" + else + "#{from.id} --> #{to.id}" + end + end + end + end + class Node SHAPES = %i[circle rectangle rounded stadium].freeze @@ -20,7 +53,7 @@ def initialize(id, label, shape) def render left_bound, right_bound = bounds - "#{id}#{left_bound}\"#{CGI.escapeHTML(label)}\"#{right_bound}" + "#{id}#{left_bound}#{Mermaid.escape(label)}#{right_bound}" end private @@ -39,51 +72,30 @@ def bounds end end - class Edge - TYPES = %i[directed].freeze - - attr_reader :from, :to, :label, :type - - def initialize(from, to, label, type) - raise unless TYPES.include?(type) - - @from = from - @to = to - @label = label - @type = type - end - - def render - case type - when :directed - if label - "#{from.id} -- \"#{CGI.escapeHTML(label)}\" --> #{to.id}" - else - "#{from.id} --> #{to.id}" - end - end - end - end - class FlowChart - attr_reader :output, :prefix, :nodes + attr_reader :output, :prefix, :nodes, :links def initialize @output = StringIO.new @output.puts("flowchart TD") @prefix = " " - @nodes = {} - end - def edge(from, to, label = nil, type: :directed) - edge = Edge.new(from, to, label, type) - output.puts("#{prefix}#{edge.render}") + @nodes = {} + @links = [] end def fetch(id) nodes.fetch(id) end + def link(from, to, label = nil, type: :directed, color: nil) + link = Link.new(from, to, label, type, color) + links << link + + output.puts("#{prefix}#{link.render}") + link + end + def node(id, label, shape: :rectangle) node = Node.new(id, label, shape) nodes[id] = node @@ -92,8 +104,8 @@ def node(id, label, shape: :rectangle) node end - def subgraph(id) - output.puts("#{prefix}subgraph #{id}") + def subgraph(label) + output.puts("#{prefix}subgraph #{Mermaid.escape(label)}") previous = prefix @prefix = "#{prefix} " @@ -107,6 +119,12 @@ def subgraph(id) end def render + links.each_with_index do |link, index| + if link.color + output.puts("#{prefix}linkStyle #{index} stroke:#{link.color}") + end + end + output.string end end diff --git a/lib/syntax_tree/visitor/mermaid_visitor.rb b/lib/syntax_tree/visitor/mermaid_visitor.rb index 1694952d..542fe192 100644 --- a/lib/syntax_tree/visitor/mermaid_visitor.rb +++ b/lib/syntax_tree/visitor/mermaid_visitor.rb @@ -27,10 +27,10 @@ def field(name, value) when nil # skip when Node - flowchart.edge(target, visit(value), name) + flowchart.link(target, visit(value), name) else to = flowchart.node("#{target.id}_#{name}", value.inspect, shape: :stadium) - flowchart.edge(target, to, name) + flowchart.link(target, to, name) end end @@ -56,9 +56,9 @@ def pairs(name, values) values.each_with_index do |(key, value), index| to = flowchart.node("#{target.id}_#{name}_#{index}", " ", shape: :circle) - flowchart.edge(target, to, "#{name}[#{index}]") - flowchart.edge(to, visit(key), "[0]") - flowchart.edge(to, visit(value), "[1]") if value + flowchart.link(target, to, "#{name}[#{index}]") + flowchart.link(to, visit(key), "[0]") + flowchart.link(to, visit(value), "[1]") if value end end diff --git a/lib/syntax_tree/yarv/control_flow_graph.rb b/lib/syntax_tree/yarv/control_flow_graph.rb index 927f535a..5da2cc14 100644 --- a/lib/syntax_tree/yarv/control_flow_graph.rb +++ b/lib/syntax_tree/yarv/control_flow_graph.rb @@ -222,7 +222,7 @@ def to_mermaid "%04d %s" % [length, insn.disasm(disasm)] ) - flowchart.edge(previous, node) if previous + flowchart.link(previous, node) if previous previous = node end end @@ -236,7 +236,7 @@ def to_mermaid from = flowchart.fetch("node_#{offset}") to = flowchart.fetch("node_#{outgoing.block_start}") - flowchart.edge(from, to) + flowchart.link(from, to) end end diff --git a/lib/syntax_tree/yarv/data_flow_graph.rb b/lib/syntax_tree/yarv/data_flow_graph.rb index 185eeee5..4adf2bcf 100644 --- a/lib/syntax_tree/yarv/data_flow_graph.rb +++ b/lib/syntax_tree/yarv/data_flow_graph.rb @@ -125,11 +125,8 @@ def to_son end def to_mermaid - output = StringIO.new - output.puts("flowchart TD") - - fmt = Disassembler::Mermaid.new - links = [] + flowchart = Mermaid::FlowChart.new + disasm = Disassembler::Mermaid.new blocks.each do |block| block_flow = block_flows.fetch(block.id) @@ -140,31 +137,28 @@ def to_mermaid block.id end - output.puts(" subgraph \"#{CGI.escapeHTML(graph_name)}\"") - previous = nil - - block.each_with_length do |insn, length| - node_id = "node_#{length}" - label = "%04d %s" % [length, insn.disasm(fmt)] - - output.puts(" #{node_id}(\"#{CGI.escapeHTML(label)}\")") + flowchart.subgraph(graph_name) do + previous = nil - if previous - output.puts(" #{previous} --> #{node_id}") - links << "red" - end - - insn_flows[length].in.each do |input| - if input.is_a?(LocalArgument) - output.puts(" node_#{input.length} --> #{node_id}") - links << "green" + block.each_with_length do |insn, length| + node = + flowchart.node( + "node_#{length}", + "%04d %s" % [length, insn.disasm(disasm)], + shape: :rounded + ) + + flowchart.link(previous, node, color: :red) if previous + insn_flows[length].in.each do |input| + if input.is_a?(LocalArgument) + from = flowchart.fetch("node_#{input.length}") + flowchart.link(from, node, color: :green) + end end - end - previous = node_id + previous = node + end end - - output.puts(" end") end blocks.each do |block| @@ -173,16 +167,13 @@ def to_mermaid block.block_start + block.insns.sum(&:length) - block.insns.last.length - output.puts(" node_#{offset} --> node_#{outgoing.block_start}") - links << "red" + from = flowchart.fetch("node_#{offset}") + to = flowchart.fetch("node_#{outgoing.block_start}") + flowchart.link(from, to, color: :red) end end - links.each_with_index do |color, index| - output.puts(" linkStyle #{index} stroke:#{color}") - end - - output.string + flowchart.render end # Verify that we constructed the data flow graph correctly. From a8fd78b0c6e4070fdf92d17bb4de834946e154df Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Fri, 10 Feb 2023 10:47:00 -0500 Subject: [PATCH 05/11] Render sea of nodes to mermaid using new API --- .rubocop.yml | 3 + lib/syntax_tree/mermaid.rb | 170 +++++++++++++-------- lib/syntax_tree/visitor/mermaid_visitor.rb | 11 +- lib/syntax_tree/yarv/control_flow_graph.rb | 55 ++++--- lib/syntax_tree/yarv/data_flow_graph.rb | 79 +++++----- lib/syntax_tree/yarv/disassembler.rb | 6 +- lib/syntax_tree/yarv/sea_of_nodes.rb | 67 +++----- test/yarv_test.rb | 100 ++++++------ 8 files changed, 261 insertions(+), 230 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 33636c44..21beca1b 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -117,6 +117,9 @@ Style/FormatStringToken: Style/GuardClause: Enabled: false +Style/HashLikeCase: + Enabled: false + Style/IdenticalConditionalBranches: Enabled: false diff --git a/lib/syntax_tree/mermaid.rb b/lib/syntax_tree/mermaid.rb index 28cc095a..70cbc054 100644 --- a/lib/syntax_tree/mermaid.rb +++ b/lib/syntax_tree/mermaid.rb @@ -3,20 +3,85 @@ require "cgi" module SyntaxTree - # This module is responsible for rendering mermaid flow charts. + # This module is responsible for rendering mermaid (https://mermaid.js.org/) + # flow charts. module Mermaid - def self.escape(label) - "\"#{CGI.escapeHTML(label)}\"" + # This is the main class that handles rendering a flowchart. It keeps track + # of its nodes and links and renders them according to the mermaid syntax. + class FlowChart + attr_reader :output, :prefix, :nodes, :links + + def initialize + @output = StringIO.new + @output.puts("flowchart TD") + @prefix = " " + + @nodes = {} + @links = [] + end + + # Retrieve a node that has already been added to the flowchart by its id. + def fetch(id) + nodes.fetch(id) + end + + # Add a link to the flowchart between two nodes with an optional label. + def link(from, to, label = nil, type: :directed, color: nil) + link = Link.new(from, to, label, type, color) + links << link + + output.puts("#{prefix}#{link.render}") + link + end + + # Add a node to the flowchart with an optional label. + def node(id, label = " ", shape: :rectangle) + node = Node.new(id, label, shape) + nodes[id] = node + + output.puts("#{prefix}#{nodes[id].render}") + node + end + + # Add a subgraph to the flowchart. Within the given block, all of the + # nodes will be rendered within the subgraph. + def subgraph(label) + output.puts("#{prefix}subgraph #{Mermaid.escape(label)}") + + previous = prefix + @prefix = "#{prefix} " + + begin + yield + ensure + @prefix = previous + output.puts("#{prefix}end") + end + end + + # Return the rendered flowchart. + def render + links.each_with_index do |link, index| + if link.color + output.puts("#{prefix}linkStyle #{index} stroke:#{link.color}") + end + end + + output.string + end end + # This class represents a link between two nodes in a flowchart. It is not + # meant to be interacted with directly, but rather used as a data structure + # by the FlowChart class. class Link - TYPES = %i[directed].freeze + TYPES = %i[directed dotted].freeze COLORS = %i[green red].freeze attr_reader :from, :to, :label, :type, :color def initialize(from, to, label, type, color) - raise if !TYPES.include?(type) + raise unless TYPES.include?(type) raise if color && !COLORS.include?(color) @from = from @@ -27,17 +92,31 @@ def initialize(from, to, label, type, color) end def render + left_side, right_side, full_side = sides + + if label + escaped = Mermaid.escape(label) + "#{from.id} #{left_side} #{escaped} #{right_side} #{to.id}" + else + "#{from.id} #{full_side} #{to.id}" + end + end + + private + + def sides case type when :directed - if label - "#{from.id} -- #{Mermaid.escape(label)} --> #{to.id}" - else - "#{from.id} --> #{to.id}" - end + %w[-- --> -->] + when :dotted + %w[-. .-> -.->] end end end + # This class represents a node in a flowchart. Unlike the Link class, it can + # be used directly. It is the return value of the #node method, and is meant + # to be passed around to #link methods to create links between nodes. class Node SHAPES = %i[circle rectangle rounded stadium].freeze @@ -61,72 +140,37 @@ def render def bounds case shape when :circle - ["((", "))"] + %w[(( ))] when :rectangle ["[", "]"] when :rounded - ["(", ")"] + %w[( )] when :stadium ["([", "])"] end end end - class FlowChart - attr_reader :output, :prefix, :nodes, :links - - def initialize - @output = StringIO.new - @output.puts("flowchart TD") - @prefix = " " - - @nodes = {} - @links = [] - end - - def fetch(id) - nodes.fetch(id) - end - - def link(from, to, label = nil, type: :directed, color: nil) - link = Link.new(from, to, label, type, color) - links << link - - output.puts("#{prefix}#{link.render}") - link + class << self + # Escape a label to be used in the mermaid syntax. This is used to escape + # HTML entities such that they render properly within the quotes. + def escape(label) + "\"#{CGI.escapeHTML(label)}\"" end - def node(id, label, shape: :rectangle) - node = Node.new(id, label, shape) - nodes[id] = node - - output.puts("#{prefix}#{nodes[id].render}") - node - end - - def subgraph(label) - output.puts("#{prefix}subgraph #{Mermaid.escape(label)}") - - previous = prefix - @prefix = "#{prefix} " - - begin - yield - ensure - @prefix = previous - output.puts("#{prefix}end") + # Create a new flowchart. If a block is given, it will be yielded to and + # the flowchart will be rendered. Otherwise, the flowchart will be + # returned. + def flowchart + flowchart = FlowChart.new + + if block_given? + yield flowchart + flowchart.render + else + flowchart end end - - def render - links.each_with_index do |link, index| - if link.color - output.puts("#{prefix}linkStyle #{index} stroke:#{link.color}") - end - end - - output.string - end end end end diff --git a/lib/syntax_tree/visitor/mermaid_visitor.rb b/lib/syntax_tree/visitor/mermaid_visitor.rb index 542fe192..504e2fb0 100644 --- a/lib/syntax_tree/visitor/mermaid_visitor.rb +++ b/lib/syntax_tree/visitor/mermaid_visitor.rb @@ -7,7 +7,7 @@ class MermaidVisitor < FieldVisitor attr_reader :flowchart, :target def initialize - @flowchart = Mermaid::FlowChart.new + @flowchart = Mermaid.flowchart @target = nil end @@ -29,7 +29,12 @@ def field(name, value) when Node flowchart.link(target, visit(value), name) else - to = flowchart.node("#{target.id}_#{name}", value.inspect, shape: :stadium) + to = + flowchart.node( + "#{target.id}_#{name}", + value.inspect, + shape: :stadium + ) flowchart.link(target, to, name) end end @@ -54,7 +59,7 @@ def node(node, type) def pairs(name, values) values.each_with_index do |(key, value), index| - to = flowchart.node("#{target.id}_#{name}_#{index}", " ", shape: :circle) + to = flowchart.node("#{target.id}_#{name}_#{index}", shape: :circle) flowchart.link(target, to, "#{name}[#{index}]") flowchart.link(to, visit(key), "[0]") diff --git a/lib/syntax_tree/yarv/control_flow_graph.rb b/lib/syntax_tree/yarv/control_flow_graph.rb index 5da2cc14..2829bb21 100644 --- a/lib/syntax_tree/yarv/control_flow_graph.rb +++ b/lib/syntax_tree/yarv/control_flow_graph.rb @@ -208,39 +208,38 @@ def to_son end def to_mermaid - flowchart = Mermaid::FlowChart.new - disasm = Disassembler::Mermaid.new - - blocks.each do |block| - flowchart.subgraph(block.id) do - previous = nil - - block.each_with_length do |insn, length| - node = - flowchart.node( - "node_#{length}", - "%04d %s" % [length, insn.disasm(disasm)] - ) - - flowchart.link(previous, node) if previous - previous = node + Mermaid.flowchart do |flowchart| + disasm = Disassembler::Squished.new + + blocks.each do |block| + flowchart.subgraph(block.id) do + previous = nil + + block.each_with_length do |insn, length| + node = + flowchart.node( + "node_#{length}", + "%04d %s" % [length, insn.disasm(disasm)] + ) + + flowchart.link(previous, node) if previous + previous = node + end end end - end - blocks.each do |block| - block.outgoing_blocks.each do |outgoing| - offset = - block.block_start + block.insns.sum(&:length) - - block.insns.last.length - - from = flowchart.fetch("node_#{offset}") - to = flowchart.fetch("node_#{outgoing.block_start}") - flowchart.link(from, to) + blocks.each do |block| + block.outgoing_blocks.each do |outgoing| + offset = + block.block_start + block.insns.sum(&:length) - + block.insns.last.length + + from = flowchart.fetch("node_#{offset}") + to = flowchart.fetch("node_#{outgoing.block_start}") + flowchart.link(from, to) + end end end - - flowchart.render end # This method is used to verify that the control flow graph is well diff --git a/lib/syntax_tree/yarv/data_flow_graph.rb b/lib/syntax_tree/yarv/data_flow_graph.rb index 4adf2bcf..aedee9ba 100644 --- a/lib/syntax_tree/yarv/data_flow_graph.rb +++ b/lib/syntax_tree/yarv/data_flow_graph.rb @@ -125,55 +125,54 @@ def to_son end def to_mermaid - flowchart = Mermaid::FlowChart.new - disasm = Disassembler::Mermaid.new + Mermaid.flowchart do |flowchart| + disasm = Disassembler::Squished.new - blocks.each do |block| - block_flow = block_flows.fetch(block.id) - graph_name = - if block_flow.in.any? - "#{block.id} #{block_flows[block.id].in.join(", ")}" - else - block.id - end - - flowchart.subgraph(graph_name) do - previous = nil + blocks.each do |block| + block_flow = block_flows.fetch(block.id) + graph_name = + if block_flow.in.any? + "#{block.id} #{block_flows[block.id].in.join(", ")}" + else + block.id + end - block.each_with_length do |insn, length| - node = - flowchart.node( - "node_#{length}", - "%04d %s" % [length, insn.disasm(disasm)], - shape: :rounded - ) - - flowchart.link(previous, node, color: :red) if previous - insn_flows[length].in.each do |input| - if input.is_a?(LocalArgument) - from = flowchart.fetch("node_#{input.length}") - flowchart.link(from, node, color: :green) + flowchart.subgraph(graph_name) do + previous = nil + + block.each_with_length do |insn, length| + node = + flowchart.node( + "node_#{length}", + "%04d %s" % [length, insn.disasm(disasm)], + shape: :rounded + ) + + flowchart.link(previous, node, color: :red) if previous + insn_flows[length].in.each do |input| + if input.is_a?(LocalArgument) + from = flowchart.fetch("node_#{input.length}") + flowchart.link(from, node, color: :green) + end end - end - previous = node + previous = node + end end end - end - blocks.each do |block| - block.outgoing_blocks.each do |outgoing| - offset = - block.block_start + block.insns.sum(&:length) - - block.insns.last.length - - from = flowchart.fetch("node_#{offset}") - to = flowchart.fetch("node_#{outgoing.block_start}") - flowchart.link(from, to, color: :red) + blocks.each do |block| + block.outgoing_blocks.each do |outgoing| + offset = + block.block_start + block.insns.sum(&:length) - + block.insns.last.length + + from = flowchart.fetch("node_#{offset}") + to = flowchart.fetch("node_#{outgoing.block_start}") + flowchart.link(from, to, color: :red) + end end end - - flowchart.render end # Verify that we constructed the data flow graph correctly. diff --git a/lib/syntax_tree/yarv/disassembler.rb b/lib/syntax_tree/yarv/disassembler.rb index f60af0fd..dac220fd 100644 --- a/lib/syntax_tree/yarv/disassembler.rb +++ b/lib/syntax_tree/yarv/disassembler.rb @@ -4,9 +4,9 @@ module SyntaxTree module YARV class Disassembler # This class is another object that handles disassembling a YARV - # instruction sequence but it does so in order to provide a label for a - # mermaid diagram. - class Mermaid + # instruction sequence but it renders it without any of the extra spacing + # or alignment. + class Squished def calldata(value) value.inspect end diff --git a/lib/syntax_tree/yarv/sea_of_nodes.rb b/lib/syntax_tree/yarv/sea_of_nodes.rb index 181d729c..33ef14f7 100644 --- a/lib/syntax_tree/yarv/sea_of_nodes.rb +++ b/lib/syntax_tree/yarv/sea_of_nodes.rb @@ -27,7 +27,7 @@ def id end def label - "%04d %s" % [offset, insn.disasm(Disassembler::Mermaid.new)] + "%04d %s" % [offset, insn.disasm(Disassembler::Squished.new)] end end @@ -466,53 +466,34 @@ def initialize(dfg, nodes, local_graphs) end def to_mermaid - output = StringIO.new - output.puts("flowchart TD") - - nodes.each do |node| - escaped = "\"#{CGI.escapeHTML(node.label)}\"" - output.puts(" node_#{node.id}(#{escaped})") - end - - link_counter = 0 - nodes.each do |producer| - producer.outputs.each do |consumer_edge| - case consumer_edge.type - when :data - edge = "-->" - edge_style = "stroke:green;" - when :control - edge = "-->" - edge_style = "stroke:red;" - when :info - edge = "-.->" - else - raise - end - - label = - if !consumer_edge.label - "" - elsif consumer_edge.to.is_a?(PhiNode) - # Edges into phi nodes are labelled by the offset of the - # instruction going into the merge. - "|%04d| " % consumer_edge.label - else - "|#{consumer_edge.label}| " - end + Mermaid.flowchart do |flowchart| + nodes.each do |node| + flowchart.node("node_#{node.id}", node.label, shape: :rounded) + end - to_id = "node_#{consumer_edge.to.id}" - output.puts(" node_#{producer.id} #{edge} #{label}#{to_id}") + nodes.each do |producer| + producer.outputs.each do |consumer_edge| + label = + if !consumer_edge.label + # No label. + elsif consumer_edge.to.is_a?(PhiNode) + # Edges into phi nodes are labelled by the offset of the + # instruction going into the merge. + "%04d" % consumer_edge.label + else + consumer_edge.label.to_s + end - if edge_style - output.puts(" linkStyle #{link_counter} #{edge_style}") + flowchart.link( + flowchart.fetch("node_#{producer.id}"), + flowchart.fetch("node_#{consumer_edge.to.id}"), + label, + type: consumer_edge.type == :info ? :dotted : :directed, + color: { data: :green, control: :red }[consumer_edge.type] + ) end - - link_counter += 1 end end - - output.string end def verify diff --git a/test/yarv_test.rb b/test/yarv_test.rb index a1e89568..78622434 100644 --- a/test/yarv_test.rb +++ b/test/yarv_test.rb @@ -386,35 +386,35 @@ def test_son node_16("0016 leave") node_1000("1000 ψ") node_1001("1001 φ") - node_0 --> |0| node_3 - linkStyle 0 stroke:green; - node_2 --> |1| node_3 - linkStyle 1 stroke:green; + node_0 -- "0" --> node_3 + node_2 -- "1" --> node_3 node_3 --> node_5 - linkStyle 2 stroke:red; - node_3 --> |0| node_5 - linkStyle 3 stroke:green; - node_5 --> |branch0| node_11 - linkStyle 4 stroke:red; - node_5 --> |fallthrough| node_1000 - linkStyle 5 stroke:red; - node_7 --> |0009| node_1001 - linkStyle 6 stroke:green; - node_11 --> |branch0| node_1000 - linkStyle 7 stroke:red; - node_11 --> |0011| node_1001 - linkStyle 8 stroke:green; - node_12 --> |1| node_14 - linkStyle 9 stroke:green; + node_3 -- "0" --> node_5 + node_5 -- "branch0" --> node_11 + node_5 -- "fallthrough" --> node_1000 + node_7 -- "0009" --> node_1001 + node_11 -- "branch0" --> node_1000 + node_11 -- "0011" --> node_1001 + node_12 -- "1" --> node_14 node_14 --> node_16 - linkStyle 10 stroke:red; - node_14 --> |0| node_16 - linkStyle 11 stroke:green; + node_14 -- "0" --> node_16 node_1000 --> node_14 - linkStyle 12 stroke:red; node_1001 -.-> node_1000 - node_1001 --> |0| node_14 - linkStyle 14 stroke:green; + node_1001 -- "0" --> node_14 + linkStyle 0 stroke:green + linkStyle 1 stroke:green + linkStyle 2 stroke:red + linkStyle 3 stroke:green + linkStyle 4 stroke:red + linkStyle 5 stroke:red + linkStyle 6 stroke:green + linkStyle 7 stroke:red + linkStyle 8 stroke:green + linkStyle 9 stroke:green + linkStyle 10 stroke:red + linkStyle 11 stroke:green + linkStyle 12 stroke:red + linkStyle 14 stroke:green MERMAID end @@ -438,35 +438,35 @@ def test_son_indirect_basic_block_argument node_16("0016 leave") node_1002("1002 ψ") node_1004("1004 φ") - node_0 --> |0| node_14 - linkStyle 0 stroke:green; - node_2 --> |0| node_5 - linkStyle 1 stroke:green; - node_4 --> |1| node_5 - linkStyle 2 stroke:green; + node_0 -- "0" --> node_14 + node_2 -- "0" --> node_5 + node_4 -- "1" --> node_5 node_5 --> node_7 - linkStyle 3 stroke:red; - node_5 --> |0| node_7 - linkStyle 4 stroke:green; - node_7 --> |branch0| node_13 - linkStyle 5 stroke:red; - node_7 --> |fallthrough| node_1002 - linkStyle 6 stroke:red; - node_9 --> |0011| node_1004 - linkStyle 7 stroke:green; - node_13 --> |branch0| node_1002 - linkStyle 8 stroke:red; - node_13 --> |0013| node_1004 - linkStyle 9 stroke:green; + node_5 -- "0" --> node_7 + node_7 -- "branch0" --> node_13 + node_7 -- "fallthrough" --> node_1002 + node_9 -- "0011" --> node_1004 + node_13 -- "branch0" --> node_1002 + node_13 -- "0013" --> node_1004 node_14 --> node_16 - linkStyle 10 stroke:red; - node_14 --> |0| node_16 - linkStyle 11 stroke:green; + node_14 -- "0" --> node_16 node_1002 --> node_14 - linkStyle 12 stroke:red; node_1004 -.-> node_1002 - node_1004 --> |1| node_14 - linkStyle 14 stroke:green; + node_1004 -- "1" --> node_14 + linkStyle 0 stroke:green + linkStyle 1 stroke:green + linkStyle 2 stroke:green + linkStyle 3 stroke:red + linkStyle 4 stroke:green + linkStyle 5 stroke:red + linkStyle 6 stroke:red + linkStyle 7 stroke:green + linkStyle 8 stroke:red + linkStyle 9 stroke:green + linkStyle 10 stroke:red + linkStyle 11 stroke:green + linkStyle 12 stroke:red + linkStyle 14 stroke:green MERMAID end From db06d7ebe75f4fb68202435c06f81a56c82526b3 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Fri, 10 Feb 2023 11:06:53 -0500 Subject: [PATCH 06/11] Start autoloading more things --- lib/syntax_tree.rb | 28 ++++++++-------------------- lib/syntax_tree/yarv.rb | 16 ++++++++++++++++ 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/lib/syntax_tree.rb b/lib/syntax_tree.rb index 9cbd49c7..220389cb 100644 --- a/lib/syntax_tree.rb +++ b/lib/syntax_tree.rb @@ -9,7 +9,6 @@ require_relative "syntax_tree/formatter" require_relative "syntax_tree/node" -require_relative "syntax_tree/dsl" require_relative "syntax_tree/version" require_relative "syntax_tree/basic_visitor" @@ -23,29 +22,10 @@ require_relative "syntax_tree/visitor/environment" require_relative "syntax_tree/visitor/with_environment" -require_relative "syntax_tree/mermaid" require_relative "syntax_tree/parser" require_relative "syntax_tree/pattern" require_relative "syntax_tree/search" require_relative "syntax_tree/index" - -require_relative "syntax_tree/yarv" -require_relative "syntax_tree/yarv/basic_block" -require_relative "syntax_tree/yarv/bf" -require_relative "syntax_tree/yarv/calldata" -require_relative "syntax_tree/yarv/compiler" -require_relative "syntax_tree/yarv/control_flow_graph" -require_relative "syntax_tree/yarv/data_flow_graph" -require_relative "syntax_tree/yarv/decompiler" -require_relative "syntax_tree/yarv/disassembler" -require_relative "syntax_tree/yarv/instruction_sequence" -require_relative "syntax_tree/yarv/instructions" -require_relative "syntax_tree/yarv/legacy" -require_relative "syntax_tree/yarv/local_table" -require_relative "syntax_tree/yarv/sea_of_nodes" -require_relative "syntax_tree/yarv/assembler" -require_relative "syntax_tree/yarv/vm" - require_relative "syntax_tree/translation" # Syntax Tree is a suite of tools built on top of the internal CRuby parser. It @@ -53,6 +33,14 @@ # tools necessary to inspect and manipulate that syntax tree. It can be used to # build formatters, linters, language servers, and more. module SyntaxTree + # Syntax Tree the library has many features that aren't always used by the + # CLI. Requiring those features takes time, so we autoload as many constants + # as possible in order to keep the CLI as fast as possible. + + autoload :DSL, "syntax_tree/dsl" + autoload :Mermaid, "syntax_tree/mermaid" + autoload :YARV, "syntax_tree/yarv" + # This holds references to objects that respond to both #parse and #format # so that we can use them in the CLI. HANDLERS = {} diff --git a/lib/syntax_tree/yarv.rb b/lib/syntax_tree/yarv.rb index 7e4da7bb..ff8d3801 100644 --- a/lib/syntax_tree/yarv.rb +++ b/lib/syntax_tree/yarv.rb @@ -1,5 +1,21 @@ # frozen_string_literal: true +require_relative "yarv/basic_block" +require_relative "yarv/bf" +require_relative "yarv/calldata" +require_relative "yarv/compiler" +require_relative "yarv/control_flow_graph" +require_relative "yarv/data_flow_graph" +require_relative "yarv/decompiler" +require_relative "yarv/disassembler" +require_relative "yarv/instruction_sequence" +require_relative "yarv/instructions" +require_relative "yarv/legacy" +require_relative "yarv/local_table" +require_relative "yarv/sea_of_nodes" +require_relative "yarv/assembler" +require_relative "yarv/vm" + module SyntaxTree # This module provides an object representation of the YARV bytecode. module YARV From 0cf3e858b2dc3cee1af05a6ee3c0913d261727be Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Fri, 10 Feb 2023 11:26:23 -0500 Subject: [PATCH 07/11] Autoload a bunch of stuff --- README.md | 4 +- lib/syntax_tree.rb | 40 +- lib/syntax_tree/cli.rb | 4 +- lib/syntax_tree/field_visitor.rb | 1028 ++++++++++++++++ lib/syntax_tree/json_visitor.rb | 55 + lib/syntax_tree/language_server.rb | 157 ++- .../language_server/inlay_hints.rb | 159 --- lib/syntax_tree/match_visitor.rb | 120 ++ lib/syntax_tree/mermaid.rb | 1 + lib/syntax_tree/mermaid_visitor.rb | 73 ++ lib/syntax_tree/mutation_visitor.rb | 922 +++++++++++++++ lib/syntax_tree/node.rb | 8 +- lib/syntax_tree/pretty_print_visitor.rb | 83 ++ lib/syntax_tree/visitor/environment.rb | 84 -- lib/syntax_tree/visitor/field_visitor.rb | 1031 ----------------- lib/syntax_tree/visitor/json_visitor.rb | 55 - lib/syntax_tree/visitor/match_visitor.rb | 122 -- lib/syntax_tree/visitor/mermaid_visitor.rb | 75 -- lib/syntax_tree/visitor/mutation_visitor.rb | 924 --------------- .../visitor/pretty_print_visitor.rb | 85 -- .../{visitor => }/with_environment.rb | 81 ++ lib/syntax_tree/yarv.rb | 2 + lib/syntax_tree/yarv/compiler.rb | 2 +- test/test_helper.rb | 2 +- 24 files changed, 2549 insertions(+), 2568 deletions(-) create mode 100644 lib/syntax_tree/field_visitor.rb create mode 100644 lib/syntax_tree/json_visitor.rb delete mode 100644 lib/syntax_tree/language_server/inlay_hints.rb create mode 100644 lib/syntax_tree/match_visitor.rb create mode 100644 lib/syntax_tree/mermaid_visitor.rb create mode 100644 lib/syntax_tree/mutation_visitor.rb create mode 100644 lib/syntax_tree/pretty_print_visitor.rb delete mode 100644 lib/syntax_tree/visitor/environment.rb delete mode 100644 lib/syntax_tree/visitor/field_visitor.rb delete mode 100644 lib/syntax_tree/visitor/json_visitor.rb delete mode 100644 lib/syntax_tree/visitor/match_visitor.rb delete mode 100644 lib/syntax_tree/visitor/mermaid_visitor.rb delete mode 100644 lib/syntax_tree/visitor/mutation_visitor.rb delete mode 100644 lib/syntax_tree/visitor/pretty_print_visitor.rb rename lib/syntax_tree/{visitor => }/with_environment.rb (58%) diff --git a/README.md b/README.md index 6ca9b01a..5f447ad8 100644 --- a/README.md +++ b/README.md @@ -341,7 +341,7 @@ This function takes an input string containing Ruby code, parses it into its und ### SyntaxTree.mutation(&block) -This function yields a new mutation visitor to the block, and then returns the initialized visitor. It's effectively a shortcut for creating a `SyntaxTree::Visitor::MutationVisitor` without having to remember the class name. For more information on that visitor, see the definition below. +This function yields a new mutation visitor to the block, and then returns the initialized visitor. It's effectively a shortcut for creating a `SyntaxTree::MutationVisitor` without having to remember the class name. For more information on that visitor, see the definition below. ### SyntaxTree.search(source, query, &block) @@ -558,7 +558,7 @@ The `MutationVisitor` is a visitor that can be used to mutate the tree. It works ```ruby # Create a new visitor -visitor = SyntaxTree::Visitor::MutationVisitor.new +visitor = SyntaxTree::MutationVisitor.new # Specify that it should mutate If nodes with assignments in their predicates visitor.mutate("IfNode[predicate: Assign | OpAssign]") do |node| diff --git a/lib/syntax_tree.rb b/lib/syntax_tree.rb index 220389cb..0bdc4827 100644 --- a/lib/syntax_tree.rb +++ b/lib/syntax_tree.rb @@ -1,32 +1,15 @@ # frozen_string_literal: true -require "etc" -require "json" -require "pp" require "prettier_print" require "ripper" -require "stringio" -require_relative "syntax_tree/formatter" require_relative "syntax_tree/node" -require_relative "syntax_tree/version" - require_relative "syntax_tree/basic_visitor" require_relative "syntax_tree/visitor" -require_relative "syntax_tree/visitor/field_visitor" -require_relative "syntax_tree/visitor/json_visitor" -require_relative "syntax_tree/visitor/match_visitor" -require_relative "syntax_tree/visitor/mermaid_visitor" -require_relative "syntax_tree/visitor/mutation_visitor" -require_relative "syntax_tree/visitor/pretty_print_visitor" -require_relative "syntax_tree/visitor/environment" -require_relative "syntax_tree/visitor/with_environment" +require_relative "syntax_tree/formatter" require_relative "syntax_tree/parser" -require_relative "syntax_tree/pattern" -require_relative "syntax_tree/search" -require_relative "syntax_tree/index" -require_relative "syntax_tree/translation" +require_relative "syntax_tree/version" # Syntax Tree is a suite of tools built on top of the internal CRuby parser. It # provides the ability to generate a syntax tree from source, as well as the @@ -38,7 +21,19 @@ module SyntaxTree # as possible in order to keep the CLI as fast as possible. autoload :DSL, "syntax_tree/dsl" + autoload :FieldVisitor, "syntax_tree/field_visitor" + autoload :Index, "syntax_tree/index" + autoload :JSONVisitor, "syntax_tree/json_visitor" + autoload :LanguageServer, "syntax_tree/language_server" + autoload :MatchVisitor, "syntax_tree/match_visitor" autoload :Mermaid, "syntax_tree/mermaid" + autoload :MermaidVisitor, "syntax_tree/mermaid_visitor" + autoload :MutationVisitor, "syntax_tree/mutation_visitor" + autoload :Pattern, "syntax_tree/pattern" + autoload :PrettyPrintVisitor, "syntax_tree/pretty_print_visitor" + autoload :Search, "syntax_tree/search" + autoload :Translation, "syntax_tree/translation" + autoload :WithEnvironment, "syntax_tree/with_environment" autoload :YARV, "syntax_tree/yarv" # This holds references to objects that respond to both #parse and #format @@ -89,7 +84,7 @@ def self.index_file(filepath) # A convenience method for creating a new mutation visitor. def self.mutation - visitor = Visitor::MutationVisitor.new + visitor = MutationVisitor.new yield visitor visitor end @@ -130,6 +125,9 @@ def self.register_handler(extension, handler) # Searches through the given source using the given pattern and yields each # node in the tree that matches the pattern to the given block. def self.search(source, query, &block) - Search.new(Pattern.new(query).compile).scan(parse(source), &block) + pattern = Pattern.new(query).compile + program = parse(source) + + Search.new(pattern).scan(program, &block) end end diff --git a/lib/syntax_tree/cli.rb b/lib/syntax_tree/cli.rb index 7e6f4067..cbe10446 100644 --- a/lib/syntax_tree/cli.rb +++ b/lib/syntax_tree/cli.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require "etc" require "optparse" module SyntaxTree @@ -238,7 +239,7 @@ def run(item) # representation. class Json < Action def run(item) - object = Visitor::JSONVisitor.new.visit(item.handler.parse(item.source)) + object = item.handler.parse(item.source).accept(JSONVisitor.new) puts JSON.pretty_generate(object) end end @@ -501,7 +502,6 @@ def run(argv) when "j", "json" Json.new(options) when "lsp" - require "syntax_tree/language_server" LanguageServer.new(print_width: options.print_width).run return 0 when "m", "match" diff --git a/lib/syntax_tree/field_visitor.rb b/lib/syntax_tree/field_visitor.rb new file mode 100644 index 00000000..f4fc00e3 --- /dev/null +++ b/lib/syntax_tree/field_visitor.rb @@ -0,0 +1,1028 @@ +# frozen_string_literal: true + +module SyntaxTree + # This is the parent class of a lot of built-in visitors for Syntax Tree. It + # reflects visiting each of the fields on every node in turn. It itself does + # not do anything with these fields, it leaves that behavior up to the + # subclass to implement. + # + # In order to properly use this class, you will need to subclass it and + # implement #comments, #field, #list, #node, #pairs, and #text. Those are + # documented here. + # + # == comments(node) + # + # This accepts the node that is being visited and does something depending on + # the comments attached to the node. + # + # == field(name, value) + # + # This accepts the name of the field being visited as a string (like "value") + # and the actual value of that field. The value can be a subclass of Node or + # any other type that can be held within the tree. + # + # == list(name, values) + # + # This accepts the name of the field being visited as well as a list of + # values. This is used, for example, when visiting something like the body of + # a Statements node. + # + # == node(name, node) + # + # This is the parent serialization method for each node. It is called with the + # node itself, as well as the type of the node as a string. The type is an + # internally used value that usually resembles the name of the ripper event + # that generated the node. The method should yield to the given block which + # then calls through to visit each of the fields on the node. + # + # == text(name, value) + # + # This accepts the name of the field being visited as well as a string value + # representing the value of the field. + # + # == pairs(name, values) + # + # This accepts the name of the field being visited as well as a list of pairs + # that represent the value of the field. It is used only in a couple of + # circumstances, like when visiting the list of optional parameters defined on + # a method. + # + class FieldVisitor < BasicVisitor + def visit_aref(node) + node(node, "aref") do + field("collection", node.collection) + field("index", node.index) + comments(node) + end + end + + def visit_aref_field(node) + node(node, "aref_field") do + field("collection", node.collection) + field("index", node.index) + comments(node) + end + end + + def visit_alias(node) + node(node, "alias") do + field("left", node.left) + field("right", node.right) + comments(node) + end + end + + def visit_arg_block(node) + node(node, "arg_block") do + field("value", node.value) if node.value + comments(node) + end + end + + def visit_arg_paren(node) + node(node, "arg_paren") do + field("arguments", node.arguments) + comments(node) + end + end + + def visit_arg_star(node) + node(node, "arg_star") do + field("value", node.value) + comments(node) + end + end + + def visit_args(node) + node(node, "args") do + list("parts", node.parts) + comments(node) + end + end + + def visit_args_forward(node) + node(node, "args_forward") { comments(node) } + end + + def visit_array(node) + node(node, "array") do + field("contents", node.contents) + comments(node) + end + end + + def visit_aryptn(node) + node(node, "aryptn") do + field("constant", node.constant) if node.constant + list("requireds", node.requireds) if node.requireds.any? + field("rest", node.rest) if node.rest + list("posts", node.posts) if node.posts.any? + comments(node) + end + end + + def visit_assign(node) + node(node, "assign") do + field("target", node.target) + field("value", node.value) + comments(node) + end + end + + def visit_assoc(node) + node(node, "assoc") do + field("key", node.key) + field("value", node.value) if node.value + comments(node) + end + end + + def visit_assoc_splat(node) + node(node, "assoc_splat") do + field("value", node.value) + comments(node) + end + end + + def visit_backref(node) + visit_token(node, "backref") + end + + def visit_backtick(node) + visit_token(node, "backtick") + end + + def visit_bare_assoc_hash(node) + node(node, "bare_assoc_hash") do + list("assocs", node.assocs) + comments(node) + end + end + + def visit_BEGIN(node) + node(node, "BEGIN") do + field("statements", node.statements) + comments(node) + end + end + + def visit_begin(node) + node(node, "begin") do + field("bodystmt", node.bodystmt) + comments(node) + end + end + + def visit_binary(node) + node(node, "binary") do + field("left", node.left) + text("operator", node.operator) + field("right", node.right) + comments(node) + end + end + + def visit_block(node) + node(node, "block") do + field("block_var", node.block_var) if node.block_var + field("bodystmt", node.bodystmt) + comments(node) + end + end + + def visit_blockarg(node) + node(node, "blockarg") do + field("name", node.name) if node.name + comments(node) + end + end + + def visit_block_var(node) + node(node, "block_var") do + field("params", node.params) + list("locals", node.locals) if node.locals.any? + comments(node) + end + end + + def visit_bodystmt(node) + node(node, "bodystmt") do + field("statements", node.statements) + field("rescue_clause", node.rescue_clause) if node.rescue_clause + field("else_clause", node.else_clause) if node.else_clause + field("ensure_clause", node.ensure_clause) if node.ensure_clause + comments(node) + end + end + + def visit_break(node) + node(node, "break") do + field("arguments", node.arguments) + comments(node) + end + end + + def visit_call(node) + node(node, "call") do + field("receiver", node.receiver) + field("operator", node.operator) + field("message", node.message) + field("arguments", node.arguments) if node.arguments + comments(node) + end + end + + def visit_case(node) + node(node, "case") do + field("keyword", node.keyword) + field("value", node.value) if node.value + field("consequent", node.consequent) + comments(node) + end + end + + def visit_CHAR(node) + visit_token(node, "CHAR") + end + + def visit_class(node) + node(node, "class") do + field("constant", node.constant) + field("superclass", node.superclass) if node.superclass + field("bodystmt", node.bodystmt) + comments(node) + end + end + + def visit_comma(node) + node(node, "comma") { field("value", node.value) } + end + + def visit_command(node) + node(node, "command") do + field("message", node.message) + field("arguments", node.arguments) + comments(node) + end + end + + def visit_command_call(node) + node(node, "command_call") do + field("receiver", node.receiver) + field("operator", node.operator) + field("message", node.message) + field("arguments", node.arguments) if node.arguments + comments(node) + end + end + + def visit_comment(node) + node(node, "comment") { field("value", node.value) } + end + + def visit_const(node) + visit_token(node, "const") + end + + def visit_const_path_field(node) + node(node, "const_path_field") do + field("parent", node.parent) + field("constant", node.constant) + comments(node) + end + end + + def visit_const_path_ref(node) + node(node, "const_path_ref") do + field("parent", node.parent) + field("constant", node.constant) + comments(node) + end + end + + def visit_const_ref(node) + node(node, "const_ref") do + field("constant", node.constant) + comments(node) + end + end + + def visit_cvar(node) + visit_token(node, "cvar") + end + + def visit_def(node) + node(node, "def") do + field("target", node.target) + field("operator", node.operator) + field("name", node.name) + field("params", node.params) + field("bodystmt", node.bodystmt) + comments(node) + end + end + + def visit_defined(node) + node(node, "defined") do + field("value", node.value) + comments(node) + end + end + + def visit_dyna_symbol(node) + node(node, "dyna_symbol") do + list("parts", node.parts) + comments(node) + end + end + + def visit_END(node) + node(node, "END") do + field("statements", node.statements) + comments(node) + end + end + + def visit_else(node) + node(node, "else") do + field("statements", node.statements) + comments(node) + end + end + + def visit_elsif(node) + node(node, "elsif") do + field("predicate", node.predicate) + field("statements", node.statements) + field("consequent", node.consequent) if node.consequent + comments(node) + end + end + + def visit_embdoc(node) + node(node, "embdoc") { field("value", node.value) } + end + + def visit_embexpr_beg(node) + node(node, "embexpr_beg") { field("value", node.value) } + end + + def visit_embexpr_end(node) + node(node, "embexpr_end") { field("value", node.value) } + end + + def visit_embvar(node) + node(node, "embvar") { field("value", node.value) } + end + + def visit_ensure(node) + node(node, "ensure") do + field("statements", node.statements) + comments(node) + end + end + + def visit_excessed_comma(node) + visit_token(node, "excessed_comma") + end + + def visit_field(node) + node(node, "field") do + field("parent", node.parent) + field("operator", node.operator) + field("name", node.name) + comments(node) + end + end + + def visit_float(node) + visit_token(node, "float") + end + + def visit_fndptn(node) + node(node, "fndptn") do + field("constant", node.constant) if node.constant + field("left", node.left) + list("values", node.values) + field("right", node.right) + comments(node) + end + end + + def visit_for(node) + node(node, "for") do + field("index", node.index) + field("collection", node.collection) + field("statements", node.statements) + comments(node) + end + end + + def visit_gvar(node) + visit_token(node, "gvar") + end + + def visit_hash(node) + node(node, "hash") do + list("assocs", node.assocs) if node.assocs.any? + comments(node) + end + end + + def visit_heredoc(node) + node(node, "heredoc") do + list("parts", node.parts) + comments(node) + end + end + + def visit_heredoc_beg(node) + visit_token(node, "heredoc_beg") + end + + def visit_heredoc_end(node) + visit_token(node, "heredoc_end") + end + + def visit_hshptn(node) + node(node, "hshptn") do + field("constant", node.constant) if node.constant + pairs("keywords", node.keywords) if node.keywords.any? + field("keyword_rest", node.keyword_rest) if node.keyword_rest + comments(node) + end + end + + def visit_ident(node) + visit_token(node, "ident") + end + + def visit_if(node) + node(node, "if") do + field("predicate", node.predicate) + field("statements", node.statements) + field("consequent", node.consequent) if node.consequent + comments(node) + end + end + + def visit_if_op(node) + node(node, "if_op") do + field("predicate", node.predicate) + field("truthy", node.truthy) + field("falsy", node.falsy) + comments(node) + end + end + + def visit_imaginary(node) + visit_token(node, "imaginary") + end + + def visit_in(node) + node(node, "in") do + field("pattern", node.pattern) + field("statements", node.statements) + field("consequent", node.consequent) if node.consequent + comments(node) + end + end + + def visit_int(node) + visit_token(node, "int") + end + + def visit_ivar(node) + visit_token(node, "ivar") + end + + def visit_kw(node) + visit_token(node, "kw") + end + + def visit_kwrest_param(node) + node(node, "kwrest_param") do + field("name", node.name) + comments(node) + end + end + + def visit_label(node) + visit_token(node, "label") + end + + def visit_label_end(node) + node(node, "label_end") { field("value", node.value) } + end + + def visit_lambda(node) + node(node, "lambda") do + field("params", node.params) + field("statements", node.statements) + comments(node) + end + end + + def visit_lambda_var(node) + node(node, "lambda_var") do + field("params", node.params) + list("locals", node.locals) if node.locals.any? + comments(node) + end + end + + def visit_lbrace(node) + visit_token(node, "lbrace") + end + + def visit_lbracket(node) + visit_token(node, "lbracket") + end + + def visit_lparen(node) + visit_token(node, "lparen") + end + + def visit_massign(node) + node(node, "massign") do + field("target", node.target) + field("value", node.value) + comments(node) + end + end + + def visit_method_add_block(node) + node(node, "method_add_block") do + field("call", node.call) + field("block", node.block) + comments(node) + end + end + + def visit_mlhs(node) + node(node, "mlhs") do + list("parts", node.parts) + comments(node) + end + end + + def visit_mlhs_paren(node) + node(node, "mlhs_paren") do + field("contents", node.contents) + comments(node) + end + end + + def visit_module(node) + node(node, "module") do + field("constant", node.constant) + field("bodystmt", node.bodystmt) + comments(node) + end + end + + def visit_mrhs(node) + node(node, "mrhs") do + list("parts", node.parts) + comments(node) + end + end + + def visit_next(node) + node(node, "next") do + field("arguments", node.arguments) + comments(node) + end + end + + def visit_not(node) + node(node, "not") do + field("statement", node.statement) + comments(node) + end + end + + def visit_op(node) + visit_token(node, "op") + end + + def visit_opassign(node) + node(node, "opassign") do + field("target", node.target) + field("operator", node.operator) + field("value", node.value) + comments(node) + end + end + + def visit_params(node) + node(node, "params") do + list("requireds", node.requireds) if node.requireds.any? + pairs("optionals", node.optionals) if node.optionals.any? + field("rest", node.rest) if node.rest + list("posts", node.posts) if node.posts.any? + pairs("keywords", node.keywords) if node.keywords.any? + field("keyword_rest", node.keyword_rest) if node.keyword_rest + field("block", node.block) if node.block + comments(node) + end + end + + def visit_paren(node) + node(node, "paren") do + field("contents", node.contents) + comments(node) + end + end + + def visit_period(node) + visit_token(node, "period") + end + + def visit_pinned_begin(node) + node(node, "pinned_begin") do + field("statement", node.statement) + comments(node) + end + end + + def visit_pinned_var_ref(node) + node(node, "pinned_var_ref") do + field("value", node.value) + comments(node) + end + end + + def visit_program(node) + node(node, "program") do + field("statements", node.statements) + comments(node) + end + end + + def visit_qsymbols(node) + node(node, "qsymbols") do + list("elements", node.elements) + comments(node) + end + end + + def visit_qsymbols_beg(node) + node(node, "qsymbols_beg") { field("value", node.value) } + end + + def visit_qwords(node) + node(node, "qwords") do + list("elements", node.elements) + comments(node) + end + end + + def visit_qwords_beg(node) + node(node, "qwords_beg") { field("value", node.value) } + end + + def visit_range(node) + node(node, "range") do + field("left", node.left) if node.left + field("operator", node.operator) + field("right", node.right) if node.right + comments(node) + end + end + + def visit_rassign(node) + node(node, "rassign") do + field("value", node.value) + field("operator", node.operator) + field("pattern", node.pattern) + comments(node) + end + end + + def visit_rational(node) + visit_token(node, "rational") + end + + def visit_rbrace(node) + node(node, "rbrace") { field("value", node.value) } + end + + def visit_rbracket(node) + node(node, "rbracket") { field("value", node.value) } + end + + def visit_redo(node) + node(node, "redo") { comments(node) } + end + + def visit_regexp_beg(node) + node(node, "regexp_beg") { field("value", node.value) } + end + + def visit_regexp_content(node) + node(node, "regexp_content") { list("parts", node.parts) } + end + + def visit_regexp_end(node) + node(node, "regexp_end") { field("value", node.value) } + end + + def visit_regexp_literal(node) + node(node, "regexp_literal") do + list("parts", node.parts) + field("options", node.options) + comments(node) + end + end + + def visit_rescue(node) + node(node, "rescue") do + field("exception", node.exception) if node.exception + field("statements", node.statements) + field("consequent", node.consequent) if node.consequent + comments(node) + end + end + + def visit_rescue_ex(node) + node(node, "rescue_ex") do + field("exceptions", node.exceptions) + field("variable", node.variable) + comments(node) + end + end + + def visit_rescue_mod(node) + node(node, "rescue_mod") do + field("statement", node.statement) + field("value", node.value) + comments(node) + end + end + + def visit_rest_param(node) + node(node, "rest_param") do + field("name", node.name) + comments(node) + end + end + + def visit_retry(node) + node(node, "retry") { comments(node) } + end + + def visit_return(node) + node(node, "return") do + field("arguments", node.arguments) + comments(node) + end + end + + def visit_rparen(node) + node(node, "rparen") { field("value", node.value) } + end + + def visit_sclass(node) + node(node, "sclass") do + field("target", node.target) + field("bodystmt", node.bodystmt) + comments(node) + end + end + + def visit_statements(node) + node(node, "statements") do + list("body", node.body) + comments(node) + end + end + + def visit_string_concat(node) + node(node, "string_concat") do + field("left", node.left) + field("right", node.right) + comments(node) + end + end + + def visit_string_content(node) + node(node, "string_content") { list("parts", node.parts) } + end + + def visit_string_dvar(node) + node(node, "string_dvar") do + field("variable", node.variable) + comments(node) + end + end + + def visit_string_embexpr(node) + node(node, "string_embexpr") do + field("statements", node.statements) + comments(node) + end + end + + def visit_string_literal(node) + node(node, "string_literal") do + list("parts", node.parts) + comments(node) + end + end + + def visit_super(node) + node(node, "super") do + field("arguments", node.arguments) + comments(node) + end + end + + def visit_symbeg(node) + node(node, "symbeg") { field("value", node.value) } + end + + def visit_symbol_content(node) + node(node, "symbol_content") { field("value", node.value) } + end + + def visit_symbol_literal(node) + node(node, "symbol_literal") do + field("value", node.value) + comments(node) + end + end + + def visit_symbols(node) + node(node, "symbols") do + list("elements", node.elements) + comments(node) + end + end + + def visit_symbols_beg(node) + node(node, "symbols_beg") { field("value", node.value) } + end + + def visit_tlambda(node) + node(node, "tlambda") { field("value", node.value) } + end + + def visit_tlambeg(node) + node(node, "tlambeg") { field("value", node.value) } + end + + def visit_top_const_field(node) + node(node, "top_const_field") do + field("constant", node.constant) + comments(node) + end + end + + def visit_top_const_ref(node) + node(node, "top_const_ref") do + field("constant", node.constant) + comments(node) + end + end + + def visit_tstring_beg(node) + node(node, "tstring_beg") { field("value", node.value) } + end + + def visit_tstring_content(node) + visit_token(node, "tstring_content") + end + + def visit_tstring_end(node) + node(node, "tstring_end") { field("value", node.value) } + end + + def visit_unary(node) + node(node, "unary") do + field("operator", node.operator) + field("statement", node.statement) + comments(node) + end + end + + def visit_undef(node) + node(node, "undef") do + list("symbols", node.symbols) + comments(node) + end + end + + def visit_unless(node) + node(node, "unless") do + field("predicate", node.predicate) + field("statements", node.statements) + field("consequent", node.consequent) if node.consequent + comments(node) + end + end + + def visit_until(node) + node(node, "until") do + field("predicate", node.predicate) + field("statements", node.statements) + comments(node) + end + end + + def visit_var_field(node) + node(node, "var_field") do + field("value", node.value) + comments(node) + end + end + + def visit_var_ref(node) + node(node, "var_ref") do + field("value", node.value) + comments(node) + end + end + + def visit_vcall(node) + node(node, "vcall") do + field("value", node.value) + comments(node) + end + end + + def visit_void_stmt(node) + node(node, "void_stmt") { comments(node) } + end + + def visit_when(node) + node(node, "when") do + field("arguments", node.arguments) + field("statements", node.statements) + field("consequent", node.consequent) if node.consequent + comments(node) + end + end + + def visit_while(node) + node(node, "while") do + field("predicate", node.predicate) + field("statements", node.statements) + comments(node) + end + end + + def visit_word(node) + node(node, "word") do + list("parts", node.parts) + comments(node) + end + end + + def visit_words(node) + node(node, "words") do + list("elements", node.elements) + comments(node) + end + end + + def visit_words_beg(node) + node(node, "words_beg") { field("value", node.value) } + end + + def visit_xstring(node) + node(node, "xstring") { list("parts", node.parts) } + end + + def visit_xstring_literal(node) + node(node, "xstring_literal") do + list("parts", node.parts) + comments(node) + end + end + + def visit_yield(node) + node(node, "yield") do + field("arguments", node.arguments) + comments(node) + end + end + + def visit_zsuper(node) + node(node, "zsuper") { comments(node) } + end + + def visit___end__(node) + visit_token(node, "__end__") + end + + private + + def visit_token(node, type) + node(node, type) do + field("value", node.value) + comments(node) + end + end + end +end diff --git a/lib/syntax_tree/json_visitor.rb b/lib/syntax_tree/json_visitor.rb new file mode 100644 index 00000000..7ad3fba0 --- /dev/null +++ b/lib/syntax_tree/json_visitor.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require "json" + +module SyntaxTree + # This visitor transforms the AST into a hash that contains only primitives + # that can be easily serialized into JSON. + class JSONVisitor < FieldVisitor + attr_reader :target + + def initialize + @target = nil + end + + private + + def comments(node) + target[:comments] = visit_all(node.comments) + end + + def field(name, value) + target[name] = value.is_a?(Node) ? visit(value) : value + end + + def list(name, values) + target[name] = visit_all(values) + end + + def node(node, type) + previous = @target + @target = { type: type, location: visit_location(node.location) } + yield + @target + ensure + @target = previous + end + + def pairs(name, values) + target[name] = values.map { |(key, value)| [visit(key), visit(value)] } + end + + def text(name, value) + target[name] = value + end + + def visit_location(location) + [ + location.start_line, + location.start_char, + location.end_line, + location.end_char + ] + end + end +end diff --git a/lib/syntax_tree/language_server.rb b/lib/syntax_tree/language_server.rb index a7b23664..afb1540e 100644 --- a/lib/syntax_tree/language_server.rb +++ b/lib/syntax_tree/language_server.rb @@ -2,10 +2,9 @@ require "cgi" require "json" +require "pp" require "uri" -require_relative "language_server/inlay_hints" - module SyntaxTree # Syntax Tree additionally ships with a language server conforming to the # language server protocol. It can be invoked through the CLI by running: @@ -13,6 +12,160 @@ module SyntaxTree # stree lsp # class LanguageServer + # This class provides inlay hints for the language server. For more + # information, see the spec here: + # https://github.com/microsoft/language-server-protocol/issues/956. + class InlayHints < Visitor + # This represents a hint that is going to be displayed in the editor. + class Hint + attr_reader :line, :character, :label + + def initialize(line:, character:, label:) + @line = line + @character = character + @label = label + end + + # This is the shape that the LSP expects. + def to_json(*opts) + { + position: { + line: line, + character: character + }, + label: label + }.to_json(*opts) + end + end + + attr_reader :stack, :hints + + def initialize + @stack = [] + @hints = [] + end + + def visit(node) + stack << node + result = super + stack.pop + result + end + + # Adds parentheses around assignments contained within the default values + # of parameters. For example, + # + # def foo(a = b = c) + # end + # + # becomes + # + # def foo(a = ₍b = c₎) + # end + # + def visit_assign(node) + parentheses(node.location) if stack[-2].is_a?(Params) + super + end + + # Adds parentheses around binary expressions to make it clear which + # subexpression will be evaluated first. For example, + # + # a + b * c + # + # becomes + # + # a + ₍b * c₎ + # + def visit_binary(node) + case stack[-2] + when Assign, OpAssign + parentheses(node.location) + when Binary + parentheses(node.location) if stack[-2].operator != node.operator + end + + super + end + + # Adds parentheses around ternary operators contained within certain + # expressions where it could be confusing which subexpression will get + # evaluated first. For example, + # + # a ? b : c ? d : e + # + # becomes + # + # a ? b : ₍c ? d : e₎ + # + def visit_if_op(node) + case stack[-2] + when Assign, Binary, IfOp, OpAssign + parentheses(node.location) + end + + super + end + + # Adds the implicitly rescued StandardError into a bare rescue clause. For + # example, + # + # begin + # rescue + # end + # + # becomes + # + # begin + # rescue StandardError + # end + # + def visit_rescue(node) + if node.exception.nil? + hints << Hint.new( + line: node.location.start_line - 1, + character: node.location.start_column + "rescue".length, + label: " StandardError" + ) + end + + super + end + + # Adds parentheses around unary statements using the - operator that are + # contained within Binary nodes. For example, + # + # -a + b + # + # becomes + # + # ₍-a₎ + b + # + def visit_unary(node) + if stack[-2].is_a?(Binary) && (node.operator == "-") + parentheses(node.location) + end + + super + end + + private + + def parentheses(location) + hints << Hint.new( + line: location.start_line - 1, + character: location.start_column, + label: "₍" + ) + + hints << Hint.new( + line: location.end_line - 1, + character: location.end_column, + label: "₎" + ) + end + end + # This is a small module that effectively mirrors pattern matching. We're # using it so that we can support truffleruby without having to ignore the # language server. diff --git a/lib/syntax_tree/language_server/inlay_hints.rb b/lib/syntax_tree/language_server/inlay_hints.rb deleted file mode 100644 index dfd63b8d..00000000 --- a/lib/syntax_tree/language_server/inlay_hints.rb +++ /dev/null @@ -1,159 +0,0 @@ -# frozen_string_literal: true - -module SyntaxTree - class LanguageServer - # This class provides inlay hints for the language server. For more - # information, see the spec here: - # https://github.com/microsoft/language-server-protocol/issues/956. - class InlayHints < Visitor - # This represents a hint that is going to be displayed in the editor. - class Hint - attr_reader :line, :character, :label - - def initialize(line:, character:, label:) - @line = line - @character = character - @label = label - end - - # This is the shape that the LSP expects. - def to_json(*opts) - { - position: { - line: line, - character: character - }, - label: label - }.to_json(*opts) - end - end - - attr_reader :stack, :hints - - def initialize - @stack = [] - @hints = [] - end - - def visit(node) - stack << node - result = super - stack.pop - result - end - - # Adds parentheses around assignments contained within the default values - # of parameters. For example, - # - # def foo(a = b = c) - # end - # - # becomes - # - # def foo(a = ₍b = c₎) - # end - # - def visit_assign(node) - parentheses(node.location) if stack[-2].is_a?(Params) - super - end - - # Adds parentheses around binary expressions to make it clear which - # subexpression will be evaluated first. For example, - # - # a + b * c - # - # becomes - # - # a + ₍b * c₎ - # - def visit_binary(node) - case stack[-2] - when Assign, OpAssign - parentheses(node.location) - when Binary - parentheses(node.location) if stack[-2].operator != node.operator - end - - super - end - - # Adds parentheses around ternary operators contained within certain - # expressions where it could be confusing which subexpression will get - # evaluated first. For example, - # - # a ? b : c ? d : e - # - # becomes - # - # a ? b : ₍c ? d : e₎ - # - def visit_if_op(node) - case stack[-2] - when Assign, Binary, IfOp, OpAssign - parentheses(node.location) - end - - super - end - - # Adds the implicitly rescued StandardError into a bare rescue clause. For - # example, - # - # begin - # rescue - # end - # - # becomes - # - # begin - # rescue StandardError - # end - # - def visit_rescue(node) - if node.exception.nil? - hints << Hint.new( - line: node.location.start_line - 1, - character: node.location.start_column + "rescue".length, - label: " StandardError" - ) - end - - super - end - - # Adds parentheses around unary statements using the - operator that are - # contained within Binary nodes. For example, - # - # -a + b - # - # becomes - # - # ₍-a₎ + b - # - def visit_unary(node) - if stack[-2].is_a?(Binary) && (node.operator == "-") - parentheses(node.location) - end - - super - end - - private - - def parentheses(location) - hints << Hint.new( - line: location.start_line - 1, - character: location.start_column, - label: "₍" - ) - - hints << Hint.new( - line: location.end_line - 1, - character: location.end_column, - label: "₎" - ) - end - end - end -end diff --git a/lib/syntax_tree/match_visitor.rb b/lib/syntax_tree/match_visitor.rb new file mode 100644 index 00000000..ca5bf234 --- /dev/null +++ b/lib/syntax_tree/match_visitor.rb @@ -0,0 +1,120 @@ +# frozen_string_literal: true + +module SyntaxTree + # This visitor transforms the AST into a Ruby pattern matching expression that + # would match correctly against the AST. + class MatchVisitor < FieldVisitor + attr_reader :q + + def initialize(q) + @q = q + end + + def visit(node) + case node + when Node + super + when String + # pp will split up a string on newlines and concat them together using a + # "+" operator. This breaks the pattern matching expression. So instead + # we're going to check here for strings and manually put the entire + # value into the output buffer. + q.text(node.inspect) + else + node.pretty_print(q) + end + end + + private + + def comments(node) + return if node.comments.empty? + + q.nest(0) do + q.text("comments: [") + q.indent do + q.breakable("") + q.seplist(node.comments) { |comment| visit(comment) } + end + q.breakable("") + q.text("]") + end + end + + def field(name, value) + q.nest(0) do + q.text(name) + q.text(": ") + visit(value) + end + end + + def list(name, values) + q.group do + q.text(name) + q.text(": [") + q.indent do + q.breakable("") + q.seplist(values) { |value| visit(value) } + end + q.breakable("") + q.text("]") + end + end + + def node(node, _type) + items = [] + q.with_target(items) { yield } + + if items.empty? + q.text(node.class.name) + return + end + + q.group do + q.text(node.class.name) + q.text("[") + q.indent do + q.breakable("") + q.seplist(items) { |item| q.target << item } + end + q.breakable("") + q.text("]") + end + end + + def pairs(name, values) + q.group do + q.text(name) + q.text(": [") + q.indent do + q.breakable("") + q.seplist(values) do |(key, value)| + q.group do + q.text("[") + q.indent do + q.breakable("") + visit(key) + q.text(",") + q.breakable + visit(value || nil) + end + q.breakable("") + q.text("]") + end + end + end + q.breakable("") + q.text("]") + end + end + + def text(name, value) + q.nest(0) do + q.text(name) + q.text(": ") + value.pretty_print(q) + end + end + end +end diff --git a/lib/syntax_tree/mermaid.rb b/lib/syntax_tree/mermaid.rb index 70cbc054..68ea4734 100644 --- a/lib/syntax_tree/mermaid.rb +++ b/lib/syntax_tree/mermaid.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "cgi" +require "stringio" module SyntaxTree # This module is responsible for rendering mermaid (https://mermaid.js.org/) diff --git a/lib/syntax_tree/mermaid_visitor.rb b/lib/syntax_tree/mermaid_visitor.rb new file mode 100644 index 00000000..52d1b5c6 --- /dev/null +++ b/lib/syntax_tree/mermaid_visitor.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +module SyntaxTree + # This visitor transforms the AST into a mermaid flow chart. + class MermaidVisitor < FieldVisitor + attr_reader :flowchart, :target + + def initialize + @flowchart = Mermaid.flowchart + @target = nil + end + + def visit_program(node) + super + flowchart.render + end + + private + + def comments(node) + # Ignore + end + + def field(name, value) + case value + when nil + # skip + when Node + flowchart.link(target, visit(value), name) + else + to = + flowchart.node( + "#{target.id}_#{name}", + value.inspect, + shape: :stadium + ) + flowchart.link(target, to, name) + end + end + + def list(name, values) + values.each_with_index do |value, index| + field("#{name}[#{index}]", value) + end + end + + def node(node, type) + previous_target = target + + begin + @target = flowchart.node("node_#{node.object_id}", type) + yield + @target + ensure + @target = previous_target + end + end + + def pairs(name, values) + values.each_with_index do |(key, value), index| + to = flowchart.node("#{target.id}_#{name}_#{index}", shape: :circle) + + flowchart.link(target, to, "#{name}[#{index}]") + flowchart.link(to, visit(key), "[0]") + flowchart.link(to, visit(value), "[1]") if value + end + end + + def text(name, value) + field(name, value) + end + end +end diff --git a/lib/syntax_tree/mutation_visitor.rb b/lib/syntax_tree/mutation_visitor.rb new file mode 100644 index 00000000..2d96620d --- /dev/null +++ b/lib/syntax_tree/mutation_visitor.rb @@ -0,0 +1,922 @@ +# frozen_string_literal: true + +module SyntaxTree + # This visitor walks through the tree and copies each node as it is being + # visited. This is useful for mutating the tree before it is formatted. + class MutationVisitor < BasicVisitor + attr_reader :mutations + + def initialize + @mutations = [] + end + + # Create a new mutation based on the given query that will mutate the node + # using the given block. The block should return a new node that will take + # the place of the given node in the tree. These blocks frequently make use + # of the `copy` method on nodes to create a new node with the same + # properties as the original node. + def mutate(query, &block) + mutations << [Pattern.new(query).compile, block] + end + + # This is the base visit method for each node in the tree. It first creates + # a copy of the node using the visit_* methods defined below. Then it checks + # each mutation in sequence and calls it if it finds a match. + def visit(node) + return unless node + result = node.accept(self) + + mutations.each do |(pattern, mutation)| + result = mutation.call(result) if pattern.call(result) + end + + result + end + + # Visit a BEGINBlock node. + def visit_BEGIN(node) + node.copy( + lbrace: visit(node.lbrace), + statements: visit(node.statements) + ) + end + + # Visit a CHAR node. + def visit_CHAR(node) + node.copy + end + + # Visit a ENDBlock node. + def visit_END(node) + node.copy( + lbrace: visit(node.lbrace), + statements: visit(node.statements) + ) + end + + # Visit a EndContent node. + def visit___end__(node) + node.copy + end + + # Visit a AliasNode node. + def visit_alias(node) + node.copy(left: visit(node.left), right: visit(node.right)) + end + + # Visit a ARef node. + def visit_aref(node) + node.copy(index: visit(node.index)) + end + + # Visit a ARefField node. + def visit_aref_field(node) + node.copy(index: visit(node.index)) + end + + # Visit a ArgParen node. + def visit_arg_paren(node) + node.copy(arguments: visit(node.arguments)) + end + + # Visit a Args node. + def visit_args(node) + node.copy(parts: visit_all(node.parts)) + end + + # Visit a ArgBlock node. + def visit_arg_block(node) + node.copy(value: visit(node.value)) + end + + # Visit a ArgStar node. + def visit_arg_star(node) + node.copy(value: visit(node.value)) + end + + # Visit a ArgsForward node. + def visit_args_forward(node) + node.copy + end + + # Visit a ArrayLiteral node. + def visit_array(node) + node.copy( + lbracket: visit(node.lbracket), + contents: visit(node.contents) + ) + end + + # Visit a AryPtn node. + def visit_aryptn(node) + node.copy( + constant: visit(node.constant), + requireds: visit_all(node.requireds), + rest: visit(node.rest), + posts: visit_all(node.posts) + ) + end + + # Visit a Assign node. + def visit_assign(node) + node.copy(target: visit(node.target)) + end + + # Visit a Assoc node. + def visit_assoc(node) + node.copy + end + + # Visit a AssocSplat node. + def visit_assoc_splat(node) + node.copy + end + + # Visit a Backref node. + def visit_backref(node) + node.copy + end + + # Visit a Backtick node. + def visit_backtick(node) + node.copy + end + + # Visit a BareAssocHash node. + def visit_bare_assoc_hash(node) + node.copy(assocs: visit_all(node.assocs)) + end + + # Visit a Begin node. + def visit_begin(node) + node.copy(bodystmt: visit(node.bodystmt)) + end + + # Visit a PinnedBegin node. + def visit_pinned_begin(node) + node.copy + end + + # Visit a Binary node. + def visit_binary(node) + node.copy + end + + # Visit a BlockVar node. + def visit_block_var(node) + node.copy(params: visit(node.params), locals: visit_all(node.locals)) + end + + # Visit a BlockArg node. + def visit_blockarg(node) + node.copy(name: visit(node.name)) + end + + # Visit a BodyStmt node. + def visit_bodystmt(node) + node.copy( + statements: visit(node.statements), + rescue_clause: visit(node.rescue_clause), + else_clause: visit(node.else_clause), + ensure_clause: visit(node.ensure_clause) + ) + end + + # Visit a Break node. + def visit_break(node) + node.copy(arguments: visit(node.arguments)) + end + + # Visit a Call node. + def visit_call(node) + node.copy( + receiver: visit(node.receiver), + operator: node.operator == :"::" ? :"::" : visit(node.operator), + message: node.message == :call ? :call : visit(node.message), + arguments: visit(node.arguments) + ) + end + + # Visit a Case node. + def visit_case(node) + node.copy( + keyword: visit(node.keyword), + value: visit(node.value), + consequent: visit(node.consequent) + ) + end + + # Visit a RAssign node. + def visit_rassign(node) + node.copy(operator: visit(node.operator)) + end + + # Visit a ClassDeclaration node. + def visit_class(node) + node.copy( + constant: visit(node.constant), + superclass: visit(node.superclass), + bodystmt: visit(node.bodystmt) + ) + end + + # Visit a Comma node. + def visit_comma(node) + node.copy + end + + # Visit a Command node. + def visit_command(node) + node.copy( + message: visit(node.message), + arguments: visit(node.arguments), + block: visit(node.block) + ) + end + + # Visit a CommandCall node. + def visit_command_call(node) + node.copy( + operator: node.operator == :"::" ? :"::" : visit(node.operator), + message: visit(node.message), + arguments: visit(node.arguments), + block: visit(node.block) + ) + end + + # Visit a Comment node. + def visit_comment(node) + node.copy + end + + # Visit a Const node. + def visit_const(node) + node.copy + end + + # Visit a ConstPathField node. + def visit_const_path_field(node) + node.copy(constant: visit(node.constant)) + end + + # Visit a ConstPathRef node. + def visit_const_path_ref(node) + node.copy(constant: visit(node.constant)) + end + + # Visit a ConstRef node. + def visit_const_ref(node) + node.copy(constant: visit(node.constant)) + end + + # Visit a CVar node. + def visit_cvar(node) + node.copy + end + + # Visit a Def node. + def visit_def(node) + node.copy( + target: visit(node.target), + operator: visit(node.operator), + name: visit(node.name), + params: visit(node.params), + bodystmt: visit(node.bodystmt) + ) + end + + # Visit a Defined node. + def visit_defined(node) + node.copy + end + + # Visit a Block node. + def visit_block(node) + node.copy( + opening: visit(node.opening), + block_var: visit(node.block_var), + bodystmt: visit(node.bodystmt) + ) + end + + # Visit a RangeNode node. + def visit_range(node) + node.copy( + left: visit(node.left), + operator: visit(node.operator), + right: visit(node.right) + ) + end + + # Visit a DynaSymbol node. + def visit_dyna_symbol(node) + node.copy(parts: visit_all(node.parts)) + end + + # Visit a Else node. + def visit_else(node) + node.copy( + keyword: visit(node.keyword), + statements: visit(node.statements) + ) + end + + # Visit a Elsif node. + def visit_elsif(node) + node.copy( + statements: visit(node.statements), + consequent: visit(node.consequent) + ) + end + + # Visit a EmbDoc node. + def visit_embdoc(node) + node.copy + end + + # Visit a EmbExprBeg node. + def visit_embexpr_beg(node) + node.copy + end + + # Visit a EmbExprEnd node. + def visit_embexpr_end(node) + node.copy + end + + # Visit a EmbVar node. + def visit_embvar(node) + node.copy + end + + # Visit a Ensure node. + def visit_ensure(node) + node.copy( + keyword: visit(node.keyword), + statements: visit(node.statements) + ) + end + + # Visit a ExcessedComma node. + def visit_excessed_comma(node) + node.copy + end + + # Visit a Field node. + def visit_field(node) + node.copy( + operator: node.operator == :"::" ? :"::" : visit(node.operator), + name: visit(node.name) + ) + end + + # Visit a FloatLiteral node. + def visit_float(node) + node.copy + end + + # Visit a FndPtn node. + def visit_fndptn(node) + node.copy( + constant: visit(node.constant), + left: visit(node.left), + values: visit_all(node.values), + right: visit(node.right) + ) + end + + # Visit a For node. + def visit_for(node) + node.copy(index: visit(node.index), statements: visit(node.statements)) + end + + # Visit a GVar node. + def visit_gvar(node) + node.copy + end + + # Visit a HashLiteral node. + def visit_hash(node) + node.copy(lbrace: visit(node.lbrace), assocs: visit_all(node.assocs)) + end + + # Visit a Heredoc node. + def visit_heredoc(node) + node.copy( + beginning: visit(node.beginning), + ending: visit(node.ending), + parts: visit_all(node.parts) + ) + end + + # Visit a HeredocBeg node. + def visit_heredoc_beg(node) + node.copy + end + + # Visit a HeredocEnd node. + def visit_heredoc_end(node) + node.copy + end + + # Visit a HshPtn node. + def visit_hshptn(node) + node.copy( + constant: visit(node.constant), + keywords: + node.keywords.map { |label, value| [visit(label), visit(value)] }, + keyword_rest: visit(node.keyword_rest) + ) + end + + # Visit a Ident node. + def visit_ident(node) + node.copy + end + + # Visit a IfNode node. + def visit_if(node) + node.copy( + predicate: visit(node.predicate), + statements: visit(node.statements), + consequent: visit(node.consequent) + ) + end + + # Visit a IfOp node. + def visit_if_op(node) + node.copy + end + + # Visit a Imaginary node. + def visit_imaginary(node) + node.copy + end + + # Visit a In node. + def visit_in(node) + node.copy( + statements: visit(node.statements), + consequent: visit(node.consequent) + ) + end + + # Visit a Int node. + def visit_int(node) + node.copy + end + + # Visit a IVar node. + def visit_ivar(node) + node.copy + end + + # Visit a Kw node. + def visit_kw(node) + node.copy + end + + # Visit a KwRestParam node. + def visit_kwrest_param(node) + node.copy(name: visit(node.name)) + end + + # Visit a Label node. + def visit_label(node) + node.copy + end + + # Visit a LabelEnd node. + def visit_label_end(node) + node.copy + end + + # Visit a Lambda node. + def visit_lambda(node) + node.copy( + params: visit(node.params), + statements: visit(node.statements) + ) + end + + # Visit a LambdaVar node. + def visit_lambda_var(node) + node.copy(params: visit(node.params), locals: visit_all(node.locals)) + end + + # Visit a LBrace node. + def visit_lbrace(node) + node.copy + end + + # Visit a LBracket node. + def visit_lbracket(node) + node.copy + end + + # Visit a LParen node. + def visit_lparen(node) + node.copy + end + + # Visit a MAssign node. + def visit_massign(node) + node.copy(target: visit(node.target)) + end + + # Visit a MethodAddBlock node. + def visit_method_add_block(node) + node.copy(call: visit(node.call), block: visit(node.block)) + end + + # Visit a MLHS node. + def visit_mlhs(node) + node.copy(parts: visit_all(node.parts)) + end + + # Visit a MLHSParen node. + def visit_mlhs_paren(node) + node.copy(contents: visit(node.contents)) + end + + # Visit a ModuleDeclaration node. + def visit_module(node) + node.copy( + constant: visit(node.constant), + bodystmt: visit(node.bodystmt) + ) + end + + # Visit a MRHS node. + def visit_mrhs(node) + node.copy(parts: visit_all(node.parts)) + end + + # Visit a Next node. + def visit_next(node) + node.copy(arguments: visit(node.arguments)) + end + + # Visit a Op node. + def visit_op(node) + node.copy + end + + # Visit a OpAssign node. + def visit_opassign(node) + node.copy(target: visit(node.target), operator: visit(node.operator)) + end + + # Visit a Params node. + def visit_params(node) + node.copy( + requireds: visit_all(node.requireds), + optionals: + node.optionals.map { |ident, value| [visit(ident), visit(value)] }, + rest: visit(node.rest), + posts: visit_all(node.posts), + keywords: + node.keywords.map { |ident, value| [visit(ident), visit(value)] }, + keyword_rest: + node.keyword_rest == :nil ? :nil : visit(node.keyword_rest), + block: visit(node.block) + ) + end + + # Visit a Paren node. + def visit_paren(node) + node.copy(lparen: visit(node.lparen), contents: visit(node.contents)) + end + + # Visit a Period node. + def visit_period(node) + node.copy + end + + # Visit a Program node. + def visit_program(node) + node.copy(statements: visit(node.statements)) + end + + # Visit a QSymbols node. + def visit_qsymbols(node) + node.copy( + beginning: visit(node.beginning), + elements: visit_all(node.elements) + ) + end + + # Visit a QSymbolsBeg node. + def visit_qsymbols_beg(node) + node.copy + end + + # Visit a QWords node. + def visit_qwords(node) + node.copy( + beginning: visit(node.beginning), + elements: visit_all(node.elements) + ) + end + + # Visit a QWordsBeg node. + def visit_qwords_beg(node) + node.copy + end + + # Visit a RationalLiteral node. + def visit_rational(node) + node.copy + end + + # Visit a RBrace node. + def visit_rbrace(node) + node.copy + end + + # Visit a RBracket node. + def visit_rbracket(node) + node.copy + end + + # Visit a Redo node. + def visit_redo(node) + node.copy + end + + # Visit a RegexpContent node. + def visit_regexp_content(node) + node.copy(parts: visit_all(node.parts)) + end + + # Visit a RegexpBeg node. + def visit_regexp_beg(node) + node.copy + end + + # Visit a RegexpEnd node. + def visit_regexp_end(node) + node.copy + end + + # Visit a RegexpLiteral node. + def visit_regexp_literal(node) + node.copy(parts: visit_all(node.parts)) + end + + # Visit a RescueEx node. + def visit_rescue_ex(node) + node.copy(variable: visit(node.variable)) + end + + # Visit a Rescue node. + def visit_rescue(node) + node.copy( + keyword: visit(node.keyword), + exception: visit(node.exception), + statements: visit(node.statements), + consequent: visit(node.consequent) + ) + end + + # Visit a RescueMod node. + def visit_rescue_mod(node) + node.copy + end + + # Visit a RestParam node. + def visit_rest_param(node) + node.copy(name: visit(node.name)) + end + + # Visit a Retry node. + def visit_retry(node) + node.copy + end + + # Visit a Return node. + def visit_return(node) + node.copy(arguments: visit(node.arguments)) + end + + # Visit a RParen node. + def visit_rparen(node) + node.copy + end + + # Visit a SClass node. + def visit_sclass(node) + node.copy(bodystmt: visit(node.bodystmt)) + end + + # Visit a Statements node. + def visit_statements(node) + node.copy(body: visit_all(node.body)) + end + + # Visit a StringContent node. + def visit_string_content(node) + node.copy(parts: visit_all(node.parts)) + end + + # Visit a StringConcat node. + def visit_string_concat(node) + node.copy(left: visit(node.left), right: visit(node.right)) + end + + # Visit a StringDVar node. + def visit_string_dvar(node) + node.copy(variable: visit(node.variable)) + end + + # Visit a StringEmbExpr node. + def visit_string_embexpr(node) + node.copy(statements: visit(node.statements)) + end + + # Visit a StringLiteral node. + def visit_string_literal(node) + node.copy(parts: visit_all(node.parts)) + end + + # Visit a Super node. + def visit_super(node) + node.copy(arguments: visit(node.arguments)) + end + + # Visit a SymBeg node. + def visit_symbeg(node) + node.copy + end + + # Visit a SymbolContent node. + def visit_symbol_content(node) + node.copy(value: visit(node.value)) + end + + # Visit a SymbolLiteral node. + def visit_symbol_literal(node) + node.copy(value: visit(node.value)) + end + + # Visit a Symbols node. + def visit_symbols(node) + node.copy( + beginning: visit(node.beginning), + elements: visit_all(node.elements) + ) + end + + # Visit a SymbolsBeg node. + def visit_symbols_beg(node) + node.copy + end + + # Visit a TLambda node. + def visit_tlambda(node) + node.copy + end + + # Visit a TLamBeg node. + def visit_tlambeg(node) + node.copy + end + + # Visit a TopConstField node. + def visit_top_const_field(node) + node.copy(constant: visit(node.constant)) + end + + # Visit a TopConstRef node. + def visit_top_const_ref(node) + node.copy(constant: visit(node.constant)) + end + + # Visit a TStringBeg node. + def visit_tstring_beg(node) + node.copy + end + + # Visit a TStringContent node. + def visit_tstring_content(node) + node.copy + end + + # Visit a TStringEnd node. + def visit_tstring_end(node) + node.copy + end + + # Visit a Not node. + def visit_not(node) + node.copy(statement: visit(node.statement)) + end + + # Visit a Unary node. + def visit_unary(node) + node.copy + end + + # Visit a Undef node. + def visit_undef(node) + node.copy(symbols: visit_all(node.symbols)) + end + + # Visit a UnlessNode node. + def visit_unless(node) + node.copy( + predicate: visit(node.predicate), + statements: visit(node.statements), + consequent: visit(node.consequent) + ) + end + + # Visit a UntilNode node. + def visit_until(node) + node.copy( + predicate: visit(node.predicate), + statements: visit(node.statements) + ) + end + + # Visit a VarField node. + def visit_var_field(node) + node.copy(value: visit(node.value)) + end + + # Visit a VarRef node. + def visit_var_ref(node) + node.copy(value: visit(node.value)) + end + + # Visit a PinnedVarRef node. + def visit_pinned_var_ref(node) + node.copy(value: visit(node.value)) + end + + # Visit a VCall node. + def visit_vcall(node) + node.copy(value: visit(node.value)) + end + + # Visit a VoidStmt node. + def visit_void_stmt(node) + node.copy + end + + # Visit a When node. + def visit_when(node) + node.copy( + arguments: visit(node.arguments), + statements: visit(node.statements), + consequent: visit(node.consequent) + ) + end + + # Visit a WhileNode node. + def visit_while(node) + node.copy( + predicate: visit(node.predicate), + statements: visit(node.statements) + ) + end + + # Visit a Word node. + def visit_word(node) + node.copy(parts: visit_all(node.parts)) + end + + # Visit a Words node. + def visit_words(node) + node.copy( + beginning: visit(node.beginning), + elements: visit_all(node.elements) + ) + end + + # Visit a WordsBeg node. + def visit_words_beg(node) + node.copy + end + + # Visit a XString node. + def visit_xstring(node) + node.copy(parts: visit_all(node.parts)) + end + + # Visit a XStringLiteral node. + def visit_xstring_literal(node) + node.copy(parts: visit_all(node.parts)) + end + + # Visit a YieldNode node. + def visit_yield(node) + node.copy(arguments: visit(node.arguments)) + end + + # Visit a ZSuper node. + def visit_zsuper(node) + node.copy + end + end +end diff --git a/lib/syntax_tree/node.rb b/lib/syntax_tree/node.rb index 0a495890..567ec0c8 100644 --- a/lib/syntax_tree/node.rb +++ b/lib/syntax_tree/node.rb @@ -135,19 +135,19 @@ def end_char end def pretty_print(q) - accept(Visitor::PrettyPrintVisitor.new(q)) + accept(PrettyPrintVisitor.new(q)) end def to_json(*opts) - accept(Visitor::JSONVisitor.new).to_json(*opts) + accept(JSONVisitor.new).to_json(*opts) end def to_mermaid - accept(Visitor::MermaidVisitor.new) + accept(MermaidVisitor.new) end def construct_keys - PrettierPrint.format(+"") { |q| accept(Visitor::MatchVisitor.new(q)) } + PrettierPrint.format(+"") { |q| accept(MatchVisitor.new(q)) } end end diff --git a/lib/syntax_tree/pretty_print_visitor.rb b/lib/syntax_tree/pretty_print_visitor.rb new file mode 100644 index 00000000..894e0cf4 --- /dev/null +++ b/lib/syntax_tree/pretty_print_visitor.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +module SyntaxTree + # This visitor pretty-prints the AST into an equivalent s-expression. + class PrettyPrintVisitor < FieldVisitor + attr_reader :q + + def initialize(q) + @q = q + end + + # This is here because we need to make sure the operator is cast to a string + # before we print it out. + def visit_binary(node) + node(node, "binary") do + field("left", node.left) + text("operator", node.operator.to_s) + field("right", node.right) + comments(node) + end + end + + # This is here to make it a little nicer to look at labels since they + # typically have their : at the end of the value. + def visit_label(node) + node(node, "label") do + q.breakable + q.text(":") + q.text(node.value[0...-1]) + comments(node) + end + end + + private + + def comments(node) + return if node.comments.empty? + + q.breakable + q.group(2, "(", ")") do + q.seplist(node.comments) { |comment| q.pp(comment) } + end + end + + def field(_name, value) + q.breakable + q.pp(value) + end + + def list(_name, values) + q.breakable + q.group(2, "(", ")") { q.seplist(values) { |value| q.pp(value) } } + end + + def node(_node, type) + q.group(2, "(", ")") do + q.text(type) + yield + end + end + + def pairs(_name, values) + q.group(2, "(", ")") do + q.seplist(values) do |(key, value)| + q.pp(key) + + if value + q.text("=") + q.group(2) do + q.breakable("") + q.pp(value) + end + end + end + end + end + + def text(_name, value) + q.breakable + q.text(value) + end + end +end diff --git a/lib/syntax_tree/visitor/environment.rb b/lib/syntax_tree/visitor/environment.rb deleted file mode 100644 index b07a5203..00000000 --- a/lib/syntax_tree/visitor/environment.rb +++ /dev/null @@ -1,84 +0,0 @@ -# frozen_string_literal: true - -module SyntaxTree - # The environment class is used to keep track of local variables and arguments - # inside a particular scope - class Environment - # This class tracks the occurrences of a local variable or argument - class Local - # [Symbol] The type of the local (e.g. :argument, :variable) - attr_reader :type - - # [Array[Location]] The locations of all definitions and assignments of - # this local - attr_reader :definitions - - # [Array[Location]] The locations of all usages of this local - attr_reader :usages - - # initialize: (Symbol type) -> void - def initialize(type) - @type = type - @definitions = [] - @usages = [] - end - - # add_definition: (Location location) -> void - def add_definition(location) - @definitions << location - end - - # add_usage: (Location location) -> void - def add_usage(location) - @usages << location - end - end - - # [Array[Local]] The local variables and arguments defined in this - # environment - attr_reader :locals - - # [Environment | nil] The parent environment - attr_reader :parent - - # initialize: (Environment | nil parent) -> void - def initialize(parent = nil) - @locals = {} - @parent = parent - end - - # Adding a local definition will either insert a new entry in the locals - # hash or append a new definition location to an existing local. Notice that - # it's not possible to change the type of a local after it has been - # registered - # add_local_definition: (Ident | Label identifier, Symbol type) -> void - def add_local_definition(identifier, type) - name = identifier.value.delete_suffix(":") - - @locals[name] ||= Local.new(type) - @locals[name].add_definition(identifier.location) - end - - # Adding a local usage will either insert a new entry in the locals - # hash or append a new usage location to an existing local. Notice that - # it's not possible to change the type of a local after it has been - # registered - # add_local_usage: (Ident | Label identifier, Symbol type) -> void - def add_local_usage(identifier, type) - name = identifier.value.delete_suffix(":") - - @locals[name] ||= Local.new(type) - @locals[name].add_usage(identifier.location) - end - - # Try to find the local given its name in this environment or any of its - # parents - # find_local: (String name) -> Local | nil - def find_local(name) - local = @locals[name] - return local unless local.nil? - - @parent&.find_local(name) - end - end -end diff --git a/lib/syntax_tree/visitor/field_visitor.rb b/lib/syntax_tree/visitor/field_visitor.rb deleted file mode 100644 index 6e643e09..00000000 --- a/lib/syntax_tree/visitor/field_visitor.rb +++ /dev/null @@ -1,1031 +0,0 @@ -# frozen_string_literal: true - -module SyntaxTree - class Visitor - # This is the parent class of a lot of built-in visitors for Syntax Tree. It - # reflects visiting each of the fields on every node in turn. It itself does - # not do anything with these fields, it leaves that behavior up to the - # subclass to implement. - # - # In order to properly use this class, you will need to subclass it and - # implement #comments, #field, #list, #node, #pairs, and #text. Those are - # documented here. - # - # == comments(node) - # - # This accepts the node that is being visited and does something depending - # on the comments attached to the node. - # - # == field(name, value) - # - # This accepts the name of the field being visited as a string (like - # "value") and the actual value of that field. The value can be a subclass - # of Node or any other type that can be held within the tree. - # - # == list(name, values) - # - # This accepts the name of the field being visited as well as a list of - # values. This is used, for example, when visiting something like the body - # of a Statements node. - # - # == node(name, node) - # - # This is the parent serialization method for each node. It is called with - # the node itself, as well as the type of the node as a string. The type - # is an internally used value that usually resembles the name of the - # ripper event that generated the node. The method should yield to the - # given block which then calls through to visit each of the fields on the - # node. - # - # == text(name, value) - # - # This accepts the name of the field being visited as well as a string - # value representing the value of the field. - # - # == pairs(name, values) - # - # This accepts the name of the field being visited as well as a list of - # pairs that represent the value of the field. It is used only in a couple - # of circumstances, like when visiting the list of optional parameters - # defined on a method. - # - class FieldVisitor < BasicVisitor - def visit_aref(node) - node(node, "aref") do - field("collection", node.collection) - field("index", node.index) - comments(node) - end - end - - def visit_aref_field(node) - node(node, "aref_field") do - field("collection", node.collection) - field("index", node.index) - comments(node) - end - end - - def visit_alias(node) - node(node, "alias") do - field("left", node.left) - field("right", node.right) - comments(node) - end - end - - def visit_arg_block(node) - node(node, "arg_block") do - field("value", node.value) if node.value - comments(node) - end - end - - def visit_arg_paren(node) - node(node, "arg_paren") do - field("arguments", node.arguments) - comments(node) - end - end - - def visit_arg_star(node) - node(node, "arg_star") do - field("value", node.value) - comments(node) - end - end - - def visit_args(node) - node(node, "args") do - list("parts", node.parts) - comments(node) - end - end - - def visit_args_forward(node) - node(node, "args_forward") { comments(node) } - end - - def visit_array(node) - node(node, "array") do - field("contents", node.contents) - comments(node) - end - end - - def visit_aryptn(node) - node(node, "aryptn") do - field("constant", node.constant) if node.constant - list("requireds", node.requireds) if node.requireds.any? - field("rest", node.rest) if node.rest - list("posts", node.posts) if node.posts.any? - comments(node) - end - end - - def visit_assign(node) - node(node, "assign") do - field("target", node.target) - field("value", node.value) - comments(node) - end - end - - def visit_assoc(node) - node(node, "assoc") do - field("key", node.key) - field("value", node.value) if node.value - comments(node) - end - end - - def visit_assoc_splat(node) - node(node, "assoc_splat") do - field("value", node.value) - comments(node) - end - end - - def visit_backref(node) - visit_token(node, "backref") - end - - def visit_backtick(node) - visit_token(node, "backtick") - end - - def visit_bare_assoc_hash(node) - node(node, "bare_assoc_hash") do - list("assocs", node.assocs) - comments(node) - end - end - - def visit_BEGIN(node) - node(node, "BEGIN") do - field("statements", node.statements) - comments(node) - end - end - - def visit_begin(node) - node(node, "begin") do - field("bodystmt", node.bodystmt) - comments(node) - end - end - - def visit_binary(node) - node(node, "binary") do - field("left", node.left) - text("operator", node.operator) - field("right", node.right) - comments(node) - end - end - - def visit_block(node) - node(node, "block") do - field("block_var", node.block_var) if node.block_var - field("bodystmt", node.bodystmt) - comments(node) - end - end - - def visit_blockarg(node) - node(node, "blockarg") do - field("name", node.name) if node.name - comments(node) - end - end - - def visit_block_var(node) - node(node, "block_var") do - field("params", node.params) - list("locals", node.locals) if node.locals.any? - comments(node) - end - end - - def visit_bodystmt(node) - node(node, "bodystmt") do - field("statements", node.statements) - field("rescue_clause", node.rescue_clause) if node.rescue_clause - field("else_clause", node.else_clause) if node.else_clause - field("ensure_clause", node.ensure_clause) if node.ensure_clause - comments(node) - end - end - - def visit_break(node) - node(node, "break") do - field("arguments", node.arguments) - comments(node) - end - end - - def visit_call(node) - node(node, "call") do - field("receiver", node.receiver) - field("operator", node.operator) - field("message", node.message) - field("arguments", node.arguments) if node.arguments - comments(node) - end - end - - def visit_case(node) - node(node, "case") do - field("keyword", node.keyword) - field("value", node.value) if node.value - field("consequent", node.consequent) - comments(node) - end - end - - def visit_CHAR(node) - visit_token(node, "CHAR") - end - - def visit_class(node) - node(node, "class") do - field("constant", node.constant) - field("superclass", node.superclass) if node.superclass - field("bodystmt", node.bodystmt) - comments(node) - end - end - - def visit_comma(node) - node(node, "comma") { field("value", node.value) } - end - - def visit_command(node) - node(node, "command") do - field("message", node.message) - field("arguments", node.arguments) - comments(node) - end - end - - def visit_command_call(node) - node(node, "command_call") do - field("receiver", node.receiver) - field("operator", node.operator) - field("message", node.message) - field("arguments", node.arguments) if node.arguments - comments(node) - end - end - - def visit_comment(node) - node(node, "comment") { field("value", node.value) } - end - - def visit_const(node) - visit_token(node, "const") - end - - def visit_const_path_field(node) - node(node, "const_path_field") do - field("parent", node.parent) - field("constant", node.constant) - comments(node) - end - end - - def visit_const_path_ref(node) - node(node, "const_path_ref") do - field("parent", node.parent) - field("constant", node.constant) - comments(node) - end - end - - def visit_const_ref(node) - node(node, "const_ref") do - field("constant", node.constant) - comments(node) - end - end - - def visit_cvar(node) - visit_token(node, "cvar") - end - - def visit_def(node) - node(node, "def") do - field("target", node.target) - field("operator", node.operator) - field("name", node.name) - field("params", node.params) - field("bodystmt", node.bodystmt) - comments(node) - end - end - - def visit_defined(node) - node(node, "defined") do - field("value", node.value) - comments(node) - end - end - - def visit_dyna_symbol(node) - node(node, "dyna_symbol") do - list("parts", node.parts) - comments(node) - end - end - - def visit_END(node) - node(node, "END") do - field("statements", node.statements) - comments(node) - end - end - - def visit_else(node) - node(node, "else") do - field("statements", node.statements) - comments(node) - end - end - - def visit_elsif(node) - node(node, "elsif") do - field("predicate", node.predicate) - field("statements", node.statements) - field("consequent", node.consequent) if node.consequent - comments(node) - end - end - - def visit_embdoc(node) - node(node, "embdoc") { field("value", node.value) } - end - - def visit_embexpr_beg(node) - node(node, "embexpr_beg") { field("value", node.value) } - end - - def visit_embexpr_end(node) - node(node, "embexpr_end") { field("value", node.value) } - end - - def visit_embvar(node) - node(node, "embvar") { field("value", node.value) } - end - - def visit_ensure(node) - node(node, "ensure") do - field("statements", node.statements) - comments(node) - end - end - - def visit_excessed_comma(node) - visit_token(node, "excessed_comma") - end - - def visit_field(node) - node(node, "field") do - field("parent", node.parent) - field("operator", node.operator) - field("name", node.name) - comments(node) - end - end - - def visit_float(node) - visit_token(node, "float") - end - - def visit_fndptn(node) - node(node, "fndptn") do - field("constant", node.constant) if node.constant - field("left", node.left) - list("values", node.values) - field("right", node.right) - comments(node) - end - end - - def visit_for(node) - node(node, "for") do - field("index", node.index) - field("collection", node.collection) - field("statements", node.statements) - comments(node) - end - end - - def visit_gvar(node) - visit_token(node, "gvar") - end - - def visit_hash(node) - node(node, "hash") do - list("assocs", node.assocs) if node.assocs.any? - comments(node) - end - end - - def visit_heredoc(node) - node(node, "heredoc") do - list("parts", node.parts) - comments(node) - end - end - - def visit_heredoc_beg(node) - visit_token(node, "heredoc_beg") - end - - def visit_heredoc_end(node) - visit_token(node, "heredoc_end") - end - - def visit_hshptn(node) - node(node, "hshptn") do - field("constant", node.constant) if node.constant - pairs("keywords", node.keywords) if node.keywords.any? - field("keyword_rest", node.keyword_rest) if node.keyword_rest - comments(node) - end - end - - def visit_ident(node) - visit_token(node, "ident") - end - - def visit_if(node) - node(node, "if") do - field("predicate", node.predicate) - field("statements", node.statements) - field("consequent", node.consequent) if node.consequent - comments(node) - end - end - - def visit_if_op(node) - node(node, "if_op") do - field("predicate", node.predicate) - field("truthy", node.truthy) - field("falsy", node.falsy) - comments(node) - end - end - - def visit_imaginary(node) - visit_token(node, "imaginary") - end - - def visit_in(node) - node(node, "in") do - field("pattern", node.pattern) - field("statements", node.statements) - field("consequent", node.consequent) if node.consequent - comments(node) - end - end - - def visit_int(node) - visit_token(node, "int") - end - - def visit_ivar(node) - visit_token(node, "ivar") - end - - def visit_kw(node) - visit_token(node, "kw") - end - - def visit_kwrest_param(node) - node(node, "kwrest_param") do - field("name", node.name) - comments(node) - end - end - - def visit_label(node) - visit_token(node, "label") - end - - def visit_label_end(node) - node(node, "label_end") { field("value", node.value) } - end - - def visit_lambda(node) - node(node, "lambda") do - field("params", node.params) - field("statements", node.statements) - comments(node) - end - end - - def visit_lambda_var(node) - node(node, "lambda_var") do - field("params", node.params) - list("locals", node.locals) if node.locals.any? - comments(node) - end - end - - def visit_lbrace(node) - visit_token(node, "lbrace") - end - - def visit_lbracket(node) - visit_token(node, "lbracket") - end - - def visit_lparen(node) - visit_token(node, "lparen") - end - - def visit_massign(node) - node(node, "massign") do - field("target", node.target) - field("value", node.value) - comments(node) - end - end - - def visit_method_add_block(node) - node(node, "method_add_block") do - field("call", node.call) - field("block", node.block) - comments(node) - end - end - - def visit_mlhs(node) - node(node, "mlhs") do - list("parts", node.parts) - comments(node) - end - end - - def visit_mlhs_paren(node) - node(node, "mlhs_paren") do - field("contents", node.contents) - comments(node) - end - end - - def visit_module(node) - node(node, "module") do - field("constant", node.constant) - field("bodystmt", node.bodystmt) - comments(node) - end - end - - def visit_mrhs(node) - node(node, "mrhs") do - list("parts", node.parts) - comments(node) - end - end - - def visit_next(node) - node(node, "next") do - field("arguments", node.arguments) - comments(node) - end - end - - def visit_not(node) - node(node, "not") do - field("statement", node.statement) - comments(node) - end - end - - def visit_op(node) - visit_token(node, "op") - end - - def visit_opassign(node) - node(node, "opassign") do - field("target", node.target) - field("operator", node.operator) - field("value", node.value) - comments(node) - end - end - - def visit_params(node) - node(node, "params") do - list("requireds", node.requireds) if node.requireds.any? - pairs("optionals", node.optionals) if node.optionals.any? - field("rest", node.rest) if node.rest - list("posts", node.posts) if node.posts.any? - pairs("keywords", node.keywords) if node.keywords.any? - field("keyword_rest", node.keyword_rest) if node.keyword_rest - field("block", node.block) if node.block - comments(node) - end - end - - def visit_paren(node) - node(node, "paren") do - field("contents", node.contents) - comments(node) - end - end - - def visit_period(node) - visit_token(node, "period") - end - - def visit_pinned_begin(node) - node(node, "pinned_begin") do - field("statement", node.statement) - comments(node) - end - end - - def visit_pinned_var_ref(node) - node(node, "pinned_var_ref") do - field("value", node.value) - comments(node) - end - end - - def visit_program(node) - node(node, "program") do - field("statements", node.statements) - comments(node) - end - end - - def visit_qsymbols(node) - node(node, "qsymbols") do - list("elements", node.elements) - comments(node) - end - end - - def visit_qsymbols_beg(node) - node(node, "qsymbols_beg") { field("value", node.value) } - end - - def visit_qwords(node) - node(node, "qwords") do - list("elements", node.elements) - comments(node) - end - end - - def visit_qwords_beg(node) - node(node, "qwords_beg") { field("value", node.value) } - end - - def visit_range(node) - node(node, "range") do - field("left", node.left) if node.left - field("operator", node.operator) - field("right", node.right) if node.right - comments(node) - end - end - - def visit_rassign(node) - node(node, "rassign") do - field("value", node.value) - field("operator", node.operator) - field("pattern", node.pattern) - comments(node) - end - end - - def visit_rational(node) - visit_token(node, "rational") - end - - def visit_rbrace(node) - node(node, "rbrace") { field("value", node.value) } - end - - def visit_rbracket(node) - node(node, "rbracket") { field("value", node.value) } - end - - def visit_redo(node) - node(node, "redo") { comments(node) } - end - - def visit_regexp_beg(node) - node(node, "regexp_beg") { field("value", node.value) } - end - - def visit_regexp_content(node) - node(node, "regexp_content") { list("parts", node.parts) } - end - - def visit_regexp_end(node) - node(node, "regexp_end") { field("value", node.value) } - end - - def visit_regexp_literal(node) - node(node, "regexp_literal") do - list("parts", node.parts) - field("options", node.options) - comments(node) - end - end - - def visit_rescue(node) - node(node, "rescue") do - field("exception", node.exception) if node.exception - field("statements", node.statements) - field("consequent", node.consequent) if node.consequent - comments(node) - end - end - - def visit_rescue_ex(node) - node(node, "rescue_ex") do - field("exceptions", node.exceptions) - field("variable", node.variable) - comments(node) - end - end - - def visit_rescue_mod(node) - node(node, "rescue_mod") do - field("statement", node.statement) - field("value", node.value) - comments(node) - end - end - - def visit_rest_param(node) - node(node, "rest_param") do - field("name", node.name) - comments(node) - end - end - - def visit_retry(node) - node(node, "retry") { comments(node) } - end - - def visit_return(node) - node(node, "return") do - field("arguments", node.arguments) - comments(node) - end - end - - def visit_rparen(node) - node(node, "rparen") { field("value", node.value) } - end - - def visit_sclass(node) - node(node, "sclass") do - field("target", node.target) - field("bodystmt", node.bodystmt) - comments(node) - end - end - - def visit_statements(node) - node(node, "statements") do - list("body", node.body) - comments(node) - end - end - - def visit_string_concat(node) - node(node, "string_concat") do - field("left", node.left) - field("right", node.right) - comments(node) - end - end - - def visit_string_content(node) - node(node, "string_content") { list("parts", node.parts) } - end - - def visit_string_dvar(node) - node(node, "string_dvar") do - field("variable", node.variable) - comments(node) - end - end - - def visit_string_embexpr(node) - node(node, "string_embexpr") do - field("statements", node.statements) - comments(node) - end - end - - def visit_string_literal(node) - node(node, "string_literal") do - list("parts", node.parts) - comments(node) - end - end - - def visit_super(node) - node(node, "super") do - field("arguments", node.arguments) - comments(node) - end - end - - def visit_symbeg(node) - node(node, "symbeg") { field("value", node.value) } - end - - def visit_symbol_content(node) - node(node, "symbol_content") { field("value", node.value) } - end - - def visit_symbol_literal(node) - node(node, "symbol_literal") do - field("value", node.value) - comments(node) - end - end - - def visit_symbols(node) - node(node, "symbols") do - list("elements", node.elements) - comments(node) - end - end - - def visit_symbols_beg(node) - node(node, "symbols_beg") { field("value", node.value) } - end - - def visit_tlambda(node) - node(node, "tlambda") { field("value", node.value) } - end - - def visit_tlambeg(node) - node(node, "tlambeg") { field("value", node.value) } - end - - def visit_top_const_field(node) - node(node, "top_const_field") do - field("constant", node.constant) - comments(node) - end - end - - def visit_top_const_ref(node) - node(node, "top_const_ref") do - field("constant", node.constant) - comments(node) - end - end - - def visit_tstring_beg(node) - node(node, "tstring_beg") { field("value", node.value) } - end - - def visit_tstring_content(node) - visit_token(node, "tstring_content") - end - - def visit_tstring_end(node) - node(node, "tstring_end") { field("value", node.value) } - end - - def visit_unary(node) - node(node, "unary") do - field("operator", node.operator) - field("statement", node.statement) - comments(node) - end - end - - def visit_undef(node) - node(node, "undef") do - list("symbols", node.symbols) - comments(node) - end - end - - def visit_unless(node) - node(node, "unless") do - field("predicate", node.predicate) - field("statements", node.statements) - field("consequent", node.consequent) if node.consequent - comments(node) - end - end - - def visit_until(node) - node(node, "until") do - field("predicate", node.predicate) - field("statements", node.statements) - comments(node) - end - end - - def visit_var_field(node) - node(node, "var_field") do - field("value", node.value) - comments(node) - end - end - - def visit_var_ref(node) - node(node, "var_ref") do - field("value", node.value) - comments(node) - end - end - - def visit_vcall(node) - node(node, "vcall") do - field("value", node.value) - comments(node) - end - end - - def visit_void_stmt(node) - node(node, "void_stmt") { comments(node) } - end - - def visit_when(node) - node(node, "when") do - field("arguments", node.arguments) - field("statements", node.statements) - field("consequent", node.consequent) if node.consequent - comments(node) - end - end - - def visit_while(node) - node(node, "while") do - field("predicate", node.predicate) - field("statements", node.statements) - comments(node) - end - end - - def visit_word(node) - node(node, "word") do - list("parts", node.parts) - comments(node) - end - end - - def visit_words(node) - node(node, "words") do - list("elements", node.elements) - comments(node) - end - end - - def visit_words_beg(node) - node(node, "words_beg") { field("value", node.value) } - end - - def visit_xstring(node) - node(node, "xstring") { list("parts", node.parts) } - end - - def visit_xstring_literal(node) - node(node, "xstring_literal") do - list("parts", node.parts) - comments(node) - end - end - - def visit_yield(node) - node(node, "yield") do - field("arguments", node.arguments) - comments(node) - end - end - - def visit_zsuper(node) - node(node, "zsuper") { comments(node) } - end - - def visit___end__(node) - visit_token(node, "__end__") - end - - private - - def visit_token(node, type) - node(node, type) do - field("value", node.value) - comments(node) - end - end - end - end -end diff --git a/lib/syntax_tree/visitor/json_visitor.rb b/lib/syntax_tree/visitor/json_visitor.rb deleted file mode 100644 index b516980c..00000000 --- a/lib/syntax_tree/visitor/json_visitor.rb +++ /dev/null @@ -1,55 +0,0 @@ -# frozen_string_literal: true - -module SyntaxTree - class Visitor - # This visitor transforms the AST into a hash that contains only primitives - # that can be easily serialized into JSON. - class JSONVisitor < FieldVisitor - attr_reader :target - - def initialize - @target = nil - end - - private - - def comments(node) - target[:comments] = visit_all(node.comments) - end - - def field(name, value) - target[name] = value.is_a?(Node) ? visit(value) : value - end - - def list(name, values) - target[name] = visit_all(values) - end - - def node(node, type) - previous = @target - @target = { type: type, location: visit_location(node.location) } - yield - @target - ensure - @target = previous - end - - def pairs(name, values) - target[name] = values.map { |(key, value)| [visit(key), visit(value)] } - end - - def text(name, value) - target[name] = value - end - - def visit_location(location) - [ - location.start_line, - location.start_char, - location.end_line, - location.end_char - ] - end - end - end -end diff --git a/lib/syntax_tree/visitor/match_visitor.rb b/lib/syntax_tree/visitor/match_visitor.rb deleted file mode 100644 index e0bdaf08..00000000 --- a/lib/syntax_tree/visitor/match_visitor.rb +++ /dev/null @@ -1,122 +0,0 @@ -# frozen_string_literal: true - -module SyntaxTree - class Visitor - # This visitor transforms the AST into a Ruby pattern matching expression - # that would match correctly against the AST. - class MatchVisitor < FieldVisitor - attr_reader :q - - def initialize(q) - @q = q - end - - def visit(node) - case node - when Node - super - when String - # pp will split up a string on newlines and concat them together using - # a "+" operator. This breaks the pattern matching expression. So - # instead we're going to check here for strings and manually put the - # entire value into the output buffer. - q.text(node.inspect) - else - node.pretty_print(q) - end - end - - private - - def comments(node) - return if node.comments.empty? - - q.nest(0) do - q.text("comments: [") - q.indent do - q.breakable("") - q.seplist(node.comments) { |comment| visit(comment) } - end - q.breakable("") - q.text("]") - end - end - - def field(name, value) - q.nest(0) do - q.text(name) - q.text(": ") - visit(value) - end - end - - def list(name, values) - q.group do - q.text(name) - q.text(": [") - q.indent do - q.breakable("") - q.seplist(values) { |value| visit(value) } - end - q.breakable("") - q.text("]") - end - end - - def node(node, _type) - items = [] - q.with_target(items) { yield } - - if items.empty? - q.text(node.class.name) - return - end - - q.group do - q.text(node.class.name) - q.text("[") - q.indent do - q.breakable("") - q.seplist(items) { |item| q.target << item } - end - q.breakable("") - q.text("]") - end - end - - def pairs(name, values) - q.group do - q.text(name) - q.text(": [") - q.indent do - q.breakable("") - q.seplist(values) do |(key, value)| - q.group do - q.text("[") - q.indent do - q.breakable("") - visit(key) - q.text(",") - q.breakable - visit(value || nil) - end - q.breakable("") - q.text("]") - end - end - end - q.breakable("") - q.text("]") - end - end - - def text(name, value) - q.nest(0) do - q.text(name) - q.text(": ") - value.pretty_print(q) - end - end - end - end -end diff --git a/lib/syntax_tree/visitor/mermaid_visitor.rb b/lib/syntax_tree/visitor/mermaid_visitor.rb deleted file mode 100644 index 504e2fb0..00000000 --- a/lib/syntax_tree/visitor/mermaid_visitor.rb +++ /dev/null @@ -1,75 +0,0 @@ -# frozen_string_literal: true - -module SyntaxTree - class Visitor - # This visitor transforms the AST into a mermaid flow chart. - class MermaidVisitor < FieldVisitor - attr_reader :flowchart, :target - - def initialize - @flowchart = Mermaid.flowchart - @target = nil - end - - def visit_program(node) - super - flowchart.render - end - - private - - def comments(node) - # Ignore - end - - def field(name, value) - case value - when nil - # skip - when Node - flowchart.link(target, visit(value), name) - else - to = - flowchart.node( - "#{target.id}_#{name}", - value.inspect, - shape: :stadium - ) - flowchart.link(target, to, name) - end - end - - def list(name, values) - values.each_with_index do |value, index| - field("#{name}[#{index}]", value) - end - end - - def node(node, type) - previous_target = target - - begin - @target = flowchart.node("node_#{node.object_id}", type) - yield - @target - ensure - @target = previous_target - end - end - - def pairs(name, values) - values.each_with_index do |(key, value), index| - to = flowchart.node("#{target.id}_#{name}_#{index}", shape: :circle) - - flowchart.link(target, to, "#{name}[#{index}]") - flowchart.link(to, visit(key), "[0]") - flowchart.link(to, visit(value), "[1]") if value - end - end - - def text(name, value) - field(name, value) - end - end - end -end diff --git a/lib/syntax_tree/visitor/mutation_visitor.rb b/lib/syntax_tree/visitor/mutation_visitor.rb deleted file mode 100644 index 65f8c5ba..00000000 --- a/lib/syntax_tree/visitor/mutation_visitor.rb +++ /dev/null @@ -1,924 +0,0 @@ -# frozen_string_literal: true - -module SyntaxTree - class Visitor - # This visitor walks through the tree and copies each node as it is being - # visited. This is useful for mutating the tree before it is formatted. - class MutationVisitor < BasicVisitor - attr_reader :mutations - - def initialize - @mutations = [] - end - - # Create a new mutation based on the given query that will mutate the node - # using the given block. The block should return a new node that will take - # the place of the given node in the tree. These blocks frequently make - # use of the `copy` method on nodes to create a new node with the same - # properties as the original node. - def mutate(query, &block) - mutations << [Pattern.new(query).compile, block] - end - - # This is the base visit method for each node in the tree. It first - # creates a copy of the node using the visit_* methods defined below. Then - # it checks each mutation in sequence and calls it if it finds a match. - def visit(node) - return unless node - result = node.accept(self) - - mutations.each do |(pattern, mutation)| - result = mutation.call(result) if pattern.call(result) - end - - result - end - - # Visit a BEGINBlock node. - def visit_BEGIN(node) - node.copy( - lbrace: visit(node.lbrace), - statements: visit(node.statements) - ) - end - - # Visit a CHAR node. - def visit_CHAR(node) - node.copy - end - - # Visit a ENDBlock node. - def visit_END(node) - node.copy( - lbrace: visit(node.lbrace), - statements: visit(node.statements) - ) - end - - # Visit a EndContent node. - def visit___end__(node) - node.copy - end - - # Visit a AliasNode node. - def visit_alias(node) - node.copy(left: visit(node.left), right: visit(node.right)) - end - - # Visit a ARef node. - def visit_aref(node) - node.copy(index: visit(node.index)) - end - - # Visit a ARefField node. - def visit_aref_field(node) - node.copy(index: visit(node.index)) - end - - # Visit a ArgParen node. - def visit_arg_paren(node) - node.copy(arguments: visit(node.arguments)) - end - - # Visit a Args node. - def visit_args(node) - node.copy(parts: visit_all(node.parts)) - end - - # Visit a ArgBlock node. - def visit_arg_block(node) - node.copy(value: visit(node.value)) - end - - # Visit a ArgStar node. - def visit_arg_star(node) - node.copy(value: visit(node.value)) - end - - # Visit a ArgsForward node. - def visit_args_forward(node) - node.copy - end - - # Visit a ArrayLiteral node. - def visit_array(node) - node.copy( - lbracket: visit(node.lbracket), - contents: visit(node.contents) - ) - end - - # Visit a AryPtn node. - def visit_aryptn(node) - node.copy( - constant: visit(node.constant), - requireds: visit_all(node.requireds), - rest: visit(node.rest), - posts: visit_all(node.posts) - ) - end - - # Visit a Assign node. - def visit_assign(node) - node.copy(target: visit(node.target)) - end - - # Visit a Assoc node. - def visit_assoc(node) - node.copy - end - - # Visit a AssocSplat node. - def visit_assoc_splat(node) - node.copy - end - - # Visit a Backref node. - def visit_backref(node) - node.copy - end - - # Visit a Backtick node. - def visit_backtick(node) - node.copy - end - - # Visit a BareAssocHash node. - def visit_bare_assoc_hash(node) - node.copy(assocs: visit_all(node.assocs)) - end - - # Visit a Begin node. - def visit_begin(node) - node.copy(bodystmt: visit(node.bodystmt)) - end - - # Visit a PinnedBegin node. - def visit_pinned_begin(node) - node.copy - end - - # Visit a Binary node. - def visit_binary(node) - node.copy - end - - # Visit a BlockVar node. - def visit_block_var(node) - node.copy(params: visit(node.params), locals: visit_all(node.locals)) - end - - # Visit a BlockArg node. - def visit_blockarg(node) - node.copy(name: visit(node.name)) - end - - # Visit a BodyStmt node. - def visit_bodystmt(node) - node.copy( - statements: visit(node.statements), - rescue_clause: visit(node.rescue_clause), - else_clause: visit(node.else_clause), - ensure_clause: visit(node.ensure_clause) - ) - end - - # Visit a Break node. - def visit_break(node) - node.copy(arguments: visit(node.arguments)) - end - - # Visit a Call node. - def visit_call(node) - node.copy( - receiver: visit(node.receiver), - operator: node.operator == :"::" ? :"::" : visit(node.operator), - message: node.message == :call ? :call : visit(node.message), - arguments: visit(node.arguments) - ) - end - - # Visit a Case node. - def visit_case(node) - node.copy( - keyword: visit(node.keyword), - value: visit(node.value), - consequent: visit(node.consequent) - ) - end - - # Visit a RAssign node. - def visit_rassign(node) - node.copy(operator: visit(node.operator)) - end - - # Visit a ClassDeclaration node. - def visit_class(node) - node.copy( - constant: visit(node.constant), - superclass: visit(node.superclass), - bodystmt: visit(node.bodystmt) - ) - end - - # Visit a Comma node. - def visit_comma(node) - node.copy - end - - # Visit a Command node. - def visit_command(node) - node.copy( - message: visit(node.message), - arguments: visit(node.arguments), - block: visit(node.block) - ) - end - - # Visit a CommandCall node. - def visit_command_call(node) - node.copy( - operator: node.operator == :"::" ? :"::" : visit(node.operator), - message: visit(node.message), - arguments: visit(node.arguments), - block: visit(node.block) - ) - end - - # Visit a Comment node. - def visit_comment(node) - node.copy - end - - # Visit a Const node. - def visit_const(node) - node.copy - end - - # Visit a ConstPathField node. - def visit_const_path_field(node) - node.copy(constant: visit(node.constant)) - end - - # Visit a ConstPathRef node. - def visit_const_path_ref(node) - node.copy(constant: visit(node.constant)) - end - - # Visit a ConstRef node. - def visit_const_ref(node) - node.copy(constant: visit(node.constant)) - end - - # Visit a CVar node. - def visit_cvar(node) - node.copy - end - - # Visit a Def node. - def visit_def(node) - node.copy( - target: visit(node.target), - operator: visit(node.operator), - name: visit(node.name), - params: visit(node.params), - bodystmt: visit(node.bodystmt) - ) - end - - # Visit a Defined node. - def visit_defined(node) - node.copy - end - - # Visit a Block node. - def visit_block(node) - node.copy( - opening: visit(node.opening), - block_var: visit(node.block_var), - bodystmt: visit(node.bodystmt) - ) - end - - # Visit a RangeNode node. - def visit_range(node) - node.copy( - left: visit(node.left), - operator: visit(node.operator), - right: visit(node.right) - ) - end - - # Visit a DynaSymbol node. - def visit_dyna_symbol(node) - node.copy(parts: visit_all(node.parts)) - end - - # Visit a Else node. - def visit_else(node) - node.copy( - keyword: visit(node.keyword), - statements: visit(node.statements) - ) - end - - # Visit a Elsif node. - def visit_elsif(node) - node.copy( - statements: visit(node.statements), - consequent: visit(node.consequent) - ) - end - - # Visit a EmbDoc node. - def visit_embdoc(node) - node.copy - end - - # Visit a EmbExprBeg node. - def visit_embexpr_beg(node) - node.copy - end - - # Visit a EmbExprEnd node. - def visit_embexpr_end(node) - node.copy - end - - # Visit a EmbVar node. - def visit_embvar(node) - node.copy - end - - # Visit a Ensure node. - def visit_ensure(node) - node.copy( - keyword: visit(node.keyword), - statements: visit(node.statements) - ) - end - - # Visit a ExcessedComma node. - def visit_excessed_comma(node) - node.copy - end - - # Visit a Field node. - def visit_field(node) - node.copy( - operator: node.operator == :"::" ? :"::" : visit(node.operator), - name: visit(node.name) - ) - end - - # Visit a FloatLiteral node. - def visit_float(node) - node.copy - end - - # Visit a FndPtn node. - def visit_fndptn(node) - node.copy( - constant: visit(node.constant), - left: visit(node.left), - values: visit_all(node.values), - right: visit(node.right) - ) - end - - # Visit a For node. - def visit_for(node) - node.copy(index: visit(node.index), statements: visit(node.statements)) - end - - # Visit a GVar node. - def visit_gvar(node) - node.copy - end - - # Visit a HashLiteral node. - def visit_hash(node) - node.copy(lbrace: visit(node.lbrace), assocs: visit_all(node.assocs)) - end - - # Visit a Heredoc node. - def visit_heredoc(node) - node.copy( - beginning: visit(node.beginning), - ending: visit(node.ending), - parts: visit_all(node.parts) - ) - end - - # Visit a HeredocBeg node. - def visit_heredoc_beg(node) - node.copy - end - - # Visit a HeredocEnd node. - def visit_heredoc_end(node) - node.copy - end - - # Visit a HshPtn node. - def visit_hshptn(node) - node.copy( - constant: visit(node.constant), - keywords: - node.keywords.map { |label, value| [visit(label), visit(value)] }, - keyword_rest: visit(node.keyword_rest) - ) - end - - # Visit a Ident node. - def visit_ident(node) - node.copy - end - - # Visit a IfNode node. - def visit_if(node) - node.copy( - predicate: visit(node.predicate), - statements: visit(node.statements), - consequent: visit(node.consequent) - ) - end - - # Visit a IfOp node. - def visit_if_op(node) - node.copy - end - - # Visit a Imaginary node. - def visit_imaginary(node) - node.copy - end - - # Visit a In node. - def visit_in(node) - node.copy( - statements: visit(node.statements), - consequent: visit(node.consequent) - ) - end - - # Visit a Int node. - def visit_int(node) - node.copy - end - - # Visit a IVar node. - def visit_ivar(node) - node.copy - end - - # Visit a Kw node. - def visit_kw(node) - node.copy - end - - # Visit a KwRestParam node. - def visit_kwrest_param(node) - node.copy(name: visit(node.name)) - end - - # Visit a Label node. - def visit_label(node) - node.copy - end - - # Visit a LabelEnd node. - def visit_label_end(node) - node.copy - end - - # Visit a Lambda node. - def visit_lambda(node) - node.copy( - params: visit(node.params), - statements: visit(node.statements) - ) - end - - # Visit a LambdaVar node. - def visit_lambda_var(node) - node.copy(params: visit(node.params), locals: visit_all(node.locals)) - end - - # Visit a LBrace node. - def visit_lbrace(node) - node.copy - end - - # Visit a LBracket node. - def visit_lbracket(node) - node.copy - end - - # Visit a LParen node. - def visit_lparen(node) - node.copy - end - - # Visit a MAssign node. - def visit_massign(node) - node.copy(target: visit(node.target)) - end - - # Visit a MethodAddBlock node. - def visit_method_add_block(node) - node.copy(call: visit(node.call), block: visit(node.block)) - end - - # Visit a MLHS node. - def visit_mlhs(node) - node.copy(parts: visit_all(node.parts)) - end - - # Visit a MLHSParen node. - def visit_mlhs_paren(node) - node.copy(contents: visit(node.contents)) - end - - # Visit a ModuleDeclaration node. - def visit_module(node) - node.copy( - constant: visit(node.constant), - bodystmt: visit(node.bodystmt) - ) - end - - # Visit a MRHS node. - def visit_mrhs(node) - node.copy(parts: visit_all(node.parts)) - end - - # Visit a Next node. - def visit_next(node) - node.copy(arguments: visit(node.arguments)) - end - - # Visit a Op node. - def visit_op(node) - node.copy - end - - # Visit a OpAssign node. - def visit_opassign(node) - node.copy(target: visit(node.target), operator: visit(node.operator)) - end - - # Visit a Params node. - def visit_params(node) - node.copy( - requireds: visit_all(node.requireds), - optionals: - node.optionals.map { |ident, value| [visit(ident), visit(value)] }, - rest: visit(node.rest), - posts: visit_all(node.posts), - keywords: - node.keywords.map { |ident, value| [visit(ident), visit(value)] }, - keyword_rest: - node.keyword_rest == :nil ? :nil : visit(node.keyword_rest), - block: visit(node.block) - ) - end - - # Visit a Paren node. - def visit_paren(node) - node.copy(lparen: visit(node.lparen), contents: visit(node.contents)) - end - - # Visit a Period node. - def visit_period(node) - node.copy - end - - # Visit a Program node. - def visit_program(node) - node.copy(statements: visit(node.statements)) - end - - # Visit a QSymbols node. - def visit_qsymbols(node) - node.copy( - beginning: visit(node.beginning), - elements: visit_all(node.elements) - ) - end - - # Visit a QSymbolsBeg node. - def visit_qsymbols_beg(node) - node.copy - end - - # Visit a QWords node. - def visit_qwords(node) - node.copy( - beginning: visit(node.beginning), - elements: visit_all(node.elements) - ) - end - - # Visit a QWordsBeg node. - def visit_qwords_beg(node) - node.copy - end - - # Visit a RationalLiteral node. - def visit_rational(node) - node.copy - end - - # Visit a RBrace node. - def visit_rbrace(node) - node.copy - end - - # Visit a RBracket node. - def visit_rbracket(node) - node.copy - end - - # Visit a Redo node. - def visit_redo(node) - node.copy - end - - # Visit a RegexpContent node. - def visit_regexp_content(node) - node.copy(parts: visit_all(node.parts)) - end - - # Visit a RegexpBeg node. - def visit_regexp_beg(node) - node.copy - end - - # Visit a RegexpEnd node. - def visit_regexp_end(node) - node.copy - end - - # Visit a RegexpLiteral node. - def visit_regexp_literal(node) - node.copy(parts: visit_all(node.parts)) - end - - # Visit a RescueEx node. - def visit_rescue_ex(node) - node.copy(variable: visit(node.variable)) - end - - # Visit a Rescue node. - def visit_rescue(node) - node.copy( - keyword: visit(node.keyword), - exception: visit(node.exception), - statements: visit(node.statements), - consequent: visit(node.consequent) - ) - end - - # Visit a RescueMod node. - def visit_rescue_mod(node) - node.copy - end - - # Visit a RestParam node. - def visit_rest_param(node) - node.copy(name: visit(node.name)) - end - - # Visit a Retry node. - def visit_retry(node) - node.copy - end - - # Visit a Return node. - def visit_return(node) - node.copy(arguments: visit(node.arguments)) - end - - # Visit a RParen node. - def visit_rparen(node) - node.copy - end - - # Visit a SClass node. - def visit_sclass(node) - node.copy(bodystmt: visit(node.bodystmt)) - end - - # Visit a Statements node. - def visit_statements(node) - node.copy(body: visit_all(node.body)) - end - - # Visit a StringContent node. - def visit_string_content(node) - node.copy(parts: visit_all(node.parts)) - end - - # Visit a StringConcat node. - def visit_string_concat(node) - node.copy(left: visit(node.left), right: visit(node.right)) - end - - # Visit a StringDVar node. - def visit_string_dvar(node) - node.copy(variable: visit(node.variable)) - end - - # Visit a StringEmbExpr node. - def visit_string_embexpr(node) - node.copy(statements: visit(node.statements)) - end - - # Visit a StringLiteral node. - def visit_string_literal(node) - node.copy(parts: visit_all(node.parts)) - end - - # Visit a Super node. - def visit_super(node) - node.copy(arguments: visit(node.arguments)) - end - - # Visit a SymBeg node. - def visit_symbeg(node) - node.copy - end - - # Visit a SymbolContent node. - def visit_symbol_content(node) - node.copy(value: visit(node.value)) - end - - # Visit a SymbolLiteral node. - def visit_symbol_literal(node) - node.copy(value: visit(node.value)) - end - - # Visit a Symbols node. - def visit_symbols(node) - node.copy( - beginning: visit(node.beginning), - elements: visit_all(node.elements) - ) - end - - # Visit a SymbolsBeg node. - def visit_symbols_beg(node) - node.copy - end - - # Visit a TLambda node. - def visit_tlambda(node) - node.copy - end - - # Visit a TLamBeg node. - def visit_tlambeg(node) - node.copy - end - - # Visit a TopConstField node. - def visit_top_const_field(node) - node.copy(constant: visit(node.constant)) - end - - # Visit a TopConstRef node. - def visit_top_const_ref(node) - node.copy(constant: visit(node.constant)) - end - - # Visit a TStringBeg node. - def visit_tstring_beg(node) - node.copy - end - - # Visit a TStringContent node. - def visit_tstring_content(node) - node.copy - end - - # Visit a TStringEnd node. - def visit_tstring_end(node) - node.copy - end - - # Visit a Not node. - def visit_not(node) - node.copy(statement: visit(node.statement)) - end - - # Visit a Unary node. - def visit_unary(node) - node.copy - end - - # Visit a Undef node. - def visit_undef(node) - node.copy(symbols: visit_all(node.symbols)) - end - - # Visit a UnlessNode node. - def visit_unless(node) - node.copy( - predicate: visit(node.predicate), - statements: visit(node.statements), - consequent: visit(node.consequent) - ) - end - - # Visit a UntilNode node. - def visit_until(node) - node.copy( - predicate: visit(node.predicate), - statements: visit(node.statements) - ) - end - - # Visit a VarField node. - def visit_var_field(node) - node.copy(value: visit(node.value)) - end - - # Visit a VarRef node. - def visit_var_ref(node) - node.copy(value: visit(node.value)) - end - - # Visit a PinnedVarRef node. - def visit_pinned_var_ref(node) - node.copy(value: visit(node.value)) - end - - # Visit a VCall node. - def visit_vcall(node) - node.copy(value: visit(node.value)) - end - - # Visit a VoidStmt node. - def visit_void_stmt(node) - node.copy - end - - # Visit a When node. - def visit_when(node) - node.copy( - arguments: visit(node.arguments), - statements: visit(node.statements), - consequent: visit(node.consequent) - ) - end - - # Visit a WhileNode node. - def visit_while(node) - node.copy( - predicate: visit(node.predicate), - statements: visit(node.statements) - ) - end - - # Visit a Word node. - def visit_word(node) - node.copy(parts: visit_all(node.parts)) - end - - # Visit a Words node. - def visit_words(node) - node.copy( - beginning: visit(node.beginning), - elements: visit_all(node.elements) - ) - end - - # Visit a WordsBeg node. - def visit_words_beg(node) - node.copy - end - - # Visit a XString node. - def visit_xstring(node) - node.copy(parts: visit_all(node.parts)) - end - - # Visit a XStringLiteral node. - def visit_xstring_literal(node) - node.copy(parts: visit_all(node.parts)) - end - - # Visit a YieldNode node. - def visit_yield(node) - node.copy(arguments: visit(node.arguments)) - end - - # Visit a ZSuper node. - def visit_zsuper(node) - node.copy - end - end - end -end diff --git a/lib/syntax_tree/visitor/pretty_print_visitor.rb b/lib/syntax_tree/visitor/pretty_print_visitor.rb deleted file mode 100644 index 674e3aac..00000000 --- a/lib/syntax_tree/visitor/pretty_print_visitor.rb +++ /dev/null @@ -1,85 +0,0 @@ -# frozen_string_literal: true - -module SyntaxTree - class Visitor - # This visitor pretty-prints the AST into an equivalent s-expression. - class PrettyPrintVisitor < FieldVisitor - attr_reader :q - - def initialize(q) - @q = q - end - - # This is here because we need to make sure the operator is cast to a - # string before we print it out. - def visit_binary(node) - node(node, "binary") do - field("left", node.left) - text("operator", node.operator.to_s) - field("right", node.right) - comments(node) - end - end - - # This is here to make it a little nicer to look at labels since they - # typically have their : at the end of the value. - def visit_label(node) - node(node, "label") do - q.breakable - q.text(":") - q.text(node.value[0...-1]) - comments(node) - end - end - - private - - def comments(node) - return if node.comments.empty? - - q.breakable - q.group(2, "(", ")") do - q.seplist(node.comments) { |comment| q.pp(comment) } - end - end - - def field(_name, value) - q.breakable - q.pp(value) - end - - def list(_name, values) - q.breakable - q.group(2, "(", ")") { q.seplist(values) { |value| q.pp(value) } } - end - - def node(_node, type) - q.group(2, "(", ")") do - q.text(type) - yield - end - end - - def pairs(_name, values) - q.group(2, "(", ")") do - q.seplist(values) do |(key, value)| - q.pp(key) - - if value - q.text("=") - q.group(2) do - q.breakable("") - q.pp(value) - end - end - end - end - end - - def text(_name, value) - q.breakable - q.text(value) - end - end - end -end diff --git a/lib/syntax_tree/visitor/with_environment.rb b/lib/syntax_tree/with_environment.rb similarity index 58% rename from lib/syntax_tree/visitor/with_environment.rb rename to lib/syntax_tree/with_environment.rb index 59033d50..60301390 100644 --- a/lib/syntax_tree/visitor/with_environment.rb +++ b/lib/syntax_tree/with_environment.rb @@ -22,6 +22,87 @@ module SyntaxTree # end # end module WithEnvironment + # The environment class is used to keep track of local variables and + # arguments inside a particular scope + class Environment + # This class tracks the occurrences of a local variable or argument + class Local + # [Symbol] The type of the local (e.g. :argument, :variable) + attr_reader :type + + # [Array[Location]] The locations of all definitions and assignments of + # this local + attr_reader :definitions + + # [Array[Location]] The locations of all usages of this local + attr_reader :usages + + # initialize: (Symbol type) -> void + def initialize(type) + @type = type + @definitions = [] + @usages = [] + end + + # add_definition: (Location location) -> void + def add_definition(location) + @definitions << location + end + + # add_usage: (Location location) -> void + def add_usage(location) + @usages << location + end + end + + # [Array[Local]] The local variables and arguments defined in this + # environment + attr_reader :locals + + # [Environment | nil] The parent environment + attr_reader :parent + + # initialize: (Environment | nil parent) -> void + def initialize(parent = nil) + @locals = {} + @parent = parent + end + + # Adding a local definition will either insert a new entry in the locals + # hash or append a new definition location to an existing local. Notice that + # it's not possible to change the type of a local after it has been + # registered + # add_local_definition: (Ident | Label identifier, Symbol type) -> void + def add_local_definition(identifier, type) + name = identifier.value.delete_suffix(":") + + @locals[name] ||= Local.new(type) + @locals[name].add_definition(identifier.location) + end + + # Adding a local usage will either insert a new entry in the locals + # hash or append a new usage location to an existing local. Notice that + # it's not possible to change the type of a local after it has been + # registered + # add_local_usage: (Ident | Label identifier, Symbol type) -> void + def add_local_usage(identifier, type) + name = identifier.value.delete_suffix(":") + + @locals[name] ||= Local.new(type) + @locals[name].add_usage(identifier.location) + end + + # Try to find the local given its name in this environment or any of its + # parents + # find_local: (String name) -> Local | nil + def find_local(name) + local = @locals[name] + return local unless local.nil? + + @parent&.find_local(name) + end + end + def current_environment @current_environment ||= Environment.new end diff --git a/lib/syntax_tree/yarv.rb b/lib/syntax_tree/yarv.rb index ff8d3801..bd5c54b9 100644 --- a/lib/syntax_tree/yarv.rb +++ b/lib/syntax_tree/yarv.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "stringio" + require_relative "yarv/basic_block" require_relative "yarv/bf" require_relative "yarv/calldata" diff --git a/lib/syntax_tree/yarv/compiler.rb b/lib/syntax_tree/yarv/compiler.rb index e1a8544a..a8044faf 100644 --- a/lib/syntax_tree/yarv/compiler.rb +++ b/lib/syntax_tree/yarv/compiler.rb @@ -8,7 +8,7 @@ module YARV # # You use this as with any other visitor. First you parse code into a tree, # then you visit it with this compiler. Visiting the root node of the tree - # will return a SyntaxTree::Visitor::Compiler::InstructionSequence object. + # will return a SyntaxTree::YARV::Compiler::InstructionSequence object. # With that object you can call #to_a on it, which will return a serialized # form of the instruction sequence as an array. This array _should_ mirror # the array given by RubyVM::InstructionSequence#to_a. diff --git a/test/test_helper.rb b/test/test_helper.rb index e4452e3d..2c8f6466 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -94,7 +94,7 @@ def assert_syntax_tree(node) assert_includes(pretty, type) # Assert that we can get back a new tree by using the mutation visitor. - assert_operator node, :===, node.accept(Visitor::MutationVisitor.new) + assert_operator node, :===, node.accept(MutationVisitor.new) # Serialize the node to JSON, parse it back out, and assert that we have # found the expected type. From 0dd027671e860975d85fd8af3cf8e2e2c117a59a Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Fri, 10 Feb 2023 11:37:12 -0500 Subject: [PATCH 08/11] More utility functions --- lib/syntax_tree.rb | 35 ++++++++++++++++++- lib/syntax_tree/mermaid_visitor.rb | 6 +--- lib/syntax_tree/mutation_visitor.rb | 25 +++----------- lib/syntax_tree/with_environment.rb | 52 +++++++++++++---------------- 4 files changed, 64 insertions(+), 54 deletions(-) diff --git a/lib/syntax_tree.rb b/lib/syntax_tree.rb index 0bdc4827..70126b14 100644 --- a/lib/syntax_tree.rb +++ b/lib/syntax_tree.rb @@ -60,9 +60,36 @@ def self.format( maxwidth = DEFAULT_PRINT_WIDTH, base_indentation = DEFAULT_INDENTATION, options: Formatter::Options.new + ) + format_node( + source, + parse(source), + maxwidth, + base_indentation, + options: options + ) + end + + # Parses the given file and returns the formatted source. + def self.format_file( + filepath, + maxwidth = DEFAULT_PRINT_WIDTH, + base_indentation = DEFAULT_INDENTATION, + options: Formatter::Options.new + ) + format(read(filepath), maxwidth, base_indentation, options: options) + end + + # Accepts a node in the tree and returns the formatted source. + def self.format_node( + source, + node, + maxwidth = DEFAULT_PRINT_WIDTH, + base_indentation = DEFAULT_INDENTATION, + options: Formatter::Options.new ) formatter = Formatter.new(source, [], maxwidth, options: options) - parse(source).format(formatter) + node.format(formatter) formatter.flush(base_indentation) formatter.output.join @@ -130,4 +157,10 @@ def self.search(source, query, &block) Search.new(pattern).scan(program, &block) end + + # Searches through the given file using the given pattern and yields each + # node in the tree that matches the pattern to the given block. + def self.search_file(filepath, query, &block) + search(read(filepath), query, &block) + end end diff --git a/lib/syntax_tree/mermaid_visitor.rb b/lib/syntax_tree/mermaid_visitor.rb index 52d1b5c6..fc9f6706 100644 --- a/lib/syntax_tree/mermaid_visitor.rb +++ b/lib/syntax_tree/mermaid_visitor.rb @@ -29,11 +29,7 @@ def field(name, value) flowchart.link(target, visit(value), name) else to = - flowchart.node( - "#{target.id}_#{name}", - value.inspect, - shape: :stadium - ) + flowchart.node("#{target.id}_#{name}", value.inspect, shape: :stadium) flowchart.link(target, to, name) end end diff --git a/lib/syntax_tree/mutation_visitor.rb b/lib/syntax_tree/mutation_visitor.rb index 2d96620d..f96e442f 100644 --- a/lib/syntax_tree/mutation_visitor.rb +++ b/lib/syntax_tree/mutation_visitor.rb @@ -35,10 +35,7 @@ def visit(node) # Visit a BEGINBlock node. def visit_BEGIN(node) - node.copy( - lbrace: visit(node.lbrace), - statements: visit(node.statements) - ) + node.copy(lbrace: visit(node.lbrace), statements: visit(node.statements)) end # Visit a CHAR node. @@ -48,10 +45,7 @@ def visit_CHAR(node) # Visit a ENDBlock node. def visit_END(node) - node.copy( - lbrace: visit(node.lbrace), - statements: visit(node.statements) - ) + node.copy(lbrace: visit(node.lbrace), statements: visit(node.statements)) end # Visit a EndContent node. @@ -101,10 +95,7 @@ def visit_args_forward(node) # Visit a ArrayLiteral node. def visit_array(node) - node.copy( - lbracket: visit(node.lbracket), - contents: visit(node.contents) - ) + node.copy(lbracket: visit(node.lbracket), contents: visit(node.contents)) end # Visit a AryPtn node. @@ -493,10 +484,7 @@ def visit_label_end(node) # Visit a Lambda node. def visit_lambda(node) - node.copy( - params: visit(node.params), - statements: visit(node.statements) - ) + node.copy(params: visit(node.params), statements: visit(node.statements)) end # Visit a LambdaVar node. @@ -541,10 +529,7 @@ def visit_mlhs_paren(node) # Visit a ModuleDeclaration node. def visit_module(node) - node.copy( - constant: visit(node.constant), - bodystmt: visit(node.bodystmt) - ) + node.copy(constant: visit(node.constant), bodystmt: visit(node.bodystmt)) end # Visit a MRHS node. diff --git a/lib/syntax_tree/with_environment.rb b/lib/syntax_tree/with_environment.rb index 60301390..13f5e080 100644 --- a/lib/syntax_tree/with_environment.rb +++ b/lib/syntax_tree/with_environment.rb @@ -5,22 +5,25 @@ module SyntaxTree # from Visitor. The module overrides a few visit methods to automatically keep # track of local variables and arguments defined in the current environment. # Example usage: - # class MyVisitor < Visitor - # include WithEnvironment # - # def visit_ident(node) - # # Check if we're visiting an identifier for an argument, a local - # variable or something else - # local = current_environment.find_local(node) + # class MyVisitor < Visitor + # include WithEnvironment # - # if local.type == :argument - # # handle identifiers for arguments - # elsif local.type == :variable - # # handle identifiers for variables - # else - # # handle other identifiers, such as method names + # def visit_ident(node) + # # Check if we're visiting an identifier for an argument, a local + # # variable or something else + # local = current_environment.find_local(node) + # + # if local.type == :argument + # # handle identifiers for arguments + # elsif local.type == :variable + # # handle identifiers for variables + # else + # # handle other identifiers, such as method names + # end # end - # end + # end + # module WithEnvironment # The environment class is used to keep track of local variables and # arguments inside a particular scope @@ -37,19 +40,16 @@ class Local # [Array[Location]] The locations of all usages of this local attr_reader :usages - # initialize: (Symbol type) -> void def initialize(type) @type = type @definitions = [] @usages = [] end - # add_definition: (Location location) -> void def add_definition(location) @definitions << location end - # add_usage: (Location location) -> void def add_usage(location) @usages << location end @@ -62,17 +62,15 @@ def add_usage(location) # [Environment | nil] The parent environment attr_reader :parent - # initialize: (Environment | nil parent) -> void def initialize(parent = nil) @locals = {} @parent = parent end # Adding a local definition will either insert a new entry in the locals - # hash or append a new definition location to an existing local. Notice that - # it's not possible to change the type of a local after it has been - # registered - # add_local_definition: (Ident | Label identifier, Symbol type) -> void + # hash or append a new definition location to an existing local. Notice + # that it's not possible to change the type of a local after it has been + # registered. def add_local_definition(identifier, type) name = identifier.value.delete_suffix(":") @@ -83,8 +81,7 @@ def add_local_definition(identifier, type) # Adding a local usage will either insert a new entry in the locals # hash or append a new usage location to an existing local. Notice that # it's not possible to change the type of a local after it has been - # registered - # add_local_usage: (Ident | Label identifier, Symbol type) -> void + # registered. def add_local_usage(identifier, type) name = identifier.value.delete_suffix(":") @@ -93,8 +90,7 @@ def add_local_usage(identifier, type) end # Try to find the local given its name in this environment or any of its - # parents - # find_local: (String name) -> Local | nil + # parents. def find_local(name) local = @locals[name] return local unless local.nil? @@ -116,7 +112,7 @@ def with_new_environment end # Visits for nodes that create new environments, such as classes, modules - # and method definitions + # and method definitions. def visit_class(node) with_new_environment { super } end @@ -127,7 +123,7 @@ def visit_module(node) # When we find a method invocation with a block, only the code that happens # inside of the block needs a fresh environment. The method invocation - # itself happens in the same environment + # itself happens in the same environment. def visit_method_add_block(node) visit(node.call) with_new_environment { visit(node.block) } @@ -138,7 +134,7 @@ def visit_def(node) end # Visit for keeping track of local arguments, such as method and block - # arguments + # arguments. def visit_params(node) add_argument_definitions(node.requireds) From 1a202316e4919eef70ed6f2945d0135686982ad9 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Fri, 10 Feb 2023 11:56:41 -0500 Subject: [PATCH 09/11] Use visit_methods {} --- .rubocop.yml | 3 + lib/syntax_tree/field_visitor.rb | 1444 ++++----- lib/syntax_tree/index.rb | 110 +- lib/syntax_tree/language_server.rb | 170 +- lib/syntax_tree/mutation_visitor.rb | 1457 ++++----- lib/syntax_tree/parser.rb | 6 +- lib/syntax_tree/translation/parser.rb | 4231 +++++++++++++------------ lib/syntax_tree/with_environment.rb | 6 +- lib/syntax_tree/yarv/compiler.rb | 199 +- test/visitor_test.rb | 14 +- test/visitor_with_environment_test.rb | 50 +- 11 files changed, 3890 insertions(+), 3800 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 21beca1b..e5a3fe96 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -84,6 +84,9 @@ Security/Eval: Style/AccessorGrouping: Enabled: false +Style/Alias: + Enabled: false + Style/CaseEquality: Enabled: false diff --git a/lib/syntax_tree/field_visitor.rb b/lib/syntax_tree/field_visitor.rb index f4fc00e3..ca1df55b 100644 --- a/lib/syntax_tree/field_visitor.rb +++ b/lib/syntax_tree/field_visitor.rb @@ -48,972 +48,974 @@ module SyntaxTree # a method. # class FieldVisitor < BasicVisitor - def visit_aref(node) - node(node, "aref") do - field("collection", node.collection) - field("index", node.index) - comments(node) + visit_methods do + def visit_aref(node) + node(node, "aref") do + field("collection", node.collection) + field("index", node.index) + comments(node) + end end - end - def visit_aref_field(node) - node(node, "aref_field") do - field("collection", node.collection) - field("index", node.index) - comments(node) + def visit_aref_field(node) + node(node, "aref_field") do + field("collection", node.collection) + field("index", node.index) + comments(node) + end end - end - def visit_alias(node) - node(node, "alias") do - field("left", node.left) - field("right", node.right) - comments(node) + def visit_alias(node) + node(node, "alias") do + field("left", node.left) + field("right", node.right) + comments(node) + end end - end - def visit_arg_block(node) - node(node, "arg_block") do - field("value", node.value) if node.value - comments(node) + def visit_arg_block(node) + node(node, "arg_block") do + field("value", node.value) if node.value + comments(node) + end end - end - def visit_arg_paren(node) - node(node, "arg_paren") do - field("arguments", node.arguments) - comments(node) + def visit_arg_paren(node) + node(node, "arg_paren") do + field("arguments", node.arguments) + comments(node) + end end - end - def visit_arg_star(node) - node(node, "arg_star") do - field("value", node.value) - comments(node) + def visit_arg_star(node) + node(node, "arg_star") do + field("value", node.value) + comments(node) + end end - end - def visit_args(node) - node(node, "args") do - list("parts", node.parts) - comments(node) + def visit_args(node) + node(node, "args") do + list("parts", node.parts) + comments(node) + end end - end - def visit_args_forward(node) - node(node, "args_forward") { comments(node) } - end + def visit_args_forward(node) + node(node, "args_forward") { comments(node) } + end - def visit_array(node) - node(node, "array") do - field("contents", node.contents) - comments(node) + def visit_array(node) + node(node, "array") do + field("contents", node.contents) + comments(node) + end end - end - def visit_aryptn(node) - node(node, "aryptn") do - field("constant", node.constant) if node.constant - list("requireds", node.requireds) if node.requireds.any? - field("rest", node.rest) if node.rest - list("posts", node.posts) if node.posts.any? - comments(node) + def visit_aryptn(node) + node(node, "aryptn") do + field("constant", node.constant) if node.constant + list("requireds", node.requireds) if node.requireds.any? + field("rest", node.rest) if node.rest + list("posts", node.posts) if node.posts.any? + comments(node) + end end - end - def visit_assign(node) - node(node, "assign") do - field("target", node.target) - field("value", node.value) - comments(node) + def visit_assign(node) + node(node, "assign") do + field("target", node.target) + field("value", node.value) + comments(node) + end end - end - def visit_assoc(node) - node(node, "assoc") do - field("key", node.key) - field("value", node.value) if node.value - comments(node) + def visit_assoc(node) + node(node, "assoc") do + field("key", node.key) + field("value", node.value) if node.value + comments(node) + end end - end - def visit_assoc_splat(node) - node(node, "assoc_splat") do - field("value", node.value) - comments(node) + def visit_assoc_splat(node) + node(node, "assoc_splat") do + field("value", node.value) + comments(node) + end end - end - def visit_backref(node) - visit_token(node, "backref") - end + def visit_backref(node) + visit_token(node, "backref") + end - def visit_backtick(node) - visit_token(node, "backtick") - end + def visit_backtick(node) + visit_token(node, "backtick") + end - def visit_bare_assoc_hash(node) - node(node, "bare_assoc_hash") do - list("assocs", node.assocs) - comments(node) + def visit_bare_assoc_hash(node) + node(node, "bare_assoc_hash") do + list("assocs", node.assocs) + comments(node) + end end - end - def visit_BEGIN(node) - node(node, "BEGIN") do - field("statements", node.statements) - comments(node) + def visit_BEGIN(node) + node(node, "BEGIN") do + field("statements", node.statements) + comments(node) + end end - end - def visit_begin(node) - node(node, "begin") do - field("bodystmt", node.bodystmt) - comments(node) + def visit_begin(node) + node(node, "begin") do + field("bodystmt", node.bodystmt) + comments(node) + end end - end - def visit_binary(node) - node(node, "binary") do - field("left", node.left) - text("operator", node.operator) - field("right", node.right) - comments(node) + def visit_binary(node) + node(node, "binary") do + field("left", node.left) + text("operator", node.operator) + field("right", node.right) + comments(node) + end end - end - def visit_block(node) - node(node, "block") do - field("block_var", node.block_var) if node.block_var - field("bodystmt", node.bodystmt) - comments(node) + def visit_block(node) + node(node, "block") do + field("block_var", node.block_var) if node.block_var + field("bodystmt", node.bodystmt) + comments(node) + end end - end - def visit_blockarg(node) - node(node, "blockarg") do - field("name", node.name) if node.name - comments(node) + def visit_blockarg(node) + node(node, "blockarg") do + field("name", node.name) if node.name + comments(node) + end end - end - def visit_block_var(node) - node(node, "block_var") do - field("params", node.params) - list("locals", node.locals) if node.locals.any? - comments(node) + def visit_block_var(node) + node(node, "block_var") do + field("params", node.params) + list("locals", node.locals) if node.locals.any? + comments(node) + end end - end - def visit_bodystmt(node) - node(node, "bodystmt") do - field("statements", node.statements) - field("rescue_clause", node.rescue_clause) if node.rescue_clause - field("else_clause", node.else_clause) if node.else_clause - field("ensure_clause", node.ensure_clause) if node.ensure_clause - comments(node) + def visit_bodystmt(node) + node(node, "bodystmt") do + field("statements", node.statements) + field("rescue_clause", node.rescue_clause) if node.rescue_clause + field("else_clause", node.else_clause) if node.else_clause + field("ensure_clause", node.ensure_clause) if node.ensure_clause + comments(node) + end end - end - def visit_break(node) - node(node, "break") do - field("arguments", node.arguments) - comments(node) + def visit_break(node) + node(node, "break") do + field("arguments", node.arguments) + comments(node) + end end - end - def visit_call(node) - node(node, "call") do - field("receiver", node.receiver) - field("operator", node.operator) - field("message", node.message) - field("arguments", node.arguments) if node.arguments - comments(node) + def visit_call(node) + node(node, "call") do + field("receiver", node.receiver) + field("operator", node.operator) + field("message", node.message) + field("arguments", node.arguments) if node.arguments + comments(node) + end end - end - def visit_case(node) - node(node, "case") do - field("keyword", node.keyword) - field("value", node.value) if node.value - field("consequent", node.consequent) - comments(node) + def visit_case(node) + node(node, "case") do + field("keyword", node.keyword) + field("value", node.value) if node.value + field("consequent", node.consequent) + comments(node) + end end - end - def visit_CHAR(node) - visit_token(node, "CHAR") - end + def visit_CHAR(node) + visit_token(node, "CHAR") + end - def visit_class(node) - node(node, "class") do - field("constant", node.constant) - field("superclass", node.superclass) if node.superclass - field("bodystmt", node.bodystmt) - comments(node) + def visit_class(node) + node(node, "class") do + field("constant", node.constant) + field("superclass", node.superclass) if node.superclass + field("bodystmt", node.bodystmt) + comments(node) + end end - end - def visit_comma(node) - node(node, "comma") { field("value", node.value) } - end + def visit_comma(node) + node(node, "comma") { field("value", node.value) } + end - def visit_command(node) - node(node, "command") do - field("message", node.message) - field("arguments", node.arguments) - comments(node) + def visit_command(node) + node(node, "command") do + field("message", node.message) + field("arguments", node.arguments) + comments(node) + end end - end - def visit_command_call(node) - node(node, "command_call") do - field("receiver", node.receiver) - field("operator", node.operator) - field("message", node.message) - field("arguments", node.arguments) if node.arguments - comments(node) + def visit_command_call(node) + node(node, "command_call") do + field("receiver", node.receiver) + field("operator", node.operator) + field("message", node.message) + field("arguments", node.arguments) if node.arguments + comments(node) + end end - end - def visit_comment(node) - node(node, "comment") { field("value", node.value) } - end + def visit_comment(node) + node(node, "comment") { field("value", node.value) } + end - def visit_const(node) - visit_token(node, "const") - end + def visit_const(node) + visit_token(node, "const") + end - def visit_const_path_field(node) - node(node, "const_path_field") do - field("parent", node.parent) - field("constant", node.constant) - comments(node) + def visit_const_path_field(node) + node(node, "const_path_field") do + field("parent", node.parent) + field("constant", node.constant) + comments(node) + end end - end - def visit_const_path_ref(node) - node(node, "const_path_ref") do - field("parent", node.parent) - field("constant", node.constant) - comments(node) + def visit_const_path_ref(node) + node(node, "const_path_ref") do + field("parent", node.parent) + field("constant", node.constant) + comments(node) + end end - end - def visit_const_ref(node) - node(node, "const_ref") do - field("constant", node.constant) - comments(node) + def visit_const_ref(node) + node(node, "const_ref") do + field("constant", node.constant) + comments(node) + end end - end - def visit_cvar(node) - visit_token(node, "cvar") - end + def visit_cvar(node) + visit_token(node, "cvar") + end - def visit_def(node) - node(node, "def") do - field("target", node.target) - field("operator", node.operator) - field("name", node.name) - field("params", node.params) - field("bodystmt", node.bodystmt) - comments(node) + def visit_def(node) + node(node, "def") do + field("target", node.target) + field("operator", node.operator) + field("name", node.name) + field("params", node.params) + field("bodystmt", node.bodystmt) + comments(node) + end end - end - def visit_defined(node) - node(node, "defined") do - field("value", node.value) - comments(node) + def visit_defined(node) + node(node, "defined") do + field("value", node.value) + comments(node) + end end - end - def visit_dyna_symbol(node) - node(node, "dyna_symbol") do - list("parts", node.parts) - comments(node) + def visit_dyna_symbol(node) + node(node, "dyna_symbol") do + list("parts", node.parts) + comments(node) + end end - end - def visit_END(node) - node(node, "END") do - field("statements", node.statements) - comments(node) + def visit_END(node) + node(node, "END") do + field("statements", node.statements) + comments(node) + end end - end - def visit_else(node) - node(node, "else") do - field("statements", node.statements) - comments(node) + def visit_else(node) + node(node, "else") do + field("statements", node.statements) + comments(node) + end end - end - def visit_elsif(node) - node(node, "elsif") do - field("predicate", node.predicate) - field("statements", node.statements) - field("consequent", node.consequent) if node.consequent - comments(node) + def visit_elsif(node) + node(node, "elsif") do + field("predicate", node.predicate) + field("statements", node.statements) + field("consequent", node.consequent) if node.consequent + comments(node) + end end - end - def visit_embdoc(node) - node(node, "embdoc") { field("value", node.value) } - end + def visit_embdoc(node) + node(node, "embdoc") { field("value", node.value) } + end - def visit_embexpr_beg(node) - node(node, "embexpr_beg") { field("value", node.value) } - end + def visit_embexpr_beg(node) + node(node, "embexpr_beg") { field("value", node.value) } + end - def visit_embexpr_end(node) - node(node, "embexpr_end") { field("value", node.value) } - end + def visit_embexpr_end(node) + node(node, "embexpr_end") { field("value", node.value) } + end - def visit_embvar(node) - node(node, "embvar") { field("value", node.value) } - end + def visit_embvar(node) + node(node, "embvar") { field("value", node.value) } + end - def visit_ensure(node) - node(node, "ensure") do - field("statements", node.statements) - comments(node) + def visit_ensure(node) + node(node, "ensure") do + field("statements", node.statements) + comments(node) + end end - end - def visit_excessed_comma(node) - visit_token(node, "excessed_comma") - end + def visit_excessed_comma(node) + visit_token(node, "excessed_comma") + end - def visit_field(node) - node(node, "field") do - field("parent", node.parent) - field("operator", node.operator) - field("name", node.name) - comments(node) + def visit_field(node) + node(node, "field") do + field("parent", node.parent) + field("operator", node.operator) + field("name", node.name) + comments(node) + end end - end - def visit_float(node) - visit_token(node, "float") - end + def visit_float(node) + visit_token(node, "float") + end - def visit_fndptn(node) - node(node, "fndptn") do - field("constant", node.constant) if node.constant - field("left", node.left) - list("values", node.values) - field("right", node.right) - comments(node) + def visit_fndptn(node) + node(node, "fndptn") do + field("constant", node.constant) if node.constant + field("left", node.left) + list("values", node.values) + field("right", node.right) + comments(node) + end end - end - def visit_for(node) - node(node, "for") do - field("index", node.index) - field("collection", node.collection) - field("statements", node.statements) - comments(node) + def visit_for(node) + node(node, "for") do + field("index", node.index) + field("collection", node.collection) + field("statements", node.statements) + comments(node) + end end - end - def visit_gvar(node) - visit_token(node, "gvar") - end + def visit_gvar(node) + visit_token(node, "gvar") + end - def visit_hash(node) - node(node, "hash") do - list("assocs", node.assocs) if node.assocs.any? - comments(node) + def visit_hash(node) + node(node, "hash") do + list("assocs", node.assocs) if node.assocs.any? + comments(node) + end end - end - def visit_heredoc(node) - node(node, "heredoc") do - list("parts", node.parts) - comments(node) + def visit_heredoc(node) + node(node, "heredoc") do + list("parts", node.parts) + comments(node) + end end - end - def visit_heredoc_beg(node) - visit_token(node, "heredoc_beg") - end + def visit_heredoc_beg(node) + visit_token(node, "heredoc_beg") + end - def visit_heredoc_end(node) - visit_token(node, "heredoc_end") - end + def visit_heredoc_end(node) + visit_token(node, "heredoc_end") + end - def visit_hshptn(node) - node(node, "hshptn") do - field("constant", node.constant) if node.constant - pairs("keywords", node.keywords) if node.keywords.any? - field("keyword_rest", node.keyword_rest) if node.keyword_rest - comments(node) + def visit_hshptn(node) + node(node, "hshptn") do + field("constant", node.constant) if node.constant + pairs("keywords", node.keywords) if node.keywords.any? + field("keyword_rest", node.keyword_rest) if node.keyword_rest + comments(node) + end end - end - def visit_ident(node) - visit_token(node, "ident") - end + def visit_ident(node) + visit_token(node, "ident") + end - def visit_if(node) - node(node, "if") do - field("predicate", node.predicate) - field("statements", node.statements) - field("consequent", node.consequent) if node.consequent - comments(node) + def visit_if(node) + node(node, "if") do + field("predicate", node.predicate) + field("statements", node.statements) + field("consequent", node.consequent) if node.consequent + comments(node) + end end - end - def visit_if_op(node) - node(node, "if_op") do - field("predicate", node.predicate) - field("truthy", node.truthy) - field("falsy", node.falsy) - comments(node) + def visit_if_op(node) + node(node, "if_op") do + field("predicate", node.predicate) + field("truthy", node.truthy) + field("falsy", node.falsy) + comments(node) + end end - end - def visit_imaginary(node) - visit_token(node, "imaginary") - end + def visit_imaginary(node) + visit_token(node, "imaginary") + end - def visit_in(node) - node(node, "in") do - field("pattern", node.pattern) - field("statements", node.statements) - field("consequent", node.consequent) if node.consequent - comments(node) + def visit_in(node) + node(node, "in") do + field("pattern", node.pattern) + field("statements", node.statements) + field("consequent", node.consequent) if node.consequent + comments(node) + end end - end - def visit_int(node) - visit_token(node, "int") - end + def visit_int(node) + visit_token(node, "int") + end - def visit_ivar(node) - visit_token(node, "ivar") - end + def visit_ivar(node) + visit_token(node, "ivar") + end - def visit_kw(node) - visit_token(node, "kw") - end + def visit_kw(node) + visit_token(node, "kw") + end - def visit_kwrest_param(node) - node(node, "kwrest_param") do - field("name", node.name) - comments(node) + def visit_kwrest_param(node) + node(node, "kwrest_param") do + field("name", node.name) + comments(node) + end end - end - def visit_label(node) - visit_token(node, "label") - end + def visit_label(node) + visit_token(node, "label") + end - def visit_label_end(node) - node(node, "label_end") { field("value", node.value) } - end + def visit_label_end(node) + node(node, "label_end") { field("value", node.value) } + end - def visit_lambda(node) - node(node, "lambda") do - field("params", node.params) - field("statements", node.statements) - comments(node) + def visit_lambda(node) + node(node, "lambda") do + field("params", node.params) + field("statements", node.statements) + comments(node) + end end - end - def visit_lambda_var(node) - node(node, "lambda_var") do - field("params", node.params) - list("locals", node.locals) if node.locals.any? - comments(node) + def visit_lambda_var(node) + node(node, "lambda_var") do + field("params", node.params) + list("locals", node.locals) if node.locals.any? + comments(node) + end end - end - def visit_lbrace(node) - visit_token(node, "lbrace") - end + def visit_lbrace(node) + visit_token(node, "lbrace") + end - def visit_lbracket(node) - visit_token(node, "lbracket") - end + def visit_lbracket(node) + visit_token(node, "lbracket") + end - def visit_lparen(node) - visit_token(node, "lparen") - end + def visit_lparen(node) + visit_token(node, "lparen") + end - def visit_massign(node) - node(node, "massign") do - field("target", node.target) - field("value", node.value) - comments(node) + def visit_massign(node) + node(node, "massign") do + field("target", node.target) + field("value", node.value) + comments(node) + end end - end - def visit_method_add_block(node) - node(node, "method_add_block") do - field("call", node.call) - field("block", node.block) - comments(node) + def visit_method_add_block(node) + node(node, "method_add_block") do + field("call", node.call) + field("block", node.block) + comments(node) + end end - end - def visit_mlhs(node) - node(node, "mlhs") do - list("parts", node.parts) - comments(node) + def visit_mlhs(node) + node(node, "mlhs") do + list("parts", node.parts) + comments(node) + end end - end - def visit_mlhs_paren(node) - node(node, "mlhs_paren") do - field("contents", node.contents) - comments(node) + def visit_mlhs_paren(node) + node(node, "mlhs_paren") do + field("contents", node.contents) + comments(node) + end end - end - def visit_module(node) - node(node, "module") do - field("constant", node.constant) - field("bodystmt", node.bodystmt) - comments(node) + def visit_module(node) + node(node, "module") do + field("constant", node.constant) + field("bodystmt", node.bodystmt) + comments(node) + end end - end - def visit_mrhs(node) - node(node, "mrhs") do - list("parts", node.parts) - comments(node) + def visit_mrhs(node) + node(node, "mrhs") do + list("parts", node.parts) + comments(node) + end end - end - def visit_next(node) - node(node, "next") do - field("arguments", node.arguments) - comments(node) + def visit_next(node) + node(node, "next") do + field("arguments", node.arguments) + comments(node) + end end - end - def visit_not(node) - node(node, "not") do - field("statement", node.statement) - comments(node) + def visit_not(node) + node(node, "not") do + field("statement", node.statement) + comments(node) + end end - end - def visit_op(node) - visit_token(node, "op") - end + def visit_op(node) + visit_token(node, "op") + end - def visit_opassign(node) - node(node, "opassign") do - field("target", node.target) - field("operator", node.operator) - field("value", node.value) - comments(node) + def visit_opassign(node) + node(node, "opassign") do + field("target", node.target) + field("operator", node.operator) + field("value", node.value) + comments(node) + end end - end - def visit_params(node) - node(node, "params") do - list("requireds", node.requireds) if node.requireds.any? - pairs("optionals", node.optionals) if node.optionals.any? - field("rest", node.rest) if node.rest - list("posts", node.posts) if node.posts.any? - pairs("keywords", node.keywords) if node.keywords.any? - field("keyword_rest", node.keyword_rest) if node.keyword_rest - field("block", node.block) if node.block - comments(node) + def visit_params(node) + node(node, "params") do + list("requireds", node.requireds) if node.requireds.any? + pairs("optionals", node.optionals) if node.optionals.any? + field("rest", node.rest) if node.rest + list("posts", node.posts) if node.posts.any? + pairs("keywords", node.keywords) if node.keywords.any? + field("keyword_rest", node.keyword_rest) if node.keyword_rest + field("block", node.block) if node.block + comments(node) + end end - end - def visit_paren(node) - node(node, "paren") do - field("contents", node.contents) - comments(node) + def visit_paren(node) + node(node, "paren") do + field("contents", node.contents) + comments(node) + end end - end - def visit_period(node) - visit_token(node, "period") - end + def visit_period(node) + visit_token(node, "period") + end - def visit_pinned_begin(node) - node(node, "pinned_begin") do - field("statement", node.statement) - comments(node) + def visit_pinned_begin(node) + node(node, "pinned_begin") do + field("statement", node.statement) + comments(node) + end end - end - def visit_pinned_var_ref(node) - node(node, "pinned_var_ref") do - field("value", node.value) - comments(node) + def visit_pinned_var_ref(node) + node(node, "pinned_var_ref") do + field("value", node.value) + comments(node) + end end - end - def visit_program(node) - node(node, "program") do - field("statements", node.statements) - comments(node) + def visit_program(node) + node(node, "program") do + field("statements", node.statements) + comments(node) + end end - end - def visit_qsymbols(node) - node(node, "qsymbols") do - list("elements", node.elements) - comments(node) + def visit_qsymbols(node) + node(node, "qsymbols") do + list("elements", node.elements) + comments(node) + end end - end - def visit_qsymbols_beg(node) - node(node, "qsymbols_beg") { field("value", node.value) } - end + def visit_qsymbols_beg(node) + node(node, "qsymbols_beg") { field("value", node.value) } + end - def visit_qwords(node) - node(node, "qwords") do - list("elements", node.elements) - comments(node) + def visit_qwords(node) + node(node, "qwords") do + list("elements", node.elements) + comments(node) + end end - end - def visit_qwords_beg(node) - node(node, "qwords_beg") { field("value", node.value) } - end + def visit_qwords_beg(node) + node(node, "qwords_beg") { field("value", node.value) } + end - def visit_range(node) - node(node, "range") do - field("left", node.left) if node.left - field("operator", node.operator) - field("right", node.right) if node.right - comments(node) + def visit_range(node) + node(node, "range") do + field("left", node.left) if node.left + field("operator", node.operator) + field("right", node.right) if node.right + comments(node) + end end - end - def visit_rassign(node) - node(node, "rassign") do - field("value", node.value) - field("operator", node.operator) - field("pattern", node.pattern) - comments(node) + def visit_rassign(node) + node(node, "rassign") do + field("value", node.value) + field("operator", node.operator) + field("pattern", node.pattern) + comments(node) + end end - end - def visit_rational(node) - visit_token(node, "rational") - end + def visit_rational(node) + visit_token(node, "rational") + end - def visit_rbrace(node) - node(node, "rbrace") { field("value", node.value) } - end + def visit_rbrace(node) + node(node, "rbrace") { field("value", node.value) } + end - def visit_rbracket(node) - node(node, "rbracket") { field("value", node.value) } - end + def visit_rbracket(node) + node(node, "rbracket") { field("value", node.value) } + end - def visit_redo(node) - node(node, "redo") { comments(node) } - end + def visit_redo(node) + node(node, "redo") { comments(node) } + end - def visit_regexp_beg(node) - node(node, "regexp_beg") { field("value", node.value) } - end + def visit_regexp_beg(node) + node(node, "regexp_beg") { field("value", node.value) } + end - def visit_regexp_content(node) - node(node, "regexp_content") { list("parts", node.parts) } - end + def visit_regexp_content(node) + node(node, "regexp_content") { list("parts", node.parts) } + end - def visit_regexp_end(node) - node(node, "regexp_end") { field("value", node.value) } - end + def visit_regexp_end(node) + node(node, "regexp_end") { field("value", node.value) } + end - def visit_regexp_literal(node) - node(node, "regexp_literal") do - list("parts", node.parts) - field("options", node.options) - comments(node) + def visit_regexp_literal(node) + node(node, "regexp_literal") do + list("parts", node.parts) + field("options", node.options) + comments(node) + end end - end - def visit_rescue(node) - node(node, "rescue") do - field("exception", node.exception) if node.exception - field("statements", node.statements) - field("consequent", node.consequent) if node.consequent - comments(node) + def visit_rescue(node) + node(node, "rescue") do + field("exception", node.exception) if node.exception + field("statements", node.statements) + field("consequent", node.consequent) if node.consequent + comments(node) + end end - end - def visit_rescue_ex(node) - node(node, "rescue_ex") do - field("exceptions", node.exceptions) - field("variable", node.variable) - comments(node) + def visit_rescue_ex(node) + node(node, "rescue_ex") do + field("exceptions", node.exceptions) + field("variable", node.variable) + comments(node) + end end - end - def visit_rescue_mod(node) - node(node, "rescue_mod") do - field("statement", node.statement) - field("value", node.value) - comments(node) + def visit_rescue_mod(node) + node(node, "rescue_mod") do + field("statement", node.statement) + field("value", node.value) + comments(node) + end end - end - def visit_rest_param(node) - node(node, "rest_param") do - field("name", node.name) - comments(node) + def visit_rest_param(node) + node(node, "rest_param") do + field("name", node.name) + comments(node) + end end - end - def visit_retry(node) - node(node, "retry") { comments(node) } - end + def visit_retry(node) + node(node, "retry") { comments(node) } + end - def visit_return(node) - node(node, "return") do - field("arguments", node.arguments) - comments(node) + def visit_return(node) + node(node, "return") do + field("arguments", node.arguments) + comments(node) + end end - end - def visit_rparen(node) - node(node, "rparen") { field("value", node.value) } - end + def visit_rparen(node) + node(node, "rparen") { field("value", node.value) } + end - def visit_sclass(node) - node(node, "sclass") do - field("target", node.target) - field("bodystmt", node.bodystmt) - comments(node) + def visit_sclass(node) + node(node, "sclass") do + field("target", node.target) + field("bodystmt", node.bodystmt) + comments(node) + end end - end - def visit_statements(node) - node(node, "statements") do - list("body", node.body) - comments(node) + def visit_statements(node) + node(node, "statements") do + list("body", node.body) + comments(node) + end end - end - def visit_string_concat(node) - node(node, "string_concat") do - field("left", node.left) - field("right", node.right) - comments(node) + def visit_string_concat(node) + node(node, "string_concat") do + field("left", node.left) + field("right", node.right) + comments(node) + end end - end - def visit_string_content(node) - node(node, "string_content") { list("parts", node.parts) } - end + def visit_string_content(node) + node(node, "string_content") { list("parts", node.parts) } + end - def visit_string_dvar(node) - node(node, "string_dvar") do - field("variable", node.variable) - comments(node) + def visit_string_dvar(node) + node(node, "string_dvar") do + field("variable", node.variable) + comments(node) + end end - end - def visit_string_embexpr(node) - node(node, "string_embexpr") do - field("statements", node.statements) - comments(node) + def visit_string_embexpr(node) + node(node, "string_embexpr") do + field("statements", node.statements) + comments(node) + end end - end - def visit_string_literal(node) - node(node, "string_literal") do - list("parts", node.parts) - comments(node) + def visit_string_literal(node) + node(node, "string_literal") do + list("parts", node.parts) + comments(node) + end end - end - def visit_super(node) - node(node, "super") do - field("arguments", node.arguments) - comments(node) + def visit_super(node) + node(node, "super") do + field("arguments", node.arguments) + comments(node) + end end - end - def visit_symbeg(node) - node(node, "symbeg") { field("value", node.value) } - end + def visit_symbeg(node) + node(node, "symbeg") { field("value", node.value) } + end - def visit_symbol_content(node) - node(node, "symbol_content") { field("value", node.value) } - end + def visit_symbol_content(node) + node(node, "symbol_content") { field("value", node.value) } + end - def visit_symbol_literal(node) - node(node, "symbol_literal") do - field("value", node.value) - comments(node) + def visit_symbol_literal(node) + node(node, "symbol_literal") do + field("value", node.value) + comments(node) + end end - end - def visit_symbols(node) - node(node, "symbols") do - list("elements", node.elements) - comments(node) + def visit_symbols(node) + node(node, "symbols") do + list("elements", node.elements) + comments(node) + end end - end - def visit_symbols_beg(node) - node(node, "symbols_beg") { field("value", node.value) } - end + def visit_symbols_beg(node) + node(node, "symbols_beg") { field("value", node.value) } + end - def visit_tlambda(node) - node(node, "tlambda") { field("value", node.value) } - end + def visit_tlambda(node) + node(node, "tlambda") { field("value", node.value) } + end - def visit_tlambeg(node) - node(node, "tlambeg") { field("value", node.value) } - end + def visit_tlambeg(node) + node(node, "tlambeg") { field("value", node.value) } + end - def visit_top_const_field(node) - node(node, "top_const_field") do - field("constant", node.constant) - comments(node) + def visit_top_const_field(node) + node(node, "top_const_field") do + field("constant", node.constant) + comments(node) + end end - end - def visit_top_const_ref(node) - node(node, "top_const_ref") do - field("constant", node.constant) - comments(node) + def visit_top_const_ref(node) + node(node, "top_const_ref") do + field("constant", node.constant) + comments(node) + end end - end - def visit_tstring_beg(node) - node(node, "tstring_beg") { field("value", node.value) } - end + def visit_tstring_beg(node) + node(node, "tstring_beg") { field("value", node.value) } + end - def visit_tstring_content(node) - visit_token(node, "tstring_content") - end + def visit_tstring_content(node) + visit_token(node, "tstring_content") + end - def visit_tstring_end(node) - node(node, "tstring_end") { field("value", node.value) } - end + def visit_tstring_end(node) + node(node, "tstring_end") { field("value", node.value) } + end - def visit_unary(node) - node(node, "unary") do - field("operator", node.operator) - field("statement", node.statement) - comments(node) + def visit_unary(node) + node(node, "unary") do + field("operator", node.operator) + field("statement", node.statement) + comments(node) + end end - end - def visit_undef(node) - node(node, "undef") do - list("symbols", node.symbols) - comments(node) + def visit_undef(node) + node(node, "undef") do + list("symbols", node.symbols) + comments(node) + end end - end - def visit_unless(node) - node(node, "unless") do - field("predicate", node.predicate) - field("statements", node.statements) - field("consequent", node.consequent) if node.consequent - comments(node) + def visit_unless(node) + node(node, "unless") do + field("predicate", node.predicate) + field("statements", node.statements) + field("consequent", node.consequent) if node.consequent + comments(node) + end end - end - def visit_until(node) - node(node, "until") do - field("predicate", node.predicate) - field("statements", node.statements) - comments(node) + def visit_until(node) + node(node, "until") do + field("predicate", node.predicate) + field("statements", node.statements) + comments(node) + end end - end - def visit_var_field(node) - node(node, "var_field") do - field("value", node.value) - comments(node) + def visit_var_field(node) + node(node, "var_field") do + field("value", node.value) + comments(node) + end end - end - def visit_var_ref(node) - node(node, "var_ref") do - field("value", node.value) - comments(node) + def visit_var_ref(node) + node(node, "var_ref") do + field("value", node.value) + comments(node) + end end - end - def visit_vcall(node) - node(node, "vcall") do - field("value", node.value) - comments(node) + def visit_vcall(node) + node(node, "vcall") do + field("value", node.value) + comments(node) + end end - end - def visit_void_stmt(node) - node(node, "void_stmt") { comments(node) } - end + def visit_void_stmt(node) + node(node, "void_stmt") { comments(node) } + end - def visit_when(node) - node(node, "when") do - field("arguments", node.arguments) - field("statements", node.statements) - field("consequent", node.consequent) if node.consequent - comments(node) + def visit_when(node) + node(node, "when") do + field("arguments", node.arguments) + field("statements", node.statements) + field("consequent", node.consequent) if node.consequent + comments(node) + end end - end - def visit_while(node) - node(node, "while") do - field("predicate", node.predicate) - field("statements", node.statements) - comments(node) + def visit_while(node) + node(node, "while") do + field("predicate", node.predicate) + field("statements", node.statements) + comments(node) + end end - end - def visit_word(node) - node(node, "word") do - list("parts", node.parts) - comments(node) + def visit_word(node) + node(node, "word") do + list("parts", node.parts) + comments(node) + end end - end - def visit_words(node) - node(node, "words") do - list("elements", node.elements) - comments(node) + def visit_words(node) + node(node, "words") do + list("elements", node.elements) + comments(node) + end end - end - def visit_words_beg(node) - node(node, "words_beg") { field("value", node.value) } - end + def visit_words_beg(node) + node(node, "words_beg") { field("value", node.value) } + end - def visit_xstring(node) - node(node, "xstring") { list("parts", node.parts) } - end + def visit_xstring(node) + node(node, "xstring") { list("parts", node.parts) } + end - def visit_xstring_literal(node) - node(node, "xstring_literal") do - list("parts", node.parts) - comments(node) + def visit_xstring_literal(node) + node(node, "xstring_literal") do + list("parts", node.parts) + comments(node) + end end - end - def visit_yield(node) - node(node, "yield") do - field("arguments", node.arguments) - comments(node) + def visit_yield(node) + node(node, "yield") do + field("arguments", node.arguments) + comments(node) + end end - end - def visit_zsuper(node) - node(node, "zsuper") { comments(node) } - end + def visit_zsuper(node) + node(node, "zsuper") { comments(node) } + end - def visit___end__(node) - visit_token(node, "__end__") + def visit___end__(node) + visit_token(node, "__end__") + end end private diff --git a/lib/syntax_tree/index.rb b/lib/syntax_tree/index.rb index 8b33f785..ab2460dd 100644 --- a/lib/syntax_tree/index.rb +++ b/lib/syntax_tree/index.rb @@ -257,74 +257,76 @@ def initialize @statements = nil end - def visit_class(node) - name = visit(node.constant).to_sym - location = - Location.new(node.location.start_line, node.location.start_column) - - results << ClassDefinition.new( - nesting.dup, - name, - location, - comments_for(node) - ) - - nesting << name - super - nesting.pop - end - - def visit_const_ref(node) - node.constant.value - end + visit_methods do + def visit_class(node) + name = visit(node.constant).to_sym + location = + Location.new(node.location.start_line, node.location.start_column) - def visit_def(node) - name = node.name.value.to_sym - location = - Location.new(node.location.start_line, node.location.start_column) - - results << if node.target.nil? - MethodDefinition.new( + results << ClassDefinition.new( nesting.dup, name, location, comments_for(node) ) - else - SingletonMethodDefinition.new( + + nesting << name + super + nesting.pop + end + + def visit_const_ref(node) + node.constant.value + end + + def visit_def(node) + name = node.name.value.to_sym + location = + Location.new(node.location.start_line, node.location.start_column) + + results << if node.target.nil? + MethodDefinition.new( + nesting.dup, + name, + location, + comments_for(node) + ) + else + SingletonMethodDefinition.new( + nesting.dup, + name, + location, + comments_for(node) + ) + end + end + + def visit_module(node) + name = visit(node.constant).to_sym + location = + Location.new(node.location.start_line, node.location.start_column) + + results << ModuleDefinition.new( nesting.dup, name, location, comments_for(node) ) - end - end - - def visit_module(node) - name = visit(node.constant).to_sym - location = - Location.new(node.location.start_line, node.location.start_column) - results << ModuleDefinition.new( - nesting.dup, - name, - location, - comments_for(node) - ) - - nesting << name - super - nesting.pop - end + nesting << name + super + nesting.pop + end - def visit_program(node) - super - results - end + def visit_program(node) + super + results + end - def visit_statements(node) - @statements = node - super + def visit_statements(node) + @statements = node + super + end end private diff --git a/lib/syntax_tree/language_server.rb b/lib/syntax_tree/language_server.rb index afb1540e..6ec81030 100644 --- a/lib/syntax_tree/language_server.rb +++ b/lib/syntax_tree/language_server.rb @@ -52,101 +52,103 @@ def visit(node) result end - # Adds parentheses around assignments contained within the default values - # of parameters. For example, - # - # def foo(a = b = c) - # end - # - # becomes - # - # def foo(a = ₍b = c₎) - # end - # - def visit_assign(node) - parentheses(node.location) if stack[-2].is_a?(Params) - super - end - - # Adds parentheses around binary expressions to make it clear which - # subexpression will be evaluated first. For example, - # - # a + b * c - # - # becomes - # - # a + ₍b * c₎ - # - def visit_binary(node) - case stack[-2] - when Assign, OpAssign - parentheses(node.location) - when Binary - parentheses(node.location) if stack[-2].operator != node.operator + visit_methods do + # Adds parentheses around assignments contained within the default + # values of parameters. For example, + # + # def foo(a = b = c) + # end + # + # becomes + # + # def foo(a = ₍b = c₎) + # end + # + def visit_assign(node) + parentheses(node.location) if stack[-2].is_a?(Params) + super end - super - end + # Adds parentheses around binary expressions to make it clear which + # subexpression will be evaluated first. For example, + # + # a + b * c + # + # becomes + # + # a + ₍b * c₎ + # + def visit_binary(node) + case stack[-2] + when Assign, OpAssign + parentheses(node.location) + when Binary + parentheses(node.location) if stack[-2].operator != node.operator + end - # Adds parentheses around ternary operators contained within certain - # expressions where it could be confusing which subexpression will get - # evaluated first. For example, - # - # a ? b : c ? d : e - # - # becomes - # - # a ? b : ₍c ? d : e₎ - # - def visit_if_op(node) - case stack[-2] - when Assign, Binary, IfOp, OpAssign - parentheses(node.location) + super end - super - end + # Adds parentheses around ternary operators contained within certain + # expressions where it could be confusing which subexpression will get + # evaluated first. For example, + # + # a ? b : c ? d : e + # + # becomes + # + # a ? b : ₍c ? d : e₎ + # + def visit_if_op(node) + case stack[-2] + when Assign, Binary, IfOp, OpAssign + parentheses(node.location) + end - # Adds the implicitly rescued StandardError into a bare rescue clause. For - # example, - # - # begin - # rescue - # end - # - # becomes - # - # begin - # rescue StandardError - # end - # - def visit_rescue(node) - if node.exception.nil? - hints << Hint.new( - line: node.location.start_line - 1, - character: node.location.start_column + "rescue".length, - label: " StandardError" - ) + super end - super - end + # Adds the implicitly rescued StandardError into a bare rescue clause. + # For example, + # + # begin + # rescue + # end + # + # becomes + # + # begin + # rescue StandardError + # end + # + def visit_rescue(node) + if node.exception.nil? + hints << Hint.new( + line: node.location.start_line - 1, + character: node.location.start_column + "rescue".length, + label: " StandardError" + ) + end - # Adds parentheses around unary statements using the - operator that are - # contained within Binary nodes. For example, - # - # -a + b - # - # becomes - # - # ₍-a₎ + b - # - def visit_unary(node) - if stack[-2].is_a?(Binary) && (node.operator == "-") - parentheses(node.location) + super end - super + # Adds parentheses around unary statements using the - operator that are + # contained within Binary nodes. For example, + # + # -a + b + # + # becomes + # + # ₍-a₎ + b + # + def visit_unary(node) + if stack[-2].is_a?(Binary) && (node.operator == "-") + parentheses(node.location) + end + + super + end end private diff --git a/lib/syntax_tree/mutation_visitor.rb b/lib/syntax_tree/mutation_visitor.rb index f96e442f..0b4b9357 100644 --- a/lib/syntax_tree/mutation_visitor.rb +++ b/lib/syntax_tree/mutation_visitor.rb @@ -33,875 +33,892 @@ def visit(node) result end - # Visit a BEGINBlock node. - def visit_BEGIN(node) - node.copy(lbrace: visit(node.lbrace), statements: visit(node.statements)) - end + visit_methods do + # Visit a BEGINBlock node. + def visit_BEGIN(node) + node.copy( + lbrace: visit(node.lbrace), + statements: visit(node.statements) + ) + end - # Visit a CHAR node. - def visit_CHAR(node) - node.copy - end + # Visit a CHAR node. + def visit_CHAR(node) + node.copy + end - # Visit a ENDBlock node. - def visit_END(node) - node.copy(lbrace: visit(node.lbrace), statements: visit(node.statements)) - end + # Visit a ENDBlock node. + def visit_END(node) + node.copy( + lbrace: visit(node.lbrace), + statements: visit(node.statements) + ) + end - # Visit a EndContent node. - def visit___end__(node) - node.copy - end + # Visit a EndContent node. + def visit___end__(node) + node.copy + end - # Visit a AliasNode node. - def visit_alias(node) - node.copy(left: visit(node.left), right: visit(node.right)) - end + # Visit a AliasNode node. + def visit_alias(node) + node.copy(left: visit(node.left), right: visit(node.right)) + end - # Visit a ARef node. - def visit_aref(node) - node.copy(index: visit(node.index)) - end + # Visit a ARef node. + def visit_aref(node) + node.copy(index: visit(node.index)) + end - # Visit a ARefField node. - def visit_aref_field(node) - node.copy(index: visit(node.index)) - end + # Visit a ARefField node. + def visit_aref_field(node) + node.copy(index: visit(node.index)) + end - # Visit a ArgParen node. - def visit_arg_paren(node) - node.copy(arguments: visit(node.arguments)) - end + # Visit a ArgParen node. + def visit_arg_paren(node) + node.copy(arguments: visit(node.arguments)) + end - # Visit a Args node. - def visit_args(node) - node.copy(parts: visit_all(node.parts)) - end + # Visit a Args node. + def visit_args(node) + node.copy(parts: visit_all(node.parts)) + end - # Visit a ArgBlock node. - def visit_arg_block(node) - node.copy(value: visit(node.value)) - end + # Visit a ArgBlock node. + def visit_arg_block(node) + node.copy(value: visit(node.value)) + end - # Visit a ArgStar node. - def visit_arg_star(node) - node.copy(value: visit(node.value)) - end + # Visit a ArgStar node. + def visit_arg_star(node) + node.copy(value: visit(node.value)) + end - # Visit a ArgsForward node. - def visit_args_forward(node) - node.copy - end + # Visit a ArgsForward node. + def visit_args_forward(node) + node.copy + end - # Visit a ArrayLiteral node. - def visit_array(node) - node.copy(lbracket: visit(node.lbracket), contents: visit(node.contents)) - end + # Visit a ArrayLiteral node. + def visit_array(node) + node.copy( + lbracket: visit(node.lbracket), + contents: visit(node.contents) + ) + end - # Visit a AryPtn node. - def visit_aryptn(node) - node.copy( - constant: visit(node.constant), - requireds: visit_all(node.requireds), - rest: visit(node.rest), - posts: visit_all(node.posts) - ) - end + # Visit a AryPtn node. + def visit_aryptn(node) + node.copy( + constant: visit(node.constant), + requireds: visit_all(node.requireds), + rest: visit(node.rest), + posts: visit_all(node.posts) + ) + end - # Visit a Assign node. - def visit_assign(node) - node.copy(target: visit(node.target)) - end + # Visit a Assign node. + def visit_assign(node) + node.copy(target: visit(node.target)) + end - # Visit a Assoc node. - def visit_assoc(node) - node.copy - end + # Visit a Assoc node. + def visit_assoc(node) + node.copy + end - # Visit a AssocSplat node. - def visit_assoc_splat(node) - node.copy - end + # Visit a AssocSplat node. + def visit_assoc_splat(node) + node.copy + end - # Visit a Backref node. - def visit_backref(node) - node.copy - end + # Visit a Backref node. + def visit_backref(node) + node.copy + end - # Visit a Backtick node. - def visit_backtick(node) - node.copy - end + # Visit a Backtick node. + def visit_backtick(node) + node.copy + end - # Visit a BareAssocHash node. - def visit_bare_assoc_hash(node) - node.copy(assocs: visit_all(node.assocs)) - end + # Visit a BareAssocHash node. + def visit_bare_assoc_hash(node) + node.copy(assocs: visit_all(node.assocs)) + end - # Visit a Begin node. - def visit_begin(node) - node.copy(bodystmt: visit(node.bodystmt)) - end + # Visit a Begin node. + def visit_begin(node) + node.copy(bodystmt: visit(node.bodystmt)) + end - # Visit a PinnedBegin node. - def visit_pinned_begin(node) - node.copy - end + # Visit a PinnedBegin node. + def visit_pinned_begin(node) + node.copy + end - # Visit a Binary node. - def visit_binary(node) - node.copy - end + # Visit a Binary node. + def visit_binary(node) + node.copy + end - # Visit a BlockVar node. - def visit_block_var(node) - node.copy(params: visit(node.params), locals: visit_all(node.locals)) - end + # Visit a BlockVar node. + def visit_block_var(node) + node.copy(params: visit(node.params), locals: visit_all(node.locals)) + end - # Visit a BlockArg node. - def visit_blockarg(node) - node.copy(name: visit(node.name)) - end + # Visit a BlockArg node. + def visit_blockarg(node) + node.copy(name: visit(node.name)) + end - # Visit a BodyStmt node. - def visit_bodystmt(node) - node.copy( - statements: visit(node.statements), - rescue_clause: visit(node.rescue_clause), - else_clause: visit(node.else_clause), - ensure_clause: visit(node.ensure_clause) - ) - end + # Visit a BodyStmt node. + def visit_bodystmt(node) + node.copy( + statements: visit(node.statements), + rescue_clause: visit(node.rescue_clause), + else_clause: visit(node.else_clause), + ensure_clause: visit(node.ensure_clause) + ) + end - # Visit a Break node. - def visit_break(node) - node.copy(arguments: visit(node.arguments)) - end + # Visit a Break node. + def visit_break(node) + node.copy(arguments: visit(node.arguments)) + end - # Visit a Call node. - def visit_call(node) - node.copy( - receiver: visit(node.receiver), - operator: node.operator == :"::" ? :"::" : visit(node.operator), - message: node.message == :call ? :call : visit(node.message), - arguments: visit(node.arguments) - ) - end + # Visit a Call node. + def visit_call(node) + node.copy( + receiver: visit(node.receiver), + operator: node.operator == :"::" ? :"::" : visit(node.operator), + message: node.message == :call ? :call : visit(node.message), + arguments: visit(node.arguments) + ) + end - # Visit a Case node. - def visit_case(node) - node.copy( - keyword: visit(node.keyword), - value: visit(node.value), - consequent: visit(node.consequent) - ) - end + # Visit a Case node. + def visit_case(node) + node.copy( + keyword: visit(node.keyword), + value: visit(node.value), + consequent: visit(node.consequent) + ) + end - # Visit a RAssign node. - def visit_rassign(node) - node.copy(operator: visit(node.operator)) - end + # Visit a RAssign node. + def visit_rassign(node) + node.copy(operator: visit(node.operator)) + end - # Visit a ClassDeclaration node. - def visit_class(node) - node.copy( - constant: visit(node.constant), - superclass: visit(node.superclass), - bodystmt: visit(node.bodystmt) - ) - end + # Visit a ClassDeclaration node. + def visit_class(node) + node.copy( + constant: visit(node.constant), + superclass: visit(node.superclass), + bodystmt: visit(node.bodystmt) + ) + end - # Visit a Comma node. - def visit_comma(node) - node.copy - end + # Visit a Comma node. + def visit_comma(node) + node.copy + end - # Visit a Command node. - def visit_command(node) - node.copy( - message: visit(node.message), - arguments: visit(node.arguments), - block: visit(node.block) - ) - end + # Visit a Command node. + def visit_command(node) + node.copy( + message: visit(node.message), + arguments: visit(node.arguments), + block: visit(node.block) + ) + end - # Visit a CommandCall node. - def visit_command_call(node) - node.copy( - operator: node.operator == :"::" ? :"::" : visit(node.operator), - message: visit(node.message), - arguments: visit(node.arguments), - block: visit(node.block) - ) - end + # Visit a CommandCall node. + def visit_command_call(node) + node.copy( + operator: node.operator == :"::" ? :"::" : visit(node.operator), + message: visit(node.message), + arguments: visit(node.arguments), + block: visit(node.block) + ) + end - # Visit a Comment node. - def visit_comment(node) - node.copy - end + # Visit a Comment node. + def visit_comment(node) + node.copy + end - # Visit a Const node. - def visit_const(node) - node.copy - end + # Visit a Const node. + def visit_const(node) + node.copy + end - # Visit a ConstPathField node. - def visit_const_path_field(node) - node.copy(constant: visit(node.constant)) - end + # Visit a ConstPathField node. + def visit_const_path_field(node) + node.copy(constant: visit(node.constant)) + end - # Visit a ConstPathRef node. - def visit_const_path_ref(node) - node.copy(constant: visit(node.constant)) - end + # Visit a ConstPathRef node. + def visit_const_path_ref(node) + node.copy(constant: visit(node.constant)) + end - # Visit a ConstRef node. - def visit_const_ref(node) - node.copy(constant: visit(node.constant)) - end + # Visit a ConstRef node. + def visit_const_ref(node) + node.copy(constant: visit(node.constant)) + end - # Visit a CVar node. - def visit_cvar(node) - node.copy - end + # Visit a CVar node. + def visit_cvar(node) + node.copy + end - # Visit a Def node. - def visit_def(node) - node.copy( - target: visit(node.target), - operator: visit(node.operator), - name: visit(node.name), - params: visit(node.params), - bodystmt: visit(node.bodystmt) - ) - end + # Visit a Def node. + def visit_def(node) + node.copy( + target: visit(node.target), + operator: visit(node.operator), + name: visit(node.name), + params: visit(node.params), + bodystmt: visit(node.bodystmt) + ) + end - # Visit a Defined node. - def visit_defined(node) - node.copy - end + # Visit a Defined node. + def visit_defined(node) + node.copy + end - # Visit a Block node. - def visit_block(node) - node.copy( - opening: visit(node.opening), - block_var: visit(node.block_var), - bodystmt: visit(node.bodystmt) - ) - end + # Visit a Block node. + def visit_block(node) + node.copy( + opening: visit(node.opening), + block_var: visit(node.block_var), + bodystmt: visit(node.bodystmt) + ) + end - # Visit a RangeNode node. - def visit_range(node) - node.copy( - left: visit(node.left), - operator: visit(node.operator), - right: visit(node.right) - ) - end + # Visit a RangeNode node. + def visit_range(node) + node.copy( + left: visit(node.left), + operator: visit(node.operator), + right: visit(node.right) + ) + end - # Visit a DynaSymbol node. - def visit_dyna_symbol(node) - node.copy(parts: visit_all(node.parts)) - end + # Visit a DynaSymbol node. + def visit_dyna_symbol(node) + node.copy(parts: visit_all(node.parts)) + end - # Visit a Else node. - def visit_else(node) - node.copy( - keyword: visit(node.keyword), - statements: visit(node.statements) - ) - end + # Visit a Else node. + def visit_else(node) + node.copy( + keyword: visit(node.keyword), + statements: visit(node.statements) + ) + end - # Visit a Elsif node. - def visit_elsif(node) - node.copy( - statements: visit(node.statements), - consequent: visit(node.consequent) - ) - end + # Visit a Elsif node. + def visit_elsif(node) + node.copy( + statements: visit(node.statements), + consequent: visit(node.consequent) + ) + end - # Visit a EmbDoc node. - def visit_embdoc(node) - node.copy - end + # Visit a EmbDoc node. + def visit_embdoc(node) + node.copy + end - # Visit a EmbExprBeg node. - def visit_embexpr_beg(node) - node.copy - end + # Visit a EmbExprBeg node. + def visit_embexpr_beg(node) + node.copy + end - # Visit a EmbExprEnd node. - def visit_embexpr_end(node) - node.copy - end + # Visit a EmbExprEnd node. + def visit_embexpr_end(node) + node.copy + end - # Visit a EmbVar node. - def visit_embvar(node) - node.copy - end + # Visit a EmbVar node. + def visit_embvar(node) + node.copy + end - # Visit a Ensure node. - def visit_ensure(node) - node.copy( - keyword: visit(node.keyword), - statements: visit(node.statements) - ) - end + # Visit a Ensure node. + def visit_ensure(node) + node.copy( + keyword: visit(node.keyword), + statements: visit(node.statements) + ) + end - # Visit a ExcessedComma node. - def visit_excessed_comma(node) - node.copy - end + # Visit a ExcessedComma node. + def visit_excessed_comma(node) + node.copy + end - # Visit a Field node. - def visit_field(node) - node.copy( - operator: node.operator == :"::" ? :"::" : visit(node.operator), - name: visit(node.name) - ) - end + # Visit a Field node. + def visit_field(node) + node.copy( + operator: node.operator == :"::" ? :"::" : visit(node.operator), + name: visit(node.name) + ) + end - # Visit a FloatLiteral node. - def visit_float(node) - node.copy - end + # Visit a FloatLiteral node. + def visit_float(node) + node.copy + end - # Visit a FndPtn node. - def visit_fndptn(node) - node.copy( - constant: visit(node.constant), - left: visit(node.left), - values: visit_all(node.values), - right: visit(node.right) - ) - end + # Visit a FndPtn node. + def visit_fndptn(node) + node.copy( + constant: visit(node.constant), + left: visit(node.left), + values: visit_all(node.values), + right: visit(node.right) + ) + end - # Visit a For node. - def visit_for(node) - node.copy(index: visit(node.index), statements: visit(node.statements)) - end + # Visit a For node. + def visit_for(node) + node.copy(index: visit(node.index), statements: visit(node.statements)) + end - # Visit a GVar node. - def visit_gvar(node) - node.copy - end + # Visit a GVar node. + def visit_gvar(node) + node.copy + end - # Visit a HashLiteral node. - def visit_hash(node) - node.copy(lbrace: visit(node.lbrace), assocs: visit_all(node.assocs)) - end + # Visit a HashLiteral node. + def visit_hash(node) + node.copy(lbrace: visit(node.lbrace), assocs: visit_all(node.assocs)) + end - # Visit a Heredoc node. - def visit_heredoc(node) - node.copy( - beginning: visit(node.beginning), - ending: visit(node.ending), - parts: visit_all(node.parts) - ) - end + # Visit a Heredoc node. + def visit_heredoc(node) + node.copy( + beginning: visit(node.beginning), + ending: visit(node.ending), + parts: visit_all(node.parts) + ) + end - # Visit a HeredocBeg node. - def visit_heredoc_beg(node) - node.copy - end + # Visit a HeredocBeg node. + def visit_heredoc_beg(node) + node.copy + end - # Visit a HeredocEnd node. - def visit_heredoc_end(node) - node.copy - end + # Visit a HeredocEnd node. + def visit_heredoc_end(node) + node.copy + end - # Visit a HshPtn node. - def visit_hshptn(node) - node.copy( - constant: visit(node.constant), - keywords: - node.keywords.map { |label, value| [visit(label), visit(value)] }, - keyword_rest: visit(node.keyword_rest) - ) - end + # Visit a HshPtn node. + def visit_hshptn(node) + node.copy( + constant: visit(node.constant), + keywords: + node.keywords.map { |label, value| [visit(label), visit(value)] }, + keyword_rest: visit(node.keyword_rest) + ) + end - # Visit a Ident node. - def visit_ident(node) - node.copy - end + # Visit a Ident node. + def visit_ident(node) + node.copy + end - # Visit a IfNode node. - def visit_if(node) - node.copy( - predicate: visit(node.predicate), - statements: visit(node.statements), - consequent: visit(node.consequent) - ) - end + # Visit a IfNode node. + def visit_if(node) + node.copy( + predicate: visit(node.predicate), + statements: visit(node.statements), + consequent: visit(node.consequent) + ) + end - # Visit a IfOp node. - def visit_if_op(node) - node.copy - end + # Visit a IfOp node. + def visit_if_op(node) + node.copy + end - # Visit a Imaginary node. - def visit_imaginary(node) - node.copy - end + # Visit a Imaginary node. + def visit_imaginary(node) + node.copy + end - # Visit a In node. - def visit_in(node) - node.copy( - statements: visit(node.statements), - consequent: visit(node.consequent) - ) - end + # Visit a In node. + def visit_in(node) + node.copy( + statements: visit(node.statements), + consequent: visit(node.consequent) + ) + end - # Visit a Int node. - def visit_int(node) - node.copy - end + # Visit a Int node. + def visit_int(node) + node.copy + end - # Visit a IVar node. - def visit_ivar(node) - node.copy - end + # Visit a IVar node. + def visit_ivar(node) + node.copy + end - # Visit a Kw node. - def visit_kw(node) - node.copy - end + # Visit a Kw node. + def visit_kw(node) + node.copy + end - # Visit a KwRestParam node. - def visit_kwrest_param(node) - node.copy(name: visit(node.name)) - end + # Visit a KwRestParam node. + def visit_kwrest_param(node) + node.copy(name: visit(node.name)) + end - # Visit a Label node. - def visit_label(node) - node.copy - end + # Visit a Label node. + def visit_label(node) + node.copy + end - # Visit a LabelEnd node. - def visit_label_end(node) - node.copy - end + # Visit a LabelEnd node. + def visit_label_end(node) + node.copy + end - # Visit a Lambda node. - def visit_lambda(node) - node.copy(params: visit(node.params), statements: visit(node.statements)) - end + # Visit a Lambda node. + def visit_lambda(node) + node.copy( + params: visit(node.params), + statements: visit(node.statements) + ) + end - # Visit a LambdaVar node. - def visit_lambda_var(node) - node.copy(params: visit(node.params), locals: visit_all(node.locals)) - end + # Visit a LambdaVar node. + def visit_lambda_var(node) + node.copy(params: visit(node.params), locals: visit_all(node.locals)) + end - # Visit a LBrace node. - def visit_lbrace(node) - node.copy - end + # Visit a LBrace node. + def visit_lbrace(node) + node.copy + end - # Visit a LBracket node. - def visit_lbracket(node) - node.copy - end + # Visit a LBracket node. + def visit_lbracket(node) + node.copy + end - # Visit a LParen node. - def visit_lparen(node) - node.copy - end + # Visit a LParen node. + def visit_lparen(node) + node.copy + end - # Visit a MAssign node. - def visit_massign(node) - node.copy(target: visit(node.target)) - end + # Visit a MAssign node. + def visit_massign(node) + node.copy(target: visit(node.target)) + end - # Visit a MethodAddBlock node. - def visit_method_add_block(node) - node.copy(call: visit(node.call), block: visit(node.block)) - end + # Visit a MethodAddBlock node. + def visit_method_add_block(node) + node.copy(call: visit(node.call), block: visit(node.block)) + end - # Visit a MLHS node. - def visit_mlhs(node) - node.copy(parts: visit_all(node.parts)) - end + # Visit a MLHS node. + def visit_mlhs(node) + node.copy(parts: visit_all(node.parts)) + end - # Visit a MLHSParen node. - def visit_mlhs_paren(node) - node.copy(contents: visit(node.contents)) - end + # Visit a MLHSParen node. + def visit_mlhs_paren(node) + node.copy(contents: visit(node.contents)) + end - # Visit a ModuleDeclaration node. - def visit_module(node) - node.copy(constant: visit(node.constant), bodystmt: visit(node.bodystmt)) - end + # Visit a ModuleDeclaration node. + def visit_module(node) + node.copy( + constant: visit(node.constant), + bodystmt: visit(node.bodystmt) + ) + end - # Visit a MRHS node. - def visit_mrhs(node) - node.copy(parts: visit_all(node.parts)) - end + # Visit a MRHS node. + def visit_mrhs(node) + node.copy(parts: visit_all(node.parts)) + end - # Visit a Next node. - def visit_next(node) - node.copy(arguments: visit(node.arguments)) - end + # Visit a Next node. + def visit_next(node) + node.copy(arguments: visit(node.arguments)) + end - # Visit a Op node. - def visit_op(node) - node.copy - end + # Visit a Op node. + def visit_op(node) + node.copy + end - # Visit a OpAssign node. - def visit_opassign(node) - node.copy(target: visit(node.target), operator: visit(node.operator)) - end + # Visit a OpAssign node. + def visit_opassign(node) + node.copy(target: visit(node.target), operator: visit(node.operator)) + end - # Visit a Params node. - def visit_params(node) - node.copy( - requireds: visit_all(node.requireds), - optionals: - node.optionals.map { |ident, value| [visit(ident), visit(value)] }, - rest: visit(node.rest), - posts: visit_all(node.posts), - keywords: - node.keywords.map { |ident, value| [visit(ident), visit(value)] }, - keyword_rest: - node.keyword_rest == :nil ? :nil : visit(node.keyword_rest), - block: visit(node.block) - ) - end + # Visit a Params node. + def visit_params(node) + node.copy( + requireds: visit_all(node.requireds), + optionals: + node.optionals.map { |ident, value| [visit(ident), visit(value)] }, + rest: visit(node.rest), + posts: visit_all(node.posts), + keywords: + node.keywords.map { |ident, value| [visit(ident), visit(value)] }, + keyword_rest: + node.keyword_rest == :nil ? :nil : visit(node.keyword_rest), + block: visit(node.block) + ) + end - # Visit a Paren node. - def visit_paren(node) - node.copy(lparen: visit(node.lparen), contents: visit(node.contents)) - end + # Visit a Paren node. + def visit_paren(node) + node.copy(lparen: visit(node.lparen), contents: visit(node.contents)) + end - # Visit a Period node. - def visit_period(node) - node.copy - end + # Visit a Period node. + def visit_period(node) + node.copy + end - # Visit a Program node. - def visit_program(node) - node.copy(statements: visit(node.statements)) - end + # Visit a Program node. + def visit_program(node) + node.copy(statements: visit(node.statements)) + end - # Visit a QSymbols node. - def visit_qsymbols(node) - node.copy( - beginning: visit(node.beginning), - elements: visit_all(node.elements) - ) - end + # Visit a QSymbols node. + def visit_qsymbols(node) + node.copy( + beginning: visit(node.beginning), + elements: visit_all(node.elements) + ) + end - # Visit a QSymbolsBeg node. - def visit_qsymbols_beg(node) - node.copy - end + # Visit a QSymbolsBeg node. + def visit_qsymbols_beg(node) + node.copy + end - # Visit a QWords node. - def visit_qwords(node) - node.copy( - beginning: visit(node.beginning), - elements: visit_all(node.elements) - ) - end + # Visit a QWords node. + def visit_qwords(node) + node.copy( + beginning: visit(node.beginning), + elements: visit_all(node.elements) + ) + end - # Visit a QWordsBeg node. - def visit_qwords_beg(node) - node.copy - end + # Visit a QWordsBeg node. + def visit_qwords_beg(node) + node.copy + end - # Visit a RationalLiteral node. - def visit_rational(node) - node.copy - end + # Visit a RationalLiteral node. + def visit_rational(node) + node.copy + end - # Visit a RBrace node. - def visit_rbrace(node) - node.copy - end + # Visit a RBrace node. + def visit_rbrace(node) + node.copy + end - # Visit a RBracket node. - def visit_rbracket(node) - node.copy - end + # Visit a RBracket node. + def visit_rbracket(node) + node.copy + end - # Visit a Redo node. - def visit_redo(node) - node.copy - end + # Visit a Redo node. + def visit_redo(node) + node.copy + end - # Visit a RegexpContent node. - def visit_regexp_content(node) - node.copy(parts: visit_all(node.parts)) - end + # Visit a RegexpContent node. + def visit_regexp_content(node) + node.copy(parts: visit_all(node.parts)) + end - # Visit a RegexpBeg node. - def visit_regexp_beg(node) - node.copy - end + # Visit a RegexpBeg node. + def visit_regexp_beg(node) + node.copy + end - # Visit a RegexpEnd node. - def visit_regexp_end(node) - node.copy - end + # Visit a RegexpEnd node. + def visit_regexp_end(node) + node.copy + end - # Visit a RegexpLiteral node. - def visit_regexp_literal(node) - node.copy(parts: visit_all(node.parts)) - end + # Visit a RegexpLiteral node. + def visit_regexp_literal(node) + node.copy(parts: visit_all(node.parts)) + end - # Visit a RescueEx node. - def visit_rescue_ex(node) - node.copy(variable: visit(node.variable)) - end + # Visit a RescueEx node. + def visit_rescue_ex(node) + node.copy(variable: visit(node.variable)) + end - # Visit a Rescue node. - def visit_rescue(node) - node.copy( - keyword: visit(node.keyword), - exception: visit(node.exception), - statements: visit(node.statements), - consequent: visit(node.consequent) - ) - end + # Visit a Rescue node. + def visit_rescue(node) + node.copy( + keyword: visit(node.keyword), + exception: visit(node.exception), + statements: visit(node.statements), + consequent: visit(node.consequent) + ) + end - # Visit a RescueMod node. - def visit_rescue_mod(node) - node.copy - end + # Visit a RescueMod node. + def visit_rescue_mod(node) + node.copy + end - # Visit a RestParam node. - def visit_rest_param(node) - node.copy(name: visit(node.name)) - end + # Visit a RestParam node. + def visit_rest_param(node) + node.copy(name: visit(node.name)) + end - # Visit a Retry node. - def visit_retry(node) - node.copy - end + # Visit a Retry node. + def visit_retry(node) + node.copy + end - # Visit a Return node. - def visit_return(node) - node.copy(arguments: visit(node.arguments)) - end + # Visit a Return node. + def visit_return(node) + node.copy(arguments: visit(node.arguments)) + end - # Visit a RParen node. - def visit_rparen(node) - node.copy - end + # Visit a RParen node. + def visit_rparen(node) + node.copy + end - # Visit a SClass node. - def visit_sclass(node) - node.copy(bodystmt: visit(node.bodystmt)) - end + # Visit a SClass node. + def visit_sclass(node) + node.copy(bodystmt: visit(node.bodystmt)) + end - # Visit a Statements node. - def visit_statements(node) - node.copy(body: visit_all(node.body)) - end + # Visit a Statements node. + def visit_statements(node) + node.copy(body: visit_all(node.body)) + end - # Visit a StringContent node. - def visit_string_content(node) - node.copy(parts: visit_all(node.parts)) - end + # Visit a StringContent node. + def visit_string_content(node) + node.copy(parts: visit_all(node.parts)) + end - # Visit a StringConcat node. - def visit_string_concat(node) - node.copy(left: visit(node.left), right: visit(node.right)) - end + # Visit a StringConcat node. + def visit_string_concat(node) + node.copy(left: visit(node.left), right: visit(node.right)) + end - # Visit a StringDVar node. - def visit_string_dvar(node) - node.copy(variable: visit(node.variable)) - end + # Visit a StringDVar node. + def visit_string_dvar(node) + node.copy(variable: visit(node.variable)) + end - # Visit a StringEmbExpr node. - def visit_string_embexpr(node) - node.copy(statements: visit(node.statements)) - end + # Visit a StringEmbExpr node. + def visit_string_embexpr(node) + node.copy(statements: visit(node.statements)) + end - # Visit a StringLiteral node. - def visit_string_literal(node) - node.copy(parts: visit_all(node.parts)) - end + # Visit a StringLiteral node. + def visit_string_literal(node) + node.copy(parts: visit_all(node.parts)) + end - # Visit a Super node. - def visit_super(node) - node.copy(arguments: visit(node.arguments)) - end + # Visit a Super node. + def visit_super(node) + node.copy(arguments: visit(node.arguments)) + end - # Visit a SymBeg node. - def visit_symbeg(node) - node.copy - end + # Visit a SymBeg node. + def visit_symbeg(node) + node.copy + end - # Visit a SymbolContent node. - def visit_symbol_content(node) - node.copy(value: visit(node.value)) - end + # Visit a SymbolContent node. + def visit_symbol_content(node) + node.copy(value: visit(node.value)) + end - # Visit a SymbolLiteral node. - def visit_symbol_literal(node) - node.copy(value: visit(node.value)) - end + # Visit a SymbolLiteral node. + def visit_symbol_literal(node) + node.copy(value: visit(node.value)) + end - # Visit a Symbols node. - def visit_symbols(node) - node.copy( - beginning: visit(node.beginning), - elements: visit_all(node.elements) - ) - end + # Visit a Symbols node. + def visit_symbols(node) + node.copy( + beginning: visit(node.beginning), + elements: visit_all(node.elements) + ) + end - # Visit a SymbolsBeg node. - def visit_symbols_beg(node) - node.copy - end + # Visit a SymbolsBeg node. + def visit_symbols_beg(node) + node.copy + end - # Visit a TLambda node. - def visit_tlambda(node) - node.copy - end + # Visit a TLambda node. + def visit_tlambda(node) + node.copy + end - # Visit a TLamBeg node. - def visit_tlambeg(node) - node.copy - end + # Visit a TLamBeg node. + def visit_tlambeg(node) + node.copy + end - # Visit a TopConstField node. - def visit_top_const_field(node) - node.copy(constant: visit(node.constant)) - end + # Visit a TopConstField node. + def visit_top_const_field(node) + node.copy(constant: visit(node.constant)) + end - # Visit a TopConstRef node. - def visit_top_const_ref(node) - node.copy(constant: visit(node.constant)) - end + # Visit a TopConstRef node. + def visit_top_const_ref(node) + node.copy(constant: visit(node.constant)) + end - # Visit a TStringBeg node. - def visit_tstring_beg(node) - node.copy - end + # Visit a TStringBeg node. + def visit_tstring_beg(node) + node.copy + end - # Visit a TStringContent node. - def visit_tstring_content(node) - node.copy - end + # Visit a TStringContent node. + def visit_tstring_content(node) + node.copy + end - # Visit a TStringEnd node. - def visit_tstring_end(node) - node.copy - end + # Visit a TStringEnd node. + def visit_tstring_end(node) + node.copy + end - # Visit a Not node. - def visit_not(node) - node.copy(statement: visit(node.statement)) - end + # Visit a Not node. + def visit_not(node) + node.copy(statement: visit(node.statement)) + end - # Visit a Unary node. - def visit_unary(node) - node.copy - end + # Visit a Unary node. + def visit_unary(node) + node.copy + end - # Visit a Undef node. - def visit_undef(node) - node.copy(symbols: visit_all(node.symbols)) - end + # Visit a Undef node. + def visit_undef(node) + node.copy(symbols: visit_all(node.symbols)) + end - # Visit a UnlessNode node. - def visit_unless(node) - node.copy( - predicate: visit(node.predicate), - statements: visit(node.statements), - consequent: visit(node.consequent) - ) - end + # Visit a UnlessNode node. + def visit_unless(node) + node.copy( + predicate: visit(node.predicate), + statements: visit(node.statements), + consequent: visit(node.consequent) + ) + end - # Visit a UntilNode node. - def visit_until(node) - node.copy( - predicate: visit(node.predicate), - statements: visit(node.statements) - ) - end + # Visit a UntilNode node. + def visit_until(node) + node.copy( + predicate: visit(node.predicate), + statements: visit(node.statements) + ) + end - # Visit a VarField node. - def visit_var_field(node) - node.copy(value: visit(node.value)) - end + # Visit a VarField node. + def visit_var_field(node) + node.copy(value: visit(node.value)) + end - # Visit a VarRef node. - def visit_var_ref(node) - node.copy(value: visit(node.value)) - end + # Visit a VarRef node. + def visit_var_ref(node) + node.copy(value: visit(node.value)) + end - # Visit a PinnedVarRef node. - def visit_pinned_var_ref(node) - node.copy(value: visit(node.value)) - end + # Visit a PinnedVarRef node. + def visit_pinned_var_ref(node) + node.copy(value: visit(node.value)) + end - # Visit a VCall node. - def visit_vcall(node) - node.copy(value: visit(node.value)) - end + # Visit a VCall node. + def visit_vcall(node) + node.copy(value: visit(node.value)) + end - # Visit a VoidStmt node. - def visit_void_stmt(node) - node.copy - end + # Visit a VoidStmt node. + def visit_void_stmt(node) + node.copy + end - # Visit a When node. - def visit_when(node) - node.copy( - arguments: visit(node.arguments), - statements: visit(node.statements), - consequent: visit(node.consequent) - ) - end + # Visit a When node. + def visit_when(node) + node.copy( + arguments: visit(node.arguments), + statements: visit(node.statements), + consequent: visit(node.consequent) + ) + end - # Visit a WhileNode node. - def visit_while(node) - node.copy( - predicate: visit(node.predicate), - statements: visit(node.statements) - ) - end + # Visit a WhileNode node. + def visit_while(node) + node.copy( + predicate: visit(node.predicate), + statements: visit(node.statements) + ) + end - # Visit a Word node. - def visit_word(node) - node.copy(parts: visit_all(node.parts)) - end + # Visit a Word node. + def visit_word(node) + node.copy(parts: visit_all(node.parts)) + end - # Visit a Words node. - def visit_words(node) - node.copy( - beginning: visit(node.beginning), - elements: visit_all(node.elements) - ) - end + # Visit a Words node. + def visit_words(node) + node.copy( + beginning: visit(node.beginning), + elements: visit_all(node.elements) + ) + end - # Visit a WordsBeg node. - def visit_words_beg(node) - node.copy - end + # Visit a WordsBeg node. + def visit_words_beg(node) + node.copy + end - # Visit a XString node. - def visit_xstring(node) - node.copy(parts: visit_all(node.parts)) - end + # Visit a XString node. + def visit_xstring(node) + node.copy(parts: visit_all(node.parts)) + end - # Visit a XStringLiteral node. - def visit_xstring_literal(node) - node.copy(parts: visit_all(node.parts)) - end + # Visit a XStringLiteral node. + def visit_xstring_literal(node) + node.copy(parts: visit_all(node.parts)) + end - # Visit a YieldNode node. - def visit_yield(node) - node.copy(arguments: visit(node.arguments)) - end + # Visit a YieldNode node. + def visit_yield(node) + node.copy(arguments: visit(node.arguments)) + end - # Visit a ZSuper node. - def visit_zsuper(node) - node.copy + # Visit a ZSuper node. + def visit_zsuper(node) + node.copy + end end end end diff --git a/lib/syntax_tree/parser.rb b/lib/syntax_tree/parser.rb index 8059b18c..426bd945 100644 --- a/lib/syntax_tree/parser.rb +++ b/lib/syntax_tree/parser.rb @@ -668,8 +668,10 @@ def visit(node) stack.pop end - def visit_var_ref(node) - node.pin(stack[-2], pins.shift) + visit_methods do + def visit_var_ref(node) + node.pin(stack[-2], pins.shift) + end end def self.visit(node, tokens) diff --git a/lib/syntax_tree/translation/parser.rb b/lib/syntax_tree/translation/parser.rb index 70c98336..ad889478 100644 --- a/lib/syntax_tree/translation/parser.rb +++ b/lib/syntax_tree/translation/parser.rb @@ -89,2538 +89,2589 @@ def visit(node) result end - # Visit an AliasNode node. - def visit_alias(node) - s( - :alias, - [visit(node.left), visit(node.right)], - smap_keyword_bare( - srange_length(node.start_char, 5), - srange_node(node) + visit_methods do + # Visit an AliasNode node. + def visit_alias(node) + s( + :alias, + [visit(node.left), visit(node.right)], + smap_keyword_bare( + srange_length(node.start_char, 5), + srange_node(node) + ) ) - ) - end + end - # Visit an ARefNode. - def visit_aref(node) - if ::Parser::Builders::Default.emit_index - if node.index.nil? - s( - :index, - [visit(node.collection)], - smap_index( - srange_find(node.collection.end_char, node.end_char, "["), - srange_length(node.end_char, -1), - srange_node(node) + # Visit an ARefNode. + def visit_aref(node) + if ::Parser::Builders::Default.emit_index + if node.index.nil? + s( + :index, + [visit(node.collection)], + smap_index( + srange_find(node.collection.end_char, node.end_char, "["), + srange_length(node.end_char, -1), + srange_node(node) + ) ) - ) + else + s( + :index, + [visit(node.collection)].concat(visit_all(node.index.parts)), + smap_index( + srange_find_between(node.collection, node.index, "["), + srange_length(node.end_char, -1), + srange_node(node) + ) + ) + end else - s( - :index, - [visit(node.collection)].concat(visit_all(node.index.parts)), - smap_index( - srange_find_between(node.collection, node.index, "["), - srange_length(node.end_char, -1), - srange_node(node) + if node.index.nil? + s( + :send, + [visit(node.collection), :[]], + smap_send_bare( + srange_find(node.collection.end_char, node.end_char, "[]"), + srange_node(node) + ) ) - ) + else + s( + :send, + [visit(node.collection), :[], *visit_all(node.index.parts)], + smap_send_bare( + srange( + srange_find_between( + node.collection, + node.index, + "[" + ).begin_pos, + node.end_char + ), + srange_node(node) + ) + ) + end end - else - if node.index.nil? - s( - :send, - [visit(node.collection), :[]], - smap_send_bare( - srange_find(node.collection.end_char, node.end_char, "[]"), - srange_node(node) + end + + # Visit an ARefField node. + def visit_aref_field(node) + if ::Parser::Builders::Default.emit_index + if node.index.nil? + s( + :indexasgn, + [visit(node.collection)], + smap_index( + srange_find(node.collection.end_char, node.end_char, "["), + srange_length(node.end_char, -1), + srange_node(node) + ) ) - ) + else + s( + :indexasgn, + [visit(node.collection)].concat(visit_all(node.index.parts)), + smap_index( + srange_find_between(node.collection, node.index, "["), + srange_length(node.end_char, -1), + srange_node(node) + ) + ) + end else - s( - :send, - [visit(node.collection), :[], *visit_all(node.index.parts)], - smap_send_bare( - srange( - srange_find_between( - node.collection, - node.index, - "[" - ).begin_pos, - node.end_char + if node.index.nil? + s( + :send, + [visit(node.collection), :[]=], + smap_send_bare( + srange_find(node.collection.end_char, node.end_char, "[]"), + srange_node(node) + ) + ) + else + s( + :send, + [visit(node.collection), :[]=].concat( + visit_all(node.index.parts) ), + smap_send_bare( + srange( + srange_find_between( + node.collection, + node.index, + "[" + ).begin_pos, + node.end_char + ), + srange_node(node) + ) + ) + end + end + end + + # Visit an ArgBlock node. + def visit_arg_block(node) + s( + :block_pass, + [visit(node.value)], + smap_operator(srange_length(node.start_char, 1), srange_node(node)) + ) + end + + # Visit an ArgStar node. + def visit_arg_star(node) + if stack[-3].is_a?(MLHSParen) && stack[-3].contents.is_a?(MLHS) + if node.value.nil? + s(:restarg, [], smap_variable(nil, srange_node(node))) + else + s( + :restarg, + [node.value.value.to_sym], + smap_variable(srange_node(node.value), srange_node(node)) + ) + end + else + s( + :splat, + node.value.nil? ? [] : [visit(node.value)], + smap_operator( + srange_length(node.start_char, 1), srange_node(node) ) ) end end - end - # Visit an ARefField node. - def visit_aref_field(node) - if ::Parser::Builders::Default.emit_index - if node.index.nil? - s( - :indexasgn, - [visit(node.collection)], - smap_index( - srange_find(node.collection.end_char, node.end_char, "["), + # Visit an ArgsForward node. + def visit_args_forward(node) + s(:forwarded_args, [], smap(srange_node(node))) + end + + # Visit an ArrayLiteral node. + def visit_array(node) + s( + :array, + node.contents ? visit_all(node.contents.parts) : [], + if node.lbracket.nil? + smap_collection_bare(srange_node(node)) + else + smap_collection( + srange_node(node.lbracket), srange_length(node.end_char, -1), srange_node(node) ) - ) - else + end + ) + end + + # Visit an AryPtn node. + def visit_aryptn(node) + type = :array_pattern + children = visit_all(node.requireds) + + if node.rest.is_a?(VarField) + if !node.rest.value.nil? + children << s(:match_rest, [visit(node.rest)], nil) + elsif node.posts.empty? && + node.rest.start_char == node.rest.end_char + # Here we have an implicit rest, as in [foo,]. parser has a + # specific type for these patterns. + type = :array_pattern_with_tail + else + children << s(:match_rest, [], nil) + end + end + + if node.constant s( - :indexasgn, - [visit(node.collection)].concat(visit_all(node.index.parts)), - smap_index( - srange_find_between(node.collection, node.index, "["), + :const_pattern, + [ + visit(node.constant), + s( + type, + children + visit_all(node.posts), + smap_collection_bare( + srange(node.constant.end_char + 1, node.end_char - 1) + ) + ) + ], + smap_collection( + srange_length(node.constant.end_char, 1), srange_length(node.end_char, -1), srange_node(node) ) ) + else + s( + type, + children + visit_all(node.posts), + if buffer.source[node.start_char] == "[" + smap_collection( + srange_length(node.start_char, 1), + srange_length(node.end_char, -1), + srange_node(node) + ) + else + smap_collection_bare(srange_node(node)) + end + ) end - else - if node.index.nil? + end + + # Visit an Assign node. + def visit_assign(node) + target = visit(node.target) + location = + target + .location + .with_operator(srange_find_between(node.target, node.value, "=")) + .with_expression(srange_node(node)) + + s(target.type, target.children + [visit(node.value)], location) + end + + # Visit an Assoc node. + def visit_assoc(node) + if node.value.nil? + expression = srange(node.start_char, node.end_char - 1) + + type, location = + if node.key.value.start_with?(/[A-Z]/) + [:const, smap_constant(nil, expression, expression)] + else + [:send, smap_send_bare(expression, expression)] + end + s( - :send, - [visit(node.collection), :[]=], - smap_send_bare( - srange_find(node.collection.end_char, node.end_char, "[]"), + :pair, + [ + visit(node.key), + s(type, [nil, node.key.value.chomp(":").to_sym], location) + ], + smap_operator( + srange_length(node.key.end_char, -1), srange_node(node) ) ) else s( - :send, - [visit(node.collection), :[]=].concat( - visit_all(node.index.parts) - ), - smap_send_bare( - srange( - srange_find_between( - node.collection, - node.index, - "[" - ).begin_pos, - node.end_char - ), + :pair, + [visit(node.key), visit(node.value)], + smap_operator( + srange_search_between(node.key, node.value, "=>") || + srange_length(node.key.end_char, -1), srange_node(node) ) ) end end - end - # Visit an ArgBlock node. - def visit_arg_block(node) - s( - :block_pass, - [visit(node.value)], - smap_operator(srange_length(node.start_char, 1), srange_node(node)) - ) - end + # Visit an AssocSplat node. + def visit_assoc_splat(node) + s( + :kwsplat, + [visit(node.value)], + smap_operator(srange_length(node.start_char, 2), srange_node(node)) + ) + end - # Visit an ArgStar node. - def visit_arg_star(node) - if stack[-3].is_a?(MLHSParen) && stack[-3].contents.is_a?(MLHS) - if node.value.nil? - s(:restarg, [], smap_variable(nil, srange_node(node))) + # Visit a Backref node. + def visit_backref(node) + location = smap(srange_node(node)) + + if node.value.match?(/^\$\d+$/) + s(:nth_ref, [node.value[1..].to_i], location) else - s( - :restarg, - [node.value.value.to_sym], - smap_variable(srange_node(node.value), srange_node(node)) - ) + s(:back_ref, [node.value.to_sym], location) end - else + end + + # Visit a BareAssocHash node. + def visit_bare_assoc_hash(node) s( - :splat, - node.value.nil? ? [] : [visit(node.value)], - smap_operator(srange_length(node.start_char, 1), srange_node(node)) + if ::Parser::Builders::Default.emit_kwargs && + !stack[-2].is_a?(ArrayLiteral) + :kwargs + else + :hash + end, + visit_all(node.assocs), + smap_collection_bare(srange_node(node)) ) end - end - # Visit an ArgsForward node. - def visit_args_forward(node) - s(:forwarded_args, [], smap(srange_node(node))) - end + # Visit a BEGINBlock node. + def visit_BEGIN(node) + s( + :preexe, + [visit(node.statements)], + smap_keyword( + srange_length(node.start_char, 5), + srange_find(node.start_char + 5, node.statements.start_char, "{"), + srange_length(node.end_char, -1), + srange_node(node) + ) + ) + end - # Visit an ArrayLiteral node. - def visit_array(node) - s( - :array, - node.contents ? visit_all(node.contents.parts) : [], - if node.lbracket.nil? - smap_collection_bare(srange_node(node)) - else + # Visit a Begin node. + def visit_begin(node) + location = smap_collection( - srange_node(node.lbracket), - srange_length(node.end_char, -1), + srange_length(node.start_char, 5), + srange_length(node.end_char, -3), srange_node(node) ) - end - ) - end - # Visit an AryPtn node. - def visit_aryptn(node) - type = :array_pattern - children = visit_all(node.requireds) - - if node.rest.is_a?(VarField) - if !node.rest.value.nil? - children << s(:match_rest, [visit(node.rest)], nil) - elsif node.posts.empty? && node.rest.start_char == node.rest.end_char - # Here we have an implicit rest, as in [foo,]. parser has a specific - # type for these patterns. - type = :array_pattern_with_tail + if node.bodystmt.empty? + s(:kwbegin, [], location) + elsif node.bodystmt.rescue_clause.nil? && + node.bodystmt.ensure_clause.nil? && + node.bodystmt.else_clause.nil? + child = visit(node.bodystmt.statements) + + s( + :kwbegin, + child.type == :begin ? child.children : [child], + location + ) else - children << s(:match_rest, [], nil) + s(:kwbegin, [visit(node.bodystmt)], location) end end - if node.constant - s( - :const_pattern, - [ - visit(node.constant), - s( - type, - children + visit_all(node.posts), - smap_collection_bare( - srange(node.constant.end_char + 1, node.end_char - 1) - ) + # Visit a Binary node. + def visit_binary(node) + case node.operator + when :| + current = -2 + while stack[current].is_a?(Binary) && stack[current].operator == :| + current -= 1 + end + + if stack[current].is_a?(In) + s(:match_alt, [visit(node.left), visit(node.right)], nil) + else + visit(canonical_binary(node)) + end + when :"=>", :"&&", :and, :"||", :or + s( + { "=>": :match_as, "&&": :and, "||": :or }.fetch( + node.operator, + node.operator + ), + [visit(node.left), visit(node.right)], + smap_operator( + srange_find_between(node.left, node.right, node.operator.to_s), + srange_node(node) ) - ], - smap_collection( - srange_length(node.constant.end_char, 1), - srange_length(node.end_char, -1), - srange_node(node) ) - ) - else - s( - type, - children + visit_all(node.posts), - if buffer.source[node.start_char] == "[" - smap_collection( - srange_length(node.start_char, 1), - srange_length(node.end_char, -1), - srange_node(node) + when :=~ + # When you use a regular expression on the left hand side of a =~ + # operator and it doesn't have interpolatoin, then its named capture + # groups introduce local variables into the scope. In this case the + # parser gem has a different node (match_with_lvasgn) instead of the + # regular send. + if node.left.is_a?(RegexpLiteral) && node.left.parts.length == 1 && + node.left.parts.first.is_a?(TStringContent) + s( + :match_with_lvasgn, + [visit(node.left), visit(node.right)], + smap_operator( + srange_find_between( + node.left, + node.right, + node.operator.to_s + ), + srange_node(node) + ) ) else - smap_collection_bare(srange_node(node)) + visit(canonical_binary(node)) end - ) + else + visit(canonical_binary(node)) + end end - end - # Visit an Assign node. - def visit_assign(node) - target = visit(node.target) - location = - target - .location - .with_operator(srange_find_between(node.target, node.value, "=")) - .with_expression(srange_node(node)) + # Visit a BlockArg node. + def visit_blockarg(node) + if node.name.nil? + s(:blockarg, [nil], smap_variable(nil, srange_node(node))) + else + s( + :blockarg, + [node.name.value.to_sym], + smap_variable(srange_node(node.name), srange_node(node)) + ) + end + end - s(target.type, target.children + [visit(node.value)], location) - end + # Visit a BlockVar node. + def visit_block_var(node) + shadowargs = + node.locals.map do |local| + s( + :shadowarg, + [local.value.to_sym], + smap_variable(srange_node(local), srange_node(local)) + ) + end - # Visit an Assoc node. - def visit_assoc(node) - if node.value.nil? - expression = srange(node.start_char, node.end_char - 1) + params = node.params + children = + if ::Parser::Builders::Default.emit_procarg0 && node.arg0? + # There is a special node type in the parser gem for when a single + # required parameter to a block would potentially be expanded + # automatically. We handle that case here. + required = params.requireds.first + procarg0 = + if ::Parser::Builders::Default.emit_arg_inside_procarg0 && + required.is_a?(Ident) + s( + :procarg0, + [ + s( + :arg, + [required.value.to_sym], + smap_variable( + srange_node(required), + srange_node(required) + ) + ) + ], + smap_collection_bare(srange_node(required)) + ) + else + child = visit(required) + s(:procarg0, child, child.location) + end - type, location = - if node.key.value.start_with?(/[A-Z]/) - [:const, smap_constant(nil, expression, expression)] + [procarg0] else - [:send, smap_send_bare(expression, expression)] + visit(params).children end s( - :pair, - [ - visit(node.key), - s(type, [nil, node.key.value.chomp(":").to_sym], location) - ], - smap_operator( - srange_length(node.key.end_char, -1), - srange_node(node) - ) - ) - else - s( - :pair, - [visit(node.key), visit(node.value)], - smap_operator( - srange_search_between(node.key, node.value, "=>") || - srange_length(node.key.end_char, -1), + :args, + children + shadowargs, + smap_collection( + srange_length(node.start_char, 1), + srange_length(node.end_char, -1), srange_node(node) ) ) end - end - - # Visit an AssocSplat node. - def visit_assoc_splat(node) - s( - :kwsplat, - [visit(node.value)], - smap_operator(srange_length(node.start_char, 2), srange_node(node)) - ) - end - # Visit a Backref node. - def visit_backref(node) - location = smap(srange_node(node)) - - if node.value.match?(/^\$\d+$/) - s(:nth_ref, [node.value[1..].to_i], location) - else - s(:back_ref, [node.value.to_sym], location) - end - end + # Visit a BodyStmt node. + def visit_bodystmt(node) + result = visit(node.statements) + + if node.rescue_clause + rescue_node = visit(node.rescue_clause) + + children = [result] + rescue_node.children + location = rescue_node.location + + if node.else_clause + children.pop + children << visit(node.else_clause) + + location = + smap_condition( + nil, + nil, + srange_length(node.else_clause.start_char - 3, -4), + nil, + srange( + location.expression.begin_pos, + node.else_clause.end_char + ) + ) + end - # Visit a BareAssocHash node. - def visit_bare_assoc_hash(node) - s( - if ::Parser::Builders::Default.emit_kwargs && - !stack[-2].is_a?(ArrayLiteral) - :kwargs - else - :hash - end, - visit_all(node.assocs), - smap_collection_bare(srange_node(node)) - ) - end + result = s(rescue_node.type, children, location) + end - # Visit a BEGINBlock node. - def visit_BEGIN(node) - s( - :preexe, - [visit(node.statements)], - smap_keyword( - srange_length(node.start_char, 5), - srange_find(node.start_char + 5, node.statements.start_char, "{"), - srange_length(node.end_char, -1), - srange_node(node) - ) - ) - end + if node.ensure_clause + ensure_node = visit(node.ensure_clause) - # Visit a Begin node. - def visit_begin(node) - location = - smap_collection( - srange_length(node.start_char, 5), - srange_length(node.end_char, -3), - srange_node(node) - ) + expression = + ( + if result + result.location.expression.join( + ensure_node.location.expression + ) + else + ensure_node.location.expression + end + ) + location = ensure_node.location.with_expression(expression) - if node.bodystmt.empty? - s(:kwbegin, [], location) - elsif node.bodystmt.rescue_clause.nil? && - node.bodystmt.ensure_clause.nil? && node.bodystmt.else_clause.nil? - child = visit(node.bodystmt.statements) + result = + s(ensure_node.type, [result] + ensure_node.children, location) + end - s(:kwbegin, child.type == :begin ? child.children : [child], location) - else - s(:kwbegin, [visit(node.bodystmt)], location) + result end - end - # Visit a Binary node. - def visit_binary(node) - case node.operator - when :| - current = -2 - while stack[current].is_a?(Binary) && stack[current].operator == :| - current -= 1 - end + # Visit a Break node. + def visit_break(node) + s( + :break, + visit_all(node.arguments.parts), + smap_keyword_bare( + srange_length(node.start_char, 5), + srange_node(node) + ) + ) + end - if stack[current].is_a?(In) - s(:match_alt, [visit(node.left), visit(node.right)], nil) - else - visit(canonical_binary(node)) + # Visit a CallNode node. + def visit_call(node) + visit_command_call( + CommandCall.new( + receiver: node.receiver, + operator: node.operator, + message: node.message, + arguments: node.arguments, + block: nil, + location: node.location + ) + ) + end + + # Visit a Case node. + def visit_case(node) + clauses = [node.consequent] + while clauses.last && !clauses.last.is_a?(Else) + clauses << clauses.last.consequent end - when :"=>", :"&&", :and, :"||", :or + + else_token = + if clauses.last.is_a?(Else) + srange_length(clauses.last.start_char, 4) + end + s( - { "=>": :match_as, "&&": :and, "||": :or }.fetch( - node.operator, - node.operator - ), - [visit(node.left), visit(node.right)], - smap_operator( - srange_find_between(node.left, node.right, node.operator.to_s), + node.consequent.is_a?(In) ? :case_match : :case, + [visit(node.value)] + clauses.map { |clause| visit(clause) }, + smap_condition( + srange_length(node.start_char, 4), + nil, + else_token, + srange_length(node.end_char, -3), srange_node(node) ) ) - when :=~ - # When you use a regular expression on the left hand side of a =~ - # operator and it doesn't have interpolatoin, then its named capture - # groups introduce local variables into the scope. In this case the - # parser gem has a different node (match_with_lvasgn) instead of the - # regular send. - if node.left.is_a?(RegexpLiteral) && node.left.parts.length == 1 && - node.left.parts.first.is_a?(TStringContent) - s( - :match_with_lvasgn, - [visit(node.left), visit(node.right)], - smap_operator( - srange_find_between(node.left, node.right, node.operator.to_s), - srange_node(node) - ) + end + + # Visit a CHAR node. + def visit_CHAR(node) + s( + :str, + [node.value[1..]], + smap_collection( + srange_length(node.start_char, 1), + nil, + srange_node(node) ) - else - visit(canonical_binary(node)) - end - else - visit(canonical_binary(node)) + ) end - end - # Visit a BlockArg node. - def visit_blockarg(node) - if node.name.nil? - s(:blockarg, [nil], smap_variable(nil, srange_node(node))) - else + # Visit a ClassDeclaration node. + def visit_class(node) + operator = + if node.superclass + srange_find_between(node.constant, node.superclass, "<") + end + s( - :blockarg, - [node.name.value.to_sym], - smap_variable(srange_node(node.name), srange_node(node)) + :class, + [ + visit(node.constant), + visit(node.superclass), + visit(node.bodystmt) + ], + smap_definition( + srange_length(node.start_char, 5), + operator, + srange_node(node.constant), + srange_length(node.end_char, -3) + ).with_expression(srange_node(node)) ) end - end - # Visit a BlockVar node. - def visit_block_var(node) - shadowargs = - node.locals.map do |local| - s( - :shadowarg, - [local.value.to_sym], - smap_variable(srange_node(local), srange_node(local)) + # Visit a Command node. + def visit_command(node) + visit_command_call( + CommandCall.new( + receiver: nil, + operator: nil, + message: node.message, + arguments: node.arguments, + block: node.block, + location: node.location ) - end + ) + end - params = node.params - children = - if ::Parser::Builders::Default.emit_procarg0 && node.arg0? - # There is a special node type in the parser gem for when a single - # required parameter to a block would potentially be expanded - # automatically. We handle that case here. - required = params.requireds.first - procarg0 = - if ::Parser::Builders::Default.emit_arg_inside_procarg0 && - required.is_a?(Ident) - s( - :procarg0, - [ - s( - :arg, - [required.value.to_sym], - smap_variable( - srange_node(required), - srange_node(required) - ) - ) - ], - smap_collection_bare(srange_node(required)) - ) - else - child = visit(required) - s(:procarg0, child, child.location) - end + # Visit a CommandCall node. + def visit_command_call(node) + children = [ + visit(node.receiver), + node.message == :call ? :call : node.message.value.to_sym + ] + + begin_token = nil + end_token = nil + + case node.arguments + when Args + children += visit_all(node.arguments.parts) + when ArgParen + case node.arguments.arguments + when nil + # skip + when ArgsForward + children << visit(node.arguments.arguments) + else + children += visit_all(node.arguments.arguments.parts) + end - [procarg0] - else - visit(params).children + begin_token = srange_length(node.arguments.start_char, 1) + end_token = srange_length(node.arguments.end_char, -1) end - s( - :args, - children + shadowargs, - smap_collection( - srange_length(node.start_char, 1), - srange_length(node.end_char, -1), - srange_node(node) - ) - ) - end + dot_bound = + if node.arguments + node.arguments.start_char + elsif node.block + node.block.start_char + else + node.end_char + end - # Visit a BodyStmt node. - def visit_bodystmt(node) - result = visit(node.statements) + expression = + if node.arguments.is_a?(ArgParen) + srange(node.start_char, node.arguments.end_char) + elsif node.arguments.is_a?(Args) && node.arguments.parts.any? + last_part = node.arguments.parts.last + end_char = + if last_part.is_a?(Heredoc) + last_part.beginning.end_char + else + last_part.end_char + end - if node.rescue_clause - rescue_node = visit(node.rescue_clause) + srange(node.start_char, end_char) + elsif node.block + srange_node(node.message) + else + srange_node(node) + end - children = [result] + rescue_node.children - location = rescue_node.location + call = + s( + if node.operator.is_a?(Op) && node.operator.value == "&." + :csend + else + :send + end, + children, + smap_send( + if node.operator == :"::" + srange_find( + node.receiver.end_char, + if node.message == :call + dot_bound + else + node.message.start_char + end, + "::" + ) + elsif node.operator + srange_node(node.operator) + end, + node.message == :call ? nil : srange_node(node.message), + begin_token, + end_token, + expression + ) + ) - if node.else_clause - children.pop - children << visit(node.else_clause) + if node.block + type, arguments = block_children(node.block) - location = - smap_condition( - nil, - nil, - srange_length(node.else_clause.start_char - 3, -4), - nil, - srange(location.expression.begin_pos, node.else_clause.end_char) + s( + type, + [call, arguments, visit(node.block.bodystmt)], + smap_collection( + srange_node(node.block.opening), + srange_length( + node.end_char, + node.block.opening.is_a?(Kw) ? -3 : -1 + ), + srange_node(node) ) + ) + else + call end - - result = s(rescue_node.type, children, location) end - if node.ensure_clause - ensure_node = visit(node.ensure_clause) + # Visit a Const node. + def visit_const(node) + s( + :const, + [nil, node.value.to_sym], + smap_constant(nil, srange_node(node), srange_node(node)) + ) + end - expression = - ( - if result - result.location.expression.join(ensure_node.location.expression) - else - ensure_node.location.expression - end + # Visit a ConstPathField node. + def visit_const_path_field(node) + if node.parent.is_a?(VarRef) && node.parent.value.is_a?(Kw) && + node.parent.value.value == "self" && node.constant.is_a?(Ident) + s(:send, [visit(node.parent), :"#{node.constant.value}="], nil) + else + s( + :casgn, + [visit(node.parent), node.constant.value.to_sym], + smap_constant( + srange_find_between(node.parent, node.constant, "::"), + srange_node(node.constant), + srange_node(node) + ) ) - location = ensure_node.location.with_expression(expression) - - result = - s(ensure_node.type, [result] + ensure_node.children, location) + end end - result - end - - # Visit a Break node. - def visit_break(node) - s( - :break, - visit_all(node.arguments.parts), - smap_keyword_bare( - srange_length(node.start_char, 5), - srange_node(node) + # Visit a ConstPathRef node. + def visit_const_path_ref(node) + s( + :const, + [visit(node.parent), node.constant.value.to_sym], + smap_constant( + srange_find_between(node.parent, node.constant, "::"), + srange_node(node.constant), + srange_node(node) + ) ) - ) - end + end - # Visit a CallNode node. - def visit_call(node) - visit_command_call( - CommandCall.new( - receiver: node.receiver, - operator: node.operator, - message: node.message, - arguments: node.arguments, - block: nil, - location: node.location + # Visit a ConstRef node. + def visit_const_ref(node) + s( + :const, + [nil, node.constant.value.to_sym], + smap_constant(nil, srange_node(node.constant), srange_node(node)) ) - ) - end + end - # Visit a Case node. - def visit_case(node) - clauses = [node.consequent] - while clauses.last && !clauses.last.is_a?(Else) - clauses << clauses.last.consequent + # Visit a CVar node. + def visit_cvar(node) + s( + :cvar, + [node.value.to_sym], + smap_variable(srange_node(node), srange_node(node)) + ) end - else_token = - if clauses.last.is_a?(Else) - srange_length(clauses.last.start_char, 4) - end + # Visit a DefNode node. + def visit_def(node) + name = node.name.value.to_sym + args = + case node.params + when Params + child = visit(node.params) - s( - node.consequent.is_a?(In) ? :case_match : :case, - [visit(node.value)] + clauses.map { |clause| visit(clause) }, - smap_condition( - srange_length(node.start_char, 4), - nil, - else_token, - srange_length(node.end_char, -3), - srange_node(node) - ) - ) - end + s( + child.type, + child.children, + smap_collection_bare(child.location&.expression) + ) + when Paren + child = visit(node.params.contents) - # Visit a CHAR node. - def visit_CHAR(node) - s( - :str, - [node.value[1..]], - smap_collection( - srange_length(node.start_char, 1), - nil, - srange_node(node) - ) - ) - end + s( + child.type, + child.children, + smap_collection( + srange_length(node.params.start_char, 1), + srange_length(node.params.end_char, -1), + srange_node(node.params) + ) + ) + else + s(:args, [], smap_collection_bare(nil)) + end - # Visit a ClassDeclaration node. - def visit_class(node) - operator = - if node.superclass - srange_find_between(node.constant, node.superclass, "<") + location = + if node.endless? + smap_method_definition( + srange_length(node.start_char, 3), + nil, + srange_node(node.name), + nil, + srange_find_between( + (node.params || node.name), + node.bodystmt, + "=" + ), + srange_node(node) + ) + else + smap_method_definition( + srange_length(node.start_char, 3), + nil, + srange_node(node.name), + srange_length(node.end_char, -3), + nil, + srange_node(node) + ) + end + + if node.target + target = + node.target.is_a?(Paren) ? node.target.contents : node.target + + s( + :defs, + [visit(target), name, args, visit(node.bodystmt)], + smap_method_definition( + location.keyword, + srange_node(node.operator), + location.name, + location.end, + location.assignment, + location.expression + ) + ) + else + s(:def, [name, args, visit(node.bodystmt)], location) end + end - s( - :class, - [visit(node.constant), visit(node.superclass), visit(node.bodystmt)], - smap_definition( - srange_length(node.start_char, 5), - operator, - srange_node(node.constant), - srange_length(node.end_char, -3) - ).with_expression(srange_node(node)) - ) - end + # Visit a Defined node. + def visit_defined(node) + paren_range = (node.start_char + 8)...node.end_char + begin_token, end_token = + if buffer.source[paren_range].include?("(") + [ + srange_find(paren_range.begin, paren_range.end, "("), + srange_length(node.end_char, -1) + ] + end - # Visit a Command node. - def visit_command(node) - visit_command_call( - CommandCall.new( - receiver: nil, - operator: nil, - message: node.message, - arguments: node.arguments, - block: node.block, - location: node.location + s( + :defined?, + [visit(node.value)], + smap_keyword( + srange_length(node.start_char, 8), + begin_token, + end_token, + srange_node(node) + ) ) - ) - end + end - # Visit a CommandCall node. - def visit_command_call(node) - children = [ - visit(node.receiver), - node.message == :call ? :call : node.message.value.to_sym - ] - - begin_token = nil - end_token = nil - - case node.arguments - when Args - children += visit_all(node.arguments.parts) - when ArgParen - case node.arguments.arguments - when nil - # skip - when ArgsForward - children << visit(node.arguments.arguments) + # Visit a DynaSymbol node. + def visit_dyna_symbol(node) + location = + if node.quote + smap_collection( + srange_length(node.start_char, node.quote.length), + srange_length(node.end_char, -1), + srange_node(node) + ) + else + smap_collection_bare(srange_node(node)) + end + + if node.parts.length == 1 && node.parts.first.is_a?(TStringContent) + s(:sym, ["\"#{node.parts.first.value}\"".undump.to_sym], location) else - children += visit_all(node.arguments.arguments.parts) + s(:dsym, visit_all(node.parts), location) end - - begin_token = srange_length(node.arguments.start_char, 1) - end_token = srange_length(node.arguments.end_char, -1) end - dot_bound = - if node.arguments - node.arguments.start_char - elsif node.block - node.block.start_char + # Visit an Else node. + def visit_else(node) + if node.statements.empty? && stack[-2].is_a?(Case) + s(:empty_else, [], nil) else - node.end_char + visit(node.statements) end + end - expression = - if node.arguments.is_a?(ArgParen) - srange(node.start_char, node.arguments.end_char) - elsif node.arguments.is_a?(Args) && node.arguments.parts.any? - last_part = node.arguments.parts.last - end_char = - if last_part.is_a?(Heredoc) - last_part.beginning.end_char - else - last_part.end_char - end + # Visit an Elsif node. + def visit_elsif(node) + else_token = + case node.consequent + when Elsif + srange_length(node.consequent.start_char, 5) + when Else + srange_length(node.consequent.start_char, 4) + end - srange(node.start_char, end_char) - elsif node.block - srange_node(node.message) - else - srange_node(node) - end + expression = srange(node.start_char, node.statements.end_char - 1) - call = s( - if node.operator.is_a?(Op) && node.operator.value == "&." - :csend - else - :send - end, - children, - smap_send( - if node.operator == :"::" - srange_find( - node.receiver.end_char, - if node.message == :call - dot_bound - else - node.message.start_char - end, - "::" - ) - elsif node.operator - srange_node(node.operator) - end, - node.message == :call ? nil : srange_node(node.message), - begin_token, - end_token, + :if, + [ + visit(node.predicate), + visit(node.statements), + visit(node.consequent) + ], + smap_condition( + srange_length(node.start_char, 5), + nil, + else_token, + nil, expression ) ) + end - if node.block - type, arguments = block_children(node.block) - + # Visit an ENDBlock node. + def visit_END(node) s( - type, - [call, arguments, visit(node.block.bodystmt)], - smap_collection( - srange_node(node.block.opening), - srange_length( - node.end_char, - node.block.opening.is_a?(Kw) ? -3 : -1 - ), + :postexe, + [visit(node.statements)], + smap_keyword( + srange_length(node.start_char, 3), + srange_find(node.start_char + 3, node.statements.start_char, "{"), + srange_length(node.end_char, -1), srange_node(node) ) ) - else - call end - end - # Visit a Const node. - def visit_const(node) - s( - :const, - [nil, node.value.to_sym], - smap_constant(nil, srange_node(node), srange_node(node)) - ) - end + # Visit an Ensure node. + def visit_ensure(node) + start_char = node.start_char + end_char = + if node.statements.empty? + start_char + 6 + else + node.statements.body.last.end_char + end - # Visit a ConstPathField node. - def visit_const_path_field(node) - if node.parent.is_a?(VarRef) && node.parent.value.is_a?(Kw) && - node.parent.value.value == "self" && node.constant.is_a?(Ident) - s(:send, [visit(node.parent), :"#{node.constant.value}="], nil) - else s( - :casgn, - [visit(node.parent), node.constant.value.to_sym], - smap_constant( - srange_find_between(node.parent, node.constant, "::"), - srange_node(node.constant), - srange_node(node) + :ensure, + [visit(node.statements)], + smap_condition( + srange_length(start_char, 6), + nil, + nil, + nil, + srange(start_char, end_char) ) ) end - end - # Visit a ConstPathRef node. - def visit_const_path_ref(node) - s( - :const, - [visit(node.parent), node.constant.value.to_sym], - smap_constant( - srange_find_between(node.parent, node.constant, "::"), - srange_node(node.constant), - srange_node(node) + # Visit a Field node. + def visit_field(node) + message = + case stack[-2] + when Assign, MLHS + Ident.new( + value: "#{node.name.value}=", + location: node.name.location + ) + else + node.name + end + + visit_command_call( + CommandCall.new( + receiver: node.parent, + operator: node.operator, + message: message, + arguments: nil, + block: nil, + location: node.location + ) ) - ) - end + end - # Visit a ConstRef node. - def visit_const_ref(node) - s( - :const, - [nil, node.constant.value.to_sym], - smap_constant(nil, srange_node(node.constant), srange_node(node)) - ) - end + # Visit a FloatLiteral node. + def visit_float(node) + operator = + if %w[+ -].include?(buffer.source[node.start_char]) + srange_length(node.start_char, 1) + end - # Visit a CVar node. - def visit_cvar(node) - s( - :cvar, - [node.value.to_sym], - smap_variable(srange_node(node), srange_node(node)) - ) - end + s( + :float, + [node.value.to_f], + smap_operator(operator, srange_node(node)) + ) + end - # Visit a DefNode node. - def visit_def(node) - name = node.name.value.to_sym - args = - case node.params - when Params - child = visit(node.params) + # Visit a FndPtn node. + def visit_fndptn(node) + left, right = + [node.left, node.right].map do |child| + location = + smap_operator( + srange_length(child.start_char, 1), + srange_node(child) + ) - s( - child.type, - child.children, - smap_collection_bare(child.location&.expression) - ) - when Paren - child = visit(node.params.contents) + if child.is_a?(VarField) && child.value.nil? + s(:match_rest, [], location) + else + s(:match_rest, [visit(child)], location) + end + end + inner = s( - child.type, - child.children, + :find_pattern, + [left, *visit_all(node.values), right], smap_collection( - srange_length(node.params.start_char, 1), - srange_length(node.params.end_char, -1), - srange_node(node.params) + srange_length(node.start_char, 1), + srange_length(node.end_char, -1), + srange_node(node) ) ) + + if node.constant + s(:const_pattern, [visit(node.constant), inner], nil) else - s(:args, [], smap_collection_bare(nil)) + inner end + end - location = - if node.endless? - smap_method_definition( - srange_length(node.start_char, 3), - nil, - srange_node(node.name), - nil, - srange_find_between( - (node.params || node.name), - node.bodystmt, - "=" - ), - srange_node(node) - ) - else - smap_method_definition( + # Visit a For node. + def visit_for(node) + s( + :for, + [visit(node.index), visit(node.collection), visit(node.statements)], + smap_for( srange_length(node.start_char, 3), - nil, - srange_node(node.name), + srange_find_between(node.index, node.collection, "in"), + srange_search_between(node.collection, node.statements, "do") || + srange_search_between(node.collection, node.statements, ";"), srange_length(node.end_char, -3), - nil, srange_node(node) ) - end - - if node.target - target = node.target.is_a?(Paren) ? node.target.contents : node.target - - s( - :defs, - [visit(target), name, args, visit(node.bodystmt)], - smap_method_definition( - location.keyword, - srange_node(node.operator), - location.name, - location.end, - location.assignment, - location.expression - ) ) - else - s(:def, [name, args, visit(node.bodystmt)], location) end - end - # Visit a Defined node. - def visit_defined(node) - paren_range = (node.start_char + 8)...node.end_char - begin_token, end_token = - if buffer.source[paren_range].include?("(") - [ - srange_find(paren_range.begin, paren_range.end, "("), - srange_length(node.end_char, -1) - ] - end - - s( - :defined?, - [visit(node.value)], - smap_keyword( - srange_length(node.start_char, 8), - begin_token, - end_token, - srange_node(node) + # Visit a GVar node. + def visit_gvar(node) + s( + :gvar, + [node.value.to_sym], + smap_variable(srange_node(node), srange_node(node)) ) - ) - end + end - # Visit a DynaSymbol node. - def visit_dyna_symbol(node) - location = - if node.quote + # Visit a HashLiteral node. + def visit_hash(node) + s( + :hash, + visit_all(node.assocs), smap_collection( - srange_length(node.start_char, node.quote.length), + srange_length(node.start_char, 1), srange_length(node.end_char, -1), srange_node(node) ) - else - smap_collection_bare(srange_node(node)) - end - - if node.parts.length == 1 && node.parts.first.is_a?(TStringContent) - s(:sym, ["\"#{node.parts.first.value}\"".undump.to_sym], location) - else - s(:dsym, visit_all(node.parts), location) + ) end - end - # Visit an Else node. - def visit_else(node) - if node.statements.empty? && stack[-2].is_a?(Case) - s(:empty_else, [], nil) - else - visit(node.statements) - end - end + # Visit a Heredoc node. + def visit_heredoc(node) + heredoc = HeredocBuilder.new(node) - # Visit an Elsif node. - def visit_elsif(node) - else_token = - case node.consequent - when Elsif - srange_length(node.consequent.start_char, 5) - when Else - srange_length(node.consequent.start_char, 4) - end + # For each part of the heredoc, if it's a string content node, split + # it into multiple string content nodes, one for each line. Otherwise, + # visit the node as normal. + node.parts.each do |part| + if part.is_a?(TStringContent) && part.value.count("\n") > 1 + index = part.start_char + lines = part.value.split("\n") - expression = srange(node.start_char, node.statements.end_char - 1) - - s( - :if, - [ - visit(node.predicate), - visit(node.statements), - visit(node.consequent) - ], - smap_condition( - srange_length(node.start_char, 5), - nil, - else_token, - nil, - expression - ) - ) - end + lines.each do |line| + length = line.length + 1 + location = smap_collection_bare(srange_length(index, length)) - # Visit an ENDBlock node. - def visit_END(node) - s( - :postexe, - [visit(node.statements)], - smap_keyword( - srange_length(node.start_char, 3), - srange_find(node.start_char + 3, node.statements.start_char, "{"), - srange_length(node.end_char, -1), - srange_node(node) - ) - ) - end + heredoc << s(:str, ["#{line}\n"], location) + index += length + end + else + heredoc << visit(part) + end + end - # Visit an Ensure node. - def visit_ensure(node) - start_char = node.start_char - end_char = - if node.statements.empty? - start_char + 6 + # Now that we have all of the pieces on the heredoc, we can trim it if + # it is a heredoc that supports trimming (i.e., it has a ~ on the + # declaration). + heredoc.trim! + + # Generate the location for the heredoc, which goes from the + # declaration to the ending delimiter. + location = + smap_heredoc( + srange_node(node.beginning), + srange( + if node.parts.empty? + node.beginning.end_char + 1 + else + node.parts.first.start_char + end, + node.ending.start_char + ), + srange(node.ending.start_char, node.ending.end_char - 1) + ) + + # Finally, decide which kind of heredoc node to generate based on its + # declaration and contents. + if node.beginning.value.match?(/`\w+`\z/) + s(:xstr, heredoc.segments, location) + elsif heredoc.segments.length == 1 + segment = heredoc.segments.first + s(segment.type, segment.children, location) else - node.statements.body.last.end_char + s(:dstr, heredoc.segments, location) end + end - s( - :ensure, - [visit(node.statements)], - smap_condition( - srange_length(start_char, 6), - nil, - nil, - nil, - srange(start_char, end_char) - ) - ) - end + # Visit a HshPtn node. + def visit_hshptn(node) + children = + node.keywords.map do |(keyword, value)| + next s(:pair, [visit(keyword), visit(value)], nil) if value + + case keyword + when DynaSymbol + raise if keyword.parts.length > 1 + s(:match_var, [keyword.parts.first.value.to_sym], nil) + when Label + s(:match_var, [keyword.value.chomp(":").to_sym], nil) + end + end - # Visit a Field node. - def visit_field(node) - message = - case stack[-2] - when Assign, MLHS - Ident.new( - value: "#{node.name.value}=", - location: node.name.location - ) + if node.keyword_rest.is_a?(VarField) + children << if node.keyword_rest.value.nil? + s(:match_rest, [], nil) + elsif node.keyword_rest.value == :nil + s(:match_nil_pattern, [], nil) + else + s(:match_rest, [visit(node.keyword_rest)], nil) + end + end + + inner = s(:hash_pattern, children, nil) + if node.constant + s(:const_pattern, [visit(node.constant), inner], nil) else - node.name + inner end + end - visit_command_call( - CommandCall.new( - receiver: node.parent, - operator: node.operator, - message: message, - arguments: nil, - block: nil, - location: node.location + # Visit an Ident node. + def visit_ident(node) + s( + :lvar, + [node.value.to_sym], + smap_variable(srange_node(node), srange_node(node)) ) - ) - end - - # Visit a FloatLiteral node. - def visit_float(node) - operator = - if %w[+ -].include?(buffer.source[node.start_char]) - srange_length(node.start_char, 1) - end + end - s(:float, [node.value.to_f], smap_operator(operator, srange_node(node))) - end + # Visit an IfNode node. + def visit_if(node) + predicate = + case node.predicate + when RangeNode + type = + node.predicate.operator.value == ".." ? :iflipflop : :eflipflop + s(type, visit(node.predicate).children, nil) + when RegexpLiteral + s(:match_current_line, [visit(node.predicate)], nil) + when Unary + if node.predicate.operator.value == "!" && + node.predicate.statement.is_a?(RegexpLiteral) + s( + :send, + [ + s(:match_current_line, [visit(node.predicate.statement)]), + :! + ], + nil + ) + else + visit(node.predicate) + end + else + visit(node.predicate) + end - # Visit a FndPtn node. - def visit_fndptn(node) - left, right = - [node.left, node.right].map do |child| - location = - smap_operator( - srange_length(child.start_char, 1), - srange_node(child) + s( + :if, + [predicate, visit(node.statements), visit(node.consequent)], + if node.modifier? + smap_keyword_bare( + srange_find_between(node.statements, node.predicate, "if"), + srange_node(node) ) - - if child.is_a?(VarField) && child.value.nil? - s(:match_rest, [], location) else - s(:match_rest, [visit(child)], location) + begin_start = node.predicate.end_char + begin_end = + if node.statements.empty? + node.statements.end_char + else + node.statements.body.first.start_char + end + + begin_token = + if buffer.source[begin_start...begin_end].include?("then") + srange_find(begin_start, begin_end, "then") + elsif buffer.source[begin_start...begin_end].include?(";") + srange_find(begin_start, begin_end, ";") + end + + else_token = + case node.consequent + when Elsif + srange_length(node.consequent.start_char, 5) + when Else + srange_length(node.consequent.start_char, 4) + end + + smap_condition( + srange_length(node.start_char, 2), + begin_token, + else_token, + srange_length(node.end_char, -3), + srange_node(node) + ) end - end + ) + end - inner = + # Visit an IfOp node. + def visit_if_op(node) s( - :find_pattern, - [left, *visit_all(node.values), right], - smap_collection( - srange_length(node.start_char, 1), - srange_length(node.end_char, -1), + :if, + [visit(node.predicate), visit(node.truthy), visit(node.falsy)], + smap_ternary( + srange_find_between(node.predicate, node.truthy, "?"), + srange_find_between(node.truthy, node.falsy, ":"), srange_node(node) ) ) - - if node.constant - s(:const_pattern, [visit(node.constant), inner], nil) - else - inner end - end - - # Visit a For node. - def visit_for(node) - s( - :for, - [visit(node.index), visit(node.collection), visit(node.statements)], - smap_for( - srange_length(node.start_char, 3), - srange_find_between(node.index, node.collection, "in"), - srange_search_between(node.collection, node.statements, "do") || - srange_search_between(node.collection, node.statements, ";"), - srange_length(node.end_char, -3), - srange_node(node) - ) - ) - end - - # Visit a GVar node. - def visit_gvar(node) - s( - :gvar, - [node.value.to_sym], - smap_variable(srange_node(node), srange_node(node)) - ) - end - # Visit a HashLiteral node. - def visit_hash(node) - s( - :hash, - visit_all(node.assocs), - smap_collection( - srange_length(node.start_char, 1), - srange_length(node.end_char, -1), - srange_node(node) + # Visit an Imaginary node. + def visit_imaginary(node) + s( + :complex, + [ + # We have to do an eval here in order to get the value in case + # it's something like 42ri. to_c will not give the right value in + # that case. Maybe there's an API for this but I can't find it. + eval(node.value) + ], + smap_operator(nil, srange_node(node)) ) - ) - end - - # Visit a Heredoc node. - def visit_heredoc(node) - heredoc = HeredocBuilder.new(node) - - # For each part of the heredoc, if it's a string content node, split it - # into multiple string content nodes, one for each line. Otherwise, - # visit the node as normal. - node.parts.each do |part| - if part.is_a?(TStringContent) && part.value.count("\n") > 1 - index = part.start_char - lines = part.value.split("\n") - - lines.each do |line| - length = line.length + 1 - location = smap_collection_bare(srange_length(index, length)) - - heredoc << s(:str, ["#{line}\n"], location) - index += length - end - else - heredoc << visit(part) - end end - # Now that we have all of the pieces on the heredoc, we can trim it if - # it is a heredoc that supports trimming (i.e., it has a ~ on the - # declaration). - heredoc.trim! + # Visit an In node. + def visit_in(node) + case node.pattern + when IfNode + s( + :in_pattern, + [ + visit(node.pattern.statements), + s(:if_guard, [visit(node.pattern.predicate)], nil), + visit(node.statements) + ], + nil + ) + when UnlessNode + s( + :in_pattern, + [ + visit(node.pattern.statements), + s(:unless_guard, [visit(node.pattern.predicate)], nil), + visit(node.statements) + ], + nil + ) + else + begin_token = + srange_search_between(node.pattern, node.statements, "then") - # Generate the location for the heredoc, which goes from the declaration - # to the ending delimiter. - location = - smap_heredoc( - srange_node(node.beginning), - srange( - if node.parts.empty? - node.beginning.end_char + 1 + end_char = + if begin_token || node.statements.empty? + node.statements.end_char - 1 else - node.parts.first.start_char - end, - node.ending.start_char - ), - srange(node.ending.start_char, node.ending.end_char - 1) - ) + node.statements.body.last.start_char + end - # Finally, decide which kind of heredoc node to generate based on its - # declaration and contents. - if node.beginning.value.match?(/`\w+`\z/) - s(:xstr, heredoc.segments, location) - elsif heredoc.segments.length == 1 - segment = heredoc.segments.first - s(segment.type, segment.children, location) - else - s(:dstr, heredoc.segments, location) + s( + :in_pattern, + [visit(node.pattern), nil, visit(node.statements)], + smap_keyword( + srange_length(node.start_char, 2), + begin_token, + nil, + srange(node.start_char, end_char) + ) + ) + end end - end - # Visit a HshPtn node. - def visit_hshptn(node) - children = - node.keywords.map do |(keyword, value)| - next s(:pair, [visit(keyword), visit(value)], nil) if value - - case keyword - when DynaSymbol - raise if keyword.parts.length > 1 - s(:match_var, [keyword.parts.first.value.to_sym], nil) - when Label - s(:match_var, [keyword.value.chomp(":").to_sym], nil) + # Visit an Int node. + def visit_int(node) + operator = + if %w[+ -].include?(buffer.source[node.start_char]) + srange_length(node.start_char, 1) end - end - if node.keyword_rest.is_a?(VarField) - children << if node.keyword_rest.value.nil? - s(:match_rest, [], nil) - elsif node.keyword_rest.value == :nil - s(:match_nil_pattern, [], nil) - else - s(:match_rest, [visit(node.keyword_rest)], nil) - end + s(:int, [node.value.to_i], smap_operator(operator, srange_node(node))) end - inner = s(:hash_pattern, children, nil) - if node.constant - s(:const_pattern, [visit(node.constant), inner], nil) - else - inner + # Visit an IVar node. + def visit_ivar(node) + s( + :ivar, + [node.value.to_sym], + smap_variable(srange_node(node), srange_node(node)) + ) end - end - # Visit an Ident node. - def visit_ident(node) - s( - :lvar, - [node.value.to_sym], - smap_variable(srange_node(node), srange_node(node)) - ) - end + # Visit a Kw node. + def visit_kw(node) + location = smap(srange_node(node)) - # Visit an IfNode node. - def visit_if(node) - predicate = - case node.predicate - when RangeNode - type = - node.predicate.operator.value == ".." ? :iflipflop : :eflipflop - s(type, visit(node.predicate).children, nil) - when RegexpLiteral - s(:match_current_line, [visit(node.predicate)], nil) - when Unary - if node.predicate.operator.value == "!" && - node.predicate.statement.is_a?(RegexpLiteral) - s( - :send, - [s(:match_current_line, [visit(node.predicate.statement)]), :!], - nil - ) + case node.value + when "__FILE__" + s(:str, [buffer.name], location) + when "__LINE__" + s( + :int, + [node.location.start_line + buffer.first_line - 1], + location + ) + when "__ENCODING__" + if ::Parser::Builders::Default.emit_encoding + s(:__ENCODING__, [], location) else - visit(node.predicate) + s(:const, [s(:const, [nil, :Encoding], nil), :UTF_8], location) end else - visit(node.predicate) + s(node.value.to_sym, [], location) end + end - s( - :if, - [predicate, visit(node.statements), visit(node.consequent)], - if node.modifier? - smap_keyword_bare( - srange_find_between(node.statements, node.predicate, "if"), - srange_node(node) - ) + # Visit a KwRestParam node. + def visit_kwrest_param(node) + if node.name.nil? + s(:kwrestarg, [], smap_variable(nil, srange_node(node))) else - begin_start = node.predicate.end_char - begin_end = - if node.statements.empty? - node.statements.end_char - else - node.statements.body.first.start_char - end + s( + :kwrestarg, + [node.name.value.to_sym], + smap_variable(srange_node(node.name), srange_node(node)) + ) + end + end - begin_token = - if buffer.source[begin_start...begin_end].include?("then") - srange_find(begin_start, begin_end, "then") - elsif buffer.source[begin_start...begin_end].include?(";") - srange_find(begin_start, begin_end, ";") - end + # Visit a Label node. + def visit_label(node) + s( + :sym, + [node.value.chomp(":").to_sym], + smap_collection_bare(srange(node.start_char, node.end_char - 1)) + ) + end - else_token = - case node.consequent - when Elsif - srange_length(node.consequent.start_char, 5) - when Else - srange_length(node.consequent.start_char, 4) - end + # Visit a Lambda node. + def visit_lambda(node) + args = + node.params.is_a?(LambdaVar) ? node.params : node.params.contents + args_node = visit(args) - smap_condition( - srange_length(node.start_char, 2), - begin_token, - else_token, - srange_length(node.end_char, -3), - srange_node(node) - ) + type = :block + if args.empty? && (maximum = num_block_type(node.statements)) + type = :numblock + args_node = maximum end - ) - end - # Visit an IfOp node. - def visit_if_op(node) - s( - :if, - [visit(node.predicate), visit(node.truthy), visit(node.falsy)], - smap_ternary( - srange_find_between(node.predicate, node.truthy, "?"), - srange_find_between(node.truthy, node.falsy, ":"), - srange_node(node) - ) - ) - end + begin_token, end_token = + if ( + srange = + srange_search_between(node.params, node.statements, "{") + ) + [srange, srange_length(node.end_char, -1)] + else + [ + srange_find_between(node.params, node.statements, "do"), + srange_length(node.end_char, -3) + ] + end - # Visit an Imaginary node. - def visit_imaginary(node) - s( - :complex, - [ - # We have to do an eval here in order to get the value in case it's - # something like 42ri. to_c will not give the right value in that - # case. Maybe there's an API for this but I can't find it. - eval(node.value) - ], - smap_operator(nil, srange_node(node)) - ) - end + selector = srange_length(node.start_char, 2) - # Visit an In node. - def visit_in(node) - case node.pattern - when IfNode - s( - :in_pattern, - [ - visit(node.pattern.statements), - s(:if_guard, [visit(node.pattern.predicate)], nil), - visit(node.statements) - ], - nil - ) - when UnlessNode s( - :in_pattern, + type, [ - visit(node.pattern.statements), - s(:unless_guard, [visit(node.pattern.predicate)], nil), + if ::Parser::Builders::Default.emit_lambda + s(:lambda, [], smap(selector)) + else + s(:send, [nil, :lambda], smap_send_bare(selector, selector)) + end, + args_node, visit(node.statements) ], - nil + smap_collection(begin_token, end_token, srange_node(node)) ) - else - begin_token = - srange_search_between(node.pattern, node.statements, "then") + end - end_char = - if begin_token || node.statements.empty? - node.statements.end_char - 1 + # Visit a LambdaVar node. + def visit_lambda_var(node) + shadowargs = + node.locals.map do |local| + s( + :shadowarg, + [local.value.to_sym], + smap_variable(srange_node(local), srange_node(local)) + ) + end + + location = + if node.start_char == node.end_char + smap_collection_bare(nil) else - node.statements.body.last.start_char + smap_collection( + srange_length(node.start_char, 1), + srange_length(node.end_char, -1), + srange_node(node) + ) end + s(:args, visit(node.params).children + shadowargs, location) + end + + # Visit an MAssign node. + def visit_massign(node) s( - :in_pattern, - [visit(node.pattern), nil, visit(node.statements)], - smap_keyword( - srange_length(node.start_char, 2), - begin_token, - nil, - srange(node.start_char, end_char) + :masgn, + [visit(node.target), visit(node.value)], + smap_operator( + srange_find_between(node.target, node.value, "="), + srange_node(node) ) ) end - end - - # Visit an Int node. - def visit_int(node) - operator = - if %w[+ -].include?(buffer.source[node.start_char]) - srange_length(node.start_char, 1) - end - s(:int, [node.value.to_i], smap_operator(operator, srange_node(node))) - end + # Visit a MethodAddBlock node. + def visit_method_add_block(node) + case node.call + when Break, Next, ReturnNode + type, arguments = block_children(node.block) + call = visit(node.call) - # Visit an IVar node. - def visit_ivar(node) - s( - :ivar, - [node.value.to_sym], - smap_variable(srange_node(node), srange_node(node)) - ) - end + s( + call.type, + [ + s( + type, + [*call.children, arguments, visit(node.block.bodystmt)], + nil + ) + ], + nil + ) + when ARef, Super, ZSuper + type, arguments = block_children(node.block) - # Visit a Kw node. - def visit_kw(node) - location = smap(srange_node(node)) - - case node.value - when "__FILE__" - s(:str, [buffer.name], location) - when "__LINE__" - s(:int, [node.location.start_line + buffer.first_line - 1], location) - when "__ENCODING__" - if ::Parser::Builders::Default.emit_encoding - s(:__ENCODING__, [], location) + s( + type, + [visit(node.call), arguments, visit(node.block.bodystmt)], + nil + ) else - s(:const, [s(:const, [nil, :Encoding], nil), :UTF_8], location) + visit_command_call( + CommandCall.new( + receiver: node.call.receiver, + operator: node.call.operator, + message: node.call.message, + arguments: node.call.arguments, + block: node.block, + location: node.location + ) + ) end - else - s(node.value.to_sym, [], location) end - end - # Visit a KwRestParam node. - def visit_kwrest_param(node) - if node.name.nil? - s(:kwrestarg, [], smap_variable(nil, srange_node(node))) - else + # Visit an MLHS node. + def visit_mlhs(node) s( - :kwrestarg, - [node.name.value.to_sym], - smap_variable(srange_node(node.name), srange_node(node)) + :mlhs, + node.parts.map do |part| + if part.is_a?(Ident) + s( + :arg, + [part.value.to_sym], + smap_variable(srange_node(part), srange_node(part)) + ) + else + visit(part) + end + end, + smap_collection_bare(srange_node(node)) ) end - end - - # Visit a Label node. - def visit_label(node) - s( - :sym, - [node.value.chomp(":").to_sym], - smap_collection_bare(srange(node.start_char, node.end_char - 1)) - ) - end - - # Visit a Lambda node. - def visit_lambda(node) - args = node.params.is_a?(LambdaVar) ? node.params : node.params.contents - args_node = visit(args) - - type = :block - if args.empty? && (maximum = num_block_type(node.statements)) - type = :numblock - args_node = maximum - end - - begin_token, end_token = - if (srange = srange_search_between(node.params, node.statements, "{")) - [srange, srange_length(node.end_char, -1)] - else - [ - srange_find_between(node.params, node.statements, "do"), - srange_length(node.end_char, -3) - ] - end - - selector = srange_length(node.start_char, 2) - - s( - type, - [ - if ::Parser::Builders::Default.emit_lambda - s(:lambda, [], smap(selector)) - else - s(:send, [nil, :lambda], smap_send_bare(selector, selector)) - end, - args_node, - visit(node.statements) - ], - smap_collection(begin_token, end_token, srange_node(node)) - ) - end - # Visit a LambdaVar node. - def visit_lambda_var(node) - shadowargs = - node.locals.map do |local| - s( - :shadowarg, - [local.value.to_sym], - smap_variable(srange_node(local), srange_node(local)) - ) - end + # Visit an MLHSParen node. + def visit_mlhs_paren(node) + child = visit(node.contents) - location = - if node.start_char == node.end_char - smap_collection_bare(nil) - else + s( + child.type, + child.children, smap_collection( srange_length(node.start_char, 1), srange_length(node.end_char, -1), srange_node(node) ) - end - - s(:args, visit(node.params).children + shadowargs, location) - end - - # Visit an MAssign node. - def visit_massign(node) - s( - :masgn, - [visit(node.target), visit(node.value)], - smap_operator( - srange_find_between(node.target, node.value, "="), - srange_node(node) ) - ) - end - - # Visit a MethodAddBlock node. - def visit_method_add_block(node) - case node.call - when Break, Next, ReturnNode - type, arguments = block_children(node.block) - call = visit(node.call) + end + # Visit a ModuleDeclaration node. + def visit_module(node) s( - call.type, - [ - s( - type, - [*call.children, arguments, visit(node.block.bodystmt)], - nil - ) - ], - nil + :module, + [visit(node.constant), visit(node.bodystmt)], + smap_definition( + srange_length(node.start_char, 6), + nil, + srange_node(node.constant), + srange_length(node.end_char, -3) + ).with_expression(srange_node(node)) ) - when ARef, Super, ZSuper - type, arguments = block_children(node.block) + end - s( - type, - [visit(node.call), arguments, visit(node.block.bodystmt)], - nil - ) - else - visit_command_call( - CommandCall.new( - receiver: node.call.receiver, - operator: node.call.operator, - message: node.call.message, - arguments: node.call.arguments, - block: node.block, + # Visit an MRHS node. + def visit_mrhs(node) + visit_array( + ArrayLiteral.new( + lbracket: nil, + contents: Args.new(parts: node.parts, location: node.location), location: node.location ) ) end - end - - # Visit an MLHS node. - def visit_mlhs(node) - s( - :mlhs, - node.parts.map do |part| - if part.is_a?(Ident) - s( - :arg, - [part.value.to_sym], - smap_variable(srange_node(part), srange_node(part)) - ) - else - visit(part) - end - end, - smap_collection_bare(srange_node(node)) - ) - end - - # Visit an MLHSParen node. - def visit_mlhs_paren(node) - child = visit(node.contents) - - s( - child.type, - child.children, - smap_collection( - srange_length(node.start_char, 1), - srange_length(node.end_char, -1), - srange_node(node) - ) - ) - end - - # Visit a ModuleDeclaration node. - def visit_module(node) - s( - :module, - [visit(node.constant), visit(node.bodystmt)], - smap_definition( - srange_length(node.start_char, 6), - nil, - srange_node(node.constant), - srange_length(node.end_char, -3) - ).with_expression(srange_node(node)) - ) - end - - # Visit an MRHS node. - def visit_mrhs(node) - visit_array( - ArrayLiteral.new( - lbracket: nil, - contents: Args.new(parts: node.parts, location: node.location), - location: node.location - ) - ) - end - - # Visit a Next node. - def visit_next(node) - s( - :next, - visit_all(node.arguments.parts), - smap_keyword_bare( - srange_length(node.start_char, 4), - srange_node(node) - ) - ) - end - - # Visit a Not node. - def visit_not(node) - if node.statement.nil? - begin_token = srange_find(node.start_char, nil, "(") - end_token = srange_find(node.start_char, nil, ")") - - s( - :send, - [ - s( - :begin, - [], - smap_collection( - begin_token, - end_token, - begin_token.join(end_token) - ) - ), - :! - ], - smap_send_bare(srange_length(node.start_char, 3), srange_node(node)) - ) - else - begin_token, end_token = - if node.parentheses? - [ - srange_find( - node.start_char + 3, - node.statement.start_char, - "(" - ), - srange_length(node.end_char, -1) - ] - end + # Visit a Next node. + def visit_next(node) s( - :send, - [visit(node.statement), :!], - smap_send( - nil, - srange_length(node.start_char, 3), - begin_token, - end_token, + :next, + visit_all(node.arguments.parts), + smap_keyword_bare( + srange_length(node.start_char, 4), srange_node(node) ) ) end - end - - # Visit an OpAssign node. - def visit_opassign(node) - target = visit(node.target) - location = - target - .location - .with_expression(srange_node(node)) - .with_operator(srange_node(node.operator)) - - case node.operator.value - when "||=" - s(:or_asgn, [target, visit(node.value)], location) - when "&&=" - s(:and_asgn, [target, visit(node.value)], location) - else - s( - :op_asgn, - [target, node.operator.value.chomp("=").to_sym, visit(node.value)], - location - ) - end - end - # Visit a Params node. - def visit_params(node) - children = [] + # Visit a Not node. + def visit_not(node) + if node.statement.nil? + begin_token = srange_find(node.start_char, nil, "(") + end_token = srange_find(node.start_char, nil, ")") - children += - node.requireds.map do |required| - case required - when MLHSParen - visit(required) - else - s( - :arg, - [required.value.to_sym], - smap_variable(srange_node(required), srange_node(required)) + s( + :send, + [ + s( + :begin, + [], + smap_collection( + begin_token, + end_token, + begin_token.join(end_token) + ) + ), + :! + ], + smap_send_bare( + srange_length(node.start_char, 3), + srange_node(node) ) - end - end + ) + else + begin_token, end_token = + if node.parentheses? + [ + srange_find( + node.start_char + 3, + node.statement.start_char, + "(" + ), + srange_length(node.end_char, -1) + ] + end - children += - node.optionals.map do |(name, value)| s( - :optarg, - [name.value.to_sym, visit(value)], - smap_variable( - srange_node(name), - srange_node(name).join(srange_node(value)) - ).with_operator(srange_find_between(name, value, "=")) + :send, + [visit(node.statement), :!], + smap_send( + nil, + srange_length(node.start_char, 3), + begin_token, + end_token, + srange_node(node) + ) ) end - - if node.rest && !node.rest.is_a?(ExcessedComma) - children << visit(node.rest) end - children += - node.posts.map do |post| + # Visit an OpAssign node. + def visit_opassign(node) + target = visit(node.target) + location = + target + .location + .with_expression(srange_node(node)) + .with_operator(srange_node(node.operator)) + + case node.operator.value + when "||=" + s(:or_asgn, [target, visit(node.value)], location) + when "&&=" + s(:and_asgn, [target, visit(node.value)], location) + else s( - :arg, - [post.value.to_sym], - smap_variable(srange_node(post), srange_node(post)) + :op_asgn, + [ + target, + node.operator.value.chomp("=").to_sym, + visit(node.value) + ], + location ) end + end - children += - node.keywords.map do |(name, value)| - key = name.value.chomp(":").to_sym + # Visit a Params node. + def visit_params(node) + children = [] - if value - s( - :kwoptarg, - [key, visit(value)], - smap_variable( - srange(name.start_char, name.end_char - 1), - srange_node(name).join(srange_node(value)) + children += + node.requireds.map do |required| + case required + when MLHSParen + visit(required) + else + s( + :arg, + [required.value.to_sym], + smap_variable(srange_node(required), srange_node(required)) ) - ) - else + end + end + + children += + node.optionals.map do |(name, value)| s( - :kwarg, - [key], + :optarg, + [name.value.to_sym, visit(value)], smap_variable( - srange(name.start_char, name.end_char - 1), - srange_node(name) - ) + srange_node(name), + srange_node(name).join(srange_node(value)) + ).with_operator(srange_find_between(name, value, "=")) ) end + + if node.rest && !node.rest.is_a?(ExcessedComma) + children << visit(node.rest) end - case node.keyword_rest - when nil, ArgsForward - # do nothing - when :nil - children << s( - :kwnilarg, - [], - smap_variable(srange_length(node.end_char, -3), srange_node(node)) - ) - else - children << visit(node.keyword_rest) - end + children += + node.posts.map do |post| + s( + :arg, + [post.value.to_sym], + smap_variable(srange_node(post), srange_node(post)) + ) + end - children << visit(node.block) if node.block + children += + node.keywords.map do |(name, value)| + key = name.value.chomp(":").to_sym - if node.keyword_rest.is_a?(ArgsForward) - location = smap(srange_node(node.keyword_rest)) + if value + s( + :kwoptarg, + [key, visit(value)], + smap_variable( + srange(name.start_char, name.end_char - 1), + srange_node(name).join(srange_node(value)) + ) + ) + else + s( + :kwarg, + [key], + smap_variable( + srange(name.start_char, name.end_char - 1), + srange_node(name) + ) + ) + end + end - # If there are no other arguments and we have the emit_forward_arg - # option enabled, then the entire argument list is represented by a - # single forward_args node. - if children.empty? && !::Parser::Builders::Default.emit_forward_arg - return s(:forward_args, [], location) + case node.keyword_rest + when nil, ArgsForward + # do nothing + when :nil + children << s( + :kwnilarg, + [], + smap_variable(srange_length(node.end_char, -3), srange_node(node)) + ) + else + children << visit(node.keyword_rest) end - # Otherwise, we need to insert a forward_arg node into the list of - # parameters before any keyword rest or block parameters. - index = - node.requireds.length + node.optionals.length + node.keywords.length - children.insert(index, s(:forward_arg, [], location)) - end + children << visit(node.block) if node.block - location = - unless children.empty? - first = children.first.location.expression - last = children.last.location.expression - smap_collection_bare(first.join(last)) + if node.keyword_rest.is_a?(ArgsForward) + location = smap(srange_node(node.keyword_rest)) + + # If there are no other arguments and we have the emit_forward_arg + # option enabled, then the entire argument list is represented by a + # single forward_args node. + if children.empty? && !::Parser::Builders::Default.emit_forward_arg + return s(:forward_args, [], location) + end + + # Otherwise, we need to insert a forward_arg node into the list of + # parameters before any keyword rest or block parameters. + index = + node.requireds.length + node.optionals.length + + node.keywords.length + children.insert(index, s(:forward_arg, [], location)) end - s(:args, children, location) - end + location = + unless children.empty? + first = children.first.location.expression + last = children.last.location.expression + smap_collection_bare(first.join(last)) + end - # Visit a Paren node. - def visit_paren(node) - location = - smap_collection( - srange_length(node.start_char, 1), - srange_length(node.end_char, -1), - srange_node(node) - ) + s(:args, children, location) + end - if node.contents.nil? || - (node.contents.is_a?(Statements) && node.contents.empty?) - s(:begin, [], location) - else - child = visit(node.contents) - child.type == :begin ? child : s(:begin, [child], location) + # Visit a Paren node. + def visit_paren(node) + location = + smap_collection( + srange_length(node.start_char, 1), + srange_length(node.end_char, -1), + srange_node(node) + ) + + if node.contents.nil? || + (node.contents.is_a?(Statements) && node.contents.empty?) + s(:begin, [], location) + else + child = visit(node.contents) + child.type == :begin ? child : s(:begin, [child], location) + end end - end - # Visit a PinnedBegin node. - def visit_pinned_begin(node) - s( - :pin, - [ - s( - :begin, - [visit(node.statement)], - smap_collection( - srange_length(node.start_char + 1, 1), - srange_length(node.end_char, -1), - srange(node.start_char + 1, node.end_char) + # Visit a PinnedBegin node. + def visit_pinned_begin(node) + s( + :pin, + [ + s( + :begin, + [visit(node.statement)], + smap_collection( + srange_length(node.start_char + 1, 1), + srange_length(node.end_char, -1), + srange(node.start_char + 1, node.end_char) + ) ) - ) - ], - smap_send_bare(srange_length(node.start_char, 1), srange_node(node)) - ) - end + ], + smap_send_bare(srange_length(node.start_char, 1), srange_node(node)) + ) + end - # Visit a PinnedVarRef node. - def visit_pinned_var_ref(node) - s( - :pin, - [visit(node.value)], - smap_send_bare(srange_length(node.start_char, 1), srange_node(node)) - ) - end + # Visit a PinnedVarRef node. + def visit_pinned_var_ref(node) + s( + :pin, + [visit(node.value)], + smap_send_bare(srange_length(node.start_char, 1), srange_node(node)) + ) + end - # Visit a Program node. - def visit_program(node) - visit(node.statements) - end + # Visit a Program node. + def visit_program(node) + visit(node.statements) + end - # Visit a QSymbols node. - def visit_qsymbols(node) - parts = - node.elements.map do |element| - SymbolLiteral.new(value: element, location: element.location) - end + # Visit a QSymbols node. + def visit_qsymbols(node) + parts = + node.elements.map do |element| + SymbolLiteral.new(value: element, location: element.location) + end - visit_array( - ArrayLiteral.new( - lbracket: node.beginning, - contents: Args.new(parts: parts, location: node.location), - location: node.location + visit_array( + ArrayLiteral.new( + lbracket: node.beginning, + contents: Args.new(parts: parts, location: node.location), + location: node.location + ) ) - ) - end + end - # Visit a QWords node. - def visit_qwords(node) - visit_array( - ArrayLiteral.new( - lbracket: node.beginning, - contents: Args.new(parts: node.elements, location: node.location), - location: node.location + # Visit a QWords node. + def visit_qwords(node) + visit_array( + ArrayLiteral.new( + lbracket: node.beginning, + contents: Args.new(parts: node.elements, location: node.location), + location: node.location + ) ) - ) - end + end - # Visit a RangeNode node. - def visit_range(node) - s( - node.operator.value == ".." ? :irange : :erange, - [visit(node.left), visit(node.right)], - smap_operator(srange_node(node.operator), srange_node(node)) - ) - end + # Visit a RangeNode node. + def visit_range(node) + s( + node.operator.value == ".." ? :irange : :erange, + [visit(node.left), visit(node.right)], + smap_operator(srange_node(node.operator), srange_node(node)) + ) + end - # Visit an RAssign node. - def visit_rassign(node) - s( - node.operator.value == "=>" ? :match_pattern : :match_pattern_p, - [visit(node.value), visit(node.pattern)], - smap_operator(srange_node(node.operator), srange_node(node)) - ) - end + # Visit an RAssign node. + def visit_rassign(node) + s( + node.operator.value == "=>" ? :match_pattern : :match_pattern_p, + [visit(node.value), visit(node.pattern)], + smap_operator(srange_node(node.operator), srange_node(node)) + ) + end - # Visit a Rational node. - def visit_rational(node) - s(:rational, [node.value.to_r], smap_operator(nil, srange_node(node))) - end + # Visit a Rational node. + def visit_rational(node) + s(:rational, [node.value.to_r], smap_operator(nil, srange_node(node))) + end - # Visit a Redo node. - def visit_redo(node) - s(:redo, [], smap_keyword_bare(srange_node(node), srange_node(node))) - end + # Visit a Redo node. + def visit_redo(node) + s(:redo, [], smap_keyword_bare(srange_node(node), srange_node(node))) + end - # Visit a RegexpLiteral node. - def visit_regexp_literal(node) - s( - :regexp, - visit_all(node.parts).push( - s( - :regopt, - node.ending.scan(/[a-z]/).sort.map(&:to_sym), - smap(srange_length(node.end_char, -(node.ending.length - 1))) + # Visit a RegexpLiteral node. + def visit_regexp_literal(node) + s( + :regexp, + visit_all(node.parts).push( + s( + :regopt, + node.ending.scan(/[a-z]/).sort.map(&:to_sym), + smap(srange_length(node.end_char, -(node.ending.length - 1))) + ) + ), + smap_collection( + srange_length(node.start_char, node.beginning.length), + srange_length(node.end_char - node.ending.length, 1), + srange_node(node) ) - ), - smap_collection( - srange_length(node.start_char, node.beginning.length), - srange_length(node.end_char - node.ending.length, 1), - srange_node(node) ) - ) - end + end - # Visit a Rescue node. - def visit_rescue(node) - # In the parser gem, there is a separation between the rescue node and - # the rescue body. They have different bounds, so we have to calculate - # those here. - start_char = node.start_char + # Visit a Rescue node. + def visit_rescue(node) + # In the parser gem, there is a separation between the rescue node and + # the rescue body. They have different bounds, so we have to calculate + # those here. + start_char = node.start_char - body_end_char = - if node.statements.empty? - start_char + 6 - else - node.statements.body.last.end_char - end + body_end_char = + if node.statements.empty? + start_char + 6 + else + node.statements.body.last.end_char + end - end_char = - if node.consequent - end_node = node.consequent - end_node = end_node.consequent while end_node.consequent + end_char = + if node.consequent + end_node = node.consequent + end_node = end_node.consequent while end_node.consequent - if end_node.statements.empty? - start_char + 6 + if end_node.statements.empty? + start_char + 6 + else + end_node.statements.body.last.end_char + end else - end_node.statements.body.last.end_char + body_end_char end - else - body_end_char - end - # These locations are reused for multiple children. - keyword = srange_length(start_char, 6) - body_expression = srange(start_char, body_end_char) - expression = srange(start_char, end_char) + # These locations are reused for multiple children. + keyword = srange_length(start_char, 6) + body_expression = srange(start_char, body_end_char) + expression = srange(start_char, end_char) - exceptions = - case node.exception&.exceptions - when nil - nil - when MRHS - visit_array( - ArrayLiteral.new( - lbracket: nil, - contents: - Args.new( - parts: node.exception.exceptions.parts, - location: node.exception.exceptions.location - ), - location: node.exception.exceptions.location + exceptions = + case node.exception&.exceptions + when nil + nil + when MRHS + visit_array( + ArrayLiteral.new( + lbracket: nil, + contents: + Args.new( + parts: node.exception.exceptions.parts, + location: node.exception.exceptions.location + ), + location: node.exception.exceptions.location + ) ) - ) - else - visit_array( - ArrayLiteral.new( - lbracket: nil, - contents: - Args.new( - parts: [node.exception.exceptions], - location: node.exception.exceptions.location + else + visit_array( + ArrayLiteral.new( + lbracket: nil, + contents: + Args.new( + parts: [node.exception.exceptions], + location: node.exception.exceptions.location + ), + location: node.exception.exceptions.location + ) + ) + end + + resbody = + if node.exception.nil? + s( + :resbody, + [nil, nil, visit(node.statements)], + smap_rescue_body(keyword, nil, nil, body_expression) + ) + elsif node.exception.variable.nil? + s( + :resbody, + [exceptions, nil, visit(node.statements)], + smap_rescue_body(keyword, nil, nil, body_expression) + ) + else + s( + :resbody, + [ + exceptions, + visit(node.exception.variable), + visit(node.statements) + ], + smap_rescue_body( + keyword, + srange_find( + node.start_char + 6, + node.exception.variable.start_char, + "=>" ), - location: node.exception.exceptions.location + nil, + body_expression + ) ) - ) - end + end - resbody = - if node.exception.nil? - s( - :resbody, - [nil, nil, visit(node.statements)], - smap_rescue_body(keyword, nil, nil, body_expression) - ) - elsif node.exception.variable.nil? - s( - :resbody, - [exceptions, nil, visit(node.statements)], - smap_rescue_body(keyword, nil, nil, body_expression) - ) + children = [resbody] + if node.consequent + children += visit(node.consequent).children else - s( - :resbody, - [ - exceptions, - visit(node.exception.variable), - visit(node.statements) - ], - smap_rescue_body( - keyword, - srange_find( - node.start_char + 6, - node.exception.variable.start_char, - "=>" - ), - nil, - body_expression - ) - ) + children << nil end - children = [resbody] - if node.consequent - children += visit(node.consequent).children - else - children << nil + s(:rescue, children, smap_condition_bare(expression)) end - s(:rescue, children, smap_condition_bare(expression)) - end - - # Visit a RescueMod node. - def visit_rescue_mod(node) - keyword = srange_find_between(node.statement, node.value, "rescue") - - s( - :rescue, - [ - visit(node.statement), - s( - :resbody, - [nil, nil, visit(node.value)], - smap_rescue_body( - keyword, - nil, - nil, - keyword.join(srange_node(node.value)) - ) - ), - nil - ], - smap_condition_bare(srange_node(node)) - ) - end + # Visit a RescueMod node. + def visit_rescue_mod(node) + keyword = srange_find_between(node.statement, node.value, "rescue") - # Visit a RestParam node. - def visit_rest_param(node) - if node.name s( - :restarg, - [node.name.value.to_sym], - smap_variable(srange_node(node.name), srange_node(node)) + :rescue, + [ + visit(node.statement), + s( + :resbody, + [nil, nil, visit(node.value)], + smap_rescue_body( + keyword, + nil, + nil, + keyword.join(srange_node(node.value)) + ) + ), + nil + ], + smap_condition_bare(srange_node(node)) ) - else - s(:restarg, [], smap_variable(nil, srange_node(node))) end - end - - # Visit a Retry node. - def visit_retry(node) - s(:retry, [], smap_keyword_bare(srange_node(node), srange_node(node))) - end - - # Visit a ReturnNode node. - def visit_return(node) - s( - :return, - node.arguments ? visit_all(node.arguments.parts) : [], - smap_keyword_bare( - srange_length(node.start_char, 6), - srange_node(node) - ) - ) - end - # Visit an SClass node. - def visit_sclass(node) - s( - :sclass, - [visit(node.target), visit(node.bodystmt)], - smap_definition( - srange_length(node.start_char, 5), - srange_find(node.start_char + 5, node.target.start_char, "<<"), - nil, - srange_length(node.end_char, -3) - ).with_expression(srange_node(node)) - ) - end - - # Visit a Statements node. - def visit_statements(node) - children = - node.body.reject do |child| - child.is_a?(Comment) || child.is_a?(EmbDoc) || - child.is_a?(EndContent) || child.is_a?(VoidStmt) + # Visit a RestParam node. + def visit_rest_param(node) + if node.name + s( + :restarg, + [node.name.value.to_sym], + smap_variable(srange_node(node.name), srange_node(node)) + ) + else + s(:restarg, [], smap_variable(nil, srange_node(node))) end + end - case children.length - when 0 - nil - when 1 - visit(children.first) - else + # Visit a Retry node. + def visit_retry(node) + s(:retry, [], smap_keyword_bare(srange_node(node), srange_node(node))) + end + + # Visit a ReturnNode node. + def visit_return(node) s( - :begin, - visit_all(children), - smap_collection_bare( - srange(children.first.start_char, children.last.end_char) + :return, + node.arguments ? visit_all(node.arguments.parts) : [], + smap_keyword_bare( + srange_length(node.start_char, 6), + srange_node(node) ) ) end - end - - # Visit a StringConcat node. - def visit_string_concat(node) - s( - :dstr, - [visit(node.left), visit(node.right)], - smap_collection_bare(srange_node(node)) - ) - end - # Visit a StringDVar node. - def visit_string_dvar(node) - visit(node.variable) - end - - # Visit a StringEmbExpr node. - def visit_string_embexpr(node) - s( - :begin, - visit(node.statements).then { |child| child ? [child] : [] }, - smap_collection( - srange_length(node.start_char, 2), - srange_length(node.end_char, -1), - srange_node(node) + # Visit an SClass node. + def visit_sclass(node) + s( + :sclass, + [visit(node.target), visit(node.bodystmt)], + smap_definition( + srange_length(node.start_char, 5), + srange_find(node.start_char + 5, node.target.start_char, "<<"), + nil, + srange_length(node.end_char, -3) + ).with_expression(srange_node(node)) ) - ) - end + end - # Visit a StringLiteral node. - def visit_string_literal(node) - location = - if node.quote - smap_collection( - srange_length(node.start_char, node.quote.length), - srange_length(node.end_char, -1), - srange_node(node) - ) + # Visit a Statements node. + def visit_statements(node) + children = + node.body.reject do |child| + child.is_a?(Comment) || child.is_a?(EmbDoc) || + child.is_a?(EndContent) || child.is_a?(VoidStmt) + end + + case children.length + when 0 + nil + when 1 + visit(children.first) else - smap_collection_bare(srange_node(node)) + s( + :begin, + visit_all(children), + smap_collection_bare( + srange(children.first.start_char, children.last.end_char) + ) + ) end + end - if node.parts.empty? - s(:str, [""], location) - elsif node.parts.length == 1 && node.parts.first.is_a?(TStringContent) - child = visit(node.parts.first) - s(child.type, child.children, location) - else - s(:dstr, visit_all(node.parts), location) + # Visit a StringConcat node. + def visit_string_concat(node) + s( + :dstr, + [visit(node.left), visit(node.right)], + smap_collection_bare(srange_node(node)) + ) end - end - # Visit a Super node. - def visit_super(node) - if node.arguments.is_a?(Args) + # Visit a StringDVar node. + def visit_string_dvar(node) + visit(node.variable) + end + + # Visit a StringEmbExpr node. + def visit_string_embexpr(node) s( - :super, - visit_all(node.arguments.parts), - smap_keyword_bare( - srange_length(node.start_char, 5), + :begin, + visit(node.statements).then { |child| child ? [child] : [] }, + smap_collection( + srange_length(node.start_char, 2), + srange_length(node.end_char, -1), srange_node(node) ) ) - else - case node.arguments.arguments - when nil - s( - :super, - [], - smap_keyword( - srange_length(node.start_char, 5), - srange_find(node.start_char + 5, node.end_char, "("), + end + + # Visit a StringLiteral node. + def visit_string_literal(node) + location = + if node.quote + smap_collection( + srange_length(node.start_char, node.quote.length), srange_length(node.end_char, -1), srange_node(node) ) - ) - when ArgsForward - s(:super, [visit(node.arguments.arguments)], nil) + else + smap_collection_bare(srange_node(node)) + end + + if node.parts.empty? + s(:str, [""], location) + elsif node.parts.length == 1 && node.parts.first.is_a?(TStringContent) + child = visit(node.parts.first) + s(child.type, child.children, location) else + s(:dstr, visit_all(node.parts), location) + end + end + + # Visit a Super node. + def visit_super(node) + if node.arguments.is_a?(Args) s( :super, - visit_all(node.arguments.arguments.parts), - smap_keyword( + visit_all(node.arguments.parts), + smap_keyword_bare( srange_length(node.start_char, 5), - srange_find(node.start_char + 5, node.end_char, "("), - srange_length(node.end_char, -1), srange_node(node) ) ) + else + case node.arguments.arguments + when nil + s( + :super, + [], + smap_keyword( + srange_length(node.start_char, 5), + srange_find(node.start_char + 5, node.end_char, "("), + srange_length(node.end_char, -1), + srange_node(node) + ) + ) + when ArgsForward + s(:super, [visit(node.arguments.arguments)], nil) + else + s( + :super, + visit_all(node.arguments.arguments.parts), + smap_keyword( + srange_length(node.start_char, 5), + srange_find(node.start_char + 5, node.end_char, "("), + srange_length(node.end_char, -1), + srange_node(node) + ) + ) + end end end - end - # Visit a SymbolLiteral node. - def visit_symbol_literal(node) - begin_token = - if buffer.source[node.start_char] == ":" - srange_length(node.start_char, 1) - end + # Visit a SymbolLiteral node. + def visit_symbol_literal(node) + begin_token = + if buffer.source[node.start_char] == ":" + srange_length(node.start_char, 1) + end - s( - :sym, - [node.value.value.to_sym], - smap_collection(begin_token, nil, srange_node(node)) - ) - end + s( + :sym, + [node.value.value.to_sym], + smap_collection(begin_token, nil, srange_node(node)) + ) + end - # Visit a Symbols node. - def visit_symbols(node) - parts = - node.elements.map do |element| - part = element.parts.first + # Visit a Symbols node. + def visit_symbols(node) + parts = + node.elements.map do |element| + part = element.parts.first - if element.parts.length == 1 && part.is_a?(TStringContent) - SymbolLiteral.new(value: part, location: part.location) - else - DynaSymbol.new( - parts: element.parts, - quote: nil, - location: element.location - ) + if element.parts.length == 1 && part.is_a?(TStringContent) + SymbolLiteral.new(value: part, location: part.location) + else + DynaSymbol.new( + parts: element.parts, + quote: nil, + location: element.location + ) + end end - end - - visit_array( - ArrayLiteral.new( - lbracket: node.beginning, - contents: Args.new(parts: parts, location: node.location), - location: node.location - ) - ) - end - # Visit a TopConstField node. - def visit_top_const_field(node) - s( - :casgn, - [ - s(:cbase, [], smap(srange_length(node.start_char, 2))), - node.constant.value.to_sym - ], - smap_constant( - srange_length(node.start_char, 2), - srange_node(node.constant), - srange_node(node) + visit_array( + ArrayLiteral.new( + lbracket: node.beginning, + contents: Args.new(parts: parts, location: node.location), + location: node.location + ) ) - ) - end + end - # Visit a TopConstRef node. - def visit_top_const_ref(node) - s( - :const, - [ - s(:cbase, [], smap(srange_length(node.start_char, 2))), - node.constant.value.to_sym - ], - smap_constant( - srange_length(node.start_char, 2), - srange_node(node.constant), - srange_node(node) + # Visit a TopConstField node. + def visit_top_const_field(node) + s( + :casgn, + [ + s(:cbase, [], smap(srange_length(node.start_char, 2))), + node.constant.value.to_sym + ], + smap_constant( + srange_length(node.start_char, 2), + srange_node(node.constant), + srange_node(node) + ) ) - ) - end - - # Visit a TStringContent node. - def visit_tstring_content(node) - dumped = node.value.gsub(/([^[:ascii:]])/) { $1.dump[1...-1] } - - s( - :str, - ["\"#{dumped}\"".undump], - smap_collection_bare(srange_node(node)) - ) - end + end - # Visit a Unary node. - def visit_unary(node) - # Special handling here for flipflops - if node.statement.is_a?(Paren) && - node.statement.contents.is_a?(Statements) && - node.statement.contents.body.length == 1 && - (range = node.statement.contents.body.first).is_a?(RangeNode) && - node.operator == "!" - type = range.operator.value == ".." ? :iflipflop : :eflipflop - return( - s( - :send, - [s(:begin, [s(type, visit(range).children, nil)], nil), :!], - nil + # Visit a TopConstRef node. + def visit_top_const_ref(node) + s( + :const, + [ + s(:cbase, [], smap(srange_length(node.start_char, 2))), + node.constant.value.to_sym + ], + smap_constant( + srange_length(node.start_char, 2), + srange_node(node.constant), + srange_node(node) ) ) end - visit(canonical_unary(node)) - end + # Visit a TStringContent node. + def visit_tstring_content(node) + dumped = node.value.gsub(/([^[:ascii:]])/) { $1.dump[1...-1] } - # Visit an Undef node. - def visit_undef(node) - s( - :undef, - visit_all(node.symbols), - smap_keyword_bare( - srange_length(node.start_char, 5), - srange_node(node) + s( + :str, + ["\"#{dumped}\"".undump], + smap_collection_bare(srange_node(node)) ) - ) - end + end - # Visit an UnlessNode node. - def visit_unless(node) - predicate = - case node.predicate - when RegexpLiteral - s(:match_current_line, [visit(node.predicate)], nil) - when Unary - if node.predicate.operator.value == "!" && - node.predicate.statement.is_a?(RegexpLiteral) + # Visit a Unary node. + def visit_unary(node) + # Special handling here for flipflops + if node.statement.is_a?(Paren) && + node.statement.contents.is_a?(Statements) && + node.statement.contents.body.length == 1 && + (range = node.statement.contents.body.first).is_a?(RangeNode) && + node.operator == "!" + type = range.operator.value == ".." ? :iflipflop : :eflipflop + return( s( :send, - [s(:match_current_line, [visit(node.predicate.statement)]), :!], + [s(:begin, [s(type, visit(range).children, nil)], nil), :!], nil ) - else - visit(node.predicate) - end - else - visit(node.predicate) - end - - s( - :if, - [predicate, visit(node.consequent), visit(node.statements)], - if node.modifier? - smap_keyword_bare( - srange_find_between(node.statements, node.predicate, "unless"), - srange_node(node) - ) - else - smap_condition( - srange_length(node.start_char, 6), - srange_search_between(node.predicate, node.statements, "then"), - nil, - srange_length(node.end_char, -3), - srange_node(node) ) end - ) - end - # Visit an UntilNode node. - def visit_until(node) - s( - loop_post?(node) ? :until_post : :until, - [visit(node.predicate), visit(node.statements)], - if node.modifier? + visit(canonical_unary(node)) + end + + # Visit an Undef node. + def visit_undef(node) + s( + :undef, + visit_all(node.symbols), smap_keyword_bare( - srange_find_between(node.statements, node.predicate, "until"), - srange_node(node) - ) - else - smap_keyword( srange_length(node.start_char, 5), - srange_search_between(node.predicate, node.statements, "do") || - srange_search_between(node.predicate, node.statements, ";"), - srange_length(node.end_char, -3), srange_node(node) ) - end - ) - end + ) + end - # Visit a VarField node. - def visit_var_field(node) - name = node.value.value.to_sym - match_var = - [stack[-3], stack[-2]].any? do |parent| - case parent - when AryPtn, FndPtn, HshPtn, In, RAssign - true - when Binary - parent.operator == :"=>" + # Visit an UnlessNode node. + def visit_unless(node) + predicate = + case node.predicate + when RegexpLiteral + s(:match_current_line, [visit(node.predicate)], nil) + when Unary + if node.predicate.operator.value == "!" && + node.predicate.statement.is_a?(RegexpLiteral) + s( + :send, + [ + s(:match_current_line, [visit(node.predicate.statement)]), + :! + ], + nil + ) + else + visit(node.predicate) + end else - false + visit(node.predicate) end - end - if match_var s( - :match_var, - [name], - smap_variable(srange_node(node.value), srange_node(node.value)) + :if, + [predicate, visit(node.consequent), visit(node.statements)], + if node.modifier? + smap_keyword_bare( + srange_find_between(node.statements, node.predicate, "unless"), + srange_node(node) + ) + else + smap_condition( + srange_length(node.start_char, 6), + srange_search_between(node.predicate, node.statements, "then"), + nil, + srange_length(node.end_char, -3), + srange_node(node) + ) + end ) - elsif node.value.is_a?(Const) + end + + # Visit an UntilNode node. + def visit_until(node) s( - :casgn, - [nil, name], - smap_constant(nil, srange_node(node.value), srange_node(node)) + loop_post?(node) ? :until_post : :until, + [visit(node.predicate), visit(node.statements)], + if node.modifier? + smap_keyword_bare( + srange_find_between(node.statements, node.predicate, "until"), + srange_node(node) + ) + else + smap_keyword( + srange_length(node.start_char, 5), + srange_search_between(node.predicate, node.statements, "do") || + srange_search_between(node.predicate, node.statements, ";"), + srange_length(node.end_char, -3), + srange_node(node) + ) + end ) - else - location = smap_variable(srange_node(node), srange_node(node)) + end - case node.value - when CVar - s(:cvasgn, [name], location) - when GVar - s(:gvasgn, [name], location) - when Ident - s(:lvasgn, [name], location) - when IVar - s(:ivasgn, [name], location) - when VarRef - s(:lvasgn, [name], location) + # Visit a VarField node. + def visit_var_field(node) + name = node.value.value.to_sym + match_var = + [stack[-3], stack[-2]].any? do |parent| + case parent + when AryPtn, FndPtn, HshPtn, In, RAssign + true + when Binary + parent.operator == :"=>" + else + false + end + end + + if match_var + s( + :match_var, + [name], + smap_variable(srange_node(node.value), srange_node(node.value)) + ) + elsif node.value.is_a?(Const) + s( + :casgn, + [nil, name], + smap_constant(nil, srange_node(node.value), srange_node(node)) + ) else - s(:match_rest, [], nil) + location = smap_variable(srange_node(node), srange_node(node)) + + case node.value + when CVar + s(:cvasgn, [name], location) + when GVar + s(:gvasgn, [name], location) + when Ident + s(:lvasgn, [name], location) + when IVar + s(:ivasgn, [name], location) + when VarRef + s(:lvasgn, [name], location) + else + s(:match_rest, [], nil) + end end end - end - # Visit a VarRef node. - def visit_var_ref(node) - visit(node.value) - end + # Visit a VarRef node. + def visit_var_ref(node) + visit(node.value) + end - # Visit a VCall node. - def visit_vcall(node) - visit_command_call( - CommandCall.new( - receiver: nil, - operator: nil, - message: node.value, - arguments: nil, - block: nil, - location: node.location + # Visit a VCall node. + def visit_vcall(node) + visit_command_call( + CommandCall.new( + receiver: nil, + operator: nil, + message: node.value, + arguments: nil, + block: nil, + location: node.location + ) ) - ) - end - - # Visit a When node. - def visit_when(node) - keyword = srange_length(node.start_char, 4) - begin_token = - if buffer.source[node.statements.start_char] == ";" - srange_length(node.statements.start_char, 1) - end + end - end_char = - if node.statements.body.empty? - node.statements.end_char - else - node.statements.body.last.end_char - end + # Visit a When node. + def visit_when(node) + keyword = srange_length(node.start_char, 4) + begin_token = + if buffer.source[node.statements.start_char] == ";" + srange_length(node.statements.start_char, 1) + end - s( - :when, - visit_all(node.arguments.parts) + [visit(node.statements)], - smap_keyword( - keyword, - begin_token, - nil, - srange(keyword.begin_pos, end_char) - ) - ) - end + end_char = + if node.statements.body.empty? + node.statements.end_char + else + node.statements.body.last.end_char + end - # Visit a WhileNode node. - def visit_while(node) - s( - loop_post?(node) ? :while_post : :while, - [visit(node.predicate), visit(node.statements)], - if node.modifier? - smap_keyword_bare( - srange_find_between(node.statements, node.predicate, "while"), - srange_node(node) - ) - else + s( + :when, + visit_all(node.arguments.parts) + [visit(node.statements)], smap_keyword( - srange_length(node.start_char, 5), - srange_search_between(node.predicate, node.statements, "do") || - srange_search_between(node.predicate, node.statements, ";"), - srange_length(node.end_char, -3), - srange_node(node) + keyword, + begin_token, + nil, + srange(keyword.begin_pos, end_char) ) - end - ) - end - - # Visit a Word node. - def visit_word(node) - visit_string_literal( - StringLiteral.new( - parts: node.parts, - quote: nil, - location: node.location ) - ) - end + end - # Visit a Words node. - def visit_words(node) - visit_array( - ArrayLiteral.new( - lbracket: node.beginning, - contents: Args.new(parts: node.elements, location: node.location), - location: node.location + # Visit a WhileNode node. + def visit_while(node) + s( + loop_post?(node) ? :while_post : :while, + [visit(node.predicate), visit(node.statements)], + if node.modifier? + smap_keyword_bare( + srange_find_between(node.statements, node.predicate, "while"), + srange_node(node) + ) + else + smap_keyword( + srange_length(node.start_char, 5), + srange_search_between(node.predicate, node.statements, "do") || + srange_search_between(node.predicate, node.statements, ";"), + srange_length(node.end_char, -3), + srange_node(node) + ) + end ) - ) - end + end - # Visit an XStringLiteral node. - def visit_xstring_literal(node) - s( - :xstr, - visit_all(node.parts), - smap_collection( - srange_length( - node.start_char, - buffer.source[node.start_char] == "%" ? 3 : 1 - ), - srange_length(node.end_char, -1), - srange_node(node) + # Visit a Word node. + def visit_word(node) + visit_string_literal( + StringLiteral.new( + parts: node.parts, + quote: nil, + location: node.location + ) ) - ) - end + end - def visit_yield(node) - case node.arguments - when nil - s( - :yield, - [], - smap_keyword_bare( - srange_length(node.start_char, 5), - srange_node(node) + # Visit a Words node. + def visit_words(node) + visit_array( + ArrayLiteral.new( + lbracket: node.beginning, + contents: Args.new(parts: node.elements, location: node.location), + location: node.location ) ) - when Args + end + + # Visit an XStringLiteral node. + def visit_xstring_literal(node) s( - :yield, - visit_all(node.arguments.parts), - smap_keyword_bare( - srange_length(node.start_char, 5), + :xstr, + visit_all(node.parts), + smap_collection( + srange_length( + node.start_char, + buffer.source[node.start_char] == "%" ? 3 : 1 + ), + srange_length(node.end_char, -1), srange_node(node) ) ) - else + end + + def visit_yield(node) + case node.arguments + when nil + s( + :yield, + [], + smap_keyword_bare( + srange_length(node.start_char, 5), + srange_node(node) + ) + ) + when Args + s( + :yield, + visit_all(node.arguments.parts), + smap_keyword_bare( + srange_length(node.start_char, 5), + srange_node(node) + ) + ) + else + s( + :yield, + visit_all(node.arguments.contents.parts), + smap_keyword( + srange_length(node.start_char, 5), + srange_length(node.arguments.start_char, 1), + srange_length(node.end_char, -1), + srange_node(node) + ) + ) + end + end + + # Visit a ZSuper node. + def visit_zsuper(node) s( - :yield, - visit_all(node.arguments.contents.parts), - smap_keyword( + :zsuper, + [], + smap_keyword_bare( srange_length(node.start_char, 5), - srange_length(node.arguments.start_char, 1), - srange_length(node.end_char, -1), srange_node(node) ) ) end end - # Visit a ZSuper node. - def visit_zsuper(node) - s( - :zsuper, - [], - smap_keyword_bare( - srange_length(node.start_char, 5), - srange_node(node) - ) - ) - end - private def block_children(node) diff --git a/lib/syntax_tree/with_environment.rb b/lib/syntax_tree/with_environment.rb index 13f5e080..da300dc0 100644 --- a/lib/syntax_tree/with_environment.rb +++ b/lib/syntax_tree/with_environment.rb @@ -121,9 +121,9 @@ def visit_module(node) with_new_environment { super } end - # When we find a method invocation with a block, only the code that happens - # inside of the block needs a fresh environment. The method invocation - # itself happens in the same environment. + # When we find a method invocation with a block, only the code that + # happens inside of the block needs a fresh environment. The method + # invocation itself happens in the same environment. def visit_method_add_block(node) visit(node.call) with_new_environment { visit(node.block) } diff --git a/lib/syntax_tree/yarv/compiler.rb b/lib/syntax_tree/yarv/compiler.rb index a8044faf..bd20bc19 100644 --- a/lib/syntax_tree/yarv/compiler.rb +++ b/lib/syntax_tree/yarv/compiler.rb @@ -124,76 +124,122 @@ def self.compile(node) rescue CompilationError end - def visit_array(node) - node.contents ? visit_all(node.contents.parts) : [] - end + visit_methods do + def visit_array(node) + node.contents ? visit_all(node.contents.parts) : [] + end - def visit_bare_assoc_hash(node) - node.assocs.to_h do |assoc| - # We can only convert regular key-value pairs. A double splat ** - # operator means it has to be converted at run-time. - raise CompilationError unless assoc.is_a?(Assoc) - [visit(assoc.key), visit(assoc.value)] + def visit_bare_assoc_hash(node) + node.assocs.to_h do |assoc| + # We can only convert regular key-value pairs. A double splat ** + # operator means it has to be converted at run-time. + raise CompilationError unless assoc.is_a?(Assoc) + [visit(assoc.key), visit(assoc.value)] + end end - end - def visit_float(node) - node.value.to_f - end + def visit_float(node) + node.value.to_f + end - alias visit_hash visit_bare_assoc_hash + alias visit_hash visit_bare_assoc_hash - def visit_imaginary(node) - node.value.to_c - end + def visit_imaginary(node) + node.value.to_c + end - def visit_int(node) - case (value = node.value) - when /^0b/ - value[2..].to_i(2) - when /^0o/ - value[2..].to_i(8) - when /^0d/ - value[2..].to_i - when /^0x/ - value[2..].to_i(16) - else - value.to_i + def visit_int(node) + case (value = node.value) + when /^0b/ + value[2..].to_i(2) + when /^0o/ + value[2..].to_i(8) + when /^0d/ + value[2..].to_i + when /^0x/ + value[2..].to_i(16) + else + value.to_i + end end - end - def visit_label(node) - node.value.chomp(":").to_sym - end + def visit_label(node) + node.value.chomp(":").to_sym + end - def visit_mrhs(node) - visit_all(node.parts) - end + def visit_mrhs(node) + visit_all(node.parts) + end - def visit_qsymbols(node) - node.elements.map { |element| visit(element).to_sym } - end + def visit_qsymbols(node) + node.elements.map { |element| visit(element).to_sym } + end - def visit_qwords(node) - visit_all(node.elements) - end + def visit_qwords(node) + visit_all(node.elements) + end - def visit_range(node) - left, right = [visit(node.left), visit(node.right)] - node.operator.value === ".." ? left..right : left...right - end + def visit_range(node) + left, right = [visit(node.left), visit(node.right)] + node.operator.value === ".." ? left..right : left...right + end - def visit_rational(node) - node.value.to_r - end + def visit_rational(node) + node.value.to_r + end - def visit_regexp_literal(node) - if node.parts.length == 1 && node.parts.first.is_a?(TStringContent) - Regexp.new(node.parts.first.value, visit_regexp_literal_flags(node)) - else - # Any interpolation of expressions or variables will result in the - # regular expression being constructed at run-time. - raise CompilationError + def visit_regexp_literal(node) + if node.parts.length == 1 && node.parts.first.is_a?(TStringContent) + Regexp.new( + node.parts.first.value, + visit_regexp_literal_flags(node) + ) + else + # Any interpolation of expressions or variables will result in the + # regular expression being constructed at run-time. + raise CompilationError + end + end + + def visit_symbol_literal(node) + node.value.value.to_sym + end + + def visit_symbols(node) + node.elements.map { |element| visit(element).to_sym } + end + + def visit_tstring_content(node) + node.value + end + + def visit_var_ref(node) + raise CompilationError unless node.value.is_a?(Kw) + + case node.value.value + when "nil" + nil + when "true" + true + when "false" + false + else + raise CompilationError + end + end + + def visit_word(node) + if node.parts.length == 1 && node.parts.first.is_a?(TStringContent) + node.parts.first.value + else + # Any interpolation of expressions or variables will result in the + # string being constructed at run-time. + raise CompilationError + end + end + + def visit_words(node) + visit_all(node.elements) end end @@ -219,47 +265,6 @@ def visit_regexp_literal_flags(node) end end - def visit_symbol_literal(node) - node.value.value.to_sym - end - - def visit_symbols(node) - node.elements.map { |element| visit(element).to_sym } - end - - def visit_tstring_content(node) - node.value - end - - def visit_var_ref(node) - raise CompilationError unless node.value.is_a?(Kw) - - case node.value.value - when "nil" - nil - when "true" - true - when "false" - false - else - raise CompilationError - end - end - - def visit_word(node) - if node.parts.length == 1 && node.parts.first.is_a?(TStringContent) - node.parts.first.value - else - # Any interpolation of expressions or variables will result in the - # string being constructed at run-time. - raise CompilationError - end - end - - def visit_words(node) - visit_all(node.elements) - end - def visit_unsupported(_node) raise CompilationError end diff --git a/test/visitor_test.rb b/test/visitor_test.rb index 86ff1b01..d9637df0 100644 --- a/test/visitor_test.rb +++ b/test/visitor_test.rb @@ -30,13 +30,15 @@ def initialize @visited_nodes = [] end - visit_method def visit_class(node) - @visited_nodes << node.constant.constant.value - super - end + visit_methods do + def visit_class(node) + @visited_nodes << node.constant.constant.value + super + end - visit_method def visit_def(node) - @visited_nodes << node.name.value + def visit_def(node) + @visited_nodes << node.name.value + end end end diff --git a/test/visitor_with_environment_test.rb b/test/visitor_with_environment_test.rb index cc4007fe..278ae361 100644 --- a/test/visitor_with_environment_test.rb +++ b/test/visitor_with_environment_test.rb @@ -14,26 +14,28 @@ def initialize @arguments = {} end - def visit_ident(node) - local = current_environment.find_local(node.value) - return unless local - - value = node.value.delete_suffix(":") - - case local.type - when :argument - @arguments[value] = local - when :variable - @variables[value] = local + visit_methods do + def visit_ident(node) + local = current_environment.find_local(node.value) + return unless local + + value = node.value.delete_suffix(":") + + case local.type + when :argument + @arguments[value] = local + when :variable + @variables[value] = local + end end - end - def visit_label(node) - value = node.value.delete_suffix(":") - local = current_environment.find_local(value) - return unless local + def visit_label(node) + value = node.value.delete_suffix(":") + local = current_environment.find_local(value) + return unless local - @arguments[value] = node if local.type == :argument + @arguments[value] = node if local.type == :argument + end end end @@ -625,13 +627,15 @@ def initialize @locals = [] end - def visit_assign(node) - level = 0 - environment = current_environment - level += 1 until (environment = environment.parent).nil? + visit_methods do + def visit_assign(node) + level = 0 + environment = current_environment + level += 1 until (environment = environment.parent).nil? - locals << [node.target.value.value, level] - super + locals << [node.target.value.value, level] + super + end end end From 174cc6bae01dc6825858906fa46a9f3213608c24 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Fri, 10 Feb 2023 12:50:59 -0500 Subject: [PATCH 10/11] Make environment break at boundaries --- lib/syntax_tree/with_environment.rb | 56 ++- test/visitor_with_environment_test.rb | 663 -------------------------- test/with_environment_test.rb | 457 ++++++++++++++++++ 3 files changed, 499 insertions(+), 677 deletions(-) delete mode 100644 test/visitor_with_environment_test.rb create mode 100644 test/with_environment_test.rb diff --git a/lib/syntax_tree/with_environment.rb b/lib/syntax_tree/with_environment.rb index da300dc0..3a6f04b9 100644 --- a/lib/syntax_tree/with_environment.rb +++ b/lib/syntax_tree/with_environment.rb @@ -55,14 +55,18 @@ def add_usage(location) end end - # [Array[Local]] The local variables and arguments defined in this + # [Integer] a unique identifier for this environment + attr_reader :id + + # [Hash[String, Local]] The local variables and arguments defined in this # environment attr_reader :locals # [Environment | nil] The parent environment attr_reader :parent - def initialize(parent = nil) + def initialize(id, parent = nil) + @id = id @locals = {} @parent = parent end @@ -74,8 +78,14 @@ def initialize(parent = nil) def add_local_definition(identifier, type) name = identifier.value.delete_suffix(":") - @locals[name] ||= Local.new(type) - @locals[name].add_definition(identifier.location) + local = + if type == :argument + locals[name] ||= Local.new(type) + else + resolve_local(name, type) + end + + local.add_definition(identifier.location) end # Adding a local usage will either insert a new entry in the locals @@ -84,28 +94,42 @@ def add_local_definition(identifier, type) # registered. def add_local_usage(identifier, type) name = identifier.value.delete_suffix(":") - - @locals[name] ||= Local.new(type) - @locals[name].add_usage(identifier.location) + resolve_local(name, type).add_usage(identifier.location) end # Try to find the local given its name in this environment or any of its # parents. def find_local(name) - local = @locals[name] - return local unless local.nil? + locals[name] || parent&.find_local(name) + end - @parent&.find_local(name) + private + + def resolve_local(name, type) + local = find_local(name) + + unless local + local = Local.new(type) + locals[name] = local + end + + local end end + def initialize(*args, **kwargs, &block) + super + @environment_id = 0 + end + def current_environment - @current_environment ||= Environment.new + @current_environment ||= Environment.new(next_environment_id) end - def with_new_environment + def with_new_environment(parent_environment = nil) previous_environment = @current_environment - @current_environment = Environment.new(previous_environment) + @current_environment = + Environment.new(next_environment_id, parent_environment) yield ensure @current_environment = previous_environment @@ -126,7 +150,7 @@ def visit_module(node) # invocation itself happens in the same environment. def visit_method_add_block(node) visit(node.call) - with_new_environment { visit(node.block) } + with_new_environment(current_environment) { visit(node.block) } end def visit_def(node) @@ -213,5 +237,9 @@ def add_argument_definitions(list) end end end + + def next_environment_id + @environment_id += 1 + end end end diff --git a/test/visitor_with_environment_test.rb b/test/visitor_with_environment_test.rb deleted file mode 100644 index 278ae361..00000000 --- a/test/visitor_with_environment_test.rb +++ /dev/null @@ -1,663 +0,0 @@ -# frozen_string_literal: true - -require_relative "test_helper" - -module SyntaxTree - class VisitorWithEnvironmentTest < Minitest::Test - class Collector < Visitor - include WithEnvironment - - attr_reader :variables, :arguments - - def initialize - @variables = {} - @arguments = {} - end - - visit_methods do - def visit_ident(node) - local = current_environment.find_local(node.value) - return unless local - - value = node.value.delete_suffix(":") - - case local.type - when :argument - @arguments[value] = local - when :variable - @variables[value] = local - end - end - - def visit_label(node) - value = node.value.delete_suffix(":") - local = current_environment.find_local(value) - return unless local - - @arguments[value] = node if local.type == :argument - end - end - end - - def test_collecting_simple_variables - tree = SyntaxTree.parse(<<~RUBY) - def foo - a = 1 - a - end - RUBY - - visitor = Collector.new - visitor.visit(tree) - - assert_equal(1, visitor.variables.length) - - variable = visitor.variables["a"] - assert_equal(1, variable.definitions.length) - assert_equal(1, variable.usages.length) - - assert_equal(2, variable.definitions[0].start_line) - assert_equal(3, variable.usages[0].start_line) - end - - def test_collecting_aref_variables - tree = SyntaxTree.parse(<<~RUBY) - def foo - a = [] - a[1] - end - RUBY - - visitor = Collector.new - visitor.visit(tree) - - assert_equal(1, visitor.variables.length) - - variable = visitor.variables["a"] - assert_equal(1, variable.definitions.length) - assert_equal(1, variable.usages.length) - - assert_equal(2, variable.definitions[0].start_line) - assert_equal(3, variable.usages[0].start_line) - end - - def test_collecting_multi_assign_variables - tree = SyntaxTree.parse(<<~RUBY) - def foo - a, b = [1, 2] - puts a - puts b - end - RUBY - - visitor = Collector.new - visitor.visit(tree) - - assert_equal(2, visitor.variables.length) - - variable_a = visitor.variables["a"] - assert_equal(1, variable_a.definitions.length) - assert_equal(1, variable_a.usages.length) - - assert_equal(2, variable_a.definitions[0].start_line) - assert_equal(3, variable_a.usages[0].start_line) - - variable_b = visitor.variables["b"] - assert_equal(1, variable_b.definitions.length) - assert_equal(1, variable_b.usages.length) - - assert_equal(2, variable_b.definitions[0].start_line) - assert_equal(4, variable_b.usages[0].start_line) - end - - def test_collecting_pattern_matching_variables - tree = SyntaxTree.parse(<<~RUBY) - def foo - case [1, 2] - in Integer => a, Integer - puts a - end - end - RUBY - - visitor = Collector.new - visitor.visit(tree) - - # There are two occurrences, one on line 3 for pinning and one on line 4 - # for reference - assert_equal(1, visitor.variables.length) - - variable = visitor.variables["a"] - - # Assignment a - assert_equal(3, variable.definitions[0].start_line) - assert_equal(4, variable.usages[0].start_line) - end - - def test_collecting_pinned_variables - tree = SyntaxTree.parse(<<~RUBY) - def foo - a = 18 - case [1, 2] - in ^a, *rest - puts a - puts rest - end - end - RUBY - - visitor = Collector.new - visitor.visit(tree) - - assert_equal(2, visitor.variables.length) - - variable_a = visitor.variables["a"] - assert_equal(2, variable_a.definitions.length) - assert_equal(1, variable_a.usages.length) - - assert_equal(2, variable_a.definitions[0].start_line) - assert_equal(4, variable_a.definitions[1].start_line) - assert_equal(5, variable_a.usages[0].start_line) - - variable_rest = visitor.variables["rest"] - assert_equal(1, variable_rest.definitions.length) - assert_equal(4, variable_rest.definitions[0].start_line) - - # Rest is considered a vcall by the parser instead of a var_ref - # assert_equal(1, variable_rest.usages.length) - # assert_equal(6, variable_rest.usages[0].start_line) - end - - if RUBY_VERSION >= "3.1" - def test_collecting_one_line_pattern_matching_variables - tree = SyntaxTree.parse(<<~RUBY) - def foo - [1] => a - puts a - end - RUBY - - visitor = Collector.new - visitor.visit(tree) - - assert_equal(1, visitor.variables.length) - - variable = visitor.variables["a"] - assert_equal(1, variable.definitions.length) - assert_equal(1, variable.usages.length) - - assert_equal(2, variable.definitions[0].start_line) - assert_equal(3, variable.usages[0].start_line) - end - - def test_collecting_endless_method_arguments - tree = SyntaxTree.parse(<<~RUBY) - def foo(a) = puts a - RUBY - - visitor = Collector.new - visitor.visit(tree) - - assert_equal(1, visitor.arguments.length) - - argument = visitor.arguments["a"] - assert_equal(1, argument.definitions.length) - assert_equal(1, argument.usages.length) - - assert_equal(1, argument.definitions[0].start_line) - assert_equal(1, argument.usages[0].start_line) - end - end - - def test_collecting_method_arguments - tree = SyntaxTree.parse(<<~RUBY) - def foo(a) - puts a - end - RUBY - - visitor = Collector.new - visitor.visit(tree) - - assert_equal(1, visitor.arguments.length) - - argument = visitor.arguments["a"] - assert_equal(1, argument.definitions.length) - assert_equal(1, argument.usages.length) - - assert_equal(1, argument.definitions[0].start_line) - assert_equal(2, argument.usages[0].start_line) - end - - def test_collecting_singleton_method_arguments - tree = SyntaxTree.parse(<<~RUBY) - def self.foo(a) - puts a - end - RUBY - - visitor = Collector.new - visitor.visit(tree) - - assert_equal(1, visitor.arguments.length) - - argument = visitor.arguments["a"] - assert_equal(1, argument.definitions.length) - assert_equal(1, argument.usages.length) - - assert_equal(1, argument.definitions[0].start_line) - assert_equal(2, argument.usages[0].start_line) - end - - def test_collecting_method_arguments_all_types - tree = SyntaxTree.parse(<<~RUBY) - def foo(a, b = 1, *c, d, e: 1, **f, &block) - puts a - puts b - puts c - puts d - puts e - puts f - block.call - end - RUBY - - visitor = Collector.new - visitor.visit(tree) - - assert_equal(7, visitor.arguments.length) - - argument_a = visitor.arguments["a"] - assert_equal(1, argument_a.definitions.length) - assert_equal(1, argument_a.usages.length) - assert_equal(1, argument_a.definitions[0].start_line) - assert_equal(2, argument_a.usages[0].start_line) - - argument_b = visitor.arguments["b"] - assert_equal(1, argument_b.definitions.length) - assert_equal(1, argument_b.usages.length) - assert_equal(1, argument_b.definitions[0].start_line) - assert_equal(3, argument_b.usages[0].start_line) - - argument_c = visitor.arguments["c"] - assert_equal(1, argument_c.definitions.length) - assert_equal(1, argument_c.usages.length) - assert_equal(1, argument_c.definitions[0].start_line) - assert_equal(4, argument_c.usages[0].start_line) - - argument_d = visitor.arguments["d"] - assert_equal(1, argument_d.definitions.length) - assert_equal(1, argument_d.usages.length) - assert_equal(1, argument_d.definitions[0].start_line) - assert_equal(5, argument_d.usages[0].start_line) - - argument_e = visitor.arguments["e"] - assert_equal(1, argument_e.definitions.length) - assert_equal(1, argument_e.usages.length) - assert_equal(1, argument_e.definitions[0].start_line) - assert_equal(6, argument_e.usages[0].start_line) - - argument_f = visitor.arguments["f"] - assert_equal(1, argument_f.definitions.length) - assert_equal(1, argument_f.usages.length) - assert_equal(1, argument_f.definitions[0].start_line) - assert_equal(7, argument_f.usages[0].start_line) - - argument_block = visitor.arguments["block"] - assert_equal(1, argument_block.definitions.length) - assert_equal(1, argument_block.usages.length) - assert_equal(1, argument_block.definitions[0].start_line) - assert_equal(8, argument_block.usages[0].start_line) - end - - def test_collecting_block_arguments - tree = SyntaxTree.parse(<<~RUBY) - def foo - [].each do |i| - puts i - end - end - RUBY - - visitor = Collector.new - visitor.visit(tree) - - assert_equal(1, visitor.arguments.length) - - argument = visitor.arguments["i"] - assert_equal(1, argument.definitions.length) - assert_equal(1, argument.usages.length) - assert_equal(2, argument.definitions[0].start_line) - assert_equal(3, argument.usages[0].start_line) - end - - def test_collecting_one_line_block_arguments - tree = SyntaxTree.parse(<<~RUBY) - def foo - [].each { |i| puts i } - end - RUBY - - visitor = Collector.new - visitor.visit(tree) - - assert_equal(1, visitor.arguments.length) - - argument = visitor.arguments["i"] - assert_equal(1, argument.definitions.length) - assert_equal(1, argument.usages.length) - assert_equal(2, argument.definitions[0].start_line) - assert_equal(2, argument.usages[0].start_line) - end - - def test_collecting_shadowed_block_arguments - tree = SyntaxTree.parse(<<~RUBY) - def foo - i = "something" - - [].each do |i| - puts i - end - - i - end - RUBY - - visitor = Collector.new - visitor.visit(tree) - - assert_equal(1, visitor.arguments.length) - assert_equal(1, visitor.variables.length) - - argument = visitor.arguments["i"] - assert_equal(1, argument.definitions.length) - assert_equal(1, argument.usages.length) - assert_equal(4, argument.definitions[0].start_line) - assert_equal(5, argument.usages[0].start_line) - - variable = visitor.variables["i"] - assert_equal(1, variable.definitions.length) - assert_equal(1, variable.usages.length) - assert_equal(2, variable.definitions[0].start_line) - assert_equal(8, variable.usages[0].start_line) - end - - def test_collecting_shadowed_local_variables - tree = SyntaxTree.parse(<<~RUBY) - def foo(a) - puts a - a = 123 - a - end - RUBY - - visitor = Collector.new - visitor.visit(tree) - - # All occurrences are considered arguments, despite overriding the - # argument value - assert_equal(1, visitor.arguments.length) - assert_equal(0, visitor.variables.length) - - argument = visitor.arguments["a"] - assert_equal(2, argument.definitions.length) - assert_equal(2, argument.usages.length) - - assert_equal(1, argument.definitions[0].start_line) - assert_equal(3, argument.definitions[1].start_line) - assert_equal(2, argument.usages[0].start_line) - assert_equal(4, argument.usages[1].start_line) - end - - def test_variables_in_the_top_level - tree = SyntaxTree.parse(<<~RUBY) - a = 123 - a - RUBY - - visitor = Collector.new - visitor.visit(tree) - - assert_equal(0, visitor.arguments.length) - assert_equal(1, visitor.variables.length) - - variable = visitor.variables["a"] - assert_equal(1, variable.definitions.length) - assert_equal(1, variable.usages.length) - - assert_equal(1, variable.definitions[0].start_line) - assert_equal(2, variable.usages[0].start_line) - end - - def test_aref_field - tree = SyntaxTree.parse(<<~RUBY) - object = {} - object["name"] = "something" - RUBY - - visitor = Collector.new - visitor.visit(tree) - - assert_equal(0, visitor.arguments.length) - assert_equal(1, visitor.variables.length) - - variable = visitor.variables["object"] - assert_equal(1, variable.definitions.length) - assert_equal(1, variable.usages.length) - - assert_equal(1, variable.definitions[0].start_line) - assert_equal(2, variable.usages[0].start_line) - end - - def test_aref_on_a_method_call - tree = SyntaxTree.parse(<<~RUBY) - object = MyObject.new - object.attributes["name"] = "something" - RUBY - - visitor = Collector.new - visitor.visit(tree) - - assert_equal(0, visitor.arguments.length) - assert_equal(1, visitor.variables.length) - - variable = visitor.variables["object"] - assert_equal(1, variable.definitions.length) - assert_equal(1, variable.usages.length) - - assert_equal(1, variable.definitions[0].start_line) - assert_equal(2, variable.usages[0].start_line) - end - - def test_aref_with_two_accesses - tree = SyntaxTree.parse(<<~RUBY) - object = MyObject.new - object["first"]["second"] ||= [] - RUBY - - visitor = Collector.new - visitor.visit(tree) - - assert_equal(0, visitor.arguments.length) - assert_equal(1, visitor.variables.length) - - variable = visitor.variables["object"] - assert_equal(1, variable.definitions.length) - assert_equal(1, variable.usages.length) - - assert_equal(1, variable.definitions[0].start_line) - assert_equal(2, variable.usages[0].start_line) - end - - def test_aref_on_a_method_call_with_arguments - tree = SyntaxTree.parse(<<~RUBY) - object = MyObject.new - object.instance_variable_get(:@attributes)[:something] = :other_thing - RUBY - - visitor = Collector.new - visitor.visit(tree) - - assert_equal(0, visitor.arguments.length) - assert_equal(1, visitor.variables.length) - - variable = visitor.variables["object"] - assert_equal(1, variable.definitions.length) - assert_equal(1, variable.usages.length) - - assert_equal(1, variable.definitions[0].start_line) - assert_equal(2, variable.usages[0].start_line) - end - - def test_double_aref_on_method_call - tree = SyntaxTree.parse(<<~RUBY) - object = MyObject.new - object["attributes"].find { |a| a["field"] == "expected" }["value"] = "changed" - RUBY - - visitor = Collector.new - visitor.visit(tree) - - assert_equal(1, visitor.arguments.length) - assert_equal(1, visitor.variables.length) - - variable = visitor.variables["object"] - assert_equal(1, variable.definitions.length) - assert_equal(1, variable.usages.length) - - assert_equal(1, variable.definitions[0].start_line) - assert_equal(2, variable.usages[0].start_line) - - argument = visitor.arguments["a"] - assert_equal(1, argument.definitions.length) - assert_equal(1, argument.usages.length) - - assert_equal(2, argument.definitions[0].start_line) - assert_equal(2, argument.usages[0].start_line) - end - - def test_nested_arguments - tree = SyntaxTree.parse(<<~RUBY) - [[1, [2, 3]]].each do |one, (two, three)| - one - two - three - end - RUBY - - visitor = Collector.new - visitor.visit(tree) - - assert_equal(3, visitor.arguments.length) - assert_equal(0, visitor.variables.length) - - argument = visitor.arguments["one"] - assert_equal(1, argument.definitions.length) - assert_equal(1, argument.usages.length) - - assert_equal(1, argument.definitions[0].start_line) - assert_equal(2, argument.usages[0].start_line) - - argument = visitor.arguments["two"] - assert_equal(1, argument.definitions.length) - assert_equal(1, argument.usages.length) - - assert_equal(1, argument.definitions[0].start_line) - assert_equal(3, argument.usages[0].start_line) - - argument = visitor.arguments["three"] - assert_equal(1, argument.definitions.length) - assert_equal(1, argument.usages.length) - - assert_equal(1, argument.definitions[0].start_line) - assert_equal(4, argument.usages[0].start_line) - end - - def test_double_nested_arguments - tree = SyntaxTree.parse(<<~RUBY) - [[1, [2, 3]]].each do |one, (two, (three, four))| - one - two - three - four - end - RUBY - - visitor = Collector.new - visitor.visit(tree) - - assert_equal(4, visitor.arguments.length) - assert_equal(0, visitor.variables.length) - - argument = visitor.arguments["one"] - assert_equal(1, argument.definitions.length) - assert_equal(1, argument.usages.length) - - assert_equal(1, argument.definitions[0].start_line) - assert_equal(2, argument.usages[0].start_line) - - argument = visitor.arguments["two"] - assert_equal(1, argument.definitions.length) - assert_equal(1, argument.usages.length) - - assert_equal(1, argument.definitions[0].start_line) - assert_equal(3, argument.usages[0].start_line) - - argument = visitor.arguments["three"] - assert_equal(1, argument.definitions.length) - assert_equal(1, argument.usages.length) - - assert_equal(1, argument.definitions[0].start_line) - assert_equal(4, argument.usages[0].start_line) - - argument = visitor.arguments["four"] - assert_equal(1, argument.definitions.length) - assert_equal(1, argument.usages.length) - - assert_equal(1, argument.definitions[0].start_line) - assert_equal(5, argument.usages[0].start_line) - end - - class Resolver < Visitor - include WithEnvironment - - attr_reader :locals - - def initialize - @locals = [] - end - - visit_methods do - def visit_assign(node) - level = 0 - environment = current_environment - level += 1 until (environment = environment.parent).nil? - - locals << [node.target.value.value, level] - super - end - end - end - - def test_class - source = <<~RUBY - module Level0 - level0 = 0 - - module Level1 - level1 = 1 - - class Level2 - level2 = 2 - end - end - end - RUBY - - visitor = Resolver.new - SyntaxTree.parse(source).accept(visitor) - - assert_equal [["level0", 0], ["level1", 1], ["level2", 2]], visitor.locals - end - end -end diff --git a/test/with_environment_test.rb b/test/with_environment_test.rb new file mode 100644 index 00000000..b6f79c14 --- /dev/null +++ b/test/with_environment_test.rb @@ -0,0 +1,457 @@ +# frozen_string_literal: true + +require_relative "test_helper" + +module SyntaxTree + class WithEnvironmentTest < Minitest::Test + class Collector < Visitor + prepend WithEnvironment + + attr_reader :arguments, :variables + + def initialize + @arguments = {} + @variables = {} + end + + def self.collect(source) + new.tap { SyntaxTree.parse(source).accept(_1) } + end + + visit_methods do + def visit_ident(node) + value = node.value.delete_suffix(":") + local = current_environment.find_local(node.value) + + case local&.type + when :argument + arguments[[current_environment.id, value]] = local + when :variable + variables[[current_environment.id, value]] = local + end + end + + def visit_label(node) + value = node.value.delete_suffix(":") + local = current_environment.find_local(value) + + if local&.type == :argument + arguments[[current_environment.id, value]] = node + end + end + end + end + + def test_collecting_simple_variables + collector = Collector.collect(<<~RUBY) + def foo + a = 1 + a + end + RUBY + + assert_equal(1, collector.variables.length) + assert_variable(collector, "a", definitions: [2], usages: [3]) + end + + def test_collecting_aref_variables + collector = Collector.collect(<<~RUBY) + def foo + a = [] + a[1] + end + RUBY + + assert_equal(1, collector.variables.length) + assert_variable(collector, "a", definitions: [2], usages: [3]) + end + + def test_collecting_multi_assign_variables + collector = Collector.collect(<<~RUBY) + def foo + a, b = [1, 2] + puts a + puts b + end + RUBY + + assert_equal(2, collector.variables.length) + assert_variable(collector, "a", definitions: [2], usages: [3]) + assert_variable(collector, "b", definitions: [2], usages: [4]) + end + + def test_collecting_pattern_matching_variables + collector = Collector.collect(<<~RUBY) + def foo + case [1, 2] + in Integer => a, Integer + puts a + end + end + RUBY + + # There are two occurrences, one on line 3 for pinning and one on line 4 + # for reference + assert_equal(1, collector.variables.length) + assert_variable(collector, "a", definitions: [3], usages: [4]) + end + + def test_collecting_pinned_variables + collector = Collector.collect(<<~RUBY) + def foo + a = 18 + case [1, 2] + in ^a, *rest + puts a + puts rest + end + end + RUBY + + assert_equal(2, collector.variables.length) + assert_variable(collector, "a", definitions: [2, 4], usages: [5]) + assert_variable(collector, "rest", definitions: [4]) + + # Rest is considered a vcall by the parser instead of a var_ref + # assert_equal(1, variable_rest.usages.length) + # assert_equal(6, variable_rest.usages[0].start_line) + end + + if RUBY_VERSION >= "3.1" + def test_collecting_one_line_pattern_matching_variables + collector = Collector.collect(<<~RUBY) + def foo + [1] => a + puts a + end + RUBY + + assert_equal(1, collector.variables.length) + assert_variable(collector, "a", definitions: [2], usages: [3]) + end + + def test_collecting_endless_method_arguments + collector = Collector.collect(<<~RUBY) + def foo(a) = puts a + RUBY + + assert_equal(1, collector.arguments.length) + assert_argument(collector, "a", definitions: [1], usages: [1]) + end + end + + def test_collecting_method_arguments + collector = Collector.collect(<<~RUBY) + def foo(a) + puts a + end + RUBY + + assert_equal(1, collector.arguments.length) + assert_argument(collector, "a", definitions: [1], usages: [2]) + end + + def test_collecting_singleton_method_arguments + collector = Collector.collect(<<~RUBY) + def self.foo(a) + puts a + end + RUBY + + assert_equal(1, collector.arguments.length) + assert_argument(collector, "a", definitions: [1], usages: [2]) + end + + def test_collecting_method_arguments_all_types + collector = Collector.collect(<<~RUBY) + def foo(a, b = 1, *c, d, e: 1, **f, &block) + puts a + puts b + puts c + puts d + puts e + puts f + block.call + end + RUBY + + assert_equal(7, collector.arguments.length) + assert_argument(collector, "a", definitions: [1], usages: [2]) + assert_argument(collector, "b", definitions: [1], usages: [3]) + assert_argument(collector, "c", definitions: [1], usages: [4]) + assert_argument(collector, "d", definitions: [1], usages: [5]) + assert_argument(collector, "e", definitions: [1], usages: [6]) + assert_argument(collector, "f", definitions: [1], usages: [7]) + assert_argument(collector, "block", definitions: [1], usages: [8]) + end + + def test_collecting_block_arguments + collector = Collector.collect(<<~RUBY) + def foo + [].each do |i| + puts i + end + end + RUBY + + assert_equal(1, collector.arguments.length) + assert_argument(collector, "i", definitions: [2], usages: [3]) + end + + def test_collecting_one_line_block_arguments + collector = Collector.collect(<<~RUBY) + def foo + [].each { |i| puts i } + end + RUBY + + assert_equal(1, collector.arguments.length) + assert_argument(collector, "i", definitions: [2], usages: [2]) + end + + def test_collecting_shadowed_block_arguments + collector = Collector.collect(<<~RUBY) + def foo + i = "something" + + [].each do |i| + puts i + end + + i + end + RUBY + + assert_equal(1, collector.arguments.length) + assert_argument(collector, "i", definitions: [4], usages: [5]) + + assert_equal(1, collector.variables.length) + assert_variable(collector, "i", definitions: [2], usages: [8]) + end + + def test_collecting_shadowed_local_variables + collector = Collector.collect(<<~RUBY) + def foo(a) + puts a + a = 123 + a + end + RUBY + + # All occurrences are considered arguments, despite overriding the + # argument value + assert_equal(1, collector.arguments.length) + assert_equal(0, collector.variables.length) + assert_argument(collector, "a", definitions: [1, 3], usages: [2, 4]) + end + + def test_variables_in_the_top_level + collector = Collector.collect(<<~RUBY) + a = 123 + a + RUBY + + assert_equal(0, collector.arguments.length) + assert_equal(1, collector.variables.length) + assert_variable(collector, "a", definitions: [1], usages: [2]) + end + + def test_aref_field + collector = Collector.collect(<<~RUBY) + object = {} + object["name"] = "something" + RUBY + + assert_equal(0, collector.arguments.length) + assert_equal(1, collector.variables.length) + assert_variable(collector, "object", definitions: [1], usages: [2]) + end + + def test_aref_on_a_method_call + collector = Collector.collect(<<~RUBY) + object = MyObject.new + object.attributes["name"] = "something" + RUBY + + assert_equal(0, collector.arguments.length) + assert_equal(1, collector.variables.length) + assert_variable(collector, "object", definitions: [1], usages: [2]) + end + + def test_aref_with_two_accesses + collector = Collector.collect(<<~RUBY) + object = MyObject.new + object["first"]["second"] ||= [] + RUBY + + assert_equal(0, collector.arguments.length) + assert_equal(1, collector.variables.length) + assert_variable(collector, "object", definitions: [1], usages: [2]) + end + + def test_aref_on_a_method_call_with_arguments + collector = Collector.collect(<<~RUBY) + object = MyObject.new + object.instance_variable_get(:@attributes)[:something] = :other_thing + RUBY + + assert_equal(0, collector.arguments.length) + assert_equal(1, collector.variables.length) + assert_variable(collector, "object", definitions: [1], usages: [2]) + end + + def test_double_aref_on_method_call + collector = Collector.collect(<<~RUBY) + object = MyObject.new + object["attributes"].find { |a| a["field"] == "expected" }["value"] = "changed" + RUBY + + assert_equal(1, collector.arguments.length) + assert_argument(collector, "a", definitions: [2], usages: [2]) + + assert_equal(1, collector.variables.length) + assert_variable(collector, "object", definitions: [1], usages: [2]) + end + + def test_nested_arguments + collector = Collector.collect(<<~RUBY) + [[1, [2, 3]]].each do |one, (two, three)| + one + two + three + end + RUBY + + assert_equal(3, collector.arguments.length) + assert_equal(0, collector.variables.length) + + assert_argument(collector, "one", definitions: [1], usages: [2]) + assert_argument(collector, "two", definitions: [1], usages: [3]) + assert_argument(collector, "three", definitions: [1], usages: [4]) + end + + def test_double_nested_arguments + collector = Collector.collect(<<~RUBY) + [[1, [2, 3]]].each do |one, (two, (three, four))| + one + two + three + four + end + RUBY + + assert_equal(4, collector.arguments.length) + assert_equal(0, collector.variables.length) + + assert_argument(collector, "one", definitions: [1], usages: [2]) + assert_argument(collector, "two", definitions: [1], usages: [3]) + assert_argument(collector, "three", definitions: [1], usages: [4]) + assert_argument(collector, "four", definitions: [1], usages: [5]) + end + + class Resolver < Visitor + prepend WithEnvironment + + attr_reader :locals + + def initialize + @locals = [] + end + + visit_methods do + def visit_assign(node) + super.tap do + level = 0 + name = node.target.value.value + + environment = current_environment + while !environment.locals.key?(name) && !environment.parent.nil? + level += 1 + environment = environment.parent + end + + locals << [name, level] + end + end + end + end + + def test_resolver + source = <<~RUBY + module Level0 + level0 = 0 + + class Level1 + level1 = 1 + + def level2 + level2 = 2 + + tap do |level3| + level2 = 2 + level3 = 3 + + tap do |level4| + level2 = 2 + level4 = 4 + end + end + end + end + end + RUBY + + resolver = Resolver.new + SyntaxTree.parse(source).accept(resolver) + + expected = [ + ["level0", 0], + ["level1", 0], + ["level2", 0], + ["level2", 1], + ["level3", 0], + ["level2", 2], + ["level4", 0] + ] + + assert_equal expected, resolver.locals + end + + private + + def assert_collected(field, name, definitions: [], usages: []) + keys = field.keys.select { |key| key[1] == name } + assert_equal(1, keys.length) + + variable = field[keys.first] + + assert_equal(definitions.length, variable.definitions.length) + definitions.each_with_index do |definition, index| + assert_equal(definition, variable.definitions[index].start_line) + end + + assert_equal(usages.length, variable.usages.length) + usages.each_with_index do |usage, index| + assert_equal(usage, variable.usages[index].start_line) + end + end + + def assert_argument(collector, name, definitions: [], usages: []) + assert_collected( + collector.arguments, + name, + definitions: definitions, + usages: usages + ) + end + + def assert_variable(collector, name, definitions: [], usages: []) + assert_collected( + collector.variables, + name, + definitions: definitions, + usages: usages + ) + end + end +end From 4a6fc77abd4c696b3d38498250ab37e571f27d9a Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Fri, 10 Feb 2023 13:00:40 -0500 Subject: [PATCH 11/11] WithEnvironment -> WithScope --- README.md | 12 +- lib/syntax_tree.rb | 2 +- .../{with_environment.rb => with_scope.rb} | 107 ++++++++---------- ...environment_test.rb => with_scope_test.rb} | 22 ++-- 4 files changed, 67 insertions(+), 76 deletions(-) rename lib/syntax_tree/{with_environment.rb => with_scope.rb} (65%) rename test/{with_environment_test.rb => with_scope_test.rb} (95%) diff --git a/README.md b/README.md index 5f447ad8..500d5fad 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ It is built with only standard library dependencies. It additionally ships with - [visit_methods](#visit_methods) - [BasicVisitor](#basicvisitor) - [MutationVisitor](#mutationvisitor) - - [WithEnvironment](#withenvironment) + - [WithScope](#withscope) - [Language server](#language-server) - [textDocument/formatting](#textdocumentformatting) - [textDocument/inlayHint](#textdocumentinlayhint) @@ -588,20 +588,18 @@ SyntaxTree::Formatter.format(source, program.accept(visitor)) # => "if (a = 1)\nend\n" ``` -### WithEnvironment +### WithScope -The `WithEnvironment` module can be included in visitors to automatically keep track of local variables and arguments -defined inside each environment. A `current_environment` accessor is made available to the request, allowing it to find -all usages and definitions of a local. +The `WithScope` module can be included in visitors to automatically keep track of local variables and arguments defined inside each scope. A `current_scope` accessor is made available to the request, allowing it to find all usages and definitions of a local. ```ruby class MyVisitor < Visitor - include WithEnvironment + prepend WithScope def visit_ident(node) # find_local will return a Local for any local variables or arguments # present in the current environment or nil if the identifier is not a local - local = current_environment.find_local(node) + local = current_scope.find_local(node) puts local.type # the type of the local (:variable or :argument) puts local.definitions # the array of locations where this local is defined diff --git a/lib/syntax_tree.rb b/lib/syntax_tree.rb index 70126b14..4e183383 100644 --- a/lib/syntax_tree.rb +++ b/lib/syntax_tree.rb @@ -33,7 +33,7 @@ module SyntaxTree autoload :PrettyPrintVisitor, "syntax_tree/pretty_print_visitor" autoload :Search, "syntax_tree/search" autoload :Translation, "syntax_tree/translation" - autoload :WithEnvironment, "syntax_tree/with_environment" + autoload :WithScope, "syntax_tree/with_scope" autoload :YARV, "syntax_tree/yarv" # This holds references to objects that respond to both #parse and #format diff --git a/lib/syntax_tree/with_environment.rb b/lib/syntax_tree/with_scope.rb similarity index 65% rename from lib/syntax_tree/with_environment.rb rename to lib/syntax_tree/with_scope.rb index 3a6f04b9..efa8d075 100644 --- a/lib/syntax_tree/with_environment.rb +++ b/lib/syntax_tree/with_scope.rb @@ -1,18 +1,18 @@ # frozen_string_literal: true module SyntaxTree - # WithEnvironment is a module intended to be included in classes inheriting - # from Visitor. The module overrides a few visit methods to automatically keep - # track of local variables and arguments defined in the current environment. + # WithScope is a module intended to be included in classes inheriting from + # Visitor. The module overrides a few visit methods to automatically keep + # track of local variables and arguments defined in the current scope. # Example usage: # # class MyVisitor < Visitor - # include WithEnvironment + # include WithScope # # def visit_ident(node) # # Check if we're visiting an identifier for an argument, a local # # variable or something else - # local = current_environment.find_local(node) + # local = current_scope.find_local(node) # # if local.type == :argument # # handle identifiers for arguments @@ -24,11 +24,11 @@ module SyntaxTree # end # end # - module WithEnvironment - # The environment class is used to keep track of local variables and - # arguments inside a particular scope - class Environment - # This class tracks the occurrences of a local variable or argument + module WithScope + # The scope class is used to keep track of local variables and arguments + # inside a particular scope. + class Scope + # This class tracks the occurrences of a local variable or argument. class Local # [Symbol] The type of the local (e.g. :argument, :variable) attr_reader :type @@ -55,20 +55,20 @@ def add_usage(location) end end - # [Integer] a unique identifier for this environment + # [Integer] a unique identifier for this scope attr_reader :id + # [scope | nil] The parent scope + attr_reader :parent + # [Hash[String, Local]] The local variables and arguments defined in this - # environment + # scope attr_reader :locals - # [Environment | nil] The parent environment - attr_reader :parent - def initialize(id, parent = nil) @id = id - @locals = {} @parent = parent + @locals = {} end # Adding a local definition will either insert a new entry in the locals @@ -97,7 +97,7 @@ def add_local_usage(identifier, type) resolve_local(name, type).add_usage(identifier.location) end - # Try to find the local given its name in this environment or any of its + # Try to find the local given its name in this scope or any of its # parents. def find_local(name) locals[name] || parent&.find_local(name) @@ -117,44 +117,35 @@ def resolve_local(name, type) end end + attr_reader :current_scope + def initialize(*args, **kwargs, &block) super - @environment_id = 0 - end - - def current_environment - @current_environment ||= Environment.new(next_environment_id) - end - def with_new_environment(parent_environment = nil) - previous_environment = @current_environment - @current_environment = - Environment.new(next_environment_id, parent_environment) - yield - ensure - @current_environment = previous_environment + @current_scope = Scope.new(0) + @next_scope_id = 0 end - # Visits for nodes that create new environments, such as classes, modules + # Visits for nodes that create new scopes, such as classes, modules # and method definitions. def visit_class(node) - with_new_environment { super } + with_scope { super } end def visit_module(node) - with_new_environment { super } + with_scope { super } end - # When we find a method invocation with a block, only the code that - # happens inside of the block needs a fresh environment. The method - # invocation itself happens in the same environment. + # When we find a method invocation with a block, only the code that happens + # inside of the block needs a fresh scope. The method invocation + # itself happens in the same scope. def visit_method_add_block(node) visit(node.call) - with_new_environment(current_environment) { visit(node.block) } + with_scope(current_scope) { visit(node.block) } end def visit_def(node) - with_new_environment { super } + with_scope { super } end # Visit for keeping track of local arguments, such as method and block @@ -163,15 +154,15 @@ def visit_params(node) add_argument_definitions(node.requireds) node.posts.each do |param| - current_environment.add_local_definition(param, :argument) + current_scope.add_local_definition(param, :argument) end node.keywords.each do |param| - current_environment.add_local_definition(param.first, :argument) + current_scope.add_local_definition(param.first, :argument) end node.optionals.each do |param| - current_environment.add_local_definition(param.first, :argument) + current_scope.add_local_definition(param.first, :argument) end super @@ -179,21 +170,21 @@ def visit_params(node) def visit_rest_param(node) name = node.name - current_environment.add_local_definition(name, :argument) if name + current_scope.add_local_definition(name, :argument) if name super end def visit_kwrest_param(node) name = node.name - current_environment.add_local_definition(name, :argument) if name + current_scope.add_local_definition(name, :argument) if name super end def visit_blockarg(node) name = node.name - current_environment.add_local_definition(name, :argument) if name + current_scope.add_local_definition(name, :argument) if name super end @@ -201,10 +192,7 @@ def visit_blockarg(node) # Visit for keeping track of local variable definitions def visit_var_field(node) value = node.value - - if value.is_a?(SyntaxTree::Ident) - current_environment.add_local_definition(value, :variable) - end + current_scope.add_local_definition(value, :variable) if value.is_a?(Ident) super end @@ -215,12 +203,9 @@ def visit_var_field(node) def visit_var_ref(node) value = node.value - if value.is_a?(SyntaxTree::Ident) - definition = current_environment.find_local(value.value) - - if definition - current_environment.add_local_usage(value, definition.type) - end + if value.is_a?(Ident) + definition = current_scope.find_local(value.value) + current_scope.add_local_usage(value, definition.type) if definition end super @@ -233,13 +218,21 @@ def add_argument_definitions(list) if param.is_a?(SyntaxTree::MLHSParen) add_argument_definitions(param.contents.parts) else - current_environment.add_local_definition(param, :argument) + current_scope.add_local_definition(param, :argument) end end end - def next_environment_id - @environment_id += 1 + def next_scope_id + @next_scope_id += 1 + end + + def with_scope(parent_scope = nil) + previous_scope = @current_scope + @current_scope = Scope.new(next_scope_id, parent_scope) + yield + ensure + @current_scope = previous_scope end end end diff --git a/test/with_environment_test.rb b/test/with_scope_test.rb similarity index 95% rename from test/with_environment_test.rb rename to test/with_scope_test.rb index b6f79c14..1a4c5468 100644 --- a/test/with_environment_test.rb +++ b/test/with_scope_test.rb @@ -3,9 +3,9 @@ require_relative "test_helper" module SyntaxTree - class WithEnvironmentTest < Minitest::Test + class WithScopeTest < Minitest::Test class Collector < Visitor - prepend WithEnvironment + prepend WithScope attr_reader :arguments, :variables @@ -21,22 +21,22 @@ def self.collect(source) visit_methods do def visit_ident(node) value = node.value.delete_suffix(":") - local = current_environment.find_local(node.value) + local = current_scope.find_local(node.value) case local&.type when :argument - arguments[[current_environment.id, value]] = local + arguments[[current_scope.id, value]] = local when :variable - variables[[current_environment.id, value]] = local + variables[[current_scope.id, value]] = local end end def visit_label(node) value = node.value.delete_suffix(":") - local = current_environment.find_local(value) + local = current_scope.find_local(value) if local&.type == :argument - arguments[[current_environment.id, value]] = node + arguments[[current_scope.id, value]] = node end end end @@ -350,7 +350,7 @@ def test_double_nested_arguments end class Resolver < Visitor - prepend WithEnvironment + prepend WithScope attr_reader :locals @@ -364,10 +364,10 @@ def visit_assign(node) level = 0 name = node.target.value.value - environment = current_environment - while !environment.locals.key?(name) && !environment.parent.nil? + scope = current_scope + while !scope.locals.key?(name) && !scope.parent.nil? level += 1 - environment = environment.parent + scope = scope.parent end locals << [name, level]