node_mutation 1.10.1 → 1.12.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: 3f69bebea36b4152c7edb233d2d179bfee796988977c36f6375a12a240a77615
4
- data.tar.gz: af1a35eb1b9ec9b3c657d976dd9b0b392d24288de6083f5d98c3d00dc036ff61
3
+ metadata.gz: 97dd9f7362d033a9572811a001fcda89634580d2af15bd7736209dbb7228ba56
4
+ data.tar.gz: e7d4eb04cd7c0d739622105b8aedf85bc8e1e72c8e4e278bf87d840d05787fea
5
5
  SHA512:
6
- metadata.gz: b07a1795e19142226bd48215514d67cf0f3428ec1b03dbcfe49ed4f9825c962427943b6a1f633bc7d1627178354c3c30989af1a5e63dcbe125af54c98f280a45
7
- data.tar.gz: f87aed954b86a4948939528ce1af8bbb779f4673ef8795cd498d015bce84d3abb271db2b44ffd65fdac2f571acbe8d03cfb2ae0cf54a8bd9d97780799e75a521
6
+ metadata.gz: 0e44b67cb7c1dc8ef15ed16ac92c10400639704df32d0a06f2db40a68b8ea6259fe73d7c0dda2df8c5dd07c03137e95d7eca62ae0948c27b2e93b36fa5752cc3
7
+ data.tar.gz: f07ebe6b3712ae3d13b9cf06b7b5dd3e45d68ee7cb7571a9e227f2318de16271f4db2aea0a1ec62a48bc71e6666f8c91e9fda00628cf32d685af083890f70b41
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # NodeMutation
2
2
 
3
+ ## 1.12.0 (2023-03-23)
4
+
5
+ * Support `{key}_pair` for a `hash` node
6
+
7
+ ## 1.11.0 (2023-03-20)
8
+
9
+ * Calculate position properly for `add_comma`
10
+ * Add `and_comma` param to `insert` dsl
11
+
3
12
  ## 1.10.1 (2023-03-13)
4
13
 
5
14
  * Remove `OpenStruct`, use `Struct` instead
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- node_mutation (1.10.1)
4
+ node_mutation (1.12.0)
5
5
  erubis
6
6
 
7
7
  GEM
@@ -26,7 +26,7 @@ class NodeMutation::DeleteAction < NodeMutation::Action
26
26
  .compact.map(&:start).min
27
27
  @end = @selectors.map { |selector| NodeMutation.adapter.child_node_range(@node, selector) }
28
28
  .compact.map(&:end).max
29
- squeeze_spaces
30
29
  remove_comma if @and_comma
30
+ remove_whitespace
31
31
  end
32
32
  end
@@ -8,17 +8,23 @@ class NodeMutation::InsertAction < NodeMutation::Action
8
8
  # @param code [String] to be inserted
9
9
  # @param at [String] position to insert, beginning or end
10
10
  # @param to [<nil|String>] name of child node
11
- def initialize(node, code, at: 'end', to: nil)
11
+ # @param and_comma [Boolean] insert extra comma.
12
+ def initialize(node, code, at: 'end', to: nil, and_comma: false)
12
13
  super(node, code)
13
14
  @at = at
14
15
  @to = to
16
+ @and_comma = and_comma
15
17
  end
16
18
 
17
19
  # The rewritten source code.
18
20
  #
19
21
  # @return [String] rewritten code.
20
22
  def new_code
21
- rewritten_source
23
+ if @and_comma
24
+ @at == 'end' ? ", #{rewritten_source}" : "#{rewritten_source}, "
25
+ else
26
+ rewritten_source
27
+ end
22
28
  end
23
29
 
24
30
  private
@@ -21,15 +21,13 @@ class NodeMutation::RemoveAction < NodeMutation::Action
21
21
 
22
22
  # Calculate the begin the end positions.
23
23
  def calculate_position
24
+ @start = NodeMutation.adapter.get_start(@node)
25
+ @end = NodeMutation.adapter.get_end(@node)
26
+ remove_comma if @and_comma
27
+ remove_whitespace
24
28
  if take_whole_line?
25
- @start = start_index
26
- @end = end_index
29
+ remove_newline
27
30
  squeeze_lines
28
- else
29
- @start = NodeMutation.adapter.get_start(@node)
30
- @end = NodeMutation.adapter.get_end(@node)
31
- squeeze_spaces
32
- remove_comma if @and_command
33
31
  end
34
32
  end
35
33
 
@@ -37,18 +35,50 @@ class NodeMutation::RemoveAction < NodeMutation::Action
37
35
  #
38
36
  # @return [Boolean]
39
37
  def take_whole_line?
