tree_haver 7.0.0 → 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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/LICENSE.md +13 -0
  4. data/README.md +1959 -0
  5. data/lib/tree_haver/backend_api.rb +392 -0
  6. data/lib/tree_haver/backend_registry.rb +153 -3
  7. data/lib/tree_haver/backends/citrus.rb +490 -0
  8. data/lib/tree_haver/backends/ffi.rb +1013 -0
  9. data/lib/tree_haver/backends/java.rb +909 -0
  10. data/lib/tree_haver/backends/mri.rb +367 -0
  11. data/lib/tree_haver/backends/parslet.rb +565 -0
  12. data/lib/tree_haver/backends/prism.rb +568 -0
  13. data/lib/tree_haver/backends/psych.rb +379 -0
  14. data/lib/tree_haver/backends/rust.rb +243 -0
  15. data/lib/tree_haver/backends/tslp.rb +274 -0
  16. data/lib/tree_haver/base/comment.rb +320 -0
  17. data/lib/tree_haver/base/language.rb +98 -0
  18. data/lib/tree_haver/base/node.rb +330 -0
  19. data/lib/tree_haver/base/parser.rb +28 -0
  20. data/lib/tree_haver/base/point.rb +48 -0
  21. data/lib/tree_haver/base/tree.rb +128 -0
  22. data/lib/tree_haver/citrus_grammar_finder.rb +213 -0
  23. data/lib/tree_haver/contracts.rb +661 -96
  24. data/lib/tree_haver/grammar_finder.rb +429 -0
  25. data/lib/tree_haver/kaitai_backend.rb +2 -2
  26. data/lib/tree_haver/language.rb +294 -0
  27. data/lib/tree_haver/language_pack.rb +17 -166
  28. data/lib/tree_haver/language_registry.rb +221 -0
  29. data/lib/tree_haver/library_path_utils.rb +80 -0
  30. data/lib/tree_haver/node.rb +588 -0
  31. data/lib/tree_haver/parser.rb +445 -0
  32. data/lib/tree_haver/parslet_grammar_finder.rb +217 -0
  33. data/lib/tree_haver/path_validator.rb +356 -0
  34. data/lib/tree_haver/peg_backends.rb +8 -8
  35. data/lib/tree_haver/point.rb +27 -0
  36. data/lib/tree_haver/rspec/dependency_tags.rb +56 -0
  37. data/lib/tree_haver/rspec.rb +3 -0
  38. data/lib/tree_haver/tree.rb +267 -0
  39. data/lib/tree_haver/version.rb +5 -3
  40. data/lib/tree_haver.rb +613 -8
  41. data/sig/tree_haver.rbs +6 -0
  42. data.tar.gz.sig +0 -0
  43. metadata +314 -13
  44. metadata.gz.sig +0 -0
