Skip to content

Commit f6e0c2e

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

File tree

4 files changed

+569
-0
lines changed

4 files changed

+569
-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
+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# frozen_string_literal: true
2+
3+
module SyntaxTree
4+
# The environment class is used to keep track of local variables and arguments inside a particular scope
5+
class Environment
6+
# [Array[Local]] The local variables and arguments defined in this
7+
# environment
8+
attr_reader :locals
9+
10+
# This class tracks the occurrences of a local variable or argument
11+
class Local
12+
# [Symbol] The type of the local (e.g. :argument, :variable)
13+
attr_reader :type
14+
15+
# [Array[Location]] The locations of all occurrences of this local,
16+
# including its definition
17+
attr_reader :locations
18+
19+
def initialize(type)
20+
@type = type
21+
@locations = []
22+
end
23+
24+
def <<(location)
25+
@locations << location
26+
end
27+
end
28+
29+
def initialize(parent = nil)
30+
@locals = {}
31+
@parent = parent
32+
end
33+
34+
# Registering a local will either insert a new entry in the locals hash or
35+
# append a new location to an existing local. Notice that it's not possible
36+
# to change the type of a local after it has been registered
37+
# find_local: (Ident | Label identifier, Symbol type) -> void
38+
def register_local(identifier, type)
39+
name = identifier.value.delete_suffix(":")
40+
41+
@locals[name] ||= Local.new(type)
42+
@locals[name] << identifier.location
43+
end
44+
45+
# Try to find the local given its name in this environment or any of its
46+
# parents
47+
# find_local: (String name) -> Array[Location] | nil
48+
def find_local(name)
49+
locations = @locals[name]
50+
return locations unless locations.nil?
51+
52+
@parent&.find_local(name)
53+
end
54+
end
55+
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)