bash-merge 2.0.6 → 7.1.3

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.
@@ -2,11 +2,12 @@
2
2
 
3
3
  module Bash
4
4
  module Merge
5
- # Version information for Bash::Merge
5
+ # Version namespace for this gem.
6
6
  module Version
7
- # Current version of the bash-merge gem
8
- VERSION = "2.0.6"
7
+ # Current gem version.
8
+ VERSION = '7.1.3'
9
9
  end
10
- VERSION = Version::VERSION # traditional location
10
+ # Current gem version exposed at the traditional constant location.
11
+ VERSION = Version::VERSION # Traditional Constant Location
11
12
  end
12
13
  end
data/lib/bash/merge.rb CHANGED
@@ -1,30 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # std libs
4
- require "set"
5
4
 
6
5
  # External gems
7
6
  # TreeHaver provides a unified cross-Ruby interface to tree-sitter.
8
- # It handles grammar discovery and backend selection automatically
9
- # via parser_for(:bash). No manual registration needed.
7
+ # Bash::Merge registers its TreeHaver grammar bootstrap when loaded so
8
+ # parser_for(:bash) can resolve a registered grammar consistently.
10
9
  #
11
10
  # BACKEND COMPATIBILITY for Bash:
12
11
  # - FFI: Most portable and reliable with bash grammar (recommended)
13
12
  # - MRI: Has ABI incompatibility with bash grammar
13
+ require_relative 'merge/version'
14
14
  # - Rust: Has version mismatch with bash grammar
15
15
  #
16
16
  # Set TREE_HAVER_BACKEND=ffi (or mri/rust) to control backend selection.
17
17
  # When MRI loads a grammar first, FFI gets incompatible pointers (symbol conflict).
18
18
  # MRI statically links tree-sitter, FFI dynamically links libtree-sitter.so.
19
- require "tree_haver"
20
-
21
- require "version_gem"
19
+ require 'tree_haver'
22
20
 
23
21
  # Shared merge infrastructure
24
- require "ast/merge"
22
+ require 'ast/merge'
25
23
 
26
24
  # This gem
27
- require_relative "merge/version"
28
25
 
29
26
  # Bash::Merge provides a generic Bash script smart merge system using tree-sitter AST analysis.
30
27
  # It intelligently merges template and destination Bash scripts by identifying matching
@@ -48,8 +45,20 @@ module Bash
48
45
  #
49
46
  # @see SmartMerger Main entry point for merge operations
50
47
  # @see FileAnalysis Analyzes Bash structure
51
- # @see ConflictResolver Resolves content conflicts
52
48
  module Merge
49
+ TREE_SITTER_BACKEND = TreeHaver::KREUZBERG_LANGUAGE_PACK_BACKEND
50
+ BACKEND_REGISTRY = Struct.new(:registered, :mutex).new(false, Mutex.new)
51
+ Availability = Struct.new(
52
+ :grammar_path,
53
+ :node_parser,
54
+ :diagnostics,
55
+ keyword_init: true
56
+ ) do
57
+ def available?
58
+ node_parser == true
59
+ end
60
+ end
61
+
53
62
  # Base error class for Bash::Merge
54
63
  # Inherits from Ast::Merge::Error for consistency across merge gems.
55
64
  class Error < Ast::Merge::Error; end
@@ -101,30 +110,116 @@ module Bash
101
110
  # end
102
111
  class DestinationParseError < ParseError; end
103
112
 
