node_query 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -23,7 +23,7 @@ module NodeQuery::Compiler
23
23
  # Check if node matches the selector.
24
24
  # @param node [Parser::AST::Node] the node
25
25
  def match?(node)
26
- node.is_a?(::Parser::AST::Node) && (!@basic_selector || @basic_selector.match?(node)) && match_pseudo_class?(node)
26
+ NodeQuery.adapter.is_node?(node) && (!@basic_selector || @basic_selector.match?(node)) && match_pseudo_class?(node)
27
27
  end
28
28
 
29
29
  # Query nodes by the selector.
@@ -33,8 +33,13 @@ module NodeQuery::Compiler
33
33
  # * If relationship is next sibling, it try to match next sibling node.
34
34
  # * If relationship is subsequent sibling, it will match in all sibling nodes.
35
35
  # @param node [Node] node to match
36
+ # @param options [Hash] if query the current node
37
+ # @option options [boolean] :including_self if query the current node, default is ture
38
+ # @option options [boolean] :stop_on_match if stop on first match, default is false
39
+ # @option options [boolean] :recursive if stop on first match, default is true
36
40
  # @return [Array<Node>] matching nodes.
37
- def query_nodes(node)
41
+ def query_nodes(node, options = {})
42
+ options = { including_self: true, stop_on_match: false, recursive: true }.merge(options)
38
43
  return find_nodes_by_relationship(node) if @relationship
39
44
 
40
45
  if node.is_a?(::Array)
@@ -44,10 +49,25 @@ module NodeQuery::Compiler
44
49
  return find_nodes_by_goto_scope(node) if @goto_scope
45
50
 
46
51
  nodes = []
47
- nodes << node if match?(node)
52
+ if options[:including_self] && match?(node)
53
+ nodes << node
54
+ return matching_nodes if options[:stop_on_match]
55
+ end
48
56
  if @basic_selector
49
- NodeQuery::Helper.handle_recursive_child(node) do |child_node|
50
- nodes << child_node if match?(child_node)
57
+ if options[:recursive]
58
+ NodeQuery::Helper.handle_recursive_child(node) do |child_node|
59
+ if match?(child_node)
60
+ nodes << child_node
61
+ break if options[:stop_on_match]
62
+ end
63
+ end
64
+ else
65
+ NodeQuery.adapter.get_children(node).each do |child_node|
66
+ if match?(child_node)
67
+ nodes << child_node
68
+ break if options[:stop_on_match]
69
+ end
70
+ end
51
71
  end
52
72
  end
53
73
  nodes
@@ -86,7 +106,13 @@ module NodeQuery::Compiler
86
106
  end
87
107
  else
88
108
  node.children.each do |child_node|
89
- nodes << child_node if @rest.match?(child_node)
109
+ if NodeQuery.adapter.is_node?(child_node) && :begin == NodeQuery.adapter.get_node_type(child_node)
110
+ child_node.children.each do |child_child_node|
111
+ nodes << child_child_node if @rest.match?(child_child_node)
112
+ end
113
+ elsif @rest.match?(child_node)
114
+ nodes << child_node
115
+ end
90
116
  end
91
117
  end
92
118
  when '+'
@@ -5,12 +5,31 @@ module NodeQuery::Compiler
5
5
  class String
6
6
  include Comparable
7
7
 
8
+ attr_accessor :base_node
9
+
8
10
  # Initialize a String.
9
11
  # @param value [String] the string value
10
12
  def initialize(value:)
11
13
  @value = value
12
14
  end
13
15
 
