rbs-merge 1.0.0

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.
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rbs
4
+ module Merge
5
+ # Wrapper to represent freeze blocks as first-class nodes in RBS files.
6
+ # A freeze block is a section marked with freeze/unfreeze comment markers that
7
+ # should be preserved from the destination during merges.
8
+ #
9
+ # Inherits from Ast::Merge::FreezeNodeBase for shared functionality including
10
+ # the Location struct, InvalidStructureError, and configurable marker patterns.
11
+ #
12
+ # Uses the `:hash_comment` pattern type by default for RBS type signature files.
13
+ #
14
+ # @example Freeze block in RBS
15
+ # # rbs-merge:freeze
16
+ # # Custom type definitions
17
+ # type custom_config = { key: String, value: untyped }
18
+ # # rbs-merge:unfreeze
19
+ #
20
+ # @example Freeze block with reason
21
+ # # rbs-merge:freeze Project-specific types
22
+ # type project_result = success | failure
23
+ # # rbs-merge:unfreeze
24
+ class FreezeNode < Ast::Merge::FreezeNodeBase
25
+ # Inherit InvalidStructureError from base class
26
+ InvalidStructureError = Ast::Merge::FreezeNodeBase::InvalidStructureError
27
+
28
+ # Inherit Location from base class
29
+ Location = Ast::Merge::FreezeNodeBase::Location
30
+
31
+ # @param start_line [Integer] Line number of freeze marker
32
+ # @param end_line [Integer] Line number of unfreeze marker
33
+ # @param analysis [FileAnalysis] The file analysis containing this block
34
+ # @param nodes [Array<RBS::AST::Declarations::Base, RBS::AST::Members::Base>] Nodes fully contained within the freeze block
35
+ # @param overlapping_nodes [Array] All nodes that overlap with freeze block (for validation)
36
+ # @param start_marker [String, nil] The freeze start marker text
37
+ # @param end_marker [String, nil] The freeze end marker text
38
+ # @param pattern_type [Symbol] Pattern type for marker matching (defaults to :hash_comment)
39
+ def initialize(start_line:, end_line:, analysis:, nodes: [], overlapping_nodes: nil, start_marker: nil, end_marker: nil, pattern_type: Ast::Merge::FreezeNodeBase::DEFAULT_PATTERN)
40
+ super(
41
+ start_line: start_line,
42
+ end_line: end_line,
43
+ analysis: analysis,
44
+ nodes: nodes,
45
+ overlapping_nodes: overlapping_nodes || nodes,
46
+ start_marker: start_marker,
47
+ end_marker: end_marker,
48
+ pattern_type: pattern_type
49
+ )
50
+
51
+ # Validate structure
52
+ validate_structure!
53
+ end
54
+
55
+ # Returns a stable signature for this freeze block
56
+ # Signature includes the normalized content to detect changes
57
+ # @return [Array] Signature array
58
+ def signature
59
+ normalized = (@start_line..@end_line).map do |ln|
60
+ @analysis.normalized_line(ln)
61
+ end.compact.join("\n")
62
+
63
+ [:FreezeNode, normalized]
64
+ end
65
+
66
+ # String representation for debugging
67
+ # @return [String]
68
+ def inspect
69
+ "#<Rbs::Merge::FreezeNode lines=#{@start_line}..#{@end_line} nodes=#{@nodes.length}>"
70
+ end
71
+
72
+ private
73
+
74
+ # Validate that the freeze block has proper structure:
75
+ # - All nodes must be either fully contained or fully outside
76
+ # - No partial overlaps allowed
77
+ def validate_structure!
78
+ unclosed = []
79
+
80
+ @overlapping_nodes.each do |node|
81
+ node_start = node.location.start_line
82
+ node_end = node.location.end_line
83
+
84
+ # Check if node is fully contained (valid)
85
+ fully_contained = node_start >= @start_line && node_end <= @end_line
86
+
87
+ # Check if node completely encompasses the freeze block
88
+ # This is valid for container nodes like classes/modules
89
+ encompasses = node_start < @start_line && node_end > @end_line
90
+
91
+ # Check if node is fully outside (valid)
92
+ fully_outside = node_end < @start_line || node_start > @end_line
93
+
94
+ # If none of the above, it's a partial overlap (invalid)
95
+ next if fully_contained || encompasses || fully_outside
96
+
97
+ unclosed << node
98
+ end
99
+
100
+ return if unclosed.empty?
101
+
102
+ node_names = unclosed.map do |n|
103
+ name = n.respond_to?(:name) ? n.name.to_s : n.class.name.split("::").last
104
+ "#{name} (lines #{n.location.start_line}-#{n.location.end_line})"
105
+ end.join(", ")
106
+
107
+ raise InvalidStructureError.new(
108
+ "Freeze block at lines #{@start_line}-#{@end_line} has partial overlap with: #{node_names}. " \
109
+ "Freeze blocks must fully contain declarations or be fully contained within them.",
110
+ start_line: @start_line,
111
+ end_line: @end_line,
112
+ unclosed_nodes: unclosed,
113
+ )
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rbs
4
+ module Merge
5
+ # Result container for RBS file merge operations.
6
+ # Inherits from Ast::Merge::MergeResultBase for shared functionality.
7
+ #
8
+ # Tracks merged content, decisions made during merge, and provides
9
+ # methods to reconstruct the final merged RBS file.
10
+ #
11
+ # @example Basic usage
12
+ # result = MergeResult.new(template_analysis, dest_analysis)
13
+ # result.add_from_template(0)
14
+ # result.add_from_destination(1)
15
+ # merged_content = result.to_s
16
+ #
17
+ # @see Ast::Merge::MergeResultBase
18
+ class MergeResult < Ast::Merge::MergeResultBase
19
+ # Decision indicating content was preserved from a freeze block
20
+ # @return [Symbol]
21
+ DECISION_FREEZE_BLOCK = :freeze_block
22
+
23
+ # Decision indicating content came from the template
24
+ # @return [Symbol]
25
+ DECISION_TEMPLATE = :template
26
+
27
+ # Decision indicating content came from the destination (customization preserved)
28
+ # @return [Symbol]
29
+ DECISION_DESTINATION = :destination
30
+
31
+ # Decision indicating content was added from template (new in template)
32
+ # @return [Symbol]
33
+ DECISION_ADDED = :added
34
+
35
+ # Decision indicating content was recursively merged
36
+ # @return [Symbol]
37
+ DECISION_RECURSIVE = :recursive
38
+
39
+ # Initialize a new merge result
40
+ # @param template_analysis [FileAnalysis] Analysis of the template file
41
+ # @param dest_analysis [FileAnalysis] Analysis of the destination file
42
+ def initialize(template_analysis, dest_analysis)
43
+ super(template_analysis: template_analysis, dest_analysis: dest_analysis)
44
+ end
45
+
46
+ # Add content from the template at the given statement index
47
+ # @param index [Integer] Statement index in template
48
+ # @param decision [Symbol] Decision type (default: DECISION_TEMPLATE)
49
+ # @return [void]
50
+ def add_from_template(index, decision: DECISION_TEMPLATE)
51
+ statement = @template_analysis.statements[index]
52
+ return unless statement
53
+
54
+ lines = extract_lines(statement, @template_analysis)
55
+ @lines.concat(lines)
56
+ @decisions << {decision: decision, source: :template, index: index, lines: lines.length}
57
+ end
58
+
59
+ # Add content from the destination at the given statement index
60
+ # @param index [Integer] Statement index in destination
61
+ # @param decision [Symbol] Decision type (default: DECISION_DESTINATION)
62
+ # @return [void]
63
+ def add_from_destination(index, decision: DECISION_DESTINATION)
64
+ statement = @dest_analysis.statements[index]
65
+ return unless statement
66
+
67
+ lines = extract_lines(statement, @dest_analysis)
68
+ @lines.concat(lines)
69
+ @decisions << {decision: decision, source: :destination, index: index, lines: lines.length}
70
+ end
71
+
72
+ # Add content from a freeze block
73
+ # @param freeze_node [FreezeNode] The freeze block to add
74
+ # @return [void]
75
+ def add_freeze_block(freeze_node)
76
+ lines = (freeze_node.start_line..freeze_node.end_line).map do |ln|
77
+ @dest_analysis.line_at(ln)
78
+ end
79
+ @lines.concat(lines)
80
+ @decisions << {
81
+ decision: DECISION_FREEZE_BLOCK,
82
+ source: :destination,
83
+ start_line: freeze_node.start_line,
84
+ end_line: freeze_node.end_line,
85
+ lines: lines.length,
86
+ }
87
+ end
88
+
89
+ # Add recursively merged content
90
+ # @param merged_content [String] The merged content string
91
+ # @param template_index [Integer] Template statement index
92
+ # @param dest_index [Integer] Destination statement index
93
+ # @return [void]
94
+ def add_recursive_merge(merged_content, template_index:, dest_index:)
95
+ # Split without trailing newlines for consistency with other methods
96
+ lines = merged_content.split("\n", -1)
97
+ # Remove trailing empty element if content ended with newline
98
+ lines.pop if lines.last == ""
99
+ @lines.concat(lines)
100
+ @decisions << {
101
+ decision: DECISION_RECURSIVE,
102
+ source: :merged,
103
+ template_index: template_index,
104
+ dest_index: dest_index,
105
+ lines: lines.length,
106
+ }
107
+ end
108
+
109
+ # Add raw content lines
110
+ # @param lines [Array<String>] Lines to add
111
+ # @param decision [Symbol] Decision type
112
+ # @return [void]
113
+ def add_raw(lines, decision:)
114
+ @lines.concat(lines)
115
+ @decisions << {decision: decision, source: :raw, lines: lines.length}
116
+ end
117
+
118
+ # Convert the merged result to a string
119
+ # @return [String] The merged RBS content
120
+ def to_s
121
+ return "" if @lines.empty?
122
+
123
+ # Lines are stored without trailing newlines, so join with newlines
124
+ result = @lines.join("\n")
125
+ # Ensure file ends with newline if content is non-empty
126
+ result += "\n" unless result.end_with?("\n")
127
+ result
128
+ end
129
+
130
+ # Check if any content has been added
131
+ # @return [Boolean]
132
+ def empty?
133
+ @lines.empty?
134
+ end
135
+
136
+ # Get summary of merge decisions
137
+ # @return [Hash] Summary with counts by decision type
138
+ def summary
139
+ counts = @decisions.group_by { |d| d[:decision] }.transform_values(&:count)
140
+ {
141
+ total_decisions: @decisions.length,
142
+ total_lines: @lines.length,
143
+ by_decision: counts,
144
+ }
145
+ end
146
+
147
+ private
148
+
149
+ # Extract lines for a statement from analysis
150
+ # @param statement [Object] The statement (declaration, member, or FreezeNode)
151
+ # @param analysis [FileAnalysis] The file analysis
152
+ # @return [Array<String>] Lines for the statement
153
+ def extract_lines(statement, analysis)
154
+ if statement.is_a?(FreezeNode)
155
+ (statement.start_line..statement.end_line).map { |ln| analysis.line_at(ln) }
156
+ else
157
+ start_line = statement.location.start_line
158
+ end_line = statement.location.end_line
159
+
160
+ # Include leading comment if present
161
+ if statement.respond_to?(:comment) && statement.comment
162
+ comment_start = statement.comment.location.start_line
163
+ start_line = comment_start if comment_start < start_line
164
+ end
165
+
166
+ (start_line..end_line).map { |ln| analysis.line_at(ln) }
167
+ end
168
+ end
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,266 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rbs
4
+ module Merge
5
+ # Orchestrates the smart merge process for RBS type signature files.
6
+ # Uses FileAnalysis, FileAligner, ConflictResolver, and MergeResult to
7
+ # merge two RBS files intelligently.
8
+ #
9
+ # SmartMerger provides flexible configuration for different merge scenarios.
10
+ # When matching class or module definitions are found in both files, the merger
11
+ # can perform recursive merging of their members.
12
+ #
13
+ # @example Basic merge (destination customizations preserved)
14
+ # merger = SmartMerger.new(template_content, dest_content)
15
+ # result = merger.merge
16
+ #
17
+ # @example Template updates win
18
+ # merger = SmartMerger.new(
19
+ # template_content,
20
+ # dest_content,
21
+ # preference: :template,
22
+ # add_template_only_nodes: true
23
+ # )
24
+ # result = merger.merge
25
+ #
26
+ # @example Custom signature matching
27
+ # sig_gen = ->(node) { [:decl, node.name.to_s] }
28
+ # merger = SmartMerger.new(
29
+ # template_content,
30
+ # dest_content,
31
+ # signature_generator: sig_gen
32
+ # )
33
+ #
34
+ # @see FileAnalysis
35
+ # @see FileAligner
36
+ # @see ConflictResolver
37
+ # @see MergeResult
38
+ class SmartMerger
39
+ # @return [FileAnalysis] Analysis of the template file
40
+ attr_reader :template_analysis
41
+
42
+ # @return [FileAnalysis] Analysis of the destination file
43
+ attr_reader :dest_analysis
44
+
45
+ # @return [FileAligner] Aligner for finding matches and differences
46
+ attr_reader :aligner
47
+
48
+ # @return [ConflictResolver] Resolver for handling conflicting content
49
+ attr_reader :resolver
50
+
51
+ # @return [MergeResult] Result object tracking merged content
52
+ attr_reader :result
53
+
54
+ # Creates a new SmartMerger for intelligent RBS file merging.
55
+ #
56
+ # @param template_content [String] Template RBS source code
57
+ # @param dest_content [String] Destination RBS source code
58
+ #
59
+ # @param signature_generator [Proc, nil] Optional proc to generate custom node signatures.
60
+ # The proc receives an RBS declaration and should return one of:
61
+ # - An array representing the node's signature
62
+ # - `nil` to indicate the node should have no signature
63
+ # - The original node to fall through to default signature computation
64
+ #
65
+ # @param preference [Symbol] Controls which version to use when nodes
66
+ # have matching signatures but different content:
67
+ # - `:destination` (default) - Use destination version (preserves customizations)
68
+ # - `:template` - Use template version (applies updates)
69
+ #
70
+ # @param add_template_only_nodes [Boolean] Controls whether to add nodes that only
71
+ # exist in template:
72
+ # - `false` (default) - Skip template-only nodes
73
+ # - `true` - Add template-only nodes to result
74
+ #
75
+ # @param freeze_token [String] Token to use for freeze block markers.
76
+ # Default: "rbs-merge" (looks for # rbs-merge:freeze / # rbs-merge:unfreeze)
77
+ #
78
+ # @param max_recursion_depth [Integer, Float] Maximum depth for recursive body merging.
79
+ # Default: Float::INFINITY (no limit)
80
+ #
81
+ # @raise [TemplateParseError] If template has syntax errors
82
+ # @raise [DestinationParseError] If destination has syntax errors
83
+ def initialize(
84
+ template_content,
85
+ dest_content,
86
+ signature_generator: nil,
87
+ preference: :destination,
88
+ add_template_only_nodes: false,
89
+ freeze_token: FileAnalysis::DEFAULT_FREEZE_TOKEN,
90
+ max_recursion_depth: Float::INFINITY
91
+ )
92
+ @preference = preference
93
+ @add_template_only_nodes = add_template_only_nodes
94
+ @max_recursion_depth = max_recursion_depth
95
+
96
+ # Parse template
97
+ begin
98
+ @template_analysis = FileAnalysis.new(
99
+ template_content,
100
+ freeze_token: freeze_token,
101
+ signature_generator: signature_generator,
102
+ )
103
+ rescue RBS::ParsingError => e
104
+ raise TemplateParseError.new([e])
105
+ end
106
+
107
+ # Parse destination
108
+ begin
109
+ @dest_analysis = FileAnalysis.new(
110
+ dest_content,
111
+ freeze_token: freeze_token,
112
+ signature_generator: signature_generator,
113
+ )
114
+ rescue RBS::ParsingError => e
115
+ raise DestinationParseError.new([e])
116
+ end
117
+
118
+ @aligner = FileAligner.new(@template_analysis, @dest_analysis)
119
+ @resolver = ConflictResolver.new(
120
+ preference: @preference,
121
+ template_analysis: @template_analysis,
122
+ dest_analysis: @dest_analysis,
123
+ )
124
+ @result = MergeResult.new(@template_analysis, @dest_analysis)
125
+ end
126
+
127
+ # Perform the merge operation
128
+ #
129
+ # @return [String] The merged content as a string
130
+ def merge
131
+ merge_result.to_s
132
+ end
133
+
134
+ # Perform the merge operation and return the full result object
135
+ #
136
+ # @return [MergeResult] The merge result containing merged content
137
+ def merge_result
138
+ return @merge_result if @merge_result
139
+
140
+ @merge_result = DebugLogger.time("SmartMerger#merge") do
141
+ alignment = @aligner.align
142
+
143
+ DebugLogger.debug("Alignment complete", {
144
+ total_entries: alignment.size,
145
+ matches: alignment.count { |e| e[:type] == :match },
146
+ template_only: alignment.count { |e| e[:type] == :template_only },
147
+ dest_only: alignment.count { |e| e[:type] == :dest_only },
148
+ })
149
+
150
+ process_alignment(alignment)
151
+ @result
152
+ end
153
+ end
154
+
155
+ private
156
+
157
+ # Process alignment entries and build result
158
+ # @param alignment [Array<Hash>] Alignment entries
159
+ # @return [void]
160
+ def process_alignment(alignment)
161
+ alignment.each do |entry|
162
+ case entry[:type]
163
+ when :match
164
+ process_match(entry)
165
+ when :template_only
166
+ process_template_only(entry)
167
+ when :dest_only
168
+ process_dest_only(entry)
169
+ end
170
+ end
171
+ end
172
+
173
+ # Process a matched declaration pair
174
+ # @param entry [Hash] Alignment entry
175
+ # @return [void]
176
+ def process_match(entry)
177
+ resolution = @resolver.resolve(
178
+ entry[:template_decl],
179
+ entry[:dest_decl],
180
+ template_index: entry[:template_index],
181
+ dest_index: entry[:dest_index],
182
+ )
183
+
184
+ case resolution[:source]
185
+ when :template
186
+ @result.add_from_template(entry[:template_index], decision: resolution[:decision])
187
+ when :destination
188
+ if entry[:dest_decl].is_a?(FreezeNode)
189
+ @result.add_freeze_block(entry[:dest_decl])
190
+ else
191
+ @result.add_from_destination(entry[:dest_index], decision: resolution[:decision])
192
+ end
193
+ when :recursive
194
+ process_recursive_merge(entry, resolution)
195
+ end
196
+ end
197
+
198
+ # Process a template-only declaration
199
+ # @param entry [Hash] Alignment entry
200
+ # @return [void]
201
+ def process_template_only(entry)
202
+ return unless @add_template_only_nodes
203
+
204
+ @result.add_from_template(entry[:template_index], decision: MergeResult::DECISION_ADDED)
205
+ end
206
+
207
+ # Process a destination-only declaration
208
+ # @param entry [Hash] Alignment entry
209
+ # @return [void]
210
+ def process_dest_only(entry)
211
+ if entry[:dest_decl].is_a?(FreezeNode)
212
+ @result.add_freeze_block(entry[:dest_decl])
213
+ else
214
+ @result.add_from_destination(entry[:dest_index], decision: MergeResult::DECISION_DESTINATION)
215
+ end
216
+ end
217
+
218
+ # Process recursive merge for container declarations
219
+ # @param entry [Hash] Alignment entry
220
+ # @param resolution [Hash] Resolution info
221
+ # @return [void]
222
+ def process_recursive_merge(entry, resolution)
223
+ template_decl = resolution[:template_declaration]
224
+ dest_decl = resolution[:dest_declaration]
225
+
226
+ # For now, just use the destination version for complex recursive merges
227
+ # A full recursive implementation would merge members individually
228
+ merged_content = reconstruct_declaration_with_merged_members(
229
+ template_decl,
230
+ dest_decl,
231
+ entry[:template_index],
232
+ entry[:dest_index],
233
+ )
234
+
235
+ @result.add_recursive_merge(
236
+ merged_content,
237
+ template_index: entry[:template_index],
238
+ dest_index: entry[:dest_index],
239
+ )
240
+ end
241
+
242
+ # Reconstruct a declaration with merged members
243
+ # @param template_decl [Object] Template declaration
244
+ # @param dest_decl [Object] Destination declaration
245
+ # @param template_index [Integer] Template index
246
+ # @param dest_index [Integer] Destination index
247
+ # @return [String] Merged declaration source
248
+ def reconstruct_declaration_with_merged_members(template_decl, dest_decl, template_index, dest_index)
249
+ # Choose which declaration to use based on preference
250
+ decl = (@preference == :template) ? template_decl : dest_decl
251
+ analysis = (@preference == :template) ? @template_analysis : @dest_analysis
252
+
253
+ start_line = decl.location.start_line
254
+ end_line = decl.location.end_line
255
+
256
+ # Include leading comment if present
257
+ if decl.respond_to?(:comment) && decl.comment
258
+ comment_start = decl.comment.location.start_line
259
+ start_line = comment_start if comment_start < start_line
260
+ end
261
+
262
+ (start_line..end_line).map { |ln| analysis.line_at(ln) }.join("\n") + "\n"
263
+ end
264
+ end
265
+ end
266
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rbs
4
+ module Merge
5
+ # Version information for Rbs::Merge
6
+ module Version
7
+ # Current version of the rbs-merge gem
8
+ VERSION = "1.0.0"
9
+ end
10
+ VERSION = Version::VERSION # traditional location
11
+ end
12
+ end
data/lib/rbs/merge.rb ADDED
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ # External gems
4
+ require "rbs"
5
+ require "version_gem"
6
+ require "set"
7
+
8
+ # Shared merge infrastructure
9
+ require "ast/merge"
10
+
11
+ # This gem
12
+ require_relative "merge/version"
13
+
14
+ module Rbs
15
+ module Merge
16
+ # Base error class for rbs-merge errors
17
+ # Inherits from Ast::Merge::Error for consistency across merge gems.
18
+ # @api public
19
+ class Error < Ast::Merge::Error; end
20
+
21
+ # Raised when an RBS file has parsing errors.
22
+ # Inherits from Ast::Merge::ParseError for consistency across merge gems.
23
+ #
24
+ # @example Handling parse errors
25
+ # begin
26
+ # analysis = FileAnalysis.new(rbs_content)
27
+ # rescue ParseError => e
28
+ # puts "RBS syntax error: #{e.message}"
29
+ # e.errors.each { |error| puts " #{error}" }
30
+ # end
31
+ #
32
+ # @api public
33
+ class ParseError < Ast::Merge::ParseError
34
+ # @param message [String, nil] Error message (auto-generated if nil)
35
+ # @param content [String, nil] The RBS source that failed to parse
36
+ # @param errors [Array] Parse errors from RBS
37
+ def initialize(message = nil, content: nil, errors: [])
38
+ super(message, errors: errors, content: content)
39
+ end
40
+ end
41
+
42
+ # Raised when the template RBS file has syntax errors.
43
+ #
44
+ # @example Handling template parse errors
45
+ # begin
46
+ # merger = SmartMerger.new(template, destination)
47
+ # result = merger.merge
48
+ # rescue TemplateParseError => e
49
+ # puts "Template syntax error: #{e.message}"
50
+ # e.errors.each { |error| puts " #{error.message}" }
51
+ # end
52
+ #
53
+ # @api public
54
+ class TemplateParseError < ParseError; end
55
+
56
+ # Raised when the destination RBS file has syntax errors.
57
+ #
58
+ # @example Handling destination parse errors
59
+ # begin
60
+ # merger = SmartMerger.new(template, destination)
61
+ # result = merger.merge
62
+ # rescue DestinationParseError => e
63
+ # puts "Destination syntax error: #{e.message}"
64
+ # e.errors.each { |error| puts " #{error.message}" }
65
+ # end
66
+ #
67
+ # @api public
68
+ class DestinationParseError < ParseError; end
69
+
70
+ autoload :DebugLogger, "rbs/merge/debug_logger"
71
+ autoload :FreezeNode, "rbs/merge/freeze_node"
72
+ autoload :MergeResult, "rbs/merge/merge_result"
73
+ autoload :FileAnalysis, "rbs/merge/file_analysis"
74
+ autoload :ConflictResolver, "rbs/merge/conflict_resolver"
75
+ autoload :FileAligner, "rbs/merge/file_aligner"
76
+ autoload :SmartMerger, "rbs/merge/smart_merger"
77
+ end
78
+ end
79
+
80
+ Rbs::Merge::Version.class_eval do
81
+ extend VersionGem::Basic
82
+ end
data/lib/rbs-merge.rb ADDED
@@ -0,0 +1,4 @@
1
+ # For technical reasons, if we move to Zeitwerk, this cannot be require_relative.
2
+ # See: https://github.com/fxn/zeitwerk#for_gem_extension
3
+ # Hook for other libraries to load this library (e.g. via bundler)
4
+ require "rbs/merge"