kettle-dev 1.2.0 → 1.2.1

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.
@@ -0,0 +1,284 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kettle
4
+ module Dev
5
+ # Prism helpers for gemspec manipulation.
6
+ module PrismGemspec
7
+ module_function
8
+
9
+ # Emit a debug warning for rescued errors when kettle-dev debugging is enabled.
10
+ # Controlled by KETTLE_DEV_DEBUG=true (or DEBUG=true as fallback).
11
+ # @param error [Exception]
12
+ # @param context [String, Symbol, nil] optional label, often __method__
13
+ # @return [void]
14
+ def debug_error(error, context = nil)
15
+ Kettle::Dev.debug_error(error, context)
16
+ end
17
+
18
+ # Replace scalar or array assignments inside a Gem::Specification.new block.
19
+ # `replacements` is a hash mapping symbol field names to string or array values.
20
+ # Operates only inside the Gem::Specification block to avoid accidental matches.
21
+ def replace_gemspec_fields(content, replacements = {})
22
+ return content if replacements.nil? || replacements.empty?
23
+
24
+ result = PrismUtils.parse_with_comments(content)
25
+ stmts = PrismUtils.extract_statements(result.value.statements)
26
+
27
+ gemspec_call = stmts.find do |s|
28
+ s.is_a?(Prism::CallNode) && s.block && PrismUtils.extract_const_name(s.receiver) == "Gem::Specification" && s.name == :new
29
+ end
30
+ return content unless gemspec_call
31
+
32
+ call_src = gemspec_call.slice
33
+
34
+ # Try to detect block parameter name (e.g., |spec|)
35
+ blk_param = nil
36
+ begin
37
+ if gemspec_call.block && gemspec_call.block.params
38
+ # Attempt a few defensive ways to extract a param name
39
+ if gemspec_call.block.params.respond_to?(:parameters) && gemspec_call.block.params.parameters.respond_to?(:first)
40
+ p = gemspec_call.block.params.parameters.first
41
+ blk_param = p.name.to_s if p.respond_to?(:name)
42
+ elsif gemspec_call.block.params.respond_to?(:first)
43
+ p = gemspec_call.block.params.first
44
+ blk_param = p.name.to_s if p && p.respond_to?(:name)
45
+ end
46
+ end
47
+ rescue StandardError
48
+ blk_param = nil
49
+ end
50
+
51
+ # Fallback to crude parse of the call_src header
52
+ unless blk_param && !blk_param.to_s.empty?
53
+ hdr_m = call_src.match(/do\b[^\n]*\|([^|]+)\|/m)
54
+ blk_param = (hdr_m && hdr_m[1]) ? hdr_m[1].strip.split(/,\s*/).first : "spec"
55
+ end
56
+ blk_param = "spec" if blk_param.nil? || blk_param.empty?
57
+
58
+ # Extract AST-level statements inside the block body when available
59
+ body_node = gemspec_call.block&.body
60
+ body_src = ""
61
+ begin
62
+ # Try to extract the textual body from call_src using the do|...| ... end capture
63
+ body_src = if (m = call_src.match(/do\b[^\n]*\|[^|]*\|\s*(.*)end\s*\z/m))
64
+ m[1]
65
+ else
66
+ # Last resort: attempt to take slice of body node
67
+ body_node ? body_node.slice : ""
68
+ end
69
+ rescue StandardError
70
+ body_src = body_node ? body_node.slice : ""
71
+ end
72
+
73
+ new_body = body_src.dup
74
+
75
+ # Helper: build literal text for replacement values
76
+ build_literal = lambda do |v|
77
+ if v.is_a?(Array)
78
+ arr = v.compact.map(&:to_s).map { |e| '"' + e.gsub('"', '\\"') + '"' }
79
+ "[" + arr.join(", ") + "]"
80
+ else
81
+ '"' + v.to_s.gsub('"', '\\"') + '"'
82
+ end
83
+ end
84
+
85
+ # Extract existing statement nodes for more precise matching
86
+ stmt_nodes = PrismUtils.extract_statements(body_node)
87
+
88
+ replacements.each do |field_sym, value|
89
+ # Skip special internal keys that are not actual gemspec fields
90
+ next if field_sym == :_remove_self_dependency
91
+
92
+ field = field_sym.to_s
93
+
94
+ # Find an existing assignment node for this field: look for call nodes where
95
+ # receiver slice matches the block param and method name matches assignment
96
+ found_node = stmt_nodes.find do |n|
97
+ next false unless n.is_a?(Prism::CallNode)
98
+ begin
99
+ recv = n.receiver
100
+ recv_name = recv ? recv.slice.strip : nil
101
+ # match receiver variable name or literal slice
102
+ recv_name && recv_name.end_with?(blk_param) && n.name.to_s.start_with?(field)
103
+ rescue StandardError
104
+ false
105
+ end
106
+ end
107
+
108
+ if found_node
109
+ # Do not replace if the existing RHS is non-literal (e.g., computed expression)
110
+ existing_arg = found_node.arguments&.arguments&.first
111
+ existing_literal = begin
112
+ PrismUtils.extract_literal_value(existing_arg)
113
+ rescue
114
+ nil
115
+ end
116
+ if existing_literal.nil? && !value.nil?
117
+ # Skip replacing a non-literal RHS to avoid altering computed expressions.
118
+ debug_error(StandardError.new("Skipping replacement for #{field} because existing RHS is non-literal"), __method__)
119
+ else
120
+ # Replace the found node's slice in the body text with the updated assignment
121
+ indent = begin
122
+ found_node.slice.lines.first.match(/^(\s*)/)[1]
123
+ rescue
124
+ " "
125
+ end
126
+ rhs = build_literal.call(value)
127
+ replacement = "#{indent}#{blk_param}.#{field} = #{rhs}"
128
+ new_body = new_body.sub(found_node.slice, replacement)
129
+ end
130
+ else
131
+ # No existing assignment; insert after spec.version if present, else append
132
+ version_node = stmt_nodes.find do |n|
133
+ n.is_a?(Prism::CallNode) && n.name.to_s.start_with?("version", "version=") && n.receiver && n.receiver.slice.strip.end_with?(blk_param)
134
+ end
135
+
136
+ insert_line = " #{blk_param}.#{field} = #{build_literal.call(value)}\n"
137
+ new_body = if version_node
138
+ # Insert after the version node slice
139
+ new_body.sub(version_node.slice, version_node.slice + "\n" + insert_line)
140
+ elsif new_body.rstrip.end_with?('\n')
141
+ # Append before the final newline if present, else just append
142
+ new_body.rstrip + "\n" + insert_line
143
+ else
144
+ new_body.rstrip + "\n" + insert_line
145
+ end
146
+ end
147
+ end
148
+
149
+ # Handle removal of self-dependency if requested via :_remove_self_dependency
150
+ if replacements[:_remove_self_dependency]
151
+ name_to_remove = replacements[:_remove_self_dependency].to_s
152
+ # Find dependency call nodes to remove (add_dependency/add_development_dependency)
153
+ dep_nodes = stmt_nodes.select do |n|
154
+ next false unless n.is_a?(Prism::CallNode)
155
+ recv = begin
156
+ n.receiver
157
+ rescue
158
+ nil
159
+ end
160
+ next false unless recv && recv.slice.strip.end_with?(blk_param)
161
+ [:add_dependency, :add_development_dependency].include?(n.name)
162
+ end
163
+ dep_nodes.each do |dn|
164
+ # Check first argument literal
165
+ first_arg = dn.arguments&.arguments&.first
166
+ arg_val = begin
167
+ PrismUtils.extract_literal_value(first_arg)
168
+ rescue
169
+ nil
170
+ end
171
+ if arg_val && arg_val.to_s == name_to_remove
172
+ # Remove this node's slice from new_body
173
+ new_body = new_body.sub(dn.slice, "")
174
+ end
175
+ end
176
+ end
177
+
178
+ # Reassemble call source by replacing the captured body portion
179
+ new_call_src = call_src.sub(body_src, new_body)
180
+ content.sub(call_src, new_call_src)
181
+ rescue StandardError => e
182
+ debug_error(e, __method__)
183
+ content
184
+ end
185
+
186
+ # Remove spec.add_dependency / add_development_dependency calls that name the given gem
187
+ # Works by locating the Gem::Specification block and filtering out matching call lines.
188
+ def remove_spec_dependency(content, gem_name)
189
+ return content if gem_name.to_s.strip.empty?
190
+ replace_gemspec_fields(content, _remove_self_dependency: gem_name)
191
+ end
192
+
193
+ # Ensure development dependency lines in a gemspec match the desired lines.
194
+ # `desired` is a hash mapping gem_name => desired_line (string, without leading indentation).
195
+ # Returns the modified gemspec content (or original on error).
196
+ def ensure_development_dependencies(content, desired)
197
+ return content if desired.nil? || desired.empty?
198
+ result = PrismUtils.parse_with_comments(content)
199
+ stmts = PrismUtils.extract_statements(result.value.statements)
200
+ gemspec_call = stmts.find do |s|
201
+ s.is_a?(Prism::CallNode) && s.block && PrismUtils.extract_const_name(s.receiver) == "Gem::Specification" && s.name == :new
202
+ end
203
+
204
+ # If we couldn't locate the Gem::Specification.new block (e.g., empty or
205
+ # truncated gemspec), fall back to appending the desired development
206
+ # dependency lines to the end of the file so callers still get the
207
+ # expected dependency declarations.
208
+ unless gemspec_call
209
+ begin
210
+ out = content.dup
211
+ out << "\n" unless out.end_with?("\n") || out.empty?
212
+ desired.each do |_gem, line|
213
+ out << line.strip + "\n"
214
+ end
215
+ return out
216
+ rescue StandardError => e
217
+ debug_error(e, __method__)
218
+ return content
219
+ end
220
+ end
221
+
222
+ call_src = gemspec_call.slice
223
+ body_node = gemspec_call.block&.body
224
+ body_src = begin
225
+ if (m = call_src.match(/do\b[^\n]*\|[^|]*\|\s*(.*)end\s*\z/m))
226
+ m[1]
227
+ else
228
+ body_node ? body_node.slice : ""
229
+ end
230
+ rescue StandardError
231
+ body_node ? body_node.slice : ""
232
+ end
233
+
234
+ new_body = body_src.dup
235
+ stmt_nodes = PrismUtils.extract_statements(body_node)
236
+
237
+ # Find version node to choose insertion point
238
+ version_node = stmt_nodes.find do |n|
239
+ n.is_a?(Prism::CallNode) && n.name.to_s.start_with?("version") && n.receiver && n.receiver.slice.strip.end_with?("spec")
240
+ end
241
+
242
+ desired.each do |gem_name, desired_line|
243
+ # Skip commented occurrences - we only act on actual AST nodes
244
+ found = stmt_nodes.find do |n|
245
+ next false unless n.is_a?(Prism::CallNode)
246
+ next false unless [:add_development_dependency, :add_dependency].include?(n.name)
247
+ first_arg = n.arguments&.arguments&.first
248
+ val = begin
249
+ PrismUtils.extract_literal_value(first_arg)
250
+ rescue
251
+ nil
252
+ end
253
+ val && val.to_s == gem_name
254
+ end
255
+
256
+ if found
257
+ # Replace existing node slice with desired_line, preserving indent
258
+ indent = begin
259
+ found.slice.lines.first.match(/^(\s*)/)[1]
260
+ rescue
261
+ " "
262
+ end
263
+ replacement = indent + desired_line.strip + "\n"
264
+ new_body = new_body.sub(found.slice, replacement)
265
+ else
266
+ # Insert after version_node if present, else append before end
267
+ insert_line = " " + desired_line.strip + "\n"
268
+ new_body = if version_node
269
+ new_body.sub(version_node.slice, version_node.slice + "\n" + insert_line)
270
+ else
271
+ new_body.rstrip + "\n" + insert_line
272
+ end
273
+ end
274
+ end
275
+
276
+ new_call_src = call_src.sub(body_src, new_body)
277
+ content.sub(call_src, new_call_src)
278
+ rescue StandardError => e
279
+ debug_error(e, __method__)
280
+ content
281
+ end
282
+ end
283
+ end
284
+ end
@@ -53,9 +53,22 @@ module Kettle
53
53
  # @param node [Prism::Node, nil] Node to extract from
