Skip to content

Commit 2eccaec

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

File tree

4 files changed

+574
-0
lines changed

4 files changed

+574
-0
lines changed

lib/syntax_tree.rb

Lines changed: 2 additions & 0 deletions
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
Lines changed: 56 additions & 0 deletions
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
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
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+
ensure
34+
@current_environment = previous_environment
35+
end
36+
37+
# Visits for nodes that create new environments, such as classes, modules
38+
# and method definitions
39+
def visit_class(node)
40+
with_new_environment { super }
41+
end
42+
43+
def visit_module(node)
44+
with_new_environment { super }
45+
end
46+
47+
def visit_method_add_block(node)
48+
with_new_environment { super }
49+
end
50+
51+
def visit_def(node)
52+
with_new_environment { super }
53+
end
54+
55+
def visit_defs(node)
56+
with_new_environment { super }
57+
end
58+
59+
def visit_def_endless(node)
60+
with_new_environment { super }
61+
end
62+
63+
# Visit for keeping track of local arguments, such as method and block
64+
# arguments
65+
def visit_params(node)
66+
node.requireds.each do |param|
67+
@current_environment.register_local(param, :argument)
68+
end
69+
70+
node.posts.each do |param|
71+
@current_environment.register_local(param, :argument)
72+
end
73+
74+
node.keywords.each do |param|
75+
@current_environment.register_local(param.first, :argument)
76+
end
77+
78+
node.optionals.each do |param|
79+
@current_environment.register_local(param.first, :argument)
80+
end
81+
82+
super
83+
end
84+
85+
def visit_rest_param(node)
86+
name = node.name
87+
@current_environment.register_local(name, :argument) if name
88+
89+
super
90+
end
91+
92+
def visit_kwrest_param(node)
93+
name = node.name
94+
@current_environment.register_local(name, :argument) if name
95+
96+
super
97+
end
98+
99+
def visit_blockarg(node)
100+
name = node.name
101+
@current_environment.register_local(name, :argument) if name
102+
103+
super
104+
end
105+
106+
# Visits for keeping track of local variables
107+
def visit_aref_field(node)
108+
name = node.collection.value
109+
@current_environment.register_local(name, :variable) if name
110+
111+
super
112+
end
113+
114+
def visit_var_field(node)
115+
value = node.value
116+
117+
if value.is_a?(SyntaxTree::Ident)
118+
@current_environment.register_local(value, :variable)
119+
end
120+
121+
super
122+
end
123+
124+
alias visit_var_ref visit_var_field
125+
end
126+
end

0 commit comments

Comments
 (0)