tree_haver 5.0.5 → 7.1.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.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/LICENSE.md +13 -0
  4. data/README.md +299 -660
  5. data/lib/tree_haver/backend_api.rb +78 -35
  6. data/lib/tree_haver/backend_context.rb +28 -0
  7. data/lib/tree_haver/backend_registry.rb +119 -382
  8. data/lib/tree_haver/backends/citrus.rb +55 -53
  9. data/lib/tree_haver/backends/ffi.rb +105 -101
  10. data/lib/tree_haver/backends/java.rb +92 -76
  11. data/lib/tree_haver/backends/mri.rb +44 -39
  12. data/lib/tree_haver/backends/parslet.rb +53 -48
  13. data/lib/tree_haver/backends/prism.rb +140 -43
  14. data/lib/tree_haver/backends/psych.rb +30 -26
  15. data/lib/tree_haver/backends/rust.rb +21 -17
  16. data/lib/tree_haver/backends/tslp.rb +274 -0
  17. data/lib/tree_haver/base/comment.rb +320 -0
  18. data/lib/tree_haver/base/language.rb +2 -2
  19. data/lib/tree_haver/base/node.rb +39 -31
  20. data/lib/tree_haver/base/parser.rb +4 -0
  21. data/lib/tree_haver/base/point.rb +3 -3
  22. data/lib/tree_haver/base/tree.rb +1 -1
  23. data/lib/tree_haver/citrus_grammar_finder.rb +21 -26
  24. data/lib/tree_haver/contracts.rb +1025 -0
  25. data/lib/tree_haver/grammar_finder.rb +125 -70
  26. data/lib/tree_haver/kaitai_backend.rb +30 -0
  27. data/lib/tree_haver/language.rb +36 -37
  28. data/lib/tree_haver/language_pack.rb +41 -0
  29. data/lib/tree_haver/language_registry.rb +34 -3
  30. data/lib/tree_haver/library_path_utils.rb +7 -7
  31. data/lib/tree_haver/node.rb +40 -31
  32. data/lib/tree_haver/parser.rb +44 -37
  33. data/lib/tree_haver/parslet_grammar_finder.rb +19 -26
  34. data/lib/tree_haver/path_validator.rb +39 -36
  35. data/lib/tree_haver/peg_backends.rb +76 -0
  36. data/lib/tree_haver/rspec/dependency_tags.rb +23 -1363
  37. data/lib/tree_haver/rspec.rb +1 -31
  38. data/lib/tree_haver/tree.rb +16 -7
  39. data/lib/tree_haver/version.rb +5 -14
  40. data/lib/tree_haver.rb +536 -1240
  41. data/sig/tree_haver.rbs +1 -229
  42. data.tar.gz.sig +0 -0
  43. metadata +154 -70
  44. metadata.gz.sig +0 -0
  45. data/CHANGELOG.md +0 -1393
  46. data/CITATION.cff +0 -20
  47. data/CODE_OF_CONDUCT.md +0 -134
  48. data/CONTRIBUTING.md +0 -359
  49. data/FUNDING.md +0 -74
  50. data/LICENSE.txt +0 -21
  51. data/REEK +0 -0
  52. data/RUBOCOP.md +0 -71
  53. data/SECURITY.md +0 -21
  54. data/lib/tree_haver/base.rb +0 -12
  55. data/lib/tree_haver/compat.rb +0 -43
  56. data/lib/tree_haver/rspec/testable_node.rb +0 -217
  57. data/sig/tree_haver/backends.rbs +0 -352
  58. data/sig/tree_haver/grammar_finder.rbs +0 -29
  59. data/sig/tree_haver/path_validator.rbs +0 -32
@@ -1,17 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "rbconfig"
3
+ require 'rbconfig'
4
4
 
5
5
  module TreeHaver
6
- # Generic utility for finding tree-sitter grammar shared libraries.
6
+ # Registration-first utility for finding tree-sitter grammar shared libraries.
7
7
  #
