@remnic/coding-graph 9.3.759
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.
- package/README.md +130 -0
- package/dist/chunk-5I2DBHOQ.js +1042 -0
- package/dist/chunk-5I2DBHOQ.js.map +1 -0
- package/dist/chunk-CPYJACC5.js +1838 -0
- package/dist/chunk-CPYJACC5.js.map +1 -0
- package/dist/chunk-ZVCMIM4T.js +216 -0
- package/dist/chunk-ZVCMIM4T.js.map +1 -0
- package/dist/cypher/query-parser.d.ts +253 -0
- package/dist/cypher/query-parser.js +17 -0
- package/dist/cypher/query-parser.js.map +1 -0
- package/dist/graph-schema.d.ts +84 -0
- package/dist/graph-schema.js +17 -0
- package/dist/graph-schema.js.map +1 -0
- package/dist/graph-store.d.ts +938 -0
- package/dist/graph-store.js +16 -0
- package/dist/graph-store.js.map +1 -0
- package/dist/index.d.ts +1953 -0
- package/dist/index.js +3509 -0
- package/dist/index.js.map +1 -0
- package/grammars/tree-sitter-bash.wasm +0 -0
- package/grammars/tree-sitter-c.wasm +0 -0
- package/grammars/tree-sitter-c_sharp.wasm +0 -0
- package/grammars/tree-sitter-cpp.wasm +0 -0
- package/grammars/tree-sitter-go.wasm +0 -0
- package/grammars/tree-sitter-java.wasm +0 -0
- package/grammars/tree-sitter-javascript.wasm +0 -0
- package/grammars/tree-sitter-kotlin.wasm +0 -0
- package/grammars/tree-sitter-php.wasm +0 -0
- package/grammars/tree-sitter-python.wasm +0 -0
- package/grammars/tree-sitter-ruby.wasm +0 -0
- package/grammars/tree-sitter-rust.wasm +0 -0
- package/grammars/tree-sitter-swift.wasm +0 -0
- package/grammars/tree-sitter-tsx.wasm +0 -0
- package/grammars/tree-sitter-typescript.wasm +0 -0
- package/package.json +79 -0
- package/src/co-change.test.ts +175 -0
- package/src/co-change.ts +167 -0
- package/src/cypher/query-parser.test.ts +1107 -0
- package/src/cypher/query-parser.ts +1692 -0
- package/src/detect-changes.test.ts +533 -0
- package/src/detect-changes.ts +367 -0
- package/src/engine/emit.ts +556 -0
- package/src/engine/engine.test.ts +1417 -0
- package/src/engine/engine.ts +182 -0
- package/src/engine/extractors.ts +486 -0
- package/src/engine/fixtures.ts +364 -0
- package/src/engine/language-sniff.ts +56 -0
- package/src/engine/parser-backend.ts +206 -0
- package/src/engine/utf16-offsets.ts +68 -0
- package/src/git-invoker.test.ts +116 -0
- package/src/git-invoker.ts +426 -0
- package/src/graph-schema.test.ts +541 -0
- package/src/graph-schema.ts +383 -0
- package/src/graph-store-pr2.test.ts +1879 -0
- package/src/graph-store.test.ts +1420 -0
- package/src/graph-store.ts +3489 -0
- package/src/index-status.test.ts +303 -0
- package/src/index-status.ts +135 -0
- package/src/index.ts +384 -0
- package/src/lsp/byte-position.ts +173 -0
- package/src/lsp/characterization.test.ts +174 -0
- package/src/lsp/client.test.ts +275 -0
- package/src/lsp/client.ts +484 -0
- package/src/lsp/config.ts +219 -0
- package/src/lsp/degradation.ts +86 -0
- package/src/lsp/fixtures/fake-server.mjs +198 -0
- package/src/lsp/framing.test.ts +180 -0
- package/src/lsp/framing.ts +177 -0
- package/src/lsp/resolution.test.ts +497 -0
- package/src/lsp/resolution.ts +483 -0
- package/src/lsp/status.ts +140 -0
- package/src/lsp/types.ts +167 -0
- package/src/reindex.test.ts +1038 -0
- package/src/reindex.ts +908 -0
- package/src/row-types.ts +45 -0
- package/src/semantic/canonical-text.test.ts +150 -0
- package/src/semantic/canonical-text.ts +219 -0
- package/src/semantic/config.ts +235 -0
- package/src/semantic/index.ts +78 -0
- package/src/semantic/minhash.test.ts +197 -0
- package/src/semantic/minhash.ts +261 -0
- package/src/semantic/semantic-query.ts +173 -0
- package/src/semantic/semantic.test.ts +1315 -0
- package/src/semantic/similarity.ts +268 -0
- package/src/semantic/types.ts +145 -0
- package/src/semantic/vectors.ts +235 -0
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synthetic fixture source code for each tier-1 language.
|
|
3
|
+
*
|
|
4
|
+
* Public-repo policy (#1551): every fixture is synthetic — never real user
|
|
5
|
+
* code. Each fixture is small enough that the expected IR can be reasoned
|
|
6
|
+
* about and asserted against without magic numbers.
|
|
7
|
+
*/
|
|
8
|
+
import type { CodingGraphLanguage } from "@remnic/core";
|
|
9
|
+
|
|
10
|
+
export interface Fixture {
|
|
11
|
+
/** Repository-relative path (used for language sniffing + IR output). */
|
|
12
|
+
readonly path: string;
|
|
13
|
+
/** Synthetic source code (UTF-8). */
|
|
14
|
+
readonly code: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const FIXTURES: Record<CodingGraphLanguage, Fixture> = {
|
|
18
|
+
typescript: {
|
|
19
|
+
path: "src/app.ts",
|
|
20
|
+
code: [
|
|
21
|
+
'import { Router } from "express";',
|
|
22
|
+
"import type { Config } from './types';",
|
|
23
|
+
"",
|
|
24
|
+
"export interface AppOptions {",
|
|
25
|
+
" port: number;",
|
|
26
|
+
"}",
|
|
27
|
+
"",
|
|
28
|
+
"export type Mode = 'dev' | 'prod';",
|
|
29
|
+
"",
|
|
30
|
+
"export class Server {",
|
|
31
|
+
" private router: Router;",
|
|
32
|
+
"",
|
|
33
|
+
" constructor(router: Router) {",
|
|
34
|
+
" this.router = router;",
|
|
35
|
+
" }",
|
|
36
|
+
"",
|
|
37
|
+
" public start(): void {",
|
|
38
|
+
" this.router.get('/', () => {});",
|
|
39
|
+
" }",
|
|
40
|
+
"}",
|
|
41
|
+
"",
|
|
42
|
+
"export function createServer(opts: AppOptions): Server {",
|
|
43
|
+
" const r = new Router();",
|
|
44
|
+
" return new Server(r);",
|
|
45
|
+
"}",
|
|
46
|
+
"",
|
|
47
|
+
"const app = createServer({ port: 3000 });",
|
|
48
|
+
].join("\n"),
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
tsx: {
|
|
52
|
+
path: "src/component.tsx",
|
|
53
|
+
code: [
|
|
54
|
+
'import React, { useState } from "react";',
|
|
55
|
+
"",
|
|
56
|
+
"export interface ButtonProps {",
|
|
57
|
+
" label: string;",
|
|
58
|
+
" onClick: () => void;",
|
|
59
|
+
"}",
|
|
60
|
+
"",
|
|
61
|
+
"export function Button(props: ButtonProps) {",
|
|
62
|
+
" const [clicked, setClicked] = useState(false);",
|
|
63
|
+
" return null;",
|
|
64
|
+
"}",
|
|
65
|
+
"",
|
|
66
|
+
"export const Container = () => {",
|
|
67
|
+
" return null;",
|
|
68
|
+
"};",
|
|
69
|
+
].join("\n"),
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
javascript: {
|
|
73
|
+
path: "lib/router.js",
|
|
74
|
+
code: [
|
|
75
|
+
'const express = require("express");',
|
|
76
|
+
"",
|
|
77
|
+
"function createRouter() {",
|
|
78
|
+
" const router = express.Router();",
|
|
79
|
+
" router.get('/health', function healthCheck(req, res) {});",
|
|
80
|
+
" return router;",
|
|
81
|
+
"}",
|
|
82
|
+
"",
|
|
83
|
+
"class App {",
|
|
84
|
+
" constructor() {",
|
|
85
|
+
" this.router = createRouter();",
|
|
86
|
+
" }",
|
|
87
|
+
"}",
|
|
88
|
+
"",
|
|
89
|
+
"module.exports = { App, createRouter };",
|
|
90
|
+
].join("\n"),
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
python: {
|
|
94
|
+
path: "app/models.py",
|
|
95
|
+
code: [
|
|
96
|
+
"from typing import List",
|
|
97
|
+
"from dataclasses import dataclass",
|
|
98
|
+
"",
|
|
99
|
+
"class User:",
|
|
100
|
+
" def __init__(self, name):",
|
|
101
|
+
" self.name = name",
|
|
102
|
+
"",
|
|
103
|
+
" def greet(self):",
|
|
104
|
+
" return format_name(self.name)",
|
|
105
|
+
"",
|
|
106
|
+
"def format_name(name):",
|
|
107
|
+
" return name.strip()",
|
|
108
|
+
"",
|
|
109
|
+
"def main():",
|
|
110
|
+
" user = User('alice')",
|
|
111
|
+
" print(user.greet())",
|
|
112
|
+
].join("\n"),
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
go: {
|
|
116
|
+
path: "cmd/server/main.go",
|
|
117
|
+
code: [
|
|
118
|
+
"package main",
|
|
119
|
+
"",
|
|
120
|
+
'import "fmt"',
|
|
121
|
+
"",
|
|
122
|
+
"type Server struct {",
|
|
123
|
+
" Addr string",
|
|
124
|
+
"}",
|
|
125
|
+
"",
|
|
126
|
+
"func main() {",
|
|
127
|
+
' s := Server{Addr: ":8080"}',
|
|
128
|
+
" fmt.Println(s.Addr)",
|
|
129
|
+
"}",
|
|
130
|
+
"",
|
|
131
|
+
"func (s *Server) Start() error {",
|
|
132
|
+
" return nil",
|
|
133
|
+
"}",
|
|
134
|
+
].join("\n"),
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
rust: {
|
|
138
|
+
path: "src/lib.rs",
|
|
139
|
+
code: [
|
|
140
|
+
"use std::collections::HashMap;",
|
|
141
|
+
"",
|
|
142
|
+
"pub fn add(a: i32, b: i32) -> i32 {",
|
|
143
|
+
" a + b",
|
|
144
|
+
"}",
|
|
145
|
+
"",
|
|
146
|
+
"pub struct Config {",
|
|
147
|
+
" pub port: u16,",
|
|
148
|
+
"}",
|
|
149
|
+
"",
|
|
150
|
+
"impl Config {",
|
|
151
|
+
" pub fn new() -> Self {",
|
|
152
|
+
" Self { port: 8080 }",
|
|
153
|
+
" }",
|
|
154
|
+
"}",
|
|
155
|
+
"",
|
|
156
|
+
"pub trait Service {",
|
|
157
|
+
" fn run(&self) -> bool;",
|
|
158
|
+
"}",
|
|
159
|
+
].join("\n"),
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
java: {
|
|
163
|
+
path: "src/main/Server.java",
|
|
164
|
+
code: [
|
|
165
|
+
"package com.example;",
|
|
166
|
+
"",
|
|
167
|
+
"import java.util.List;",
|
|
168
|
+
"",
|
|
169
|
+
"public class Server {",
|
|
170
|
+
" private String name;",
|
|
171
|
+
"",
|
|
172
|
+
" public void start() {",
|
|
173
|
+
" System.out.println(name);",
|
|
174
|
+
" }",
|
|
175
|
+
"",
|
|
176
|
+
" protected static int compute() {",
|
|
177
|
+
" return 0;",
|
|
178
|
+
" }",
|
|
179
|
+
"}",
|
|
180
|
+
"",
|
|
181
|
+
"interface Handler {",
|
|
182
|
+
" void handle();",
|
|
183
|
+
"}",
|
|
184
|
+
].join("\n"),
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
c: {
|
|
188
|
+
path: "src/main.c",
|
|
189
|
+
code: [
|
|
190
|
+
"#include <stdio.h>",
|
|
191
|
+
"#include <string.h>",
|
|
192
|
+
"",
|
|
193
|
+
"typedef struct {",
|
|
194
|
+
" int x;",
|
|
195
|
+
" int y;",
|
|
196
|
+
"} Point;",
|
|
197
|
+
"",
|
|
198
|
+
"int add(int a, int b) {",
|
|
199
|
+
" return a + b;",
|
|
200
|
+
"}",
|
|
201
|
+
"",
|
|
202
|
+
"void main_loop(void) {",
|
|
203
|
+
" int sum = add(1, 2);",
|
|
204
|
+
" printf(\"%d\\n\", sum);",
|
|
205
|
+
"}",
|
|
206
|
+
].join("\n"),
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
cpp: {
|
|
210
|
+
path: "src/engine.cpp",
|
|
211
|
+
code: [
|
|
212
|
+
"#include <vector>",
|
|
213
|
+
"#include <string>",
|
|
214
|
+
"",
|
|
215
|
+
"namespace engine {",
|
|
216
|
+
"",
|
|
217
|
+
"class Widget {",
|
|
218
|
+
"public:",
|
|
219
|
+
" void render();",
|
|
220
|
+
" int getWidth() { return width_; }",
|
|
221
|
+
"private:",
|
|
222
|
+
" int width_;",
|
|
223
|
+
"};",
|
|
224
|
+
"",
|
|
225
|
+
"int compute_total(int count) {",
|
|
226
|
+
" return count * 2;",
|
|
227
|
+
"}",
|
|
228
|
+
"",
|
|
229
|
+
"} // namespace engine",
|
|
230
|
+
].join("\n"),
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
csharp: {
|
|
234
|
+
path: "src/Server.cs",
|
|
235
|
+
code: [
|
|
236
|
+
"using System;",
|
|
237
|
+
"using System.Collections.Generic;",
|
|
238
|
+
"",
|
|
239
|
+
"namespace MyApp {",
|
|
240
|
+
" public class Server {",
|
|
241
|
+
" public void Start() {",
|
|
242
|
+
" Console.WriteLine(\"started\");",
|
|
243
|
+
" }",
|
|
244
|
+
" private static int Compute() {",
|
|
245
|
+
" return 42;",
|
|
246
|
+
" }",
|
|
247
|
+
" }",
|
|
248
|
+
"",
|
|
249
|
+
" interface IService {",
|
|
250
|
+
" void Run();",
|
|
251
|
+
" }",
|
|
252
|
+
"}",
|
|
253
|
+
].join("\n"),
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
ruby: {
|
|
257
|
+
path: "lib/app.rb",
|
|
258
|
+
code: [
|
|
259
|
+
'require "json"',
|
|
260
|
+
"",
|
|
261
|
+
"class User",
|
|
262
|
+
" def initialize(name)",
|
|
263
|
+
" @name = name",
|
|
264
|
+
" end",
|
|
265
|
+
"",
|
|
266
|
+
" def greet",
|
|
267
|
+
" format_name(@name)",
|
|
268
|
+
" end",
|
|
269
|
+
"end",
|
|
270
|
+
"",
|
|
271
|
+
"module Auth",
|
|
272
|
+
" def login",
|
|
273
|
+
" true",
|
|
274
|
+
" end",
|
|
275
|
+
"end",
|
|
276
|
+
].join("\n"),
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
php: {
|
|
280
|
+
path: "src/Order.php",
|
|
281
|
+
code: [
|
|
282
|
+
"<?php",
|
|
283
|
+
"namespace App;",
|
|
284
|
+
"",
|
|
285
|
+
"use Lib\\Client;",
|
|
286
|
+
"",
|
|
287
|
+
"class Order {",
|
|
288
|
+
" public function total(): float {",
|
|
289
|
+
" return 0.0;",
|
|
290
|
+
" }",
|
|
291
|
+
" private function validate(): bool {",
|
|
292
|
+
" return true;",
|
|
293
|
+
" }",
|
|
294
|
+
"}",
|
|
295
|
+
"",
|
|
296
|
+
"function helper(): void {",
|
|
297
|
+
" $order = new Order();",
|
|
298
|
+
" $order->total();",
|
|
299
|
+
"}",
|
|
300
|
+
].join("\n"),
|
|
301
|
+
},
|
|
302
|
+
|
|
303
|
+
kotlin: {
|
|
304
|
+
path: "src/main/kotlin/Server.kt",
|
|
305
|
+
code: [
|
|
306
|
+
"package com.example",
|
|
307
|
+
"",
|
|
308
|
+
"import kotlin.collections.*",
|
|
309
|
+
"",
|
|
310
|
+
"class Server(val port: Int) {",
|
|
311
|
+
" fun start() {",
|
|
312
|
+
" println(port)",
|
|
313
|
+
" }",
|
|
314
|
+
"}",
|
|
315
|
+
"",
|
|
316
|
+
"object Singleton {",
|
|
317
|
+
" val count = 0",
|
|
318
|
+
"}",
|
|
319
|
+
"",
|
|
320
|
+
"fun create(): Server {",
|
|
321
|
+
" return Server(8080)",
|
|
322
|
+
"}",
|
|
323
|
+
].join("\n"),
|
|
324
|
+
},
|
|
325
|
+
|
|
326
|
+
swift: {
|
|
327
|
+
path: "Sources/App.swift",
|
|
328
|
+
code: [
|
|
329
|
+
"import Foundation",
|
|
330
|
+
"",
|
|
331
|
+
"class View {",
|
|
332
|
+
" func render() {",
|
|
333
|
+
" print(\"rendering\")",
|
|
334
|
+
" }",
|
|
335
|
+
"}",
|
|
336
|
+
"",
|
|
337
|
+
"protocol Drawable {",
|
|
338
|
+
" func draw()",
|
|
339
|
+
"}",
|
|
340
|
+
"",
|
|
341
|
+
"func main() {",
|
|
342
|
+
" let view = View()",
|
|
343
|
+
" view.render()",
|
|
344
|
+
"}",
|
|
345
|
+
].join("\n"),
|
|
346
|
+
},
|
|
347
|
+
|
|
348
|
+
bash: {
|
|
349
|
+
path: "scripts/deploy.sh",
|
|
350
|
+
code: [
|
|
351
|
+
"#!/bin/bash",
|
|
352
|
+
"",
|
|
353
|
+
"function deploy() {",
|
|
354
|
+
" echo \"deploying\"",
|
|
355
|
+
" rsync -avz dist/ server:/app/",
|
|
356
|
+
"}",
|
|
357
|
+
"",
|
|
358
|
+
"function rollback() {",
|
|
359
|
+
" echo \"rolling back\"",
|
|
360
|
+
" return 0",
|
|
361
|
+
"}",
|
|
362
|
+
].join("\n"),
|
|
363
|
+
},
|
|
364
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Language sniffing — map a file path to a tier-1 CodingGraphLanguage by
|
|
3
|
+
* its extension. Used when `ParseFileInput.language` is omitted.
|
|
4
|
+
*
|
|
5
|
+
* If the path doesn't match any tier-1 extension, returns `null` and the
|
|
6
|
+
* engine emits a `parse_failed` result (rule 44 — no silent skip).
|
|
7
|
+
*/
|
|
8
|
+
import { TIER_1_LANGUAGES, type CodingGraphLanguage } from "@remnic/core";
|
|
9
|
+
|
|
10
|
+
/** Extension (lowercase, dot-prefixed) → language. Static table. */
|
|
11
|
+
const EXTENSION_MAP: Record<string, CodingGraphLanguage> = {
|
|
12
|
+
".ts": "typescript",
|
|
13
|
+
".tsx": "tsx",
|
|
14
|
+
".mts": "typescript",
|
|
15
|
+
".cts": "typescript",
|
|
16
|
+
".js": "javascript",
|
|
17
|
+
".jsx": "javascript",
|
|
18
|
+
".mjs": "javascript",
|
|
19
|
+
".cjs": "javascript",
|
|
20
|
+
".py": "python",
|
|
21
|
+
".pyi": "python",
|
|
22
|
+
".go": "go",
|
|
23
|
+
".rs": "rust",
|
|
24
|
+
".java": "java",
|
|
25
|
+
".c": "c",
|
|
26
|
+
".h": "c",
|
|
27
|
+
".cpp": "cpp",
|
|
28
|
+
".cc": "cpp",
|
|
29
|
+
".cxx": "cpp",
|
|
30
|
+
".hpp": "cpp",
|
|
31
|
+
".hxx": "cpp",
|
|
32
|
+
".cs": "csharp",
|
|
33
|
+
".rb": "ruby",
|
|
34
|
+
".php": "php",
|
|
35
|
+
".kt": "kotlin",
|
|
36
|
+
".kts": "kotlin",
|
|
37
|
+
".swift": "swift",
|
|
38
|
+
".sh": "bash",
|
|
39
|
+
".bash": "bash",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Sniff the tier-1 language from a file path's extension.
|
|
44
|
+
* Returns `null` if the extension is not recognized.
|
|
45
|
+
*/
|
|
46
|
+
export function sniffLanguage(filePath: string): CodingGraphLanguage | null {
|
|
47
|
+
const dot = filePath.lastIndexOf(".");
|
|
48
|
+
if (dot < 0) return null;
|
|
49
|
+
const ext = filePath.slice(dot).toLowerCase();
|
|
50
|
+
return EXTENSION_MAP[ext] ?? null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Return true if `lang` is a valid tier-1 language. */
|
|
54
|
+
export function isTier1Language(lang: string): lang is CodingGraphLanguage {
|
|
55
|
+
return (TIER_1_LANGUAGES as readonly string[]).includes(lang);
|
|
56
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ParserBackend — the abstraction layer between the tree-sitter parsing
|
|
3
|
+
* engine and the per-language extractors.
|
|
4
|
+
*
|
|
5
|
+
* Design rationale (issue #1551): everything goes through this interface so
|
|
6
|
+
* that a native `node-tree-sitter` backend (or the external C binary via the
|
|
7
|
+
* subprocess provider) can slot in later without touching extractors. The WASM
|
|
8
|
+
* backend (`WasmTreeSitterBackend`) is the default; it costs ~2–3× parse speed
|
|
9
|
+
* vs native but has no per-platform build toolchain — the class of pain
|
|
10
|
+
* documented in #1518 and #1538.
|
|
11
|
+
*
|
|
12
|
+
* Rule 11 (no module-level caches): every parser instance, grammar cache, and
|
|
13
|
+
* initialization flag lives on the *engine instance*, never at module scope.
|
|
14
|
+
* This means two engines in the same process have fully isolated state.
|
|
15
|
+
*/
|
|
16
|
+
import { Parser, Language, type Node as TSNode, type Tree as TSTree, type Language as TSLanguage } from "web-tree-sitter";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
import { statSync } from "node:fs";
|
|
20
|
+
import type { CodingGraphLanguage } from "@remnic/core";
|
|
21
|
+
|
|
22
|
+
/** Re-export tree-sitter types so extractors don't depend on web-tree-sitter directly. */
|
|
23
|
+
export type { TSNode, TSTree, TSLanguage };
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The opaque interface a backend exposes to extractors. Each method is keyed
|
|
27
|
+
* per-instance so the engine lifecycle (`dispose()`) can clean everything up.
|
|
28
|
+
*/
|
|
29
|
+
export interface ParserBackend {
|
|
30
|
+
/** Initialize the backend runtime (e.g. load the WASM core). Idempotent per instance. */
|
|
31
|
+
init(): Promise<void>;
|
|
32
|
+
/** Ensure the grammar for `lang` is loaded and a parser is ready. Idempotent per instance. */
|
|
33
|
+
ensureLanguage(lang: CodingGraphLanguage): Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Parse `content` (a UTF-8 string) using the grammar for `lang`. Returns the
|
|
36
|
+
* tree-sitter Tree on success, or `null` if the grammar is not loaded / the
|
|
37
|
+
* parser cannot produce a tree. Node offsets are UTF-16 code-unit offsets;
|
|
38
|
+
* callers must convert to UTF-8 byte offsets for multibyte content (#1659).
|
|
39
|
+
*/
|
|
40
|
+
parse(lang: CodingGraphLanguage, content: string): TSTree | null;
|
|
41
|
+
/** Return the loaded Language for `lang`, or null if not loaded. */
|
|
42
|
+
getLanguage(lang: CodingGraphLanguage): TSLanguage | null;
|
|
43
|
+
/** Release all parsers, grammars, and the WASM runtime. Safe to call multiple times. */
|
|
44
|
+
dispose(): Promise<void>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// Grammar directory resolution.
|
|
49
|
+
//
|
|
50
|
+
// The .wasm grammar files ship inside the package at `grammars/`. Their
|
|
51
|
+
// location relative to the running module varies by context:
|
|
52
|
+
// - tsx source run (src/engine/): ../../grammars
|
|
53
|
+
// - tsup dist (dist/): ../grammars
|
|
54
|
+
// - published package (dist/): ../grammars
|
|
55
|
+
// We probe candidates and pick the first that exists. This avoids hard-coding
|
|
56
|
+
// a depth that breaks under different bundler layouts.
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
function resolveGrammarDir(): string {
|
|
60
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
61
|
+
const candidates = [
|
|
62
|
+
path.join(here, "..", "grammars"), // dist/ → ../grammars
|
|
63
|
+
path.join(here, "..", "..", "grammars"), // src/engine/ → ../../grammars
|
|
64
|
+
path.join(here, "grammars"), // flat layout fallback
|
|
65
|
+
];
|
|
66
|
+
for (const candidate of candidates) {
|
|
67
|
+
try {
|
|
68
|
+
if (statSync(candidate).isDirectory()) return candidate;
|
|
69
|
+
} catch {
|
|
70
|
+
// try next candidate
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
throw new Error(
|
|
74
|
+
`coding-graph: could not locate the grammars/ directory from ${here}. ` +
|
|
75
|
+
`Expected one of: ${candidates.join(", ")}`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* web-tree-sitter 0.25 WASM backend.
|
|
81
|
+
*
|
|
82
|
+
* Per rule 11, all mutable state (init flag, parser instance, grammar cache)
|
|
83
|
+
* lives on the instance — never at module scope. The backend is constructed by
|
|
84
|
+
* `createCodingGraphEngine` and disposed alongside the engine.
|
|
85
|
+
*/
|
|
86
|
+
export class WasmTreeSitterBackend implements ParserBackend {
|
|
87
|
+
private initialized = false;
|
|
88
|
+
private initializing: Promise<void> | null = null;
|
|
89
|
+
private parser: Parser | null = null;
|
|
90
|
+
private readonly languages = new Map<CodingGraphLanguage, Language>();
|
|
91
|
+
private readonly loadingLanguages = new Map<CodingGraphLanguage, Promise<void>>();
|
|
92
|
+
private grammarDir = "";
|
|
93
|
+
private grammarDirResolved = false;
|
|
94
|
+
private readonly grammarDirHint: string | null;
|
|
95
|
+
|
|
96
|
+
constructor(grammarDir?: string) {
|
|
97
|
+
// Defer resolveGrammarDir() to first use so that a missing grammars/
|
|
98
|
+
// directory surfaces as a per-file parse_failed (rule 44) rather than
|
|
99
|
+
// a constructor exception that bricks createCodingGraphEngine.
|
|
100
|
+
this.grammarDirHint = grammarDir ?? null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private getGrammarDir(): string {
|
|
104
|
+
if (this.grammarDirResolved) return this.grammarDir;
|
|
105
|
+
this.grammarDir = this.grammarDirHint ?? resolveGrammarDir();
|
|
106
|
+
this.grammarDirResolved = true;
|
|
107
|
+
return this.grammarDir;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async init(): Promise<void> {
|
|
111
|
+
if (this.initialized) return;
|
|
112
|
+
if (this.initializing) return this.initializing;
|
|
113
|
+
this.initializing = (async () => {
|
|
114
|
+
try {
|
|
115
|
+
await Parser.init();
|
|
116
|
+
this.parser = new Parser();
|
|
117
|
+
this.initialized = true;
|
|
118
|
+
} finally {
|
|
119
|
+
// Clear the in-flight promise on BOTH success and failure. Without
|
|
120
|
+
// this, a transient Parser.init() rejection leaves the rejected
|
|
121
|
+
// promise cached in `initializing`, so every subsequent call
|
|
122
|
+
// re-awaits the same failure instead of retrying.
|
|
123
|
+
this.initializing = null;
|
|
124
|
+
}
|
|
125
|
+
})();
|
|
126
|
+
return this.initializing;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async ensureLanguage(lang: CodingGraphLanguage): Promise<void> {
|
|
130
|
+
await this.init();
|
|
131
|
+
if (this.languages.has(lang)) return;
|
|
132
|
+
if (this.loadingLanguages.has(lang)) return this.loadingLanguages.get(lang)!;
|
|
133
|
+
const p = (async () => {
|
|
134
|
+
try {
|
|
135
|
+
const wasmPath = path.join(this.getGrammarDir(), grammarFileName(lang));
|
|
136
|
+
const language = await Language.load(wasmPath);
|
|
137
|
+
this.languages.set(lang, language);
|
|
138
|
+
} finally {
|
|
139
|
+
// Drop the in-flight promise on BOTH success and failure. Without
|
|
140
|
+
// this, a transient Language.load() rejection leaves the rejected
|
|
141
|
+
// promise cached in `loadingLanguages`, permanently bricking that
|
|
142
|
+
// language until dispose() — the next call would re-await the same
|
|
143
|
+
// failure instead of retrying the load.
|
|
144
|
+
this.loadingLanguages.delete(lang);
|
|
145
|
+
}
|
|
146
|
+
})();
|
|
147
|
+
this.loadingLanguages.set(lang, p);
|
|
148
|
+
return p;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
parse(lang: CodingGraphLanguage, content: string): TSTree | null {
|
|
152
|
+
if (!this.parser) return null;
|
|
153
|
+
const language = this.languages.get(lang);
|
|
154
|
+
if (!language) return null;
|
|
155
|
+
this.parser.setLanguage(language);
|
|
156
|
+
return this.parser.parse(content);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Return the loaded Language object for `lang`, or null if not loaded. */
|
|
160
|
+
getLanguage(lang: CodingGraphLanguage): Language | null {
|
|
161
|
+
return this.languages.get(lang) ?? null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async dispose(): Promise<void> {
|
|
165
|
+
if (this.parser) {
|
|
166
|
+
try {
|
|
167
|
+
this.parser.delete();
|
|
168
|
+
} catch {
|
|
169
|
+
// already deleted — ignore
|
|
170
|
+
}
|
|
171
|
+
this.parser = null;
|
|
172
|
+
}
|
|
173
|
+
this.languages.clear();
|
|
174
|
+
this.loadingLanguages.clear();
|
|
175
|
+
this.initialized = false;
|
|
176
|
+
this.initializing = null;
|
|
177
|
+
this.grammarDirResolved = false;
|
|
178
|
+
this.grammarDir = "";
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Map a tier-1 language to its .wasm grammar filename. The names match the
|
|
184
|
+
* files shipped by `tree-sitter-wasms` (0.1.13) and the grammars/ directory.
|
|
185
|
+
* C# uses `c_sharp` because the upstream grammar names C# with an underscore.
|
|
186
|
+
*/
|
|
187
|
+
export function grammarFileName(lang: CodingGraphLanguage): string {
|
|
188
|
+
const map: Record<CodingGraphLanguage, string> = {
|
|
189
|
+
typescript: "tree-sitter-typescript.wasm",
|
|
190
|
+
tsx: "tree-sitter-tsx.wasm",
|
|
191
|
+
javascript: "tree-sitter-javascript.wasm",
|
|
192
|
+
python: "tree-sitter-python.wasm",
|
|
193
|
+
go: "tree-sitter-go.wasm",
|
|
194
|
+
rust: "tree-sitter-rust.wasm",
|
|
195
|
+
java: "tree-sitter-java.wasm",
|
|
196
|
+
c: "tree-sitter-c.wasm",
|
|
197
|
+
cpp: "tree-sitter-cpp.wasm",
|
|
198
|
+
csharp: "tree-sitter-c_sharp.wasm",
|
|
199
|
+
ruby: "tree-sitter-ruby.wasm",
|
|
200
|
+
php: "tree-sitter-php.wasm",
|
|
201
|
+
kotlin: "tree-sitter-kotlin.wasm",
|
|
202
|
+
swift: "tree-sitter-swift.wasm",
|
|
203
|
+
bash: "tree-sitter-bash.wasm",
|
|
204
|
+
};
|
|
205
|
+
return map[lang];
|
|
206
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UTF-16 code-unit offset → UTF-8 byte offset conversion (issue #1659 item 3).
|
|
3
|
+
*
|
|
4
|
+
* web-tree-sitter 0.25 parses strings internally by converting them to UTF-16
|
|
5
|
+
* (`stringToUTF16` in the WASM glue), so every node's `startIndex`/`endIndex`
|
|
6
|
+
* is a UTF-16 code-unit offset, NOT a UTF-8 byte offset. For pure-ASCII
|
|
7
|
+
* content these are identical, but multibyte content (comments, strings, or
|
|
8
|
+
* identifiers containing non-ASCII characters) produces incorrect spans:
|
|
9
|
+
* dead-code analysis, navigation, and body extraction all rely on byte
|
|
10
|
+
* offsets matching the on-disk file.
|
|
11
|
+
*
|
|
12
|
+
* This module builds a single lookup array per file (O(n) build, O(1)
|
|
13
|
+
* lookup) so all offsets in a FileIR can be converted in one pass.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Build a map from UTF-16 code-unit offset → UTF-8 byte offset for the given
|
|
18
|
+
* string. `map[i]` is the byte offset of the i-th UTF-16 code unit.
|
|
19
|
+
* `map[content.length]` is the total byte length.
|
|
20
|
+
*
|
|
21
|
+
* Surrogate pairs (U+10000–U+10FFFF) occupy 2 UTF-16 code units but encode to
|
|
22
|
+
* 4 UTF-8 bytes. The high surrogate's entry points to the start of the pair;
|
|
23
|
+
* the low surrogate's entry also points there (its byte delta is 0, added by
|
|
24
|
+
* the high surrogate).
|
|
25
|
+
*/
|
|
26
|
+
export function buildUtf16ToByteOffsetMap(content: string): Uint32Array {
|
|
27
|
+
const map = new Uint32Array(content.length + 1);
|
|
28
|
+
let byteOffset = 0;
|
|
29
|
+
for (let i = 0; i < content.length; i++) {
|
|
30
|
+
map[i] = byteOffset;
|
|
31
|
+
const code = content.charCodeAt(i);
|
|
32
|
+
if (code < 0x80) {
|
|
33
|
+
byteOffset += 1;
|
|
34
|
+
} else if (code < 0x800) {
|
|
35
|
+
byteOffset += 2;
|
|
36
|
+
} else if (code >= 0xd800 && code <= 0xdbff) {
|
|
37
|
+
// High surrogate of a surrogate pair. The full code point encodes to
|
|
38
|
+
// 4 UTF-8 bytes; we charge all 4 here and skip the low surrogate.
|
|
39
|
+
byteOffset += 4;
|
|
40
|
+
// Both code units of the pair represent the SAME character, which
|
|
41
|
+
// starts at map[i] (set at the top of the loop, before the increment).
|
|
42
|
+
// The low surrogate must map to that SAME start byte — not to
|
|
43
|
+
// byteOffset (which is now past the pair) — or a span whose start
|
|
44
|
+
// falls on the low code unit gets an inflated startByte (cursor
|
|
45
|
+
// #1659 review: 'Surrogate UTF-16 index maps wrong').
|
|
46
|
+
map[i + 1] = map[i]!;
|
|
47
|
+
i++;
|
|
48
|
+
} else if (code >= 0xdc00 && code <= 0xdfff) {
|
|
49
|
+
// Lone low surrogate (unpaired) — treat as 3-byte UTF-8 replacement.
|
|
50
|
+
// This path is only hit for malformed input.
|
|
51
|
+
byteOffset += 3;
|
|
52
|
+
} else {
|
|
53
|
+
byteOffset += 3;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
map[content.length] = byteOffset;
|
|
57
|
+
return map;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Convert a single UTF-16 code-unit offset to a UTF-8 byte offset using a
|
|
62
|
+
* pre-built map. Returns 0 for negative offsets; clamps to the map length.
|
|
63
|
+
*/
|
|
64
|
+
export function utf16ToByte(map: Uint32Array, utf16Offset: number): number {
|
|
65
|
+
if (utf16Offset <= 0) return 0;
|
|
66
|
+
if (utf16Offset >= map.length) return map[map.length - 1];
|
|
67
|
+
return map[utf16Offset];
|
|
68
|
+
}
|