diff --git a/.rubocop.yml b/.rubocop.yml index 33636c44..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 @@ -117,6 +120,9 @@ Style/FormatStringToken: Style/GuardClause: Enabled: false +Style/HashLikeCase: + Enabled: false + Style/IdenticalConditionalBranches: Enabled: false diff --git a/README.md b/README.md index 6ca9b01a..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) @@ -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| @@ -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 e5bc5ab5..4e183383 100644 --- a/lib/syntax_tree.rb +++ b/lib/syntax_tree.rb @@ -1,58 +1,41 @@ # frozen_string_literal: true -require "cgi" -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/dsl" -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/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" +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 # 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 :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 :WithScope, "syntax_tree/with_scope" + 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 = {} @@ -71,40 +54,80 @@ 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 + # Parses the given source and returns the formatted source. + def self.format( + source, + 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 source and returns the syntax tree. - def self.parse(source) - parser = Parser.new(source) - response = parser.parse - response unless parser.error? + # 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 - # Parses the given source and returns the formatted source. - def self.format( + # 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 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 + visitor = MutationVisitor.new yield visitor 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 +143,24 @@ 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 + pattern = Pattern.new(query).compile + program = parse(source) - # 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) + Search.new(pattern).scan(program, &block) 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) + # 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/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/visitor/field_visitor.rb b/lib/syntax_tree/field_visitor.rb similarity index 91% rename from lib/syntax_tree/visitor/field_visitor.rb rename to lib/syntax_tree/field_visitor.rb index 6e643e09..ca1df55b 100644 --- a/lib/syntax_tree/visitor/field_visitor.rb +++ b/lib/syntax_tree/field_visitor.rb @@ -1,55 +1,54 @@ # 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 + # 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 + visit_methods do def visit_aref(node) node(node, "aref") do field("collection", node.collection) @@ -1017,14 +1016,14 @@ def visit_zsuper(node) def visit___end__(node) visit_token(node, "__end__") end + end - private + private - def visit_token(node, type) - node(node, type) do - field("value", node.value) - comments(node) - end + def visit_token(node, type) + node(node, type) do + field("value", node.value) + comments(node) end end end 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/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..6ec81030 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,162 @@ 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 + + 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 + + # 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 + 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 new file mode 100644 index 00000000..68ea4734 --- /dev/null +++ b/lib/syntax_tree/mermaid.rb @@ -0,0 +1,177 @@ +# frozen_string_literal: true + +require "cgi" +require "stringio" + +module SyntaxTree + # This module is responsible for rendering mermaid (https://mermaid.js.org/) + # flow charts. + module Mermaid + # 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 dotted].freeze + COLORS = %i[green red].freeze + + attr_reader :from, :to, :label, :type, :color + + def initialize(from, to, label, type, color) + raise unless TYPES.include?(type) + raise if color && !COLORS.include?(color) + + @from = from + @to = to + @label = label + @type = type + @color = 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 + %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 + + 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 = bounds + "#{id}#{left_bound}#{Mermaid.escape(label)}#{right_bound}" + end + + private + + def bounds + case shape + when :circle + %w[(( ))] + when :rectangle + ["[", "]"] + when :rounded + %w[( )] + when :stadium + ["([", "])"] + end + end + end + + 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 + + # 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 + end + end +end diff --git a/lib/syntax_tree/mermaid_visitor.rb b/lib/syntax_tree/mermaid_visitor.rb new file mode 100644 index 00000000..fc9f6706 --- /dev/null +++ b/lib/syntax_tree/mermaid_visitor.rb @@ -0,0 +1,69 @@ +# 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/visitor/mutation_visitor.rb b/lib/syntax_tree/mutation_visitor.rb similarity index 94% rename from lib/syntax_tree/visitor/mutation_visitor.rb rename to lib/syntax_tree/mutation_visitor.rb index 65f8c5ba..0b4b9357 100644 --- a/lib/syntax_tree/visitor/mutation_visitor.rb +++ b/lib/syntax_tree/mutation_visitor.rb @@ -1,39 +1,39 @@ # 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 + # 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 + def initialize + @mutations = [] + 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) + # 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 - mutations.each do |(pattern, mutation)| - result = mutation.call(result) if pattern.call(result) - 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) - result + mutations.each do |(pattern, mutation)| + result = mutation.call(result) if pattern.call(result) end + result + end + + visit_methods do # Visit a BEGINBlock node. def visit_BEGIN(node) node.copy( 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/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/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/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/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/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 2b06049a..00000000 --- a/lib/syntax_tree/visitor/mermaid_visitor.rb +++ /dev/null @@ -1,81 +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 :output, :target - - def initialize - @output = StringIO.new - @output.puts("flowchart TD") - - @target = nil - end - - def visit_program(node) - super - output.string - end - - private - - def comments(node) - # Ignore - end - - 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 - else - node_id = "#{target}_#{name}" - output.puts(" #{node_id}([\"#{CGI.escapeHTML(value.inspect)}\"])") - output.puts(" #{target} -- \"#{name}\" --> #{node_id}") - 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 = "node_#{node.object_id}" - - yield - - output.puts(" #{@target}[\"#{type}\"]") - @target - ensure - @target = previous_target - end - end - - 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 - end - end - - def text(name, value) - field(name, value) - 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/visitor/with_environment.rb deleted file mode 100644 index 59033d50..00000000 --- a/lib/syntax_tree/visitor/with_environment.rb +++ /dev/null @@ -1,140 +0,0 @@ -# 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. - # 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) - # - # 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 - module WithEnvironment - def current_environment - @current_environment ||= Environment.new - end - - def with_new_environment - previous_environment = @current_environment - @current_environment = Environment.new(previous_environment) - yield - ensure - @current_environment = previous_environment - end - - # Visits for nodes that create new environments, such as classes, modules - # and method definitions - def visit_class(node) - with_new_environment { super } - end - - 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 - def visit_method_add_block(node) - visit(node.call) - with_new_environment { visit(node.block) } - end - - def visit_def(node) - with_new_environment { super } - end - - # Visit for keeping track of local arguments, such as method and block - # arguments - def visit_params(node) - add_argument_definitions(node.requireds) - - node.posts.each do |param| - current_environment.add_local_definition(param, :argument) - end - - node.keywords.each do |param| - current_environment.add_local_definition(param.first, :argument) - end - - node.optionals.each do |param| - current_environment.add_local_definition(param.first, :argument) - end - - super - end - - def visit_rest_param(node) - name = node.name - current_environment.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 - - super - end - - def visit_blockarg(node) - name = node.name - current_environment.add_local_definition(name, :argument) if name - - super - end - - # 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 - - super - end - - alias visit_pinned_var_ref visit_var_field - - # Visits for keeping track of variable and argument usages - 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 - end - - super - end - - private - - def add_argument_definitions(list) - list.each do |param| - if param.is_a?(SyntaxTree::MLHSParen) - add_argument_definitions(param.contents.parts) - else - current_environment.add_local_definition(param, :argument) - end - end - end - end -end diff --git a/lib/syntax_tree/with_scope.rb b/lib/syntax_tree/with_scope.rb new file mode 100644 index 00000000..efa8d075 --- /dev/null +++ b/lib/syntax_tree/with_scope.rb @@ -0,0 +1,238 @@ +# frozen_string_literal: true + +module SyntaxTree + # 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 WithScope + # + # def visit_ident(node) + # # Check if we're visiting an identifier for an argument, a local + # # variable or something else + # local = current_scope.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 + # + 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 + + # [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 + + def initialize(type) + @type = type + @definitions = [] + @usages = [] + end + + def add_definition(location) + @definitions << location + end + + def add_usage(location) + @usages << location + end + end + + # [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 + # scope + attr_reader :locals + + def initialize(id, parent = nil) + @id = id + @parent = parent + @locals = {} + 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. + def add_local_definition(identifier, type) + name = identifier.value.delete_suffix(":") + + 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 + # 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. + def add_local_usage(identifier, type) + name = identifier.value.delete_suffix(":") + resolve_local(name, type).add_usage(identifier.location) + end + + # 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) + end + + private + + def resolve_local(name, type) + local = find_local(name) + + unless local + local = Local.new(type) + locals[name] = local + end + + local + end + end + + attr_reader :current_scope + + def initialize(*args, **kwargs, &block) + super + + @current_scope = Scope.new(0) + @next_scope_id = 0 + end + + # Visits for nodes that create new scopes, such as classes, modules + # and method definitions. + def visit_class(node) + with_scope { super } + end + + def visit_module(node) + 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 scope. The method invocation + # itself happens in the same scope. + def visit_method_add_block(node) + visit(node.call) + with_scope(current_scope) { visit(node.block) } + end + + def visit_def(node) + with_scope { super } + end + + # Visit for keeping track of local arguments, such as method and block + # arguments. + def visit_params(node) + add_argument_definitions(node.requireds) + + node.posts.each do |param| + current_scope.add_local_definition(param, :argument) + end + + node.keywords.each do |param| + current_scope.add_local_definition(param.first, :argument) + end + + node.optionals.each do |param| + current_scope.add_local_definition(param.first, :argument) + end + + super + end + + def visit_rest_param(node) + name = node.name + current_scope.add_local_definition(name, :argument) if name + + super + end + + def visit_kwrest_param(node) + name = node.name + current_scope.add_local_definition(name, :argument) if name + + super + end + + def visit_blockarg(node) + name = node.name + current_scope.add_local_definition(name, :argument) if name + + super + end + + # Visit for keeping track of local variable definitions + def visit_var_field(node) + value = node.value + current_scope.add_local_definition(value, :variable) if value.is_a?(Ident) + + super + end + + alias visit_pinned_var_ref visit_var_field + + # Visits for keeping track of variable and argument usages + def visit_var_ref(node) + value = node.value + + if value.is_a?(Ident) + definition = current_scope.find_local(value.value) + current_scope.add_local_usage(value, definition.type) if definition + end + + super + end + + private + + def add_argument_definitions(list) + list.each do |param| + if param.is_a?(SyntaxTree::MLHSParen) + add_argument_definitions(param.contents.parts) + else + current_scope.add_local_definition(param, :argument) + end + end + end + + 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/lib/syntax_tree/yarv.rb b/lib/syntax_tree/yarv.rb index 7e4da7bb..bd5c54b9 100644 --- a/lib/syntax_tree/yarv.rb +++ b/lib/syntax_tree/yarv.rb @@ -1,5 +1,23 @@ # frozen_string_literal: true +require "stringio" + +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 diff --git a/lib/syntax_tree/yarv/compiler.rb b/lib/syntax_tree/yarv/compiler.rb index e1a8544a..bd20bc19 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. @@ -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/lib/syntax_tree/yarv/control_flow_graph.rb b/lib/syntax_tree/yarv/control_flow_graph.rb index 73d30208..2829bb21 100644 --- a/lib/syntax_tree/yarv/control_flow_graph.rb +++ b/lib/syntax_tree/yarv/control_flow_graph.rb @@ -208,38 +208,38 @@ def to_son end def to_mermaid - output = StringIO.new - output.puts("flowchart TD") - - 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 + 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 - output.puts(" end") - end - - blocks.each do |block| - block.outgoing_blocks.each do |outgoing| - offset = - block.block_start + block.insns.sum(&:length) - - block.insns.last.length + blocks.each do |block| + block.outgoing_blocks.each do |outgoing| + offset = + 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.link(from, to) + end end end - - output.string 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 ace40296..aedee9ba 100644 --- a/lib/syntax_tree/yarv/data_flow_graph.rb +++ b/lib/syntax_tree/yarv/data_flow_graph.rb @@ -125,64 +125,54 @@ def to_son end def to_mermaid - output = StringIO.new - output.puts("flowchart TD") + Mermaid.flowchart do |flowchart| + disasm = Disassembler::Squished.new - fmt = Disassembler::Mermaid.new - links = [] - - 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 - - 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)}\")") + 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 - if previous - output.puts(" #{previous} --> #{node_id}") - links << "red" - end + 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 - insn_flows[length].in.each do |input| - if input.is_a?(Integer) - output.puts(" node_#{input} --> #{node_id}") - links << "green" + previous = node end end - - previous = node_id end - output.puts(" end") - end - - blocks.each do |block| - block.outgoing_blocks.each do |outgoing| - offset = - block.block_start + block.insns.sum(&:length) - - block.insns.last.length + blocks.each do |block| + block.outgoing_blocks.each do |outgoing| + offset = + 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 end - - links.each_with_index do |color, index| - output.puts(" linkStyle #{index} stroke:#{color}") - end - - output.string 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/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. 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 deleted file mode 100644 index cc4007fe..00000000 --- a/test/visitor_with_environment_test.rb +++ /dev/null @@ -1,659 +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 - - 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 - - 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 - - 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 - - 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_scope_test.rb b/test/with_scope_test.rb new file mode 100644 index 00000000..1a4c5468 --- /dev/null +++ b/test/with_scope_test.rb @@ -0,0 +1,457 @@ +# frozen_string_literal: true + +require_relative "test_helper" + +module SyntaxTree + class WithScopeTest < Minitest::Test + class Collector < Visitor + prepend WithScope + + 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_scope.find_local(node.value) + + case local&.type + when :argument + arguments[[current_scope.id, value]] = local + when :variable + variables[[current_scope.id, value]] = local + end + end + + def visit_label(node) + value = node.value.delete_suffix(":") + local = current_scope.find_local(value) + + if local&.type == :argument + arguments[[current_scope.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 WithScope + + attr_reader :locals + + def initialize + @locals = [] + end + + visit_methods do + def visit_assign(node) + super.tap do + level = 0 + name = node.target.value.value + + scope = current_scope + while !scope.locals.key?(name) && !scope.parent.nil? + level += 1 + scope = scope.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 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