8
- # GrammarFinder provides platform-aware discovery of tree-sitter grammar
9
- # libraries. Given a language name, it searches common installation paths
10
- # and supports environment variable overrides.
8
+ # GrammarFinder resolves tree-sitter grammars in a constrained order:
9
+ #
10
+ # 1. explicit environment override
11
+ # 2. existing TreeHaver registration
12
+ # 3. explicit extra paths
13
+ # 4. tree_sitter_language_pack parser backend registration
11
14
  #
12
15
  # This class is designed to be used by language-specific merge gems
13
- # (toml-merge, json-merge, bash-merge, etc.) without requiring TreeHaver
14
- # to have knowledge of each specific language.
16
+ # without requiring TreeHaver to own parser- or grammar-specific policy.
15
17
  #
16
18
  # == Security Considerations
17
19
  #
@@ -29,7 +31,7 @@ module TreeHaver
29
31
  # @example Basic usage
30
32
  # finder = TreeHaver::GrammarFinder.new(:toml)
31
33
  # path = finder.find_library_path
32
- # # => "/usr/lib/libtree-sitter-toml.so"
34
+ # # => "/usr/local/lib/libtree_sitter_toml.so"
33
35
  #
34
36
  # @example Check availability
35
37
  # finder = TreeHaver::GrammarFinder.new(:json)
@@ -51,15 +53,6 @@ module TreeHaver
51
53
  #
52
54
  # @see PathValidator For details on security validations
53
55
  class GrammarFinder
54
- # Common base directories where tree-sitter libraries are installed
55
- # Platform-specific extensions are appended automatically
56
- BASE_SEARCH_DIRS = [
57
- "/usr/lib",
58
- "/usr/lib64",
59
- "/usr/local/lib",
60
- "/opt/homebrew/lib",
61
- ].freeze
62
-
63
56
  # @return [Symbol] the language identifier
64
57
  attr_reader :language_name
65
58
 
@@ -77,7 +70,7 @@ module TreeHaver
77
70
 
78
71
  if validate && !PathValidator.safe_language_name?(name_str)
79
72
  raise ArgumentError, "Invalid language name: #{language_name.inspect}. " \
80
- "Language names must start with a letter and contain only lowercase letters, numbers, and underscores."
73
+ 'Language names must start with a letter and contain only lowercase letters, numbers, and underscores.'
81
74
  end
82
75
 
83
76
  @language_name = name_str.to_sym
@@ -98,41 +91,56 @@ module TreeHaver
98
91
  "tree_sitter_#{@language_name}"
99
92
  end
100
93
 
101
- # Get the library filename for the current platform
94
+ # Get the canonical tree-sitter-language-pack filename for the current platform
102
95
  #
103
- # @return [String] the library filename (e.g., "libtree-sitter-toml.so")
96
+ # @return [String] the library filename (e.g., "libtree_sitter_toml.so")
104
97
  def library_filename
98
+ library_filenames.first
99
+ end
100
+
101
+ # Get all accepted library filenames for this language
102
+ #
103
+ # Accept both the tree-sitter-language-pack naming convention and the
104
+ # historical hyphenated form used by some standalone grammar builds.
105
+ #
106
+ # @return [Array<String>]
107
+ def library_filenames
105
108
  ext = platform_extension
106
- "libtree-sitter-#{@language_name}#{ext}"
109
+ [
110
+ "libtree_sitter_#{@language_name}#{ext}",
111
+ "libtree-sitter-#{@language_name}#{ext}"
112
+ ]
107
113
  end
108
114
 
109
115
  # Generate the full list of search paths for this language
110
116
  #
111
- # Order: ENV override, extra_paths, then common system paths
117
+ # Order: registered path, then explicit extra paths.
112
118
  #
113
119
  # @return [Array<String>] all paths to search
114
120
  def search_paths
115
121
  paths = []
116
122
 