40
- NodeMutation.adapter.get_source(@node) == file_source[start_index...end_index].strip
38
+ NodeMutation.adapter.get_source(@node) == file_source[@start...@end].strip.chomp(',')
41
39
  end
42
40
 
43
- # Get the start position of the line
44
- def start_index
45
- index = file_source[0..NodeMutation.adapter.get_start(@node)].rindex("\n")
46
- index ? index + "\n".length : NodeMutation.adapter.get_start(@node)
41
+ def remove_newline
42
+ leading_count = 1
43
+ loop do
44
+ if file_source[@start - leading_count] == "\n"
45
+ break
46
+ elsif ["\t", ' '].include?(file_source[@start - leading_count])
47
+ leading_count += 1
48
+ else
49
+ break
50
+ end
51
+ end
52
+
53
+ trailing_count = 0
54
+ loop do
55
+ if file_source[@end + trailing_count] == "\n"
56
+ break
57
+ elsif ["\t", ' '].include?(file_source[@end + trailing_count])
58
+ trailing_count += 1
59
+ else
60
+ break
61
+ end
62
+ end
63
+
64
+ if file_source[@end + trailing_count] == "\n"
65
+ @end += trailing_count + 1
66
+ end
67
+
68
+ if file_source[@start - leading_count] == "\n"
69
+ @start -= leading_count - 1
70
+ end
47
71
  end
48
72
 
49
- # Get the end position of the line
50
- def end_index
51
- index = file_source[NodeMutation.adapter.get_end(@node)..-1].index("\n")
52
- index ? NodeMutation.adapter.get_end(@node) + index + "\n".length : NodeMutation.adapter.get_end(@node)
73
+ def squeeze_lines
74
+ lines = file_source.split("\n")
75
+ begin_line = NodeMutation.adapter.get_start_loc(@node).line
76
+ end_line = NodeMutation.adapter.get_end_loc(@node).line
77
+ before_line_is_blank = begin_line == 1 || lines[begin_line - 2] == ''
78
+ after_line_is_blank = lines[end_line] == ''
79
+
80
+ if lines.length > 1 && before_line_is_blank && after_line_is_blank
81
+ @end += "\n".length
82
+ end
53
83
  end
54
84
  end
@@ -53,23 +53,14 @@ class NodeMutation::Action
53
53
  @rewritten_source ||= NodeMutation.adapter.rewritten_source(@node, @code)
54
54
  end
55
55
 
56
- # Squeeze spaces from source code.
57
- def squeeze_spaces
58
- if file_source[@start - 1] == ' ' && [' ', "\n", ';'].include?(file_source[@end])
59
- @start -= 1
60
- end
61
- end
62
-
63
- # Squeeze empty lines from source code.
64
- def squeeze_lines
65
- lines = file_source.split("\n")
66
- begin_line = NodeMutation.adapter.get_start_loc(@node).line
67
- end_line = NodeMutation.adapter.get_end_loc(@node).line
68
- before_line_is_blank = begin_line == 1 || lines[begin_line - 2] == ''
69
- after_line_is_blank = lines[end_line] == ''
56
+ # remove unused whitespace.
57
+ # e.g. `foobar(foo, bar)`, if we remove `foo`, the whitespace should also be removed,
58
+ # the code should be changed to `foobar(bar)`.
59
+ def remove_whitespace
60
+ return unless [' ', '('].include?(file_source[@start - 1])
70
61
 
71
- if lines.length > 1 && before_line_is_blank && after_line_is_blank
72
- @end += "\n".length
62
+ while file_source[@end] == ' '
63
+ @end += 1
73
64
  end
74
65
  end
75
66
 
@@ -77,14 +68,28 @@ class NodeMutation::Action
77
68
  # e.g. `foobar(foo, bar)`, if we remove `foo`, the comma should also be removed,
78
69
  # the code should be changed to `foobar(bar)`.
79
70
  def remove_comma
80
- if ',' == file_source[@start - 1]
81
- @start -= 1
82
- elsif ', ' == file_source[@start - 2, 2]
83
- @start -= 2
84
- elsif ', ' == file_source[@end, 2]
85
- @end += 2
86
- elsif ',' == file_source[@end]
87
- @end += 1
71
+ leading_count = 1
72
+ loop do
73
+ if file_source[@start - leading_count] == ','
74
+ @start -= leading_count
75
+ return
76
+ elsif ["\n", "\r", "\t", ' '].include?(file_source[@start - leading_count])
77
+ leading_count += 1
78
+ else
79
+ break
80
+ end
81
+ end
82
+
83
+ trailing_count = 0
84
+ loop do
85
+ if file_source[@end + trailing_count] == ','
86
+ @end += trailing_count + 1
87
+ return
88
+ elsif file_source[@end + trailing_count] == ' '
89
+ trailing_count += 1
90
+ else
91
+ break
92
+ end
88
93
  end
