From 933f685c8751db8eff83e786b211a447663a5536 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Tue, 17 May 2022 15:45:48 -0400 Subject: [PATCH] Provide a BasicVisitor --- README.md | 15 +++++ lib/syntax_tree.rb | 2 + lib/syntax_tree/basic_visitor.rb | 74 ++++++++++++++++++++++++ lib/syntax_tree/visitor.rb | 67 +-------------------- lib/syntax_tree/visitor/field_visitor.rb | 4 +- 5 files changed, 93 insertions(+), 69 deletions(-) create mode 100644 lib/syntax_tree/basic_visitor.rb 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/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)