117
- # Extra paths provided at initialization (searched after ENV)
118
- @extra_paths.each do |dir|
119
- paths << File.join(dir, library_filename)
120
- end
123
+ registration = registered_tree_sitter_registration
124
+ paths << registration[:path] if registration&.dig(:path)
121
125
 
122
- # Common system paths with platform-appropriate extension
123
- BASE_SEARCH_DIRS.each do |dir|
124
- paths << File.join(dir, library_filename)
126
+ @extra_paths.each do |dir|
127
+ library_filenames.each do |filename|
128
+ paths << File.join(dir, filename)
129
+ end
125
130
  end
126
131
 
127
- paths
132
+ paths.uniq
128
133
  end
129
134
 
130
135
  # Find the grammar library path
131
136
  #
132
137
  # Searches in order:
133
138
  # 1. Environment variable override (validated for safety)
134
- # 2. Extra paths provided at initialization
135
- # 3. Common system installation paths
139
+ # 2. Existing TreeHaver tree-sitter registration
140
+ # 3. Extra paths provided at initialization
141
+ # tree_sitter_language_pack is intentionally not exposed as a shared-library
142
+ # path fallback here. It is registered as a TreeHaver backend module when
143
+ # its parser API is available.
136
144
  #
137
145
  # @note Paths from ENV are validated using {PathValidator.safe_library_path?}
138
146
  # to prevent path traversal and other attacks. Invalid ENV paths cause
@@ -151,18 +159,18 @@ module TreeHaver
151
159
  if ENV[env_var] || ENV.key?(env_var)
152
160
  env_path = ENV[env_var]
153
161
 
154
- # :nocov: defensive - ENV.key? true with nil value is rare edge case
162
+ # simplecov:disable defensive - ENV.key? true with nil value is rare edge case
155
163
  if env_path.nil?
156
- @env_rejection_reason = "explicitly disabled (set to nil)"
164
+ @env_rejection_reason = 'explicitly disabled (set to nil)'
157
165
  return
158
166
  end
159
- # :nocov:
167
+ # simplecov:enable
160
168
 
161
169
  # Empty string means "explicitly skip this grammar"
162
170
  # This allows users to disable tree-sitter for specific languages
163
171
  # and fall back to alternative backends like Citrus
164
172
  if env_path.empty?
165
- @env_rejection_reason = "explicitly disabled (set to empty string)"
173
+ @env_rejection_reason = 'explicitly disabled (set to empty string)'
166
174
  return
167
175
  end
168
176
 
@@ -173,35 +181,36 @@ module TreeHaver
173
181
  # to a path, that path MUST work. Don't silently fall back to auto-discovery.
174
182
  if @env_rejection_reason
175
183
  raise TreeHaver::NotAvailable,
176
- "#{env_var_name} is set to #{env_path.inspect} but #{@env_rejection_reason}. " \
177
- "Either fix the path, unset the variable to use auto-discovery, " \
178
- "or set it to empty string to explicitly disable this grammar."
184
+ "#{env_var_name} is set to #{env_path.inspect} but #{@env_rejection_reason}. " \
185
+ 'Either fix the path, unset the variable to use auto-discovery, ' \
186
+ 'or set it to empty string to explicitly disable this grammar.'
179
187
  end
180
188
 
181
189
  return env_path
182
190
  end
183
191
 
184
- # Search all paths (these are constructed from trusted base dirs)
185
- search_paths.find { |path| File.exist?(path) }
192
+ registered_path = registered_tree_sitter_path
193
+ return registered_path if registered_path
194
+
195
+ explicit_path = explicit_search_path
196
+ return explicit_path if explicit_path
197
+
198
+ nil
186
199
  end
187
200
 
188
201
  # Validate an environment variable path and return reason if invalid
189
202
  # @return [String, nil] rejection reason or nil if valid
190
203
  def validate_env_path(path)
191
204
  # Check for leading/trailing whitespace
192
- if path != path.strip
193
- return "contains leading or trailing whitespace (use #{path.strip.inspect})"
194
- end
205
+ return "contains leading or trailing whitespace (use #{path.strip.inspect})" if path != path.strip
195
206
 
