prism-merge 2.0.0 → 2.0.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1425ee625e5f45061e5faa4d942371f94498082aa00660a4e8b42d50c49b1368
4
- data.tar.gz: 45b4f7343c85a8b87b95929c4a17da4db132cb69dfdcfdf3a4078e2ea88f1838
3
+ metadata.gz: f43aef831e7dd032d7607b541bcc542cb2899514150346632084148b167dc0a5
4
+ data.tar.gz: 104a80156b7b15d02c8b7f5b237998a4d4c225f22f7d108c46642ddea7e6c51e
5
5
  SHA512:
6
- metadata.gz: 45baf957dc196b7f81084a98ae8c52a3d9121e40d8b39355320825cf02b033b2381ae30ce54b439519808fc3dc859ba653e29b2042770497cbef5b53dd796a7c
7
- data.tar.gz: cb6bab2681c92c7900265fbc5fe136dc712bfe1d20cade3aa3145962f7966c4db66aa5d3f34b19887aa707ad362429edae8e7bdb5d371de46acd6ee3221f39c6
6
+ metadata.gz: 425c404b3e0396b5db166b0c4e2fca941af3f6b6d0727ece8b0cd19bc60f9901883d587dee4a90c43356b6fa4b6b98d841fbf791785fb410662d72df0e7c5d2b
7
+ data.tar.gz: eb666082134afb97479cd8dd020baa6b8ef45b489d0b8c3a4df8ad6e19e8100d53cba60d48c930d7ed43ce8830bf2ec5b309fc432a24dfb5440c1fa9eb9a9b7f
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -30,6 +30,63 @@ Please file a bug if you notice a violation of semantic versioning.
30
30
 
31
31
  ### Security
32
32
 
33
+ ## [2.0.2] - 2026-02-22
34
+
35
+ - TAG: [v2.0.2][2.0.2t]
36
+ - COVERAGE: 98.80% -- 823/833 lines in 12 files
37
+ - BRANCH COVERAGE: 85.61% -- 452/528 branches in 12 files
38
+ - 93.51% documented
39
+
40
+ ### Fixed
41
+
42
+ - Fix node duplication when merging files with inline trailing comments (e.g.,
43
+ gemspec `add_dependency` lines with `# ruby >= 3.2.0`). `add_node_to_result`
44
+ output the full source line (which already includes inline comments via
45
+ `analysis.line_at`), then also iterated `trailing_comments` and re-emitted any
46
+ comment on the same line — duplicating the entire line. Now skips trailing
47
+ comments whose `start_line` falls within the node's own line range. This was the
48
+ root cause of every `add_dependency` / `add_development_dependency` being
49
+ duplicated in gemspec and gemfile merges when inline comments were present.
50
+ - Prevent potential double-wrapping in `merge_node_body_recursively` — store the
51
+ raw (unwrapped) `signature_generator` as `@raw_signature_generator` and pass it
52
+ (instead of the already-effective generator) to inner `SmartMerger` instances.
53
+ This ensures `build_effective_signature_generator` wraps it only once when
54
+ `node_typing` is also configured.
55
+
56
+ ## [2.0.1] - 2026-02-22
57
+
58
+ - TAG: [v2.0.1][2.0.1t]
59
+ - COVERAGE: 98.80% -- 820/830 lines in 12 files
60
+ - BRANCH COVERAGE: 85.55% -- 450/526 branches in 12 files
61
+ - 93.51% documented
62
+
63
+ ### Added
64
+
65
+ - `SmartMerger#emit_dest_prefix_lines` preserves magic comments (e.g., `# frozen_string_literal: true`)
66
+ and blank lines that appear before the first AST node in the destination file
67
+ - `SmartMerger#emit_dest_gap_lines` preserves blank lines between consecutive top-level blocks
68
+ in the destination, preventing them from being silently stripped during merge
69
+
70
+ ### Changed
71
+
72
+ - `SmartMerger#merge_with_debug` now uses `merge_result` (returns `MergeResult` object)
73
+ instead of `merge` (returns `String`), so `statistics` and `decision_summary` are accessible
74
+ - `SmartMerger#build_result` now passes `template_analysis` and `dest_analysis` to
75
+ `MergeResult.new` for consistency with `SmartMergerBase` API
76
+
77
+ ### Removed
78
+
79
+ - Removed redundant `attr_reader :node_typing` from `SmartMerger` — already provided
80
+ by `SmartMergerBase`
81
+
82
+ ### Fixed
83
+
84
+ - Inter-node blank line stripping: blank lines between top-level blocks (e.g., between
85
+ `appraise` blocks in Appraisals, between `gem` calls in Gemfiles) are now preserved
86
+ from the destination source during merge
87
+ - Prefix line stripping: magic comments and blank lines before the first AST statement
88
+ (e.g., `# frozen_string_literal: true` in Appraisal.root.gemfile) are now preserved
89
+
33
90
  ## [2.0.0] - 2026-02-19