@@ -0,0 +1,392 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TreeHaver
4
+ # Backend API contract definitions and validation
5
+ #
6
+ # This module defines the expected API surface for TreeHaver backends.
7
+ # Each backend must provide Parser, Language, Tree, and Node classes/objects
8
+ # that conform to these interfaces.
9
+ #
10
+ # == Architecture
11
+ #
12
+ # TreeHaver backends fall into two categories:
13
+ #
14
+ # 1. **Raw backends** (MRI, FFI, Rust) - Return raw tree-sitter objects
15
+ # (e.g., ::TreeSitter::Node). TreeHaver::Node wraps these and provides
16
+ # a unified API via method delegation.
17
+ #
18
+ # 2. **Wrapper backends** (Java, Citrus, Prism, Psych, Commonmarker, Markly) -
19
+ # Return their own wrapper objects that must implement the expected API
20
+ # directly, since TreeHaver::Node will delegate to them.
21
+ #
22
+ # == Usage
23
+ #
24
+ # # Validate a backend's API compliance
25
+ # TreeHaver::BackendAPI.validate!(backend_module)
26
+ #
27
+ # # Check specific class compliance
28
+ # TreeHaver::BackendAPI.validate_node!(node_instance)
29
+ #
30
+ module BackendAPI
31
+ # Descriptive levels for backend comment support.
32
+ #
33
+ # - :full - backend can expose comment nodes plus strong attachment hints
34
+ # - :partial - backend can expose comments, but ownership/attachment is incomplete
35
+ # - :nodes_only - backend can surface comment nodes/tokens only
36
+ # - :none - backend does not expose comments through TreeHaver
37
+ COMMENT_SUPPORT_LEVELS = %i[
38
+ full
39
+ partial
40
+ nodes_only
41
+ none
42
+ ].freeze
43
+
44
+ # Required methods for Language class/instances
45
+ #
46
+ # All backends MUST implement `from_library` for API consistency.
47
+ # Language-specific backends (Psych, Prism, Commonmarker, Markly) should
48
+ # implement `from_library` to accept (and ignore) path/symbol parameters,
49
+ # returning their single supported language.
50
+ #
51
+ # This ensures `TreeHaver.parser_for(:yaml)` works regardless of backend -
52
+ # tree-sitter backends load the YAML grammar, while Psych returns its
53
+ # built-in YAML support.
54
+ #
55
+ # Convenience methods (yaml, ruby, markdown) are OPTIONAL and only make
56
+ # sense on backends that only support one language family.
57
+ LANGUAGE_CLASS_METHODS = %i[
58
+ from_library
59
+ ].freeze
60
+
61
+ # Optional convenience methods for language-specific backends
62
+ # These are NOT required - they're just shortcuts for single-language backends
63
+ LANGUAGE_OPTIONAL_CLASS_METHODS = %i[
64
+ yaml
65
+ ruby
66
+ markdown
67
+ ].freeze
68
+
69
+ LANGUAGE_INSTANCE_METHODS = %i[
70
+ backend
71
+ ].freeze
72
+
73
+ # Required methods for Parser class/instances
74
+ PARSER_CLASS_METHODS = %i[
75
+ new
76
+ ].freeze
77
+
78
+ PARSER_INSTANCE_METHODS = %i[
79
+ language=
80
+ parse
81
+ ].freeze
82
+
83
+ # Optional Parser methods (for incremental parsing)
84
+ PARSER_OPTIONAL_METHODS = %i[
85
+ parse_string
86
+ ].freeze
87
+
88
+ # Required methods for Tree instances
89
+ # Note: Tree is returned by Parser#parse, not instantiated directly
90
+ TREE_INSTANCE_METHODS = %i[
91
+ root_node
92
+ ].freeze
93
+
94
+ # Optional Tree methods (for incremental parsing)
95
+ TREE_OPTIONAL_METHODS = %i[
96
+ edit
97
+ ].freeze
98
+
99
+ # Required methods for Node instances returned by wrapper backends
100
+ # These are the methods TreeHaver::Node delegates to inner_node
101
+ #
102
+ # Raw backends (MRI, FFI, Rust) return tree-sitter native nodes which
103
+ # have their own API. TreeHaver::Node handles the translation.
104
+ #
105
+ # Wrapper backends (Java, Citrus, etc.) must implement these methods
106
+ # on their Node class since TreeHaver::Node delegates to them.
107
+ NODE_INSTANCE_METHODS = %i[
108
+ type
109
+ child_count
110
+ child
111
+ start_byte
112
+ end_byte
113
+ ].freeze
114
+
115
+ # Optional Node methods - should return nil if not supported
116
+ NODE_OPTIONAL_METHODS = %i[
117
+ parent
118
+ next_sibling
119
+ prev_sibling
120
+ named?
121
+ has_error?
122
+ missing?
123
+ text
124
+ child_by_field_name
125
+ start_point
126
+ end_point
127
+ ].freeze
128
+
129
+ # Methods that have common aliases across backends
130
+ NODE_ALIASES = {
131
+ type: %i[kind],
132
+ named?: %i[is_named? is_named],
133
+ has_error?: %i[has_error],
134
+ missing?: %i[is_missing? is_missing],
135
+ next_sibling: %i[next_named_sibling],
136
+ prev_sibling: %i[previous_sibling previous_named_sibling prev_named_sibling]
137
+ }.freeze
138
+
139
+ class << self
140
+ # Validate a backend module for API compliance
141
+ #
142
+ # @param backend_module [Module] The backend module (e.g., TreeHaver::Backends::Java)
143
+ # @param strict [Boolean] If true, raise on missing optional methods
144
+ # @return [Hash] Validation results with :valid, :errors, :warnings keys
145
+ def validate(backend_module, strict: false)
146
+ results = {
147
+ valid: true,
148
+ errors: [],
149
+ warnings: [],
150
+ capabilities: {}
151
+ }
152
+
153
+ # Check module-level methods
154
+ validate_module_methods(backend_module, results)
155
+
156
+ # Check Language class
157
+ if backend_module.const_defined?(:Language)
158
+ validate_language(backend_module::Language, results)
159
+ else
160
+ results[:errors] << 'Missing Language class'
161
+ results[:valid] = false
162
+ end
163
+
164
+ # Check Parser class
165
+ if backend_module.const_defined?(:Parser)
166
+ validate_parser(backend_module::Parser, results)
167
+ else
168
+ results[:errors] << 'Missing Parser class'
169
+ results[:valid] = false
170
+ end
171
+
172
+ # Check Tree class if present (some backends return raw trees)
173
+ if backend_module.const_defined?(:Tree)
174
+ validate_tree(backend_module::Tree, results)
175
+ else
176
+ results[:warnings] << 'No Tree class (backend returns raw trees)'
177
+ end
178
+
179
+ # Check Node class if present (wrapper backends)
180
+ if backend_module.const_defined?(:Node)
181
+ validate_node_class(backend_module::Node, results, strict: strict)
182
+ else
183
+ results[:warnings] << 'No Node class (backend returns raw nodes, TreeHaver::Node will wrap)'
184
+ end
185
+
186
+ # Fail on warnings in strict mode
187
+ results[:valid] = false if strict && results[:warnings].any?
188
+
189
+ results
190
+ end
191
+
192
+ # Validate and raise on failure
193
+ #
194
+ # @param backend_module [Module] The backend module to validate
195
+ # @param strict [Boolean] If true, treat warnings as errors
196
+ # @raise [TreeHaver::Error] if validation fails
197
+ # @return [Hash] Validation results if valid
198
+ def validate!(backend_module, strict: false)
199
+ results = validate(backend_module, strict: strict)
200
+ unless results[:valid]
201
+ raise TreeHaver::Error,
202
+ "Backend #{backend_module.name} API validation failed:\n " \
203
+ "Errors: #{results[:errors].join(', ')}\n " \
204
+ "Warnings: #{results[:warnings].join(', ')}"
205
+ end
206
+ results
207
+ end
208
+
209
+ # Validate a Node instance for API compliance
210
+ #
211
+ # @param node [Object] A node instance to validate
212
+ # @return [Hash] Validation results
213
+ def validate_node_instance(node)
214
+ results = {
215
+ valid: true,
216
+ errors: [],
217
+ warnings: [],
218
+ supported_methods: [],
219
+ unsupported_methods: []
220
+ }
221
+
222
+ # Check required methods
223
+ NODE_INSTANCE_METHODS.each do |method|
224
+ if responds_to_with_aliases?(node, method)
225
+ results[:supported_methods] << method
226
+ else
227
+ results[:errors] << "Missing required method: #{method}"
228
+ results[:valid] = false
229
+ end
230
+ end
231
+
232
+ # Check optional methods
233
+ NODE_OPTIONAL_METHODS.each do |method|
234
+ if responds_to_with_aliases?(node, method)
235
+ results[:supported_methods] << method
236
+ else
237
+ results[:unsupported_methods] << method
238
+ results[:warnings] << "Missing optional method: #{method}"
239
+ end
240
+ end
241
+
242
+ results
243
+ end
244
+
245
+ private
246
+
247
+ def validate_module_methods(mod, results)
248
+ unless mod.singleton_class.method_defined?(:available?)
249
+ results[:errors] << 'Missing module method: available?'
250
+ results[:valid] = false
251
+ end
252
+
253
+ unless mod.singleton_class.method_defined?(:capabilities)
254
+ results[:warnings] << 'Missing module method: capabilities'
255
+ return
256
+ end
257
+
258
+ validate_capabilities_hash(mod.capabilities, results)
259
+ end
260
+
261
+ def validate_capabilities_hash(capabilities, results)
262
+ return if capabilities.nil? || capabilities.empty?
263
+
264
+ unless capabilities.is_a?(Hash)
265
+ results[:errors] << 'Backend capabilities must return a Hash'
266
+ results[:valid] = false
267
+ return
268
+ end
269
+
270
+ comment_support = capabilities[:comment_support]
271
+ if comment_support.nil?
272
+ results[:warnings] << 'Capabilities missing :comment_support'
273
+ return
274
+ end
275
+
276
+ unless COMMENT_SUPPORT_LEVELS.include?(comment_support)
277
+ results[:errors] << "Invalid :comment_support #{comment_support.inspect}; expected one of #{COMMENT_SUPPORT_LEVELS.inspect}"
278
+ results[:valid] = false
279
+ return
280
+ end
281
+
282
+ results[:capabilities][:comment_support] = comment_support
283
+
284
+ return unless capabilities.key?(:comment_attachment_hints)
285
+
286
+ attachment_hints = capabilities[:comment_attachment_hints]
287
+ unless [true, false].include?(attachment_hints)
288
+ results[:errors] << "Invalid :comment_attachment_hints #{attachment_hints.inspect}; expected true or false"
289
+ results[:valid] = false
290
+ return
291
+ end
292
+
293
+ results[:capabilities][:comment_attachment_hints] = attachment_hints
294
+ end
295
+
296
+ def validate_language(klass, results)
297
+ # from_library is REQUIRED for all backends
298
+ # Language-specific backends should implement it to ignore path/symbol
299
+ # and return their single language (for API consistency)
300
+ unless klass.singleton_class.method_defined?(:from_library)
301
+ results[:errors] << 'Language missing required class method: from_library'
302
+ results[:valid] = false
303
+ end
304
+
305
+ # Check for optional convenience methods
306
+ optional_methods = LANGUAGE_OPTIONAL_CLASS_METHODS.select { |m| klass.singleton_class.method_defined?(m) }
307
+ results[:capabilities][:language_shortcuts] = optional_methods if optional_methods.any?
308
+
309
+ results[:capabilities][:language] = {
310
+ class_methods: LANGUAGE_CLASS_METHODS.select { |m| klass.singleton_class.method_defined?(m) } +
311
+ optional_methods
312
+ }
313
+ end
314
+
315
+ def validate_parser(klass, results)
316
+ PARSER_CLASS_METHODS.each do |method|
317
+ unless klass.singleton_class.method_defined?(method)
318
+ results[:errors] << "Parser missing class method: #{method}"
319
+ results[:valid] = false
320
+ end
321
+ end
322
+
323
+ # Check instance methods by inspecting the class
324
+ PARSER_INSTANCE_METHODS.each do |method|
325
+ unless klass.method_defined?(method) || klass.private_method_defined?(method)
326
+ results[:errors] << "Parser missing instance method: #{method}"
327
+ results[:valid] = false
328
+ end
329
+ end
330
+
331
+ PARSER_OPTIONAL_METHODS.each do |method|
332
+ results[:warnings] << "Parser missing optional method: #{method}" unless klass.method_defined?(method)
333
+ end
334
+ end
335
+
336
+ def validate_tree(klass, results)
337
+ TREE_INSTANCE_METHODS.each do |method|
338
+ unless klass.method_defined?(method)
339
+ results[:errors] << "Tree missing instance method: #{method}"
340
+ results[:valid] = false
341
+ end
342
+ end
343
+
344
+ TREE_OPTIONAL_METHODS.each do |method|
345
+ results[:warnings] << "Tree missing optional method: #{method}" unless klass.method_defined?(method)
346
+ end
347
+ end
348
+
349
+ def validate_node_class(klass, results, strict: false)
350
+ NODE_INSTANCE_METHODS.each do |method|
351
+ unless has_method_or_alias?(klass, method)
352
+ results[:errors] << "Node missing required method: #{method}"
353
+ results[:valid] = false
354
+ end
355
+ end
356
+
357
+ NODE_OPTIONAL_METHODS.each do |method|
358
+ next if has_method_or_alias?(klass, method)
359
+
360
+ msg = "Node missing optional method: #{method}"
361
+ if strict
362
+ results[:errors] << msg
363
+ results[:valid] = false
364
+ else
365
+ results[:warnings] << msg
366
+ end
367
+ end
368
+
369
+ results[:capabilities][:node] = {
370
+ required: NODE_INSTANCE_METHODS.select { |m| has_method_or_alias?(klass, m) },
371
+ optional: NODE_OPTIONAL_METHODS.select { |m| has_method_or_alias?(klass, m) }
372
+ }
373
+ end
374
+
375
+ def has_method_or_alias?(klass, method)
376
+ return true if klass.method_defined?(method)
377
+
378
+ # Check aliases
379
+ aliases = NODE_ALIASES[method] || []
380
+ aliases.any? { |alt| klass.method_defined?(alt) }
381
+ end
382
+
383
+ def responds_to_with_aliases?(obj, method)
384
+ return true if obj.respond_to?(method)
385
+
386
+ # Check aliases
387
+ aliases = NODE_ALIASES[method] || []
388
+ aliases.any? { |alt| obj.respond_to?(alt) }
389
+ end
390
+ end
391
+ end
392
+ end
@@ -2,6 +2,8 @@
2
2
 