196
207
  # Check if path is safe
197
208
  unless PathValidator.safe_library_path?(path)
198
- return "failed security validation (may contain path traversal or suspicious characters)"
209
+ return 'failed security validation (may contain path traversal or suspicious characters)'
199
210
  end
200
211
 
201
212
  # Check if file exists
202
- unless File.exist?(path)
203
- return "file does not exist"
204
- end
213
+ return 'file does not exist' unless File.exist?(path)
205
214
 
206
215
  nil # Valid!
207
216
  end
@@ -215,7 +224,6 @@ module TreeHaver
215
224
  # @return [String, nil] the path to the library, or nil if not found
216
225
  # @see PathValidator::TRUSTED_DIRECTORIES For the list of trusted directories
217
226
  def find_library_path_safe
218
- # Environment variable is NOT checked in safe mode - only trusted system paths
219
227
  search_paths.find do |path|
220
228
  File.exist?(path) && PathValidator.in_trusted_directory?(path)
221
229
  end
@@ -232,6 +240,8 @@ module TreeHaver
232
240
  #
233
241
  # @return [Boolean] true if the library can be found AND tree-sitter runtime works
234
242
  def available?
243
+ return true if tree_sitter_language_pack_parser_available?
244
+
235
245
  path = find_library_path
236
246
  return false if path.nil?
237
247
 
@@ -246,7 +256,7 @@ module TreeHaver
246
256
  TreeHaver::Backends::MRI,
247
257
  TreeHaver::Backends::FFI,
248
258
  TreeHaver::Backends::Rust,
249
- TreeHaver::Backends::Java,
259
+ TreeHaver::Backends::Java
250
260
  ].freeze
251
261
 
252
262
  class << self
@@ -273,7 +283,7 @@ module TreeHaver
273
283
  true
274
284
  end
275
285
  rescue NoMethodError, LoadError, NotAvailable => _e
276
- # Note: FFI::NotFoundError inherits from LoadError, so it's caught here too
286
+ # NOTE: FFI::NotFoundError inherits from LoadError, so it's caught here too
277
287
  false
278
288
  end
279
289
  end
@@ -303,11 +313,20 @@ module TreeHaver
303
313
  # @return [Boolean] true if registration succeeded
304
314
  # @raise [NotAvailable] if library not found and raise_on_missing is true
305
315
  def register!(raise_on_missing: false)
316
+ if tree_sitter_language_pack_parser_available?
317
+ TreeHaver.register_language(
318
+ @language_name,
319
+ backend_module: TreeHaver::Backends::Tslp,
320
+ backend_type: :tslp,
321
+ gem_name: 'tree_sitter_language_pack'
322
+ )
323
+ return true
324
+ end
325
+
306
326
  path = find_library_path
307
327
  unless path
308
- if raise_on_missing
309
- raise NotAvailable, not_found_message
310
- end
328
+ raise NotAvailable, not_found_message if raise_on_missing
329
+
311
330
  return false
312
331
  end
313
332
 
@@ -325,11 +344,13 @@ module TreeHaver
325
344
  env_var: env_var_name,
326
345
  env_value: ENV[env_var_name],
327
346
  env_rejection_reason: @env_rejection_reason,
347
+ tree_sitter_language_pack_parser_available: tree_sitter_language_pack_parser_available?,
328
348
  symbol: symbol_name,
329
349
  library_filename: library_filename,
350
+ library_filenames: library_filenames,
330
351
  search_paths: search_paths,
331
352
  found_path: found,
332
- available: !found.nil?,
353
+ available: tree_sitter_language_pack_parser_available? || !found.nil?
333
354
  }
334
355
  end
335
356
 
@@ -342,32 +363,66 @@ module TreeHaver
342
363
  # Check if env var is set but rejected
343
364
  env_value = ENV[env_var_name]
