prism-merge 1.1.0 → 1.1.1

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: edeb460ce4645e2e6a8caf390cca9b23abeb88df9b3c314f6f86b42bc4ed68f5
4
- data.tar.gz: 2f2fb56d28d1d8ddb2563b8b312186f06bd96b26bdf89753e1382724faac9332
3
+ metadata.gz: ae8f1b1cf1ae576917fae80dea44c38c181296c46f60f4dc70546d12ac0e6a90
4
+ data.tar.gz: 131295bf35a6da2038ea889f46ee9520f4a83a7daf9d983c2d0b91fb67644985
5
5
  SHA512:
6
- metadata.gz: 1a5fa5900b24ba063df2de14c8772fda7b4dcebacfd2529dc41b465550b2baad3f8b8597d2c9759eac69b4025218843c35a41c14273c4ce69da294469b09b166
7
- data.tar.gz: ac7e0ba8364617dcc2e9c67955b7c5cf303028c54cfaee272eb86a4f26378bd7c0eeed585db714b851bdbd42aae710b24a1fe9a6d629d880d0e26482e222cd9d
6
+ metadata.gz: 82dcffd3874394148696583ee3cddbb6cde3e33f4136ccd43dbae01213e8252999216320192be3af51fc3fd1662a4d7a524a44722254a19513de84d57efbf048
7
+ data.tar.gz: 49ab9213cb3ac2e2d691d3460983d18502c7ceef28a594b1c93578c8c3a8fc44c583472f4920aa9482058cd3815fed2365b75951cdf7e4c52b6c1d37150c3ec9
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -30,6 +30,44 @@ Please file a bug if you notice a violation of semantic versioning.
30
30
 
31
31
  ### Security
32
32
 
33
+ ## [1.1.1] - 2025-12-04
34
+
35
+ - TAG: [v1.1.1][1.1.1t]
36
+ - COVERAGE: 96.62% -- 857/887 lines in 9 files
37
+ - BRANCH COVERAGE: 82.75% -- 331/400 branches in 9 files
38
+ - 100.00% documented
39
+
40
+ ### Added
41
+
42
+ - Documented comparison of this tool, git-merge, and IDE "Smart Merge" in README.md
43
+ - Comprehensive node signature support for all Prism node types with nested content:
44
+ - `SingletonClassNode` - singleton class definitions (`class << self`)
45
+ - `CaseNode` / `CaseMatchNode` - case statements and pattern matching
46
+ - `WhileNode` / `UntilNode` / `ForNode` - loop constructs
47
+ - `BeginNode` - exception handling blocks
48
+ - `SuperNode` / `ForwardingSuperNode` - super calls with blocks
49
+ - `LambdaNode` - lambda expressions
50
+ - `PreExecutionNode` / `PostExecutionNode` - BEGIN/END blocks
51
+ - `ParenthesesNode` / `EmbeddedStatementsNode` - parenthesized expressions
52
+ - Smart signature matching for assignment method calls (`config.setting = value`) now matches by receiver and method name, not by value, enabling proper merging of configuration blocks
53
+
54
+ ### Changed
55
+
56
+ - Improved boundary ordering in merge timeline - destination-only content appearing after all template content now correctly appears at the end of merged output (was incorrectly appearing at the beginning)
57
+ - Extended recursive merge support to handle `SingletonClassNode` and `BeginNode` in addition to existing `ClassNode`, `ModuleNode`, and `CallNode` types
58
+ - **Freeze block validation expanded** - freeze blocks can now be placed inside more container node types:
59
+ - `SingletonClassNode` (`class << self ... end`)
60
+ - `DefNode` (method definitions)
61
+ - `LambdaNode` (lambda/proc definitions)
62
+ - `CallNode` with blocks (e.g., RSpec `describe`/`context` blocks)
63
+ - This allows protecting portions of method implementations or DSL block contents
64
+ - Added README documentation comparing Prism::Merge algorithm to git merge and IDE smart merge strategies
65
+ - Added RBS type definitions for `FreezeNode` class
66
+
67
+ ### Fixed
68
+
69
+ - Documentation of freeze blocks, and configuration to customize the freeze token
70
+
33
71
  ## [1.1.0] - 2025-12-04
34
72
 
35
73
  - TAG: [v1.1.0][1.1.0t]
@@ -116,7 +154,9 @@ Please file a bug if you notice a violation of semantic versioning.
116
154
 
117
155
  - Initial release
118
156
 
119
- [Unreleased]: https://github.com/kettle-rb/prism-merge/compare/v1.1.0...HEAD
157
+ [Unreleased]: https://github.com/kettle-rb/prism-merge/compare/v1.1.1...HEAD
158
+ [1.1.1]: https://github.com/kettle-rb/prism-merge/compare/v1.1.0...v1.1.1
159
+ [1.1.1t]: https://github.com/kettle-rb/prism-merge/releases/tag/v1.1.1
120
160
  [1.1.0]: https://github.com/kettle-rb/prism-merge/compare/v1.0.3...v1.1.0
121
161
  [1.1.0t]: https://github.com/kettle-rb/prism-merge/releases/tag/v1.1.0
122
162
  [1.0.3]: https://github.com/kettle-rb/prism-merge/compare/v1.0.2...v1.0.3
data/README.md CHANGED
@@ -62,13 +62,14 @@ Prism::Merge is a standalone Ruby module that intelligently merges two versions
62
62
  - **Intelligent**: Matches nodes by structural signatures
63
63
  - **Recursive Merge**: Automatically merges class and module bodies recursively, intelligently combining nested methods and constants
64
64
  - **Comment-Preserving**: Comments are properly attached to relevant nodes and/or placement
65
- - **Freeze Block Support**: Respects `kettle-dev:freeze` markers for template merge control
65
+ - **Freeze Block Support**: Respects freeze markers (default: `prism-merge:freeze` / `prism-merge:unfreeze`) for template merge control - customizable to match your project's conventions
66
66
  - **Full Provenance**: Tracks origin of every line
67
67
  - **Standalone**: No dependencies other than `prism` and `version_gem` (which is a tiny tool all my gems depend on)
68
68
  - **Customizable**:
69
69
  - `signature_generator` - callable custom signature generators
70
70
  - `signature_match_preference` - setting of `:template` or `:destination`
