Skip to content

Commit a1d5501

Browse files
committed
Add WithEnvironment mixin for visitors
1 parent cf307d1 commit a1d5501

File tree

3 files changed

+139
-0
lines changed

3 files changed

+139
-0
lines changed

lib/syntax_tree.rb

+2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
require_relative "syntax_tree/visitor/json_visitor"
2020
require_relative "syntax_tree/visitor/match_visitor"
2121
require_relative "syntax_tree/visitor/pretty_print_visitor"
22+
require_relative "syntax_tree/visitor/environment"
23+
require_relative "syntax_tree/visitor/with_environment"
2224

2325
# Syntax Tree is a suite of tools built on top of the internal CRuby parser. It
2426
# provides the ability to generate a syntax tree from source, as well as the
+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# frozen_string_literal: true
2+
3+
module SyntaxTree
4+
class Environment
5+
# [Array[Local]] The local variables and arguments defined in this environment
6+
attr_reader :locals
7+
8+
class Local
9+
# [Symbol] The type of the local (e.g. :argument, :variable)
10+
attr_reader :type
11+
12+
# [Array[Location]] The locations of all occurrences of this local, including its definition
13+
attr_reader :locations
14+
15+
def initialize(type)
16+
@type = type
17+
@locations = []
18+
end
19+
20+
def <<(location)
21+
@locations << location
22+
end
23+
end
24+
25+
def initialize(parent = nil)
26+
@locals = {}
27+
@parent = parent
28+
end
29+
30+
# Registering a local will either insert a new entry in the locals hash or append a new location to an existing
31+
# local. Notice that it's not possible to change the type of a local after it has been registered
32+
# find_local: (Ident | Label identifier, Symbol type) -> void
33+
def register_local(identifier, type)
34+
name = identifier.value.delete_suffix(":")
35+
36+
@locals[name] ||= Local.new(type)
37+
@locals[name] << identifier.location
38+
end
39+
40+
# Try to find the local given its name in this environment or any of its parents
41+
# find_local: (String name) -> Array[Location] | nil
42+
def find_local(name)
43+
locations = @locals[name]
44+
return locations unless locations.nil?
45+
46+
@parent&.find_local(name)
47+
end
48+
end
49+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# frozen_string_literal: true
2+
3+
module SyntaxTree
4+
# WithEnvironment is a module intended to be included in classes inheriting from Visitor. The module overrides a few
5+
# visit methods to automatically keep track of local variables and arguments defined in the current environment.
6+
# Example usage:
7+
# class MyVisitor < Visitor
8+
# include WithEnvironment
9+
#
10+
# def visit_ident(node)
11+
# # Check if we're visiting an identifier for an argument, a local variable or something else
12+
# local = current_environment.find_local(node)
13+
#
14+
# if local.type == :argument
15+
# # handle identifiers for arguments
16+
# elsif local.type == :variable
17+
# # handle identifiers for variables
18+
# else
19+
# # handle other identifiers, such as method names
20+
# end
21+
# end
22+
module WithEnvironment
23+
def current_environment
24+
@current_environment ||= Environment.new
25+
end
26+
27+
# Visits for nodes that create new environments, such as classes, modules and method definitions
28+
def visit_with_new_environment(node)
29+
previous_environment = @current_environment
30+
@current_environment = Environment.new(previous_environment)
31+
super
32+
@current_environment = previous_environment
33+
end
34+
35+
alias visit_class visit_with_new_environment
36+
alias visit_module visit_with_new_environment
37+
alias visit_method_add_block visit_with_new_environment
38+
alias visit_def visit_with_new_environment
39+
alias visit_defs visit_with_new_environment
40+
alias visit_def_endless visit_with_new_environment
41+
42+
# Visit for keeping track of local arguments, such as method and block arguments
43+
def visit_params(node)
44+
node.requireds.each { |param| @current_environment.register_local(param, :argument) }
45+
node.posts.each { |param| @current_environment.register_local(param, :argument) }
46+
node.keywords.each { |param| @current_environment.register_local(param.first, :argument) }
47+
node.optionals.each { |param| @current_environment.register_local(param.first, :argument) }
48+
49+
super
50+
end
51+
52+
def visit_register_param(node)
53+
name = node.name
54+
@current_environment.register_local(name, :argument) if name
55+
56+
super
57+
end
58+
59+
alias visit_rest_param visit_register_param
60+
alias visit_kwrest_param visit_register_param
61+
alias visit_blockarg visit_register_param
62+
63+
# Visits for keeping track of local variables
64+
def visit_a_ref_field(node)
65+
name = node.collection.value
66+
@current_environment.register_local(name, :variable) if name
67+
68+
super
69+
end
70+
71+
def visit_m_assign(node)
72+
node.target.parts.each do |var_ref|
73+
@current_environment.register_local(var_ref.value, :variable)
74+
end
75+
76+
super
77+
end
78+
79+
def visit_var_field(node)
80+
value = node.value
81+
@current_environment.register_local(value, :variable) if value.is_a?(SyntaxTree::Ident)
82+
83+
super
84+
end
85+
86+
alias visit_var_ref visit_var_field
87+
end
88+
end

0 commit comments

Comments
 (0)