tree_haver 3.2.3 → 3.2.4

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.
@@ -0,0 +1,349 @@
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
+ # Required methods for Language class/instances
32
+ #
33
+ # All backends MUST implement `from_library` for API consistency.
34
+ # Language-specific backends (Psych, Prism, Commonmarker, Markly) should
35
+ # implement `from_library` to accept (and ignore) path/symbol parameters,
36
+ # returning their single supported language.
37
+ #
38
+ # This ensures `TreeHaver.parser_for(:yaml)` works regardless of backend -
39
+ # tree-sitter backends load the YAML grammar, while Psych returns its
40
+ # built-in YAML support.
41
+ #
42
+ # Convenience methods (yaml, ruby, markdown) are OPTIONAL and only make
43
+ # sense on backends that only support one language family.
44
+ LANGUAGE_CLASS_METHODS = %i[
45
+ from_library
46
+ ].freeze
47
+
48
+ # Optional convenience methods for language-specific backends
49
+ # These are NOT required - they're just shortcuts for single-language backends
50
+ LANGUAGE_OPTIONAL_CLASS_METHODS = %i[
51
+ yaml
52
+ ruby
53
+ markdown
54
+ ].freeze
55
+
56
+ LANGUAGE_INSTANCE_METHODS = %i[
57
+ backend
58
+ ].freeze
59
+
60
+ # Required methods for Parser class/instances
61
+ PARSER_CLASS_METHODS = %i[
62
+ new
63
+ ].freeze
64
+
65
+ PARSER_INSTANCE_METHODS = %i[
66
+ language=
67
+ parse
68
+ ].freeze
69
+
70
+ # Optional Parser methods (for incremental parsing)
71
+ PARSER_OPTIONAL_METHODS = %i[
72
+ parse_string
73
+ ].freeze
74
+
75
+ # Required methods for Tree instances
76
+ # Note: Tree is returned by Parser#parse, not instantiated directly
77
+ TREE_INSTANCE_METHODS = %i[
78
+ root_node
79
+ ].freeze
80
+
81
+ # Optional Tree methods (for incremental parsing)
82
+ TREE_OPTIONAL_METHODS = %i[
83
+ edit
84
+ ].freeze
85
+
86
+ # Required methods for Node instances returned by wrapper backends
87
+ # These are the methods TreeHaver::Node delegates to inner_node
88
+ #
89
+ # Raw backends (MRI, FFI, Rust) return tree-sitter native nodes which
90
+ # have their own API. TreeHaver::Node handles the translation.
91
+ #
92
+ # Wrapper backends (Java, Citrus, etc.) must implement these methods
93
+ # on their Node class since TreeHaver::Node delegates to them.
94
+ NODE_INSTANCE_METHODS = %i[
95
+ type
96
+ child_count
97
+ child
98
+ start_byte
99
+ end_byte
100
+ ].freeze
101
+
102
+ # Optional Node methods - should return nil if not supported
103
+ NODE_OPTIONAL_METHODS = %i[
104
+ parent
105
+ next_sibling
106
+ prev_sibling
107
+ named?
108
+ has_error?
109
+ missing?
110
+ text
111
+ child_by_field_name
112
+ start_point
113
+ end_point
114
+ ].freeze
115
+
116
+ # Methods that have common aliases across backends
117
+ NODE_ALIASES = {
118
+ type: %i[kind],
119
+ named?: %i[is_named? is_named],
120
+ has_error?: %i[has_error],
121
+ missing?: %i[is_missing? is_missing],
122
+ next_sibling: %i[next_named_sibling],
123
+ prev_sibling: %i[previous_sibling previous_named_sibling prev_named_sibling],
124
+ }.freeze
125
+
126
+ class << self
127
+ # Validate a backend module for API compliance
128
+ #
129
+ # @param backend_module [Module] The backend module (e.g., TreeHaver::Backends::Java)
130
+ # @param strict [Boolean] If true, raise on missing optional methods
131
+ # @return [Hash] Validation results with :valid, :errors, :warnings keys
132
+ def validate(backend_module, strict: false)
133
+ results = {
134
+ valid: true,
135
+ errors: [],
136
+ warnings: [],
137
+ capabilities: {},
138
+ }
139
+
140
+ # Check module-level methods
141
+ validate_module_methods(backend_module, results)
142
+
143
+ # Check Language class
144
+ if backend_module.const_defined?(:Language)
145
+ validate_language(backend_module::Language, results)
146
+ else
147
+ results[:errors] << "Missing Language class"
148
+ results[:valid] = false
149
+ end
150
+
151
+ # Check Parser class
152
+ if backend_module.const_defined?(:Parser)
153
+ validate_parser(backend_module::Parser, results)
154
+ else
155
+ results[:errors] << "Missing Parser class"
156
+ results[:valid] = false
157
+ end
158
+
159
+ # Check Tree class if present (some backends return raw trees)
160
+ if backend_module.const_defined?(:Tree)
161
+ validate_tree(backend_module::Tree, results)
162
+ else
163
+ results[:warnings] << "No Tree class (backend returns raw trees)"
164
+ end
165
+
166
+ # Check Node class if present (wrapper backends)
167
+ if backend_module.const_defined?(:Node)
168
+ validate_node_class(backend_module::Node, results, strict: strict)
169
+ else
170
+ results[:warnings] << "No Node class (backend returns raw nodes, TreeHaver::Node will wrap)"
171
+ end
172
+
173
+ # Fail on warnings in strict mode
174
+ if strict && results[:warnings].any?
175
+ results[:valid] = false
176
+ end
177
+
178
+ results
179
+ end
180
+
181
+ # Validate and raise on failure
182
+ #
183
+ # @param backend_module [Module] The backend module to validate
184
+ # @param strict [Boolean] If true, treat warnings as errors
185
+ # @raise [TreeHaver::Error] if validation fails
186
+ # @return [Hash] Validation results if valid
187
+ def validate!(backend_module, strict: false)
188
+ results = validate(backend_module, strict: strict)
189
+ unless results[:valid]
190
+ raise TreeHaver::Error,
191
+ "Backend #{backend_module.name} API validation failed:\n " \
192
+ "Errors: #{results[:errors].join(", ")}\n " \
193
+ "Warnings: #{results[:warnings].join(", ")}"
194
+ end
195
+ results
196
+ end
197
+
198
+ # Validate a Node instance for API compliance
199
+ #
200
+ # @param node [Object] A node instance to validate
201
+ # @return [Hash] Validation results
202
+ def validate_node_instance(node)
203
+ results = {
204
+ valid: true,
205
+ errors: [],
206
+ warnings: [],
207
+ supported_methods: [],
208
+ unsupported_methods: [],
209
+ }
210
+
211
+ # Check required methods
212
+ NODE_INSTANCE_METHODS.each do |method|
213
+ if responds_to_with_aliases?(node, method)
214
+ results[:supported_methods] << method
215
+ else
216
+ results[:errors] << "Missing required method: #{method}"
217
+ results[:valid] = false
218
+ end
219
+ end
220
+
221
+ # Check optional methods
222
+ NODE_OPTIONAL_METHODS.each do |method|
223
+ if responds_to_with_aliases?(node, method)
224
+ results[:supported_methods] << method
225
+ else
226
+ results[:unsupported_methods] << method
227
+ results[:warnings] << "Missing optional method: #{method}"
228
+ end
229
+ end
230
+
231
+ results
232
+ end
233
+
234
+ private
235
+
236
+ def validate_module_methods(mod, results)
237
+ unless mod.respond_to?(:available?)
238
+ results[:errors] << "Missing module method: available?"
239
+ results[:valid] = false
240
+ end
241
+
242
+ unless mod.respond_to?(:capabilities)
243
+ results[:warnings] << "Missing module method: capabilities"
244
+ end
245
+ end
246
+
247
+ def validate_language(klass, results)
248
+ # from_library is REQUIRED for all backends
249
+ # Language-specific backends should implement it to ignore path/symbol
250
+ # and return their single language (for API consistency)
251
+ unless klass.respond_to?(:from_library)
252
+ results[:errors] << "Language missing required class method: from_library"
253
+ results[:valid] = false
254
+ end
255
+
256
+ # Check for optional convenience methods
257
+ optional_methods = LANGUAGE_OPTIONAL_CLASS_METHODS.select { |m| klass.respond_to?(m) }
258
+ if optional_methods.any?
259
+ results[:capabilities][:language_shortcuts] = optional_methods
260
+ end
261
+
262
+ results[:capabilities][:language] = {
263
+ class_methods: LANGUAGE_CLASS_METHODS.select { |m| klass.respond_to?(m) } +
264
+ optional_methods,
265
+ }
266
+ end
267
+
268
+ def validate_parser(klass, results)
269
+ PARSER_CLASS_METHODS.each do |method|
270
+ unless klass.respond_to?(method)
271
+ results[:errors] << "Parser missing class method: #{method}"
272
+ results[:valid] = false
273
+ end
274
+ end
275
+
276
+ # Check instance methods by inspecting the class
277
+ PARSER_INSTANCE_METHODS.each do |method|
278
+ unless klass.instance_methods.include?(method) || klass.private_instance_methods.include?(method)
279
+ results[:errors] << "Parser missing instance method: #{method}"
280
+ results[:valid] = false
281
+ end
282
+ end
283
+
284
+ PARSER_OPTIONAL_METHODS.each do |method|
285
+ unless klass.instance_methods.include?(method)
286
+ results[:warnings] << "Parser missing optional method: #{method}"
287
+ end
288
+ end
289
+ end
290
+
291
+ def validate_tree(klass, results)
292
+ TREE_INSTANCE_METHODS.each do |method|
293
+ unless klass.instance_methods.include?(method)
294
+ results[:errors] << "Tree missing instance method: #{method}"
295
+ results[:valid] = false
296
+ end
297
+ end
298
+
299
+ TREE_OPTIONAL_METHODS.each do |method|
300
+ unless klass.instance_methods.include?(method)
301
+ results[:warnings] << "Tree missing optional method: #{method}"
302
+ end
303
+ end
304
+ end
305
+
306
+ def validate_node_class(klass, results, strict: false)
307
+ NODE_INSTANCE_METHODS.each do |method|
308
+ unless has_method_or_alias?(klass, method)
309
+ results[:errors] << "Node missing required method: #{method}"
310
+ results[:valid] = false
311
+ end
312
+ end
313
+
314
+ NODE_OPTIONAL_METHODS.each do |method|
315
+ unless has_method_or_alias?(klass, method)
316
+ msg = "Node missing optional method: #{method}"
317
+ if strict
318
+ results[:errors] << msg
319
+ results[:valid] = false
320
+ else
321
+ results[:warnings] << msg
322
+ end
323
+ end
324
+ end
325
+
326
+ results[:capabilities][:node] = {
327
+ required: NODE_INSTANCE_METHODS.select { |m| has_method_or_alias?(klass, m) },
328
+ optional: NODE_OPTIONAL_METHODS.select { |m| has_method_or_alias?(klass, m) },
329
+ }
330
+ end
331
+
332
+ def has_method_or_alias?(klass, method)
333
+ return true if klass.instance_methods.include?(method)
334
+
335
+ # Check aliases
336
+ aliases = NODE_ALIASES[method] || []
337
+ aliases.any? { |alt| klass.instance_methods.include?(alt) }
338
+ end
339
+
340
+ def responds_to_with_aliases?(obj, method)
341
+ return true if obj.respond_to?(method)
342
+
343
+ # Check aliases
344
+ aliases = NODE_ALIASES[method] || []
345
+ aliases.any? { |alt| obj.respond_to?(alt) }
346
+ end
347
+ end
348
+ end
349
+ end
@@ -131,17 +131,46 @@ module TreeHaver
131
131
  # Alias eql? to ==
