Skip to content

Commit dca699a

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

File tree

3 files changed

+153
-0
lines changed

3 files changed

+153
-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
+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
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 do |param|
45+
@current_environment.register_local(param, :argument)
46+
end
47+
48+
node.posts.each do |param|
49+
@current_environment.register_local(param, :argument)
50+
end
51+
52+
node.keywords.each do |param|
53+
@current_environment.register_local(param.first, :argument)
54+
end
55+
56+
node.optionals.each do |param|
57+
@current_environment.register_local(param.first, :argument)
58+
end
59+
60+
super
61+
end
62+
63+
def visit_register_param(node)
64+
name = node.name
65+
@current_environment.register_local(name, :argument) if name
66+
67+
super
68+
end
69+
70+
alias visit_rest_param visit_register_param
71+
alias visit_kwrest_param visit_register_param
72+
alias visit_blockarg visit_register_param
73+
74+
# Visits for keeping track of local variables
75+
def visit_a_ref_field(node)
76+
name = node.collection.value
77+
@current_environment.register_local(name, :variable) if name
78+
79+
super
80+
end
81+
82+
def visit_m_assign(node)
83+
node.target.parts.each do |var_ref|
84+
@current_environment.register_local(var_ref.value, :variable)
85+
end
86+
87+
super
88+
end
89+
90+
def visit_var_field(node)
91+
value = node.value
92+
93+
if value.is_a?(SyntaxTree::Ident)
94+
@current_environment.register_local(value, :variable)
95+
end
96+
97+
super
98+
end
99+
100+
alias visit_var_ref visit_var_field
101+
end
102+
end

0 commit comments

Comments
 (0)