syntax_tree 4.1.0 → 4.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/main.yml +2 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +3 -0
- data/CHANGELOG.md +26 -1
- data/Gemfile.lock +1 -1
- data/README.md +25 -1
- data/lib/syntax_tree/cli.rb +30 -4
- data/lib/syntax_tree/language_server/inlay_hints.rb +4 -6
- data/lib/syntax_tree/language_server.rb +64 -17
- data/lib/syntax_tree/pattern.rb +287 -0
- data/lib/syntax_tree/rake/task.rb +1 -1
- data/lib/syntax_tree/search.rb +4 -70
- data/lib/syntax_tree/version.rb +1 -1
- data/lib/syntax_tree.rb +7 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4ca1eae46ba326b73e129d5b066d144aeac2743947956efc1453a3539c2caac9
|
4
|
+
data.tar.gz: 7c833e7b7bf25df7a82653d9dddb30aa172320a625a0655c52a29640c7a2154f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1720e9ef9dd52399564607a9eca7794875e41f36f8ee2c4af2d97fb973c38ec0bf3a3cc254d26f3d656e67256543afed7c818e274395af122790ae6730ccab8c
|
7
|
+
data.tar.gz: 0f00dbe7739f71bfa1b81ff204fdd4dc505277a65e374810d2f482c6ef3c81078b39f864d55d8deed001a5b7a294491feb0b930948546bcd4194a88058ca9783
|
data/.github/workflows/main.yml
CHANGED
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -6,6 +6,29 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a
|
|
6
6
|
|
7
7
|
## [Unreleased]
|
8
8
|
|
9
|
+
## [4.3.0] - 2022-10-28
|
10
|
+
|
11
|
+
### Added
|
12
|
+
|
13
|
+
- [#183](https://github.com/ruby-syntax-tree/syntax_tree/pull/183) - Support TruffleRuby by eliminating internal pattern matching in some places and stopping some tests from running in other places.
|
14
|
+
- [#184](https://github.com/ruby-syntax-tree/syntax_tree/pull/184) - Remove internal pattern matching entirely.
|
15
|
+
|
16
|
+
### Changed
|
17
|
+
|
18
|
+
- [#183](https://github.com/ruby-syntax-tree/syntax_tree/pull/183) - Pattern matching works against dynamic symbols now.
|
19
|
+
- [#184](https://github.com/ruby-syntax-tree/syntax_tree/pull/184) - Exit with the correct exit status within the rake tasks.
|
20
|
+
|
21
|
+
## [4.2.0] - 2022-10-25
|
22
|
+
|
23
|
+
### Added
|
24
|
+
|
25
|
+
- [#182](https://github.com/ruby-syntax-tree/syntax_tree/pull/182) - The new `stree expr` CLI command will function similarly to the `stree match` CLI command except that it only outputs the first expression of the program.
|
26
|
+
- [#182](https://github.com/ruby-syntax-tree/syntax_tree/pull/182) - Added the `SyntaxTree::Pattern` class for compiling `in` expressions into procs.
|
27
|
+
|
28
|
+
### Changed
|
29
|
+
|
30
|
+
- [#182](https://github.com/ruby-syntax-tree/syntax_tree/pull/182) - Much more syntax is now supported by the search command.
|
31
|
+
|
9
32
|
## [4.1.0] - 2022-10-24
|
10
33
|
|
11
34
|
### Added
|
@@ -403,7 +426,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a
|
|
403
426
|
|
404
427
|
- 🎉 Initial release! 🎉
|
405
428
|
|
406
|
-
[unreleased]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v4.
|
429
|
+
[unreleased]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v4.3.0...HEAD
|
430
|
+
[4.3.0]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v4.2.0...v4.3.0
|
431
|
+
[4.2.0]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v4.1.0...v4.2.0
|
407
432
|
[4.1.0]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v4.0.2...v4.1.0
|
408
433
|
[4.0.2]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v4.0.1...v4.0.2
|
409
434
|
[4.0.1]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v4.0.0...v4.0.1
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -15,6 +15,7 @@ It is built with only standard library dependencies. It additionally ships with
|
|
15
15
|
- [CLI](#cli)
|
16
16
|
- [ast](#ast)
|
17
17
|
- [check](#check)
|
18
|
+
- [expr](#expr)
|
18
19
|
- [format](#format)
|
19
20
|
- [json](#json)
|
20
21
|
- [match](#match)
|
@@ -26,6 +27,7 @@ It is built with only standard library dependencies. It additionally ships with
|
|
26
27
|
- [SyntaxTree.read(filepath)](#syntaxtreereadfilepath)
|
27
28
|
- [SyntaxTree.parse(source)](#syntaxtreeparsesource)
|
28
29
|
- [SyntaxTree.format(source)](#syntaxtreeformatsource)
|
30
|
+
- [SyntaxTree.search(source, query, &block)](#syntaxtreesearchsource-query-block)
|
29
31
|
- [Nodes](#nodes)
|
30
32
|
- [child_nodes](#child_nodes)
|
31
33
|
- [Pattern matching](#pattern-matching)
|
@@ -129,6 +131,24 @@ To change the print width that you are checking against, specify the `--print-wi
|
|
129
131
|
stree check --print-width=100 path/to/file.rb
|
130
132
|
```
|
131
133
|
|
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
|
+
|
132
152
|
### format
|
133
153
|
|
134
154
|
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.
|
@@ -226,7 +246,7 @@ stree search VarRef path/to/file.rb
|
|
226
246
|
|
227
247
|
For a file that contains `Foo + Bar` you will receive:
|
228
248
|
|
229
|
-
```
|
249
|
+
```
|
230
250
|
path/to/file.rb:1:0: Foo + Bar
|
231
251
|
path/to/file.rb:1:6: Foo + Bar
|
232
252
|
```
|
@@ -312,6 +332,10 @@ This function takes an input string containing Ruby code and returns the syntax
|
|
312
332
|
|
313
333
|
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`.
|
314
334
|
|
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
|
+
|
315
339
|
## Nodes
|
316
340
|
|
317
341
|
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.
|
data/lib/syntax_tree/cli.rb
CHANGED
@@ -188,6 +188,21 @@ module SyntaxTree
|
|
188
188
|
end
|
189
189
|
end
|
190
190
|
|
191
|
+
# An action of the CLI that outputs a pattern-matching Ruby expression that
|
192
|
+
# would match the first expression of the input given.
|
193
|
+
class Expr < Action
|
194
|
+
def run(item)
|
195
|
+
program = item.handler.parse(item.source)
|
196
|
+
|
197
|
+
if (expressions = program.statements.body) && expressions.size == 1
|
198
|
+
puts expressions.first.construct_keys
|
199
|
+
else
|
200
|
+
warn("The input to `stree expr` must be a single expression.")
|
201
|
+
exit(1)
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
191
206
|
# An action of the CLI that formats the input source and prints it out.
|
192
207
|
class Format < Action
|
193
208
|
def run(item)
|
@@ -219,10 +234,15 @@ module SyntaxTree
|
|
219
234
|
|
220
235
|
def initialize(query)
|
221
236
|
query = File.read(query) if File.readable?(query)
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
237
|
+
pattern =
|
238
|
+
begin
|
239
|
+
Pattern.new(query).compile
|
240
|
+
rescue Pattern::CompilationError => error
|
241
|
+
warn(error.message)
|
242
|
+
exit(1)
|
243
|
+
end
|
244
|
+
|
245
|
+
@search = SyntaxTree::Search.new(pattern)
|
226
246
|
end
|
227
247
|
|
228
248
|
def run(item)
|
@@ -281,6 +301,10 @@ module SyntaxTree
|
|
281
301
|
#{Color.bold("stree doc [--plugins=...] [-e SCRIPT] FILE")}
|
282
302
|
Print out the doc tree that would be used to format the given files
|
283
303
|
|
304
|
+
#{Color.bold("stree expr [-e SCRIPT] FILE")}
|
305
|
+
Print out a pattern-matching Ruby expression that would match the first
|
306
|
+
expression of the given files
|
307
|
+
|
284
308
|
#{Color.bold("stree format [--plugins=...] [--print-width=NUMBER] [-e SCRIPT] FILE")}
|
285
309
|
Print out the formatted version of the given files
|
286
310
|
|
@@ -436,6 +460,8 @@ module SyntaxTree
|
|
436
460
|
Debug.new(options)
|
437
461
|
when "doc"
|
438
462
|
Doc.new(options)
|
463
|
+
when "e", "expr"
|
464
|
+
Expr.new(options)
|
439
465
|
when "f", "format"
|
440
466
|
Format.new(options)
|
441
467
|
when "help"
|
@@ -69,11 +69,10 @@ module SyntaxTree
|
|
69
69
|
#
|
70
70
|
def visit_binary(node)
|
71
71
|
case stack[-2]
|
72
|
-
|
72
|
+
when Assign, OpAssign
|
73
73
|
parentheses(node.location)
|
74
|
-
|
75
|
-
parentheses(node.location)
|
76
|
-
else
|
74
|
+
when Binary
|
75
|
+
parentheses(node.location) if stack[-2].operator != node.operator
|
77
76
|
end
|
78
77
|
|
79
78
|
super
|
@@ -91,9 +90,8 @@ module SyntaxTree
|
|
91
90
|
#
|
92
91
|
def visit_if_op(node)
|
93
92
|
case stack[-2]
|
94
|
-
|
93
|
+
when Assign, Binary, IfOp, OpAssign
|
95
94
|
parentheses(node.location)
|
96
|
-
else
|
97
95
|
end
|
98
96
|
|
99
97
|
super
|
@@ -13,6 +13,50 @@ module SyntaxTree
|
|
13
13
|
# stree lsp
|
14
14
|
#
|
15
15
|
class LanguageServer
|
16
|
+
# This is a small module that effectively mirrors pattern matching. We're
|
17
|
+
# using it so that we can support truffleruby without having to ignore the
|
18
|
+
# language server.
|
19
|
+
module Request
|
20
|
+
# Represents a hash pattern.
|
21
|
+
class Shape
|
22
|
+
attr_reader :values
|
23
|
+
|
24
|
+
def initialize(values)
|
25
|
+
@values = values
|
26
|
+
end
|
27
|
+
|
28
|
+
def ===(other)
|
29
|
+
values.all? do |key, value|
|
30
|
+
value == :any ? other.key?(key) : value === other[key]
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Represents an array pattern.
|
36
|
+
class Tuple
|
37
|
+
attr_reader :values
|
38
|
+
|
39
|
+
def initialize(values)
|
40
|
+
@values = values
|
41
|
+
end
|
42
|
+
|
43
|
+
def ===(other)
|
44
|
+
values.each_with_index.all? { |value, index| value === other[index] }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.[](value)
|
49
|
+
case value
|
50
|
+
when Array
|
51
|
+
Tuple.new(value.map { |child| self[child] })
|
52
|
+
when Hash
|
53
|
+
Shape.new(value.transform_values { |child| self[child] })
|
54
|
+
else
|
55
|
+
value
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
16
60
|
attr_reader :input, :output, :print_width
|
17
61
|
|
18
62
|
def initialize(
|
@@ -39,30 +83,33 @@ module SyntaxTree
|
|
39
83
|
|
40
84
|
# stree-ignore
|
41
85
|
case request
|
42
|
-
|
86
|
+
when Request[method: "initialize", id: :any]
|
43
87
|
store.clear
|
44
|
-
write(id: id, result: { capabilities: capabilities })
|
45
|
-
|
88
|
+
write(id: request[:id], result: { capabilities: capabilities })
|
89
|
+
when Request[method: "initialized"]
|
46
90
|
# ignored
|
47
|
-
|
91
|
+
when Request[method: "shutdown"] # tolerate missing ID to be a good citizen
|
48
92
|
store.clear
|
49
93
|
write(id: request[:id], result: {})
|
50
94
|
return
|
51
|
-
|
52
|
-
store[uri] = text
|
53
|
-
|
54
|
-
store[uri] = text
|
55
|
-
|
56
|
-
store.delete(uri)
|
57
|
-
|
95
|
+
when Request[method: "textDocument/didChange", params: { textDocument: { uri: :any }, contentChanges: [{ text: :any }] }]
|
96
|
+
store[request.dig(:params, :textDocument, :uri)] = request.dig(:params, :contentChanges, 0, :text)
|
97
|
+
when Request[method: "textDocument/didOpen", params: { textDocument: { uri: :any, text: :any } }]
|
98
|
+
store[request.dig(:params, :textDocument, :uri)] = request.dig(:params, :textDocument, :text)
|
99
|
+
when Request[method: "textDocument/didClose", params: { textDocument: { uri: :any } }]
|
100
|
+
store.delete(request.dig(:params, :textDocument, :uri))
|
101
|
+
when Request[method: "textDocument/formatting", id: :any, params: { textDocument: { uri: :any } }]
|
102
|
+
uri = request.dig(:params, :textDocument, :uri)
|
58
103
|
contents = store[uri]
|
59
|
-
write(id: id, result: contents ? format(contents, uri.split(".").last) : nil)
|
60
|
-
|
104
|
+
write(id: request[:id], result: contents ? format(contents, uri.split(".").last) : nil)
|
105
|
+
when Request[method: "textDocument/inlayHint", id: :any, params: { textDocument: { uri: :any } }]
|
106
|
+
uri = request.dig(:params, :textDocument, :uri)
|
61
107
|
contents = store[uri]
|
62
|
-
write(id: id, result: contents ? inlay_hints(contents) : nil)
|
63
|
-
|
64
|
-
|
65
|
-
|
108
|
+
write(id: request[:id], result: contents ? inlay_hints(contents) : nil)
|
109
|
+
when Request[method: "syntaxTree/visualizing", id: :any, params: { textDocument: { uri: :any } }]
|
110
|
+
uri = request.dig(:params, :textDocument, :uri)
|
111
|
+
write(id: request[:id], result: PP.pp(SyntaxTree.parse(store[uri]), +""))
|
112
|
+
when Request[method: %r{\$/.+}]
|
66
113
|
# ignored
|
67
114
|
else
|
68
115
|
raise ArgumentError, "Unhandled: #{request}"
|
@@ -0,0 +1,287 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SyntaxTree
|
4
|
+
# A pattern is an object that wraps a Ruby pattern matching expression. The
|
5
|
+
# expression would normally be passed to an `in` clause within a `case`
|
6
|
+
# expression or a rightward assignment expression. For example, in the
|
7
|
+
# following snippet:
|
8
|
+
#
|
9
|
+
# case node
|
10
|
+
# in Const[value: "SyntaxTree"]
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
# the pattern is the `Const[value: "SyntaxTree"]` expression. Within Syntax
|
14
|
+
# Tree, every node generates these kinds of expressions using the
|
15
|
+
# #construct_keys method.
|
16
|
+
#
|
17
|
+
# The pattern gets compiled into an object that responds to call by running
|
18
|
+
# the #compile method. This method itself will run back through Syntax Tree to
|
19
|
+
# parse the expression into a tree, then walk the tree to generate the
|
20
|
+
# necessary callable objects. For example, if you wanted to compile the
|
21
|
+
# expression above into a callable, you would:
|
22
|
+
#
|
23
|
+
# callable = SyntaxTree::Pattern.new("Const[value: 'SyntaxTree']").compile
|
24
|
+
# callable.call(node)
|
25
|
+
#
|
26
|
+
# The callable object returned by #compile is guaranteed to respond to #call
|
27
|
+
# with a single argument, which is the node to match against. It also is
|
28
|
+
# guaranteed to respond to #===, which means it itself can be used in a `case`
|
29
|
+
# expression, as in:
|
30
|
+
#
|
31
|
+
# case node
|
32
|
+
# when callable
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
# If the query given to the initializer cannot be compiled into a valid
|
36
|
+
# matcher (either because of a syntax error or because it is using syntax we
|
37
|
+
# do not yet support) then a SyntaxTree::Pattern::CompilationError will be
|
38
|
+
# raised.
|
39
|
+
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.
|
42
|
+
class CompilationError < StandardError
|
43
|
+
def initialize(repr)
|
44
|
+
super(<<~ERROR)
|
45
|
+
Syntax Tree was unable to compile the pattern you provided to search
|
46
|
+
into a usable expression. It failed on to understand the node
|
47
|
+
represented by:
|
48
|
+
|
49
|
+
#{repr}
|
50
|
+
|
51
|
+
Note that not all syntax supported by Ruby's pattern matching syntax
|
52
|
+
is also supported by Syntax Tree's code search. If you're using some
|
53
|
+
syntax that you believe should be supported, please open an issue on
|
54
|
+
GitHub at https://github.com/ruby-syntax-tree/syntax_tree/issues/new.
|
55
|
+
ERROR
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
attr_reader :query
|
60
|
+
|
61
|
+
def initialize(query)
|
62
|
+
@query = query
|
63
|
+
end
|
64
|
+
|
65
|
+
def compile
|
66
|
+
program =
|
67
|
+
begin
|
68
|
+
SyntaxTree.parse("case nil\nin #{query}\nend")
|
69
|
+
rescue Parser::ParseError
|
70
|
+
raise CompilationError, query
|
71
|
+
end
|
72
|
+
|
73
|
+
compile_node(program.statements.body.first.consequent.pattern)
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
# Shortcut for combining two procs into one that returns true if both return
|
79
|
+
# true.
|
80
|
+
def combine_and(left, right)
|
81
|
+
->(other) { left.call(other) && right.call(other) }
|
82
|
+
end
|
83
|
+
|
84
|
+
# Shortcut for combining two procs into one that returns true if either
|
85
|
+
# returns true.
|
86
|
+
def combine_or(left, right)
|
87
|
+
->(other) { left.call(other) || right.call(other) }
|
88
|
+
end
|
89
|
+
|
90
|
+
# Raise an error because the given node is not supported.
|
91
|
+
def compile_error(node)
|
92
|
+
raise CompilationError, PP.pp(node, +"").chomp
|
93
|
+
end
|
94
|
+
|
95
|
+
# There are a couple of nodes (string literals, dynamic symbols, and regexp)
|
96
|
+
# that contain list of parts. This can include plain string content,
|
97
|
+
# interpolated expressions, and interpolated variables. We only support
|
98
|
+
# plain string content, so this method will extract out the plain string
|
99
|
+
# content if it is the only element in the list.
|
100
|
+
def extract_string(node)
|
101
|
+
parts = node.parts
|
102
|
+
|
103
|
+
if parts.length == 1 && (part = parts.first) && part.is_a?(TStringContent)
|
104
|
+
part.value
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# in [foo, bar, baz]
|
109
|
+
def compile_aryptn(node)
|
110
|
+
compile_error(node) if !node.rest.nil? || node.posts.any?
|
111
|
+
|
112
|
+
constant = node.constant
|
113
|
+
compiled_constant = compile_node(constant) if constant
|
114
|
+
|
115
|
+
preprocessed = node.requireds.map { |required| compile_node(required) }
|
116
|
+
|
117
|
+
compiled_requireds = ->(other) do
|
118
|
+
deconstructed = other.deconstruct
|
119
|
+
|
120
|
+
deconstructed.length == preprocessed.length &&
|
121
|
+
preprocessed
|
122
|
+
.zip(deconstructed)
|
123
|
+
.all? { |(matcher, value)| matcher.call(value) }
|
124
|
+
end
|
125
|
+
|
126
|
+
if compiled_constant
|
127
|
+
combine_and(compiled_constant, compiled_requireds)
|
128
|
+
else
|
129
|
+
compiled_requireds
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
# in foo | bar
|
134
|
+
def compile_binary(node)
|
135
|
+
compile_error(node) if node.operator != :|
|
136
|
+
|
137
|
+
combine_or(compile_node(node.left), compile_node(node.right))
|
138
|
+
end
|
139
|
+
|
140
|
+
# in Ident
|
141
|
+
# in String
|
142
|
+
def compile_const(node)
|
143
|
+
value = node.value
|
144
|
+
|
145
|
+
if SyntaxTree.const_defined?(value)
|
146
|
+
clazz = SyntaxTree.const_get(value)
|
147
|
+
|
148
|
+
->(other) { clazz === other }
|
149
|
+
elsif Object.const_defined?(value)
|
150
|
+
clazz = Object.const_get(value)
|
151
|
+
|
152
|
+
->(other) { clazz === other }
|
153
|
+
else
|
154
|
+
compile_error(node)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# in SyntaxTree::Ident
|
159
|
+
def compile_const_path_ref(node)
|
160
|
+
parent = node.parent
|
161
|
+
compile_error(node) if !parent.is_a?(VarRef) || !parent.value.is_a?(Const)
|
162
|
+
|
163
|
+
if parent.value.value == "SyntaxTree"
|
164
|
+
compile_node(node.constant)
|
165
|
+
else
|
166
|
+
compile_error(node)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
# in :""
|
171
|
+
# in :"foo"
|
172
|
+
def compile_dyna_symbol(node)
|
173
|
+
if node.parts.empty?
|
174
|
+
symbol = :""
|
175
|
+
|
176
|
+
->(other) { symbol === other }
|
177
|
+
elsif (value = extract_string(node))
|
178
|
+
symbol = value.to_sym
|
179
|
+
|
180
|
+
->(other) { symbol === other }
|
181
|
+
else
|
182
|
+
compile_error(root)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
# in Ident[value: String]
|
187
|
+
# in { value: String }
|
188
|
+
def compile_hshptn(node)
|
189
|
+
compile_error(node) unless node.keyword_rest.nil?
|
190
|
+
compiled_constant = compile_node(node.constant) if node.constant
|
191
|
+
|
192
|
+
preprocessed =
|
193
|
+
node.keywords.to_h do |keyword, value|
|
194
|
+
compile_error(node) unless keyword.is_a?(Label)
|
195
|
+
[keyword.value.chomp(":").to_sym, compile_node(value)]
|
196
|
+
end
|
197
|
+
|
198
|
+
compiled_keywords = ->(other) do
|
199
|
+
deconstructed = other.deconstruct_keys(preprocessed.keys)
|
200
|
+
|
201
|
+
preprocessed.all? do |keyword, matcher|
|
202
|
+
matcher.call(deconstructed[keyword])
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
if compiled_constant
|
207
|
+
combine_and(compiled_constant, compiled_keywords)
|
208
|
+
else
|
209
|
+
compiled_keywords
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
# in /foo/
|
214
|
+
def compile_regexp_literal(node)
|
215
|
+
if (value = extract_string(node))
|
216
|
+
regexp = /#{value}/
|
217
|
+
|
218
|
+
->(attribute) { regexp === attribute }
|
219
|
+
else
|
220
|
+
compile_error(node)
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
# in ""
|
225
|
+
# in "foo"
|
226
|
+
def compile_string_literal(node)
|
227
|
+
if node.parts.empty?
|
228
|
+
->(attribute) { "" === attribute }
|
229
|
+
elsif (value = extract_string(node))
|
230
|
+
->(attribute) { value === attribute }
|
231
|
+
else
|
232
|
+
compile_error(node)
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
# in :+
|
237
|
+
# in :foo
|
238
|
+
def compile_symbol_literal(node)
|
239
|
+
symbol = node.value.value.to_sym
|
240
|
+
|
241
|
+
->(attribute) { symbol === attribute }
|
242
|
+
end
|
243
|
+
|
244
|
+
# in Foo
|
245
|
+
# in nil
|
246
|
+
def compile_var_ref(node)
|
247
|
+
value = node.value
|
248
|
+
|
249
|
+
if value.is_a?(Const)
|
250
|
+
compile_node(value)
|
251
|
+
elsif value.is_a?(Kw) && value.value.nil?
|
252
|
+
->(attribute) { nil === attribute }
|
253
|
+
else
|
254
|
+
compile_error(node)
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
# Compile any kind of node. Dispatch out to the individual compilation
|
259
|
+
# methods based on the type of node.
|
260
|
+
def compile_node(node)
|
261
|
+
case node
|
262
|
+
when AryPtn
|
263
|
+
compile_aryptn(node)
|
264
|
+
when Binary
|
265
|
+
compile_binary(node)
|
266
|
+
when Const
|
267
|
+
compile_const(node)
|
268
|
+
when ConstPathRef
|
269
|
+
compile_const_path_ref(node)
|
270
|
+
when DynaSymbol
|
271
|
+
compile_dyna_symbol(node)
|
272
|
+
when HshPtn
|
273
|
+
compile_hshptn(node)
|
274
|
+
when RegexpLiteral
|
275
|
+
compile_regexp_literal(node)
|
276
|
+
when StringLiteral
|
277
|
+
compile_string_literal(node)
|
278
|
+
when SymbolLiteral
|
279
|
+
compile_symbol_literal(node)
|
280
|
+
when VarRef
|
281
|
+
compile_var_ref(node)
|
282
|
+
else
|
283
|
+
compile_error(node)
|
284
|
+
end
|
285
|
+
end
|
286
|
+
end
|
287
|
+
end
|
data/lib/syntax_tree/search.rb
CHANGED
@@ -4,14 +4,10 @@ module SyntaxTree
|
|
4
4
|
# Provides an interface for searching for a pattern of nodes against a
|
5
5
|
# subtree of an AST.
|
6
6
|
class Search
|
7
|
-
|
8
|
-
end
|
9
|
-
|
10
|
-
attr_reader :matcher
|
7
|
+
attr_reader :pattern
|
11
8
|
|
12
|
-
def initialize(
|
13
|
-
|
14
|
-
@matcher = compile(root.statements.body.first.consequent.pattern)
|
9
|
+
def initialize(pattern)
|
10
|
+
@pattern = pattern
|
15
11
|
end
|
16
12
|
|
17
13
|
def scan(root)
|
@@ -22,71 +18,9 @@ module SyntaxTree
|
|
22
18
|
node = queue.shift
|
23
19
|
next unless node
|
24
20
|
|
25
|
-
yield node if
|
21
|
+
yield node if pattern.call(node)
|
26
22
|
queue += node.child_nodes
|
27
23
|
end
|
28
24
|
end
|
29
|
-
|
30
|
-
private
|
31
|
-
|
32
|
-
def compile(pattern)
|
33
|
-
case pattern
|
34
|
-
in Binary[left:, operator: :|, right:]
|
35
|
-
compiled_left = compile(left)
|
36
|
-
compiled_right = compile(right)
|
37
|
-
|
38
|
-
->(node) { compiled_left.call(node) || compiled_right.call(node) }
|
39
|
-
in Const[value:] if SyntaxTree.const_defined?(value)
|
40
|
-
clazz = SyntaxTree.const_get(value)
|
41
|
-
|
42
|
-
->(node) { node.is_a?(clazz) }
|
43
|
-
in Const[value:] if Object.const_defined?(value)
|
44
|
-
clazz = Object.const_get(value)
|
45
|
-
|
46
|
-
->(node) { node.is_a?(clazz) }
|
47
|
-
in ConstPathRef[parent: VarRef[value: Const[value: "SyntaxTree"]]]
|
48
|
-
compile(pattern.constant)
|
49
|
-
in HshPtn[constant:, keywords:, keyword_rest: nil]
|
50
|
-
compiled_constant = compile(constant)
|
51
|
-
|
52
|
-
preprocessed_keywords =
|
53
|
-
keywords.to_h do |keyword, value|
|
54
|
-
raise NoMatchingPatternError unless keyword.is_a?(Label)
|
55
|
-
[keyword.value.chomp(":").to_sym, compile(value)]
|
56
|
-
end
|
57
|
-
|
58
|
-
compiled_keywords = ->(node) do
|
59
|
-
deconstructed = node.deconstruct_keys(preprocessed_keywords.keys)
|
60
|
-
preprocessed_keywords.all? do |keyword, matcher|
|
61
|
-
matcher.call(deconstructed[keyword])
|
62
|
-
end
|
63
|
-
end
|
64
|
-
|
65
|
-
->(node) do
|
66
|
-
compiled_constant.call(node) && compiled_keywords.call(node)
|
67
|
-
end
|
68
|
-
in RegexpLiteral[parts: [TStringContent[value:]]]
|
69
|
-
regexp = /#{value}/
|
70
|
-
|
71
|
-
->(attribute) { regexp.match?(attribute) }
|
72
|
-
in StringLiteral[parts: [TStringContent[value:]]]
|
73
|
-
->(attribute) { attribute == value }
|
74
|
-
in VarRef[value: Const => value]
|
75
|
-
compile(value)
|
76
|
-
end
|
77
|
-
rescue NoMatchingPatternError
|
78
|
-
raise UncompilableError, <<~ERROR
|
79
|
-
Syntax Tree was unable to compile the pattern you provided to search
|
80
|
-
into a usable expression. It failed on the node within the pattern
|
81
|
-
matching expression represented by:
|
82
|
-
|
83
|
-
#{PP.pp(pattern, +"").chomp}
|
84
|
-
|
85
|
-
Note that not all syntax supported by Ruby's pattern matching syntax is
|
86
|
-
also supported by Syntax Tree's code search. If you're using some syntax
|
87
|
-
that you believe should be supported, please open an issue on the GitHub
|
88
|
-
repository at https://github.com/ruby-syntax-tree/syntax_tree.
|
89
|
-
ERROR
|
90
|
-
end
|
91
25
|
end
|
92
26
|
end
|
data/lib/syntax_tree/version.rb
CHANGED
data/lib/syntax_tree.rb
CHANGED
@@ -21,6 +21,7 @@ require_relative "syntax_tree/visitor/environment"
|
|
21
21
|
require_relative "syntax_tree/visitor/with_environment"
|
22
22
|
|
23
23
|
require_relative "syntax_tree/parser"
|
24
|
+
require_relative "syntax_tree/pattern"
|
24
25
|
require_relative "syntax_tree/search"
|
25
26
|
|
26
27
|
# Syntax Tree is a suite of tools built on top of the internal CRuby parser. It
|
@@ -74,4 +75,10 @@ module SyntaxTree
|
|
74
75
|
|
75
76
|
File.read(filepath, encoding: encoding)
|
76
77
|
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
|
77
84
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: syntax_tree
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 4.
|
4
|
+
version: 4.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kevin Newton
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-10-
|
11
|
+
date: 2022-10-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: prettier_print
|
@@ -129,6 +129,7 @@ files:
|
|
129
129
|
- lib/syntax_tree/language_server/inlay_hints.rb
|
130
130
|
- lib/syntax_tree/node.rb
|
131
131
|
- lib/syntax_tree/parser.rb
|
132
|
+
- lib/syntax_tree/pattern.rb
|
132
133
|
- lib/syntax_tree/plugin/single_quotes.rb
|
133
134
|
- lib/syntax_tree/plugin/trailing_comma.rb
|
134
135
|
- lib/syntax_tree/rake/check_task.rb
|