3
3
  module TreeHaver
4
4
  module BackendRegistry
5
+ CATEGORIES = %i[backend gem parsing grammar engine capability other].freeze
6
+
5
7
  module_function
6
8
 
7
9
  def register(backend)
@@ -22,9 +24,142 @@ module TreeHaver
22
24
  end
23
25
  end
24
26
 
27
+ def register_availability_checker(name, checker = nil, &block)
28
+ callable = checker || block
29
+ raise ArgumentError, 'Must provide a checker callable or block' unless callable
30
+ raise ArgumentError, 'Checker must respond to #call' unless callable.respond_to?(:call)
31
+
32
+ mutex.synchronize do
33
+ availability_checkers[name.to_sym] = callable
34
+ availability_cache.delete(name.to_sym)
35
+ end
36
+ nil
37
+ end
38
+
39
+ def available?(name)
40
+ key = name.to_sym
41
+ checker = mutex.synchronize do
42
+ return availability_cache[key] if availability_cache.key?(key)
43
+
44
+ availability_checkers[key]
45
+ end
46
+ return false unless checker
47
+
48
+ result = checker.call ? true : false
49
+ mutex.synchronize { availability_cache[key] = result }
50
+ result
51
+ rescue StandardError
52
+ false
53
+ end
54
+
55
+ def register_tag(tag_name, category:, backend_name: nil, require_path: nil, checker: nil, &block)
56
+ callable = checker || block
57
+ raise ArgumentError, 'Must provide a checker callable or block' unless callable
58
+ raise ArgumentError, 'Checker must respond to #call' unless callable.respond_to?(:call)
59
+ raise ArgumentError, "Invalid category: #{category}" unless CATEGORIES.include?(category)
60
+
61
+ tag = tag_name.to_sym
62
+ backend = backend_name || inferred_backend_name(tag)
63
+
64
+ mutex.synchronize do
65
+ tag_registry[tag] = {
66
+ category: category,
67
+ backend_name: backend.to_sym,
68
+ require_path: require_path,
69
+ checker: callable
70
+ }
71
+ availability_checkers[backend.to_sym] = callable
72
+ availability_cache.delete(backend.to_sym)
73
+ end
74
+
75
+ define_availability_method(backend.to_sym, tag)
76
+ nil
77
+ end
78
+
79
+ def registered?(name)
80
+ mutex.synchronize { availability_checkers.key?(name.to_sym) }
81
+ end
82
+
83
+ def registered_backends
84
+ mutex.synchronize { availability_checkers.keys.dup }
85
+ end
86
+
87
+ def registered_tags
88
+ mutex.synchronize { tag_registry.keys.dup }
89
+ end
90
+
91
+ def tags_by_category(category)
92
+ mutex.synchronize do
93
+ tag_registry.select { |_, metadata| metadata[:category] == category }.keys
94
+ end
95
+ end
96
+
97
+ def tag_metadata(tag_name)
98
+ mutex.synchronize { tag_registry[tag_name.to_sym]&.dup }
99
+ end
100
+
101
+ def tag_registered?(tag_name)
102
+ mutex.synchronize { tag_registry.key?(tag_name.to_sym) }
103
+ end
104
+
105
+ def tag_available?(tag_name)
106
+ tag = tag_name.to_sym
107
+ metadata = mutex.synchronize { tag_registry[tag]&.dup }
108
+ return available?(inferred_backend_name(tag)) unless metadata
109
+
110
+ if metadata[:require_path]
111
+ begin
112
+ require metadata[:require_path]
113
+ rescue LoadError
114
+ return false
115
+ end
116
+ end
117
+
118
+ available?(metadata[:backend_name])
119
+ end
120
+
121
+ def tag_summary
122
+ registered_tags.each_with_object({}) do |tag, summary|
123
+ summary[tag] = tag_available?(tag)
124
+ end
125
+ end
126
+
127
+ def clear_cache!
128
+ mutex.synchronize { availability_cache.clear }
129
+ nil
130
+ end
131
+
25
132
  def clear!