16
+ # Get the expected value.
17
+ # @example
18
+ # if the source code of the node is @id = id,
19
+ # and the @value is "@{{right_vaue}}",
20
+ # then it returns "@id".
21
+ # @return [String] the expected string, if it contains evaluated value, evaluate the node value.
22
+ def expected_value
23
+ NodeQuery::Helper.evaluate_node_value(base_node, @value)
24
+ end
25
+
26
+ # Check if the actual value equals the node value.
27
+ # @param node [Node] the node
28
+ # @return [Boolean] true if the actual value equals the node value.
29
+ def is_equal?(node)
30
+ actual_value(node).to_s == expected_value
31
+ end
32
+
14
33
  # Get valid operators.
15
34
  # @return [Array] valid operators
16
35
  def valid_operators
@@ -15,7 +15,6 @@ module NodeQuery::Compiler
15
15
 
16
16
  autoload :ArrayValue, 'node_query/compiler/array_value'
17
17
  autoload :Boolean, 'node_query/compiler/boolean'
18
- autoload :EvaluatedValue, 'node_query/compiler/evaluated_value'
19
18
  autoload :Float, 'node_query/compiler/float'
20
19
  autoload :Identifier, 'node_query/compiler/identifier'
21
20
  autoload :Integer, 'node_query/compiler/integer'
@@ -1,40 +1,58 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class NodeQuery::Helper
4
- # Get target node by the keys.
5
- # @param node [Node] ast node
6
- # @param keys [String] keys of child node.
7
- # @return [Node|] the target node.
8
- def self.get_target_node(node, keys)
9
- return unless node
4
+ class <<self
5
+ # Get target node by the keys.
6
+ # @param node [Node] ast node
7
+ # @param keys [String|Array] keys of child node.
8
+ # @return [Node|] the target node.
9
+ def get_target_node(node, keys)
10
+ return unless node
10
11
 
11
- first_key, rest_keys = keys.to_s.split('.', 2)
12
- if node.is_a?(Array) && first_key === "*"
13
- return node.map { |child_node| get_target_node(child_node, rest_keys) }
14
- end
12
+ first_key, rest_keys = keys.to_s.split('.', 2)
13
+ if node.is_a?(Array) && first_key === "*"
14
+ return node.map { |child_node| get_target_node(child_node, rest_keys) }
15
+ end
15
16
 
16
- if node.is_a?(Array) && first_key =~ /\d+/
17
- child_node = node[first_key.to_i]
18
- end
19
- if node.respond_to?(first_key)
20
- child_node = node.send(first_key)
21
- end
17
+ if node.is_a?(Array) && first_key =~ /\d+/
18
+ child_node = node[first_key.to_i]
19
+ elsif node.respond_to?(first_key)
20
+ child_node = node.send(first_key)
21
+ elsif first_key == "nodeType"
22
+ child_node = NodeQuery.adapter.get_node_type(node)
23
+ end
22
24
 
23
- return child_node unless rest_keys
25
+ return child_node unless rest_keys
24
26
 
25
- return get_target_node(child_node, rest_keys)
26
- end
27
+ return get_target_node(child_node, rest_keys)
28
+ end
29
+
30
+ # Recursively handle child nodes.
31
+ # @param node [Node] ast node
32
+ # @yield [child] Gives a child node.
33
+ # @yieldparam child [Parser::AST::Node] child node
34
+ def handle_recursive_child(node, &block)
35
+ NodeQuery.adapter.get_children(node).each do |child|
36
+ if NodeQuery.adapter.is_node?(child)
37
+ block.call(child)
38
+ handle_recursive_child(child, &block)
39
+ end
40
+ end
41
+ end
27
42
 
28
- # Recursively handle child nodes.
29
- # @param node [Node] ast node
30
- # @yield [child] Gives a child node.
31
- # @yieldparam child [Parser::AST::Node] child node
32
- def self.handle_recursive_child(node, &block)
33
- NodeQuery.adapter.get_children(node).each do |child|
34
- if NodeQuery.adapter.is_node?(child)
35
- block.call(child)
36
- handle_recursive_child(child, &block)
43
+ # Evaluate node value.
44
+ # @example
45
+ # source code of the node is @id = id
46
+ # evaluated_node_value(node, "@{{right_value}}") # => @id
47
+ # @param node [Node] ast node
48
+ # @param str [String] string to be evaluated
49
+ # @return [String] evaluated string
50
+ def evaluate_node_value(node, str)
51
+ str.scan(/{{(.*?)}}/).each do |match_data|
52
+ target_node = NodeQuery::Helper.get_target_node(node, match_data.first)
53
+ str = str.sub("{{#{match_data.first}}}", NodeQuery.adapter.get_source(target_node))
37
54
  end
