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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f708082625b70a33be17a520d51fab328979f7640f6125f7cec8a4f8c0fa348c
4
- data.tar.gz: 71219747c6be37cb7282658c5dc6087f33ac63ed195de48ab29f2aa79347ea85
3
+ metadata.gz: 181790d29f44cf36742b7666a2b687c0befdd6ff70e30f21a3edcaf6c05ba2d6
4
+ data.tar.gz: 898ee69b167c2b6b12b2c3ddb196514deac6084c3b3066e7e634c5b9876dc0a8
5
5
  SHA512:
6
- metadata.gz: 74f6fdb99bec0cd69ae0310ebc728661981ab7e7c41624e2409238426095cfec7576f1905d49f7e9d25393723b9c43dbe6ebf9c72730df38a19308e68682fc49
7
- data.tar.gz: d2323af8c25e3a69779848be92594075970eb3c22b6c26fc411bc95710f51c9d37881a32c4196b3330eb28544d05dcbf72ef45521bce2e61025ea1ab5681eaae
6
+ metadata.gz: 0ad795cc5d87cb7e37d225d503946bb7afcd49a04de356306d3d9424845d98a7223812fc35f38122e954b38b9a446523d05033186ba312a4bccfc8492c131f83
7
+ data.tar.gz: 1dff8c627b676e4e3b7d333229abb1e1a5eb92871089a9b23c2ee96e872bd47121928d9e6e3b56e7b47b0d97c9bac7104c06991d42c2e414eb7d149c12042203
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # NodeMutation
2
2
 
3
+ ## 1.1.0 (2022-07-02)
4
+
5
+ * Add erb engine
6
+ * Add `ReplaceErbStmtWithExprAction`
7
+
3
8
  ## 1.0.0 (2022-07-01)
4
9
 
5
10
  * Initial release
data/Gemfile.lock CHANGED
@@ -1,8 +1,9 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- node_mutation (1.0.0)
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, source)
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 (begin_pos and end_pos are overlapped),
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.
@@ -70,7 +70,7 @@ class NodeMutation::Action
70
70
  after_line_is_blank = lines[end_line] == ''
71
71
 
72
72
  if lines.length > 1 && before_line_is_blank && after_line_is_blank
73
- @end_pos += "\n".length
73
+ @end += "\n".length
74
74
  end
75
75
  end
76
76
 
@@ -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
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NodeMutation::Engine
4
+ # Engine defines how to encode / decode other files (like erb).
5
+ autoload :Erb, 'node_mutation/engine/erb'
6
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class NodeMutation
4
- VERSION = "1.0.0"
4
+ VERSION = "1.1.0"
5
5
  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
- KEEPING_RUNNING = 1
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::KEEPING_RUNNING} or {NodeMutation::THROW_ERROR},
51
- # by default is {NodeMutation::KEEPING_RUNNING}
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
- # @param source [String] source of the file
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
- @source[action.start...action.end] = action.new_code
245
+ source[action.start...action.end] = action.new_code
231
246
  end
232
247
  @actions = []
233
248
 
234
- File.write(@file_path, @source)
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
@@ -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
@@ -7,7 +7,7 @@ module NodeMutation[T]
7
7
  class ConflictActionError < StandardError
8
8
  end
9
9
 
10
- KEEPING_RUNNING: Integer
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, source: String) -> NodeMutation
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.0.0
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-01 00:00:00.000000000 Z
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