kettle-dev 1.2.3 → 2.0.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.
Files changed (133) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +176 -3
  4. data/CITATION.cff +2 -2
  5. data/CONTRIBUTING.md +11 -17
  6. data/README.md +390 -319
  7. data/exe/kettle-dev-setup +12 -63
  8. data/exe/kettle-gh-release +82 -0
  9. data/lib/kettle/dev/gem_spec_reader.rb +2 -2
  10. data/lib/kettle/dev/open_collective_config.rb +12 -0
  11. data/lib/kettle/dev/rakelib/yard.rake +15 -0
  12. data/lib/kettle/dev/tasks/ci_task.rb +4 -4
  13. data/lib/kettle/dev/version.rb +1 -1
  14. data/lib/kettle/dev.rb +4 -12
  15. data/sig/kettle/dev/source_merger.rbs +40 -56
  16. data.tar.gz.sig +0 -0
  17. metadata +15 -144
  18. metadata.gz.sig +0 -0
  19. data/.aiignore.example +0 -19
  20. data/.devcontainer/apt-install/devcontainer-feature.json +0 -9
  21. data/.devcontainer/apt-install/install.sh +0 -11
  22. data/.devcontainer/devcontainer.json +0 -28
  23. data/.env.local.example +0 -31
  24. data/.envrc +0 -47
  25. data/.envrc.example +0 -51
  26. data/.envrc.no-osc.example +0 -51
  27. data/.git-hooks/commit-msg +0 -54
  28. data/.git-hooks/commit-subjects-goalie.txt +0 -8
  29. data/.git-hooks/footer-template.erb.txt +0 -16
  30. data/.git-hooks/prepare-commit-msg +0 -8
  31. data/.github/.codecov.yml.example +0 -14
  32. data/.github/FUNDING.yml +0 -13
  33. data/.github/FUNDING.yml.no-osc.example +0 -13
  34. data/.github/dependabot.yml +0 -13
  35. data/.github/workflows/ancient.yml +0 -83
  36. data/.github/workflows/ancient.yml.example +0 -81
  37. data/.github/workflows/auto-assign.yml +0 -21
  38. data/.github/workflows/codeql-analysis.yml +0 -70
  39. data/.github/workflows/coverage.yml +0 -127
  40. data/.github/workflows/coverage.yml.example +0 -127
  41. data/.github/workflows/current.yml +0 -116
  42. data/.github/workflows/current.yml.example +0 -115
  43. data/.github/workflows/dep-heads.yml +0 -117
  44. data/.github/workflows/dependency-review.yml +0 -20
  45. data/.github/workflows/discord-notifier.yml.example +0 -39
  46. data/.github/workflows/heads.yml +0 -117
  47. data/.github/workflows/heads.yml.example +0 -116
  48. data/.github/workflows/jruby.yml +0 -82
  49. data/.github/workflows/jruby.yml.example +0 -72
  50. data/.github/workflows/legacy.yml +0 -76
  51. data/.github/workflows/license-eye.yml +0 -40
  52. data/.github/workflows/locked_deps.yml +0 -85
  53. data/.github/workflows/opencollective.yml +0 -40
  54. data/.github/workflows/style.yml +0 -67
  55. data/.github/workflows/supported.yml +0 -75
  56. data/.github/workflows/truffle.yml +0 -99
  57. data/.github/workflows/unlocked_deps.yml +0 -84
  58. data/.github/workflows/unsupported.yml +0 -76
  59. data/.gitignore +0 -50
  60. data/.gitlab-ci.yml.example +0 -134
  61. data/.idea/.gitignore +0 -45
  62. data/.junie/guidelines-rbs.md +0 -49
  63. data/.junie/guidelines.md +0 -141
  64. data/.junie/guidelines.md.example +0 -140
  65. data/.licenserc.yaml +0 -7
  66. data/.opencollective.yml +0 -3
  67. data/.opencollective.yml.example +0 -3
  68. data/.qlty/qlty.toml +0 -79
  69. data/.rspec +0 -9
  70. data/.rubocop.yml +0 -13
  71. data/.rubocop_rspec.yml +0 -33
  72. data/.simplecov +0 -16
  73. data/.simplecov.example +0 -11
  74. data/.tool-versions +0 -1
  75. data/.yardignore +0 -13
  76. data/.yardopts +0 -14
  77. data/Appraisal.root.gemfile +0 -10
  78. data/Appraisals +0 -151
  79. data/Appraisals.example +0 -102
  80. data/CHANGELOG.md.example +0 -47
  81. data/CONTRIBUTING.md.example +0 -227
  82. data/FUNDING.md.no-osc.example +0 -63
  83. data/Gemfile +0 -40
  84. data/Gemfile.example +0 -34
  85. data/README.md.example +0 -570
  86. data/README.md.no-osc.example +0 -536
  87. data/Rakefile.example +0 -68
  88. data/gemfiles/modular/coverage.gemfile +0 -6
  89. data/gemfiles/modular/debug.gemfile +0 -13
  90. data/gemfiles/modular/documentation.gemfile +0 -14
  91. data/gemfiles/modular/erb/r2/v3.0.gemfile +0 -1
  92. data/gemfiles/modular/erb/r2.3/default.gemfile +0 -6
  93. data/gemfiles/modular/erb/r2.6/v2.2.gemfile +0 -3
  94. data/gemfiles/modular/erb/r3/v5.0.gemfile +0 -1
  95. data/gemfiles/modular/erb/r3.1/v4.0.gemfile +0 -2
  96. data/gemfiles/modular/erb/vHEAD.gemfile +0 -2
  97. data/gemfiles/modular/injected.gemfile +0 -60
  98. data/gemfiles/modular/mutex_m/r2/v0.3.gemfile +0 -2
  99. data/gemfiles/modular/mutex_m/r2.4/v0.1.gemfile +0 -3
  100. data/gemfiles/modular/mutex_m/r3/v0.3.gemfile +0 -2
  101. data/gemfiles/modular/mutex_m/vHEAD.gemfile +0 -2
  102. data/gemfiles/modular/optional.gemfile +0 -8
  103. data/gemfiles/modular/optional.gemfile.example +0 -5
  104. data/gemfiles/modular/runtime_heads.gemfile +0 -10
  105. data/gemfiles/modular/runtime_heads.gemfile.example +0 -8
  106. data/gemfiles/modular/stringio/r2/v3.0.gemfile +0 -5
  107. data/gemfiles/modular/stringio/r2.4/v0.0.2.gemfile +0 -4
  108. data/gemfiles/modular/stringio/r3/v3.0.gemfile +0 -5
  109. data/gemfiles/modular/stringio/vHEAD.gemfile +0 -2
  110. data/gemfiles/modular/style.gemfile +0 -25
  111. data/gemfiles/modular/style.gemfile.example +0 -25
  112. data/gemfiles/modular/templating.gemfile +0 -3
  113. data/gemfiles/modular/x_std_libs/r2/libs.gemfile +0 -3
  114. data/gemfiles/modular/x_std_libs/r2.3/libs.gemfile +0 -3
  115. data/gemfiles/modular/x_std_libs/r2.4/libs.gemfile +0 -3
  116. data/gemfiles/modular/x_std_libs/r2.6/libs.gemfile +0 -3
  117. data/gemfiles/modular/x_std_libs/r3/libs.gemfile +0 -3
  118. data/gemfiles/modular/x_std_libs/r3.1/libs.gemfile +0 -3
  119. data/gemfiles/modular/x_std_libs/vHEAD.gemfile +0 -3
  120. data/gemfiles/modular/x_std_libs.gemfile +0 -2
  121. data/kettle-dev.gemspec.example +0 -154
  122. data/lib/kettle/dev/modular_gemfiles.rb +0 -119
  123. data/lib/kettle/dev/prism_appraisals.rb +0 -351
  124. data/lib/kettle/dev/prism_gemfile.rb +0 -177
  125. data/lib/kettle/dev/prism_gemspec.rb +0 -284
  126. data/lib/kettle/dev/prism_utils.rb +0 -201
  127. data/lib/kettle/dev/rakelib/install.rake +0 -10
  128. data/lib/kettle/dev/rakelib/template.rake +0 -10
  129. data/lib/kettle/dev/setup_cli.rb +0 -403
  130. data/lib/kettle/dev/source_merger.rb +0 -622
  131. data/lib/kettle/dev/tasks/install_task.rb +0 -553
  132. data/lib/kettle/dev/tasks/template_task.rb +0 -975
  133. data/lib/kettle/dev/template_helpers.rb +0 -685
