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
|
@@ -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
|
-
|
|
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
|
data/lib/kettle/dev/setup_cli.rb
CHANGED
|
@@ -197,37 +197,26 @@ module Kettle
|
|
|
197
197
|
end
|
|
198
198
|
end
|
|
199
199
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
lines
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
215
|
+
say("Development dependencies already up to date.")
|
|
224
216
|
end
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
#
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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__)
|