# frozen_string_literal: true module Prism module Translation class Parser # A visitor that knows how to convert a prism syntax tree into the # whitequark/parser gem's syntax tree. class Compiler < ::Prism::Compiler # Raised when the tree is malformed or there is a bug in the compiler. class CompilationError < StandardError end # The Parser::Base instance that is being used to build the AST. attr_reader :parser # The Parser::Builders::Default instance that is being used to build the # AST. attr_reader :builder # The Parser::Source::Buffer instance that is holding a reference to the # source code. attr_reader :source_buffer # The offset cache that is used to map between byte and character # offsets in the file. attr_reader :offset_cache # The types of values that can be forwarded in the current scope. attr_reader :forwarding # Whether or not the current node is in a destructure. attr_reader :in_destructure # Whether or not the current node is in a pattern. attr_reader :in_pattern # Initialize a new compiler with the given parser, offset cache, and # options. def initialize(parser, offset_cache, forwarding: [], in_destructure: false, in_pattern: false) @parser = parser @builder = parser.builder @source_buffer = parser.source_buffer @offset_cache = offset_cache @forwarding = forwarding @in_destructure = in_destructure @in_pattern = in_pattern end # alias foo bar # ^^^^^^^^^^^^^ def visit_alias_method_node(node) builder.alias(token(node.keyword_loc), visit(node.new_name), visit(node.old_name)) end # alias $foo $bar # ^^^^^^^^^^^^^^^ def visit_alias_global_variable_node(node) builder.alias(token(node.keyword_loc), visit(node.new_name), visit(node.old_name)) end # foo => bar | baz # ^^^^^^^^^ def visit_alternation_pattern_node(node) builder.match_alt(visit(node.left), token(node.operator_loc), visit(node.right)) end # a and b # ^^^^^^^ def visit_and_node(node) builder.logical_op(:and, visit(node.left), token(node.operator_loc), visit(node.right)) end # [] # ^^ def visit_array_node(node) builder.array(token(node.opening_loc), visit_all(node.elements), token(node.closing_loc)) end # foo => [bar] # ^^^^^ def visit_array_pattern_node(node) elements = [*node.requireds] elements << node.rest if !node.rest.nil? && !node.rest.is_a?(ImplicitRestNode) elements.concat(node.posts) visited = visit_all(elements) if node.rest.is_a?(ImplicitRestNode) visited[-1] = builder.match_with_trailing_comma(visited[-1], token(node.rest.location)) end if node.constant if visited.empty? builder.const_pattern(visit(node.constant), token(node.opening_loc), builder.array_pattern(token(node.opening_loc), visited, token(node.closing_loc)), token(node.closing_loc)) else builder.const_pattern(visit(node.constant), token(node.opening_loc), builder.array_pattern(nil, visited, nil), token(node.closing_loc)) end else builder.array_pattern(token(node.opening_loc), visited, token(node.closing_loc)) end end # foo(bar) # ^^^ def visit_arguments_node(node) visit_all(node.arguments) end # { a: 1 } # ^^^^ def visit_assoc_node(node) key = node.key if in_pattern if node.value.is_a?(ImplicitNode) if key.is_a?(SymbolNode) if key.opening.nil? builder.match_hash_var([key.unescaped, srange(key.location)]) else builder.match_hash_var_from_str(token(key.opening_loc), [builder.string_internal([key.unescaped, srange(key.value_loc)])], token(key.closing_loc)) end else builder.match_hash_var_from_str(token(key.opening_loc), visit_all(key.parts), token(key.closing_loc)) end elsif key.opening.nil? builder.pair_keyword([key.unescaped, srange(key.location)], visit(node.value)) else builder.pair_quoted(token(key.opening_loc), [builder.string_internal([key.unescaped, srange(key.value_loc)])], token(key.closing_loc), visit(node.value)) end elsif node.value.is_a?(ImplicitNode) if (value = node.value.value).is_a?(LocalVariableReadNode) builder.pair_keyword( [key.unescaped, srange(key)], builder.ident([value.name, srange(key.value_loc)]).updated(:lvar) ) else builder.pair_label([key.unescaped, srange(key.location)]) end elsif node.operator_loc builder.pair(visit(key), token(node.operator_loc), visit(node.value)) elsif key.is_a?(SymbolNode) && key.opening_loc.nil? builder.pair_keyword([key.unescaped, srange(key.location)], visit(node.value)) else parts = if key.is_a?(SymbolNode) [builder.string_internal([key.unescaped, srange(key.value_loc)])] else visit_all(key.parts) end builder.pair_quoted(token(key.opening_loc), parts, token(key.closing_loc), visit(node.value)) end end # def foo(**); bar(**); end # ^^ # # { **foo } # ^^^^^ def visit_assoc_splat_node(node) if in_pattern builder.match_rest(token(node.operator_loc), token(node.value&.location)) elsif node.value.nil? && forwarding.include?(:**) builder.forwarded_kwrestarg(token(node.operator_loc)) else builder.kwsplat(token(node.operator_loc), visit(node.value)) end end # $+ # ^^ def visit_back_reference_read_node(node) builder.back_ref(token(node.location)) end # begin end # ^^^^^^^^^ def visit_begin_node(node) rescue_bodies = [] if (rescue_clause = node.rescue_clause) begin find_start_offset = (rescue_clause.reference&.location || rescue_clause.exceptions.last&.location || rescue_clause.keyword_loc).end_offset find_end_offset = (rescue_clause.statements&.location&.start_offset || rescue_clause.subsequent&.location&.start_offset || (find_start_offset + 1)) rescue_bodies << builder.rescue_body( token(rescue_clause.keyword_loc), rescue_clause.exceptions.any? ? builder.array(nil, visit_all(rescue_clause.exceptions), nil) : nil, token(rescue_clause.operator_loc), visit(rescue_clause.reference), srange_find(find_start_offset, find_end_offset, ";"), visit(rescue_clause.statements) ) end until (rescue_clause = rescue_clause.subsequent).nil? end begin_body = builder.begin_body( visit(node.statements), rescue_bodies, token(node.else_clause&.else_keyword_loc), visit(node.else_clause), token(node.ensure_clause&.ensure_keyword_loc), visit(node.ensure_clause&.statements) ) if node.begin_keyword_loc builder.begin_keyword(token(node.begin_keyword_loc), begin_body, token(node.end_keyword_loc)) else begin_body end end # foo(&bar) # ^^^^ def visit_block_argument_node(node) builder.block_pass(token(node.operator_loc), visit(node.expression)) end # foo { |; bar| } # ^^^ def visit_block_local_variable_node(node) builder.shadowarg(token(node.location)) end # A block on a keyword or method call. def visit_block_node(node) raise CompilationError, "Cannot directly compile block nodes" end # def foo(&bar); end # ^^^^ def visit_block_parameter_node(node) builder.blockarg(token(node.operator_loc), token(node.name_loc)) end # A block's parameters. def visit_block_parameters_node(node) [*visit(node.parameters)].concat(visit_all(node.locals)) end # break # ^^^^^ # # break foo # ^^^^^^^^^ def visit_break_node(node) builder.keyword_cmd(:break, token(node.keyword_loc), nil, visit(node.arguments) || [], nil) end # foo # ^^^ # # foo.bar # ^^^^^^^ # # foo.bar() {} # ^^^^^^^^^^^^ def visit_call_node(node) name = node.name arguments = node.arguments&.arguments || [] block = node.block if block.is_a?(BlockArgumentNode) arguments = [*arguments, block] block = nil end if node.call_operator_loc.nil? case name when :-@ case (receiver = node.receiver).type when :integer_node, :float_node, :rational_node, :imaginary_node return visit(numeric_negate(node.message_loc, receiver)) end when :! return visit_block(builder.not_op(token(node.message_loc), token(node.opening_loc), visit(node.receiver), token(node.closing_loc)), block) when :=~ if (receiver = node.receiver).is_a?(RegularExpressionNode) return builder.match_op(visit(receiver), token(node.message_loc), visit(node.arguments.arguments.first)) end when :[] return visit_block(builder.index(visit(node.receiver), token(node.opening_loc), visit_all(arguments), token(node.closing_loc)), block) when :[]= if node.message != "[]=" && node.arguments && block.nil? && !node.safe_navigation? arguments = node.arguments.arguments[...-1] arguments << node.block if node.block return visit_block( builder.assign( builder.index_asgn( visit(node.receiver), token(node.opening_loc), visit_all(arguments), token(node.closing_loc), ), srange_find(node.message_loc.end_offset, node.arguments.arguments.last.location.start_offset, "="), visit(node.arguments.arguments.last) ), block ) end end end message_loc = node.message_loc call_operator_loc = node.call_operator_loc call_operator = [{ "." => :dot, "&." => :anddot, "::" => "::" }.fetch(call_operator_loc.slice), srange(call_operator_loc)] if call_operator_loc visit_block( if name.end_with?("=") && !message_loc.slice.end_with?("=") && node.arguments && block.nil? builder.assign( builder.attr_asgn(visit(node.receiver), call_operator, token(message_loc)), srange_find(message_loc.end_offset, node.arguments.location.start_offset, "="), visit(node.arguments.arguments.last) ) else builder.call_method( visit(node.receiver), call_operator, message_loc ? [node.name, srange(message_loc)] : nil, token(node.opening_loc), visit_all(arguments), token(node.closing_loc) ) end, block ) end # foo.bar += baz # ^^^^^^^^^^^^^^^ def visit_call_operator_write_node(node) call_operator_loc = node.call_operator_loc builder.op_assign( builder.call_method( visit(node.receiver), call_operator_loc.nil? ? nil : [{ "." => :dot, "&." => :anddot, "::" => "::" }.fetch(call_operator_loc.slice), srange(call_operator_loc)], node.message_loc ? [node.read_name, srange(node.message_loc)] : nil, nil, [], nil ), [node.binary_operator_loc.slice.chomp("="), srange(node.binary_operator_loc)], visit(node.value) ) end # foo.bar &&= baz # ^^^^^^^^^^^^^^^ def visit_call_and_write_node(node) call_operator_loc = node.call_operator_loc builder.op_assign( builder.call_method( visit(node.receiver), call_operator_loc.nil? ? nil : [{ "." => :dot, "&." => :anddot, "::" => "::" }.fetch(call_operator_loc.slice), srange(call_operator_loc)], node.message_loc ? [node.read_name, srange(node.message_loc)] : nil, nil, [], nil ), [node.operator_loc.slice.chomp("="), srange(node.operator_loc)], visit(node.value) ) end # foo.bar ||= baz # ^^^^^^^^^^^^^^^ def visit_call_or_write_node(node) call_operator_loc = node.call_operator_loc builder.op_assign( builder.call_method( visit(node.receiver), call_operator_loc.nil? ? nil : [{ "." => :dot, "&." => :anddot, "::" => "::" }.fetch(call_operator_loc.slice), srange(call_operator_loc)], node.message_loc ? [node.read_name, srange(node.message_loc)] : nil, nil, [], nil ), [node.operator_loc.slice.chomp("="), srange(node.operator_loc)], visit(node.value) ) end # foo.bar, = 1 # ^^^^^^^ def visit_call_target_node(node) call_operator_loc = node.call_operator_loc builder.attr_asgn( visit(node.receiver), call_operator_loc.nil? ? nil : [{ "." => :dot, "&." => :anddot, "::" => "::" }.fetch(call_operator_loc.slice), srange(call_operator_loc)], token(node.message_loc) ) end # foo => bar => baz # ^^^^^^^^^^ def visit_capture_pattern_node(node) builder.match_as(visit(node.value), token(node.operator_loc), visit(node.target)) end # case foo; when bar; end # ^^^^^^^^^^^^^^^^^^^^^^^ def visit_case_node(node) builder.case( token(node.case_keyword_loc), visit(node.predicate), visit_all(node.conditions), token(node.else_clause&.else_keyword_loc), visit(node.else_clause), token(node.end_keyword_loc) ) end # case foo; in bar; end # ^^^^^^^^^^^^^^^^^^^^^ def visit_case_match_node(node) builder.case_match( token(node.case_keyword_loc), visit(node.predicate), visit_all(node.conditions), token(node.else_clause&.else_keyword_loc), visit(node.else_clause), token(node.end_keyword_loc) ) end # class Foo; end # ^^^^^^^^^^^^^^ def visit_class_node(node) builder.def_class( token(node.class_keyword_loc), visit(node.constant_path), token(node.inheritance_operator_loc), visit(node.superclass), node.body&.accept(copy_compiler(forwarding: [])), token(node.end_keyword_loc) ) end # @@foo # ^^^^^ def visit_class_variable_read_node(node) builder.cvar(token(node.location)) end # @@foo = 1 # ^^^^^^^^^ def visit_class_variable_write_node(node) builder.assign( builder.assignable(builder.cvar(token(node.name_loc))), token(node.operator_loc), visit(node.value) ) end # @@foo += bar # ^^^^^^^^^^^^ def visit_class_variable_operator_write_node(node) builder.op_assign( builder.assignable(builder.cvar(token(node.name_loc))), [node.binary_operator_loc.slice.chomp("="), srange(node.binary_operator_loc)], visit(node.value) ) end # @@foo &&= bar # ^^^^^^^^^^^^^ def visit_class_variable_and_write_node(node) builder.op_assign( builder.assignable(builder.cvar(token(node.name_loc))), [node.operator_loc.slice.chomp("="), srange(node.operator_loc)], visit(node.value) ) end # @@foo ||= bar # ^^^^^^^^^^^^^ def visit_class_variable_or_write_node(node) builder.op_assign( builder.assignable(builder.cvar(token(node.name_loc))), [node.operator_loc.slice.chomp("="), srange(node.operator_loc)], visit(node.value) ) end # @@foo, = bar # ^^^^^ def visit_class_variable_target_node(node) builder.assignable(builder.cvar(token(node.location))) end # Foo # ^^^ def visit_constant_read_node(node) builder.const([node.name, srange(node.location)]) end # Foo = 1 # ^^^^^^^ # # Foo, Bar = 1 # ^^^ ^^^ def visit_constant_write_node(node) builder.assign(builder.assignable(builder.const([node.name, srange(node.name_loc)])), token(node.operator_loc), visit(node.value)) end # Foo += bar # ^^^^^^^^^^^ def visit_constant_operator_write_node(node) builder.op_assign( builder.assignable(builder.const([node.name, srange(node.name_loc)])), [node.binary_operator_loc.slice.chomp("="), srange(node.binary_operator_loc)], visit(node.value) ) end # Foo &&= bar # ^^^^^^^^^^^^ def visit_constant_and_write_node(node) builder.op_assign( builder.assignable(builder.const([node.name, srange(node.name_loc)])), [node.operator_loc.slice.chomp("="), srange(node.operator_loc)], visit(node.value) ) end # Foo ||= bar # ^^^^^^^^^^^^ def visit_constant_or_write_node(node) builder.op_assign( builder.assignable(builder.const([node.name, srange(node.name_loc)])), [node.operator_loc.slice.chomp("="), srange(node.operator_loc)], visit(node.value) ) end # Foo, = bar # ^^^ def visit_constant_target_node(node) builder.assignable(builder.const([node.name, srange(node.location)])) end # Foo::Bar # ^^^^^^^^ def visit_constant_path_node(node) if node.parent.nil? builder.const_global( token(node.delimiter_loc), [node.name, srange(node.name_loc)] ) else builder.const_fetch( visit(node.parent), token(node.delimiter_loc), [node.name, srange(node.name_loc)] ) end end # Foo::Bar = 1 # ^^^^^^^^^^^^ # # Foo::Foo, Bar::Bar = 1 # ^^^^^^^^ ^^^^^^^^ def visit_constant_path_write_node(node) builder.assign( builder.assignable(visit(node.target)), token(node.operator_loc), visit(node.value) ) end # Foo::Bar += baz # ^^^^^^^^^^^^^^^ def visit_constant_path_operator_write_node(node) builder.op_assign( builder.assignable(visit(node.target)), [node.binary_operator_loc.slice.chomp("="), srange(node.binary_operator_loc)], visit(node.value) ) end # Foo::Bar &&= baz # ^^^^^^^^^^^^^^^^ def visit_constant_path_and_write_node(node) builder.op_assign( builder.assignable(visit(node.target)), [node.operator_loc.slice.chomp("="), srange(node.operator_loc)], visit(node.value) ) end # Foo::Bar ||= baz # ^^^^^^^^^^^^^^^^ def visit_constant_path_or_write_node(node) builder.op_assign( builder.assignable(visit(node.target)), [node.operator_loc.slice.chomp("="), srange(node.operator_loc)], visit(node.value) ) end # Foo::Bar, = baz # ^^^^^^^^ def visit_constant_path_target_node(node) builder.assignable(visit_constant_path_node(node)) end # def foo; end # ^^^^^^^^^^^^ # # def self.foo; end # ^^^^^^^^^^^^^^^^^ def visit_def_node(node) if node.equal_loc if node.receiver builder.def_endless_singleton( token(node.def_keyword_loc), visit(node.receiver.is_a?(ParenthesesNode) ? node.receiver.body : node.receiver), token(node.operator_loc), token(node.name_loc), builder.args(token(node.lparen_loc), visit(node.parameters) || [], token(node.rparen_loc), false), token(node.equal_loc), node.body&.accept(copy_compiler(forwarding: find_forwarding(node.parameters))) ) else builder.def_endless_method( token(node.def_keyword_loc), token(node.name_loc), builder.args(token(node.lparen_loc), visit(node.parameters) || [], token(node.rparen_loc), false), token(node.equal_loc), node.body&.accept(copy_compiler(forwarding: find_forwarding(node.parameters))) ) end elsif node.receiver builder.def_singleton( token(node.def_keyword_loc), visit(node.receiver.is_a?(ParenthesesNode) ? node.receiver.body : node.receiver), token(node.operator_loc), token(node.name_loc), builder.args(token(node.lparen_loc), visit(node.parameters) || [], token(node.rparen_loc), false), node.body&.accept(copy_compiler(forwarding: find_forwarding(node.parameters))), token(node.end_keyword_loc) ) else builder.def_method( token(node.def_keyword_loc), token(node.name_loc), builder.args(token(node.lparen_loc), visit(node.parameters) || [], token(node.rparen_loc), false), node.body&.accept(copy_compiler(forwarding: find_forwarding(node.parameters))), token(node.end_keyword_loc) ) end end # defined? a # ^^^^^^^^^^ # # defined?(a) # ^^^^^^^^^^^ def visit_defined_node(node) builder.keyword_cmd( :defined?, token(node.keyword_loc), token(node.lparen_loc), [visit(node.value)], token(node.rparen_loc) ) end # if foo then bar else baz end # ^^^^^^^^^^^^ def visit_else_node(node) visit(node.statements) end # "foo #{bar}" # ^^^^^^ def visit_embedded_statements_node(node) builder.begin( token(node.opening_loc), visit(node.statements), token(node.closing_loc) ) end # "foo #@bar" # ^^^^^ def visit_embedded_variable_node(node) visit(node.variable) end # begin; foo; ensure; bar; end # ^^^^^^^^^^^^ def visit_ensure_node(node) raise CompilationError, "Cannot directly compile ensure nodes" end # false # ^^^^^ def visit_false_node(node) builder.false(token(node.location)) end # foo => [*, bar, *] # ^^^^^^^^^^^ def visit_find_pattern_node(node) elements = [node.left, *node.requireds, node.right] if node.constant builder.const_pattern(visit(node.constant), token(node.opening_loc), builder.find_pattern(nil, visit_all(elements), nil), token(node.closing_loc)) else builder.find_pattern(token(node.opening_loc), visit_all(elements), token(node.closing_loc)) end end # 1.0 # ^^^ def visit_float_node(node) visit_numeric(node, builder.float([node.value, srange(node.location)])) end # for foo in bar do end # ^^^^^^^^^^^^^^^^^^^^^ def visit_for_node(node) builder.for( token(node.for_keyword_loc), visit(node.index), token(node.in_keyword_loc), visit(node.collection), if (do_keyword_loc = node.do_keyword_loc) token(do_keyword_loc) else srange_find(node.collection.location.end_offset, (node.statements&.location || node.end_keyword_loc).start_offset, ";") end, visit(node.statements), token(node.end_keyword_loc) ) end # def foo(...); bar(...); end # ^^^ def visit_forwarding_arguments_node(node) builder.forwarded_args(token(node.location)) end # def foo(...); end # ^^^ def visit_forwarding_parameter_node(node) builder.forward_arg(token(node.location)) end # super # ^^^^^ # # super {} # ^^^^^^^^ def visit_forwarding_super_node(node) visit_block( builder.keyword_cmd( :zsuper, ["super", srange_offsets(node.location.start_offset, node.location.start_offset + 5)] ), node.block ) end # $foo # ^^^^ def visit_global_variable_read_node(node) builder.gvar(token(node.location)) end # $foo = 1 # ^^^^^^^^ def visit_global_variable_write_node(node) builder.assign( builder.assignable(builder.gvar(token(node.name_loc))), token(node.operator_loc), visit(node.value) ) end # $foo += bar # ^^^^^^^^^^^ def visit_global_variable_operator_write_node(node) builder.op_assign( builder.assignable(builder.gvar(token(node.name_loc))), [node.binary_operator_loc.slice.chomp("="), srange(node.binary_operator_loc)], visit(node.value) ) end # $foo &&= bar # ^^^^^^^^^^^^ def visit_global_variable_and_write_node(node) builder.op_assign( builder.assignable(builder.gvar(token(node.name_loc))), [node.operator_loc.slice.chomp("="), srange(node.operator_loc)], visit(node.value) ) end # $foo ||= bar # ^^^^^^^^^^^^ def visit_global_variable_or_write_node(node) builder.op_assign( builder.assignable(builder.gvar(token(node.name_loc))), [node.operator_loc.slice.chomp("="), srange(node.operator_loc)], visit(node.value) ) end # $foo, = bar # ^^^^ def visit_global_variable_target_node(node) builder.assignable(builder.gvar([node.slice, srange(node.location)])) end # {} # ^^ def visit_hash_node(node) builder.associate( token(node.opening_loc), visit_all(node.elements), token(node.closing_loc) ) end # foo => {} # ^^ def visit_hash_pattern_node(node) elements = [*node.elements, *node.rest] if node.constant builder.const_pattern(visit(node.constant), token(node.opening_loc), builder.hash_pattern(nil, visit_all(elements), nil), token(node.closing_loc)) else builder.hash_pattern(token(node.opening_loc), visit_all(elements), token(node.closing_loc)) end end # if foo then bar end # ^^^^^^^^^^^^^^^^^^^ # # bar if foo # ^^^^^^^^^^ # # foo ? bar : baz # ^^^^^^^^^^^^^^^ def visit_if_node(node) if !node.if_keyword_loc builder.ternary( visit(node.predicate), token(node.then_keyword_loc), visit(node.statements), token(node.subsequent.else_keyword_loc), visit(node.subsequent) ) elsif node.if_keyword_loc.start_offset == node.location.start_offset builder.condition( token(node.if_keyword_loc), visit(node.predicate), if (then_keyword_loc = node.then_keyword_loc) token(then_keyword_loc) else srange_find(node.predicate.location.end_offset, (node.statements&.location || node.subsequent&.location || node.end_keyword_loc).start_offset, ";") end, visit(node.statements), case node.subsequent when IfNode token(node.subsequent.if_keyword_loc) when ElseNode token(node.subsequent.else_keyword_loc) end, visit(node.subsequent), if node.if_keyword != "elsif" token(node.end_keyword_loc) end ) else builder.condition_mod( visit(node.statements), visit(node.subsequent), token(node.if_keyword_loc), visit(node.predicate) ) end end # 1i # ^^ def visit_imaginary_node(node) visit_numeric(node, builder.complex([Complex(0, node.numeric.value), srange(node.location)])) end # { foo: } # ^^^^ def visit_implicit_node(node) raise CompilationError, "Cannot directly compile implicit nodes" end # foo { |bar,| } # ^ def visit_implicit_rest_node(node) raise CompilationError, "Cannot compile implicit rest nodes" end # case foo; in bar; end # ^^^^^^^^^^^^^^^^^^^^^ def visit_in_node(node) pattern = nil guard = nil case node.pattern when IfNode pattern = within_pattern { |compiler| node.pattern.statements.accept(compiler) } guard = builder.if_guard(token(node.pattern.if_keyword_loc), visit(node.pattern.predicate)) when UnlessNode pattern = within_pattern { |compiler| node.pattern.statements.accept(compiler) } guard = builder.unless_guard(token(node.pattern.keyword_loc), visit(node.pattern.predicate)) else pattern = within_pattern { |compiler| node.pattern.accept(compiler) } end builder.in_pattern( token(node.in_loc), pattern, guard, if (then_loc = node.then_loc) token(then_loc) else srange_find(node.pattern.location.end_offset, node.statements&.location&.start_offset, ";") end, visit(node.statements) ) end # foo[bar] += baz # ^^^^^^^^^^^^^^^ def visit_index_operator_write_node(node) arguments = node.arguments&.arguments || [] arguments << node.block if node.block builder.op_assign( builder.index( visit(node.receiver), token(node.opening_loc), visit_all(arguments), token(node.closing_loc) ), [node.binary_operator_loc.slice.chomp("="), srange(node.binary_operator_loc)], visit(node.value) ) end # foo[bar] &&= baz # ^^^^^^^^^^^^^^^^ def visit_index_and_write_node(node) arguments = node.arguments&.arguments || [] arguments << node.block if node.block builder.op_assign( builder.index( visit(node.receiver), token(node.opening_loc), visit_all(arguments), token(node.closing_loc) ), [node.operator_loc.slice.chomp("="), srange(node.operator_loc)], visit(node.value) ) end # foo[bar] ||= baz # ^^^^^^^^^^^^^^^^ def visit_index_or_write_node(node) arguments = node.arguments&.arguments || [] arguments << node.block if node.block builder.op_assign( builder.index( visit(node.receiver), token(node.opening_loc), visit_all(arguments), token(node.closing_loc) ), [node.operator_loc.slice.chomp("="), srange(node.operator_loc)], visit(node.value) ) end # foo[bar], = 1 # ^^^^^^^^ def visit_index_target_node(node) builder.index_asgn( visit(node.receiver), token(node.opening_loc), visit_all(node.arguments.arguments), token(node.closing_loc), ) end # @foo # ^^^^ def visit_instance_variable_read_node(node) builder.ivar(token(node.location)) end # @foo = 1 # ^^^^^^^^ def visit_instance_variable_write_node(node) builder.assign( builder.assignable(builder.ivar(token(node.name_loc))), token(node.operator_loc), visit(node.value) ) end # @foo += bar # ^^^^^^^^^^^ def visit_instance_variable_operator_write_node(node) builder.op_assign( builder.assignable(builder.ivar(token(node.name_loc))), [node.binary_operator_loc.slice.chomp("="), srange(node.binary_operator_loc)], visit(node.value) ) end # @foo &&= bar # ^^^^^^^^^^^^ def visit_instance_variable_and_write_node(node) builder.op_assign( builder.assignable(builder.ivar(token(node.name_loc))), [node.operator_loc.slice.chomp("="), srange(node.operator_loc)], visit(node.value) ) end # @foo ||= bar # ^^^^^^^^^^^^ def visit_instance_variable_or_write_node(node) builder.op_assign( builder.assignable(builder.ivar(token(node.name_loc))), [node.operator_loc.slice.chomp("="), srange(node.operator_loc)], visit(node.value) ) end # @foo, = bar # ^^^^ def visit_instance_variable_target_node(node) builder.assignable(builder.ivar(token(node.location))) end # 1 # ^ def visit_integer_node(node) visit_numeric(node, builder.integer([node.value, srange(node.location)])) end # /foo #{bar}/ # ^^^^^^^^^^^^ def visit_interpolated_regular_expression_node(node) builder.regexp_compose( token(node.opening_loc), visit_all(node.parts), [node.closing[0], srange_offsets(node.closing_loc.start_offset, node.closing_loc.start_offset + 1)], builder.regexp_options([node.closing[1..], srange_offsets(node.closing_loc.start_offset + 1, node.closing_loc.end_offset)]) ) end # if /foo #{bar}/ then end # ^^^^^^^^^^^^ alias visit_interpolated_match_last_line_node visit_interpolated_regular_expression_node # "foo #{bar}" # ^^^^^^^^^^^^ def visit_interpolated_string_node(node) if node.heredoc? return visit_heredoc(node) { |children, closing| builder.string_compose(token(node.opening_loc), children, closing) } end parts = if node.parts.one? { |part| part.type == :string_node } node.parts.flat_map do |node| if node.type == :string_node && node.unescaped.lines.count >= 2 start_offset = node.content_loc.start_offset node.unescaped.lines.map do |line| end_offset = start_offset + line.length offsets = srange_offsets(start_offset, end_offset) start_offset = end_offset builder.string_internal([line, offsets]) end else visit(node) end end else visit_all(node.parts) end builder.string_compose( token(node.opening_loc), parts, token(node.closing_loc) ) end # :"foo #{bar}" # ^^^^^^^^^^^^^ def visit_interpolated_symbol_node(node) builder.symbol_compose( token(node.opening_loc), visit_all(node.parts), token(node.closing_loc) ) end # `foo #{bar}` # ^^^^^^^^^^^^ def visit_interpolated_x_string_node(node) if node.heredoc? visit_heredoc(node) { |children, closing| builder.xstring_compose(token(node.opening_loc), children, closing) } else builder.xstring_compose( token(node.opening_loc), visit_all(node.parts), token(node.closing_loc) ) end end # -> { it } # ^^ def visit_it_local_variable_read_node(node) builder.ident([:it, srange(node.location)]).updated(:lvar) end # -> { it } # ^^^^^^^^^ def visit_it_parameters_node(node) builder.args(nil, [], nil, false) end # foo(bar: baz) # ^^^^^^^^ def visit_keyword_hash_node(node) builder.associate(nil, visit_all(node.elements), nil) end # def foo(**bar); end # ^^^^^ # # def foo(**); end # ^^ def visit_keyword_rest_parameter_node(node) builder.kwrestarg( token(node.operator_loc), node.name ? [node.name, srange(node.name_loc)] : nil ) end # -> {} # ^^^^^ def visit_lambda_node(node) parameters = node.parameters implicit_parameters = parameters.is_a?(NumberedParametersNode) || parameters.is_a?(ItParametersNode) builder.block( builder.call_lambda(token(node.operator_loc)), [node.opening, srange(node.opening_loc)], if parameters.nil? builder.args(nil, [], nil, false) elsif implicit_parameters visit(node.parameters) else builder.args( token(node.parameters.opening_loc), visit(node.parameters), token(node.parameters.closing_loc), false ) end, node.body&.accept(copy_compiler(forwarding: implicit_parameters ? [] : find_forwarding(parameters&.parameters))), [node.closing, srange(node.closing_loc)] ) end # foo # ^^^ def visit_local_variable_read_node(node) builder.ident([node.name, srange(node.location)]).updated(:lvar) end # foo = 1 # ^^^^^^^ def visit_local_variable_write_node(node) builder.assign( builder.assignable(builder.ident(token(node.name_loc))), token(node.operator_loc), visit(node.value) ) end # foo += bar # ^^^^^^^^^^ def visit_local_variable_operator_write_node(node) builder.op_assign( builder.assignable(builder.ident(token(node.name_loc))), [node.binary_operator_loc.slice.chomp("="), srange(node.binary_operator_loc)], visit(node.value) ) end # foo &&= bar # ^^^^^^^^^^^ def visit_local_variable_and_write_node(node) builder.op_assign( builder.assignable(builder.ident(token(node.name_loc))), [node.operator_loc.slice.chomp("="), srange(node.operator_loc)], visit(node.value) ) end # foo ||= bar # ^^^^^^^^^^^ def visit_local_variable_or_write_node(node) builder.op_assign( builder.assignable(builder.ident(token(node.name_loc))), [node.operator_loc.slice.chomp("="), srange(node.operator_loc)], visit(node.value) ) end # foo, = bar # ^^^ def visit_local_variable_target_node(node) if in_pattern builder.assignable(builder.match_var([node.name, srange(node.location)])) else builder.assignable(builder.ident(token(node.location))) end end # foo in bar # ^^^^^^^^^^ def visit_match_predicate_node(node) builder.match_pattern_p( visit(node.value), token(node.operator_loc), within_pattern { |compiler| node.pattern.accept(compiler) } ) end # foo => bar # ^^^^^^^^^^ def visit_match_required_node(node) builder.match_pattern( visit(node.value), token(node.operator_loc), within_pattern { |compiler| node.pattern.accept(compiler) } ) end # /(?foo)/ =~ bar # ^^^^^^^^^^^^^^^^^^^^ def visit_match_write_node(node) builder.match_op( visit(node.call.receiver), token(node.call.message_loc), visit(node.call.arguments.arguments.first) ) end # A node that is missing from the syntax tree. This is only used in the # case of a syntax error. The parser gem doesn't have such a concept, so # we invent our own here. def visit_missing_node(node) ::AST::Node.new(:missing, [], location: ::Parser::Source::Map.new(srange(node.location))) end # module Foo; end # ^^^^^^^^^^^^^^^ def visit_module_node(node) builder.def_module( token(node.module_keyword_loc), visit(node.constant_path), node.body&.accept(copy_compiler(forwarding: [])), token(node.end_keyword_loc) ) end # foo, bar = baz # ^^^^^^^^ def visit_multi_target_node(node) builder.multi_lhs( token(node.lparen_loc), visit_all(multi_target_elements(node)), token(node.rparen_loc) ) end # foo, bar = baz # ^^^^^^^^^^^^^^ def visit_multi_write_node(node) elements = multi_target_elements(node) if elements.length == 1 && elements.first.is_a?(MultiTargetNode) elements = multi_target_elements(elements.first) end builder.multi_assign( builder.multi_lhs( token(node.lparen_loc), visit_all(elements), token(node.rparen_loc) ), token(node.operator_loc), visit(node.value) ) end # next # ^^^^ # # next foo # ^^^^^^^^ def visit_next_node(node) builder.keyword_cmd( :next, token(node.keyword_loc), nil, visit(node.arguments) || [], nil ) end # nil # ^^^ def visit_nil_node(node) builder.nil(token(node.location)) end # def foo(**nil); end # ^^^^^ def visit_no_keywords_parameter_node(node) if in_pattern builder.match_nil_pattern(token(node.operator_loc), token(node.keyword_loc)) else builder.kwnilarg(token(node.operator_loc), token(node.keyword_loc)) end end # -> { _1 + _2 } # ^^^^^^^^^^^^^^ def visit_numbered_parameters_node(node) builder.numargs(node.maximum) end # $1 # ^^ def visit_numbered_reference_read_node(node) builder.nth_ref([node.number, srange(node.location)]) end # def foo(bar: baz); end # ^^^^^^^^ def visit_optional_keyword_parameter_node(node) builder.kwoptarg([node.name, srange(node.name_loc)], visit(node.value)) end # def foo(bar = 1); end # ^^^^^^^ def visit_optional_parameter_node(node) builder.optarg(token(node.name_loc), token(node.operator_loc), visit(node.value)) end # a or b # ^^^^^^ def visit_or_node(node) builder.logical_op(:or, visit(node.left), token(node.operator_loc), visit(node.right)) end # def foo(bar, *baz); end # ^^^^^^^^^ def visit_parameters_node(node) params = [] if node.requireds.any? node.requireds.each do |required| params << if required.is_a?(RequiredParameterNode) visit(required) else required.accept(copy_compiler(in_destructure: true)) end end end params.concat(visit_all(node.optionals)) if node.optionals.any? params << visit(node.rest) if !node.rest.nil? && !node.rest.is_a?(ImplicitRestNode) if node.posts.any? node.posts.each do |post| params << if post.is_a?(RequiredParameterNode) visit(post) else post.accept(copy_compiler(in_destructure: true)) end end end params.concat(visit_all(node.keywords)) if node.keywords.any? params << visit(node.keyword_rest) if !node.keyword_rest.nil? params << visit(node.block) if !node.block.nil? params end # () # ^^ # # (1) # ^^^ def visit_parentheses_node(node) builder.begin( token(node.opening_loc), visit(node.body), token(node.closing_loc) ) end # foo => ^(bar) # ^^^^^^ def visit_pinned_expression_node(node) expression = builder.begin(token(node.lparen_loc), visit(node.expression), token(node.rparen_loc)) builder.pin(token(node.operator_loc), expression) end # foo = 1 and bar => ^foo # ^^^^ def visit_pinned_variable_node(node) builder.pin(token(node.operator_loc), visit(node.variable)) end # END {} def visit_post_execution_node(node) builder.postexe( token(node.keyword_loc), token(node.opening_loc), visit(node.statements), token(node.closing_loc) ) end # BEGIN {} def visit_pre_execution_node(node) builder.preexe( token(node.keyword_loc), token(node.opening_loc), visit(node.statements), token(node.closing_loc) ) end # The top-level program node. def visit_program_node(node) visit(node.statements) end # 0..5 # ^^^^ def visit_range_node(node) if node.exclude_end? builder.range_exclusive( visit(node.left), token(node.operator_loc), visit(node.right) ) else builder.range_inclusive( visit(node.left), token(node.operator_loc), visit(node.right) ) end end # if foo .. bar; end # ^^^^^^^^^^ alias visit_flip_flop_node visit_range_node # 1r # ^^ def visit_rational_node(node) visit_numeric(node, builder.rational([node.value, srange(node.location)])) end # redo # ^^^^ def visit_redo_node(node) builder.keyword_cmd(:redo, token(node.location)) end # /foo/ # ^^^^^ def visit_regular_expression_node(node) content = node.content parts = if content.include?("\n") offset = node.content_loc.start_offset content.lines.map do |line| builder.string_internal([line, srange_offsets(offset, offset += line.bytesize)]) end else [builder.string_internal(token(node.content_loc))] end builder.regexp_compose( token(node.opening_loc), parts, [node.closing[0], srange_offsets(node.closing_loc.start_offset, node.closing_loc.start_offset + 1)], builder.regexp_options([node.closing[1..], srange_offsets(node.closing_loc.start_offset + 1, node.closing_loc.end_offset)]) ) end # if /foo/ then end # ^^^^^ alias visit_match_last_line_node visit_regular_expression_node # def foo(bar:); end # ^^^^ def visit_required_keyword_parameter_node(node) builder.kwarg([node.name, srange(node.name_loc)]) end # def foo(bar); end # ^^^ def visit_required_parameter_node(node) builder.arg(token(node.location)) end # foo rescue bar # ^^^^^^^^^^^^^^ def visit_rescue_modifier_node(node) builder.begin_body( visit(node.expression), [ builder.rescue_body( token(node.keyword_loc), nil, nil, nil, nil, visit(node.rescue_expression) ) ] ) end # begin; rescue; end # ^^^^^^^ def visit_rescue_node(node) raise CompilationError, "Cannot directly compile rescue nodes" end # def foo(*bar); end # ^^^^ # # def foo(*); end # ^ def visit_rest_parameter_node(node) builder.restarg(token(node.operator_loc), token(node.name_loc)) end # retry # ^^^^^ def visit_retry_node(node) builder.keyword_cmd(:retry, token(node.location)) end # return # ^^^^^^ # # return 1 # ^^^^^^^^ def visit_return_node(node) builder.keyword_cmd( :return, token(node.keyword_loc), nil, visit(node.arguments) || [], nil ) end # self # ^^^^ def visit_self_node(node) builder.self(token(node.location)) end # A shareable constant. def visit_shareable_constant_node(node) visit(node.write) end # class << self; end # ^^^^^^^^^^^^^^^^^^ def visit_singleton_class_node(node) builder.def_sclass( token(node.class_keyword_loc), token(node.operator_loc), visit(node.expression), node.body&.accept(copy_compiler(forwarding: [])), token(node.end_keyword_loc) ) end # __ENCODING__ # ^^^^^^^^^^^^ def visit_source_encoding_node(node) builder.accessible(builder.__ENCODING__(token(node.location))) end # __FILE__ # ^^^^^^^^ def visit_source_file_node(node) builder.accessible(builder.__FILE__(token(node.location))) end # __LINE__ # ^^^^^^^^ def visit_source_line_node(node) builder.accessible(builder.__LINE__(token(node.location))) end # foo(*bar) # ^^^^ # # def foo((bar, *baz)); end # ^^^^ # # def foo(*); bar(*); end # ^ def visit_splat_node(node) if node.expression.nil? && forwarding.include?(:*) builder.forwarded_restarg(token(node.operator_loc)) elsif in_destructure builder.restarg(token(node.operator_loc), token(node.expression&.location)) elsif in_pattern builder.match_rest(token(node.operator_loc), token(node.expression&.location)) else builder.splat(token(node.operator_loc), visit(node.expression)) end end # A list of statements. def visit_statements_node(node) builder.compstmt(visit_all(node.body)) end # "foo" # ^^^^^ def visit_string_node(node) if node.heredoc? visit_heredoc(node.to_interpolated) { |children, closing| builder.string_compose(token(node.opening_loc), children, closing) } elsif node.opening == "?" builder.character([node.unescaped, srange(node.location)]) elsif node.opening&.start_with?("%") && node.unescaped.empty? builder.string_compose(token(node.opening_loc), [], token(node.closing_loc)) else content_lines = node.content.lines unescaped_lines = node.unescaped.lines parts = if content_lines.length <= 1 || unescaped_lines.length <= 1 [builder.string_internal([node.unescaped, srange(node.content_loc)])] elsif content_lines.length != unescaped_lines.length # This occurs when we have line continuations in the string. We # need to come back and fix this, but for now this stops the # code from breaking when we encounter it because of trying to # transpose arrays of different lengths. [builder.string_internal([node.unescaped, srange(node.content_loc)])] else start_offset = node.content_loc.start_offset [content_lines, unescaped_lines].transpose.map do |content_line, unescaped_line| end_offset = start_offset + content_line.length offsets = srange_offsets(start_offset, end_offset) start_offset = end_offset builder.string_internal([unescaped_line, offsets]) end end builder.string_compose( token(node.opening_loc), parts, token(node.closing_loc) ) end end # super(foo) # ^^^^^^^^^^ def visit_super_node(node) arguments = node.arguments&.arguments || [] block = node.block if block.is_a?(BlockArgumentNode) arguments = [*arguments, block] block = nil end visit_block( builder.keyword_cmd( :super, token(node.keyword_loc), token(node.lparen_loc), visit_all(arguments), token(node.rparen_loc) ), block ) end # :foo # ^^^^ def visit_symbol_node(node) if node.closing_loc.nil? if node.opening_loc.nil? builder.symbol_internal([node.unescaped, srange(node.location)]) else builder.symbol([node.unescaped, srange(node.location)]) end else parts = if node.value.lines.one? [builder.string_internal([node.unescaped, srange(node.value_loc)])] else start_offset = node.value_loc.start_offset node.value.lines.map do |line| end_offset = start_offset + line.length offsets = srange_offsets(start_offset, end_offset) start_offset = end_offset builder.string_internal([line, offsets]) end end builder.symbol_compose( token(node.opening_loc), parts, token(node.closing_loc) ) end end # true # ^^^^ def visit_true_node(node) builder.true(token(node.location)) end # undef foo # ^^^^^^^^^ def visit_undef_node(node) builder.undef_method(token(node.keyword_loc), visit_all(node.names)) end # unless foo; bar end # ^^^^^^^^^^^^^^^^^^^ # # bar unless foo # ^^^^^^^^^^^^^^ def visit_unless_node(node) if node.keyword_loc.start_offset == node.location.start_offset builder.condition( token(node.keyword_loc), visit(node.predicate), if (then_keyword_loc = node.then_keyword_loc) token(then_keyword_loc) else srange_find(node.predicate.location.end_offset, (node.statements&.location || node.else_clause&.location || node.end_keyword_loc).start_offset, ";") end, visit(node.else_clause), token(node.else_clause&.else_keyword_loc), visit(node.statements), token(node.end_keyword_loc) ) else builder.condition_mod( visit(node.else_clause), visit(node.statements), token(node.keyword_loc), visit(node.predicate) ) end end # until foo; bar end # ^^^^^^^^^^^^^^^^^^ # # bar until foo # ^^^^^^^^^^^^^ def visit_until_node(node) if node.location.start_offset == node.keyword_loc.start_offset builder.loop( :until, token(node.keyword_loc), visit(node.predicate), if (do_keyword_loc = node.do_keyword_loc) token(do_keyword_loc) else srange_find(node.predicate.location.end_offset, (node.statements&.location || node.closing_loc).start_offset, ";") end, visit(node.statements), token(node.closing_loc) ) else builder.loop_mod( :until, visit(node.statements), token(node.keyword_loc), visit(node.predicate) ) end end # case foo; when bar; end # ^^^^^^^^^^^^^ def visit_when_node(node) builder.when( token(node.keyword_loc), visit_all(node.conditions), if (then_keyword_loc = node.then_keyword_loc) token(then_keyword_loc) else srange_find(node.conditions.last.location.end_offset, node.statements&.location&.start_offset, ";") end, visit(node.statements) ) end # while foo; bar end # ^^^^^^^^^^^^^^^^^^ # # bar while foo # ^^^^^^^^^^^^^ def visit_while_node(node) if node.location.start_offset == node.keyword_loc.start_offset builder.loop( :while, token(node.keyword_loc), visit(node.predicate), if (do_keyword_loc = node.do_keyword_loc) token(do_keyword_loc) else srange_find(node.predicate.location.end_offset, (node.statements&.location || node.closing_loc).start_offset, ";") end, visit(node.statements), token(node.closing_loc) ) else builder.loop_mod( :while, visit(node.statements), token(node.keyword_loc), visit(node.predicate) ) end end # `foo` # ^^^^^ def visit_x_string_node(node) if node.heredoc? visit_heredoc(node.to_interpolated) { |children, closing| builder.xstring_compose(token(node.opening_loc), children, closing) } else parts = if node.unescaped.lines.one? [builder.string_internal([node.unescaped, srange(node.content_loc)])] else start_offset = node.content_loc.start_offset node.unescaped.lines.map do |line| end_offset = start_offset + line.length offsets = srange_offsets(start_offset, end_offset) start_offset = end_offset builder.string_internal([line, offsets]) end end builder.xstring_compose( token(node.opening_loc), parts, token(node.closing_loc) ) end end # yield # ^^^^^ # # yield 1 # ^^^^^^^ def visit_yield_node(node) builder.keyword_cmd( :yield, token(node.keyword_loc), token(node.lparen_loc), visit(node.arguments) || [], token(node.rparen_loc) ) end private # Initialize a new compiler with the given option overrides, used to # visit a subtree with the given options. def copy_compiler(forwarding: self.forwarding, in_destructure: self.in_destructure, in_pattern: self.in_pattern) Compiler.new(parser, offset_cache, forwarding: forwarding, in_destructure: in_destructure, in_pattern: in_pattern) end # When *, **, &, or ... are used as an argument in a method call, we # check if they were allowed by the current context. To determine that # we build this lookup table. def find_forwarding(node) return [] if node.nil? forwarding = [] forwarding << :* if node.rest.is_a?(RestParameterNode) && node.rest.name.nil? forwarding << :** if node.keyword_rest.is_a?(KeywordRestParameterNode) && node.keyword_rest.name.nil? forwarding << :& if !node.block.nil? && node.block.name.nil? forwarding |= [:&, :"..."] if node.keyword_rest.is_a?(ForwardingParameterNode) forwarding end # Returns the set of targets for a MultiTargetNode or a MultiWriteNode. def multi_target_elements(node) elements = [*node.lefts] elements << node.rest if !node.rest.nil? && !node.rest.is_a?(ImplicitRestNode) elements.concat(node.rights) elements end # Negate the value of a numeric node. This is a special case where you # have a negative sign on one line and then a number on the next line. # In normal Ruby, this will always be a method call. The parser gem, # however, marks this as a numeric literal. We have to massage the tree # here to get it into the correct form. def numeric_negate(message_loc, receiver) case receiver.type when :integer_node, :float_node receiver.copy(value: -receiver.value, location: message_loc.join(receiver.location)) when :rational_node receiver.copy(numerator: -receiver.numerator, location: message_loc.join(receiver.location)) when :imaginary_node receiver.copy(numeric: numeric_negate(message_loc, receiver.numeric), location: message_loc.join(receiver.location)) end end # Blocks can have a special set of parameters that automatically expand # when given arrays if they have a single required parameter and no # other parameters. def procarg0?(parameters) parameters && parameters.requireds.length == 1 && parameters.optionals.empty? && parameters.rest.nil? && parameters.posts.empty? && parameters.keywords.empty? && parameters.keyword_rest.nil? && parameters.block.nil? end # Locations in the parser gem AST are generated using this class. We # store a reference to its constant to make it slightly faster to look # up. Range = ::Parser::Source::Range # Constructs a new source range from the given start and end offsets. def srange(location) Range.new(source_buffer, offset_cache[location.start_offset], offset_cache[location.end_offset]) if location end # Constructs a new source range from the given start and end offsets. def srange_offsets(start_offset, end_offset) Range.new(source_buffer, offset_cache[start_offset], offset_cache[end_offset]) end # Constructs a new source range by finding the given character between # the given start offset and end offset. If the needle is not found, it # returns nil. Importantly it does not search past newlines or comments. # # Note that end_offset is allowed to be nil, in which case this will # search until the end of the string. def srange_find(start_offset, end_offset, character) if (match = source_buffer.source.byteslice(start_offset...end_offset)[/\A\s*#{character}/]) final_offset = start_offset + match.bytesize [character, Range.new(source_buffer, offset_cache[final_offset - character.bytesize], offset_cache[final_offset])] end end # Transform a location into a token that the parser gem expects. def token(location) [location.slice, Range.new(source_buffer, offset_cache[location.start_offset], offset_cache[location.end_offset])] if location end # Visit a block node on a call. def visit_block(call, block) if block parameters = block.parameters implicit_parameters = parameters.is_a?(NumberedParametersNode) || parameters.is_a?(ItParametersNode) builder.block( call, token(block.opening_loc), if parameters.nil? builder.args(nil, [], nil, false) elsif implicit_parameters visit(parameters) else builder.args( token(parameters.opening_loc), if procarg0?(parameters.parameters) parameter = parameters.parameters.requireds.first visited = parameter.is_a?(RequiredParameterNode) ? visit(parameter) : parameter.accept(copy_compiler(in_destructure: true)) [builder.procarg0(visited)].concat(visit_all(parameters.locals)) else visit(parameters) end, token(parameters.closing_loc), false ) end, block.body&.accept(copy_compiler(forwarding: implicit_parameters ? [] : find_forwarding(parameters&.parameters))), token(block.closing_loc) ) else call end end # The parser gem automatically converts \r\n to \n, meaning our offsets # need to be adjusted to always subtract 1 from the length. def chomped_bytesize(line) chomped = line.chomp chomped.bytesize + (chomped == line ? 0 : 1) end # Visit a heredoc that can be either a string or an xstring. def visit_heredoc(node) children = Array.new indented = false # If this is a dedenting heredoc, then we need to insert the opening # content into the children as well. if node.opening.start_with?("<<~") && node.parts.length > 0 && !node.parts.first.is_a?(StringNode) location = node.parts.first.location location = location.copy(start_offset: location.start_offset - location.start_line_slice.bytesize) children << builder.string_internal(token(location)) indented = true end node.parts.each do |part| pushing = if part.is_a?(StringNode) && part.unescaped.include?("\n") unescaped = part.unescaped.lines escaped = part.content.lines escaped_lengths = [] normalized_lengths = [] if node.opening.end_with?("'") escaped.each do |line| escaped_lengths << line.bytesize normalized_lengths << chomped_bytesize(line) end else escaped .chunk_while { |before, after| before.match?(/(?