89
94
  end
90
95
 
@@ -5,6 +5,8 @@ INDEX_REGEXP = /\A-?\d+\z/
5
5
  class NodeMutation::ParserAdapter < NodeMutation::Adapter
6
6
  def get_source(node)
7
7
  if node.is_a?(Array)
8
+ return "" if node.empty?
9
+
8
10
  source = file_content(node.first)
9
11
  source[node.first.loc.expression.begin_pos...node.last.loc.expression.end_pos]
10
12
  else
@@ -15,41 +17,36 @@ class NodeMutation::ParserAdapter < NodeMutation::Adapter
15
17
  def rewritten_source(node, code)
16
18
  code.gsub(/{{(.+?)}}/m) do
17
19
  old_code = Regexp.last_match(1)
18
- first_key = old_code.split('.').first
19
- if node.respond_to?(first_key)
20
- evaluated = child_node_by_name(node, old_code)
21
- case evaluated
22
- when Parser::AST::Node
23
- if evaluated.type == :args
24
- evaluated.loc.expression.source[1...-1]
20
+ evaluated = child_node_by_name(node, old_code)
21
+ case evaluated
22
+ when Parser::AST::Node
23
+ if evaluated.type == :args
24
+ evaluated.loc.expression.source[1...-1]
25
+ else
26
+ evaluated.loc.expression.source
27
+ end
28
+ when Array
29
+ if evaluated.size > 0
30
+ file_source = file_content(evaluated.first)
31
+ source = file_source[evaluated.first.loc.expression.begin_pos...evaluated.last.loc.expression.end_pos]
32
+ lines = source.split "\n"
33
+ lines_count = lines.length
34
+ if lines_count > 1 && lines_count == evaluated.size
35
+ new_code = []
36
+ lines.each_with_index { |line, index|
37
+ new_code << (index == 0 ? line : line[evaluated.first.indent - 2..-1])
38
+ }
39
+ new_code.join("\n")
25
40
  else
26
- evaluated.loc.expression.source
27
- end
28
- when Array
29
- if evaluated.size > 0
30
- file_source = file_content(evaluated.first)
31
- source = file_source[evaluated.first.loc.expression.begin_pos...evaluated.last.loc.expression.end_pos]
32
- lines = source.split "\n"
33
- lines_count = lines.length
34
- if lines_count > 1 && lines_count == evaluated.size
35
- new_code = []
36
- lines.each_with_index { |line, index|
37
- new_code << (index == 0 ? line : line[evaluated.first.indent - 2..-1])
38
- }
39
- new_code.join("\n")
40
- else
41
- source
42
- end
41
+ source
43
42
  end
44
- when String, Symbol, Integer, Float
45
- evaluated
46
- when NilClass
47
- ''
48
- else
49
- raise "can not parse \"#{code}\""
50
43
  end
44
+ when String, Symbol, Integer, Float
45
+ evaluated
46
+ when NilClass
47
+ ''
51
48
  else
52
- raise NodeMutation::MethodNotSupported, "#{first_key} is not supported for #{get_source(node)}"
49
+ raise "can not parse \"#{code}\""
53
50
  end
54
51
  end
55
52
  end
@@ -82,7 +79,10 @@ class NodeMutation::ParserAdapter < NodeMutation::Adapter
82
79
 
83
80
  case [node.type, child_name.to_sym]
84
81
  when %i[block pipes], %i[def parentheses], %i[defs parentheses]
85
- NodeMutation::Range.new(node.arguments.first.loc.expression.begin_pos - 1, node.arguments.last.loc.expression.end_pos + 1)
82
+ NodeMutation::Range.new(
83
+ node.arguments.first.loc.expression.begin_pos - 1,
84
+ node.arguments.last.loc.expression.end_pos + 1
85
+ )
86
86
  when %i[block arguments], %i[def arguments], %i[defs arguments]
87
87
  NodeMutation::Range.new(node.arguments.first.loc.expression.begin_pos, node.arguments.last.loc.expression.end_pos)
88
88
  when %i[class name], %i[const name], %i[def name], %i[defs name]
@@ -106,6 +106,15 @@ class NodeMutation::ParserAdapter < NodeMutation::Adapter
106
106
  NodeMutation::Range.new(node.loc.begin.begin_pos, node.loc.end.end_pos)
107
107
  end
108
108
  else
109
+ if node.type == :hash && child_name.to_s.end_with?('_pair')
110
+ pair_node = node.pairs.find { |pair| pair.key.to_value.to_s == child_name.to_s[0..-6] }
111
+ raise NodeMutation::MethodNotSupported,
112
+ "#{direct_child_name} is not supported for #{get_source(node)}" unless pair_node
113
+ return child_node_range(pair, nested_child_name) if nested_child_name
114
+
115
+ return NodeMutation::Range.new(pair_node.loc.expression.begin_pos, pair_node.loc.expression.end_pos)
116
+ end
117
+
109
118
  raise NodeMutation::MethodNotSupported,