55
+ str
38
56
  end
39
57
  end
40
- end
58
+ end
@@ -0,0 +1,189 @@
1
+ # frozen_string_literal: true
2
+
3
+ class NodeQuery::NodeRules
4
+ KEYWORDS = %i[any includes not in not_in gt gte lt lte]
5
+
6
+ # Initialize a NodeRules.
7
+ # @param rules [Hash] the nod rules
8
+ def initialize(rules)
9
+ @rules = rules
10
+ end
11
+
12
+ # Query nodes by the rules.
13
+ # @param node [Node] node to query
14
+ # @param options [Hash] if query the current node
15
+ # @option options [boolean] :including_self if query the current node, default is ture
16
+ # @option options [boolean] :stop_on_match if stop on first match, default is false
17
+ # @option options [boolean] :recursive if stop on first match, default is true
18
+ # @return [Array<Node>] matching nodes.
19
+ def query_nodes(node, options = {})
20
+ options = { including_self: true, stop_on_match: false, recursive: true }.merge(options)
21
+ matching_nodes = []
22
+ if options[:including_self] && match_node?(node)
23
+ matching_nodes.push(node)
24
+ return matching_nodes if options[:stop_on_match]
25
+ end
26
+ if options[:recursive]
27
+ NodeQuery::Helper.handle_recursive_child(node) do |child_node|
28
+ if match_node?(child_node)
29
+ matching_nodes.push(child_node)
30
+ break if options[:stop_on_match]
31
+ end
32
+ end
33
+ else
34
+ NodeQuery.adapter.get_children(node).each do |child_node|
35
+ if match_node?(child_node)
36
+ matching_nodes.push(child_node)
37
+ break if options[:stop_on_match]
38
+ end
39
+ end
40
+ end
41
+ matching_nodes
42
+ end
43
+
44
+ # Check if the node matches the rules.
45
+ # @param node [Node] the node
46
+ # @return [Boolean]
47
+ def match_node?(node)
48
+ flat_hash(@rules).keys.all? do |multi_keys|
49
+ last_key = multi_keys.last
50
+ actual = KEYWORDS.include?(last_key) ?
51
+ NodeQuery::Helper.get_target_node(node, multi_keys[0...-1].join('.')) :
52
+ NodeQuery::Helper.get_target_node(node, multi_keys.join('.'))
53
+ expected = expected_value(@rules, multi_keys)
54
+ expected = NodeQuery::Helper.evaluate_node_value(node, expected) if expected.is_a?(String)
55
+ case last_key
56
+ when :any, :includes
57
+ actual.any? { |actual_value| match_value?(actual_value, expected) }
58
+ when :not
59
+ !match_value?(actual, expected)
60
+ when :in
61
+ expected.any? { |expected_value| match_value?(actual, expected_value) }
62
+ when :not_in
63
+ expected.all? { |expected_value| !match_value?(actual, expected_value) }
64
+ when :gt
65
+ actual > expected
66
+ when :gte
67
+ actual >= expected
68
+ when :lt
69
+ actual < expected
70
+ when :lte
71
+ actual <= expected
72
+ else
73
+ match_value?(actual, expected)
74
+ end
75
+ end
76
+ end
77
+
78
+ private
79
+
80
+ # Compare actual value with expected value.
81
+ #
82
+ # @param actual [Object] actual value.
83
+ # @param expected [Object] expected value.
84
+ # @return [Boolean]
85
+ # @raise [Synvert::Core::MethodNotSupported] if expected class is not supported.
86
+ def match_value?(actual, expected)
87
+ return true if actual == expected
88
+
89
+ case expected
90
+ when Symbol
91
+ if actual.is_a?(Parser::AST::Node)
92
+ actual_source = NodeQuery.adapter.get_source(actual)
93
+ actual_source == ":#{expected}" || actual_source == expected.to_s
94
+ else
95
+ actual.to_sym == expected
96
+ end
97
+ when String
98
+ if actual.is_a?(Parser::AST::Node)
99
+ actual_source = NodeQuery.adapter.get_source(actual)
100
+ actual_source == expected || actual_source == unwrap_quote(expected) ||
101
+ unwrap_quote(actual_source) == expected || unwrap_quote(actual_source) == unwrap_quote(expected)
102
+ else
103
+ actual.to_s == expected || wrap_quote(actual.to_s) == expected
104
+ end
105
+ when Regexp
106
+ if actual.is_a?(Parser::AST::Node)
107
+ actual.to_source =~ Regexp.new(expected.to_s, Regexp::MULTILINE)
108
+ else
109
+ actual.to_s =~ Regexp.new(expected.to_s, Regexp::MULTILINE)
110
+ end
111
+ when Array
112
+ return false unless expected.length == actual.length
113
+
114
+ actual.zip(expected).all? { |a, e| match_value?(a, e) }
115
+ when NilClass
116
+ if actual.is_a?(Parser::AST::Node)
117
+ :nil == actual.type
118
+ else
119
+ actual.nil?
120
+ end
121
+ when Numeric
122
+ if actual.is_a?(Parser::AST::Node)
123
+ actual.children[0] == expected
124
+ else
125
+ actual == expected
126
+ end
127
+ when TrueClass
128
+ :true == actual.type
129
+ when FalseClass
130
+ :false == actual.type
131
+ when Parser::AST::Node
132
+ actual == expected
133
+ when Synvert::Core::Rewriter::AnyValue
134
+ !actual.nil?
135
+ else
136
+ raise Synvert::Core::MethodNotSupported, "#{expected.class} is not handled for match_value?"
137
+ end
138
+ end
139
+
140
+ # Convert a hash to flat one.
141
+ #
142
+ # @example
143
+ # flat_hash(type: 'block', caller: {type: 'send', receiver: 'RSpec'})
144
+ # # {[:type] => 'block', [:caller, :type] => 'send', [:caller, :receiver] => 'RSpec'}
145
+ # @param h [Hash] original hash.
146
+ # @return flatten hash.
147
+ def flat_hash(h, k = [])
148
+ new_hash = {}
149
+ h.each_pair do |key, val|
150
+ if val.is_a?(Hash)
151
+ new_hash.merge!(flat_hash(val, k + [key]))
152
+ else
153
+ new_hash[k + [key]] = val
154
+ end
155
+ end
156
+ new_hash
157
+ end
158
+
159
+ # Get expected value from rules.
160
+ #
161
+ # @param rules [Hash]
162
+ # @param multi_keys [Array<Symbol>]
163
+ # @return [Object] expected value.
164
+ def expected_value(rules, multi_keys)
165
+ multi_keys.inject(rules) { |o, key| o[key] }
166
+ end
167
+
168
+ # Wrap the string with single or double quote.
169
+ # @param string [String]
170
+ # @return [String]
171
+ def wrap_quote(string)
172
+ if string.include?("'")
173
+ "\"#{string}\""
174
+ else
175
+ "'#{string}'"
176
+ end
177
+ end
178
+
179
+ # Unwrap the quote from the string.
180
+ # @param string [String]
181
+ # @return [String]
182
+ def unwrap_quote(string)
183
+ if (string[0] == '"' && string[-1] == '"') || (string[0] == "'" && string[-1] == "'")
184
+ string[1...-1]
185
+ else
186
+ string
187
+ end
188
+ end
189
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class NodeQuery
4
- VERSION = "1.2.0"
4
+ VERSION = "1.4.0"
5
5
  end
