tree_haver 5.0.5 → 7.0.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 (57) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/lib/tree_haver/backend_context.rb +28 -0
  4. data/lib/tree_haver/backend_registry.rb +19 -432
  5. data/lib/tree_haver/contracts.rb +460 -0
  6. data/lib/tree_haver/kaitai_backend.rb +30 -0
  7. data/lib/tree_haver/language_pack.rb +190 -0
  8. data/lib/tree_haver/peg_backends.rb +76 -0
  9. data/lib/tree_haver/version.rb +1 -12
  10. data/lib/tree_haver.rb +7 -1316
  11. data.tar.gz.sig +0 -0
  12. metadata +34 -251
  13. metadata.gz.sig +0 -0
  14. data/CHANGELOG.md +0 -1393
  15. data/CITATION.cff +0 -20
  16. data/CODE_OF_CONDUCT.md +0 -134
  17. data/CONTRIBUTING.md +0 -359
  18. data/FUNDING.md +0 -74
  19. data/LICENSE.txt +0 -21
  20. data/README.md +0 -2320
  21. data/REEK +0 -0
  22. data/RUBOCOP.md +0 -71
  23. data/SECURITY.md +0 -21
  24. data/lib/tree_haver/backend_api.rb +0 -349
  25. data/lib/tree_haver/backends/citrus.rb +0 -487
  26. data/lib/tree_haver/backends/ffi.rb +0 -1009
  27. data/lib/tree_haver/backends/java.rb +0 -893
  28. data/lib/tree_haver/backends/mri.rb +0 -362
  29. data/lib/tree_haver/backends/parslet.rb +0 -560
  30. data/lib/tree_haver/backends/prism.rb +0 -471
  31. data/lib/tree_haver/backends/psych.rb +0 -375
  32. data/lib/tree_haver/backends/rust.rb +0 -239
  33. data/lib/tree_haver/base/language.rb +0 -98
  34. data/lib/tree_haver/base/node.rb +0 -322
  35. data/lib/tree_haver/base/parser.rb +0 -24
  36. data/lib/tree_haver/base/point.rb +0 -48
  37. data/lib/tree_haver/base/tree.rb +0 -128
  38. data/lib/tree_haver/base.rb +0 -12
  39. data/lib/tree_haver/citrus_grammar_finder.rb +0 -218
  40. data/lib/tree_haver/compat.rb +0 -43
  41. data/lib/tree_haver/grammar_finder.rb +0 -374
  42. data/lib/tree_haver/language.rb +0 -295
  43. data/lib/tree_haver/language_registry.rb +0 -190
  44. data/lib/tree_haver/library_path_utils.rb +0 -80
  45. data/lib/tree_haver/node.rb +0 -579
  46. data/lib/tree_haver/parser.rb +0 -438
  47. data/lib/tree_haver/parslet_grammar_finder.rb +0 -224
  48. data/lib/tree_haver/path_validator.rb +0 -353
  49. data/lib/tree_haver/point.rb +0 -27
  50. data/lib/tree_haver/rspec/dependency_tags.rb +0 -1392
  51. data/lib/tree_haver/rspec/testable_node.rb +0 -217
  52. data/lib/tree_haver/rspec.rb +0 -33
  53. data/lib/tree_haver/tree.rb +0 -258
  54. data/sig/tree_haver/backends.rbs +0 -352
  55. data/sig/tree_haver/grammar_finder.rbs +0 -29
  56. data/sig/tree_haver/path_validator.rbs +0 -32
  57. data/sig/tree_haver.rbs +0 -234
