Module: Kettle::Dev::PrismGemspec
- Defined in:
- lib/kettle/dev/prism_gemspec.rb
Overview
Prism helpers for gemspec manipulation.
Class Method Summary collapse
-
.debug_error(error, context = nil) ⇒ void
Emit a debug warning for rescued errors when kettle-dev debugging is enabled.
-
.ensure_development_dependencies(content, desired) ⇒ Object
Ensure development dependency lines in a gemspec match the desired lines.
-
.remove_spec_dependency(content, gem_name) ⇒ Object
Remove spec.add_dependency / add_development_dependency calls that name the given gem Works by locating the Gem::Specification block and filtering out matching call lines.
-
.replace_gemspec_fields(content, replacements = {}) ⇒ Object
Replace scalar or array assignments inside a Gem::Specification.new block.
Class Method Details
.debug_error(error, context = nil) ⇒ void
This method returns an undefined value.
Emit a debug warning for rescued errors when kettle-dev debugging is enabled.
Controlled by KETTLE_DEV_DEBUG=true (or DEBUG=true as fallback).
14 15 16 |
# File 'lib/kettle/dev/prism_gemspec.rb', line 14 def debug_error(error, context = nil) Kettle::Dev.debug_error(error, context) end |
.ensure_development_dependencies(content, desired) ⇒ Object
Ensure development dependency lines in a gemspec match the desired lines.
desired is a hash mapping gem_name => desired_line (string, without leading indentation).
Returns the modified gemspec content (or original on error).
196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 |
# File 'lib/kettle/dev/prism_gemspec.rb', line 196 def ensure_development_dependencies(content, desired) return content if desired.nil? || desired.empty? result = PrismUtils.parse_with_comments(content) stmts = PrismUtils.extract_statements(result.value.statements) gemspec_call = stmts.find do |s| s.is_a?(Prism::CallNode) && s.block && PrismUtils.extract_const_name(s.receiver) == "Gem::Specification" && s.name == :new end # If we couldn't locate the Gem::Specification.new block (e.g., empty or # truncated gemspec), fall back to appending the desired development # dependency lines to the end of the file so callers still get the # expected dependency declarations. unless gemspec_call begin out = content.dup out << "\n" unless out.end_with?("\n") || out.empty? desired.each do |_gem, line| out << line.strip + "\n" end return out rescue StandardError => e debug_error(e, __method__) return content end end call_src = gemspec_call.slice body_node = gemspec_call.block&.body body_src = begin if (m = call_src.match(/do\b[^\n]*\|[^|]*\|\s*(.*)end\s*\z/m)) m[1] else body_node ? body_node.slice : "" end rescue StandardError body_node ? body_node.slice : "" end new_body = body_src.dup stmt_nodes = PrismUtils.extract_statements(body_node) # Find version node to choose insertion point version_node = stmt_nodes.find do |n| n.is_a?(Prism::CallNode) && n.name.to_s.start_with?("version") && n.receiver && n.receiver.slice.strip.end_with?("spec") end desired.each do |gem_name, desired_line| # Skip commented occurrences - we only act on actual AST nodes found = stmt_nodes.find do |n| next false unless n.is_a?(Prism::CallNode) next false unless [:add_development_dependency, :add_dependency].include?(n.name) first_arg = n.arguments&.arguments&.first val = begin PrismUtils.extract_literal_value(first_arg) rescue nil end val && val.to_s == gem_name end if found # Replace existing node slice with desired_line, preserving indent indent = begin found.slice.lines.first.match(/^(\s*)/)[1] rescue " " end replacement = indent + desired_line.strip + "\n" new_body = new_body.sub(found.slice, replacement) else # Insert after version_node if present, else append before end insert_line = " " + desired_line.strip + "\n" new_body = if version_node new_body.sub(version_node.slice, version_node.slice + "\n" + insert_line) else new_body.rstrip + "\n" + insert_line end end end new_call_src = call_src.sub(body_src, new_body) content.sub(call_src, new_call_src) rescue StandardError => e debug_error(e, __method__) content end |
.remove_spec_dependency(content, gem_name) ⇒ Object
Remove spec.add_dependency / add_development_dependency calls that name the given gem
Works by locating the Gem::Specification block and filtering out matching call lines.
188 189 190 191 |
# File 'lib/kettle/dev/prism_gemspec.rb', line 188 def remove_spec_dependency(content, gem_name) return content if gem_name.to_s.strip.empty? replace_gemspec_fields(content, _remove_self_dependency: gem_name) end |
.replace_gemspec_fields(content, replacements = {}) ⇒ Object
Replace scalar or array assignments inside a Gem::Specification.new block.
replacements is a hash mapping symbol field names to string or array values.
Operates only inside the Gem::Specification block to avoid accidental matches.
21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 |
# File 'lib/kettle/dev/prism_gemspec.rb', line 21 def replace_gemspec_fields(content, replacements = {}) return content if replacements.nil? || replacements.empty? result = PrismUtils.parse_with_comments(content) stmts = PrismUtils.extract_statements(result.value.statements) gemspec_call = stmts.find do |s| s.is_a?(Prism::CallNode) && s.block && PrismUtils.extract_const_name(s.receiver) == "Gem::Specification" && s.name == :new end return content unless gemspec_call call_src = gemspec_call.slice # Try to detect block parameter name (e.g., |spec|) blk_param = nil begin if gemspec_call.block && gemspec_call.block.params # Attempt a few defensive ways to extract a param name if gemspec_call.block.params.respond_to?(:parameters) && gemspec_call.block.params.parameters.respond_to?(:first) p = gemspec_call.block.params.parameters.first blk_param = p.name.to_s if p.respond_to?(:name) elsif gemspec_call.block.params.respond_to?(:first) p = gemspec_call.block.params.first blk_param = p.name.to_s if p && p.respond_to?(:name) end end rescue StandardError blk_param = nil end # Fallback to crude parse of the call_src header unless blk_param && !blk_param.to_s.empty? hdr_m = call_src.match(/do\b[^\n]*\|([^|]+)\|/m) blk_param = (hdr_m && hdr_m[1]) ? hdr_m[1].strip.split(/,\s*/).first : "spec" end blk_param = "spec" if blk_param.nil? || blk_param.empty? # Extract AST-level statements inside the block body when available body_node = gemspec_call.block&.body body_src = "" begin # Try to extract the textual body from call_src using the do|...| ... end capture body_src = if (m = call_src.match(/do\b[^\n]*\|[^|]*\|\s*(.*)end\s*\z/m)) m[1] else # Last resort: attempt to take slice of body node body_node ? body_node.slice : "" end rescue StandardError body_src = body_node ? body_node.slice : "" end new_body = body_src.dup # Helper: build literal text for replacement values build_literal = lambda do |v| if v.is_a?(Array) arr = v.compact.map(&:to_s).map { |e| '"' + e.gsub('"', '\\"') + '"' } "[" + arr.join(", ") + "]" else '"' + v.to_s.gsub('"', '\\"') + '"' end end # Extract existing statement nodes for more precise matching stmt_nodes = PrismUtils.extract_statements(body_node) replacements.each do |field_sym, value| # Skip special internal keys that are not actual gemspec fields next if field_sym == :_remove_self_dependency field = field_sym.to_s # Find an existing assignment node for this field: look for call nodes where # receiver slice matches the block param and method name matches assignment found_node = stmt_nodes.find do |n| next false unless n.is_a?(Prism::CallNode) begin recv = n.receiver recv_name = recv ? recv.slice.strip : nil # match receiver variable name or literal slice recv_name && recv_name.end_with?(blk_param) && n.name.to_s.start_with?(field) rescue StandardError false end end if found_node # Do not replace if the existing RHS is non-literal (e.g., computed expression) existing_arg = found_node.arguments&.arguments&.first existing_literal = begin PrismUtils.extract_literal_value(existing_arg) rescue nil end if existing_literal.nil? && !value.nil? # Skip replacing a non-literal RHS to avoid altering computed expressions. debug_error(StandardError.new("Skipping replacement for #{field} because existing RHS is non-literal"), __method__) else # Replace the found node's slice in the body text with the updated assignment indent = begin found_node.slice.lines.first.match(/^(\s*)/)[1] rescue " " end rhs = build_literal.call(value) replacement = "#{indent}#{blk_param}.#{field} = #{rhs}" new_body = new_body.sub(found_node.slice, replacement) end else # No existing assignment; insert after spec.version if present, else append version_node = stmt_nodes.find do |n| n.is_a?(Prism::CallNode) && n.name.to_s.start_with?("version", "version=") && n.receiver && n.receiver.slice.strip.end_with?(blk_param) end insert_line = " #{blk_param}.#{field} = #{build_literal.call(value)}\n" new_body = if version_node # Insert after the version node slice new_body.sub(version_node.slice, version_node.slice + "\n" + insert_line) elsif new_body.rstrip.end_with?('\n') # Append before the final newline if present, else just append new_body.rstrip + "\n" + insert_line else new_body.rstrip + "\n" + insert_line end end end # Handle removal of self-dependency if requested via :_remove_self_dependency if replacements[:_remove_self_dependency] name_to_remove = replacements[:_remove_self_dependency].to_s # Find dependency call nodes to remove (add_dependency/add_development_dependency) dep_nodes = stmt_nodes.select do |n| next false unless n.is_a?(Prism::CallNode) recv = begin n.receiver rescue nil end next false unless recv && recv.slice.strip.end_with?(blk_param) [:add_dependency, :add_development_dependency].include?(n.name) end dep_nodes.each do |dn| # Check first argument literal first_arg = dn.arguments&.arguments&.first arg_val = begin PrismUtils.extract_literal_value(first_arg) rescue nil end if arg_val && arg_val.to_s == name_to_remove # Remove this node's slice from new_body new_body = new_body.sub(dn.slice, "") end end end # Reassemble call source by replacing the captured body portion new_call_src = call_src.sub(body_src, new_body) content.sub(call_src, new_call_src) rescue StandardError => e debug_error(e, __method__) content end |