54
54
  # @return [String, Symbol, nil] Literal value or nil
55
55
  def extract_literal_value(node)
56
+ return unless node
56
57
  case node
57
58
  when Prism::StringNode then node.unescaped
58
59
  when Prism::SymbolNode then node.unescaped
60
+ else
61
+ # Attempt to handle array literals
62
+ if node.respond_to?(:elements) && node.elements
63
+ arr = node.elements.map do |el|
64
+ case el
65
+ when Prism::StringNode then el.unescaped
66
+ when Prism::SymbolNode then el.unescaped
67
+ end
68
+ end
69
+ return arr if arr.all?
70
+ end
71
+ nil
59
72
  end
60
73
  end
61
74
 
@@ -33,10 +33,7 @@ module Kettle
33
33
  # @param msg [String]
34
34
  # @return [void]
35
35
  def debug_log(msg)
36
- return unless Kettle::Dev::DEBUGGING
37
- Kernel.warn("[readme_backers] #{msg}")
38
- rescue StandardError
39
- # never raise from a standard error within debug logging
36
+ Kettle::Dev.debug_log(msg)
40
37
  end
41
38
 
42
39
  public
@@ -197,37 +197,26 @@ module Kettle
197
197
  end
198
198
  end
199
199
 
200
- modified = target.dup
201
- wanted.each do |gem_name, desired_line|
202
- lines = modified.lines
203
- found = false
204
- lines.map! do |ln|
205
- if ln =~ /add_development_dependency\s*\(?\s*["']#{Regexp.escape(gem_name)}["']/
206
- found = true
207
- indent = ln[/^\s*/] || ""
208
- "#{indent}#{desired_line.strip}\n"
209
- else
210
- ln
211
- end
200
+ # Use Prism-based gemspec edit to ensure development dependencies match
201
+ begin
202
+ modified = Kettle::Dev::PrismGemspec.ensure_development_dependencies(target, wanted)
203
+ # Check if any actual changes were made to development dependency declarations.
204
+ # Extract dependency lines from both and compare sets to avoid false positives
205
+ # from whitespace/formatting differences.
206
+ extract_deps = lambda do |content|
207
+ content.to_s.lines.select { |ln| ln =~ /add_development_dependency\s*\(?/ }.map(&:strip).sort
212
208
  end
213
- modified = lines.join
214
-
215
- next if found
216
-
217
- if (idx = modified.rindex(/\nend\s*\z/))
218
- before = modified[0...idx]
219
- after = modified[idx..-1]
220
- insertion = "\n #{desired_line.strip}\n"
221
- modified = before + insertion + after
209
+ target_deps = extract_deps.call(target)
210
+ modified_deps = extract_deps.call(modified)
211
+ if modified_deps != target_deps
212
+ File.write(@gemspec_path, modified)
213
+ say("Updated development dependencies in #{@gemspec_path}.")
222
214
  else
223
- modified << "\n#{desired_line}\n"
215
+ say("Development dependencies already up to date.")
224
216
  end
225
- end
226
-
227
- if modified != target
228
- File.write(@gemspec_path, modified)
229
- say("Updated development dependencies in #{@gemspec_path}.")
230
- else
217
+ rescue StandardError => e
218
+ Kettle::Dev.debug_error(e, __method__)
219
+ # Fall back to previous behavior: write nothing and report up-to-date
231
220
  say("Development dependencies already up to date.")
232
221
  end
233
222
  end
@@ -3,7 +3,6 @@
3
3
  require "yaml"
4
4
  require "set"
5
5
  require "prism"
6
- require "kettle/dev/prism_utils"
7
6
 
8
7
  module Kettle
9
8
  module Dev
@@ -235,12 +234,68 @@ module Kettle
235
234
  src_result = PrismUtils.parse_with_comments(src_content)
236
235
  dest_result = PrismUtils.parse_with_comments(dest_content)
237
236
 
237
+ # If src parsing failed, return src unchanged to avoid losing content
238
+ unless src_result.success?
239
+ puts "WARNING: Source content parse failed, returning unchanged"
240
+ return src_content
241
+ end
242
+
238
243
  src_nodes = extract_nodes_with_comments(src_result)
239
244
  dest_nodes = extract_nodes_with_comments(dest_result)
240
245
 
241
246
  merged_nodes = yield(src_nodes, dest_nodes, src_result, dest_result)
242
247
 
243
- build_source_from_nodes(merged_nodes)
248
+ # Extract magic comments from source (frozen_string_literal, etc.)
249
+ magic_comments = extract_magic_comments(src_result)
250
+
251
+ # Extract file-level leading comments (comments before first statement)
252
+ file_leading_comments = extract_file_leading_comments(src_result)
253
+
254
+ build_source_from_nodes(merged_nodes, magic_comments: magic_comments, file_leading_comments: file_leading_comments)
255
+ end
256
+
257
+ def extract_magic_comments(parse_result)
258
+ return [] unless parse_result.success?
259
+
260
+ magic_comments = []
261
+ source_lines = parse_result.source.lines
262
+
263
+ # Magic comments appear at the very top of the file (possibly after shebang)
264
+ # They must be on the first or second line
265
+ source_lines.first(2).each do |line|
266
+ stripped = line.strip
267
+ # Check for shebang
268
+ if stripped.start_with?("#!")
269
+ magic_comments << line.rstrip
270
+ # Check for magic comments like frozen_string_literal, encoding, etc.
271
+ elsif stripped.start_with?("#") &&
272
+ (stripped.include?("frozen_string_literal:") ||
273
+ stripped.include?("encoding:") ||
274
+ stripped.include?("warn_indent:") ||
275
+ stripped.include?("shareable_constant_value:"))
276
+ magic_comments << line.rstrip
277
+ end
278
+ end
279
+
280
+ magic_comments
281
+ end
282
+
283
+ def extract_file_leading_comments(parse_result)
284
+ return [] unless parse_result.success?
285
+
286
+ statements = PrismUtils.extract_statements(parse_result.value.statements)
287
+ return [] if statements.empty?
288
+
289
+ first_stmt = statements.first
290
+ first_stmt_line = first_stmt.location.start_line
291
+
292
+ # Extract file-level comments that appear after magic comments (line 1-2)
293
+ # but before the first executable statement. These are typically documentation
294
+ # comments describing the file's purpose.
295
+ parse_result.comments.select do |comment|
296
+ comment.location.start_line > 2 &&
297
+ comment.location.start_line < first_stmt_line
298
+ end.map { |comment| comment.slice.rstrip }
244
299
  end
245
300
 
246
301
  def extract_nodes_with_comments(parse_result)
@@ -249,23 +304,81 @@ module Kettle
249
304
  statements = PrismUtils.extract_statements(parse_result.value.statements)
250
305
  return [] if statements.empty?
251
306
 
307
+ source_lines = parse_result.source.lines
308
+
252
309
  statements.map.with_index do |stmt, idx|
253
310
  prev_stmt = (idx > 0) ? statements[idx - 1] : nil
254
311
  body_node = parse_result.value.statements
255
312
 
313
+ # Count blank lines before this statement
314
+ blank_lines_before = count_blank_lines_before(source_lines, stmt, prev_stmt, body_node)
315
+
256
316
  {
257
317
  node: stmt,
258
318
  leading_comments: PrismUtils.find_leading_comments(parse_result, stmt, prev_stmt, body_node),
259
319
  inline_comments: PrismUtils.inline_comments_for_node(parse_result, stmt),
320
+ blank_lines_before: blank_lines_before,
260
321
  }
261
322
  end
262
323
  end
263
324
 
264
- def build_source_from_nodes(node_infos)
325
+ def count_blank_lines_before(source_lines, current_stmt, prev_stmt, body_node)
326
+ # Determine the starting line to search from
327
+ start_line = if prev_stmt
328
+ prev_stmt.location.end_line
329
+ else
330
+ # For the first statement, start from the beginning of the body
331
+ body_node.location.start_line
332
+ end
333
+
334
+ end_line = current_stmt.location.start_line
335
+
336
+ # Count consecutive blank lines before the current statement
337
+ # (after any comments and the previous statement)
338
+ blank_count = 0
339
+ (start_line...end_line).each do |line_num|
340
+ line_idx = line_num - 1
341
+ next if line_idx < 0 || line_idx >= source_lines.length
342
+
343
+ line = source_lines[line_idx]
344
+ # Skip comment lines (they're handled separately)
345
+ next if line.strip.start_with?("#")
346
+
347
+ # Count blank lines
348
+ if line.strip.empty?
349
+ blank_count += 1
350
+ else
351
+ # Reset count if we hit a non-blank, non-comment line
352
+ # This ensures we only count consecutive blank lines immediately before the statement
353
+ blank_count = 0
354
+ end
355
+ end
356
+
357
+ blank_count
358
+ end
359
+
360
+ def build_source_from_nodes(node_infos, magic_comments: [], file_leading_comments: [])
265
361
  return "" if node_infos.empty?
266
362
 
267
363
  lines = []
364
+
365
+ # Add magic comments at the top (frozen_string_literal, etc.)
366
+ if magic_comments.any?
367
+ lines.concat(magic_comments)
368
+ lines << "" # Add blank line after magic comments
369
+ end
370
+
371
+ # Add file-level leading comments (comments before first statement)
372
+ if file_leading_comments.any?
373
+ lines.concat(file_leading_comments)
374
+ lines << "" # Add blank line after file-level comments
375
+ end
376
+
268
377
  node_infos.each do |node_info|
378
+ # Add blank lines before this statement (for visual grouping)
379
+ blank_lines = node_info[:blank_lines_before] || 0
380
+ blank_lines.times { lines << "" }
381
+
269
382
  # Add leading comments
270
383
  node_info[:leading_comments].each do |comment|
271
384
  lines << comment.slice.rstrip
@@ -282,88 +282,28 @@ module Kettle
282
282
  end
283
283
 
284
284
  if orig_meta
285
- # Replace a scalar string assignment like: spec.field = "..."
286
- replace_string_field = lambda do |txt, field, value|
287
- v = value.to_s
288
- txt.gsub(/(\bspec\.#{Regexp.escape(field)}\s*=\s*)(["']).*?(\2)/) do
289
- pre = Regexp.last_match(1)
290
- q = '"'
291
- pre + q + v.gsub('"', '\\"') + q
292
- end
285
+ # Build replacements using AST-aware helper to carry over fields
286
+ repl = {}
287
+ if (name = orig_meta[:gem_name]) && !name.to_s.empty?
288
+ repl[:name] = name.to_s
293
289
  end
294
-
295
- # Replace an array assignment like: spec.field = ["a", "b"]
296
- replace_array_field = lambda do |txt, field, ary|
297
- ary = Array(ary).compact.map(&:to_s).reject(&:empty?).uniq
298
- # literal = "[" + arr.map { |e| '"' + e.gsub('"', '\\"') + '"' }.join(", ") + "]"
299
- literal = ary.inspect
300
- if txt =~ /(\bspec\.#{Regexp.escape(field)}\s*=\s*)\[[^\]]*\]/
301
- txt.gsub(/(\bspec\.#{Regexp.escape(field)}\s*=\s*)\[[^\]]*\]/, "\\1#{literal}")
302
- else
303
- # If no existing assignment, insert a new line after spec.version if possible
304
- insert_after = (txt =~ /^\s*spec\.version\s*=.*$/) ? :version : nil
305
- if insert_after == :version
306
- txt.sub(/^(\s*spec\.version\s*=.*$)/) { |line| line + "\n spec.#{field} = #{literal}" }
307
- else
308
- txt + "\n spec.#{field} = #{literal}\n"
309
- end
310
- end
290
+ repl[:authors] = Array(orig_meta[:authors]).map(&:to_s) if orig_meta[:authors]
291
+ repl[:email] = Array(orig_meta[:email]).map(&:to_s) if orig_meta[:email]
292
+ repl[:summary] = orig_meta[:summary].to_s if orig_meta[:summary]
293
+ repl[:description] = orig_meta[:description].to_s if orig_meta[:description]
294
+ repl[:licenses] = Array(orig_meta[:licenses]).map(&:to_s) if orig_meta[:licenses]
295
+ if orig_meta[:required_ruby_version]
296
+ repl[:required_ruby_version] = orig_meta[:required_ruby_version].to_s
311
297
  end
298
+ repl[:require_paths] = Array(orig_meta[:require_paths]).map(&:to_s) if orig_meta[:require_paths]
299
+ repl[:bindir] = orig_meta[:bindir].to_s if orig_meta[:bindir]
300
+ repl[:executables] = Array(orig_meta[:executables]).map(&:to_s) if orig_meta[:executables]
312
301
 
313
302
  begin
314
- # 1. spec.name — retain original
315
- if (name = orig_meta[:gem_name]) && !name.to_s.empty?
316
- c = replace_string_field.call(c, "name", name)
317
- end
318
-
319
- # 2. spec.authors — retain original, normalize to array
320
- orig_auth = orig_meta[:authors]
321
- c = replace_array_field.call(c, "authors", orig_auth)
322
-
323
- # 3. spec.email — retain original, normalize to array
324
- orig_email = orig_meta[:email]
325
- c = replace_array_field.call(c, "email", orig_email)
326
-
327
- # 4. spec.summary — retain original; grapheme emoji prefix handled by "install" task
328
- if (sum = orig_meta[:summary]) && !sum.to_s.empty?
329
- c = replace_string_field.call(c, "summary", sum)
330
- end
331
-
332
- # 5. spec.description — retain original; grapheme emoji prefix handled by "install" task
333
- if (desc = orig_meta[:description]) && !desc.to_s.empty?
334
- c = replace_string_field.call(c, "description", desc)
335
- end
336
-
337
- # 6. spec.licenses — retain original, normalize to array
338
- lic = orig_meta[:licenses]
339
- if lic && !lic.empty?
340
- c = replace_array_field.call(c, "licenses", lic)
341
- end
342
-
343
- # 7. spec.required_ruby_version — retain original
344
- if (rrv = orig_meta[:required_ruby_version].to_s) && !rrv.empty?
345
- c = replace_string_field.call(c, "required_ruby_version", rrv)
346
- end
347
-
348
- # 8. spec.require_paths — retain original, normalize to array
349
- req_paths = orig_meta[:require_paths]
350
- unless req_paths.empty?
351
- c = replace_array_field.call(c, "require_paths", req_paths)
352
- end
353
-
354
- # 9. spec.bindir — retain original
355
- if (bd = orig_meta[:bindir]) && !bd.to_s.empty?
356
- c = replace_string_field.call(c, "bindir", bd)
357
- end
358
-
359
- # 10. spec.executables — retain original, normalize to array
360
- exes = orig_meta[:executables]
361
- unless exes.empty?
362
- c = replace_array_field.call(c, "executables", exes)
363
- end
303
+ c = Kettle::Dev::PrismGemspec.replace_gemspec_fields(c, repl)
364
304
  rescue StandardError => e
365
305
  Kettle::Dev.debug_error(e, __method__)
366
- # Best-effort carry-over; ignore any individual failure
306
+ # Best-effort carry-over; ignore failure and keep c as-is
367
307
  end
368
308
  end
369
309
 
@@ -373,15 +313,11 @@ module Kettle
373
313
  # Strip any dependency lines that name the destination gem.
374
314
  begin
375
315
  if gem_name && !gem_name.to_s.empty?
376
- name_escaped = Regexp.escape(gem_name)
377
- # Matches both runtime and development dependency lines, with or without parentheses.
378
- # Examples matched:
379
- # spec.add_dependency("my-gem", "~> 1.0")
380
- # spec.add_dependency 'my-gem'
381
- # spec.add_development_dependency "my-gem"
382
- # spec.add_development_dependency 'my-gem', ">= 0"
383
- self_dep_re = /\A\s*spec\.add_(?:development_)?dependency(?:\s*\(|\s+)\s*["']#{name_escaped}["'][^\n]*\)?\s*\z/
384
- c = c.lines.reject { |ln| self_dep_re =~ ln }.join
316
+ begin
317
+ c = Kettle::Dev::PrismGemspec.remove_spec_dependency(c, gem_name)
318
+ rescue StandardError => e
319
+ Kettle::Dev.debug_error(e, __method__)
320
+ end
385
321
  end
386
322
  rescue StandardError => e
387
323
  Kettle::Dev.debug_error(e, __method__)