diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5a7c30c9..63d51a3c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -4,6 +4,14 @@ on: - pull_request_target jobs: ci: + strategy: + fail-fast: false + matrix: + ruby: + - '2.7' + - '3.0' + - '3.1' + - head name: CI runs-on: ubuntu-latest env: @@ -13,7 +21,7 @@ jobs: - uses: ruby/setup-ruby@v1 with: bundler-cache: true - ruby-version: '3.1' + ruby-version: ${{ matrix.ruby }} - name: Test run: bundle exec rake test automerge: diff --git a/Gemfile.lock b/Gemfile.lock index 8c8e93bd..b489b9ec 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -26,6 +26,7 @@ GEM PLATFORMS arm64-darwin-21 + ruby x86_64-darwin-19 x86_64-darwin-21 x86_64-linux diff --git a/lib/syntax_tree/node.rb b/lib/syntax_tree/node.rb index a10cca60..24cb49ea 100644 --- a/lib/syntax_tree/node.rb +++ b/lib/syntax_tree/node.rb @@ -2233,6 +2233,9 @@ class BodyStmt < Node # [nil | Rescue] the optional rescue chain attached to the begin clause attr_reader :rescue_clause + # [nil | Kw] the optional else keyword + attr_reader :else_keyword + # [nil | Statements] the optional set of statements inside the else clause attr_reader :else_clause @@ -2245,6 +2248,7 @@ class BodyStmt < Node def initialize( statements:, rescue_clause:, + else_keyword:, else_clause:, ensure_clause:, location:, @@ -2252,6 +2256,7 @@ def initialize( ) @statements = statements @rescue_clause = rescue_clause + @else_keyword = else_keyword @else_clause = else_clause @ensure_clause = ensure_clause @location = location @@ -2294,7 +2299,7 @@ def accept(visitor) end def child_nodes - [statements, rescue_clause, else_clause, ensure_clause] + [statements, rescue_clause, else_keyword, else_clause, ensure_clause] end alias deconstruct child_nodes @@ -2324,10 +2329,13 @@ def format(q) if else_clause q.nest(-2) do q.breakable(force: true) - q.text("else") + q.format(else_keyword) + end + + unless else_clause.empty? + q.breakable(force: true) + q.format(else_clause) end - q.breakable(force: true) - q.format(else_clause) end if ensure_clause @@ -3295,9 +3303,10 @@ def to_json(*opts) private def align?(node) - if node.arguments in Args[parts: [Def | Defs | DefEndless]] + case node.arguments + in Args[parts: [Def | Defs | DefEndless]] false - elsif node.arguments in Args[parts: [Command => command]] + in Args[parts: [Command => command]] align?(command) else true @@ -4710,13 +4719,17 @@ def quotes(q) # end # class Else < Node + # [Kw] the else keyword + attr_reader :keyword + # [Statements] the expressions to be executed attr_reader :statements # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(statements:, location:, comments: []) + def initialize(keyword:, statements:, location:, comments: []) + @keyword = keyword @statements = statements @location = location @comments = comments @@ -4727,18 +4740,23 @@ def accept(visitor) end def child_nodes - [statements] + [keyword, statements] end alias deconstruct child_nodes def deconstruct_keys(keys) - { statements: statements, location: location, comments: comments } + { + keyword: keyword, + statements: statements, + location: location, + comments: comments + } end def format(q) q.group do - q.text("else") + q.format(keyword) unless statements.empty? q.indent do @@ -8992,6 +9010,9 @@ def to_json(*opts) # end # class Rescue < Node + # [Kw] the rescue keyword + attr_reader :keyword + # [RescueEx] the exceptions being rescued attr_reader :exception @@ -9005,12 +9026,14 @@ class Rescue < Node attr_reader :comments def initialize( + keyword:, exception:, statements:, consequent:, location:, comments: [] ) + @keyword = keyword @exception = exception @statements = statements @consequent = consequent @@ -9040,13 +9063,14 @@ def accept(visitor) end def child_nodes - [exception, statements, consequent] + [keyword, exception, statements, consequent] end alias deconstruct child_nodes def deconstruct_keys(keys) { + keyword: keyword, exception: exception, statements: statements, consequent: consequent, @@ -9057,10 +9081,10 @@ def deconstruct_keys(keys) def format(q) q.group do - q.text("rescue") + q.format(keyword) if exception - q.nest("rescue ".length) { q.format(exception) } + q.nest(keyword.value.length + 1) { q.format(exception) } else q.text(" StandardError") end diff --git a/lib/syntax_tree/parser.rb b/lib/syntax_tree/parser.rb index 33eeef8b..5bd89dc2 100644 --- a/lib/syntax_tree/parser.rb +++ b/lib/syntax_tree/parser.rb @@ -682,6 +682,7 @@ def on_bodystmt(statements, rescue_clause, else_clause, ensure_clause) BodyStmt.new( statements: statements, rescue_clause: rescue_clause, + else_keyword: else_clause && find_token(Kw, "else"), else_clause: else_clause, ensure_clause: ensure_clause, location: Location.fixed(line: lineno, char: char_pos) @@ -1115,7 +1116,7 @@ def on_dyna_symbol(string_content) # :call-seq: # on_else: (Statements statements) -> Else def on_else(statements) - beginning = find_token(Kw, "else") + keyword = find_token(Kw, "else") # else can either end with an end keyword (in which case we'll want to # consume that event) or it can end with an ensure keyword (in which case @@ -1127,13 +1128,16 @@ def on_else(statements) node = tokens[index] ending = node.value == "end" ? tokens.delete_at(index) : node - # ending = node - statements.bind(beginning.location.end_char, ending.location.start_char) + statements.bind( + find_next_statement_start(keyword.location.end_char), + ending.location.start_char + ) Else.new( + keyword: keyword, statements: statements, - location: beginning.location.to(ending.location) + location: keyword.location.to(ending.location) ) end @@ -2316,6 +2320,7 @@ def on_rescue(exceptions, variable, statements, consequent) end Rescue.new( + keyword: keyword, exception: rescue_ex, statements: statements, consequent: consequent, diff --git a/test/fixtures/begin.rb b/test/fixtures/begin.rb index f9e5b775..efd12dad 100644 --- a/test/fixtures/begin.rb +++ b/test/fixtures/begin.rb @@ -5,7 +5,3 @@ begin expression end -% -case value -in ^(expression) -end diff --git a/test/fixtures/bodystmt.rb b/test/fixtures/bodystmt.rb index 120255a8..4cbb8f5e 100644 --- a/test/fixtures/bodystmt.rb +++ b/test/fixtures/bodystmt.rb @@ -34,3 +34,23 @@ ensure foo end +% +begin +else # else +end +% +begin +ensure # ensure +end +% +begin +rescue # rescue +else # else +ensure # ensure +end +- +begin +rescue StandardError # rescue +else # else +ensure # ensure +end diff --git a/test/fixtures/command.rb b/test/fixtures/command.rb index d457aa28..7f061acd 100644 --- a/test/fixtures/command.rb +++ b/test/fixtures/command.rb @@ -23,9 +23,3 @@ % meta3 meta2 meta1 def self.foo end -% -meta1 def foo = 1 -% -meta2 meta1 def foo = 1 -% -meta3 meta2 meta1 def foo = 1 diff --git a/test/fixtures/command_def_endless.rb b/test/fixtures/command_def_endless.rb new file mode 100644 index 00000000..e6890b83 --- /dev/null +++ b/test/fixtures/command_def_endless.rb @@ -0,0 +1,6 @@ +% +meta1 def foo = 1 +% +meta2 meta1 def foo = 1 +% +meta3 meta2 meta1 def foo = 1 diff --git a/test/fixtures/else.rb b/test/fixtures/else.rb index d3675c27..e440514a 100644 --- a/test/fixtures/else.rb +++ b/test/fixtures/else.rb @@ -18,3 +18,7 @@ else bar end +% +if foo +else # bar +end diff --git a/test/fixtures/pinned_begin.rb b/test/fixtures/pinned_begin.rb new file mode 100644 index 00000000..d67ac9cf --- /dev/null +++ b/test/fixtures/pinned_begin.rb @@ -0,0 +1,4 @@ +% +case value +in ^(expression) +end diff --git a/test/fixtures/var_field.rb b/test/fixtures/var_field.rb index 2b78c098..8c1258af 100644 --- a/test/fixtures/var_field.rb +++ b/test/fixtures/var_field.rb @@ -8,13 +8,3 @@ foo = bar % @foo = bar -% -foo in bar -% -foo in ^bar -% -foo in ^@bar -% -foo in ^@@bar -% -foo in ^$gvar diff --git a/test/fixtures/var_field_rassign.rb b/test/fixtures/var_field_rassign.rb new file mode 100644 index 00000000..3e019c5c --- /dev/null +++ b/test/fixtures/var_field_rassign.rb @@ -0,0 +1,10 @@ +% +foo in bar +% +foo in ^bar +% +foo in ^@bar +% +foo in ^@@bar +% +foo in ^$gvar diff --git a/test/formatting_test.rb b/test/formatting_test.rb index f7c3b3f7..4b649052 100644 --- a/test/formatting_test.rb +++ b/test/formatting_test.rb @@ -4,13 +4,28 @@ module SyntaxTree class FormattingTest < Minitest::Test - delimiter = /%(?: # (.+?))?\n/ + FIXTURES_3_0_0 = %w[ + command_def_endless + def_endless + fndptn + rassign + rassign_rocket + ] + + FIXTURES_3_1_0 = %w[ + pinned_begin + var_field_rassign + ] - Dir[File.join(__dir__, "fixtures", "*.rb")].each do |filepath| - basename = File.basename(filepath, ".rb") - sources = File.readlines(filepath).slice_before(delimiter) + fixtures = Dir[File.join(__dir__, "fixtures", "*.rb")].map { |filepath| File.basename(filepath, ".rb") } + fixtures -= FIXTURES_3_1_0 if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.1.0") + fixtures -= FIXTURES_3_0_0 if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.0.0") + + delimiter = /%(?: # (.+?))?\n/ + fixtures.each do |fixture| + filepath = File.join(__dir__, "fixtures", "#{fixture}.rb") - sources.each_with_index do |source, index| + File.readlines(filepath).slice_before(delimiter).each_with_index do |source, index| comment = source.shift.match(delimiter)[1] original, expected = source.join.split("-\n") @@ -22,7 +37,7 @@ class FormattingTest < Minitest::Test next if Gem::Version.new(RUBY_VERSION) < version end - define_method(:"test_formatting_#{basename}_#{index}") do + define_method(:"test_formatting_#{fixture}_#{index}") do assert_equal(expected || original, SyntaxTree.format(original)) end end diff --git a/test/node_test.rb b/test/node_test.rb index 1382ed79..9c29f79d 100644 --- a/test/node_test.rb +++ b/test/node_test.rb @@ -289,12 +289,14 @@ def test_case assert_node(Case, "case", source) end - def test_rassign_in - assert_node(RAssign, "rassign", "value in pattern") - end + guard_version("3.0.0") do + def test_rassign_in + assert_node(RAssign, "rassign", "value in pattern") + end - def test_rassign_rocket - assert_node(RAssign, "rassign", "value => pattern") + def test_rassign_rocket + assert_node(RAssign, "rassign", "value => pattern") + end end def test_class @@ -352,8 +354,10 @@ def method assert_node(Def, "def", source) end - def test_def_endless - assert_node(DefEndless, "def_endless", "def method = result") + guard_version("3.0.0") do + def test_def_endless + assert_node(DefEndless, "def_endless", "def method = result") + end end guard_version("3.1.0") do @@ -478,16 +482,18 @@ def test_float_literal assert_node(FloatLiteral, "float", "1.0") end - def test_fndptn - source = <<~SOURCE - case value - in Container[*, 7, *] - end - SOURCE + guard_version("3.0.0") do + def test_fndptn + source = <<~SOURCE + case value + in Container[*, 7, *] + end + SOURCE - at = location(lines: 2..2, chars: 14..32) - assert_node(FndPtn, "fndptn", source, at: at) do |node| - node.consequent.pattern + at = location(lines: 2..2, chars: 14..32) + assert_node(FndPtn, "fndptn", source, at: at) do |node| + node.consequent.pattern + end end end diff --git a/test/parser_test.rb b/test/parser_test.rb new file mode 100644 index 00000000..e5861398 --- /dev/null +++ b/test/parser_test.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require_relative "test_helper" + +module SyntaxTree + class ParserTest < Minitest::Test + def test_parses_ripper_methods + # First, get a list of all of the dispatched events from ripper. + events = Ripper::EVENTS + + # Next, subtract all of the events that we have explicitly defined. + events -= Parser.private_instance_methods(false).grep(/^on_(\w+)/) { $1.to_sym } + + # Next, subtract the list of events that we purposefully skipped. + events -= %i[ + arg_ambiguous + assoclist_from_args + ignored_nl + ignored_sp + magic_comment + nl + nokw_param + operator_ambiguous + semicolon + sp + words_sep + ] + + # Finally, assert that we have no remaining events. + assert_empty(events) + end + end +end diff --git a/test/visitor_test.rb b/test/visitor_test.rb index 3cdf0d85..dd9f1e11 100644 --- a/test/visitor_test.rb +++ b/test/visitor_test.rb @@ -11,12 +11,27 @@ def test_visit_all_nodes program = SyntaxTree.parse(SyntaxTree.read(filepath)) program.statements.body.last.bodystmt.statements.body.each do |node| - next unless node in SyntaxTree::ClassDeclaration[superclass: { value: { value: "Node" } }] + case node + in SyntaxTree::ClassDeclaration[superclass: { value: { value: "Node" } }] + # this is a class we want to look at + else + next + end - accept = node.bodystmt.statements.body.detect { |defm| defm in SyntaxTree::Def[name: { value: "accept" }] } - accept => { bodystmt: { statements: { body: [SyntaxTree::Call[message: { value: visit_method }]] } } } + accept = + node.bodystmt.statements.body.detect do |defm| + case defm + in SyntaxTree::Def[name: { value: "accept" }] + true + else + false + end + end - assert_respond_to(visitor, visit_method) + case accept + in { bodystmt: { statements: { body: [SyntaxTree::Call[message: { value: visit_method }]] } } } + assert_respond_to(visitor, visit_method) + end end end end