ast-merge 4.0.0 → 4.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +74 -1
- data/README.md +53 -58
- data/exe/ast-merge-recipe +19 -9
- data/lib/ast/merge/ast_node.rb +15 -19
- data/lib/ast/merge/diff_mapper_base.rb +1 -1
- data/lib/ast/merge/recipe/runner.rb +16 -4
- data/lib/ast/merge/rspec/dependency_tags.rb +41 -182
- data/lib/ast/merge/rspec/merge_gem_registry.rb +382 -0
- data/lib/ast/merge/version.rb +1 -1
- data/lib/ast/merge.rb +3 -0
- data.tar.gz.sig +0 -0
- metadata +7 -6
- 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: 29e3c1eb54812e59b30a8ae0dc42792486ef2e4992dbd43234452cfce0f2b1ab
|
|
4
|
+
data.tar.gz: bb1b200aa2a2d8a48c127952510984cf2ccd71e2517ced1d422fe542a7969c78
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b0823b5b64b77947faa3213463c18289a4275b5b8fd7d5a0dcf258c1ae4088dcc50a34923f459944473cfae67e7c18dd646e3236f7ff6eb0898edfdb8b5d9ab2
|
|
7
|
+
data.tar.gz: 0f8dafa6c932a75504c898793fb0f51dfa9977d288f70e6e8e9fc8aff9f0f3452742a0302dbdd23aee33db8ab4528834048322af37d7c4d44ec9fb473e682855
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/CHANGELOG.md
CHANGED
|
@@ -30,6 +30,73 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
30
30
|
|
|
31
31
|
### Security
|
|
32
32
|
|
|
33
|
+
## [4.0.2] - 2026-01-12
|
|
34
|
+
|
|
35
|
+
- TAG: [v4.0.2][4.0.2t]
|
|
36
|
+
- COVERAGE: 97.30% -- 2739/2815 lines in 53 files
|
|
37
|
+
- BRANCH COVERAGE: 89.84% -- 893/994 branches in 53 files
|
|
38
|
+
- 98.81% documented
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
- **`Recipe::Runner` target file override**: Accept `target_files` parameter to override recipe targets
|
|
43
|
+
- `Runner.new(recipe, target_files: ["file1.md", "file2.md"])` - Process only specified files
|
|
44
|
+
- Paths are expanded relative to `base_dir`
|
|
45
|
+
- When not specified, falls back to recipe's configured targets
|
|
46
|
+
- **`exe/ast-merge-recipe` file arguments**: Accept target files on command line
|
|
47
|
+
- `ast-merge-recipe recipe.yml file1.md file2.md` - Override recipe targets
|
|
48
|
+
- Updated help text and banner to document new usage
|
|
49
|
+
- **`bin/update_gem_family_section` file arguments**: Accept target files on command line
|
|
50
|
+
- `bin/update_gem_family_section vendor/my-gem/README.md` - Process specific file(s)
|
|
51
|
+
- If no files specified, defaults to `README.md` + `vendor/*/README.md`
|
|
52
|
+
- Added `--skip-fix` option to skip the formatting fix step
|
|
53
|
+
|
|
54
|
+
### Changed
|
|
55
|
+
|
|
56
|
+
- **`bin/update_gem_family_section`**: Refactored to use `OptionParser` for clean option handling
|
|
57
|
+
- Consistent with `bin/fix_readme_formatting` style
|
|
58
|
+
- Properly separates options from file arguments
|
|
59
|
+
|
|
60
|
+
## [4.0.1] - 2026-01-11
|
|
61
|
+
|
|
62
|
+
- TAG: [v4.0.1][4.0.1t]
|
|
63
|
+
- COVERAGE: 96.45% -- 2553/2647 lines in 51 files
|
|
64
|
+
- BRANCH COVERAGE: 87.41% -- 812/929 branches in 51 files
|
|
65
|
+
- 98.80% documented
|
|
66
|
+
|
|
67
|
+
### Added
|
|
68
|
+
|
|
69
|
+
- **`Ast::Merge::RSpec::MergeGemRegistry`** - Fully dynamic merge gem registration for RSpec dependency tags
|
|
70
|
+
- `register(tag_name, require_path:, merger_class:, test_source:, category:)` - Register a merge gem
|
|
71
|
+
- `available?(tag_name)` - Check if a merge gem is available and functional
|
|
72
|
+
- `registered_gems` - Get all registered gem tag names
|
|
73
|
+
- `gems_by_category(category)` - Filter gems by category (:markdown, :data, :code, :config, :other)
|
|
74
|
+
- `summary` - Get availability status of all registered gems
|
|
75
|
+
- Automatically defines `*_available?` methods on `DependencyTags` at registration time
|
|
76
|
+
- External merge gems can now get full RSpec tag support without modifying ast-merge
|
|
77
|
+
|
|
78
|
+
### Changed
|
|
79
|
+
|
|
80
|
+
- Upgrade to [tree_haver v5.0.1](https://github.com/kettle-rb/tree_haver/releases/tag/v5.0.1)
|
|
81
|
+
- **`Ast::Merge::AstNode` now inherits from `TreeHaver::Base::Node`**
|
|
82
|
+
- Ensures synthetic nodes stay in sync with the canonical Node API
|
|
83
|
+
- Inherits `Comparable`, `Enumerable` from base class
|
|
84
|
+
- Retains all existing methods and behavior (Point, Location, signature, etc.)
|
|
85
|
+
- Constructor calls `super(self, source: source)` to properly initialize base class
|
|
86
|
+
- **RSpec Dependency Tags refactored to use MergeGemRegistry**
|
|
87
|
+
- Removed hardcoded merge gem availability checks
|
|
88
|
+
- Removed `MERGE_GEM_TEST_SOURCES` constant
|
|
89
|
+
- `*_available?` methods are now defined dynamically when gems register
|
|
90
|
+
- `any_markdown_merge_available?` now queries registry by category
|
|
91
|
+
- RSpec exclusion filters are configured dynamically from registry
|
|
92
|
+
- `Ast::Merge::Testing::TestableNode` now delegates to `TreeHaver::RSpec::TestableNode`
|
|
93
|
+
- The TestableNode implementation has been moved to tree_haver for sharing across all merge gems
|
|
94
|
+
- `spec/support/testable_node.rb` now requires and re-exports the tree_haver version
|
|
95
|
+
- Backward compatible: existing tests continue to work unchanged
|
|
96
|
+
- `spec/ast/merge/node_wrapper_base_spec.rb` refactored to use `TestableNode` instead of mocks
|
|
97
|
+
- Real TreeHaver::Node behavior for most tests
|
|
98
|
+
- Mocks only retained for edge case testing (e.g., invalid end_line before start_line)
|
|
99
|
+
|
|
33
100
|
## [4.0.0] - 2026-01-11
|
|
34
101
|
|
|
35
102
|
- TAG: [v4.0.0][4.0.0t]
|
|
@@ -597,7 +664,13 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
597
664
|
|
|
598
665
|
- Initial release
|
|
599
666
|
|
|
600
|
-
[Unreleased]: https://github.com/kettle-rb/ast-merge/compare/v4.0.
|
|
667
|
+
[Unreleased]: https://github.com/kettle-rb/ast-merge/compare/v4.0.3...HEAD
|
|
668
|
+
[4.0.3]: https://github.com/kettle-rb/ast-merge/compare/v4.0.2...v4.0.3
|
|
669
|
+
[4.0.3t]: https://github.com/kettle-rb/ast-merge/releases/tag/v4.0.3
|
|
670
|
+
[4.0.2]: https://github.com/kettle-rb/ast-merge/compare/v4.0.1...v4.0.2
|
|
671
|
+
[4.0.2t]: https://github.com/kettle-rb/ast-merge/releases/tag/v4.0.2
|
|
672
|
+
[4.0.1]: https://github.com/kettle-rb/ast-merge/compare/v4.0.0...v4.0.1
|
|
673
|
+
[4.0.1t]: https://github.com/kettle-rb/ast-merge/releases/tag/v4.0.1
|
|
601
674
|
[4.0.0]: https://github.com/kettle-rb/ast-merge/compare/v3.1.0...v4.0.0
|
|
602
675
|
[4.0.0t]: https://github.com/kettle-rb/ast-merge/releases/tag/v4.0.0
|
|
603
676
|
[3.1.0]: https://github.com/kettle-rb/ast-merge/compare/v3.0.0...v3.1.0
|
data/README.md
CHANGED
|
@@ -208,24 +208,24 @@ The `*-merge` gem family is built on a two-layer architecture:
|
|
|
208
208
|
|
|
209
209
|
[tree\_haver][tree_haver] provides cross-Ruby parsing capabilities:
|
|
210
210
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
211
|
+
- **Universal Backend Support**: Automatically selects the best parsing backend for your Ruby implementation (MRI, JRuby, TruffleRuby)
|
|
212
|
+
- **10 Backend Options**: MRI C extensions, Rust bindings, FFI, Java (JRuby), language-specific parsers (Prism, Psych, Commonmarker, Markly), and pure Ruby fallback (Citrus)
|
|
213
|
+
- **Unified API**: Write parsing code once, run on any Ruby implementation
|
|
214
|
+
- **Grammar Discovery**: Built-in `GrammarFinder` for platform-aware grammar library discovery
|
|
215
|
+
- **Thread-Safe**: Language registry with thread-safe caching
|
|
216
216
|
|
|
217
217
|
#### Layer 2: ast-merge (Merge Infrastructure)
|
|
218
218
|
|
|
219
219
|
Ast::Merge builds on tree\_haver to provide:
|
|
220
220
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
221
|
+
- **Base Classes**: `FreezeNode`, `MergeResult` base classes with unified constructors
|
|
222
|
+
- **Shared Modules**: `FileAnalysisBase`, `FileAnalyzable`, `MergerConfig`, `DebugLogger`
|
|
223
|
+
- **Freeze Block Support**: Configurable marker patterns for multiple comment syntaxes (preserve sections during merge)
|
|
224
|
+
- **Node Typing System**: `NodeTyping` for canonical node type identification across different parsers
|
|
225
|
+
- **Conflict Resolution**: `ConflictResolverBase` with pluggable strategies
|
|
226
|
+
- **Error Classes**: `ParseError`, `TemplateParseError`, `DestinationParseError`
|
|
227
|
+
- **Region Detection**: `RegionDetectorBase`, `FencedCodeBlockDetector` for text-based analysis
|
|
228
|
+
- **RSpec Shared Examples**: Test helpers for implementing new merge gems
|
|
229
229
|
|
|
230
230
|
### Creating a New Merge Gem
|
|
231
231
|
|
|
@@ -372,10 +372,10 @@ merger = MyFormat::SmartMerger.new(
|
|
|
372
372
|
|
|
373
373
|
This is particularly useful for:
|
|
374
374
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
375
|
+
- Paragraphs with minor edits (typos, rewording)
|
|
376
|
+
- Headings with slight changes
|
|
377
|
+
- Comments with updated text
|
|
378
|
+
- Any text-based node that may have been slightly modified
|
|
379
379
|
|
|
380
380
|
### Namespace Reference
|
|
381
381
|
|
|
@@ -391,11 +391,11 @@ The `Ast::Merge` module is organized into several namespaces, each with detailed
|
|
|
391
391
|
|
|
392
392
|
**Key Classes by Namespace:**
|
|
393
393
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
394
|
+
- **Detector**: `Region`, `Base`, `Mergeable`, `FencedCodeBlock`, `YamlFrontmatter`, `TomlFrontmatter`
|
|
395
|
+
- **Recipe**: `Config`, `Runner`, `ScriptLoader`
|
|
396
|
+
- **Comment**: `Line`, `Block`, `Empty`, `Parser`, `Style`
|
|
397
|
+
- **Text**: `SmartMerger`, `FileAnalysis`, `LineNode`, `WordNode`, `Section`
|
|
398
|
+
- **RSpec**: Shared examples and dependency tags for testing `*-merge` implementations
|
|
399
399
|
|
|
400
400
|
## 💡 Info you can shake a stick at
|
|
401
401
|
|
|
@@ -447,18 +447,13 @@ The maintainers of this and thousands of other packages are working with Tidelif
|
|
|
447
447
|
|
|
448
448
|
[![Get help from me on Tidelift][🏙️entsup-tidelift-img]][🏙️entsup-tidelift]
|
|
449
449
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
- [![Live Chat on Discord][✉️discord-invite-img-ftb]][🖼️galtzo-discord]
|
|
458
|
-
|
|
459
|
-
- [![Get help from me on Upwork][👨🏼🏫expsup-upwork-img]][👨🏼🏫expsup-upwork]
|
|
460
|
-
|
|
461
|
-
- [![Get help from me on Codementor][👨🏼🏫expsup-codementor-img]][👨🏼🏫expsup-codementor]
|
|
450
|
+
- 💡Subscribe for support guarantees covering *all* your FLOSS dependencies
|
|
451
|
+
- 💡Tidelift is part of [Sonar][🏙️entsup-tidelift-sonar]
|
|
452
|
+
- 💡Tidelift pays maintainers to maintain the software you depend on\!<br/>📊`@`Pointy Haired Boss: An [enterprise support][🏙️entsup-tidelift] subscription is "[never gonna let you down][🧮kloc]", and *supports* open source maintainers
|
|
453
|
+
Alternatively:
|
|
454
|
+
- [![Live Chat on Discord][✉️discord-invite-img-ftb]][🖼️galtzo-discord]
|
|
455
|
+
- [![Get help from me on Upwork][👨🏼🏫expsup-upwork-img]][👨🏼🏫expsup-upwork]
|
|
456
|
+
- [![Get help from me on Codementor][👨🏼🏫expsup-codementor-img]][👨🏼🏫expsup-codementor]
|
|
462
457
|
|
|
463
458
|
</details>
|
|
464
459
|
|
|
@@ -537,17 +532,17 @@ merger = SomeFormat::Merge::SmartMerger.new(
|
|
|
537
532
|
|
|
538
533
|
Control which source wins when both files have the same structural element:
|
|
539
534
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
535
|
+
- **`:template`** - Template values replace destination values
|
|
536
|
+
- **`:destination`** (default) - Destination values are preserved
|
|
537
|
+
- **Hash** - Per-node-type preference (see Advanced Configuration)
|
|
543
538
|
|
|
544
539
|
### Template-Only Nodes
|
|
545
540
|
|
|
546
541
|
Control whether to add nodes that only exist in the template:
|
|
547
542
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
543
|
+
- **`true`** - Add all template-only nodes
|
|
544
|
+
- **`false`** (default) - Skip template-only nodes
|
|
545
|
+
- **Callable** - Filter which template-only nodes to add
|
|
551
546
|
|
|
552
547
|
#### Callable Filter
|
|
553
548
|
|
|
@@ -577,10 +572,10 @@ merger = Markly::Merge::SmartMerger.new(
|
|
|
577
572
|
|
|
578
573
|
The `entry` hash contains:
|
|
579
574
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
575
|
+
- `:template_node` - The node being considered for addition
|
|
576
|
+
- `:signature` - The node's signature (Array or other value)
|
|
577
|
+
- `:template_index` - Index in the template statements
|
|
578
|
+
- `:dest_index` - Always `nil` for template-only nodes
|
|
584
579
|
|
|
585
580
|
## 🔧 Basic Usage
|
|
586
581
|
|
|
@@ -606,11 +601,11 @@ end
|
|
|
606
601
|
|
|
607
602
|
### Available Shared Examples
|
|
608
603
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
604
|
+
- `"Ast::Merge::FreezeNode"` - Tests for FreezeNode implementations
|
|
605
|
+
- `"Ast::Merge::MergeResult"` - Tests for MergeResult implementations
|
|
606
|
+
- `"Ast::Merge::DebugLogger"` - Tests for DebugLogger implementations
|
|
607
|
+
- `"Ast::Merge::FileAnalysisBase"` - Tests for FileAnalysis implementations
|
|
608
|
+
- `"Ast::Merge::MergerConfig"` - Tests for SmartMerger implementations
|
|
614
609
|
|
|
615
610
|
## 🎛️ Advanced Configuration
|
|
616
611
|
|
|
@@ -622,9 +617,9 @@ This is useful for hand-edited customizations you never want overwritten.
|
|
|
622
617
|
|
|
623
618
|
A freeze block consists of:
|
|
624
619
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
620
|
+
- A **start marker** comment (e.g., `# mytoken:freeze`)
|
|
621
|
+
- The protected content
|
|
622
|
+
- An **end marker** comment (e.g., `# mytoken:unfreeze`)
|
|
628
623
|
|
|
629
624
|
```ruby
|
|
630
625
|
# In a Ruby file with prism-merge:
|
|
@@ -669,13 +664,13 @@ preferences for different types of nodes (e.g., prefer template for linter confi
|
|
|
669
664
|
|
|
670
665
|
1. **Define a `node_typing`**: A Hash mapping node type symbols to callables that receive a node and return either:
|
|
671
666
|
|
|
672
|
-
|
|
673
|
-
|
|
667
|
+
- The original node (no special handling)
|
|
668
|
+
- A wrapped node with a `merge_type` attribute (via `Ast::Merge::NodeTyping::Wrapper`)
|
|
674
669
|
|
|
675
670
|
2. **Use a Hash-based preference**: Instead of a simple `:destination` or `:template` Symbol, pass a Hash with:
|
|
676
671
|
|
|
677
|
-
|
|
678
|
-
|
|
672
|
+
- `:default` key for the fallback preference
|
|
673
|
+
- Custom keys matching the `merge_type` values from your `node_typing`
|
|
679
674
|
|
|
680
675
|
```ruby
|
|
681
676
|
# Example: Prefer template for lint gem configs, destination for everything else
|
|
@@ -903,7 +898,7 @@ is a *breaking change* to an API, and for that reason the bike shedding is endle
|
|
|
903
898
|
To get a better understanding of how SemVer is intended to work over a project's lifetime,
|
|
904
899
|
read this article from the creator of SemVer:
|
|
905
900
|
|
|
906
|
-
|
|
901
|
+
- ["Major Version Numbers are Not Sacred"][📌major-versions-not-sacred]
|
|
907
902
|
|
|
908
903
|
</details>
|
|
909
904
|
|
|
@@ -1108,7 +1103,7 @@ Thanks for RTFM. ☺️
|
|
|
1108
1103
|
[📌gitmoji]: https://gitmoji.dev
|
|
1109
1104
|
[📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
|
|
1110
1105
|
[🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
|
|
1111
|
-
[🧮kloc-img]: https://img.shields.io/badge/KLOC-2.
|
|
1106
|
+
[🧮kloc-img]: https://img.shields.io/badge/KLOC-2.815-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
|
|
1112
1107
|
[🔐security]: SECURITY.md
|
|
1113
1108
|
[🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
|
|
1114
1109
|
[📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
|
data/exe/ast-merge-recipe
CHANGED
|
@@ -171,9 +171,10 @@ class AstMergeRecipeCLI
|
|
|
171
171
|
|
|
172
172
|
def parse_options(argv)
|
|
173
173
|
@option_parser = OptionParser.new do |opts|
|
|
174
|
-
opts.banner = "Usage: #{File.basename($0)} RECIPE_FILE [options]"
|
|
174
|
+
opts.banner = "Usage: #{File.basename($0)} RECIPE_FILE [FILES...] [options]"
|
|
175
175
|
opts.separator("")
|
|
176
176
|
opts.separator("Run a YAML-based merge recipe against target files.")
|
|
177
|
+
opts.separator("If FILES are specified, they override the recipe's targets.")
|
|
177
178
|
opts.separator("")
|
|
178
179
|
opts.separator("Options:")
|
|
179
180
|
|
|
@@ -225,9 +226,15 @@ class AstMergeRecipeCLI
|
|
|
225
226
|
opts.on("-h", "--help", "Show this help message") do
|
|
226
227
|
puts opts
|
|
227
228
|
puts
|
|
229
|
+
puts "Arguments:"
|
|
230
|
+
puts " RECIPE_FILE Path to the YAML recipe file (required)"
|
|
231
|
+
puts " [FILES...] Target files to process (optional, overrides recipe targets)"
|
|
232
|
+
puts
|
|
228
233
|
puts "Examples:"
|
|
229
234
|
puts " #{File.basename($0)} .merge-recipes/gem_family_section.yml --dry-run"
|
|
230
235
|
puts " #{File.basename($0)} recipe.yml --verbose --parser=commonmarker"
|
|
236
|
+
puts " #{File.basename($0)} recipe.yml vendor/my-gem/README.md"
|
|
237
|
+
puts " #{File.basename($0)} recipe.yml README.md vendor/*/README.md"
|
|
231
238
|
puts
|
|
232
239
|
puts "Recipe YAML format:"
|
|
233
240
|
puts " See lib/ast/merge/recipe/README.md for full documentation"
|
|
@@ -241,10 +248,8 @@ class AstMergeRecipeCLI
|
|
|
241
248
|
# First non-option argument is the recipe file
|
|
242
249
|
@options[:recipe_file] = remaining.shift
|
|
243
250
|
|
|
244
|
-
#
|
|
245
|
-
if remaining.any?
|
|
246
|
-
$stderr.puts Colors.yellow("WARNING: Ignoring extra arguments: #{remaining.join(", ")}")
|
|
247
|
-
end
|
|
251
|
+
# Remaining arguments are target files (override recipe targets)
|
|
252
|
+
@options[:target_files] = remaining if remaining.any?
|
|
248
253
|
end
|
|
249
254
|
|
|
250
255
|
def validate_options!
|
|
@@ -271,14 +276,19 @@ class AstMergeRecipeCLI
|
|
|
271
276
|
recipe = Ast::Merge::Recipe::Config.load(@options[:recipe_file])
|
|
272
277
|
print_recipe_info(recipe)
|
|
273
278
|
|
|
274
|
-
#
|
|
275
|
-
|
|
276
|
-
recipe,
|
|
279
|
+
# Build runner options
|
|
280
|
+
runner_opts = {
|
|
277
281
|
dry_run: @options[:dry_run],
|
|
278
282
|
base_dir: @options[:base_dir],
|
|
279
283
|
parser: @options[:parser],
|
|
280
284
|
verbose: @options[:verbose],
|
|
281
|
-
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
# If target files specified on command line, override recipe targets
|
|
288
|
+
runner_opts[:target_files] = @options[:target_files] if @options[:target_files]
|
|
289
|
+
|
|
290
|
+
# Create runner
|
|
291
|
+
runner = Ast::Merge::Recipe::Runner.new(recipe, **runner_opts)
|
|
282
292
|
|
|
283
293
|
# Run and display results
|
|
284
294
|
puts Colors.cyan("Processing files...")
|
data/lib/ast/merge/ast_node.rb
CHANGED
|
@@ -8,9 +8,9 @@ module Ast
|
|
|
8
8
|
# created by ast-merge for representing content that doesn't have a native
|
|
9
9
|
# AST (comments, text lines, env file entries, etc.).
|
|
10
10
|
#
|
|
11
|
-
# This class
|
|
12
|
-
# with
|
|
13
|
-
#
|
|
11
|
+
# This class inherits from TreeHaver::Base::Node, ensuring it stays in sync
|
|
12
|
+
# with the canonical Node API. This allows synthetic nodes to be used
|
|
13
|
+
# interchangeably with parser-backed nodes in merge operations.
|
|
14
14
|
#
|
|
15
15
|
# Implements the TreeHaver::Node protocol:
|
|
16
16
|
# - type → String node type
|
|
@@ -36,12 +36,10 @@ module Ast
|
|
|
36
36
|
# end
|
|
37
37
|
# end
|
|
38
38
|
#
|
|
39
|
-
# @see TreeHaver::Node The
|
|
39
|
+
# @see TreeHaver::Base::Node The base class defining the canonical Node API
|
|
40
40
|
# @see Comment::Line Example synthetic node for comments
|
|
41
41
|
# @see Text::LineNode Example synthetic node for text lines
|
|
42
|
-
class AstNode
|
|
43
|
-
include Comparable
|
|
44
|
-
|
|
42
|
+
class AstNode < TreeHaver::Base::Node
|
|
45
43
|
# Point class compatible with TreeHaver::Point
|
|
46
44
|
# Provides both method and hash-style access to row/column
|
|
47
45
|
Point = Struct.new(:row, :column, keyword_init: true) do
|
|
@@ -83,9 +81,6 @@ module Ast
|
|
|
83
81
|
# @return [String] The source text for this node
|
|
84
82
|
attr_reader :slice
|
|
85
83
|
|
|
86
|
-
# @return [String, nil] The full source text (for text extraction)
|
|
87
|
-
attr_reader :source
|
|
88
|
-
|
|
89
84
|
# Initialize a new AstNode.
|
|
90
85
|
#
|
|
91
86
|
# @param slice [String] The source text for this node
|
|
@@ -94,15 +89,14 @@ module Ast
|
|
|
94
89
|
def initialize(slice:, location:, source: nil)
|
|
95
90
|
@slice = slice
|
|
96
91
|
@location = location
|
|
97
|
-
|
|
92
|
+
# Call parent constructor with self as inner_node
|
|
93
|
+
super(self, source: source)
|
|
98
94
|
end
|
|
99
95
|
|
|
100
|
-
#
|
|
101
|
-
#
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
def inner_node
|
|
105
|
-
self
|
|
96
|
+
# Override source to return stored value (not parent's)
|
|
97
|
+
# @return [String, nil] The full source text (for text extraction)
|
|
98
|
+
def source
|
|
99
|
+
@source || super
|
|
106
100
|
end
|
|
107
101
|
|
|
108
102
|
# TreeHaver::Node protocol: type
|
|
@@ -132,10 +126,11 @@ module Ast
|
|
|
132
126
|
#
|
|
133
127
|
# @return [Integer] Starting byte offset
|
|
134
128
|
def start_byte
|
|
135
|
-
|
|
129
|
+
src = source
|
|
130
|
+
return 0 unless src && location
|
|
136
131
|
|
|
137
132
|
# Calculate byte offset from line/column
|
|
138
|
-
lines =
|
|
133
|
+
lines = src.lines
|
|
139
134
|
byte_offset = 0
|
|
140
135
|
(0...(location.start_line - 1)).each do |i|
|
|
141
136
|
byte_offset += lines[i]&.bytesize || 0
|
|
@@ -249,6 +244,7 @@ module Ast
|
|
|
249
244
|
end
|
|
250
245
|
|
|
251
246
|
# Comparable: compare nodes by position
|
|
247
|
+
# Note: Inherits Comparable from TreeHaver::Base::Node
|
|
252
248
|
#
|
|
253
249
|
# @param other [AstNode] node to compare with
|
|
254
250
|
# @return [Integer, nil] -1, 0, 1, or nil if not comparable
|
|
@@ -227,7 +227,7 @@ module Ast
|
|
|
227
227
|
next unless node.respond_to?(:start_line) && node.respond_to?(:end_line)
|
|
228
228
|
next unless node.start_line && node.end_line
|
|
229
229
|
|
|
230
|
-
line_num
|
|
230
|
+
line_num.between?(node.start_line, node.end_line)
|
|
231
231
|
end
|
|
232
232
|
end
|
|
233
233
|
|
|
@@ -40,6 +40,9 @@ module Ast
|
|
|
40
40
|
# @return [Symbol] Parser to use (:markly, :commonmarker, :prism, :psych, etc.)
|
|
41
41
|
attr_reader :parser
|
|
42
42
|
|
|
43
|
+
# @return [Array<String>, nil] Target files override (from command line)
|
|
44
|
+
attr_reader :target_files
|
|
45
|
+
|
|
43
46
|
# @return [Array<Result>] Results from the last run
|
|
44
47
|
attr_reader :results
|
|
45
48
|
|
|
@@ -50,12 +53,14 @@ module Ast
|
|
|
50
53
|
# @param base_dir [String, nil] Base directory for path resolution
|
|
51
54
|
# @param parser [Symbol] Which parser to use
|
|
52
55
|
# @param verbose [Boolean] Enable verbose output
|
|
53
|
-
|
|
56
|
+
# @param target_files [Array<String>, nil] Override recipe targets with these files
|
|
57
|
+
def initialize(recipe, dry_run: false, base_dir: nil, parser: :markly, verbose: false, target_files: nil, **options)
|
|
54
58
|
@recipe = recipe
|
|
55
59
|
@dry_run = dry_run
|
|
56
60
|
@base_dir = base_dir || Dir.pwd
|
|
57
61
|
@parser = parser
|
|
58
62
|
@verbose = verbose
|
|
63
|
+
@target_files = target_files
|
|
59
64
|
@results = []
|
|
60
65
|
end
|
|
61
66
|
|
|
@@ -66,10 +71,17 @@ module Ast
|
|
|
66
71
|
@results = []
|
|
67
72
|
|
|
68
73
|
template_content = load_template
|
|
69
|
-
# Let the recipe expand targets from its own location
|
|
70
|
-
target_files = recipe.expand_targets
|
|
71
74
|
|
|
72
|
-
|
|
75
|
+
# Use command-line targets if provided, otherwise expand from recipe
|
|
76
|
+
files_to_process = if @target_files && !@target_files.empty?
|
|
77
|
+
# Expand paths relative to base_dir
|
|
78
|
+
@target_files.map { |f| File.expand_path(f, @base_dir) }
|
|
79
|
+
else
|
|
80
|
+
# Let the recipe expand targets from its own location
|
|
81
|
+
recipe.expand_targets
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
files_to_process.each do |target_path|
|
|
73
85
|
result = process_file(target_path, template_content)
|
|
74
86
|
@results << result
|
|
75
87
|
yield result if block_given?
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "merge_gem_registry"
|
|
4
|
+
|
|
3
5
|
# Ast::Merge RSpec Dependency Tags
|
|
4
6
|
#
|
|
5
7
|
# This module provides dependency detection helpers for conditional test execution
|
|
6
|
-
# in the ast-merge gem family. It
|
|
7
|
-
# merge-gem-specific checks.
|
|
8
|
+
# in the ast-merge gem family. It uses MergeGemRegistry for dynamic merge gem detection.
|
|
8
9
|
#
|
|
9
10
|
# @example Loading in spec_helper.rb
|
|
10
11
|
# require "ast/merge/rspec/dependency_tags"
|
|
@@ -18,50 +19,26 @@
|
|
|
18
19
|
# # This test only runs when prism-merge is available
|
|
19
20
|
# end
|
|
20
21
|
#
|
|
21
|
-
# ==
|
|
22
|
-
#
|
|
23
|
-
# === Merge Gem Tags (run when dependency IS available)
|
|
24
|
-
#
|
|
25
|
-
# [:markly_merge]
|
|
26
|
-
# markly-merge gem is available and functional.
|
|
27
|
-
#
|
|
28
|
-
# [:commonmarker_merge]
|
|
29
|
-
# commonmarker-merge gem is available and functional.
|
|
30
|
-
#
|
|
31
|
-
# [:markdown_merge]
|
|
32
|
-
# markdown-merge gem is available and functional.
|
|
33
|
-
#
|
|
34
|
-
# [:prism_merge]
|
|
35
|
-
# prism-merge gem is available and functional.
|
|
36
|
-
#
|
|
37
|
-
# [:json_merge]
|
|
38
|
-
# json-merge gem is available and functional.
|
|
22
|
+
# == Dynamic Tag Registration
|
|
39
23
|
#
|
|
40
|
-
#
|
|
41
|
-
#
|
|
24
|
+
# Merge gems register themselves with MergeGemRegistry, which automatically:
|
|
25
|
+
# - Defines `*_available?` methods on DependencyTags
|
|
26
|
+
# - Configures RSpec exclusion filters for the tag
|
|
27
|
+
# - Supports negated tags (`:not_*`)
|
|
42
28
|
#
|
|
43
|
-
#
|
|
44
|
-
#
|
|
29
|
+
# @example How merge gems register (in their lib file)
|
|
30
|
+
# Ast::Merge::RSpec::MergeGemRegistry.register(
|
|
31
|
+
# :markly_merge,
|
|
32
|
+
# require_path: "markly/merge",
|
|
33
|
+
# merger_class: "Markly::Merge::SmartMerger",
|
|
34
|
+
# test_source: "# Test\n\nParagraph",
|
|
35
|
+
# category: :markdown
|
|
36
|
+
# )
|
|
45
37
|
#
|
|
46
|
-
#
|
|
47
|
-
# bash-merge gem is available and functional.
|
|
48
|
-
#
|
|
49
|
-
# [:psych_merge]
|
|
50
|
-
# psych-merge gem is available and functional.
|
|
51
|
-
#
|
|
52
|
-
# [:rbs_merge]
|
|
53
|
-
# rbs-merge gem is available and functional.
|
|
38
|
+
# == Built-in Composite Tags
|
|
54
39
|
#
|
|
55
40
|
# [:any_markdown_merge]
|
|
56
|
-
# At least one markdown merge gem (
|
|
57
|
-
#
|
|
58
|
-
# === Negated Tags (run when dependency is NOT available)
|
|
59
|
-
#
|
|
60
|
-
# All positive tags have negated versions prefixed with `not_`:
|
|
61
|
-
# - :not_markly_merge, :not_commonmarker_merge, :not_markdown_merge
|
|
62
|
-
# - :not_prism_merge, :not_json_merge, :not_jsonc_merge
|
|
63
|
-
# - :not_toml_merge, :not_bash_merge, :not_psych_merge, :not_rbs_merge
|
|
64
|
-
# - :not_any_markdown_merge
|
|
41
|
+
# At least one markdown merge gem is available (category: :markdown).
|
|
65
42
|
|
|
66
43
|
module Ast
|
|
67
44
|
module Merge
|
|
@@ -70,96 +47,16 @@ module Ast
|
|
|
70
47
|
module DependencyTags
|
|
71
48
|
class << self
|
|
72
49
|
# ============================================================
|
|
73
|
-
#
|
|
50
|
+
# Composite Availability Checks
|
|
74
51
|
# ============================================================
|
|
75
52
|
|
|
76
|
-
# rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
77
|
-
# Check if markly-merge is available and functional
|
|
78
|
-
#
|
|
79
|
-
# @return [Boolean] true if markly-merge works
|
|
80
|
-
def markly_merge_available?
|
|
81
|
-
return @markly_merge_available if defined?(@markly_merge_available)
|
|
82
|
-
@markly_merge_available = merge_gem_works?("markly/merge", "Markly::Merge::SmartMerger", "# Test\n\nParagraph")
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
# Check if commonmarker-merge is available and functional
|
|
86
|
-
#
|
|
87
|
-
# @return [Boolean] true if commonmarker-merge works
|
|
88
|
-
def commonmarker_merge_available?
|
|
89
|
-
return @commonmarker_merge_available if defined?(@commonmarker_merge_available)
|
|
90
|
-
@commonmarker_merge_available = merge_gem_works?("commonmarker/merge", "Commonmarker::Merge::SmartMerger", "# Test\n\nParagraph")
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
# Check if markdown-merge is available and functional
|
|
94
|
-
#
|
|
95
|
-
# @return [Boolean] true if markdown-merge works
|
|
96
|
-
def markdown_merge_available?
|
|
97
|
-
return @markdown_merge_available if defined?(@markdown_merge_available)
|
|
98
|
-
@markdown_merge_available = merge_gem_works?("markdown/merge", "Markdown::Merge::SmartMerger", "# Test\n\nParagraph")
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
# Check if prism-merge is available and functional
|
|
102
|
-
#
|
|
103
|
-
# @return [Boolean] true if prism-merge works
|
|
104
|
-
def prism_merge_available?
|
|
105
|
-
return @prism_merge_available if defined?(@prism_merge_available)
|
|
106
|
-
@prism_merge_available = merge_gem_works?("prism/merge", "Prism::Merge::SmartMerger", "puts 1")
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
# Check if json-merge is available and functional
|
|
110
|
-
#
|
|
111
|
-
# @return [Boolean] true if json-merge works
|
|
112
|
-
def json_merge_available?
|
|
113
|
-
return @json_merge_available if defined?(@json_merge_available)
|
|
114
|
-
@json_merge_available = merge_gem_works?("json/merge", "Json::Merge::SmartMerger", '{"key": "value"}')
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
# Check if jsonc-merge is available and functional
|
|
118
|
-
#
|
|
119
|
-
# @return [Boolean] true if jsonc-merge works
|
|
120
|
-
def jsonc_merge_available?
|
|
121
|
-
return @jsonc_merge_available if defined?(@jsonc_merge_available)
|
|
122
|
-
@jsonc_merge_available = merge_gem_works?("jsonc/merge", "Jsonc::Merge::SmartMerger", '{"key": "value" /* comment */}')
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
# Check if toml-merge is available and functional
|
|
126
|
-
#
|
|
127
|
-
# @return [Boolean] true if toml-merge works
|
|
128
|
-
def toml_merge_available?
|
|
129
|
-
return @toml_merge_available if defined?(@toml_merge_available)
|
|
130
|
-
@toml_merge_available = merge_gem_works?("toml/merge", "Toml::Merge::SmartMerger", 'key = "value"')
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
# Check if bash-merge is available and functional
|
|
134
|
-
#
|
|
135
|
-
# @return [Boolean] true if bash-merge works
|
|
136
|
-
def bash_merge_available?
|
|
137
|
-
return @bash_merge_available if defined?(@bash_merge_available)
|
|
138
|
-
@bash_merge_available = merge_gem_works?("bash/merge", "Bash::Merge::SmartMerger", "echo hello")
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
# Check if psych-merge is available and functional
|
|
142
|
-
#
|
|
143
|
-
# @return [Boolean] true if psych-merge works
|
|
144
|
-
def psych_merge_available?
|
|
145
|
-
return @psych_merge_available if defined?(@psych_merge_available)
|
|
146
|
-
@psych_merge_available = merge_gem_works?("psych/merge", "Psych::Merge::SmartMerger", "key: value")
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
# Check if rbs-merge is available and functional
|
|
150
|
-
#
|
|
151
|
-
# @return [Boolean] true if rbs-merge works
|
|
152
|
-
def rbs_merge_available?
|
|
153
|
-
return @rbs_merge_available if defined?(@rbs_merge_available)
|
|
154
|
-
@rbs_merge_available = merge_gem_works?("rbs/merge", "Rbs::Merge::SmartMerger", "class Foo end")
|
|
155
|
-
end
|
|
156
|
-
# rubocop:enable ThreadSafety/ClassInstanceVariable
|
|
157
|
-
|
|
158
53
|
# Check if at least one markdown merge gem is available
|
|
159
54
|
#
|
|
160
55
|
# @return [Boolean] true if any markdown merge gem works
|
|
161
56
|
def any_markdown_merge_available?
|
|
162
|
-
|
|
57
|
+
MergeGemRegistry.gems_by_category(:markdown).any? do |tag|
|
|
58
|
+
MergeGemRegistry.available?(tag)
|
|
59
|
+
end
|
|
163
60
|
end
|
|
164
61
|
|
|
165
62
|
# ============================================================
|
|
@@ -170,45 +67,16 @@ module Ast
|
|
|
170
67
|
#
|
|
171
68
|
# @return [Hash{Symbol => Boolean}] map of dependency name to availability
|
|
172
69
|
def summary
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
markdown_merge: markdown_merge_available?,
|
|
177
|
-
prism_merge: prism_merge_available?,
|
|
178
|
-
json_merge: json_merge_available?,
|
|
179
|
-
jsonc_merge: jsonc_merge_available?,
|
|
180
|
-
toml_merge: toml_merge_available?,
|
|
181
|
-
bash_merge: bash_merge_available?,
|
|
182
|
-
psych_merge: psych_merge_available?,
|
|
183
|
-
rbs_merge: rbs_merge_available?,
|
|
184
|
-
any_markdown_merge: any_markdown_merge_available?,
|
|
185
|
-
}
|
|
70
|
+
result = MergeGemRegistry.summary
|
|
71
|
+
result[:any_markdown_merge] = any_markdown_merge_available?
|
|
72
|
+
result
|
|
186
73
|
end
|
|
187
74
|
|
|
188
75
|
# Reset all memoized availability checks
|
|
189
76
|
#
|
|
190
77
|
# @return [void]
|
|
191
78
|
def reset!
|
|
192
|
-
|
|
193
|
-
remove_instance_variable(ivar) if ivar.to_s.end_with?("_available")
|
|
194
|
-
end
|
|
195
|
-
end
|
|
196
|
-
|
|
197
|
-
private
|
|
198
|
-
|
|
199
|
-
# Generic helper to check if a merge gem is available and functional
|
|
200
|
-
#
|
|
201
|
-
# @param require_path [String] the require path for the gem
|
|
202
|
-
# @param merger_class [String] the full class name of the SmartMerger
|
|
203
|
-
# @param test_source [String] sample source code to test merging
|
|
204
|
-
# @return [Boolean] true if the merger can be instantiated
|
|
205
|
-
def merge_gem_works?(require_path, merger_class, test_source)
|
|
206
|
-
require require_path
|
|
207
|
-
klass = Object.const_get(merger_class)
|
|
208
|
-
klass.new(test_source, test_source)
|
|
209
|
-
true
|
|
210
|
-
rescue LoadError, StandardError
|
|
211
|
-
false
|
|
79
|
+
MergeGemRegistry.reset_availability!
|
|
212
80
|
end
|
|
213
81
|
end
|
|
214
82
|
end
|
|
@@ -219,10 +87,11 @@ end
|
|
|
219
87
|
# Configure RSpec with dependency-based exclusion filters
|
|
220
88
|
RSpec.configure do |config|
|
|
221
89
|
deps = Ast::Merge::RSpec::DependencyTags
|
|
90
|
+
registry = Ast::Merge::RSpec::MergeGemRegistry
|
|
222
91
|
|
|
223
92
|
config.before(:suite) do
|
|
224
93
|
# Print dependency summary if AST_MERGE_DEBUG is set
|
|
225
|
-
|
|
94
|
+
unless ENV.fetch("AST_MERGE_DEBUG", "false").casecmp?("false")
|
|
226
95
|
puts "\n=== Ast::Merge Test Dependencies ==="
|
|
227
96
|
deps.summary.each do |dep, available|
|
|
228
97
|
status = available ? "✓ available" : "✗ not available"
|
|
@@ -233,34 +102,24 @@ RSpec.configure do |config|
|
|
|
233
102
|
end
|
|
234
103
|
|
|
235
104
|
# ============================================================
|
|
236
|
-
# Merge Gem Tags
|
|
105
|
+
# Dynamic Merge Gem Tags
|
|
237
106
|
# ============================================================
|
|
107
|
+
# Tags are configured dynamically based on what's registered in MergeGemRegistry.
|
|
108
|
+
# Each merge gem registers itself, and exclusion filters are set up automatically.
|
|
238
109
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
config.filter_run_excluding(psych_merge: true) unless deps.psych_merge_available?
|
|
248
|
-
config.filter_run_excluding(rbs_merge: true) unless deps.rbs_merge_available?
|
|
249
|
-
config.filter_run_excluding(any_markdown_merge: true) unless deps.any_markdown_merge_available?
|
|
110
|
+
registry.registered_gems.each do |tag|
|
|
111
|
+
# Positive tag: run when gem IS available
|
|
112
|
+
config.filter_run_excluding(tag => true) unless registry.available?(tag)
|
|
113
|
+
|
|
114
|
+
# Negated tag: run when gem is NOT available
|
|
115
|
+
negated_tag = :"not_#{tag}"
|
|
116
|
+
config.filter_run_excluding(negated_tag => true) if registry.available?(tag)
|
|
117
|
+
end
|
|
250
118
|
|
|
251
119
|
# ============================================================
|
|
252
|
-
#
|
|
120
|
+
# Composite Tags
|
|
253
121
|
# ============================================================
|
|
254
122
|
|
|
255
|
-
config.filter_run_excluding(
|
|
256
|
-
config.filter_run_excluding(not_commonmarker_merge: true) if deps.commonmarker_merge_available?
|
|
257
|
-
config.filter_run_excluding(not_markdown_merge: true) if deps.markdown_merge_available?
|
|
258
|
-
config.filter_run_excluding(not_prism_merge: true) if deps.prism_merge_available?
|
|
259
|
-
config.filter_run_excluding(not_json_merge: true) if deps.json_merge_available?
|
|
260
|
-
config.filter_run_excluding(not_jsonc_merge: true) if deps.jsonc_merge_available?
|
|
261
|
-
config.filter_run_excluding(not_toml_merge: true) if deps.toml_merge_available?
|
|
262
|
-
config.filter_run_excluding(not_bash_merge: true) if deps.bash_merge_available?
|
|
263
|
-
config.filter_run_excluding(not_psych_merge: true) if deps.psych_merge_available?
|
|
264
|
-
config.filter_run_excluding(not_rbs_merge: true) if deps.rbs_merge_available?
|
|
123
|
+
config.filter_run_excluding(any_markdown_merge: true) unless deps.any_markdown_merge_available?
|
|
265
124
|
config.filter_run_excluding(not_any_markdown_merge: true) if deps.any_markdown_merge_available?
|
|
266
125
|
end
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ast
|
|
4
|
+
module Merge
|
|
5
|
+
module RSpec
|
|
6
|
+
# Registry for merge gem dependency tag availability checkers
|
|
7
|
+
#
|
|
8
|
+
# This module allows merge gems (like markly-merge, prism-merge, json-merge)
|
|
9
|
+
# to register their availability checker for RSpec dependency tags without
|
|
10
|
+
# ast-merge needing to know about them directly.
|
|
11
|
+
#
|
|
12
|
+
# == Purpose
|
|
13
|
+
#
|
|
14
|
+
# When running RSpec tests with dependency tags (e.g., `:markly_merge`),
|
|
15
|
+
# ast-merge needs to know if each merge gem is available. The MergeGemRegistry
|
|
16
|
+
# provides a way for gems to register their availability checkers, and also
|
|
17
|
+
# pre-configures known merge gems so they can be checked before being loaded.
|
|
18
|
+
#
|
|
19
|
+
# == Pre-configured Gems
|
|
20
|
+
#
|
|
21
|
+
# The following merge gems are pre-configured so that their availability can
|
|
22
|
+
# be checked before they are loaded (e.g., during RSpec setup):
|
|
23
|
+
# - :markly_merge, :commonmarker_merge, :markdown_merge (markdown)
|
|
24
|
+
# - :prism_merge, :bash_merge, :rbs_merge (code)
|
|
25
|
+
# - :json_merge, :jsonc_merge (data)
|
|
26
|
+
# - :toml_merge, :psych_merge, :dotenv_merge (config)
|
|
27
|
+
#
|
|
28
|
+
# External merge gems can also register themselves by calling {register}
|
|
29
|
+
# when loaded.
|
|
30
|
+
#
|
|
31
|
+
# == Registration
|
|
32
|
+
#
|
|
33
|
+
# Each merge gem registers itself when loaded using {register}:
|
|
34
|
+
# - Tag name (e.g., :markly_merge)
|
|
35
|
+
# - Require path (e.g., "markly/merge")
|
|
36
|
+
# - Merger class name (e.g., "Markly::Merge::SmartMerger")
|
|
37
|
+
# - Test source code to verify the merger works
|
|
38
|
+
# - Optional category for grouping (e.g., :markdown, :data, :code)
|
|
39
|
+
#
|
|
40
|
+
# When a tag is registered, an availability method is automatically defined
|
|
41
|
+
# on `Ast::Merge::RSpec::DependencyTags`.
|
|
42
|
+
#
|
|
43
|
+
# == Thread Safety
|
|
44
|
+
#
|
|
45
|
+
# All operations are thread-safe using a Mutex for synchronization.
|
|
46
|
+
# Results are cached after first check for performance.
|
|
47
|
+
#
|
|
48
|
+
# @example Registering a merge gem (in your gem's lib file)
|
|
49
|
+
# # In markly-merge/lib/markly/merge.rb
|
|
50
|
+
# if defined?(Ast::Merge::RSpec::MergeGemRegistry)
|
|
51
|
+
# Ast::Merge::RSpec::MergeGemRegistry.register(
|
|
52
|
+
# :markly_merge,
|
|
53
|
+
# require_path: "markly/merge",
|
|
54
|
+
# merger_class: "Markly::Merge::SmartMerger",
|
|
55
|
+
# test_source: "# Test\n\nParagraph",
|
|
56
|
+
# category: :markdown
|
|
57
|
+
# )
|
|
58
|
+
# end
|
|
59
|
+
#
|
|
60
|
+
# @example Checking availability
|
|
61
|
+
# Ast::Merge::RSpec::MergeGemRegistry.available?(:markly_merge) # => true/false
|
|
62
|
+
#
|
|
63
|
+
# @example Getting all registered gems
|
|
64
|
+
# Ast::Merge::RSpec::MergeGemRegistry.registered_gems # => [:markly_merge, :prism_merge, ...]
|
|
65
|
+
#
|
|
66
|
+
# @see Ast::Merge::RSpec::DependencyTags Uses MergeGemRegistry for dynamic gem detection
|
|
67
|
+
# @api public
|
|
68
|
+
module MergeGemRegistry
|
|
69
|
+
@mutex = Mutex.new
|
|
70
|
+
@registry = {} # rubocop:disable ThreadSafety/MutableClassInstanceVariable
|
|
71
|
+
@availability_cache = {} # rubocop:disable ThreadSafety/MutableClassInstanceVariable
|
|
72
|
+
|
|
73
|
+
# Valid categories for merge gems
|
|
74
|
+
CATEGORIES = %i[markdown data code config other].freeze
|
|
75
|
+
|
|
76
|
+
# Pre-configured known merge gems
|
|
77
|
+
# These can be checked before the gems are actually loaded
|
|
78
|
+
KNOWN_GEMS = {
|
|
79
|
+
# Markdown gems
|
|
80
|
+
markly_merge: {
|
|
81
|
+
require_path: "markly/merge",
|
|
82
|
+
merger_class: "Markly::Merge::SmartMerger",
|
|
83
|
+
test_source: "# Test\n\nParagraph",
|
|
84
|
+
category: :markdown,
|
|
85
|
+
skip_instantiation: false,
|
|
86
|
+
},
|
|
87
|
+
commonmarker_merge: {
|
|
88
|
+
require_path: "commonmarker/merge",
|
|
89
|
+
merger_class: "Commonmarker::Merge::SmartMerger",
|
|
90
|
+
test_source: "# Test\n\nParagraph",
|
|
91
|
+
category: :markdown,
|
|
92
|
+
skip_instantiation: false,
|
|
93
|
+
},
|
|
94
|
+
markdown_merge: {
|
|
95
|
+
require_path: "markdown/merge",
|
|
96
|
+
merger_class: "Markdown::Merge::SmartMerger",
|
|
97
|
+
test_source: "# Test\n\nParagraph",
|
|
98
|
+
category: :markdown,
|
|
99
|
+
skip_instantiation: true, # Requires backend
|
|
100
|
+
},
|
|
101
|
+
# Code gems
|
|
102
|
+
prism_merge: {
|
|
103
|
+
require_path: "prism/merge",
|
|
104
|
+
merger_class: "Prism::Merge::SmartMerger",
|
|
105
|
+
test_source: "def foo; end",
|
|
106
|
+
category: :code,
|
|
107
|
+
skip_instantiation: false,
|
|
108
|
+
},
|
|
109
|
+
bash_merge: {
|
|
110
|
+
require_path: "bash/merge",
|
|
111
|
+
merger_class: "Bash::Merge::SmartMerger",
|
|
112
|
+
test_source: "#!/bin/bash\necho hello",
|
|
113
|
+
category: :code,
|
|
114
|
+
skip_instantiation: false,
|
|
115
|
+
},
|
|
116
|
+
rbs_merge: {
|
|
117
|
+
require_path: "rbs/merge",
|
|
118
|
+
merger_class: "Rbs::Merge::SmartMerger",
|
|
119
|
+
test_source: "class Foo\nend",
|
|
120
|
+
category: :code,
|
|
121
|
+
skip_instantiation: false,
|
|
122
|
+
},
|
|
123
|
+
# Data gems
|
|
124
|
+
json_merge: {
|
|
125
|
+
require_path: "json/merge",
|
|
126
|
+
merger_class: "Json::Merge::SmartMerger",
|
|
127
|
+
test_source: '{"key": "value"}',
|
|
128
|
+
category: :data,
|
|
129
|
+
skip_instantiation: false,
|
|
130
|
+
},
|
|
131
|
+
jsonc_merge: {
|
|
132
|
+
require_path: "jsonc/merge",
|
|
133
|
+
merger_class: "Jsonc::Merge::SmartMerger",
|
|
134
|
+
test_source: "// comment\n{\"key\": \"value\"}",
|
|
135
|
+
category: :data,
|
|
136
|
+
skip_instantiation: false,
|
|
137
|
+
},
|
|
138
|
+
# Config gems
|
|
139
|
+
toml_merge: {
|
|
140
|
+
require_path: "toml/merge",
|
|
141
|
+
merger_class: "Toml::Merge::SmartMerger",
|
|
142
|
+
test_source: "[section]\nkey = \"value\"",
|
|
143
|
+
category: :config,
|
|
144
|
+
skip_instantiation: false,
|
|
145
|
+
},
|
|
146
|
+
psych_merge: {
|
|
147
|
+
require_path: "psych/merge",
|
|
148
|
+
merger_class: "Psych::Merge::SmartMerger",
|
|
149
|
+
test_source: "key: value",
|
|
150
|
+
category: :config,
|
|
151
|
+
skip_instantiation: false,
|
|
152
|
+
},
|
|
153
|
+
dotenv_merge: {
|
|
154
|
+
require_path: "dotenv/merge",
|
|
155
|
+
merger_class: "Dotenv::Merge::SmartMerger",
|
|
156
|
+
test_source: "KEY=value",
|
|
157
|
+
category: :config,
|
|
158
|
+
skip_instantiation: false,
|
|
159
|
+
},
|
|
160
|
+
}.freeze
|
|
161
|
+
|
|
162
|
+
module_function
|
|
163
|
+
|
|
164
|
+
# Register a merge gem for dependency tag support
|
|
165
|
+
#
|
|
166
|
+
# When a gem is registered, this also dynamically defines a `*_available?` method
|
|
167
|
+
# on `Ast::Merge::RSpec::DependencyTags` if it doesn't already exist.
|
|
168
|
+
#
|
|
169
|
+
# @param tag_name [Symbol] the RSpec tag name (e.g., :markly_merge)
|
|
170
|
+
# @param require_path [String] the require path for the gem (e.g., "markly/merge")
|
|
171
|
+
# @param merger_class [String] the full class name of the SmartMerger
|
|
172
|
+
# @param test_source [String] sample source code to test merging
|
|
173
|
+
# @param category [Symbol] category for grouping (:markdown, :data, :code, :config, :other)
|
|
174
|
+
# @param skip_instantiation [Boolean] if true, only check class exists (for gems requiring backends)
|
|
175
|
+
# @return [void]
|
|
176
|
+
#
|
|
177
|
+
# @example Register a merge gem
|
|
178
|
+
# Ast::Merge::RSpec::MergeGemRegistry.register(
|
|
179
|
+
# :markly_merge,
|
|
180
|
+
# require_path: "markly/merge",
|
|
181
|
+
# merger_class: "Markly::Merge::SmartMerger",
|
|
182
|
+
# test_source: "# Test\n\nParagraph",
|
|
183
|
+
# category: :markdown
|
|
184
|
+
# )
|
|
185
|
+
def register(tag_name, require_path:, merger_class:, test_source:, category: :other, skip_instantiation: false)
|
|
186
|
+
raise ArgumentError, "Invalid category: #{category}" unless CATEGORIES.include?(category)
|
|
187
|
+
|
|
188
|
+
tag_sym = tag_name.to_sym
|
|
189
|
+
|
|
190
|
+
@mutex.synchronize do
|
|
191
|
+
@registry[tag_sym] = {
|
|
192
|
+
require_path: require_path,
|
|
193
|
+
merger_class: merger_class,
|
|
194
|
+
test_source: test_source,
|
|
195
|
+
category: category,
|
|
196
|
+
skip_instantiation: skip_instantiation,
|
|
197
|
+
}
|
|
198
|
+
# Clear cache when re-registering
|
|
199
|
+
@availability_cache.delete(tag_sym)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Define availability method on DependencyTags
|
|
203
|
+
define_availability_method(tag_sym)
|
|
204
|
+
|
|
205
|
+
nil
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Check if a merge gem is available and functional
|
|
209
|
+
#
|
|
210
|
+
# This method will try to load the gem if it's not yet registered but
|
|
211
|
+
# is known (in KNOWN_GEMS). This allows availability checking before
|
|
212
|
+
# the gem is explicitly loaded.
|
|
213
|
+
#
|
|
214
|
+
# @param tag_name [Symbol] the tag name to check
|
|
215
|
+
# @return [Boolean] true if the merge gem is available and works
|
|
216
|
+
def available?(tag_name)
|
|
217
|
+
tag_sym = tag_name.to_sym
|
|
218
|
+
|
|
219
|
+
# Check cache first
|
|
220
|
+
@mutex.synchronize do
|
|
221
|
+
return @availability_cache[tag_sym] if @availability_cache.key?(tag_sym)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Get registration info (from registry or known gems)
|
|
225
|
+
info = @mutex.synchronize { @registry[tag_sym] }
|
|
226
|
+
info ||= KNOWN_GEMS[tag_sym]
|
|
227
|
+
|
|
228
|
+
return false unless info
|
|
229
|
+
|
|
230
|
+
# Check if gem works
|
|
231
|
+
result = gem_works?(
|
|
232
|
+
info[:require_path],
|
|
233
|
+
info[:merger_class],
|
|
234
|
+
info[:test_source],
|
|
235
|
+
info[:skip_instantiation],
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
# Cache result
|
|
239
|
+
@mutex.synchronize do
|
|
240
|
+
@availability_cache[tag_sym] = result
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
result
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Check if a tag is registered
|
|
247
|
+
#
|
|
248
|
+
# @param tag_name [Symbol] the tag name
|
|
249
|
+
# @return [Boolean] true if the tag is registered
|
|
250
|
+
def registered?(tag_name)
|
|
251
|
+
@mutex.synchronize do
|
|
252
|
+
@registry.key?(tag_name.to_sym)
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Get all registered gem tag names (including pre-configured known gems)
|
|
257
|
+
#
|
|
258
|
+
# @return [Array<Symbol>] list of registered tag names
|
|
259
|
+
def registered_gems
|
|
260
|
+
@mutex.synchronize do
|
|
261
|
+
(KNOWN_GEMS.keys + @registry.keys).uniq
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# Get gems filtered by category
|
|
266
|
+
#
|
|
267
|
+
# @param category [Symbol] one of :markdown, :data, :code, :config, :other
|
|
268
|
+
# @return [Array<Symbol>] list of tag names in that category
|
|
269
|
+
def gems_by_category(category)
|
|
270
|
+
@mutex.synchronize do
|
|
271
|
+
known = KNOWN_GEMS.select { |_, info| info[:category] == category }.keys
|
|
272
|
+
registered = @registry.select { |_, info| info[:category] == category }.keys
|
|
273
|
+
(known + registered).uniq
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# Get registration info for a gem
|
|
278
|
+
#
|
|
279
|
+
# @param tag_name [Symbol] the tag name
|
|
280
|
+
# @return [Hash, nil] registration info or nil if not registered/known
|
|
281
|
+
def info(tag_name)
|
|
282
|
+
tag_sym = tag_name.to_sym
|
|
283
|
+
@mutex.synchronize do
|
|
284
|
+
@registry[tag_sym]&.dup || KNOWN_GEMS[tag_sym]&.dup
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# Get a summary of all registered gems and their availability
|
|
289
|
+
#
|
|
290
|
+
# @return [Hash{Symbol => Boolean}] map of tag name to availability
|
|
291
|
+
def summary
|
|
292
|
+
registered_gems.each_with_object({}) do |tag, result|
|
|
293
|
+
result[tag] = available?(tag)
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# Clear the availability cache
|
|
298
|
+
#
|
|
299
|
+
# @return [void]
|
|
300
|
+
def clear_cache!
|
|
301
|
+
@mutex.synchronize do
|
|
302
|
+
@availability_cache.clear
|
|
303
|
+
end
|
|
304
|
+
nil
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# Clear all registrations and cache
|
|
308
|
+
#
|
|
309
|
+
# @return [void]
|
|
310
|
+
def clear!
|
|
311
|
+
@mutex.synchronize do
|
|
312
|
+
@registry.clear
|
|
313
|
+
@availability_cache.clear
|
|
314
|
+
end
|
|
315
|
+
nil
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Reset memoized availability on DependencyTags
|
|
319
|
+
#
|
|
320
|
+
# @return [void]
|
|
321
|
+
def reset_availability!
|
|
322
|
+
clear_cache!
|
|
323
|
+
return unless defined?(DependencyTags)
|
|
324
|
+
|
|
325
|
+
registered_gems.each do |tag|
|
|
326
|
+
ivar = :"@#{tag}_available"
|
|
327
|
+
DependencyTags.remove_instance_variable(ivar) if DependencyTags.instance_variable_defined?(ivar)
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# ============================================================
|
|
332
|
+
# Private Helpers
|
|
333
|
+
# ============================================================
|
|
334
|
+
|
|
335
|
+
# Check if a merge gem is available and functional
|
|
336
|
+
#
|
|
337
|
+
# @param require_path [String] the require path for the gem
|
|
338
|
+
# @param merger_class [String] the full class name of the SmartMerger
|
|
339
|
+
# @param test_source [String] sample source code to test merging
|
|
340
|
+
# @param skip_instantiation [Boolean] if true, only check class exists
|
|
341
|
+
# @return [Boolean] true if the merger can be loaded/instantiated
|
|
342
|
+
# @api private
|
|
343
|
+
def gem_works?(require_path, merger_class, test_source, skip_instantiation)
|
|
344
|
+
require require_path
|
|
345
|
+
klass = Object.const_get(merger_class)
|
|
346
|
+
|
|
347
|
+
if skip_instantiation
|
|
348
|
+
# Just check that the class exists and looks like a SmartMerger
|
|
349
|
+
klass.is_a?(Class) && klass.ancestors.any? { |a| a.name&.include?("SmartMergerBase") }
|
|
350
|
+
else
|
|
351
|
+
klass.new(test_source, test_source)
|
|
352
|
+
true
|
|
353
|
+
end
|
|
354
|
+
rescue LoadError, StandardError
|
|
355
|
+
false
|
|
356
|
+
end
|
|
357
|
+
private_class_method :gem_works?
|
|
358
|
+
|
|
359
|
+
# Dynamically define an availability method on DependencyTags
|
|
360
|
+
#
|
|
361
|
+
# @param tag_name [Symbol] the tag name (e.g., :markly_merge)
|
|
362
|
+
# @return [void]
|
|
363
|
+
# @api private
|
|
364
|
+
def define_availability_method(tag_name)
|
|
365
|
+
method_name = :"#{tag_name}_available?"
|
|
366
|
+
|
|
367
|
+
# Only define if DependencyTags is loaded
|
|
368
|
+
return unless defined?(DependencyTags)
|
|
369
|
+
|
|
370
|
+
# Don't override existing methods
|
|
371
|
+
return if DependencyTags.respond_to?(method_name)
|
|
372
|
+
|
|
373
|
+
# Define the method dynamically - MergeGemRegistry.available? handles caching
|
|
374
|
+
DependencyTags.define_singleton_method(method_name) do
|
|
375
|
+
MergeGemRegistry.available?(tag_name)
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
private_class_method :define_availability_method
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
end
|
data/lib/ast/merge/version.rb
CHANGED
data/lib/ast/merge.rb
CHANGED
data.tar.gz.sig
CHANGED
|
Binary file
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ast-merge
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 4.0.
|
|
4
|
+
version: 4.0.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Peter H. Boling
|
|
@@ -66,7 +66,7 @@ dependencies:
|
|
|
66
66
|
version: '5.0'
|
|
67
67
|
- - ">="
|
|
68
68
|
- !ruby/object:Gem::Version
|
|
69
|
-
version: 5.0.
|
|
69
|
+
version: 5.0.1
|
|
70
70
|
type: :runtime
|
|
71
71
|
prerelease: false
|
|
72
72
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -76,7 +76,7 @@ dependencies:
|
|
|
76
76
|
version: '5.0'
|
|
77
77
|
- - ">="
|
|
78
78
|
- !ruby/object:Gem::Version
|
|
79
|
-
version: 5.0.
|
|
79
|
+
version: 5.0.1
|
|
80
80
|
- !ruby/object:Gem::Dependency
|
|
81
81
|
name: kettle-dev
|
|
82
82
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -355,6 +355,7 @@ files:
|
|
|
355
355
|
- lib/ast/merge/recipe/script_loader.rb
|
|
356
356
|
- lib/ast/merge/rspec.rb
|
|
357
357
|
- lib/ast/merge/rspec/dependency_tags.rb
|
|
358
|
+
- lib/ast/merge/rspec/merge_gem_registry.rb
|
|
358
359
|
- lib/ast/merge/rspec/shared_examples.rb
|
|
359
360
|
- lib/ast/merge/rspec/shared_examples/conflict_resolver_base.rb
|
|
360
361
|
- lib/ast/merge/rspec/shared_examples/debug_logger.rb
|
|
@@ -381,10 +382,10 @@ licenses:
|
|
|
381
382
|
- MIT
|
|
382
383
|
metadata:
|
|
383
384
|
homepage_uri: https://ast-merge.galtzo.com/
|
|
384
|
-
source_code_uri: https://github.com/kettle-rb/ast-merge/tree/v4.0.
|
|
385
|
-
changelog_uri: https://github.com/kettle-rb/ast-merge/blob/v4.0.
|
|
385
|
+
source_code_uri: https://github.com/kettle-rb/ast-merge/tree/v4.0.2
|
|
386
|
+
changelog_uri: https://github.com/kettle-rb/ast-merge/blob/v4.0.2/CHANGELOG.md
|
|
386
387
|
bug_tracker_uri: https://github.com/kettle-rb/ast-merge/issues
|
|
387
|
-
documentation_uri: https://www.rubydoc.info/gems/ast-merge/4.0.
|
|
388
|
+
documentation_uri: https://www.rubydoc.info/gems/ast-merge/4.0.2
|
|
388
389
|
funding_uri: https://github.com/sponsors/pboling
|
|
389
390
|
wiki_uri: https://github.com/kettle-rb/ast-merge/wiki
|
|
390
391
|
news_uri: https://www.railsbling.com/tags/ast-merge
|
metadata.gz.sig
CHANGED
|
Binary file
|