ast-merge 2.0.5 → 2.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e16be7b53f49d2a1a96a2f47cd21963f3c257e316c9af6479355b2e16f502028
4
- data.tar.gz: d07924b47b8f20f8cb6a5b73c3616db6bc1bbef6be52e7927e1be580bc22f40e
3
+ metadata.gz: ac53c3bf60949d4ce02f751351d3f4be08368945140d94dd8c090b700c92a0dc
4
+ data.tar.gz: 1eed56c786b9f3b9d86183dabe7aecaad8c989a68987abeb4133a12f20aea6ac
5
5
  SHA512:
6
- metadata.gz: 5e1dc52b4951bf79dbbc4e4806e00fd322cf88effd65324939770219c66bf204ad63d932f9be2c6777e693e08881bb93aee9ccda579de21d7f9c01a974115513
7
- data.tar.gz: 0622e0110fb955d46a567f23831bd4aa823fc550229c31828514da51c2269730d0bdd59e808fbaa5f80eb134cdd0ebd4d1698da2eeaf7f62eb8b8a38dc8a1986
6
+ metadata.gz: d693729fe0371f440fd8223c7896d1cbfc267229a384aa36484d891a3b5b9e59c872646a721f168e696382d23f7fa54d4dd80073615e45dfb30e3e065b9a85d5
7
+ data.tar.gz: 487bc8bf2ad2c2031c37f0fccbce0aeffc06aa21d2e0d962b74a71586b99d60011177abc92fc1fddc6b0675cf4e6a1894d60743c23d6e3dcf7746d42f9fd9153
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -30,6 +30,54 @@ Please file a bug if you notice a violation of semantic versioning.
30
30
 
31
31
  ### Security
32
32
 
33
+ ## [2.0.7] - 2026-01-01
34
+
35
+ - TAG: [v2.0.7][2.0.7t]
36
+ - COVERAGE: 97.31% -- 2569/2640 lines in 47 files
37
+ - BRANCH COVERAGE: 89.87% -- 869/967 branches in 47 files
38
+ - 98.84% documented
39
+
40
+ ### Added
41
+
42
+ - `Ast::Merge::NodeTyping::Normalizer` module for thread-safe backend type normalization
43
+ - Provides shared infrastructure for format-specific normalizers (toml-merge, markdown-merge)
44
+ - Thread-safe backend registration via mutex-protected operations
45
+ - `configure_normalizer` for initial backend mappings configuration
46
+ - `register_backend` for runtime registration of new backends
47
+ - `canonical_type` for mapping backend-specific types to canonical types
48
+ - `wrap` for wrapping nodes with canonical merge_type
49
+ - `registered_backends`, `backend_registered?`, `mappings_for`, `canonical_types` query methods
50
+ - `Ast::Merge::NodeTyping::FrozenWrapper` class for frozen AST nodes
51
+ - Includes `Freezable` behavior for freeze marker support
52
+ - `frozen_node?`, `slice`, `signature` methods
53
+ - Split `NodeTyping` module into separate files following autoload pattern:
54
+ - `ast/merge/node_typing/wrapper.rb`
55
+ - `ast/merge/node_typing/frozen_wrapper.rb`
56
+ - `ast/merge/node_typing/normalizer.rb`
57
+ - Comprehensive specs for `NodeTyping::Normalizer` including thread-safety tests
58
+ - RBS type signatures for `NodeTyping::Normalizer` and `NodeTyping::FrozenWrapper`
59
+
60
+ ## [2.0.6] - 2026-01-01
61
+
62
+ - TAG: [v2.0.6][2.0.6t]
63
+ - COVERAGE: 97.19% -- 2522/2595 lines in 44 files
64
+ - BRANCH COVERAGE: 89.91% -- 864/961 branches in 44 files
65
+ - 98.82% documented
66
+
67
+ ### Added
68
+
69
+ - Comprehensive mocked tests for `Ast::Merge::Recipe::Runner` (47 new tests):
70
+ - Tests for `#run` method with section found, changed, and unchanged scenarios
71
+ - Tests for `#run` with section not found (skipped vs appended)
72
+ - Tests for actual file writes in non-dry_run mode
73
+ - Tests for exception handling during merge
74
+ - Tests for `#summary` with all status counts (updated, would_update, unchanged, skipped, errors)
75
+ - Tests for `#results_by_status` grouping
76
+ - Tests for `#results_table` formatting (file, status, changed, message)
77
+ - Tests for `#summary_table` in both dry_run and non-dry_run modes
78
+ - Tests for `#make_relative` edge cases (base_dir, recipe base, unknown paths)
79
+ - Tests for `#make_relative` without recipe_path
80
+
33
81
  ## [2.0.5] - 2025-12-31
