node_mutation 1.19.3 → 1.20.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.
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