104
- autoload :CommentTracker, "bash/merge/comment_tracker"
105
- autoload :DebugLogger, "bash/merge/debug_logger"
106
- autoload :Emitter, "bash/merge/emitter"
107
- autoload :FreezeNode, "bash/merge/freeze_node"
108
- autoload :FileAnalysis, "bash/merge/file_analysis"
109
- autoload :MergeResult, "bash/merge/merge_result"
110
- autoload :NodeWrapper, "bash/merge/node_wrapper"
111
- autoload :ConflictResolver, "bash/merge/conflict_resolver"
112
- autoload :SmartMerger, "bash/merge/smart_merger"
113
+ class CorruptionDetectedError < Error; end
114
+
115
+ autoload :CommentTracker, 'bash/merge/comment_tracker'
116
+ autoload :DebugLogger, 'bash/merge/debug_logger'
117
+ autoload :Emitter, 'bash/merge/emitter'
118
+ autoload :FreezeNode, 'bash/merge/freeze_node'
119
+ autoload :FileAnalysis, 'bash/merge/file_analysis'
120
+ autoload :MergeResult, 'bash/merge/merge_result'
121
+ autoload :NodeWrapper, 'bash/merge/node_wrapper'
122
+ autoload :Provider, 'bash/merge/provider'
123
+ autoload :SmartMerger, 'bash/merge/smart_merger'
124
+
125
+ class << self
126
+ def register_backend!
127
+ BACKEND_REGISTRY.mutex.synchronize do
128
+ return if BACKEND_REGISTRY.registered
129
+
130
+ TreeHaver::BackendRegistry.register(TREE_SITTER_BACKEND)
131
+
132
+ grammar_finder = TreeHaver::GrammarFinder.new(:bash)
133
+ grammar_finder.register! if grammar_finder.available?
134
+
135
+ BACKEND_REGISTRY.registered = true
136
+ end
137
+ end
138
+
139
+ def available_bash_backends
140
+ bash_backend_available_for_analysis?(TREE_SITTER_BACKEND.id) ? [TREE_SITTER_BACKEND] : []
141
+ end
142
+
143
+ def available?(source: "#!/usr/bin/env bash\necho hello\n")
144
+ availability(source: source).available?
145
+ end
146
+
147
+ def availability(source: "#!/usr/bin/env bash\necho hello\n")
148
+ diagnostics = []
149
+ grammar_path = bash_grammar_path(diagnostics)
150
+ Availability.new(
151
+ grammar_path: grammar_path,
152
+ node_parser: node_parser_available_for?(source, grammar_path, diagnostics),
153
+ diagnostics: diagnostics
154
+ )
155
+ end
156
+
157
+ private
158
+
159
+ def bash_grammar_path(diagnostics)
160
+ TreeHaver::GrammarFinder.new(:bash).find_library_path
161
+ rescue TreeHaver::Error => e
162
+ diagnostics << { kind: 'bash_grammar_unavailable', message: e.message }
163
+ nil
164
+ end
165
+
166
+ def node_parser_available_for?(source, grammar_path, diagnostics)
167
+ register_backend!
168
+ parser = if grammar_path
169
+ TreeHaver.with_language_registration(
170
+ :bash,
171
+ :tree_sitter,
172
+ path: grammar_path,
173
+ symbol: TreeHaver::GrammarFinder.new(:bash).symbol_name
174
+ ) { TreeHaver.parser_for(:bash, backend_type: :tree_sitter) }
175
+ else
176
+ TreeHaver.parser_for(:bash, backend_type: :tree_sitter)
177
+ end
178
+ tree = parser.parse(source)
179
+ !tree.nil? && !tree.root_node.nil?
180
+ rescue TreeHaver::Error => e
181
+ diagnostics << { kind: 'bash_node_parser_unavailable', message: e.message }
182
+ false
183
+ rescue StandardError => e
184
+ diagnostics << { kind: 'bash_node_parser_unavailable', message: e.message }
185
+ false
186
+ end
187
+
188
+ def bash_backend_available_for_analysis?(backend_id)
189
+ register_backend!
190
+
191
+ if backend_id.to_s.empty?
192
+ TreeHaver.parser_for(:bash, backend_type: :tree_sitter)
193
+ else
194
+ TreeHaver.with_backend(backend_id) { TreeHaver.parser_for(:bash, backend_type: :tree_sitter) }
195
+ end
196
+ true
197
+ rescue TreeHaver::Error, ArgumentError
198
+ false
199
+ end
200
+ end
113
201
  end
114
202
  end
115
203
 
204
+ Bash::Merge.register_backend!
205
+
206
+ # Register grammar availability with TreeHaver so shared RSpec dependency tags
207
+ # can exclude Bash grammar examples when no parser and grammar are available.
208
+ TreeHaver::BackendRegistry.register_tag(:bash_grammar, category: :grammar, backend_name: :bash_grammar) do
209
+ Bash::Merge.available?
210
+ end
211
+
212
+ require_relative 'merge/provider'
213
+ Bash::Merge.register_provider!
214
+
116
215
  # Register with ast-merge's MergeGemRegistry for RSpec dependency tags
117
216
  # Only register if MergeGemRegistry is loaded (i.e., in test environment)
118
217
  if defined?(Ast::Merge::RSpec::MergeGemRegistry)
