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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 61bd75fb26dcf665293adb21c2cf21a5f0f26b56a2077d80ce5ff6806eaf434d
4
- data.tar.gz: 545306a76376f972805159e8eed622e825011b7fa9ef6b93cb9649c6caf9eccf
3
+ metadata.gz: 4ca1eae46ba326b73e129d5b066d144aeac2743947956efc1453a3539c2caac9
4
+ data.tar.gz: 7c833e7b7bf25df7a82653d9dddb30aa172320a625a0655c52a29640c7a2154f
5
5
  SHA512:
6
- metadata.gz: 633701480cbe5c34bfcff52c4dd6c48f10cb22cba377b295ac56f2c288ce6a5dfdde57df5e682a3226f3d49ca7c3dd8d02db5d2d6d37acfc54df5c304be8a16f
7
- data.tar.gz: e4224136ab12c158c45bd6375308e01a4e7075c7da30536f82f77ebecd3d4c426c4798e80fb5eb6fef9a1771036d4ac7e5d709dc49c5b620cbe9a2c3f2dd15a0
6
+ metadata.gz: 1720e9ef9dd52399564607a9eca7794875e41f36f8ee2c4af2d97fb973c38ec0bf3a3cc254d26f3d656e67256543afed7c818e274395af122790ae6730ccab8c
7
+ data.tar.gz: 0f00dbe7739f71bfa1b81ff204fdd4dc505277a65e374810d2f482c6ef3c81078b39f864d55d8deed001a5b7a294491feb0b930948546bcd4194a88058ca9783
@@ -12,10 +12,12 @@ jobs:
12
12
  - '3.0'
13
13
  - '3.1'
14
14
  - head
15
+ - truffleruby-head
15
16
  name: CI
16
17
  runs-on: ubuntu-latest
17
18
  env:
18
19
  CI: true
20
+ TESTOPTS: --verbose
19
21
  steps:
20
22
  - uses: actions/checkout@master
21
23
  - uses: ruby/setup-ruby@v1
data/.gitignore CHANGED
@@ -9,3 +9,4 @@
9
9
  /vendor/
10
10
 
11
11
  test.rb
12
+ query.txt
data/.rubocop.yml CHANGED
@@ -46,6 +46,9 @@ Naming/MethodParameterName:
46
46
  Naming/RescuedExceptionsVariableName:
47
47
  PreferredName: error
48
48
 
49
+ Style/CaseEquality:
50
+ Enabled: false
51
+
49
52
  Style/ExplicitBlockArgument:
50
53
  Enabled: false
51
54
 
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.1.0...HEAD
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
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- syntax_tree (4.1.0)
4
+ syntax_tree (4.3.0)
5
5
  prettier_print (>= 1.0.2)
6
6
 
7
7
  GEM
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
- ```ruby
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.
@@ -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
- @search = SyntaxTree::Search.new(query)
223
- rescue SyntaxTree::Search::UncompilableError => error
224
- warn(error.message)
225
- exit(1)
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
- in Assign | OpAssign
72
+ when Assign, OpAssign
73
73
  parentheses(node.location)
74
- in Binary[operator: operator] if operator != node.operator
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
- in Assign | Binary | IfOp | OpAssign
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
- in { method: "initialize", id: }
86
+ when Request[method: "initialize", id: :any]
43
87
  store.clear
44
- write(id: id, result: { capabilities: capabilities })
45
- in { method: "initialized" }
88
+ write(id: request[:id], result: { capabilities: capabilities })
89
+ when Request[method: "initialized"]
46
90
  # ignored
47
- in { method: "shutdown" } # tolerate missing ID to be a good citizen
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
- in { method: "textDocument/didChange", params: { textDocument: { uri: }, contentChanges: [{ text: }, *] } }
52
- store[uri] = text
53
- in { method: "textDocument/didOpen", params: { textDocument: { uri:, text: } } }
54
- store[uri] = text
55
- in { method: "textDocument/didClose", params: { textDocument: { uri: } } }
56
- store.delete(uri)
57
- in { method: "textDocument/formatting", id:, params: { textDocument: { uri: } } }
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
- in { method: "textDocument/inlayHint", id:, params: { textDocument: { uri: } } }
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
- in { method: "syntaxTree/visualizing", id:, params: { textDocument: { uri: } } }
64
- write(id: id, result: PP.pp(SyntaxTree.parse(store[uri]), +""))
65
- in { method: %r{\$/.+} }
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
@@ -78,7 +78,7 @@ module SyntaxTree
78
78
 
79
79
  arguments << "--ignore-files=#{ignore_files}" if ignore_files != ""
80
80
 
81
- SyntaxTree::CLI.run(arguments + Array(source_files))
81
+ abort if SyntaxTree::CLI.run(arguments + Array(source_files)) != 0
82
82
  end
83
83
  end
84
84
  end
@@ -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
- class UncompilableError < StandardError
8
- end
9
-
10
- attr_reader :matcher
7
+ attr_reader :pattern
11
8
 
12
- def initialize(query)
13
- root = SyntaxTree.parse("case nil\nin #{query}\nend")
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 matcher.call(node)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SyntaxTree
4
- VERSION = "4.1.0"
4
+ VERSION = "4.3.0"
5
5
  end
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.1.0
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-24 00:00:00.000000000 Z
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