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 +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
|