Module: Kettle::Dev::PrismAppraisals
- Defined in:
- lib/kettle/dev/prism_appraisals.rb
Overview
AST-driven merger for Appraisals files using Prism.
Preserves all comments: preamble headers, block headers, and inline comments.
Uses PrismUtils for shared Prism AST operations.
Constant Summary collapse
- TRACKED_METHODS =
[:gem, :eval_gemfile, :gemfile].freeze
Class Method Summary collapse
-
.appraise_call?(node) ⇒ Boolean
-
.build_output(preamble_lines, blocks) ⇒ Object
-
.extract_appraise_name(node) ⇒ Object
-
.extract_block_header(node, source_lines, previous_blocks) ⇒ Object
-
.extract_blocks(parse_result, source_content) ⇒ Object
…existing helper methods copied from original AppraisalsAstMerger…
-
.extract_original_statements(node) ⇒ Object
-
.merge(template_content, dest_content) ⇒ Object
Merge template and destination Appraisals files preserving comments.
-
.merge_block_headers(tmpl_header, dest_header) ⇒ Object
-
.merge_block_statements(tmpl_body, dest_body, dest_result) ⇒ Object
-
.merge_blocks(template_blocks, dest_blocks, tmpl_result, dest_result) ⇒ Object
-
.merge_preambles(tmpl_comments, dest_comments) ⇒ Object
-
.normalize_argument(arg) ⇒ Object
-
.normalize_statement(node) ⇒ Object
-
.remove_gem_dependency(content, gem_name) ⇒ String
Remove gem calls that reference the given gem name (to prevent self-dependency).
-
.statement_key(node) ⇒ Object
Class Method Details
.appraise_call?(node) ⇒ Boolean
72 73 74 |
# File 'lib/kettle/dev/prism_appraisals.rb', line 72 def appraise_call?(node) PrismUtils.block_call_to?(node, :appraise) end |
.build_output(preamble_lines, blocks) ⇒ Object
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 282 283 284 285 286 287 |
# File 'lib/kettle/dev/prism_appraisals.rb', line 250 def build_output(preamble_lines, blocks) output = [] preamble_lines.each { |line| output << line } output << "" unless preamble_lines.empty? blocks.each do |block| header = block[:header] if header && !header.strip.empty? output << header.rstrip end name = block[:name] output << "appraise(\"#{name}\") {" statements = block[:statements] || extract_original_statements(block[:node]) statements.each do |stmt_info| leading = stmt_info[:leading_comments] || [] leading.each do |comment| output << " #{comment.slice.strip}" end node = stmt_info[:node] line = normalize_statement(node) # Remove any leading whitespace/newlines from the normalized line line = line.to_s.sub(/\A\s+/, "") inline = stmt_info[:inline_comments] || [] inline_str = inline.map { |c| c.slice.strip }.join(" ") output << " #{line}#{" " + inline_str unless inline_str.empty?}" end output << "}" output << "" end build = output.join("\n").strip + "\n" build end |
.extract_appraise_name(node) ⇒ Object
76 77 78 79 |
# File 'lib/kettle/dev/prism_appraisals.rb', line 76 def extract_appraise_name(node) return unless node.is_a?(Prism::CallNode) PrismUtils.extract_literal_value(node.arguments&.arguments&.first) end |
.extract_block_header(node, source_lines, previous_blocks) ⇒ Object
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 |
# File 'lib/kettle/dev/prism_appraisals.rb', line 104 def extract_block_header(node, source_lines, previous_blocks) begin_line = node.location.start_line min_line = if previous_blocks.empty? 1 else previous_blocks.last[:node].location.end_line + 1 end check_line = begin_line - 2 header_lines = [] while check_line >= 0 && (check_line + 1) >= min_line line = source_lines[check_line] break unless line if line.strip.empty? break elsif line.lstrip.start_with?("#") header_lines.unshift(line) check_line -= 1 else break end end header_lines.join rescue StandardError => e Kettle::Dev.debug_error(e, __method__) if defined?(Kettle::Dev.debug_error) "" end |
.extract_blocks(parse_result, source_content) ⇒ Object
…existing helper methods copied from original AppraisalsAstMerger…
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 |
# File 'lib/kettle/dev/prism_appraisals.rb', line 36 def extract_blocks(parse_result, source_content) root = parse_result.value return [[], []] unless root&.statements&.body source_lines = source_content.lines blocks = [] first_appraise_line = nil root.statements.body.each do |node| if appraise_call?(node) first_appraise_line ||= node.location.start_line name = extract_appraise_name(node) next unless name block_header = extract_block_header(node, source_lines, blocks) blocks << { node: node, name: name, header: block_header, } end end preamble_comments = if first_appraise_line parse_result.comments.select { |c| c.location.start_line < first_appraise_line } else parse_result.comments end block_header_lines = blocks.flat_map { |b| b[:header].lines.map { |l| l.strip } }.to_set preamble_comments = preamble_comments.reject { |c| block_header_lines.include?(c.slice.strip) } [preamble_comments, blocks] end |
.extract_original_statements(node) ⇒ Object
298 299 300 301 302 303 |
# File 'lib/kettle/dev/prism_appraisals.rb', line 298 def extract_original_statements(node) body = node.block&.body return [] unless body statements = body.is_a?(Prism::StatementsNode) ? body.body : [body] statements.compact.map { |stmt| {node: stmt, inline_comments: [], leading_comments: []} } end |
.merge(template_content, dest_content) ⇒ Object
Merge template and destination Appraisals files preserving comments
16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
# File 'lib/kettle/dev/prism_appraisals.rb', line 16 def merge(template_content, dest_content) template_content ||= "" dest_content ||= "" return template_content if dest_content.strip.empty? return dest_content if template_content.strip.empty? tmpl_result = PrismUtils.parse_with_comments(template_content) dest_result = PrismUtils.parse_with_comments(dest_content) tmpl_preamble, tmpl_blocks = extract_blocks(tmpl_result, template_content) dest_preamble, dest_blocks = extract_blocks(dest_result, dest_content) merged_preamble = merge_preambles(tmpl_preamble, dest_preamble) merged_blocks = merge_blocks(tmpl_blocks, dest_blocks, tmpl_result, dest_result) build_output(merged_preamble, merged_blocks) end |
.merge_block_headers(tmpl_header, dest_header) ⇒ Object
181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 |
# File 'lib/kettle/dev/prism_appraisals.rb', line 181 def merge_block_headers(tmpl_header, dest_header) tmpl_lines = tmpl_header.to_s.lines.map(&:strip).reject(&:empty?) dest_lines = dest_header.to_s.lines.map(&:strip).reject(&:empty?) merged = [] seen = Set.new (tmpl_lines + dest_lines).each do |line| normalized = line.downcase unless seen.include?(normalized) merged << line seen << normalized end end return "" if merged.empty? merged.join("\n") + "\n" end |
.merge_block_statements(tmpl_body, dest_body, dest_result) ⇒ Object
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 |
# File 'lib/kettle/dev/prism_appraisals.rb', line 197 def merge_block_statements(tmpl_body, dest_body, dest_result) tmpl_stmts = PrismUtils.extract_statements(tmpl_body) dest_stmts = PrismUtils.extract_statements(dest_body) tmpl_keys = Set.new tmpl_key_to_node = {} tmpl_stmts.each do |stmt| key = statement_key(stmt) if key tmpl_keys << key tmpl_key_to_node[key] = stmt end end dest_keys = Set.new dest_stmts.each do |stmt| key = statement_key(stmt) dest_keys << key if key end merged = [] dest_stmts.each_with_index do |dest_stmt, idx| dest_key = statement_key(dest_stmt) if dest_key && tmpl_keys.include?(dest_key) merged << {node: tmpl_key_to_node[dest_key], inline_comments: [], leading_comments: [], shared: true, key: dest_key} else inline_comments = PrismUtils.inline_comments_for_node(dest_result, dest_stmt) prev_stmt = (idx > 0) ? dest_stmts[idx - 1] : nil leading_comments = PrismUtils.find_leading_comments(dest_result, dest_stmt, prev_stmt, dest_body) merged << {node: dest_stmt, inline_comments: inline_comments, leading_comments: leading_comments, shared: false} end end tmpl_stmts.each do |tmpl_stmt| tmpl_key = statement_key(tmpl_stmt) unless tmpl_key && dest_keys.include?(tmpl_key) merged << {node: tmpl_stmt, inline_comments: [], leading_comments: [], shared: false} end end merged.each do |item| item.delete(:shared) item.delete(:key) end merged end |
.merge_blocks(template_blocks, dest_blocks, tmpl_result, dest_result) ⇒ Object
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 |
# File 'lib/kettle/dev/prism_appraisals.rb', line 131 def merge_blocks(template_blocks, dest_blocks, tmpl_result, dest_result) merged = [] dest_by_name = dest_blocks.each_with_object({}) { |b, h| h[b[:name]] = b } template_names = template_blocks.map { |b| b[:name] }.to_set placed_dest = Set.new template_blocks.each_with_index do |tmpl_block, idx| name = tmpl_block[:name] if idx == 0 || dest_by_name[name] dest_blocks.each do |db| next if template_names.include?(db[:name]) next if placed_dest.include?(db[:name]) dest_idx_of_shared = dest_blocks.index { |b| b[:name] == name } dest_idx_of_only = dest_blocks.index { |b| b[:name] == db[:name] } if dest_idx_of_only && dest_idx_of_shared && dest_idx_of_only < dest_idx_of_shared merged << db placed_dest << db[:name] end end end dest_block = dest_by_name[name] if dest_block merged_header = merge_block_headers(tmpl_block[:header], dest_block[:header]) merged_statements = merge_block_statements( tmpl_block[:node].block.body, dest_block[:node].block.body, dest_result, ) merged << { name: name, header: merged_header, node: tmpl_block[:node], statements: merged_statements, } placed_dest << name else merged << tmpl_block end end dest_blocks.each do |dest_block| next if placed_dest.include?(dest_block[:name]) next if template_names.include?(dest_block[:name]) merged << dest_block end merged end |
.merge_preambles(tmpl_comments, dest_comments) ⇒ Object
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 |
# File 'lib/kettle/dev/prism_appraisals.rb', line 81 def merge_preambles(tmpl_comments, dest_comments) tmpl_lines = tmpl_comments.map { |c| c.slice.strip } dest_lines = dest_comments.map { |c| c.slice.strip } magic_pattern = /^#.*frozen_string_literal/ if tmpl_lines.any? { |line| line.match?(magic_pattern) } dest_lines.reject! { |line| line.match?(magic_pattern) } end merged = [] seen = Set.new (tmpl_lines + dest_lines).each do |line| normalized = line.downcase unless seen.include?(normalized) merged << line seen << normalized end end merged end |
.normalize_argument(arg) ⇒ Object
294 295 296 |
# File 'lib/kettle/dev/prism_appraisals.rb', line 294 def normalize_argument(arg) PrismUtils.normalize_argument(arg) end |
.normalize_statement(node) ⇒ Object
289 290 291 292 |
# File 'lib/kettle/dev/prism_appraisals.rb', line 289 def normalize_statement(node) return PrismUtils.node_to_source(node) unless node.is_a?(Prism::CallNode) PrismUtils.normalize_call_node(node) end |
.remove_gem_dependency(content, gem_name) ⇒ String
Remove gem calls that reference the given gem name (to prevent self-dependency).
Works by locating gem() call nodes within appraise blocks where the first argument matches gem_name.
310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 |
# File 'lib/kettle/dev/prism_appraisals.rb', line 310 def remove_gem_dependency(content, gem_name) return content if gem_name.to_s.strip.empty? result = PrismUtils.parse_with_comments(content) root = result.value return content unless root&.statements&.body out = content.dup # Iterate through all appraise blocks root.statements.body.each do |node| next unless appraise_call?(node) next unless node.block&.body body_stmts = PrismUtils.extract_statements(node.block.body) # Find gem call nodes within this appraise block where first argument matches gem_name body_stmts.each do |stmt| next unless stmt.is_a?(Prism::CallNode) && stmt.name == :gem first_arg = stmt.arguments&.arguments&.first arg_val = begin PrismUtils.extract_literal_value(first_arg) rescue StandardError nil end if arg_val && arg_val.to_s == gem_name.to_s # Remove this gem call from content out = out.sub(stmt.slice, "") end end end out rescue StandardError => e Kettle::Dev.debug_error(e, __method__) if defined?(Kettle::Dev.debug_error) content end |
.statement_key(node) ⇒ Object
246 247 248 |
# File 'lib/kettle/dev/prism_appraisals.rb', line 246 def statement_key(node) PrismUtils.statement_key(node, tracked_methods: TRACKED_METHODS) end |