node_query 1.2.0 → 1.4.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: 6e8376d63c29cbdec1d39e74083667724b0b84e872d7a26fc281b18061e82f02
4
- data.tar.gz: '078fc70a86d2bfca404c791c6f9466780daca529aca5e984aca4ecdc6fb4c760'
3
+ metadata.gz: 95dcf07d501c704e59bb792454be0bae1ddfafb915c77b6a062c6331d654564b
4
+ data.tar.gz: 7b835b8479ff5239ea77dcb5ed09f4868b1a09cbdf5edc2f4ab9413d352030be
5
5
  SHA512:
6
- metadata.gz: 444e804e949a7ebd40ebc10312de06bd570115995044598fe324210a60e85fd90931393f0eaa6845ec46c916101ab69e23ccd9490ffa40c99626ecc96c0acfb1
7
- data.tar.gz: e0c5ab049ddc1f608b6559b186bd9f43b16b4d6a6b8ed906bcf261cebdb18a482768673727644797dc8f29465d423fc3148c722db001904439d1890cd80541db
6
+ metadata.gz: e2dbea64418363ad059ea2b6e9d93b3c2d010bdba59317327904b6a48411517a511ab6737fe40bdf4847b2833ec4d81eb5d474be7db7d831a6119c89fe904fea
7
+ data.tar.gz: c2cf0bfec6edfa28b644a38957dce1276cc287507c3c3034a39e0ce018f25e420faded6ad0c7c3c3838757df5107082c30d05fcf9f4323c5d5d3f32bc68337f1
data/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 1.4.0 (2022-09-14)
4
+
5
+ * Add options `including_self`, `stop_on_match` and `recursive`
6
+ * Fix regex to match evaluated value
7
+
8
+ ## 1.3.0 (2022-09-13)
9
+
10
+ * Rename `NodeQuery#parse` to `NodeQuery#query_nodes`
11
+ * `NodeQuery#query_ndoes` accepts `including_self` argument
12
+ * `NodeQuery#query_ndoes` supports both nql and rules
13
+ * Add `NodeQuery#match_node?`
14
+ * Add `NdoeRules`
15
+ * Drop `EvaluatedValue`, use `String` instead
16
+ * Write better test cases
17
+
3
18
  ## 1.2.0 (2022-07-01)
4
19
 
5
20
  * Rename `NodeQuery.get_adapter` to `NodeQuery.adapter`
data/Gemfile.lock CHANGED
@@ -1,17 +1,18 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- node_query (1.2.0)
5
- activesupport
4
+ node_query (1.4.0)
5
+ activesupport (< 7.0.0)
6
6
 
7
7
  GEM
8
8
  remote: https://rubygems.org/
9
9
  specs:
10
- activesupport (7.0.3)
10
+ activesupport (6.1.7)
11
11
  concurrent-ruby (~> 1.0, >= 1.0.2)
12
12
  i18n (>= 1.6, < 2)
13
13
  minitest (>= 5.1)
14
14
  tzinfo (~> 2.0)
15
+ zeitwerk (~> 2.3)
15
16
  ast (2.4.2)
16
17
  coderay (1.1.3)
17
18
  concurrent-ruby (1.1.10)
@@ -35,14 +36,14 @@ GEM
35
36
  guard (~> 2.1)
36
37
  guard-compat (~> 1.1)
37
38
  rspec (>= 2.99.0, < 4.0)
38
- i18n (1.10.0)
39
+ i18n (1.12.0)
39
40
  concurrent-ruby (~> 1.0)
40
41
  listen (3.7.1)
41
42
  rb-fsevent (~> 0.10, >= 0.10.3)
42
43
  rb-inotify (~> 0.9, >= 0.9.10)
43
44
  lumberjack (1.2.8)
44
45
  method_source (1.0.0)
45
- minitest (5.16.1)
46
+ minitest (5.16.3)
46
47
  nenv (0.3.0)
47
48
  notiffany (0.1.3)
48
49
  nenv (~> 0.1)
@@ -50,7 +51,7 @@ GEM
50
51
  oedipus_lex (2.6.0)
51
52
  parser (3.1.2.0)
