ast-merge 2.0.6 → 2.0.8
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 +54 -1
- data/README.md +1 -1
- data/lib/ast/merge/node_typing/frozen_wrapper.rb +99 -0
- data/lib/ast/merge/node_typing/normalizer.rb +197 -0
- data/lib/ast/merge/node_typing/wrapper.rb +77 -0
- data/lib/ast/merge/node_typing.rb +3 -161
- data/lib/ast/merge/node_wrapper_base.rb +267 -0
- data/lib/ast/merge/version.rb +1 -1
- data/lib/ast/merge.rb +1 -0
- data/sig/ast/merge.rbs +53 -2
- data.tar.gz.sig +0 -0
- metadata +8 -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: ca82f3d67dd5ed8339d2ebad3032875db7c22bb5446747c7f6cf9a03080a1da4
|
|
4
|
+
data.tar.gz: c523f74b6be2660bd2c41c63d1d3afd895d4e849d5b1858813b252000e71093f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a41a7321e1fafe4882c59b77b16822eabc70f2d255ed6de92ef373bbbbc98a2855895c22175a0ad72270b97a6de9ff8e8246f72110f98ddf7fecc8682799048a
|
|
7
|
+
data.tar.gz: 30bc1323ae070faf65765549aeb479cb2e4c6b060676997ca1a35f31cf45658dab6f1d3adb55f70bf43ac103748b34f0b87e41d8349372f6cea811e55547fc83
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/CHANGELOG.md
CHANGED
|
@@ -30,6 +30,55 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
30
30
|
|
|
31
31
|
### Security
|
|
32
32
|
|
|
33
|
+
## [2.0.8] - 2026-01-01
|
|
34
|
+
|
|
35
|
+
- TAG: [v2.0.8][2.0.8t]
|
|
36
|
+
- COVERAGE: 97.09% -- 2636/2715 lines in 48 files
|
|
37
|
+
- BRANCH COVERAGE: 89.73% -- 882/983 branches in 48 files
|
|
38
|
+
- 98.71% documented
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
- `Ast::Merge::NodeWrapperBase` abstract base class for format-specific node wrappers
|
|
43
|
+
- Provides common functionality shared by `*::Merge::NodeWrapper` classes across gems
|
|
44
|
+
- Handles source context (lines, source string), line info, comments, content extraction
|
|
45
|
+
- Defines abstract `#compute_signature` that subclasses must implement
|
|
46
|
+
- Includes `#node_wrapper?` to distinguish from `NodeTyping::Wrapper`
|
|
47
|
+
- Includes `#underlying_node` to access the raw TreeHaver node (NOT `#unwrap` to avoid
|
|
48
|
+
conflict with `NodeTyping::Wrapper#unwrap` semantics in `FileAnalyzable`)
|
|
49
|
+
- Documents relationship between `NodeWrapperBase` and `NodeTyping::Wrapper`:
|
|
50
|
+
- `NodeWrapperBase`: Format-specific functionality (line info, signatures, type predicates)
|
|
51
|
+
- `NodeTyping::Wrapper`: Custom merge classification (`merge_type` attribute)
|
|
52
|
+
- Nodes can be double-wrapped: `NodeTyping::Wrapper(Format::Merge::NodeWrapper(tree_sitter_node))`
|
|
53
|
+
- Accepts `**options` in initialize for subclass-specific parameters (e.g., `backend`, `document_root`)
|
|
54
|
+
|
|
55
|
+
## [2.0.7] - 2026-01-01
|
|
56
|
+
|
|
57
|
+
- TAG: [v2.0.7][2.0.7t]
|
|
58
|
+
- COVERAGE: 97.31% -- 2569/2640 lines in 47 files
|
|
59
|
+
- BRANCH COVERAGE: 89.87% -- 869/967 branches in 47 files
|
|
60
|
+
- 98.84% documented
|
|
61
|
+
|
|
62
|
+
### Added
|
|
63
|
+
|
|
64
|
+
- `Ast::Merge::NodeTyping::Normalizer` module for thread-safe backend type normalization
|
|
65
|
+
- Provides shared infrastructure for format-specific normalizers (toml-merge, markdown-merge)
|
|
66
|
+
- Thread-safe backend registration via mutex-protected operations
|
|
67
|
+
- `configure_normalizer` for initial backend mappings configuration
|
|
68
|
+
- `register_backend` for runtime registration of new backends
|
|
69
|
+
- `canonical_type` for mapping backend-specific types to canonical types
|
|
70
|
+
- `wrap` for wrapping nodes with canonical merge_type
|
|
71
|
+
- `registered_backends`, `backend_registered?`, `mappings_for`, `canonical_types` query methods
|
|
72
|
+
- `Ast::Merge::NodeTyping::FrozenWrapper` class for frozen AST nodes
|
|
73
|
+
- Includes `Freezable` behavior for freeze marker support
|
|
74
|
+
- `frozen_node?`, `slice`, `signature` methods
|
|
75
|
+
- Split `NodeTyping` module into separate files following autoload pattern:
|
|
76
|
+
- `ast/merge/node_typing/wrapper.rb`
|
|
77
|
+
- `ast/merge/node_typing/frozen_wrapper.rb`
|
|
78
|
+
- `ast/merge/node_typing/normalizer.rb`
|
|
79
|
+
- Comprehensive specs for `NodeTyping::Normalizer` including thread-safety tests
|
|
80
|
+
- RBS type signatures for `NodeTyping::Normalizer` and `NodeTyping::FrozenWrapper`
|
|
81
|
+
|
|
33
82
|
## [2.0.6] - 2026-01-01
|
|
34
83
|
|
|
35
84
|
- TAG: [v2.0.6][2.0.6t]
|
|
@@ -380,7 +429,11 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
380
429
|
|
|
381
430
|
- Initial release
|
|
382
431
|
|
|
383
|
-
[Unreleased]: https://github.com/kettle-rb/ast-merge/compare/v2.0.
|
|
432
|
+
[Unreleased]: https://github.com/kettle-rb/ast-merge/compare/v2.0.8...HEAD
|
|
433
|
+
[2.0.8]: https://github.com/kettle-rb/ast-merge/compare/v2.0.7...v2.0.8
|
|
434
|
+
[2.0.8t]: https://github.com/kettle-rb/ast-merge/releases/tag/v2.0.8
|
|
435
|
+
[2.0.7]: https://github.com/kettle-rb/ast-merge/compare/v2.0.6...v2.0.7
|
|
436
|
+
[2.0.7t]: https://github.com/kettle-rb/ast-merge/releases/tag/v2.0.7
|
|
384
437
|
[2.0.6]: https://github.com/kettle-rb/ast-merge/compare/v2.0.5...v2.0.6
|
|
385
438
|
[2.0.6t]: https://github.com/kettle-rb/ast-merge/releases/tag/v2.0.6
|
|
386
439
|
[2.0.5]: https://github.com/kettle-rb/ast-merge/compare/v2.0.4...v2.0.5
|
data/README.md
CHANGED
|
@@ -1032,7 +1032,7 @@ Thanks for RTFM. ☺️
|
|
|
1032
1032
|
[📌gitmoji]: https://gitmoji.dev
|
|
1033
1033
|
[📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
|
|
1034
1034
|
[🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
|
|
1035
|
-
[🧮kloc-img]: https://img.shields.io/badge/KLOC-2.
|
|
1035
|
+
[🧮kloc-img]: https://img.shields.io/badge/KLOC-2.715-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
|
|
1036
1036
|
[🔐security]: SECURITY.md
|
|
1037
1037
|
[🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
|
|
1038
1038
|
[📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ast
|
|
4
|
+
module Merge
|
|
5
|
+
module NodeTyping
|
|
6
|
+
# Wrapper for frozen AST nodes that includes Freezable behavior.
|
|
7
|
+
#
|
|
8
|
+
# FrozenWrapper extends Wrapper to add freeze node semantics, making the
|
|
9
|
+
# wrapped node satisfy both the NodeTyping API and the Freezable API.
|
|
10
|
+
# This enables composition where frozen nodes are:
|
|
11
|
+
# - Wrapped AST nodes (can unwrap to get original)
|
|
12
|
+
# - Typed nodes (have merge_type)
|
|
13
|
+
# - Freeze nodes (satisfy is_a?(Freezable) and freeze_node?)
|
|
14
|
+
#
|
|
15
|
+
# ## Key Distinction from FreezeNodeBase
|
|
16
|
+
#
|
|
17
|
+
# FrozenWrapper and FreezeNodeBase both include Freezable, but they represent
|
|
18
|
+
# fundamentally different concepts:
|
|
19
|
+
#
|
|
20
|
+
# ### FrozenWrapper (this class)
|
|
21
|
+
# - Wraps an AST node that has a freeze marker in its leading comments
|
|
22
|
+
# - The node is still a structural AST node (e.g., a `gem` call in a gemspec)
|
|
23
|
+
# - During matching, we want to match by the underlying node's IDENTITY
|
|
24
|
+
# (e.g., the gem name), NOT by the full content
|
|
25
|
+
# - Signature generation should unwrap and use the underlying node's structure
|
|
26
|
+
# - Example: `# token:freeze\ngem "example_gem", "~> 1.0"` wraps a CallNode
|
|
27
|
+
#
|
|
28
|
+
# ### FreezeNodeBase
|
|
29
|
+
# - Represents an explicit freeze block with `# token:freeze ... # token:unfreeze`
|
|
30
|
+
# - The entire block is opaque content that should be preserved verbatim
|
|
31
|
+
# - During matching, we match by the full CONTENT of the block
|
|
32
|
+
# - Signature generation uses freeze_signature (content-based)
|
|
33
|
+
# - Example: A multi-line comment block with custom formatting
|
|
34
|
+
#
|
|
35
|
+
# ## Signature Generation Behavior
|
|
36
|
+
#
|
|
37
|
+
# When FileAnalyzable#generate_signature encounters a FrozenWrapper:
|
|
38
|
+
# 1. It unwraps to get the underlying AST node
|
|
39
|
+
# 2. Passes the unwrapped node to the signature_generator
|
|
40
|
+
# 3. This allows the signature generator to recognize the node type
|
|
41
|
+
# (e.g., Prism::CallNode) and generate appropriate signatures
|
|
42
|
+
#
|
|
43
|
+
# This is critical because signature generators check for specific AST types.
|
|
44
|
+
# If we passed the wrapper, the generator wouldn't recognize it as a CallNode
|
|
45
|
+
# and would fall back to a generic signature, breaking matching.
|
|
46
|
+
#
|
|
47
|
+
# @example Creating a frozen wrapper
|
|
48
|
+
# frozen = NodeTyping::FrozenWrapper.new(prism_node, :frozen)
|
|
49
|
+
# frozen.freeze_node? # => true
|
|
50
|
+
# frozen.is_a?(Ast::Merge::Freezable) # => true
|
|
51
|
+
# frozen.unwrap # => prism_node
|
|
52
|
+
#
|
|
53
|
+
# @see Wrapper
|
|
54
|
+
# @see Ast::Merge::Freezable
|
|
55
|
+
# @see FreezeNodeBase
|
|
56
|
+
# @see FileAnalyzable#generate_signature
|
|
57
|
+
class FrozenWrapper < Wrapper
|
|
58
|
+
include Ast::Merge::Freezable
|
|
59
|
+
|
|
60
|
+
# Create a frozen wrapper for an AST node.
|
|
61
|
+
#
|
|
62
|
+
# @param node [Object] The AST node to wrap
|
|
63
|
+
# @param merge_type [Symbol] The merge type (defaults to :frozen)
|
|
64
|
+
def initialize(node, merge_type = :frozen)
|
|
65
|
+
super(node, merge_type)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Returns true to indicate this is a frozen node.
|
|
69
|
+
# Overrides both Wrapper#typed_node? context and provides freeze_node? from Freezable.
|
|
70
|
+
#
|
|
71
|
+
# @return [Boolean] true
|
|
72
|
+
def frozen_node?
|
|
73
|
+
true
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Returns the content of this frozen node.
|
|
77
|
+
# Delegates to the wrapped node's slice method.
|
|
78
|
+
#
|
|
79
|
+
# @return [String] The node content
|
|
80
|
+
def slice
|
|
81
|
+
@node.slice
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Returns the signature for this frozen node.
|
|
85
|
+
# Uses the freeze_signature from Freezable module.
|
|
86
|
+
#
|
|
87
|
+
# @return [Array] Signature in the form [:FreezeNode, content]
|
|
88
|
+
def signature
|
|
89
|
+
freeze_signature
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Forward inspect to show frozen status.
|
|
93
|
+
def inspect
|
|
94
|
+
"#<NodeTyping::FrozenWrapper merge_type=#{@merge_type.inspect} node=#{@node.inspect}>"
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ast
|
|
4
|
+
module Merge
|
|
5
|
+
module NodeTyping
|
|
6
|
+
# Thread-safe backend registration and type normalization for AST merge libraries.
|
|
7
|
+
#
|
|
8
|
+
# Normalizer provides a shared, thread-safe mechanism for registering backend-specific
|
|
9
|
+
# node type mappings and normalizing them to canonical types. This enables portable
|
|
10
|
+
# merge rules across different parsers/backends for the same file format.
|
|
11
|
+
#
|
|
12
|
+
# ## Thread Safety
|
|
13
|
+
#
|
|
14
|
+
# All registration and lookup operations are protected by a mutex to ensure
|
|
15
|
+
# thread-safe access to the backend mappings. This follows the same pattern
|
|
16
|
+
# used in TreeHaver::LanguageRegistry and TreeHaver::PathValidator.
|
|
17
|
+
#
|
|
18
|
+
# ## Usage Pattern
|
|
19
|
+
#
|
|
20
|
+
# File-format-specific merge libraries (e.g., toml-merge, markdown-merge) should:
|
|
21
|
+
# 1. Create their own NodeTypeNormalizer module
|
|
22
|
+
# 2. Include or extend Ast::Merge::NodeTyping::Normalizer
|
|
23
|
+
# 3. Define their canonical types and default backend mappings
|
|
24
|
+
# 4. Call `configure_normalizer` with their initial mappings
|
|
25
|
+
#
|
|
26
|
+
# @example Creating a format-specific normalizer
|
|
27
|
+
# module Toml
|
|
28
|
+
# module Merge
|
|
29
|
+
# module NodeTypeNormalizer
|
|
30
|
+
# extend Ast::Merge::NodeTyping::Normalizer
|
|
31
|
+
#
|
|
32
|
+
# configure_normalizer(
|
|
33
|
+
# tree_sitter_toml: {
|
|
34
|
+
# table_array_element: :array_of_tables,
|
|
35
|
+
# pair: :pair,
|
|
36
|
+
# # ...
|
|
37
|
+
# }.freeze,
|
|
38
|
+
# citrus_toml: {
|
|
39
|
+
# table_array_element: :array_of_tables,
|
|
40
|
+
# pair: :pair,
|
|
41
|
+
# # ...
|
|
42
|
+
# }.freeze
|
|
43
|
+
# )
|
|
44
|
+
#
|
|
45
|
+
# # Optional: Add format-specific helper methods
|
|
46
|
+
# def self.table_type?(type)
|
|
47
|
+
# %i[table array_of_tables].include?(type.to_sym)
|
|
48
|
+
# end
|
|
49
|
+
# end
|
|
50
|
+
# end
|
|
51
|
+
# end
|
|
52
|
+
#
|
|
53
|
+
# @see TreeHaver::LanguageRegistry
|
|
54
|
+
# @see TreeHaver::PathValidator
|
|
55
|
+
module Normalizer
|
|
56
|
+
# Called when this module is extended into another module.
|
|
57
|
+
# Sets up the mutex and backend mappings storage.
|
|
58
|
+
#
|
|
59
|
+
# @param base [Module] The module extending this one
|
|
60
|
+
class << self
|
|
61
|
+
def extended(base)
|
|
62
|
+
base.instance_variable_set(:@normalizer_mutex, Mutex.new)
|
|
63
|
+
base.instance_variable_set(:@backend_mappings, {})
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Configure the normalizer with initial backend mappings.
|
|
68
|
+
#
|
|
69
|
+
# This should be called once when defining the format-specific normalizer,
|
|
70
|
+
# providing the default backend mappings. Additional backends can be
|
|
71
|
+
# registered later via `register_backend`.
|
|
72
|
+
#
|
|
73
|
+
# @param mappings [Hash{Symbol => Hash{Symbol => Symbol}}] Initial backend mappings
|
|
74
|
+
# Keys are backend identifiers, values are hashes mapping backend types to canonical types
|
|
75
|
+
# @return [void]
|
|
76
|
+
#
|
|
77
|
+
# @example
|
|
78
|
+
# configure_normalizer(
|
|
79
|
+
# tree_sitter_toml: { table_array_element: :array_of_tables }.freeze,
|
|
80
|
+
# citrus_toml: { table_array_element: :array_of_tables }.freeze
|
|
81
|
+
# )
|
|
82
|
+
def configure_normalizer(**mappings)
|
|
83
|
+
@normalizer_mutex.synchronize do
|
|
84
|
+
mappings.each do |backend, type_mappings|
|
|
85
|
+
@backend_mappings[backend.to_sym] = type_mappings.frozen? ? type_mappings : type_mappings.freeze
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
nil
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Register type mappings for a new backend.
|
|
92
|
+
#
|
|
93
|
+
# This allows extending the normalizer to support additional parsers
|
|
94
|
+
# beyond those configured initially. Thread-safe for runtime registration.
|
|
95
|
+
#
|
|
96
|
+
# @param backend [Symbol] Backend identifier (e.g., :my_parser)
|
|
97
|
+
# @param mappings [Hash{Symbol => Symbol}] Backend type → canonical type mappings
|
|
98
|
+
# @return [void]
|
|
99
|
+
#
|
|
100
|
+
# @example
|
|
101
|
+
# NodeTypeNormalizer.register_backend(:my_parser, {
|
|
102
|
+
# my_table: :table,
|
|
103
|
+
# my_pair: :pair,
|
|
104
|
+
# })
|
|
105
|
+
def register_backend(backend, mappings)
|
|
106
|
+
@normalizer_mutex.synchronize do
|
|
107
|
+
@backend_mappings[backend.to_sym] = mappings.frozen? ? mappings : mappings.freeze
|
|
108
|
+
end
|
|
109
|
+
nil
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Get the canonical type for a backend-specific type.
|
|
113
|
+
#
|
|
114
|
+
# If no mapping exists, returns the original type unchanged (passthrough).
|
|
115
|
+
# This allows backend-specific types to pass through for backend-specific
|
|
116
|
+
# merge rules.
|
|
117
|
+
#
|
|
118
|
+
# @param backend_type [Symbol, String, nil] The backend's node type
|
|
119
|
+
# @param backend [Symbol] The backend identifier
|
|
120
|
+
# @return [Symbol, nil] Canonical type (or original if no mapping), nil if input was nil
|
|
121
|
+
#
|
|
122
|
+
# @example
|
|
123
|
+
# NodeTypeNormalizer.canonical_type(:table_array_element, :tree_sitter_toml)
|
|
124
|
+
# # => :array_of_tables
|
|
125
|
+
#
|
|
126
|
+
# NodeTypeNormalizer.canonical_type(:unknown_type, :tree_sitter_toml)
|
|
127
|
+
# # => :unknown_type (passthrough)
|
|
128
|
+
def canonical_type(backend_type, backend = nil)
|
|
129
|
+
return backend_type if backend_type.nil?
|
|
130
|
+
|
|
131
|
+
type_sym = backend_type.to_sym
|
|
132
|
+
@normalizer_mutex.synchronize do
|
|
133
|
+
@backend_mappings.dig(backend, type_sym) || type_sym
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Wrap a node with its canonical type as merge_type.
|
|
138
|
+
#
|
|
139
|
+
# Uses Ast::Merge::NodeTyping.with_merge_type to create a wrapper
|
|
140
|
+
# that delegates all methods to the underlying node while adding
|
|
141
|
+
# a canonical merge_type attribute.
|
|
142
|
+
#
|
|
143
|
+
# @param node [Object] The backend node to wrap (must respond to #type)
|
|
144
|
+
# @param backend [Symbol] The backend identifier
|
|
145
|
+
# @return [Ast::Merge::NodeTyping::Wrapper] Wrapped node with canonical merge_type
|
|
146
|
+
#
|
|
147
|
+
# @example
|
|
148
|
+
# wrapped = NodeTypeNormalizer.wrap(node, :tree_sitter_toml)
|
|
149
|
+
# wrapped.type # => :table_array_element (original)
|
|
150
|
+
# wrapped.merge_type # => :array_of_tables (canonical)
|
|
151
|
+
# wrapped.unwrap # => node (original node)
|
|
152
|
+
def wrap(node, backend)
|
|
153
|
+
canonical = canonical_type(node.type, backend)
|
|
154
|
+
Ast::Merge::NodeTyping.with_merge_type(node, canonical)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Get all registered backends.
|
|
158
|
+
#
|
|
159
|
+
# @return [Array<Symbol>] Backend identifiers
|
|
160
|
+
def registered_backends
|
|
161
|
+
@normalizer_mutex.synchronize do
|
|
162
|
+
@backend_mappings.keys
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Check if a backend is registered.
|
|
167
|
+
#
|
|
168
|
+
# @param backend [Symbol] Backend identifier
|
|
169
|
+
# @return [Boolean]
|
|
170
|
+
def backend_registered?(backend)
|
|
171
|
+
@normalizer_mutex.synchronize do
|
|
172
|
+
@backend_mappings.key?(backend.to_sym)
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Get the mappings for a specific backend.
|
|
177
|
+
#
|
|
178
|
+
# @param backend [Symbol] Backend identifier
|
|
179
|
+
# @return [Hash{Symbol => Symbol}, nil] The mappings or nil if not registered
|
|
180
|
+
def mappings_for(backend)
|
|
181
|
+
@normalizer_mutex.synchronize do
|
|
182
|
+
@backend_mappings[backend.to_sym]
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Get all canonical types across all backends.
|
|
187
|
+
#
|
|
188
|
+
# @return [Array<Symbol>] Unique canonical type symbols
|
|
189
|
+
def canonical_types
|
|
190
|
+
@normalizer_mutex.synchronize do
|
|
191
|
+
@backend_mappings.values.flat_map(&:values).uniq
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ast
|
|
4
|
+
module Merge
|
|
5
|
+
module NodeTyping
|
|
6
|
+
# Node wrapper that adds a merge_type attribute to an existing node.
|
|
7
|
+
# This uses a simple delegation pattern to preserve all original node
|
|
8
|
+
# behavior while adding the merge_type.
|
|
9
|
+
class Wrapper
|
|
10
|
+
# @return [Object] The original node being wrapped
|
|
11
|
+
attr_reader :node
|
|
12
|
+
|
|
13
|
+
# @return [Symbol] The custom merge type for this node
|
|
14
|
+
attr_reader :merge_type
|
|
15
|
+
|
|
16
|
+
# Create a new node type wrapper.
|
|
17
|
+
#
|
|
18
|
+
# @param node [Object] The original node to wrap
|
|
19
|
+
# @param merge_type [Symbol] The custom merge type
|
|
20
|
+
def initialize(node, merge_type)
|
|
21
|
+
@node = node
|
|
22
|
+
@merge_type = merge_type
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Delegate all unknown methods to the wrapped node.
|
|
26
|
+
# This allows the wrapper to be used transparently in place of the node.
|
|
27
|
+
def method_missing(method, *args, &block)
|
|
28
|
+
if @node.respond_to?(method)
|
|
29
|
+
@node.send(method, *args, &block)
|
|
30
|
+
else
|
|
31
|
+
super
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Check if the wrapped node responds to a method.
|
|
36
|
+
def respond_to_missing?(method, include_private = false)
|
|
37
|
+
@node.respond_to?(method, include_private) || super
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Returns true to indicate this is a node type wrapper.
|
|
41
|
+
def typed_node?
|
|
42
|
+
true
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Unwrap to get the original node.
|
|
46
|
+
# @return [Object] The original unwrapped node
|
|
47
|
+
def unwrap
|
|
48
|
+
@node
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Forward equality check to the wrapped node.
|
|
52
|
+
def ==(other)
|
|
53
|
+
if other.is_a?(Wrapper)
|
|
54
|
+
@node == other.node && @merge_type == other.merge_type
|
|
55
|
+
else
|
|
56
|
+
@node == other
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Forward hash to the wrapped node.
|
|
61
|
+
def hash
|
|
62
|
+
[@node, @merge_type].hash
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Forward eql? to the wrapped node.
|
|
66
|
+
def eql?(other)
|
|
67
|
+
self == other
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Forward inspect to show both the type and node.
|
|
71
|
+
def inspect
|
|
72
|
+
"#<NodeTyping::Wrapper merge_type=#{@merge_type.inspect} node=#{@node.inspect}>"
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -46,167 +46,9 @@ module Ast
|
|
|
46
46
|
# @see MergerConfig
|
|
47
47
|
# @see ConflictResolverBase
|
|
48
48
|
module NodeTyping
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
class Wrapper
|
|
53
|
-
# @return [Object] The original node being wrapped
|
|
54
|
-
attr_reader :node
|
|
55
|
-
|
|
56
|
-
# @return [Symbol] The custom merge type for this node
|
|
57
|
-
attr_reader :merge_type
|
|
58
|
-
|
|
59
|
-
# Create a new node type wrapper.
|
|
60
|
-
#
|
|
61
|
-
# @param node [Object] The original node to wrap
|
|
62
|
-
# @param merge_type [Symbol] The custom merge type
|
|
63
|
-
def initialize(node, merge_type)
|
|
64
|
-
@node = node
|
|
65
|
-
@merge_type = merge_type
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
# Delegate all unknown methods to the wrapped node.
|
|
69
|
-
# This allows the wrapper to be used transparently in place of the node.
|
|
70
|
-
def method_missing(method, *args, &block)
|
|
71
|
-
if @node.respond_to?(method)
|
|
72
|
-
@node.send(method, *args, &block)
|
|
73
|
-
else
|
|
74
|
-
super
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
# Check if the wrapped node responds to a method.
|
|
79
|
-
def respond_to_missing?(method, include_private = false)
|
|
80
|
-
@node.respond_to?(method, include_private) || super
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
# Returns true to indicate this is a node type wrapper.
|
|
84
|
-
def typed_node?
|
|
85
|
-
true
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
# Unwrap to get the original node.
|
|
89
|
-
# @return [Object] The original unwrapped node
|
|
90
|
-
def unwrap
|
|
91
|
-
@node
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
# Forward equality check to the wrapped node.
|
|
95
|
-
def ==(other)
|
|
96
|
-
if other.is_a?(Wrapper)
|
|
97
|
-
@node == other.node && @merge_type == other.merge_type
|
|
98
|
-
else
|
|
99
|
-
@node == other
|
|
100
|
-
end
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
# Forward hash to the wrapped node.
|
|
104
|
-
def hash
|
|
105
|
-
[@node, @merge_type].hash
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
# Forward eql? to the wrapped node.
|
|
109
|
-
def eql?(other)
|
|
110
|
-
self == other
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
# Forward inspect to show both the type and node.
|
|
114
|
-
def inspect
|
|
115
|
-
"#<NodeTyping::Wrapper merge_type=#{@merge_type.inspect} node=#{@node.inspect}>"
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
# Wrapper for frozen AST nodes that includes Freezable behavior.
|
|
120
|
-
#
|
|
121
|
-
# FrozenWrapper extends Wrapper to add freeze node semantics, making the
|
|
122
|
-
# wrapped node satisfy both the NodeTyping API and the Freezable API.
|
|
123
|
-
# This enables composition where frozen nodes are:
|
|
124
|
-
# - Wrapped AST nodes (can unwrap to get original)
|
|
125
|
-
# - Typed nodes (have merge_type)
|
|
126
|
-
# - Freeze nodes (satisfy is_a?(Freezable) and freeze_node?)
|
|
127
|
-
#
|
|
128
|
-
# ## Key Distinction from FreezeNodeBase
|
|
129
|
-
#
|
|
130
|
-
# FrozenWrapper and FreezeNodeBase both include Freezable, but they represent
|
|
131
|
-
# fundamentally different concepts:
|
|
132
|
-
#
|
|
133
|
-
# ### FrozenWrapper (this class)
|
|
134
|
-
# - Wraps an AST node that has a freeze marker in its leading comments
|
|
135
|
-
# - The node is still a structural AST node (e.g., a `gem` call in a gemspec)
|
|
136
|
-
# - During matching, we want to match by the underlying node's IDENTITY
|
|
137
|
-
# (e.g., the gem name), NOT by the full content
|
|
138
|
-
# - Signature generation should unwrap and use the underlying node's structure
|
|
139
|
-
# - Example: `# token:freeze\ngem "example_gem", "~> 1.0"` wraps a CallNode
|
|
140
|
-
#
|
|
141
|
-
# ### FreezeNodeBase
|
|
142
|
-
# - Represents an explicit freeze block with `# token:freeze ... # token:unfreeze`
|
|
143
|
-
# - The entire block is opaque content that should be preserved verbatim
|
|
144
|
-
# - During matching, we match by the full CONTENT of the block
|
|
145
|
-
# - Signature generation uses freeze_signature (content-based)
|
|
146
|
-
# - Example: A multi-line comment block with custom formatting
|
|
147
|
-
#
|
|
148
|
-
# ## Signature Generation Behavior
|
|
149
|
-
#
|
|
150
|
-
# When FileAnalyzable#generate_signature encounters a FrozenWrapper:
|
|
151
|
-
# 1. It unwraps to get the underlying AST node
|
|
152
|
-
# 2. Passes the unwrapped node to the signature_generator
|
|
153
|
-
# 3. This allows the signature generator to recognize the node type
|
|
154
|
-
# (e.g., Prism::CallNode) and generate appropriate signatures
|
|
155
|
-
#
|
|
156
|
-
# This is critical because signature generators check for specific AST types.
|
|
157
|
-
# If we passed the wrapper, the generator wouldn't recognize it as a CallNode
|
|
158
|
-
# and would fall back to a generic signature, breaking matching.
|
|
159
|
-
#
|
|
160
|
-
# @example Creating a frozen wrapper
|
|
161
|
-
# frozen = NodeTyping::FrozenWrapper.new(prism_node, :frozen)
|
|
162
|
-
# frozen.freeze_node? # => true
|
|
163
|
-
# frozen.is_a?(Ast::Merge::Freezable) # => true
|
|
164
|
-
# frozen.unwrap # => prism_node
|
|
165
|
-
#
|
|
166
|
-
# @see Wrapper
|
|
167
|
-
# @see Ast::Merge::Freezable
|
|
168
|
-
# @see FreezeNodeBase
|
|
169
|
-
# @see FileAnalyzable#generate_signature
|
|
170
|
-
class FrozenWrapper < Wrapper
|
|
171
|
-
include Ast::Merge::Freezable
|
|
172
|
-
|
|
173
|
-
# Create a frozen wrapper for an AST node.
|
|
174
|
-
#
|
|
175
|
-
# @param node [Object] The AST node to wrap
|
|
176
|
-
# @param merge_type [Symbol] The merge type (defaults to :frozen)
|
|
177
|
-
def initialize(node, merge_type = :frozen)
|
|
178
|
-
super(node, merge_type)
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
# Returns true to indicate this is a frozen node.
|
|
182
|
-
# Overrides both Wrapper#typed_node? context and provides freeze_node? from Freezable.
|
|
183
|
-
#
|
|
184
|
-
# @return [Boolean] true
|
|
185
|
-
def frozen_node?
|
|
186
|
-
true
|
|
187
|
-
end
|
|
188
|
-
|
|
189
|
-
# Returns the content of this frozen node.
|
|
190
|
-
# Delegates to the wrapped node's slice method.
|
|
191
|
-
#
|
|
192
|
-
# @return [String] The node content
|
|
193
|
-
def slice
|
|
194
|
-
@node.slice
|
|
195
|
-
end
|
|
196
|
-
|
|
197
|
-
# Returns the signature for this frozen node.
|
|
198
|
-
# Uses the freeze_signature from Freezable module.
|
|
199
|
-
#
|
|
200
|
-
# @return [Array] Signature in the form [:FreezeNode, content]
|
|
201
|
-
def signature
|
|
202
|
-
freeze_signature
|
|
203
|
-
end
|
|
204
|
-
|
|
205
|
-
# Forward inspect to show frozen status.
|
|
206
|
-
def inspect
|
|
207
|
-
"#<NodeTyping::FrozenWrapper merge_type=#{@merge_type.inspect} node=#{@node.inspect}>"
|
|
208
|
-
end
|
|
209
|
-
end
|
|
49
|
+
autoload :FrozenWrapper, "ast/merge/node_typing/frozen_wrapper"
|
|
50
|
+
autoload :Normalizer, "ast/merge/node_typing/normalizer"
|
|
51
|
+
autoload :Wrapper, "ast/merge/node_typing/wrapper"
|
|
210
52
|
|
|
211
53
|
class << self
|
|
212
54
|
# Wrap a node with a custom merge_type.
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ast
|
|
4
|
+
module Merge
|
|
5
|
+
# Base class for format-specific node wrappers used in *-merge gems.
|
|
6
|
+
#
|
|
7
|
+
# This provides common functionality for wrapping TreeHaver nodes with:
|
|
8
|
+
# - Source context (lines, source string)
|
|
9
|
+
# - Line information (start_line, end_line)
|
|
10
|
+
# - Comment associations (leading_comments, inline_comment)
|
|
11
|
+
# - Content extraction (text, content)
|
|
12
|
+
# - Signature generation (abstract)
|
|
13
|
+
#
|
|
14
|
+
# ## Relationship to NodeTyping::Wrapper
|
|
15
|
+
#
|
|
16
|
+
# This class is DIFFERENT from `Ast::Merge::NodeTyping::Wrapper`:
|
|
17
|
+
#
|
|
18
|
+
# - **NodeWrapperBase**: Provides format-specific functionality (line info,
|
|
19
|
+
# signatures, comments, type predicates). Used to wrap raw TreeHaver nodes
|
|
20
|
+
# with rich context needed for merging.
|
|
21
|
+
#
|
|
22
|
+
# - **NodeTyping::Wrapper**: Adds a custom `merge_type` attribute for merge
|
|
23
|
+
# classification. Used by SmartMergerBase to apply custom typing rules.
|
|
24
|
+
#
|
|
25
|
+
# A node CAN be wrapped by both:
|
|
26
|
+
# ```
|
|
27
|
+
# NodeTyping::Wrapper(Toml::Merge::NodeWrapper(tree_sitter_node))
|
|
28
|
+
# ```
|
|
29
|
+
#
|
|
30
|
+
# The `NodeTyping.unwrap` method handles unwrapping `NodeTyping::Wrapper`,
|
|
31
|
+
# while `NodeWrapperBase#node` provides access to the underlying TreeHaver node.
|
|
32
|
+
#
|
|
33
|
+
# ## Subclass Responsibilities
|
|
34
|
+
#
|
|
35
|
+
# Subclasses MUST implement:
|
|
36
|
+
# - `#compute_signature(node)` - Generate a signature for node matching
|
|
37
|
+
#
|
|
38
|
+
# Subclasses SHOULD implement format-specific type predicates:
|
|
39
|
+
# - TOML: `#table?`, `#pair?`, `#array_of_tables?`, etc.
|
|
40
|
+
# - JSON: `#object?`, `#array?`, `#pair?`, etc.
|
|
41
|
+
# - Bash: `#function_definition?`, `#variable_assignment?`, etc.
|
|
42
|
+
#
|
|
43
|
+
# @example Creating a format-specific wrapper
|
|
44
|
+
# class NodeWrapper < Ast::Merge::NodeWrapperBase
|
|
45
|
+
# def table?
|
|
46
|
+
# type == :table
|
|
47
|
+
# end
|
|
48
|
+
#
|
|
49
|
+
# private
|
|
50
|
+
#
|
|
51
|
+
# def compute_signature(node)
|
|
52
|
+
# case node.type.to_sym
|
|
53
|
+
# when :table
|
|
54
|
+
# [:table, table_name]
|
|
55
|
+
# else
|
|
56
|
+
# [node.type.to_sym]
|
|
57
|
+
# end
|
|
58
|
+
# end
|
|
59
|
+
# end
|
|
60
|
+
#
|
|
61
|
+
# @abstract Subclass and implement `#compute_signature`
|
|
62
|
+
class NodeWrapperBase
|
|
63
|
+
# @return [Object] The wrapped TreeHaver node
|
|
64
|
+
attr_reader :node
|
|
65
|
+
|
|
66
|
+
# @return [Array<String>] Source lines for content extraction
|
|
67
|
+
attr_reader :lines
|
|
68
|
+
|
|
69
|
+
# @return [String] The original source string
|
|
70
|
+
attr_reader :source
|
|
71
|
+
|
|
72
|
+
# @return [Array<Hash>] Leading comments associated with this node
|
|
73
|
+
attr_reader :leading_comments
|
|
74
|
+
|
|
75
|
+
# @return [Hash, nil] Inline/trailing comment on the same line
|
|
76
|
+
attr_reader :inline_comment
|
|
77
|
+
|
|
78
|
+
# @return [Integer, nil] Start line (1-based)
|
|
79
|
+
attr_reader :start_line
|
|
80
|
+
|
|
81
|
+
# @return [Integer, nil] End line (1-based)
|
|
82
|
+
attr_reader :end_line
|
|
83
|
+
|
|
84
|
+
# Initialize the node wrapper with source context.
|
|
85
|
+
#
|
|
86
|
+
# @param node [Object] TreeHaver node to wrap
|
|
87
|
+
# @param lines [Array<String>] Source lines for content extraction
|
|
88
|
+
# @param source [String, nil] Original source string for byte-based text extraction
|
|
89
|
+
# @param leading_comments [Array<Hash>] Comments before this node
|
|
90
|
+
# @param inline_comment [Hash, nil] Inline comment on the node's line
|
|
91
|
+
# @param options [Hash] Additional options for subclasses (forward compatibility)
|
|
92
|
+
def initialize(node, lines:, source: nil, leading_comments: [], inline_comment: nil, **options)
|
|
93
|
+
@node = node
|
|
94
|
+
@lines = lines
|
|
95
|
+
@source = source || lines.join("\n")
|
|
96
|
+
@leading_comments = leading_comments
|
|
97
|
+
@inline_comment = inline_comment
|
|
98
|
+
|
|
99
|
+
# Store additional options for subclasses to use
|
|
100
|
+
process_additional_options(options)
|
|
101
|
+
|
|
102
|
+
# Extract line information from the node (0-indexed to 1-indexed)
|
|
103
|
+
extract_line_info(node)
|
|
104
|
+
|
|
105
|
+
# Handle edge case where end_line might be before start_line
|
|
106
|
+
@end_line = @start_line if @start_line && @end_line && @end_line < @start_line
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Process additional options. Override in subclasses to handle format-specific options.
|
|
110
|
+
# @param options [Hash] Additional options
|
|
111
|
+
def process_additional_options(options)
|
|
112
|
+
# Default: no-op. Subclasses can override to process options like :backend, :document_root
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Generate a signature for this node for matching purposes.
|
|
116
|
+
# Signatures are used to identify corresponding nodes between template and destination.
|
|
117
|
+
#
|
|
118
|
+
# @return [Array, nil] Signature array or nil if not signaturable
|
|
119
|
+
def signature
|
|
120
|
+
compute_signature(@node)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Get the node type as a symbol.
|
|
124
|
+
# @return [Symbol]
|
|
125
|
+
def type
|
|
126
|
+
@node.type.to_sym
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Check if this node has a specific type.
|
|
130
|
+
# @param type_name [Symbol, String] Type to check
|
|
131
|
+
# @return [Boolean]
|
|
132
|
+
def type?(type_name)
|
|
133
|
+
@node.type.to_s == type_name.to_s
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Check if this is a freeze node.
|
|
137
|
+
# Override in subclasses if freeze node detection differs.
|
|
138
|
+
# @return [Boolean]
|
|
139
|
+
def freeze_node?
|
|
140
|
+
false
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Get the text content for this node by extracting from source using byte positions.
|
|
144
|
+
# @return [String]
|
|
145
|
+
def text
|
|
146
|
+
node_text(@node)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Extract text from a node using byte positions.
|
|
150
|
+
# @param ts_node [Object] The TreeHaver node
|
|
151
|
+
# @return [String]
|
|
152
|
+
def node_text(ts_node)
|
|
153
|
+
return "" unless ts_node.respond_to?(:start_byte) && ts_node.respond_to?(:end_byte)
|
|
154
|
+
|
|
155
|
+
@source[ts_node.start_byte...ts_node.end_byte] || ""
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Get the content for this node from source lines.
|
|
159
|
+
# @return [String]
|
|
160
|
+
def content
|
|
161
|
+
return "" unless @start_line && @end_line
|
|
162
|
+
|
|
163
|
+
(@start_line..@end_line).map { |ln| @lines[ln - 1] }.compact.join("\n")
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Check if this node is a container (has children for merging).
|
|
167
|
+
# Override in subclasses to define container types.
|
|
168
|
+
# @return [Boolean]
|
|
169
|
+
def container?
|
|
170
|
+
false
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Check if this node is a leaf (no mergeable children).
|
|
174
|
+
# @return [Boolean]
|
|
175
|
+
def leaf?
|
|
176
|
+
!container?
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Get children wrapped as NodeWrappers.
|
|
180
|
+
# Override in subclasses to return wrapped children.
|
|
181
|
+
# @return [Array<NodeWrapperBase>]
|
|
182
|
+
def children
|
|
183
|
+
return [] unless @node.respond_to?(:each)
|
|
184
|
+
|
|
185
|
+
result = []
|
|
186
|
+
@node.each do |child|
|
|
187
|
+
result << wrap_child(child)
|
|
188
|
+
end
|
|
189
|
+
result
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Get mergeable children - the semantically meaningful children for tree merging.
|
|
193
|
+
# Override in subclasses to return format-specific mergeable children.
|
|
194
|
+
# @return [Array<NodeWrapperBase>]
|
|
195
|
+
def mergeable_children
|
|
196
|
+
children
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# String representation for debugging.
|
|
200
|
+
# @return [String]
|
|
201
|
+
def inspect
|
|
202
|
+
"#<#{self.class.name} type=#{@node.type} lines=#{@start_line}..#{@end_line}>"
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Returns true to indicate this is a node wrapper.
|
|
206
|
+
# Used to distinguish from NodeTyping::Wrapper.
|
|
207
|
+
# @return [Boolean]
|
|
208
|
+
def node_wrapper?
|
|
209
|
+
true
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Get the underlying TreeHaver node.
|
|
213
|
+
# Note: This is NOT the same as NodeTyping::Wrapper#unwrap which removes
|
|
214
|
+
# the typing wrapper. This method provides access to the raw parser node.
|
|
215
|
+
# @return [Object] The underlying TreeHaver node
|
|
216
|
+
def underlying_node
|
|
217
|
+
@node
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
protected
|
|
221
|
+
|
|
222
|
+
# Wrap a child node. Override in subclasses to use the specific wrapper class.
|
|
223
|
+
# @param child [Object] Child node to wrap
|
|
224
|
+
# @return [NodeWrapperBase]
|
|
225
|
+
def wrap_child(child)
|
|
226
|
+
self.class.new(child, lines: @lines, source: @source)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Compute signature for a node. Subclasses MUST implement this.
|
|
230
|
+
# @param node [Object] The node to compute signature for
|
|
231
|
+
# @return [Array, nil] Signature array
|
|
232
|
+
# @abstract
|
|
233
|
+
def compute_signature(node)
|
|
234
|
+
raise NotImplementedError, "#{self.class} must implement #compute_signature"
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
private
|
|
238
|
+
|
|
239
|
+
# Extract line information from the node.
|
|
240
|
+
# @param node [Object] The node to extract line info from
|
|
241
|
+
def extract_line_info(node)
|
|
242
|
+
if node.respond_to?(:start_point)
|
|
243
|
+
point = node.start_point
|
|
244
|
+
@start_line = extract_row(point) + 1
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
if node.respond_to?(:end_point)
|
|
248
|
+
point = node.end_point
|
|
249
|
+
@end_line = extract_row(point) + 1
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Extract row from a point, handling different point implementations.
|
|
254
|
+
# @param point [Object] The point object
|
|
255
|
+
# @return [Integer]
|
|
256
|
+
def extract_row(point)
|
|
257
|
+
if point.respond_to?(:row)
|
|
258
|
+
point.row
|
|
259
|
+
elsif point.is_a?(Hash)
|
|
260
|
+
point[:row]
|
|
261
|
+
else
|
|
262
|
+
0
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
end
|
data/lib/ast/merge/version.rb
CHANGED
data/lib/ast/merge.rb
CHANGED
|
@@ -153,6 +153,7 @@ module Ast
|
|
|
153
153
|
autoload :MergerConfig, "ast/merge/merger_config"
|
|
154
154
|
autoload :NavigableStatement, "ast/merge/navigable_statement"
|
|
155
155
|
autoload :NodeTyping, "ast/merge/node_typing"
|
|
156
|
+
autoload :NodeWrapperBase, "ast/merge/node_wrapper_base"
|
|
156
157
|
autoload :PartialTemplateMerger, "ast/merge/partial_template_merger"
|
|
157
158
|
autoload :SectionTyping, "ast/merge/section_typing"
|
|
158
159
|
autoload :SmartMergerBase, "ast/merge/smart_merger_base"
|
data/sig/ast/merge.rbs
CHANGED
|
@@ -574,9 +574,60 @@ module Ast
|
|
|
574
574
|
def inspect: () -> String
|
|
575
575
|
end
|
|
576
576
|
|
|
577
|
+
# Wrapper for frozen AST nodes that includes Freezable behavior
|
|
578
|
+
class FrozenWrapper < Wrapper
|
|
579
|
+
include Freezable
|
|
580
|
+
|
|
581
|
+
def initialize: (untyped node, ?Symbol merge_type) -> void
|
|
582
|
+
def frozen_node?: () -> bool
|
|
583
|
+
def slice: () -> String
|
|
584
|
+
def signature: () -> Array[untyped]
|
|
585
|
+
def inspect: () -> String
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
# Thread-safe backend registration and type normalization module.
|
|
589
|
+
# Extended by format-specific normalizers (e.g., Toml::Merge::NodeTypeNormalizer).
|
|
590
|
+
module Normalizer
|
|
591
|
+
@normalizer_mutex: Mutex
|
|
592
|
+
@backend_mappings: Hash[Symbol, Hash[Symbol, Symbol]]
|
|
593
|
+
|
|
594
|
+
# Called when this module is extended into another module
|
|
595
|
+
def self.extended: (Module base) -> void
|
|
596
|
+
|
|
597
|
+
# Configure initial backend mappings
|
|
598
|
+
# @param mappings Keyword args where keys are backend symbols and values are type mapping hashes
|
|
599
|
+
def configure_normalizer: (**untyped) -> void
|
|
600
|
+
|
|
601
|
+
# Register type mappings for a new backend (thread-safe)
|
|
602
|
+
def register_backend: (Symbol backend, Hash[Symbol, Symbol] mappings) -> void
|
|
603
|
+
|
|
604
|
+
# Get the canonical type for a backend-specific type
|
|
605
|
+
def canonical_type: (Symbol | String | nil backend_type, ?Symbol? backend) -> Symbol?
|
|
606
|
+
|
|
607
|
+
# Wrap a node with its canonical type as merge_type
|
|
608
|
+
def wrap: (untyped node, Symbol backend) -> Wrapper
|
|
609
|
+
|
|
610
|
+
# Get all registered backends
|
|
611
|
+
def registered_backends: () -> Array[Symbol]
|
|
612
|
+
|
|
613
|
+
# Check if a backend is registered
|
|
614
|
+
def backend_registered?: (Symbol | String backend) -> bool
|
|
615
|
+
|
|
616
|
+
# Get the mappings for a specific backend
|
|
617
|
+
def mappings_for: (Symbol backend) -> Hash[Symbol, Symbol]?
|
|
618
|
+
|
|
619
|
+
# Get all canonical types across all backends
|
|
620
|
+
def canonical_types: () -> Array[Symbol]
|
|
621
|
+
end
|
|
622
|
+
|
|
577
623
|
def self.with_merge_type: (untyped node, Symbol merge_type) -> Wrapper
|
|
578
|
-
def self.
|
|
579
|
-
def self.
|
|
624
|
+
def self.frozen: (untyped node, ?Symbol merge_type) -> FrozenWrapper
|
|
625
|
+
def self.frozen_node?: (untyped node) -> bool
|
|
626
|
+
def self.typed_node?: (untyped node) -> bool
|
|
627
|
+
def self.merge_type_for: (untyped node) -> Symbol?
|
|
628
|
+
def self.unwrap: (untyped node) -> untyped
|
|
629
|
+
def self.process: (untyped node, node_typing_hash? typing_config) -> untyped?
|
|
630
|
+
def self.validate!: (node_typing_hash? typing_config) -> void
|
|
580
631
|
end
|
|
581
632
|
end
|
|
582
633
|
end
|
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: 2.0.
|
|
4
|
+
version: 2.0.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Peter H. Boling
|
|
@@ -318,6 +318,10 @@ files:
|
|
|
318
318
|
- lib/ast/merge/merger_config.rb
|
|
319
319
|
- lib/ast/merge/navigable_statement.rb
|
|
320
320
|
- lib/ast/merge/node_typing.rb
|
|
321
|
+
- lib/ast/merge/node_typing/frozen_wrapper.rb
|
|
322
|
+
- lib/ast/merge/node_typing/normalizer.rb
|
|
323
|
+
- lib/ast/merge/node_typing/wrapper.rb
|
|
324
|
+
- lib/ast/merge/node_wrapper_base.rb
|
|
321
325
|
- lib/ast/merge/partial_template_merger.rb
|
|
322
326
|
- lib/ast/merge/recipe.rb
|
|
323
327
|
- lib/ast/merge/recipe/config.rb
|
|
@@ -352,10 +356,10 @@ licenses:
|
|
|
352
356
|
- MIT
|
|
353
357
|
metadata:
|
|
354
358
|
homepage_uri: https://ast-merge.galtzo.com/
|
|
355
|
-
source_code_uri: https://github.com/kettle-rb/ast-merge/tree/v2.0.
|
|
356
|
-
changelog_uri: https://github.com/kettle-rb/ast-merge/blob/v2.0.
|
|
359
|
+
source_code_uri: https://github.com/kettle-rb/ast-merge/tree/v2.0.8
|
|
360
|
+
changelog_uri: https://github.com/kettle-rb/ast-merge/blob/v2.0.8/CHANGELOG.md
|
|
357
361
|
bug_tracker_uri: https://github.com/kettle-rb/ast-merge/issues
|
|
358
|
-
documentation_uri: https://www.rubydoc.info/gems/ast-merge/2.0.
|
|
362
|
+
documentation_uri: https://www.rubydoc.info/gems/ast-merge/2.0.8
|
|
359
363
|
funding_uri: https://github.com/sponsors/pboling
|
|
360
364
|
wiki_uri: https://github.com/kettle-rb/ast-merge/wiki
|
|
361
365
|
news_uri: https://www.railsbling.com/tags/ast-merge
|
metadata.gz.sig
CHANGED
|
Binary file
|