@optave/codegraph 3.11.0 → 3.11.1
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 +38 -31
- package/dist/ast-analysis/engine.d.ts.map +1 -1
- package/dist/ast-analysis/engine.js +91 -60
- package/dist/ast-analysis/engine.js.map +1 -1
- package/dist/ast-analysis/visitor-utils.d.ts +3 -0
- package/dist/ast-analysis/visitor-utils.d.ts.map +1 -1
- package/dist/ast-analysis/visitor-utils.js +83 -49
- package/dist/ast-analysis/visitor-utils.js.map +1 -1
- package/dist/ast-analysis/visitors/ast-store-visitor.d.ts.map +1 -1
- package/dist/ast-analysis/visitors/ast-store-visitor.js +78 -62
- package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
- package/dist/ast-analysis/visitors/dataflow-visitor.d.ts.map +1 -1
- package/dist/ast-analysis/visitors/dataflow-visitor.js +61 -42
- package/dist/ast-analysis/visitors/dataflow-visitor.js.map +1 -1
- package/dist/cli/commands/embed.d.ts.map +1 -1
- package/dist/cli/commands/embed.js +49 -4
- package/dist/cli/commands/embed.js.map +1 -1
- package/dist/domain/analysis/dependencies.d.ts.map +1 -1
- package/dist/domain/analysis/dependencies.js +106 -80
- package/dist/domain/analysis/dependencies.js.map +1 -1
- package/dist/domain/analysis/fn-impact.d.ts.map +1 -1
- package/dist/domain/analysis/fn-impact.js +77 -52
- package/dist/domain/analysis/fn-impact.js.map +1 -1
- package/dist/domain/analysis/module-map.d.ts.map +1 -1
- package/dist/domain/analysis/module-map.js +132 -121
- package/dist/domain/analysis/module-map.js.map +1 -1
- package/dist/domain/graph/builder/helpers.d.ts +4 -4
- package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
- package/dist/domain/graph/builder/helpers.js +47 -33
- package/dist/domain/graph/builder/helpers.js.map +1 -1
- package/dist/domain/graph/builder/incremental.d.ts +6 -0
- package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
- package/dist/domain/graph/builder/incremental.js +142 -76
- package/dist/domain/graph/builder/incremental.js.map +1 -1
- package/dist/domain/graph/builder/pipeline.d.ts +1 -44
- package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
- package/dist/domain/graph/builder/pipeline.js +10 -766
- package/dist/domain/graph/builder/pipeline.js.map +1 -1
- package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/build-edges.js +133 -96
- package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
- package/dist/domain/graph/builder/stages/build-structure.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/build-structure.js +82 -65
- package/dist/domain/graph/builder/stages/build-structure.js.map +1 -1
- package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/detect-changes.js +84 -56
- package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
- package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/finalize.js +60 -51
- package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
- package/dist/domain/graph/builder/stages/insert-nodes.d.ts +8 -6
- package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/insert-nodes.js +107 -122
- package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
- package/dist/domain/graph/builder/stages/native-db-lifecycle.d.ts +14 -0
- package/dist/domain/graph/builder/stages/native-db-lifecycle.d.ts.map +1 -0
- package/dist/domain/graph/builder/stages/native-db-lifecycle.js +77 -0
- package/dist/domain/graph/builder/stages/native-db-lifecycle.js.map +1 -0
- package/dist/domain/graph/builder/stages/native-orchestrator.d.ts +62 -0
- package/dist/domain/graph/builder/stages/native-orchestrator.d.ts.map +1 -0
- package/dist/domain/graph/builder/stages/native-orchestrator.js +747 -0
- package/dist/domain/graph/builder/stages/native-orchestrator.js.map +1 -0
- package/dist/domain/graph/cycles.d.ts +6 -4
- package/dist/domain/graph/cycles.d.ts.map +1 -1
- package/dist/domain/graph/cycles.js +50 -55
- package/dist/domain/graph/cycles.js.map +1 -1
- package/dist/domain/graph/journal.d.ts.map +1 -1
- package/dist/domain/graph/journal.js +89 -70
- package/dist/domain/graph/journal.js.map +1 -1
- package/dist/domain/graph/watcher.d.ts.map +1 -1
- package/dist/domain/graph/watcher.js +5 -2
- package/dist/domain/graph/watcher.js.map +1 -1
- package/dist/domain/parser.d.ts +12 -23
- package/dist/domain/parser.d.ts.map +1 -1
- package/dist/domain/parser.js +126 -79
- package/dist/domain/parser.js.map +1 -1
- package/dist/domain/search/generator.d.ts +3 -1
- package/dist/domain/search/generator.d.ts.map +1 -1
- package/dist/domain/search/generator.js +68 -45
- package/dist/domain/search/generator.js.map +1 -1
- package/dist/domain/search/models.d.ts +2 -0
- package/dist/domain/search/models.d.ts.map +1 -1
- package/dist/domain/search/models.js +37 -3
- package/dist/domain/search/models.js.map +1 -1
- package/dist/domain/search/search/hybrid.d.ts.map +1 -1
- package/dist/domain/search/search/hybrid.js +49 -40
- package/dist/domain/search/search/hybrid.js.map +1 -1
- package/dist/domain/search/search/semantic.d.ts.map +1 -1
- package/dist/domain/search/search/semantic.js +69 -49
- package/dist/domain/search/search/semantic.js.map +1 -1
- package/dist/domain/wasm-worker-entry.js +201 -136
- package/dist/domain/wasm-worker-entry.js.map +1 -1
- package/dist/extractors/elixir.js +95 -71
- package/dist/extractors/elixir.js.map +1 -1
- package/dist/extractors/gleam.d.ts.map +1 -1
- package/dist/extractors/gleam.js +23 -31
- package/dist/extractors/gleam.js.map +1 -1
- package/dist/extractors/helpers.d.ts +79 -1
- package/dist/extractors/helpers.d.ts.map +1 -1
- package/dist/extractors/helpers.js +137 -0
- package/dist/extractors/helpers.js.map +1 -1
- package/dist/extractors/java.d.ts.map +1 -1
- package/dist/extractors/java.js +37 -49
- package/dist/extractors/java.js.map +1 -1
- package/dist/extractors/javascript.d.ts.map +1 -1
- package/dist/extractors/javascript.js +44 -44
- package/dist/extractors/javascript.js.map +1 -1
- package/dist/extractors/julia.js +27 -34
- package/dist/extractors/julia.js.map +1 -1
- package/dist/extractors/r.d.ts.map +1 -1
- package/dist/extractors/r.js +33 -58
- package/dist/extractors/r.js.map +1 -1
- package/dist/extractors/solidity.d.ts.map +1 -1
- package/dist/extractors/solidity.js +38 -61
- package/dist/extractors/solidity.js.map +1 -1
- package/dist/features/boundaries.d.ts.map +1 -1
- package/dist/features/boundaries.js +49 -39
- package/dist/features/boundaries.js.map +1 -1
- package/dist/features/cfg.d.ts.map +1 -1
- package/dist/features/cfg.js +90 -63
- package/dist/features/cfg.js.map +1 -1
- package/dist/features/check.d.ts.map +1 -1
- package/dist/features/check.js +43 -34
- package/dist/features/check.js.map +1 -1
- package/dist/features/cochange.d.ts.map +1 -1
- package/dist/features/cochange.js +68 -56
- package/dist/features/cochange.js.map +1 -1
- package/dist/features/complexity.d.ts.map +1 -1
- package/dist/features/complexity.js +105 -75
- package/dist/features/complexity.js.map +1 -1
- package/dist/features/dataflow.d.ts.map +1 -1
- package/dist/features/dataflow.js +37 -29
- package/dist/features/dataflow.js.map +1 -1
- package/dist/features/flow.d.ts.map +1 -1
- package/dist/features/flow.js +31 -22
- package/dist/features/flow.js.map +1 -1
- package/dist/features/graph-enrichment.d.ts.map +1 -1
- package/dist/features/graph-enrichment.js +77 -70
- package/dist/features/graph-enrichment.js.map +1 -1
- package/dist/features/owners.d.ts +17 -26
- package/dist/features/owners.d.ts.map +1 -1
- package/dist/features/owners.js +120 -109
- package/dist/features/owners.js.map +1 -1
- package/dist/features/sequence.d.ts.map +1 -1
- package/dist/features/sequence.js +59 -54
- package/dist/features/sequence.js.map +1 -1
- package/dist/features/structure-query.d.ts.map +1 -1
- package/dist/features/structure-query.js +60 -60
- package/dist/features/structure-query.js.map +1 -1
- package/dist/features/structure.js +28 -36
- package/dist/features/structure.js.map +1 -1
- package/dist/graph/algorithms/leiden/optimiser.d.ts.map +1 -1
- package/dist/graph/algorithms/leiden/optimiser.js +100 -69
- package/dist/graph/algorithms/leiden/optimiser.js.map +1 -1
- package/dist/graph/classifiers/roles.d.ts.map +1 -1
- package/dist/graph/classifiers/roles.js +63 -59
- package/dist/graph/classifiers/roles.js.map +1 -1
- package/dist/infrastructure/config.d.ts +1 -1
- package/dist/infrastructure/config.d.ts.map +1 -1
- package/dist/infrastructure/config.js +1 -1
- package/dist/infrastructure/config.js.map +1 -1
- package/dist/presentation/cfg.d.ts.map +1 -1
- package/dist/presentation/cfg.js +44 -29
- package/dist/presentation/cfg.js.map +1 -1
- package/dist/presentation/flow.d.ts.map +1 -1
- package/dist/presentation/flow.js +58 -38
- package/dist/presentation/flow.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +7 -7
- package/src/ast-analysis/engine.ts +145 -61
- package/src/ast-analysis/visitor-utils.ts +86 -46
- package/src/ast-analysis/visitors/ast-store-visitor.ts +104 -69
- package/src/ast-analysis/visitors/dataflow-visitor.ts +86 -47
- package/src/cli/commands/embed.ts +54 -4
- package/src/domain/analysis/dependencies.ts +166 -85
- package/src/domain/analysis/fn-impact.ts +120 -50
- package/src/domain/analysis/module-map.ts +175 -140
- package/src/domain/graph/builder/helpers.ts +85 -76
- package/src/domain/graph/builder/incremental.ts +217 -90
- package/src/domain/graph/builder/pipeline.ts +19 -957
- package/src/domain/graph/builder/stages/build-edges.ts +198 -140
- package/src/domain/graph/builder/stages/build-structure.ts +115 -82
- package/src/domain/graph/builder/stages/detect-changes.ts +107 -64
- package/src/domain/graph/builder/stages/finalize.ts +72 -70
- package/src/domain/graph/builder/stages/insert-nodes.ts +154 -120
- package/src/domain/graph/builder/stages/native-db-lifecycle.ts +74 -0
- package/src/domain/graph/builder/stages/native-orchestrator.ts +942 -0
- package/src/domain/graph/cycles.ts +51 -49
- package/src/domain/graph/journal.ts +84 -69
- package/src/domain/graph/watcher.ts +8 -2
- package/src/domain/parser.ts +143 -66
- package/src/domain/search/generator.ts +132 -74
- package/src/domain/search/models.ts +39 -3
- package/src/domain/search/search/hybrid.ts +53 -42
- package/src/domain/search/search/semantic.ts +105 -65
- package/src/domain/wasm-worker-entry.ts +235 -152
- package/src/extractors/elixir.ts +91 -64
- package/src/extractors/gleam.ts +33 -37
- package/src/extractors/helpers.ts +205 -1
- package/src/extractors/java.ts +42 -45
- package/src/extractors/javascript.ts +44 -43
- package/src/extractors/julia.ts +28 -35
- package/src/extractors/r.ts +38 -56
- package/src/extractors/solidity.ts +43 -71
- package/src/features/boundaries.ts +64 -46
- package/src/features/cfg.ts +145 -74
- package/src/features/check.ts +60 -43
- package/src/features/cochange.ts +95 -72
- package/src/features/complexity.ts +134 -79
- package/src/features/dataflow.ts +57 -34
- package/src/features/flow.ts +48 -24
- package/src/features/graph-enrichment.ts +105 -70
- package/src/features/owners.ts +186 -146
- package/src/features/sequence.ts +99 -69
- package/src/features/structure-query.ts +94 -79
- package/src/features/structure.ts +56 -56
- package/src/graph/algorithms/leiden/optimiser.ts +142 -87
- package/src/graph/classifiers/roles.ts +64 -54
- package/src/infrastructure/config.ts +1 -1
- package/src/presentation/cfg.ts +48 -32
- package/src/presentation/flow.ts +100 -52
- package/src/types.ts +1 -1
|
@@ -3,6 +3,45 @@ import { loadNative } from '../../infrastructure/native.js';
|
|
|
3
3
|
import { isTestFile } from '../../infrastructure/test-filter.js';
|
|
4
4
|
import type { BetterSqlite3Database } from '../../types.js';
|
|
5
5
|
|
|
6
|
+
type Edge = { source: string; target: string };
|
|
7
|
+
type DbEdge = { source_id: number; target_id: number };
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Build a label-based edge list from DB rows, filtering to known nodes and
|
|
11
|
+
* deduplicating. Self-loops are skipped (Tarjan treats them as trivial SCCs).
|
|
12
|
+
*/
|
|
13
|
+
function buildLabelEdges(dbEdges: DbEdge[], idToLabel: Map<number, string>): Edge[] {
|
|
14
|
+
const edges: Edge[] = [];
|
|
15
|
+
const seen = new Set<string>();
|
|
16
|
+
for (const e of dbEdges) {
|
|
17
|
+
if (e.source_id === e.target_id) continue;
|
|
18
|
+
const src = idToLabel.get(e.source_id);
|
|
19
|
+
const tgt = idToLabel.get(e.target_id);
|
|
20
|
+
if (src === undefined || tgt === undefined) continue;
|
|
21
|
+
const key = `${src}\0${tgt}`;
|
|
22
|
+
if (seen.has(key)) continue;
|
|
23
|
+
seen.add(key);
|
|
24
|
+
edges.push({ source: src, target: tgt });
|
|
25
|
+
}
|
|
26
|
+
return edges;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function buildFileLevelEdges(db: BetterSqlite3Database, noTests: boolean): Edge[] {
|
|
30
|
+
let nodes = getFileNodesAll(db);
|
|
31
|
+
if (noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
|
|
32
|
+
const idToLabel = new Map<number, string>();
|
|
33
|
+
for (const n of nodes) idToLabel.set(n.id, n.file);
|
|
34
|
+
return buildLabelEdges(getImportEdges(db), idToLabel);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function buildCallableEdges(db: BetterSqlite3Database, noTests: boolean): Edge[] {
|
|
38
|
+
let nodes = getCallableNodes(db);
|
|
39
|
+
if (noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
|
|
40
|
+
const idToLabel = new Map<number, string>();
|
|
41
|
+
for (const n of nodes) idToLabel.set(n.id, `${n.name}|${n.file}`);
|
|
42
|
+
return buildLabelEdges(getCallEdges(db), idToLabel);
|
|
43
|
+
}
|
|
44
|
+
|
|
6
45
|
/**
|
|
7
46
|
* Find cycles using Tarjan's SCC algorithm.
|
|
8
47
|
*
|
|
@@ -16,66 +55,20 @@ export function findCycles(
|
|
|
16
55
|
const fileLevel = opts.fileLevel !== false;
|
|
17
56
|
const noTests = opts.noTests || false;
|
|
18
57
|
|
|
19
|
-
const edges
|
|
20
|
-
const seen = new Set<string>();
|
|
21
|
-
|
|
22
|
-
if (fileLevel) {
|
|
23
|
-
let nodes = getFileNodesAll(db);
|
|
24
|
-
if (noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
|
|
25
|
-
const nodeIds = new Set<number>();
|
|
26
|
-
const idToFile = new Map<number, string>();
|
|
27
|
-
for (const n of nodes) {
|
|
28
|
-
nodeIds.add(n.id);
|
|
29
|
-
idToFile.set(n.id, n.file);
|
|
30
|
-
}
|
|
31
|
-
for (const e of getImportEdges(db)) {
|
|
32
|
-
if (!nodeIds.has(e.source_id) || !nodeIds.has(e.target_id)) continue;
|
|
33
|
-
if (e.source_id === e.target_id) continue;
|
|
34
|
-
const src = idToFile.get(e.source_id)!;
|
|
35
|
-
const tgt = idToFile.get(e.target_id)!;
|
|
36
|
-
const key = `${src}\0${tgt}`;
|
|
37
|
-
if (seen.has(key)) continue;
|
|
38
|
-
seen.add(key);
|
|
39
|
-
edges.push({ source: src, target: tgt });
|
|
40
|
-
}
|
|
41
|
-
} else {
|
|
42
|
-
let nodes = getCallableNodes(db);
|
|
43
|
-
if (noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
|
|
44
|
-
const nodeIds = new Set<number>();
|
|
45
|
-
const idToLabel = new Map<number, string>();
|
|
46
|
-
for (const n of nodes) {
|
|
47
|
-
nodeIds.add(n.id);
|
|
48
|
-
idToLabel.set(n.id, `${n.name}|${n.file}`);
|
|
49
|
-
}
|
|
50
|
-
for (const e of getCallEdges(db)) {
|
|
51
|
-
if (!nodeIds.has(e.source_id) || !nodeIds.has(e.target_id)) continue;
|
|
52
|
-
if (e.source_id === e.target_id) continue;
|
|
53
|
-
const src = idToLabel.get(e.source_id)!;
|
|
54
|
-
const tgt = idToLabel.get(e.target_id)!;
|
|
55
|
-
const key = `${src}\0${tgt}`;
|
|
56
|
-
if (seen.has(key)) continue;
|
|
57
|
-
seen.add(key);
|
|
58
|
-
edges.push({ source: src, target: tgt });
|
|
59
|
-
}
|
|
60
|
-
}
|
|
58
|
+
const edges = fileLevel ? buildFileLevelEdges(db, noTests) : buildCallableEdges(db, noTests);
|
|
61
59
|
|
|
62
60
|
const native = loadNative();
|
|
63
61
|
if (native) {
|
|
64
62
|
return native.detectCycles(edges) as string[][];
|
|
65
63
|
}
|
|
66
|
-
|
|
67
64
|
return tarjanFromEdges(edges);
|
|
68
65
|
}
|
|
69
66
|
|
|
70
|
-
export function findCyclesJS(edges:
|
|
67
|
+
export function findCyclesJS(edges: Edge[]): string[][] {
|
|
71
68
|
return tarjanFromEdges(edges);
|
|
72
69
|
}
|
|
73
70
|
|
|
74
|
-
|
|
75
|
-
* Run Tarjan's SCC on a flat edge list. Returns SCCs with length > 1 (cycles).
|
|
76
|
-
* Uses a simple adjacency-list Map instead of a full CodeGraph.
|
|
77
|
-
*/
|
|
78
|
-
function tarjanFromEdges(edges: Array<{ source: string; target: string }>): string[][] {
|
|
71
|
+
function buildAdjacency(edges: Edge[]): { adj: Map<string, string[]>; allNodes: Set<string> } {
|
|
79
72
|
const adj = new Map<string, string[]>();
|
|
80
73
|
const allNodes = new Set<string>();
|
|
81
74
|
for (const { source, target } of edges) {
|
|
@@ -88,6 +81,15 @@ function tarjanFromEdges(edges: Array<{ source: string; target: string }>): stri
|
|
|
88
81
|
}
|
|
89
82
|
list.push(target);
|
|
90
83
|
}
|
|
84
|
+
return { adj, allNodes };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Run Tarjan's SCC on a flat edge list. Returns SCCs with length > 1 (cycles).
|
|
89
|
+
* Uses a simple adjacency-list Map instead of a full CodeGraph.
|
|
90
|
+
*/
|
|
91
|
+
function tarjanFromEdges(edges: Edge[]): string[][] {
|
|
92
|
+
const { adj, allNodes } = buildAdjacency(edges);
|
|
91
93
|
|
|
92
94
|
let index = 0;
|
|
93
95
|
const stack: string[] = [];
|
|
@@ -91,62 +91,69 @@ function trySteal(lockPath: string): AcquiredLock | null {
|
|
|
91
91
|
return { fd, nonce };
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
94
|
+
/**
|
|
95
|
+
* Try to create the lockfile fresh via `wx`. Returns the acquired lock on
|
|
96
|
+
* success, `null` if another holder exists, or throws on unexpected errors.
|
|
97
|
+
*
|
|
98
|
+
* If the stamp write fails (ENOSPC, I/O error) we release the empty file —
|
|
99
|
+
* leaving it would look stale to concurrent waiters and admit double-acquire.
|
|
100
|
+
*/
|
|
101
|
+
function tryFreshAcquire(lockPath: string): AcquiredLock | null {
|
|
102
|
+
const nonce = `${process.pid}-${crypto.randomBytes(8).toString('hex')}`;
|
|
103
|
+
let fd: number;
|
|
104
|
+
try {
|
|
105
|
+
fd = fs.openSync(lockPath, 'wx');
|
|
106
|
+
} catch (e) {
|
|
107
|
+
if ((e as NodeJS.ErrnoException).code === 'EEXIST') return null;
|
|
108
|
+
throw e;
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
fs.writeSync(fd, `${process.pid}\n${nonce}\n`);
|
|
112
|
+
} catch {
|
|
98
113
|
try {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
} catch {
|
|
103
|
-
// Stamp write failed (ENOSPC, I/O error). An empty lockfile would
|
|
104
|
-
// look stale to concurrent waiters (Number('') === 0, isPidAlive(0)
|
|
105
|
-
// returns false), so they'd steal our live lock. Release and retry.
|
|
106
|
-
try {
|
|
107
|
-
fs.closeSync(fd);
|
|
108
|
-
} catch {
|
|
109
|
-
/* ignore */
|
|
110
|
-
}
|
|
111
|
-
try {
|
|
112
|
-
fs.unlinkSync(lockPath);
|
|
113
|
-
} catch {
|
|
114
|
-
/* ignore */
|
|
115
|
-
}
|
|
116
|
-
if (Date.now() - start > LOCK_TIMEOUT_MS) {
|
|
117
|
-
throw new Error(
|
|
118
|
-
`Failed to acquire journal lock at ${lockPath} within ${LOCK_TIMEOUT_MS}ms`,
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
sleepSync(LOCK_RETRY_MS);
|
|
122
|
-
continue;
|
|
123
|
-
}
|
|
124
|
-
return { fd, nonce };
|
|
125
|
-
} catch (e) {
|
|
126
|
-
if ((e as NodeJS.ErrnoException).code !== 'EEXIST') throw e;
|
|
114
|
+
fs.closeSync(fd);
|
|
115
|
+
} catch {
|
|
116
|
+
/* ignore */
|
|
127
117
|
}
|
|
128
|
-
|
|
129
|
-
let holderAlive = true;
|
|
130
118
|
try {
|
|
131
|
-
|
|
132
|
-
holderAlive = isPidAlive(Number(pidContent));
|
|
119
|
+
fs.unlinkSync(lockPath);
|
|
133
120
|
} catch {
|
|
134
|
-
/*
|
|
121
|
+
/* ignore */
|
|
135
122
|
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
return { fd, nonce };
|
|
126
|
+
}
|
|
136
127
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
128
|
+
/**
|
|
129
|
+
* Decide whether the current lock holder is stale and should be stolen.
|
|
130
|
+
* Returns true if the PID is dead, or if the lockfile mtime exceeds the
|
|
131
|
+
* staleness threshold.
|
|
132
|
+
*/
|
|
133
|
+
function isLockStale(lockPath: string): boolean {
|
|
134
|
+
let holderAlive = true;
|
|
135
|
+
try {
|
|
136
|
+
const pidContent = fs.readFileSync(lockPath, 'utf-8').split('\n')[0]!.trim();
|
|
137
|
+
holderAlive = isPidAlive(Number(pidContent));
|
|
138
|
+
} catch {
|
|
139
|
+
/* unreadable — fall through to age check */
|
|
140
|
+
}
|
|
141
|
+
if (!holderAlive) return true;
|
|
142
|
+
try {
|
|
143
|
+
const stat = fs.statSync(lockPath);
|
|
144
|
+
return Date.now() - stat.mtimeMs > LOCK_STALE_MS;
|
|
145
|
+
} catch {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
148
149
|
|
|
149
|
-
|
|
150
|
+
function acquireJournalLock(lockPath: string): AcquiredLock {
|
|
151
|
+
const start = Date.now();
|
|
152
|
+
for (;;) {
|
|
153
|
+
const fresh = tryFreshAcquire(lockPath);
|
|
154
|
+
if (fresh) return fresh;
|
|
155
|
+
|
|
156
|
+
if (isLockStale(lockPath)) {
|
|
150
157
|
const stolen = trySteal(lockPath);
|
|
151
158
|
if (stolen) return stolen;
|
|
152
159
|
// Steal failed or lost the race — fall through to timeout check & retry.
|
|
@@ -227,27 +234,20 @@ interface JournalResult {
|
|
|
227
234
|
removed?: string[];
|
|
228
235
|
}
|
|
229
236
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
let content: string;
|
|
233
|
-
try {
|
|
234
|
-
content = fs.readFileSync(journalPath, 'utf-8');
|
|
235
|
-
} catch {
|
|
236
|
-
return { valid: false };
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const lines = content.split('\n');
|
|
240
|
-
if (lines.length === 0 || !lines[0]!.startsWith(HEADER_PREFIX)) {
|
|
237
|
+
function parseJournalHeader(firstLine: string | undefined): number | null {
|
|
238
|
+
if (!firstLine || !firstLine.startsWith(HEADER_PREFIX)) {
|
|
241
239
|
debug('Journal has malformed or missing header');
|
|
242
|
-
return
|
|
240
|
+
return null;
|
|
243
241
|
}
|
|
244
|
-
|
|
245
|
-
const timestamp = Number(lines[0]!.slice(HEADER_PREFIX.length).trim());
|
|
242
|
+
const timestamp = Number(firstLine.slice(HEADER_PREFIX.length).trim());
|
|
246
243
|
if (!Number.isFinite(timestamp) || timestamp <= 0) {
|
|
247
244
|
debug('Journal has invalid timestamp');
|
|
248
|
-
return
|
|
245
|
+
return null;
|
|
249
246
|
}
|
|
247
|
+
return timestamp;
|
|
248
|
+
}
|
|
250
249
|
|
|
250
|
+
function parseJournalBody(lines: string[]): { changed: string[]; removed: string[] } {
|
|
251
251
|
const changed: string[] = [];
|
|
252
252
|
const removed: string[] = [];
|
|
253
253
|
const seenChanged = new Set<string>();
|
|
@@ -263,14 +263,29 @@ export function readJournal(rootDir: string): JournalResult {
|
|
|
263
263
|
seenRemoved.add(filePath);
|
|
264
264
|
removed.push(filePath);
|
|
265
265
|
}
|
|
266
|
-
} else {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
changed.push(line);
|
|
270
|
-
}
|
|
266
|
+
} else if (!seenChanged.has(line)) {
|
|
267
|
+
seenChanged.add(line);
|
|
268
|
+
changed.push(line);
|
|
271
269
|
}
|
|
272
270
|
}
|
|
273
271
|
|
|
272
|
+
return { changed, removed };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function readJournal(rootDir: string): JournalResult {
|
|
276
|
+
const journalPath = path.join(rootDir, '.codegraph', JOURNAL_FILENAME);
|
|
277
|
+
let content: string;
|
|
278
|
+
try {
|
|
279
|
+
content = fs.readFileSync(journalPath, 'utf-8');
|
|
280
|
+
} catch {
|
|
281
|
+
return { valid: false };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const lines = content.split('\n');
|
|
285
|
+
const timestamp = parseJournalHeader(lines[0]);
|
|
286
|
+
if (timestamp === null) return { valid: false };
|
|
287
|
+
|
|
288
|
+
const { changed, removed } = parseJournalBody(lines);
|
|
274
289
|
return { valid: true, timestamp, changed, removed };
|
|
275
290
|
}
|
|
276
291
|
|
|
@@ -31,6 +31,9 @@ function prepareWatcherStatements(db: ReturnType<typeof openDb>): IncrementalStm
|
|
|
31
31
|
'INSERT INTO edges (source_id, target_id, kind, confidence, dynamic) VALUES (?, ?, ?, ?, ?)',
|
|
32
32
|
),
|
|
33
33
|
countNodes: db.prepare('SELECT COUNT(*) as c FROM nodes WHERE file = ?'),
|
|
34
|
+
countEdges: db.prepare(
|
|
35
|
+
'SELECT COUNT(*) as c FROM edges WHERE source_id IN (SELECT id FROM nodes WHERE file = ?)',
|
|
36
|
+
),
|
|
34
37
|
findNodeInFile: db.prepare(
|
|
35
38
|
"SELECT id, file FROM nodes WHERE name = ? AND kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module', 'constant') AND file = ?",
|
|
36
39
|
),
|
|
@@ -52,6 +55,7 @@ interface RebuildResult {
|
|
|
52
55
|
nodesAdded: number;
|
|
53
56
|
nodesRemoved: number;
|
|
54
57
|
edgesAdded: number;
|
|
58
|
+
edgesBefore: number;
|
|
55
59
|
}
|
|
56
60
|
|
|
57
61
|
/** Process a batch of pending file changes: rebuild, journal, and log. */
|
|
@@ -107,7 +111,7 @@ function writeJournalAndChangeEvents(rootDir: string, updates: RebuildResult[]):
|
|
|
107
111
|
buildChangeEvent(r.file, r.event, r.symbolDiff, {
|
|
108
112
|
nodesBefore: r.nodesBefore,
|
|
109
113
|
nodesAfter: r.nodesAfter,
|
|
110
|
-
edgesAdded: r.edgesAdded,
|
|
114
|
+
edgesAdded: r.edgesAdded - r.edgesBefore,
|
|
111
115
|
}),
|
|
112
116
|
);
|
|
113
117
|
try {
|
|
@@ -125,7 +129,9 @@ function logRebuildResults(updates: RebuildResult[]): void {
|
|
|
125
129
|
if (r.deleted) {
|
|
126
130
|
info(`Removed: ${r.file} (-${r.nodesRemoved} nodes)`);
|
|
127
131
|
} else {
|
|
128
|
-
|
|
132
|
+
const edgeDelta = r.edgesAdded - r.edgesBefore;
|
|
133
|
+
const edgeStr = edgeDelta >= 0 ? `+${edgeDelta}` : `${edgeDelta}`;
|
|
134
|
+
info(`Updated: ${r.file} (${nodeStr} nodes, ${edgeStr} edges)`);
|
|
129
135
|
}
|
|
130
136
|
}
|
|
131
137
|
}
|
package/src/domain/parser.ts
CHANGED
|
@@ -322,12 +322,15 @@ export function getParser(parsers: Map<string, Parser | null>, filePath: string)
|
|
|
322
322
|
* without _tree", which was the source of #1036 — a single file missing one
|
|
323
323
|
* analysis triggered a full-build re-parse of every WASM-parseable file.
|
|
324
324
|
*/
|
|
325
|
-
|
|
325
|
+
/**
|
|
326
|
+
* Select files from `fileSymbols` that still need analysis data and are
|
|
327
|
+
* parseable by an installed WASM grammar. Pure (no I/O) — safe to unit-test.
|
|
328
|
+
*/
|
|
329
|
+
function collectBackfillPending(
|
|
326
330
|
fileSymbols: Map<string, any>,
|
|
327
331
|
rootDir: string,
|
|
328
332
|
needsFn?: (relPath: string, symbols: any) => boolean,
|
|
329
|
-
):
|
|
330
|
-
// Collect files that still need analysis data and are parseable by WASM.
|
|
333
|
+
): Array<{ relPath: string; absPath: string; symbols: any }> {
|
|
331
334
|
const pending: Array<{ relPath: string; absPath: string; symbols: any }> = [];
|
|
332
335
|
for (const [relPath, symbols] of fileSymbols) {
|
|
333
336
|
if (symbols._tree) continue; // legacy path — leave existing trees alone
|
|
@@ -335,6 +338,15 @@ export async function ensureWasmTrees(
|
|
|
335
338
|
if (needsFn && !needsFn(relPath, symbols)) continue;
|
|
336
339
|
pending.push({ relPath, absPath: path.join(rootDir, relPath), symbols });
|
|
337
340
|
}
|
|
341
|
+
return pending;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export async function ensureWasmTrees(
|
|
345
|
+
fileSymbols: Map<string, any>,
|
|
346
|
+
rootDir: string,
|
|
347
|
+
needsFn?: (relPath: string, symbols: any) => boolean,
|
|
348
|
+
): Promise<void> {
|
|
349
|
+
const pending = collectBackfillPending(fileSymbols, rootDir, needsFn);
|
|
338
350
|
if (pending.length === 0) return;
|
|
339
351
|
|
|
340
352
|
const pool = getWasmWorkerPool();
|
|
@@ -352,30 +364,37 @@ export async function ensureWasmTrees(
|
|
|
352
364
|
}
|
|
353
365
|
}
|
|
354
366
|
|
|
355
|
-
/**
|
|
356
|
-
|
|
357
|
-
* Only fills gaps — never overwrites fields the caller already populated.
|
|
358
|
-
* Used to patch native-parsed symbols with worker-produced astNodes / dataflow /
|
|
359
|
-
* per-definition complexity and cfg.
|
|
360
|
-
*/
|
|
361
|
-
function mergeAnalysisData(symbols: any, worker: ExtractorOutput): void {
|
|
367
|
+
/** Fill gap-only scalar metadata (`_langId`, `_lineCount`) from the worker output. */
|
|
368
|
+
function mergeScalarMetadata(symbols: any, worker: ExtractorOutput): void {
|
|
362
369
|
if (!symbols._langId && worker._langId) symbols._langId = worker._langId;
|
|
363
370
|
if (!symbols._lineCount && worker._lineCount) symbols._lineCount = worker._lineCount;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** Fill gap-only analysis arrays (`astNodes`, `dataflow`) from the worker output. */
|
|
374
|
+
function mergeAnalysisArrays(symbols: any, worker: ExtractorOutput): void {
|
|
364
375
|
if (!Array.isArray(symbols.astNodes) && Array.isArray(worker.astNodes)) {
|
|
365
376
|
symbols.astNodes = worker.astNodes;
|
|
366
377
|
}
|
|
367
378
|
if (!symbols.dataflow && worker.dataflow) symbols.dataflow = worker.dataflow;
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** Merge worker typeMap into existing symbols.typeMap with first-wins semantics. */
|
|
382
|
+
function mergeTypeMap(symbols: any, worker: ExtractorOutput): void {
|
|
383
|
+
if (!worker.typeMap || worker.typeMap.size === 0) return;
|
|
384
|
+
if (!symbols.typeMap || !(symbols.typeMap instanceof Map)) {
|
|
385
|
+
symbols.typeMap = new Map(worker.typeMap);
|
|
386
|
+
return;
|
|
376
387
|
}
|
|
388
|
+
for (const [k, v] of worker.typeMap) {
|
|
389
|
+
if (!symbols.typeMap.has(k)) symbols.typeMap.set(k, v);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** Patch existing definitions with worker complexity/cfg when absent. */
|
|
394
|
+
function mergeDefinitionAnalysis(symbols: any, worker: ExtractorOutput): void {
|
|
377
395
|
const existingDefs: any[] = Array.isArray(symbols.definitions) ? symbols.definitions : [];
|
|
378
396
|
const workerDefs: any[] = Array.isArray(worker.definitions) ? worker.definitions : [];
|
|
397
|
+
if (existingDefs.length === 0 || workerDefs.length === 0) return;
|
|
379
398
|
// Index existing defs by (kind, name, line) — mirrors engine.ts matching key.
|
|
380
399
|
const byKey = new Map<string, any>();
|
|
381
400
|
for (const d of existingDefs) byKey.set(`${d.kind}|${d.name}|${d.line}`, d);
|
|
@@ -389,6 +408,19 @@ function mergeAnalysisData(symbols: any, worker: ExtractorOutput): void {
|
|
|
389
408
|
}
|
|
390
409
|
}
|
|
391
410
|
|
|
411
|
+
/**
|
|
412
|
+
* Merge pre-computed analysis data from a worker result onto existing symbols.
|
|
413
|
+
* Only fills gaps — never overwrites fields the caller already populated.
|
|
414
|
+
* Used to patch native-parsed symbols with worker-produced astNodes / dataflow /
|
|
415
|
+
* per-definition complexity and cfg.
|
|
416
|
+
*/
|
|
417
|
+
function mergeAnalysisData(symbols: any, worker: ExtractorOutput): void {
|
|
418
|
+
mergeScalarMetadata(symbols, worker);
|
|
419
|
+
mergeAnalysisArrays(symbols, worker);
|
|
420
|
+
mergeTypeMap(symbols, worker);
|
|
421
|
+
mergeDefinitionAnalysis(symbols, worker);
|
|
422
|
+
}
|
|
423
|
+
|
|
392
424
|
/**
|
|
393
425
|
* Check whether the required WASM grammar files exist on disk.
|
|
394
426
|
*/
|
|
@@ -539,25 +571,36 @@ export function classifyNativeDrops(relPaths: Iterable<string>): NativeDropClass
|
|
|
539
571
|
}
|
|
540
572
|
|
|
541
573
|
/**
|
|
542
|
-
* Render `{ ext → paths[] }` as
|
|
543
|
-
*
|
|
544
|
-
*
|
|
545
|
-
*
|
|
546
|
-
*
|
|
574
|
+
* Render `{ ext → paths[] }` as a multi-line tabular breakdown for log lines.
|
|
575
|
+
* Each extension occupies its own line so a long warning scans like a table
|
|
576
|
+
* instead of a wall of semicolon-separated slices. Caps at 3 sample paths per
|
|
577
|
+
* extension and 6 extensions total to keep output bounded when many languages
|
|
578
|
+
* are dropped at once. Extensions are sorted by descending file count so the
|
|
579
|
+
* loudest offender shows up first; ties keep insertion order.
|
|
580
|
+
*
|
|
581
|
+
* Returns the empty string for empty input, and otherwise a string that
|
|
582
|
+
* begins with `\n` so callers can append it directly after the header line
|
|
583
|
+
* (`"Backfilling via WASM:" + formatDropExtensionSummary(...)`).
|
|
584
|
+
*
|
|
585
|
+
* Pure function — safe to unit-test independently.
|
|
547
586
|
*/
|
|
548
587
|
export function formatDropExtensionSummary(buckets: Map<string, string[]>): string {
|
|
549
588
|
const MAX_EXTS = 6;
|
|
550
589
|
const MAX_SAMPLES = 3;
|
|
551
590
|
const entries = Array.from(buckets.entries()).sort((a, b) => b[1].length - a[1].length);
|
|
552
|
-
|
|
591
|
+
if (entries.length === 0) return '';
|
|
592
|
+
const shown = entries.slice(0, MAX_EXTS);
|
|
593
|
+
const extWidth = Math.max(...shown.map(([ext]) => ext.length));
|
|
594
|
+
const countWidth = Math.max(...shown.map(([, paths]) => String(paths.length).length));
|
|
595
|
+
const lines = shown.map(([ext, paths]) => {
|
|
553
596
|
const sample = paths.slice(0, MAX_SAMPLES).join(', ');
|
|
554
|
-
const more = paths.length > MAX_SAMPLES ?
|
|
555
|
-
return
|
|
597
|
+
const more = paths.length > MAX_SAMPLES ? ` (+${paths.length - MAX_SAMPLES} more)` : '';
|
|
598
|
+
return ` ${ext.padEnd(extWidth)} ${String(paths.length).padStart(countWidth)} ${sample}${more}`;
|
|
556
599
|
});
|
|
557
600
|
if (entries.length > MAX_EXTS) {
|
|
558
|
-
|
|
601
|
+
lines.push(` (+${entries.length - MAX_EXTS} more extension(s))`);
|
|
559
602
|
}
|
|
560
|
-
return
|
|
603
|
+
return `\n${lines.join('\n')}`;
|
|
561
604
|
}
|
|
562
605
|
|
|
563
606
|
// ── Unified API ──────────────────────────────────────────────────────────────
|
|
@@ -592,24 +635,36 @@ function patchDefinitions(definitions: any[]): void {
|
|
|
592
635
|
}
|
|
593
636
|
}
|
|
594
637
|
|
|
638
|
+
/**
|
|
639
|
+
* Field renames applied to each import record to bridge older native binaries
|
|
640
|
+
* that emit snake_case names. Each `[camel, snake]` pair becomes:
|
|
641
|
+
* `if (imp[camel] === undefined) imp[camel] = imp[snake];`
|
|
642
|
+
* Defined as data so the loop body stays trivially linear in cognitive complexity.
|
|
643
|
+
*/
|
|
644
|
+
const IMPORT_FIELD_RENAMES: ReadonlyArray<readonly [string, string]> = [
|
|
645
|
+
['typeOnly', 'type_only'],
|
|
646
|
+
['wildcardReexport', 'wildcard_reexport'],
|
|
647
|
+
['pythonImport', 'python_import'],
|
|
648
|
+
['goImport', 'go_import'],
|
|
649
|
+
['rustUse', 'rust_use'],
|
|
650
|
+
['javaImport', 'java_import'],
|
|
651
|
+
['csharpUsing', 'csharp_using'],
|
|
652
|
+
['rubyRequire', 'ruby_require'],
|
|
653
|
+
['phpUse', 'php_use'],
|
|
654
|
+
['cInclude', 'c_include'],
|
|
655
|
+
['kotlinImport', 'kotlin_import'],
|
|
656
|
+
['swiftImport', 'swift_import'],
|
|
657
|
+
['scalaImport', 'scala_import'],
|
|
658
|
+
['bashSource', 'bash_source'],
|
|
659
|
+
['dynamicImport', 'dynamic_import'],
|
|
660
|
+
];
|
|
661
|
+
|
|
595
662
|
/** Patch import fields for backward compat with older native binaries. */
|
|
596
663
|
function patchImports(imports: any[]): void {
|
|
597
664
|
for (const i of imports) {
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
if (i.goImport === undefined) i.goImport = i.go_import;
|
|
602
|
-
if (i.rustUse === undefined) i.rustUse = i.rust_use;
|
|
603
|
-
if (i.javaImport === undefined) i.javaImport = i.java_import;
|
|
604
|
-
if (i.csharpUsing === undefined) i.csharpUsing = i.csharp_using;
|
|
605
|
-
if (i.rubyRequire === undefined) i.rubyRequire = i.ruby_require;
|
|
606
|
-
if (i.phpUse === undefined) i.phpUse = i.php_use;
|
|
607
|
-
if (i.cInclude === undefined) i.cInclude = i.c_include;
|
|
608
|
-
if (i.kotlinImport === undefined) i.kotlinImport = i.kotlin_import;
|
|
609
|
-
if (i.swiftImport === undefined) i.swiftImport = i.swift_import;
|
|
610
|
-
if (i.scalaImport === undefined) i.scalaImport = i.scala_import;
|
|
611
|
-
if (i.bashSource === undefined) i.bashSource = i.bash_source;
|
|
612
|
-
if (i.dynamicImport === undefined) i.dynamicImport = i.dynamic_import;
|
|
665
|
+
for (const [camel, snake] of IMPORT_FIELD_RENAMES) {
|
|
666
|
+
if (i[camel] === undefined) i[camel] = i[snake];
|
|
667
|
+
}
|
|
613
668
|
}
|
|
614
669
|
}
|
|
615
670
|
|
|
@@ -1159,18 +1214,16 @@ export async function parseFilesWasmForBackfill(
|
|
|
1159
1214
|
}
|
|
1160
1215
|
|
|
1161
1216
|
/**
|
|
1162
|
-
*
|
|
1217
|
+
* Run the native engine over `filePaths` and ingest the results into `result`.
|
|
1218
|
+
* Returns the set of file paths the native engine successfully parsed and the
|
|
1219
|
+
* TS/TSX files that need a typeMap backfill pass.
|
|
1163
1220
|
*/
|
|
1164
|
-
|
|
1221
|
+
function ingestNativeResults(
|
|
1222
|
+
native: any,
|
|
1165
1223
|
filePaths: string[],
|
|
1166
1224
|
rootDir: string,
|
|
1167
|
-
|
|
1168
|
-
):
|
|
1169
|
-
const { native } = resolveEngine(opts);
|
|
1170
|
-
|
|
1171
|
-
if (!native) return parseFilesWasm(filePaths, rootDir);
|
|
1172
|
-
|
|
1173
|
-
const result = new Map<string, ExtractorOutput>();
|
|
1225
|
+
result: Map<string, ExtractorOutput>,
|
|
1226
|
+
): { nativeParsed: Set<string>; needsTypeMap: { filePath: string; relPath: string }[] } {
|
|
1174
1227
|
// Always extract all analysis data (dataflow + AST nodes) during native parse.
|
|
1175
1228
|
// This eliminates the need for any downstream WASM re-parse or native standalone calls.
|
|
1176
1229
|
const nativeResults = native.parseFilesFull
|
|
@@ -1193,27 +1246,51 @@ export async function parseFilesAuto(
|
|
|
1193
1246
|
needsTypeMap.push({ filePath: r.file, relPath });
|
|
1194
1247
|
}
|
|
1195
1248
|
}
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
}
|
|
1249
|
+
return { nativeParsed, needsTypeMap };
|
|
1250
|
+
}
|
|
1199
1251
|
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1252
|
+
/**
|
|
1253
|
+
* Engine parity: native may silently drop files whose extensions are in
|
|
1254
|
+
* SUPPORTED_EXTENSIONS (because a WASM grammar exists) but whose Rust
|
|
1255
|
+
* extractor/grammar is missing or fails. WASM handles these — fall back so
|
|
1256
|
+
* both engines process the same file set (#967). Restrict to installed WASM
|
|
1257
|
+
* grammars so we don't warn about files that neither engine can parse.
|
|
1258
|
+
*/
|
|
1259
|
+
async function backfillNativeDrops(
|
|
1260
|
+
filePaths: string[],
|
|
1261
|
+
nativeParsed: Set<string>,
|
|
1262
|
+
rootDir: string,
|
|
1263
|
+
result: Map<string, ExtractorOutput>,
|
|
1264
|
+
): Promise<void> {
|
|
1205
1265
|
const installedExts = getInstalledWasmExtensions();
|
|
1206
1266
|
const dropped = filePaths.filter(
|
|
1207
1267
|
(f) => !nativeParsed.has(f) && installedExts.has(path.extname(f).toLowerCase()),
|
|
1208
1268
|
);
|
|
1209
|
-
if (dropped.length
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
}
|
|
1269
|
+
if (dropped.length === 0) return;
|
|
1270
|
+
warn(`Native engine dropped ${dropped.length} file(s); falling back to WASM for parity`);
|
|
1271
|
+
const wasmResults = await parseFilesWasmForBackfill(dropped, rootDir);
|
|
1272
|
+
for (const [relPath, symbols] of wasmResults) {
|
|
1273
|
+
result.set(relPath, symbols);
|
|
1215
1274
|
}
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
/**
|
|
1278
|
+
* Parse multiple files in bulk and return a Map<relPath, symbols>.
|
|
1279
|
+
*/
|
|
1280
|
+
export async function parseFilesAuto(
|
|
1281
|
+
filePaths: string[],
|
|
1282
|
+
rootDir: string,
|
|
1283
|
+
opts: ParseEngineOpts = {},
|
|
1284
|
+
): Promise<Map<string, ExtractorOutput>> {
|
|
1285
|
+
const { native } = resolveEngine(opts);
|
|
1286
|
+
if (!native) return parseFilesWasm(filePaths, rootDir);
|
|
1216
1287
|
|
|
1288
|
+
const result = new Map<string, ExtractorOutput>();
|
|
1289
|
+
const { nativeParsed, needsTypeMap } = ingestNativeResults(native, filePaths, rootDir, result);
|
|
1290
|
+
if (needsTypeMap.length > 0) {
|
|
1291
|
+
await backfillTypeMapBatch(needsTypeMap, result);
|
|
1292
|
+
}
|
|
1293
|
+
await backfillNativeDrops(filePaths, nativeParsed, rootDir, result);
|
|
1217
1294
|
return result;
|
|
1218
1295
|
}
|
|
1219
1296
|
|