34
82
 
35
83
  - TAG: [v2.0.5][2.0.5t]
@@ -359,7 +407,11 @@ Please file a bug if you notice a violation of semantic versioning.
359
407
 
360
408
  - Initial release
361
409
 
362
- [Unreleased]: https://github.com/kettle-rb/ast-merge/compare/v2.0.5...HEAD
410
+ [Unreleased]: https://github.com/kettle-rb/ast-merge/compare/v2.0.7...HEAD
411
+ [2.0.7]: https://github.com/kettle-rb/ast-merge/compare/v2.0.6...v2.0.7
412
+ [2.0.7t]: https://github.com/kettle-rb/ast-merge/releases/tag/v2.0.7
413
+ [2.0.6]: https://github.com/kettle-rb/ast-merge/compare/v2.0.5...v2.0.6
414
+ [2.0.6t]: https://github.com/kettle-rb/ast-merge/releases/tag/v2.0.6
363
415
  [2.0.5]: https://github.com/kettle-rb/ast-merge/compare/v2.0.4...v2.0.5
364
416
  [2.0.5t]: https://github.com/kettle-rb/ast-merge/releases/tag/v2.0.5
365
417
  [2.0.4]: https://github.com/kettle-rb/ast-merge/compare/v2.0.3...v2.0.4
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.595-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
1035
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-2.640-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,195 @@
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
+ def self.extended(base)
61
+ base.instance_variable_set(:@normalizer_mutex, Mutex.new)
62
+ base.instance_variable_set(:@backend_mappings, {})
63
+ end
64
+
65
+ # Configure the normalizer with initial backend mappings.
66
+ #
67
+ # This should be called once when defining the format-specific normalizer,
68
+ # providing the default backend mappings. Additional backends can be
69
+ # registered later via `register_backend`.
70
+ #
71
+ # @param mappings [Hash{Symbol => Hash{Symbol => Symbol}}] Initial backend mappings
72
+ # Keys are backend identifiers, values are hashes mapping backend types to canonical types
73
+ # @return [void]
74
+ #
75
+ # @example
76
+ # configure_normalizer(
77
+ # tree_sitter_toml: { table_array_element: :array_of_tables }.freeze,
78
+ # citrus_toml: { table_array_element: :array_of_tables }.freeze
79
+ # )
80
+ def configure_normalizer(**mappings)
81
+ @normalizer_mutex.synchronize do
82
+ mappings.each do |backend, type_mappings|
83
+ @backend_mappings[backend.to_sym] = type_mappings.frozen? ? type_mappings : type_mappings.freeze
84
+ end
85
+ end
86
+ nil
87
+ end
88
+
89
+ # Register type mappings for a new backend.
90
+ #
91
+ # This allows extending the normalizer to support additional parsers
92
+ # beyond those configured initially. Thread-safe for runtime registration.
93
+ #
94
+ # @param backend [Symbol] Backend identifier (e.g., :my_parser)
95
+ # @param mappings [Hash{Symbol => Symbol}] Backend type → canonical type mappings
96
+ # @return [void]
97
+ #
98
+ # @example
99
+ # NodeTypeNormalizer.register_backend(:my_parser, {
100
+ # my_table: :table,
101
+ # my_pair: :pair,
102
+ # })
103
+ def register_backend(backend, mappings)
104
+ @normalizer_mutex.synchronize do
105
+ @backend_mappings[backend.to_sym] = mappings.frozen? ? mappings : mappings.freeze
106
+ end
107
+ nil
108
+ end
109
+
110
+ # Get the canonical type for a backend-specific type.
111
+ #
112
+ # If no mapping exists, returns the original type unchanged (passthrough).
113
+ # This allows backend-specific types to pass through for backend-specific
114
+ # merge rules.
115
+ #
116
+ # @param backend_type [Symbol, String, nil] The backend's node type
117
+ # @param backend [Symbol] The backend identifier
118
+ # @return [Symbol, nil] Canonical type (or original if no mapping), nil if input was nil
119
+ #
120
+ # @example
121
+ # NodeTypeNormalizer.canonical_type(:table_array_element, :tree_sitter_toml)
122
+ # # => :array_of_tables
123
+ #
124
+ # NodeTypeNormalizer.canonical_type(:unknown_type, :tree_sitter_toml)
125
+ # # => :unknown_type (passthrough)
126
+ def canonical_type(backend_type, backend = nil)
127
+ return backend_type if backend_type.nil?
128
+
129
+ type_sym = backend_type.to_sym
130
+ @normalizer_mutex.synchronize do
131
+ @backend_mappings.dig(backend, type_sym) || type_sym
132
+ end
133
+ end
134
+
135
+ # Wrap a node with its canonical type as merge_type.
136
+ #
137
+ # Uses Ast::Merge::NodeTyping.with_merge_type to create a wrapper
138
+ # that delegates all methods to the underlying node while adding
139
+ # a canonical merge_type attribute.
140
+ #
141
+ # @param node [Object] The backend node to wrap (must respond to #type)
142
+ # @param backend [Symbol] The backend identifier
143
+ # @return [Ast::Merge::NodeTyping::Wrapper] Wrapped node with canonical merge_type
144
+ #
145
+ # @example
146
+ # wrapped = NodeTypeNormalizer.wrap(node, :tree_sitter_toml)
147
+ # wrapped.type # => :table_array_element (original)
148
+ # wrapped.merge_type # => :array_of_tables (canonical)
149
+ # wrapped.unwrap # => node (original node)
150
+ def wrap(node, backend)
151
+ canonical = canonical_type(node.type, backend)
152
+ Ast::Merge::NodeTyping.with_merge_type(node, canonical)
153
+ end
154
+
155
+ # Get all registered backends.
156
+ #
157
+ # @return [Array<Symbol>] Backend identifiers
158
+ def registered_backends
159
+ @normalizer_mutex.synchronize do
160
+ @backend_mappings.keys
161
+ end
162
+ end
163
+
164
+ # Check if a backend is registered.
165
+ #
166
+ # @param backend [Symbol] Backend identifier
167
+ # @return [Boolean]
168
+ def backend_registered?(backend)
169
+ @normalizer_mutex.synchronize do
170
+ @backend_mappings.key?(backend.to_sym)
171
+ end
172
+ end
173
+
174
+ # Get the mappings for a specific backend.
175
+ #
176
+ # @param backend [Symbol] Backend identifier
177
+ # @return [Hash{Symbol => Symbol}, nil] The mappings or nil if not registered
178
+ def mappings_for(backend)
179
+ @normalizer_mutex.synchronize do
180
+ @backend_mappings[backend.to_sym]
181
+ end
182
+ end
183
+
184
+ # Get all canonical types across all backends.
185
+ #
186
+ # @return [Array<Symbol>] Unique canonical type symbols
187
+ def canonical_types
188
+ @normalizer_mutex.synchronize do
189
+ @backend_mappings.values.flat_map(&:values).uniq
190
+ end
191
+ end
192
+ end
193
+ end
194
+ end
195
+ 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
- # Node wrapper that adds a merge_type attribute to an existing node.
50
- # This uses a simple delegation pattern to preserve all original node
51
- # behavior while adding the merge_type.
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.
@@ -5,7 +5,7 @@ module Ast
5
5
  # Version information for Ast::Merge
