Skip to content

Commit f6d02c6

Browse files
committed
Add WithEnvironment mixin for visitors
1 parent cb72efc commit f6d02c6

File tree

6 files changed

+668
-12
lines changed

6 files changed

+668
-12
lines changed

README.md

+32-10
Original file line numberDiff line numberDiff line change
@@ -368,16 +368,16 @@ program = SyntaxTree.parse("1 + 1")
368368
puts program.construct_keys
369369

370370
# SyntaxTree::Program[
371-
# statements: SyntaxTree::Statements[
372-
# body: [
373-
# SyntaxTree::Binary[
374-
# left: SyntaxTree::Int[value: "1"],
375-
# operator: :+,
376-
# right: SyntaxTree::Int[value: "1"]
377-
# ]
378-
# ]
379-
# ]
380-
# ]
371+
# statements: SyntaxTree::Statements[
372+
# body: [
373+
# SyntaxTree::Binary[
374+
# left: SyntaxTree::Int[value: "1"],
375+
# operator: :+,
376+
# right: SyntaxTree::Int[value: "1"]
377+
# ]
378+
# ]
379+
# ]
380+
# ]
381381
```
382382

383383
## Visitor
@@ -447,6 +447,28 @@ end
447447

448448
The visitor defined above will error out unless it's only visiting a `SyntaxTree::Int` node. This is useful in a couple of ways, e.g., if you're trying to define a visitor to handle the whole tree but it's currently a work-in-progress.
449449

450+
### WithEnvironment
451+
452+
The `WithEnvironment` module can be included in visitors to automatically keep track of local variables and arguments
453+
defined inside each environment. A `current_environment` accessor is made availble to the request, allowing it to find
454+
all usages and definitions of a local.
455+
456+
```ruby
457+
class MyVisitor < Visitor
458+
include WithEnvironment
459+
460+
def visit_ident(node)
461+
# find_local will return a Local for any local variables or arguments present in the current environment or nil if
462+
# the identifier is not a local
463+
local = current_environment.find_local(node)
464+
465+
puts local.type # print the type of the local (:variable or :argument)
466+
puts local.definitions # print the array of locations where this local is defined
467+
puts local.usages # print the array of locations where this local occurs
468+
end
469+
end
470+
```
471+
450472
## Language server
451473

452474
Syntax Tree additionally ships with a language server conforming to the [language server protocol](https://microsoft.github.io/language-server-protocol/). It can be invoked through the CLI by running:

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

lib/syntax_tree/node.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -1956,7 +1956,7 @@ def format(q)
19561956
# are no parentheses around the arguments to that command, so we need to
19571957
# break the block.
19581958
case q.parent
1959-
in { call: Command | CommandCall }
1959+
in call: Command | CommandCall
19601960
q.break_parent
19611961
format_break(q, break_opening, break_closing)
19621962
return
@@ -1994,7 +1994,7 @@ def unchangeable_bounds?(q)
19941994
# use the do..end bounds.
19951995
def forced_do_end_bounds?(q)
19961996
case q.parent
1997-
in { call: Break | Next | Return | Super }
1997+
in call: Break | Next | Return | Super
19981998
true
19991999
else
20002000
false
+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
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 definitions and assignments of
17+
# this local
18+
attr_reader :definitions
19+
20+
# [Array[Location]] The locations of all usages of this local
21+
attr_reader :usages
22+
23+
# initialize: (Symbol type) -> void
24+
def initialize(type)
25+
@type = type
26+
@definitions = []
27+
@usages = []
28+
end
29+
30+
# add_definition: (Location location) -> void
31+
def add_definition(location)
32+
@definitions << location
33+
end
34+
35+
# add_usage: (Location location) -> void
36+
def add_usage(location)
37+
@usages << location
38+
end
39+
end
40+
41+
# initialize: (Environment | nil parent) -> void
42+
def initialize(parent = nil)
43+
@locals = {}
44+
@parent = parent
45+
end
46+
47+
# Adding a local definition will either insert a new entry in the locals
48+
# hash or append a new definition location to an existing local. Notice that
49+
# it's not possible to change the type of a local after it has been
50+
# registered
51+
# add_local_definition: (Ident | Label identifier, Symbol type) -> void
52+
def add_local_definition(identifier, type)
53+
name = identifier.value.delete_suffix(":")
54+
55+
@locals[name] ||= Local.new(type)
56+
@locals[name].add_definition(identifier.location)
57+
end
58+
59+
# Adding a local usage will either insert a new entry in the locals
60+
# hash or append a new usage location to an existing local. Notice that
61+
# it's not possible to change the type of a local after it has been
62+
# registered
63+
# add_local_usage: (Ident | Label identifier, Symbol type) -> void
64+
def add_local_usage(identifier, type)
65+
name = identifier.value.delete_suffix(":")
66+
67+
@locals[name] ||= Local.new(type)
68+
@locals[name].add_usage(identifier.location)
69+
end
70+
71+
# Try to find the local given its name in this environment or any of its
72+
# parents
73+
# find_local: (String name) -> Local | nil
74+
def find_local(name)
75+
local = @locals[name]
76+
return local unless local.nil?
77+
78+
@parent&.find_local(name)
79+
end
80+
end
81+
end
+141
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
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.add_local_definition(param, :argument)
68+
end
69+
70+
node.posts.each do |param|
71+
@current_environment.add_local_definition(param, :argument)
72+
end
73+
74+
node.keywords.each do |param|
75+
@current_environment.add_local_definition(param.first, :argument)
76+
end
77+
78+
node.optionals.each do |param|
79+
@current_environment.add_local_definition(param.first, :argument)
80+
end
81+
82+
super
83+
end
84+
85+
def visit_rest_param(node)
86+
name = node.name
87+
@current_environment.add_local_definition(name, :argument) if name
88+
89+
super
90+
end
91+
92+
def visit_kwrest_param(node)
93+
name = node.name
94+
@current_environment.add_local_definition(name, :argument) if name
95+
96+
super
97+
end
98+
99+
def visit_blockarg(node)
100+
name = node.name
101+
@current_environment.add_local_definition(name, :argument) if name
102+
103+
super
104+
end
105+
106+
# Visit for keeping track of local variable definitions
107+
def visit_var_field(node)
108+
value = node.value
109+
110+
if value.is_a?(SyntaxTree::Ident)
111+
@current_environment.add_local_definition(value, :variable)
112+
end
113+
114+
super
115+
end
116+
117+
alias visit_pinned_var_ref visit_var_field
118+
119+
# Visits for keeping track of variable and argument usages
120+
def visit_aref_field(node)
121+
name = node.collection.value
122+
@current_environment.add_local_usage(name, :variable) if name
123+
124+
super
125+
end
126+
127+
def visit_var_ref(node)
128+
value = node.value
129+
130+
if value.is_a?(SyntaxTree::Ident)
131+
definition = @current_environment.find_local(value.value)
132+
133+
if definition
134+
@current_environment.add_local_usage(value, definition.type)
135+
end
136+
end
137+
138+
super
139+
end
140+
end
141+
end

0 commit comments

Comments
 (0)