Skip to content

Visitors #29

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 1, 2022
Merged

Visitors #29

merged 2 commits into from
Apr 1, 2022

Conversation

kddnewton
Copy link
Member

Piggy-backing on the great work by @wildmaples and team.

I want to keep the visitor pattern around because I think it's going to be very useful. However, I'm really not a fan of the class_eval approach. A couple of questions arise:

  • How do you discover the name of the visit method for each of the nodes?
  • Where does the documentation for the visit methods live?
  • How fast is it to load the node.rb file?
  • Is it clear what is happening within the regex?

I'd rather be very explicit about the names of the methods on the classes. Also, since their names (for the most part) come from ripper, I'd like to keep their visit methods as close to those as possible. For example, the ripper method for visiting string content is def on_tstring_content, the class name is TStringContent, but the visit method would be def visit_t_string_content. I think that would be pretty surprising/confusing for new contributors.

In this PR instead I've chosen to write out each one manually (don't worry, I generated it with syntax tree). I think this will be more maintainable and lead to better documentation. I've also provided a Visitor.visit_method method, which should hopefully make it easier to write visitors. You would use it like this:

class MyVisitor < SyntaxTree::Visitor
  visit_method def visit_comment(node)
    # do whatever here
  end
end

That visit_method call will ensure that you're naming the method correctly. If you have a typo (like visit_coment), then it will error. As a bonus, this PR uses DidYouMean to give an ever better DX.

For posterity, I generated the code below with the following script:

#!/usr/bin/env ruby
# frozen_string_literal: true

require "bundler/setup"
require "syntax_tree"

NAMES = {
  "BEGINBlock" => "BEGIN",
  "CHAR" => "CHAR",
  "ENDBlock" => "END",
  "EndContent" => "__end__",
  "ArrayLiteral" => "array",
  "ARef" => "aref",
  "ARefField" => "aref_field",
  "AryPtn" => "aryptn",
  "BodyStmt" => "bodystmt",
  "RAssign" => "rassign",
  "ClassDeclaration" => "class",
  "CVar" => "cvar",
  "EmbDoc" => "embdoc",
  "EmbExprBeg" => "embexpr_beg",
  "EmbExprEnd" => "embexpr_end",
  "EmbVar" => "embvar",
  "FCall" => "fcall",
  "FloatLiteral" => "float",
  "FndPtn" => "fndptn",
  "GVar" => "gvar",
  "HashLiteral" => "hash",
  "HshPtn" => "hshptn",
  "IVar" => "ivar",
  "KwRestParam" => "kwrest_param",
  "LBrace" => "lbrace",
  "LBracket" => "lbracket",
  "LParen" => "lparen",
  "MAssign" => "massign",
  "MLHS" => "mlhs",
  "MLHSParen" => "mlhs_paren",
  "ModuleDeclaration" => "module",
  "MRHS" => "mrhs",
  "QSymbols" => "qsymbols",
  "QSymbolsBeg" => "qsymbols_beg",
  "QWords" => "qwords",
  "QWordsBeg" => "qwords_beg",
  "RationalLiteral" => "rational",
  "RBrace" => "rbrace",
  "RBracket" => "rbracket",
  "RParen" => "rparen",
  "SClass" => "sclass",
  "StringDVar" => "string_dvar",
  "StringEmbExpr" => "string_embexpr",
  "TLambda" => "tlambda",
  "TLamBeg" => "tlambeg",
  "TStringBeg" => "tstring_beg",
  "TStringContent" => "tstring_content",
  "TStringEnd" => "tstring_end",
  "VCall" => "vcall",
  "XString" => "xstring",
  "XStringLiteral" => "xstring_literal",
  "ZSuper" => "zsuper"
}

NAMES.default_proc = -> (hash, key) do
  value = key.dup
  value.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
  value.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
  value.tr!("-", "_")
  value.downcase!
  hash[key] = value
end

filepath = File.expand_path("../lib/syntax_tree/node.rb", __dir__)
source = SyntaxTree.read(filepath)

ast = SyntaxTree.parse(source)
ast => SyntaxTree::Program[statements: { body: [*, SyntaxTree::ModuleDeclaration[bodystmt: { statements: { body: classes } }]] }]

classes.each do |node|
  next unless node in SyntaxTree::ClassDeclaration[superclass: { value: { value: "Node" } }]

  body = node.bodystmt.statements.body
  index = body.index { _1 in SyntaxTree::Def[name: { value: "child_nodes" }] }
  index ||= body.length - 1

  body.insert(index, SyntaxTree.parse(<<~RUBY).statements.body.first)
    def accept(visitor)
      visitor.visit_#{NAMES[node.constant.constant.value]}(self)
    end
  RUBY

  body[index].location.instance_variable_set(:@start_line, node.location.end_line)
end

NAMES.sort.to_h.each do |original, change|
  puts <<~RUBY
    # Visit a#{original.match?(/^[AEIOU]/) ? "n" : ""} #{original} node.
    alias visit_#{change} visit_child_nodes

  RUBY
end

formatter = SyntaxTree::Formatter.new(source, [])
ast.format(formatter)

formatter.flush
File.write(filepath, formatter.output.join)

@kddnewton kddnewton merged commit 71c496f into main Apr 1, 2022
@kddnewton kddnewton deleted the visitors branch April 1, 2022 15:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant