node_mutation 1.19.3 → 1.20.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: 8089525a411cf3e7400148cba0d3cd21b763d6ec034dd5a627c0faf2419d8ec3
4
- data.tar.gz: c5e0137f1da08ac49ffa45124cc39cb095543e5b5d5cc788e32093c581de2e2d
3
+ metadata.gz: fd49bceca35429738e6137151b99634e0ef83801214ba58e80c5e1d67b299b07
4
+ data.tar.gz: ef1f95f7edcf89b73b3eeb11137e374c813460cd4c5c8c67b970a5b29acc4e47
5
5
  SHA512:
6
- metadata.gz: 0ecd574ae42a8cabdee2bdae86a14b905fdb4df3205af93753de47e9eb86f595371332f7b9dc5c560c3cd0b48db45d41680784b44fa4be9fba7ef36eefa8d9a7
7
- data.tar.gz: c67d25471f221f0c9d7bd1bd6c40c95c2dfc995154dd9264ce4268eb4990c380ca38c7732a5da6c1b75518c68aeb8e3ea32c644259de12ce01559abac722d63a
6
+ metadata.gz: 71f84423b7cc2edd182ce760c984c1fef78f17ca267c72fb02f2eea9674dad5cf7c284180e03dbb888b5872e1157110e656d1f3587df17fe72140110594f0665
7
+ data.tar.gz: 951e9c6ba7afb239dab12428b40509e7d66178fe4776a317ebe5757a8d142e4fabe59ed1e75c5bedc4fe6a08e531c5176112cd48131b91422d1366e9d36af89a
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # NodeMutation
2
2
 
3
+ ## 1.20.0 (2023-09-24)
4
+
5
+ * Add `CombinedAction` to combine multiple actions.
6
+ * Add `combine` dsl to combine multiple actions.
7
+ * Add `NodeMutation::Helper.iterate_actions`
8
+
9
+ ## 1.19.4 (2023-08-17)
10
+
11
+ * Use `NodeMutation.adapter.get_indent`
12
+
3
13
  ## 1.19.3 (2023-07-01)
4
14
 