110
119
  "#{direct_child_name} is not supported for #{get_source(node)}" unless node.respond_to?(direct_child_name)
111
120
 
@@ -176,10 +185,19 @@ class NodeMutation::ParserAdapter < NodeMutation::Adapter
176
185
  return child_node
177
186
  end
178
187
 
188
+ if node.is_a?(Parser::AST::Node) && node.type == :hash && direct_child_name.end_with?('_pair')
189
+ pair_node = node.pairs.find { |pair| pair.key.to_value.to_s == direct_child_name[0..-6] }
190
+ raise NodeMutation::MethodNotSupported,
191
+ "#{direct_child_name} is not supported for #{get_source(node)}" unless pair_node
192
+ return child_node_by_name(pair_node, nested_child_name) if nested_child_name
193
+
194
+ return pair_node
195
+ end
196
+
179
197
  if node.respond_to?(direct_child_name)
180
198
  child_node = node.send(direct_child_name)
181
199
  elsif direct_child_name.include?('(') && direct_child_name.include?(')')
182
- child_node = eval("node.#{direct_child_name}")
200
+ child_node = node.instance_eval(direct_child_name)
183
201
  else
184
202
  raise NodeMutation::MethodNotSupported, "#{direct_child_name} is not supported for #{get_source(node)}"
185
203
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class NodeMutation
4
- VERSION = "1.10.1"
4
+ VERSION = "1.12.0"
5
5
  end
data/lib/node_mutation.rb CHANGED
@@ -92,8 +92,7 @@ class NodeMutation
92
92
  # Delete source code of the child ast node.
93
93
  # @param node [Node] ast node
94
94
  # @param selectors [Array<Symbol>] selector names of child node.
95
- # @param options [Hash]
96
- # @option and_comma [Boolean] delete extra comma.
95
+ # @param and_comma [Boolean] delete extra comma.
97
96
  # @example
98
97
  # source code of the ast node is
99
98
  # FactoryBot.create(...)
@@ -101,8 +100,8 @@ class NodeMutation
101
100
  # mutation.delete(node, :receiver, :dot)
102
101
  # the source code will be rewritten to
103
102
  # create(...)
104
- def delete(node, *selectors, **options)
105
- @actions << DeleteAction.new(node, *selectors, **options).process
103
+ def delete(node, *selectors, and_comma: false)
104
+ @actions << DeleteAction.new(node, *selectors, and_comma: and_comma).process
106
105
  end
107
106
 
108
107
  # Insert code to the ast node.
@@ -110,6 +109,7 @@ class NodeMutation
110
109
  # @param code [String] code need to be inserted.
111
110
  # @param at [String] insert position, beginning or end
112
111
  # @param to [String] where to insert, if it is nil, will insert to current node.
112
+ # @param and_comma [Boolean] insert extra comma.
113
113
  # @example
114
114
  # source code of the ast node is
115
115
  # open('http://test.com')
@@ -117,8 +117,8 @@ class NodeMutation
117
117
  # mutation.insert(node, 'URI.', at: 'beginning')
118
118
  # the source code will be rewritten to
119
119
  # URI.open('http://test.com')
120
- def insert(node, code, at: 'end', to: nil)
121
- @actions << InsertAction.new(node, code, at: at, to: to).process
120
+ def insert(node, code, at: 'end', to: nil, and_comma: false)
121
+ @actions << InsertAction.new(node, code, at: at, to: to, and_comma: and_comma).process
122
122
  end
123
123
 
124
124
  # Prepend code to the ast node.
@@ -142,16 +142,15 @@ class NodeMutation
142
142
 
143
143
  # Remove source code of the ast node.
144
144
  # @param node [Node] ast node
145
- # @param options [Hash] options.
146
- # @option and_comma [Boolean] delete extra comma.
145
+ # @param and_comma [Boolean] delete extra comma.
147
146
  # @example
148
147
  # source code of the ast node is
149
148
  # puts "test"
150
149
  # then we call
151
150
  # mutation.remove(node)
152
151
  # the source code will be removed
153
- def remove(node, **options)
154
- @actions << RemoveAction.new(node, **options).process
152
+ def remove(node, and_comma: false)
153
+ @actions << RemoveAction.new(node, and_comma: and_comma).process
155
154
  end
156
155
 
157
156
  # Replace child node of the ast node with new code.
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: node_mutation
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.10.1
4
+ version: 1.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Richard Huang
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-03-13 00:00:00.000000000 Z
11
+ date: 2023-03-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: erubis