Skip to content

Commit c9dca3a

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

File tree

4 files changed

+573
-0
lines changed

4 files changed

+573
-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
+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# frozen_string_literal: true
2+
3+
module SyntaxTree
4+
# The environment class is used to keep track of local variables and arguments
5+
# inside a particular scope
6+
class Environment
7+
# [Array[Local]] The local variables and arguments defined in this
8+
# environment
9+
attr_reader :locals
10+
11+
# This class tracks the occurrences of a local variable or argument
12+
class Local
13+
# [Symbol] The type of the local (e.g. :argument, :variable)
14+
attr_reader :type
15+
16+
# [Array[Location]] The locations of all occurrences of this local,
17+
# including its definition
18+
attr_reader :locations
19+
20+
def initialize(type)
21+
@type = type
22+
@locations = []
23+
end
24+
25+
def <<(location)
26+
@locations << location
27+
end
28+
end
29+
30+
def initialize(parent = nil)
31+
@locals = {}
32+
@parent = parent
33+
end
34+
35+
# Registering a local will either insert a new entry in the locals hash or
36+
# append a new location to an existing local. Notice that it's not possible
37+
# to change the type of a local after it has been registered
38+
# find_local: (Ident | Label identifier, Symbol type) -> void
39+
def register_local(identifier, type)
40+
name = identifier.value.delete_suffix(":")
41+
42+
@locals[name] ||= Local.new(type)
43+
@locals[name] << identifier.location
44+
end
45+
46+
# Try to find the local given its name in this environment or any of its
47+
# parents
48+
# find_local: (String name) -> Array[Location] | nil
49+
def find_local(name)
50+
locations = @locals[name]
51+
return locations unless locations.nil?
52+
53+
@parent&.find_local(name)
54+
end
55+
end
56+
end
+125
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
# frozen_string_literal: true
2+
3+
module SyntaxTree
4+
# WithEnvironment is a module intended to be included in classes inheriting
5+
# from Visitor. The module overrides a few visit methods to automatically keep
6+
# track of local variables and arguments defined in the current environment.
7+
# Example usage:
8+
# class MyVisitor < Visitor
9+
# include WithEnvironment
10+
#
11+
# def visit_ident(node)
12+
# # Check if we're visiting an identifier for an argument, a local
13+
# variable or something else
14+
# local = current_environment.find_local(node)
15+
#
16+
# if local.type == :argument
17+
# # handle identifiers for arguments
18+
# elsif local.type == :variable
19+
# # handle identifiers for variables
20+
# else
21+
# # handle other identifiers, such as method names
22+
# end
23+
# end
24+
module WithEnvironment
25+
def current_environment
26+
@current_environment ||= Environment.new
27+
end
28+
29+
def with_new_environment
30+
previous_environment = @current_environment
31+
@current_environment = Environment.new(previous_environment)
32+
yield
33+
@current_environment = previous_environment
34+
end
35+
36+
# Visits for nodes that create new environments, such as classes, modules
37+
# and method definitions
38+
def visit_class(node)
39+
with_new_environment { super }
40+
end
41+
42+
def visit_module(node)
43+
with_new_environment { super }
44+
end
45+
46+
def visit_method_add_block(node)
47+
with_new_environment { super }
48+
end
49+
50+
def visit_def(node)
51+
with_new_environment { super }
52+
end
53+
54+
def visit_defs(node)
55+
with_new_environment { super }
56+
end
57+
58+
def visit_def_endless(node)
59+
with_new_environment { super }
60+
end
61+
62+
# Visit for keeping track of local arguments, such as method and block
63+
# arguments
64+
def visit_params(node)
65+
node.requireds.each do |param|
66+
@current_environment.register_local(param, :argument)
67+
end
68+
69+
node.posts.each do |param|
70+
@current_environment.register_local(param, :argument)
71+
end
72+
73+
node.keywords.each do |param|
74+
@current_environment.register_local(param.first, :argument)
75+
end
76+
77+
node.optionals.each do |param|
78+
@current_environment.register_local(param.first, :argument)
79+
end
80+
81+
super
82+
end
83+
84+
def visit_rest_param(node)
85+
name = node.name
86+
@current_environment.register_local(name, :argument) if name
87+
88+
super
89+
end
90+
91+
def visit_kwrest_param(node)
92+
name = node.name
93+
@current_environment.register_local(name, :argument) if name
94+
95+
super
96+
end
97+
98+
def visit_blockarg(node)
99+
name = node.name
100+
@current_environment.register_local(name, :argument) if name
101+
102+
super
103+
end
104+
105+
# Visits for keeping track of local variables
106+
def visit_aref_field(node)
107+
name = node.collection.value
108+
@current_environment.register_local(name, :variable) if name
109+
110+
super
111+
end
112+
113+
def visit_var_field(node)
114+
value = node.value
115+
116+
if value.is_a?(SyntaxTree::Ident)
117+
@current_environment.register_local(value, :variable)
118+
end
119+
120+
super
121+
end
122+
123+
alias visit_var_ref visit_var_field
124+
end
125+
end

0 commit comments

Comments
 (0)