Skip to content

Commit 02b3c49

Browse files
committed
Documentation for search
1 parent bfa8c39 commit 02b3c49

File tree

3 files changed

+56
-22
lines changed

3 files changed

+56
-22
lines changed

README.md

+24
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ It is built with only standard library dependencies. It additionally ships with
1515
- [CLI](#cli)
1616
- [ast](#ast)
1717
- [check](#check)
18+
- [expr](#expr)
1819
- [format](#format)
1920
- [json](#json)
2021
- [match](#match)
@@ -26,6 +27,7 @@ It is built with only standard library dependencies. It additionally ships with
2627
- [SyntaxTree.read(filepath)](#syntaxtreereadfilepath)
2728
- [SyntaxTree.parse(source)](#syntaxtreeparsesource)
2829
- [SyntaxTree.format(source)](#syntaxtreeformatsource)
30+
- [SyntaxTree.search(source, query, &block)](#syntaxtreesearchsource-query-block)
2931
- [Nodes](#nodes)
3032
- [child_nodes](#child_nodes)
3133
- [Pattern matching](#pattern-matching)
@@ -129,6 +131,24 @@ To change the print width that you are checking against, specify the `--print-wi
129131
stree check --print-width=100 path/to/file.rb
130132
```
131133

134+
### expr
135+
136+
This command will output a Ruby case-match expression that would match correctly against the first expression of the input.
137+
138+
```sh
139+
stree expr path/to/file.rb
140+
```
141+
142+
For a file that contains `1 + 1`, you will receive:
143+
144+
```ruby
145+
SyntaxTree::Binary[
146+
left: SyntaxTree::Int[value: "1"],
147+
operator: :+,
148+
right: SyntaxTree::Int[value: "1"]
149+
]
150+
```
151+
132152
### format
133153

134154
This command will output the formatted version of each of the listed files. Importantly, it will not write that content back to the source files. It is meant to display the formatted version only.
@@ -312,6 +332,10 @@ This function takes an input string containing Ruby code and returns the syntax
312332

313333
This function takes an input string containing Ruby code, parses it into its underlying syntax tree, and formats it back out to a string. You can optionally pass a second argument to this method as well that is the maximum width to print. It defaults to `80`.
314334

335+
### SyntaxTree.search(source, query, &block)
336+
337+
This function takes an input string containing Ruby code, an input string containing a valid Ruby `in` clause expression that can be used to match against nodes in the tree (can be generated using `stree expr`, `stree match`, or `Node#construct_keys`), and a block. Each node that matches the given query will be yielded to the block. The block will receive the node as its only argument.
338+
315339
## Nodes
316340

317341
There are many different node types in the syntax tree. They are meant to be treated as immutable structs containing links to child nodes with minimal logic contained within their implementation. However, for the most part they all respond to a certain set of APIs, listed below.

lib/syntax_tree.rb

+6
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,10 @@ def self.read(filepath)
7575

7676
File.read(filepath, encoding: encoding)
7777
end
78+
79+
# Searches through the given source using the given pattern and yields each
80+
# node in the tree that matches the pattern to the given block.
81+
def self.search(source, query, &block)
82+
Search.new(Pattern.new(query).compile).scan(parse(source), &block)
83+
end
7884
end

lib/syntax_tree/pattern.rb

+26-22
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ module SyntaxTree
3737
# do not yet support) then a SyntaxTree::Pattern::CompilationError will be
3838
# raised.
3939
class Pattern
40+
# Raised when the query given to a pattern is either invalid Ruby syntax or
41+
# is using syntax that we don't yet support.
4042
class CompilationError < StandardError
4143
def initialize(repr)
4244
super(<<~ERROR)
@@ -76,27 +78,27 @@ def compile
7678
def combine_and(left, right)
7779
->(node) { left.call(node) && right.call(node) }
7880
end
79-
81+
8082
def combine_or(left, right)
8183
->(node) { left.call(node) || right.call(node) }
8284
end
8385

84-
def compile_node(node)
85-
case node
86+
def compile_node(root)
87+
case root
8688
in AryPtn[constant:, requireds:, rest: nil, posts: []]
8789
compiled_constant = compile_node(constant) if constant
88-
90+
8991
preprocessed = requireds.map { |required| compile_node(required) }
90-
92+
9193
compiled_requireds = ->(node) do
9294
deconstructed = node.deconstruct
93-
95+
9496
deconstructed.length == preprocessed.length &&
95-
preprocessed.zip(deconstructed).all? do |(matcher, value)|
96-
matcher.call(value)
97-
end
97+
preprocessed
98+
.zip(deconstructed)
99+
.all? { |(matcher, value)| matcher.call(value) }
98100
end
99-
101+
100102
if compiled_constant
101103
combine_and(compiled_constant, compiled_requireds)
102104
else
@@ -106,63 +108,65 @@ def compile_node(node)
106108
combine_or(compile_node(left), compile_node(right))
107109
in Const[value:] if SyntaxTree.const_defined?(value)
108110
clazz = SyntaxTree.const_get(value)
109-
111+
110112
->(node) { node.is_a?(clazz) }
111113
in Const[value:] if Object.const_defined?(value)
112114
clazz = Object.const_get(value)
113-
115+
114116
->(node) { node.is_a?(clazz) }
115-
in ConstPathRef[parent: VarRef[value: Const[value: "SyntaxTree"]], constant:]
117+
in ConstPathRef[
118+
parent: VarRef[value: Const[value: "SyntaxTree"]], constant:
119+
]
116120
compile_node(constant)
117121
in DynaSymbol[parts: []]
118-
symbol = "".to_sym
122+
symbol = :""
119123

120124
->(node) { node == symbol }
121125
in DynaSymbol[parts: [TStringContent[value:]]]
122126
symbol = value.to_sym
123-
127+
124128
->(attribute) { attribute == value }
125129
in HshPtn[constant:, keywords:, keyword_rest: nil]
126130
compiled_constant = compile_node(constant)
127-
131+
128132
preprocessed =
129133
keywords.to_h do |keyword, value|
130134
raise NoMatchingPatternError unless keyword.is_a?(Label)
131135
[keyword.value.chomp(":").to_sym, compile_node(value)]
132136
end
133-
137+
134138
compiled_keywords = ->(node) do
135139
deconstructed = node.deconstruct_keys(preprocessed.keys)
136-
140+
137141
preprocessed.all? do |keyword, matcher|
138142
matcher.call(deconstructed[keyword])
139143
end
140144
end
141-
145+
142146
if compiled_constant
143147
combine_and(compiled_constant, compiled_keywords)
144148
else
145149
compiled_keywords
146150
end
147151
in RegexpLiteral[parts: [TStringContent[value:]]]
148152
regexp = /#{value}/
149-
153+
150154
->(attribute) { regexp.match?(attribute) }
151155
in StringLiteral[parts: []]
152156
->(attribute) { attribute == "" }
153157
in StringLiteral[parts: [TStringContent[value:]]]
154158
->(attribute) { attribute == value }
155159
in SymbolLiteral[value:]
156160
symbol = value.value.to_sym
157-
161+
158162
->(attribute) { attribute == symbol }
159163
in VarRef[value: Const => value]
160164
compile_node(value)
161165
in VarRef[value: Kw[value: "nil"]]
162166
->(attribute) { attribute.nil? }
163167
end
164168
rescue NoMatchingPatternError
165-
raise CompilationError, PP.pp(node, +"").chomp
169+
raise CompilationError, PP.pp(root, +"").chomp
166170
end
167171
end
168172
end

0 commit comments

Comments
 (0)