diff --git a/lib/prism.rb b/lib/prism.rb index 5d78b42c4dc041..8a2e7a61083168 100644 --- a/lib/prism.rb +++ b/lib/prism.rb @@ -22,7 +22,6 @@ module Prism autoload :LexRipper, "prism/lex_compat" autoload :MutationCompiler, "prism/mutation_compiler" autoload :NodeInspector, "prism/node_inspector" - autoload :RipperCompat, "prism/ripper_compat" autoload :Pack, "prism/pack" autoload :Pattern, "prism/pattern" autoload :Serialize, "prism/serialize" diff --git a/lib/prism/prism.gemspec b/lib/prism/prism.gemspec index 0d0d548d35395a..3d2602d430660f 100644 --- a/lib/prism/prism.gemspec +++ b/lib/prism/prism.gemspec @@ -83,13 +83,13 @@ Gem::Specification.new do |spec| "lib/prism/parse_result/comments.rb", "lib/prism/parse_result/newlines.rb", "lib/prism/pattern.rb", - "lib/prism/ripper_compat.rb", "lib/prism/serialize.rb", "lib/prism/translation.rb", "lib/prism/translation/parser.rb", "lib/prism/translation/parser/compiler.rb", "lib/prism/translation/parser/lexer.rb", "lib/prism/translation/parser/rubocop.rb", + "lib/prism/translation/ripper.rb", "lib/prism/visitor.rb", "src/diagnostic.c", "src/encoding.c", diff --git a/lib/prism/ripper_compat.rb b/lib/prism/ripper_compat.rb deleted file mode 100644 index 6e10737e0d5bd6..00000000000000 --- a/lib/prism/ripper_compat.rb +++ /dev/null @@ -1,575 +0,0 @@ -# frozen_string_literal: true - -require "ripper" - -module Prism - # Note: This integration is not finished, and therefore still has many - # inconsistencies with Ripper. If you'd like to help out, pull requests would - # be greatly appreciated! - # - # This class is meant to provide a compatibility layer between prism and - # Ripper. It functions by parsing the entire tree first and then walking it - # and executing each of the Ripper callbacks as it goes. - # - # This class is going to necessarily be slower than the native Ripper API. It - # is meant as a stopgap until developers migrate to using prism. It is also - # meant as a test harness for the prism parser. - # - # To use this class, you treat `Prism::RipperCompat` effectively as you would - # treat the `Ripper` class. - class RipperCompat < Compiler - # This class mirrors the ::Ripper::SexpBuilder subclass of ::Ripper that - # returns the arrays of [type, *children]. - class SexpBuilder < RipperCompat - private - - Ripper::PARSER_EVENTS.each do |event| - define_method(:"on_#{event}") do |*args| - [event, *args] - end - end - - Ripper::SCANNER_EVENTS.each do |event| - define_method(:"on_#{event}") do |value| - [:"@#{event}", value, [lineno, column]] - end - end - end - - # This class mirrors the ::Ripper::SexpBuilderPP subclass of ::Ripper that - # returns the same values as ::Ripper::SexpBuilder except with a couple of - # niceties that flatten linked lists into arrays. - class SexpBuilderPP < SexpBuilder - private - - def _dispatch_event_new # :nodoc: - [] - end - - def _dispatch_event_push(list, item) # :nodoc: - list << item - list - end - - Ripper::PARSER_EVENT_TABLE.each do |event, arity| - case event - when /_new\z/ - alias_method :"on_#{event}", :_dispatch_event_new if arity == 0 - when /_add\z/ - alias_method :"on_#{event}", :_dispatch_event_push - end - end - end - - # The source that is being parsed. - attr_reader :source - - # The current line number of the parser. - attr_reader :lineno - - # The current column number of the parser. - attr_reader :column - - # Create a new RipperCompat object with the given source. - def initialize(source) - @source = source - @result = nil - @lineno = nil - @column = nil - end - - ############################################################################ - # Public interface - ############################################################################ - - # True if the parser encountered an error during parsing. - def error? - result.failure? - end - - # Parse the source and return the result. - def parse - result.magic_comments.each do |magic_comment| - on_magic_comment(magic_comment.key, magic_comment.value) - end - - if error? - result.errors.each do |error| - on_parse_error(error.message) - end - - nil - else - result.value.accept(self) - end - end - - ############################################################################ - # Visitor methods - ############################################################################ - - # Visit an ArrayNode node. - def visit_array_node(node) - elements = visit_elements(node.elements) unless node.elements.empty? - bounds(node.location) - on_array(elements) - end - - # Visit a CallNode node. - # Ripper distinguishes between many different method-call - # nodes -- unary and binary operators, "command" calls with - # no parentheses, and call/fcall/vcall. - def visit_call_node(node) - return visit_aref_node(node) if node.name == :[] - return visit_aref_field_node(node) if node.name == :[]= - - if node.variable_call? - raise NotImplementedError unless node.receiver.nil? - - bounds(node.message_loc) - return on_vcall(on_ident(node.message)) - end - - if node.opening_loc.nil? - return visit_no_paren_call(node) - end - - # A non-operator method call with parentheses - - args = if node.arguments.nil? - on_arg_paren(nil) - else - on_arg_paren(on_args_add_block(visit_elements(node.arguments.arguments), false)) - end - - bounds(node.message_loc) - ident_val = on_ident(node.message) - - bounds(node.location) - args_call_val = on_method_add_arg(on_fcall(ident_val), args) - if node.block - block_val = visit(node.block) - - return on_method_add_block(args_call_val, block_val) - else - return args_call_val - end - end - - # Visit a LocalVariableWriteNode. - def visit_local_variable_write_node(node) - bounds(node.name_loc) - ident_val = on_ident(node.name.to_s) - on_assign(on_var_field(ident_val), visit(node.value)) - end - - # Visit a LocalVariableAndWriteNode. - def visit_local_variable_and_write_node(node) - visit_binary_op_assign(node) - end - - # Visit a LocalVariableOrWriteNode. - def visit_local_variable_or_write_node(node) - visit_binary_op_assign(node) - end - - # Visit nodes for +=, *=, -=, etc., called LocalVariableOperatorWriteNodes. - def visit_local_variable_operator_write_node(node) - visit_binary_op_assign(node, operator: "#{node.operator}=") - end - - # Visit a LocalVariableReadNode. - def visit_local_variable_read_node(node) - bounds(node.location) - ident_val = on_ident(node.slice) - - on_var_ref(ident_val) - end - - # Visit a BlockNode. - def visit_block_node(node) - params_val = node.parameters.nil? ? nil : visit(node.parameters) - - body_val = node.body.nil? ? on_stmts_add(on_stmts_new, on_void_stmt) : visit(node.body) - - on_brace_block(params_val, body_val) - end - - # Visit a BlockParametersNode. - def visit_block_parameters_node(node) - on_block_var(visit(node.parameters), no_block_value) - end - - # Visit a ParametersNode. - # This will require expanding as we support more kinds of parameters. - def visit_parameters_node(node) - #on_params(required, optional, nil, nil, nil, nil, nil) - on_params(visit_all(node.requireds), nil, nil, nil, nil, nil, nil) - end - - # Visit a RequiredParameterNode. - def visit_required_parameter_node(node) - bounds(node.location) - on_ident(node.name.to_s) - end - - # Visit a BreakNode. - def visit_break_node(node) - return on_break(on_args_new) if node.arguments.nil? - - args_val = visit_elements(node.arguments.arguments) - on_break(on_args_add_block(args_val, false)) - end - - # Visit an AndNode. - def visit_and_node(node) - visit_binary_operator(node) - end - - # Visit an OrNode. - def visit_or_node(node) - visit_binary_operator(node) - end - - # Visit a TrueNode. - def visit_true_node(node) - bounds(node.location) - on_var_ref(on_kw("true")) - end - - # Visit a FalseNode. - def visit_false_node(node) - bounds(node.location) - on_var_ref(on_kw("false")) - end - - # Visit a FloatNode node. - def visit_float_node(node) - visit_number(node) { |text| on_float(text) } - end - - # Visit a ImaginaryNode node. - def visit_imaginary_node(node) - visit_number(node) { |text| on_imaginary(text) } - end - - # Visit an IntegerNode node. - def visit_integer_node(node) - visit_number(node) { |text| on_int(text) } - end - - # Visit a ParenthesesNode node. - def visit_parentheses_node(node) - body = - if node.body.nil? - on_stmts_add(on_stmts_new, on_void_stmt) - else - visit(node.body) - end - - bounds(node.location) - on_paren(body) - end - - # Visit a BeginNode node. - # This is not at all bulletproof against different structures of begin/rescue/else/ensure/end. - def visit_begin_node(node) - rescue_val = node.rescue_clause ? on_rescue(nil, nil, visit(node.rescue_clause), nil) : nil - ensure_val = node.ensure_clause ? on_ensure(visit(node.ensure_clause.statements)) : nil - on_begin(on_bodystmt(visit(node.statements), rescue_val, nil, ensure_val)) - end - - # Visit a RescueNode node. - def visit_rescue_node(node) - visit(node.statements) - end - - # Visit a ProgramNode node. - def visit_program_node(node) - statements = visit(node.statements) - bounds(node.location) - on_program(statements) - end - - # Visit a RangeNode node. - def visit_range_node(node) - left = visit(node.left) - right = visit(node.right) - - bounds(node.location) - if node.exclude_end? - on_dot3(left, right) - else - on_dot2(left, right) - end - end - - # Visit a RationalNode node. - def visit_rational_node(node) - visit_number(node) { |text| on_rational(text) } - end - - # Visit a StringNode node. - def visit_string_node(node) - bounds(node.content_loc) - tstring_val = on_tstring_content(node.unescaped.to_s) - on_string_literal(on_string_add(on_string_content, tstring_val)) - end - - # Visit an XStringNode node. - def visit_x_string_node(node) - bounds(node.content_loc) - tstring_val = on_tstring_content(node.unescaped.to_s) - on_xstring_literal(on_xstring_add(on_xstring_new, tstring_val)) - end - - # Visit an InterpolatedStringNode node. - def visit_interpolated_string_node(node) - parts = node.parts.map do |part| - case part - when StringNode - bounds(part.content_loc) - on_tstring_content(part.content) - when EmbeddedStatementsNode - on_string_embexpr(visit(part)) - else - raise NotImplementedError, "Unexpected node type in InterpolatedStringNode" - end - end - - string_list = parts.inject(on_string_content) do |items, item| - on_string_add(items, item) - end - - on_string_literal(string_list) - end - - # Visit an EmbeddedStatementsNode node. - def visit_embedded_statements_node(node) - visit(node.statements) - end - - # Visit a SymbolNode node. - def visit_symbol_node(node) - if (opening = node.opening) && (['"', "'"].include?(opening[-1]) || opening.start_with?("%s")) - bounds(node.value_loc) - tstring_val = on_tstring_content(node.value.to_s) - return on_dyna_symbol(on_string_add(on_string_content, tstring_val)) - end - - bounds(node.value_loc) - ident_val = on_ident(node.value.to_s) - on_symbol_literal(on_symbol(ident_val)) - end - - # Visit a StatementsNode node. - def visit_statements_node(node) - bounds(node.location) - node.body.inject(on_stmts_new) do |stmts, stmt| - on_stmts_add(stmts, visit(stmt)) - end - end - - ############################################################################ - # Entrypoints for subclasses - ############################################################################ - - # This is a convenience method that runs the SexpBuilder subclass parser. - def self.sexp_raw(source) - SexpBuilder.new(source).parse - end - - # This is a convenience method that runs the SexpBuilderPP subclass parser. - def self.sexp(source) - SexpBuilderPP.new(source).parse - end - - private - - # Generate Ripper events for a CallNode with no opening_loc - def visit_no_paren_call(node) - # No opening_loc can mean an operator. It can also mean a - # method call with no parentheses. - if node.message.match?(/^[[:punct:]]/) - left = visit(node.receiver) - if node.arguments&.arguments&.length == 1 - right = visit(node.arguments.arguments.first) - - return on_binary(left, node.name, right) - elsif !node.arguments || node.arguments.empty? - return on_unary(node.name, left) - else - raise NotImplementedError, "More than two arguments for operator" - end - elsif node.call_operator_loc.nil? - # In Ripper a method call like "puts myvar" with no parentheses is a "command". - bounds(node.message_loc) - ident_val = on_ident(node.message) - - # Unless it has a block, and then it's an fcall (e.g. "foo { bar }") - if node.block - block_val = visit(node.block) - # In these calls, even if node.arguments is nil, we still get an :args_new call. - args = if node.arguments.nil? - on_args_new - else - on_args_add_block(visit_elements(node.arguments.arguments)) - end - method_args_val = on_method_add_arg(on_fcall(ident_val), args) - return on_method_add_block(method_args_val, block_val) - else - if node.arguments.nil? - return on_command(ident_val, nil) - else - args = on_args_add_block(visit_elements(node.arguments.arguments), false) - return on_command(ident_val, args) - end - end - else - operator = node.call_operator_loc.slice - if operator == "." || operator == "&." - left_val = visit(node.receiver) - - bounds(node.call_operator_loc) - operator_val = operator == "." ? on_period(node.call_operator) : on_op(node.call_operator) - - bounds(node.message_loc) - right_val = on_ident(node.message) - - call_val = on_call(left_val, operator_val, right_val) - - if node.block - block_val = visit(node.block) - return on_method_add_block(call_val, block_val) - else - return call_val - end - else - raise NotImplementedError, "operator other than . or &. for call: #{operator.inspect}" - end - end - end - - # Visit a list of elements, like the elements of an array or arguments. - def visit_elements(elements) - bounds(elements.first.location) - elements.inject(on_args_new) do |args, element| - on_args_add(args, visit(element)) - end - end - - # Visit an operation-and-assign node, such as +=. - def visit_binary_op_assign(node, operator: node.operator) - bounds(node.name_loc) - ident_val = on_ident(node.name.to_s) - - bounds(node.operator_loc) - op_val = on_op(operator) - - on_opassign(on_var_field(ident_val), op_val, visit(node.value)) - end - - # In Prism this is a CallNode with :[] as the operator. - # In Ripper it's an :aref. - def visit_aref_node(node) - first_arg_val = visit(node.arguments.arguments[0]) - args_val = on_args_add_block(on_args_add(on_args_new, first_arg_val), false) - on_aref(visit(node.receiver), args_val) - end - - # In Prism this is a CallNode with :[]= as the operator. - # In Ripper it's an :aref_field. - def visit_aref_field_node(node) - first_arg_val = visit(node.arguments.arguments[0]) - args_val = on_args_add_block(on_args_add(on_args_new, first_arg_val), false) - assign_val = visit(node.arguments.arguments[1]) - on_assign(on_aref_field(visit(node.receiver), args_val), assign_val) - end - - # Visit a node that represents a number. We need to explicitly handle the - # unary - operator. - def visit_number(node) - slice = node.slice - location = node.location - - if slice[0] == "-" - bounds_values(location.start_line, location.start_column + 1) - value = yield slice[1..-1] - - bounds(node.location) - on_unary(visit_unary_operator(:-@), value) - else - bounds(location) - yield slice - end - end - - if RUBY_ENGINE == "jruby" && Gem::Version.new(JRUBY_VERSION) < Gem::Version.new("9.4.6.0") - # JRuby before 9.4.6.0 uses :- for unary minus instead of :-@ - def visit_unary_operator(value) - value == :-@ ? :- : value - end - else - # For most Rubies and JRuby after 9.4.6.0 this is a no-op. - def visit_unary_operator(value) - value - end - end - - if RUBY_ENGINE == "jruby" - # For JRuby, "no block" in an on_block_var is nil - def no_block_value - nil - end - else - # For CRuby et al, "no block" in an on_block_var is false - def no_block_value - false - end - end - - # Visit a binary operator node like an AndNode or OrNode - def visit_binary_operator(node) - left_val = visit(node.left) - right_val = visit(node.right) - on_binary(left_val, node.operator.to_sym, right_val) - end - - # This method is responsible for updating lineno and column information - # to reflect the current node. - # - # This method could be drastically improved with some caching on the start - # of every line, but for now it's good enough. - def bounds(location) - @lineno = location.start_line - @column = location.start_column - end - - # If we need to do something unusual, we can directly update the line number - # and column to reflect the current node. - def bounds_values(lineno, column) - @lineno = lineno - @column = column - end - - # Lazily initialize the parse result. - def result - @result ||= Prism.parse(source) - end - - def _dispatch0; end # :nodoc: - def _dispatch1(_); end # :nodoc: - def _dispatch2(_, _); end # :nodoc: - def _dispatch3(_, _, _); end # :nodoc: - def _dispatch4(_, _, _, _); end # :nodoc: - def _dispatch5(_, _, _, _, _); end # :nodoc: - def _dispatch7(_, _, _, _, _, _, _); end # :nodoc: - - alias_method :on_parse_error, :_dispatch1 - alias_method :on_magic_comment, :_dispatch2 - - (Ripper::SCANNER_EVENT_TABLE.merge(Ripper::PARSER_EVENT_TABLE)).each do |event, arity| - alias_method :"on_#{event}", :"_dispatch#{arity}" - end - end -end diff --git a/lib/prism/translation.rb b/lib/prism/translation.rb index 9a7cedac46a4ea..31d4cd7b7f5202 100644 --- a/lib/prism/translation.rb +++ b/lib/prism/translation.rb @@ -7,5 +7,6 @@ module Prism # seattlerb/ruby_parser gem's syntax tree as well. module Translation autoload :Parser, "prism/translation/parser" + autoload :Ripper, "prism/translation/ripper" end end diff --git a/lib/prism/translation/ripper.rb b/lib/prism/translation/ripper.rb new file mode 100644 index 00000000000000..e0b76abded7855 --- /dev/null +++ b/lib/prism/translation/ripper.rb @@ -0,0 +1,577 @@ +# frozen_string_literal: true + +require "ripper" + +module Prism + module Translation + # Note: This integration is not finished, and therefore still has many + # inconsistencies with Ripper. If you'd like to help out, pull requests would + # be greatly appreciated! + # + # This class is meant to provide a compatibility layer between prism and + # Ripper. It functions by parsing the entire tree first and then walking it + # and executing each of the Ripper callbacks as it goes. + # + # This class is going to necessarily be slower than the native Ripper API. It + # is meant as a stopgap until developers migrate to using prism. It is also + # meant as a test harness for the prism parser. + # + # To use this class, you treat `Prism::Translation::Ripper` effectively as you would + # treat the `Ripper` class. + class Ripper < Compiler + # This class mirrors the ::Ripper::SexpBuilder subclass of ::Ripper that + # returns the arrays of [type, *children]. + class SexpBuilder < Ripper + private + + ::Ripper::PARSER_EVENTS.each do |event| + define_method(:"on_#{event}") do |*args| + [event, *args] + end + end + + ::Ripper::SCANNER_EVENTS.each do |event| + define_method(:"on_#{event}") do |value| + [:"@#{event}", value, [lineno, column]] + end + end + end + + # This class mirrors the ::Ripper::SexpBuilderPP subclass of ::Ripper that + # returns the same values as ::Ripper::SexpBuilder except with a couple of + # niceties that flatten linked lists into arrays. + class SexpBuilderPP < SexpBuilder + private + + def _dispatch_event_new # :nodoc: + [] + end + + def _dispatch_event_push(list, item) # :nodoc: + list << item + list + end + + ::Ripper::PARSER_EVENT_TABLE.each do |event, arity| + case event + when /_new\z/ + alias_method :"on_#{event}", :_dispatch_event_new if arity == 0 + when /_add\z/ + alias_method :"on_#{event}", :_dispatch_event_push + end + end + end + + # The source that is being parsed. + attr_reader :source + + # The current line number of the parser. + attr_reader :lineno + + # The current column number of the parser. + attr_reader :column + + # Create a new Translation::Ripper object with the given source. + def initialize(source) + @source = source + @result = nil + @lineno = nil + @column = nil + end + + ############################################################################ + # Public interface + ############################################################################ + + # True if the parser encountered an error during parsing. + def error? + result.failure? + end + + # Parse the source and return the result. + def parse + result.magic_comments.each do |magic_comment| + on_magic_comment(magic_comment.key, magic_comment.value) + end + + if error? + result.errors.each do |error| + on_parse_error(error.message) + end + + nil + else + result.value.accept(self) + end + end + + ############################################################################ + # Visitor methods + ############################################################################ + + # Visit an ArrayNode node. + def visit_array_node(node) + elements = visit_elements(node.elements) unless node.elements.empty? + bounds(node.location) + on_array(elements) + end + + # Visit a CallNode node. + # Ripper distinguishes between many different method-call + # nodes -- unary and binary operators, "command" calls with + # no parentheses, and call/fcall/vcall. + def visit_call_node(node) + return visit_aref_node(node) if node.name == :[] + return visit_aref_field_node(node) if node.name == :[]= + + if node.variable_call? + raise NotImplementedError unless node.receiver.nil? + + bounds(node.message_loc) + return on_vcall(on_ident(node.message)) + end + + if node.opening_loc.nil? + return visit_no_paren_call(node) + end + + # A non-operator method call with parentheses + + args = if node.arguments.nil? + on_arg_paren(nil) + else + on_arg_paren(on_args_add_block(visit_elements(node.arguments.arguments), false)) + end + + bounds(node.message_loc) + ident_val = on_ident(node.message) + + bounds(node.location) + args_call_val = on_method_add_arg(on_fcall(ident_val), args) + if node.block + block_val = visit(node.block) + + return on_method_add_block(args_call_val, block_val) + else + return args_call_val + end + end + + # Visit a LocalVariableWriteNode. + def visit_local_variable_write_node(node) + bounds(node.name_loc) + ident_val = on_ident(node.name.to_s) + on_assign(on_var_field(ident_val), visit(node.value)) + end + + # Visit a LocalVariableAndWriteNode. + def visit_local_variable_and_write_node(node) + visit_binary_op_assign(node) + end + + # Visit a LocalVariableOrWriteNode. + def visit_local_variable_or_write_node(node) + visit_binary_op_assign(node) + end + + # Visit nodes for +=, *=, -=, etc., called LocalVariableOperatorWriteNodes. + def visit_local_variable_operator_write_node(node) + visit_binary_op_assign(node, operator: "#{node.operator}=") + end + + # Visit a LocalVariableReadNode. + def visit_local_variable_read_node(node) + bounds(node.location) + ident_val = on_ident(node.slice) + + on_var_ref(ident_val) + end + + # Visit a BlockNode. + def visit_block_node(node) + params_val = node.parameters.nil? ? nil : visit(node.parameters) + + body_val = node.body.nil? ? on_stmts_add(on_stmts_new, on_void_stmt) : visit(node.body) + + on_brace_block(params_val, body_val) + end + + # Visit a BlockParametersNode. + def visit_block_parameters_node(node) + on_block_var(visit(node.parameters), no_block_value) + end + + # Visit a ParametersNode. + # This will require expanding as we support more kinds of parameters. + def visit_parameters_node(node) + #on_params(required, optional, nil, nil, nil, nil, nil) + on_params(visit_all(node.requireds), nil, nil, nil, nil, nil, nil) + end + + # Visit a RequiredParameterNode. + def visit_required_parameter_node(node) + bounds(node.location) + on_ident(node.name.to_s) + end + + # Visit a BreakNode. + def visit_break_node(node) + return on_break(on_args_new) if node.arguments.nil? + + args_val = visit_elements(node.arguments.arguments) + on_break(on_args_add_block(args_val, false)) + end + + # Visit an AndNode. + def visit_and_node(node) + visit_binary_operator(node) + end + + # Visit an OrNode. + def visit_or_node(node) + visit_binary_operator(node) + end + + # Visit a TrueNode. + def visit_true_node(node) + bounds(node.location) + on_var_ref(on_kw("true")) + end + + # Visit a FalseNode. + def visit_false_node(node) + bounds(node.location) + on_var_ref(on_kw("false")) + end + + # Visit a FloatNode node. + def visit_float_node(node) + visit_number(node) { |text| on_float(text) } + end + + # Visit a ImaginaryNode node. + def visit_imaginary_node(node) + visit_number(node) { |text| on_imaginary(text) } + end + + # Visit an IntegerNode node. + def visit_integer_node(node) + visit_number(node) { |text| on_int(text) } + end + + # Visit a ParenthesesNode node. + def visit_parentheses_node(node) + body = + if node.body.nil? + on_stmts_add(on_stmts_new, on_void_stmt) + else + visit(node.body) + end + + bounds(node.location) + on_paren(body) + end + + # Visit a BeginNode node. + # This is not at all bulletproof against different structures of begin/rescue/else/ensure/end. + def visit_begin_node(node) + rescue_val = node.rescue_clause ? on_rescue(nil, nil, visit(node.rescue_clause), nil) : nil + ensure_val = node.ensure_clause ? on_ensure(visit(node.ensure_clause.statements)) : nil + on_begin(on_bodystmt(visit(node.statements), rescue_val, nil, ensure_val)) + end + + # Visit a RescueNode node. + def visit_rescue_node(node) + visit(node.statements) + end + + # Visit a ProgramNode node. + def visit_program_node(node) + statements = visit(node.statements) + bounds(node.location) + on_program(statements) + end + + # Visit a RangeNode node. + def visit_range_node(node) + left = visit(node.left) + right = visit(node.right) + + bounds(node.location) + if node.exclude_end? + on_dot3(left, right) + else + on_dot2(left, right) + end + end + + # Visit a RationalNode node. + def visit_rational_node(node) + visit_number(node) { |text| on_rational(text) } + end + + # Visit a StringNode node. + def visit_string_node(node) + bounds(node.content_loc) + tstring_val = on_tstring_content(node.unescaped.to_s) + on_string_literal(on_string_add(on_string_content, tstring_val)) + end + + # Visit an XStringNode node. + def visit_x_string_node(node) + bounds(node.content_loc) + tstring_val = on_tstring_content(node.unescaped.to_s) + on_xstring_literal(on_xstring_add(on_xstring_new, tstring_val)) + end + + # Visit an InterpolatedStringNode node. + def visit_interpolated_string_node(node) + parts = node.parts.map do |part| + case part + when StringNode + bounds(part.content_loc) + on_tstring_content(part.content) + when EmbeddedStatementsNode + on_string_embexpr(visit(part)) + else + raise NotImplementedError, "Unexpected node type in InterpolatedStringNode" + end + end + + string_list = parts.inject(on_string_content) do |items, item| + on_string_add(items, item) + end + + on_string_literal(string_list) + end + + # Visit an EmbeddedStatementsNode node. + def visit_embedded_statements_node(node) + visit(node.statements) + end + + # Visit a SymbolNode node. + def visit_symbol_node(node) + if (opening = node.opening) && (['"', "'"].include?(opening[-1]) || opening.start_with?("%s")) + bounds(node.value_loc) + tstring_val = on_tstring_content(node.value.to_s) + return on_dyna_symbol(on_string_add(on_string_content, tstring_val)) + end + + bounds(node.value_loc) + ident_val = on_ident(node.value.to_s) + on_symbol_literal(on_symbol(ident_val)) + end + + # Visit a StatementsNode node. + def visit_statements_node(node) + bounds(node.location) + node.body.inject(on_stmts_new) do |stmts, stmt| + on_stmts_add(stmts, visit(stmt)) + end + end + + ############################################################################ + # Entrypoints for subclasses + ############################################################################ + + # This is a convenience method that runs the SexpBuilder subclass parser. + def self.sexp_raw(source) + SexpBuilder.new(source).parse + end + + # This is a convenience method that runs the SexpBuilderPP subclass parser. + def self.sexp(source) + SexpBuilderPP.new(source).parse + end + + private + + # Generate Ripper events for a CallNode with no opening_loc + def visit_no_paren_call(node) + # No opening_loc can mean an operator. It can also mean a + # method call with no parentheses. + if node.message.match?(/^[[:punct:]]/) + left = visit(node.receiver) + if node.arguments&.arguments&.length == 1 + right = visit(node.arguments.arguments.first) + + return on_binary(left, node.name, right) + elsif !node.arguments || node.arguments.empty? + return on_unary(node.name, left) + else + raise NotImplementedError, "More than two arguments for operator" + end + elsif node.call_operator_loc.nil? + # In Ripper a method call like "puts myvar" with no parentheses is a "command". + bounds(node.message_loc) + ident_val = on_ident(node.message) + + # Unless it has a block, and then it's an fcall (e.g. "foo { bar }") + if node.block + block_val = visit(node.block) + # In these calls, even if node.arguments is nil, we still get an :args_new call. + args = if node.arguments.nil? + on_args_new + else + on_args_add_block(visit_elements(node.arguments.arguments)) + end + method_args_val = on_method_add_arg(on_fcall(ident_val), args) + return on_method_add_block(method_args_val, block_val) + else + if node.arguments.nil? + return on_command(ident_val, nil) + else + args = on_args_add_block(visit_elements(node.arguments.arguments), false) + return on_command(ident_val, args) + end + end + else + operator = node.call_operator_loc.slice + if operator == "." || operator == "&." + left_val = visit(node.receiver) + + bounds(node.call_operator_loc) + operator_val = operator == "." ? on_period(node.call_operator) : on_op(node.call_operator) + + bounds(node.message_loc) + right_val = on_ident(node.message) + + call_val = on_call(left_val, operator_val, right_val) + + if node.block + block_val = visit(node.block) + return on_method_add_block(call_val, block_val) + else + return call_val + end + else + raise NotImplementedError, "operator other than . or &. for call: #{operator.inspect}" + end + end + end + + # Visit a list of elements, like the elements of an array or arguments. + def visit_elements(elements) + bounds(elements.first.location) + elements.inject(on_args_new) do |args, element| + on_args_add(args, visit(element)) + end + end + + # Visit an operation-and-assign node, such as +=. + def visit_binary_op_assign(node, operator: node.operator) + bounds(node.name_loc) + ident_val = on_ident(node.name.to_s) + + bounds(node.operator_loc) + op_val = on_op(operator) + + on_opassign(on_var_field(ident_val), op_val, visit(node.value)) + end + + # In Prism this is a CallNode with :[] as the operator. + # In Ripper it's an :aref. + def visit_aref_node(node) + first_arg_val = visit(node.arguments.arguments[0]) + args_val = on_args_add_block(on_args_add(on_args_new, first_arg_val), false) + on_aref(visit(node.receiver), args_val) + end + + # In Prism this is a CallNode with :[]= as the operator. + # In Ripper it's an :aref_field. + def visit_aref_field_node(node) + first_arg_val = visit(node.arguments.arguments[0]) + args_val = on_args_add_block(on_args_add(on_args_new, first_arg_val), false) + assign_val = visit(node.arguments.arguments[1]) + on_assign(on_aref_field(visit(node.receiver), args_val), assign_val) + end + + # Visit a node that represents a number. We need to explicitly handle the + # unary - operator. + def visit_number(node) + slice = node.slice + location = node.location + + if slice[0] == "-" + bounds_values(location.start_line, location.start_column + 1) + value = yield slice[1..-1] + + bounds(node.location) + on_unary(visit_unary_operator(:-@), value) + else + bounds(location) + yield slice + end + end + + if RUBY_ENGINE == "jruby" && Gem::Version.new(JRUBY_VERSION) < Gem::Version.new("9.4.6.0") + # JRuby before 9.4.6.0 uses :- for unary minus instead of :-@ + def visit_unary_operator(value) + value == :-@ ? :- : value + end + else + # For most Rubies and JRuby after 9.4.6.0 this is a no-op. + def visit_unary_operator(value) + value + end + end + + if RUBY_ENGINE == "jruby" + # For JRuby, "no block" in an on_block_var is nil + def no_block_value + nil + end + else + # For CRuby et al, "no block" in an on_block_var is false + def no_block_value + false + end + end + + # Visit a binary operator node like an AndNode or OrNode + def visit_binary_operator(node) + left_val = visit(node.left) + right_val = visit(node.right) + on_binary(left_val, node.operator.to_sym, right_val) + end + + # This method is responsible for updating lineno and column information + # to reflect the current node. + # + # This method could be drastically improved with some caching on the start + # of every line, but for now it's good enough. + def bounds(location) + @lineno = location.start_line + @column = location.start_column + end + + # If we need to do something unusual, we can directly update the line number + # and column to reflect the current node. + def bounds_values(lineno, column) + @lineno = lineno + @column = column + end + + # Lazily initialize the parse result. + def result + @result ||= Prism.parse(source) + end + + def _dispatch0; end # :nodoc: + def _dispatch1(_); end # :nodoc: + def _dispatch2(_, _); end # :nodoc: + def _dispatch3(_, _, _); end # :nodoc: + def _dispatch4(_, _, _, _); end # :nodoc: + def _dispatch5(_, _, _, _, _); end # :nodoc: + def _dispatch7(_, _, _, _, _, _, _); end # :nodoc: + + alias_method :on_parse_error, :_dispatch1 + alias_method :on_magic_comment, :_dispatch2 + + (::Ripper::SCANNER_EVENT_TABLE.merge(::Ripper::PARSER_EVENT_TABLE)).each do |event, arity| + alias_method :"on_#{event}", :"_dispatch#{arity}" + end + end + end +end diff --git a/test/prism/ripper_compat_test.rb b/test/prism/ripper_test.rb similarity index 95% rename from test/prism/ripper_compat_test.rb rename to test/prism/ripper_test.rb index 40c609d58ca824..7abb78c72388be 100644 --- a/test/prism/ripper_compat_test.rb +++ b/test/prism/ripper_test.rb @@ -3,7 +3,7 @@ require_relative "test_helper" module Prism - class RipperCompatTest < TestCase + class RipperTest < TestCase def test_binary assert_equivalent("1 + 2") assert_equivalent("3 - 4 * 5") @@ -148,11 +148,11 @@ def assert_equivalent(source) expected = Ripper.sexp_raw(source) refute_nil expected - assert_equal expected, RipperCompat.sexp_raw(source) + assert_equal expected, Prism::Translation::Ripper.sexp_raw(source) end end - class RipperCompatFixturesTest < TestCase + class RipperFixturesTest < TestCase #base = File.join(__dir__, "fixtures") #relatives = ENV["FOCUS"] ? [ENV["FOCUS"]] : Dir["**/*.txt", base: base] relatives = [ @@ -177,7 +177,7 @@ class RipperCompatFixturesTest < TestCase puts "Could not parse #{path.inspect}!" end refute_nil expected - assert_equal expected, RipperCompat.sexp_raw(source) + assert_equal expected, Translation::Ripper.sexp_raw(source) end end