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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +41 -1
- data/README.md +149 -15
- data/lib/prism/merge/file_analysis.rb +232 -2
- data/lib/prism/merge/freeze_node.rb +15 -4
- data/lib/prism/merge/smart_merger.rb +135 -32
- data/lib/prism/merge/version.rb +1 -1
- data/sig/prism/merge.rbs +104 -0
- data.tar.gz.sig +0 -0
- metadata +4 -4
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ae8f1b1cf1ae576917fae80dea44c38c181296c46f60f4dc70546d12ac0e6a90
|
|
4
|
+
data.tar.gz: 131295bf35a6da2038ea889f46ee9520f4a83a7daf9d983c2d0b91fb67644985
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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 `
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
#
|
|
377
|
+
# prism-merge:freeze
|
|
344
378
|
gem "custom-gem", path: "../custom"
|
|
345
379
|
# Add any custom configuration you want to preserve
|
|
346
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
711
|
+
# prism-merge:freeze
|
|
600
712
|
CONFIG = { key: "secret" }
|
|
601
|
-
#
|
|
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.
|
|
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,
|
|
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
|
|
122
|
-
#
|
|
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 && (
|
|
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
|
|
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
|
|
274
|
-
|
|
275
|
-
|
|
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
|
|
390
|
-
#
|
|
391
|
-
#
|
|
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
|
-
|
|
405
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
506
|
+
# those bodies, and then reassembles the complete node with the merged body.
|
|
446
507
|
#
|
|
447
|
-
# @param template_node [Prism::ClassNode, Prism::ModuleNode]
|
|
448
|
-
# @param dest_node [Prism::ClassNode, Prism::ModuleNode]
|
|
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
|
-
#
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
data/lib/prism/merge/version.rb
CHANGED
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.
|
|
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.
|
|
269
|
-
changelog_uri: https://github.com/kettle-rb/prism-merge/blob/v1.1.
|
|
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.
|
|
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
|