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 +4 -4
- data/CHANGELOG.md +10 -0
- data/Gemfile.lock +1 -1
- data/lib/node_mutation/action/combined_action.rb +29 -0
- data/lib/node_mutation/adapter/parser.rb +23 -23
- data/lib/node_mutation/adapter/syntax_tree.rb +16 -16
- data/lib/node_mutation/helper.rb +14 -0
- data/lib/node_mutation/version.rb +1 -1
- data/lib/node_mutation.rb +110 -21
- data/sig/node_mutation/helper.rbs +3 -0
- data/sig/node_mutation.rbs +3 -1
- metadata +6 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fd49bceca35429738e6137151b99634e0ef83801214ba58e80c5e1d67b299b07
|
4
|
+
data.tar.gz: ef1f95f7edcf89b73b3eeb11137e374c813460cd4c5c8c67b970a5b29acc4e47
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
@@ -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
|
-
#
|
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}}')
|
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}}'))
|
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}}'))
|
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}}'))
|
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')
|
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')
|
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')
|
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')
|
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')
|
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')
|
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')
|
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
|
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')
|
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')
|
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')
|
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')
|
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')
|
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')
|
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')
|
137
|
-
# child_node_range(node, 'dot')
|
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')
|
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')
|
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}}')
|
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}}'))
|
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}}'))
|
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}}'))
|
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')
|
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')
|
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')
|
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')
|
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')
|
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')
|
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')
|
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
|
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')
|
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')
|
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')
|
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')
|
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
|
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
|
-
|
212
|
-
|
213
|
-
|
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
|
-
|
216
|
-
|
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
|
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
|
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 =
|
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
|
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 =
|
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 =
|
289
|
-
end_pos =
|
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 <
|
293
|
-
conflict_actions <<
|
378
|
+
if begin_pos < actions[j].end
|
379
|
+
conflict_actions << actions.delete_at(j)
|
294
380
|
else
|
295
381
|
i = j
|
296
|
-
begin_pos =
|
297
|
-
end_pos =
|
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
|
|
data/sig/node_mutation.rbs
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
|
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.
|
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-
|
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.
|
76
|
+
rubygems_version: 3.4.18
|
74
77
|
signing_key:
|
75
78
|
specification_version: 4
|
76
79
|
summary: ast node mutation apis
|