diff options
Diffstat (limited to 'test/prism/ruby')
-rw-r--r-- | test/prism/ruby/compiler_test.rb | 31 | ||||
-rw-r--r-- | test/prism/ruby/desugar_compiler_test.rb | 80 | ||||
-rw-r--r-- | test/prism/ruby/dispatcher_test.rb | 46 | ||||
-rw-r--r-- | test/prism/ruby/location_test.rb | 173 | ||||
-rw-r--r-- | test/prism/ruby/parameters_signature_test.rb | 91 | ||||
-rw-r--r-- | test/prism/ruby/parser_test.rb | 288 | ||||
-rw-r--r-- | test/prism/ruby/pattern_test.rb | 132 | ||||
-rw-r--r-- | test/prism/ruby/reflection_test.rb | 22 | ||||
-rw-r--r-- | test/prism/ruby/ripper_test.rb | 62 | ||||
-rw-r--r-- | test/prism/ruby/ruby_parser_test.rb | 127 | ||||
-rw-r--r-- | test/prism/ruby/tunnel_test.rb | 26 |
11 files changed, 1078 insertions, 0 deletions
diff --git a/test/prism/ruby/compiler_test.rb b/test/prism/ruby/compiler_test.rb new file mode 100644 index 0000000000..35ccfd5950 --- /dev/null +++ b/test/prism/ruby/compiler_test.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true +# typed: ignore + +require_relative "../test_helper" + +module Prism + class CompilerTest < TestCase + class SExpressions < Prism::Compiler + def visit_arguments_node(node) + [:arguments, super] + end + + def visit_call_node(node) + [:call, super] + end + + def visit_integer_node(node) + [:integer] + end + + def visit_program_node(node) + [:program, super] + end + end + + def test_compiler + expected = [:program, [[[:call, [[:integer], [:arguments, [[:integer]]]]]]]] + assert_equal expected, Prism.parse("1 + 2").value.accept(SExpressions.new) + end + end +end diff --git a/test/prism/ruby/desugar_compiler_test.rb b/test/prism/ruby/desugar_compiler_test.rb new file mode 100644 index 0000000000..fe9a25e030 --- /dev/null +++ b/test/prism/ruby/desugar_compiler_test.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +require_relative "../test_helper" + +module Prism + class DesugarCompilerTest < TestCase + def test_and_write + assert_desugars("(AndNode (ClassVariableReadNode) (ClassVariableWriteNode (CallNode)))", "@@foo &&= bar") + assert_not_desugared("Foo::Bar &&= baz", "Desugaring would execute Foo twice or need temporary variables") + assert_desugars("(AndNode (ConstantReadNode) (ConstantWriteNode (CallNode)))", "Foo &&= bar") + assert_desugars("(AndNode (GlobalVariableReadNode) (GlobalVariableWriteNode (CallNode)))", "$foo &&= bar") + assert_desugars("(AndNode (InstanceVariableReadNode) (InstanceVariableWriteNode (CallNode)))", "@foo &&= bar") + assert_desugars("(AndNode (LocalVariableReadNode) (LocalVariableWriteNode (CallNode)))", "foo &&= bar") + assert_desugars("(AndNode (LocalVariableReadNode) (LocalVariableWriteNode (CallNode)))", "foo = 1; foo &&= bar") + end + + def test_or_write + assert_desugars("(IfNode (DefinedNode (ClassVariableReadNode)) (StatementsNode (ClassVariableReadNode)) (ElseNode (StatementsNode (ClassVariableWriteNode (CallNode)))))", "@@foo ||= bar") + assert_not_desugared("Foo::Bar ||= baz", "Desugaring would execute Foo twice or need temporary variables") + assert_desugars("(IfNode (DefinedNode (ConstantReadNode)) (StatementsNode (ConstantReadNode)) (ElseNode (StatementsNode (ConstantWriteNode (CallNode)))))", "Foo ||= bar") + assert_desugars("(IfNode (DefinedNode (GlobalVariableReadNode)) (StatementsNode (GlobalVariableReadNode)) (ElseNode (StatementsNode (GlobalVariableWriteNode (CallNode)))))", "$foo ||= bar") + assert_desugars("(OrNode (InstanceVariableReadNode) (InstanceVariableWriteNode (CallNode)))", "@foo ||= bar") + assert_desugars("(OrNode (LocalVariableReadNode) (LocalVariableWriteNode (CallNode)))", "foo ||= bar") + assert_desugars("(OrNode (LocalVariableReadNode) (LocalVariableWriteNode (CallNode)))", "foo = 1; foo ||= bar") + end + + def test_operator_write + assert_desugars("(ClassVariableWriteNode (CallNode (ClassVariableReadNode) (ArgumentsNode (CallNode))))", "@@foo += bar") + assert_not_desugared("Foo::Bar += baz", "Desugaring would execute Foo twice or need temporary variables") + assert_desugars("(ConstantWriteNode (CallNode (ConstantReadNode) (ArgumentsNode (CallNode))))", "Foo += bar") + assert_desugars("(GlobalVariableWriteNode (CallNode (GlobalVariableReadNode) (ArgumentsNode (CallNode))))", "$foo += bar") + assert_desugars("(InstanceVariableWriteNode (CallNode (InstanceVariableReadNode) (ArgumentsNode (CallNode))))", "@foo += bar") + assert_desugars("(LocalVariableWriteNode (CallNode (LocalVariableReadNode) (ArgumentsNode (CallNode))))", "foo += bar") + assert_desugars("(LocalVariableWriteNode (CallNode (LocalVariableReadNode) (ArgumentsNode (CallNode))))", "foo = 1; foo += bar") + end + + private + + def ast_inspect(node) + parts = [node.class.name.split("::").last] + + node.deconstruct_keys(nil).each do |_, value| + case value + when Node + parts << ast_inspect(value) + when Array + parts.concat(value.map { |element| ast_inspect(element) }) + end + end + + "(#{parts.join(" ")})" + end + + # Ensure every node is only present once in the AST. + # If the same node is present twice it would most likely indicate it is executed twice, which is invalid semantically. + # This also acts as a sanity check that Node#child_nodes returns only nodes or nil (which caught a couple bugs). + def ensure_every_node_once_in_ast(node, all_nodes = {}.compare_by_identity) + if all_nodes.include?(node) + raise "#{node.inspect} is present multiple times in the desugared AST and likely executed multiple times" + else + all_nodes[node] = true + end + node.child_nodes.each do |child| + ensure_every_node_once_in_ast(child, all_nodes) unless child.nil? + end + end + + def assert_desugars(expected, source) + ast = Prism.parse(source).value.accept(DesugarCompiler.new) + assert_equal expected, ast_inspect(ast.statements.body.last) + + ensure_every_node_once_in_ast(ast) + end + + def assert_not_desugared(source, reason) + ast = Prism.parse(source).value + assert_equal_nodes(ast, ast.accept(DesugarCompiler.new)) + end + end +end diff --git a/test/prism/ruby/dispatcher_test.rb b/test/prism/ruby/dispatcher_test.rb new file mode 100644 index 0000000000..1b6d7f4117 --- /dev/null +++ b/test/prism/ruby/dispatcher_test.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require_relative "../test_helper" + +module Prism + class DispatcherTest < TestCase + class TestListener + attr_reader :events_received + + def initialize + @events_received = [] + end + + def on_call_node_enter(node) + events_received << :on_call_node_enter + end + + def on_call_node_leave(node) + events_received << :on_call_node_leave + end + + def on_integer_node_enter(node) + events_received << :on_integer_node_enter + end + end + + def test_dispatching_events + listener = TestListener.new + dispatcher = Dispatcher.new + dispatcher.register(listener, :on_call_node_enter, :on_call_node_leave, :on_integer_node_enter) + + root = Prism.parse(<<~RUBY).value + def foo + something(1, 2, 3) + end + RUBY + + dispatcher.dispatch(root) + assert_equal([:on_call_node_enter, :on_integer_node_enter, :on_integer_node_enter, :on_integer_node_enter, :on_call_node_leave], listener.events_received) + + listener.events_received.clear + dispatcher.dispatch_once(root.statements.body.first.body.body.first) + assert_equal([:on_call_node_enter, :on_call_node_leave], listener.events_received) + end + end +end diff --git a/test/prism/ruby/location_test.rb b/test/prism/ruby/location_test.rb new file mode 100644 index 0000000000..fc80a5b875 --- /dev/null +++ b/test/prism/ruby/location_test.rb @@ -0,0 +1,173 @@ +# frozen_string_literal: true + +require_relative "../test_helper" + +module Prism + class LocationTest < TestCase + def test_join + call = Prism.parse_statement("1234 + 567") + receiver = call.receiver + argument = call.arguments.arguments.first + + joined = receiver.location.join(argument.location) + assert_equal 0, joined.start_offset + assert_equal 10, joined.length + + assert_raise(RuntimeError, "Incompatible locations") do + argument.location.join(receiver.location) + end + + other_argument = Prism.parse_statement("1234 + 567").arguments.arguments.first + + assert_raise(RuntimeError, "Incompatible sources") do + other_argument.location.join(receiver.location) + end + + assert_raise(RuntimeError, "Incompatible sources") do + receiver.location.join(other_argument.location) + end + end + + def test_character_offsets + program = Prism.parse("š + š\nš ||= š").value + + # first š + location = program.statements.body.first.receiver.location + assert_equal 0, location.start_character_offset + assert_equal 1, location.end_character_offset + assert_equal 0, location.start_character_column + assert_equal 1, location.end_character_column + + # second š + location = program.statements.body.first.arguments.arguments.first.location + assert_equal 4, location.start_character_offset + assert_equal 5, location.end_character_offset + assert_equal 4, location.start_character_column + assert_equal 5, location.end_character_column + + # first š + location = program.statements.body.last.name_loc + assert_equal 6, location.start_character_offset + assert_equal 7, location.end_character_offset + assert_equal 0, location.start_character_column + assert_equal 1, location.end_character_column + + # second š + location = program.statements.body.last.value.location + assert_equal 12, location.start_character_offset + assert_equal 13, location.end_character_offset + assert_equal 6, location.start_character_column + assert_equal 7, location.end_character_column + end + + def test_code_units + program = Prism.parse("š + š\nš ||= š").value + + # first š + location = program.statements.body.first.receiver.location + + assert_equal 0, location.start_code_units_offset(Encoding::UTF_8) + assert_equal 0, location.start_code_units_offset(Encoding::UTF_16LE) + assert_equal 0, location.start_code_units_offset(Encoding::UTF_32LE) + + assert_equal 1, location.end_code_units_offset(Encoding::UTF_8) + assert_equal 2, location.end_code_units_offset(Encoding::UTF_16LE) + assert_equal 1, location.end_code_units_offset(Encoding::UTF_32LE) + + assert_equal 0, location.start_code_units_column(Encoding::UTF_8) + assert_equal 0, location.start_code_units_column(Encoding::UTF_16LE) + assert_equal 0, location.start_code_units_column(Encoding::UTF_32LE) + + assert_equal 1, location.end_code_units_column(Encoding::UTF_8) + assert_equal 2, location.end_code_units_column(Encoding::UTF_16LE) + assert_equal 1, location.end_code_units_column(Encoding::UTF_32LE) + + # second š + location = program.statements.body.first.arguments.arguments.first.location + + assert_equal 4, location.start_code_units_offset(Encoding::UTF_8) + assert_equal 5, location.start_code_units_offset(Encoding::UTF_16LE) + assert_equal 4, location.start_code_units_offset(Encoding::UTF_32LE) + + assert_equal 5, location.end_code_units_offset(Encoding::UTF_8) + assert_equal 7, location.end_code_units_offset(Encoding::UTF_16LE) + assert_equal 5, location.end_code_units_offset(Encoding::UTF_32LE) + + assert_equal 4, location.start_code_units_column(Encoding::UTF_8) + assert_equal 5, location.start_code_units_column(Encoding::UTF_16LE) + assert_equal 4, location.start_code_units_column(Encoding::UTF_32LE) + + assert_equal 5, location.end_code_units_column(Encoding::UTF_8) + assert_equal 7, location.end_code_units_column(Encoding::UTF_16LE) + assert_equal 5, location.end_code_units_column(Encoding::UTF_32LE) + + # first š + location = program.statements.body.last.name_loc + + assert_equal 6, location.start_code_units_offset(Encoding::UTF_8) + assert_equal 8, location.start_code_units_offset(Encoding::UTF_16LE) + assert_equal 6, location.start_code_units_offset(Encoding::UTF_32LE) + + assert_equal 7, location.end_code_units_offset(Encoding::UTF_8) + assert_equal 10, location.end_code_units_offset(Encoding::UTF_16LE) + assert_equal 7, location.end_code_units_offset(Encoding::UTF_32LE) + + assert_equal 0, location.start_code_units_column(Encoding::UTF_8) + assert_equal 0, location.start_code_units_column(Encoding::UTF_16LE) + assert_equal 0, location.start_code_units_column(Encoding::UTF_32LE) + + assert_equal 1, location.end_code_units_column(Encoding::UTF_8) + assert_equal 2, location.end_code_units_column(Encoding::UTF_16LE) + assert_equal 1, location.end_code_units_column(Encoding::UTF_32LE) + + # second š + location = program.statements.body.last.value.location + + assert_equal 12, location.start_code_units_offset(Encoding::UTF_8) + assert_equal 15, location.start_code_units_offset(Encoding::UTF_16LE) + assert_equal 12, location.start_code_units_offset(Encoding::UTF_32LE) + + assert_equal 13, location.end_code_units_offset(Encoding::UTF_8) + assert_equal 17, location.end_code_units_offset(Encoding::UTF_16LE) + assert_equal 13, location.end_code_units_offset(Encoding::UTF_32LE) + + assert_equal 6, location.start_code_units_column(Encoding::UTF_8) + assert_equal 7, location.start_code_units_column(Encoding::UTF_16LE) + assert_equal 6, location.start_code_units_column(Encoding::UTF_32LE) + + assert_equal 7, location.end_code_units_column(Encoding::UTF_8) + assert_equal 9, location.end_code_units_column(Encoding::UTF_16LE) + assert_equal 7, location.end_code_units_column(Encoding::UTF_32LE) + end + + def test_chop + location = Prism.parse("foo").value.location + + assert_equal "fo", location.chop.slice + assert_equal "", location.chop.chop.chop.slice + + # Check that we don't go negative. + 10.times { location = location.chop } + assert_equal "", location.slice + end + + def test_slice_lines + method = Prism.parse_statement("\nprivate def foo\nend\n").arguments.arguments.first + + assert_equal "private def foo\nend\n", method.slice_lines + end + + def test_adjoin + program = Prism.parse("foo.bar = 1").value + + location = program.statements.body.first.message_loc + adjoined = location.adjoin("=") + + assert_kind_of Location, adjoined + refute_equal location, adjoined + + assert_equal 4, adjoined.start_offset + assert_equal 9, adjoined.end_offset + end + end +end diff --git a/test/prism/ruby/parameters_signature_test.rb b/test/prism/ruby/parameters_signature_test.rb new file mode 100644 index 0000000000..9256bcc070 --- /dev/null +++ b/test/prism/ruby/parameters_signature_test.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +return if RUBY_VERSION < "3.2" + +require_relative "../test_helper" + +module Prism + class ParametersSignatureTest < TestCase + def test_req + assert_parameters([[:req, :a]], "a") + end + + def test_req_destructure + assert_parameters([[:req]], "(a, b)") + end + + def test_opt + assert_parameters([[:opt, :a]], "a = 1") + end + + def test_rest + assert_parameters([[:rest, :a]], "*a") + end + + def test_rest_anonymous + assert_parameters([[:rest, :*]], "*") + end + + def test_post + assert_parameters([[:rest, :a], [:req, :b]], "*a, b") + end + + def test_post_destructure + assert_parameters([[:rest, :a], [:req]], "*a, (b, c)") + end + + def test_keyreq + assert_parameters([[:keyreq, :a]], "a:") + end + + def test_key + assert_parameters([[:key, :a]], "a: 1") + end + + def test_keyrest + assert_parameters([[:keyrest, :a]], "**a") + end + + def test_nokey + assert_parameters([[:nokey]], "**nil") + end + + def test_keyrest_anonymous + assert_parameters([[:keyrest, :**]], "**") + end + + def test_key_ordering + omit("TruffleRuby returns keys in order they were declared") if RUBY_ENGINE == "truffleruby" + assert_parameters([[:keyreq, :a], [:keyreq, :b], [:key, :c], [:key, :d]], "a:, c: 1, b:, d: 2") + end + + def test_block + assert_parameters([[:block, :a]], "&a") + end + + def test_block_anonymous + assert_parameters([[:block, :&]], "&") + end + + def test_forwarding + assert_parameters([[:rest, :*], [:keyrest, :**], [:block, :&]], "...") + end + + private + + def assert_parameters(expected, source) + # Compare against our expectation. + assert_equal(expected, signature(source)) + + # Compare against Ruby's expectation. + object = Object.new + eval("def object.m(#{source}); end") + assert_equal(expected, object.method(:m).parameters) + end + + def signature(source) + program = Prism.parse("def m(#{source}); end").value + program.statements.body.first.parameters.signature + end + end +end diff --git a/test/prism/ruby/parser_test.rb b/test/prism/ruby/parser_test.rb new file mode 100644 index 0000000000..a76f193f52 --- /dev/null +++ b/test/prism/ruby/parser_test.rb @@ -0,0 +1,288 @@ +# frozen_string_literal: true + +require_relative "../test_helper" + +begin + verbose, $VERBOSE = $VERBOSE, nil + require "parser/ruby33" + require "prism/translation/parser33" +rescue LoadError + # In CRuby's CI, we're not going to test against the parser gem because we + # don't want to have to install it. So in this case we'll just skip this test. + return +ensure + $VERBOSE = verbose +end + +# First, opt in to every AST feature. +Parser::Builders::Default.modernize + +# Modify the source map == check so that it doesn't check against the node +# itself so we don't get into a recursive loop. +Parser::Source::Map.prepend( + Module.new { + def ==(other) + self.class == other.class && + (instance_variables - %i[@node]).map do |ivar| + instance_variable_get(ivar) == other.instance_variable_get(ivar) + end.reduce(:&) + end + } +) + +# Next, ensure that we're comparing the nodes and also comparing the source +# ranges so that we're getting all of the necessary information. +Parser::AST::Node.prepend( + Module.new { + def ==(other) + super && (location == other.location) + end + } +) + +module Prism + class ParserTest < TestCase + # These files are erroring because of the parser gem being wrong. + skip_incorrect = [ + "embdoc_no_newline_at_end.txt" + ] + + # These files are either failing to parse or failing to translate, so we'll + # skip them for now. + skip_all = skip_incorrect | [ + "dash_heredocs.txt", + "dos_endings.txt", + "heredocs_with_ignored_newlines.txt", + "regex.txt", + "regex_char_width.txt", + "spanning_heredoc.txt", + "spanning_heredoc_newlines.txt", + "unescaping.txt", + "seattlerb/backticks_interpolation_line.txt", + "seattlerb/block_decomp_anon_splat_arg.txt", + "seattlerb/block_decomp_arg_splat_arg.txt", + "seattlerb/block_decomp_arg_splat.txt", + "seattlerb/block_decomp_splat.txt", + "seattlerb/block_paren_splat.txt", + "seattlerb/bug190.txt", + "seattlerb/case_in_hash_pat_rest_solo.txt", + "seattlerb/case_in_hash_pat_rest.txt", + "seattlerb/case_in.txt", + "seattlerb/heredoc_nested.txt", + "seattlerb/heredoc_squiggly_blank_line_plus_interpolation.txt", + "seattlerb/heredoc_with_carriage_return_escapes_windows.txt", + "seattlerb/heredoc_with_carriage_return_escapes.txt", + "seattlerb/heredoc_with_extra_carriage_returns_windows.txt", + "seattlerb/heredoc_with_only_carriage_returns_windows.txt", + "seattlerb/heredoc_with_only_carriage_returns.txt", + "seattlerb/masgn_double_paren.txt", + "seattlerb/parse_line_heredoc_hardnewline.txt", + "seattlerb/parse_pattern_044.txt", + "seattlerb/parse_pattern_058_2.txt", + "seattlerb/parse_pattern_058.txt", + "seattlerb/pct_nl.txt", + "seattlerb/pctW_lineno.txt", + "seattlerb/regexp_esc_C_slash.txt", + "seattlerb/TestRubyParserShared.txt", + "unparser/corpus/literal/assignment.txt", + "unparser/corpus/literal/block.txt", + "unparser/corpus/literal/def.txt", + "unparser/corpus/literal/dstr.txt", + "unparser/corpus/literal/literal.txt", + "unparser/corpus/literal/pattern.txt", + "unparser/corpus/semantic/dstr.txt", + "unparser/corpus/semantic/opasgn.txt", + "whitequark/dedenting_interpolating_heredoc_fake_line_continuation.txt", + "whitequark/masgn_nested.txt", + "whitequark/newline_in_hash_argument.txt", + "whitequark/parser_bug_640.txt", + "whitequark/parser_slash_slash_n_escaping_in_literals.txt", + "whitequark/ruby_bug_11989.txt", + "whitequark/slash_newline_in_heredocs.txt", + "whitequark/unary_num_pow_precedence.txt" + ] + + # Not sure why these files are failing on JRuby, but skipping them for now. + if RUBY_ENGINE == "jruby" + skip_all.push("emoji_method_calls.txt", "symbols.txt") + end + + # These files are failing to translate their lexer output into the lexer + # output expected by the parser gem, so we'll skip them for now. + skip_tokens = [ + "comments.txt", + "heredoc_with_comment.txt", + "indented_file_end.txt", + "methods.txt", + "strings.txt", + "tilde_heredocs.txt", + "xstring_with_backslash.txt", + "seattlerb/bug169.txt", + "seattlerb/class_comments.txt", + "seattlerb/difficult4__leading_dots2.txt", + "seattlerb/difficult6__7.txt", + "seattlerb/difficult6__8.txt", + "seattlerb/dsym_esc_to_sym.txt", + "seattlerb/heredoc__backslash_dos_format.txt", + "seattlerb/heredoc_backslash_nl.txt", + "seattlerb/heredoc_comma_arg.txt", + "seattlerb/heredoc_squiggly_blank_lines.txt", + "seattlerb/heredoc_squiggly_interp.txt", + "seattlerb/heredoc_squiggly_tabs_extra.txt", + "seattlerb/heredoc_squiggly_tabs.txt", + "seattlerb/heredoc_squiggly_visually_blank_lines.txt", + "seattlerb/heredoc_squiggly.txt", + "seattlerb/heredoc_unicode.txt", + "seattlerb/heredoc_with_interpolation_and_carriage_return_escapes_windows.txt", + "seattlerb/heredoc_with_interpolation_and_carriage_return_escapes.txt", + "seattlerb/interpolated_symbol_array_line_breaks.txt", + "seattlerb/interpolated_word_array_line_breaks.txt", + "seattlerb/label_vs_string.txt", + "seattlerb/module_comments.txt", + "seattlerb/non_interpolated_symbol_array_line_breaks.txt", + "seattlerb/non_interpolated_word_array_line_breaks.txt", + "seattlerb/parse_line_block_inline_comment_leading_newlines.txt", + "seattlerb/parse_line_block_inline_comment.txt", + "seattlerb/parse_line_block_inline_multiline_comment.txt", + "seattlerb/parse_line_dstr_escaped_newline.txt", + "seattlerb/parse_line_heredoc.txt", + "seattlerb/parse_line_multiline_str_literal_n.txt", + "seattlerb/parse_line_str_with_newline_escape.txt", + "seattlerb/pct_Q_backslash_nl.txt", + "seattlerb/pct_w_heredoc_interp_nested.txt", + "seattlerb/qsymbols_empty_space.txt", + "seattlerb/qw_escape_term.txt", + "seattlerb/qWords_space.txt", + "seattlerb/read_escape_unicode_curlies.txt", + "seattlerb/read_escape_unicode_h4.txt", + "seattlerb/required_kwarg_no_value.txt", + "seattlerb/slashy_newlines_within_string.txt", + "seattlerb/str_double_escaped_newline.txt", + "seattlerb/str_double_newline.txt", + "seattlerb/str_evstr_escape.txt", + "seattlerb/str_newline_hash_line_number.txt", + "seattlerb/str_single_newline.txt", + "seattlerb/symbol_empty.txt", + "seattlerb/symbols_empty_space.txt", + "whitequark/args.txt", + "whitequark/beginless_erange_after_newline.txt", + "whitequark/beginless_irange_after_newline.txt", + "whitequark/bug_ascii_8bit_in_literal.txt", + "whitequark/bug_def_no_paren_eql_begin.txt", + "whitequark/dedenting_heredoc.txt", + "whitequark/dedenting_non_interpolating_heredoc_line_continuation.txt", + "whitequark/forward_arg_with_open_args.txt", + "whitequark/interp_digit_var.txt", + "whitequark/lbrace_arg_after_command_args.txt", + "whitequark/multiple_pattern_matches.txt", + "whitequark/parser_drops_truncated_parts_of_squiggly_heredoc.txt", + "whitequark/ruby_bug_11990.txt", + "whitequark/ruby_bug_14690.txt", + "whitequark/ruby_bug_9669.txt", + "whitequark/space_args_arg_block.txt", + "whitequark/space_args_block.txt" + ] + + Fixture.each(except: skip_all) do |fixture| + define_method(fixture.test_name) do + assert_equal_parses(fixture, compare_tokens: !skip_tokens.include?(fixture.path)) + end + end + + private + + def assert_equal_parses(fixture, compare_tokens: true) + buffer = Parser::Source::Buffer.new(fixture.path, 1) + buffer.source = fixture.read + + parser = Parser::Ruby33.new + parser.diagnostics.consumer = ->(*) {} + parser.diagnostics.all_errors_are_fatal = true + + expected_ast, expected_comments, expected_tokens = + begin + ignore_warnings { parser.tokenize(buffer) } + rescue ArgumentError, Parser::SyntaxError + return + end + + actual_ast, actual_comments, actual_tokens = + ignore_warnings { Prism::Translation::Parser33.new.tokenize(buffer) } + + assert_equal expected_ast, actual_ast, -> { assert_equal_asts_message(expected_ast, actual_ast) } + assert_equal_tokens(expected_tokens, actual_tokens) if compare_tokens + assert_equal_comments(expected_comments, actual_comments) + end + + def assert_equal_asts_message(expected_ast, actual_ast) + queue = [[expected_ast, actual_ast]] + + while (left, right = queue.shift) + if left.type != right.type + return "expected: #{left.type}\nactual: #{right.type}" + end + + if left.location != right.location + return "expected:\n#{left.inspect}\n#{left.location.inspect}\nactual:\n#{right.inspect}\n#{right.location.inspect}" + end + + if left.type == :str && left.children[0] != right.children[0] + return "expected: #{left.inspect}\nactual: #{right.inspect}" + end + + left.children.zip(right.children).each do |left_child, right_child| + queue << [left_child, right_child] if left_child.is_a?(Parser::AST::Node) + end + end + + "expected: #{expected_ast.inspect}\nactual: #{actual_ast.inspect}" + end + + def assert_equal_tokens(expected_tokens, actual_tokens) + if expected_tokens != actual_tokens + expected_index = 0 + actual_index = 0 + + while expected_index < expected_tokens.length + expected_token = expected_tokens[expected_index] + actual_token = actual_tokens[actual_index] + + expected_index += 1 + actual_index += 1 + + # The parser gem always has a space before a string end in list + # literals, but we don't. So we'll skip over the space. + if expected_token[0] == :tSPACE && actual_token[0] == :tSTRING_END + expected_index += 1 + next + end + + # There are a lot of tokens that have very specific meaning according + # to the context of the parser. We don't expose that information in + # prism, so we need to normalize these tokens a bit. + case actual_token[0] + when :kDO + actual_token[0] = expected_token[0] if %i[kDO_BLOCK kDO_LAMBDA].include?(expected_token[0]) + when :tLPAREN + actual_token[0] = expected_token[0] if expected_token[0] == :tLPAREN2 + when :tPOW + actual_token[0] = expected_token[0] if expected_token[0] == :tDSTAR + end + + # Now we can assert that the tokens are actually equal. + assert_equal expected_token, actual_token, -> { + "expected: #{expected_token.inspect}\n" \ + "actual: #{actual_token.inspect}" + } + end + end + end + + def assert_equal_comments(expected_comments, actual_comments) + assert_equal expected_comments, actual_comments, -> { + "expected: #{expected_comments.inspect}\n" \ + "actual: #{actual_comments.inspect}" + } + end + end +end diff --git a/test/prism/ruby/pattern_test.rb b/test/prism/ruby/pattern_test.rb new file mode 100644 index 0000000000..23f512fc1c --- /dev/null +++ b/test/prism/ruby/pattern_test.rb @@ -0,0 +1,132 @@ +# frozen_string_literal: true + +require_relative "../test_helper" + +module Prism + class PatternTest < TestCase + def test_invalid_syntax + assert_raise(Pattern::CompilationError) { scan("", "<>") } + end + + def test_invalid_constant + assert_raise(Pattern::CompilationError) { scan("", "Foo") } + end + + def test_invalid_nested_constant + assert_raise(Pattern::CompilationError) { scan("", "Foo::Bar") } + end + + def test_regexp_with_interpolation + assert_raise(Pattern::CompilationError) { scan("", "/\#{foo}/") } + end + + def test_string_with_interpolation + assert_raise(Pattern::CompilationError) { scan("", '"#{foo}"') } + end + + def test_symbol_with_interpolation + assert_raise(Pattern::CompilationError) { scan("", ":\"\#{foo}\"") } + end + + def test_invalid_node + assert_raise(Pattern::CompilationError) { scan("", "IntegerNode[^foo]") } + end + + def test_self + assert_raise(Pattern::CompilationError) { scan("", "self") } + end + + def test_array_pattern_no_constant + results = scan("1 + 2", "[IntegerNode]") + + assert_equal 1, results.length + end + + def test_array_pattern + results = scan("1 + 2", "CallNode[name: :+, receiver: IntegerNode, arguments: [IntegerNode]]") + + assert_equal 1, results.length + end + + def test_alternation_pattern + results = scan("Foo + Bar + 1", "ConstantReadNode | IntegerNode") + + assert_equal 3, results.length + assert_equal 1, results.grep(IntegerNode).first.value + end + + def test_constant_read_node + results = scan("Foo + Bar + Baz", "ConstantReadNode") + + assert_equal 3, results.length + assert_equal %w[Bar Baz Foo], results.map(&:slice).sort + end + + def test_object_const + results = scan("1 + 2 + 3", "IntegerNode[]") + + assert_equal 3, results.length + end + + def test_constant_path + results = scan("Foo + Bar + Baz", "Prism::ConstantReadNode") + + assert_equal 3, results.length + end + + def test_hash_pattern_no_constant + results = scan("Foo + Bar + Baz", "{ name: :+ }") + + assert_equal 2, results.length + end + + def test_hash_pattern_regexp + results = scan("Foo + Bar + Baz", "{ name: /^[[:punct:]]$/ }") + + assert_equal 2, results.length + assert_equal ["Prism::CallNode"], results.map { |node| node.class.name }.uniq + end + + def test_nil + results = scan("foo", "{ receiver: nil }") + + assert_equal 1, results.length + end + + def test_regexp_options + results = scan("@foo + @bar + @baz", "InstanceVariableReadNode[name: /^@B/i]") + + assert_equal 2, results.length + end + + def test_string_empty + results = scan("", "''") + + assert_empty results + end + + def test_symbol_empty + results = scan("", ":''") + + assert_empty results + end + + def test_symbol_plain + results = scan("@foo", "{ name: :\"@foo\" }") + + assert_equal 1, results.length + end + + def test_symbol + results = scan("@foo", "{ name: :@foo }") + + assert_equal 1, results.length + end + + private + + def scan(source, query) + Prism::Pattern.new(query).scan(Prism.parse(source).value).to_a + end + end +end diff --git a/test/prism/ruby/reflection_test.rb b/test/prism/ruby/reflection_test.rb new file mode 100644 index 0000000000..3ac462e1ac --- /dev/null +++ b/test/prism/ruby/reflection_test.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require_relative "../test_helper" + +module Prism + class ReflectionTest < TestCase + def test_fields_for + fields = Reflection.fields_for(CallNode) + methods = CallNode.instance_methods(false) + + fields.each do |field| + if field.is_a?(Reflection::FlagsField) + field.flags.each do |flag| + assert_includes methods, flag + end + else + assert_includes methods, field.name + end + end + end + end +end diff --git a/test/prism/ruby/ripper_test.rb b/test/prism/ruby/ripper_test.rb new file mode 100644 index 0000000000..8db47da3d3 --- /dev/null +++ b/test/prism/ruby/ripper_test.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +return if RUBY_VERSION < "3.3" + +require_relative "../test_helper" + +module Prism + class RipperTest < TestCase + # Skip these tests that Ripper is reporting the wrong results for. + incorrect = [ + # Ripper incorrectly attributes the block to the keyword. + "seattlerb/block_break.txt", + "seattlerb/block_next.txt", + "seattlerb/block_return.txt", + "whitequark/break_block.txt", + "whitequark/next_block.txt", + "whitequark/return_block.txt", + + # Ripper is not accounting for locals created by patterns using the ** + # operator within an `in` clause. + "seattlerb/parse_pattern_058.txt", + + # Ripper cannot handle named capture groups in regular expressions. + "regex.txt", + "regex_char_width.txt", + "whitequark/lvar_injecting_match.txt", + + # Ripper fails to understand some structures that span across heredocs. + "spanning_heredoc.txt" + ] + + # Skip these tests that we haven't implemented yet. + omitted = [ + "dos_endings.txt", + "heredocs_with_ignored_newlines.txt", + "seattlerb/block_call_dot_op2_brace_block.txt", + "seattlerb/block_command_operation_colon.txt", + "seattlerb/block_command_operation_dot.txt", + "seattlerb/heredoc__backslash_dos_format.txt", + "seattlerb/heredoc_backslash_nl.txt", + "seattlerb/heredoc_nested.txt", + "seattlerb/heredoc_squiggly_blank_line_plus_interpolation.txt", + "tilde_heredocs.txt", + "unparser/corpus/semantic/dstr.txt", + "whitequark/dedenting_heredoc.txt", + "whitequark/parser_drops_truncated_parts_of_squiggly_heredoc.txt", + "whitequark/parser_slash_slash_n_escaping_in_literals.txt", + "whitequark/send_block_chain_cmd.txt", + "whitequark/slash_newline_in_heredocs.txt" + ] + + Fixture.each(except: incorrect | omitted) do |fixture| + define_method(fixture.test_name) { assert_ripper(fixture.read) } + end + + private + + def assert_ripper(source) + assert_equal Ripper.sexp_raw(source), Prism::Translation::Ripper.sexp_raw(source) + end + end +end diff --git a/test/prism/ruby/ruby_parser_test.rb b/test/prism/ruby/ruby_parser_test.rb new file mode 100644 index 0000000000..a13daeeb84 --- /dev/null +++ b/test/prism/ruby/ruby_parser_test.rb @@ -0,0 +1,127 @@ +# frozen_string_literal: true + +return if RUBY_ENGINE == "jruby" + +require_relative "../test_helper" + +begin + require "ruby_parser" +rescue LoadError + # In CRuby's CI, we're not going to test against the ruby_parser gem because + # we don't want to have to install it. So in this case we'll just skip this + # test. + return +end + +# We want to also compare lines and files to make sure we're setting them +# correctly. +Sexp.prepend( + Module.new do + def ==(other) + super && line == other.line && file == other.file # && line_max == other.line_max + end + end +) + +module Prism + class RubyParserTest < TestCase + todos = [ + "newline_terminated.txt", + "regex_char_width.txt", + "seattlerb/bug169.txt", + "seattlerb/masgn_colon3.txt", + "seattlerb/messy_op_asgn_lineno.txt", + "seattlerb/op_asgn_primary_colon_const_command_call.txt", + "seattlerb/regexp_esc_C_slash.txt", + "seattlerb/str_lit_concat_bad_encodings.txt", + "unescaping.txt", + "unparser/corpus/literal/kwbegin.txt", + "unparser/corpus/literal/send.txt", + "whitequark/masgn_const.txt", + "whitequark/ruby_bug_12402.txt", + "whitequark/ruby_bug_14690.txt", + "whitequark/space_args_block.txt" + ] + + # https://github.com/seattlerb/ruby_parser/issues/344 + failures = [ + "alias.txt", + "dos_endings.txt", + "heredocs_with_ignored_newlines.txt", + "method_calls.txt", + "methods.txt", + "multi_write.txt", + "not.txt", + "patterns.txt", + "regex.txt", + "seattlerb/and_multi.txt", + "seattlerb/heredoc__backslash_dos_format.txt", + "seattlerb/heredoc_bad_hex_escape.txt", + "seattlerb/heredoc_bad_oct_escape.txt", + "seattlerb/heredoc_with_extra_carriage_horrible_mix.txt", + "seattlerb/heredoc_with_extra_carriage_returns_windows.txt", + "seattlerb/heredoc_with_only_carriage_returns_windows.txt", + "seattlerb/heredoc_with_only_carriage_returns.txt", + "spanning_heredoc_newlines.txt", + "spanning_heredoc.txt", + "tilde_heredocs.txt", + "unparser/corpus/literal/literal.txt", + "while.txt", + "whitequark/cond_eflipflop.txt", + "whitequark/cond_iflipflop.txt", + "whitequark/cond_match_current_line.txt", + "whitequark/dedenting_heredoc.txt", + "whitequark/lvar_injecting_match.txt", + "whitequark/not.txt", + "whitequark/numparam_ruby_bug_19025.txt", + "whitequark/op_asgn_cmd.txt", + "whitequark/parser_bug_640.txt", + "whitequark/parser_slash_slash_n_escaping_in_literals.txt", + "whitequark/pattern_matching_single_line_allowed_omission_of_parentheses.txt", + "whitequark/pattern_matching_single_line.txt", + "whitequark/ruby_bug_11989.txt", + "whitequark/slash_newline_in_heredocs.txt" + ] + + Fixture.each(except: failures) do |fixture| + define_method(fixture.test_name) do + assert_ruby_parser(fixture, todos.include?(fixture.path)) + end + end + + private + + def assert_ruby_parser(fixture, allowed_failure) + source = fixture.read + expected = ignore_warnings { ::RubyParser.new.parse(source, fixture.path) } + actual = Prism::Translation::RubyParser.new.parse(source, fixture.path) + + if !allowed_failure + assert_equal(expected, actual, -> { message(expected, actual) }) + elsif expected == actual + puts "#{name} now passes" + end + end + + def message(expected, actual) + if expected == actual + nil + elsif expected.is_a?(Sexp) && actual.is_a?(Sexp) + if expected.line != actual.line + "expected: (#{expected.inspect} line=#{expected.line}), actual: (#{actual.inspect} line=#{actual.line})" + elsif expected.file != actual.file + "expected: (#{expected.inspect} file=#{expected.file}), actual: (#{actual.inspect} file=#{actual.file})" + elsif expected.length != actual.length + "expected: (#{expected.inspect} length=#{expected.length}), actual: (#{actual.inspect} length=#{actual.length})" + else + expected.zip(actual).find do |expected_field, actual_field| + result = message(expected_field, actual_field) + break result if result + end + end + else + "expected: #{expected.inspect}, actual: #{actual.inspect}" + end + end + end +end diff --git a/test/prism/ruby/tunnel_test.rb b/test/prism/ruby/tunnel_test.rb new file mode 100644 index 0000000000..0214681604 --- /dev/null +++ b/test/prism/ruby/tunnel_test.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require_relative "../test_helper" + +module Prism + class TunnelTest < TestCase + def test_tunnel + program = Prism.parse("foo(1) +\n bar(2, 3) +\n baz(3, 4, 5)").value + + tunnel = program.tunnel(1, 4).last + assert_kind_of IntegerNode, tunnel + assert_equal 1, tunnel.value + + tunnel = program.tunnel(2, 6).last + assert_kind_of IntegerNode, tunnel + assert_equal 2, tunnel.value + + tunnel = program.tunnel(3, 9).last + assert_kind_of IntegerNode, tunnel + assert_equal 4, tunnel.value + + tunnel = program.tunnel(3, 8) + assert_equal [ProgramNode, StatementsNode, CallNode, ArgumentsNode, CallNode, ArgumentsNode], tunnel.map(&:class) + end + end +end |