6
6
  module Version
7
7
  # Current version of the ast-merge gem
8
- VERSION = "2.0.5"
8
+ VERSION = "2.0.7"
9
9
  end
10
10
  VERSION = Version::VERSION # traditional location
11
11
  end
data/sig/ast/merge.rbs CHANGED
@@ -574,9 +574,59 @@ 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
+ def configure_normalizer: (**Hash[Symbol, Symbol] mappings) -> void
599
+
600
+ # Register type mappings for a new backend (thread-safe)
601
+ def register_backend: (Symbol backend, Hash[Symbol, Symbol] mappings) -> void
602
+
603
+ # Get the canonical type for a backend-specific type
604
+ def canonical_type: (Symbol | String | nil backend_type, ?Symbol? backend) -> Symbol?
605
+
606
+ # Wrap a node with its canonical type as merge_type
607
+ def wrap: (untyped node, Symbol backend) -> Wrapper
608
+
609
+ # Get all registered backends
610
+ def registered_backends: () -> Array[Symbol]
611
+
612
+ # Check if a backend is registered
613
+ def backend_registered?: (Symbol | String backend) -> bool
614
+
615
+ # Get the mappings for a specific backend
616
+ def mappings_for: (Symbol backend) -> Hash[Symbol, Symbol]?
617
+
618
+ # Get all canonical types across all backends
619
+ def canonical_types: () -> Array[Symbol]
620
+ end
621
+
577
622
  def self.with_merge_type: (untyped node, Symbol merge_type) -> Wrapper