52
53
  ast (~> 2.4.1)
53
- parser_node_ext (0.2.0)
54
+ parser_node_ext (0.4.0)
54
55
  parser
55
56
  pry (0.14.1)
56
57
  coderay (~> 1.1)
@@ -75,8 +76,9 @@ GEM
75
76
  rspec-support (3.11.0)
76
77
  shellany (0.0.1)
77
78
  thor (1.2.1)
78
- tzinfo (2.0.4)
79
+ tzinfo (2.0.5)
79
80
  concurrent-ruby (~> 1.0)
81
+ zeitwerk (2.6.0)
80
82
 
81
83
  PLATFORMS
82
84
  x86_64-darwin-21
data/README.md CHANGED
@@ -1,6 +1,44 @@
1
1
  # NodeQuery
2
2
 
3
- NodeQuery defines an AST node query language, which is a css like syntax for matching nodes.
3
+ NodeQuery defines an AST node query language, which is a css like syntax for matching nodes,
4
+ it supports other ast parser if it implements `NodeQuery::Adapter`.
5
+
6
+ ## Table of Contents
7
+
8
+ - [NodeQuery](#nodequery)
9
+ - [Table of Contents](#table-of-contents)
10
+ - [Installation](#installation)
11
+ - [Usage](#usage)
12
+ - [Node Query Language](#node-query-language)
13
+ - [nql matches node type](#nql-matches-node-type)
14
+ - [nql matches attribute](#nql-matches-attribute)
15
+ - [nql matches nested attribute](#nql-matches-nested-attribute)
16
+ - [nql matches evaluated value](#nql-matches-evaluated-value)
17
+ - [nql matches nested selector](#nql-matches-nested-selector)
18
+ - [nql matches method result](#nql-matches-method-result)
19
+ - [nql matches operators](#nql-matches-operators)
20
+ - [nql matches array node attribute](#nql-matches-array-node-attribute)
21
+ - [nql matches * in attribute key](#nql-matches--in-attribute-key)
22
+ - [nql matches multiple selectors](#nql-matches-multiple-selectors)
23
+ - [Descendant combinator](#descendant-combinator)
24
+ - [Child combinator](#child-combinator)
25
+ - [Adjacent sibling combinator](#adjacent-sibling-combinator)
26
+ - [General sibling combinator](#general-sibling-combinator)
27
+ - [nql matches goto scope](#nql-matches-goto-scope)
28
+ - [nql matches pseudo selector](#nql-matches-pseudo-selector)
29
+ - [nql matches multiple expressions](#nql-matches-multiple-expressions)
30
+ - [Node Rules](#node-rules)
31
+ - [rules matches node type](#rules-matches-node-type)
32
+ - [rules matches attribute](#rules-matches-attribute)
33
+ - [rules matches nested attribute](#rules-matches-nested-attribute)
34
+ - [rules matches evaluated value](#rules-matches-evaluated-value)
35
+ - [rules matches nested selector](#rules-matches-nested-selector)
36
+ - [rules matches method result](#rules-matches-method-result)
37
+ - [rules matches operators](#rules-matches-operators)
38
+ - [rules matches array nodes attribute](#rules-matches-array-nodes-attribute)
39
+ - [Write Adapter](#write-adapter)
40
+ - [Development](#development)
41
+ - [Contributing](#contributing)
4
42
 
5
43
  ## Installation
6
44
 
@@ -20,12 +58,408 @@ Or install it yourself as:
20
58
 
21
59
  ## Usage
22
60
 
23
- It provides only one api:
61
+ It provides two apis: `query_nodes` and `match_node?`
62
+
63
+ ```ruby
64
+ node_query = NodeQuery.new(nqlOrRules: String | Hash) # Initialize NodeQuery
65
+ node_query.query_nodes(node: Node, options = { including_self: true, stop_on_match: false, recursive: true }): Node[] # Get the matching nodes.
66
+ node_query.match_node?(node: Node): boolean # Check if the node matches nql or rules.
67
+ ```
68
+
69
+ Here is an example for parser ast node.
70
+
71
+ ```ruby
72
+ source = `
73
+ class User
74
+ def initialize(id, name)
75
+ @id = id
76
+ @name = name
77
+ end
78
+ end
79
+
80
+ user = User.new(1, "Murphy")
81
+ `
82
+ node = Parser::CurrentRuby.parse(source)
83
+
84
+ # It will get the node of initialize.
85
+ NodeQuery.new('.def[name=initialize]').query_nodes(node)
86
+ NodeQuery.new({ nodeType: 'def', name: 'initialize' }).query_nodes(node)
87
+ ```
88
+
89
+ ## Node Query Language
90
+
91
+ ### nql matches node type
92
+
93
+ ```
94
+ .class
95
+ ```
96
+
97
+ It matches class node
98
+
99
+ ### nql matches attribute
100
+
101
+ ```
102
+ .class[name=User]
103
+ ```
104
+
105
+ It matches class node whose name is User
106
+
107
+ ### nql matches nested attribute
108
+
109
+ ```
110
+ .class[parent_class.name=Base]
111
+ ```
112
+
113
+ It matches class node whose parent class name is Base
114
+
115
+ ### nql matches evaluated value
116
+
117
+ ```
118
+ .ivasgn[left_value="@{{right_value}}"]
119
+ ```
120
+
121
+ It matches ivasgn node whose left value equals '@' plus the evaluated value of right value.
122
+
123
+ ### nql matches nested selector
124
+
125
+ ```
126
+ .def[body.0=.ivasgn]
127
+ ```
128
+
129
+ It matches def node whose first child node is an ivasgn node.
130
+
131
+ ### nql matches method result
132
+
133
+ ```
134
+ .def[arguments.size=2]
135
+ ```
136
+
137
+ It matches def node whose arguments size is 2.
138
+
139
+ ### nql matches operators
140
+
141
+ ```
142
+ .class[name=User]
143
+ ```
144
+
145
+ Value of name is equal to User
146
+
147
+ ```
148
+ .class[name^=User]
149
+ ```
150
+
151
+ Value of name starts with User
152
+
153
+ ```
154
+ .class[name$=User]
155
+ ```
156
+
157
+ Value of name ends with User
158
+
159
+ ```
160
+ .class[name*=User]
161
+ ```
162
+
163
+ Value of name contains User
164
+
165
+ ```
166
+ .def[arguments.size!=2]
167
+ ```
168
+
169
+ Size of arguments is not equal to 2
170
+
171
+ ```
172
+ .def[arguments.size>=2]
173
+ ```
174
+
175
+ Size of arguments is greater than or equal to 2
176
+
177
+ ```
178
+ .def[arguments.size>2]
179
+ ```
180
+
181
+ Size of arguments is greater than 2
182
+
183
+ ```
184
+ .def[arguments.size<=2]
185
+ ```
186
+
187
+ Size of arguments is less than or equal to 2
188
+
189
+ ```
190
+ .def[arguments.size<2]
191
+ ```
192
+
193
+ Size of arguments is less than 2
194
+
195
+ ```
196
+ .class[name IN (User Account)]
197
+ ```
198
+
199
+ Value of name is either User or Account
200
+
201
+ ```
202
+ .class[name NOT IN (User Account)]
203
+ ```
204
+
205
+ Value of name is neither User nor Account
206
+
207
+ ```
208
+ .def[arguments INCLUDES id]
209
+ ```
210
+
211
+ Value of arguments includes id
212
+
213
+ ```
214
+ .class[name=~/User/]
215
+ ```
216
+
217
+ Value of name matches User
218
+
219
+ ```
220
+ .class[name!~/User/]
221
+ ```
222
+
223
+ Value of name does not match User
224
+
225
+ ```
226
+ .class[name IN (/User/ /Account/)]
227
+ ```
228
+
229
+ Value of name matches either /User/ or /Account/
230
+
231
+ ### nql matches array node attribute
232
+
233
+ ```
234
+ .def[arguments=(id name)]
235
+ ```
236
+
237
+ It matches def node whose arguments are id and name.
238
+
239
+ ### nql matches * in attribute key
240
+
241
+ ```
242
+ .def[arguments.*.name IN (id name)]
243
+ ```
244
+
245
+ It matches def node whose arguments are either id or name.
246
+
247
+ ### nql matches multiple selectors
248
+
249
+ #### Descendant combinator
250
+
251
+ ```
252
+ .class .send
253
+ ```
254
+
255
+ It matches send node whose ansestor is class node.
256
+
257
+ #### Child combinator
258
+
259
+ ```
260
+ .def > .send
261
+ ```
262
+
263
+ It matches send node whose parent is def node.
264
+
265
+ #### Adjacent sibling combinator
266
+
267
+ ```
268
+ .send[left_value=@id] + .send
269
+ ```
270
+
271
+ It matches send node only if it is immediately follows the send node whose left value is @id.
272
+
273
+ #### General sibling combinator
274
+
275
+ ```
276
+ .send[left_value=@id] ~ .send
277
+ ```
278
+
279
+ It matches send node only if it is follows the send node whose left value is @id.
280
+
281
+ ### nql matches goto scope
282
+
283
+ ```
284
+ .def body .send
285
+ ```
286
+
287
+ It matches send node who is in the body of def node.
288
+
289
+ ### nql matches pseudo selector
290
+
291
+ ```
292
+ .class:has(.def[name=initialize])
293
+ ```
294
+
295
+ It matches class node who has an initialize def node.
296
+
297
+ ```
298
+ .class:not_has(.def[name=initialize])
299
+ ```
300
+
301
+ It matches class node who does not have an initialize def node.
302
+
303
+ ### nql matches multiple expressions
304
+
305
+ ```
306
+ .ivasgn[left_value=@id], .ivasgn[left_value=@name]
307
+ ```
308
+
309
+ It matches ivasgn node whose left value is either @id or @name.
310
+
311
+ ## Node Rules
312
+
313
+ ### rules matches node type
314
+
315
+ ```
316
+ { nodeType: 'class' }
317
+ ```
318
+
319
+ It matches class node
320
+
321
+ ### rules matches attribute
322
+
323
+ ```
324
+ { nodeType: 'def', name: 'initialize' }
325
+ ```
326
+
327
+ It matches def node whose name is initialize
328
+
329
+ ```
330
+ { nodeType: 'def', arguments: { "0": 1, "1": "Murphy" } }
331
+ ```
332
+
333
+ It matches def node whose arguments are 1 and Murphy.
334
+
335
+ ### rules matches nested attribute
336
+
337
+ ```
338
+ { nodeType: 'class', parent_class: { name: 'Base' } }
339
+ ```
340
+
341
+ It matches class node whose parent class name is Base
342
+
343
+ ### rules matches evaluated value
344
+
345
+ ```
346
+ { nodeType: 'ivasgn', left_value: '@{{right_value}}' }
347
+ ```
348
+
349
+ It matches ivasgn node whose left value equals '@' plus the evaluated value of right value.
350
+
351
+ ### rules matches nested selector
352
+
353
+ ```
354
+ { nodeType: 'def', body: { "0": { nodeType: 'ivasgn' } } }
355
+ ```
356
+
357
+ It matches def node whose first child node is an ivasgn node.
358
+
359
+ ### rules matches method result
360
+
361
+ ```
362
+ { nodeType: 'def', arguments: { size: 2 } }
363
+ ```
364
+
365
+ It matches def node whose arguments size is 2.
366
+
367
+ ### rules matches operators
368
+
369
+ ```
370
+ { nodeType: 'class', name: 'User' }
371
+ ```
372
+
373
+ Value of name is equal to User
374
+
375
+ ```
376
+ { nodeType: 'def', arguments: { size { not: 2 } }
377
+ ```
378
+
379
+ Size of arguments is not equal to 2
380
+
381
+ ```
382
+ { nodeType: 'def', arguments: { size { gte: 2 } }
383
+ ```
384
+
385
+ Size of arguments is greater than or equal to 2
386
+
387
+ ```
388
+ { nodeType: 'def', arguments: { size { gt: 2 } }
389
+ ```
390
+
391
+ Size of arguments is greater than 2
392
+
393
+ ```
394
+ { nodeType: 'def', arguments: { size { lte: 2 } }
395
+ ```
396
+
397
+ Size of arguments is less than or equal to 2
398
+
399
+ ```
400
+ { nodeType: 'def', arguments: { size { lt: 2 } }
401
+ ```
402
+
403
+ Size of arguments is less than 2
404
+
405
+ ```
406
+ { nodeType: 'class', name: { in: ['User', 'Account'] } }
407
+ ```
408
+
409
+ Value of name is either User or Account
410
+
411
+ ```
412
+ { nodeType: 'class', name: { not_in: ['User', 'Account'] } }
413
+ ```
414
+
415
+ Value of name is neither User nor Account
416
+
417
+ ```
418
+ { nodeType: 'def', arguments: { includes: 'id' } }
419
+ ```
420
+
421
+ Value of arguments includes id
422
+
423
+ ```
424
+ { nodeType: 'class', name: /User/ }
425
+ ```
426
+
427
+ Value of name matches User
428
+
429
+ ```
430
+ { nodeType: 'class', name: { not: /User/ } }
431
+ ```
432
+
433
+ Value of name does not match User
434
+
435
+ ```
436
+ { nodeType: 'class', name: { in: [/User/, /Account/] } }
437
+ ```
438
+
439
+ Value of name matches either /User/ or /Account/
440
+
441
+ ### rules matches array nodes attribute
442
+
443
+ ```
444
+ { nodeType: 'def', arguments: ['id', 'name'] }
445
+ ```
446
+
447
+ It matches def node whose arguments are id and name.
448
+
449
+ ## Write Adapter
450
+
451
+ Different parser, like parser, will generate different AST nodes, to make NodeQuery work for them all,
452
+ we define an [Adapter](https://github.com/xinminlabs/node-query-ruby/blob/main/lib/node_query/adapter.rb) interface,
453
+ if you implement the Adapter interface, you can set it as NodeQuery's adapter.
24
454
 
25
455
  ```ruby
26
- NodeQuery.new(nodeQueryString).parse(node)
456
+ NodeQuery.configure(adapter: ParserAdapter.new)
27
457
  ```
28
458
 
459
+ Here is the ParserAdapter implementation:
460
+
461
+ [ParserAdapter](https://github.com/xinminlabs/node-query-ruby/blob/main/lib/node_query/parser_adapter.rb)
462
+
29
463
  ## Development
30
464
 
31
465
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -17,7 +17,7 @@ module NodeQuery::Compiler
17
17
  # @param node [Node] the node
18
18
  # @return [Boolean]
19
19
  def match?(node)
20
- @value.base_node = node if @value.is_a?(EvaluatedValue)
20
+ @value.base_node = node if @value.is_a?(String)
21
21
  node && @value.match?(NodeQuery::Helper.get_target_node(node, @key), @operator)
22
22
  end
23
23
 
@@ -25,7 +25,7 @@ module NodeQuery::Compiler
25
25
  !actual.is_a?(::Array) || actual.size != expected_value.size ||
26
26
  actual.zip(expected_value).any? { |actual_node, expected_node| expected_node.match?(actual_node, '!=') }
27
27
  else
28
- actual_value(node) != expected_value
28
+ !is_equal?(node)
29
29
  end
30
30
  when '=~'
31
31
  actual_value(node) =~ expected_value
@@ -65,16 +65,23 @@ module NodeQuery::Compiler
65
65
  actual.is_a?(::Array) && actual.size == expected_value.size &&
66
66
  actual.zip(expected_value).all? { |actual_node, expected_node| expected_node.match?(actual_node, '==') }
67
67
  else
68
- actual_value(node) == expected_value
68
+ is_equal?(node)
69
69
  end
70
70
  end
71
71
  end
72
72
 
73
+ # Check if the actual value equals the node value.
74
+ # @param node [Node] the node
75
+ # @return [Boolean] true if the actual value equals the node value.
76
+ def is_equal?(node)
77
+ actual_value(node) == expected_value
78
+ end
79
+
73
80
  # Get the actual value from ast node.
74
81
  # @param node [Node] ast node
75
82
  # @return the node value, could be integer, float, string, boolean, nil, range, and etc.
76
83
  def actual_value(node)
77
- if node.is_a?(::Parser::AST::Node)
84
+ if NodeQuery.adapter.is_node?(node)
78
85
  case NodeQuery.adapter.get_node_type(node)
79
86
  when :int, :float, :str, :sym
80
87
  NodeQuery.adapter.get_children(node).last
@@ -11,22 +11,19 @@ module NodeQuery::Compiler
11
11
  @rest = rest
12
12
  end
13
13
 
14
- # Check if the node matches the expression.
15
- # @param node [Node] the node
16
- # @return [Boolean]
17
- def match?(node)
18
- !query_nodes(node).empty?
19
- end
20
-
21
14
  # Query nodes by the selector and the rest expression.
22
15
  # @param node [Node] node to match
16
+ # @param options [Hash] if query the current node
17
+ # @option options [boolean] :including_self if query the current node, default is ture
18
+ # @option options [boolean] :stop_on_match if stop on first match, default is false
19
+ # @option options [boolean] :recursive if stop on first match, default is true
23
20
  # @return [Array<Node>] matching nodes.
24
- def query_nodes(node)
25
- matching_nodes = @selector.query_nodes(node)
21
+ def query_nodes(node, options = {})
22
+ matching_nodes = @selector.query_nodes(node, options)
26
23
  return matching_nodes if @rest.nil?
27
24
 
28
25
  matching_nodes.flat_map do |matching_node|
29
- @rest.query_nodes(matching_node)
26
+ @rest.query_nodes(matching_node, options)
30
27
  end
31
28
  end
32
29
 
@@ -13,12 +13,23 @@ module NodeQuery::Compiler
13
13
 
14
14
  # Query nodes by the current and the rest expression.
15
15
  # @param node [Node] node to match
16
+ # @param options [Hash] if query the current node
17
+ # @option options [boolean] :including_self if query the current node, default is ture
18
+ # @option options [boolean] :stop_on_match if stop on first match, default is false
19
+ # @option options [boolean] :recursive if stop on first match, default is true
16
20
  # @return [Array<Node>] matching nodes.
17
- def query_nodes(node)
18
- matching_nodes = @expression.query_nodes(node)
21
+ def query_nodes(node, options = {})
22
+ matching_nodes = @expression.query_nodes(node, options)
19
23
  return matching_nodes if @rest.nil?
20
24
 
21
- matching_nodes + @rest.query_nodes(node)
25
+ matching_nodes + @rest.query_nodes(node, options)
26
+ end
27
+
28
+ # Check if the node matches the expression list.
29
+ # @param node [Node] the node
30
+ # @return [Boolean]
31
+ def match_node?(node)
32
+ !query_nodes(node).empty?
22
33
  end
23
34
 
24
35
  def to_s
@@ -19,7 +19,7 @@ module NodeQuery::Compiler
19
19
  # if the node is an Array, return the array of each element's actual value,
20
20
  # otherwise, return the String value.
21
21
  def actual_value(node)
22
- if node.is_a?(::Parser::AST::Node)
22
+ if NodeQuery.adapter.is_node?(node)
23
23
  NodeQuery.adapter.get_source(node)
24
24
  elsif node.is_a?(::Array)
25
25
  node.map { |n| actual_value(n) }
@@ -13,16 +13,13 @@ module NodeQuery::Compiler
13
13
 
14
14
  # Check if the regexp value matches the node value.
15
15
  # @param node [Node] the node
16
- # @param operator [String] the operator
17
- # @return [Boolean] true if the regexp value matches the node value, otherwise, false.
18
- def match?(node, operator = '=~')
19
- match =
20
- if node.is_a?(::Parser::AST::Node)
21
- @value.match(NodeQuery.adapter.get_source(node))
22
- else
23
- @value.match(node.to_s)
24
- end
25
- operator == '=~' ? match : !match
16
+ # @return [Boolean] true if the regexp value matches the node value.
17
+ def is_equal?(node)
18
+ if NodeQuery.adapter.is_node?(node)
19
+ @value.match(NodeQuery.adapter.get_source(node))
20
+ else
21
+ @value.match(node.to_s)
22
+ end
26
23
  end
27
24
 
28
25
  # Get valid operators.