26
- mutex.synchronize { backends.clear }
133
+ mutex.synchronize do
134
+ backends.clear
135
+ availability_checkers.clear
136
+ availability_cache.clear
137
+ tag_registry.clear
138
+ end
139
+ end
140
+
141
+ def inferred_backend_name(tag_name)
142
+ tag = tag_name.to_s
143
+ tag = tag.delete_suffix('_backend')
144
+ tag.to_sym
27
145
  end
146
+ private_class_method :inferred_backend_name
147
+
148
+ def define_availability_method(backend_name, tag_name)
149
+ return unless defined?(TreeHaver::RSpec::DependencyTags)
150
+
151
+ deps = TreeHaver::RSpec::DependencyTags
152
+ method_name = :"#{backend_name}_available?"
153
+ return if deps.respond_to?(method_name)
154
+
155
+ ivar = :"@#{backend_name}_available"
156
+ deps.define_singleton_method(method_name) do
157
+ return instance_variable_get(ivar) if instance_variable_defined?(ivar)
158
+
159
+ instance_variable_set(ivar, TreeHaver::BackendRegistry.tag_available?(tag_name))
160
+ end
161
+ end
162
+ private_class_method :define_availability_method
28
163
 
29
164
  def deep_dup(value)
30
165
  Marshal.load(Marshal.dump(value))
@@ -32,13 +167,28 @@ module TreeHaver
32
167
  private_class_method :deep_dup
33
168
 
34
169
  def backends
35
- @backends ||= {} # rubocop:disable ThreadSafety/MutableClassInstanceVariable
170
+ @backends ||= {}
36
171
  end
37
172
  private_class_method :backends
38
173
 
39
174
  def mutex
40
- @mutex ||= Mutex.new # rubocop:disable ThreadSafety/MutableClassInstanceVariable
175
+ @mutex ||= Mutex.new
41
176
  end
42
177
  private_class_method :mutex
178
+
179
+ def availability_checkers
180
+ @availability_checkers ||= {}
181
+ end
182
+ private_class_method :availability_checkers
183
+
184
+ def availability_cache
185
+ @availability_cache ||= {}
186
+ end
187
+ private_class_method :availability_cache
188
+
189
+ def tag_registry
190
+ @tag_registry ||= {}
191
+ end
192
+ private_class_method :tag_registry
43
193
  end
44
194
  end