kettle-dev 1.1.59 → 1.2.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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +1 -2
- data/.envrc +1 -0
- data/.envrc.example +1 -0
- data/.envrc.no-osc.example +1 -0
- data/.github/workflows/ancient.yml +1 -1
- data/.github/workflows/ancient.yml.example +1 -1
- data/.github/workflows/codeql-analysis.yml +1 -1
- data/.github/workflows/coverage.yml +1 -1
- data/.github/workflows/coverage.yml.example +1 -1
- data/.github/workflows/current.yml +1 -1
- data/.github/workflows/current.yml.example +1 -1
- data/.github/workflows/dep-heads.yml +1 -1
- data/.github/workflows/dependency-review.yml +1 -1
- data/.github/workflows/heads.yml +1 -1
- data/.github/workflows/heads.yml.example +1 -1
- data/.github/workflows/jruby.yml +1 -1
- data/.github/workflows/jruby.yml.example +1 -1
- data/.github/workflows/legacy.yml +1 -1
- data/.github/workflows/license-eye.yml +1 -1
- data/.github/workflows/locked_deps.yml +1 -1
- data/.github/workflows/opencollective.yml +1 -1
- data/.github/workflows/style.yml +1 -1
- data/.github/workflows/supported.yml +1 -1
- data/.github/workflows/truffle.yml +1 -1
- data/.github/workflows/unlocked_deps.yml +1 -1
- data/.github/workflows/unsupported.yml +1 -1
- data/CHANGELOG.md +35 -1
- data/Gemfile +3 -0
- data/Gemfile.example +3 -0
- data/README.md +90 -37
- data/README.md.example +16 -12
- data/README.md.no-osc.example +16 -12
- data/Rakefile.example +1 -1
- data/gemfiles/modular/style.gemfile.example +1 -1
- data/gemfiles/modular/templating.gemfile +3 -0
- data/lib/kettle/dev/appraisals_ast_merger.rb +383 -0
- data/lib/kettle/dev/changelog_cli.rb +13 -0
- data/lib/kettle/dev/modular_gemfiles.rb +11 -3
- data/lib/kettle/dev/prism_utils.rb +188 -0
- data/lib/kettle/dev/rakelib/spec_test.rake +70 -20
- data/lib/kettle/dev/source_merger.rb +345 -0
- data/lib/kettle/dev/tasks/template_task.rb +11 -1
- data/lib/kettle/dev/template_helpers.rb +70 -226
- data/lib/kettle/dev/version.rb +1 -1
- data/lib/kettle/dev.rb +2 -0
- data/sig/kettle/dev/appraisals_ast_merger.rbs +72 -0
- data/sig/kettle/dev/changelog_cli.rbs +64 -0
- data/sig/kettle/dev/prism_utils.rbs +56 -0
- data/sig/kettle/dev/source_merger.rbs +86 -0
- data/sig/kettle/dev/versioning.rbs +21 -0
- data.tar.gz.sig +0 -0
- metadata +14 -5
- metadata.gz.sig +0 -0
- /data/sig/kettle/dev/{dvcscli.rbs → dvcs_cli.rbs} +0 -0
|
@@ -12,7 +12,9 @@ begin
|
|
|
12
12
|
t.test_files = FileList["test/**/*test*.rb"]
|
|
13
13
|
t.verbose = true
|
|
14
14
|
end
|
|
15
|
-
|
|
15
|
+
# The test task is invoked by the coverage task, so of the two, (i.e., when outside CI),
|
|
16
|
+
# only coverage should be registered as default.
|
|
17
|
+
Kettle::Dev.register_default("test") unless Kettle::Dev.default_registered?("coverage")
|
|
16
18
|
rescue LoadError
|
|
17
19
|
warn("[kettle-dev][spec_test.rake] failed to load rake/testtask") if Kettle::Dev::DEBUGGING
|
|
18
20
|
desc("test task stub")
|
|
@@ -21,34 +23,82 @@ rescue LoadError
|
|
|
21
23
|
end
|
|
22
24
|
end
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
begin
|
|
26
|
-
|
|
26
|
+
setup_spec_task = ->(default:) {
|
|
27
|
+
begin
|
|
28
|
+
require "rspec/core/rake_task"
|
|
27
29
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
30
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
31
|
+
if default
|
|
32
|
+
# This takes the place of the `coverage` task if/when it isn't already registered.
|
|
33
|
+
# This is because spec and coverage run the same tests
|
|
34
|
+
# (via the coverage task invoking the test task which invokes the spec task),
|
|
35
|
+
# so we can't have both in the default task.
|
|
36
|
+
Kettle::Dev.register_default("spec") unless Kettle::Dev.default_registered?("coverage")
|
|
37
|
+
end
|
|
38
|
+
rescue LoadError
|
|
39
|
+
warn("[kettle-dev][spec_test.rake] failed to load rspec/core/rake_task") if Kettle::Dev::DEBUGGING
|
|
40
|
+
desc("spec task stub")
|
|
41
|
+
task(:spec) do
|
|
42
|
+
warn("NOTE: rspec isn't installed, or is disabled for #{RUBY_VERSION} in the current environment")
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# Setup RSpec
|
|
48
|
+
if defined?(Kettle::Dev::IS_CI)
|
|
49
|
+
if Kettle::Dev::IS_CI
|
|
50
|
+
# then we should not have a coverage task, but do want a spec test.
|
|
51
|
+
setup_spec_task.call(default: true)
|
|
52
|
+
else
|
|
53
|
+
# then we should have a coverage task.
|
|
54
|
+
# The coverage task will invoke the "test" task, which will invoke the spec task.
|
|
55
|
+
setup_spec_task.call(default: false)
|
|
36
56
|
end
|
|
57
|
+
else
|
|
58
|
+
# then we do not have a coverage task setup by this gem, and are not in a coverage context.
|
|
59
|
+
# So setup a spec test.
|
|
60
|
+
setup_spec_task.call(default: true)
|
|
37
61
|
end
|
|
38
62
|
|
|
39
63
|
spec_registered = Kettle::Dev.default_registered?("spec")
|
|
40
64
|
coverage_registered = Kettle::Dev.default_registered?("coverage")
|
|
41
65
|
test_registered = Kettle::Dev.default_registered?("test")
|
|
66
|
+
spec_and_coverage = spec_registered && coverage_registered
|
|
42
67
|
spec_or_coverage = spec_registered || coverage_registered
|
|
43
68
|
|
|
44
|
-
if
|
|
45
|
-
task test: :spec
|
|
46
|
-
elsif test_registered && !spec_or_coverage
|
|
69
|
+
if test_registered && !spec_or_coverage
|
|
47
70
|
task spec: :test
|
|
48
|
-
elsif test_registered &&
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
71
|
+
# elsif test_registered && spec_registered
|
|
72
|
+
# # When we have both tasks registered as default, making spec run as part of test would be redundant.
|
|
73
|
+
# # task test: :spec
|
|
74
|
+
# elsif test_registered && coverage_registered
|
|
75
|
+
# # When we have both tasks registered as default, making coverage run as part of test would be circular.
|
|
76
|
+
# # task test: :coverage
|
|
77
|
+
elsif !test_registered
|
|
78
|
+
if spec_registered && !coverage_registered
|
|
79
|
+
puts "Spec task is registered as default task. Creating test task with spec as pre-requisite" if Kettle::Dev::DEBUGGING
|
|
80
|
+
# If spec is registered as default, it should be invoked by the test task when test is not default,
|
|
81
|
+
# because some CI workflows will be configured to run bin/rake test.
|
|
82
|
+
desc "A test task with spec as prerequisite"
|
|
83
|
+
task test: :spec
|
|
84
|
+
elsif coverage_registered && !spec_registered
|
|
85
|
+
puts "Coverage task is registered as default task, and will call test task, with spec as pre-requisite." if Kettle::Dev::DEBUGGING
|
|
86
|
+
# If coverage is registered as default, it will invoke test.
|
|
87
|
+
# We need to make spec a prerequisite of test so that it runs as part of the test task,
|
|
88
|
+
# which will be invoked by the coverage task.
|
|
89
|
+
desc "A test task with spec as prerequisite"
|
|
90
|
+
task test: :spec
|
|
91
|
+
end
|
|
53
92
|
end
|
|
54
93
|
# rubocop:enable Rake/DuplicateTask
|
|
94
|
+
|
|
95
|
+
if spec_and_coverage
|
|
96
|
+
# They should not both be registered as default tasks, as they run the same tests.
|
|
97
|
+
warn("[kettle-dev][spec_test.rake] both spec and coverage are registered as default tasks!") if Kettle::Dev::DEBUGGING
|
|
98
|
+
elsif test_registered && spec_registered
|
|
99
|
+
# They should not both be registered as default tasks, as they will be setup to run the same tests.
|
|
100
|
+
warn("[kettle-dev][spec_test.rake] both test and spec are registered as default tasks!") if Kettle::Dev::DEBUGGING
|
|
101
|
+
elsif test_registered && coverage_registered
|
|
102
|
+
# They should not both be registered as default tasks, coverage invokes the test task.
|
|
103
|
+
warn("[kettle-dev][spec_test.rake] both test and coverage are registered as default tasks!") if Kettle::Dev::DEBUGGING
|
|
104
|
+
end
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require "set"
|
|
5
|
+
require "prism"
|
|
6
|
+
require "kettle/dev/prism_utils"
|
|
7
|
+
|
|
8
|
+
module Kettle
|
|
9
|
+
module Dev
|
|
10
|
+
# Prism-based AST merging for templated Ruby files.
|
|
11
|
+
# Handles universal freeze reminders, kettle-dev:freeze blocks, and
|
|
12
|
+
# strategy dispatch (skip/replace/append/merge).
|
|
13
|
+
#
|
|
14
|
+
# Uses Prism for parsing with first-class comment support, enabling
|
|
15
|
+
# preservation of inline and leading comments throughout the merge process.
|
|
16
|
+
module SourceMerger
|
|
17
|
+
FREEZE_START = /#\s*kettle-dev:freeze/i
|
|
18
|
+
FREEZE_END = /#\s*kettle-dev:unfreeze/i
|
|
19
|
+
FREEZE_BLOCK = Regexp.new("(#{FREEZE_START.source}).*?(#{FREEZE_END.source})", Regexp::IGNORECASE | Regexp::MULTILINE)
|
|
20
|
+
FREEZE_REMINDER = <<~RUBY
|
|
21
|
+
# To retain during kettle-dev templating:
|
|
22
|
+
# kettle-dev:freeze
|
|
23
|
+
# # ... your code
|
|
24
|
+
# kettle-dev:unfreeze
|
|
25
|
+
RUBY
|
|
26
|
+
BUG_URL = "https://github.com/kettle-rb/kettle-dev/issues"
|
|
27
|
+
|
|
28
|
+
module_function
|
|
29
|
+
|
|
30
|
+
# Apply a templating strategy to merge source and destination Ruby files
|
|
31
|
+
#
|
|
32
|
+
# @param strategy [Symbol] Merge strategy - :skip, :replace, :append, or :merge
|
|
33
|
+
# @param src [String] Template source content
|
|
34
|
+
# @param dest [String] Destination file content
|
|
35
|
+
# @param path [String] File path (for error messages)
|
|
36
|
+
# @return [String] Merged content with freeze blocks and comments preserved
|
|
37
|
+
# @raise [Kettle::Dev::Error] If strategy is unknown or merge fails
|
|
38
|
+
# @example
|
|
39
|
+
# SourceMerger.apply(
|
|
40
|
+
# strategy: :merge,
|
|
41
|
+
# src: 'gem "foo"',
|
|
42
|
+
# dest: 'gem "bar"',
|
|
43
|
+
# path: "Gemfile"
|
|
44
|
+
# )
|
|
45
|
+
def apply(strategy:, src:, dest:, path:)
|
|
46
|
+
strategy = normalize_strategy(strategy)
|
|
47
|
+
dest ||= ""
|
|
48
|
+
src_with_reminder = ensure_reminder(src)
|
|
49
|
+
content =
|
|
50
|
+
case strategy
|
|
51
|
+
when :skip
|
|
52
|
+
src_with_reminder
|
|
53
|
+
when :replace
|
|
54
|
+
normalize_source(src_with_reminder)
|
|
55
|
+
when :append
|
|
56
|
+
apply_append(src_with_reminder, dest)
|
|
57
|
+
when :merge
|
|
58
|
+
apply_merge(src_with_reminder, dest)
|
|
59
|
+
else
|
|
60
|
+
raise Kettle::Dev::Error, "Unknown templating strategy '#{strategy}' for #{path}."
|
|
61
|
+
end
|
|
62
|
+
content = merge_freeze_blocks(content, dest)
|
|
63
|
+
content = restore_custom_leading_comments(dest, content)
|
|
64
|
+
ensure_trailing_newline(content)
|
|
65
|
+
rescue StandardError => error
|
|
66
|
+
warn_bug(path, error)
|
|
67
|
+
raise Kettle::Dev::Error, "Template merge failed for #{path}: #{error.message}"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Ensure freeze reminder comment is present at the top of content
|
|
71
|
+
#
|
|
72
|
+
# @param content [String] Ruby source content
|
|
73
|
+
# @return [String] Content with freeze reminder prepended if missing
|
|
74
|
+
# @api private
|
|
75
|
+
def ensure_reminder(content)
|
|
76
|
+
return content if reminder_present?(content)
|
|
77
|
+
insertion_index = reminder_insertion_index(content)
|
|
78
|
+
before = content[0...insertion_index]
|
|
79
|
+
after = content[insertion_index..-1]
|
|
80
|
+
snippet = FREEZE_REMINDER
|
|
81
|
+
snippet += "\n" unless snippet.end_with?("\n\n")
|
|
82
|
+
[before, snippet, after].join
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Normalize source code while preserving formatting
|
|
86
|
+
#
|
|
87
|
+
# @param source [String] Ruby source code
|
|
88
|
+
# @return [String] Normalized source with trailing newline
|
|
89
|
+
# @api private
|
|
90
|
+
def normalize_source(source)
|
|
91
|
+
parse_result = PrismUtils.parse_with_comments(source)
|
|
92
|
+
return ensure_trailing_newline(source) unless parse_result.success?
|
|
93
|
+
|
|
94
|
+
# Use Prism's slice to preserve original formatting
|
|
95
|
+
ensure_trailing_newline(source)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def reminder_present?(content)
|
|
99
|
+
content.include?(FREEZE_REMINDER.lines.first.strip)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def reminder_insertion_index(content)
|
|
103
|
+
cursor = 0
|
|
104
|
+
lines = content.lines
|
|
105
|
+
lines.each do |line|
|
|
106
|
+
break unless shebang?(line) || frozen_comment?(line)
|
|
107
|
+
cursor += line.length
|
|
108
|
+
end
|
|
109
|
+
cursor
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def shebang?(line)
|
|
113
|
+
line.start_with?("#!")
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def frozen_comment?(line)
|
|
117
|
+
line.match?(/#\s*frozen_string_literal:/)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Merge kettle-dev:freeze blocks from destination into source content
|
|
121
|
+
# Preserves user customizations wrapped in freeze/unfreeze markers
|
|
122
|
+
#
|
|
123
|
+
# @param src_content [String] Template source content
|
|
124
|
+
# @param dest_content [String] Destination file content
|
|
125
|
+
# @return [String] Merged content with freeze blocks from destination
|
|
126
|
+
# @api private
|
|
127
|
+
def merge_freeze_blocks(src_content, dest_content)
|
|
128
|
+
dest_blocks = freeze_blocks(dest_content)
|
|
129
|
+
return src_content if dest_blocks.empty?
|
|
130
|
+
src_blocks = freeze_blocks(src_content)
|
|
131
|
+
updated = src_content.dup
|
|
132
|
+
# Replace matching freeze sections by textual markers rather than index ranges
|
|
133
|
+
dest_blocks.each do |dest_block|
|
|
134
|
+
marker = dest_block[:text]
|
|
135
|
+
next if updated.include?(marker)
|
|
136
|
+
# If the template had a placeholder block, replace the first occurrence of a freeze stub
|
|
137
|
+
placeholder = src_blocks.find { |blk| blk[:start_marker] == dest_block[:start_marker] }
|
|
138
|
+
if placeholder
|
|
139
|
+
updated.sub!(placeholder[:text], marker)
|
|
140
|
+
else
|
|
141
|
+
updated << "\n" unless updated.end_with?("\n")
|
|
142
|
+
updated << marker
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
updated
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def freeze_blocks(text)
|
|
149
|
+
return [] unless text&.match?(FREEZE_START)
|
|
150
|
+
blocks = []
|
|
151
|
+
text.to_enum(:scan, FREEZE_BLOCK).each do
|
|
152
|
+
match = Regexp.last_match
|
|
153
|
+
start_idx = match&.begin(0)
|
|
154
|
+
end_idx = match&.end(0)
|
|
155
|
+
next unless start_idx && end_idx
|
|
156
|
+
segment = match[0]
|
|
157
|
+
start_marker = segment.lines.first&.strip
|
|
158
|
+
blocks << {range: start_idx...end_idx, text: segment, start_marker: start_marker}
|
|
159
|
+
end
|
|
160
|
+
blocks
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def normalize_strategy(strategy)
|
|
164
|
+
return :skip if strategy.nil?
|
|
165
|
+
strategy.to_s.downcase.strip.to_sym
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def warn_bug(path, error)
|
|
169
|
+
puts "ERROR: kettle-dev templating failed for #{path}: #{error.message}"
|
|
170
|
+
puts "Please file a bug at #{BUG_URL} with the file contents so we can improve the AST merger."
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def ensure_trailing_newline(text)
|
|
174
|
+
return "" if text.nil?
|
|
175
|
+
text.end_with?("\n") ? text : text + "\n"
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def apply_append(src_content, dest_content)
|
|
179
|
+
prism_merge(src_content, dest_content) do |src_nodes, dest_nodes, _src_result, _dest_result|
|
|
180
|
+
existing = Set.new(dest_nodes.map { |node| node_signature(node[:node]) })
|
|
181
|
+
appended = dest_nodes.dup
|
|
182
|
+
src_nodes.each do |node_info|
|
|
183
|
+
sig = node_signature(node_info[:node])
|
|
184
|
+
next if existing.include?(sig)
|
|
185
|
+
appended << node_info
|
|
186
|
+
existing << sig
|
|
187
|
+
end
|
|
188
|
+
appended
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def apply_merge(src_content, dest_content)
|
|
193
|
+
prism_merge(src_content, dest_content) do |src_nodes, dest_nodes, _src_result, _dest_result|
|
|
194
|
+
src_map = src_nodes.each_with_object({}) do |node_info, memo|
|
|
195
|
+
sig = node_signature(node_info[:node])
|
|
196
|
+
memo[sig] ||= node_info
|
|
197
|
+
end
|
|
198
|
+
merged = dest_nodes.map do |node_info|
|
|
199
|
+
sig = node_signature(node_info[:node])
|
|
200
|
+
if (src_node_info = src_map[sig])
|
|
201
|
+
merge_node_info(sig, node_info, src_node_info)
|
|
202
|
+
else
|
|
203
|
+
node_info
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
existing = merged.map { |ni| node_signature(ni[:node]) }.to_set
|
|
207
|
+
src_nodes.each do |node_info|
|
|
208
|
+
sig = node_signature(node_info[:node])
|
|
209
|
+
next if existing.include?(sig)
|
|
210
|
+
merged << node_info
|
|
211
|
+
existing << sig
|
|
212
|
+
end
|
|
213
|
+
merged
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def merge_node_info(signature, _dest_node_info, src_node_info)
|
|
218
|
+
return src_node_info unless signature.is_a?(Array)
|
|
219
|
+
case signature[1]
|
|
220
|
+
when :gem_specification
|
|
221
|
+
merge_block_node_info(src_node_info)
|
|
222
|
+
else
|
|
223
|
+
src_node_info
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def merge_block_node_info(src_node_info)
|
|
228
|
+
# For block merging, we need to merge the statements within the block
|
|
229
|
+
# This is complex - for now, prefer template version
|
|
230
|
+
# TODO: Implement deep block statement merging with comment preservation
|
|
231
|
+
src_node_info
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def prism_merge(src_content, dest_content)
|
|
235
|
+
src_result = PrismUtils.parse_with_comments(src_content)
|
|
236
|
+
dest_result = PrismUtils.parse_with_comments(dest_content)
|
|
237
|
+
|
|
238
|
+
src_nodes = extract_nodes_with_comments(src_result)
|
|
239
|
+
dest_nodes = extract_nodes_with_comments(dest_result)
|
|
240
|
+
|
|
241
|
+
merged_nodes = yield(src_nodes, dest_nodes, src_result, dest_result)
|
|
242
|
+
|
|
243
|
+
build_source_from_nodes(merged_nodes)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def extract_nodes_with_comments(parse_result)
|
|
247
|
+
return [] unless parse_result.success?
|
|
248
|
+
|
|
249
|
+
statements = PrismUtils.extract_statements(parse_result.value.statements)
|
|
250
|
+
return [] if statements.empty?
|
|
251
|
+
|
|
252
|
+
statements.map.with_index do |stmt, idx|
|
|
253
|
+
prev_stmt = (idx > 0) ? statements[idx - 1] : nil
|
|
254
|
+
body_node = parse_result.value.statements
|
|
255
|
+
|
|
256
|
+
{
|
|
257
|
+
node: stmt,
|
|
258
|
+
leading_comments: PrismUtils.find_leading_comments(parse_result, stmt, prev_stmt, body_node),
|
|
259
|
+
inline_comments: PrismUtils.inline_comments_for_node(parse_result, stmt),
|
|
260
|
+
}
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def build_source_from_nodes(node_infos)
|
|
265
|
+
return "" if node_infos.empty?
|
|
266
|
+
|
|
267
|
+
lines = []
|
|
268
|
+
node_infos.each do |node_info|
|
|
269
|
+
# Add leading comments
|
|
270
|
+
node_info[:leading_comments].each do |comment|
|
|
271
|
+
lines << comment.slice.rstrip
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# Add the node's source
|
|
275
|
+
node_source = PrismUtils.node_to_source(node_info[:node])
|
|
276
|
+
|
|
277
|
+
# Add inline comments on the same line
|
|
278
|
+
if node_info[:inline_comments].any?
|
|
279
|
+
inline = node_info[:inline_comments].map { |c| c.slice.strip }.join(" ")
|
|
280
|
+
node_source = node_source.rstrip + " " + inline
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
lines << node_source
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
lines.join("\n")
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def node_signature(node)
|
|
290
|
+
return [:nil] unless node
|
|
291
|
+
|
|
292
|
+
case node
|
|
293
|
+
when Prism::CallNode
|
|
294
|
+
method_name = node.name
|
|
295
|
+
if node.block
|
|
296
|
+
# Block call
|
|
297
|
+
first_arg = PrismUtils.extract_literal_value(node.arguments&.arguments&.first)
|
|
298
|
+
receiver_name = PrismUtils.extract_const_name(node.receiver)
|
|
299
|
+
|
|
300
|
+
if receiver_name == "Gem::Specification" && method_name == :new
|
|
301
|
+
[:block, :gem_specification]
|
|
302
|
+
elsif method_name == :task
|
|
303
|
+
[:block, :task, first_arg]
|
|
304
|
+
elsif method_name == :git_source
|
|
305
|
+
[:block, :git_source, first_arg]
|
|
306
|
+
else
|
|
307
|
+
[:block, method_name, first_arg, node.slice]
|
|
308
|
+
end
|
|
309
|
+
elsif [:source, :git_source, :gem, :eval_gemfile].include?(method_name)
|
|
310
|
+
# Simple call
|
|
311
|
+
first_literal = PrismUtils.extract_literal_value(node.arguments&.arguments&.first)
|
|
312
|
+
[:send, method_name, first_literal]
|
|
313
|
+
else
|
|
314
|
+
[:send, method_name, node.slice]
|
|
315
|
+
end
|
|
316
|
+
else
|
|
317
|
+
# Other node types
|
|
318
|
+
[node.class.name.split("::").last.to_sym, node.slice]
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def restore_custom_leading_comments(dest_content, merged_content)
|
|
323
|
+
block = leading_comment_block(dest_content)
|
|
324
|
+
return merged_content if block.strip.empty?
|
|
325
|
+
return merged_content if merged_content.start_with?(block)
|
|
326
|
+
|
|
327
|
+
# Insert after shebang / frozen string literal comments (same place reminder goes)
|
|
328
|
+
insertion_index = reminder_insertion_index(merged_content)
|
|
329
|
+
block = ensure_trailing_newline(block)
|
|
330
|
+
merged_content.dup.insert(insertion_index, block)
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def leading_comment_block(content)
|
|
334
|
+
lines = content.to_s.lines
|
|
335
|
+
collected = []
|
|
336
|
+
lines.each do |line|
|
|
337
|
+
stripped = line.strip
|
|
338
|
+
break unless stripped.empty? || stripped.start_with?("#")
|
|
339
|
+
collected << line
|
|
340
|
+
end
|
|
341
|
+
collected.join
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
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
|
-
|
|
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
|
|
@@ -387,6 +388,15 @@ module Kettle
|
|
|
387
388
|
# If anything goes wrong, keep the content as-is rather than failing the task
|
|
388
389
|
end
|
|
389
390
|
|
|
391
|
+
if dest_existed
|
|
392
|
+
begin
|
|
393
|
+
merged = helpers.apply_strategy(c, dest_gemspec)
|
|
394
|
+
c = merged if merged.is_a?(String) && !merged.empty?
|
|
395
|
+
rescue StandardError => e
|
|
396
|
+
Kettle::Dev.debug_error(e, __method__)
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
|
|
390
400
|
c
|
|
391
401
|
end
|
|
392
402
|
end
|