data/lib/node_query.rb CHANGED
@@ -11,6 +11,7 @@ class NodeQuery
11
11
  autoload :ParserAdapter, "node_query/parser_adapter"
12
12
  autoload :Compiler, "node_query/compiler"
13
13
  autoload :Helper, "node_query/helper"
14
+ autoload :NodeRules, "node_query/node_rules"
14
15
 
15
16
  # Configure NodeQuery
16
17
  # @param [Hash] options options to configure
@@ -26,15 +27,42 @@ class NodeQuery
26
27
  end
27
28
 
28
29
  # Initialize a NodeQuery.
29
- # @param nql [String] node query language
30
- def initialize(nql)
31
- @expression = NodeQueryParser.new.parse(nql)
30
+ # @param nqlOrRules [String | Hash] node query language or node rules
31
+ def initialize(nqlOrRules)
32
+ if nqlOrRules.is_a?(String)
33
+ @expression = NodeQueryParser.new.parse(nqlOrRules)
34
+ else
35
+ @rules = NodeRules.new(nqlOrRules)
36
+ end
32
37
  end
33
38
 
34
- # Parse ast node.
39
+ # Query matching nodes.
35
40
  # @param node [Node] ast node
41
+ # @param options [Hash] if query the current node
42
+ # @option options [boolean] :including_self if query the current node, default is ture
43
+ # @option options [boolean] :stop_on_match if stop on first match, default is false
44
+ # @option options [boolean] :recursive if stop on first match, default is true
36
45
  # @return [Array<Node>] matching child nodes