34
91
 
35
92
  - TAG: [v2.0.0][2.0.0t]
@@ -281,7 +338,11 @@ Please file a bug if you notice a violation of semantic versioning.
281
338
 
282
339
  - Initial release
283
340
 
284
- [Unreleased]: https://github.com/kettle-rb/prism-merge/compare/v2.0.0...HEAD
341
+ [Unreleased]: https://github.com/kettle-rb/prism-merge/compare/v2.0.2...HEAD
342
+ [2.0.2]: https://github.com/kettle-rb/prism-merge/compare/v2.0.1...v2.0.2
343
+ [2.0.2t]: https://github.com/kettle-rb/prism-merge/releases/tag/v2.0.2
344
+ [2.0.1]: https://github.com/kettle-rb/prism-merge/compare/v2.0.0...v2.0.1
345
+ [2.0.1t]: https://github.com/kettle-rb/prism-merge/releases/tag/v2.0.1
285
346
  [2.0.0]: https://github.com/kettle-rb/prism-merge/compare/v1.1.6...v2.0.0
286
347
  [2.0.0t]: https://github.com/kettle-rb/prism-merge/releases/tag/v2.0.0
287
348
  [1.1.6]: https://github.com/kettle-rb/prism-merge/compare/v1.1.5...v1.1.6
data/CITATION.cff CHANGED
@@ -7,8 +7,8 @@ type: software
7
7
  authors:
8
8
  - given-names: Peter Hurn
9
9
  family-names: Boling
10
- email: peter@railsbling.com
11
- affiliation: railsbling.com
10
+ email: floss@galtzo.com
11
+ affiliation: galtzo.com
12
12
  orcid: 'https://orcid.org/0009-0008-8519-441X'
13
13
  identifiers:
14
14
  - type: url
data/README.md CHANGED
@@ -80,7 +80,7 @@ The `*-merge` gem family provides intelligent, AST-based merging for various fil
80
80
  | [markly-merge][markly-merge] | [![Version][markly-merge-gem-i]][markly-merge-gem] <br/> [![CI][markly-merge-ci-i]][markly-merge-ci] | Markdown | [Markly][markly] (via tree_haver) | Smart merge for Markdown (CommonMark via cmark-gfm C) |
81
81
  | [prism-merge][prism-merge] | [![Version][prism-merge-gem-i]][prism-merge-gem] <br/> [![CI][prism-merge-ci-i]][prism-merge-ci] | Ruby | [Prism][prism] (`prism` std lib gem) | Smart merge for Ruby source files |
82
82
  | [psych-merge][psych-merge] | [![Version][psych-merge-gem-i]][psych-merge-gem] <br/> [![CI][psych-merge-ci-i]][psych-merge-ci] | YAML | [Psych][psych] (`psych` std lib gem) | Smart merge for YAML files |