132
132
  alias_method :eql?, :==
133
133
 
134
- # Not applicable for Citrus (tree-sitter-specific)
134
+ # Load language from library path (API compatibility)
135
135
  #
136
- # Citrus grammars are Ruby modules, not shared libraries.
137
- # This method exists for API compatibility but will raise an error.
136
+ # Citrus grammars are Ruby modules, not shared libraries. This method
137
+ # provides API compatibility with tree-sitter backends by looking up
138
+ # registered Citrus grammars by name.
138
139
  #
139
- # @raise [TreeHaver::NotAvailable] always raises
140
+ # For full API consistency, register a Citrus grammar with:
141
+ # TreeHaver.register_language(:toml, grammar_module: TomlRB::Document)
142
+ #
143
+ # Then this method will find it when called via `TreeHaver.parser_for(:toml)`.
144
+ #
145
+ # @param path [String, nil] Ignored for Citrus (used to derive language name)
146
+ # @param symbol [String, nil] Used to derive language name if path not provided
147
+ # @param name [String, Symbol, nil] Language name to look up
148
+ # @return [Language] Citrus language wrapper
149
+ # @raise [TreeHaver::NotAvailable] if no Citrus grammar is registered for the language
140
150
  class << self
141
- def from_library(path, symbol: nil, name: nil)
142
- raise TreeHaver::NotAvailable,
143
- "Citrus backend doesn't use shared libraries. " \
144
- "Use Citrus::Language.new(GrammarModule) instead."
151
+ def from_library(path = nil, symbol: nil, name: nil)
152
+ # Derive language name from path, symbol, or explicit name
153
+ lang_name = name&.to_sym ||
154
+ symbol&.to_s&.sub(/^tree_sitter_/, "")&.to_sym ||
155
+ path && TreeHaver::LibraryPathUtils.derive_language_name_from_path(path)&.to_sym
156
+
157
+ unless lang_name
158
+ raise TreeHaver::NotAvailable,
159
+ "Citrus backend requires a language name. " \
160
+ "Provide name: parameter or register a grammar with TreeHaver.register_language."
161
+ end
162
+
163
+ # Look up registered Citrus grammar
164
+ registration = TreeHaver::LanguageRegistry.registered(lang_name, :citrus)
165
+
166
+ unless registration
167
+ raise TreeHaver::NotAvailable,
168
+ "No Citrus grammar registered for #{lang_name.inspect}. " \
169
+ "Register one with: TreeHaver.register_language(:#{lang_name}, grammar_module: YourGrammar)"
170
+ end
171
+
172
+ grammar_module = registration[:grammar_module]
173
+ new(grammar_module)
145
174
  end
