@optave/codegraph 3.7.0 → 3.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -14
- package/dist/ast-analysis/engine.d.ts.map +1 -1
- package/dist/ast-analysis/engine.js +158 -1
- package/dist/ast-analysis/engine.js.map +1 -1
- package/dist/ast-analysis/rules/javascript.d.ts.map +1 -1
- package/dist/ast-analysis/rules/javascript.js +0 -1
- package/dist/ast-analysis/rules/javascript.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 +2 -75
- package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
- package/dist/cli/commands/ast.js +2 -2
- package/dist/cli/commands/ast.js.map +1 -1
- package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
- package/dist/domain/graph/builder/pipeline.js +128 -6
- 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 +101 -1
- package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
- package/dist/domain/graph/builder/stages/collect-files.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/collect-files.js +17 -5
- package/dist/domain/graph/builder/stages/collect-files.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 +98 -50
- 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 +32 -5
- package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
- package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/insert-nodes.js +20 -7
- package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
- package/dist/domain/parser.d.ts +1 -1
- package/dist/domain/parser.d.ts.map +1 -1
- package/dist/domain/parser.js +88 -4
- package/dist/domain/parser.js.map +1 -1
- package/dist/extractors/clojure.d.ts +12 -0
- package/dist/extractors/clojure.d.ts.map +1 -0
- package/dist/extractors/clojure.js +245 -0
- package/dist/extractors/clojure.js.map +1 -0
- package/dist/extractors/cuda.d.ts +11 -0
- package/dist/extractors/cuda.d.ts.map +1 -0
- package/dist/extractors/cuda.js +302 -0
- package/dist/extractors/cuda.js.map +1 -0
- package/dist/extractors/erlang.d.ts +14 -0
- package/dist/extractors/erlang.d.ts.map +1 -0
- package/dist/extractors/erlang.js +239 -0
- package/dist/extractors/erlang.js.map +1 -0
- package/dist/extractors/fsharp.d.ts +13 -0
- package/dist/extractors/fsharp.d.ts.map +1 -0
- package/dist/extractors/fsharp.js +218 -0
- package/dist/extractors/fsharp.js.map +1 -0
- package/dist/extractors/gleam.d.ts +14 -0
- package/dist/extractors/gleam.d.ts.map +1 -0
- package/dist/extractors/gleam.js +229 -0
- package/dist/extractors/gleam.js.map +1 -0
- package/dist/extractors/groovy.d.ts +10 -0
- package/dist/extractors/groovy.d.ts.map +1 -0
- package/dist/extractors/groovy.js +304 -0
- package/dist/extractors/groovy.js.map +1 -0
- package/dist/extractors/index.d.ts +11 -0
- package/dist/extractors/index.d.ts.map +1 -1
- package/dist/extractors/index.js +11 -0
- package/dist/extractors/index.js.map +1 -1
- package/dist/extractors/julia.d.ts +16 -0
- package/dist/extractors/julia.d.ts.map +1 -0
- package/dist/extractors/julia.js +287 -0
- package/dist/extractors/julia.js.map +1 -0
- package/dist/extractors/objc.d.ts +9 -0
- package/dist/extractors/objc.d.ts.map +1 -0
- package/dist/extractors/objc.js +406 -0
- package/dist/extractors/objc.js.map +1 -0
- package/dist/extractors/ocaml.js +74 -0
- package/dist/extractors/ocaml.js.map +1 -1
- package/dist/extractors/r.d.ts +13 -0
- package/dist/extractors/r.d.ts.map +1 -0
- package/dist/extractors/r.js +251 -0
- package/dist/extractors/r.js.map +1 -0
- package/dist/extractors/solidity.d.ts +9 -0
- package/dist/extractors/solidity.d.ts.map +1 -0
- package/dist/extractors/solidity.js +374 -0
- package/dist/extractors/solidity.js.map +1 -0
- package/dist/extractors/verilog.d.ts +9 -0
- package/dist/extractors/verilog.d.ts.map +1 -0
- package/dist/extractors/verilog.js +286 -0
- package/dist/extractors/verilog.js.map +1 -0
- package/dist/features/ast.d.ts.map +1 -1
- package/dist/features/ast.js +1 -2
- package/dist/features/ast.js.map +1 -1
- package/dist/graph/algorithms/bfs.d.ts +2 -0
- package/dist/graph/algorithms/bfs.d.ts.map +1 -1
- package/dist/graph/algorithms/bfs.js +27 -0
- package/dist/graph/algorithms/bfs.js.map +1 -1
- package/dist/graph/algorithms/centrality.d.ts +2 -0
- package/dist/graph/algorithms/centrality.d.ts.map +1 -1
- package/dist/graph/algorithms/centrality.js +28 -0
- package/dist/graph/algorithms/centrality.js.map +1 -1
- package/dist/graph/algorithms/louvain.d.ts +3 -4
- package/dist/graph/algorithms/louvain.d.ts.map +1 -1
- package/dist/graph/algorithms/louvain.js +29 -0
- package/dist/graph/algorithms/louvain.js.map +1 -1
- package/dist/graph/algorithms/shortest-path.d.ts +2 -0
- package/dist/graph/algorithms/shortest-path.d.ts.map +1 -1
- package/dist/graph/algorithms/shortest-path.js +18 -1
- package/dist/graph/algorithms/shortest-path.js.map +1 -1
- package/dist/types.d.ts +122 -2
- package/dist/types.d.ts.map +1 -1
- package/grammars/tree-sitter-clojure.wasm +0 -0
- package/grammars/tree-sitter-cuda.wasm +0 -0
- package/grammars/tree-sitter-erlang.wasm +0 -0
- package/grammars/tree-sitter-fsharp.wasm +0 -0
- package/grammars/tree-sitter-gleam.wasm +0 -0
- package/grammars/tree-sitter-groovy.wasm +0 -0
- package/grammars/tree-sitter-julia.wasm +0 -0
- package/grammars/tree-sitter-objc.wasm +0 -0
- package/grammars/tree-sitter-ocaml_interface.wasm +0 -0
- package/grammars/tree-sitter-r.wasm +0 -0
- package/grammars/tree-sitter-solidity.wasm +0 -0
- package/grammars/tree-sitter-verilog.wasm +0 -0
- package/package.json +18 -7
- package/src/ast-analysis/engine.ts +183 -1
- package/src/ast-analysis/rules/javascript.ts +0 -1
- package/src/ast-analysis/visitors/ast-store-visitor.ts +2 -75
- package/src/cli/commands/ast.ts +2 -2
- package/src/domain/graph/builder/pipeline.ts +142 -6
- package/src/domain/graph/builder/stages/build-edges.ts +158 -1
- package/src/domain/graph/builder/stages/collect-files.ts +18 -7
- package/src/domain/graph/builder/stages/detect-changes.ts +109 -55
- package/src/domain/graph/builder/stages/finalize.ts +39 -9
- package/src/domain/graph/builder/stages/insert-nodes.ts +18 -7
- package/src/domain/parser.ts +108 -2
- package/src/extractors/clojure.ts +273 -0
- package/src/extractors/cuda.ts +316 -0
- package/src/extractors/erlang.ts +252 -0
- package/src/extractors/fsharp.ts +253 -0
- package/src/extractors/gleam.ts +246 -0
- package/src/extractors/groovy.ts +332 -0
- package/src/extractors/index.ts +11 -0
- package/src/extractors/julia.ts +318 -0
- package/src/extractors/objc.ts +431 -0
- package/src/extractors/ocaml.ts +78 -0
- package/src/extractors/r.ts +253 -0
- package/src/extractors/solidity.ts +398 -0
- package/src/extractors/verilog.ts +315 -0
- package/src/features/ast.ts +1 -2
- package/src/graph/algorithms/bfs.ts +34 -0
- package/src/graph/algorithms/centrality.ts +30 -0
- package/src/graph/algorithms/louvain.ts +31 -4
- package/src/graph/algorithms/shortest-path.ts +20 -1
- package/src/types.ts +117 -2
|
@@ -15,10 +15,12 @@
|
|
|
15
15
|
* output). This eliminates redundant tree traversals per file.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
import fs from 'node:fs';
|
|
18
19
|
import path from 'node:path';
|
|
19
20
|
import { performance } from 'node:perf_hooks';
|
|
20
21
|
import { bulkNodeIdsByFile } from '../db/index.js';
|
|
21
22
|
import { debug } from '../infrastructure/logger.js';
|
|
23
|
+
import { loadNative } from '../infrastructure/native.js';
|
|
22
24
|
import type {
|
|
23
25
|
AnalysisOpts,
|
|
24
26
|
AnalysisTiming,
|
|
@@ -30,6 +32,9 @@ import type {
|
|
|
30
32
|
Definition,
|
|
31
33
|
EngineOpts,
|
|
32
34
|
ExtractorOutput,
|
|
35
|
+
NativeAddon,
|
|
36
|
+
NativeFunctionCfgResult,
|
|
37
|
+
NativeFunctionComplexityResult,
|
|
33
38
|
TreeSitterNode,
|
|
34
39
|
Visitor,
|
|
35
40
|
WalkOptions,
|
|
@@ -95,6 +100,173 @@ async function getParserModule(): Promise<typeof import('../domain/parser.js')>
|
|
|
95
100
|
return _parserModule;
|
|
96
101
|
}
|
|
97
102
|
|
|
103
|
+
// ─── Native standalone analysis ─────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Try native Rust analysis for files missing complexity/CFG/dataflow data.
|
|
107
|
+
* Reads source from disk, calls the native standalone functions, and stores
|
|
108
|
+
* results directly on definitions/symbols. Returns the set of files that
|
|
109
|
+
* were fully handled (no remaining gaps except possibly AST store).
|
|
110
|
+
*/
|
|
111
|
+
function runNativeAnalysis(
|
|
112
|
+
native: NativeAddon,
|
|
113
|
+
fileSymbols: Map<string, ExtractorOutput>,
|
|
114
|
+
rootDir: string,
|
|
115
|
+
opts: AnalysisOpts,
|
|
116
|
+
extToLang: Map<string, string>,
|
|
117
|
+
): void {
|
|
118
|
+
const doComplexity = opts.complexity !== false;
|
|
119
|
+
const doCfg = opts.cfg !== false;
|
|
120
|
+
const doDataflow = opts.dataflow !== false;
|
|
121
|
+
|
|
122
|
+
for (const [relPath, symbols] of fileSymbols) {
|
|
123
|
+
if (symbols._tree) continue; // already has WASM tree, skip native
|
|
124
|
+
const ext = path.extname(relPath).toLowerCase();
|
|
125
|
+
const langId = symbols._langId || extToLang.get(ext);
|
|
126
|
+
if (!langId) continue;
|
|
127
|
+
|
|
128
|
+
const defs = symbols.definitions || [];
|
|
129
|
+
|
|
130
|
+
const needsComplexity =
|
|
131
|
+
doComplexity &&
|
|
132
|
+
COMPLEXITY_EXTENSIONS.has(ext) &&
|
|
133
|
+
defs.some((d) => hasFuncBody(d) && !d.complexity);
|
|
134
|
+
const needsCfg =
|
|
135
|
+
doCfg &&
|
|
136
|
+
CFG_EXTENSIONS.has(ext) &&
|
|
137
|
+
defs.some((d) => hasFuncBody(d) && d.cfg !== null && !Array.isArray(d.cfg?.blocks));
|
|
138
|
+
const needsDataflow = doDataflow && !symbols.dataflow && DATAFLOW_EXTENSIONS.has(ext);
|
|
139
|
+
|
|
140
|
+
if (!needsComplexity && !needsCfg && !needsDataflow) continue;
|
|
141
|
+
|
|
142
|
+
// Read source from disk
|
|
143
|
+
const absPath = path.join(rootDir, relPath);
|
|
144
|
+
let source: string;
|
|
145
|
+
try {
|
|
146
|
+
source = fs.readFileSync(absPath, 'utf-8');
|
|
147
|
+
} catch {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Complexity
|
|
152
|
+
if (needsComplexity && native.analyzeComplexity) {
|
|
153
|
+
try {
|
|
154
|
+
const results = native.analyzeComplexity(source, absPath);
|
|
155
|
+
storeNativeComplexityResults(results, defs);
|
|
156
|
+
} catch (err: unknown) {
|
|
157
|
+
debug(`native analyzeComplexity failed for ${relPath}: ${(err as Error).message}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// CFG
|
|
162
|
+
if (needsCfg && native.buildCfgAnalysis) {
|
|
163
|
+
try {
|
|
164
|
+
const results = native.buildCfgAnalysis(source, absPath);
|
|
165
|
+
storeNativeCfgResults(results, defs);
|
|
166
|
+
} catch (err: unknown) {
|
|
167
|
+
debug(`native buildCfgAnalysis failed for ${relPath}: ${(err as Error).message}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Dataflow
|
|
172
|
+
if (needsDataflow && native.extractDataflowAnalysis) {
|
|
173
|
+
try {
|
|
174
|
+
const result = native.extractDataflowAnalysis(source, absPath);
|
|
175
|
+
if (result) symbols.dataflow = result;
|
|
176
|
+
} catch (err: unknown) {
|
|
177
|
+
debug(`native extractDataflowAnalysis failed for ${relPath}: ${(err as Error).message}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Store native complexity results on definitions, matched by line number. */
|
|
184
|
+
function storeNativeComplexityResults(
|
|
185
|
+
results: NativeFunctionComplexityResult[],
|
|
186
|
+
defs: Definition[],
|
|
187
|
+
): void {
|
|
188
|
+
const byLine = new Map<number, NativeFunctionComplexityResult[]>();
|
|
189
|
+
for (const r of results) {
|
|
190
|
+
if (!byLine.has(r.line)) byLine.set(r.line, []);
|
|
191
|
+
byLine.get(r.line)!.push(r);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
for (const def of defs) {
|
|
195
|
+
if ((def.kind === 'function' || def.kind === 'method') && def.line && !def.complexity) {
|
|
196
|
+
const candidates = byLine.get(def.line);
|
|
197
|
+
if (!candidates) continue;
|
|
198
|
+
const match =
|
|
199
|
+
candidates.length === 1
|
|
200
|
+
? candidates[0]
|
|
201
|
+
: (candidates.find((r) => r.name === def.name) ?? candidates[0]);
|
|
202
|
+
if (!match) continue;
|
|
203
|
+
const { complexity: c } = match;
|
|
204
|
+
def.complexity = {
|
|
205
|
+
cognitive: c.cognitive,
|
|
206
|
+
cyclomatic: c.cyclomatic,
|
|
207
|
+
maxNesting: c.maxNesting,
|
|
208
|
+
halstead: c.halstead
|
|
209
|
+
? {
|
|
210
|
+
volume: c.halstead.volume,
|
|
211
|
+
difficulty: c.halstead.difficulty,
|
|
212
|
+
effort: c.halstead.effort,
|
|
213
|
+
bugs: c.halstead.bugs,
|
|
214
|
+
}
|
|
215
|
+
: undefined,
|
|
216
|
+
loc: c.loc
|
|
217
|
+
? { loc: c.loc.loc, sloc: c.loc.sloc, commentLines: c.loc.commentLines }
|
|
218
|
+
: undefined,
|
|
219
|
+
maintainabilityIndex: c.maintainabilityIndex ?? undefined,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Store native CFG results on definitions, matched by line number. */
|
|
226
|
+
function storeNativeCfgResults(results: NativeFunctionCfgResult[], defs: Definition[]): void {
|
|
227
|
+
const byLine = new Map<number, NativeFunctionCfgResult[]>();
|
|
228
|
+
for (const r of results) {
|
|
229
|
+
if (!byLine.has(r.line)) byLine.set(r.line, []);
|
|
230
|
+
byLine.get(r.line)!.push(r);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
for (const def of defs) {
|
|
234
|
+
if (
|
|
235
|
+
(def.kind === 'function' || def.kind === 'method') &&
|
|
236
|
+
def.line &&
|
|
237
|
+
def.cfg !== null &&
|
|
238
|
+
!def.cfg?.blocks?.length
|
|
239
|
+
) {
|
|
240
|
+
const candidates = byLine.get(def.line);
|
|
241
|
+
if (!candidates) continue;
|
|
242
|
+
const match =
|
|
243
|
+
candidates.length === 1
|
|
244
|
+
? candidates[0]
|
|
245
|
+
: (candidates.find((r) => r.name === def.name) ?? candidates[0]);
|
|
246
|
+
if (!match) continue;
|
|
247
|
+
def.cfg = match.cfg;
|
|
248
|
+
|
|
249
|
+
// Override complexity cyclomatic with CFG-derived value
|
|
250
|
+
const { edges, blocks } = match.cfg;
|
|
251
|
+
if (def.complexity && edges && blocks) {
|
|
252
|
+
const cfgCyclomatic = edges.length - blocks.length + 2;
|
|
253
|
+
if (cfgCyclomatic > 0) {
|
|
254
|
+
def.complexity.cyclomatic = cfgCyclomatic;
|
|
255
|
+
const { loc, halstead } = def.complexity;
|
|
256
|
+
const volume = halstead ? halstead.volume : 0;
|
|
257
|
+
const commentRatio = loc && loc.loc > 0 ? loc.commentLines / loc.loc : 0;
|
|
258
|
+
def.complexity.maintainabilityIndex = computeMaintainabilityIndex(
|
|
259
|
+
volume,
|
|
260
|
+
cfgCyclomatic,
|
|
261
|
+
loc?.sloc ?? 0,
|
|
262
|
+
commentRatio,
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
98
270
|
// ─── WASM pre-parse ─────────────────────────────────────────────────────
|
|
99
271
|
|
|
100
272
|
async function ensureWasmTreesIfNeeded(
|
|
@@ -429,7 +601,17 @@ export async function runAnalyses(
|
|
|
429
601
|
|
|
430
602
|
const extToLang = buildExtToLangMap();
|
|
431
603
|
|
|
432
|
-
//
|
|
604
|
+
// Native analysis pass: try Rust standalone functions before WASM fallback.
|
|
605
|
+
// This fills in complexity/CFG/dataflow for files that the native parse pipeline
|
|
606
|
+
// missed, avoiding the need to parse with WASM + run JS visitors.
|
|
607
|
+
const native = loadNative();
|
|
608
|
+
if (native?.analyzeComplexity ?? native?.buildCfgAnalysis ?? native?.extractDataflowAnalysis) {
|
|
609
|
+
const t0native = performance.now();
|
|
610
|
+
runNativeAnalysis(native, fileSymbols, rootDir, opts, extToLang);
|
|
611
|
+
debug(`native standalone analysis: ${(performance.now() - t0native).toFixed(1)}ms`);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// WASM pre-parse for files that still need it (AST store, or native gaps)
|
|
433
615
|
await ensureWasmTreesIfNeeded(fileSymbols, opts, rootDir);
|
|
434
616
|
|
|
435
617
|
// Unified pre-walk: run all applicable visitors in a single DFS per file
|
|
@@ -237,7 +237,6 @@ export const dataflow: DataflowRulesConfig = makeDataflowRules({
|
|
|
237
237
|
// ─── AST Node Types ───────────────────────────────────────────────────────
|
|
238
238
|
|
|
239
239
|
export const astTypes: Record<string, string> | null = {
|
|
240
|
-
call_expression: 'call',
|
|
241
240
|
new_expression: 'new',
|
|
242
241
|
throw_statement: 'throw',
|
|
243
242
|
await_expression: 'await',
|
|
@@ -44,22 +44,6 @@ function extractExpressionText(node: TreeSitterNode): string | null {
|
|
|
44
44
|
return truncate(node.text);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
function extractCallName(node: TreeSitterNode): string {
|
|
48
|
-
for (const field of ['function', 'method', 'name']) {
|
|
49
|
-
const fn = node.childForFieldName(field);
|
|
50
|
-
if (fn) return fn.text;
|
|
51
|
-
}
|
|
52
|
-
return node.text?.split('(')[0] || '?';
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** Extract receiver for call expressions (e.g. "obj" in "obj.method()"). */
|
|
56
|
-
function extractCallReceiver(node: TreeSitterNode): string | null {
|
|
57
|
-
const fn = node.childForFieldName('function');
|
|
58
|
-
if (!fn || fn.type !== 'member_expression') return null;
|
|
59
|
-
const obj = fn.childForFieldName('object');
|
|
60
|
-
return obj ? obj.text : null;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
47
|
function extractName(kind: string, node: TreeSitterNode): string | null {
|
|
64
48
|
if (kind === 'throw') {
|
|
65
49
|
for (let i = 0; i < node.childCount; i++) {
|
|
@@ -118,64 +102,14 @@ export function createAstStoreVisitor(
|
|
|
118
102
|
return nodeIdMap.get(`${parentDef.name}|${parentDef.kind}|${parentDef.line}`) || null;
|
|
119
103
|
}
|
|
120
104
|
|
|
121
|
-
/** Recursively walk a subtree collecting AST nodes — used for arguments-only traversal. */
|
|
122
|
-
function walkSubtree(node: TreeSitterNode | null): void {
|
|
123
|
-
if (!node) return;
|
|
124
|
-
if (matched.has(node.id)) return;
|
|
125
|
-
|
|
126
|
-
const kind = astTypeMap[node.type];
|
|
127
|
-
if (kind === 'call') {
|
|
128
|
-
// Capture this call and recurse only into its arguments
|
|
129
|
-
collectNode(node, kind);
|
|
130
|
-
walkCallArguments(node);
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
if (kind) {
|
|
134
|
-
collectNode(node, kind);
|
|
135
|
-
if (kind !== 'string' && kind !== 'regex') return; // skipChildren for non-leaf kinds
|
|
136
|
-
}
|
|
137
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
138
|
-
walkSubtree(node.child(i));
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Recurse into only the arguments of a call node — mirrors the native engine's
|
|
144
|
-
* strategy that prevents double-counting nested calls in the function field
|
|
145
|
-
* (e.g. chained calls like `a().b()`).
|
|
146
|
-
*/
|
|
147
|
-
function walkCallArguments(callNode: TreeSitterNode): void {
|
|
148
|
-
// Try field-based lookup first, fall back to kind-based matching
|
|
149
|
-
const argsNode =
|
|
150
|
-
callNode.childForFieldName('arguments') ??
|
|
151
|
-
findChildByKind(callNode, ['arguments', 'argument_list', 'method_arguments']);
|
|
152
|
-
if (!argsNode) return;
|
|
153
|
-
for (let i = 0; i < argsNode.childCount; i++) {
|
|
154
|
-
walkSubtree(argsNode.child(i));
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function findChildByKind(node: TreeSitterNode, kinds: string[]): TreeSitterNode | null {
|
|
159
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
160
|
-
const child = node.child(i);
|
|
161
|
-
if (child && kinds.includes(child.type)) return child;
|
|
162
|
-
}
|
|
163
|
-
return null;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
105
|
function collectNode(node: TreeSitterNode, kind: string): void {
|
|
167
106
|
if (matched.has(node.id)) return;
|
|
168
107
|
|
|
169
108
|
const line = node.startPosition.row + 1;
|
|
170
109
|
let name: string | null | undefined;
|
|
171
110
|
let text: string | null = null;
|
|
172
|
-
let receiver: string | null = null;
|
|
173
111
|
|
|
174
|
-
if (kind === '
|
|
175
|
-
name = extractCallName(node);
|
|
176
|
-
text = truncate(node.text);
|
|
177
|
-
receiver = extractCallReceiver(node);
|
|
178
|
-
} else if (kind === 'new') {
|
|
112
|
+
if (kind === 'new') {
|
|
179
113
|
name = extractNewName(node);
|
|
180
114
|
text = truncate(node.text);
|
|
181
115
|
} else if (kind === 'throw') {
|
|
@@ -200,7 +134,7 @@ export function createAstStoreVisitor(
|
|
|
200
134
|
kind,
|
|
201
135
|
name,
|
|
202
136
|
text,
|
|
203
|
-
receiver,
|
|
137
|
+
receiver: null,
|
|
204
138
|
parentNodeId: resolveParentNodeId(line),
|
|
205
139
|
});
|
|
206
140
|
|
|
@@ -221,13 +155,6 @@ export function createAstStoreVisitor(
|
|
|
221
155
|
|
|
222
156
|
collectNode(node, kind);
|
|
223
157
|
|
|
224
|
-
if (kind === 'call') {
|
|
225
|
-
// Mirror native: skip full subtree, recurse only into arguments.
|
|
226
|
-
// Prevents double-counting chained calls like service.getUser().getName().
|
|
227
|
-
walkCallArguments(node);
|
|
228
|
-
return { skipChildren: true };
|
|
229
|
-
}
|
|
230
|
-
|
|
231
158
|
if (kind !== 'string' && kind !== 'regex') {
|
|
232
159
|
return { skipChildren: true };
|
|
233
160
|
}
|
package/src/cli/commands/ast.ts
CHANGED
|
@@ -4,10 +4,10 @@ import type { CommandDefinition } from '../types.js';
|
|
|
4
4
|
|
|
5
5
|
export const command: CommandDefinition = {
|
|
6
6
|
name: 'ast [pattern]',
|
|
7
|
-
description: 'Search stored AST nodes (
|
|
7
|
+
description: 'Search stored AST nodes (new, string, regex, throw, await) by pattern',
|
|
8
8
|
queryOpts: true,
|
|
9
9
|
options: [
|
|
10
|
-
['-k, --kind <kind>', 'Filter by AST node kind (
|
|
10
|
+
['-k, --kind <kind>', 'Filter by AST node kind (new, string, regex, throw, await)'],
|
|
11
11
|
['-f, --file <path>', 'Scope to file (partial match, repeatable)', collectFile],
|
|
12
12
|
],
|
|
13
13
|
async execute([pattern], opts, ctx) {
|
|
@@ -35,18 +35,26 @@ function initializeEngine(ctx: PipelineContext): void {
|
|
|
35
35
|
dataflow: ctx.opts.dataflow !== false,
|
|
36
36
|
ast: ctx.opts.ast !== false,
|
|
37
37
|
nativeDb: ctx.nativeDb,
|
|
38
|
-
// WAL checkpoint callbacks for dual-connection WAL guard (#696).
|
|
38
|
+
// WAL checkpoint callbacks for dual-connection WAL guard (#696, #715).
|
|
39
39
|
// Feature modules (ast, cfg, complexity, dataflow) receive `db` as a
|
|
40
40
|
// parameter and cannot tolerate close/reopen (stale reference). Instead,
|
|
41
|
-
// checkpoint the WAL so native writes start with a clean slate.
|
|
42
|
-
//
|
|
43
|
-
//
|
|
41
|
+
// checkpoint the WAL so native writes start with a clean slate.
|
|
42
|
+
// After native writes, resumeJsDb checkpoints through rusqlite so
|
|
43
|
+
// better-sqlite3 never reads WAL frames from a different SQLite library.
|
|
44
44
|
suspendJsDb: ctx.nativeDb
|
|
45
45
|
? () => {
|
|
46
46
|
ctx.db.pragma('wal_checkpoint(TRUNCATE)');
|
|
47
47
|
}
|
|
48
48
|
: undefined,
|
|
49
|
-
resumeJsDb: ctx.nativeDb
|
|
49
|
+
resumeJsDb: ctx.nativeDb
|
|
50
|
+
? () => {
|
|
51
|
+
try {
|
|
52
|
+
ctx.nativeDb?.exec('PRAGMA wal_checkpoint(TRUNCATE)');
|
|
53
|
+
} catch {
|
|
54
|
+
/* ignore — nativeDb may already be closed */
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
: undefined,
|
|
50
58
|
};
|
|
51
59
|
const { name: engineName, version: engineVersion } = getActiveEngine(ctx.engineOpts);
|
|
52
60
|
ctx.engineName = engineName as 'native' | 'wasm';
|
|
@@ -120,8 +128,16 @@ function setupPipeline(ctx: PipelineContext): void {
|
|
|
120
128
|
try {
|
|
121
129
|
ctx.nativeDb = native.NativeDatabase.openReadWrite(ctx.dbPath);
|
|
122
130
|
ctx.nativeDb.initSchema();
|
|
131
|
+
// Checkpoint WAL through rusqlite so better-sqlite3 sees a clean DB
|
|
132
|
+
// with no cross-library WAL frames (#715, #717).
|
|
133
|
+
ctx.nativeDb.exec('PRAGMA wal_checkpoint(TRUNCATE)');
|
|
123
134
|
} catch (err) {
|
|
124
|
-
warn(`NativeDatabase
|
|
135
|
+
warn(`NativeDatabase setup failed, falling back to JS: ${(err as Error).message}`);
|
|
136
|
+
try {
|
|
137
|
+
ctx.nativeDb?.close();
|
|
138
|
+
} catch {
|
|
139
|
+
/* ignore close errors */
|
|
140
|
+
}
|
|
125
141
|
ctx.nativeDb = undefined;
|
|
126
142
|
}
|
|
127
143
|
// Always run JS initSchema so better-sqlite3 sees the schema —
|
|
@@ -179,6 +195,15 @@ async function runPipelineStages(ctx: PipelineContext): Promise<void> {
|
|
|
179
195
|
// that use suspendJsDb/resumeJsDb WAL checkpoint pattern (#696).
|
|
180
196
|
const hadNativeDb = !!ctx.nativeDb;
|
|
181
197
|
if (ctx.db && ctx.nativeDb) {
|
|
198
|
+
// Checkpoint WAL through rusqlite before closing so better-sqlite3 never
|
|
199
|
+
// needs to apply WAL frames written by a different SQLite library (#715, #717).
|
|
200
|
+
// Separate try/catch blocks ensure close() always runs even if checkpoint throws,
|
|
201
|
+
// preventing a live rusqlite connection from lingering until GC.
|
|
202
|
+
try {
|
|
203
|
+
ctx.nativeDb.exec('PRAGMA wal_checkpoint(TRUNCATE)');
|
|
204
|
+
} catch {
|
|
205
|
+
/* ignore checkpoint errors */
|
|
206
|
+
}
|
|
182
207
|
try {
|
|
183
208
|
ctx.nativeDb.close();
|
|
184
209
|
} catch {
|
|
@@ -198,7 +223,51 @@ async function runPipelineStages(ctx: PipelineContext): Promise<void> {
|
|
|
198
223
|
if (ctx.earlyExit) return;
|
|
199
224
|
|
|
200
225
|
await parseFiles(ctx);
|
|
226
|
+
|
|
227
|
+
// Temporarily reopen nativeDb for insertNodes — it uses the WAL checkpoint
|
|
228
|
+
// guard internally (same pattern as feature modules). Closed again before
|
|
229
|
+
// resolveImports/buildEdges which don't yet have the guard (#709).
|
|
230
|
+
if (hadNativeDb && ctx.engineName === 'native') {
|
|
231
|
+
const native = loadNative();
|
|
232
|
+
if (native?.NativeDatabase) {
|
|
233
|
+
try {
|
|
234
|
+
ctx.nativeDb = native.NativeDatabase.openReadWrite(ctx.dbPath);
|
|
235
|
+
} catch {
|
|
236
|
+
ctx.nativeDb = undefined;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
201
241
|
await insertNodes(ctx);
|
|
242
|
+
|
|
243
|
+
// Close nativeDb after insertNodes — remaining pipeline stages use JS paths.
|
|
244
|
+
if (ctx.nativeDb && ctx.db) {
|
|
245
|
+
// Checkpoint WAL through rusqlite before closing so better-sqlite3 never
|
|
246
|
+
// needs to apply WAL frames written by a different SQLite library (#715, #717).
|
|
247
|
+
try {
|
|
248
|
+
ctx.nativeDb.exec('PRAGMA wal_checkpoint(TRUNCATE)');
|
|
249
|
+
} catch {
|
|
250
|
+
/* ignore checkpoint errors */
|
|
251
|
+
}
|
|
252
|
+
try {
|
|
253
|
+
ctx.nativeDb.close();
|
|
254
|
+
} catch {
|
|
255
|
+
/* ignore close errors */
|
|
256
|
+
}
|
|
257
|
+
ctx.nativeDb = undefined;
|
|
258
|
+
// Reopen better-sqlite3 connection to get a fresh page cache.
|
|
259
|
+
// After rusqlite truncates the WAL, better-sqlite3's internal WAL index
|
|
260
|
+
// (shared-memory mapping) may reference frames that no longer exist,
|
|
261
|
+
// causing SQLITE_CORRUPT on the next read. Closing and reopening
|
|
262
|
+
// forces a clean slate — the only reliable cross-library handoff (#715, #736).
|
|
263
|
+
try {
|
|
264
|
+
ctx.db.close();
|
|
265
|
+
} catch {
|
|
266
|
+
/* ignore close errors */
|
|
267
|
+
}
|
|
268
|
+
ctx.db = openDb(ctx.dbPath);
|
|
269
|
+
}
|
|
270
|
+
|
|
202
271
|
await resolveImports(ctx);
|
|
203
272
|
await buildEdges(ctx);
|
|
204
273
|
await buildStructure(ctx);
|
|
@@ -227,6 +296,14 @@ async function runPipelineStages(ctx: PipelineContext): Promise<void> {
|
|
|
227
296
|
// Close nativeDb after analyses — finalize uses JS paths for setBuildMeta
|
|
228
297
|
// and closeDbPair handles cleanup. Avoids dual-connection during finalize.
|
|
229
298
|
if (ctx.nativeDb) {
|
|
299
|
+
// Checkpoint WAL through rusqlite before closing so better-sqlite3 never
|
|
300
|
+
// needs to apply WAL frames written by a different SQLite library (#715, #717).
|
|
301
|
+
// Separate try/catch blocks ensure close() always runs even if checkpoint throws.
|
|
302
|
+
try {
|
|
303
|
+
ctx.nativeDb.exec('PRAGMA wal_checkpoint(TRUNCATE)');
|
|
304
|
+
} catch {
|
|
305
|
+
/* ignore checkpoint errors */
|
|
306
|
+
}
|
|
230
307
|
try {
|
|
231
308
|
ctx.nativeDb.close();
|
|
232
309
|
} catch {
|
|
@@ -256,6 +333,65 @@ export async function buildGraph(
|
|
|
256
333
|
|
|
257
334
|
try {
|
|
258
335
|
setupPipeline(ctx);
|
|
336
|
+
|
|
337
|
+
// ── Rust orchestrator fast path (#695) ────────────────────────────
|
|
338
|
+
// When available, run the entire build pipeline in Rust with zero
|
|
339
|
+
// napi crossings (eliminates WAL dual-connection dance). Falls back
|
|
340
|
+
// to the JS pipeline on failure or when native is unavailable.
|
|
341
|
+
const forceJs = process.env.CODEGRAPH_FORCE_JS_PIPELINE === '1';
|
|
342
|
+
if (!forceJs && ctx.nativeDb?.buildGraph) {
|
|
343
|
+
try {
|
|
344
|
+
const resultJson = ctx.nativeDb.buildGraph(
|
|
345
|
+
ctx.rootDir,
|
|
346
|
+
JSON.stringify(ctx.config),
|
|
347
|
+
JSON.stringify(ctx.aliases),
|
|
348
|
+
JSON.stringify(opts),
|
|
349
|
+
);
|
|
350
|
+
const result = JSON.parse(resultJson) as {
|
|
351
|
+
phases: Record<string, number>;
|
|
352
|
+
earlyExit?: boolean;
|
|
353
|
+
nodeCount?: number;
|
|
354
|
+
edgeCount?: number;
|
|
355
|
+
fileCount?: number;
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
if (result.earlyExit) {
|
|
359
|
+
closeDbPair({ db: ctx.db, nativeDb: ctx.nativeDb });
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Map Rust timing fields to the JS BuildResult format.
|
|
364
|
+
// Rust handles collect+detect+parse+insert+resolve+edges+structure+roles.
|
|
365
|
+
// AST/complexity/CFG/dataflow analyses are not yet ported to Rust.
|
|
366
|
+
const p = result.phases;
|
|
367
|
+
closeDbPair({ db: ctx.db, nativeDb: ctx.nativeDb });
|
|
368
|
+
info(
|
|
369
|
+
`Native build orchestrator completed: ${result.nodeCount ?? 0} nodes, ${result.edgeCount ?? 0} edges, ${result.fileCount ?? 0} files`,
|
|
370
|
+
);
|
|
371
|
+
return {
|
|
372
|
+
phases: {
|
|
373
|
+
setupMs: +((p.setupMs ?? 0) + (p.collectMs ?? 0) + (p.detectMs ?? 0)).toFixed(1),
|
|
374
|
+
parseMs: +(p.parseMs ?? 0).toFixed(1),
|
|
375
|
+
insertMs: +(p.insertMs ?? 0).toFixed(1),
|
|
376
|
+
resolveMs: +(p.resolveMs ?? 0).toFixed(1),
|
|
377
|
+
edgesMs: +(p.edgesMs ?? 0).toFixed(1),
|
|
378
|
+
structureMs: +(p.structureMs ?? 0).toFixed(1),
|
|
379
|
+
rolesMs: +(p.rolesMs ?? 0).toFixed(1),
|
|
380
|
+
astMs: 0,
|
|
381
|
+
complexityMs: 0,
|
|
382
|
+
cfgMs: 0,
|
|
383
|
+
dataflowMs: 0,
|
|
384
|
+
finalizeMs: +(p.finalizeMs ?? 0).toFixed(1),
|
|
385
|
+
},
|
|
386
|
+
};
|
|
387
|
+
} catch (err) {
|
|
388
|
+
warn(
|
|
389
|
+
`Native build orchestrator failed, falling back to JS pipeline: ${(err as Error).message}`,
|
|
390
|
+
);
|
|
391
|
+
// Fall through to JS pipeline
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
259
395
|
await runPipelineStages(ctx);
|
|
260
396
|
} catch (err) {
|
|
261
397
|
if (!ctx.earlyExit && ctx.db) {
|