119
218
  Ast::Merge::RSpec::MergeGemRegistry.register(
120
219
  :bash_merge,
121
- require_path: "bash/merge",
122
- merger_class: "Bash::Merge::SmartMerger",
220
+ require_path: 'bash/merge',
221
+ merger_class: 'Bash::Merge::SmartMerger',
123
222
  test_source: "#!/bin/bash\necho hello",
124
- category: :code,
223
+ category: :code
125
224
  )
126
225
  end
127
-
128
- Bash::Merge::Version.class_eval do
129
- extend VersionGem::Basic
130
- end
data/lib/bash-merge.rb CHANGED
@@ -3,4 +3,10 @@
3
3
  # For technical reasons, if we move to Zeitwerk, this cannot be require_relative.
4
4
  # See: https://github.com/fxn/zeitwerk#for_gem_extension
5
5
  # Hook for other libraries to load this library (e.g. via bundler)
6
- require "bash/merge"
6
+ require 'bash/merge'
7
+ require 'version_gem'
8
+ require_relative 'bash/merge/version'
9
+
10
+ Bash::Merge::Version.class_eval do
11
+ extend VersionGem::Basic
12
+ end
data/sig/bash/merge.rbs CHANGED
@@ -1,260 +1,30 @@
1
- # Type signatures for bash-merge gem
2
- # Smart merge for Bash scripts using tree-sitter AST analysis
3
-
4
1
  module Bash
5
2
  module Merge
6
- VERSION: String
7
-
8
- # Base error class for bash-merge errors
9
- class Error < StandardError
10
- end
11
-
12
- # Error raised when template Bash file has syntax errors
13
- class TemplateParseError < Error
14
- attr_reader errors: Array[untyped]
15
- attr_reader content: String?
16
-
17
- def initialize: (?String? message, ?errors: Array[untyped], ?content: String?) -> void
18
- end
19
-
20
- # Error raised when destination Bash file has syntax errors
21
- class DestinationParseError < Error
22
- attr_reader errors: Array[untyped]
23
- attr_reader content: String?
24
-
25
- def initialize: (?String? message, ?errors: Array[untyped], ?content: String?) -> void
26
- end
27
-
28
- # Debug logging utility for Bash::Merge
29
- module DebugLogger
30
- extend Ast::Merge::DebugLogger
31
-
32
- def self.env_var_name: () -> String
33
- def self.env_var_name=: (String name) -> String
34
- def self.log_prefix: () -> String
35
- def self.log_prefix=: (String prefix) -> String
36
- def self.enabled?: () -> bool
37
- def self.debug: (String message, ?Hash[Symbol, untyped] context) -> void
38
- def self.info: (String message) -> void
39
- def self.warning: (String message) -> void
40
- def self.time: [T] (String operation) { () -> T } -> T
41
- def self.extract_node_info: (untyped node) -> Hash[Symbol, untyped]
42
- end
43
-
44
- # Freeze block node for Bash scripts
45
- class FreezeNode < Ast::Merge::FreezeNode
46
- InvalidStructureError: singleton(Ast::Merge::FreezeNode::InvalidStructureError)
47
- Location: singleton(Ast::Merge::FreezeNode::Location)
48
-
49
- attr_reader lines: Array[String]
50
-
51
- def initialize: (
52
- start_line: Integer,
53
- end_line: Integer,
54
- lines: Array[String],
55
- ?start_marker: String?,
56
- ?end_marker: String?,
57
- ?pattern_type: Symbol
58
- ) -> void
59
-
60
- def signature: () -> Array[Symbol | String]
61
- def function_definition?: () -> bool
62
- def variable_assignment?: () -> bool
63
- def command?: () -> bool
64
- def slice: () -> String?
65
- def inspect: () -> String
66
-
67
- private
68
-
69
- def validate_structure!: () -> void
70
- end
71
-
72
- # Wrapper for tree-sitter Bash nodes
73
3
  class NodeWrapper
74
4
  attr_reader node: untyped