146
175
 
147
176
  alias_method :from_path, :from_library
@@ -102,6 +102,30 @@ module TreeHaver
102
102
  def markdown(options: {})
103
103
  new(:markdown, options: options)
104
104
  end
105
+
106
+ # Load language from library path (API compatibility)
107
+ #
108
+ # Commonmarker only supports Markdown, so path and symbol parameters are ignored.
109
+ # This method exists for API consistency with tree-sitter backends,
110
+ # allowing `TreeHaver.parser_for(:markdown)` to work regardless of backend.
111
+ #
112
+ # @param _path [String] Ignored - Commonmarker doesn't load external grammars
113
+ # @param symbol [String, nil] Ignored
114
+ # @param name [String, nil] Language name hint (defaults to :markdown)
115
+ # @return [Language] Markdown language
116
+ # @raise [TreeHaver::NotAvailable] if requested language is not Markdown
117
+ def from_library(_path = nil, symbol: nil, name: nil)
118
+ # Derive language name from symbol if provided
119
+ lang_name = name || symbol&.to_s&.sub(/^tree_sitter_/, "")&.to_sym || :markdown
120
+
121
+ unless lang_name == :markdown
122
+ raise TreeHaver::NotAvailable,
123
+ "Commonmarker backend only supports Markdown, not #{lang_name}. " \
124
+ "Use a tree-sitter backend for #{lang_name} support."
125
+ end
126
+
127
+ markdown
128
+ end
105
129
  end
106
130
 
107
131
  # Comparison for sorting/equality