344
365
  msg += if env_value && @env_rejection_reason
345
- " #{env_var_name} is set to #{env_value.inspect} but #{@env_rejection_reason}."
346
- elsif env_value && File.exist?(env_value) && !self.class.tree_sitter_runtime_usable?
347
- " #{env_var_name} is set and file exists, but no tree-sitter runtime is available. " \
348
- "Add ruby_tree_sitter, ffi, or tree_stump gem to your Gemfile."
349
- elsif env_value
350
- " #{env_var_name} is set but was not used (file may have been removed)."
351
- else
352
- " Searched: #{search_paths.join(", ")}."
353
- end
354
-
355
- msg + " Install tree-sitter-#{@language_name} or set #{env_var_name} to a valid path."
366
+ " #{env_var_name} is set to #{env_value.inspect} but #{@env_rejection_reason}."
367
+ elsif env_value && File.exist?(env_value) && !self.class.tree_sitter_runtime_usable?
368
+ " #{env_var_name} is set and file exists, but no tree-sitter runtime is available. " \
369
+ 'Add ruby_tree_sitter, ffi, or tree_stump gem to your Gemfile.'
370
+ else
371
+ " Searched: #{search_paths.join(', ')}."
372
+ end
373
+
374
+ msg + ' Register the grammar, install tree_sitter_language_pack with parser API support, ' \
375
+ "or set #{env_var_name} to a valid path."
356
376
  end
357
377
 
358
378
  private
359
379
 
380
+ def registered_tree_sitter_registration
381
+ TreeHaver::LanguageRegistry.registered(@language_name, :tree_sitter)
382
+ end
383
+
384
+ def registered_tree_sitter_path
385
+ registration = registered_tree_sitter_registration
386
+ path = registration&.dig(:path)
387
+ return unless path
388
+ return path if File.exist?(path)
389
+
390
+ @registered_rejection_reason = "registered path does not exist: #{path}"
391
+ nil
392
+ end
393
+
394
+ def explicit_search_path
395
+ search_paths.find { |path| File.exist?(path) }
396
+ end
397
+
398
+ def tree_sitter_language_pack_parser_available?
399
+ return @tree_sitter_language_pack_parser_available if defined?(@tree_sitter_language_pack_parser_available)
400
+
401
+ @tree_sitter_language_pack_parser_available = begin
402
+ require 'tree_sitter_language_pack' unless defined?(::TreeSitterLanguagePack)
403
+ TreeHaver::Backends::Tslp.available? &&
404
+ ::TreeSitterLanguagePack.respond_to?(:has_language) &&
405
+ ::TreeSitterLanguagePack.has_language(@language_name.to_s) &&
406
+ TreeHaver::Backends::Tslp.parser_available_for?(@language_name)
407
+ rescue LoadError
408
+ false
409
+ rescue StandardError => e
410
+ @tree_sitter_language_pack_rejection_reason = e.message
411
+ false
412
+ end
413
+ end
414
+
360
415
  # Get the platform-appropriate shared library extension
361
416
  #
362
417
  # @return [String] ".so" on Linux, ".dylib" on macOS
363
418
  def platform_extension
364
- case RbConfig::CONFIG["host_os"]
419
+ case RbConfig::CONFIG['host_os']
365
420
  when /darwin/i
366
- ".dylib"
421
+ '.dylib'
367
422
  when /mswin|mingw|cygwin/i
368
- ".dll"
423
+ '.dll'
369
424
  else
370
- ".so"
425
+ '.so'
371
426
  end
372
427
  end
373
428
  end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TreeHaver