37
- def parse(node)
38
- @expression.query_nodes(node)
46
+ def query_nodes(node, options = {})
47
+ if @expression
48
+ @expression.query_nodes(node, options)
49
+ elsif @rules
50
+ @rules.query_nodes(node, options)
51
+ else
52
+ []
53
+ end
54
+ end
55
+
56
+ # Check if the node matches the nql or rules.
57
+ # @param node [Node] the node
58
+ # @return [Boolean]
59
+ def match_node?(node)
60
+ if @expression
61
+ @expression.match_node?(node)
62
+ elsif @rules
63
+ @rules.match_node?(node)
64
+ else
65
+ false
66
+ end
39
67
  end
40
68
  end
@@ -7,11 +7,9 @@ macros
7
7
  CLOSE_ARRAY /\)/
8
8
  OPEN_SELECTOR /\(/
9
9
  CLOSE_SELECTOR /\)/
10
- OPEN_DYNAMIC_ATTRIBUTE /{{/
11
- CLOSE_DYNAMIC_ATTRIBUTE /}}/
12
10
  NODE_TYPE /\.[a-z]+/
13
- IDENTIFIER /[\*\.\w]*\w/
14
- IDENTIFIER_VALUE /[\.\w!&:\?<>=]+/
11
+ IDENTIFIER /[@\*\.\w]*\w/
12
+ IDENTIFIER_VALUE /[@\.\w!&:\?<>=]+/
15
13
  FALSE /false/
16
14
  FLOAT /\d+\.\d+/
17
15
  INTEGER /\d+/
@@ -59,7 +57,6 @@ rules
59
57
  :VALUE /\[\]/ { [:tIDENTIFIER_VALUE, text] }
60
58
  :VALUE /:\[\]=/ { [:tSYMBOL, text[1..-1].to_sym] }
61
59
  :VALUE /:\[\]/ { [:tSYMBOL, text[1..-1].to_sym] }
