synvert-core 0.64.0 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/main.yml +1 -1
  3. data/.gitignore +4 -0
  4. data/CHANGELOG.md +5 -0
  5. data/Guardfile +11 -2
  6. data/README.md +2 -2
  7. data/Rakefile +15 -1
  8. data/lib/synvert/core/array_ext.rb +41 -0
  9. data/lib/synvert/core/engine/erb.rb +9 -8
  10. data/lib/synvert/core/node_ext.rb +47 -44
  11. data/lib/synvert/core/node_query/compiler/array.rb +34 -0
  12. data/lib/synvert/core/node_query/compiler/attribute.rb +51 -0
  13. data/lib/synvert/core/node_query/compiler/attribute_list.rb +24 -0
  14. data/lib/synvert/core/node_query/compiler/boolean.rb +23 -0
  15. data/lib/synvert/core/node_query/compiler/comparable.rb +79 -0
  16. data/lib/synvert/core/node_query/compiler/dynamic_attribute.rb +51 -0
  17. data/lib/synvert/core/node_query/compiler/expression.rb +88 -0
  18. data/lib/synvert/core/node_query/compiler/float.rb +23 -0
  19. data/lib/synvert/core/node_query/compiler/identifier.rb +41 -0
  20. data/lib/synvert/core/node_query/compiler/integer.rb +23 -0
  21. data/lib/synvert/core/node_query/compiler/invalid_operator_error.rb +7 -0
  22. data/lib/synvert/core/node_query/compiler/nil.rb +23 -0
  23. data/lib/synvert/core/node_query/compiler/parse_error.rb +7 -0
  24. data/lib/synvert/core/node_query/compiler/regexp.rb +37 -0
  25. data/lib/synvert/core/node_query/compiler/selector.rb +51 -0
  26. data/lib/synvert/core/node_query/compiler/string.rb +34 -0
  27. data/lib/synvert/core/node_query/compiler/symbol.rb +23 -0
  28. data/lib/synvert/core/node_query/compiler.rb +24 -0
  29. data/lib/synvert/core/node_query/lexer.rex +96 -0
  30. data/lib/synvert/core/node_query/lexer.rex.rb +293 -0
  31. data/lib/synvert/core/node_query/parser.racc.rb +518 -0
  32. data/lib/synvert/core/node_query/parser.y +84 -0
  33. data/lib/synvert/core/node_query.rb +36 -0
  34. data/lib/synvert/core/rewriter/action/delete_action.rb +4 -2
  35. data/lib/synvert/core/rewriter/action/replace_action.rb +4 -2
  36. data/lib/synvert/core/rewriter/action/replace_erb_stmt_with_expr_action.rb +2 -2
  37. data/lib/synvert/core/rewriter/action/wrap_action.rb +3 -2
  38. data/lib/synvert/core/rewriter/action.rb +2 -1
  39. data/lib/synvert/core/rewriter/helper.rb +5 -2
  40. data/lib/synvert/core/rewriter/instance.rb +25 -13
  41. data/lib/synvert/core/rewriter/scope/query_scope.rb +36 -0
  42. data/lib/synvert/core/rewriter/scope/within_scope.rb +1 -1
  43. data/lib/synvert/core/rewriter.rb +1 -0
  44. data/lib/synvert/core/version.rb +1 -1
  45. data/lib/synvert/core.rb +22 -5
  46. data/spec/synvert/core/engine/erb_spec.rb +2 -2
  47. data/spec/synvert/core/node_ext_spec.rb +36 -5
  48. data/spec/synvert/core/node_query/lexer_spec.rb +512 -0
  49. data/spec/synvert/core/node_query/parser_spec.rb +270 -0
  50. data/spec/synvert/core/rewriter/condition/if_only_exist_condition_spec.rb +1 -6
  51. data/spec/synvert/core/rewriter/helper_spec.rb +4 -1
  52. data/spec/synvert/core/rewriter/instance_spec.rb +24 -3
  53. data/spec/synvert/core/rewriter/scope/query_scope_spec.rb +74 -0
  54. data/spec/synvert/core/rewriter/scope/within_scope_spec.rb +12 -9
  55. data/spec/synvert/core/rewriter_spec.rb +4 -2
  56. data/synvert-core-ruby.gemspec +5 -1
  57. metadata +75 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ea27da2bceb8ec0f2dd837e5aa80135860a2539c7e49170b2bd46af227d1043e
