bash-merge 2.0.6 → 7.1.1

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.1'
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,112 @@ 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 :SmartMerger, 'bash/merge/smart_merger'
123
+
124
+ class << self
125
+ def register_backend!
126
+ BACKEND_REGISTRY.mutex.synchronize do
127
+ return if BACKEND_REGISTRY.registered
128
+
129
+ TreeHaver::BackendRegistry.register(TREE_SITTER_BACKEND)
130
+
131
+ grammar_finder = TreeHaver::GrammarFinder.new(:bash)
132
+ grammar_finder.register! if grammar_finder.available?
133
+
134
+ BACKEND_REGISTRY.registered = true
135
+ end
136
+ end
137
+
138
+ def available_bash_backends
139
+ bash_backend_available_for_analysis?(TREE_SITTER_BACKEND.id) ? [TREE_SITTER_BACKEND] : []
140
+ end
141
+
142
+ def available?(source: "#!/usr/bin/env bash\necho hello\n")
143
+ availability(source: source).available?
144
+ end
145
+
146
+ def availability(source: "#!/usr/bin/env bash\necho hello\n")
147
+ diagnostics = []
148
+ grammar_path = bash_grammar_path(diagnostics)
149
+ Availability.new(
150
+ grammar_path: grammar_path,
151
+ node_parser: node_parser_available_for?(source, grammar_path, diagnostics),
152
+ diagnostics: diagnostics
153
+ )
154
+ end
155
+
156
+ private
157
+
158
+ def bash_grammar_path(diagnostics)
159
+ TreeHaver::GrammarFinder.new(:bash).find_library_path
160
+ rescue TreeHaver::Error => e
161
+ diagnostics << { kind: 'bash_grammar_unavailable', message: e.message }
162
+ nil
163
+ end
164
+
165
+ def node_parser_available_for?(source, grammar_path, diagnostics)
166
+ register_backend!
167
+ parser = if grammar_path
168
+ TreeHaver.with_language_registration(
169
+ :bash,
170
+ :tree_sitter,
171
+ path: grammar_path,
172
+ symbol: TreeHaver::GrammarFinder.new(:bash).symbol_name
173
+ ) { TreeHaver.parser_for(:bash, backend_type: :tree_sitter) }
174
+ else
175
+ TreeHaver.parser_for(:bash, backend_type: :tree_sitter)
176
+ end
177
+ tree = parser.parse(source)
178
+ !tree.nil? && !tree.root_node.nil?
179
+ rescue TreeHaver::Error => e
180
+ diagnostics << { kind: 'bash_node_parser_unavailable', message: e.message }
181
+ false
182
+ rescue StandardError => e
183
+ diagnostics << { kind: 'bash_node_parser_unavailable', message: e.message }
184
+ false
185
+ end
186
+
187
+ def bash_backend_available_for_analysis?(backend_id)
188
+ register_backend!
189
+
190
+ if backend_id.to_s.empty?
191
+ TreeHaver.parser_for(:bash, backend_type: :tree_sitter)
192
+ else
193
+ TreeHaver.with_backend(backend_id) { TreeHaver.parser_for(:bash, backend_type: :tree_sitter) }
194
+ end
195
+ true
196
+ rescue TreeHaver::Error, ArgumentError
197
+ false
198
+ end
199
+ end
113
200
  end
114
201
  end
115
202
 
203
+ Bash::Merge.register_backend!
204
+
205
+ # Register grammar availability with TreeHaver so shared RSpec dependency tags
206
+ # can exclude Bash grammar examples when no parser and grammar are available.
207
+ TreeHaver::BackendRegistry.register_tag(:bash_grammar, category: :grammar, backend_name: :bash_grammar) do
208
+ Bash::Merge.available?
209
+ end
210
+
116
211
  # Register with ast-merge's MergeGemRegistry for RSpec dependency tags
117
212
  # Only register if MergeGemRegistry is loaded (i.e., in test environment)
118
213
  if defined?(Ast::Merge::RSpec::MergeGemRegistry)
119
214
  Ast::Merge::RSpec::MergeGemRegistry.register(
120
215
  :bash_merge,
121
- require_path: "bash/merge",
122
- merger_class: "Bash::Merge::SmartMerger",
216
+ require_path: 'bash/merge',
217
+ merger_class: 'Bash::Merge::SmartMerger',
123
218
  test_source: "#!/bin/bash\necho hello",
124
- category: :code,
219
+ category: :code
125
220
  )
126
221
  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,8 @@
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
- class NodeWrapper
74
- 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
232
- end
233
-
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
249
- end
250
-
251
- # Emitter for reconstructing Bash output
252
- class Emitter
253
- attr_reader result: MergeResult
254
-
255
- def initialize: (MergeResult result) -> void
256
-
257
- def emit: () -> String
3
+ module Version
4
+ VERSION: String
258
5
  end
6
+ VERSION: String
259
7
  end
260
8
  end
data.tar.gz.sig CHANGED
Binary file