@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,1417 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Engine test suite (issue #1551 PR2):
|
|
3
|
+
* 1. Gate-off parity — createCodingGraphEngine no longer throws
|
|
4
|
+
* 2. Fixture-IR snapshot tests — all 15 tier-1 languages
|
|
5
|
+
* 3. Determinism — same file parsed twice → byte-identical serialized IR
|
|
6
|
+
* 4. Error handling — unsupported language returns parse_failed, no throw
|
|
7
|
+
* 5. Qualified-name nesting — parent linkage is correct
|
|
8
|
+
* 6. Dispose lifecycle — engine can be disposed and re-created
|
|
9
|
+
*/
|
|
10
|
+
import assert from "node:assert/strict";
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
import test from "node:test";
|
|
13
|
+
|
|
14
|
+
import { TIER_1_LANGUAGES, type CodingGraphLanguage, type FileIR } from "@remnic/core";
|
|
15
|
+
|
|
16
|
+
import { createCodingGraphEngine } from "./engine.js";
|
|
17
|
+
import { FIXTURES } from "./fixtures.js";
|
|
18
|
+
import { hashContent } from "./emit.js";
|
|
19
|
+
import { hashContent as hashContentFromReindex } from "../reindex.js";
|
|
20
|
+
import { sniffLanguage } from "./language-sniff.js";
|
|
21
|
+
import { buildUtf16ToByteOffsetMap } from "./utf16-offsets.js";
|
|
22
|
+
import { WasmTreeSitterBackend } from "./parser-backend.js";
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Deterministic IR serialization — sorts every collection and object key so
|
|
26
|
+
// the output is byte-identical across runs (rule 38).
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
function canonicalize(value: unknown): unknown {
|
|
30
|
+
if (value === null || typeof value !== "object") return value;
|
|
31
|
+
if (Array.isArray(value)) {
|
|
32
|
+
return value.map(canonicalize);
|
|
33
|
+
}
|
|
34
|
+
const sortedKeys = Object.keys(value as Record<string, unknown>).sort();
|
|
35
|
+
const result: Record<string, unknown> = {};
|
|
36
|
+
for (const key of sortedKeys) {
|
|
37
|
+
result[key] = canonicalize((value as Record<string, unknown>)[key]);
|
|
38
|
+
}
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function serializeIR(ir: FileIR): string {
|
|
43
|
+
return JSON.stringify(canonicalize(ir));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// 1. Gate-off parity — createCodingGraphEngine no longer throws.
|
|
48
|
+
// PR1 threw CodingGraphError("not_implemented"); PR2 returns a real engine.
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
test("gate-off: createCodingGraphEngine does not throw (no longer not_implemented)", () => {
|
|
52
|
+
const engine = createCodingGraphEngine();
|
|
53
|
+
assert.equal(typeof engine.engineVersion, "string");
|
|
54
|
+
assert.ok(engine.engineVersion.length > 0);
|
|
55
|
+
assert.ok(engine.supportedLanguages.length >= 15);
|
|
56
|
+
assert.deepEqual(
|
|
57
|
+
[...engine.supportedLanguages].sort(),
|
|
58
|
+
[...TIER_1_LANGUAGES].sort(),
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("gate-off: engine reports correct version from @remnic/core", () => {
|
|
63
|
+
const engine = createCodingGraphEngine();
|
|
64
|
+
// The version must match the single source of truth in core.
|
|
65
|
+
assert.equal(engine.engineVersion, "0.1.0-pr1");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// 2. Fixture-IR snapshot tests for all 15 tier-1 languages.
|
|
70
|
+
//
|
|
71
|
+
// Each fixture is parsed and its IR is validated for structural correctness:
|
|
72
|
+
// - contentHash is a 64-char hex SHA-256 of the raw bytes
|
|
73
|
+
// - symbols have correct kinds, names, qualified names, and parent linkage
|
|
74
|
+
// - imports have correct module specifiers
|
|
75
|
+
// - the IR path and language match the input
|
|
76
|
+
//
|
|
77
|
+
// The serialized IR is also captured as a snapshot string for future
|
|
78
|
+
// regression comparison.
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
test("fixture-IR: all tier-1 languages produce valid IR", async (t) => {
|
|
82
|
+
const engine = createCodingGraphEngine();
|
|
83
|
+
|
|
84
|
+
for (const lang of TIER_1_LANGUAGES) {
|
|
85
|
+
await t.test(`${lang} fixture parses successfully`, async () => {
|
|
86
|
+
const fixture = FIXTURES[lang];
|
|
87
|
+
const content = Buffer.from(fixture.code, "utf-8");
|
|
88
|
+
const result = await engine.parseFile({
|
|
89
|
+
path: fixture.path,
|
|
90
|
+
content,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
assert.ok(result.ok, `${lang}: parseFile must succeed, got: ${result.ok ? "" : result.message}`);
|
|
94
|
+
if (!result.ok) return;
|
|
95
|
+
|
|
96
|
+
const ir = result.ir;
|
|
97
|
+
|
|
98
|
+
// Path and language match.
|
|
99
|
+
assert.equal(ir.path, fixture.path, `${lang}: path mismatch`);
|
|
100
|
+
assert.equal(ir.language, lang, `${lang}: language mismatch`);
|
|
101
|
+
|
|
102
|
+
// contentHash is a valid SHA-256 hex string of the raw bytes.
|
|
103
|
+
assert.equal(ir.contentHash, hashContent(content), `${lang}: contentHash mismatch`);
|
|
104
|
+
assert.match(ir.contentHash, /^[0-9a-f]{64}$/, `${lang}: contentHash must be SHA-256 hex`);
|
|
105
|
+
|
|
106
|
+
// Every symbol has required fields.
|
|
107
|
+
for (const sym of ir.symbols) {
|
|
108
|
+
assert.ok(sym.name.length > 0, `${lang}: symbol name must be non-empty`);
|
|
109
|
+
assert.ok(sym.qualifiedName.length > 0, `${lang}: qualifiedName must be non-empty`);
|
|
110
|
+
assert.ok(sym.span.endByte > sym.span.startByte, `${lang}: span must be non-empty`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Every import has a module.
|
|
114
|
+
for (const imp of ir.imports) {
|
|
115
|
+
assert.ok(imp.module.length > 0, `${lang}: import module must be non-empty`);
|
|
116
|
+
assert.ok(imp.span.endByte > imp.span.startByte, `${lang}: import span must be non-empty`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Capture snapshot for regression detection.
|
|
120
|
+
const snapshot = serializeIR(ir);
|
|
121
|
+
assert.ok(snapshot.length > 0, `${lang}: IR must serialize`);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
await engine.dispose();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// Per-language structural assertions — verify specific symbols, imports,
|
|
130
|
+
// exports, and call sites for representative languages.
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
test("fixture-IR: TypeScript has correct symbol structure", async () => {
|
|
134
|
+
const engine = createCodingGraphEngine();
|
|
135
|
+
const fixture = FIXTURES.typescript;
|
|
136
|
+
const result = await engine.parseFile({
|
|
137
|
+
path: fixture.path,
|
|
138
|
+
content: Buffer.from(fixture.code, "utf-8"),
|
|
139
|
+
});
|
|
140
|
+
assert.ok(result.ok);
|
|
141
|
+
if (!result.ok) return;
|
|
142
|
+
|
|
143
|
+
const ir = result.ir;
|
|
144
|
+
|
|
145
|
+
// Should have at least: AppOptions (interface), Mode (type), Server (class),
|
|
146
|
+
// createServer (function), and the methods inside Server.
|
|
147
|
+
const names = ir.symbols.map((s) => s.name);
|
|
148
|
+
assert.ok(names.includes("AppOptions"), `TS: expected AppOptions, got ${names.join(", ")}`);
|
|
149
|
+
assert.ok(names.includes("Mode"), `TS: expected Mode, got ${names.join(", ")}`);
|
|
150
|
+
assert.ok(names.includes("Server"), `TS: expected Server, got ${names.join(", ")}`);
|
|
151
|
+
assert.ok(names.includes("createServer"), `TS: expected createServer, got ${names.join(", ")}`);
|
|
152
|
+
|
|
153
|
+
// Server class should have qualified name "Server" and contain a method.
|
|
154
|
+
const server = ir.symbols.find((s) => s.name === "Server");
|
|
155
|
+
assert.ok(server, "TS: Server symbol must exist");
|
|
156
|
+
assert.equal(server!.kind, "class");
|
|
157
|
+
|
|
158
|
+
// The method start() should be nested under Server.
|
|
159
|
+
const startMethod = ir.symbols.find((s) => s.name === "start");
|
|
160
|
+
assert.ok(startMethod, "TS: start method must exist");
|
|
161
|
+
assert.equal(startMethod!.kind, "method");
|
|
162
|
+
assert.ok(
|
|
163
|
+
startMethod!.parentQualifiedName?.includes("Server"),
|
|
164
|
+
`TS: start should be under Server, got parentQualifiedName=${startMethod!.parentQualifiedName}`,
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
// Imports: express + types.
|
|
168
|
+
assert.ok(ir.imports.some((i) => i.module === "express"), "TS: should import express");
|
|
169
|
+
assert.ok(ir.imports.some((i) => i.module === "./types"), "TS: should import ./types");
|
|
170
|
+
|
|
171
|
+
// Exports.
|
|
172
|
+
const exportNames = ir.exports.map((e) => e.name);
|
|
173
|
+
assert.ok(exportNames.includes("AppOptions"), `TS: should export AppOptions, got ${exportNames.join(", ")}`);
|
|
174
|
+
assert.ok(exportNames.includes("Server"), `TS: should export Server`);
|
|
175
|
+
assert.ok(exportNames.includes("createServer"), `TS: should export createServer`);
|
|
176
|
+
|
|
177
|
+
await engine.dispose();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("fixture-IR: Python has correct class/method nesting", async () => {
|
|
181
|
+
const engine = createCodingGraphEngine();
|
|
182
|
+
const fixture = FIXTURES.python;
|
|
183
|
+
const result = await engine.parseFile({
|
|
184
|
+
path: fixture.path,
|
|
185
|
+
content: Buffer.from(fixture.code, "utf-8"),
|
|
186
|
+
});
|
|
187
|
+
assert.ok(result.ok);
|
|
188
|
+
if (!result.ok) return;
|
|
189
|
+
|
|
190
|
+
const ir = result.ir;
|
|
191
|
+
|
|
192
|
+
const user = ir.symbols.find((s) => s.name === "User");
|
|
193
|
+
assert.ok(user, "Python: User class must exist");
|
|
194
|
+
assert.equal(user!.kind, "class");
|
|
195
|
+
|
|
196
|
+
const greet = ir.symbols.find((s) => s.name === "greet");
|
|
197
|
+
assert.ok(greet, "Python: greet method must exist");
|
|
198
|
+
assert.equal(greet!.kind, "function"); // tree-sitter-python treats def as function_definition
|
|
199
|
+
assert.ok(
|
|
200
|
+
greet!.parentQualifiedName?.includes("User"),
|
|
201
|
+
`Python: greet should be under User, got ${greet!.parentQualifiedName}`,
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
// format_name and main should be top-level functions.
|
|
205
|
+
const formatName = ir.symbols.find((s) => s.name === "format_name");
|
|
206
|
+
assert.ok(formatName, "Python: format_name must exist");
|
|
207
|
+
assert.ok(!formatName!.parentQualifiedName, "Python: format_name should be top-level");
|
|
208
|
+
|
|
209
|
+
// Imports.
|
|
210
|
+
assert.ok(ir.imports.some((i) => i.module === "typing"), "Python: should import from typing");
|
|
211
|
+
assert.ok(ir.imports.some((i) => i.module === "dataclasses"), "Python: should import from dataclasses");
|
|
212
|
+
|
|
213
|
+
await engine.dispose();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("fixture-IR: Go has struct/function/method structure", async () => {
|
|
217
|
+
const engine = createCodingGraphEngine();
|
|
218
|
+
const fixture = FIXTURES.go;
|
|
219
|
+
const result = await engine.parseFile({
|
|
220
|
+
path: fixture.path,
|
|
221
|
+
content: Buffer.from(fixture.code, "utf-8"),
|
|
222
|
+
});
|
|
223
|
+
assert.ok(result.ok);
|
|
224
|
+
if (!result.ok) return;
|
|
225
|
+
|
|
226
|
+
const ir = result.ir;
|
|
227
|
+
|
|
228
|
+
const server = ir.symbols.find((s) => s.name === "Server");
|
|
229
|
+
assert.ok(server, "Go: Server struct must exist");
|
|
230
|
+
assert.equal(server!.kind, "class"); // struct_type → class
|
|
231
|
+
|
|
232
|
+
const start = ir.symbols.find((s) => s.name === "Start");
|
|
233
|
+
assert.ok(start, "Go: Start method must exist");
|
|
234
|
+
assert.equal(start!.kind, "method"); // method_declaration → method
|
|
235
|
+
|
|
236
|
+
assert.ok(ir.imports.some((i) => i.module === "fmt"), "Go: should import fmt");
|
|
237
|
+
|
|
238
|
+
await engine.dispose();
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("fixture-IR: Rust has struct/impl/trait structure", async () => {
|
|
242
|
+
const engine = createCodingGraphEngine();
|
|
243
|
+
const fixture = FIXTURES.rust;
|
|
244
|
+
const result = await engine.parseFile({
|
|
245
|
+
path: fixture.path,
|
|
246
|
+
content: Buffer.from(fixture.code, "utf-8"),
|
|
247
|
+
});
|
|
248
|
+
assert.ok(result.ok);
|
|
249
|
+
if (!result.ok) return;
|
|
250
|
+
|
|
251
|
+
const ir = result.ir;
|
|
252
|
+
|
|
253
|
+
const add = ir.symbols.find((s) => s.name === "add");
|
|
254
|
+
assert.ok(add, "Rust: add function must exist");
|
|
255
|
+
assert.equal(add!.kind, "function");
|
|
256
|
+
|
|
257
|
+
const config = ir.symbols.find((s) => s.name === "Config");
|
|
258
|
+
assert.ok(config, "Rust: Config struct must exist");
|
|
259
|
+
assert.equal(config!.kind, "class");
|
|
260
|
+
|
|
261
|
+
const service = ir.symbols.find((s) => s.name === "Service");
|
|
262
|
+
assert.ok(service, "Rust: Service trait must exist");
|
|
263
|
+
assert.equal(service!.kind, "interface");
|
|
264
|
+
|
|
265
|
+
assert.ok(ir.imports.some((i) => i.module === "std::collections::HashMap"), "Rust: should import HashMap");
|
|
266
|
+
|
|
267
|
+
await engine.dispose();
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("fixture-IR: C# has class/interface structure", async () => {
|
|
271
|
+
const engine = createCodingGraphEngine();
|
|
272
|
+
const fixture = FIXTURES.csharp;
|
|
273
|
+
const result = await engine.parseFile({
|
|
274
|
+
path: fixture.path,
|
|
275
|
+
content: Buffer.from(fixture.code, "utf-8"),
|
|
276
|
+
});
|
|
277
|
+
assert.ok(result.ok);
|
|
278
|
+
if (!result.ok) return;
|
|
279
|
+
|
|
280
|
+
const ir = result.ir;
|
|
281
|
+
const names = ir.symbols.map((s) => s.name);
|
|
282
|
+
assert.ok(names.includes("Server"), `C#: expected Server, got ${names.join(", ")}`);
|
|
283
|
+
assert.ok(names.includes("IService"), `C#: expected IService, got ${names.join(", ")}`);
|
|
284
|
+
assert.ok(ir.imports.some((i) => i.module === "System"), "C#: should import System");
|
|
285
|
+
|
|
286
|
+
await engine.dispose();
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test("fixture-IR: Ruby has class/module structure", async () => {
|
|
290
|
+
const engine = createCodingGraphEngine();
|
|
291
|
+
const fixture = FIXTURES.ruby;
|
|
292
|
+
const result = await engine.parseFile({
|
|
293
|
+
path: fixture.path,
|
|
294
|
+
content: Buffer.from(fixture.code, "utf-8"),
|
|
295
|
+
});
|
|
296
|
+
assert.ok(result.ok);
|
|
297
|
+
if (!result.ok) return;
|
|
298
|
+
|
|
299
|
+
const ir = result.ir;
|
|
300
|
+
const userClass = ir.symbols.find((s) => s.name === "User");
|
|
301
|
+
assert.ok(userClass, "Ruby: User class must exist");
|
|
302
|
+
assert.equal(userClass!.kind, "class");
|
|
303
|
+
|
|
304
|
+
const authModule = ir.symbols.find((s) => s.name === "Auth");
|
|
305
|
+
assert.ok(authModule, "Ruby: Auth module must exist");
|
|
306
|
+
assert.equal(authModule!.kind, "module");
|
|
307
|
+
|
|
308
|
+
assert.ok(ir.imports.some((i) => i.module === "json"), "Ruby: should require json");
|
|
309
|
+
|
|
310
|
+
await engine.dispose();
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test("fixture-IR: JavaScript has class/method/function structure", async () => {
|
|
314
|
+
const engine = createCodingGraphEngine();
|
|
315
|
+
const fixture = FIXTURES.javascript;
|
|
316
|
+
const result = await engine.parseFile({
|
|
317
|
+
path: fixture.path,
|
|
318
|
+
content: Buffer.from(fixture.code, "utf-8"),
|
|
319
|
+
});
|
|
320
|
+
assert.ok(result.ok);
|
|
321
|
+
if (!result.ok) return;
|
|
322
|
+
|
|
323
|
+
const ir = result.ir;
|
|
324
|
+
const names = ir.symbols.map((s) => s.name);
|
|
325
|
+
assert.ok(names.includes("createRouter"), `JS: expected createRouter, got ${names.join(", ")}`);
|
|
326
|
+
assert.ok(names.includes("App"), `JS: expected App class, got ${names.join(", ")}`);
|
|
327
|
+
|
|
328
|
+
const app = ir.symbols.find((s) => s.name === "App");
|
|
329
|
+
assert.ok(app, "JS: App class must exist");
|
|
330
|
+
assert.equal(app!.kind, "class");
|
|
331
|
+
|
|
332
|
+
await engine.dispose();
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
test("fixture-IR: C has function/typedef structure", async () => {
|
|
336
|
+
const engine = createCodingGraphEngine();
|
|
337
|
+
const fixture = FIXTURES.c;
|
|
338
|
+
const result = await engine.parseFile({
|
|
339
|
+
path: fixture.path,
|
|
340
|
+
content: Buffer.from(fixture.code, "utf-8"),
|
|
341
|
+
});
|
|
342
|
+
assert.ok(result.ok);
|
|
343
|
+
if (!result.ok) return;
|
|
344
|
+
|
|
345
|
+
const ir = result.ir;
|
|
346
|
+
const add = ir.symbols.find((s) => s.name === "add");
|
|
347
|
+
assert.ok(add, "C: add function must exist");
|
|
348
|
+
assert.equal(add!.kind, "function");
|
|
349
|
+
|
|
350
|
+
assert.ok(ir.imports.some((i) => i.module === "stdio.h"), "C: should include stdio.h");
|
|
351
|
+
assert.ok(ir.imports.some((i) => i.module === "string.h"), "C: should include string.h");
|
|
352
|
+
|
|
353
|
+
await engine.dispose();
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
test("fixture-IR: Java has class/interface/method structure", async () => {
|
|
357
|
+
const engine = createCodingGraphEngine();
|
|
358
|
+
const fixture = FIXTURES.java;
|
|
359
|
+
const result = await engine.parseFile({
|
|
360
|
+
path: fixture.path,
|
|
361
|
+
content: Buffer.from(fixture.code, "utf-8"),
|
|
362
|
+
});
|
|
363
|
+
assert.ok(result.ok);
|
|
364
|
+
if (!result.ok) return;
|
|
365
|
+
|
|
366
|
+
const ir = result.ir;
|
|
367
|
+
const names = ir.symbols.map((s) => s.name);
|
|
368
|
+
assert.ok(names.includes("Server"), `Java: expected Server, got ${names.join(", ")}`);
|
|
369
|
+
assert.ok(names.includes("Handler"), `Java: expected Handler, got ${names.join(", ")}`);
|
|
370
|
+
|
|
371
|
+
// Server.start should be a method.
|
|
372
|
+
const start = ir.symbols.find((s) => s.name === "start");
|
|
373
|
+
assert.ok(start, "Java: start method must exist");
|
|
374
|
+
assert.equal(start!.kind, "method");
|
|
375
|
+
assert.ok(start!.parentQualifiedName?.includes("Server"), "Java: start should be under Server");
|
|
376
|
+
|
|
377
|
+
assert.ok(ir.imports.some((i) => i.module === "java.util.List"), "Java: should import java.util.List");
|
|
378
|
+
|
|
379
|
+
await engine.dispose();
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
// ---------------------------------------------------------------------------
|
|
383
|
+
// 2b. Review-thread fixes (#1551 PR2 — PR #1652).
|
|
384
|
+
// - CommonJS require() imports (threads PRRT_kwDORJXyws6Oc9au / -M8)
|
|
385
|
+
// - Arrow/function-expression declarators as symbols (thread -M-)
|
|
386
|
+
// - Full C# qualified_name in using directives (thread -NB)
|
|
387
|
+
// - Failed grammar load allows retry, not a cached rejection (thread 9at)
|
|
388
|
+
// ---------------------------------------------------------------------------
|
|
389
|
+
|
|
390
|
+
test("require-imports: CommonJS require() produces an import edge", async () => {
|
|
391
|
+
const engine = createCodingGraphEngine();
|
|
392
|
+
const fixture = FIXTURES.javascript; // starts with: const express = require("express");
|
|
393
|
+
const result = await engine.parseFile({
|
|
394
|
+
path: fixture.path,
|
|
395
|
+
content: Buffer.from(fixture.code, "utf-8"),
|
|
396
|
+
});
|
|
397
|
+
assert.ok(result.ok);
|
|
398
|
+
if (!result.ok) return;
|
|
399
|
+
|
|
400
|
+
assert.ok(
|
|
401
|
+
result.ir.imports.some((i) => i.module === "express"),
|
|
402
|
+
`JS: require("express") must produce an import, got modules: ${result.ir.imports.map((i) => i.module).join(", ")}`,
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
await engine.dispose();
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
test("require-imports: CommonJS require with destructuring variants", async () => {
|
|
409
|
+
const engine = createCodingGraphEngine();
|
|
410
|
+
const code = [
|
|
411
|
+
'const path = require("path");',
|
|
412
|
+
'const { readFile } = require("fs");',
|
|
413
|
+
"",
|
|
414
|
+
"function main() { readFile('x'); }",
|
|
415
|
+
"",
|
|
416
|
+
"module.exports = { main };",
|
|
417
|
+
].join("\n");
|
|
418
|
+
const result = await engine.parseFile({ path: "lib/cjs.js", content: Buffer.from(code, "utf-8") });
|
|
419
|
+
assert.ok(result.ok);
|
|
420
|
+
if (!result.ok) return;
|
|
421
|
+
|
|
422
|
+
const modules = result.ir.imports.map((i) => i.module);
|
|
423
|
+
assert.ok(modules.includes("path"), `should import path via require, got: ${modules.join(", ")}`);
|
|
424
|
+
assert.ok(modules.includes("fs"), `should import fs via require, got: ${modules.join(", ")}`);
|
|
425
|
+
|
|
426
|
+
await engine.dispose();
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
test("arrow-fn-decls: const handler = () => {} indexed as function symbol", async () => {
|
|
430
|
+
const engine = createCodingGraphEngine();
|
|
431
|
+
const fixture = FIXTURES.tsx; // contains: export const Container = () => { ... };
|
|
432
|
+
const result = await engine.parseFile({
|
|
433
|
+
path: fixture.path,
|
|
434
|
+
content: Buffer.from(fixture.code, "utf-8"),
|
|
435
|
+
});
|
|
436
|
+
assert.ok(result.ok);
|
|
437
|
+
if (!result.ok) return;
|
|
438
|
+
|
|
439
|
+
const container = result.ir.symbols.find((s) => s.name === "Container");
|
|
440
|
+
assert.ok(container, "TSX: Container arrow-fn symbol must exist");
|
|
441
|
+
assert.equal(container!.kind, "function", "TSX: Container should be a function symbol");
|
|
442
|
+
|
|
443
|
+
await engine.dispose();
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
test("arrow-fn-decls: function-expression declarators also indexed", async () => {
|
|
447
|
+
const engine = createCodingGraphEngine();
|
|
448
|
+
const code = [
|
|
449
|
+
'import { Router } from "express";',
|
|
450
|
+
"",
|
|
451
|
+
"const handler = function () { return 42; };",
|
|
452
|
+
"const arrow = () => { return handler(); };",
|
|
453
|
+
"",
|
|
454
|
+
"export { handler, arrow };",
|
|
455
|
+
].join("\n");
|
|
456
|
+
const result = await engine.parseFile({ path: "src/fns.ts", content: Buffer.from(code, "utf-8") });
|
|
457
|
+
assert.ok(result.ok);
|
|
458
|
+
if (!result.ok) return;
|
|
459
|
+
|
|
460
|
+
const names = result.ir.symbols.map((s) => s.name);
|
|
461
|
+
assert.ok(names.includes("handler"), `TS: function-expression declarator should be a symbol, got: ${names.join(", ")}`);
|
|
462
|
+
assert.ok(names.includes("arrow"), `TS: arrow-function declarator should be a symbol, got: ${names.join(", ")}`);
|
|
463
|
+
|
|
464
|
+
await engine.dispose();
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
test("csharp-usings: qualified namespace captured in full", async () => {
|
|
468
|
+
const engine = createCodingGraphEngine();
|
|
469
|
+
const fixture = FIXTURES.csharp; // contains: using System.Collections.Generic;
|
|
470
|
+
const result = await engine.parseFile({
|
|
471
|
+
path: fixture.path,
|
|
472
|
+
content: Buffer.from(fixture.code, "utf-8"),
|
|
473
|
+
});
|
|
474
|
+
assert.ok(result.ok);
|
|
475
|
+
if (!result.ok) return;
|
|
476
|
+
|
|
477
|
+
const modules = result.ir.imports.map((i) => i.module);
|
|
478
|
+
assert.ok(
|
|
479
|
+
modules.includes("System.Collections.Generic"),
|
|
480
|
+
`C#: using System.Collections.Generic must capture the full qualified name, got: ${modules.join(", ")}`,
|
|
481
|
+
);
|
|
482
|
+
assert.ok(modules.includes("System"), "C#: simple using System should still work");
|
|
483
|
+
|
|
484
|
+
await engine.dispose();
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
test("retry: failed grammar load allows a fresh retry, not a cached rejection", async () => {
|
|
488
|
+
// Point the backend at a nonexistent grammar dir so Language.load rejects.
|
|
489
|
+
const backend = new WasmTreeSitterBackend(`/nonexistent-grammar-${Date.now()}`);
|
|
490
|
+
await backend.init(); // Parser.init() succeeds without grammar files.
|
|
491
|
+
|
|
492
|
+
// First load attempt fails — the wasm file doesn't exist.
|
|
493
|
+
const p1 = backend.ensureLanguage("javascript");
|
|
494
|
+
try {
|
|
495
|
+
await p1;
|
|
496
|
+
assert.fail("first ensureLanguage should have rejected on a missing grammar");
|
|
497
|
+
} catch {
|
|
498
|
+
// expected ENOENT / load failure
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Second attempt: before the fix, loadingLanguages was never cleaned on
|
|
502
|
+
// failure, so this returned the SAME cached rejected promise. After the
|
|
503
|
+
// fix, the finally block clears the cache, so this is a FRESH attempt
|
|
504
|
+
// (a brand-new promise that re-runs Language.load).
|
|
505
|
+
const p2 = backend.ensureLanguage("javascript");
|
|
506
|
+
try {
|
|
507
|
+
await p2;
|
|
508
|
+
} catch {
|
|
509
|
+
// expected to fail again (still the same bad dir)
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
assert.notStrictEqual(
|
|
513
|
+
p1,
|
|
514
|
+
p2,
|
|
515
|
+
"retry must return a fresh promise, not the cached rejection — loadingLanguages should be cleared on failure",
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
await backend.dispose();
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
// ---------------------------------------------------------------------------
|
|
522
|
+
// 2c. Round-2 review-thread fixes (#1551 PR2 — PR #1652).
|
|
523
|
+
// - Constructor does not throw on missing grammar dir (thread dEv5)
|
|
524
|
+
// - parseFile catches extraction errors → parse_failed (thread dEv7)
|
|
525
|
+
// - Named JS route handlers: app.get("/x", handler) (thread dFOD)
|
|
526
|
+
// - Multi-module imports: Python import os, sys (thread dFOF)
|
|
527
|
+
// - Python route handler names not 'anonymous' (thread dFOG)
|
|
528
|
+
// ---------------------------------------------------------------------------
|
|
529
|
+
|
|
530
|
+
test("ctor-safe: WasmTreeSitterBackend constructor does not throw on missing grammar dir", () => {
|
|
531
|
+
// Before the fix, resolveGrammarDir() ran eagerly in the constructor and
|
|
532
|
+
// threw if grammars/ was missing — bricking createCodingGraphEngine.
|
|
533
|
+
// Now resolution is deferred to first ensureLanguage() call.
|
|
534
|
+
const backend = new WasmTreeSitterBackend("/nonexistent-dir-" + Date.now());
|
|
535
|
+
assert.ok(backend instanceof WasmTreeSitterBackend, "constructor must not throw");
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
test("parse-safe: extraction errors surface as parse_failed, not thrown", async () => {
|
|
539
|
+
const engine = createCodingGraphEngine();
|
|
540
|
+
// Feed content that could trigger query errors in edge-case grammars.
|
|
541
|
+
// The key contract: parseFile NEVER throws — it returns { ok: false }.
|
|
542
|
+
const result = await engine.parseFile({
|
|
543
|
+
path: "test.js",
|
|
544
|
+
content: Buffer.from("", "utf-8"),
|
|
545
|
+
language: "javascript",
|
|
546
|
+
});
|
|
547
|
+
assert.ok(result.ok, "empty JS file should still parse (no extraction error)");
|
|
548
|
+
|
|
549
|
+
await engine.dispose();
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
test("named-routes: app.get('/users', getUsers) captures the handler name", async () => {
|
|
553
|
+
const engine = createCodingGraphEngine();
|
|
554
|
+
const code = [
|
|
555
|
+
'const express = require("express");',
|
|
556
|
+
"const app = express();",
|
|
557
|
+
"function getUsers() { return []; }",
|
|
558
|
+
'app.get("/users", getUsers);',
|
|
559
|
+
'app.post("/items", (req, res) => {});',
|
|
560
|
+
].join("\n");
|
|
561
|
+
const result = await engine.parseFile({ path: "lib/server.js", content: Buffer.from(code, "utf-8") });
|
|
562
|
+
assert.ok(result.ok);
|
|
563
|
+
if (!result.ok) return;
|
|
564
|
+
|
|
565
|
+
const routes = result.ir.routes ?? [];
|
|
566
|
+
const usersRoute = routes.find((r) => r.pathTemplate === "/users");
|
|
567
|
+
assert.ok(usersRoute, "JS: should have a /users route");
|
|
568
|
+
assert.equal(usersRoute!.handlerQualifiedName, "getUsers", "named handler should use the identifier text");
|
|
569
|
+
|
|
570
|
+
const itemsRoute = routes.find((r) => r.pathTemplate === "/items");
|
|
571
|
+
assert.ok(itemsRoute, "JS: should have a /items route");
|
|
572
|
+
|
|
573
|
+
await engine.dispose();
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
test("multi-import: Python 'import os, sys' produces two import entries", async () => {
|
|
577
|
+
const engine = createCodingGraphEngine();
|
|
578
|
+
const code = [
|
|
579
|
+
"import os, sys",
|
|
580
|
+
"from collections import defaultdict",
|
|
581
|
+
"",
|
|
582
|
+
"def main():",
|
|
583
|
+
" pass",
|
|
584
|
+
].join("\n");
|
|
585
|
+
const result = await engine.parseFile({ path: "app.py", content: Buffer.from(code, "utf-8") });
|
|
586
|
+
assert.ok(result.ok);
|
|
587
|
+
if (!result.ok) return;
|
|
588
|
+
|
|
589
|
+
const modules = result.ir.imports.map((i) => i.module);
|
|
590
|
+
assert.ok(modules.includes("os"), `Python: should import os, got: ${modules.join(", ")}`);
|
|
591
|
+
assert.ok(modules.includes("sys"), `Python: should import sys, got: ${modules.join(", ")}`);
|
|
592
|
+
assert.ok(modules.includes("collections"), `Python: should import collections, got: ${modules.join(", ")}`);
|
|
593
|
+
|
|
594
|
+
await engine.dispose();
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
test("python-routes: @app.get('/users') def users() captures handler name", async () => {
|
|
598
|
+
const engine = createCodingGraphEngine();
|
|
599
|
+
const code = [
|
|
600
|
+
"from flask import Flask",
|
|
601
|
+
"app = Flask(__name__)",
|
|
602
|
+
"",
|
|
603
|
+
"@app.get('/users')",
|
|
604
|
+
"def users():",
|
|
605
|
+
" return []",
|
|
606
|
+
"",
|
|
607
|
+
"@app.route('/items')",
|
|
608
|
+
"def list_items():",
|
|
609
|
+
" return []",
|
|
610
|
+
].join("\n");
|
|
611
|
+
const result = await engine.parseFile({ path: "app.py", content: Buffer.from(code, "utf-8") });
|
|
612
|
+
assert.ok(result.ok);
|
|
613
|
+
if (!result.ok) return;
|
|
614
|
+
|
|
615
|
+
const routes = result.ir.routes ?? [];
|
|
616
|
+
const usersRoute = routes.find((r) => r.pathTemplate === "/users");
|
|
617
|
+
assert.ok(usersRoute, "Python: should have a /users route");
|
|
618
|
+
assert.notEqual(
|
|
619
|
+
usersRoute!.handlerQualifiedName,
|
|
620
|
+
"anonymous",
|
|
621
|
+
"Python route handler should be 'users', not 'anonymous'",
|
|
622
|
+
);
|
|
623
|
+
assert.equal(usersRoute!.handlerQualifiedName, "users", "should capture the function name");
|
|
624
|
+
|
|
625
|
+
const itemsRoute = routes.find((r) => r.pathTemplate === "/items");
|
|
626
|
+
assert.ok(itemsRoute, "Python: should have a /items route");
|
|
627
|
+
assert.notEqual(itemsRoute!.handlerQualifiedName, "anonymous", "items route handler should not be anonymous");
|
|
628
|
+
|
|
629
|
+
await engine.dispose();
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
// ---------------------------------------------------------------------------
|
|
633
|
+
// 2d. Round-3 review-thread fixes (#1551 PR2 — PR #1652).
|
|
634
|
+
// - Go receiver-qualified method names (thread dHXe)
|
|
635
|
+
// - Concurrent parseFile serialization (thread dHXZ)
|
|
636
|
+
// ---------------------------------------------------------------------------
|
|
637
|
+
|
|
638
|
+
test("go-receivers: method qualified names use receiver type", async () => {
|
|
639
|
+
const engine = createCodingGraphEngine();
|
|
640
|
+
const code = [
|
|
641
|
+
'package main',
|
|
642
|
+
'',
|
|
643
|
+
'type Server struct {',
|
|
644
|
+
' port int',
|
|
645
|
+
'}',
|
|
646
|
+
'',
|
|
647
|
+
'func (s *Server) Start() {}',
|
|
648
|
+
'func (s Server) Stop() {}',
|
|
649
|
+
'func NewServer() *Server { return nil }',
|
|
650
|
+
].join("\n");
|
|
651
|
+
const result = await engine.parseFile({ path: "main.go", content: Buffer.from(code, "utf-8") });
|
|
652
|
+
assert.ok(result.ok);
|
|
653
|
+
if (!result.ok) return;
|
|
654
|
+
|
|
655
|
+
const start = result.ir.symbols.find((s) => s.name === "Start");
|
|
656
|
+
assert.ok(start, "Go: Start method must exist");
|
|
657
|
+
assert.equal(start!.qualifiedName, "Server.Start", "pointer receiver method should be Server.Start");
|
|
658
|
+
assert.equal(start!.parentQualifiedName, "Server");
|
|
659
|
+
|
|
660
|
+
const stop = result.ir.symbols.find((s) => s.name === "Stop");
|
|
661
|
+
assert.ok(stop, "Go: Stop method must exist");
|
|
662
|
+
assert.equal(stop!.qualifiedName, "Server.Stop", "value receiver method should be Server.Stop");
|
|
663
|
+
assert.equal(stop!.parentQualifiedName, "Server");
|
|
664
|
+
|
|
665
|
+
const newServer = result.ir.symbols.find((s) => s.name === "NewServer");
|
|
666
|
+
assert.ok(newServer, "Go: NewServer function must exist");
|
|
667
|
+
assert.equal(newServer!.qualifiedName, "NewServer", "free functions should have no parent qualifier");
|
|
668
|
+
|
|
669
|
+
await engine.dispose();
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
test("concurrent-parse: parallel parseFile calls do not race", async () => {
|
|
673
|
+
const engine = createCodingGraphEngine();
|
|
674
|
+
const jsCode = 'const x = require("x"); function f() {}';
|
|
675
|
+
const pyCode = 'def f():\n pass\nclass C:\n pass';
|
|
676
|
+
const tsCode = 'export function g(): void {}';
|
|
677
|
+
|
|
678
|
+
// Fire all three concurrently — the shared parser must serialize them.
|
|
679
|
+
const results = await Promise.all([
|
|
680
|
+
engine.parseFile({ path: "a.js", content: Buffer.from(jsCode, "utf-8") }),
|
|
681
|
+
engine.parseFile({ path: "b.py", content: Buffer.from(pyCode, "utf-8") }),
|
|
682
|
+
engine.parseFile({ path: "c.ts", content: Buffer.from(tsCode, "utf-8") }),
|
|
683
|
+
]);
|
|
684
|
+
|
|
685
|
+
for (const [i, result] of results.entries()) {
|
|
686
|
+
assert.ok(result.ok, `concurrent parse #${i} must succeed, got: ${result.ok ? "" : result.message}`);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// Verify the results are correct (not mixed up by a race).
|
|
690
|
+
const [jsR, pyR, tsR] = results;
|
|
691
|
+
assert.ok(jsR.ok && jsR.ir.symbols.some((s) => s.name === "f"), "JS parse should have function f");
|
|
692
|
+
assert.ok(pyR.ok && pyR.ir.symbols.some((s) => s.name === "C"), "Python parse should have class C");
|
|
693
|
+
assert.ok(tsR.ok && tsR.ir.symbols.some((s) => s.name === "g"), "TS parse should have function g");
|
|
694
|
+
|
|
695
|
+
await engine.dispose();
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
// ---------------------------------------------------------------------------
|
|
699
|
+
// 2e. Round-4 review-thread fixes (#1551 PR2 — PR #1652).
|
|
700
|
+
// - CommonJS module.exports / exports.X export capture (thread dITa)
|
|
701
|
+
// - Kotlin import path segments removed (thread dITb)
|
|
702
|
+
// ---------------------------------------------------------------------------
|
|
703
|
+
|
|
704
|
+
test("cjs-exports: module.exports = { App, createRouter } captured as exports", async () => {
|
|
705
|
+
const engine = createCodingGraphEngine();
|
|
706
|
+
const fixture = FIXTURES.javascript; // ends with: module.exports = { App, createRouter };
|
|
707
|
+
const result = await engine.parseFile({
|
|
708
|
+
path: fixture.path,
|
|
709
|
+
content: Buffer.from(fixture.code, "utf-8"),
|
|
710
|
+
});
|
|
711
|
+
assert.ok(result.ok);
|
|
712
|
+
if (!result.ok) return;
|
|
713
|
+
|
|
714
|
+
const exportNames = result.ir.exports.map((e) => e.name);
|
|
715
|
+
assert.ok(exportNames.includes("App"), `JS: module.exports should export App, got: ${exportNames.join(", ")}`);
|
|
716
|
+
assert.ok(exportNames.includes("createRouter"), `JS: module.exports should export createRouter, got: ${exportNames.join(", ")}`);
|
|
717
|
+
|
|
718
|
+
await engine.dispose();
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
test("cjs-exports: exports.handler = handler captured", async () => {
|
|
722
|
+
const engine = createCodingGraphEngine();
|
|
723
|
+
const code = [
|
|
724
|
+
"function handler() { return 42; }",
|
|
725
|
+
"exports.handler = handler;",
|
|
726
|
+
"exports.version = '1.0.0';",
|
|
727
|
+
].join("\n");
|
|
728
|
+
const result = await engine.parseFile({ path: "lib/cjs2.js", content: Buffer.from(code, "utf-8") });
|
|
729
|
+
assert.ok(result.ok);
|
|
730
|
+
if (!result.ok) return;
|
|
731
|
+
|
|
732
|
+
const exportNames = result.ir.exports.map((e) => e.name);
|
|
733
|
+
assert.ok(exportNames.includes("handler"), `exports.handler should be exported, got: ${exportNames.join(", ")}`);
|
|
734
|
+
assert.ok(exportNames.includes("version"), `exports.version should be exported, got: ${exportNames.join(", ")}`);
|
|
735
|
+
|
|
736
|
+
await engine.dispose();
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
test("kotlin-imports: qualified import path not split into segments", async () => {
|
|
740
|
+
const engine = createCodingGraphEngine();
|
|
741
|
+
const fixture = FIXTURES.kotlin;
|
|
742
|
+
const result = await engine.parseFile({
|
|
743
|
+
path: fixture.path,
|
|
744
|
+
content: Buffer.from(fixture.code, "utf-8"),
|
|
745
|
+
});
|
|
746
|
+
assert.ok(result.ok);
|
|
747
|
+
if (!result.ok) return;
|
|
748
|
+
|
|
749
|
+
const modules = result.ir.imports.map((i) => i.module);
|
|
750
|
+
// Should NOT have bare segment names like "kotlin" or "collections"
|
|
751
|
+
// from a qualified import like "kotlin.collections.*"
|
|
752
|
+
for (const mod of modules) {
|
|
753
|
+
assert.ok(
|
|
754
|
+
mod.length > 0,
|
|
755
|
+
`Kotlin: all imports should be full paths, got segment: ${mod}`,
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
// Verify the fixture's actual import is captured as a full path
|
|
759
|
+
if (modules.length > 0) {
|
|
760
|
+
assert.ok(
|
|
761
|
+
modules.some((m) => m.includes(".") || m.length > 2),
|
|
762
|
+
`Kotlin: expected full import paths, got: ${modules.join(", ")}`,
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
await engine.dispose();
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
// ---------------------------------------------------------------------------
|
|
770
|
+
// 2f. Round-5 review-thread fixes (#1551 PR2 — PR #1652).
|
|
771
|
+
// - Rust impl methods get parent qualification (thread OdKT0 / OdLZF)
|
|
772
|
+
// - Member-receiver routes: this.router.get(...) (thread OdKTz)
|
|
773
|
+
// - Dispose drains in-flight parses before backend teardown (thread OdLZE)
|
|
774
|
+
// - Duplicate SHA-256 removed from reindex.ts (thread OdLZH)
|
|
775
|
+
// ---------------------------------------------------------------------------
|
|
776
|
+
|
|
777
|
+
test("rust-impl-parent: impl methods get struct-qualified name", async () => {
|
|
778
|
+
const engine = createCodingGraphEngine();
|
|
779
|
+
const fixture = FIXTURES.rust; // has: impl Config { pub fn new() -> Self { ... } }
|
|
780
|
+
const result = await engine.parseFile({
|
|
781
|
+
path: fixture.path,
|
|
782
|
+
content: Buffer.from(fixture.code, "utf-8"),
|
|
783
|
+
});
|
|
784
|
+
assert.ok(result.ok);
|
|
785
|
+
if (!result.ok) return;
|
|
786
|
+
|
|
787
|
+
// The impl method `new` should be qualified as `Config.new`, not bare `new`.
|
|
788
|
+
const newMethod = result.ir.symbols.find((s) => s.name === "new");
|
|
789
|
+
assert.ok(newMethod, "Rust: impl method 'new' must exist");
|
|
790
|
+
assert.equal(
|
|
791
|
+
newMethod!.parentQualifiedName,
|
|
792
|
+
"Config",
|
|
793
|
+
`Rust: new should have parentQualifiedName 'Config', got: ${newMethod!.parentQualifiedName}`,
|
|
794
|
+
);
|
|
795
|
+
assert.equal(
|
|
796
|
+
newMethod!.qualifiedName,
|
|
797
|
+
"Config.new",
|
|
798
|
+
`Rust: new should have qualifiedName 'Config.new', got: ${newMethod!.qualifiedName}`,
|
|
799
|
+
);
|
|
800
|
+
|
|
801
|
+
// Free function `add` should NOT have a parent (it's not inside an impl).
|
|
802
|
+
const add = result.ir.symbols.find((s) => s.name === "add");
|
|
803
|
+
assert.ok(add, "Rust: free function 'add' must exist");
|
|
804
|
+
assert.equal(add!.parentQualifiedName, undefined, "Rust: add should not have a parent");
|
|
805
|
+
|
|
806
|
+
await engine.dispose();
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
test("rust-nested-qualified: function inside impl method gets full parent chain", async () => {
|
|
810
|
+
const engine = createCodingGraphEngine();
|
|
811
|
+
const code = [
|
|
812
|
+
"pub struct Config {",
|
|
813
|
+
" pub port: u16,",
|
|
814
|
+
"}",
|
|
815
|
+
"",
|
|
816
|
+
"impl Config {",
|
|
817
|
+
" pub fn new() -> Self {",
|
|
818
|
+
" fn default_port() -> u16 { 8080 }",
|
|
819
|
+
" Self { port: default_port() }",
|
|
820
|
+
" }",
|
|
821
|
+
"}",
|
|
822
|
+
].join("\n");
|
|
823
|
+
const result = await engine.parseFile({
|
|
824
|
+
path: "src/nested.rs",
|
|
825
|
+
content: Buffer.from(code, "utf-8"),
|
|
826
|
+
});
|
|
827
|
+
assert.ok(result.ok);
|
|
828
|
+
if (!result.ok) return;
|
|
829
|
+
|
|
830
|
+
// The impl method `new` → Config.new (verified by prior test).
|
|
831
|
+
const newMethod = result.ir.symbols.find((s) => s.name === "new");
|
|
832
|
+
assert.ok(newMethod, "Rust: impl method 'new' must exist");
|
|
833
|
+
assert.equal(newMethod!.qualifiedName, "Config.new");
|
|
834
|
+
|
|
835
|
+
// The nested function inside `new` should get Config.new.default_port,
|
|
836
|
+
// not just new.default_port.
|
|
837
|
+
const helper = result.ir.symbols.find((s) => s.name === "default_port");
|
|
838
|
+
assert.ok(helper, "Rust: nested function 'default_port' must exist");
|
|
839
|
+
assert.equal(
|
|
840
|
+
helper!.parentQualifiedName,
|
|
841
|
+
"Config.new",
|
|
842
|
+
`Rust: nested function should have parent 'Config.new', got: ${helper!.parentQualifiedName}`,
|
|
843
|
+
);
|
|
844
|
+
|
|
845
|
+
await engine.dispose();
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
test("member-routes: this.router.get(...) produces a route", async () => {
|
|
849
|
+
const engine = createCodingGraphEngine();
|
|
850
|
+
const fixture = FIXTURES.typescript; // has: this.router.get('/', () => {});
|
|
851
|
+
const result = await engine.parseFile({
|
|
852
|
+
path: fixture.path,
|
|
853
|
+
content: Buffer.from(fixture.code, "utf-8"),
|
|
854
|
+
});
|
|
855
|
+
assert.ok(result.ok);
|
|
856
|
+
if (!result.ok) return;
|
|
857
|
+
|
|
858
|
+
const routes = result.ir.routes;
|
|
859
|
+
assert.ok(
|
|
860
|
+
routes.some((r) => r.verb === "GET" && r.pathTemplate === "/"),
|
|
861
|
+
`TS: expected GET / route from this.router.get(...), got: ${JSON.stringify(routes)}`,
|
|
862
|
+
);
|
|
863
|
+
|
|
864
|
+
await engine.dispose();
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
test("dispose-race: concurrent dispose and parse activity is safe", async () => {
|
|
868
|
+
const engine = createCodingGraphEngine();
|
|
869
|
+
const fixture = FIXTURES.typescript;
|
|
870
|
+
|
|
871
|
+
// Fire concurrent parses and a dispose in the same synchronous tick.
|
|
872
|
+
// dispose() sets disposed=true and awaits parseChain; the queued parses
|
|
873
|
+
// re-check disposed after acquiring the chain and reject cleanly.
|
|
874
|
+
const parse1 = engine.parseFile({
|
|
875
|
+
path: fixture.path,
|
|
876
|
+
content: Buffer.from(fixture.code, "utf-8"),
|
|
877
|
+
});
|
|
878
|
+
const parse2 = engine.parseFile({
|
|
879
|
+
path: fixture.path,
|
|
880
|
+
content: Buffer.from(fixture.code, "utf-8"),
|
|
881
|
+
});
|
|
882
|
+
const disposePromise = engine.dispose();
|
|
883
|
+
|
|
884
|
+
// All three must resolve without throwing — no unhandled rejection,
|
|
885
|
+
// no use-after-free on the backend's shared parser instance.
|
|
886
|
+
await Promise.all([parse1, parse2, disposePromise]);
|
|
887
|
+
|
|
888
|
+
// After dispose completes, new parses must fail.
|
|
889
|
+
const afterDispose = await engine.parseFile({
|
|
890
|
+
path: fixture.path,
|
|
891
|
+
content: Buffer.from(fixture.code, "utf-8"),
|
|
892
|
+
});
|
|
893
|
+
assert.equal(afterDispose.ok, false, "disposed engine should reject new parses");
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
test("hash-dedup: reindex.ts re-exports the single emit.ts hashContent", () => {
|
|
897
|
+
// Verify the re-export path works — both point to the same implementation
|
|
898
|
+
// in emit.ts (single hashing contract, rule 23).
|
|
899
|
+
const data = new TextEncoder().encode("hello world");
|
|
900
|
+
assert.equal(hashContentFromReindex(data), hashContent(data), "reindex.ts hashContent must match emit.ts hashContent");
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
// ---------------------------------------------------------------------------
|
|
904
|
+
// 3. Determinism — same file parsed twice must produce byte-identical IR.
|
|
905
|
+
// ---------------------------------------------------------------------------
|
|
906
|
+
|
|
907
|
+
test("determinism: same file parsed twice yields byte-identical IR", async () => {
|
|
908
|
+
const engine = createCodingGraphEngine();
|
|
909
|
+
const fixture = FIXTURES.typescript;
|
|
910
|
+
const content = Buffer.from(fixture.code, "utf-8");
|
|
911
|
+
|
|
912
|
+
const result1 = await engine.parseFile({ path: fixture.path, content });
|
|
913
|
+
const result2 = await engine.parseFile({ path: fixture.path, content });
|
|
914
|
+
|
|
915
|
+
assert.ok(result1.ok && result2.ok, "both parses must succeed");
|
|
916
|
+
if (!result1.ok || !result2.ok) return;
|
|
917
|
+
|
|
918
|
+
const ir1 = serializeIR(result1.ir);
|
|
919
|
+
const ir2 = serializeIR(result2.ir);
|
|
920
|
+
|
|
921
|
+
assert.equal(ir1, ir2, "IR must be byte-identical across two parses of the same file");
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
test("determinism: two separate engine instances produce identical IR", async () => {
|
|
925
|
+
const engine1 = createCodingGraphEngine();
|
|
926
|
+
const engine2 = createCodingGraphEngine();
|
|
927
|
+
const fixture = FIXTURES.python;
|
|
928
|
+
const content = Buffer.from(fixture.code, "utf-8");
|
|
929
|
+
|
|
930
|
+
const result1 = await engine1.parseFile({ path: fixture.path, content });
|
|
931
|
+
const result2 = await engine2.parseFile({ path: fixture.path, content });
|
|
932
|
+
|
|
933
|
+
assert.ok(result1.ok && result2.ok);
|
|
934
|
+
if (!result1.ok || !result2.ok) return;
|
|
935
|
+
|
|
936
|
+
assert.equal(serializeIR(result1.ir), serializeIR(result2.ir));
|
|
937
|
+
|
|
938
|
+
await engine1.dispose();
|
|
939
|
+
await engine2.dispose();
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
test("determinism: contentHash matches manual SHA-256 of raw bytes", async () => {
|
|
943
|
+
const engine = createCodingGraphEngine();
|
|
944
|
+
const fixture = FIXTURES.go;
|
|
945
|
+
const content = Buffer.from(fixture.code, "utf-8");
|
|
946
|
+
const expected = createHash("sha256").update(content).digest("hex");
|
|
947
|
+
|
|
948
|
+
const result = await engine.parseFile({ path: fixture.path, content });
|
|
949
|
+
assert.ok(result.ok);
|
|
950
|
+
if (!result.ok) return;
|
|
951
|
+
|
|
952
|
+
assert.equal(result.ir.contentHash, expected, "contentHash must be SHA-256 of raw bytes");
|
|
953
|
+
|
|
954
|
+
await engine.dispose();
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
// ---------------------------------------------------------------------------
|
|
958
|
+
// 4. Error handling — unsupported language returns parse_failed (rule 44).
|
|
959
|
+
// ---------------------------------------------------------------------------
|
|
960
|
+
|
|
961
|
+
test("error: unsupported file extension returns parse_failed", async () => {
|
|
962
|
+
const engine = createCodingGraphEngine();
|
|
963
|
+
const result = await engine.parseFile({
|
|
964
|
+
path: "readme.md",
|
|
965
|
+
content: Buffer.from("# Hello", "utf-8"),
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
assert.equal(result.ok, false);
|
|
969
|
+
if (result.ok) return;
|
|
970
|
+
assert.equal(result.code, "parse_failed");
|
|
971
|
+
assert.equal(result.path, "readme.md");
|
|
972
|
+
assert.ok(result.message.includes("unsupported language"), `unexpected message: ${result.message}`);
|
|
973
|
+
|
|
974
|
+
await engine.dispose();
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
test("error: explicit language override is respected", async () => {
|
|
978
|
+
const engine = createCodingGraphEngine();
|
|
979
|
+
const result = await engine.parseFile({
|
|
980
|
+
path: "unknown.xyz",
|
|
981
|
+
content: Buffer.from("function f() {}", "utf-8"),
|
|
982
|
+
language: "javascript",
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
assert.ok(result.ok, "explicit language override should work");
|
|
986
|
+
|
|
987
|
+
await engine.dispose();
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
// ---------------------------------------------------------------------------
|
|
991
|
+
// 5. Language sniffing — extension mapping.
|
|
992
|
+
// ---------------------------------------------------------------------------
|
|
993
|
+
|
|
994
|
+
test("sniff: file extensions map to correct languages", () => {
|
|
995
|
+
assert.equal(sniffLanguage("foo.ts"), "typescript");
|
|
996
|
+
assert.equal(sniffLanguage("foo.tsx"), "tsx");
|
|
997
|
+
assert.equal(sniffLanguage("foo.js"), "javascript");
|
|
998
|
+
assert.equal(sniffLanguage("foo.mjs"), "javascript");
|
|
999
|
+
assert.equal(sniffLanguage("foo.py"), "python");
|
|
1000
|
+
assert.equal(sniffLanguage("foo.go"), "go");
|
|
1001
|
+
assert.equal(sniffLanguage("foo.rs"), "rust");
|
|
1002
|
+
assert.equal(sniffLanguage("foo.java"), "java");
|
|
1003
|
+
assert.equal(sniffLanguage("foo.c"), "c");
|
|
1004
|
+
assert.equal(sniffLanguage("foo.h"), "c");
|
|
1005
|
+
assert.equal(sniffLanguage("foo.cpp"), "cpp");
|
|
1006
|
+
assert.equal(sniffLanguage("foo.hpp"), "cpp");
|
|
1007
|
+
assert.equal(sniffLanguage("foo.cs"), "csharp");
|
|
1008
|
+
assert.equal(sniffLanguage("foo.rb"), "ruby");
|
|
1009
|
+
assert.equal(sniffLanguage("foo.php"), "php");
|
|
1010
|
+
assert.equal(sniffLanguage("foo.kt"), "kotlin");
|
|
1011
|
+
assert.equal(sniffLanguage("foo.swift"), "swift");
|
|
1012
|
+
assert.equal(sniffLanguage("foo.sh"), "bash");
|
|
1013
|
+
assert.equal(sniffLanguage("foo.bash"), "bash");
|
|
1014
|
+
assert.equal(sniffLanguage("foo.unknown"), null);
|
|
1015
|
+
assert.equal(sniffLanguage("noextension"), null);
|
|
1016
|
+
});
|
|
1017
|
+
|
|
1018
|
+
// ---------------------------------------------------------------------------
|
|
1019
|
+
// 6. Dispose lifecycle.
|
|
1020
|
+
// ---------------------------------------------------------------------------
|
|
1021
|
+
|
|
1022
|
+
test("lifecycle: dispose releases resources; engine can be recreated", async () => {
|
|
1023
|
+
const engine1 = createCodingGraphEngine();
|
|
1024
|
+
const fixture = FIXTURES.javascript;
|
|
1025
|
+
const result1 = await engine1.parseFile({
|
|
1026
|
+
path: fixture.path,
|
|
1027
|
+
content: Buffer.from(fixture.code, "utf-8"),
|
|
1028
|
+
});
|
|
1029
|
+
assert.ok(result1.ok, "first engine should work before dispose");
|
|
1030
|
+
await engine1.dispose();
|
|
1031
|
+
|
|
1032
|
+
// After dispose, parseFile should return parse_failed.
|
|
1033
|
+
const resultAfterDispose = await engine1.parseFile({
|
|
1034
|
+
path: fixture.path,
|
|
1035
|
+
content: Buffer.from(fixture.code, "utf-8"),
|
|
1036
|
+
});
|
|
1037
|
+
assert.equal(resultAfterDispose.ok, false, "disposed engine should fail");
|
|
1038
|
+
|
|
1039
|
+
// A new engine works fine.
|
|
1040
|
+
const engine2 = createCodingGraphEngine();
|
|
1041
|
+
const result2 = await engine2.parseFile({
|
|
1042
|
+
path: fixture.path,
|
|
1043
|
+
content: Buffer.from(fixture.code, "utf-8"),
|
|
1044
|
+
});
|
|
1045
|
+
assert.ok(result2.ok, "new engine should work after disposing the old one");
|
|
1046
|
+
await engine2.dispose();
|
|
1047
|
+
});
|
|
1048
|
+
|
|
1049
|
+
test("lifecycle: double dispose is safe", async () => {
|
|
1050
|
+
const engine = createCodingGraphEngine();
|
|
1051
|
+
await engine.dispose();
|
|
1052
|
+
await engine.dispose(); // should not throw
|
|
1053
|
+
});
|
|
1054
|
+
|
|
1055
|
+
// ===========================================================================
|
|
1056
|
+
// Issue #1659: export-capture edge cases (default exports, CJS alias,
|
|
1057
|
+
// UTF-16 offsets, C++ qualified methods, Express middleware, Python relative).
|
|
1058
|
+
// ===========================================================================
|
|
1059
|
+
|
|
1060
|
+
test("1659-1: export default App (bare identifier) captured as export", async () => {
|
|
1061
|
+
const engine = createCodingGraphEngine();
|
|
1062
|
+
const code = [
|
|
1063
|
+
"function App() { return null; }",
|
|
1064
|
+
"export default App;",
|
|
1065
|
+
].join("\n");
|
|
1066
|
+
const result = await engine.parseFile({ path: "src/app.tsx", content: Buffer.from(code, "utf-8") });
|
|
1067
|
+
assert.ok(result.ok);
|
|
1068
|
+
if (!result.ok) return;
|
|
1069
|
+
const exportNames = result.ir.exports.map((e) => e.name);
|
|
1070
|
+
assert.ok(exportNames.includes("App"), `TSX: export default App should be exported, got: ${exportNames.join(", ")}`);
|
|
1071
|
+
await engine.dispose();
|
|
1072
|
+
});
|
|
1073
|
+
|
|
1074
|
+
test("1659-1b: export default App in JS captured as export", async () => {
|
|
1075
|
+
const engine = createCodingGraphEngine();
|
|
1076
|
+
const code = [
|
|
1077
|
+
"const App = () => {}",
|
|
1078
|
+
"export default App;",
|
|
1079
|
+
].join("\n");
|
|
1080
|
+
const result = await engine.parseFile({ path: "src/app.js", content: Buffer.from(code, "utf-8") });
|
|
1081
|
+
assert.ok(result.ok);
|
|
1082
|
+
if (!result.ok) return;
|
|
1083
|
+
const exportNames = result.ir.exports.map((e) => e.name);
|
|
1084
|
+
assert.ok(exportNames.includes("App"), `JS: export default App should be exported, got: ${exportNames.join(", ")}`);
|
|
1085
|
+
await engine.dispose();
|
|
1086
|
+
});
|
|
1087
|
+
|
|
1088
|
+
test("1659-2: CJS alias target captures value identifier, not key", async () => {
|
|
1089
|
+
const engine = createCodingGraphEngine();
|
|
1090
|
+
const code = [
|
|
1091
|
+
"function createRouter() { return {}; }",
|
|
1092
|
+
"module.exports = { publicName: createRouter };",
|
|
1093
|
+
].join("\n");
|
|
1094
|
+
const result = await engine.parseFile({ path: "src/router.js", content: Buffer.from(code, "utf-8") });
|
|
1095
|
+
assert.ok(result.ok);
|
|
1096
|
+
if (!result.ok) return;
|
|
1097
|
+
const exportNames = result.ir.exports.map((e) => e.name);
|
|
1098
|
+
assert.ok(
|
|
1099
|
+
exportNames.includes("createRouter"),
|
|
1100
|
+
`CJS: module.exports = { publicName: createRouter } should export createRouter (the value), got: ${exportNames.join(", ")}`,
|
|
1101
|
+
);
|
|
1102
|
+
await engine.dispose();
|
|
1103
|
+
});
|
|
1104
|
+
|
|
1105
|
+
|
|
1106
|
+
test("1659-2b: CJS pair with a Unicode identifier value exports the value once (no alias-key duplicate)", async () => {
|
|
1107
|
+
// The non-identifier fallback's #not-match? regex is ASCII-only, so a
|
|
1108
|
+
// Unicode identifier value (Universität) defeats it and BOTH the
|
|
1109
|
+
// value-identifier pattern and the fallback fire on the same pair.
|
|
1110
|
+
// extractExports dedups by pair so only the real value symbol is kept
|
|
1111
|
+
// (cursor #1659 review: 'CJS export fallback duplicates Unicode').
|
|
1112
|
+
const engine = createCodingGraphEngine();
|
|
1113
|
+
const code = [
|
|
1114
|
+
"function Universität() { return 1; }",
|
|
1115
|
+
"module.exports = { alias: Universität };",
|
|
1116
|
+
].join("\n");
|
|
1117
|
+
const result = await engine.parseFile({ path: "src/u.js", content: Buffer.from(code, "utf-8") });
|
|
1118
|
+
assert.ok(result.ok);
|
|
1119
|
+
if (!result.ok) return;
|
|
1120
|
+
const exportNames = result.ir.exports.map((e) => e.name);
|
|
1121
|
+
assert.ok(
|
|
1122
|
+
exportNames.includes("Universität"),
|
|
1123
|
+
`CJS Unicode value should export Universität, got: ${exportNames.join(", ")}`,
|
|
1124
|
+
);
|
|
1125
|
+
assert.ok(
|
|
1126
|
+
!exportNames.includes("alias"),
|
|
1127
|
+
`CJS Unicode value must NOT also export the alias key, got: ${exportNames.join(", ")}`,
|
|
1128
|
+
);
|
|
1129
|
+
await engine.dispose();
|
|
1130
|
+
});
|
|
1131
|
+
|
|
1132
|
+
test("1659-3: UTF-16 offsets produce correct byte spans for multibyte content", async () => {
|
|
1133
|
+
const engine = createCodingGraphEngine();
|
|
1134
|
+
// Leading comment with multibyte chars so UTF-16 and byte offsets diverge.
|
|
1135
|
+
// "café" = 4 UTF-16 code units but 5 UTF-8 bytes (é = 2 bytes).
|
|
1136
|
+
const code = "// café comment\nfunction hello() { return 1; }\n";
|
|
1137
|
+
const buf = Buffer.from(code, "utf-8");
|
|
1138
|
+
const result = await engine.parseFile({ path: "src/multibyte.ts", content: buf });
|
|
1139
|
+
assert.ok(result.ok);
|
|
1140
|
+
if (!result.ok) return;
|
|
1141
|
+
const fn = result.ir.symbols.find((s) => s.name === "hello");
|
|
1142
|
+
assert.ok(fn, "should find hello function");
|
|
1143
|
+
if (!fn) return;
|
|
1144
|
+
// The function keyword starts after "// café comment\n".
|
|
1145
|
+
// "// " = 3 bytes, "café" = 5 bytes, " comment" = 8 bytes, "\n" = 1 byte = 17 bytes total prefix.
|
|
1146
|
+
// So "function hello..." starts at byte offset 17.
|
|
1147
|
+
assert.equal(
|
|
1148
|
+
fn.span.startByte, 17,
|
|
1149
|
+
`hello startByte should be 17 (byte offset), got ${fn.span.startByte}`,
|
|
1150
|
+
);
|
|
1151
|
+
// Verify we can slice the correct text from the byte buffer.
|
|
1152
|
+
const sliced = buf.subarray(fn.span.startByte, fn.span.endByte).toString("utf-8");
|
|
1153
|
+
assert.ok(sliced.startsWith("function hello"), `byte-offset slice should start with 'function hello', got: ${sliced}`);
|
|
1154
|
+
await engine.dispose();
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1157
|
+
test("1659-3b: buildUtf16ToByteOffsetMap maps BOTH surrogate code units to the pair's start byte", () => {
|
|
1158
|
+
// "𝕏" (U+1D54F) is one astral code point: 2 UTF-16 code units (a surrogate
|
|
1159
|
+
// pair), 4 UTF-8 bytes. Before the fix the low surrogate's map entry pointed
|
|
1160
|
+
// PAST the pair, so a span starting on the low code unit got an inflated
|
|
1161
|
+
// startByte (cursor #1659 review: 'Surrogate UTF-16 index maps wrong').
|
|
1162
|
+
const content = "a𝕏b"; // idx 0='a', 1=high surrogate, 2=low surrogate, 3='b'
|
|
1163
|
+
const map = buildUtf16ToByteOffsetMap(content);
|
|
1164
|
+
// byte layout: 'a'=1B, '𝕏'=4B, 'b'=1B → 0,1,5,6
|
|
1165
|
+
assert.equal(map[0], 0, "'a' starts at byte 0");
|
|
1166
|
+
assert.equal(map[1], 1, "high surrogate (pair start) at byte 1");
|
|
1167
|
+
assert.equal(map[2], 1, "low surrogate must map to the PAIR start (byte 1), not past it");
|
|
1168
|
+
assert.equal(map[3], 5, "'b' starts at byte 5 (1 + 4 for the pair)");
|
|
1169
|
+
assert.equal(map[content.length], 6, "total byte length is 6");
|
|
1170
|
+
});
|
|
1171
|
+
|
|
1172
|
+
test("1659-4: C++ out-of-class method A::start captured as method", async () => {
|
|
1173
|
+
const engine = createCodingGraphEngine();
|
|
1174
|
+
const code = [
|
|
1175
|
+
"class Engine {",
|
|
1176
|
+
"public:",
|
|
1177
|
+
" void start();",
|
|
1178
|
+
"};",
|
|
1179
|
+
"void Engine::start() { /* impl */ }",
|
|
1180
|
+
].join("\n");
|
|
1181
|
+
const result = await engine.parseFile({ path: "src/engine.cpp", content: Buffer.from(code, "utf-8") });
|
|
1182
|
+
assert.ok(result.ok);
|
|
1183
|
+
if (!result.ok) return;
|
|
1184
|
+
// The qualified_identifier pattern captures "Engine::start" as the name.
|
|
1185
|
+
const method = result.ir.symbols.find((s) => s.kind === "method");
|
|
1186
|
+
assert.ok(method, "should find a method symbol for Engine::start");
|
|
1187
|
+
await engine.dispose();
|
|
1188
|
+
});
|
|
1189
|
+
|
|
1190
|
+
test("1659-5: Express middleware route captures handler, not middleware", async () => {
|
|
1191
|
+
const engine = createCodingGraphEngine();
|
|
1192
|
+
const code = [
|
|
1193
|
+
"function requireAuth(req, res, next) { next(); }",
|
|
1194
|
+
"function getUsers(req, res) { res.json([]); }",
|
|
1195
|
+
"app.get(\"/users\", requireAuth, getUsers);",
|
|
1196
|
+
].join("\n");
|
|
1197
|
+
const result = await engine.parseFile({ path: "src/server.js", content: Buffer.from(code, "utf-8") });
|
|
1198
|
+
assert.ok(result.ok);
|
|
1199
|
+
if (!result.ok) return;
|
|
1200
|
+
const routes = result.ir.routes ?? [];
|
|
1201
|
+
const usersRoute = routes.find((r) => r.pathTemplate === "/users");
|
|
1202
|
+
assert.ok(usersRoute, "should have a /users route");
|
|
1203
|
+
if (!usersRoute) return;
|
|
1204
|
+
assert.equal(
|
|
1205
|
+
usersRoute.handlerQualifiedName, "getUsers",
|
|
1206
|
+
`middleware route handler should be 'getUsers' (the handler), not 'requireAuth' (the middleware), got: ${usersRoute.handlerQualifiedName}`,
|
|
1207
|
+
);
|
|
1208
|
+
await engine.dispose();
|
|
1209
|
+
});
|
|
1210
|
+
|
|
1211
|
+
|
|
1212
|
+
test("1659-5b: route handler after a trailing comment is still captured (not 'anonymous')", async () => {
|
|
1213
|
+
// tree-sitter treats the inline comment as a named child of the
|
|
1214
|
+
// arguments node; without skipping it the comment becomes the "last
|
|
1215
|
+
// arg" and the real handler is missed (chatgpt-codex-connector #1659
|
|
1216
|
+
// review: 'Skip comments when selecting route handler').
|
|
1217
|
+
const engine = createCodingGraphEngine();
|
|
1218
|
+
const code = [
|
|
1219
|
+
"function getUsers(req, res) { res.json([]); }",
|
|
1220
|
+
'app.get("/users", getUsers /* auth middleware */);',
|
|
1221
|
+
].join("\n");
|
|
1222
|
+
const result = await engine.parseFile({ path: "src/server.js", content: Buffer.from(code, "utf-8") });
|
|
1223
|
+
assert.ok(result.ok);
|
|
1224
|
+
if (!result.ok) return;
|
|
1225
|
+
const routes = result.ir.routes;
|
|
1226
|
+
const usersRoute = routes.find((r) => r.pathTemplate === "/users");
|
|
1227
|
+
assert.ok(usersRoute, "should have a /users route");
|
|
1228
|
+
if (!usersRoute) return;
|
|
1229
|
+
assert.equal(
|
|
1230
|
+
usersRoute.handlerQualifiedName, "getUsers",
|
|
1231
|
+
`route with trailing comment should still capture 'getUsers', got: ${usersRoute.handlerQualifiedName}`,
|
|
1232
|
+
);
|
|
1233
|
+
await engine.dispose();
|
|
1234
|
+
});
|
|
1235
|
+
test("1688-route: cache.get('user') does NOT produce a route (empty-handler skip)", async () => {
|
|
1236
|
+
// The unified route matcher captures any call_expression where the method
|
|
1237
|
+
// name matches a verb (get/post/...) and the first arg is a string. Without
|
|
1238
|
+
// the empty-handler skip, ordinary code like cache.get("user") matches and
|
|
1239
|
+
// emits a route with handlerQualifiedName: "", which GraphStore rejects
|
|
1240
|
+
// (chatgpt-codex-connector #1688 review: 'Require a handler before
|
|
1241
|
+
// matching JS routes'). Routes without a real handler are now skipped.
|
|
1242
|
+
const engine = createCodingGraphEngine();
|
|
1243
|
+
const code = [
|
|
1244
|
+
"const cache = new Map();",
|
|
1245
|
+
'cache.get("user");',
|
|
1246
|
+
'cache.delete("session");',
|
|
1247
|
+
'cache.set("key", "value");',
|
|
1248
|
+
"",
|
|
1249
|
+
"function getUsers(req, res) { res.json([]); }",
|
|
1250
|
+
'app.get("/users", getUsers);',
|
|
1251
|
+
].join("\n");
|
|
1252
|
+
const result = await engine.parseFile({ path: "src/app.js", content: Buffer.from(code, "utf-8") });
|
|
1253
|
+
assert.ok(result.ok);
|
|
1254
|
+
if (!result.ok) return;
|
|
1255
|
+
const routes = result.ir.routes ?? [];
|
|
1256
|
+
// The real route must still be captured.
|
|
1257
|
+
const usersRoute = routes.find((r) => r.pathTemplate === "/users");
|
|
1258
|
+
assert.ok(usersRoute, "should still capture the real /users route");
|
|
1259
|
+
// Non-route calls must NOT produce routes.
|
|
1260
|
+
const cacheRoutes = routes.filter((r) => r.pathTemplate === "user" || r.pathTemplate === "session" || r.pathTemplate === "key");
|
|
1261
|
+
assert.equal(cacheRoutes.length, 0, "cache.get/delete/set must NOT produce routes");
|
|
1262
|
+
await engine.dispose();
|
|
1263
|
+
});
|
|
1264
|
+
test("1688-route-2: HTTP client calls with options object do NOT produce spurious routes", async () => {
|
|
1265
|
+
// The cursor Bugbot thread @14:47: the unified route matcher treats any
|
|
1266
|
+
// call whose method name is an HTTP verb and first arg is a string as a
|
|
1267
|
+
// route when there are >= 2 args. extractHandlerFromArgs previously
|
|
1268
|
+
// returned "anonymous" for non-handler last args (objects, numbers),
|
|
1269
|
+
// producing spurious routes from client calls like httpClient.get(url, opts).
|
|
1270
|
+
// Now non-handler last args return "" so the route is skipped.
|
|
1271
|
+
const engine = createCodingGraphEngine();
|
|
1272
|
+
const code = [
|
|
1273
|
+
'const httpClient = { get: (url, opts) => {} };',
|
|
1274
|
+
'httpClient.get("https://api.example.com", { headers: { auth: "token" } });',
|
|
1275
|
+
'httpClient.delete("https://api.example.com/resource", { method: "DELETE" });',
|
|
1276
|
+
'httpClient.put("https://api.example.com/data", 42);',
|
|
1277
|
+
"",
|
|
1278
|
+
"function getUsers(req, res) { res.json([]); }",
|
|
1279
|
+
'app.get("/users", getUsers);',
|
|
1280
|
+
'app.post("/items", (req, res) => {});',
|
|
1281
|
+
].join("\n");
|
|
1282
|
+
const result = await engine.parseFile({ path: "src/client.js", content: Buffer.from(code, "utf-8") });
|
|
1283
|
+
assert.ok(result.ok);
|
|
1284
|
+
if (!result.ok) return;
|
|
1285
|
+
const routes = result.ir.routes ?? [];
|
|
1286
|
+
// Real routes must still be captured.
|
|
1287
|
+
const usersRoute = routes.find((r) => r.pathTemplate === "/users");
|
|
1288
|
+
assert.ok(usersRoute, "should still capture the real /users route");
|
|
1289
|
+
const itemsRoute = routes.find((r) => r.pathTemplate === "/items");
|
|
1290
|
+
assert.ok(itemsRoute, "should still capture the real /items route with arrow fn handler");
|
|
1291
|
+
// HTTP client calls must NOT produce routes.
|
|
1292
|
+
const clientRoutes = routes.filter((r) =>
|
|
1293
|
+
r.pathTemplate.startsWith("https://") || r.pathTemplate.includes("api.example.com"),
|
|
1294
|
+
);
|
|
1295
|
+
assert.equal(clientRoutes.length, 0, "httpClient.get/delete/put must NOT produce spurious routes");
|
|
1296
|
+
await engine.dispose();
|
|
1297
|
+
});
|
|
1298
|
+
test("1688-route-3: full-URL client calls do NOT produce routes (path-prefix guard)", async () => {
|
|
1299
|
+
// Route paths always start with "/" or "*". HTTP client calls with full
|
|
1300
|
+
// URLs (httpClient.get("https://api.example.com", opts, cb)) are filtered
|
|
1301
|
+
// by the path-prefix guard (chatgpt-codex-connector #1688 P2).
|
|
1302
|
+
const engine = createCodingGraphEngine();
|
|
1303
|
+
const code = [
|
|
1304
|
+
'const http = require("http");',
|
|
1305
|
+
'http.get("https://api.example.com/data", (res) => {});',
|
|
1306
|
+
'http.request("http://localhost:3000/api", { method: "POST" }, (res) => {});',
|
|
1307
|
+
"",
|
|
1308
|
+
"function listUsers(req, res) { res.json([]); }",
|
|
1309
|
+
'app.get("/users", listUsers);',
|
|
1310
|
+
].join("\n");
|
|
1311
|
+
const result = await engine.parseFile({ path: "src/client2.js", content: Buffer.from(code, "utf-8") });
|
|
1312
|
+
assert.ok(result.ok);
|
|
1313
|
+
if (!result.ok) return;
|
|
1314
|
+
const routes = result.ir.routes ?? [];
|
|
1315
|
+
// Real route must still be captured.
|
|
1316
|
+
const usersRoute = routes.find((r) => r.pathTemplate === "/users");
|
|
1317
|
+
assert.ok(usersRoute, "should still capture the real /users route");
|
|
1318
|
+
// Full-URL client calls must NOT produce routes.
|
|
1319
|
+
const urlRoutes = routes.filter((r) => r.pathTemplate.startsWith("http"));
|
|
1320
|
+
assert.equal(urlRoutes.length, 0, "full-URL http.get/request calls must NOT produce routes");
|
|
1321
|
+
await engine.dispose();
|
|
1322
|
+
});
|
|
1323
|
+
|
|
1324
|
+
test("1688-route-4: HTTP client objects (http/client/axios) do NOT produce routes", async () => {
|
|
1325
|
+
const engine = createCodingGraphEngine();
|
|
1326
|
+
const code = [
|
|
1327
|
+
"const httpClient = { get: (url, opts, cb) => {} };",
|
|
1328
|
+
'httpClient.get("/api/users", { headers: {} }, cb);',
|
|
1329
|
+
"const client = { post: (url, data, cb) => {} };",
|
|
1330
|
+
'client.post("/api/data", { id: 1 }, handler);',
|
|
1331
|
+
"const axios = { delete: (url) => {} };",
|
|
1332
|
+
'axios.delete("/api/item/1");',
|
|
1333
|
+
"",
|
|
1334
|
+
"function getUsers(req, res) { res.json([]); }",
|
|
1335
|
+
'app.get("/users", getUsers);',
|
|
1336
|
+
].join("\n");
|
|
1337
|
+
const result = await engine.parseFile({ path: "src/client3.js", content: Buffer.from(code, "utf-8") });
|
|
1338
|
+
assert.ok(result.ok);
|
|
1339
|
+
if (!result.ok) return;
|
|
1340
|
+
const routes = result.ir.routes ?? [];
|
|
1341
|
+
const usersRoute = routes.find((r) => r.pathTemplate === "/users");
|
|
1342
|
+
assert.ok(usersRoute, "should still capture the real app.get /users route");
|
|
1343
|
+
const clientRoutes = routes.filter((r) => r.pathTemplate.startsWith("/api"));
|
|
1344
|
+
assert.equal(clientRoutes.length, 0, "httpClient/client/axios calls must NOT produce routes");
|
|
1345
|
+
await engine.dispose();
|
|
1346
|
+
});
|
|
1347
|
+
|
|
1348
|
+
test("1688-route-5: nested HTTP client receivers (this.client) do NOT produce routes", async () => {
|
|
1349
|
+
// A nested receiver keeps its full expression in objectNode.text
|
|
1350
|
+
// ("this.client"), which missed the ^client$ pattern and let
|
|
1351
|
+
// this.client.get("/api", opts, cb) emit a spurious route (marking cb as
|
|
1352
|
+
// a route handler). The receiver is now normalized to its tail property
|
|
1353
|
+
// (chatgpt-codex-connector #1688 P2: 'Normalize receiver names').
|
|
1354
|
+
const engine = createCodingGraphEngine();
|
|
1355
|
+
const code = [
|
|
1356
|
+
'this.client.get("/api/users", { headers: {} }, cb);',
|
|
1357
|
+
'svc.httpClient.post("/api/data", { id: 1 }, handler);',
|
|
1358
|
+
'foo.bar.client.put("/api/item", 42);',
|
|
1359
|
+
"",
|
|
1360
|
+
"function getUsers(req, res) { res.json([]); }",
|
|
1361
|
+
'app.get("/users", getUsers);',
|
|
1362
|
+
].join("\n");
|
|
1363
|
+
const result = await engine.parseFile({ path: "src/client-nested.js", content: Buffer.from(code, "utf-8") });
|
|
1364
|
+
assert.ok(result.ok);
|
|
1365
|
+
if (!result.ok) return;
|
|
1366
|
+
const routes = result.ir.routes ?? [];
|
|
1367
|
+
const usersRoute = routes.find((r) => r.pathTemplate === "/users");
|
|
1368
|
+
assert.ok(usersRoute, "should still capture the real app.get /users route");
|
|
1369
|
+
const clientRoutes = routes.filter((r) => r.pathTemplate.startsWith("/api"));
|
|
1370
|
+
assert.equal(
|
|
1371
|
+
clientRoutes.length,
|
|
1372
|
+
0,
|
|
1373
|
+
"nested-receiver client calls (this.client / svc.httpClient / foo.bar.client) must NOT produce routes",
|
|
1374
|
+
);
|
|
1375
|
+
await engine.dispose();
|
|
1376
|
+
});
|
|
1377
|
+
|
|
1378
|
+
|
|
1379
|
+
|
|
1380
|
+
|
|
1381
|
+
|
|
1382
|
+
test("1659-6: Python relative import from .models captures module", async () => {
|
|
1383
|
+
const engine = createCodingGraphEngine();
|
|
1384
|
+
const code = "from .models import User\n";
|
|
1385
|
+
const result = await engine.parseFile({ path: "src/app.py", content: Buffer.from(code, "utf-8") });
|
|
1386
|
+
assert.ok(result.ok);
|
|
1387
|
+
if (!result.ok) return;
|
|
1388
|
+
const modules = result.ir.imports.map((i) => i.module);
|
|
1389
|
+
assert.ok(
|
|
1390
|
+
modules.some((m) => m.includes("models")),
|
|
1391
|
+
`Python: from .models import User should capture 'models' module, got: ${modules.join(", ")}`,
|
|
1392
|
+
);
|
|
1393
|
+
await engine.dispose();
|
|
1394
|
+
});
|
|
1395
|
+
|
|
1396
|
+
test("1688-import: Python relative imports preserve prefix dots (..parent not parent)", async () => {
|
|
1397
|
+
// tree-sitter-python wraps the module inside a relative_import node.
|
|
1398
|
+
// Capturing only the inner dotted_name drops the prefix dots, collapsing
|
|
1399
|
+
// different relative levels (..parent vs .parent) to the same module name.
|
|
1400
|
+
// Now the relative_import node is captured directly, preserving the dots
|
|
1401
|
+
// (chatgpt-codex-connector #1688 P2: "Preserve dots in Python relative imports").
|
|
1402
|
+
const engine = createCodingGraphEngine();
|
|
1403
|
+
const code = [
|
|
1404
|
+
"from .models import User",
|
|
1405
|
+
"from ..parent import X",
|
|
1406
|
+
"from ...grandparent import Y",
|
|
1407
|
+
].join("\n");
|
|
1408
|
+
const result = await engine.parseFile({ path: "src/app2.py", content: Buffer.from(code, "utf-8") });
|
|
1409
|
+
assert.ok(result.ok);
|
|
1410
|
+
if (!result.ok) return;
|
|
1411
|
+
const modules = result.ir.imports.map((i) => i.module);
|
|
1412
|
+
assert.ok(modules.includes(".models"), "expected .models in " + JSON.stringify(modules));
|
|
1413
|
+
assert.ok(modules.includes("..parent"), "expected ..parent in " + JSON.stringify(modules));
|
|
1414
|
+
assert.ok(modules.includes("...grandparent"), "expected ...grandparent in " + JSON.stringify(modules));
|
|
1415
|
+
await engine.dispose();
|
|
1416
|
+
});
|
|
1417
|
+
|