4
- data.tar.gz: a901be4a294000eecfbbbf407416015cd9b38499ebc24c744147aebdeba5388f
3
+ metadata.gz: f889cb977b7bf6e847bdeeac011ee0d23210ae8db7a0652e8c00d5d98c72a701
4
+ data.tar.gz: 46d220401cc8c9c0422428c2915862148b4ad8dc236f637ed52dbc45207de742
5
5
  SHA512:
6
- metadata.gz: b9d0451d59718fe04e23d48a90e7e705fb455592d2636055064d9c5d7f6a1db20980ff1a38b147d77b16fdc8b059b95a8a95848c183a4115ddb1730541bf0757
7
- data.tar.gz: 63246572cc0d9c3d44d7bb12f6d716e11b15fd1978376588e235a4c2f9b5e7849962ed4a81e38bd1a9f74c2cb877325148c486c9f3045ebf8c0aa80459d9b6aa
6
+ metadata.gz: 926df3dd8aa3689938e90c530fb432e0703cff2f89f35f46950b3630eb98b8da6de0e0d5eb75c5c24262f3712400d4570ac05abd5ebbbe6c6954d6c997048432
7
+ data.tar.gz: 321fb626763ca31e6156c462bb6846ce73d00639a620b0b7048b024fbe50d14f63f6d3209e3a534b0475c29c29e74dcf0a7c204f14ad0927244bd5cbb157b3d6
@@ -19,7 +19,7 @@ jobs:
19
19
  runs-on: ubuntu-latest
20
20
  strategy:
21
21
  matrix:
22
- ruby-version: ['2.5', '2.6', '2.7', '3.0', '3.1']
22
+ ruby-version: ['2.6', '2.7', '3.0', '3.1']
23
23
 
24
24
  steps:
25
25
  - uses: actions/checkout@v2
data/.gitignore CHANGED
@@ -15,3 +15,7 @@ spec/reports
15
15
  test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
+
19
+ lib/synvert/core/node_query/lexer.rex.rb
20
+ lib/synvert/core/node_query/parser.racc.rb
21
+ lib/synvert/core/node_query/parser.output
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 1.0.0 (2022-04-25)
4
+
5
+ * Introduce new node query language
6
+ * Drop ruby 2.5 support
7
+
3
8
  ## 0.64.0 (2022-04-02)
4
9
 
5
10
  * Read absolute path of Gemfile.lock
data/Guardfile CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  guard :rspec, cmd: 'bundle exec rspec' do
4
4
  watch(%r{^spec/.+_spec\.rb$})
5
- watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
6
- watch('spec/spec_helper.rb') { "spec" }
5
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
6
+ watch('lib/synvert/core/node_query/lexer.rex.rb') { 'spec/synvert/core/node_query/lexer_spec.rb' }
7
+ watch('lib/synvert/core/node_query/compiler.rb') { 'spec/synvert/core/node_query/parser_spec.rb' }
8
+ watch(%r{^lib/synvert/core/node_query/compiler/.*\.rb$}) { 'spec/synvert/core/node_query/parser_spec.rb' }
9
+ watch('lib/synvert/core/node_query/parser.racc.rb') { 'spec/synvert/core/node_query/parser_spec.rb' }
10
+ watch('spec/spec_helper.rb') { "spec" }
11
+ end
12
+
13
+ guard :rake, task: 'generate' do
14
+ watch('lib/synvert/core/node_query/lexer.rex')
15
+ watch('lib/synvert/core/node_query/parser.y')
7
16
  end
