kettle-dev 1.1.60 → 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,458 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require "set"
5
+ require "prism"
6
+
7
+ module Kettle
8
+ module Dev
9
+ # Prism-based AST merging for templated Ruby files.
10
+ # Handles universal freeze reminders, kettle-dev:freeze blocks, and
11
+ # strategy dispatch (skip/replace/append/merge).
12
+ #
13
+ # Uses Prism for parsing with first-class comment support, enabling
14
+ # preservation of inline and leading comments throughout the merge process.
15
+ module SourceMerger
16
+ FREEZE_START = /#\s*kettle-dev:freeze/i
17
+ FREEZE_END = /#\s*kettle-dev:unfreeze/i
18
+ FREEZE_BLOCK = Regexp.new("(#{FREEZE_START.source}).*?(#{FREEZE_END.source})", Regexp::IGNORECASE | Regexp::MULTILINE)
19
+ FREEZE_REMINDER = <<~RUBY
20
+ # To retain during kettle-dev templating:
21
+ # kettle-dev:freeze
22
+ # # ... your code
23
+ # kettle-dev:unfreeze
24
+ RUBY
25
+ BUG_URL = "https://github.com/kettle-rb/kettle-dev/issues"
26
+
27
+ module_function
28
+
29
+ # Apply a templating strategy to merge source and destination Ruby files
30
+ #
31
+ # @param strategy [Symbol] Merge strategy - :skip, :replace, :append, or :merge
32
+ # @param src [String] Template source content
33
+ # @param dest [String] Destination file content
34
+ # @param path [String] File path (for error messages)
35
+ # @return [String] Merged content with freeze blocks and comments preserved
36
+ # @raise [Kettle::Dev::Error] If strategy is unknown or merge fails
37
+ # @example
38
+ # SourceMerger.apply(
39
+ # strategy: :merge,
40
+ # src: 'gem "foo"',
41
+ # dest: 'gem "bar"',
42
+ # path: "Gemfile"
43
+ # )
44
+ def apply(strategy:, src:, dest:, path:)
45
+ strategy = normalize_strategy(strategy)
46
+ dest ||= ""
47
+ src_with_reminder = ensure_reminder(src)
48
+ content =
49
+ case strategy
50
+ when :skip
51
+ src_with_reminder
52
+ when :replace
53
+ normalize_source(src_with_reminder)
54
+ when :append
55
+ apply_append(src_with_reminder, dest)
56
+ when :merge
57
+ apply_merge(src_with_reminder, dest)
58
+ else
59
+ raise Kettle::Dev::Error, "Unknown templating strategy '#{strategy}' for #{path}."
60
+ end
61
+ content = merge_freeze_blocks(content, dest)
62
+ content = restore_custom_leading_comments(dest, content)
63
+ ensure_trailing_newline(content)
64
+ rescue StandardError => error
65
+ warn_bug(path, error)
66
+ raise Kettle::Dev::Error, "Template merge failed for #{path}: #{error.message}"
67
+ end
68
+
69
+ # Ensure freeze reminder comment is present at the top of content
70
+ #
71
+ # @param content [String] Ruby source content
72
+ # @return [String] Content with freeze reminder prepended if missing
73
+ # @api private
74
+ def ensure_reminder(content)
75
+ return content if reminder_present?(content)
76
+ insertion_index = reminder_insertion_index(content)
77
+ before = content[0...insertion_index]
78
+ after = content[insertion_index..-1]
79
+ snippet = FREEZE_REMINDER
80
+ snippet += "\n" unless snippet.end_with?("\n\n")
81
+ [before, snippet, after].join
82
+ end
83
+
84
+ # Normalize source code while preserving formatting
85
+ #
86
+ # @param source [String] Ruby source code
87
+ # @return [String] Normalized source with trailing newline
88
+ # @api private
89
+ def normalize_source(source)
90
+ parse_result = PrismUtils.parse_with_comments(source)
91
+ return ensure_trailing_newline(source) unless parse_result.success?
92
+
93
+ # Use Prism's slice to preserve original formatting
94
+ ensure_trailing_newline(source)
95
+ end
96
+
97
+ def reminder_present?(content)
98
+ content.include?(FREEZE_REMINDER.lines.first.strip)
99
+ end
100
+
101
+ def reminder_insertion_index(content)
102
+ cursor = 0
103
+ lines = content.lines
104
+ lines.each do |line|
105
+ break unless shebang?(line) || frozen_comment?(line)
106
+ cursor += line.length
107
+ end
108
+ cursor
109
+ end
110
+
111
+ def shebang?(line)
112
+ line.start_with?("#!")
113
+ end
114
+
115
+ def frozen_comment?(line)
116
+ line.match?(/#\s*frozen_string_literal:/)
117
+ end
118
+
119
+ # Merge kettle-dev:freeze blocks from destination into source content
120
+ # Preserves user customizations wrapped in freeze/unfreeze markers
121
+ #
122
+ # @param src_content [String] Template source content
123
+ # @param dest_content [String] Destination file content
124
+ # @return [String] Merged content with freeze blocks from destination
125
+ # @api private
126
+ def merge_freeze_blocks(src_content, dest_content)
127
+ dest_blocks = freeze_blocks(dest_content)
128
+ return src_content if dest_blocks.empty?
129
+ src_blocks = freeze_blocks(src_content)
130
+ updated = src_content.dup
131
+ # Replace matching freeze sections by textual markers rather than index ranges
132
+ dest_blocks.each do |dest_block|
133
+ marker = dest_block[:text]
134
+ next if updated.include?(marker)
135
+ # If the template had a placeholder block, replace the first occurrence of a freeze stub
136
+ placeholder = src_blocks.find { |blk| blk[:start_marker] == dest_block[:start_marker] }
137
+ if placeholder
138
+ updated.sub!(placeholder[:text], marker)
139
+ else
140
+ updated << "\n" unless updated.end_with?("\n")
141
+ updated << marker
142
+ end
143
+ end
144
+ updated
145
+ end
146
+
147
+ def freeze_blocks(text)
148
+ return [] unless text&.match?(FREEZE_START)
149
+ blocks = []
150
+ text.to_enum(:scan, FREEZE_BLOCK).each do
151
+ match = Regexp.last_match
152
+ start_idx = match&.begin(0)
153
+ end_idx = match&.end(0)
154
+ next unless start_idx && end_idx
155
+ segment = match[0]
156
+ start_marker = segment.lines.first&.strip
157
+ blocks << {range: start_idx...end_idx, text: segment, start_marker: start_marker}
158
+ end
159
+ blocks
160
+ end
161
+
162
+ def normalize_strategy(strategy)
163
+ return :skip if strategy.nil?
164
+ strategy.to_s.downcase.strip.to_sym
165
+ end
166
+
167
+ def warn_bug(path, error)
168
+ puts "ERROR: kettle-dev templating failed for #{path}: #{error.message}"
169
+ puts "Please file a bug at #{BUG_URL} with the file contents so we can improve the AST merger."
170
+ end
171
+
172
+ def ensure_trailing_newline(text)
173
+ return "" if text.nil?
174
+ text.end_with?("\n") ? text : text + "\n"
175
+ end
176
+
177
+ def apply_append(src_content, dest_content)
178
+ prism_merge(src_content, dest_content) do |src_nodes, dest_nodes, _src_result, _dest_result|
179
+ existing = Set.new(dest_nodes.map { |node| node_signature(node[:node]) })
180
+ appended = dest_nodes.dup
181
+ src_nodes.each do |node_info|
182
+ sig = node_signature(node_info[:node])
183
+ next if existing.include?(sig)
184
+ appended << node_info
185
+ existing << sig
186
+ end
187
+ appended
188
+ end
189
+ end
190
+
191
+ def apply_merge(src_content, dest_content)
192
+ prism_merge(src_content, dest_content) do |src_nodes, dest_nodes, _src_result, _dest_result|
193
+ src_map = src_nodes.each_with_object({}) do |node_info, memo|
194
+ sig = node_signature(node_info[:node])
195
+ memo[sig] ||= node_info
196
+ end
197
+ merged = dest_nodes.map do |node_info|
198
+ sig = node_signature(node_info[:node])
199
+ if (src_node_info = src_map[sig])
200
+ merge_node_info(sig, node_info, src_node_info)
201
+ else
202
+ node_info
203
+ end
204
+ end
205
+ existing = merged.map { |ni| node_signature(ni[:node]) }.to_set
206
+ src_nodes.each do |node_info|
207
+ sig = node_signature(node_info[:node])
208
+ next if existing.include?(sig)
209
+ merged << node_info
210
+ existing << sig
211
+ end
212
+ merged
213
+ end
214
+ end
215
+
216
+ def merge_node_info(signature, _dest_node_info, src_node_info)
217
+ return src_node_info unless signature.is_a?(Array)
218
+ case signature[1]
219
+ when :gem_specification
220
+ merge_block_node_info(src_node_info)
221
+ else
222
+ src_node_info
223
+ end
224
+ end
225
+
226
+ def merge_block_node_info(src_node_info)
227
+ # For block merging, we need to merge the statements within the block
228
+ # This is complex - for now, prefer template version
229
+ # TODO: Implement deep block statement merging with comment preservation
230
+ src_node_info
231
+ end
232
+
233
+ def prism_merge(src_content, dest_content)
234
+ src_result = PrismUtils.parse_with_comments(src_content)
235
+ dest_result = PrismUtils.parse_with_comments(dest_content)
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
+
243
+ src_nodes = extract_nodes_with_comments(src_result)
244
+ dest_nodes = extract_nodes_with_comments(dest_result)
245
+
246
+ merged_nodes = yield(src_nodes, dest_nodes, src_result, dest_result)
247
+
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 }
299
+ end
300
+
301
+ def extract_nodes_with_comments(parse_result)
302
+ return [] unless parse_result.success?
303
+
304
+ statements = PrismUtils.extract_statements(parse_result.value.statements)
305
+ return [] if statements.empty?
306
+
307
+ source_lines = parse_result.source.lines
308
+
309
+ statements.map.with_index do |stmt, idx|
310
+ prev_stmt = (idx > 0) ? statements[idx - 1] : nil
311
+ body_node = parse_result.value.statements
312
+
313
+ # Count blank lines before this statement
314
+ blank_lines_before = count_blank_lines_before(source_lines, stmt, prev_stmt, body_node)
315
+
316
+ {
317
+ node: stmt,
318
+ leading_comments: PrismUtils.find_leading_comments(parse_result, stmt, prev_stmt, body_node),
319
+ inline_comments: PrismUtils.inline_comments_for_node(parse_result, stmt),
320
+ blank_lines_before: blank_lines_before,
321
+ }
322
+ end
323
+ end
324
+
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: [])
361
+ return "" if node_infos.empty?
362
+
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
+
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
+
382
+ # Add leading comments
383
+ node_info[:leading_comments].each do |comment|
384
+ lines << comment.slice.rstrip
385
+ end
386
+
387
+ # Add the node's source
388
+ node_source = PrismUtils.node_to_source(node_info[:node])
389
+
390
+ # Add inline comments on the same line
391
+ if node_info[:inline_comments].any?
392
+ inline = node_info[:inline_comments].map { |c| c.slice.strip }.join(" ")
393
+ node_source = node_source.rstrip + " " + inline
394
+ end
395
+
396
+ lines << node_source
397
+ end
398
+
399
+ lines.join("\n")
400
+ end
401
+
402
+ def node_signature(node)
403
+ return [:nil] unless node
404
+
405
+ case node
406
+ when Prism::CallNode
407
+ method_name = node.name
408
+ if node.block
409
+ # Block call
410
+ first_arg = PrismUtils.extract_literal_value(node.arguments&.arguments&.first)
411
+ receiver_name = PrismUtils.extract_const_name(node.receiver)
412
+
413
+ if receiver_name == "Gem::Specification" && method_name == :new
414
+ [:block, :gem_specification]
415
+ elsif method_name == :task
416
+ [:block, :task, first_arg]
417
+ elsif method_name == :git_source
418
+ [:block, :git_source, first_arg]
419
+ else
420
+ [:block, method_name, first_arg, node.slice]
421
+ end
422
+ elsif [:source, :git_source, :gem, :eval_gemfile].include?(method_name)
423
+ # Simple call
424
+ first_literal = PrismUtils.extract_literal_value(node.arguments&.arguments&.first)
425
+ [:send, method_name, first_literal]
426
+ else
427
+ [:send, method_name, node.slice]
428
+ end
429
+ else
430
+ # Other node types
431
+ [node.class.name.split("::").last.to_sym, node.slice]
432
+ end
433
+ end
434
+
435
+ def restore_custom_leading_comments(dest_content, merged_content)
436
+ block = leading_comment_block(dest_content)
437
+ return merged_content if block.strip.empty?
438
+ return merged_content if merged_content.start_with?(block)
439
+
440
+ # Insert after shebang / frozen string literal comments (same place reminder goes)
441
+ insertion_index = reminder_insertion_index(merged_content)
442
+ block = ensure_trailing_newline(block)
443
+ merged_content.dup.insert(insertion_index, block)
444
+ end
445
+
446
+ def leading_comment_block(content)
447
+ lines = content.to_s.lines
448
+ collected = []
449
+ lines.each do |line|
450
+ stripped = line.strip
451
+ break unless stripped.empty? || stripped.start_with?("#")
452
+ collected << line
453
+ end
454
+ collected.join
455
+ end
456
+ end
457
+ end
458
+ end
@@ -252,7 +252,8 @@ module Kettle
252
252
 