83
- | [rbs-merge][rbs-merge] | [![Version][rbs-merge-gem-i]][rbs-merge-gem] <br/> [![CI][rbs-merge-ci-i]][rbs-merge-ci] | RBS | [tree-sitter-bash][ts-rbs] (via tree_haver), [RBS][rbs] (`rbs` std lib gem) | Smart merge for Ruby type signatures |
83
+ | [rbs-merge][rbs-merge] | [![Version][rbs-merge-gem-i]][rbs-merge-gem] <br/> [![CI][rbs-merge-ci-i]][rbs-merge-ci] | RBS | [tree-sitter-rbs][ts-rbs] (via tree_haver), [RBS][rbs] (`rbs` std lib gem) | Smart merge for Ruby type signatures |
84
84
  | [toml-merge][toml-merge] | [![Version][toml-merge-gem-i]][toml-merge-gem] <br/> [![CI][toml-merge-ci-i]][toml-merge-ci] | TOML | [Parslet + toml][toml], [Citrus + toml-rb][toml-rb], [tree-sitter-toml][ts-toml] (all via tree_haver) | Smart merge for TOML files |
85
85
 
86
86
  #### Backend Platform Compatibility
@@ -1564,7 +1564,7 @@ Thanks for RTFM. ☺️
1564
1564
  [📌gitmoji]: https://gitmoji.dev
