ast-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.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +46 -0
- data/CITATION.cff +20 -0
- data/CODE_OF_CONDUCT.md +134 -0
- data/CONTRIBUTING.md +227 -0
- data/FUNDING.md +74 -0
- data/LICENSE.txt +21 -0
- data/README.md +852 -0
- data/REEK +0 -0
- data/RUBOCOP.md +71 -0
- data/SECURITY.md +21 -0
- data/lib/ast/merge/ast_node.rb +87 -0
- data/lib/ast/merge/comment/block.rb +195 -0
- data/lib/ast/merge/comment/empty.rb +78 -0
- data/lib/ast/merge/comment/line.rb +138 -0
- data/lib/ast/merge/comment/parser.rb +278 -0
- data/lib/ast/merge/comment/style.rb +282 -0
- data/lib/ast/merge/comment.rb +36 -0
- data/lib/ast/merge/conflict_resolver_base.rb +399 -0
- data/lib/ast/merge/debug_logger.rb +271 -0
- data/lib/ast/merge/fenced_code_block_detector.rb +211 -0
- data/lib/ast/merge/file_analyzable.rb +307 -0
- data/lib/ast/merge/freezable.rb +82 -0
- data/lib/ast/merge/freeze_node_base.rb +434 -0
- data/lib/ast/merge/match_refiner_base.rb +312 -0
- data/lib/ast/merge/match_score_base.rb +135 -0
- data/lib/ast/merge/merge_result_base.rb +169 -0
- data/lib/ast/merge/merger_config.rb +258 -0
- data/lib/ast/merge/node_typing.rb +373 -0
- data/lib/ast/merge/region.rb +124 -0
- data/lib/ast/merge/region_detector_base.rb +114 -0
- data/lib/ast/merge/region_mergeable.rb +364 -0
- data/lib/ast/merge/rspec/shared_examples/conflict_resolver_base.rb +416 -0
- data/lib/ast/merge/rspec/shared_examples/debug_logger.rb +174 -0
- data/lib/ast/merge/rspec/shared_examples/file_analyzable.rb +193 -0
- data/lib/ast/merge/rspec/shared_examples/freeze_node_base.rb +219 -0
- data/lib/ast/merge/rspec/shared_examples/merge_result_base.rb +106 -0
- data/lib/ast/merge/rspec/shared_examples/merger_config.rb +202 -0
- data/lib/ast/merge/rspec/shared_examples/reproducible_merge.rb +115 -0
- data/lib/ast/merge/rspec/shared_examples.rb +26 -0
- data/lib/ast/merge/rspec.rb +4 -0
- data/lib/ast/merge/section_typing.rb +303 -0
- data/lib/ast/merge/smart_merger_base.rb +417 -0
- data/lib/ast/merge/text/conflict_resolver.rb +161 -0
- data/lib/ast/merge/text/file_analysis.rb +168 -0
- data/lib/ast/merge/text/line_node.rb +142 -0
- data/lib/ast/merge/text/merge_result.rb +42 -0
- data/lib/ast/merge/text/section.rb +93 -0
- data/lib/ast/merge/text/section_splitter.rb +397 -0
- data/lib/ast/merge/text/smart_merger.rb +141 -0
- data/lib/ast/merge/text/word_node.rb +86 -0
- data/lib/ast/merge/text.rb +35 -0
- data/lib/ast/merge/toml_frontmatter_detector.rb +88 -0
- data/lib/ast/merge/version.rb +12 -0
- data/lib/ast/merge/yaml_frontmatter_detector.rb +108 -0
- data/lib/ast/merge.rb +165 -0
- data/lib/ast-merge.rb +4 -0
- data/sig/ast/merge.rbs +195 -0
- data.tar.gz.sig +0 -0
- metadata +326 -0
- metadata.gz.sig +0 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ast
|
|
4
|
+
module Merge
|
|
5
|
+
##
|
|
6
|
+
# Detects TOML frontmatter at the beginning of a document.
|
|
7
|
+
#
|
|
8
|
+
# TOML frontmatter is delimited by `+++` at the start and end,
|
|
9
|
+
# and must begin on the first line of the document (optionally
|
|
10
|
+
# preceded by a UTF-8 BOM). This format is commonly used by
|
|
11
|
+
# Hugo and other static site generators.
|
|
12
|
+
#
|
|
13
|
+
# @example TOML frontmatter
|
|
14
|
+
# +++
|
|
15
|
+
# title = "My Document"
|
|
16
|
+
# author = "Jane Doe"
|
|
17
|
+
# +++
|
|
18
|
+
#
|
|
19
|
+
# @example Usage
|
|
20
|
+
# detector = TomlFrontmatterDetector.new
|
|
21
|
+
# regions = detector.detect_all(markdown_source)
|
|
22
|
+
# # => [#<Region type=:toml_frontmatter content="title = \"My Document\"\n...">]
|
|
23
|
+
#
|
|
24
|
+
class TomlFrontmatterDetector < RegionDetectorBase
|
|
25
|
+
##
|
|
26
|
+
# Pattern for detecting TOML frontmatter.
|
|
27
|
+
# - Must start at beginning of document (or after BOM)
|
|
28
|
+
# - Opening delimiter is `+++` followed by optional whitespace and newline
|
|
29
|
+
# - Content is captured (non-greedy)
|
|
30
|
+
# - Closing delimiter is `+++` at start of line, followed by optional whitespace and newline/EOF
|
|
31
|
+
#
|
|
32
|
+
FRONTMATTER_PATTERN = /\A(?:\xEF\xBB\xBF)?(\+\+\+[ \t]*\r?\n)(.*?)(^\+\+\+[ \t]*(?:\r?\n|\z))/m
|
|
33
|
+
|
|
34
|
+
##
|
|
35
|
+
# @return [Symbol] the type identifier for TOML frontmatter regions
|
|
36
|
+
#
|
|
37
|
+
def region_type
|
|
38
|
+
:toml_frontmatter
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
##
|
|
42
|
+
# Detects TOML frontmatter at the beginning of the document.
|
|
43
|
+
#
|
|
44
|
+
# @param source [String] the source document to scan
|
|
45
|
+
# @return [Array<Region>] array containing at most one Region for frontmatter
|
|
46
|
+
#
|
|
47
|
+
def detect_all(source)
|
|
48
|
+
return [] if source.nil? || source.empty?
|
|
49
|
+
|
|
50
|
+
match = source.match(FRONTMATTER_PATTERN)
|
|
51
|
+
return [] unless match
|
|
52
|
+
|
|
53
|
+
opening_delimiter = match[1]
|
|
54
|
+
content = match[2]
|
|
55
|
+
closing_delimiter = match[3]
|
|
56
|
+
|
|
57
|
+
# Calculate line numbers
|
|
58
|
+
start_line = 1
|
|
59
|
+
|
|
60
|
+
# Count total newlines in the full match to determine end line
|
|
61
|
+
full_match = match[0]
|
|
62
|
+
total_newlines = full_match.count("\n")
|
|
63
|
+
end_line = total_newlines + (full_match.end_with?("\n") ? 0 : 1)
|
|
64
|
+
|
|
65
|
+
[
|
|
66
|
+
Region.new(
|
|
67
|
+
type: region_type,
|
|
68
|
+
content: content,
|
|
69
|
+
start_line: start_line,
|
|
70
|
+
end_line: end_line,
|
|
71
|
+
delimiters: [opening_delimiter.strip, closing_delimiter.strip],
|
|
72
|
+
metadata: {format: :toml},
|
|
73
|
+
),
|
|
74
|
+
]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
##
|
|
80
|
+
# @return [Array<Region>] array containing at most one Region
|
|
81
|
+
#
|
|
82
|
+
def build_regions(source, matches)
|
|
83
|
+
# Not used - detect_all is overridden directly
|
|
84
|
+
raise NotImplementedError, "TomlFrontmatterDetector overrides detect_all directly"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ast
|
|
4
|
+
module Merge
|
|
5
|
+
##
|
|
6
|
+
# Detects YAML frontmatter at the beginning of a document.
|
|
7
|
+
#
|
|
8
|
+
# YAML frontmatter is delimited by `---` at the start and end,
|
|
9
|
+
# and must begin on the first line of the document (optionally
|
|
10
|
+
# preceded by a UTF-8 BOM).
|
|
11
|
+
#
|
|
12
|
+
# @example YAML frontmatter
|
|
13
|
+
# ---
|
|
14
|
+
# title: My Document
|
|
15
|
+
# author: Jane Doe
|
|
16
|
+
# ---
|
|
17
|
+
#
|
|
18
|
+
# @example Usage
|
|
19
|
+
# detector = YamlFrontmatterDetector.new
|
|
20
|
+
# regions = detector.detect_all(markdown_source)
|
|
21
|
+
# # => [#<Region type=:yaml_frontmatter content="title: My Document\n...">]
|
|
22
|
+
#
|
|
23
|
+
class YamlFrontmatterDetector < RegionDetectorBase
|
|
24
|
+
##
|
|
25
|
+
# Pattern for detecting YAML frontmatter.
|
|
26
|
+
# - Must start at beginning of document (or after BOM)
|
|
27
|
+
# - Opening delimiter is `---` followed by optional whitespace and newline
|
|
28
|
+
# - Content is captured (non-greedy)
|
|
29
|
+
# - Closing delimiter is `---` at start of line, followed by optional whitespace and newline/EOF
|
|
30
|
+
#
|
|
31
|
+
FRONTMATTER_PATTERN = /\A(?:\xEF\xBB\xBF)?(---[ \t]*\r?\n)(.*?)(^---[ \t]*(?:\r?\n|\z))/m
|
|
32
|
+
|
|
33
|
+
##
|
|
34
|
+
# @return [Symbol] the type identifier for YAML frontmatter regions
|
|
35
|
+
#
|
|
36
|
+
def region_type
|
|
37
|
+
:yaml_frontmatter
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
##
|
|
41
|
+
# Detects YAML frontmatter at the beginning of the document.
|
|
42
|
+
#
|
|
43
|
+
# @param source [String] the source document to scan
|
|
44
|
+
# @return [Array<Region>] array containing at most one Region for frontmatter
|
|
45
|
+
#
|
|
46
|
+
def detect_all(source)
|
|
47
|
+
return [] if source.nil? || source.empty?
|
|
48
|
+
|
|
49
|
+
match = source.match(FRONTMATTER_PATTERN)
|
|
50
|
+
return [] unless match
|
|
51
|
+
|
|
52
|
+
opening_delimiter = match[1]
|
|
53
|
+
content = match[2]
|
|
54
|
+
closing_delimiter = match[3]
|
|
55
|
+
|
|
56
|
+
# Calculate line numbers
|
|
57
|
+
# Frontmatter starts at line 1 (or after BOM)
|
|
58
|
+
start_line = 1
|
|
59
|
+
# Count newlines in content to determine end line
|
|
60
|
+
# Opening delimiter ends at line 1
|
|
61
|
+
# Content spans from line 2 to line 2 + content_lines - 1
|
|
62
|
+
# Closing delimiter is on the next line
|
|
63
|
+
content_newlines = content.count("\n")
|
|
64
|
+
# end_line is the line with the closing ---
|
|
65
|
+
end_line = start_line + 1 + content_newlines
|
|
66
|
+
|
|
67
|
+
# Adjust if content ends without newline
|
|
68
|
+
end_line - 1 if content.end_with?("\n") && content_newlines > 0
|
|
69
|
+
|
|
70
|
+
# Actually, let's calculate more carefully
|
|
71
|
+
# Line 1: ---
|
|
72
|
+
# Line 2 to N: content
|
|
73
|
+
# Line N+1: ---
|
|
74
|
+
if content.empty?
|
|
75
|
+
0
|
|
76
|
+
else
|
|
77
|
+
content.count("\n") + (content.end_with?("\n") ? 0 : 1)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Simplify: count total newlines in the full match to determine end line
|
|
81
|
+
full_match = match[0]
|
|
82
|
+
total_newlines = full_match.count("\n")
|
|
83
|
+
end_line = total_newlines + (full_match.end_with?("\n") ? 0 : 1)
|
|
84
|
+
|
|
85
|
+
[
|
|
86
|
+
Region.new(
|
|
87
|
+
type: region_type,
|
|
88
|
+
content: content,
|
|
89
|
+
start_line: start_line,
|
|
90
|
+
end_line: end_line,
|
|
91
|
+
delimiters: [opening_delimiter.strip, closing_delimiter.strip],
|
|
92
|
+
metadata: {format: :yaml},
|
|
93
|
+
),
|
|
94
|
+
]
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
##
|
|
100
|
+
# @return [Array<Region>] array containing at most one Region
|
|
101
|
+
#
|
|
102
|
+
def build_regions(source, matches)
|
|
103
|
+
# Not used - detect_all is overridden directly
|
|
104
|
+
raise NotImplementedError, "YamlFrontmatterDetector overrides detect_all directly"
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
data/lib/ast/merge.rb
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# External gems
|
|
4
|
+
require "version_gem"
|
|
5
|
+
|
|
6
|
+
# This gem - only version can be required (never autoloaded)
|
|
7
|
+
require_relative "merge/version"
|
|
8
|
+
|
|
9
|
+
module Ast
|
|
10
|
+
module Merge
|
|
11
|
+
# Base error class for all AST merge operations.
|
|
12
|
+
# All *-merge gems should have their Error class inherit from this.
|
|
13
|
+
# @api public
|
|
14
|
+
class Error < StandardError; end
|
|
15
|
+
|
|
16
|
+
# Base class for parse errors in merge operations.
|
|
17
|
+
#
|
|
18
|
+
# This class provides a flexible interface that can be extended by
|
|
19
|
+
# specific merge implementations. It supports:
|
|
20
|
+
# - An `errors` array for parser-specific error objects
|
|
21
|
+
# - An optional `content` attribute for the source that failed to parse
|
|
22
|
+
#
|
|
23
|
+
# Subclasses (TemplateParseError, DestinationParseError) identify whether
|
|
24
|
+
# the error occurred in the template or destination file.
|
|
25
|
+
#
|
|
26
|
+
# @example Basic usage with errors array
|
|
27
|
+
# raise ParseError.new(errors: [syntax_error])
|
|
28
|
+
#
|
|
29
|
+
# @example With content for debugging
|
|
30
|
+
# raise ParseError.new(errors: parse_result.errors, content: source_code)
|
|
31
|
+
#
|
|
32
|
+
# @example With custom message
|
|
33
|
+
# raise ParseError.new("Custom message", errors: [e])
|
|
34
|
+
#
|
|
35
|
+
# @api public
|
|
36
|
+
class ParseError < Error
|
|
37
|
+
# @return [Array] Parser-specific error objects (e.g., Prism::ParseError, RBS::BaseError)
|
|
38
|
+
attr_reader :errors
|
|
39
|
+
|
|
40
|
+
# @return [String, nil] The source content that failed to parse (optional)
|
|
41
|
+
attr_reader :content
|
|
42
|
+
|
|
43
|
+
# Initialize a new ParseError.
|
|
44
|
+
#
|
|
45
|
+
# @param message [String, nil] Custom error message (auto-generated if nil)
|
|
46
|
+
# @param errors [Array] Array of parser-specific error objects
|
|
47
|
+
# @param content [String, nil] The source content that failed to parse
|
|
48
|
+
def initialize(message = nil, errors: [], content: nil)
|
|
49
|
+
@errors = Array(errors)
|
|
50
|
+
@content = content
|
|
51
|
+
super(message || build_message)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
# Build a default error message from the errors array.
|
|
57
|
+
# Override in subclasses for more specific messages.
|
|
58
|
+
#
|
|
59
|
+
# @return [String] Error message
|
|
60
|
+
def build_message
|
|
61
|
+
if @errors.empty?
|
|
62
|
+
"Unknown #{self.class.name.split("::").map(&:downcase).join(" ")}"
|
|
63
|
+
else
|
|
64
|
+
error_messages = @errors.map { |e| e.respond_to?(:message) ? e.message : e.to_s }
|
|
65
|
+
"#{self.class.name.split("::").map(&:downcase).join(" ")}: #{error_messages.join(", ")}"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Raised when the template file has syntax errors.
|
|
71
|
+
#
|
|
72
|
+
# Template files are the "source of truth" that destination files
|
|
73
|
+
# are merged against. When a template cannot be parsed, the merge
|
|
74
|
+
# operation cannot proceed.
|
|
75
|
+
#
|
|
76
|
+
# @example Handling template parse errors
|
|
77
|
+
# begin
|
|
78
|
+
# merger = SmartMerger.new(template, destination)
|
|
79
|
+
# rescue Ast::Merge::TemplateParseError => e
|
|
80
|
+
# puts "Template syntax error: #{e.message}"
|
|
81
|
+
# e.errors.each { |error| puts " #{error.message}" }
|
|
82
|
+
# end
|
|
83
|
+
#
|
|
84
|
+
# @api public
|
|
85
|
+
class TemplateParseError < ParseError; end
|
|
86
|
+
|
|
87
|
+
# Raised when the destination file has syntax errors.
|
|
88
|
+
#
|
|
89
|
+
# Destination files contain user customizations that should be preserved
|
|
90
|
+
# during merges. When a destination cannot be parsed, the merge operation
|
|
91
|
+
# cannot proceed.
|
|
92
|
+
#
|
|
93
|
+
# @example Handling destination parse errors
|
|
94
|
+
# begin
|
|
95
|
+
# merger = SmartMerger.new(template, destination)
|
|
96
|
+
# rescue Ast::Merge::DestinationParseError => e
|
|
97
|
+
# puts "Destination syntax error: #{e.message}"
|
|
98
|
+
# e.errors.each { |error| puts " #{error.message}" }
|
|
99
|
+
# end
|
|
100
|
+
#
|
|
101
|
+
# @api public
|
|
102
|
+
class DestinationParseError < ParseError; end
|
|
103
|
+
|
|
104
|
+
# Raised when the document contains text that matches the region placeholder.
|
|
105
|
+
#
|
|
106
|
+
# Region placeholders are used internally to mark positions in a document
|
|
107
|
+
# where nested regions will be substituted after merging. If the document
|
|
108
|
+
# already contains text that looks like a placeholder, the merge cannot
|
|
109
|
+
# proceed safely.
|
|
110
|
+
#
|
|
111
|
+
# @example Handling placeholder collision
|
|
112
|
+
# begin
|
|
113
|
+
# merger = SmartMerger.new(template, destination, regions: [...])
|
|
114
|
+
# rescue Ast::Merge::PlaceholderCollisionError => e
|
|
115
|
+
# # Use a custom placeholder to avoid the collision
|
|
116
|
+
# merger = SmartMerger.new(template, destination,
|
|
117
|
+
# regions: [...],
|
|
118
|
+
# region_placeholder: "###MY_CUSTOM_PLACEHOLDER_"
|
|
119
|
+
# )
|
|
120
|
+
# end
|
|
121
|
+
#
|
|
122
|
+
# @api public
|
|
123
|
+
class PlaceholderCollisionError < Error
|
|
124
|
+
# @return [String] The placeholder that caused the collision
|
|
125
|
+
attr_reader :placeholder
|
|
126
|
+
|
|
127
|
+
# Initialize a new PlaceholderCollisionError.
|
|
128
|
+
#
|
|
129
|
+
# @param placeholder [String] The placeholder string that was found in the document
|
|
130
|
+
def initialize(placeholder)
|
|
131
|
+
@placeholder = placeholder
|
|
132
|
+
super(
|
|
133
|
+
"Document contains placeholder text '#{placeholder}'. " \
|
|
134
|
+
"Use the :region_placeholder option to specify a custom placeholder."
|
|
135
|
+
)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
autoload :AstNode, "ast/merge/ast_node"
|
|
140
|
+
autoload :Comment, "ast/merge/comment"
|
|
141
|
+
autoload :ConflictResolverBase, "ast/merge/conflict_resolver_base"
|
|
142
|
+
autoload :DebugLogger, "ast/merge/debug_logger"
|
|
143
|
+
autoload :FencedCodeBlockDetector, "ast/merge/fenced_code_block_detector"
|
|
144
|
+
autoload :FileAnalyzable, "ast/merge/file_analyzable"
|
|
145
|
+
autoload :Freezable, "ast/merge/freezable"
|
|
146
|
+
autoload :FreezeNodeBase, "ast/merge/freeze_node_base"
|
|
147
|
+
autoload :MatchRefinerBase, "ast/merge/match_refiner_base"
|
|
148
|
+
autoload :MatchScoreBase, "ast/merge/match_score_base"
|
|
149
|
+
autoload :MergeResultBase, "ast/merge/merge_result_base"
|
|
150
|
+
autoload :MergerConfig, "ast/merge/merger_config"
|
|
151
|
+
autoload :NodeTyping, "ast/merge/node_typing"
|
|
152
|
+
autoload :Region, "ast/merge/region"
|
|
153
|
+
autoload :RegionDetectorBase, "ast/merge/region_detector_base"
|
|
154
|
+
autoload :RegionMergeable, "ast/merge/region_mergeable"
|
|
155
|
+
autoload :SectionTyping, "ast/merge/section_typing"
|
|
156
|
+
autoload :SmartMergerBase, "ast/merge/smart_merger_base"
|
|
157
|
+
autoload :Text, "ast/merge/text"
|
|
158
|
+
autoload :TomlFrontmatterDetector, "ast/merge/toml_frontmatter_detector"
|
|
159
|
+
autoload :YamlFrontmatterDetector, "ast/merge/yaml_frontmatter_detector"
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
Ast::Merge::Version.class_eval do
|
|
164
|
+
extend VersionGem::Basic
|
|
165
|
+
end
|
data/lib/ast-merge.rb
ADDED
data/sig/ast/merge.rbs
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
module Ast
|
|
2
|
+
module Merge
|
|
3
|
+
VERSION: String
|
|
4
|
+
|
|
5
|
+
# Base error class for all merge operations
|
|
6
|
+
class Error < StandardError
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# Raised when parsing fails (template or destination)
|
|
10
|
+
class ParseError < Error
|
|
11
|
+
attr_reader errors: untyped
|
|
12
|
+
attr_reader content: String?
|
|
13
|
+
|
|
14
|
+
def initialize: (?String? message, ?errors: untyped, ?content: String?) -> void
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Raised when parsing the template (source) content fails
|
|
18
|
+
class TemplateParseError < ParseError
|
|
19
|
+
def initialize: (?String? message, ?errors: untyped, ?content: String?) -> void
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Raised when parsing the destination content fails
|
|
23
|
+
class DestinationParseError < ParseError
|
|
24
|
+
def initialize: (?String? message, ?errors: untyped, ?content: String?) -> void
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Base class for freeze block nodes in AST merge libraries.
|
|
28
|
+
class FreezeNode
|
|
29
|
+
# Pattern configuration for freeze block markers
|
|
30
|
+
MARKER_PATTERNS: Hash[Symbol, Hash[Symbol, Regexp]]
|
|
31
|
+
|
|
32
|
+
# Default pattern when none specified
|
|
33
|
+
DEFAULT_PATTERN: Symbol
|
|
34
|
+
|
|
35
|
+
# Error raised when a freeze block has invalid structure
|
|
36
|
+
class InvalidStructureError < StandardError
|
|
37
|
+
attr_reader start_line: Integer?
|
|
38
|
+
attr_reader end_line: Integer?
|
|
39
|
+
attr_reader unclosed_nodes: Array[untyped]
|
|
40
|
+
|
|
41
|
+
def initialize: (
|
|
42
|
+
String message,
|
|
43
|
+
?start_line: Integer?,
|
|
44
|
+
?end_line: Integer?,
|
|
45
|
+
?unclosed_nodes: Array[untyped]
|
|
46
|
+
) -> void
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Simple location struct for compatibility with AST nodes
|
|
50
|
+
class Location < Struct[Integer]
|
|
51
|
+
attr_accessor start_line: Integer
|
|
52
|
+
attr_accessor end_line: Integer
|
|
53
|
+
|
|
54
|
+
def cover?: (Integer line) -> bool
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Class methods
|
|
58
|
+
def self.register_pattern: (Symbol name, start: Regexp, end_pattern: Regexp) -> Hash[Symbol, Regexp]
|
|
59
|
+
def self.start_pattern: (?Symbol pattern_type) -> Regexp
|
|
60
|
+
def self.end_pattern: (?Symbol pattern_type) -> Regexp
|
|
61
|
+
def self.freeze_start?: (String? line, ?Symbol pattern_type) -> bool
|
|
62
|
+
def self.freeze_end?: (String? line, ?Symbol pattern_type) -> bool
|
|
63
|
+
def self.pattern_types: () -> Array[Symbol]
|
|
64
|
+
|
|
65
|
+
# Instance attributes
|
|
66
|
+
attr_reader start_line: Integer
|
|
67
|
+
attr_reader end_line: Integer
|
|
68
|
+
attr_reader content: String?
|
|
69
|
+
attr_reader start_marker: String?
|
|
70
|
+
attr_reader end_marker: String?
|
|
71
|
+
attr_reader pattern_type: Symbol
|
|
72
|
+
|
|
73
|
+
def initialize: (
|
|
74
|
+
start_line: Integer,
|
|
75
|
+
end_line: Integer,
|
|
76
|
+
?start_marker: String?,
|
|
77
|
+
?end_marker: String?,
|
|
78
|
+
?pattern_type: Symbol
|
|
79
|
+
) -> void
|
|
80
|
+
|
|
81
|
+
def location: () -> Location
|
|
82
|
+
def slice: () -> String?
|
|
83
|
+
def freeze_node?: () -> bool
|
|
84
|
+
def signature: () -> Array[untyped]
|
|
85
|
+
def inspect: () -> String
|
|
86
|
+
def to_s: () -> String
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def validate_line_order!: () -> void
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Debug logging module
|
|
94
|
+
module DebugLogger
|
|
95
|
+
def self.env_var_name: () -> String
|
|
96
|
+
def self.env_var_name=: (String name) -> String
|
|
97
|
+
def self.log_prefix: () -> String
|
|
98
|
+
def self.log_prefix=: (String prefix) -> String
|
|
99
|
+
def self.enabled?: () -> bool
|
|
100
|
+
def self.debug: (*untyped args) -> void
|
|
101
|
+
def self.info: (*untyped args) -> void
|
|
102
|
+
def self.warning: (*untyped args) -> void
|
|
103
|
+
def self.time: (String label) { () -> untyped } -> untyped
|
|
104
|
+
def self.log_node: (untyped node, ?label: String) -> void
|
|
105
|
+
def self.extract_lines: (untyped node) -> String
|
|
106
|
+
def self.safe_type_name: (untyped node) -> String
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Base module for file analysis classes
|
|
110
|
+
module FileAnalysisBase
|
|
111
|
+
# Required instance attributes (must be defined by including class)
|
|
112
|
+
attr_reader statements: Array[untyped]
|
|
113
|
+
attr_reader lines: Array[String]
|
|
114
|
+
attr_reader signature_generator: (^(untyped) -> (Array[untyped] | untyped | nil))?
|
|
115
|
+
|
|
116
|
+
# Get all freeze blocks from statements
|
|
117
|
+
def freeze_blocks: () -> Array[FreezeNode]
|
|
118
|
+
|
|
119
|
+
# Check if a line is within a freeze block
|
|
120
|
+
def in_freeze_block?: (Integer line_num) -> bool
|
|
121
|
+
|
|
122
|
+
# Get the freeze block containing the given line
|
|
123
|
+
def freeze_block_at: (Integer line_num) -> FreezeNode?
|
|
124
|
+
|
|
125
|
+
# Get structural signature for a statement at given index
|
|
126
|
+
def signature_at: (Integer index) -> Array[untyped]?
|
|
127
|
+
|
|
128
|
+
# Get a specific line (1-indexed)
|
|
129
|
+
def line_at: (Integer line_num) -> String?
|
|
130
|
+
|
|
131
|
+
# Get a normalized line (whitespace-trimmed)
|
|
132
|
+
def normalized_line: (Integer line_num) -> String?
|
|
133
|
+
|
|
134
|
+
# Generate signature for a node
|
|
135
|
+
def generate_signature: (untyped node) -> Array[untyped]?
|
|
136
|
+
|
|
137
|
+
# Check if a value represents a fallthrough node
|
|
138
|
+
def fallthrough_node?: (untyped value) -> bool
|
|
139
|
+
|
|
140
|
+
# Compute default signature for a node (abstract - must be implemented)
|
|
141
|
+
def compute_node_signature: (untyped node) -> Array[untyped]?
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Base merge result tracking
|
|
145
|
+
class MergeResult
|
|
146
|
+
DECISION_KEPT_TEMPLATE: Symbol
|
|
147
|
+
DECISION_KEPT_DEST: Symbol
|
|
148
|
+
DECISION_MERGED: Symbol
|
|
149
|
+
DECISION_ADDED: Symbol
|
|
150
|
+
DECISION_FREEZE_BLOCK: Symbol
|
|
151
|
+
DECISION_REPLACED: Symbol
|
|
152
|
+
DECISION_APPENDED: Symbol
|
|
153
|
+
|
|
154
|
+
attr_reader decisions: Array[Hash[Symbol, untyped]]
|
|
155
|
+
|
|
156
|
+
def initialize: () -> void
|
|
157
|
+
def track_decision: (
|
|
158
|
+
untyped node,
|
|
159
|
+
Symbol decision,
|
|
160
|
+
?reason: String?
|
|
161
|
+
) -> void
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Configuration object for SmartMerger options
|
|
165
|
+
class MergerConfig
|
|
166
|
+
VALID_PREFERENCES: Array[Symbol]
|
|
167
|
+
|
|
168
|
+
attr_reader signature_match_preference: Symbol | Hash[Symbol, Symbol]
|
|
169
|
+
attr_reader node_splitter: Hash[Symbol, untyped]?
|
|
170
|
+
attr_reader add_template_only_nodes: bool
|
|
171
|
+
attr_reader freeze_token: String?
|
|
172
|
+
attr_reader signature_generator: (^(untyped) -> (Array[untyped] | untyped | nil))?
|
|
173
|
+
|
|
174
|
+
def initialize: (
|
|
175
|
+
?signature_match_preference: (Symbol | Hash[Symbol, Symbol]),
|
|
176
|
+
?add_template_only_nodes: bool,
|
|
177
|
+
?freeze_token: String?,
|
|
178
|
+
?signature_generator: (^(untyped) -> (Array[untyped] | untyped | nil))?,
|
|
179
|
+
?node_splitter: Hash[Symbol, untyped]?
|
|
180
|
+
) -> void
|
|
181
|
+
|
|
182
|
+
def prefer_destination?: () -> bool
|
|
183
|
+
def prefer_template?: () -> bool
|
|
184
|
+
def to_h: (?default_freeze_token: String?) -> Hash[Symbol, untyped]
|
|
185
|
+
def with: (**untyped options) -> MergerConfig
|
|
186
|
+
|
|
187
|
+
def self.destination_wins: (?freeze_token: String?, ?signature_generator: (^(untyped) -> (Array[untyped] | untyped | nil))?, ?node_splitter: Hash[Symbol, untyped]?) -> MergerConfig
|
|
188
|
+
def self.template_wins: (?freeze_token: String?, ?signature_generator: (^(untyped) -> (Array[untyped] | untyped | nil))?, ?node_splitter: Hash[Symbol, untyped]?) -> MergerConfig
|
|
189
|
+
|
|
190
|
+
private
|
|
191
|
+
|
|
192
|
+
def validate_preference!: (Symbol preference) -> void
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
data.tar.gz.sig
ADDED
|
Binary file
|