diff --git a/CHANGELOG.md b/CHANGELOG.md index afeb8899..b99b3bfb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,31 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a ## [Unreleased] +## [2.1.1] - 2022-04-16 + +### Changed + +- [#45](https://github.com/ruby-syntax-tree/syntax_tree/issues/45) - Fix parsing expressions like `foo.instance_exec(&T.must(block))`, where there are two `args_add_block` calls with a single `&`. Previously it was associating the `&` with the wrong block. +- [#47](https://github.com/ruby-syntax-tree/syntax_tree/pull/47) - Handle expressions like `not()`. +- [#48](https://github.com/ruby-syntax-tree/syntax_tree/pull/48) - Handle special call syntax with `::` operator. +- [#49](https://github.com/ruby-syntax-tree/syntax_tree/pull/49) - Handle expressions like `case foo; in {}; end`. +- [#50](https://github.com/ruby-syntax-tree/syntax_tree/pull/50) - Parsing expressions like `case foo; in **nil; end`. + +## [2.1.0] - 2022-04-12 + +### Added + +- The `SyntaxTree::Visitor` class now implements the visitor pattern for Ruby nodes. +- The `SyntaxTree::Visitor.visit_method(name)` method. +- Support for Ruby 2.7. +- Support for comments on `rescue` and `else` keywords. +- `SyntaxTree::Location` now additionally has `start_column` and `end_column`. +- The CLI now accepts content over STDIN for the `ast`, `check`, `debug`, `doc`, `format`, and `write` commands. + +### Removed + +- The missing hash value inlay hints have been removed. + ## [2.0.1] - 2022-03-31 ### Changed @@ -128,7 +153,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a - 🎉 Initial release! 🎉 -[unreleased]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v2.0.1...HEAD +[unreleased]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v2.1.1...HEAD +[2.1.1]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v2.1.0...v2.1.1 +[2.1.0]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v2.0.1...v2.1.0 [2.0.1]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v2.0.0...v2.0.1 [2.0.0]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v1.2.0...v2.0.0 [1.2.0]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v1.1.1...v1.2.0 diff --git a/Gemfile.lock b/Gemfile.lock index 989aeb21..0fd9de74 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - syntax_tree (2.0.1) + syntax_tree (2.1.1) GEM remote: https://rubygems.org/ diff --git a/README.md b/README.md index 32c13b77..384cc605 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ It is built with only standard library dependencies. It additionally ships with - [textDocument/formatting](#textdocumentformatting) - [textDocument/inlayHints](#textdocumentinlayhints) - [syntaxTree/visualizing](#syntaxtreevisualizing) +- [Plugins](#plugins) - [Contributing](#contributing) - [License](#license) @@ -307,6 +308,26 @@ Implicity, the `2 * 3` is going to be executed first because the `*` operator ha The language server additionally includes this custom request to return a textual representation of the syntax tree underlying the source code of a file. Language server clients can use this to (for example) open an additional tab with this information displayed. +## Plugins + +You can register additional languages that can flow through the same CLI with Syntax Tree's plugin system. To register a new language, call: + +```ruby +SyntaxTree.register_handler(".mylang", MyLanguage) +``` + +In this case, whenever the CLI encounters a filepath that ends with the given extension, it will invoke methods on `MyLanguage` instead of `SyntaxTree` itself. To make sure your object conforms to each of the necessary APIs, it should implement: + +* `MyLanguage.read(filepath)` - usually this is just an alias to `File.read(filepath)`, but if you need anything else that hook is here. +* `MyLanguage.parse(source)` - this should return the syntax tree corresponding to the given source. Those objects should implement the `pretty_print` interface. +* `MyLanguage.format(source)` - this should return the formatted version of the given source. + +Below are listed all of the "official" plugins hosted under the same GitHub organization, which can be used as references for how to implement other plugins. + +* [SyntaxTree::Haml](https://github.com/ruby-syntax-tree/syntax_tree-haml) for the [Haml template language](https://haml.info/). +* [SyntaxTree::JSON](https://github.com/ruby-syntax-tree/syntax_tree-json) for JSON. +* [SyntaxTree::RBS](https://github.com/ruby-syntax-tree/syntax_tree-rbs) for the [RBS type language](https://github.com/ruby/rbs). + ## Contributing Bug reports and pull requests are welcome on GitHub at https://github.com/ruby-syntax-tree/syntax_tree. diff --git a/lib/syntax_tree/node.rb b/lib/syntax_tree/node.rb index fa4e0829..e43eeae1 100644 --- a/lib/syntax_tree/node.rb +++ b/lib/syntax_tree/node.rb @@ -4536,6 +4536,7 @@ def deconstruct_keys(keys) def format(q) parts = keywords.map { |(key, value)| KeywordFormatter.new(key, value) } parts << KeywordRestFormatter.new(keyword_rest) if keyword_rest + contents = -> do q.seplist(parts) { |part| q.format(part, stackable: false) } end @@ -4546,8 +4547,9 @@ def format(q) return end - parent = q.parent - if PATTERNS.include?(parent.class) + if parts.empty? + q.text("{}") + elsif PATTERNS.include?(q.parent.class) q.text("{ ") contents.call q.text(" }") @@ -8168,7 +8170,7 @@ def deconstruct_keys(keys) # not value # class Not < Node - # [untyped] the statement on which to operate + # [nil | untyped] the statement on which to operate attr_reader :statement # [boolean] whether or not parentheses were used @@ -8205,7 +8207,7 @@ def deconstruct_keys(keys) def format(q) q.text(parentheses ? "not(" : "not ") - q.format(statement) + q.format(statement) if statement q.text(")") if parentheses end end @@ -8675,7 +8677,11 @@ def deconstruct_keys(keys) end def format(q) - q.format(value) if value + if value == :nil + q.text("nil") + elsif value + q.format(value) + end end end diff --git a/lib/syntax_tree/parser.rb b/lib/syntax_tree/parser.rb index 60923b57..38ea4eef 100644 --- a/lib/syntax_tree/parser.rb +++ b/lib/syntax_tree/parser.rb @@ -414,12 +414,19 @@ def on_args_add(arguments, argument) # (false | untyped) block # ) -> Args def on_args_add_block(arguments, block) + # First, see if there is an & operator that could potentially be + # associated with the block part of this args_add_block. If there is not, + # then just return the arguments. operator = find_token(Op, "&", consume: false) - - # 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 + # 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? && operator.location.start_char < arguments.location.end_char + return arguments + end + # 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) @@ -428,13 +435,6 @@ def on_args_add_block(arguments, block) 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) @@ -761,8 +761,14 @@ def on_break(arguments) # (:call | Backtick | Const | Ident | Op) message # ) -> Call def on_call(receiver, operator, message) - ending = message - ending = operator if message == :call + ending = + if message != :call + message + elsif operator != :"::" + operator + else + receiver + end Call.new( receiver: receiver, @@ -1495,13 +1501,19 @@ def on_heredoc_end(value) # (nil | VarField) keyword_rest # ) -> HshPtn def on_hshptn(constant, keywords, keyword_rest) - parts = [constant, keywords, keyword_rest].flatten(2).compact + parts = [constant, *keywords&.flatten(1), keyword_rest].compact + location = + if parts.empty? + find_token(LBrace).location.to(find_token(RBrace).location) + else + parts[0].location.to(parts[-1].location) + end HshPtn.new( constant: constant, - keywords: keywords, + keywords: keywords || [], keyword_rest: keyword_rest, - location: parts[0].location.to(parts[-1].location) + location: location ) end @@ -2837,19 +2849,17 @@ def on_unary(operator, statement) # parentheses they don't get reported as a paren node for some reason. beginning = find_token(Kw, "not") - ending = statement - - range = beginning.location.end_char...statement.location.start_char - paren = source[range].include?("(") + ending = statement || beginning + parentheses = source[beginning.location.end_char] == "(" - if paren + if parentheses find_token(LParen) ending = find_token(RParen) end Not.new( statement: statement, - parentheses: paren, + parentheses: parentheses, location: beginning.location.to(ending.location) ) else @@ -2981,7 +2991,7 @@ def on_var_alias(left, right) # ) -> VarField def on_var_field(value) location = - if value + if value && value != :nil value.location else # You can hit this pattern if you're assigning to a splat using diff --git a/lib/syntax_tree/version.rb b/lib/syntax_tree/version.rb index fde7d633..aed99b33 100644 --- a/lib/syntax_tree/version.rb +++ b/lib/syntax_tree/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module SyntaxTree - VERSION = "2.0.1" + VERSION = "2.1.1" end diff --git a/test/fixtures/arg_block.rb b/test/fixtures/arg_block.rb index d423efa8..3e8524ce 100644 --- a/test/fixtures/arg_block.rb +++ b/test/fixtures/arg_block.rb @@ -18,3 +18,5 @@ def foo(&) bar(&) end +% # https://github.com/ruby-syntax-tree/syntax_tree/issues/45 +foo.instance_exec(&T.must(block)) diff --git a/test/fixtures/call.rb b/test/fixtures/call.rb new file mode 100644 index 00000000..874d290c --- /dev/null +++ b/test/fixtures/call.rb @@ -0,0 +1,14 @@ +% +foo.bar +% +foo.() +% +foo::() +- +foo.() +% +foo.(1) +% +foo::(1) +- +foo.(1) diff --git a/test/fixtures/hshptn.rb b/test/fixtures/hshptn.rb index 2efe2fd3..8220e72f 100644 --- a/test/fixtures/hshptn.rb +++ b/test/fixtures/hshptn.rb @@ -44,3 +44,11 @@ case foo in Foo[**bar] end +% +case foo +in {} +end +% +case foo +in **nil +end diff --git a/test/fixtures/not.rb b/test/fixtures/not.rb index eaa456f1..0204e345 100644 --- a/test/fixtures/not.rb +++ b/test/fixtures/not.rb @@ -1,4 +1,10 @@ % +not() +% +not () +% not foo % not(foo) +% +not (foo)