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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +176 -3
- data/CITATION.cff +2 -2
- data/CONTRIBUTING.md +11 -17
- data/README.md +390 -319
- data/exe/kettle-dev-setup +12 -63
- data/exe/kettle-gh-release +82 -0
- data/lib/kettle/dev/gem_spec_reader.rb +2 -2
- data/lib/kettle/dev/open_collective_config.rb +12 -0
- data/lib/kettle/dev/rakelib/yard.rake +15 -0
- data/lib/kettle/dev/tasks/ci_task.rb +4 -4
- data/lib/kettle/dev/version.rb +1 -1
- data/lib/kettle/dev.rb +4 -12
- data/sig/kettle/dev/source_merger.rbs +40 -56
- data.tar.gz.sig +0 -0
- metadata +15 -144
- metadata.gz.sig +0 -0
- data/.aiignore.example +0 -19
- data/.devcontainer/apt-install/devcontainer-feature.json +0 -9
- data/.devcontainer/apt-install/install.sh +0 -11
- data/.devcontainer/devcontainer.json +0 -28
- data/.env.local.example +0 -31
- data/.envrc +0 -47
- data/.envrc.example +0 -51
- data/.envrc.no-osc.example +0 -51
- data/.git-hooks/commit-msg +0 -54
- data/.git-hooks/commit-subjects-goalie.txt +0 -8
- data/.git-hooks/footer-template.erb.txt +0 -16
- data/.git-hooks/prepare-commit-msg +0 -8
- data/.github/.codecov.yml.example +0 -14
- data/.github/FUNDING.yml +0 -13
- data/.github/FUNDING.yml.no-osc.example +0 -13
- data/.github/dependabot.yml +0 -13
- data/.github/workflows/ancient.yml +0 -83
- data/.github/workflows/ancient.yml.example +0 -81
- data/.github/workflows/auto-assign.yml +0 -21
- data/.github/workflows/codeql-analysis.yml +0 -70
- data/.github/workflows/coverage.yml +0 -127
- data/.github/workflows/coverage.yml.example +0 -127
- data/.github/workflows/current.yml +0 -116
- data/.github/workflows/current.yml.example +0 -115
- data/.github/workflows/dep-heads.yml +0 -117
- data/.github/workflows/dependency-review.yml +0 -20
- data/.github/workflows/discord-notifier.yml.example +0 -39
- data/.github/workflows/heads.yml +0 -117
- data/.github/workflows/heads.yml.example +0 -116
- data/.github/workflows/jruby.yml +0 -82
- data/.github/workflows/jruby.yml.example +0 -72
- data/.github/workflows/legacy.yml +0 -76
- data/.github/workflows/license-eye.yml +0 -40
- data/.github/workflows/locked_deps.yml +0 -85
- data/.github/workflows/opencollective.yml +0 -40
- data/.github/workflows/style.yml +0 -67
- data/.github/workflows/supported.yml +0 -75
- data/.github/workflows/truffle.yml +0 -99
- data/.github/workflows/unlocked_deps.yml +0 -84
- data/.github/workflows/unsupported.yml +0 -76
- data/.gitignore +0 -50
- data/.gitlab-ci.yml.example +0 -134
- data/.idea/.gitignore +0 -45
- data/.junie/guidelines-rbs.md +0 -49
- data/.junie/guidelines.md +0 -141
- data/.junie/guidelines.md.example +0 -140
- data/.licenserc.yaml +0 -7
- data/.opencollective.yml +0 -3
- data/.opencollective.yml.example +0 -3
- data/.qlty/qlty.toml +0 -79
- data/.rspec +0 -9
- data/.rubocop.yml +0 -13
- data/.rubocop_rspec.yml +0 -33
- data/.simplecov +0 -16
- data/.simplecov.example +0 -11
- data/.tool-versions +0 -1
- data/.yardignore +0 -13
- data/.yardopts +0 -14
- data/Appraisal.root.gemfile +0 -10
- data/Appraisals +0 -151
- data/Appraisals.example +0 -102
- data/CHANGELOG.md.example +0 -47
- data/CONTRIBUTING.md.example +0 -227
- data/FUNDING.md.no-osc.example +0 -63
- data/Gemfile +0 -40
- data/Gemfile.example +0 -34
- data/README.md.example +0 -570
- data/README.md.no-osc.example +0 -536
- data/Rakefile.example +0 -68
- data/gemfiles/modular/coverage.gemfile +0 -6
- data/gemfiles/modular/debug.gemfile +0 -13
- data/gemfiles/modular/documentation.gemfile +0 -14
- data/gemfiles/modular/erb/r2/v3.0.gemfile +0 -1
- data/gemfiles/modular/erb/r2.3/default.gemfile +0 -6
- data/gemfiles/modular/erb/r2.6/v2.2.gemfile +0 -3
- data/gemfiles/modular/erb/r3/v5.0.gemfile +0 -1
- data/gemfiles/modular/erb/r3.1/v4.0.gemfile +0 -2
- data/gemfiles/modular/erb/vHEAD.gemfile +0 -2
- data/gemfiles/modular/injected.gemfile +0 -60
- data/gemfiles/modular/mutex_m/r2/v0.3.gemfile +0 -2
- data/gemfiles/modular/mutex_m/r2.4/v0.1.gemfile +0 -3
- data/gemfiles/modular/mutex_m/r3/v0.3.gemfile +0 -2
- data/gemfiles/modular/mutex_m/vHEAD.gemfile +0 -2
- data/gemfiles/modular/optional.gemfile +0 -8
- data/gemfiles/modular/optional.gemfile.example +0 -5
- data/gemfiles/modular/runtime_heads.gemfile +0 -10
- data/gemfiles/modular/runtime_heads.gemfile.example +0 -8
- data/gemfiles/modular/stringio/r2/v3.0.gemfile +0 -5
- data/gemfiles/modular/stringio/r2.4/v0.0.2.gemfile +0 -4
- data/gemfiles/modular/stringio/r3/v3.0.gemfile +0 -5
- data/gemfiles/modular/stringio/vHEAD.gemfile +0 -2
- data/gemfiles/modular/style.gemfile +0 -25
- data/gemfiles/modular/style.gemfile.example +0 -25
- data/gemfiles/modular/templating.gemfile +0 -3
- data/gemfiles/modular/x_std_libs/r2/libs.gemfile +0 -3
- data/gemfiles/modular/x_std_libs/r2.3/libs.gemfile +0 -3
- data/gemfiles/modular/x_std_libs/r2.4/libs.gemfile +0 -3
- data/gemfiles/modular/x_std_libs/r2.6/libs.gemfile +0 -3
- data/gemfiles/modular/x_std_libs/r3/libs.gemfile +0 -3
- data/gemfiles/modular/x_std_libs/r3.1/libs.gemfile +0 -3
- data/gemfiles/modular/x_std_libs/vHEAD.gemfile +0 -3
- data/gemfiles/modular/x_std_libs.gemfile +0 -2
- data/kettle-dev.gemspec.example +0 -154
- data/lib/kettle/dev/modular_gemfiles.rb +0 -119
- data/lib/kettle/dev/prism_appraisals.rb +0 -351
- data/lib/kettle/dev/prism_gemfile.rb +0 -177
- data/lib/kettle/dev/prism_gemspec.rb +0 -284
- data/lib/kettle/dev/prism_utils.rb +0 -201
- data/lib/kettle/dev/rakelib/install.rake +0 -10
- data/lib/kettle/dev/rakelib/template.rake +0 -10
- data/lib/kettle/dev/setup_cli.rb +0 -403
- data/lib/kettle/dev/source_merger.rb +0 -622
- data/lib/kettle/dev/tasks/install_task.rb +0 -553
- data/lib/kettle/dev/tasks/template_task.rb +0 -975
- data/lib/kettle/dev/template_helpers.rb +0 -685
|
@@ -1,284 +0,0 @@
|
|
|
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
|
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "prism"
|
|
4
|
-
|
|
5
|
-
module Kettle
|
|
6
|
-
module Dev
|
|
7
|
-
# Shared utilities for working with Prism AST nodes.
|
|
8
|
-
# Provides parsing, node inspection, and source generation helpers
|
|
9
|
-
# used by both PrismMerger and AppraisalsAstMerger.
|
|
10
|
-
#
|
|
11
|
-
# Uses Prism's native methods for source generation (via .slice) to preserve
|
|
12
|
-
# original formatting and comments. For normalized output (e.g., adding parentheses),
|
|
13
|
-
# use normalize_call_node instead.
|
|
14
|
-
module PrismUtils
|
|
15
|
-
module_function
|
|
16
|
-
|
|
17
|
-
# Parse Ruby source code and return Prism parse result with comments
|
|
18
|
-
# @param source [String] Ruby source code
|
|
19
|
-
# @return [Prism::ParseResult] Parse result containing AST and comments
|
|
20
|
-
def parse_with_comments(source)
|
|
21
|
-
Prism.parse(source)
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
# Extract statements from a Prism body node
|
|
25
|
-
# @param body_node [Prism::Node, nil] Body node (typically StatementsNode)
|
|
26
|
-
# @return [Array<Prism::Node>] Array of statement nodes
|
|
27
|
-
def extract_statements(body_node)
|
|
28
|
-
return [] unless body_node
|
|
29
|
-
|
|
30
|
-
if body_node.is_a?(Prism::StatementsNode)
|
|
31
|
-
body_node.body.compact
|
|
32
|
-
else
|
|
33
|
-
[body_node].compact
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
# Generate a unique key for a statement node to identify equivalent statements
|
|
38
|
-
# Used for merge/append operations to detect duplicates
|
|
39
|
-
# @param node [Prism::Node] Statement node
|
|
40
|
-
# @param tracked_methods [Array<Symbol>] Methods to track (default: gem, source, eval_gemfile, git_source)
|
|
41
|
-
# @return [Array, nil] Key array like [:gem, "foo"] or nil if not trackable
|
|
42
|
-
def statement_key(node, tracked_methods: %i[gem source eval_gemfile git_source])
|
|
43
|
-
return unless node.is_a?(Prism::CallNode)
|
|
44
|
-
return unless tracked_methods.include?(node.name)
|
|
45
|
-
|
|
46
|
-
first_arg = node.arguments&.arguments&.first
|
|
47
|
-
arg_value = extract_literal_value(first_arg)
|
|
48
|
-
|
|
49
|
-
[node.name, arg_value] if arg_value
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
# Extract literal value from string or symbol nodes
|
|
53
|
-
# @param node [Prism::Node, nil] Node to extract from
|
|
54
|
-
# @return [String, Symbol, nil] Literal value or nil
|
|
55
|
-
def extract_literal_value(node)
|
|
56
|
-
return unless node
|
|
57
|
-
case node
|
|
58
|
-
when Prism::StringNode then node.unescaped
|
|
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
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
# Extract qualified constant name from a constant node
|
|
76
|
-
# @param node [Prism::Node, nil] Constant node
|
|
77
|
-
# @return [String, nil] Qualified name like "Gem::Specification" or nil
|
|
78
|
-
def extract_const_name(node)
|
|
79
|
-
case node
|
|
80
|
-
when Prism::ConstantReadNode
|
|
81
|
-
node.name.to_s
|
|
82
|
-
when Prism::ConstantPathNode
|
|
83
|
-
parent = extract_const_name(node.parent)
|
|
84
|
-
child = node.name || node.child&.name
|
|
85
|
-
(parent && child) ? "#{parent}::#{child}" : child.to_s
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
# Find leading comments for a statement node
|
|
90
|
-
# Leading comments are those that appear after the previous statement
|
|
91
|
-
# and before the current statement
|
|
92
|
-
# @param parse_result [Prism::ParseResult] Parse result with comments
|
|
93
|
-
# @param current_stmt [Prism::Node] Current statement node
|
|
94
|
-
# @param prev_stmt [Prism::Node, nil] Previous statement node
|
|
95
|
-
# @param body_node [Prism::Node] Body containing the statements
|
|
96
|
-
# @return [Array<Prism::Comment>] Leading comments
|
|
97
|
-
def find_leading_comments(parse_result, current_stmt, prev_stmt, body_node)
|
|
98
|
-
start_line = prev_stmt ? prev_stmt.location.end_line : body_node.location.start_line
|
|
99
|
-
end_line = current_stmt.location.start_line
|
|
100
|
-
|
|
101
|
-
parse_result.comments.select do |comment|
|
|
102
|
-
comment.location.start_line > start_line &&
|
|
103
|
-
comment.location.start_line < end_line
|
|
104
|
-
end
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
# Find inline comments for a statement node
|
|
108
|
-
# Inline comments are those that appear on the same line as the statement's end
|
|
109
|
-
# @param parse_result [Prism::ParseResult] Parse result with comments
|
|
110
|
-
# @param stmt [Prism::Node] Statement node
|
|
111
|
-
# @return [Array<Prism::Comment>] Inline comments
|
|
112
|
-
def inline_comments_for_node(parse_result, stmt)
|
|
113
|
-
parse_result.comments.select do |comment|
|
|
114
|
-
comment.location.start_line == stmt.location.end_line &&
|
|
115
|
-
comment.location.start_offset > stmt.location.end_offset
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
# Convert a Prism AST node to Ruby source code
|
|
120
|
-
# Uses Prism's native slice method which preserves the original source exactly.
|
|
121
|
-
# This is preferable to Unparser for Prism nodes as it maintains original formatting
|
|
122
|
-
# and comments without requiring transformation.
|
|
123
|
-
# @param node [Prism::Node] AST node
|
|
124
|
-
# @return [String] Ruby source code
|
|
125
|
-
def node_to_source(node)
|
|
126
|
-
return "" unless node
|
|
127
|
-
# Prism nodes have a slice method that returns the original source
|
|
128
|
-
node.slice
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
# Normalize a call node to use parentheses format
|
|
132
|
-
# Converts `gem "foo"` to `gem("foo")` style
|
|
133
|
-
# @param node [Prism::CallNode] Call node
|
|
134
|
-
# @return [String] Normalized source code
|
|
135
|
-
def normalize_call_node(node)
|
|
136
|
-
return node.slice.strip unless node.is_a?(Prism::CallNode)
|
|
137
|
-
|
|
138
|
-
method_name = node.name
|
|
139
|
-
args = node.arguments&.arguments || []
|
|
140
|
-
|
|
141
|
-
if args.empty?
|
|
142
|
-
"#{method_name}()"
|
|
143
|
-
else
|
|
144
|
-
arg_strings = args.map { |arg| normalize_argument(arg) }
|
|
145
|
-
"#{method_name}(#{arg_strings.join(", ")})"
|
|
146
|
-
end
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
# Normalize an argument node to canonical format
|
|
150
|
-
# @param arg [Prism::Node] Argument node
|
|
151
|
-
# @return [String] Normalized argument source
|
|
152
|
-
def normalize_argument(arg)
|
|
153
|
-
case arg
|
|
154
|
-
when Prism::StringNode
|
|
155
|
-
"\"#{arg.unescaped}\""
|
|
156
|
-
when Prism::SymbolNode
|
|
157
|
-
":#{arg.unescaped}"
|
|
158
|
-
when Prism::KeywordHashNode
|
|
159
|
-
# Handle hash arguments like {key: value}
|
|
160
|
-
pairs = arg.elements.map do |assoc|
|
|
161
|
-
key = case assoc.key
|
|
162
|
-
when Prism::SymbolNode then "#{assoc.key.unescaped}:"
|
|
163
|
-
when Prism::StringNode then "\"#{assoc.key.unescaped}\" =>"
|
|
164
|
-
else "#{assoc.key.slice} =>"
|
|
165
|
-
end
|
|
166
|
-
value = normalize_argument(assoc.value)
|
|
167
|
-
"#{key} #{value}"
|
|
168
|
-
end.join(", ")
|
|
169
|
-
pairs
|
|
170
|
-
when Prism::HashNode
|
|
171
|
-
# Handle explicit hash syntax
|
|
172
|
-
pairs = arg.elements.map do |assoc|
|
|
173
|
-
key_part = normalize_argument(assoc.key)
|
|
174
|
-
value_part = normalize_argument(assoc.value)
|
|
175
|
-
"#{key_part} => #{value_part}"
|
|
176
|
-
end.join(", ")
|
|
177
|
-
"{#{pairs}}"
|
|
178
|
-
else
|
|
179
|
-
# For other types (numbers, arrays, etc.), use the original source
|
|
180
|
-
arg.slice.strip
|
|
181
|
-
end
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
# Check if a node is a specific method call
|
|
185
|
-
# @param node [Prism::Node] Node to check
|
|
186
|
-
# @param method_name [Symbol] Method name to check for
|
|
187
|
-
# @return [Boolean] True if node is a call to the specified method
|
|
188
|
-
def call_to?(node, method_name)
|
|
189
|
-
node.is_a?(Prism::CallNode) && node.name == method_name
|
|
190
|
-
end
|
|
191
|
-
|
|
192
|
-
# Check if a node is a block call to a specific method
|
|
193
|
-
# @param node [Prism::Node] Node to check
|
|
194
|
-
# @param method_name [Symbol] Method name to check for
|
|
195
|
-
# @return [Boolean] True if node is a block call to the specified method
|
|
196
|
-
def block_call_to?(node, method_name)
|
|
197
|
-
node.is_a?(Prism::CallNode) && node.name == method_name && !node.block.nil?
|
|
198
|
-
end
|
|
199
|
-
end
|
|
200
|
-
end
|
|
201
|
-
end
|