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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +2 -1
- data/CHANGELOG.md +8 -1
- data/Rakefile.example +1 -1
- data/lib/kettle/dev/modular_gemfiles.rb +6 -5
- data/lib/kettle/dev/{appraisals_ast_merger.rb → prism_appraisals.rb} +8 -85
- data/lib/kettle/dev/prism_gemfile.rb +136 -0
- data/lib/kettle/dev/prism_gemspec.rb +284 -0
- data/lib/kettle/dev/prism_utils.rb +13 -0
- data/lib/kettle/dev/readme_backers.rb +1 -4
- data/lib/kettle/dev/setup_cli.rb +17 -28
- data/lib/kettle/dev/source_merger.rb +116 -3
- data/lib/kettle/dev/tasks/template_task.rb +21 -85
- data/lib/kettle/dev/template_helpers.rb +8 -116
- data/lib/kettle/dev/version.rb +1 -1
- data/lib/kettle/dev.rb +21 -3
- data.tar.gz.sig +0 -0
- metadata +7 -5
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cf48cdba2c3cbe1c95eab4c61a4073f7849fda6b7dbc41ec2dedaca59b414242
|
|
4
|
+
data.tar.gz: 171598b7227ced01474c97a14a8167d3c6c64785b08daeb6996e64d1ed42973d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 224a2b34db6d14760ae6105eb67407baebb25a67d7ac1ccad37eaa01823278d3d6c68e6822430f47d986c756f232ce637df248184ec61ca7a4394df3c4ced4c8
|
|
7
|
+
data.tar.gz: f8d133803fdbcf798e05faf1749a3e03efc539d5513142bbe76da9b3954662cf043ae868b79151c0c1cb84129f15b4e3e3af7a9764e301d14134fa352ba38ff5
|
checksums.yaml.gz.sig
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
YGZ�ܧ*٫e�.ڴ��C��G�7���QMhz�G��o����7v��<^�x��g�|2��������I@ -N�9pG}��D�^��ӓU���F�nA�|3oރ���苊E�
|
|
2
|
+
Z�ff�^v���hP�xvD���ޓ���HV�~60-6���h��UM��/q�o0}�dy+���k��3�����tr:��t��$�u����%`���{�`z!XBG+�$[��z��_�Y</l�3���)�X�; �c����U=��l$��a��<O0��q��۠�U�nr�f�1y����J �V� ���T7�T���=�O��
|
data/CHANGELOG.md
CHANGED
|
@@ -20,6 +20,13 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
20
20
|
|
|
21
21
|
### Added
|
|
22
22
|
|
|
23
|
+
- Prism AST-based manipulation of ruby during templating
|
|
24
|
+
- Gemfiles
|
|
25
|
+
- gemspecs
|
|
26
|
+
- .simplecov
|
|
27
|
+
- Stop rescuing Exception in certain scenarios (just StandardError)
|
|
28
|
+
- Refactored logging logic and documentation
|
|
29
|
+
|
|
23
30
|
### Changed
|
|
24
31
|
|
|
25
32
|
### Deprecated
|
|
@@ -30,7 +37,7 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
30
37
|
|
|
31
38
|
### Security
|
|
32
39
|
|
|
33
|
-
## [1.2.
|
|
40
|
+
## [1.2.1] - 2025-11-25
|
|
34
41
|
|
|
35
42
|
- TAG: [v1.2.0][1.2.0t]
|
|
36
43
|
- COVERAGE: 94.38% -- 4066/4308 lines in 26 files
|
data/Rakefile.example
CHANGED
|
@@ -36,9 +36,9 @@ module Kettle
|
|
|
36
36
|
modular_gemfile = "#{base}.gemfile"
|
|
37
37
|
src = helpers.prefer_example(File.join(gem_checkout_root, MODULAR_GEMFILE_DIR, modular_gemfile))
|
|
38
38
|
dest = File.join(project_root, MODULAR_GEMFILE_DIR, modular_gemfile)
|
|
39
|
-
existing = File.exist?(dest) ? File.read(dest) : nil
|
|
40
39
|
helpers.copy_file_with_prompt(src, dest, allow_create: true, allow_replace: true) do |content|
|
|
41
|
-
|
|
40
|
+
# Use apply_strategy for proper AST-based merging with Prism
|
|
41
|
+
helpers.apply_strategy(content, dest)
|
|
42
42
|
end
|
|
43
43
|
end
|
|
44
44
|
|
|
@@ -46,7 +46,6 @@ module Kettle
|
|
|
46
46
|
modular_gemfile = "style.gemfile"
|
|
47
47
|
src = helpers.prefer_example(File.join(gem_checkout_root, MODULAR_GEMFILE_DIR, modular_gemfile))
|
|
48
48
|
dest = File.join(project_root, MODULAR_GEMFILE_DIR, modular_gemfile)
|
|
49
|
-
existing_style = File.exist?(dest) ? File.read(dest) : nil
|
|
50
49
|
if File.basename(src).sub(/\.example\z/, "") == "style.gemfile"
|
|
51
50
|
helpers.copy_file_with_prompt(src, dest, allow_create: true, allow_replace: true) do |content|
|
|
52
51
|
# Adjust rubocop-lts constraint based on min_ruby
|
|
@@ -98,11 +97,13 @@ module Kettle
|
|
|
98
97
|
token = "{RUBOCOP|RUBY|GEM}"
|
|
99
98
|
content.gsub!(token, "rubocop-ruby#{rubocop_ruby_gem_version}") if content.include?(token)
|
|
100
99
|
end
|
|
101
|
-
|
|
100
|
+
# Use apply_strategy for proper AST-based merging with Prism
|
|
101
|
+
helpers.apply_strategy(content, dest)
|
|
102
102
|
end
|
|
103
103
|
else
|
|
104
104
|
helpers.copy_file_with_prompt(src, dest, allow_create: true, allow_replace: true) do |content|
|
|
105
|
-
|
|
105
|
+
# Use apply_strategy for proper AST-based merging with Prism
|
|
106
|
+
helpers.apply_strategy(content, dest)
|
|
106
107
|
end
|
|
107
108
|
end
|
|
108
109
|
|
|
@@ -1,34 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "prism"
|
|
4
|
-
require "kettle/dev/prism_utils"
|
|
5
4
|
|
|
6
5
|
module Kettle
|
|
7
6
|
module Dev
|
|
8
7
|
# AST-driven merger for Appraisals files using Prism.
|
|
9
8
|
# Preserves all comments: preamble headers, block headers, and inline comments.
|
|
10
9
|
# Uses PrismUtils for shared Prism AST operations.
|
|
11
|
-
module
|
|
10
|
+
module PrismAppraisals
|
|
12
11
|
TRACKED_METHODS = [:gem, :eval_gemfile, :gemfile].freeze
|
|
13
12
|
|
|
14
13
|
module_function
|
|
15
14
|
|
|
16
15
|
# Merge template and destination Appraisals files preserving comments
|
|
17
|
-
#
|
|
18
|
-
# Merges appraise blocks by name, preserving:
|
|
19
|
-
# - Preamble comments (deduplicated)
|
|
20
|
-
# - Block headers (comments before each appraise block)
|
|
21
|
-
# - Inline comments on gem/eval_gemfile statements
|
|
22
|
-
# - Leading comments before statements
|
|
23
|
-
#
|
|
24
|
-
# @param template_content [String] Template Appraisals content
|
|
25
|
-
# @param dest_content [String] Destination Appraisals content
|
|
26
|
-
# @return [String] Merged Appraisals content with preserved comments
|
|
27
|
-
# @example
|
|
28
|
-
# AppraisalsAstMerger.merge(
|
|
29
|
-
# 'appraise "rails-7" { gem "rails", "~> 7.0" }',
|
|
30
|
-
# 'appraise "custom" { gem "foo" }'
|
|
31
|
-
# )
|
|
32
16
|
def merge(template_content, dest_content)
|
|
33
17
|
template_content ||= ""
|
|
34
18
|
dest_content ||= ""
|
|
@@ -36,24 +20,19 @@ module Kettle
|
|
|
36
20
|
return template_content if dest_content.strip.empty?
|
|
37
21
|
return dest_content if template_content.strip.empty?
|
|
38
22
|
|
|
39
|
-
# Parse with Prism to get AST and comments
|
|
40
23
|
tmpl_result = PrismUtils.parse_with_comments(template_content)
|
|
41
24
|
dest_result = PrismUtils.parse_with_comments(dest_content)
|
|
42
25
|
|
|
43
|
-
# Extract preamble and blocks with their headers
|
|
44
26
|
tmpl_preamble, tmpl_blocks = extract_blocks(tmpl_result, template_content)
|
|
45
27
|
dest_preamble, dest_blocks = extract_blocks(dest_result, dest_content)
|
|
46
28
|
|
|
47
|
-
# Merge preambles
|
|
48
29
|
merged_preamble = merge_preambles(tmpl_preamble, dest_preamble)
|
|
49
|
-
|
|
50
|
-
# Merge blocks
|
|
51
30
|
merged_blocks = merge_blocks(tmpl_blocks, dest_blocks, tmpl_result, dest_result)
|
|
52
31
|
|
|
53
|
-
# Build output
|
|
54
32
|
build_output(merged_preamble, merged_blocks)
|
|
55
33
|
end
|
|
56
34
|
|
|
35
|
+
# ...existing helper methods copied from original AppraisalsAstMerger...
|
|
57
36
|
def extract_blocks(parse_result, source_content)
|
|
58
37
|
root = parse_result.value
|
|
59
38
|
return [[], []] unless root&.statements&.body
|
|
@@ -62,14 +41,12 @@ module Kettle
|
|
|
62
41
|
blocks = []
|
|
63
42
|
first_appraise_line = nil
|
|
64
43
|
|
|
65
|
-
# Find all appraise blocks
|
|
66
44
|
root.statements.body.each do |node|
|
|
67
45
|
if appraise_call?(node)
|
|
68
46
|
first_appraise_line ||= node.location.start_line
|
|
69
47
|
name = extract_appraise_name(node)
|
|
70
48
|
next unless name
|
|
71
49
|
|
|
72
|
-
# Extract block header (comments immediately before this block)
|
|
73
50
|
block_header = extract_block_header(node, source_lines, blocks)
|
|
74
51
|
|
|
75
52
|
blocks << {
|
|
@@ -80,14 +57,12 @@ module Kettle
|
|
|
80
57
|
end
|
|
81
58
|
end
|
|
82
59
|
|
|
83
|
-
# Preamble is all comments before first appraise block
|
|
84
60
|
preamble_comments = if first_appraise_line
|
|
85
61
|
parse_result.comments.select { |c| c.location.start_line < first_appraise_line }
|
|
86
62
|
else
|
|
87
63
|
parse_result.comments
|
|
88
64
|
end
|
|
89
65
|
|
|
90
|
-
# Filter out comments that are part of block headers
|
|
91
66
|
block_header_lines = blocks.flat_map { |b| b[:header].lines.map { |l| l.strip } }.to_set
|
|
92
67
|
preamble_comments = preamble_comments.reject { |c| block_header_lines.include?(c.slice.strip) }
|
|
93
68
|
|
|
@@ -100,7 +75,6 @@ module Kettle
|
|
|
100
75
|
|
|
101
76
|
def extract_appraise_name(node)
|
|
102
77
|
return unless node.is_a?(Prism::CallNode)
|
|
103
|
-
# Use PrismUtils for extracting literal values
|
|
104
78
|
PrismUtils.extract_literal_value(node.arguments&.arguments&.first)
|
|
105
79
|
end
|
|
106
80
|
|
|
@@ -108,13 +82,11 @@ module Kettle
|
|
|
108
82
|
tmpl_lines = tmpl_comments.map { |c| c.slice.strip }
|
|
109
83
|
dest_lines = dest_comments.map { |c| c.slice.strip }
|
|
110
84
|
|
|
111
|
-
# Remove magic comments from dest if template has them
|
|
112
85
|
magic_pattern = /^#.*frozen_string_literal/
|
|
113
86
|
if tmpl_lines.any? { |line| line.match?(magic_pattern) }
|
|
114
87
|
dest_lines.reject! { |line| line.match?(magic_pattern) }
|
|
115
88
|
end
|
|
116
89
|
|
|
117
|
-
# Merge unique lines (case-insensitive), template first
|
|
118
90
|
merged = []
|
|
119
91
|
seen = Set.new
|
|
120
92
|
|
|
@@ -130,38 +102,26 @@ module Kettle
|
|
|
130
102
|
end
|
|
131
103
|
|
|
132
104
|
def extract_block_header(node, source_lines, previous_blocks)
|
|
133
|
-
# Get the line number where this block starts (1-indexed from Prism)
|
|
134
105
|
begin_line = node.location.start_line
|
|
135
|
-
|
|
136
|
-
# Find the end of the previous block or start of file
|
|
137
106
|
min_line = if previous_blocks.empty?
|
|
138
107
|
1
|
|
139
108
|
else
|
|
140
109
|
previous_blocks.last[:node].location.end_line + 1
|
|
141
110
|
end
|
|
142
|
-
|
|
143
|
-
# Convert to 0-indexed and look at the line before the block
|
|
144
|
-
check_line = begin_line - 2 # e.g., if block is on line 2 (1-indexed), check line 0 (0-indexed, which is line 1 in file)
|
|
145
|
-
|
|
146
|
-
# Look backwards from the block start to find contiguous comment lines
|
|
111
|
+
check_line = begin_line - 2
|
|
147
112
|
header_lines = []
|
|
148
|
-
|
|
149
113
|
while check_line >= 0 && (check_line + 1) >= min_line
|
|
150
114
|
line = source_lines[check_line]
|
|
151
115
|
break unless line
|
|
152
|
-
|
|
153
|
-
# Stop at empty line
|
|
154
116
|
if line.strip.empty?
|
|
155
117
|
break
|
|
156
118
|
elsif line.lstrip.start_with?("#")
|
|
157
119
|
header_lines.unshift(line)
|
|
158
120
|
check_line -= 1
|
|
159
121
|
else
|
|
160
|
-
# Non-comment, non-blank line - stop
|
|
161
122
|
break
|
|
162
123
|
end
|
|
163
124
|
end
|
|
164
|
-
|
|
165
125
|
header_lines.join
|
|
166
126
|
rescue StandardError => e
|
|
167
127
|
Kettle::Dev.debug_error(e, __method__) if defined?(Kettle::Dev.debug_error)
|
|
@@ -172,27 +132,16 @@ module Kettle
|
|
|
172
132
|
merged = []
|
|
173
133
|
dest_by_name = dest_blocks.each_with_object({}) { |b, h| h[b[:name]] = b }
|
|
174
134
|
template_names = template_blocks.map { |b| b[:name] }.to_set
|
|
175
|
-
|
|
176
|
-
# Track which dest blocks we've already placed
|
|
177
135
|
placed_dest = Set.new
|
|
178
136
|
|
|
179
|
-
# For each template block, merge it and insert any dest-only blocks that come before it
|
|
180
137
|
template_blocks.each_with_index do |tmpl_block, idx|
|
|
181
138
|
name = tmpl_block[:name]
|
|
182
|
-
|
|
183
|
-
# Find dest-only blocks that should come before this template block
|
|
184
|
-
# (i.e., blocks that appear in dest before this shared block)
|
|
185
139
|
if idx == 0 || dest_by_name[name]
|
|
186
|
-
# For first template block or when we have a dest version of this block,
|
|
187
|
-
# check if there are dest-only blocks to insert before it
|
|
188
140
|
dest_blocks.each do |db|
|
|
189
141
|
next if template_names.include?(db[:name])
|
|
190
142
|
next if placed_dest.include?(db[:name])
|
|
191
|
-
|
|
192
|
-
# Check if this dest-only block comes before current shared block in dest
|
|
193
143
|
dest_idx_of_shared = dest_blocks.index { |b| b[:name] == name }
|
|
194
144
|
dest_idx_of_only = dest_blocks.index { |b| b[:name] == db[:name] }
|
|
195
|
-
|
|
196
145
|
if dest_idx_of_only && dest_idx_of_shared && dest_idx_of_only < dest_idx_of_shared
|
|
197
146
|
merged << db
|
|
198
147
|
placed_dest << db[:name]
|
|
@@ -200,10 +149,8 @@ module Kettle
|
|
|
200
149
|
end
|
|
201
150
|
end
|
|
202
151
|
|
|
203
|
-
# Now add the template block (merged or template-only)
|
|
204
152
|
dest_block = dest_by_name[name]
|
|
205
153
|
if dest_block
|
|
206
|
-
# Merge this block
|
|
207
154
|
merged_header = merge_block_headers(tmpl_block[:header], dest_block[:header])
|
|
208
155
|
merged_statements = merge_block_statements(
|
|
209
156
|
tmpl_block[:node].block.body,
|
|
@@ -218,12 +165,10 @@ module Kettle
|
|
|
218
165
|
}
|
|
219
166
|
placed_dest << name
|
|
220
167
|
else
|
|
221
|
-
# Template-only block
|
|
222
168
|
merged << tmpl_block
|
|
223
169
|
end
|
|
224
170
|
end
|
|
225
171
|
|
|
226
|
-
# Add any remaining destination-only blocks that haven't been placed
|
|
227
172
|
dest_blocks.each do |dest_block|
|
|
228
173
|
next if placed_dest.include?(dest_block[:name])
|
|
229
174
|
next if template_names.include?(dest_block[:name])
|
|
@@ -236,11 +181,8 @@ module Kettle
|
|
|
236
181
|
def merge_block_headers(tmpl_header, dest_header)
|
|
237
182
|
tmpl_lines = tmpl_header.to_s.lines.map(&:strip).reject(&:empty?)
|
|
238
183
|
dest_lines = dest_header.to_s.lines.map(&:strip).reject(&:empty?)
|
|
239
|
-
|
|
240
|
-
# Merge without duplicates (case-insensitive comparison), template first
|
|
241
184
|
merged = []
|
|
242
185
|
seen = Set.new
|
|
243
|
-
|
|
244
186
|
(tmpl_lines + dest_lines).each do |line|
|
|
245
187
|
normalized = line.downcase
|
|
246
188
|
unless seen.include?(normalized)
|
|
@@ -248,7 +190,6 @@ module Kettle
|
|
|
248
190
|
seen << normalized
|
|
249
191
|
end
|
|
250
192
|
end
|
|
251
|
-
|
|
252
193
|
return "" if merged.empty?
|
|
253
194
|
merged.join("\n") + "\n"
|
|
254
195
|
end
|
|
@@ -257,7 +198,6 @@ module Kettle
|
|
|
257
198
|
tmpl_stmts = PrismUtils.extract_statements(tmpl_body)
|
|
258
199
|
dest_stmts = PrismUtils.extract_statements(dest_body)
|
|
259
200
|
|
|
260
|
-
# Build statement keys for both
|
|
261
201
|
tmpl_keys = Set.new
|
|
262
202
|
tmpl_key_to_node = {}
|
|
263
203
|
tmpl_stmts.each do |stmt|
|
|
@@ -274,27 +214,20 @@ module Kettle
|
|
|
274
214
|
dest_keys << key if key
|
|
275
215
|
end
|
|
276
216
|
|
|
277
|
-
# Process dest statements in order, preserving their position and comments
|
|
278
217
|
merged = []
|
|
279
218
|
dest_stmts.each_with_index do |dest_stmt, idx|
|
|
280
219
|
dest_key = statement_key(dest_stmt)
|
|
281
220
|
|
|
282
221
|
if dest_key && tmpl_keys.include?(dest_key)
|
|
283
|
-
# Shared statement - use template version but preserve dest position
|
|
284
|
-
# Add it with no comments (template version is canonical)
|
|
285
222
|
merged << {node: tmpl_key_to_node[dest_key], inline_comments: [], leading_comments: [], shared: true, key: dest_key}
|
|
286
223
|
else
|
|
287
|
-
# Dest-only statement - preserve with all comments
|
|
288
224
|
inline_comments = PrismUtils.inline_comments_for_node(dest_result, dest_stmt)
|
|
289
|
-
|
|
290
225
|
prev_stmt = (idx > 0) ? dest_stmts[idx - 1] : nil
|
|
291
226
|
leading_comments = PrismUtils.find_leading_comments(dest_result, dest_stmt, prev_stmt, dest_body)
|
|
292
|
-
|
|
293
227
|
merged << {node: dest_stmt, inline_comments: inline_comments, leading_comments: leading_comments, shared: false}
|
|
294
228
|
end
|
|
295
229
|
end
|
|
296
230
|
|
|
297
|
-
# Add template-only statements (those not in dest) at the end
|
|
298
231
|
tmpl_stmts.each do |tmpl_stmt|
|
|
299
232
|
tmpl_key = statement_key(tmpl_stmt)
|
|
300
233
|
unless tmpl_key && dest_keys.include?(tmpl_key)
|
|
@@ -302,7 +235,6 @@ module Kettle
|
|
|
302
235
|
end
|
|
303
236
|
end
|
|
304
237
|
|
|
305
|
-
# Clean up - remove the tracking fields
|
|
306
238
|
merged.each do |item|
|
|
307
239
|
item.delete(:shared)
|
|
308
240
|
item.delete(:key)
|
|
@@ -312,69 +244,60 @@ module Kettle
|
|
|
312
244
|
end
|
|
313
245
|
|
|
314
246
|
def statement_key(node)
|
|
315
|
-
# Use PrismUtils for statement key generation with Appraisals-specific tracked methods
|
|
316
247
|
PrismUtils.statement_key(node, tracked_methods: TRACKED_METHODS)
|
|
317
248
|
end
|
|
318
249
|
|
|
319
250
|
def build_output(preamble_lines, blocks)
|
|
320
251
|
output = []
|
|
321
|
-
|
|
322
|
-
# Add preamble
|
|
323
252
|
preamble_lines.each { |line| output << line }
|
|
324
253
|
output << "" unless preamble_lines.empty?
|
|
325
254
|
|
|
326
|
-
# Add blocks
|
|
327
255
|
blocks.each do |block|
|
|
328
|
-
# Add block header (no blank line before it)
|
|
329
256
|
header = block[:header]
|
|
330
257
|
if header && !header.strip.empty?
|
|
331
258
|
output << header.rstrip
|
|
332
259
|
end
|
|
333
260
|
|
|
334
|
-
# Add appraise call - using parentheses and curly braces format
|
|
335
261
|
name = block[:name]
|
|
336
262
|
output << "appraise(\"#{name}\") {"
|
|
337
263
|
|
|
338
|
-
# Add statements
|
|
339
264
|
statements = block[:statements] || extract_original_statements(block[:node])
|
|
340
265
|
statements.each do |stmt_info|
|
|
341
|
-
# Add any leading comments before this statement
|
|
342
266
|
leading = stmt_info[:leading_comments] || []
|
|
343
267
|
leading.each do |comment|
|
|
344
268
|
output << " #{comment.slice.strip}"
|
|
345
269
|
end
|
|
346
270
|
|
|
347
271
|
node = stmt_info[:node]
|
|
348
|
-
# Normalize the statement to use parentheses
|
|
349
272
|
line = normalize_statement(node)
|
|
273
|
+
# Remove any leading whitespace/newlines from the normalized line
|
|
274
|
+
line = line.to_s.sub(/\A\s+/, "")
|
|
350
275
|
|
|
351
276
|
inline = stmt_info[:inline_comments] || []
|
|
352
277
|
inline_str = inline.map { |c| c.slice.strip }.join(" ")
|
|
353
|
-
output << " #{line}#{"
|
|
278
|
+
output << " #{line}#{" " + inline_str unless inline_str.empty?}"
|
|
354
279
|
end
|
|
355
280
|
|
|
356
281
|
output << "}"
|
|
357
282
|
output << ""
|
|
358
283
|
end
|
|
359
284
|
|
|
360
|
-
output.join("\n").strip + "\n"
|
|
285
|
+
build = output.join("\n").strip + "\n"
|
|
286
|
+
build
|
|
361
287
|
end
|
|
362
288
|
|
|
363
289
|
def normalize_statement(node)
|
|
364
|
-
# Use PrismUtils for normalizing call nodes
|
|
365
290
|
return PrismUtils.node_to_source(node) unless node.is_a?(Prism::CallNode)
|
|
366
291
|
PrismUtils.normalize_call_node(node)
|
|
367
292
|
end
|
|
368
293
|
|
|
369
294
|
def normalize_argument(arg)
|
|
370
|
-
# Use PrismUtils for argument normalization
|
|
371
295
|
PrismUtils.normalize_argument(arg)
|
|
372
296
|
end
|
|
373
297
|
|
|
374
298
|
def extract_original_statements(node)
|
|
375
299
|
body = node.block&.body
|
|
376
300
|
return [] unless body
|
|
377
|
-
|
|
378
301
|
statements = body.is_a?(Prism::StatementsNode) ? body.body : [body]
|
|
379
302
|
statements.compact.map { |stmt| {node: stmt, inline_comments: [], leading_comments: []} }
|
|
380
303
|
end
|
|
@@ -0,0 +1,136 @@
|
|
|
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
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|