From 56a9eb68b95a55960a7346817ee17bda4b882506 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Dec 2021 17:30:52 +0000 Subject: [PATCH 01/10] Bump minitest from 5.14.4 to 5.15.0 Bumps [minitest](https://github.com/seattlerb/minitest) from 5.14.4 to 5.15.0. - [Release notes](https://github.com/seattlerb/minitest/releases) - [Changelog](https://github.com/seattlerb/minitest/blob/master/History.rdoc) - [Commits](https://github.com/seattlerb/minitest/compare/v5.14.4...v5.15.0) --- updated-dependencies: - dependency-name: minitest dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 74c7611b..d342541b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -9,7 +9,7 @@ GEM ast (2.4.2) benchmark-ips (2.9.2) docile (1.4.0) - minitest (5.14.4) + minitest (5.15.0) parser (3.0.3.2) ast (~> 2.4.1) rake (13.0.6) From 68f2d37bc64d585348d14aa62172b2cf50ff4715 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Wed, 22 Dec 2021 20:41:08 -0500 Subject: [PATCH 02/10] Ensure you can use child_nodes on all nodes --- lib/syntax_tree.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/syntax_tree.rb b/lib/syntax_tree.rb index 858b70a3..c337c1e7 100644 --- a/lib/syntax_tree.rb +++ b/lib/syntax_tree.rb @@ -3968,6 +3968,10 @@ def comments [] end + def child_nodes + [] + end + def format(q) q.text(value) end @@ -12976,6 +12980,10 @@ def initialize(location:, comments: []) @comments = comments end + def child_nodes + [] + end + def format(q) end From aca89c37fbc120af3f077c6a0c320c1f9c444c64 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Sat, 8 Jan 2022 14:05:00 -0500 Subject: [PATCH 03/10] Bump deps --- Gemfile.lock | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index d342541b..40a8c2d7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -10,7 +10,7 @@ GEM benchmark-ips (2.9.2) docile (1.4.0) minitest (5.15.0) - parser (3.0.3.2) + parser (3.1.0.0) ast (~> 2.4.1) rake (13.0.6) ruby_parser (3.18.1) @@ -26,6 +26,7 @@ GEM PLATFORMS x86_64-darwin-19 + x86_64-darwin-21 x86_64-linux DEPENDENCIES From 4cec867d8525b3c5d5b63e857a9e68eeaf5a670c Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Sat, 8 Jan 2022 14:05:59 -0500 Subject: [PATCH 04/10] Ruby 3.1 syntax --- Gemfile.lock | 2 +- lib/syntax_tree.rb | 223 ++++++++++++++++++++-------------- test/fixtures/arg_block.rb | 4 + test/fixtures/args_forward.rb | 8 +- test/fixtures/assoc.rb | 2 + test/fixtures/def_endless.rb | 2 + test/formatting_test.rb | 31 +++++ test/node_test.rb | 62 +++++++--- 8 files changed, 226 insertions(+), 108 deletions(-) create mode 100644 test/formatting_test.rb diff --git a/Gemfile.lock b/Gemfile.lock index 40a8c2d7..be7967ac 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -41,4 +41,4 @@ DEPENDENCIES syntax_tree! BUNDLED WITH - 2.2.15 + 2.2.31 diff --git a/lib/syntax_tree.rb b/lib/syntax_tree.rb index c337c1e7..b487de9b 100644 --- a/lib/syntax_tree.rb +++ b/lib/syntax_tree.rb @@ -1173,7 +1173,7 @@ def on_args_add(arguments, argument) # method(&expression) # class ArgBlock - # [untyped] the expression being turned into a block + # [nil | untyped] the expression being turned into a block attr_reader :value # [Location] the location of this node @@ -1194,15 +1194,17 @@ def child_nodes def format(q) q.text("&") - q.format(value) + q.format(value) if value end def pretty_print(q) q.group(2, "(", ")") do q.text("arg_block") - q.breakable - q.pp(value) + if value + q.breakable + q.pp(value) + end q.pp(Comment::List.new(comments)) end @@ -1221,17 +1223,34 @@ def to_json(*opts) # (false | untyped) block # ) -> Args def on_args_add_block(arguments, block) - return arguments unless block + operator = find_token(Op, "&", consume: false) - arg_block = - ArgBlock.new( - value: block, - location: find_token(Op, "&").location.to(block.location) - ) + # If we can't find the & operator, then there's no block to add to the list, + # so we're just going to return the arguments as-is. + return arguments unless operator + + # Now we know we have an & operator, so we're going to delete it from the + # list of tokens to make sure it doesn't get confused with anything else. + tokens.delete(operator) + + # Construct the location that represents the block argument. + location = operator.location + location = operator.location.to(block.location) if block + + # If there are any arguments and the operator we found from the list is not + # after them, then we're going to return the arguments as-is because we're + # looking at an & that occurs before the arguments are done. + if arguments.parts.any? && location.start_char < arguments.location.end_char + return arguments + end + + # Otherwise, we're looking at an actual block argument (with or without a + # block, which could be missing because it could be a bare & since 3.1.0). + arg_block = ArgBlock.new(value: block, location: location) Args.new( parts: arguments.parts << arg_block, - location: arguments.location.to(arg_block.location) + location: arguments.location.to(location) ) end @@ -1896,7 +1915,7 @@ def child_nodes end def format(q) - if value.is_a?(HashLiteral) + if value&.is_a?(HashLiteral) format_contents(q) else q.group { format_contents(q) } @@ -1910,8 +1929,10 @@ def pretty_print(q) q.breakable q.pp(key) - q.breakable - q.pp(value) + if value + q.breakable + q.pp(value) + end q.pp(Comment::List.new(comments)) end @@ -1931,6 +1952,7 @@ def to_json(*opts) def format_contents(q) q.parent.format_key(q, key) + return unless value if key.comments.empty? && AssignFormatting.skip_indent?(value) q.text(" ") @@ -1947,7 +1969,10 @@ def format_contents(q) # :call-seq: # on_assoc_new: (untyped key, untyped value) -> Assoc def on_assoc_new(key, value) - Assoc.new(key: key, value: value, location: key.location.to(value.location)) + location = key.location + location = location.to(value.location) if value + + Assoc.new(key: key, value: value, location: location) end # AssocSplat represents double-splatting a value into a hash (either a hash @@ -2423,12 +2448,22 @@ def to_json(*opts) # :call-seq: # on_binary: (untyped left, (Op | Symbol) operator, untyped right) -> Binary def on_binary(left, operator, right) - # On most Ruby implementations, operator is a Symbol that represents that - # operation being performed. For instance in the example `1 < 2`, the - # `operator` object would be `:<`. However, on JRuby, it's an `@op` node, - # so here we're going to explicitly convert it into the same normalized - # form. - operator = tokens.delete(operator).value unless operator.is_a?(Symbol) + if operator.is_a?(Symbol) + # Here, we're going to search backward for the nearest token that matches + # the operator so we can delete it from the list. + token = find_token(Op, operator.to_s, consume: false) + + if token && token.location.start_char > left.location.end_char + tokens.delete(token) + end + else + # On most Ruby implementations, operator is a Symbol that represents that + # operation being performed. For instance in the example `1 < 2`, the + # `operator` object would be `:<`. However, on JRuby, it's an `@op` node, + # so here we're going to explicitly convert it into the same normalized + # form. + operator = tokens.delete(operator).value + end Binary.new( left: left, @@ -2578,7 +2613,7 @@ def on_block_var(params, locals) # def method(&block); end # class BlockArg - # [Ident] the name of the block argument + # [nil | Ident] the name of the block argument attr_reader :name # [Location] the location of this node @@ -2599,15 +2634,17 @@ def child_nodes def format(q) q.text("&") - q.format(name) + q.format(name) if name end def pretty_print(q) q.group(2, "(", ")") do q.text("blockarg") - q.breakable - q.pp(name) + if name + q.breakable + q.pp(name) + end q.pp(Comment::List.new(comments)) end @@ -2625,7 +2662,10 @@ def to_json(*opts) def on_blockarg(name) operator = find_token(Op, "&") - BlockArg.new(name: name, location: operator.location.to(name.location)) + location = operator.location + location = location.to(name.location) if name + + BlockArg.new(name: name, location: location) end # bodystmt can't actually determine its bounds appropriately because it @@ -4423,7 +4463,7 @@ class DefEndless # [Backtick | Const | Ident | Kw | Op] the name of the method attr_reader :name - # [nil | Paren] the parameter declaration for the method + # [nil | Params | Paren] the parameter declaration for the method attr_reader :paren # [untyped] the expression to be executed by the method @@ -4467,7 +4507,12 @@ def format(q) end q.format(name) - q.format(paren) if paren && !paren.contents.empty? + + if paren + params = paren + params = params.contents if params.is_a?(Paren) + q.format(paren) unless params.empty? + end q.text(" =") q.group do @@ -4533,21 +4578,6 @@ def on_def(name, params, bodystmt) # and normal method definitions. beginning = find_token(Kw, "def") - # If we don't have a bodystmt node, then we have a single-line method - unless bodystmt.is_a?(BodyStmt) - node = - DefEndless.new( - target: nil, - operator: nil, - name: name, - paren: params, - statement: bodystmt, - location: beginning.location.to(bodystmt.location) - ) - - return node - end - # If there aren't any params then we need to correct the params node # location information if params.is_a?(Params) && params.empty? @@ -4563,18 +4593,35 @@ def on_def(name, params, bodystmt) params = Params.new(location: location) end - ending = find_token(Kw, "end") - bodystmt.bind( - find_next_statement_start(params.location.end_char), - ending.location.start_char - ) + ending = find_token(Kw, "end", consume: false) - Def.new( - name: name, - params: params, - bodystmt: bodystmt, - location: beginning.location.to(ending.location) - ) + if ending + tokens.delete(ending) + bodystmt.bind( + find_next_statement_start(params.location.end_char), + ending.location.start_char + ) + + Def.new( + name: name, + params: params, + bodystmt: bodystmt, + location: beginning.location.to(ending.location) + ) + else + # In Ruby >= 3.1.0, this is a BodyStmt that wraps a single statement in + # the statements list. Before, it was just the individual statement. + statement = bodystmt.is_a?(BodyStmt) ? bodystmt.statements : bodystmt + + DefEndless.new( + target: nil, + operator: nil, + name: name, + paren: params, + statement: statement, + location: beginning.location.to(bodystmt.location) + ) + end end # Defined represents the use of the +defined?+ operator. It can be used with @@ -4782,37 +4829,37 @@ def on_defs(target, operator, name, params, bodystmt) end beginning = find_token(Kw, "def") + ending = find_token(Kw, "end", consume: false) - # If we don't have a bodystmt node, then we have a single-line method - unless bodystmt.is_a?(BodyStmt) - node = - DefEndless.new( - target: target, - operator: operator, - name: name, - paren: params, - statement: bodystmt, - location: beginning.location.to(bodystmt.location) - ) - - return node - end - - ending = find_token(Kw, "end") + if ending + tokens.delete(ending) + bodystmt.bind( + find_next_statement_start(params.location.end_char), + ending.location.start_char + ) - bodystmt.bind( - find_next_statement_start(params.location.end_char), - ending.location.start_char - ) + Defs.new( + target: target, + operator: operator, + name: name, + params: params, + bodystmt: bodystmt, + location: beginning.location.to(ending.location) + ) + else + # In Ruby >= 3.1.0, this is a BodyStmt that wraps a single statement in + # the statements list. Before, it was just the individual statement. + statement = bodystmt.is_a?(BodyStmt) ? bodystmt.statements : bodystmt - Defs.new( - target: target, - operator: operator, - name: name, - params: params, - bodystmt: bodystmt, - location: beginning.location.to(ending.location) - ) + DefEndless.new( + target: target, + operator: operator, + name: name, + paren: params, + statement: statement, + location: beginning.location.to(bodystmt.location) + ) + end end # DoBlock represents passing a block to a method call using the +do+ and +end+ @@ -8931,7 +8978,7 @@ def format(q) end class KeywordRestFormatter - # [:nil | KwRestParam] the value of the parameter + # [:nil | ArgsForward | KwRestParam] the value of the parameter attr_reader :value def initialize(value) @@ -9046,7 +9093,7 @@ def format(q) q.format(rest) if rest && rest.is_a?(ExcessedComma) end - if [Def, Defs].include?(q.parent.class) + if [Def, Defs, DefEndless].include?(q.parent.class) q.group(0, "(", ")") do q.indent do q.breakable("") @@ -9146,8 +9193,8 @@ def to_json(*opts) # (nil | ArgsForward | ExcessedComma | RestParam) rest, # (nil | Array[Ident]) posts, # (nil | Array[[Ident, nil | untyped]]) keywords, - # (nil | :nil | KwRestParam) keyword_rest, - # (nil | BlockArg) block + # (nil | :nil | ArgsForward | KwRestParam) keyword_rest, + # (nil | :& | BlockArg) block # ) -> Params def on_params( requireds, @@ -9165,7 +9212,7 @@ def on_params( *posts, *keywords&.flat_map { |(key, value)| [key, value || nil] }, (keyword_rest if keyword_rest != :nil), - block + (block if block != :&) ].compact location = @@ -9182,7 +9229,7 @@ def on_params( posts: posts || [], keywords: keywords || [], keyword_rest: keyword_rest, - block: block, + block: (block if block != :&), location: location ) end diff --git a/test/fixtures/arg_block.rb b/test/fixtures/arg_block.rb index 74be5b2b..d423efa8 100644 --- a/test/fixtures/arg_block.rb +++ b/test/fixtures/arg_block.rb @@ -14,3 +14,7 @@ foo( &bar.bazzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz ) +% # >= 3.1.0 +def foo(&) + bar(&) +end diff --git a/test/fixtures/args_forward.rb b/test/fixtures/args_forward.rb index e38a22cc..5ba618a8 100644 --- a/test/fixtures/args_forward.rb +++ b/test/fixtures/args_forward.rb @@ -1,4 +1,8 @@ % -def get(...) - request(:GET, ...) +def foo(...) + bar(:baz, ...) +end +% # >= 3.1.0 +def foo(foo, bar = baz, ...) + bar(:baz, ...) end diff --git a/test/fixtures/assoc.rb b/test/fixtures/assoc.rb index ceed0d0c..43bb2b08 100644 --- a/test/fixtures/assoc.rb +++ b/test/fixtures/assoc.rb @@ -38,3 +38,5 @@ foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo ] } +% # >= 3.1.0 +{ foo: } diff --git a/test/fixtures/def_endless.rb b/test/fixtures/def_endless.rb index 2f316e6c..5e14dbc7 100644 --- a/test/fixtures/def_endless.rb +++ b/test/fixtures/def_endless.rb @@ -6,3 +6,5 @@ def foo(bar) = baz def foo() = bar - def foo = bar +% # >= 3.1.0 +def foo = bar baz diff --git a/test/formatting_test.rb b/test/formatting_test.rb new file mode 100644 index 00000000..8fcb6ae2 --- /dev/null +++ b/test/formatting_test.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require_relative "test_helper" + +class SyntaxTree + class FormattingTest < Minitest::Test + delimiter = /%(?: # (.+?))?\n/ + + Dir[File.join(__dir__, "fixtures", "*.rb")].each do |filepath| + basename = File.basename(filepath, ".rb") + sources = File.readlines(filepath).slice_before(delimiter) + + sources.each_with_index do |source, index| + comment = source.shift.match(delimiter)[1] + original, expected = source.join.split("-\n") + + # If there's a comment starting with >= that starts after the % that + # delineates the test, then we're going to check if the version + # satisfies that constraint. + if comment&.start_with?(">=") + version = Gem::Version.new(comment.split[1]) + next if Gem::Version.new(RUBY_VERSION) < version + end + + define_method(:"test_formatting_#{basename}_#{index}") do + assert_equal(expected || original, SyntaxTree.format(original)) + end + end + end + end +end diff --git a/test/node_test.rb b/test/node_test.rb index 84dc4891..b5b064e6 100644 --- a/test/node_test.rb +++ b/test/node_test.rb @@ -4,6 +4,10 @@ class SyntaxTree class NodeTest < Minitest::Test + def self.guard_version(version) + yield if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new(version) + end + def test_BEGIN assert_node(BEGINBlock, "BEGIN", "BEGIN {}") end @@ -78,6 +82,21 @@ def test_arg_block end end + guard_version("3.1.0") do + def test_arg_block_anonymous + source = <<~SOURCE + def method(&) + child_method(&) + end + SOURCE + + at = location(lines: 2..2, chars: 29..30) + assert_node(ArgBlock, "arg_block", source, at: at) do |node| + node.bodystmt.statements.body.first.arguments.arguments.parts[0] + end + end + end + def test_arg_star source = "method(prefix, *arguments, suffix)" @@ -129,6 +148,15 @@ def test_assoc assert_node(Assoc, "assoc", source, at: at) { |node| node.assocs.first } end + guard_version("3.1.0") do + def test_assoc_no_value + source = "{ key1:, key2: }" + + at = location(chars: 2..7) + assert_node(Assoc, "assoc", source, at: at) { |node| node.assocs.first } + end + end + def test_assoc_splat source = "{ **pairs }" @@ -207,6 +235,17 @@ def test_blockarg end end + guard_version("3.1.0") do + def test_blockarg_anonymous + source = "def method(&); end" + + at = location(chars: 11..12) + assert_node(BlockArg, "blockarg", source, at: at) do |node| + node.params.contents.block + end + end + end + def test_bodystmt source = <<~SOURCE begin @@ -317,6 +356,12 @@ def test_def_endless assert_node(DefEndless, "def_endless", "def method = result") end + guard_version("3.1.0") do + def test_def_endless_command + assert_node(DefEndless, "def_endless", "def method = result argument") + end + end + def test_defined assert_node(Defined, "defined", "defined?(variable)") end @@ -948,23 +993,6 @@ def test_zsuper assert_node(ZSuper, "zsuper", "super") end - # -------------------------------------------------------------------------- - # Tests for formatting - # -------------------------------------------------------------------------- - - Dir[File.join(__dir__, "fixtures", "*.rb")].each do |filepath| - basename = File.basename(filepath, ".rb") - - File.read(filepath).split(/%(?: #.+?)?\n/).drop( - 1 - ).each_with_index do |source, index| - define_method(:"test_formatting_#{basename}_#{index}") do - original, expected = source.split("-\n") - assert_equal(expected || original, SyntaxTree.format(original)) - end - end - end - private def location(lines: 1..1, chars: 0..0) From 878a3c646a4e0224f5316e66643e629f55854a15 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Sat, 8 Jan 2022 14:06:50 -0500 Subject: [PATCH 05/10] Explicit 3.1 Ruby in test suite --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 18e96080..5a7c30c9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -13,7 +13,7 @@ jobs: - uses: ruby/setup-ruby@v1 with: bundler-cache: true - ruby-version: 3.0 + ruby-version: '3.1' - name: Test run: bundle exec rake test automerge: From f82b9adec0596e23cf6cc75ba252575770adca2f Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Sun, 9 Jan 2022 13:31:09 -0500 Subject: [PATCH 06/10] Pinned expressions and variables in pattern matching --- CHANGELOG.md | 5 ++ lib/syntax_tree.rb | 170 ++++++++++++++++++++++++++++++++----- test/fixtures/begin.rb | 4 + test/fixtures/var_field.rb | 10 +++ 4 files changed, 169 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 973a891c..14a84e5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a ## [Unreleased] +### Added + +- Support for Ruby 3.1 syntax, including: blocks without names, hash keys without values, endless methods without parentheses, and new argument forwarding. +- Support for pinned expressions and variables within pattern matching. + ## [1.1.1] - 2021-12-09 ### Added diff --git a/lib/syntax_tree.rb b/lib/syntax_tree.rb index b487de9b..25c55004 100644 --- a/lib/syntax_tree.rb +++ b/lib/syntax_tree.rb @@ -1695,10 +1695,13 @@ def format(q) parts += posts if constant - q.format(constant) - q.text("[") - q.seplist(parts) { |part| q.format(part) } - q.text("]") + q.group do + q.format(constant) + q.text("[") + q.seplist(parts) { |part| q.format(part) } + q.text("]") + end + return end @@ -1708,7 +1711,7 @@ def format(q) q.seplist(parts) { |part| q.format(part) } q.text("]") else - q.seplist(parts) { |part| q.format(part) } + q.group { q.seplist(parts) { |part| q.format(part) } } end end @@ -2340,24 +2343,93 @@ def to_json(*opts) end end + # PinnedBegin represents a pinning a nested statement within pattern matching. + # + # case value + # in ^(statement) + # end + # + class PinnedBegin + # [untyped] the expression being pinned + attr_reader :statement + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(statement:, location:, comments: []) + @statement = statement + @location = location + @comments = comments + end + + def child_nodes + [statement] + end + + def format(q) + q.group do + q.text("^(") + q.nest(1) do + q.indent do + q.breakable("") + q.format(statement) + end + q.breakable("") + q.text(")") + end + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("pinned_begin") + + q.breakable + q.pp(statement) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :pinned_begin, + stmt: statement, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + # :call-seq: - # on_begin: (BodyStmt bodystmt) -> Begin + # on_begin: (untyped bodystmt) -> Begin | PinnedBegin def on_begin(bodystmt) - keyword = find_token(Kw, "begin") - end_char = - if bodystmt.rescue_clause || bodystmt.ensure_clause || - bodystmt.else_clause - bodystmt.location.end_char - else - find_token(Kw, "end").location.end_char - end + if beginning = find_token(Op, "^", consume: false) + tokens.delete(beginning) + find_token(LParen) - bodystmt.bind(keyword.location.end_char, end_char) + ending = find_token(RParen) + location = beginning.location.to(ending.location) - Begin.new( - bodystmt: bodystmt, - location: keyword.location.to(bodystmt.location) - ) + PinnedBegin.new(statement: bodystmt, location: location) + else + keyword = find_token(Kw, "begin") + end_char = + if bodystmt.rescue_clause || bodystmt.ensure_clause || + bodystmt.else_clause + bodystmt.location.end_char + else + find_token(Kw, "end").location.end_char + end + + bodystmt.bind(keyword.location.end_char, end_char) + location = keyword.location.to(bodystmt.location) + + Begin.new(bodystmt: bodystmt, location: location) + end end # Binary represents any expression that involves two sub-expressions with an @@ -12952,10 +13024,68 @@ def to_json(*opts) end end + # PinnedVarRef represents a pinned variable reference within a pattern + # matching pattern. + # + # case value + # in ^variable + # end + # + # This can be a plain local variable like the example above. It can also be a + # a class variable, a global variable, or an instance variable. + class PinnedVarRef + # [VarRef] the value of this node + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) + @value = value + @location = location + @comments = comments + end + + def child_nodes + [value] + end + + def format(q) + q.group do + q.text("^") + q.format(value) + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("pinned_var_ref") + + q.breakable + q.pp(value) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { type: :pinned_var_ref, value: value, loc: location, cmts: comments } + .to_json(*opts) + end + end + # :call-seq: # on_var_ref: ((Const | CVar | GVar | Ident | IVar | Kw) value) -> VarRef def on_var_ref(value) - VarRef.new(value: value, location: value.location) + if pin = find_token(Op, "^", consume: false) + tokens.delete(pin) + PinnedVarRef.new(value: value, location: pin.location.to(value.location)) + else + VarRef.new(value: value, location: value.location) + end end # VCall represent any plain named object with Ruby that could be either a diff --git a/test/fixtures/begin.rb b/test/fixtures/begin.rb index efd12dad..f9e5b775 100644 --- a/test/fixtures/begin.rb +++ b/test/fixtures/begin.rb @@ -5,3 +5,7 @@ begin expression end +% +case value +in ^(expression) +end diff --git a/test/fixtures/var_field.rb b/test/fixtures/var_field.rb index 8c1258af..2b78c098 100644 --- a/test/fixtures/var_field.rb +++ b/test/fixtures/var_field.rb @@ -8,3 +8,13 @@ foo = bar % @foo = bar +% +foo in bar +% +foo in ^bar +% +foo in ^@bar +% +foo in ^@@bar +% +foo in ^$gvar From 1cae7ec9f82b9f822588bb1b3edaacfbb45c071a Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Sun, 9 Jan 2022 13:42:18 -0500 Subject: [PATCH 07/10] Find the correct binary operator --- lib/syntax_tree.rb | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/lib/syntax_tree.rb b/lib/syntax_tree.rb index 25c55004..3cbf2d35 100644 --- a/lib/syntax_tree.rb +++ b/lib/syntax_tree.rb @@ -2521,13 +2521,20 @@ def to_json(*opts) # on_binary: (untyped left, (Op | Symbol) operator, untyped right) -> Binary def on_binary(left, operator, right) if operator.is_a?(Symbol) - # Here, we're going to search backward for the nearest token that matches - # the operator so we can delete it from the list. - token = find_token(Op, operator.to_s, consume: false) + # Here, we're going to search backward for the token that's between the + # two operands that matches the operator so we can delete it from the + # list. + index = + tokens.rindex do |token| + location = token.location - if token && token.location.start_char > left.location.end_char - tokens.delete(token) - end + token.is_a?(Op) && + token.value == operator.to_s && + location.start_char > left.location.end_char && + location.end_char < right.location.start_char + end + + tokens.delete_at(index) if index else # On most Ruby implementations, operator is a Symbol that represents that # operation being performed. For instance in the example `1 < 2`, the From bf8e3b13463682dc8b7081b09fe2a1880530c407 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Sun, 9 Jan 2022 13:52:35 -0500 Subject: [PATCH 08/10] Ensure the pin is before the expressions --- lib/syntax_tree.rb | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/syntax_tree.rb b/lib/syntax_tree.rb index 3cbf2d35..ffe874c7 100644 --- a/lib/syntax_tree.rb +++ b/lib/syntax_tree.rb @@ -2407,12 +2407,14 @@ def to_json(*opts) # :call-seq: # on_begin: (untyped bodystmt) -> Begin | PinnedBegin def on_begin(bodystmt) - if beginning = find_token(Op, "^", consume: false) - tokens.delete(beginning) + pin = find_token(Op, "^", consume: false) + + if pin && pin.location.start_char < bodystmt.location.start_char + tokens.delete(pin) find_token(LParen) - ending = find_token(RParen) - location = beginning.location.to(ending.location) + rparen = find_token(RParen) + location = pin.location.to(rparen.location) PinnedBegin.new(statement: bodystmt, location: location) else @@ -13087,7 +13089,9 @@ def to_json(*opts) # :call-seq: # on_var_ref: ((Const | CVar | GVar | Ident | IVar | Kw) value) -> VarRef def on_var_ref(value) - if pin = find_token(Op, "^", consume: false) + pin = find_token(Op, "^", consume: false) + + if pin && pin.location.start_char == value.location.start_char - 1 tokens.delete(pin) PinnedVarRef.new(value: value, location: pin.location.to(value.location)) else From 05bb5ac3c06c389111eef5096047cb3f17c3ba77 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Sun, 9 Jan 2022 13:59:41 -0500 Subject: [PATCH 09/10] Support endless ranges as the final argument to a `when` clause. --- CHANGELOG.md | 1 + lib/syntax_tree.rb | 8 ++++++++ test/fixtures/when.rb | 8 ++++++++ 3 files changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14a84e5d..972cca66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a - Support for Ruby 3.1 syntax, including: blocks without names, hash keys without values, endless methods without parentheses, and new argument forwarding. - Support for pinned expressions and variables within pattern matching. +- Support endless ranges as the final argument to a `when` clause. ## [1.1.1] - 2021-12-09 diff --git a/lib/syntax_tree.rb b/lib/syntax_tree.rb index ffe874c7..22cc283a 100644 --- a/lib/syntax_tree.rb +++ b/lib/syntax_tree.rb @@ -13246,6 +13246,14 @@ def format(q) separator = -> { q.group { q.comma_breakable } } q.seplist(arguments.parts, separator) { |part| q.format(part) } end + + # Very special case here. If you're inside of a when clause and the + # last argument to the predicate is and endless range, then you are + # forced to use the "then" keyword to make it parse properly. + last = arguments.parts.last + if (last.is_a?(Dot2) || last.is_a?(Dot3)) && !last.right + q.text(" then") + end end end diff --git a/test/fixtures/when.rb b/test/fixtures/when.rb index 22ebdd1d..1fb102da 100644 --- a/test/fixtures/when.rb +++ b/test/fixtures/when.rb @@ -46,3 +46,11 @@ when foo else end +% +case +when foo.. then +end +% +case +when foo... then +end From dfddb13f02d8c79c12c5a46c38b185d2b132adeb Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Sun, 9 Jan 2022 14:13:31 -0500 Subject: [PATCH 10/10] Bump to v1.2.0 --- CHANGELOG.md | 5 ++++- Gemfile.lock | 2 +- lib/syntax_tree/version.rb | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 972cca66..6b049bd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a ## [Unreleased] +## [1.2.0] - 2022-01-09 + ### Added - Support for Ruby 3.1 syntax, including: blocks without names, hash keys without values, endless methods without parentheses, and new argument forwarding. @@ -103,7 +105,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a - 🎉 Initial release! 🎉 -[unreleased]: https://github.com/kddnewton/syntax_tree/compare/v1.1.1...HEAD +[unreleased]: https://github.com/kddnewton/syntax_tree/compare/v1.2.0...HEAD +[1.2.0]: https://github.com/kddnewton/syntax_tree/compare/v1.1.1...v1.2.0 [1.1.1]: https://github.com/kddnewton/syntax_tree/compare/v1.1.0...v1.1.1 [1.1.0]: https://github.com/kddnewton/syntax_tree/compare/v1.0.0...v1.1.0 [1.0.0]: https://github.com/kddnewton/syntax_tree/compare/v0.1.0...v1.0.0 diff --git a/Gemfile.lock b/Gemfile.lock index be7967ac..8dcdb592 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - syntax_tree (1.1.1) + syntax_tree (1.2.0) GEM remote: https://rubygems.org/ diff --git a/lib/syntax_tree/version.rb b/lib/syntax_tree/version.rb index b8a9c2fb..ed245aa7 100644 --- a/lib/syntax_tree/version.rb +++ b/lib/syntax_tree/version.rb @@ -3,5 +3,5 @@ require "ripper" class SyntaxTree < Ripper - VERSION = "1.1.1" + VERSION = "1.2.0" end