71
71
  - `add_template_only_nodes` - setting to retain nodes that do not exist in destination
72
+ - `freeze_token` - customize freeze block markers (default: `"prism-merge"`)
72
73
 
73
74
  ### Example
74
75
 
@@ -299,11 +300,42 @@ merger = Prism::Merge::SmartMerger.new(
299
300
 
300
301
  ### Custom Signature Generator
301
302
 
302
- By default, Prism::Merge uses intelligent structural signatures to match nodes:
303
- - **Conditionals** (`if`/`unless`) are matched by their condition only
304
- - **Assignments** (constants, variables) are matched by their name only
305
- - **Method calls** are matched by name and arguments (not block body)
306
- - **Other nodes** are matched by class and full source code
303
+ By default, Prism::Merge uses intelligent structural signatures to match nodes. The signature determines how nodes are matched between template and destination files.
304
+
305
+ #### Default Signature Matching
306
+
307
+ | Node Type | Signature Format | Matching Behavior |
308
+ |-----------|-----------------|-------------------|
309
+ | `DefNode` | `[:def, name, params]` | Methods match by name and parameter names |
310
+ | `ClassNode` | `[:class, name]` | Classes match by name |
311
+ | `ModuleNode` | `[:module, name]` | Modules match by name |
312
+ | `SingletonClassNode` | `[:singleton_class, expr]` | Singleton classes match by expression (`class << self`) |
313
+ | `ConstantWriteNode` | `[:const, name]` | Constants match by name only (not value) |
314
+ | `IfNode` / `UnlessNode` | `[:if, condition]` | Conditionals match by condition expression |
315
+ | `CaseNode` | `[:case, predicate]` | Case statements match by the expression being switched |
316
+ | `CaseMatchNode` | `[:case_match, predicate]` | Pattern matching cases match by expression |
317
+ | `WhileNode` / `UntilNode` | `[:while, condition]` | Loops match by condition |
318
+ | `ForNode` | `[:for, index, collection]` | For loops match by index variable and collection |
319
+ | `BeginNode` | `[:begin, first_stmt]` | Begin blocks match by first statement (partial) |
320
+ | `CallNode` (regular) | `[:call, name, first_arg]` | Method calls match by name and first argument |
321
+ | `CallNode` (assignment) | `[:call, :method=, receiver]` | Assignment calls (`x.y = z`) match by receiver, not value |
322
+ | `CallNode` (with block) | `[:call_with_block, name, first_arg]` | Block calls match by name and first argument |
323
+ | `SuperNode` | `[:super, :with_block]` | Super calls match by presence of block |
324
+ | `LambdaNode` | `[:lambda, params]` | Lambdas match by parameter signature |
325
+ | `PreExecutionNode` | `[:pre_execution, line]` | BEGIN blocks match by line number |
326
+ | `PostExecutionNode` | `[:post_execution, line]` | END blocks match by line number |
327
+
328
+ #### Recursive Merge Support
329
+
330
+ The following node types support **recursive body merging**, where nested content is intelligently combined:
331
+
332
+ - `ClassNode` - class bodies are recursively merged
333
+ - `ModuleNode` - module bodies are recursively merged
334
+ - `SingletonClassNode` - singleton class bodies are recursively merged
335
+ - `CallNode` with block - block bodies are recursively merged (e.g., `configure do ... end`)
336
+ - `BeginNode` - begin/rescue/ensure blocks are recursively merged
337
+
338
+ #### Custom Signature Generator
307
339
 
308
340
  You can provide a custom signature generator to control matching behavior:
309
341
 
@@ -336,17 +368,38 @@ merger = Prism::Merge::SmartMerger.new(
336
368
 
337
369
  ### Freeze Blocks
338
370
 
339
- Protect sections in the destination file from being overwritten by the template using freeze markers:
371
+ Protect sections in the destination file from being overwritten by the template using freeze markers.
372
+
373
+ By default, Prism::Merge uses `prism-merge` as the freeze token:
340
374
 
341
375
  ```ruby
342
376
  # In your destination.rb file
343
- # kettle-dev:freeze
377
+ # prism-merge:freeze
344
378
  gem "custom-gem", path: "../custom"
345
379
  # Add any custom configuration you want to preserve
346
- # kettle-dev:unfreeze
380
+ # prism-merge:unfreeze
381
+ ```
382
+
383
+ You can customize the freeze token to match your project's conventions:
384
+
385
+ ```ruby
386
+ # Use a custom freeze token (e.g., for kettle-dev projects)
387
+ merger = Prism::Merge::SmartMerger.new(
388
+ template,
389
+ destination,
390
+ freeze_token: "kettle-dev", # Now uses # kettle-dev:freeze / # kettle-dev:unfreeze
391
+ )
347
392
  ```
348
393
 
349
- Freeze blocks are **always preserved** from the destination file during merge, regardless of template content.
394
+ Freeze blocks are **always preserved** from the destination file during merge, regardless of template content. They can be placed inside:
395
+
396
+ - Class and module bodies (`class Foo ... end`, `module Bar ... end`)
397
+ - Singleton class bodies (`class << self ... end`)
398
+ - Method definitions (`def method_name ... end`)
399
+ - Lambda/proc bodies (`-> { ... }`)
400
+ - Block-based DSLs (e.g., RSpec `describe`/`context` blocks)
401
+
402
+ This allows you to protect entire methods, portions of method implementations, or sections within DSL blocks.
350
403
 
351
404
  ### Integration with Existing Systems
352
405
 
@@ -417,6 +470,53 @@ end
417
470
  # - custom_method is preserved (destination-only)
418
471
  ```
419
472
 
473
+ ### How Prism::Merge Compares to Other Merge Strategies
474
+
475
+ Prism::Merge uses a **single-pass, AST-aware** algorithm that differs fundamentally from line-based merge tools like `git merge` and IDE smart merges:
476
+
477
+ | Aspect | Git Merge (3-way) | IDE Smart Merge | Prism::Merge |
478
+ |--------|-------------------|-----------------|--------------|
479
+ | **Input** | 3 files (base, ours, theirs) | 2-3 files | 2 files (template, destination) |
480
+ | **Unit of comparison** | Lines of text | Lines + some syntax awareness | AST nodes (Ruby structures) |
481
+ | **Passes** | Multi-pass (LCS algorithm) | Multi-pass | Single-pass with anchors |
482
+ | **Conflict handling** | Manual resolution with markers (`<<<<<<<`) | Interactive resolution | Automatic via signature matching |
483
+ | **Language awareness** | None (text-only) | Basic (indentation, brackets) | Full Ruby AST understanding |
484
+ | **Comment handling** | Treated as text | Treated as text | Attached to relevant nodes |
485
+ | **Structural matching** | Line equality only | Line + heuristics | Node signatures (type + identifier) |
486
+ | **Recursive merge** | No | Sometimes | Yes (class/module bodies) |
487
+ | **Freeze blocks** | No | No | Yes (preserve destination sections) |
488
+
489
+ #### Key Differences Explained
490
+
491
+ **Git Merge (3-way merge):**
492
+ - Requires a common ancestor (base) to detect changes from each side
493
+ - Uses Longest Common Subsequence (LCS) algorithm in multiple passes
494
+ - Produces conflict markers when both sides modify the same lines
495
+ - Language-agnostic: treats Ruby, Python, and prose identically
496
+
497
+ **IDE Smart Merge:**
498
+ - Often uses 3-way merge as foundation
499
+ - Adds heuristics for common patterns (moved blocks, reformatting)
500
+ - May understand basic syntax for better conflict detection
501
+ - Still fundamentally line-based with enhancements
502
+
503
+ **Prism::Merge:**
504
+ - Uses 2 files: template (source of truth) and destination (customized version)
505
+ - Single-pass algorithm that builds a timeline of anchors (matches) and boundaries (differences)
506
+ - Matches by **structural signature** (e.g., `[:def, :method_name]`), not line content
507
+ - Automatically resolves conflicts based on configurable preference
508
+ - Never produces conflict markers - always produces valid, runnable Ruby
509
+
510
+ #### When to Use Each
511
+
512
+ | Scenario | Best Tool |
513
+ |----------|-----------|
514
+ | Merging git branches with divergent changes | Git Merge |
515
+ | Resolving complex conflicts interactively | IDE Smart Merge |
516
+ | Updating project files from a template | **Prism::Merge** |
517
+ | Maintaining customizations across template updates | **Prism::Merge** |
518
+ | Merging non-Ruby files | Git Merge / IDE |
519
+
420
520
  ### With Debug Information
421
521
 
422
522
  Get detailed information about merge decisions:
@@ -486,12 +586,12 @@ Protect custom sections from template updates:
486
586
  ```ruby
487
587
  # destination.rb
488
588
  class MyApp
489
- # kettle-dev:freeze
589
+ # prism-merge:freeze
490
590
  CUSTOM_CONFIG = {
491
591
  api_key: ENV.fetch("API_KEY"),
492
592
  endpoint: "https://custom.example.com",
493
593
  }
494
- # kettle-dev:unfreeze
594
+ # prism-merge:unfreeze
495
595
 
496
596
  VERSION = "1.0.0"
497
597
  end
@@ -503,6 +603,18 @@ class MyApp
503
603
  VERSION = "2.0.0"
504
604
  end
505
605
 
606
+ # Merge with default freeze token
607
+ merger = Prism::Merge::SmartMerger.new(template, destination)
608
+ result = merger.merge
609
+
610
+ # Or use a custom freeze token if your project uses a different convention
611
+ merger = Prism::Merge::SmartMerger.new(
612
+ template,
613
+ destination,
614
+ freeze_token: "kettle-dev", # for kettle-dev projects
615
+ )
616
+ result = merger.merge
617
+
506
618
  # After merge, CUSTOM_CONFIG keeps destination values
507
619
  # but VERSION is updated to 2.0.0
508
620
  ```
@@ -596,9 +708,9 @@ RSpec.describe("Ruby file merging") do
596
708
  RUBY
597
709
 
598
710
  destination = <<~RUBY
599
- # kettle-dev:freeze
711
+ # prism-merge:freeze
600
712
  CONFIG = { key: "secret" }
601
- # kettle-dev:unfreeze
713
+ # prism-merge:unfreeze
602
714
  RUBY
603
715
 
604
716
  merger = Prism::Merge::SmartMerger.new(template, destination)
@@ -607,6 +719,28 @@ RSpec.describe("Ruby file merging") do
607
719
  # Freeze block content preserved
608
720
  expect(result).to(include('CONFIG = { key: "secret" }'))
609
721
  end
722
+
723
+ it "works with custom freeze tokens" do
724
+ template = <<~RUBY
725
+ CONFIG = {}
726
+ RUBY
727
+
728
+ destination = <<~RUBY
729
+ # my-app:freeze
730
+ CONFIG = { key: "secret" }
731
+ # my-app:unfreeze
732
+ RUBY
733
+
734
+ merger = Prism::Merge::SmartMerger.new(
735
+ template,
736
+ destination,
737
+ freeze_token: "my-app", # Match your project's freeze token
738
+ )
739
+ result = merger.merge
740
+
741
+ # Freeze block content preserved
742
+ expect(result).to(include('CONFIG = { key: "secret" }'))
743
+ end
610
744
  end
611
745
  ```
612
746
 
@@ -973,7 +1107,7 @@ Thanks for RTFM. ☺️
973
1107
  [📌gitmoji]: https://gitmoji.dev
974
1108
  [📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
975
1109
  [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
976
- [🧮kloc-img]: https://img.shields.io/badge/KLOC-0.805-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
1110
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-0.887-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
977
1111
  [🔐security]: SECURITY.md
978
1112
  [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
979
1113
  [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
@@ -322,15 +322,118 @@ module Prism
322
322
  end
323
323
  end
324
324
 
325
- # Generate default signature for a node
325
+ # Generate default structural signature for a Prism node.
326
+ #
327
+ # Signatures are used to match nodes between template and destination files.
328
+ # Nodes with identical signatures are considered "the same" for merge purposes.
329
+ #
326
330
  # @param node [Prism::Node] Node to generate signature for
327
- # @return [Array] Signature array [type, name, params, ...]
331
+ # @return [Array] Signature array with format [:type, identifier, ...]
332
+ #
333
+ # @note Supported node types and their signature formats:
334
+ #
335
+ # **Method/Class Definitions:**
336
+ # - `DefNode` → `[:def, name, [param_names]]`
337
+ # - `ClassNode` → `[:class, constant_path]`
338
+ # - `ModuleNode` → `[:module, constant_path]`
339
+ # - `SingletonClassNode` → `[:singleton_class, expression]`
340
+ #
341
+ # **Constants:**
342
+ # - `ConstantWriteNode` → `[:const, name]`
343
+ # - `ConstantPathWriteNode` → `[:const, target]`
344
+ #
345
+ # **Conditionals:**
346
+ # - `IfNode` → `[:if, condition_source]`
347
+ # - `UnlessNode` → `[:unless, condition_source]`
348
+ #
349
+ # **Case Statements:**
350
+ # - `CaseNode` → `[:case, predicate]`
351
+ # - `CaseMatchNode` → `[:case_match, predicate]`
352
+ #
353
+ # **Loops:**
354
+ # - `WhileNode` → `[:while, condition]`
355
+ # - `UntilNode` → `[:until, condition]`
356
+ # - `ForNode` → `[:for, index, collection]`
357
+ #
358
+ # **Exception Handling:**
359
+ # - `BeginNode` → `[:begin, first_statement_preview]`
360
+ #
361
+ # **Method Calls:**
362
+ # - `CallNode` (regular) → `[:call, method_name, first_arg]`
363
+ # - `CallNode` (assignment, e.g., `x.y = z`) → `[:call, :method=, receiver]`
364
+ # - `CallNode` (with block) → `[:call_with_block, method_name, first_arg_or_receiver]`
365
+ #
366
+ # **Super Calls:**
367
+ # - `SuperNode` → `[:super, :with_block | :no_block]`
368
+ # - `ForwardingSuperNode` → `[:forwarding_super, :with_block | :no_block]`
369
+ #
370
+ # **Lambdas:**
371
+ # - `LambdaNode` → `[:lambda, parameters_source]`
372
+ #
373
+ # **Special Blocks:**
374
+ # - `PreExecutionNode` → `[:pre_execution, line_number]`
375
+ # - `PostExecutionNode` → `[:post_execution, line_number]`
376
+ #
377
+ # **Other:**
378
+ # - `ParenthesesNode` → `[:parens, first_expression_preview]`
379
+ # - `EmbeddedStatementsNode` → `[:embedded, statements_source]`
380
+ # - `FreezeNode` → Uses FreezeNode#signature
381
+ # - Unknown nodes → `[:other, class_name, line_number]`
382
+ #
383
+ # @example Method definition signature
384
+ # # def greet(name, greeting: "Hello")
385
+ # compute_node_signature(def_node)
386
+ # # => [:def, :greet, [:name, :greeting]]
387
+ #
388
+ # @example Assignment method call signature
389
+ # # config.setting = "value"
390
+ # compute_node_signature(call_node)
391
+ # # => [:call, :setting=, "config"]
392
+ #
393
+ # @example Block method call signature
394
+ # # appraise "ruby-3.3" do ... end
395
+ # compute_node_signature(call_node)
396
+ # # => [:call_with_block, :appraise, "ruby-3.3"]
397
+ #
398
+ # @api private
328
399
  def compute_node_signature(node)
329
400
  # IMPORTANT: Do NOT call node.signature - Prism nodes have their own signature method
330
401
  # that returns [node_type_symbol, source_text] which is not what we want for matching.
331
402
  # We need our own signature format: [:type_symbol, identifier, params]
403
+ #
404
+ # Node types with nested content (from Prism) that we may encounter:
405
+ # - BeginNode: statements, rescue_clause, else_clause, ensure_clause
406
+ # - BlockNode: body (handled via parent CallNode)
407
+ # - CallNode: block
408
+ # - CaseMatchNode: else_clause, conditions, consequent
409
+ # - CaseNode: else_clause, conditions, consequent
410
+ # - ClassNode: body
411
+ # - DefNode: body
412
+ # - ElseNode: statements (handled via parent)
413
+ # - EmbeddedStatementsNode: statements
414
+ # - EnsureNode: statements (handled via parent BeginNode)
415
+ # - ForNode: statements
416
+ # - ForwardingSuperNode: block
417
+ # - IfNode: statements, consequent
418
+ # - InNode: statements (handled via parent CaseMatchNode)
419
+ # - IndexAndWriteNode, IndexOperatorWriteNode, IndexOrWriteNode: block
420
+ # - LambdaNode: body
421
+ # - ModuleNode: body
422
+ # - ParenthesesNode: body
423
+ # - PostExecutionNode: statements (END { })
424
+ # - PreExecutionNode: statements (BEGIN { })
425
+ # - ProgramNode: statements (top-level)
426
+ # - RescueNode: statements, consequent (handled via parent BeginNode)
427
+ # - SingletonClassNode: body
428
+ # - StatementsNode: body
429
+ # - SuperNode: block
430
+ # - UnlessNode: statements, else_clause, consequent
431
+ # - UntilNode: statements
432
+ # - WhenNode: statements, conditions (handled via parent CaseNode)
433
+ # - WhileNode: statements
332
434
 
333
435
  case node
436
+ # === Method definitions ===
334
437
  when Prism::DefNode
335
438
  # Extract parameter names from ParametersNode
336
439
  params = if node.parameters
@@ -347,23 +450,150 @@ module Prism
347
450
  []
348
451
  end
349
452
  [:def, node.name, params]
453
+
454
+ # === Class/Module definitions ===
350
455
  when Prism::ClassNode
351
456
  [:class, node.constant_path.slice]
352
457
  when Prism::ModuleNode
353
458
  [:module, node.constant_path.slice]
459
+ when Prism::SingletonClassNode
460
+ # class << self or class << expr
461
+ expr = begin
462
+ node.expression.slice
463
+ rescue
464
+ "self"
465
+ end
466
+ [:singleton_class, expr]
467
+
468
+ # === Constants ===
354
469
  when Prism::ConstantWriteNode, Prism::ConstantPathWriteNode
355
470
  [:const, node.name || node.target.slice]
471
+
472
+ # === Conditionals ===
356
473
  when Prism::IfNode, Prism::UnlessNode
357
474
  # Conditionals match by their condition expression
358
475
  condition_source = node.predicate.slice
359
476
  [node.is_a?(Prism::IfNode) ? :if : :unless, condition_source]
477
+
478
+ # === Case/Switch statements ===
479
+ when Prism::CaseNode
480
+ # case expr; when ... end - match by the expression being switched on
481
+ predicate = node.predicate&.slice || ""
482
+ [:case, predicate]
483
+ when Prism::CaseMatchNode
484
+ # case expr; in ... end (pattern matching) - match by the expression
485
+ predicate = node.predicate&.slice || ""
486
+ [:case_match, predicate]
487
+
488
+ # === Loops ===
489
+ when Prism::WhileNode
490
+ [:while, node.predicate.slice]
491
+ when Prism::UntilNode
492
+ [:until, node.predicate.slice]
493
+ when Prism::ForNode
494
+ # for i in collection - match by index and collection
495
+ index = node.index.slice
496
+ collection = node.collection.slice
497
+ [:for, index, collection]
498
+
499
+ # === Exception handling ===
500
+ when Prism::BeginNode
501
+ # begin/rescue/ensure blocks - unique by position within parent
502
+ # Since these don't have a natural identifier, use first statement
503
+ first_stmt = node.statements&.body&.first&.slice&.[](0, 30) || ""
504
+ [:begin, first_stmt]
505
+
506
+ # === Method calls ===
507
+ when Prism::CallNode
508
+ # Method calls match by name and context
509
+ # For assignment methods (ending in =), match by receiver + method name only
510
+ # For other calls, include first argument as identifier (e.g., appraise "name")
511
+ method_name = node.name.to_s
512
+ receiver = node.receiver&.slice
513
+
514
+ if method_name.end_with?("=")
515
+ # Assignment method: config.setting = "value"
516
+ # Match by receiver and method name, NOT the value being assigned
517
+ if node.block
518
+ [:call_with_block, node.name, receiver]
519
+ else
520
+ [:call, node.name, receiver]
521
+ end
522
+ else
523
+ # Regular method call: appraise "unlocked" do ... end
524
+ # Match by method name and first argument (which identifies the call)
525
+ first_arg = extract_first_argument_value(node)
526
+ if node.block
527
+ [:call_with_block, node.name, first_arg]
528
+ else
529
+ [:call, node.name, first_arg]
530
+ end
531
+ end
532
+
533
+ # === Super calls ===
534
+ when Prism::SuperNode
535
+ [:super, node.block ? :with_block : :no_block]
536
+ when Prism::ForwardingSuperNode
537
+ [:forwarding_super, node.block ? :with_block : :no_block]
538
+
539
+ # === Lambdas ===
540
+ when Prism::LambdaNode
541
+ # Lambdas don't have names, but we can identify by parameter signature
542
+ params = if node.parameters
543
+ node.parameters.slice
544
+ else
545
+ ""
546
+ end
547
+ [:lambda, params]
548
+
549
+ # === Special blocks ===
550
+ when Prism::PreExecutionNode
551
+ # BEGIN { } blocks
552
+ [:pre_execution, node.location.start_line]
553
+ when Prism::PostExecutionNode
554
+ # END { } blocks
555
+ [:post_execution, node.location.start_line]
556
+
557
+ # === Parenthesized expressions ===
558
+ when Prism::ParenthesesNode
559
+ # Usually transparent, but if it appears at top level, identify by content
560
+ first_expr = node.body&.body&.first&.slice&.[](0, 30) || ""
561
+ [:parens, first_expr]
562
+
563
+ # === Embedded statements (string interpolation) ===
564
+ when Prism::EmbeddedStatementsNode
565
+ [:embedded, node.statements&.slice || ""]
566
+
567
+ # === FreezeNode (our custom wrapper) ===
360
568
  when FreezeNode
361
569
  # FreezeNode has its own signature method with normalized content
362
570
  node.signature
571
+
363
572
  else
573
+ # Fallback: use class name and line number
574
+ # Nodes that reach here may not merge well across files
364
575
  [:other, node.class.name, node.location.start_line]
365
576
  end
366
577
  end
578
+
579
+ # Extract the value of the first argument from a CallNode for signature matching.
580
+ # Returns the unescaped string value for StringNode, or the slice for other node types.
581
+ #
582
+ # @param node [Prism::CallNode] The call node to extract argument from
583
+ # @return [String, nil] The first argument value, or nil if no arguments
584
+ def extract_first_argument_value(node)
585
+ return unless node.arguments&.arguments&.any?
586
+
587
+ first_arg = node.arguments.arguments.first
588
+ case first_arg
589
+ when Prism::StringNode
590
+ first_arg.unescaped
591
+ when Prism::SymbolNode
592
+ first_arg.unescaped.to_sym
593
+ else
594
+ first_arg.slice
595
+ end
596
+ end
367
597
  end
368
598
  end
369
599
  end
@@ -118,17 +118,28 @@ module Prism
118
118
  fully_contained = node_start >= @start_line && node_end <= @end_line
119
119
 
120
120
  # Check if node completely encompasses the freeze block
121
- # This is only valid for ClassNode/ModuleNode (freeze blocks at class/module body level)
122
- # For other nodes (methods, etc.), this is invalid
121
+ # This is valid for nodes that define a body scope where freeze blocks make sense:
122
+ # - ClassNode, ModuleNode, SingletonClassNode (class/module definitions)
123
+ # - CallNode with blocks (like RSpec describe/context blocks)
124
+ # - DefNode (method definitions)
125
+ # - LambdaNode (lambda/proc definitions)
123
126
  encompasses = node_start < @start_line && node_end > @end_line
124
- valid_encompass = encompasses && (node.is_a?(Prism::ClassNode) || node.is_a?(Prism::ModuleNode))
127
+ valid_encompass = encompasses && (
128
+ node.is_a?(Prism::ClassNode) ||
129
+ node.is_a?(Prism::ModuleNode) ||
130
+ node.is_a?(Prism::SingletonClassNode) ||
131
+ node.is_a?(Prism::DefNode) ||
132
+ node.is_a?(Prism::LambdaNode) ||
133
+ (node.is_a?(Prism::CallNode) && node.block) ||
134
+ (node.is_a?(Prism::LocalVariableWriteNode) && node.value.is_a?(Prism::LambdaNode))
135
+ )
125
136
 
126
137
  # Check if node partially overlaps (invalid - unclosed/incomplete structure)
127
138
  partially_overlaps = !fully_contained && !encompasses &&
128
139
  ((node_start < @start_line && node_end >= @start_line) ||
129
140
  (node_start <= @end_line && node_end > @end_line))
130
141
 
131
- # Invalid if: partial overlap OR if a non-class/module node encompasses the freeze block
142
+ # Invalid if: partial overlap OR if an unsupported node type encompasses the freeze block
132
143
  if partially_overlaps || (encompasses && !valid_encompass)
133
144
  unclosed << node
134
145
  end
@@ -270,9 +270,25 @@ module Prism
270
270
  end
271
271
 
272
272
  boundaries.each do |boundary|
273
- # Sort boundaries by their starting position
274
- t_start = boundary.template_range&.begin || 0
275
- d_start = boundary.dest_range&.begin || 0
273
+ # Sort boundaries by their position relative to anchors
274
+ # - Boundaries before first anchor: use their position (or 0 if no template range)
275
+ # - Boundaries between anchors: use template position
276
+ # - Boundaries after last anchor: use Float::INFINITY to place at end
277
+
278
+ if boundary.prev_anchor.nil? && boundary.next_anchor
279
+ # Before first anchor - place at beginning
280
+ t_start = boundary.template_range&.begin || 0
281
+ d_start = boundary.dest_range&.begin || 0
282
+ elsif boundary.prev_anchor && boundary.next_anchor.nil?
283
+ # After last anchor - place at end
284
+ t_start = Float::INFINITY
285
+ d_start = boundary.dest_range&.begin || Float::INFINITY
286
+ else
287
+ # Between anchors or no anchors at all
288
+ t_start = boundary.template_range&.begin || boundary.prev_anchor&.template_end&.+(1) || 0
289
+ d_start = boundary.dest_range&.begin || 0
290
+ end
291
+
276
292
  sort_key = [t_start, d_start, 1] # 1 ensures boundaries come after anchors at same position
277
293
 
278
294
  timeline << {type: :boundary, boundary: boundary, sort_key: sort_key}
@@ -386,9 +402,10 @@ module Prism
386
402
 
387
403
  # Determines if two matching nodes should be recursively merged.
388
404
  #
389
- # Recursive merge is performed for matching class/module definitions to intelligently
390
- # combine their body contents (nested methods, constants, etc.). This allows template
391
- # updates to class internals to be merged with destination customizations.
405
+ # Recursive merge is performed for matching class/module definitions and
406
+ # CallNodes with blocks to intelligently combine their body contents
407
+ # (nested methods, constants, etc.). This allows template updates to
408
+ # internals to be merged with destination customizations.
392
409
  #
393
410
  # @param template_node [Prism::Node, nil] Node from template file
394
411
  # @param dest_node [Prism::Node, nil] Node from destination file
@@ -396,15 +413,40 @@ module Prism
396
413
  #
397
414
  # @note Recursive merge is NOT performed for:
398
415
  # - Conditional nodes (if/unless) - treated as atomic units
399
- # - Classes/modules containing freeze blocks - frozen content would be lost
416
+ # - Classes/modules/blocks containing freeze blocks - frozen content would be lost
400
417
  # - Nodes of different types
401
418
  def should_merge_recursively?(template_node, dest_node)
402
419
  return false unless template_node && dest_node
403
420
 
404
- is_class_or_module = (template_node.is_a?(Prism::ClassNode) && dest_node.is_a?(Prism::ClassNode)) ||
405
- (template_node.is_a?(Prism::ModuleNode) && dest_node.is_a?(Prism::ModuleNode))
421
+ # Both nodes must be the same type
422
+ return false unless template_node.class == dest_node.class
423
+
424
+ # Determine if this node type supports recursive merging
425
+ can_merge_recursively = case template_node
426
+ when Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode
427
+ # Class/module definitions - merge their body contents
428
+ true
429
+ when Prism::CallNode
430
+ # Only merge if both have blocks
431
+ template_node.block && dest_node.block
432
+ when Prism::BeginNode
433
+ # begin/rescue/ensure blocks - merge statements
434
+ template_node.statements && dest_node.statements
435
+ when Prism::CaseNode, Prism::CaseMatchNode
436
+ # Case statements could potentially merge conditions, but this is complex
437
+ # For now, treat as atomic unless both have same structure
438
+ false
439
+ when Prism::WhileNode, Prism::UntilNode, Prism::ForNode
440
+ # Loops - could merge body, but usually should be atomic
441
+ false
442
+ when Prism::LambdaNode
443
+ # Lambdas - could merge body, but typically atomic
444
+ false
445
+ else
446
+ false
447
+ end
406
448
 
407
- return false unless is_class_or_module
449
+ return false unless can_merge_recursively
408
450
 
409
451
  # Don't recursively merge if either node contains freeze blocks
410
452
  # (they would be lost in the nested merge since we pass freeze_token: nil)
@@ -421,7 +463,26 @@ module Prism
421
463
  # @api private
422
464
  def node_contains_freeze_blocks?(node)
423
465
  return false unless @freeze_token
424
- return false unless node.body
466
+
467
+ # Check if node has nested content that could contain freeze blocks
468
+ # Different node types store content in different attributes
469
+ has_content = case node
470
+ when Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode,
471
+ Prism::LambdaNode, Prism::ParenthesesNode
472
+ node.body
473
+ when Prism::IfNode, Prism::UnlessNode, Prism::WhileNode, Prism::UntilNode,
474
+ Prism::ForNode, Prism::BeginNode
475
+ node.statements
476
+ when Prism::CallNode, Prism::SuperNode, Prism::ForwardingSuperNode
477
+ node.block
478
+ else
479
+ # Fallback for any other nodes
480
+ node.respond_to?(:body) && node.body ||
481
+ node.respond_to?(:statements) && node.statements ||
482
+ node.respond_to?(:block) && node.block
483
+ end
484
+
485
+ return false unless has_content
425
486
 
426
487
  # Check if any comments in the node's range contain freeze markers
427
488
  freeze_pattern = /#\s*#{Regexp.escape(@freeze_token)}:(freeze|unfreeze)/i
@@ -438,14 +499,14 @@ module Prism
438
499
  end
439
500
  end
440
501
 
441
- # Recursively merges the body of matching class or module nodes.
502
+ # Recursively merges the body of matching class, module, or call-with-block nodes.
442
503
  #
443
- # This method extracts the body content (everything between the class/module
504
+ # This method extracts the body content (everything between the opening
444
505
  # declaration and the closing 'end'), creates a new nested SmartMerger to merge
445
- # those bodies, and then reassembles the complete class/module with the merged body.
506
+ # those bodies, and then reassembles the complete node with the merged body.
446
507
  #
447
- # @param template_node [Prism::ClassNode, Prism::ModuleNode] Class/module from template
448
- # @param dest_node [Prism::ClassNode, Prism::ModuleNode] Class/module from destination
508
+ # @param template_node [Prism::ClassNode, Prism::ModuleNode, Prism::CallNode] Node from template
509
+ # @param dest_node [Prism::ClassNode, Prism::ModuleNode, Prism::CallNode] Node from destination
449
510
  # @param anchor [FileAligner::Anchor] The anchor representing this match
450
511
  #
451
512
  # @note The nested merger is configured with:
@@ -469,23 +530,39 @@ module Prism
469
530
  )
470
531
  merged_body = body_merger.merge.rstrip
471
532
 
472
- # Add the opening line (class/module declaration) with leading comments
473
- source_analysis = (@signature_match_preference == :template) ? @template_analysis : @dest_analysis
474
- source_node = (@signature_match_preference == :template) ? template_node : dest_node
475
- source_anchor_start = (@signature_match_preference == :template) ? anchor.template_start : anchor.dest_start
533
+ # Determine leading comments handling:
534
+ # - If template has leading comments, use template's based on signature_match_preference
535
+ # - If template has NO leading comments but destination does, preserve destination's
536
+ template_has_leading = anchor.template_start < template_node.location.start_line
537
+ dest_has_leading = anchor.dest_start < dest_node.location.start_line
476
538
 
477
- # Add leading comments
478
- (source_anchor_start...source_node.location.start_line).each do |line_num|
479
- line = source_analysis.line_at(line_num)
480
- @result.add_line(
481
- line.chomp,
482
- decision: MergeResult::DECISION_REPLACED,
483
- template_line: ((@signature_match_preference == :template) ? line_num : nil),
484
- dest_line: ((@signature_match_preference == :destination) ? line_num : nil),
485
- )
539
+ if template_has_leading && @signature_match_preference == :template
540
+ # Use template's leading comments
541
+ (anchor.template_start...template_node.location.start_line).each do |line_num|
542
+ line = @template_analysis.line_at(line_num)
543
+ @result.add_line(
544
+ line.chomp,
545
+ decision: MergeResult::DECISION_REPLACED,
546
+ template_line: line_num,
547
+ )
548
+ end
549
+ elsif dest_has_leading
550
+ # Preserve destination's leading comments (either because preference is :destination,
551
+ # or because template has none)
552
+ (anchor.dest_start...dest_node.location.start_line).each do |line_num|
553
+ line = @dest_analysis.line_at(line_num)
554
+ @result.add_line(
555
+ line.chomp,
556
+ decision: MergeResult::DECISION_KEPT_DEST,
557
+ dest_line: line_num,
558
+ )
559
+ end
486
560
  end
487
561
 
488
- # Add the class/module opening line
562
+ # Add the opening line (based on signature_match_preference)
563
+ source_analysis = (@signature_match_preference == :template) ? @template_analysis : @dest_analysis
564
+ source_node = (@signature_match_preference == :template) ? template_node : dest_node
565
+
489
566
  opening_line = source_analysis.line_at(source_node.location.start_line)
490
567
  @result.add_line(
491
568
  opening_line.chomp,
@@ -526,14 +603,40 @@ module Prism
526
603
  # @note Handles different node types:
527
604
  # - ClassNode/ModuleNode: Uses node.body (StatementsNode)
528
605
  # - IfNode/UnlessNode: Uses node.statements (StatementsNode)
606
+ # - CallNode with block: Uses node.block.body (StatementsNode)
529
607
  #
530
608
  # @api private
531
609
  def extract_node_body(node, analysis)
532
610
  # Get the statements node based on node type
533
- statements_node = if node.is_a?(Prism::ClassNode) || node.is_a?(Prism::ModuleNode)
611
+ # Different node types store their body/statements in different attributes
612
+ statements_node = case node
613
+ when Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode, Prism::LambdaNode
614
+ # These use .body which returns a StatementsNode
534
615
  node.body
535
- elsif node.is_a?(Prism::IfNode) || node.is_a?(Prism::UnlessNode)
616
+ when Prism::IfNode, Prism::UnlessNode, Prism::WhileNode, Prism::UntilNode, Prism::ForNode
617
+ # These use .statements
536
618
  node.statements
619
+ when Prism::CallNode
620
+ # CallNode stores body inside block.body
621
+ node.block&.body
622
+ when Prism::BeginNode
623
+ # BeginNode uses .statements for the main body
624
+ node.statements
625
+ when Prism::CaseNode, Prism::CaseMatchNode
626
+ # Case nodes have conditions (WhenNode/InNode array), not a simple body
627
+ # Return nil for now - these need special handling
628
+ nil
629
+ when Prism::ParenthesesNode
630
+ node.body
631
+ else
632
+ # Try common patterns
633
+ if node.respond_to?(:body)
634
+ node.body
635
+ elsif node.respond_to?(:statements)
636
+ node.statements
637
+ elsif node.respond_to?(:block) && node.block
638
+ node.block.body
639
+ end
537
640
  end
538
641
 
539
642
  return "" unless statements_node&.is_a?(Prism::StatementsNode)
@@ -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 = "1.1.0"
8
+ VERSION = "1.1.1"
9
9
  end
10
10
  VERSION = Version::VERSION # traditional location
11
11
  end
data/sig/prism/merge.rbs CHANGED
@@ -22,6 +22,64 @@ module Prism
22
22
  class DestinationParseError < ParseError
23
23
  end
24
24
 
25
+ # Wrapper to represent freeze blocks as first-class nodes.
26
+ # Freeze blocks can be placed inside:
27
+ # - ClassNode, ModuleNode, SingletonClassNode (class/module definitions)
28
+ # - DefNode (method definitions)
29
+ # - LambdaNode (lambda/proc definitions)
30
+ # - CallNode with blocks (e.g., RSpec describe/context blocks)
31
+ class FreezeNode
32
+ class InvalidStructureError < StandardError
33
+ attr_reader start_line: Integer?
34
+ attr_reader end_line: Integer?
35
+ attr_reader unclosed_nodes: Array[untyped]
36
+
37
+ def initialize: (String message, ?start_line: Integer?, ?end_line: Integer?, ?unclosed_nodes: Array[untyped]) -> void
38
+ end
39
+
40
+ class Location
41
+ attr_reader start_line: Integer
42
+ attr_reader end_line: Integer
43
+
44
+ def initialize: (Integer start_line, Integer end_line) -> void
45
+ end
46
+
47
+ attr_reader start_line: Integer
48
+ attr_reader end_line: Integer
49
+ attr_reader content: String
50
+ attr_reader nodes: Array[untyped]
51
+ attr_reader start_marker: String?
52
+ attr_reader end_marker: String?
53
+
54
+ def initialize: (
55
+ start_line: Integer,
56
+ end_line: Integer,
57
+ analysis: FileAnalysis,
58
+ ?nodes: Array[untyped],
59
+ ?overlapping_nodes: Array[untyped]?,
60
+ ?start_marker: String?,
61
+ ?end_marker: String?
62
+ ) -> void
63
+
64
+ def location: () -> Location
65
+
66
+ def signature: () -> Array[Symbol | Integer]
67
+
68
+ def line_range: () -> Range[Integer]
69
+
70
+ def contains_line?: (Integer line_num) -> bool
71
+
72
+ def overlaps?: (untyped other) -> bool
73
+
74
+ def to_s: () -> String
75
+
76
+ def inspect: () -> String
77
+
78
+ private
79
+
80
+ def validate_structure!: () -> void
81
+ end
82
+
25
83
  class FileAnalysis
26
84
  FREEZE_START: Regexp
27
85
  FREEZE_END: Regexp
@@ -92,6 +150,33 @@ module Prism
92
150
  def build_comment_map: () -> Hash[Integer, Array[untyped]]
93
151
 
94
152
  def default_signature: (untyped node) -> Array[untyped]
153
+
154
+ # Compute structural signature for node matching
155
+ # Returns signature arrays like:
156
+ # [:def, Symbol, Array[Symbol]] - method definitions
157
+ # [:class, String] - class definitions
158
+ # [:module, String] - module definitions
159
+ # [:singleton_class, String] - singleton class definitions
160
+ # [:const, Symbol | String] - constant assignments
161
+ # [:if, String] | [:unless, String] - conditionals
162
+ # [:case, String] | [:case_match, String] - case statements
163
+ # [:while, String] | [:until, String] - while/until loops
164
+ # [:for, String, String] - for loops
165
+ # [:begin, String] - begin blocks
166
+ # [:call, Symbol, String?] - method calls
167
+ # [:call_with_block, Symbol, String?] - method calls with blocks
168
+ # [:super, Symbol] - super calls
169
+ # [:forwarding_super, Symbol] - forwarding super calls
170
+ # [:lambda, String] - lambda expressions
171
+ # [:pre_execution, Integer] - BEGIN blocks
172
+ # [:post_execution, Integer] - END blocks
173
+ # [:parens, String] - parenthesized expressions
174
+ # [:embedded, String] - embedded statements
175
+ # [:other, String, Integer] - fallback for unknown nodes
176
+ def compute_node_signature: (untyped node) -> Array[untyped]
177
+
178
+ # Extract first argument value from a CallNode
179
+ def extract_first_argument_value: (untyped node) -> (String | Symbol | nil)
95
180
  end
96
181
 
97
182
  class FileAligner
@@ -260,6 +345,25 @@ module Prism
260
345
  def add_exact_match_from_template: (FileAligner::Anchor anchor) -> void
261
346
 
262
347
  def process_boundary: (FileAligner::Boundary boundary) -> void
348
+
349
+ # Find a node that overlaps with a line range
350
+ def find_node_in_range: (FileAnalysis analysis, Integer start_line, Integer end_line) -> untyped?
351
+
352
+ # Find a node at a specific line (deprecated)
353
+ def find_node_at_line: (FileAnalysis analysis, Integer line_num) -> untyped?
354
+
355
+ # Determine if two matching nodes should be recursively merged
356
+ # Returns true for ClassNode, ModuleNode, SingletonClassNode, CallNode with blocks, BeginNode
357
+ def should_merge_recursively?: (untyped? template_node, untyped? dest_node) -> bool
358
+
359
+ # Check if a node's body contains freeze block markers
360
+ def node_contains_freeze_blocks?: (untyped node) -> bool
361
+
362
+ # Recursively merge the body of matching class, module, or call-with-block nodes
363
+ def merge_node_body_recursively: (untyped template_node, untyped dest_node, FileAligner::Anchor anchor) -> void
364
+
365
+ # Extract the body content of a node (without declaration and closing 'end')
366
+ def extract_node_body: (untyped node, FileAnalysis analysis) -> String
263
367
  end
264
368
  end
265
369
  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: 1.1.0
4
+ version: 1.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter H. Boling
@@ -265,10 +265,10 @@ licenses:
265
265
  - MIT
266
266
  metadata:
267
267
  homepage_uri: https://prism-merge.galtzo.com/
268
- source_code_uri: https://github.com/kettle-rb/prism-merge/tree/v1.1.0
269
- changelog_uri: https://github.com/kettle-rb/prism-merge/blob/v1.1.0/CHANGELOG.md
268
+ source_code_uri: https://github.com/kettle-rb/prism-merge/tree/v1.1.1
269
+ changelog_uri: https://github.com/kettle-rb/prism-merge/blob/v1.1.1/CHANGELOG.md
270
270
  bug_tracker_uri: https://github.com/kettle-rb/prism-merge/issues
271
- documentation_uri: https://www.rubydoc.info/gems/prism-merge/1.1.0
271
+ documentation_uri: https://www.rubydoc.info/gems/prism-merge/1.1.1
272
272
  funding_uri: https://github.com/sponsors/pboling
273
273
  wiki_uri: https://github.com/kettle-rb/prism-merge/wiki
274
274
  news_uri: https://www.railsbling.com/tags/prism-merge
metadata.gz.sig CHANGED
Binary file