diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c57136c..1d80cfc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,16 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a ## [Unreleased] +## [2.7.0] - 2022-05-19 + +### Added + +- [#88](https://github.com/ruby-syntax-tree/syntax_tree/pull/88) - Provide a `SyntaxTree::BasicVisitor` that has no visit methods implemented. + +### Changed + +- [#90](https://github.com/ruby-syntax-tree/syntax_tree/pull/90) - Provide better formatting for `SyntaxTree::AryPtn` when its nested inside a `SyntaxTree::RAssign`. + ## [2.6.0] - 2022-05-16 ### Added @@ -236,7 +246,8 @@ 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.6.0...HEAD +[unreleased]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v2.7.0...HEAD +[2.7.0]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v2.6.0...v2.7.0 [2.6.0]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v2.5.0...v2.6.0 [2.5.0]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v2.4.1...v2.5.0 [2.4.1]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v2.4.0...v2.4.1 diff --git a/Gemfile.lock b/Gemfile.lock index b642d5dc..3f892652 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - syntax_tree (2.6.0) + syntax_tree (2.7.0) prettier_print GEM diff --git a/README.md b/README.md index 81dfdd71..e3e995cf 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ It is built with only standard library dependencies. It additionally ships with - [construct_keys](#construct_keys) - [Visitor](#visitor) - [visit_method](#visit_method) + - [BasicVisitor](#basicvisitor) - [Language server](#language-server) - [textDocument/formatting](#textdocumentformatting) - [textDocument/inlayHints](#textdocumentinlayhints) @@ -373,6 +374,20 @@ Did you mean? visit_binary from bin/console:8:in `
' ``` +### BasicVisitor + +When you're defining your own visitor, by default it will walk down the tree even if you don't define `visit_*` methods. This is to ensure you can define a subset of the necessary methods in order to only interact with the nodes you're interested in. If you'd like to change this default to instead raise an error if you visit a node you haven't explicitly handled, you can instead inherit from `BasicVisitor`. + +```ruby +class MyVisitor < SyntaxTree::BasicVisitor + def visit_int(node) + # ... + end +end +``` + +The visitor defined above will error out unless it's only visiting a `SyntaxTree::Int` node. This is useful in a couple of ways, e.g., if you're trying to define a visitor to handle the whole tree but it's currently a work-in-progress. + ## Language server Syntax Tree additionally ships with a language server conforming to the [language server protocol](https://microsoft.github.io/language-server-protocol/). It can be invoked through the CLI by running: diff --git a/lib/syntax_tree.rb b/lib/syntax_tree.rb index faefd4df..60979d04 100644 --- a/lib/syntax_tree.rb +++ b/lib/syntax_tree.rb @@ -10,6 +10,8 @@ require_relative "syntax_tree/node" require_relative "syntax_tree/parser" require_relative "syntax_tree/version" + +require_relative "syntax_tree/basic_visitor" require_relative "syntax_tree/visitor" require_relative "syntax_tree/visitor/field_visitor" require_relative "syntax_tree/visitor/json_visitor" diff --git a/lib/syntax_tree/basic_visitor.rb b/lib/syntax_tree/basic_visitor.rb new file mode 100644 index 00000000..1ad6a80f --- /dev/null +++ b/lib/syntax_tree/basic_visitor.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +module SyntaxTree + # BasicVisitor is the parent class of the Visitor class that provides the + # ability to walk down the tree. It does not define any handlers, so you + # should extend this class if you want your visitor to raise an error if you + # attempt to visit a node that you don't handle. + class BasicVisitor + # This is raised when you use the Visitor.visit_method method and it fails. + # It is correctable to through DidYouMean. + class VisitMethodError < StandardError + attr_reader :visit_method + + def initialize(visit_method) + @visit_method = visit_method + super("Invalid visit method: #{visit_method}") + end + end + + # This class is used by DidYouMean to offer corrections to invalid visit + # method names. + class VisitMethodChecker + attr_reader :visit_method + + def initialize(error) + @visit_method = error.visit_method + end + + def corrections + @corrections ||= + DidYouMean::SpellChecker.new( + dictionary: Visitor.visit_methods + ).correct(visit_method) + end + + DidYouMean.correct_error(VisitMethodError, self) + end + + class << self + # This method is here to help folks write visitors. + # + # It's not always easy to ensure you're writing the correct method name in + # the visitor since it's perfectly valid to define methods that don't + # override these parent methods. + # + # If you use this method, you can ensure you're writing the correct method + # name. It will raise an error if the visit method you're defining isn't + # actually a method on the parent visitor. + def visit_method(method_name) + return if visit_methods.include?(method_name) + + raise VisitMethodError, method_name + end + + # This is the list of all of the valid visit methods. + def visit_methods + @visit_methods ||= + Visitor.instance_methods.grep(/^visit_(?!child_nodes)/) + end + end + + def visit(node) + node&.accept(self) + end + + def visit_all(nodes) + nodes.map { |node| visit(node) } + end + + def visit_child_nodes(node) + visit_all(node.child_nodes) + end + end +end diff --git a/lib/syntax_tree/node.rb b/lib/syntax_tree/node.rb index 0a1fc394..6c2617cc 100644 --- a/lib/syntax_tree/node.rb +++ b/lib/syntax_tree/node.rb @@ -1131,7 +1131,11 @@ def format(q) q.group do q.format(constant) q.text("[") - q.seplist(parts) { |part| q.format(part) } + q.indent do + q.breakable("") + q.seplist(parts) { |part| q.format(part) } + end + q.breakable("") q.text("]") end @@ -1141,7 +1145,11 @@ def format(q) parent = q.parent if parts.length == 1 || PATTERNS.include?(parent.class) q.text("[") - q.seplist(parts) { |part| q.format(part) } + q.indent do + q.breakable("") + q.seplist(parts) { |part| q.format(part) } + end + q.breakable("") q.text("]") elsif parts.empty? q.text("[]") @@ -2777,10 +2785,17 @@ def format(q) q.format(value) q.text(" ") q.format(operator) - q.group do - q.indent do - q.breakable - q.format(pattern) + + case pattern + in AryPtn | FndPtn | HshPtn + q.text(" ") + q.format(pattern) + else + q.group do + q.indent do + q.breakable + q.format(pattern) + end end end end @@ -4573,16 +4588,26 @@ def deconstruct_keys(_keys) def format(q) q.format(constant) if constant - q.group(0, "[", "]") do - q.text("*") - q.format(left) - q.comma_breakable - q.seplist(values) { |value| q.format(value) } - q.comma_breakable + q.group do + q.text("[") - q.text("*") - q.format(right) + q.indent do + q.breakable("") + + q.text("*") + q.format(left) + q.comma_breakable + + q.seplist(values) { |value| q.format(value) } + q.comma_breakable + + q.text("*") + q.format(right) + end + + q.breakable("") + q.text("]") end end end diff --git a/lib/syntax_tree/version.rb b/lib/syntax_tree/version.rb index afa6cc12..851d9565 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.6.0" + VERSION = "2.7.0" end diff --git a/lib/syntax_tree/visitor.rb b/lib/syntax_tree/visitor.rb index fa1173eb..348a05a2 100644 --- a/lib/syntax_tree/visitor.rb +++ b/lib/syntax_tree/visitor.rb @@ -4,72 +4,7 @@ module SyntaxTree # Visitor is a parent class that provides the ability to walk down the tree # and handle a subset of nodes. By defining your own subclass, you can # explicitly handle a node type by defining a visit_* method. - class Visitor - # This is raised when you use the Visitor.visit_method method and it fails. - # It is correctable to through DidYouMean. - class VisitMethodError < StandardError - attr_reader :visit_method - - def initialize(visit_method) - @visit_method = visit_method - super("Invalid visit method: #{visit_method}") - end - end - - # This class is used by DidYouMean to offer corrections to invalid visit - # method names. - class VisitMethodChecker - attr_reader :visit_method - - def initialize(error) - @visit_method = error.visit_method - end - - def corrections - @corrections ||= - DidYouMean::SpellChecker.new( - dictionary: Visitor.visit_methods - ).correct(visit_method) - end - - DidYouMean.correct_error(VisitMethodError, self) - end - - class << self - # This method is here to help folks write visitors. - # - # It's not always easy to ensure you're writing the correct method name in - # the visitor since it's perfectly valid to define methods that don't - # override these parent methods. - # - # If you use this method, you can ensure you're writing the correct method - # name. It will raise an error if the visit method you're defining isn't - # actually a method on the parent visitor. - def visit_method(method_name) - return if visit_methods.include?(method_name) - - raise VisitMethodError, method_name - end - - # This is the list of all of the valid visit methods. - def visit_methods - @visit_methods ||= - Visitor.instance_methods.grep(/^visit_(?!child_nodes)/) - end - end - - def visit(node) - node&.accept(self) - end - - def visit_all(nodes) - nodes.map { |node| visit(node) } - end - - def visit_child_nodes(node) - visit_all(node.child_nodes) - end - + class Visitor < BasicVisitor # Visit an ARef node. alias visit_aref visit_child_nodes diff --git a/lib/syntax_tree/visitor/field_visitor.rb b/lib/syntax_tree/visitor/field_visitor.rb index 4527e0d3..1cc74f3d 100644 --- a/lib/syntax_tree/visitor/field_visitor.rb +++ b/lib/syntax_tree/visitor/field_visitor.rb @@ -49,9 +49,7 @@ class Visitor # of circumstances, like when visiting the list of optional parameters # defined on a method. # - class FieldVisitor < Visitor - attr_reader :q - + class FieldVisitor < BasicVisitor def visit_aref(node) node(node, "aref") do field("collection", node.collection) diff --git a/test/fixtures/rassign.rb b/test/fixtures/rassign.rb index 882ce890..ce749550 100644 --- a/test/fixtures/rassign.rb +++ b/test/fixtures/rassign.rb @@ -12,3 +12,11 @@ - foooooooooooooooooooooooooooooooooooooo => barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr +% +foo => [ + ConstantConstantConstant, + ConstantConstantConstant, + ConstantConstantConstant, + ConstantConstantConstant, + ConstantConstantConstant +]