253
253
  # If a destination gemspec already exists, get metadata from GemSpecReader via helpers
254
254
  orig_meta = nil
255
- if File.exist?(dest_gemspec)
255
+ dest_existed = File.exist?(dest_gemspec)
256
+ if dest_existed
256
257
  begin
257
258
  orig_meta = helpers.gemspec_metadata(File.dirname(dest_gemspec))
258
259
  rescue StandardError => e
@@ -281,88 +282,28 @@ module Kettle
281
282
  end
282
283
 
283
284
  if orig_meta
284
- # Replace a scalar string assignment like: spec.field = "..."
285
- replace_string_field = lambda do |txt, field, value|
286
- v = value.to_s
287
- txt.gsub(/(\bspec\.#{Regexp.escape(field)}\s*=\s*)(["']).*?(\2)/) do
288
- pre = Regexp.last_match(1)
289
- q = '"'
290
- pre + q + v.gsub('"', '\\"') + q
291
- 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
292
289
  end
293
-
294
- # Replace an array assignment like: spec.field = ["a", "b"]
295
- replace_array_field = lambda do |txt, field, ary|
296
- ary = Array(ary).compact.map(&:to_s).reject(&:empty?).uniq
297
- # literal = "[" + arr.map { |e| '"' + e.gsub('"', '\\"') + '"' }.join(", ") + "]"
298
- literal = ary.inspect
299
- if txt =~ /(\bspec\.#{Regexp.escape(field)}\s*=\s*)\[[^\]]*\]/
300
- txt.gsub(/(\bspec\.#{Regexp.escape(field)}\s*=\s*)\[[^\]]*\]/, "\\1#{literal}")
301
- else
302
- # If no existing assignment, insert a new line after spec.version if possible
303
- insert_after = (txt =~ /^\s*spec\.version\s*=.*$/) ? :version : nil
304
- if insert_after == :version
305
- txt.sub(/^(\s*spec\.version\s*=.*$)/) { |line| line + "\n spec.#{field} = #{literal}" }
306
- else
307
- txt + "\n spec.#{field} = #{literal}\n"
308
- end
309
- 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
310
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]
311
301
 
312
302
  begin
313
- # 1. spec.name — retain original
314
- if (name = orig_meta[:gem_name]) && !name.to_s.empty?
315
- c = replace_string_field.call(c, "name", name)
316
- end
317
-
318
- # 2. spec.authors — retain original, normalize to array
319
- orig_auth = orig_meta[:authors]
320
- c = replace_array_field.call(c, "authors", orig_auth)
321
-
322
- # 3. spec.email — retain original, normalize to array
323
- orig_email = orig_meta[:email]
324
- c = replace_array_field.call(c, "email", orig_email)
325
-
326
- # 4. spec.summary — retain original; grapheme emoji prefix handled by "install" task
327
- if (sum = orig_meta[:summary]) && !sum.to_s.empty?
328
- c = replace_string_field.call(c, "summary", sum)
329
- end
330
-
331
- # 5. spec.description — retain original; grapheme emoji prefix handled by "install" task
332
- if (desc = orig_meta[:description]) && !desc.to_s.empty?
333
- c = replace_string_field.call(c, "description", desc)
334
- end
335
-
336
- # 6. spec.licenses — retain original, normalize to array
337
- lic = orig_meta[:licenses]
338
- if lic && !lic.empty?
339
- c = replace_array_field.call(c, "licenses", lic)
340
- end
341
-
342
- # 7. spec.required_ruby_version — retain original
343
- if (rrv = orig_meta[:required_ruby_version].to_s) && !rrv.empty?
344
- c = replace_string_field.call(c, "required_ruby_version", rrv)
345
- end
346
-
347
- # 8. spec.require_paths — retain original, normalize to array
348
- req_paths = orig_meta[:require_paths]
349
- unless req_paths.empty?
350
- c = replace_array_field.call(c, "require_paths", req_paths)
351
- end
352
-
353
- # 9. spec.bindir — retain original
354
- if (bd = orig_meta[:bindir]) && !bd.to_s.empty?
355
- c = replace_string_field.call(c, "bindir", bd)
356
- end
357
-
358
- # 10. spec.executables — retain original, normalize to array
359
- exes = orig_meta[:executables]
360
- unless exes.empty?
361
- c = replace_array_field.call(c, "executables", exes)
362
- end
303
+ c = Kettle::Dev::PrismGemspec.replace_gemspec_fields(c, repl)
363
304
  rescue StandardError => e
364
305
  Kettle::Dev.debug_error(e, __method__)
365
- # Best-effort carry-over; ignore any individual failure
306
+ # Best-effort carry-over; ignore failure and keep c as-is
366
307
  end
367
308
  end
368
309
 
@@ -372,21 +313,26 @@ module Kettle
372
313
  # Strip any dependency lines that name the destination gem.
373
314
  begin
374
315
  if gem_name && !gem_name.to_s.empty?
375
- name_escaped = Regexp.escape(gem_name)
376
- # Matches both runtime and development dependency lines, with or without parentheses.
377
- # Examples matched:
378
- # spec.add_dependency("my-gem", "~> 1.0")
379
- # spec.add_dependency 'my-gem'
380
- # spec.add_development_dependency "my-gem"
381
- # spec.add_development_dependency 'my-gem', ">= 0"
382
- self_dep_re = /\A\s*spec\.add_(?:development_)?dependency(?:\s*\(|\s+)\s*["']#{name_escaped}["'][^\n]*\)?\s*\z/
383
- 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
384
321
  end
385
322
  rescue StandardError => e
386
323
  Kettle::Dev.debug_error(e, __method__)
387
324
  # If anything goes wrong, keep the content as-is rather than failing the task
388
325
  end
389
326
 
327
+ if dest_existed
328
+ begin
329
+ merged = helpers.apply_strategy(c, dest_gemspec)
330
+ c = merged if merged.is_a?(String) && !merged.empty?
331
+ rescue StandardError => e
332
+ Kettle::Dev.debug_error(e, __method__)
333
+ end
334
+ end
335
+
390
336
  c
391
337
  end
392
338
  end