node_mutation 1.0.0 → 1.1.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 +5 -0
- data/Gemfile.lock +3 -1
- data/README.md +3 -1
- data/lib/node_mutation/action/replace_erb_stmt_with_expr_action.rb +41 -0
- data/lib/node_mutation/action/wrap_action.rb +1 -1
- data/lib/node_mutation/action.rb +1 -1
- data/lib/node_mutation/engine/erb.rb +148 -0
- data/lib/node_mutation/engine.rb +6 -0
- data/lib/node_mutation/version.rb +1 -1
- data/lib/node_mutation.rb +42 -10
- data/node_mutation.gemspec +1 -0
- data/sig/node_mutation.rbs +4 -2
- metadata +19 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 181790d29f44cf36742b7666a2b687c0befdd6ff70e30f21a3edcaf6c05ba2d6
|
4
|
+
data.tar.gz: 898ee69b167c2b6b12b2c3ddb196514deac6084c3b3066e7e634c5b9876dc0a8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0ad795cc5d87cb7e37d225d503946bb7afcd49a04de356306d3d9424845d98a7223812fc35f38122e954b38b9a446523d05033186ba312a4bccfc8492c131f83
|
7
|
+
data.tar.gz: 1dff8c627b676e4e3b7d333229abb1e1a5eb92871089a9b23c2ee96e872bd47121928d9e6e3b56e7b47b0d97c9bac7104c06991d42c2e414eb7d149c12042203
|
data/CHANGELOG.md
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,8 +1,9 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
node_mutation (1.
|
4
|
+
node_mutation (1.1.0)
|
5
5
|
activesupport
|
6
|
+
erubis
|
6
7
|
|
7
8
|
GEM
|
8
9
|
remote: https://rubygems.org/
|
@@ -16,6 +17,7 @@ GEM
|
|
16
17
|
coderay (1.1.3)
|
17
18
|
concurrent-ruby (1.1.10)
|
18
19
|
diff-lcs (1.5.0)
|
20
|
+
erubis (2.7.0)
|
19
21
|
fakefs (1.8.0)
|
20
22
|
ffi (1.15.5)
|
21
23
|
formatador (1.1.0)
|
data/README.md
CHANGED
@@ -25,7 +25,7 @@ Or install it yourself as:
|
|
25
25
|
```ruby
|
26
26
|
require 'node_mutation'
|
27
27
|
|
28
|
-
mutation = NodeMutation.new(file_path
|
28
|
+
mutation = NodeMutation.new(file_path)
|
29
29
|
```
|
30
30
|
|
31
31
|
2. call the rewrite apis:
|
@@ -45,6 +45,8 @@ mutation.prepend node, '{{arguments.first}}.include FactoryGirl::Syntax::Methods
|
|
45
45
|
mutation.remove(node: Node)
|
46
46
|
# replace child node of the ast node with new code
|
47
47
|
mutation.replace node, :message, with: 'test'
|
48
|
+
# replace erb stmt node with expr code
|
49
|
+
replace_erb_stmt_with_expr node
|
48
50
|
# replace the ast node with new code
|
49
51
|
mutation.replace_with node, 'create {{arguments}}'
|
50
52
|
# wrap node within a block, class or module
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# ReplaceErbStmtWithExprAction to replace erb stmt code to expr,
|
4
|
+
# @example
|
5
|
+
# e.g. <% form_for ... %> => <%= form_for ... %>.
|
6
|
+
class NodeMutation::ReplaceErbStmtWithExprAction < NodeMutation::Action
|
7
|
+
# Initialize a ReplaceErbStmtWithExprAction.
|
8
|
+
#
|
9
|
+
# @param node [Synvert::Core::Rewriter::Node]
|
10
|
+
def initialize(node)
|
11
|
+
super(node, nil)
|
12
|
+
end
|
13
|
+
|
14
|
+
# The new erb expr code.
|
15
|
+
#
|
16
|
+
# @return [String] new code.
|
17
|
+
def new_code
|
18
|
+
NodeMutation.adapter.file_content(@node)[@start...@end]
|
19
|
+
.sub(NodeMutation::Engine::ERUBY_STMT_SPLITTER, '@output_buffer.append= ')
|
20
|
+
.sub(NodeMutation::Engine::ERUBY_STMT_SPLITTER, NodeMutation::Engine::ERUBY_EXPR_SPLITTER)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
# Calculate the begin the end positions.
|
26
|
+
def calculate_position
|
27
|
+
node_start = NodeMutation.adapter.get_start(@node)
|
28
|
+
node_source = NodeMutation.adapter.get_source(@node)
|
29
|
+
file_content = NodeMutation.adapter.file_content(@node)
|
30
|
+
|
31
|
+
whitespace_index = node_start
|
32
|
+
while file_content[whitespace_index -= 1] == ' '
|
33
|
+
end
|
34
|
+
@start = whitespace_index - NodeMutation::Engine::ERUBY_STMT_SPLITTER.length + 1
|
35
|
+
|
36
|
+
at_index = node_start + node_source.index('do')
|
37
|
+
while file_content[at_index += 1] != '@'
|
38
|
+
end
|
39
|
+
@end = at_index
|
40
|
+
end
|
41
|
+
end
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
# WrapAction to wrap node within a block, class or module.
|
4
4
|
#
|
5
|
-
# Note: if WrapAction is conflicted with another action (
|
5
|
+
# Note: if WrapAction is conflicted with another action (start and end are overlapped),
|
6
6
|
# we have to put those 2 actions into 2 within_file scopes.
|
7
7
|
class NodeMutation::WrapAction < NodeMutation::Action
|
8
8
|
# Initialize a WrapAction.
|
data/lib/node_mutation/action.rb
CHANGED
@@ -0,0 +1,148 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'erubis'
|
4
|
+
|
5
|
+
module NodeMutation::Engine
|
6
|
+
ERUBY_EXPR_SPLITTER = '; ;'
|
7
|
+
ERUBY_STMT_SPLITTER = '; ;'
|
8
|
+
|
9
|
+
class Erb
|
10
|
+
class << self
|
11
|
+
# convert erb to ruby code.
|
12
|
+
#
|
13
|
+
# @param source [String] erb source code.
|
14
|
+
# @return [String] ruby source code.
|
15
|
+
def encode(source)
|
16
|
+
Erubis.new(source.gsub('-%>', '%>'), escape: false, trim: false).src
|
17
|
+
end
|
18
|
+
|
19
|
+
# convert ruby code to erb.
|
20
|
+
#
|
21
|
+
# @param source [String] ruby source code.
|
22
|
+
# @return [String] erb source code.
|
23
|
+
def decode(source)
|
24
|
+
source = decode_ruby_stmt(source)
|
25
|
+
source = decode_ruby_output(source)
|
26
|
+
source = decode_html_output(source)
|
27
|
+
source = remove_erubis_buf(source)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def decode_ruby_stmt(source)
|
33
|
+
source.gsub(/#{ERUBY_STMT_SPLITTER}(.+?)#{ERUBY_STMT_SPLITTER}/mo) { "<%#{Regexp.last_match(1)}%>" }
|
34
|
+
end
|
35
|
+
|
36
|
+
def decode_ruby_output(source)
|
37
|
+
source.gsub(/@output_buffer.append=\((.+?)\);#{ERUBY_EXPR_SPLITTER}/mo) {
|
38
|
+
"<%=#{Regexp.last_match(1)}%>"
|
39
|
+
}.gsub(/@output_buffer.append= (.+?)\s+(do|\{)(\s*\|[^|]*\|)?\s*#{ERUBY_EXPR_SPLITTER}/mo) { |m|
|
40
|
+
"<%=#{m.sub('@output_buffer.append= ', '').sub(ERUBY_EXPR_SPLITTER, '')}%>"
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
def decode_html_output(source)
|
45
|
+
source.gsub(/@output_buffer.safe_append='(.+?)'.freeze;/m) { reverse_escape_text(Regexp.last_match(1)) }
|
46
|
+
.gsub(
|
47
|
+
/@output_buffer.safe_append=\((.+?)\);#{ERUBY_EXPR_SPLITTER}/mo
|
48
|
+
) { reverse_escape_text(Regexp.last_match(1)) }
|
49
|
+
.gsub(
|
50
|
+
/@output_buffer.safe_append=(.+?)\s+(do|\{)(\s*\|[^|]*\|)?\s*#{ERUBY_EXPR_SPLITTER}/mo
|
51
|
+
) { reverse_escape_text(Regexp.last_match(1)) }
|
52
|
+
end
|
53
|
+
|
54
|
+
def remove_erubis_buf(source)
|
55
|
+
source
|
56
|
+
.sub('@output_buffer = output_buffer || ActionView::OutputBuffer.new;', '')
|
57
|
+
.sub('@output_buffer.to_s', '')
|
58
|
+
end
|
59
|
+
|
60
|
+
def reverse_escape_text(source)
|
61
|
+
source.gsub("\\\\", "\\").gsub("\\'", "'")
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# borrowed from rails
|
67
|
+
class Erubis < ::Erubis::Eruby
|
68
|
+
BLOCK_EXPR = /\s+(do|\{)(\s*\|[^|]*\|)?\s*\Z/
|
69
|
+
def add_preamble(src)
|
70
|
+
@newline_pending = 0
|
71
|
+
src << '@output_buffer = output_buffer || ActionView::OutputBuffer.new;'
|
72
|
+
end
|
73
|
+
|
74
|
+
def add_text(src, text)
|
75
|
+
return if text.empty?
|
76
|
+
|
77
|
+
if text == "\n"
|
78
|
+
@newline_pending += 1
|
79
|
+
else
|
80
|
+
src << "@output_buffer.safe_append='"
|
81
|
+
src << ("\n" * @newline_pending) if @newline_pending > 0
|
82
|
+
src << escape_text(text)
|
83
|
+
src << "'.freeze;"
|
84
|
+
|
85
|
+
@newline_pending = 0
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Erubis toggles <%= and <%== behavior when escaping is enabled.
|
90
|
+
# We override to always treat <%== as escaped.
|
91
|
+
def add_expr(src, code, indicator)
|
92
|
+
case indicator
|
93
|
+
when '=='
|
94
|
+
add_expr_escaped(src, code)
|
95
|
+
else
|
96
|
+
super
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def add_expr_literal(src, code)
|
101
|
+
flush_newline_if_pending(src)
|
102
|
+
if BLOCK_EXPR.match?(code)
|
103
|
+
src << '@output_buffer.append= ' << code << ERUBY_EXPR_SPLITTER
|
104
|
+
else
|
105
|
+
src << '@output_buffer.append=(' << code << ');' << ERUBY_EXPR_SPLITTER
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def add_expr_escaped(src, code)
|
110
|
+
flush_newline_if_pending(src)
|
111
|
+
if BLOCK_EXPR.match?(code)
|
112
|
+
src << '@output_buffer.safe_append= ' << code << ERUBY_EXPR_SPLITTER
|
113
|
+
else
|
114
|
+
src << '@output_buffer.safe_append=(' << code << ');' << ERUBY_EXPR_SPLITTER
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def add_stmt(src, code)
|
119
|
+
flush_newline_if_pending(src)
|
120
|
+
if code != "\n" && code != ''
|
121
|
+
index =
|
122
|
+
case code
|
123
|
+
when /\A(\s*)\r?\n/
|
124
|
+
Regexp.last_match(1).length
|
125
|
+
when /\A(\s+)/
|
126
|
+
Regexp.last_match(1).end_with?(' ') ? Regexp.last_match(1).length - 1 : Regexp.last_match(1).length
|
127
|
+
else
|
128
|
+
0
|
129
|
+
end
|
130
|
+
code.insert(index, ERUBY_STMT_SPLITTER)
|
131
|
+
code.insert(-1, ERUBY_STMT_SPLITTER[0...-1])
|
132
|
+
end
|
133
|
+
super
|
134
|
+
end
|
135
|
+
|
136
|
+
def add_postamble(src)
|
137
|
+
flush_newline_if_pending(src)
|
138
|
+
src << '@output_buffer.to_s'
|
139
|
+
end
|
140
|
+
|
141
|
+
def flush_newline_if_pending(src)
|
142
|
+
if @newline_pending > 0
|
143
|
+
src << "@output_buffer.safe_append='#{"\n" * @newline_pending}'.freeze;"
|
144
|
+
@newline_pending = 0
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
data/lib/node_mutation.rb
CHANGED
@@ -9,7 +9,7 @@ class NodeMutation
|
|
9
9
|
class MethodNotSupported < StandardError; end
|
10
10
|
class ConflictActionError < StandardError; end
|
11
11
|
|
12
|
-
|
12
|
+
KEEP_RUNNING = 1
|
13
13
|
THROW_ERROR = 2
|
14
14
|
|
15
15
|
autoload :Adapter, "node_mutation/adapter"
|
@@ -25,6 +25,7 @@ class NodeMutation
|
|
25
25
|
autoload :ReplaceErbStmtWithExprAction, 'node_mutation/action/replace_erb_stmt_with_expr_action'
|
26
26
|
autoload :ReplaceWithAction, 'node_mutation/action/replace_with_action'
|
27
27
|
autoload :WrapAction, 'node_mutation/action/wrap_action'
|
28
|
+
autoload :Engine, 'node_mutation/engine'
|
28
29
|
|
29
30
|
attr_reader :actions
|
30
31
|
|
@@ -47,18 +48,16 @@ class NodeMutation
|
|
47
48
|
end
|
48
49
|
|
49
50
|
# Get the strategy
|
50
|
-
# @return [Integer] current strategy, could be {NodeMutation::
|
51
|
-
# by default is {NodeMutation::
|
51
|
+
# @return [Integer] current strategy, could be {NodeMutation::KEEP_RUNNING} or {NodeMutation::THROW_ERROR},
|
52
|
+
# by default is {NodeMutation::KEEP_RUNNING}
|
52
53
|
def self.strategy
|
53
54
|
@strategy ||= KEEP_RUNNING
|
54
55
|
end
|
55
56
|
|
56
57
|
# Initialize a NodeMutation.
|
57
58
|
# @param file_path [String] file path
|
58
|
-
|
59
|
-
def initialize(file_path, source)
|
59
|
+
def initialize(file_path)
|
60
60
|
@file_path = file_path
|
61
|
-
@source = +source
|
62
61
|
@actions = []
|
63
62
|
end
|
64
63
|
|
@@ -169,14 +168,29 @@ class NodeMutation
|
|
169
168
|
# source code of the ast node is
|
170
169
|
# assert(object.empty?)
|
171
170
|
# then we call
|
172
|
-
# replace :message, with: 'assert_empty'
|
173
|
-
# replace :arguments, with: '{{arguments.first.receiver}}'
|
171
|
+
# mutation.replace(node, :message, with: 'assert_empty')
|
172
|
+
# mutation.replace(node, :arguments, with: '{{arguments.first.receiver}}')
|
174
173
|
# the source code will be rewritten to
|
175
174
|
# assert_empty(object)
|
176
175
|
def replace(node, *selectors, with:)
|
177
176
|
@actions << ReplaceAction.new(node, *selectors, with: with).process
|
178
177
|
end
|
179
178
|
|
179
|
+
# Replace erb stmt node with expr code.
|
180
|
+
# @param node [Node] ast node
|
181
|
+
# @example
|
182
|
+
# source code of the ast node is
|
183
|
+
# <% form_for post do |f| %>
|
184
|
+
# <% end %>
|
185
|
+
# then we call
|
186
|
+
# replace_erb_stmt_with_expr(node)
|
187
|
+
# the source code will be rewritten to
|
188
|
+
# # <%= form_for post do |f| %>
|
189
|
+
# # <% end %>
|
190
|
+
def replace_erb_stmt_with_expr(node)
|
191
|
+
@actions << ReplaceErbStmtWithExprAction.new(node).process
|
192
|
+
end
|
193
|
+
|
180
194
|
# Replace source code of the ast node with new code.
|
181
195
|
# @param node [Node] ast node
|
182
196
|
# @param code [String] code need to be replaced with.
|
@@ -221,23 +235,41 @@ class NodeMutation
|
|
221
235
|
def process
|
222
236
|
conflict_actions = []
|
223
237
|
if @actions.length > 0
|
238
|
+
source = +read_source(@file_path)
|
224
239
|
@actions.sort_by! { |action| [action.start, action.end] }
|
225
240
|
conflict_actions = get_conflict_actions
|
226
241
|
if conflict_actions.size > 0 && NodeMutation.strategy == THROW_ERROR
|
227
242
|
raise ConflictActionError, "mutation actions are conflicted"
|
228
243
|
end
|
229
244
|
@actions.reverse_each do |action|
|
230
|
-
|
245
|
+
source[action.start...action.end] = action.new_code
|
231
246
|
end
|
232
247
|
@actions = []
|
233
248
|
|
234
|
-
|
249
|
+
write_source(@file_path, source)
|
235
250
|
end
|
236
251
|
OpenStruct.new(conflict: !conflict_actions.empty?)
|
237
252
|
end
|
238
253
|
|
239
254
|
private
|
240
255
|
|
256
|
+
# Read file source.
|
257
|
+
# @param file_path [String] file path
|
258
|
+
# @return [String] file source
|
259
|
+
def read_source(file_path)
|
260
|
+
source = File.read(file_path, encoding: 'UTF-8')
|
261
|
+
source = Engine::Erb.encode(source) if /\.erb$/.match?(file_path)
|
262
|
+
source
|
263
|
+
end
|
264
|
+
|
265
|
+
# Write file source to file.
|
266
|
+
# @param file_path [String] file path
|
267
|
+
# @param source [String] file source
|
268
|
+
def write_source(file_path, source)
|
269
|
+
source = Engine::ERB.decode(source) if /\.erb/.match?(file_path)
|
270
|
+
File.write(file_path, source.gsub(/ +\n/, "\n"))
|
271
|
+
end
|
272
|
+
|
241
273
|
# It changes source code from bottom to top, and it can change source code twice at the same time,
|
242
274
|
# So if there is an overlap between two actions, it removes the conflict actions and operate them in the next loop.
|
243
275
|
def get_conflict_actions
|
data/node_mutation.gemspec
CHANGED
@@ -30,6 +30,7 @@ Gem::Specification.new do |spec|
|
|
30
30
|
|
31
31
|
# Uncomment to register a new dependency of your gem
|
32
32
|
spec.add_dependency "activesupport"
|
33
|
+
spec.add_dependency "erubis"
|
33
34
|
|
34
35
|
# For more information and examples about making a new gem, check out our
|
35
36
|
# guide at: https://bundler.io/guides/creating_gem.html
|
data/sig/node_mutation.rbs
CHANGED
@@ -7,7 +7,7 @@ module NodeMutation[T]
|
|
7
7
|
class ConflictActionError < StandardError
|
8
8
|
end
|
9
9
|
|
10
|
-
|
10
|
+
KEEP_RUNNING: Integer
|
11
11
|
|
12
12
|
THROW_ERROR: Integer
|
13
13
|
|
@@ -19,7 +19,7 @@ module NodeMutation[T]
|
|
19
19
|
|
20
20
|
def self.strategry: () -> Integer
|
21
21
|
|
22
|
-
def initialize: (file_path: String
|
22
|
+
def initialize: (file_path: String) -> NodeMutation
|
23
23
|
|
24
24
|
def append: (node: T, code: String) -> void
|
25
25
|
|
@@ -35,6 +35,8 @@ module NodeMutation[T]
|
|
35
35
|
|
36
36
|
def replace: (node: T, *selectors: Array[String], with: String) -> void
|
37
37
|
|
38
|
+
def replace_erb_stmt_with_expr: (node: T) -> void
|
39
|
+
|
38
40
|
def replace_with: (node: T, code: String) -> void
|
39
41
|
|
40
42
|
def wrap: (node: T, with: String) -> void
|
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.1.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: 2022-07-
|
11
|
+
date: 2022-07-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -24,6 +24,20 @@ dependencies:
|
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: erubis
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
27
41
|
description: ast node mutation apis
|
28
42
|
email:
|
29
43
|
- flyerhzm@gmail.com
|
@@ -48,9 +62,12 @@ files:
|
|
48
62
|
- lib/node_mutation/action/prepend_action.rb
|
49
63
|
- lib/node_mutation/action/remove_action.rb
|
50
64
|
- lib/node_mutation/action/replace_action.rb
|
65
|
+
- lib/node_mutation/action/replace_erb_stmt_with_expr_action.rb
|
51
66
|
- lib/node_mutation/action/replace_with_action.rb
|
52
67
|
- lib/node_mutation/action/wrap_action.rb
|
53
68
|
- lib/node_mutation/adapter.rb
|
69
|
+
- lib/node_mutation/engine.rb
|
70
|
+
- lib/node_mutation/engine/erb.rb
|
54
71
|
- lib/node_mutation/parser_adapter.rb
|
55
72
|
- lib/node_mutation/version.rb
|
56
73
|
- node_mutation.gemspec
|