75
- attr_reader start_line: Integer
76
- attr_reader end_line: Integer
77
- attr_reader source: String
78
- attr_reader analysis: FileAnalysis?
79
-
80
- def initialize: (
81
- untyped node,
82
- String source,
83
- ?analysis: FileAnalysis?
84
- ) -> void
85
-
86
- def location: () -> Ast::Merge::FreezeNode::Location
87
- def signature: () -> Array[untyped]
88
- def freeze_node?: () -> bool
89
- def function_definition?: () -> bool
90
- def variable_assignment?: () -> bool
91
- def command?: () -> bool
92
- def comment?: () -> bool
93
- def name: () -> String?
94
- def value: () -> String?
95
- def slice: () -> String?
96
- def text: () -> String
97
- def type: () -> Symbol
98
- def inspect: () -> String
99
- end
100
-
101
- # Comment tracker for Bash files
102
- class CommentTracker
103
- attr_reader comments: Array[Hash[Symbol, untyped]]
104
- attr_reader source: String
105
- attr_reader lines: Array[String]
106
-
107
- def initialize: (String source) -> void
108
-
109
- def extract_comments: () -> Array[Hash[Symbol, untyped]]
110
- def comments_for_line: (Integer line) -> Array[Hash[Symbol, untyped]]
111
- def leading_comments_for: (Integer line) -> Array[Hash[Symbol, untyped]]
112
- def trailing_comment_for: (Integer line) -> Hash[Symbol, untyped]?
113
- def shebang?: (String line) -> bool
114
- end
115
-
116
- # File analysis for Bash scripts
117
- class FileAnalysis
118
- include Ast::Merge::FileAnalysisBase
119
-
120
- DEFAULT_FREEZE_TOKEN: String
121
- PARSER_SEARCH_PATHS: Array[String]
122
-
123
- attr_reader source: String
124
- attr_reader lines: Array[String]
125
- attr_reader ast: untyped
126
- attr_reader statements: Array[NodeWrapper | FreezeNode]
127
- attr_reader freeze_blocks: Array[FreezeNode]
128
- attr_reader freeze_token: String
129
- attr_reader signature_generator: (^(untyped) -> Array[untyped]?)?
130
- attr_reader comment_tracker: CommentTracker
131
- attr_reader errors: Array[untyped]
132
-
133
- def self.find_parser_path: () -> String?
134
-
135
- def initialize: (
136
- String source,
137
- ?freeze_token: String,
138
- ?signature_generator: (^(untyped) -> Array[untyped]?)?,
139
- ?parser_path: String?
140
- ) -> void
141
-
142
- def valid?: () -> bool
143
- def nodes: () -> Array[NodeWrapper | FreezeNode]
144
- def line_at: (Integer line_num) -> String?
145
- def normalized_line: (Integer line_num) -> String?
146
- def in_freeze_block?: (Integer line_num) -> bool
147
- def freeze_block_at: (Integer line_num) -> FreezeNode?
148
- def signature_at: (Integer index) -> Array[untyped]?
149
- def generate_signature: (untyped node) -> Array[untyped]?
150
- def compute_node_signature: (untyped node) -> Array[untyped]?
151
-
152
- private
153
-
154
- def parse_bash: () -> void
155
- def extract_nodes: (untyped tree_node) -> Array[NodeWrapper]
156
- def extract_freeze_blocks: () -> Array[FreezeNode]
157
- def integrate_nodes_and_freeze_blocks: () -> Array[NodeWrapper | FreezeNode]
158
- end
159
-
160
- # Result of a Bash merge operation
161
- class MergeResult < Ast::Merge::MergeResult
162
- DECISION_KEPT_TEMPLATE: Symbol
163
- DECISION_KEPT_DEST: Symbol
164
- DECISION_MERGED: Symbol
165
- DECISION_ADDED: Symbol
166
- DECISION_FREEZE_BLOCK: Symbol
167
-
168
- attr_reader lines: Array[Hash[Symbol, untyped]]
169
- attr_reader decisions: Array[Hash[Symbol, untyped]]
170
- attr_reader statistics: Hash[Symbol, Integer]
171
-
172
- def initialize: (
173
- ?template_analysis: FileAnalysis?,
174
- ?dest_analysis: FileAnalysis?,
175
- ?conflicts: Array[Hash[Symbol, untyped]],
176
- ?frozen_blocks: Array[FreezeNode],
177
- ?stats: Hash[Symbol, untyped]
178
- ) -> void
179
-
180
- def add_line: (
181
- String line,
182
- decision: Symbol,
183
- source: Symbol,
184
- ?original_line: Integer?
185
- ) -> void
186
-
187
- def add_lines: (
188
- Array[String] lines,
189
- decision: Symbol,
190
- source: Symbol,
191
- ?start_line: Integer?
192
- ) -> void
193
-
194
- def add_blank_line: (?decision: Symbol, ?source: Symbol) -> void
195
- def add_freeze_block: (FreezeNode freeze_node) -> void
196
- def to_bash: () -> String
197
- def content: () -> Array[Hash[Symbol, untyped]]
198
- def content_string: () -> String
199
- def empty?: () -> bool
200
-
201
- private
202
-
203
- def track_statistics: (Symbol decision, Symbol source) -> void
204
- end
205
-
206
- # Smart merger for Bash scripts
207
- class SmartMerger
208
- include Ast::Merge::MergerConfig
209
-
210
- attr_reader template_analysis: FileAnalysis
211
- attr_reader dest_analysis: FileAnalysis
212
- attr_reader signature_match_preference: (Symbol | Hash[Symbol, Symbol])
213
- attr_reader add_template_only_nodes: bool
214
-
215
- def initialize: (
216
- String template_content,
217
- String dest_content,
218
- ?signature_match_preference: (Symbol | Hash[Symbol, Symbol]),
219
- ?add_template_only_nodes: bool,
220
- ?freeze_token: String,
221
- ?signature_generator: (^(untyped) -> Array[untyped]?)?,
222
- ?node_splitter: Hash[Symbol, untyped]?,
223
- ?parser_path: String?
224
- ) -> void
225
-
226
- def merge: () -> MergeResult
227
-
228
- private
229
-
230
- def perform_merge: () -> MergeResult
231
- def merge_nodes: (MergeResult result) -> void
5
+ def start_byte: () -> Integer
6
+ def end_byte: () -> Integer
7
+ def source_text: () -> String
8
+ def semantic_tree: () -> Array[untyped]
9
+ def heredoc?: () -> bool
232
10
  end