4
+ KAITAI_STRUCT_BACKEND = BackendReference.new(
5
+ id: 'kaitai-struct',
6
+ family: 'kaitai'
7
+ ).freeze
8
+
9
+ BackendRegistry.register(KAITAI_STRUCT_BACKEND)
10
+
11
+ module_function
12
+
13
+ def kaitai_adapter_info
14
+ AdapterInfo.new(
15
+ backend: KAITAI_STRUCT_BACKEND.id,
16
+ backend_ref: KAITAI_STRUCT_BACKEND,
17
+ supports_dialects: false,
18
+ supported_policies: []
19
+ )
20
+ end
21
+
22
+ def kaitai_feature_profile
23
+ FeatureProfile.new(
24
+ backend: KAITAI_STRUCT_BACKEND.id,
25
+ backend_ref: KAITAI_STRUCT_BACKEND,
26
+ supports_dialects: false,
27
+ supported_policies: []
28
+ )
29
+ end
30
+ end
@@ -90,12 +90,12 @@ module TreeHaver
90
90
  if validate
91
91
  unless PathValidator.safe_library_path?(path)
92
92
  errors = PathValidator.validation_errors(path)
93
- raise ArgumentError, "Unsafe library path: #{path.inspect}. Errors: #{errors.join("; ")}"
93
+ raise ArgumentError, "Unsafe library path: #{path.inspect}. Errors: #{errors.join('; ')}"
94
94
  end
95
95
 
96
96
  if symbol && !PathValidator.safe_symbol_name?(symbol)
97
97
  raise ArgumentError, "Unsafe symbol name: #{symbol.inspect}. " \
98
- "Symbol names must be valid C identifiers."
98
+ 'Symbol names must be valid C identifiers.'
99
99
  end
100
100
  end
101
101
 
@@ -105,19 +105,20 @@ module TreeHaver
105
105
 
106
106
  if mod.nil?
107
107
  if backend
108
- raise NotAvailable, "Requested backend #{backend.inspect} is not available or does not support shared libraries"
108
+ raise NotAvailable,
109
+ "Requested backend #{backend.inspect} is not available or does not support shared libraries"
109
110
  else
110
111
  raise NotAvailable,
111
- "No native tree-sitter backend is available for loading shared libraries. " \
112
- "Available native backends (MRI, Rust, FFI, Java) require platform-specific setup. " \
113
- "For pure-Ruby parsing, use backend-specific Language classes directly (e.g., Prism, Psych, Citrus)."
112
+ 'No native tree-sitter backend is available for loading shared libraries. ' \
113
+ 'Available native backends (MRI, Rust, FFI, Java) require platform-specific setup. ' \
114
+ 'For pure-Ruby parsing, use backend-specific Language classes directly (e.g., Prism, Psych, Citrus).'
114
115
  end
115
116
  end
116
117
 
117
118
  # Backend must implement .from_library; fallback to .from_path for older impls
118
119
  # Include effective backend AND ENV vars in cache key since they affect loading
119
120
  effective_b = TreeHaver.resolve_effective_backend(backend)
120
- key = [effective_b, path, symbol, name, ENV["TREE_SITTER_LANG_SYMBOL"]]
121
+ key = [effective_b, path, symbol, name, ENV['TREE_SITTER_LANG_SYMBOL']]
121
122
  LanguageRegistry.fetch(key) do
122
123
  if mod::Language.respond_to?(:from_library)
123
124
  mod::Language.from_library(path, symbol: symbol, name: name)
@@ -128,7 +129,7 @@ module TreeHaver
128
129
  end
129
130
  # Alias for {from_library}
130
131
  # @see from_library
131
- alias_method :from_path, :from_library
132
+ alias from_path from_library
132
133
 
133
134
  # Dynamic helper to load a registered language by name
134
135
  #
@@ -162,40 +163,38 @@ module TreeHaver
162
163
 
163
164
  # Determine which backend type to use
164
165
  backend_type = if current_backend == Backends::Citrus
165
- :citrus
166
- elsif current_backend == Backends::Parslet
167
- :parslet
168
- else
169
- :tree_sitter # MRI, Rust, FFI, Java all use tree-sitter
170
- end
166
+ :citrus
167
+ elsif current_backend == Backends::Parslet
168
+ :parslet
169
+ else
170
+ :tree_sitter # MRI, Rust, FFI, Java all use tree-sitter
171
+ end
171
172
 