62
- :VALUE /{{#{IDENTIFIER}}}/ { [:tDYNAMIC_ATTRIBUTE, text[2..-3]] }
63
60
  :VALUE /#{OPEN_ARRAY}/ { @state = :ARRAY_VALUE; [:tOPEN_ARRAY, text] }
64
61
  :VALUE /#{CLOSE_ATTRIBUTE}/ { @nested_count -= 1; @state = @nested_count == 0 ? nil : :VALUE; [:tCLOSE_ATTRIBUTE, text] }
65
62
  :VALUE /#{NIL}\?/ { [:tIDENTIFIER_VALUE, text] }
@@ -14,27 +14,25 @@ class NodeQueryLexer
14
14
  require 'strscan'
15
15
 
16
16
  # :stopdoc:
17
- OPEN_ATTRIBUTE = /\[/
18
- CLOSE_ATTRIBUTE = /\]/
19
- OPEN_ARRAY = /\(/
20
- CLOSE_ARRAY = /\)/
21
- OPEN_SELECTOR = /\(/
22
- CLOSE_SELECTOR = /\)/
23
- OPEN_DYNAMIC_ATTRIBUTE = /{{/
24
- CLOSE_DYNAMIC_ATTRIBUTE = /}}/
25
- NODE_TYPE = /\.[a-z]+/
26
- IDENTIFIER = /[\*\.\w]*\w/
27
- IDENTIFIER_VALUE = /[\.\w!&:\?<>=]+/
28
- FALSE = /false/
29
- FLOAT = /\d+\.\d+/
30
- INTEGER = /\d+/
31
- NIL = /nil/
32
- REGEXP_BODY = /(?:[^\/]|\\\/)*/
33
- REGEXP = /\/(#{REGEXP_BODY})(?<!\\)\/([imxo]*)/
34
- SYMBOL = /:[\w!\?<>=]+/
35
- TRUE = /true/
36
- SINGLE_QUOTE_STRING = /'.*?'/
37
- DOUBLE_QUOTE_STRING = /".*?"/
17
+ OPEN_ATTRIBUTE = /\[/
18
+ CLOSE_ATTRIBUTE = /\]/
19
+ OPEN_ARRAY = /\(/
20
+ CLOSE_ARRAY = /\)/
21
+ OPEN_SELECTOR = /\(/
22
+ CLOSE_SELECTOR = /\)/
23
+ NODE_TYPE = /\.[a-z]+/
24
+ IDENTIFIER = /[@\*\.\w]*\w/
25
+ IDENTIFIER_VALUE = /[@\.\w!&:\?<>=]+/
26
+ FALSE = /false/
27
+ FLOAT = /\d+\.\d+/
28
+ INTEGER = /\d+/
29
+ NIL = /nil/
30
+ REGEXP_BODY = /(?:[^\/]|\\\/)*/
31
+ REGEXP = /\/(#{REGEXP_BODY})(?<!\\)\/([imxo]*)/
32
+ SYMBOL = /:[\w!\?<>=]+/
33
+ TRUE = /true/
34
+ SINGLE_QUOTE_STRING = /'.*?'/
35
+ DOUBLE_QUOTE_STRING = /".*?"/
38
36
  # :startdoc:
39
37
  # :stopdoc:
40
38
  class LexerError < StandardError ; end
@@ -201,8 +199,6 @@ class NodeQueryLexer
201
199
  action { [:tSYMBOL, text[1..-1].to_sym] }
202
200
  when text = ss.scan(/:\[\]/) then
203
201
  action { [:tSYMBOL, text[1..-1].to_sym] }
204
- when text = ss.scan(/{{#{IDENTIFIER}}}/) then
205
- action { [:tDYNAMIC_ATTRIBUTE, text[2..-3]] }
206
202
  when text = ss.scan(/#{OPEN_ARRAY}/) then
207
203
  action { @state = :ARRAY_VALUE; [:tOPEN_ARRAY, text] }
208
204
  when text = ss.scan(/#{CLOSE_ATTRIBUTE}/) then