data/README.md CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  <img src="https://synvert.xinminlabs.com/img/logo_96.png" alt="logo" width="32" height="32" />
4
4
 
5
+ [![AwesomeCode Status for xinminlabs/synvert-core-ruby](https://awesomecode.io/projects/033f7f02-7b22-41c3-a902-fca37f1ec72a/status)](https://awesomecode.io/repos/xinminlabs/synvert-core-ruby)
5
6
  ![Main workflow](https://github.com/xinminlabs/synvert-core-ruby/actions/workflows/main.yml/badge.svg)
6
- [![Gem Version](https://badge.fury.io/rb/synvert-core.png)](http://badge.fury.io/rb/synvert-core)
7
7
 
8
8
  Synvert core provides a set of DSLs to rewrite ruby code. e.g.
9
9
 
@@ -77,4 +77,4 @@ Actions:
77
77
  * [wrap](./Synvert/Core/Rewriter/Instance.html#wrap-instance_method) - wrap the current node with code
78
78
  * [replace_with](./Synvert/Core/Rewriter/Instance.html#replace_with-instance_method) - replace the whole code of current node
79
79
  * [warn](./Synvert/Core/Rewriter/Instance.html#warn-instance_method) - warn message
80
- * [replace_erb_stmt_with_expr](./Synvert/Core/Rewriter/Instance.html#replace_erb_stmt_with_expr-instance_method) - replace erb stmt code to expr code
80
+ * [replace_erb_stmt_with_expr](./Synvert/Core/Rewriter/Instance.html#replace_erb_stmt_with_expr-instance_method) - replace erb stmt code to expr code
data/Rakefile CHANGED
@@ -2,7 +2,21 @@
2
2
 
3
3
  require "bundler/gem_tasks"
4
4
  require 'rspec/core/rake_task'
5
-
6
5
  RSpec::Core::RakeTask.new(:spec)
6
+ require 'oedipus_lex'
7
+ Rake.application.rake_require "oedipus_lex"
8
+
9
+ file "lib/synvert/core/node_query/lexer.rex.rb" => "lib/synvert/core/node_query/lexer.rex"
10
+ file "lib/synvert/core/node_query/parser.racc.rb" => "lib/synvert/core/node_query/parser.y"
11
+
12
+ task :lexer => "lib/synvert/core/node_query/lexer.rex.rb"
13
+ task :parser => "lib/synvert/core/node_query/parser.racc.rb"
14
+ task :generate => [:lexer, :parser]
15
+
16
+ rule '.racc.rb' => '.y' do |t|
17
+ cmd = "bundle exec racc -l -v -o #{t.name} #{t.source}"
18
+ sh cmd
19
+ end
7
20
 
8
21
  task :default => :spec
22
+ task :spec => :generate
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Extend Array.
4
+ class Array
5
+ # Get child node by the name.
6
+ #
7
+ # @param child_name [String] name of child node.
8
+ # @return [Parser::AST::Node] the child node.
9
+ def child_node_by_name(child_name)
10
+ direct_child_name, nested_child_name = child_name.split('.', 2)
11
+ child_direct_child_node = direct_child_name =~ /\A\d+\z/ ? self[direct_child_name.to_i - 1] : self.send(direct_child_name)
12
+ return child_direct_child_node.child_node_by_name(nested_child_name) if nested_child_name
13
+ return child_direct_child_node if child_direct_child_node
14
+
15
+ raise Synvert::Core::MethodNotSupported,
16
+ "child_node_by_name is not handled for #{map(&:debug_info).join("\n")}, child_name: #{child_name}"
17
+ end
18
+
19
+ # Get the source range of child node.
20
+ #
21
+ # @param child_name [String] name of child node.
22
+ # @return [Parser::Source::Range] source range of child node.
23
+ def child_node_range(child_name)
24
+ direct_child_name, nested_child_name = child_name.split('.', 2)
25
+ child_direct_child_node = direct_child_name =~ /\A\d+\z/ ? self[direct_child_name.to_i - 1] : self.send(direct_child_name)
26
+ if nested_child_name
27
+ return child_direct_child_node.child_node_range(nested_child_name)
28
+ elsif child_direct_child_node
29
+ return (
30
+ Parser::Source::Range.new(
31
+ '(string)',
32
+ child_direct_child_node.loc.expression.begin_pos,
33
+ child_direct_child_node.loc.expression.end_pos
34
+ )
35
+ )
36
+ else
37
+ raise Synvert::Core::MethodNotSupported,
38
+ "child_node_range is not handled for #{map(&:debug_info).join("\n")}, child_name: #{child_name}"
39
+ end
40
+ end
41
+ end
@@ -43,11 +43,13 @@ module Synvert::Core
43
43
  end
44
44
 
45
45
  def decode_html_output(source)
46
- source.gsub(/@output_buffer.safe_append='(.+?)'.freeze;/m) { reverse_escape_text(Regexp.last_match(1)) }.gsub(
47
- /@output_buffer.safe_append=\((.+?)\);#{ERUBY_EXPR_SPLITTER}/mo
48
- ) { reverse_escape_text(Regexp.last_match(1)) }.gsub(
49
- /@output_buffer.safe_append=(.+?)\s+(do|\{)(\s*\|[^|]*\|)?\s*#{ERUBY_EXPR_SPLITTER}/mo
50
- ) { reverse_escape_text(Regexp.last_match(1)) }
46
+ source.gsub(/@output_buffer.safe_append='(.+?)'.freeze;/m) { reverse_escape_text(Regexp.last_match(1)) }
47
+ .gsub(
48
+ /@output_buffer.safe_append=\((.+?)\);#{ERUBY_EXPR_SPLITTER}/mo
49
+ ) { reverse_escape_text(Regexp.last_match(1)) }
50
+ .gsub(
51
+ /@output_buffer.safe_append=(.+?)\s+(do|\{)(\s*\|[^|]*\|)?\s*#{ERUBY_EXPR_SPLITTER}/mo
52
+ ) { reverse_escape_text(Regexp.last_match(1)) }
51
53
  end
52
54
 
53
55
  def remove_erubis_buf(source)
@@ -64,6 +66,7 @@ module Synvert::Core
64
66
 
65
67
  # borrowed from rails
66
68
  class Erubis < ::Erubis::Eruby
69
+ BLOCK_EXPR = /\s+(do|\{)(\s*\|[^|]*\|)?\s*\Z/
67
70
  def add_preamble(src)
68
71
  @newline_pending = 0
69
72
  src << '@output_buffer = output_buffer || ActionView::OutputBuffer.new;'
@@ -76,7 +79,7 @@ module Synvert::Core
76
79
  @newline_pending += 1
77
80
  else
78
81
  src << "@output_buffer.safe_append='"
79
- src << "\n" * @newline_pending if @newline_pending > 0
82
+ src << ("\n" * @newline_pending) if @newline_pending > 0
80
83
  src << escape_text(text)
81
84
  src << "'.freeze;"
82
85
 
@@ -95,8 +98,6 @@ module Synvert::Core
95
98
  end
96
99
  end
97
100
 
98
- BLOCK_EXPR = /\s+(do|\{)(\s*\|[^|]*\|)?\s*\Z/
99
-
100
101
  def add_expr_literal(src, code)
101
102
  flush_newline_if_pending(src)
102
103
  if BLOCK_EXPR.match?(code)
@@ -27,8 +27,41 @@ module Parser::AST
27
27
  # +type: 'send', receiver: { type: 'send', receiver: { type: 'send', message: 'config' }, message: 'active_record' }, message: 'identity_map='+
28
28
  #
29
29
  # Source Code to Ast Node
30
- # {https://synvert-playground.xinminlabs.com}
30
+ # {https://synvert-playground.xinminlabs.com?language=ruby}
31
31
  class Node
32
+ # Initialize a Node.
33
+ #
34
+ # It extends Parser::AST::Node and set parent for its child nodes.
35
+ def initialize(type, children = [], properties = {})
36
+ @mutable_attributes = {}
37
+ super
38
+ # children could be nil for s(:array)
39
+ Array(children).each do |child_node|
40
+ if child_node.is_a?(Parser::AST::Node)
41
+ child_node.parent = self
42
+ end
43
+ end
44
+ end
45
+
46
+ # Get the parent node.
47
+ # @return [Parser::AST::Node] parent node.
48
+ def parent
49
+ @mutable_attributes[:parent]
50
+ end
51
+
52
+ # Set the parent node.
53
+ # @param node [Parser::AST::Node] parent node.
54
+ def parent=(node)
55
+ @mutable_attributes[:parent] = node
56
+ end
57
+
58
+ # Get the sibling nodes.
59
+ # @return [Array<Parser::AST::Node>] sibling nodes.
60
+ def siblings
61
+ index = parent.children.index(self)
62
+ parent.children[index + 1..]
63
+ end
64
+
32
65
  # Get the name of node.
33
66
  # It supports :arg, :blockarg, :class, :const, :cvar, :def, :defs, :ivar,
34
67
  # :lvar, :mlhs, :module and :restarg nodes.
@@ -123,9 +156,9 @@ module Parser::AST
123
156
  def arguments
124
157
  case type
125
158
  when :def, :block
126
- children[1]
159
+ children[1].children
127
160
  when :defs
128
- children[2]
161
+ children[2].children
129
162
  when :send, :csend
130
163
  children[2..-1]
131
164
  when :defined?
@@ -311,7 +344,7 @@ module Parser::AST
311
344
  # Return the exact value of node.
312
345
  # It supports :array, :begin, :erange, :false, :float, :irange, :int, :str, :sym and :true nodes.
313
346
  # @example
314
- # node # s(:array, s(:str, "str"), s(:sym, :str)) ["str", :str]
347
+ # node # s(:array, s(:str, "str"), s(:sym, :str))
315
348
  # node.to_value # ['str', :str]
316
349
  # @return [Object] exact value.
317
350
  # @raise [Synvert::Core::MethodNotSupported] if calls on other node.
@@ -323,6 +356,8 @@ module Parser::AST
323
356
  true
324
357
  when :false
325
358
  false
359
+ when :nil
360
+ nil
326
361
  when :array
327
362
  children.map(&:to_value)
328
363
  when :irange
@@ -425,27 +460,12 @@ module Parser::AST
425
460
  if respond_to?(direct_child_name)
426
461
  child_node = send(direct_child_name)
427
462
 
428
- if nested_child_name
429
- if child_node.is_a?(Array)
430
- child_direct_child_name, child_nested_child_name = nested_child_name.split('.', 2)
431
- child_direct_child_node = child_direct_child_name =~ /\A\d+\z/ ? child_node[child_direct_child_name.to_i - 1] : child_node.send(child_direct_child_name)
432
- return child_direct_child_node.child_node_by_name(child_nested_child_name) if child_nested_child_name
433
- return child_direct_child_node if child_direct_child_node
434
-
435
- raise Synvert::Core::MethodNotSupported,
436
- "child_node_by_name is not handled for #{debug_info}, child_name: #{child_name}"
437
- end
438
-
439
- return child_node.child_node_by_name(nested_child_name)
440
- end
463
+ return child_node.child_node_by_name(nested_child_name) if nested_child_name
441
464
 
442
465
  return nil if child_node.nil?
443
466
 
444
467
  return child_node if child_node.is_a?(Parser::AST::Node)
445
468
 
446
- # arguments
447
- return nil if child_node.empty?
448
-
449
469
  return child_node
450
470
  end
451
471
 
@@ -460,7 +480,11 @@ module Parser::AST
460
480
  def child_node_range(child_name)
461
481
  case [type, child_name.to_sym]
462
482
  when %i[block pipes], %i[def parentheses], %i[defs parentheses]
463
- Parser::Source::Range.new('(string)', arguments.loc.expression.begin_pos, arguments.loc.expression.end_pos)
483
+ Parser::Source::Range.new(
484
+ '(string)',
485
+ arguments.first.loc.expression.begin_pos - 1,
486
+ arguments.last.loc.expression.end_pos + 1
487
+ )
464
488
  when %i[block arguments], %i[def arguments], %i[defs arguments]
465
489
  Parser::Source::Range.new(
466
490
  '(string)',
@@ -490,28 +514,7 @@ module Parser::AST
490
514
  if respond_to?(direct_child_name)
491
515
  child_node = send(direct_child_name)
492
516
 
493
- if nested_child_name
494
- if child_node.is_a?(Array)
495
- child_direct_child_name, child_nested_child_name = nested_child_name.split('.', 2)
496
- child_direct_child_node = child_direct_child_name =~ /\A\d+\z/ ? child_node[child_direct_child_name.to_i - 1] : child_node.send(child_direct_child_name)
497
- if child_nested_child_name
498
- return child_direct_child_node.child_node_range(child_nested_child_name)
499
- elsif child_direct_child_node
500
- return (
501
- Parser::Source::Range.new(
502
- '(string)',
503
- child_direct_child_node.loc.expression.begin_pos,
504
- child_direct_child_node.loc.expression.end_pos
505
- )
506
- )
507
- else
508
- raise Synvert::Core::MethodNotSupported,
509
- "child_node_range is not handled for #{debug_info}, child_name: #{child_name}"
510
- end
511
- end
512
-
513
- return child_node.child_node_range(nested_child_name)
514
- end
517
+ return child_node.child_node_range(nested_child_name) if nested_child_name
515
518
 
516
519
  return nil if child_node.nil?
517
520
 
@@ -703,7 +706,7 @@ module Parser::AST
703
706
  if type == :block && caller.type == :send && caller.receiver.nil? && caller.message == :lambda
704
707
  new_source = to_source
705
708
  if arguments.size > 1
706
- new_source = new_source[0...arguments.loc.begin.to_range.begin - 1] + new_source[arguments.loc.end.to_range.end..-1]
709
+ new_source = new_source[0...arguments.first.loc.expression.begin_pos - 2] + new_source[arguments.last.loc.expression.end_pos + 1..-1]
707
710
  new_source = new_source.sub('lambda', "->(#{arguments.map(&:to_source).join(', ')})")
708
711
  else
709
712
  new_source = new_source.sub('lambda', '->')
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Synvert::Core::NodeQuery::Compiler
4
+ # Array represents a ruby array value.
5
+ class Array
6
+ include Comparable
7
+
8
+ # Initialize an Array.
9
+ # @param value the first value of the array
10
+ # @param rest the rest value of the array
11
+ def initialize(value: nil, rest: nil)
12
+ @value = value
13
+ @rest = rest
14
+ end
15
+
16
+ # Get the expected value.
17
+ # @return [Array]
18
+ def expected_value
19
+ expected = []
20
+ expected.push(@value) if @value
21
+ expected += @rest.expected_value if @rest
22
+ expected
23
+ end
24
+
25
+ # Get valid operators.
26
+ def valid_operators
27
+ ARRAY_VALID_OPERATORS
28
+ end
29
+
30
+ def to_s
31
+ [@value, @rest].compact.join(', ')
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Synvert::Core::NodeQuery::Compiler
4
+ # Attribute is a pair of key, value and operator,
5
+ class Attribute
6
+ # Initialize a Attribute.
7
+ # @param key [String] the key
8
+ # @param value the value can be any class implement {Synvert::Core::NodeQuery::Compiler::Comparable}
9
+ # @param operator [Symbol] the operator
10
+ def initialize(key:, value:, operator: :==)
11
+ @key = key
12
+ @value = value
13
+ @operator = operator
14
+ end
15
+
16
+ # Check if the node matches the attribute.
17
+ # @param node [Parser::AST::Node] the node
18
+ # @return [Boolean]
19
+ def match?(node)
20
+ @value.base_node = node if @value.is_a?(DynamicAttribute)
21
+ node && @value.match?(node.child_node_by_name(@key), @operator)
22
+ end
23
+
24
+ def to_s
25
+ case @operator
26
+ when :!=
27
+ "#{@key}!=#{@value}"
28
+ when :=~
29
+ "#{@key}=~#{@value}"
30
+ when :!~
31
+ "#{@key}!~#{@value}"
32
+ when :>
33
+ "#{@key}>#{@value}"
34
+ when :>=
35
+ "#{@key}>=#{@value}"
36
+ when :<
37
+ "#{@key}<#{@value}"
38
+ when :<=
39
+ "#{@key}<=#{@value}"
40
+ when :in
41
+ "#{@key} in (#{@value})"
42
+ when :not_in
43
+ "#{@key} not in (#{@value})"
44
+ when :includes
45
+ "#{@key} includes #{@value}"
46
+ else
47
+ "#{@key}=#{@value}"
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Synvert::Core::NodeQuery::Compiler
4
+ # AttributeList contains one or more {Synvert::Core::NodeQuery::Compiler::Attribute}.
5
+ class AttributeList
6
+ # Initialize a AttributeList.
7
+ # @param attribute [Synvert::Core::NodeQuery::Compiler::Attribute] the attribute
8
+ # @param rest [Synvert::Core::NodeQuery::Compiler::AttributeList] the rest attribute list
9
+ def initialize(attribute:, rest: nil)
10
+ @attribute = attribute
11
+ @rest = rest
12
+ end
13
+
14
+ # Check if the node matches the attribute list.
15
+ # @return [Boolean]
16
+ def match?(node)
17
+ @attribute.match?(node) && (!@rest || @rest.match?(node))
18
+ end
19
+
20
+ def to_s
21
+ "[#{@attribute}]#{@rest}"
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Synvert::Core::NodeQuery::Compiler
4
+ # Boolean represents a ruby boolean value.
5
+ class Boolean
6
+ include Comparable
7
+
8
+ # Initialize a Boolean.
9
+ # @param value [Boolean] the boolean value
10
+ def initialize(value:)
11
+ @value = value
12
+ end
13
+
14
+ # Get valid operators.
15
+ def valid_operators
16
+ SIMPLE_VALID_OPERATORS
17
+ end
18
+
19
+ def to_s
20
+ @value.to_s
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Synvert::Core::NodeQuery::Compiler
4
+ # Compare acutal value with expected value.
5
+ module Comparable
6
+ SIMPLE_VALID_OPERATORS = [:==, :!=, :includes]
7
+ NUMBER_VALID_OPERATORS = [:==, :!=, :>, :>=, :<, :<=, :includes]
8
+ ARRAY_VALID_OPERATORS = [:==, :!=, :in, :not_in]
9
+ REGEXP_VALID_OPERATORS = [:=~, :!~]
10
+
11
+ # Check if the actual value matches the expected value.
12
+ #
13
+ # @param node [Parser::AST::Node] node to calculate actual value
14
+ # @param operator [Symbol] operator to compare with expected value, operator can be <code>:==</code>, <code>:!=</code>, <code>:></code>, <code>:>=</code>, <code>:<</code>, <code>:<=</code>, <code>:includes</code>, <code>:in</code>, <code>:not_in</code>, <code>:=~</code>, <code>!~</code>
15
+ # @return [Boolean] true if actual value matches the expected value
16
+ # @raise [Synvert::Core::NodeQuery::Compiler::InvalidOperatorError] if operator is invalid
17
+ def match?(node, operator)
18
+ raise InvalidOperatorError, "invalid operator #{operator}" unless valid_operator?(operator)
19
+
20
+ case operator
21
+ when :!=
22
+ if expected_value.is_a?(::Array)
23
+ actual = actual_value(node)
24
+ !actual.is_a?(::Array) || actual.size != expected_value.size ||
25
+ actual.zip(expected_value).any? { |actual_node, expected_node| expected_node.match?(actual_node, :!=) }
26
+ else
27
+ actual_value(node) != expected_value
28
+ end
29
+ when :=~
30
+ actual_value(node) =~ expected_value
31
+ when :!~
32
+ actual_value(node) !~ expected_value
33
+ when :>
34
+ actual_value(node) > expected_value
35
+ when :>=
36
+ actual_value(node) >= expected_value
37
+ when :<
38
+ actual_value(node) < expected_value
39
+ when :<=
40
+ actual_value(node) <= expected_value
41
+ when :in
42
+ expected_value.any? { |expected| expected.match?(node, :==) }
43
+ when :not_in
44
+ expected_value.all? { |expected| expected.match?(node, :!=) }
45
+ when :includes
46
+ actual_value(node).any? { |actual| actual == expected_value }
47
+ else
48
+ if expected_value.is_a?(::Array)
49
+ actual = actual_value(node)
50
+ actual.is_a?(::Array) && actual.size == expected_value.size &&
51
+ actual.zip(expected_value).all? { |actual_node, expected_node| expected_node.match?(actual_node, :==) }
52
+ else
53
+ actual_value(node) == expected_value
54
+ end
55
+ end
56
+ end
57
+
58
+ # Get the actual value from ast node.
59
+ # @return if node is a {Parser::AST::Node}, return the node value, otherwise, return the node itself.
60
+ def actual_value(node)
61
+ if node.is_a?(::Parser::AST::Node)
62
+ node.to_value
63
+ else
64
+ node
65
+ end
66
+ end
67
+
68
+ # Get the expected value
69
+ def expected_value
70
+ @value
71
+ end
72
+
73
+ # Check if the operator is valid.
74
+ # @return [Boolean] true if the operator is valid
75
+ def valid_operator?(operator)
76
+ valid_operators.include?(operator)
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Synvert::Core::NodeQuery::Compiler
4
+ # DynamicAttribute represents a ruby dynamic attribute.
5
+ # e.g. code is <code>{ a: a }</code>, query is <code>'.hash > .pair[key={{value}}]'</code>,
6
+ # <code>{{value}}</code> is the dynamic attribute.
7
+ class DynamicAttribute
8
+ include Comparable
9
+
10
+ attr_accessor :base_node
11
+
12
+ # Initialize a DynamicAttribute.
13
+ # @param value [String] the dynamic attribute value
14
+ def initialize(value:)
15
+ @value = value
16
+ end
17
+
18
+ # Get the actual value of a node.
19
+ #
20
+ # @param node the node
21
+ # @return [String] if node is a {Parser::AST::Node}, return the node source code, otherwise return the node itself.
22
+ def actual_value(node)
23
+ if node.is_a?(::Parser::AST::Node)
24
+ node.to_source
25
+ else
26
+ node
27
+ end
28
+ end
29
+
30
+ # Get the expected value.
31
+ #
32
+ # @return [String] Query the node by @valud from base_node, if the node is a {Parser::AST::Node}, return the node source code, otherwise return the node itself.
33
+ def expected_value
34
+ node = base_node.child_node_by_name(@value)
35
+ if node.is_a?(::Parser::AST::Node)
36
+ node.to_source
37
+ else
38
+ node
39
+ end
40
+ end
41
+
42
+ # Get valid operators.
43
+ def valid_operators
44
+ SIMPLE_VALID_OPERATORS
45
+ end
46
+
47
+ def to_s
48
+ "{{#{@value}}}"
49
+ end
50
+ end
51
+ end