1565
1565
  [📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
1566
1566
  [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
1567
- [🧮kloc-img]: https://img.shields.io/badge/KLOC-0.802-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
1567
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-0.833-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
1568
1568
  [🔐security]: SECURITY.md
1569
1569
  [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
1570
1570
  [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
@@ -46,9 +46,6 @@ module Prism
46
46
  # result = merger.merge
47
47
  #
48
48
  class SmartMerger < ::Ast::Merge::SmartMergerBase
49
- # @return [Hash{Symbol,String => #call}, nil] Node typing configuration
50
- attr_reader :node_typing
51
-
52
49
  # @return [Integer, Float] Maximum recursion depth for body merging
53
50
  attr_reader :max_recursion_depth
54
51
 
@@ -97,6 +94,11 @@ module Prism
97
94
  @current_depth = current_depth
98
95
  @text_merger_options = text_merger_options
99
96
 
97
+ # Store the raw (unwrapped) signature_generator so that
98
+ # merge_node_body_recursively can pass it to inner SmartMergers
99
+ # without double-wrapping.
100
+ @raw_signature_generator = signature_generator
101
+
100
102
  # Wrap signature_generator to include node_typing processing
101
103
  effective_signature_generator = build_effective_signature_generator(signature_generator, node_typing)
102
104
 
@@ -139,9 +141,9 @@ module Prism
139
141
  #
140
142
  # @return [Hash] Hash with :content, :debug, and :statistics keys
141
143
  def merge_with_debug
142
- result = merge
144
+ result_obj = merge_result
143
145
  {
144
- content: result.to_s,
146
+ content: result_obj.to_s,
145
147
  debug: {
146
148
  template_statements: @template_analysis&.statements&.size || 0,
147
149
  dest_statements: @dest_analysis&.statements&.size || 0,
@@ -149,7 +151,7 @@ module Prism
149
151
  add_template_only_nodes: @add_template_only_nodes,
150
152
  freeze_token: @freeze_token,
151
153
  },
152
- statistics: result.respond_to?(:statistics) ? result.statistics : {},
154
+ statistics: result_obj.respond_to?(:statistics) ? result_obj.statistics : result_obj.decision_summary,
153
155
  }
154
156
  end
155
157
 
@@ -180,9 +182,12 @@ module Prism
180
182
  nil
181
183
  end
182
184
 
183
- # Build the result (no-arg constructor for Prism)
185
+ # Build the result with analysis references
184
186
  def build_result
185
- MergeResult.new
187
+ MergeResult.new(
188
+ template_analysis: @template_analysis,
189
+ dest_analysis: @dest_analysis,
190
+ )
186
191
  end
187
192
 
188
193
  # @return [Class] The template parse error class for Ruby
@@ -240,6 +245,10 @@ module Prism
240
245
 
241
246
  # Phase 2: Process dest nodes in their original order
242
247
  # This preserves dest-only nodes in their original position relative to matched nodes
248
+
249
+ # Emit prefix lines from the dest source (magic comments, blank lines before first node)
250
+ last_output_dest_line = emit_dest_prefix_lines(@result, @dest_analysis)
251
+
243
252
  @dest_analysis.statements.each do |dest_node|
244
253
  dest_signature = @dest_analysis.generate_signature(dest_node)
245
254
 
@@ -250,6 +259,9 @@ module Prism
250
259
  node_range = dest_node.location.start_line..dest_node.location.end_line
251
260
  next if output_dest_line_ranges.any? { |range| range.cover?(node_range.begin) && range.cover?(node_range.end) }
252
261
 
262
+ # Emit inter-node gap lines from the dest source (blank lines between blocks)
263
+ last_output_dest_line = emit_dest_gap_lines(@result, @dest_analysis, last_output_dest_line, dest_node)
264
+
253
265
  if dest_signature && template_by_signature.key?(dest_signature)
254
266
  # Matched node - merge with template version
255
267
  template_node = template_by_signature[dest_signature]
@@ -277,6 +289,12 @@ module Prism
277
289
  output_dest_line_ranges << node_range
278
290
  output_signatures << dest_signature if dest_signature
279
291
  end
292
+
293
+ # Update last_output_dest_line to track trailing blank line from add_node_to_result
294
+ last_output_dest_line = dest_node.location.end_line
295
+ trailing_line = last_output_dest_line + 1
296
+ trailing_content = @dest_analysis.line_at(trailing_line)
297
+ last_output_dest_line = trailing_line if trailing_content && trailing_content.strip.empty?
280
298
  end
281
299
 
282
300
  @result
@@ -596,6 +614,64 @@ module Prism
596
614
  }
597
615
  end
598
616
 
617
+ # Emit prefix lines from the destination source that appear before the first node.
618
+ # This preserves magic comments (e.g., `# frozen_string_literal: true`) and blank
619
+ # lines that precede any AST statements.
620
+ #
621
+ # @param result [MergeResult] The merge result
622
+ # @param analysis [FileAnalysis] The destination file analysis
623
+ # @return [Integer] The last line number emitted (0 if none)
624
+ def emit_dest_prefix_lines(result, analysis)
625
+ return 0 if analysis.statements.empty?
626
+
627
+ first_node = analysis.statements.first
628
+ # Find the first line of content: either leading comment or node start
629
+ leading_comments = first_node.location.respond_to?(:leading_comments) ? first_node.location.leading_comments : []
630
+ first_content_line = leading_comments.any? ? leading_comments.first.location.start_line : first_node.location.start_line
631
+
632
+ return 0 if first_content_line <= 1
633
+
634
+ # Emit lines before the first node (magic comments, blank lines)
635
+ last_emitted = 0
636
+ (1...first_content_line).each do |line_num|
637
+ line = analysis.line_at(line_num)&.chomp || ""
638
+ result.add_line(line, decision: MergeResult::DECISION_KEPT_DEST, dest_line: line_num)
639
+ last_emitted = line_num
640
+ end
641
+ last_emitted
642
+ end
643
+
644
+ # Emit blank/gap lines from the destination source between the last output line
645
+ # and the next node (including its leading comments). This preserves blank lines
646
+ # that separate top-level blocks.
647
+ #
648
+ # @param result [MergeResult] The merge result
649
+ # @param analysis [FileAnalysis] The destination file analysis
650
+ # @param last_output_line [Integer] The last dest line number that was output
651
+ # @param next_node [Prism::Node] The next node about to be output
652
+ # @return [Integer] The updated last output line number
653
+ def emit_dest_gap_lines(result, analysis, last_output_line, next_node)
654
+ return last_output_line if last_output_line == 0
655
+
656
+ # Find where the next node's content starts (leading comment or node itself)
657
+ leading_comments = next_node.location.respond_to?(:leading_comments) ? next_node.location.leading_comments : []
658
+ next_start_line = leading_comments.any? ? leading_comments.first.location.start_line : next_node.location.start_line
659
+
660
+ # Emit gap lines (blank lines between last output and next node)
661
+ gap_start = last_output_line + 1
662
+ return last_output_line if gap_start >= next_start_line
663
+
664
+ (gap_start...next_start_line).each do |line_num|
665
+ line = analysis.line_at(line_num)&.chomp || ""
666
+ # Only emit blank lines in the gap (don't re-emit content)
667
+ next unless line.strip.empty?
668
+
669
+ result.add_line(line, decision: MergeResult::DECISION_KEPT_DEST, dest_line: line_num)
670
+ end
671
+
672
+ last_output_line
673
+ end
674
+
599
675
  # Add a node to the result, including its leading and trailing comments.
600
676
  #
601
677
  # @param result [MergeResult] The merge result
@@ -680,10 +756,15 @@ module Prism
680
756
  end
681
757
  end
682
758
 
683
- # Add trailing comments attached to the node (e.g., end-of-file comments)
759
+ # Add trailing comments attached to the node (e.g., end-of-file comments).
760
+ # Skip comments on the same line as the node — inline comments are already
761
+ # included when we output the node's source lines via analysis.line_at.
684
762
  trailing_comments = node.location.respond_to?(:trailing_comments) ? node.location.trailing_comments : []
763
+ node_line_range = node.location.start_line..node.location.end_line
685
764
  trailing_comments.each do |comment|
686
765
  line_num = comment.location.start_line
766
+ next if node_line_range.cover?(line_num)
767
+
687
768
  line = analysis.line_at(line_num)&.chomp || comment.slice.rstrip
688
769
 
689
770
  if source == :template
@@ -804,11 +885,15 @@ module Prism
804
885
  template_body = extract_node_body(actual_template, @template_analysis)
805
886
  dest_body = extract_node_body(actual_dest, @dest_analysis)
806
887
 
807
- # Recursively merge the bodies with incremented depth
888
+ # Recursively merge the bodies with incremented depth.
889
+ # Use the raw (unwrapped) signature_generator so the inner SmartMerger
890
+ # can wrap it fresh via build_effective_signature_generator. Using the
891
+ # already-effective generator would cause double-wrapping when
892
+ # node_typing is also passed, making is_a? checks fail.
808
893
  body_merger = SmartMerger.new(
809
894
  template_body,
810
895
  dest_body,
811
- signature_generator: @template_analysis.instance_variable_get(:@signature_generator),
896
+ signature_generator: @raw_signature_generator,
812
897
  preference: @preference,
813
898
  add_template_only_nodes: @add_template_only_nodes,
814
899
  freeze_token: @freeze_token,
@@ -5,7 +5,7 @@ module Prism
5
5
  # Version information for Prism::Merge
6
6
  module Version
7
7
  # Current version of the prism-merge gem
8
- VERSION = "2.0.0"
8
+ VERSION = "2.0.2"
9
9
  end
10
10
  VERSION = Version::VERSION # traditional location
11
11
  end
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: prism-merge
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter H. Boling
@@ -314,10 +314,10 @@ licenses:
314
314
  - MIT
315
315
  metadata:
316
316
  homepage_uri: https://prism-merge.galtzo.com/
317
- source_code_uri: https://github.com/kettle-rb/prism-merge/tree/v2.0.0
318
- changelog_uri: https://github.com/kettle-rb/prism-merge/blob/v2.0.0/CHANGELOG.md
317
+ source_code_uri: https://github.com/kettle-rb/prism-merge/tree/v2.0.2
318
+ changelog_uri: https://github.com/kettle-rb/prism-merge/blob/v2.0.2/CHANGELOG.md
319
319
  bug_tracker_uri: https://github.com/kettle-rb/prism-merge/issues
320
- documentation_uri: https://www.rubydoc.info/gems/prism-merge/2.0.0
320
+ documentation_uri: https://www.rubydoc.info/gems/prism-merge/2.0.2
321
321
  funding_uri: https://github.com/sponsors/pboling
322
322
  wiki_uri: https://github.com/kettle-rb/prism-merge/wiki
323
323
  news_uri: https://www.railsbling.com/tags/prism-merge
metadata.gz.sig CHANGED
Binary file