172
173
  # Get backend-specific registration
173
174
  reg = all_backends[backend_type]
174
175
 
175
176
  # If Citrus backend is active
176
177
  if backend_type == :citrus
177
- if reg && reg[:grammar_module]
178
- return Backends::Citrus::Language.new(reg[:grammar_module])
179
- end
178
+ return Backends::Citrus::Language.new(reg[:grammar_module]) if reg && reg[:grammar_module]
180
179
 
181
- # Fall back to error if no Citrus grammar registered
180
+ # No Citrus grammar for this language — provide actionable error
182
181
  raise NotAvailable,
183
- "Citrus backend is active but no Citrus grammar registered for :#{method_name}. " \
184
- "Either register a Citrus grammar or use a tree-sitter backend. " \
185
- "Registered backends: #{all_backends.keys.inspect}"
182
+ "No Citrus grammar registered for :#{method_name}. " \
183
+ 'This language may only be available via tree-sitter. ' \
184
+ 'Check that the correct backend is selected (current: citrus). ' \
185
+ "Registered backends for :#{method_name}: #{all_backends.keys.inspect}"
186
186
  end
187
187
 
188
188
  # If Parslet backend is active
189
189
  if backend_type == :parslet
190
- if reg && reg[:grammar_class]
191
- return Backends::Parslet::Language.new(reg[:grammar_class])
192
- end
190
+ return Backends::Parslet::Language.new(reg[:grammar_class]) if reg && reg[:grammar_class]
193
191
 
194
- # Fall back to error if no Parslet grammar registered
192
+ # No Parslet grammar for this language — provide actionable error
195
193
  raise NotAvailable,
196
- "Parslet backend is active but no Parslet grammar registered for :#{method_name}. " \
197
- "Either register a Parslet grammar or use a tree-sitter backend. " \
198
- "Registered backends: #{all_backends.keys.inspect}"
194
+ "No Parslet grammar registered for :#{method_name}. " \
195
+ 'This language may only be available via tree-sitter. ' \
196
+ 'Check that the correct backend is selected (current: parslet). ' \
197
+ "Registered backends for :#{method_name}: #{all_backends.keys.inspect}"
199
198
  end
200
199
 
201
200
  # For tree-sitter backends, try to load from path
@@ -204,16 +203,16 @@ module TreeHaver
204
203
  path = kwargs[:path] || args.first || reg[:path]
205
204
  # Symbol priority: kwargs override > registration > derive from method_name
206
205
  symbol = if kwargs.key?(:symbol)
207
- kwargs[:symbol]
208
- elsif reg[:symbol]
209
- reg[:symbol]
210
- else
211
- "tree_sitter_#{method_name}"
212
- end
206
+ kwargs[:symbol]
207
+ elsif reg[:symbol]
208
+ reg[:symbol]
209
+ else
210
+ "tree_sitter_#{method_name}"
211
+ end
213
212
  # Name priority: kwargs override > derive from symbol (strip tree_sitter_ prefix)
214
213
  # Using symbol-derived name ensures ruby_tree_sitter gets the correct language name
215
214
  # e.g., "toml" not "toml_both" when symbol is "tree_sitter_toml"
216
- name = kwargs[:name] || symbol&.sub(/\Atree_sitter_/, "")
215
+ name = kwargs[:name] || symbol&.sub(/\Atree_sitter_/, '')
217
216
 
218
217
  begin
219
218
  return from_library(path, symbol: symbol, name: name)
@@ -242,8 +241,8 @@ module TreeHaver
242
241
 
243
242
  # No appropriate registration found
244
243
  raise ArgumentError,
245
- "No grammar registered for :#{method_name} compatible with #{backend_type} backend. " \
246
- "Registered backends: #{all_backends.keys.inspect}"
244
+ "No grammar registered for :#{method_name} compatible with #{backend_type} backend. " \
245
+ "Registered backends: #{all_backends.keys.inspect}"
247
246
  end
