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
-
.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 |
.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 |