578
- def self.validate!: (node_typing_hash? node_typing) -> void
579
- def self.apply: (untyped node, node_typing_hash? node_typing) -> untyped
623
+ def self.frozen: (untyped node, ?Symbol merge_type) -> FrozenWrapper
624
+ def self.frozen_node?: (untyped node) -> bool
625
+ def self.typed_node?: (untyped node) -> bool
626
+ def self.merge_type_for: (untyped node) -> Symbol?
627
+ def self.unwrap: (untyped node) -> untyped
628
+ def self.process: (untyped node, node_typing_hash? typing_config) -> untyped?
629
+ def self.validate!: (node_typing_hash? typing_config) -> void
580
630
  end
581
631
  end
582
632
  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.5
4
+ version: 2.0.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter H. Boling
@@ -66,7 +66,7 @@ dependencies:
66
66
  version: '3.2'
67
67
  - - ">="
68
68
  - !ruby/object:Gem::Version
69
- version: 3.2.1
69
+ version: 3.2.2
70
70
  type: :runtime
71
71
  prerelease: false
72
72
  version_requirements: !ruby/object:Gem::Requirement
@@ -76,7 +76,7 @@ dependencies:
76
76
  version: '3.2'
77
77
  - - ">="
78
78
  - !ruby/object:Gem::Version
79
- version: 3.2.1
79
+ version: 3.2.2
80
80
  - !ruby/object:Gem::Dependency
81
81
  name: kettle-dev
82
82
  requirement: !ruby/object:Gem::Requirement
@@ -318,6 +318,9 @@ 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
321
324
  - lib/ast/merge/partial_template_merger.rb
322
325
  - lib/ast/merge/recipe.rb
323
326
  - lib/ast/merge/recipe/config.rb
@@ -352,10 +355,10 @@ licenses:
352
355
  - MIT
353
356
  metadata:
354
357
  homepage_uri: https://ast-merge.galtzo.com/
355
- source_code_uri: https://github.com/kettle-rb/ast-merge/tree/v2.0.5
356
- changelog_uri: https://github.com/kettle-rb/ast-merge/blob/v2.0.5/CHANGELOG.md
358
+ source_code_uri: https://github.com/kettle-rb/ast-merge/tree/v2.0.7
359
+ changelog_uri: https://github.com/kettle-rb/ast-merge/blob/v2.0.7/CHANGELOG.md
357
360
  bug_tracker_uri: https://github.com/kettle-rb/ast-merge/issues
358
- documentation_uri: https://www.rubydoc.info/gems/ast-merge/2.0.5
361
+ documentation_uri: https://www.rubydoc.info/gems/ast-merge/2.0.7
359
362
  funding_uri: https://github.com/sponsors/pboling
360
363
  wiki_uri: https://github.com/kettle-rb/ast-merge/wiki
361
364
  news_uri: https://www.railsbling.com/tags/ast-merge
metadata.gz.sig CHANGED
Binary file