@@ -1,351 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "prism"
4
-
5
- module Kettle
6
- module Dev
7
- # AST-driven merger for Appraisals files using Prism.
8
- # Preserves all comments: preamble headers, block headers, and inline comments.
9
- # Uses PrismUtils for shared Prism AST operations.
10
- module PrismAppraisals
11
- TRACKED_METHODS = [:gem, :eval_gemfile, :gemfile].freeze
12
-
13
- module_function
14
-
15
- # Merge template and destination Appraisals files preserving comments
16
- def merge(template_content, dest_content)
17
- template_content ||= ""
18
- dest_content ||= ""
19
-
20
- return template_content if dest_content.strip.empty?
21
- return dest_content if template_content.strip.empty?
22
-
23
- tmpl_result = PrismUtils.parse_with_comments(template_content)
24
- dest_result = PrismUtils.parse_with_comments(dest_content)
25
-
26
- tmpl_preamble, tmpl_blocks = extract_blocks(tmpl_result, template_content)
27
- dest_preamble, dest_blocks = extract_blocks(dest_result, dest_content)
28
-
29
- merged_preamble = merge_preambles(tmpl_preamble, dest_preamble)
30
- merged_blocks = merge_blocks(tmpl_blocks, dest_blocks, tmpl_result, dest_result)
31
-
32
- build_output(merged_preamble, merged_blocks)
33
- end
34
-
35
- # ...existing helper methods copied from original AppraisalsAstMerger...
36
- def extract_blocks(parse_result, source_content)
37
- root = parse_result.value
38
- return [[], []] unless root&.statements&.body
39
-
40
- source_lines = source_content.lines
41
- blocks = []
42
- first_appraise_line = nil
43
-
44
- root.statements.body.each do |node|
45
- if appraise_call?(node)
46
- first_appraise_line ||= node.location.start_line
47
- name = extract_appraise_name(node)
48
- next unless name
49
-
50
- block_header = extract_block_header(node, source_lines, blocks)
51
-
52
- blocks << {
53
- node: node,
54
- name: name,
55
- header: block_header,
56
- }
57
- end
58
- end
59
-
60
- preamble_comments = if first_appraise_line
61
- parse_result.comments.select { |c| c.location.start_line < first_appraise_line }
62
- else
63
- parse_result.comments
64
- end
65
-
66
- block_header_lines = blocks.flat_map { |b| b[:header].lines.map { |l| l.strip } }.to_set
67
- preamble_comments = preamble_comments.reject { |c| block_header_lines.include?(c.slice.strip) }
68
-
69
- [preamble_comments, blocks]
70
- end
71
-
72
- def appraise_call?(node)
73
- PrismUtils.block_call_to?(node, :appraise)
74
- end
75
-
76
- def extract_appraise_name(node)
77
- return unless node.is_a?(Prism::CallNode)
78
- PrismUtils.extract_literal_value(node.arguments&.arguments&.first)
79
- end
80
-
81
- def merge_preambles(tmpl_comments, dest_comments)
82
- tmpl_lines = tmpl_comments.map { |c| c.slice.strip }
83
- dest_lines = dest_comments.map { |c| c.slice.strip }
84
-
85
- magic_pattern = /^#.*frozen_string_literal/
86
- if tmpl_lines.any? { |line| line.match?(magic_pattern) }
87
- dest_lines.reject! { |line| line.match?(magic_pattern) }
88
- end
89
-
90
- merged = []
91
- seen = Set.new
92
-
93
- (tmpl_lines + dest_lines).each do |line|
94
- normalized = line.downcase
95
- unless seen.include?(normalized)
96
- merged << line
97
- seen << normalized
98
- end
99
- end
100
-
101
- merged
102
- end
103
-
104
- def extract_block_header(node, source_lines, previous_blocks)
105
- begin_line = node.location.start_line
106
- min_line = if previous_blocks.empty?
107
- 1
108
- else
109
- previous_blocks.last[:node].location.end_line + 1
110
- end
111
- check_line = begin_line - 2
112
- header_lines = []
113
- while check_line >= 0 && (check_line + 1) >= min_line
114
- line = source_lines[check_line]
115
- break unless line
116
- if line.strip.empty?
117
- break
118
- elsif line.lstrip.start_with?("#")
119
- header_lines.unshift(line)
120
- check_line -= 1
121
- else
122
- break
123
- end
124
- end
125
- header_lines.join
126
- rescue StandardError => e
127
- Kettle::Dev.debug_error(e, __method__) if defined?(Kettle::Dev.debug_error)
128
- ""
129
- end
130
-
131
- def merge_blocks(template_blocks, dest_blocks, tmpl_result, dest_result)
132
- merged = []
133
- dest_by_name = dest_blocks.each_with_object({}) { |b, h| h[b[:name]] = b }
134
- template_names = template_blocks.map { |b| b[:name] }.to_set
135
- placed_dest = Set.new
136
-
137
- template_blocks.each_with_index do |tmpl_block, idx|
138
- name = tmpl_block[:name]
139
- if idx == 0 || dest_by_name[name]
140
- dest_blocks.each do |db|
141
- next if template_names.include?(db[:name])
142
- next if placed_dest.include?(db[:name])
143
- dest_idx_of_shared = dest_blocks.index { |b| b[:name] == name }
144
- dest_idx_of_only = dest_blocks.index { |b| b[:name] == db[:name] }
145
- if dest_idx_of_only && dest_idx_of_shared && dest_idx_of_only < dest_idx_of_shared
146
- merged << db
147
- placed_dest << db[:name]
148
- end
149
- end
150
- end
151
-
152
- dest_block = dest_by_name[name]
153
- if dest_block
154
- merged_header = merge_block_headers(tmpl_block[:header], dest_block[:header])
155
- merged_statements = merge_block_statements(
156
- tmpl_block[:node].block.body,
157
- dest_block[:node].block.body,
158
- dest_result,
159
- )
160
- merged << {
161
- name: name,
162
- header: merged_header,
163
- node: tmpl_block[:node],
164
- statements: merged_statements,
165
- }
166
- placed_dest << name
167
- else
168
- merged << tmpl_block
169
- end
170
- end
171
-
172
- dest_blocks.each do |dest_block|
173
- next if placed_dest.include?(dest_block[:name])
174
- next if template_names.include?(dest_block[:name])
175
- merged << dest_block
176
- end
177
-
178
- merged
179
- end
180
-
181
- def merge_block_headers(tmpl_header, dest_header)
182
- tmpl_lines = tmpl_header.to_s.lines.map(&:strip).reject(&:empty?)
183
- dest_lines = dest_header.to_s.lines.map(&:strip).reject(&:empty?)
184
- merged = []
185
- seen = Set.new
186
- (tmpl_lines + dest_lines).each do |line|
187
- normalized = line.downcase
188
- unless seen.include?(normalized)
189
- merged << line
190
- seen << normalized
191
- end
192
- end
193
- return "" if merged.empty?
194
- merged.join("\n") + "\n"
195
- end
196
-
197
- def merge_block_statements(tmpl_body, dest_body, dest_result)
198
- tmpl_stmts = PrismUtils.extract_statements(tmpl_body)
199
- dest_stmts = PrismUtils.extract_statements(dest_body)
200
-
201
- tmpl_keys = Set.new
202
- tmpl_key_to_node = {}
203
- tmpl_stmts.each do |stmt|
204
- key = statement_key(stmt)
205
- if key
206
- tmpl_keys << key
207
- tmpl_key_to_node[key] = stmt
208
- end
209
- end
210
-
211
- dest_keys = Set.new
212
- dest_stmts.each do |stmt|
213
- key = statement_key(stmt)
214
- dest_keys << key if key
215
- end
216
-
217
- merged = []
218
- dest_stmts.each_with_index do |dest_stmt, idx|
219
- dest_key = statement_key(dest_stmt)
220
-
221
- if dest_key && tmpl_keys.include?(dest_key)
222
- merged << {node: tmpl_key_to_node[dest_key], inline_comments: [], leading_comments: [], shared: true, key: dest_key}
223
- else
224
- inline_comments = PrismUtils.inline_comments_for_node(dest_result, dest_stmt)
225
- prev_stmt = (idx > 0) ? dest_stmts[idx - 1] : nil
226
- leading_comments = PrismUtils.find_leading_comments(dest_result, dest_stmt, prev_stmt, dest_body)
227
- merged << {node: dest_stmt, inline_comments: inline_comments, leading_comments: leading_comments, shared: false}
228
- end
229
- end
230
-
231
- tmpl_stmts.each do |tmpl_stmt|
232
- tmpl_key = statement_key(tmpl_stmt)
233
- unless tmpl_key && dest_keys.include?(tmpl_key)
234
- merged << {node: tmpl_stmt, inline_comments: [], leading_comments: [], shared: false}
235
- end
236
- end
237
-
238
- merged.each do |item|
239
- item.delete(:shared)
240
- item.delete(:key)
241
- end
242
-
243
- merged
244
- end
245
-
246
- def statement_key(node)
247
- PrismUtils.statement_key(node, tracked_methods: TRACKED_METHODS)
248
- end
249
-
250
- def build_output(preamble_lines, blocks)
251
- output = []
252
- preamble_lines.each { |line| output << line }
253
- output << "" unless preamble_lines.empty?
254
-
255
- blocks.each do |block|
256
- header = block[:header]
257
- if header && !header.strip.empty?
258
- output << header.rstrip
259
- end
260
-
261
- name = block[:name]
262
- output << "appraise(\"#{name}\") {"
263
-
264
- statements = block[:statements] || extract_original_statements(block[:node])
265
- statements.each do |stmt_info|
266
- leading = stmt_info[:leading_comments] || []
267
- leading.each do |comment|
268
- output << " #{comment.slice.strip}"
269
- end
270
-
271
- node = stmt_info[:node]
272
- line = normalize_statement(node)
273
- # Remove any leading whitespace/newlines from the normalized line
274
- line = line.to_s.sub(/\A\s+/, "")
275
-
276
- inline = stmt_info[:inline_comments] || []
277
- inline_str = inline.map { |c| c.slice.strip }.join(" ")
278
- output << " #{line}#{" " + inline_str unless inline_str.empty?}"
279
- end
280
-
281
- output << "}"
282
- output << ""
283
- end
284
-
285
- build = output.join("\n").strip + "\n"
286
- build
287
- end
288
-
289
- def normalize_statement(node)
290
- return PrismUtils.node_to_source(node) unless node.is_a?(Prism::CallNode)
291
- PrismUtils.normalize_call_node(node)
292
- end
293
-
294
- def normalize_argument(arg)
295
- PrismUtils.normalize_argument(arg)
296
- end
297
-
298
- def extract_original_statements(node)
299
- body = node.block&.body
300
- return [] unless body
301
- statements = body.is_a?(Prism::StatementsNode) ? body.body : [body]
302
- statements.compact.map { |stmt| {node: stmt, inline_comments: [], leading_comments: []} }
303
- end
304
-
305
- # Remove gem calls that reference the given gem name (to prevent self-dependency).
306
- # Works by locating gem() call nodes within appraise blocks where the first argument matches gem_name.
307
- # @param content [String] Appraisals content
308
- # @param gem_name [String] the gem name to remove
309
- # @return [String] modified content with self-referential gem calls removed
310
- def remove_gem_dependency(content, gem_name)
311
- return content if gem_name.to_s.strip.empty?
312
-
313
- result = PrismUtils.parse_with_comments(content)
314
- root = result.value
315
- return content unless root&.statements&.body
316
-
317
- out = content.dup
318
-
319
- # Iterate through all appraise blocks
320
- root.statements.body.each do |node|
321
- next unless appraise_call?(node)
322
- next unless node.block&.body
323
-
324
- body_stmts = PrismUtils.extract_statements(node.block.body)
325
-
326
- # Find gem call nodes within this appraise block where first argument matches gem_name
327
- body_stmts.each do |stmt|
328
- next unless stmt.is_a?(Prism::CallNode) && stmt.name == :gem
329
-
330
- first_arg = stmt.arguments&.arguments&.first
331
- arg_val = begin
332
- PrismUtils.extract_literal_value(first_arg)
333
- rescue StandardError
334
- nil
335
- end
336
-
337
- if arg_val && arg_val.to_s == gem_name.to_s
338
- # Remove this gem call from content
339
- out = out.sub(stmt.slice, "")
340
- end
341
- end
342
- end
343
-
344
- out
345
- rescue StandardError => e
346
- Kettle::Dev.debug_error(e, __method__) if defined?(Kettle::Dev.debug_error)
347
- content
348
- end
349
- end
350
- end
351
- end
@@ -1,177 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Kettle
4
- module Dev
5
- # Prism helpers for Gemfile-like merging.
6
- module PrismGemfile
7
- module_function
8
-
9
- # Merge gem calls from src_content into dest_content.
10
- # - Replaces dest `source` call with src's if present.
11
- # - Replaces or inserts non-comment `git_source` definitions.
12
- # - Appends missing `gem` calls (by name) from src to dest preserving dest content and newlines.
13
- # This is a conservative, comment-preserving approach using Prism to detect call nodes.
14
- def merge_gem_calls(src_content, dest_content)
15
- src_res = PrismUtils.parse_with_comments(src_content)
16
- dest_res = PrismUtils.parse_with_comments(dest_content)
17
-
18
- src_stmts = PrismUtils.extract_statements(src_res.value.statements)
19
- dest_stmts = PrismUtils.extract_statements(dest_res.value.statements)
20
-
21
- # Find source nodes
22
- src_source_node = src_stmts.find { |n| PrismUtils.call_to?(n, :source) }
23
- dest_source_node = dest_stmts.find { |n| PrismUtils.call_to?(n, :source) }
24
-
25
- out = dest_content.dup
26
- dest_lines = out.lines
27
-
28
- # Replace or insert source line
29
- if src_source_node
30
- src_src = src_source_node.slice
31
- if dest_source_node
32
- out = out.sub(dest_source_node.slice, src_src)
33
- dest_lines = out.lines
34
- else
35
- # insert after any leading comment/blank block
36
- insert_idx = 0
37
- while insert_idx < dest_lines.length && (dest_lines[insert_idx].strip.empty? || dest_lines[insert_idx].lstrip.start_with?("#"))
38
- insert_idx += 1
39
- end
40
- dest_lines.insert(insert_idx, src_src.rstrip + "\n")
41
- out = dest_lines.join
42
- dest_lines = out.lines
43
- end
44
- end
45
-
46
- # --- Handle git_source replacement/insertion ---
47
- src_git_nodes = src_stmts.select { |n| PrismUtils.call_to?(n, :git_source) }
48
- if src_git_nodes.any?
49
- # We'll operate on dest_lines for insertion; recompute dest_stmts if we changed out
50
- dest_res = PrismUtils.parse_with_comments(out)
51
- dest_stmts = PrismUtils.extract_statements(dest_res.value.statements)
52
-
53
- # Iterate in reverse when inserting so that inserting at the same index
54
- # preserves the original order from the source (we insert at a fixed index).
55
- src_git_nodes.reverse_each do |gnode|
56
- key = PrismUtils.statement_key(gnode) # => [:git_source, name]
57
- name = key && key[1]
58
- replaced = false
59
-
60
- if name
61
- dest_same_idx = dest_stmts.index { |d| PrismUtils.statement_key(d) && PrismUtils.statement_key(d)[0] == :git_source && PrismUtils.statement_key(d)[1] == name }
62
- if dest_same_idx
63
- # Replace the matching dest node slice
64
- out = out.sub(dest_stmts[dest_same_idx].slice, gnode.slice)
65
- replaced = true
66
- end
67
- end
68
-
69
- # If not replaced, prefer to replace an existing github entry in destination
70
- # (this mirrors previous behavior in template_helpers which favored replacing
71
- # a github git_source when inserting others).
72
- unless replaced
73
- dest_github_idx = dest_stmts.index { |d| PrismUtils.statement_key(d) && PrismUtils.statement_key(d)[0] == :git_source && PrismUtils.statement_key(d)[1] == "github" }
74
- if dest_github_idx
75
- out = out.sub(dest_stmts[dest_github_idx].slice, gnode.slice)
76
- replaced = true
77
- end
78
- end
79
-
80
- unless replaced
81
- # Insert below source line if present, else at top after comments
82
- dest_lines = out.lines
83
- insert_idx = dest_lines.index { |ln| !ln.strip.start_with?("#") && ln =~ /^\s*source\s+/ } || 0
84
- insert_idx += 1 if insert_idx
85
- dest_lines.insert(insert_idx, gnode.slice.rstrip + "\n")
86
- out = dest_lines.join
87
- end
88
-
89
- # Recompute dest_stmts for subsequent iterations
90
- dest_res = PrismUtils.parse_with_comments(out)
91
- dest_stmts = PrismUtils.extract_statements(dest_res.value.statements)
92
- end
93
- end
94
-
95
- # Collect gem names present in dest (top-level only)
96
- dest_res = PrismUtils.parse_with_comments(out)
97
- dest_stmts = PrismUtils.extract_statements(dest_res.value.statements)
98
- dest_gem_names = dest_stmts.map { |n| PrismUtils.statement_key(n) }.compact.select { |k| k[0] == :gem }.map { |k| k[1] }.to_set
99
-
100
- # Find gem call nodes in src and append missing ones (top-level only)
101
- missing_nodes = src_stmts.select do |n|
102
- k = PrismUtils.statement_key(n)
103
- k && k.first == :gem && !dest_gem_names.include?(k[1])
104
- end
105
- if missing_nodes.any?
106
- out << "\n" unless out.end_with?("\n") || out.empty?
107
- missing_nodes.each do |n|
108
- # Preserve inline comments for the source node when appending
109
- inline = begin
110
- PrismUtils.inline_comments_for_node(src_res, n)
111
- rescue
112
- []
113
- end
114
- line = n.slice.rstrip
115
- if inline && inline.any?
116
- inline_text = inline.map { |c| c.slice.strip }.join(" ")
117
- # Only append the inline text if it's not already part of the slice
118
- line = line + " " + inline_text unless line.include?(inline_text)
119
- end
120
- out << line + "\n"
121
- end
122
- end
123
-
124
- out
125
- rescue StandardError => e
126
- # Use debug_log if available, otherwise Kettle::Dev.debug_error
127
- if defined?(Kettle::Dev) && Kettle::Dev.respond_to?(:debug_error)
128
- Kettle::Dev.debug_error(e, __method__)
129
- else
130
- Kernel.warn("[#{__method__}] #{e.class}: #{e.message}")
131
- end
132
- dest_content
133
- end
134
-
135
- # Remove gem calls that reference the given gem name (to prevent self-dependency).
136
- # Works by locating gem() call nodes where the first argument matches gem_name.
137
- # @param content [String] Gemfile-like content
138
- # @param gem_name [String] the gem name to remove
139
- # @return [String] modified content with self-referential gem calls removed
140
- def remove_gem_dependency(content, gem_name)
141
- return content if gem_name.to_s.strip.empty?
142
-
143
- result = PrismUtils.parse_with_comments(content)
144
- stmts = PrismUtils.extract_statements(result.value.statements)
145
-
146
- # Find gem call nodes where first argument matches gem_name
147
- gem_nodes = stmts.select do |n|
148
- next false unless n.is_a?(Prism::CallNode) && n.name == :gem
149
-
150
- first_arg = n.arguments&.arguments&.first
151
- arg_val = begin
152
- PrismUtils.extract_literal_value(first_arg)
153
- rescue StandardError
154
- nil
155
- end
156
- arg_val && arg_val.to_s == gem_name.to_s
157
- end
158
-
159
- # Remove each matching gem call from content
160
- out = content.dup
161
- gem_nodes.each do |gn|
162
- # Remove the entire line(s) containing this node
163
- out = out.sub(gn.slice, "")
164
- end
165
-
166
- out
167
- rescue StandardError => e
168
- if defined?(Kettle::Dev) && Kettle::Dev.respond_to?(:debug_error)
169
- Kettle::Dev.debug_error(e, __method__)
170
- else
171
- Kernel.warn("[#{__method__}] #{e.class}: #{e.message}")
172
- end
173
- content
174
- end
175
- end
176
- end
177
- end