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: diff --git a/CHANGELOG.md b/CHANGELOG.md index 973a891c..6b049bd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ 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. +- 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 ### Added @@ -97,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 74c7611b..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/ @@ -9,8 +9,8 @@ GEM ast (2.4.2) benchmark-ips (2.9.2) docile (1.4.0) - minitest (5.14.4) - parser (3.0.3.2) + minitest (5.15.0) + 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 @@ -40,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 858b70a3..22cc283a 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 @@ -1676,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 @@ -1689,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 @@ -1896,7 +1918,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 +1932,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 +1955,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 +1972,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 @@ -2315,24 +2343,95 @@ 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 + pin = find_token(Op, "^", consume: false) - bodystmt.bind(keyword.location.end_char, end_char) + if pin && pin.location.start_char < bodystmt.location.start_char + tokens.delete(pin) + find_token(LParen) - Begin.new( - bodystmt: bodystmt, - location: keyword.location.to(bodystmt.location) - ) + rparen = find_token(RParen) + location = pin.location.to(rparen.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 @@ -2423,12 +2522,29 @@ 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 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 + + 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 + # `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 +2694,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 +2715,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 +2743,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 @@ -3968,6 +4089,10 @@ def comments [] end + def child_nodes + [] + end + def format(q) q.text(value) end @@ -4419,7 +4544,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 @@ -4463,7 +4588,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 @@ -4529,21 +4659,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? @@ -4559,18 +4674,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 @@ -4778,37 +4910,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+ @@ -8927,7 +9059,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) @@ -9042,7 +9174,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("") @@ -9142,8 +9274,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, @@ -9161,7 +9293,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 = @@ -9178,7 +9310,7 @@ def on_params( posts: posts || [], keywords: keywords || [], keyword_rest: keyword_rest, - block: block, + block: (block if block != :&), location: location ) end @@ -12901,10 +13033,70 @@ 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) + 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 + VarRef.new(value: value, location: value.location) + end end # VCall represent any plain named object with Ruby that could be either a @@ -12976,6 +13168,10 @@ def initialize(location:, comments: []) @comments = comments end + def child_nodes + [] + end + def format(q) end @@ -13050,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/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 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/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/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/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 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 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)