248
247
 
249
248
  # @api private
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TreeHaver
4
+ TSLP_BACKEND = BackendReference.new(
5
+ id: 'tslp',
6
+ family: 'tree-sitter'
7
+ ).freeze
8
+ KREUZBERG_LANGUAGE_PACK_BACKEND = BackendReference.new(
9
+ id: 'kreuzberg-language-pack',
10
+ family: 'tree-sitter'
11
+ ).freeze
12
+
13
+ BackendRegistry.register(TSLP_BACKEND)
14
+ BackendRegistry.register(KREUZBERG_LANGUAGE_PACK_BACKEND)
15
+ BackendRegistry.register_availability_checker(:tslp) do
16
+ Backends::Tslp.available?
17
+ end
18
+ BackendRegistry.register_availability_checker(:"kreuzberg-language-pack") do
19
+ BackendRegistry.available?(:tslp)
20
+ end
21
+
22
+ module_function
23
+
24
+ def language_pack_adapter_info
25
+ AdapterInfo.new(
26
+ backend: TSLP_BACKEND.id,
27
+ backend_ref: TSLP_BACKEND,
28
+ supports_dialects: false,
29
+ supported_policies: []
30
+ )
31
+ end
32
+
33
+ def language_pack_feature_profile
34
+ FeatureProfile.new(
35
+ backend: TSLP_BACKEND.id,
36
+ backend_ref: TSLP_BACKEND,
37
+ supports_dialects: false,
38
+ supported_policies: []
39
+ )
40
+ end
41
+ end
@@ -26,7 +26,7 @@ module TreeHaver
26
26
  #
27
27
  # Registration structure:
28
28
  # ```ruby
29
- # @registrations = {
29
+ # registrations = {
30
30
  # toml: {
31
31
  # tree_sitter: { path: "/path/to/lib.so", symbol: "tree_sitter_toml" },
32
32
  # citrus: { grammar_module: TomlRB::Document, gem_name: "toml-rb" }
@@ -69,8 +69,8 @@ module TreeHaver
69
69
  # @api private
70
70
  module LanguageRegistry
71
71
  @mutex = Mutex.new
72
- @cache = {} # rubocop:disable ThreadSafety/MutableClassInstanceVariable
73
- @registrations = {} # rubocop:disable ThreadSafety/MutableClassInstanceVariable
72
+ @cache = {}
73
+ @registrations = {}
74
74
 
75
75
  module_function
76
76
 
@@ -108,6 +108,36 @@ module TreeHaver
108
108
  nil
109
109
  end
110
110
 
111
+ def with_registration(name, backend_type, **config)
112
+ key = name.to_sym
113
+ backend_key = backend_type.to_sym
114
+ original = nil
115
+ had_language = false
116
+ had_backend = false
117
+
118
+ @mutex.synchronize do
119
+ had_language = @registrations.key?(key)
120
+ had_backend = @registrations.fetch(key, {}).key?(backend_key)
121
+ original = @registrations.fetch(key, {})[backend_key]&.dup
122
+ @registrations[key] ||= {}
123
+ @registrations[key][backend_key] = config.compact
124
+ @cache.clear
125
+ end
126
+
127
+ yield
128
+ ensure
129
+ @mutex.synchronize do
130
+ if had_backend
131
+ @registrations[key][backend_key] = original
132
+ elsif had_language
133
+ @registrations[key].delete(backend_key)
134
+ else
135
+ @registrations.delete(key)
136
+ end
137
+ @cache.clear
138
+ end
139
+ end
140
+
111
141
  # Fetch registration entries for a language
112
142
  #
113
143
  # Returns all backend-specific configurations for a language.
@@ -153,6 +183,7 @@ module TreeHaver
153
183
  def fetch(key)
154
184
  @mutex.synchronize do
155
185
  return @cache[key] if @cache.key?(key)
186
+
156
187
  value = yield
157
188
  @cache[key] = value
158
189
  end