tree_haver 7.0.0 → 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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/LICENSE.md +13 -0
- data/README.md +1959 -0
- data/lib/tree_haver/backend_api.rb +392 -0
- data/lib/tree_haver/backend_registry.rb +153 -3
- data/lib/tree_haver/backends/citrus.rb +489 -0
- data/lib/tree_haver/backends/ffi.rb +1013 -0
- data/lib/tree_haver/backends/java.rb +909 -0
- data/lib/tree_haver/backends/mri.rb +367 -0
- data/lib/tree_haver/backends/parslet.rb +565 -0
- data/lib/tree_haver/backends/prism.rb +568 -0
- data/lib/tree_haver/backends/psych.rb +379 -0
- data/lib/tree_haver/backends/rust.rb +243 -0
- data/lib/tree_haver/backends/tslp.rb +274 -0
- data/lib/tree_haver/base/comment.rb +320 -0
- data/lib/tree_haver/base/language.rb +98 -0
- data/lib/tree_haver/base/node.rb +330 -0
- data/lib/tree_haver/base/parser.rb +28 -0
- data/lib/tree_haver/base/point.rb +48 -0
- data/lib/tree_haver/base/tree.rb +128 -0
- data/lib/tree_haver/citrus_grammar_finder.rb +213 -0
- data/lib/tree_haver/contracts.rb +661 -96
- data/lib/tree_haver/grammar_finder.rb +429 -0
- data/lib/tree_haver/kaitai_backend.rb +2 -2
- data/lib/tree_haver/language.rb +294 -0
- data/lib/tree_haver/language_pack.rb +17 -166
- data/lib/tree_haver/language_registry.rb +221 -0
- data/lib/tree_haver/library_path_utils.rb +80 -0
- data/lib/tree_haver/node.rb +588 -0
- data/lib/tree_haver/parser.rb +445 -0
- data/lib/tree_haver/parslet_grammar_finder.rb +217 -0
- data/lib/tree_haver/path_validator.rb +356 -0
- data/lib/tree_haver/peg_backends.rb +7 -7
- data/lib/tree_haver/point.rb +27 -0
- data/lib/tree_haver/rspec/dependency_tags.rb +52 -0
- data/lib/tree_haver/rspec.rb +3 -0
- data/lib/tree_haver/tree.rb +267 -0
- data/lib/tree_haver/version.rb +5 -3
- data/lib/tree_haver.rb +613 -8
- data/sig/tree_haver.rbs +6 -0
- data.tar.gz.sig +0 -0
- metadata +314 -13
- metadata.gz.sig +0 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TreeHaver
|
|
4
|
+
# Factory module for loading language grammars
|
|
5
|
+
#
|
|
6
|
+
# Language is the entry point for loading and using grammars. It provides
|
|
7
|
+
# a unified interface that works across all backends (MRI, Rust, FFI, Java, Citrus, Parslet).
|
|
8
|
+
#
|
|
9
|
+
# This is a module with only module methods (factory pattern), not a class.
|
|
10
|
+
# Backend-specific Language classes (e.g., Backends::Citrus::Language,
|
|
11
|
+
# Backends::Parslet::Language) inherit from Base::Language.
|
|
12
|
+
#
|
|
13
|
+
# For tree-sitter backends, languages are loaded from shared library files (.so/.dylib/.dll).
|
|
14
|
+
# For pure-Ruby backends (Citrus, Parslet, Prism, Psych), languages are built-in or provided by gems.
|
|
15
|
+
#
|
|
16
|
+
# == Loading Languages
|
|
17
|
+
#
|
|
18
|
+
# The primary way to load a language is via registration:
|
|
19
|
+
#
|
|
20
|
+
# TreeHaver.register_language(:toml, path: "/path/to/libtree-sitter-toml.so")
|
|
21
|
+
# language = TreeHaver::Language.toml
|
|
22
|
+
#
|
|
23
|
+
# For explicit loading without registration:
|
|
24
|
+
#
|
|
25
|
+
# language = TreeHaver::Language.from_library(
|
|
26
|
+
# "/path/to/libtree-sitter-toml.so",
|
|
27
|
+
# symbol: "tree_sitter_toml"
|
|
28
|
+
# )
|
|
29
|
+
#
|
|
30
|
+
# For ruby_tree_sitter compatibility:
|
|
31
|
+
#
|
|
32
|
+
# language = TreeHaver::Language.load("toml", "/path/to/libtree-sitter-toml.so")
|
|
33
|
+
#
|
|
34
|
+
# @example Register and load a language
|
|
35
|
+
# TreeHaver.register_language(:toml, path: "/path/to/grammar.so")
|
|
36
|
+
# language = TreeHaver::Language.toml
|
|
37
|
+
#
|
|
38
|
+
# @see Base::Language The base class that backend Language classes inherit from
|
|
39
|
+
module Language
|
|
40
|
+
class << self
|
|
41
|
+
# Load a language grammar from a shared library (ruby_tree_sitter compatibility)
|
|
42
|
+
#
|
|
43
|
+
# This method provides API compatibility with ruby_tree_sitter which uses
|
|
44
|
+
# `Language.load(name, path)`.
|
|
45
|
+
#
|
|
46
|
+
# @param name [String] the language name (e.g., "toml")
|
|
47
|
+
# @param path [String] absolute path to the language shared library
|
|
48
|
+
# @param validate [Boolean] if true, validates the path for safety (default: true)
|
|
49
|
+
# @return [Language] loaded language handle
|
|
50
|
+
# @raise [NotAvailable] if the library cannot be loaded
|
|
51
|
+
# @raise [ArgumentError] if the path fails security validation
|
|
52
|
+
# @example
|
|
53
|
+
# language = TreeHaver::Language.load("toml", "/usr/local/lib/libtree-sitter-toml.so")
|
|
54
|
+
def load(name, path, validate: true)
|
|
55
|
+
from_library(path, symbol: "tree_sitter_#{name}", name: name, validate: validate)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Load a language grammar from a shared library
|
|
59
|
+
#
|
|
60
|
+
# The library must export a function that returns a pointer to a TSLanguage struct.
|
|
61
|
+
# By default, TreeHaver looks for a symbol named "tree_sitter_<name>".
|
|
62
|
+
#
|
|
63
|
+
# == Security
|
|
64
|
+
#
|
|
65
|
+
# By default, paths are validated using {PathValidator} to prevent path traversal
|
|
66
|
+
# and other attacks. Set `validate: false` to skip validation (not recommended
|
|
67
|
+
# unless you've already validated the path).
|
|
68
|
+
#
|
|
69
|
+
# @param path [String] absolute path to the language shared library (.so/.dylib/.dll)
|
|
70
|
+
# @param symbol [String, nil] name of the exported function (defaults to auto-detection)
|
|
71
|
+
# @param name [String, nil] logical name for the language (used in caching)
|
|
72
|
+
# @param validate [Boolean] if true, validates path and symbol for safety (default: true)
|
|
73
|
+
# @param backend [Symbol, String, nil] optional backend to use (overrides context/global)
|
|
74
|
+
# @return [Language] loaded language handle
|
|
75
|
+
# @raise [NotAvailable] if the library cannot be loaded or the symbol is not found
|
|
76
|
+
# @raise [ArgumentError] if path or symbol fails security validation
|
|
77
|
+
# @example
|
|
78
|
+
# language = TreeHaver::Language.from_library(
|
|
79
|
+
# "/usr/local/lib/libtree-sitter-toml.so",
|
|
80
|
+
# symbol: "tree_sitter_toml",
|
|
81
|
+
# name: "toml"
|
|
82
|
+
# )
|
|
83
|
+
# @example With explicit backend
|
|
84
|
+
# language = TreeHaver::Language.from_library(
|
|
85
|
+
# "/usr/local/lib/libtree-sitter-toml.so",
|
|
86
|
+
# symbol: "tree_sitter_toml",
|
|
87
|
+
# backend: :ffi
|
|
88
|
+
# )
|
|
89
|
+
def from_library(path, symbol: nil, name: nil, validate: true, backend: nil)
|
|
90
|
+
if validate
|
|
91
|
+
unless PathValidator.safe_library_path?(path)
|
|
92
|
+
errors = PathValidator.validation_errors(path)
|
|
93
|
+
raise ArgumentError, "Unsafe library path: #{path.inspect}. Errors: #{errors.join('; ')}"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
if symbol && !PathValidator.safe_symbol_name?(symbol)
|
|
97
|
+
raise ArgumentError, "Unsafe symbol name: #{symbol.inspect}. " \
|
|
98
|
+
'Symbol names must be valid C identifiers.'
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# from_library only works with tree-sitter backends that support .so files
|
|
103
|
+
# Pure Ruby backends (Citrus, Prism, Psych, Commonmarker, Markly) don't support from_library
|
|
104
|
+
mod = TreeHaver.resolve_native_backend_module(backend)
|
|
105
|
+
|
|
106
|
+
if mod.nil?
|
|
107
|
+
if backend
|
|
108
|
+
raise NotAvailable,
|
|
109
|
+
"Requested backend #{backend.inspect} is not available or does not support shared libraries"
|
|
110
|
+
else
|
|
111
|
+
raise NotAvailable,
|
|
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).'
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Backend must implement .from_library; fallback to .from_path for older impls
|
|
119
|
+
# Include effective backend AND ENV vars in cache key since they affect loading
|
|
120
|
+
effective_b = TreeHaver.resolve_effective_backend(backend)
|
|
121
|
+
key = [effective_b, path, symbol, name, ENV['TREE_SITTER_LANG_SYMBOL']]
|
|
122
|
+
LanguageRegistry.fetch(key) do
|
|
123
|
+
if mod::Language.respond_to?(:from_library)
|
|
124
|
+
mod::Language.from_library(path, symbol: symbol, name: name)
|
|
125
|
+
else
|
|
126
|
+
mod::Language.from_path(path)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
# Alias for {from_library}
|
|
131
|
+
# @see from_library
|
|
132
|
+
alias from_path from_library
|
|
133
|
+
|
|
134
|
+
# Dynamic helper to load a registered language by name
|
|
135
|
+
#
|
|
136
|
+
# After registering a language with {TreeHaver.register_language},
|
|
137
|
+
# you can load it using a method call. The appropriate backend will be
|
|
138
|
+
# used based on registration and current backend.
|
|
139
|
+
#
|
|
140
|
+
# @example With tree-sitter
|
|
141
|
+
# TreeHaver.register_language(:toml, path: "/path/to/libtree-sitter-toml.so")
|
|
142
|
+
# language = TreeHaver::Language.toml
|
|
143
|
+
#
|
|
144
|
+
# @example With both backends
|
|
145
|
+
# TreeHaver.register_language(:toml,
|
|
146
|
+
# path: "/path/to/libtree-sitter-toml.so", symbol: "tree_sitter_toml")
|
|
147
|
+
# TreeHaver.register_language(:toml,
|
|
148
|
+
# grammar_module: TomlRB::Document)
|
|
149
|
+
# language = TreeHaver::Language.toml # Uses appropriate grammar for active backend
|
|
150
|
+
#
|
|
151
|
+
# @param method_name [Symbol] the registered language name
|
|
152
|
+
# @param args [Array] positional arguments
|
|
153
|
+
# @param kwargs [Hash] keyword arguments
|
|
154
|
+
# @return [Language] loaded language handle
|
|
155
|
+
# @raise [NoMethodError] if the language name is not registered
|
|
156
|
+
def method_missing(method_name, *args, **kwargs, &block)
|
|
157
|
+
# Resolve only if the language name was registered
|
|
158
|
+
all_backends = TreeHaver.registered_language(method_name)
|
|
159
|
+
return super unless all_backends
|
|
160
|
+
|
|
161
|
+
# Check current backend
|
|
162
|
+
current_backend = TreeHaver.backend_module
|
|
163
|
+
|
|
164
|
+
# Determine which backend type to use
|
|
165
|
+
backend_type = if current_backend == Backends::Citrus
|
|
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
|
|
172
|
+
|
|
173
|
+
# Get backend-specific registration
|
|
174
|
+
reg = all_backends[backend_type]
|
|
175
|
+
|
|
176
|
+
# If Citrus backend is active
|
|
177
|
+
if backend_type == :citrus
|
|
178
|
+
return Backends::Citrus::Language.new(reg[:grammar_module]) if reg && reg[:grammar_module]
|
|
179
|
+
|
|
180
|
+
# No Citrus grammar for this language — provide actionable error
|
|
181
|
+
raise NotAvailable,
|
|
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
|
+
end
|
|
187
|
+
|
|
188
|
+
# If Parslet backend is active
|
|
189
|
+
if backend_type == :parslet
|
|
190
|
+
return Backends::Parslet::Language.new(reg[:grammar_class]) if reg && reg[:grammar_class]
|
|
191
|
+
|
|
192
|
+
# No Parslet grammar for this language — provide actionable error
|
|
193
|
+
raise NotAvailable,
|
|
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}"
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# For tree-sitter backends, try to load from path
|
|
201
|
+
# If that fails, fall back to Citrus if available
|
|
202
|
+
if reg && reg[:path]
|
|
203
|
+
path = kwargs[:path] || args.first || reg[:path]
|
|
204
|
+
# Symbol priority: kwargs override > registration > derive from method_name
|
|
205
|
+
symbol = if kwargs.key?(:symbol)
|
|
206
|
+
kwargs[:symbol]
|
|
207
|
+
elsif reg[:symbol]
|
|
208
|
+
reg[:symbol]
|
|
209
|
+
else
|
|
210
|
+
"tree_sitter_#{method_name}"
|
|
211
|
+
end
|
|
212
|
+
# Name priority: kwargs override > derive from symbol (strip tree_sitter_ prefix)
|
|
213
|
+
# Using symbol-derived name ensures ruby_tree_sitter gets the correct language name
|
|
214
|
+
# e.g., "toml" not "toml_both" when symbol is "tree_sitter_toml"
|
|
215
|
+
name = kwargs[:name] || symbol&.sub(/\Atree_sitter_/, '')
|
|
216
|
+
|
|
217
|
+
begin
|
|
218
|
+
return from_library(path, symbol: symbol, name: name)
|
|
219
|
+
rescue NotAvailable, ArgumentError, LoadError => e
|
|
220
|
+
# Tree-sitter failed to load - check for Citrus fallback
|
|
221
|
+
# Note: FFI::NotFoundError inherits from LoadError, so it's caught here too
|
|
222
|
+
handle_tree_sitter_load_failure(e, all_backends)
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# No tree-sitter path registered - check for Citrus or Parslet fallback
|
|
227
|
+
# This enables auto-fallback when tree-sitter grammar is not installed
|
|
228
|
+
# but a pure Ruby grammar (Citrus or Parslet) is available.
|
|
229
|
+
# Only fall back when backend is :auto - explicit native backend requests should fail.
|
|
230
|
+
if TreeHaver.effective_backend == :auto
|
|
231
|
+
citrus_reg = all_backends[:citrus]
|
|
232
|
+
if citrus_reg && citrus_reg[:grammar_module]
|
|
233
|
+
return Backends::Citrus::Language.new(citrus_reg[:grammar_module])
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
parslet_reg = all_backends[:parslet]
|
|
237
|
+
if parslet_reg && parslet_reg[:grammar_class]
|
|
238
|
+
return Backends::Parslet::Language.new(parslet_reg[:grammar_class])
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# No appropriate registration found
|
|
243
|
+
raise ArgumentError,
|
|
244
|
+
"No grammar registered for :#{method_name} compatible with #{backend_type} backend. " \
|
|
245
|
+
"Registered backends: #{all_backends.keys.inspect}"
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# @api private
|
|
249
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
250
|
+
!!TreeHaver.registered_language(method_name) || super
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
private
|
|
254
|
+
|
|
255
|
+
# Handle tree-sitter load failure with optional Citrus/Parslet fallback
|
|
256
|
+
#
|
|
257
|
+
# This handles cases where:
|
|
258
|
+
# - The .so file doesn't exist or can't be loaded (NotAvailable, LoadError)
|
|
259
|
+
# - FFI can't find required symbols like ts_parser_new (FFI::NotFoundError inherits from LoadError)
|
|
260
|
+
# - Invalid arguments were provided (ArgumentError)
|
|
261
|
+
#
|
|
262
|
+
# Fallback to Citrus/Parslet ONLY happens when:
|
|
263
|
+
# - The effective backend is :auto (user didn't explicitly request a native backend)
|
|
264
|
+
# - A Citrus or Parslet grammar is registered for the language
|
|
265
|
+
#
|
|
266
|
+
# If the user explicitly requested a native backend (:mri, :rust, :ffi, :java),
|
|
267
|
+
# we should NOT silently fall back to pure Ruby - that would violate the user's intent.
|
|
268
|
+
#
|
|
269
|
+
# @param error [Exception] the original error
|
|
270
|
+
# @param all_backends [Hash] all registered backends for the language
|
|
271
|
+
# @return [Backends::Citrus::Language, Backends::Parslet::Language] if fallback available and allowed
|
|
272
|
+
# @raise [Exception] re-raises original error if no fallback or fallback not allowed
|
|
273
|
+
# @api private
|
|
274
|
+
def handle_tree_sitter_load_failure(error, all_backends)
|
|
275
|
+
# Only fall back to pure Ruby when backend is :auto
|
|
276
|
+
# If user explicitly requested a native backend, respect that choice
|
|
277
|
+
effective = TreeHaver.effective_backend
|
|
278
|
+
if effective == :auto
|
|
279
|
+
citrus_reg = all_backends[:citrus]
|
|
280
|
+
if citrus_reg && citrus_reg[:grammar_module]
|
|
281
|
+
return Backends::Citrus::Language.new(citrus_reg[:grammar_module])
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
parslet_reg = all_backends[:parslet]
|
|
285
|
+
if parslet_reg && parslet_reg[:grammar_class]
|
|
286
|
+
return Backends::Parslet::Language.new(parslet_reg[:grammar_class])
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
# No pure Ruby fallback allowed or available, re-raise the original error
|
|
290
|
+
raise error
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
end
|
|
@@ -1,22 +1,30 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "json"
|
|
4
|
-
require "tree_sitter_language_pack"
|
|
5
|
-
|
|
6
3
|
module TreeHaver
|
|
4
|
+
TSLP_BACKEND = BackendReference.new(
|
|
5
|
+
id: 'tslp',
|
|
6
|
+
family: 'tree-sitter'
|
|
7
|
+
).freeze
|
|
7
8
|
KREUZBERG_LANGUAGE_PACK_BACKEND = BackendReference.new(
|
|
8
|
-
id:
|
|
9
|
-
family:
|
|
9
|
+
id: 'kreuzberg-language-pack',
|
|
10
|
+
family: 'tree-sitter'
|
|
10
11
|
).freeze
|
|
11
12
|
|
|
13
|
+
BackendRegistry.register(TSLP_BACKEND)
|
|
12
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
|
|
13
21
|
|
|
14
22
|
module_function
|
|
15
23
|
|
|
16
24
|
def language_pack_adapter_info
|
|
17
25
|
AdapterInfo.new(
|
|
18
|
-
backend:
|
|
19
|
-
backend_ref:
|
|
26
|
+
backend: TSLP_BACKEND.id,
|
|
27
|
+
backend_ref: TSLP_BACKEND,
|
|
20
28
|
supports_dialects: false,
|
|
21
29
|
supported_policies: []
|
|
22
30
|
)
|
|
@@ -24,167 +32,10 @@ module TreeHaver
|
|
|
24
32
|
|
|
25
33
|
def language_pack_feature_profile
|
|
26
34
|
FeatureProfile.new(
|
|
27
|
-
backend:
|
|
28
|
-
backend_ref:
|
|
35
|
+
backend: TSLP_BACKEND.id,
|
|
36
|
+
backend_ref: TSLP_BACKEND,
|
|
29
37
|
supports_dialects: false,
|
|
30
38
|
supported_policies: []
|
|
31
39
|
)
|
|
32
40
|
end
|
|
33
|
-
|
|
34
|
-
def parse_with_language_pack(request)
|
|
35
|
-
ensure_language_pack_language(request.language)
|
|
36
|
-
raw = TreeSitterLanguagePack.process(
|
|
37
|
-
request.source,
|
|
38
|
-
JSON.generate(language: request.language, diagnostics: true)
|
|
39
|
-
)
|
|
40
|
-
diagnostics = Array(raw["diagnostics"])
|
|
41
|
-
return parse_error_result(request.language) unless diagnostics.empty?
|
|
42
|
-
|
|
43
|
-
analysis = LanguagePackAnalysis.new(
|
|
44
|
-
language: request.language,
|
|
45
|
-
dialect: request.dialect,
|
|
46
|
-
root_type: inferred_root_type(request),
|
|
47
|
-
has_error: false,
|
|
48
|
-
backend_ref: KREUZBERG_LANGUAGE_PACK_BACKEND
|
|
49
|
-
)
|
|
50
|
-
parse_result(ok: true, analysis: analysis, diagnostics: [])
|
|
51
|
-
rescue StandardError => e
|
|
52
|
-
parse_result(
|
|
53
|
-
ok: false,
|
|
54
|
-
diagnostics: [diagnostic("error", "unsupported_feature", e.message)]
|
|
55
|
-
)
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def process_with_language_pack(request)
|
|
59
|
-
ensure_language_pack_language(request.language)
|
|
60
|
-
raw = TreeSitterLanguagePack.process(
|
|
61
|
-
request.source,
|
|
62
|
-
JSON.generate(language: request.language, structure: true, imports: true, diagnostics: true)
|
|
63
|
-
)
|
|
64
|
-
analysis = LanguagePackProcessAnalysis.new(
|
|
65
|
-
language: raw.fetch("language"),
|
|
66
|
-
structure: Array(raw["structure"]).map do |item|
|
|
67
|
-
ProcessStructureItem.new(
|
|
68
|
-
kind: item.fetch("kind").downcase,
|
|
69
|
-
name: item["name"],
|
|
70
|
-
span: process_span(item.fetch("span"))
|
|
71
|
-
)
|
|
72
|
-
end,
|
|
73
|
-
imports: normalize_imports(request.language, Array(raw["imports"])),
|
|
74
|
-
diagnostics: Array(raw["diagnostics"]).map do |item|
|
|
75
|
-
ProcessDiagnostic.new(
|
|
76
|
-
message: item.fetch("message"),
|
|
77
|
-
severity: item.fetch("severity")
|
|
78
|
-
)
|
|
79
|
-
end,
|
|
80
|
-
backend_ref: KREUZBERG_LANGUAGE_PACK_BACKEND
|
|
81
|
-
)
|
|
82
|
-
parse_result(ok: true, analysis: analysis, diagnostics: [])
|
|
83
|
-
rescue StandardError => e
|
|
84
|
-
parse_result(
|
|
85
|
-
ok: false,
|
|
86
|
-
diagnostics: [diagnostic("error", "unsupported_feature", e.message)]
|
|
87
|
-
)
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def ensure_language_pack_language(language)
|
|
91
|
-
return if TreeSitterLanguagePack.has_language(language)
|
|
92
|
-
|
|
93
|
-
TreeSitterLanguagePack.init(JSON.generate(languages: [language]))
|
|
94
|
-
end
|
|
95
|
-
private_class_method :ensure_language_pack_language
|
|
96
|
-
|
|
97
|
-
def parse_error_result(language)
|
|
98
|
-
parse_result(
|
|
99
|
-
ok: false,
|
|
100
|
-
diagnostics: [
|
|
101
|
-
diagnostic(
|
|
102
|
-
"error",
|
|
103
|
-
"parse_error",
|
|
104
|
-
"tree-sitter-language-pack reported syntax errors for #{language}."
|
|
105
|
-
)
|
|
106
|
-
]
|
|
107
|
-
)
|
|
108
|
-
end
|
|
109
|
-
private_class_method :parse_error_result
|
|
110
|
-
|
|
111
|
-
def process_span(raw)
|
|
112
|
-
ProcessSpan.new(
|
|
113
|
-
start_byte: raw.fetch("start_byte"),
|
|
114
|
-
end_byte: raw.fetch("end_byte"),
|
|
115
|
-
start_row: raw["start_row"] || raw.fetch("start_line"),
|
|
116
|
-
start_col: raw["start_col"] || raw.fetch("start_column"),
|
|
117
|
-
end_row: raw["end_row"] || raw.fetch("end_line"),
|
|
118
|
-
end_col: raw["end_col"] || raw.fetch("end_column")
|
|
119
|
-
)
|
|
120
|
-
end
|
|
121
|
-
private_class_method :process_span
|
|
122
|
-
|
|
123
|
-
def inferred_root_type(request)
|
|
124
|
-
stripped = request.source.lstrip
|
|
125
|
-
case request.language
|
|
126
|
-
when "json"
|
|
127
|
-
return "object" if stripped.start_with?("{")
|
|
128
|
-
return "array" if stripped.start_with?("[")
|
|
129
|
-
|
|
130
|
-
"scalar"
|
|
131
|
-
else
|
|
132
|
-
request.language
|
|
133
|
-
end
|
|
134
|
-
end
|
|
135
|
-
private_class_method :inferred_root_type
|
|
136
|
-
|
|
137
|
-
def normalize_imports(language, raw_imports)
|
|
138
|
-
raw_imports.map do |item|
|
|
139
|
-
source, items =
|
|
140
|
-
if language == "typescript"
|
|
141
|
-
normalize_typescript_import(item)
|
|
142
|
-
else
|
|
143
|
-
[item["module"] || item["source"] || "", Array(item["names"] || item["items"])]
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
ProcessImportInfo.new(
|
|
147
|
-
source: source,
|
|
148
|
-
items: items,
|
|
149
|
-
span: process_span(item.fetch("span"))
|
|
150
|
-
)
|
|
151
|
-
end
|
|
152
|
-
end
|
|
153
|
-
private_class_method :normalize_imports
|
|
154
|
-
|
|
155
|
-
def normalize_typescript_import(item)
|
|
156
|
-
raw_source = item["module"] || item["source"] || ""
|
|
157
|
-
source_match = raw_source.match(/from\s+['"]([^'"]+)['"]|import\s+['"]([^'"]+)['"]/)
|
|
158
|
-
source = source_match&.captures&.compact&.first || raw_source.strip
|
|
159
|
-
names = if (named_items = raw_source.match(/\{([^}]+)\}/))
|
|
160
|
-
named_items[1]
|
|
161
|
-
.split(",")
|
|
162
|
-
.map { |part| part.gsub(/\btype\b/, "").strip }
|
|
163
|
-
.reject(&:empty?)
|
|
164
|
-
else
|
|
165
|
-
Array(item["names"] || item["items"])
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
[source, names]
|
|
169
|
-
end
|
|
170
|
-
private_class_method :normalize_typescript_import
|
|
171
|
-
|
|
172
|
-
def parse_result(ok:, diagnostics:, analysis: nil, policies: [])
|
|
173
|
-
{
|
|
174
|
-
ok: ok,
|
|
175
|
-
diagnostics: diagnostics,
|
|
176
|
-
**(analysis ? { analysis: analysis } : {}),
|
|
177
|
-
policies: policies
|
|
178
|
-
}
|
|
179
|
-
end
|
|
180
|
-
private_class_method :parse_result
|
|
181
|
-
|
|
182
|
-
def diagnostic(severity, category, message)
|
|
183
|
-
{
|
|
184
|
-
severity: severity,
|
|
185
|
-
category: category,
|
|
186
|
-
message: message
|
|
187
|
-
}
|
|
188
|
-
end
|
|
189
|
-
private_class_method :diagnostic
|
|
190
41
|
end
|