@@ -0,0 +1,460 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TreeHaver
4
+ ParserRequest = Struct.new(:source, :language, :dialect, keyword_init: true) do
5
+ def to_h
6
+ {
7
+ source: source,
8
+ language: language,
9
+ **(dialect ? { dialect: dialect } : {})
10
+ }
11
+ end
12
+ end
13
+
14
+ BackendReference = Struct.new(:id, :family, keyword_init: true) do
15
+ def to_h
16
+ { id: id, family: family }
17
+ end
18
+ end
19
+
20
+ AdapterInfo = Struct.new(:backend, :backend_ref, :supports_dialects, :supported_policies, keyword_init: true) do
21
+ def to_h
22
+ {
23
+ backend: backend,
24
+ **(backend_ref ? { backend_ref: backend_ref.to_h } : {}),
25
+ supports_dialects: supports_dialects,
26
+ supported_policies: deep_dup(supported_policies || [])
27
+ }
28
+ end
29
+
30
+ private
31
+
32
+ def deep_dup(value)
33
+ Marshal.load(Marshal.dump(value))
34
+ end
35
+ end
36
+
37
+ FeatureProfile = Struct.new(:backend, :backend_ref, :supports_dialects, :supported_policies, keyword_init: true) do
38
+ def to_h
39
+ {
40
+ backend: backend,
41
+ **(backend_ref ? { backend_ref: backend_ref.to_h } : {}),
42
+ supports_dialects: supports_dialects,
43
+ supported_policies: deep_dup(supported_policies || [])
44
+ }
45
+ end
46
+
47
+ private
48
+
49
+ def deep_dup(value)
50
+ Marshal.load(Marshal.dump(value))
51
+ end
52
+ end
53
+
54
+ ParserDiagnostics = Struct.new(:backend, :backend_ref, :diagnostics, keyword_init: true) do
55
+ def to_h
56
+ {
57
+ backend: backend,
58
+ **(backend_ref ? { backend_ref: backend_ref.to_h } : {}),
59
+ diagnostics: deep_dup(diagnostics || [])
60
+ }
61
+ end
62
+
63
+ private
64
+
65
+ def deep_dup(value)
66
+ Marshal.load(Marshal.dump(value))
67
+ end
68
+ end
69
+
70
+ ProcessRequest = Struct.new(:source, :language, keyword_init: true) do
71
+ def to_h
72
+ {
73
+ source: source,
74
+ language: language
75
+ }
76
+ end
77
+ end
78
+
79
+ ProcessSpan = Struct.new(:start_byte, :end_byte, :start_row, :start_col, :end_row, :end_col, keyword_init: true) do
80
+ def to_h
81
+ {
82
+ start_byte: start_byte,
83
+ end_byte: end_byte,
84
+ start_row: start_row,
85
+ start_col: start_col,
86
+ end_row: end_row,
87
+ end_col: end_col
88
+ }
89
+ end
90
+ end
91
+
92
+ ByteRange = Struct.new(:start_byte, :end_byte, keyword_init: true) do
93
+ def valid?
94
+ start_byte.to_i >= 0 && end_byte.to_i >= start_byte.to_i
95
+ end
96
+
97
+ def length
98
+ valid? ? end_byte.to_i - start_byte.to_i : 0
99
+ end
100
+
101
+ def contains_byte?(offset)
102
+ valid? && offset.to_i >= start_byte.to_i && offset.to_i < end_byte.to_i
103
+ end
104
+
105
+ def contains_range?(other)
106
+ valid? && other.valid? && other.start_byte.to_i >= start_byte.to_i && other.end_byte.to_i <= end_byte.to_i
107
+ end
108
+
109
+ def overlaps?(other)
110
+ valid? && other.valid? && start_byte.to_i < other.end_byte.to_i && other.start_byte.to_i < end_byte.to_i
111
+ end
112
+
113
+ def to_h
114
+ {
115
+ start_byte: start_byte,
116
+ end_byte: end_byte
117
+ }
118
+ end
119
+ end
120
+
121
+ SourcePoint = Struct.new(:row, :column, keyword_init: true) do
122
+ def to_h
123
+ {
124
+ row: row,
125
+ column: column
126
+ }
127
+ end
128
+ end
129
+
130
+ SourceSpan = Struct.new(:range, :start_point, :end_point, keyword_init: true) do
131
+ def to_h
132
+ {
133
+ range: range.to_h,
134
+ start_point: start_point.to_h,
135
+ end_point: end_point.to_h
136
+ }
137
+ end
138
+ end
139
+
140
+ ByteEditSpan = Struct.new(:start_byte, :old_end_byte, :new_end_byte, :start_point, :old_end_point, :new_end_point, keyword_init: true) do
141
+ def old_range
142
+ ByteRange.new(start_byte: start_byte, end_byte: old_end_byte)
143
+ end
144
+
145
+ def new_range
146
+ ByteRange.new(start_byte: start_byte, end_byte: new_end_byte)
147
+ end
148
+
149
+ def byte_delta
150
+ new_end_byte.to_i - old_end_byte.to_i
151
+ end
152
+
153
+ def to_h
154
+ {
155
+ start_byte: start_byte,
156
+ old_end_byte: old_end_byte,
157
+ new_end_byte: new_end_byte,
158
+ start_point: start_point.to_h,
159
+ old_end_point: old_end_point.to_h,
160
+ new_end_point: new_end_point.to_h
161
+ }
162
+ end
163
+ end
164
+
165
+ BinaryScalarValue = Struct.new(:kind, :value, :symbol, :raw_value, :encoding, :format, :description, keyword_init: true) do
166
+ def to_h
167
+ {
168
+ kind: kind,
169
+ **(value.nil? ? {} : { value: value }),
170
+ **(symbol.nil? ? {} : { symbol: symbol }),
171
+ **(raw_value.nil? ? {} : { raw_value: raw_value }),
172
+ **(encoding.nil? ? {} : { encoding: encoding }),
173
+ **(format.nil? ? {} : { format: format }),
174
+ **(description.nil? ? {} : { description: description })
175
+ }
176
+ end
177
+ end
178
+
179
+ BinaryRenderPolicy = Struct.new(:schema_path, :byte_range, :operation, :disposition, :reason, keyword_init: true) do
180
+ def to_h
181
+ {
182
+ schema_path: schema_path,
183
+ **(byte_range ? { byte_range: byte_range.to_h } : {}),
184
+ operation: operation,
185
+ disposition: disposition,
186
+ reason: reason
187
+ }
188
+ end
189
+ end
190
+
191
+ BinaryDiagnostic = Struct.new(:severity, :category, :message, :schema_path, :byte_range, keyword_init: true) do
192
+ def to_h
193
+ {
194
+ severity: severity,
195
+ category: category,
196
+ message: message,
197
+ schema_path: schema_path,
198
+ **(byte_range ? { byte_range: byte_range.to_h } : {})
199
+ }
200
+ end
201
+ end
202
+
203
+ BinaryNestedDispatch = Struct.new(:schema_path, :family, :status, keyword_init: true) do
204
+ def to_h
205
+ {
206
+ schema_path: schema_path,
207
+ family: family,
208
+ status: status
209
+ }
210
+ end
211
+ end
212
+
213
+ BinaryPayloadRegion = Struct.new(:kind, :schema_path, :byte_range, :expected_hex, keyword_init: true) do
214
+ def to_h
215
+ {
216
+ kind: kind,
217
+ schema_path: schema_path,
218
+ byte_range: byte_range.to_h,
219
+ expected_hex: expected_hex
220
+ }
221
+ end
222
+ end
223
+
224
+ BinaryRawPayload = Struct.new(:encoding, :value, :byte_length, :regions, keyword_init: true) do
225
+ def to_h
226
+ {
227
+ encoding: encoding,
228
+ value: value,
229
+ byte_length: byte_length,
230
+ regions: (regions || []).map(&:to_h)
231
+ }
232
+ end
233
+ end
234
+
235
+ BinaryMergeReport = Struct.new(:format, :schema, :matched_schema_paths, :preserved_ranges, :rewritten_nodes, :checksum_updates, :nested_dispatches, :diagnostics, keyword_init: true) do
236
+ def to_h
237
+ {
238
+ format: format,
239
+ schema: schema,
240
+ matched_schema_paths: deep_dup(matched_schema_paths || []),
241
+ preserved_ranges: (preserved_ranges || []).map(&:to_h),
242
+ rewritten_nodes: deep_dup(rewritten_nodes || []),
243
+ checksum_updates: deep_dup(checksum_updates || []),
244
+ nested_dispatches: (nested_dispatches || []).map(&:to_h),
245
+ diagnostics: (diagnostics || []).map(&:to_h)
246
+ }
247
+ end
248
+
249
+ private
250
+
251
+ def deep_dup(value)
252
+ Marshal.load(Marshal.dump(value))
253
+ end
254
+ end
255
+
256
+ ZipArchiveInfo = Struct.new(:format, :schema, :entry_count, :central_directory_range, keyword_init: true) do
257
+ def to_h
258
+ {
259
+ format: format,
260
+ schema: schema,
261
+ entry_count: entry_count,
262
+ central_directory_range: central_directory_range.to_h
263
+ }
264
+ end
265
+ end
266
+
267
+ ZipArchiveEntry = Struct.new(:path, :normalized_path, :directory, :compression, :compressed_size, :uncompressed_size, :crc32, :local_header_range, :data_range, :central_directory_range, keyword_init: true) do
268
+ def to_h
269
+ {
270
+ path: path,
271
+ normalized_path: normalized_path,
272
+ directory: directory,
273
+ compression: compression,
274
+ compressed_size: compressed_size,
275
+ uncompressed_size: uncompressed_size,
276
+ crc32: crc32,
277
+ local_header_range: local_header_range.to_h,
278
+ data_range: data_range.to_h,
279
+ central_directory_range: central_directory_range.to_h
280
+ }
281
+ end
282
+ end
283
+
284
+ ZipMemberDecision = Struct.new(:normalized_path, :operation, :disposition, :nested_family, :reason, keyword_init: true) do
285
+ def to_h
286
+ {
287
+ normalized_path: normalized_path,
288
+ operation: operation,
289
+ disposition: disposition,
290
+ **(nested_family ? { nested_family: nested_family } : {}),
291
+ reason: reason
292
+ }
293
+ end
294
+ end
295
+
296
+ ZipUnsafeEntry = Struct.new(:path, :normalized_path, :category, :reason, keyword_init: true) do
297
+ def to_h
298
+ {
299
+ path: path,
300
+ normalized_path: normalized_path,
301
+ category: category,
302
+ reason: reason
303
+ }
304
+ end
305
+ end
306
+
307
+ ZipFamilyReport = Struct.new(:archive, :entries, :member_decisions, :merge_report, :unsafe_entries, keyword_init: true) do
308
+ def to_h
309
+ {
310
+ archive: archive.to_h,
311
+ entries: (entries || []).map(&:to_h),
312
+ member_decisions: (member_decisions || []).map(&:to_h),
313
+ unsafe_entries: (unsafe_entries || []).map(&:to_h),
314
+ merge_report: merge_report.to_h
315
+ }
316
+ end
317
+ end
318
+
319
+ def self.slice_byte_range(source, byte_range)
320
+ source_bytesize = source.to_s.bytesize
321
+ unless byte_range.valid? && byte_range.end_byte.to_i <= source_bytesize
322
+ raise RangeError, "invalid byte range [#{byte_range.start_byte}, #{byte_range.end_byte}) for source length #{source_bytesize}"
323
+ end
324
+
325
+ source.to_s.byteslice(byte_range.start_byte.to_i...byte_range.end_byte.to_i)
326
+ end
327
+
328
+ def self.byte_offset_for_point(source, point)
329
+ raise RangeError, "invalid source point (#{point.row}, #{point.column})" if point.row.to_i.negative? || point.column.to_i.negative?
330
+
331
+ row = 0
332
+ column = 0
333
+ source.to_s.bytes.each_with_index do |byte, offset|
334
+ return offset if row == point.row.to_i && column == point.column.to_i
335
+
336
+ if byte == 10
337
+ row += 1
338
+ column = 0
339
+ else
340
+ column += 1
341
+ end
342
+ end
343
+ return source.to_s.bytesize if row == point.row.to_i && column == point.column.to_i
344
+
345
+ raise RangeError, "source point (#{point.row}, #{point.column}) is outside source"
346
+ end
347
+
348
+ ProcessStructureItem = Struct.new(:kind, :name, :span, keyword_init: true) do
349
+ def to_h
350
+ {
351
+ kind: kind,
352
+ **(name ? { name: name } : {}),
353
+ span: span.to_h
354
+ }
355
+ end
356
+ end
357
+
358
+ ProcessImportInfo = Struct.new(:source, :items, :span, keyword_init: true) do
359
+ def to_h
360
+ {
361
+ source: source,
362
+ items: deep_dup(items || []),
363
+ span: span.to_h
364
+ }
365
+ end
366
+
367
+ private
368
+
369
+ def deep_dup(value)
370
+ Marshal.load(Marshal.dump(value))
371
+ end
372
+ end
373
+
374
+ ProcessDiagnostic = Struct.new(:message, :severity, keyword_init: true) do
375
+ def to_h
376
+ {
377
+ message: message,
378
+ severity: severity
379
+ }
380
+ end
381
+ end
382
+
383
+ LanguagePackAnalysis = Struct.new(:language, :dialect, :root_type, :has_error, :backend_ref, keyword_init: true) do
384
+ def kind
385
+ "tree-sitter"
386
+ end
387
+
388
+ def to_h
389
+ {
390
+ kind: kind,
391
+ language: language,
392
+ **(dialect ? { dialect: dialect } : {}),
393
+ root_type: root_type,
394
+ has_error: has_error,
395
+ backend_ref: backend_ref.to_h
396
+ }
397
+ end
398
+ end
399
+
400
+ LanguagePackProcessAnalysis = Struct.new(:language, :structure, :imports, :diagnostics, :backend_ref, keyword_init: true) do
401
+ def kind
402
+ "tree-sitter-process"
403
+ end
404
+
405
+ def to_h
406
+ {
407
+ kind: kind,
408
+ language: language,
409
+ structure: (structure || []).map(&:to_h),
410
+ imports: (imports || []).map(&:to_h),
411
+ diagnostics: (diagnostics || []).map(&:to_h),
412
+ backend_ref: backend_ref.to_h
413
+ }
414
+ end
415
+ end
416
+
417
+ KaitaiByteSpan = Struct.new(:start_byte, :end_byte, keyword_init: true) do
418
+ def to_h
419
+ {
420
+ start_byte: start_byte,
421
+ end_byte: end_byte
422
+ }
423
+ end
424
+ end
425
+
426
+ KaitaiTreeNode = Struct.new(:kind, :schema_path, :span, :fields, :children, keyword_init: true) do
427
+ def to_h
428
+ {
429
+ kind: kind,
430
+ schema_path: schema_path,
431
+ span: span.to_h,
432
+ fields: deep_dup(fields || {}),
433
+ children: (children || []).map(&:to_h)
434
+ }
435
+ end
436
+
437
+ private
438
+
439
+ def deep_dup(value)
440
+ Marshal.load(Marshal.dump(value))
441
+ end
442
+ end
443
+
444
+ KaitaiTreeAnalysis = Struct.new(:schema, :root, :backend_ref, :source_byte_length, :diagnostics, keyword_init: true) do
445
+ def kind
446
+ "kaitai-tree"
447
+ end
448
+
449
+ def to_h
450
+ {
451
+ kind: kind,
452
+ schema: schema,
453
+ **(source_byte_length.nil? ? {} : { source_byte_length: source_byte_length }),
454
+ root: root.to_h,
455
+ backend_ref: backend_ref.to_h,
456
+ diagnostics: (diagnostics || []).map(&:to_h)
457
+ }
458
+ end
459
+ end
460
+ 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
@@ -0,0 +1,190 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "tree_sitter_language_pack"
5
+
6
+ module TreeHaver
7
+ KREUZBERG_LANGUAGE_PACK_BACKEND = BackendReference.new(
8
+ id: "kreuzberg-language-pack",
9
+ family: "tree-sitter"
10
+ ).freeze
11
+
12
+ BackendRegistry.register(KREUZBERG_LANGUAGE_PACK_BACKEND)
13
+
14
+ module_function
15
+
16
+ def language_pack_adapter_info
17
+ AdapterInfo.new(
18
+ backend: KREUZBERG_LANGUAGE_PACK_BACKEND.id,
19
+ backend_ref: KREUZBERG_LANGUAGE_PACK_BACKEND,
20
+ supports_dialects: false,
21
+ supported_policies: []
22
+ )
23
+ end
24
+
25
+ def language_pack_feature_profile
26
+ FeatureProfile.new(
27
+ backend: KREUZBERG_LANGUAGE_PACK_BACKEND.id,
28
+ backend_ref: KREUZBERG_LANGUAGE_PACK_BACKEND,
29
+ supports_dialects: false,
30
+ supported_policies: []
31
+ )
32
+ 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
+ end