233
11
 
234
- # Conflict resolver for Bash merges
235
- class ConflictResolver
236
- attr_reader template_analysis: FileAnalysis
237
- attr_reader dest_analysis: FileAnalysis
238
- attr_reader signature_match_preference: (Symbol | Hash[Symbol, Symbol])
239
- attr_reader add_template_only_nodes: bool
240
-
241
- def initialize: (
242
- FileAnalysis template_analysis,
243
- FileAnalysis dest_analysis,
244
- ?signature_match_preference: (Symbol | Hash[Symbol, Symbol]),
245
- ?add_template_only_nodes: bool
246
- ) -> void
247
-
248
- def resolve: (untyped boundary, MergeResult result) -> void
12
+ class Provider
13
+ def provider_id: () -> String
14
+ def family: () -> String
15
+ def capabilities: () -> Hash[Symbol, untyped]
16
+ def analyze: (Hash[Symbol, untyped] request) -> Hash[Symbol, untyped]
17
+ def diff2: (Hash[Symbol, untyped] request) -> Hash[Symbol, untyped]
18
+ def merge2: (Hash[Symbol, untyped] request) -> Hash[Symbol, untyped]
19
+ def merge3: (Hash[Symbol, untyped] request) -> Hash[Symbol, untyped]
249
20
  end
250
21
 
251
- # Emitter for reconstructing Bash output
252
- class Emitter
253
- attr_reader result: MergeResult
254
-
255
- def initialize: (MergeResult result) -> void
22
+ def self.merge_provider: () -> Provider
23
+ def self.register_provider!: (?replace: bool) -> untyped
256
24
 
257
- def emit: () -> String
25
+ module Version
26
+ VERSION: String
258
27
  end
28
+ VERSION: String
259
29
  end
260
30
  end
data.tar.gz.sig CHANGED
@@ -1 +1,3 @@
1
- Oq ���������]*Ꙩ �w�Hq)�x��q
1
+ _W}�=��:���"]$����"'J`�rjS--����)�
2
+ ���޾V�
3
+ k�E�؀ `P$@�u���di�[�{xՠ~Q֓�;�Sj�����I��Kd�.��:A3�x�H�Q���EA�EpV�+�b���CjU�2��@�� �ijc��e�X�U�a_7­м%S#�Ί�-��8��=妌UC���S���kBpBF_є�G6�H]wa���'�-q�W�M��>���j��ĵ(����q�Q�썬�T�r��XZ���Ѻp����{md>R��f�n�o���h ����2�m#����jա`1�ze\2