5
15
  * Rewrite `SyntaxTreeAdapter#child_node_range` to support Binary operator
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- node_mutation (1.19.3)
4
+ node_mutation (1.20.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ # CombinedAction combines multiple actions.
4
+ class NodeMutation::CombinedAction < NodeMutation::Action
5
+ DEFAULT_START = 2**30
6
+
7
+ attr_accessor :actions
8
+
9
+ def initialize
10
+ @actions = []
11
+ @type = :combined
12
+ end
13
+
14
+ def new_code
15
+ nil
16
+ end
17
+
18
+ private
19
+
20
+ # Calculate the begin and end positions.
21
+ def calculate_position
22
+ @start = DEFAULT_START
23
+ @end = 0
24
+ NodeMutation::Helper.iterate_actions(@actions) do |action|
25
+ @start = [action.start, @start].min
26
+ @end = [action.end, @end].max
27
+ end
28
+ end
29
+ end
@@ -15,53 +15,53 @@ class NodeMutation::ParserAdapter < NodeMutation::Adapter
15
15
  end
16
16
  end
17
17
 
18
- # It gets the new source code after evaluating the node.
18
+ # Gets the new source code after evaluating the node.
19
19
  # @param node [Parser::AST::Node] The node to evaluate.
20
20
  # @param code [String] The code to evaluate.
21
21
  # @return [String] The new source code.
22
22
  # @example
23
23
  # node = Parser::CurrentRuby.parse('Factory.define :user do; end')
24
- # rewritten_source(node, '{{call.receiver}}').to eq 'Factory'
24
+ # rewritten_source(node, '{{call.receiver}}') # 'Factory'
25
25
  #
26
26
  # # index for node array
27
27
  # node = Parser::CurrentRuby.parse("test(foo, bar)")
28
- # rewritten_source(node, '{{arguments.0}}')).to eq 'foo'
28
+ # rewritten_source(node, '{{arguments.0}}')) # 'foo'
29
29
  #
30
30
  # # {key}_pair for hash node
31
31
  # node = Parser::CurrentRuby.parse("after_commit :do_index, on: :create, if: :indexable?")
32
- # rewritten_source(node, '{{arguments.-1.on_pair}}')).to eq 'on: :create'
32
+ # rewritten_source(node, '{{arguments.-1.on_pair}}')) # 'on: :create'
33
33
  #
34
34
  # # {key}_value for hash node
35
35
  # node = Parser::CurrentRuby.parse("after_commit :do_index, on: :create, if: :indexable?")
36
- # rewritten_source(node, '{{arguments.-1.on_value}}')).to eq ':create'
36
+ # rewritten_source(node, '{{arguments.-1.on_value}}')) # ':create'
37
37
  #
38
38
  # # to_single_quote for str node
39
39
  # node = Parser::CurrentRuby.parse('"foo"')
40
- # rewritten_source(node, 'to_single_quote') => "'foo'"
40
+ # rewritten_source(node, 'to_single_quote') # "'foo'"
41
41
  #
42
42
  # # to_double_quote for str node
43
43
  # node = Parser::CurrentRuby.parse("'foo'")
44
- # rewritten_source(node, 'to_double_quote') => '"foo"'
44
+ # rewritten_source(node, 'to_double_quote') # '"foo"'
45
45
  #
46
46
  # # to_symbol for str node
47
47
  # node = Parser::CurrentRuby.parse("'foo'")
48
- # rewritten_source(node, 'to_symbol') => ':foo'
48
+ # rewritten_source(node, 'to_symbol') # ':foo'
49
49
  #
50
50
  # # to_string for sym node
51
51
  # node = Parser::CurrentRuby.parse(":foo")
52
- # rewritten_source(node, 'to_string') => 'foo'
52
+ # rewritten_source(node, 'to_string') # 'foo'
53
53
  #
54
54
  # # to_lambda_literal for block node
55
55
  # node = Parser::CurrentRuby.parse('lambda { foobar }')
56
- # rewritten_source(node, 'to_lambda_literal') => '-> { foobar }'
56
+ # rewritten_source(node, 'to_lambda_literal') # '-> { foobar }'
57
57
  #
58
58
  # # strip_curly_braces for hash node
59
59
  # node = Parser::CurrentRuby.parse("{ foo: 'bar' }")
60
- # rewritten_source(node, 'strip_curly_braces') => "foo: 'bar'"
60
+ # rewritten_source(node, 'strip_curly_braces') # "foo: 'bar'"
61
61
  #
62
62
  # # wrap_curly_braces for hash node
63
63
  # node = Parser::CurrentRuby.parse("test(foo: 'bar')")
64
- # rewritten_source(node.arguments.first, 'wrap_curly_braces') => "{ foo: 'bar' }"
64
+ # rewritten_source(node.arguments.first, 'wrap_curly_braces') # "{ foo: 'bar' }"
65
65
  def rewritten_source(node, code)
66
66
  code.gsub(/{{(.+?)}}/m) do
67
67
  old_code = Regexp.last_match(1)
@@ -82,7 +82,7 @@ class NodeMutation::ParserAdapter < NodeMutation::Adapter
82
82
  if lines_count > 1 && lines_count == evaluated.size
83
83
  new_code = []
84
84
  lines.each_with_index { |line, index|
85
- new_code << (index == 0 ? line : line[evaluated.first.indent - 2..-1])
85
+ new_code << (index == 0 ? line : line[NodeMutation.adapter.get_indent(evaluated.first) - NodeMutation.tab_width..-1])
86
86
  }
87
87
  new_code.join("\n")
88
88
  else
@@ -109,40 +109,40 @@ class NodeMutation::ParserAdapter < NodeMutation::Adapter
109
109
  # @return {NodeMutation::Struct::Range} The range of the child node.
110
110
  # @example
111
111
  # node = Parser::CurrentRuby.parse('Factory.define :user do; end')
112
- # child_node_range(node, 'caller.receiver') => { start: 0, end: 'Factory'.length }
112
+ # child_node_range(node, 'caller.receiver') # { start: 0, end: 'Factory'.length }
113
113
  #
114
114
  # # node array
115
115
  # node = Parser::CurrentRuby.parse('foobar arg1, arg2)')
116
- # child_node_range(node, 'arguments') => { start: 'foobar '.length, end: 'foobar arg1, arg2'.length }
116
+ # child_node_range(node, 'arguments') # { start: 'foobar '.length, end: 'foobar arg1, arg2'.length }
117
117
  #
118
118
  # # index for node array
119
119
  # node = Parser::CurrentRuby.parse('foobar(arg1, arg2)')
120
- # child_node_range(node, 'arguments.-1') => { start: 'foobar(arg1, '.length, end: 'foobar(arg1, arg2'.length }
120
+ # child_node_range(node, 'arguments.-1') # { start: 'foobar(arg1, '.length, end: 'foobar(arg1, arg2'.length }
121
121
  #
122
122
  # # pips for block node
123
123
  # node = Parser::CurrentRuby.parse('Factory.define :user do |user|; end')
124
- # child_node_range(node, 'pipes') => { start: 'Factory.deine :user do '.length, end: 'Factory.define :user do |user|'.length }
124
+ # child_node_range(node, 'pipes') # { start: 'Factory.deine :user do '.length, end: 'Factory.define :user do |user|'.length }
125
125
  #
126
126
  # # parentheses for def and defs node
127
127
  # node = Parser::CurrentRuby.parse('def foo(bar); end')
128
- # child_node_range(node, 'parentheses') => { start: 'def foo'.length, end: 'def foo(bar)'.length }
128
+ # child_node_range(node, 'parentheses') # { start: 'def foo'.length, end: 'def foo(bar)'.length }
129
129
  #
130
130
  # # double_colon for const node
131
131
  # node = Parser::CurrentRuby.parse('Foo::Bar')
132
- # child_node_range(node, 'double_colon') => { start: 'Foo'.length, end: 'Foo::'.length }
132
+ # child_node_range(node, 'double_colon') # { start: 'Foo'.length, end: 'Foo::'.length }
133
133
  #
134
134
  # # self and dot for defs node
135
135
  # node = Parser::CurrentRuby.parse('def self.foo(bar); end')
136
- # child_node_range(node, 'self') => { start: 'def '.length, end: 'def self'.length }
137
- # child_node_range(node, 'dot') => { start: 'def self'.length, end: 'def self.'.length }
136
+ # child_node_range(node, 'self') # { start: 'def '.length, end: 'def self'.length }
137
+ # child_node_range(node, 'dot') # { start: 'def self'.length, end: 'def self.'.length }
138
138
  #
139
139
  # # dot for send and csend node
140
140
  # node = Parser::CurrentRuby.parse('foo.bar(test)')
141
- # child_node_range(node, 'self') => { start: 'foo'.length, end: 'foo.'.length }
141
+ # child_node_range(node, 'self') # { start: 'foo'.length, end: 'foo.'.length }
142
142
  #
143
143
  # # parentheses for send and csend node
144
144
  # node = Parser::CurrentRuby.parse('foo.bar(test)')
145
- # child_node_range(node, 'parentheses') => { start: 'foo.bar'.length, end: 'foo.bar(test)'.length }
145
+ # child_node_range(node, 'parentheses') # { start: 'foo.bar'.length, end: 'foo.bar(test)'.length }
146
146
  def child_node_range(node, child_name)
147
147
  direct_child_name, nested_child_name = child_name.to_s.split('.', 2)
148
148
 
@@ -18,47 +18,47 @@ class NodeMutation::SyntaxTreeAdapter < NodeMutation::Adapter
18
18
  # @return [String] The new source code.
19
19
  # @example
20
20
  # node = SyntaxTree::Parser.new('class Synvert; end').parse.statements.body.first
21
- # rewritten_source(node, '{{constant}}').to eq 'Synvert'
21
+ # rewritten_source(node, '{{constant}}') # 'Synvert'
22
22
  #
23
23
  # # index for node array
24
24
  # node = SyntaxTree::Parser.new("foo.bar(a, b)").parse.statements.body.first
25
- # rewritten_source(node, '{{arguments.arguments.parts.-1}}')).to eq 'b'
25
+ # rewritten_source(node, '{{arguments.arguments.parts.-1}}')) # 'b'
26
26
  #
27
27
  # # {key}_assoc for HashLiteral node
28
28
  # node = SyntaxTree::Parser.new("after_commit :do_index, on: :create, if: :indexable?").parse.statements.body.first
29
- # rewritten_source(node, '{{arguments.parts.-1.on_assoc}}')).to eq 'on: :create'
29
+ # rewritten_source(node, '{{arguments.parts.-1.on_assoc}}')) # 'on: :create'
30
30
  #
31
31
  # # {key}_value for hash node
32
32
  # node = SyntaxTree::Parser.new("after_commit :do_index, on: :create, if: :indexable?").parse.statements.body.first
33
- # rewritten_source(node, '{{arguments.parts.-1.on_value}}')).to eq ':create'
33
+ # rewritten_source(node, '{{arguments.parts.-1.on_value}}')) # ':create'
34
34
  #
35
35
  # # to_single_quote for StringLiteral node
36
36
  # node = SyntaxTree::Parser.new('"foo"').parse.statements.body.first
37
- # rewritten_source(node, 'to_single_quote') => "'foo'"
37
+ # rewritten_source(node, 'to_single_quote') # "'foo'"
38
38
  #
39
39
  # # to_double_quote for StringLiteral node
40
40
  # node = SyntaxTree::Parser.new("'foo'").parse.statements.body.first
41
- # rewritten_source(node, 'to_double_quote') => '"foo"'
41
+ # rewritten_source(node, 'to_double_quote') # '"foo"'
42
42
  #
43
43
  # # to_symbol for StringLiteral node
44
44
  # node = SyntaxTree::Parser.new("'foo'").parse.statements.body.first
45
- # rewritten_source(node, 'to_symbol') => ':foo'
45
+ # rewritten_source(node, 'to_symbol') # ':foo'
46
46
  #
47
47
  # # to_string for SymbolLiteral node
48
48
  # node = SyntaxTree::Parser.new(":foo").parse.statements.body.first
49
- # rewritten_source(node, 'to_string') => 'foo'
49
+ # rewritten_source(node, 'to_string') # 'foo'
50
50
  #
51
51
  # # to_lambda_literal for MethodAddBlock node
52
52
  # node = SyntaxTree::Parser.new('lambda { foobar }').parse.statements.body.first
53
- # rewritten_source(node, 'to_lambda_literal') => '-> { foobar }'
53
+ # rewritten_source(node, 'to_lambda_literal') # '-> { foobar }'
54
54
  #
55
55
  # # strip_curly_braces for HashLiteral node
56
56
  # node = SyntaxTree::Parser.new("{ foo: 'bar' }").parse.statements.body.first
57
- # rewritten_source(node, 'strip_curly_braces') => "foo: 'bar'"
57
+ # rewritten_source(node, 'strip_curly_braces') # "foo: 'bar'"
58
58
  #
59
59
  # # wrap_curly_braces for BareAssocHash node
60
60
  # node = SyntaxTree::Parser.new("test(foo: 'bar')").parse.statements.body.first
61
- # rewritten_source(node.arguments.arguments.parts.first, 'wrap_curly_braces') => "{ foo: 'bar' }"
61
+ # rewritten_source(node.arguments.arguments.parts.first, 'wrap_curly_braces') # "{ foo: 'bar' }"
62
62
  def rewritten_source(node, code)
63
63
  code.gsub(/{{(.+?)}}/m) do
64
64
  old_code = Regexp.last_match(1)
@@ -74,7 +74,7 @@ class NodeMutation::SyntaxTreeAdapter < NodeMutation::Adapter
74
74
  if lines_count > 1 && lines_count == evaluated.size
75
75
  new_code = []
76
76
  lines.each_with_index { |line, index|
77
- new_code << (index == 0 ? line : line[evaluated.first.indent - 2..-1])
77
+ new_code << (index == 0 ? line : line[NodeMutation.adapter.get_indent(evaluated.first) - NodeMutation.tab_width..-1])
78
78
  }
79
79
  new_code.join("\n")
80
80
  else
@@ -101,19 +101,19 @@ class NodeMutation::SyntaxTreeAdapter < NodeMutation::Adapter
101
101
  # @return {NodeMutation::Struct::Range} The range of the child node.
102
102
  # @example
103
103
  # node = SyntaxTree::Parser.new('foo.bar(test)').parse.statements.body.first
104
- # child_node_range(node, 'receiver') => { start: 0, end: 'foo'.length }
104
+ # child_node_range(node, 'receiver') # { start: 0, end: 'foo'.length }
105
105
  #
106
106
  # # node array
107
107
  # node = SyntaxTree::Parser.new('foo.bar(a, b)').parse.statements.body.first
108
- # child_node_range(node, 'arguments.arguments') => { start: 'foo.bar('.length, end: 'foo.bar(a, b'.length }
108
+ # child_node_range(node, 'arguments.arguments') # { start: 'foo.bar('.length, end: 'foo.bar(a, b'.length }
109
109
  #
110
110
  # # index for node array
111
111
  # node = SyntaxTree::Parser.new('foo.bar(a, b)').parse.statements.body.first
112
- # child_node_range(node, 'arguments.arguments.parts.-1') => { start: 'foo.bar(a, '.length, end: 'foo.bar(a, b'.length }
112
+ # child_node_range(node, 'arguments.arguments.parts.-1') # { start: 'foo.bar(a, '.length, end: 'foo.bar(a, b'.length }
113
113
  #
114
114
  # # operator of Binary node
115
115
  # node = SyntaxTree::Parser.new('foo | bar').parse.statements.body.first
116
- # child_node_range(node, 'operator') => { start: 'foo '.length, end: 'foo |'.length }
116
+ # child_node_range(node, 'operator') # { start: 'foo '.length, end: 'foo |'.length }
117
117
  def child_node_range(node, child_name)
118
118
  direct_child_name, nested_child_name = child_name.to_s.split('.', 2)
119
119
 
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ class NodeMutation::Helper
4
+ # It iterates over all actions, and calls the given block with each action.
5
+ def self.iterate_actions(actions, &block)
6
+ actions.each do |action|
7
+ if action.is_a?(NodeMutation::CombinedAction)
8
+ iterate_actions(action.actions, &block)
9
+ else
10
+ block.call(action)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class NodeMutation
4
- VERSION = "1.19.3"
4
+ VERSION = "1.20.0"
5
5
  end
data/lib/node_mutation.rb CHANGED
@@ -11,6 +11,7 @@ class NodeMutation
11
11
  autoload :SyntaxTreeAdapter, "node_mutation/adapter/syntax_tree"
12
12
  autoload :Action, 'node_mutation/action'
13
13
  autoload :AppendAction, 'node_mutation/action/append_action'
14
+ autoload :CombinedAction, 'node_mutation/action/combined_action'
14
15
  autoload :DeleteAction, 'node_mutation/action/delete_action'
15
16
  autoload :IndentAction, 'node_mutation/action/indent_action'
16
17
  autoload :InsertAction, 'node_mutation/action/insert_action'
@@ -22,6 +23,7 @@ class NodeMutation
22
23
  autoload :Result, 'node_mutation/result'
23
24
  autoload :Strategy, 'node_mutation/strategy'
24
25
  autoload :Struct, 'node_mutation/struct'
26
+ autoload :Helper, 'node_mutation/helper'
25
27
 
26
28
  # @!attribute [r] actions
27
29
  # @return [Array<NodeMutation::Struct::Action>]
@@ -208,21 +210,50 @@ class NodeMutation
208
210
  def wrap(node, prefix:, suffix:, newline: false)
209
211
  if newline
210
212
  indentation = NodeMutation.adapter.get_start_loc(node).column
211
- @actions << InsertAction.new(node, prefix + "\n" + (' ' * indentation), at: 'beginning').process
212
- @actions << InsertAction.new(node, "\n" + (' ' * indentation) + suffix, at: 'end').process
213
- @actions << IndentAction.new(node).process
213
+ combine do
214
+ insert node, prefix + "\n" + (' ' * indentation), at: 'beginning'
215
+ insert node, "\n" + (' ' * indentation) + suffix, at: 'end'
216
+ indent node
217
+ end
214
218
  else
215
- @actions << InsertAction.new(node, prefix, at: 'beginning').process
216
- @actions << InsertAction.new(node, suffix, at: 'end').process
219
+ combine do
220
+ insert node, prefix, at: 'beginning'
221
+ insert node, suffix, at: 'end'
222
+ end
217
223
  end
218
224
  end
219
225
 
226
+ # Indent source code of the ast node
227
+ # @param node [Node] ast node
228
+ # @example
229
+ # source code of ast node is
230
+ # class Foobar
231
+ # end
232
+ # then we call
233
+ # indent(node)
234
+ # the source code will be rewritten to
235
+ # class Foobar
236
+ # end
237
+ def indent(node)
238
+ @actions << IndentAction.new(node).process
239
+ end
240
+
220
241
  # No operation.
221
242
  # @param node [Node] ast node
222
243
  def noop(node)
223
244
  @actions << NoopAction.new(node).process
224
245
  end
225
246
 
247
+ # Combine multiple actions
248
+ def combine
249
+ current_actions = @actions
250
+ combined_action = CombinedAction.new
251
+ @actions = combined_action.actions
252
+ yield
253
+ @actions = current_actions
254
+ @actions << combined_action.process
255
+ end
256
+
226
257
  # Process actions and return the new source.
227
258
  #
228
259
  # If there's an action range conflict,
@@ -231,23 +262,22 @@ class NodeMutation
231
262
  # if strategy is set to KEEP_RUNNING.
232
263
  # @return {NodeMutation::Result}
233
264
  def process
265
+ @actions = flatten_actions(@actions)
234
266
  if @actions.length == 0
235
267
  return NodeMutation::Result.new(affected: false, conflicted: false)
236
268
  end
237
269
 
238
270
  source = +@source
239
271
  @transform_proc.call(@actions) if @transform_proc
240
- @actions.sort_by! { |action| [action.start, action.end] }
241
- conflict_actions = get_conflict_actions
272
+ sort_actions!(@actions)
273
+ conflict_actions = get_conflict_actions(@actions)
242
274
  if conflict_actions.size > 0 && strategy?(Strategy::THROW_ERROR)
243
275
  raise ConflictActionError, "mutation actions are conflicted"
244
276
  end
245
277
 
246
- @actions.reverse_each do |action|
247
- source[action.start...action.end] = action.new_code if action.new_code
248
- end
278
+ new_source = rewrite_source(source, @actions)
249
279
  result = NodeMutation::Result.new(affected: true, conflicted: !conflict_actions.empty?)
250
- result.new_source = source
280
+ result.new_source = new_source
251
281
  result
252
282
  end
253
283
 
@@ -259,13 +289,14 @@ class NodeMutation
259
289
  # if strategy is set to KEEP_RUNNING.
260
290
  # @return {NodeMutation::Result}
261
291
  def test
292
+ @actions = flatten_actions(@actions)
262
293
  if @actions.length == 0
263
294
  return NodeMutation::Result.new(affected: false, conflicted: false)
264
295
  end
265
296
 
266
297
  @transform_proc.call(@actions) if @transform_proc
267
- @actions.sort_by! { |action| [action.start, action.end] }
268
- conflict_actions = get_conflict_actions
298
+ sort_actions!(@actions)
299
+ conflict_actions = get_conflict_actions(@actions)
269
300
  if conflict_actions.size > 0 && strategy?(Strategy::THROW_ERROR)
270
301
  raise ConflictActionError, "mutation actions are conflicted"
271
302
  end
@@ -277,27 +308,85 @@ class NodeMutation
277
308
 
278
309
  private
279
310
 
311
+ # It flattens a series of actions by removing any CombinedAction
312
+ # objects that contain only a single action. This is done recursively.
313
+ def flatten_actions(actions)
314
+ new_actions = []
315
+ actions.each do |action|
316
+ if action.is_a?(CombinedAction)
317
+ new_actions << flatten_combined_action(action)
318
+ else
319
+ new_actions << action
320
+ end
321
+ end
322
+ new_actions.compact
323
+ end
324
+
325
+ # It flattens a combined action.
326
+ def flatten_combined_action(action)
327
+ if action.actions.empty?
328
+ nil
329
+ elsif action.actions.size == 1
330
+ if action.actions.first.is_a?(CombinedAction)
331
+ flatten_combined_action(action.actions.first)
332
+ else
333
+ action.actions.first
334
+ end
335
+ else
336
+ action.actions = flatten_actions(action.actions)
337
+ action
338
+ end
339
+ end
340
+
341
+ # Sort actions by start position and end position.
342
+ # @param actions [Array<NodeMutation::Action>]
343
+ # @return [Array<NodeMutation::Action>] sorted actions
344
+ def sort_actions!(actions)
345
+ actions.sort_by! { |action| [action.start, action.end] }
346
+ actions.each do |action|
347
+ sort_actions!(action.actions) if action.is_a?(CombinedAction)
348
+ end
349
+ end
350
+
351
+ # Rewrite source code with actions.
352
+ # @param source [String] source code
353
+ # @param actions [Array<NodeMutation::Action>] actions
354
+ # @return [String] new source code
355
+ def rewrite_source(source, actions)
356
+ actions.reverse_each do |action|
357
+ if action.is_a?(CombinedAction)
358
+ source = rewrite_source(source, action.actions)
359
+ else
360
+ source[action.start...action.end] = action.new_code if action.new_code
361
+ end
362
+ end
363
+ source
364
+ end
365
+
280
366
  # It changes source code from bottom to top, and it can change source code twice at the same time,
281
367
  # So if there is an overlap between two actions, it removes the conflict actions and operate them in the next loop.
282
- def get_conflict_actions
283
- i = @actions.length - 1
368
+ def get_conflict_actions(actions)
369
+ i = actions.length - 1
284
370
  j = i - 1
285
371
  conflict_actions = []
286
372
  return [] if i < 0
287
373
 
288
- begin_pos = @actions[i].start
289
- end_pos = @actions[i].end
374
+ begin_pos = actions[i].start
375
+ end_pos = actions[i].end
290
376
  while j > -1
291
377
  # if we have two actions with overlapped range.
292
- if begin_pos < @actions[j].end
293
- conflict_actions << @actions.delete_at(j)
378
+ if begin_pos < actions[j].end
379
+ conflict_actions << actions.delete_at(j)
294
380
  else
295
381
  i = j
296
- begin_pos = @actions[i].start
297
- end_pos = @actions[i].end
382
+ begin_pos = actions[i].start
383
+ end_pos = actions[i].end
298
384
  end
299
385
  j -= 1
300
386
  end
387
+ actions.each do |action|
388
+ conflict_actions.concat(get_conflict_actions(action.actions)) if action.is_a?(CombinedAction)
389
+ end
301
390
  conflict_actions
302
391
  end
303
392
 
@@ -0,0 +1,3 @@
1
+ class NodeMutation::Helper
2
+ def self.iterate_actions: (actions: Array[NodeMutation::Action]) { (action: NodeMutation::Action) -> void } -> void
3
+ end
@@ -1,4 +1,4 @@
1
- module NodeMutation[T]
1
+ class NodeMutation[T]
2
2
  VERSION: String
3
3
 
4
4
  class MethodNotSupported < StandardError
@@ -39,6 +39,8 @@ module NodeMutation[T]
39
39
 
40
40
  def noop: (node: T) -> void
41
41
 
42
+ def combine: () { () -> void } -> void
43
+
42
44
  def process: () -> NodeMutation::Result
43
45
 
44
46
  def test: () -> NodeMutation::Result
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.19.3
4
+ version: 1.20.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-06-30 00:00:00.000000000 Z
11
+ date: 2023-09-24 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: ast node mutation apis
14
14
  email:
@@ -28,6 +28,7 @@ files:
28
28
  - lib/node_mutation.rb
29
29
  - lib/node_mutation/action.rb
30
30
  - lib/node_mutation/action/append_action.rb
31
+ - lib/node_mutation/action/combined_action.rb
31
32
  - lib/node_mutation/action/delete_action.rb
32
33
  - lib/node_mutation/action/indent_action.rb
33
34
  - lib/node_mutation/action/insert_action.rb
@@ -39,6 +40,7 @@ files:
39
40
  - lib/node_mutation/adapter.rb
40
41
  - lib/node_mutation/adapter/parser.rb
41
42
  - lib/node_mutation/adapter/syntax_tree.rb
43
+ - lib/node_mutation/helper.rb
42
44
  - lib/node_mutation/result.rb
43
45
  - lib/node_mutation/strategy.rb
44
46
  - lib/node_mutation/struct.rb
@@ -46,6 +48,7 @@ files:
46
48
  - node_mutation.gemspec
47
49
  - sig/node_mutation.rbs
48
50
  - sig/node_mutation/adapter.rbs
51
+ - sig/node_mutation/helper.rbs
49
52
  - sig/node_mutation/result.rbs
50
53
  - sig/node_mutation/strategy.rbs
51
54
  - sig/node_mutation/struct.rbs
@@ -70,7 +73,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
70
73
  - !ruby/object:Gem::Version
71
74
  version: '0'
72
75
  requirements: []
73
- rubygems_version: 3.4.13
76
+ rubygems_version: 3.4.18
74
77
  signing_key:
75
78
  specification_version: 4
76
79
  summary: ast node mutation apis