@optave/codegraph 3.8.1 → 3.9.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 +12 -7
- package/dist/ast-analysis/engine.d.ts.map +1 -1
- package/dist/ast-analysis/engine.js +121 -48
- package/dist/ast-analysis/engine.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 +15 -18
- package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
- package/dist/ast-analysis/visitors/complexity-visitor.d.ts.map +1 -1
- package/dist/ast-analysis/visitors/complexity-visitor.js +50 -1
- package/dist/ast-analysis/visitors/complexity-visitor.js.map +1 -1
- package/dist/cli/commands/branch-compare.d.ts.map +1 -1
- package/dist/cli/commands/branch-compare.js +4 -0
- package/dist/cli/commands/branch-compare.js.map +1 -1
- package/dist/cli/commands/diff-impact.d.ts.map +1 -1
- package/dist/cli/commands/diff-impact.js +2 -1
- package/dist/cli/commands/diff-impact.js.map +1 -1
- package/dist/cli/commands/info.d.ts.map +1 -1
- package/dist/cli/commands/info.js +3 -2
- package/dist/cli/commands/info.js.map +1 -1
- package/dist/db/connection.d.ts +1 -0
- package/dist/db/connection.d.ts.map +1 -1
- package/dist/db/connection.js +22 -4
- package/dist/db/connection.js.map +1 -1
- package/dist/db/repository/base.d.ts +41 -0
- package/dist/db/repository/base.d.ts.map +1 -1
- package/dist/db/repository/base.js +22 -0
- package/dist/db/repository/base.js.map +1 -1
- package/dist/db/repository/index.d.ts +1 -0
- package/dist/db/repository/index.d.ts.map +1 -1
- package/dist/db/repository/index.js.map +1 -1
- package/dist/db/repository/native-repository.d.ts +8 -1
- package/dist/db/repository/native-repository.d.ts.map +1 -1
- package/dist/db/repository/native-repository.js +69 -1
- package/dist/db/repository/native-repository.js.map +1 -1
- package/dist/db/repository/sqlite-repository.d.ts +1 -0
- package/dist/db/repository/sqlite-repository.d.ts.map +1 -1
- package/dist/db/repository/sqlite-repository.js +25 -0
- package/dist/db/repository/sqlite-repository.js.map +1 -1
- package/dist/domain/analysis/dependencies.d.ts +1 -28
- package/dist/domain/analysis/dependencies.d.ts.map +1 -1
- package/dist/domain/analysis/dependencies.js +24 -8
- package/dist/domain/analysis/dependencies.js.map +1 -1
- package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
- package/dist/domain/graph/builder/incremental.js +18 -0
- package/dist/domain/graph/builder/incremental.js.map +1 -1
- package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
- package/dist/domain/graph/builder/pipeline.js +298 -206
- 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 +56 -3
- package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
- package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/resolve-imports.js +19 -23
- package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
- package/dist/domain/graph/watcher.d.ts.map +1 -1
- package/dist/domain/graph/watcher.js +99 -95
- package/dist/domain/graph/watcher.js.map +1 -1
- package/dist/domain/parser.d.ts +4 -0
- package/dist/domain/parser.d.ts.map +1 -1
- package/dist/domain/parser.js +130 -61
- package/dist/domain/parser.js.map +1 -1
- package/dist/domain/search/models.d.ts.map +1 -1
- package/dist/domain/search/models.js +7 -5
- package/dist/domain/search/models.js.map +1 -1
- package/dist/extractors/go.js +53 -35
- package/dist/extractors/go.js.map +1 -1
- package/dist/extractors/javascript.js +85 -36
- package/dist/extractors/javascript.js.map +1 -1
- package/dist/features/complexity.d.ts.map +1 -1
- package/dist/features/complexity.js +78 -58
- package/dist/features/complexity.js.map +1 -1
- package/dist/features/dataflow.d.ts.map +1 -1
- package/dist/features/dataflow.js +109 -118
- package/dist/features/dataflow.js.map +1 -1
- package/dist/features/structure.d.ts.map +1 -1
- package/dist/features/structure.js +147 -97
- package/dist/features/structure.js.map +1 -1
- package/dist/graph/algorithms/louvain.d.ts.map +1 -1
- package/dist/graph/algorithms/louvain.js +4 -2
- package/dist/graph/algorithms/louvain.js.map +1 -1
- package/dist/graph/classifiers/roles.d.ts +2 -0
- package/dist/graph/classifiers/roles.d.ts.map +1 -1
- package/dist/graph/classifiers/roles.js +13 -5
- package/dist/graph/classifiers/roles.js.map +1 -1
- package/dist/presentation/communities.d.ts.map +1 -1
- package/dist/presentation/communities.js +38 -34
- package/dist/presentation/communities.js.map +1 -1
- package/dist/presentation/manifesto.d.ts.map +1 -1
- package/dist/presentation/manifesto.js +31 -33
- package/dist/presentation/manifesto.js.map +1 -1
- package/dist/presentation/queries-cli/inspect.d.ts.map +1 -1
- package/dist/presentation/queries-cli/inspect.js +47 -46
- package/dist/presentation/queries-cli/inspect.js.map +1 -1
- package/dist/shared/file-utils.d.ts.map +1 -1
- package/dist/shared/file-utils.js +94 -72
- package/dist/shared/file-utils.js.map +1 -1
- package/dist/types.d.ts +83 -2
- package/dist/types.d.ts.map +1 -1
- package/grammars/tree-sitter-erlang.wasm +0 -0
- package/grammars/tree-sitter-gleam.wasm +0 -0
- package/package.json +9 -9
- package/src/ast-analysis/engine.ts +150 -55
- package/src/ast-analysis/visitors/ast-store-visitor.ts +19 -21
- package/src/ast-analysis/visitors/complexity-visitor.ts +55 -1
- package/src/cli/commands/branch-compare.ts +4 -0
- package/src/cli/commands/diff-impact.ts +2 -1
- package/src/cli/commands/info.ts +3 -2
- package/src/db/connection.ts +24 -5
- package/src/db/repository/base.ts +57 -0
- package/src/db/repository/index.ts +1 -0
- package/src/db/repository/native-repository.ts +92 -1
- package/src/db/repository/sqlite-repository.ts +26 -0
- package/src/domain/analysis/dependencies.ts +24 -6
- package/src/domain/graph/builder/incremental.ts +21 -0
- package/src/domain/graph/builder/pipeline.ts +396 -245
- package/src/domain/graph/builder/stages/build-edges.ts +53 -2
- package/src/domain/graph/builder/stages/resolve-imports.ts +20 -20
- package/src/domain/graph/watcher.ts +118 -98
- package/src/domain/parser.ts +131 -63
- package/src/domain/search/models.ts +11 -5
- package/src/extractors/go.ts +57 -32
- package/src/extractors/javascript.ts +88 -35
- package/src/features/complexity.ts +94 -58
- package/src/features/dataflow.ts +153 -132
- package/src/features/structure.ts +167 -95
- package/src/graph/algorithms/louvain.ts +5 -2
- package/src/graph/classifiers/roles.ts +14 -5
- package/src/presentation/communities.ts +44 -39
- package/src/presentation/manifesto.ts +35 -38
- package/src/presentation/queries-cli/inspect.ts +48 -46
- package/src/shared/file-utils.ts +116 -77
- package/src/types.ts +87 -1
|
@@ -215,25 +215,37 @@ function runNativeAnalysis(
|
|
|
215
215
|
}
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
+
/** Index native results by line number and match to a definition by name. */
|
|
219
|
+
function indexNativeByLine<T extends { line: number; name: string }>(
|
|
220
|
+
results: T[],
|
|
221
|
+
): Map<number, T[]> {
|
|
222
|
+
const byLine = new Map<number, T[]>();
|
|
223
|
+
for (const r of results) {
|
|
224
|
+
if (!byLine.has(r.line)) byLine.set(r.line, []);
|
|
225
|
+
byLine.get(r.line)!.push(r);
|
|
226
|
+
}
|
|
227
|
+
return byLine;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function matchNativeResult<T extends { name: string }>(
|
|
231
|
+
candidates: T[] | undefined,
|
|
232
|
+
defName: string,
|
|
233
|
+
): T | undefined {
|
|
234
|
+
if (!candidates) return undefined;
|
|
235
|
+
if (candidates.length === 1) return candidates[0];
|
|
236
|
+
return candidates.find((r) => r.name === defName) ?? candidates[0];
|
|
237
|
+
}
|
|
238
|
+
|
|
218
239
|
/** Store native complexity results on definitions, matched by line number. */
|
|
219
240
|
function storeNativeComplexityResults(
|
|
220
241
|
results: NativeFunctionComplexityResult[],
|
|
221
242
|
defs: Definition[],
|
|
222
243
|
): void {
|
|
223
|
-
const byLine =
|
|
224
|
-
for (const r of results) {
|
|
225
|
-
if (!byLine.has(r.line)) byLine.set(r.line, []);
|
|
226
|
-
byLine.get(r.line)!.push(r);
|
|
227
|
-
}
|
|
244
|
+
const byLine = indexNativeByLine(results);
|
|
228
245
|
|
|
229
246
|
for (const def of defs) {
|
|
230
247
|
if ((def.kind === 'function' || def.kind === 'method') && def.line && !def.complexity) {
|
|
231
|
-
const
|
|
232
|
-
if (!candidates) continue;
|
|
233
|
-
const match =
|
|
234
|
-
candidates.length === 1
|
|
235
|
-
? candidates[0]
|
|
236
|
-
: (candidates.find((r) => r.name === def.name) ?? candidates[0]);
|
|
248
|
+
const match = matchNativeResult(byLine.get(def.line), def.name);
|
|
237
249
|
if (!match) continue;
|
|
238
250
|
const { complexity: c } = match;
|
|
239
251
|
def.complexity = {
|
|
@@ -242,6 +254,12 @@ function storeNativeComplexityResults(
|
|
|
242
254
|
maxNesting: c.maxNesting,
|
|
243
255
|
halstead: c.halstead
|
|
244
256
|
? {
|
|
257
|
+
n1: c.halstead.n1,
|
|
258
|
+
n2: c.halstead.n2,
|
|
259
|
+
bigN1: c.halstead.bigN1,
|
|
260
|
+
bigN2: c.halstead.bigN2,
|
|
261
|
+
vocabulary: c.halstead.vocabulary,
|
|
262
|
+
length: c.halstead.length,
|
|
245
263
|
volume: c.halstead.volume,
|
|
246
264
|
difficulty: c.halstead.difficulty,
|
|
247
265
|
effort: c.halstead.effort,
|
|
@@ -278,11 +296,7 @@ function overrideCyclomaticFromCfg(def: Definition, cfgCyclomatic: number): void
|
|
|
278
296
|
|
|
279
297
|
/** Store native CFG results on definitions, matched by line number. */
|
|
280
298
|
function storeNativeCfgResults(results: NativeFunctionCfgResult[], defs: Definition[]): void {
|
|
281
|
-
const byLine =
|
|
282
|
-
for (const r of results) {
|
|
283
|
-
if (!byLine.has(r.line)) byLine.set(r.line, []);
|
|
284
|
-
byLine.get(r.line)!.push(r);
|
|
285
|
-
}
|
|
299
|
+
const byLine = indexNativeByLine(results);
|
|
286
300
|
|
|
287
301
|
for (const def of defs) {
|
|
288
302
|
if (
|
|
@@ -291,12 +305,7 @@ function storeNativeCfgResults(results: NativeFunctionCfgResult[], defs: Definit
|
|
|
291
305
|
def.cfg !== null &&
|
|
292
306
|
!def.cfg?.blocks?.length
|
|
293
307
|
) {
|
|
294
|
-
const
|
|
295
|
-
if (!candidates) continue;
|
|
296
|
-
const match =
|
|
297
|
-
candidates.length === 1
|
|
298
|
-
? candidates[0]
|
|
299
|
-
: (candidates.find((r) => r.name === def.name) ?? candidates[0]);
|
|
308
|
+
const match = matchNativeResult(byLine.get(def.line), def.name);
|
|
300
309
|
if (!match) continue;
|
|
301
310
|
def.cfg = match.cfg;
|
|
302
311
|
|
|
@@ -309,44 +318,99 @@ function storeNativeCfgResults(results: NativeFunctionCfgResult[], defs: Definit
|
|
|
309
318
|
}
|
|
310
319
|
}
|
|
311
320
|
|
|
321
|
+
// ─── CFG cyclomatic reconciliation ──────────────────────────────────────
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Apply CFG-derived cyclomatic override for definitions that already have both
|
|
325
|
+
* `complexity` and `cfg` with blocks/edges but whose cyclomatic was never
|
|
326
|
+
* overridden (e.g., native extractors provide both fields inline, so the
|
|
327
|
+
* normal override path in storeNativeCfgResults / storeCfgResults is skipped).
|
|
328
|
+
*/
|
|
329
|
+
/** Type guard for cfg objects with blocks and edges arrays. */
|
|
330
|
+
function hasCfgBlocksAndEdges(cfg: unknown): cfg is { blocks: unknown[]; edges: unknown[] } {
|
|
331
|
+
return (
|
|
332
|
+
cfg != null &&
|
|
333
|
+
typeof cfg === 'object' &&
|
|
334
|
+
Array.isArray((cfg as { blocks?: unknown }).blocks) &&
|
|
335
|
+
Array.isArray((cfg as { edges?: unknown }).edges)
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function reconcileCfgCyclomatic(fileSymbols: Map<string, ExtractorOutput>): void {
|
|
340
|
+
for (const [, symbols] of fileSymbols) {
|
|
341
|
+
const defs = symbols.definitions || [];
|
|
342
|
+
for (const def of defs) {
|
|
343
|
+
if (
|
|
344
|
+
(def.kind === 'function' || def.kind === 'method') &&
|
|
345
|
+
def.complexity &&
|
|
346
|
+
hasCfgBlocksAndEdges(def.cfg)
|
|
347
|
+
) {
|
|
348
|
+
const cfgCyclomatic = Math.max(def.cfg.edges.length - def.cfg.blocks.length + 2, 1);
|
|
349
|
+
if (cfgCyclomatic !== def.complexity.cyclomatic) {
|
|
350
|
+
overrideCyclomaticFromCfg(def, cfgCyclomatic);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
312
357
|
// ─── WASM pre-parse ─────────────────────────────────────────────────────
|
|
313
358
|
|
|
359
|
+
/** Check whether a single file needs a WASM tree for any enabled analysis pass. */
|
|
360
|
+
function fileNeedsWasmTree(
|
|
361
|
+
relPath: string,
|
|
362
|
+
symbols: ExtractorOutput,
|
|
363
|
+
flags: { doAst: boolean; doComplexity: boolean; doCfg: boolean; doDataflow: boolean },
|
|
364
|
+
): boolean {
|
|
365
|
+
if (symbols._tree) return false;
|
|
366
|
+
const ext = path.extname(relPath).toLowerCase();
|
|
367
|
+
const defs = symbols.definitions || [];
|
|
368
|
+
const lid = symbols._langId || '';
|
|
369
|
+
|
|
370
|
+
if (
|
|
371
|
+
flags.doAst &&
|
|
372
|
+
!Array.isArray(symbols.astNodes) &&
|
|
373
|
+
(WALK_EXTENSIONS.has(ext) || AST_TYPE_MAPS.has(lid))
|
|
374
|
+
)
|
|
375
|
+
return true;
|
|
376
|
+
if (
|
|
377
|
+
flags.doComplexity &&
|
|
378
|
+
(COMPLEXITY_EXTENSIONS.has(ext) || COMPLEXITY_RULES.has(lid)) &&
|
|
379
|
+
defs.some((d) => hasFuncBody(d) && !d.complexity)
|
|
380
|
+
)
|
|
381
|
+
return true;
|
|
382
|
+
if (
|
|
383
|
+
flags.doCfg &&
|
|
384
|
+
(CFG_EXTENSIONS.has(ext) || CFG_RULES.has(lid)) &&
|
|
385
|
+
defs.some((d) => hasFuncBody(d) && d.cfg !== null && !Array.isArray(d.cfg?.blocks))
|
|
386
|
+
)
|
|
387
|
+
return true;
|
|
388
|
+
if (
|
|
389
|
+
flags.doDataflow &&
|
|
390
|
+
!symbols.dataflow &&
|
|
391
|
+
(DATAFLOW_EXTENSIONS.has(ext) || DATAFLOW_RULES.has(lid))
|
|
392
|
+
)
|
|
393
|
+
return true;
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
|
|
314
397
|
async function ensureWasmTreesIfNeeded(
|
|
315
398
|
fileSymbols: Map<string, ExtractorOutput>,
|
|
316
399
|
opts: AnalysisOpts,
|
|
317
400
|
rootDir: string,
|
|
318
401
|
): Promise<void> {
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
402
|
+
const flags = {
|
|
403
|
+
doAst: opts.ast !== false,
|
|
404
|
+
doComplexity: opts.complexity !== false,
|
|
405
|
+
doCfg: opts.cfg !== false,
|
|
406
|
+
doDataflow: opts.dataflow !== false,
|
|
407
|
+
};
|
|
323
408
|
|
|
324
|
-
if (!doAst && !doComplexity && !doCfg && !doDataflow) return;
|
|
409
|
+
if (!flags.doAst && !flags.doComplexity && !flags.doCfg && !flags.doDataflow) return;
|
|
325
410
|
|
|
326
411
|
let needsWasmTrees = false;
|
|
327
412
|
for (const [relPath, symbols] of fileSymbols) {
|
|
328
|
-
if (symbols
|
|
329
|
-
const ext = path.extname(relPath).toLowerCase();
|
|
330
|
-
const defs = symbols.definitions || [];
|
|
331
|
-
|
|
332
|
-
// AST: need tree when native didn't provide non-call astNodes
|
|
333
|
-
const lid = symbols._langId || '';
|
|
334
|
-
const needsAst =
|
|
335
|
-
doAst &&
|
|
336
|
-
!Array.isArray(symbols.astNodes) &&
|
|
337
|
-
(WALK_EXTENSIONS.has(ext) || AST_TYPE_MAPS.has(lid));
|
|
338
|
-
const needsComplexity =
|
|
339
|
-
doComplexity &&
|
|
340
|
-
(COMPLEXITY_EXTENSIONS.has(ext) || COMPLEXITY_RULES.has(lid)) &&
|
|
341
|
-
defs.some((d) => hasFuncBody(d) && !d.complexity);
|
|
342
|
-
const needsCfg =
|
|
343
|
-
doCfg &&
|
|
344
|
-
(CFG_EXTENSIONS.has(ext) || CFG_RULES.has(lid)) &&
|
|
345
|
-
defs.some((d) => hasFuncBody(d) && d.cfg !== null && !Array.isArray(d.cfg?.blocks));
|
|
346
|
-
const needsDataflow =
|
|
347
|
-
doDataflow && !symbols.dataflow && (DATAFLOW_EXTENSIONS.has(ext) || DATAFLOW_RULES.has(lid));
|
|
348
|
-
|
|
349
|
-
if (needsAst || needsComplexity || needsCfg || needsDataflow) {
|
|
413
|
+
if (fileNeedsWasmTree(relPath, symbols, flags)) {
|
|
350
414
|
needsWasmTrees = true;
|
|
351
415
|
break;
|
|
352
416
|
}
|
|
@@ -565,7 +629,7 @@ async function delegateToBuildFunctions(
|
|
|
565
629
|
} catch (err: unknown) {
|
|
566
630
|
debug(`buildAstNodes failed: ${toErrorMessage(err)}`);
|
|
567
631
|
}
|
|
568
|
-
timing.astMs
|
|
632
|
+
timing.astMs += performance.now() - t0;
|
|
569
633
|
}
|
|
570
634
|
|
|
571
635
|
if (opts.complexity !== false) {
|
|
@@ -576,7 +640,7 @@ async function delegateToBuildFunctions(
|
|
|
576
640
|
} catch (err: unknown) {
|
|
577
641
|
debug(`buildComplexityMetrics failed: ${toErrorMessage(err)}`);
|
|
578
642
|
}
|
|
579
|
-
timing.complexityMs
|
|
643
|
+
timing.complexityMs += performance.now() - t0;
|
|
580
644
|
}
|
|
581
645
|
|
|
582
646
|
if (opts.cfg !== false) {
|
|
@@ -587,7 +651,7 @@ async function delegateToBuildFunctions(
|
|
|
587
651
|
} catch (err: unknown) {
|
|
588
652
|
debug(`buildCFGData failed: ${toErrorMessage(err)}`);
|
|
589
653
|
}
|
|
590
|
-
timing.cfgMs
|
|
654
|
+
timing.cfgMs += performance.now() - t0;
|
|
591
655
|
}
|
|
592
656
|
|
|
593
657
|
if (opts.dataflow !== false) {
|
|
@@ -598,7 +662,7 @@ async function delegateToBuildFunctions(
|
|
|
598
662
|
} catch (err: unknown) {
|
|
599
663
|
debug(`buildDataflowEdges failed: ${toErrorMessage(err)}`);
|
|
600
664
|
}
|
|
601
|
-
timing.dataflowMs
|
|
665
|
+
timing.dataflowMs += performance.now() - t0;
|
|
602
666
|
}
|
|
603
667
|
}
|
|
604
668
|
|
|
@@ -626,7 +690,7 @@ export async function runAnalyses(
|
|
|
626
690
|
// This fills in complexity/CFG/dataflow for files that the native parse pipeline
|
|
627
691
|
// missed, avoiding the need to parse with WASM + run JS visitors.
|
|
628
692
|
const native = loadNative();
|
|
629
|
-
if (native?.analyzeComplexity
|
|
693
|
+
if (native?.analyzeComplexity || native?.buildCfgAnalysis || native?.extractDataflowAnalysis) {
|
|
630
694
|
const t0native = performance.now();
|
|
631
695
|
runNativeAnalysis(native, fileSymbols, rootDir, opts, extToLang);
|
|
632
696
|
debug(`native standalone analysis: ${(performance.now() - t0native).toFixed(1)}ms`);
|
|
@@ -635,7 +699,10 @@ export async function runAnalyses(
|
|
|
635
699
|
// WASM pre-parse for files that still need it (AST store, or native gaps)
|
|
636
700
|
await ensureWasmTreesIfNeeded(fileSymbols, opts, rootDir);
|
|
637
701
|
|
|
638
|
-
// Unified pre-walk: run all applicable visitors in a single DFS per file
|
|
702
|
+
// Unified pre-walk: run all applicable visitors in a single DFS per file.
|
|
703
|
+
// Time each file's walk and distribute equally among active visitors
|
|
704
|
+
// so that phase timers (astMs, complexityMs, etc.) reflect real work — not
|
|
705
|
+
// just the DB-write tail in delegateToBuildFunctions.
|
|
639
706
|
const t0walk = performance.now();
|
|
640
707
|
|
|
641
708
|
for (const [relPath, symbols] of fileSymbols) {
|
|
@@ -650,7 +717,22 @@ export async function runAnalyses(
|
|
|
650
717
|
|
|
651
718
|
if (visitors.length === 0) continue;
|
|
652
719
|
|
|
720
|
+
const walkStart = performance.now();
|
|
653
721
|
const results = walkWithVisitors(symbols._tree.rootNode, visitors, langId, walkerOpts);
|
|
722
|
+
const walkMs = performance.now() - walkStart;
|
|
723
|
+
|
|
724
|
+
// Distribute walk time equally among active visitors
|
|
725
|
+
const activeCount = [astVisitor, complexityVisitor, cfgVisitor, dataflowVisitor].filter(
|
|
726
|
+
Boolean,
|
|
727
|
+
).length;
|
|
728
|
+
if (activeCount > 0) {
|
|
729
|
+
const share = walkMs / activeCount;
|
|
730
|
+
if (astVisitor) timing.astMs += share;
|
|
731
|
+
if (complexityVisitor) timing.complexityMs += share;
|
|
732
|
+
if (cfgVisitor) timing.cfgMs += share;
|
|
733
|
+
if (dataflowVisitor) timing.dataflowMs += share;
|
|
734
|
+
}
|
|
735
|
+
|
|
654
736
|
const defs = symbols.definitions || [];
|
|
655
737
|
|
|
656
738
|
if (astVisitor) {
|
|
@@ -663,8 +745,21 @@ export async function runAnalyses(
|
|
|
663
745
|
if (dataflowVisitor) symbols.dataflow = results.dataflow as DataflowResult;
|
|
664
746
|
}
|
|
665
747
|
|
|
748
|
+
// Total wall-clock time for the unified walk loop, including per-file
|
|
749
|
+
// setupVisitors overhead. Walk time is already distributed into per-phase
|
|
750
|
+
// timers above, so this field overlaps with (astMs + complexityMs + ...).
|
|
751
|
+
// It is kept as a diagnostic cross-check, not an additive bucket.
|
|
666
752
|
timing._unifiedWalkMs = performance.now() - t0walk;
|
|
667
753
|
|
|
754
|
+
// Reconcile: apply CFG-derived cyclomatic override for any definitions that have
|
|
755
|
+
// both precomputed complexity and CFG data but whose cyclomatic was never overridden.
|
|
756
|
+
// This closes a parity gap where native extractors provide both fields inline but
|
|
757
|
+
// the override step (storeNativeCfgResults / storeCfgResults) is skipped because
|
|
758
|
+
// detectNativeNeeds sees both as already present.
|
|
759
|
+
if (doComplexity && doCfg) {
|
|
760
|
+
reconcileCfgCyclomatic(fileSymbols);
|
|
761
|
+
}
|
|
762
|
+
|
|
668
763
|
// Delegate to buildXxx functions for DB writes + native fallback
|
|
669
764
|
await delegateToBuildFunctions(db, fileSymbols, rootDir, opts, engineOpts, timing);
|
|
670
765
|
|
|
@@ -102,27 +102,25 @@ export function createAstStoreVisitor(
|
|
|
102
102
|
return nodeIdMap.get(`${parentDef.name}|${parentDef.kind}|${parentDef.line}`) || null;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
return { name: undefined, text: null };
|
|
125
|
-
}
|
|
105
|
+
type NameTextResult = { name: string | null | undefined; text: string | null; skip?: boolean };
|
|
106
|
+
type KindHandler = (node: TreeSitterNode) => NameTextResult;
|
|
107
|
+
|
|
108
|
+
const kindHandlers: Record<string, KindHandler> = {
|
|
109
|
+
new: (node) => ({ name: extractNewName(node), text: truncate(node.text) }),
|
|
110
|
+
throw: (node) => ({ name: extractThrowName(node), text: extractExpressionText(node) }),
|
|
111
|
+
await: (node) => ({ name: extractAwaitName(node), text: extractExpressionText(node) }),
|
|
112
|
+
string: (node) => {
|
|
113
|
+
const content = node.text?.replace(/^['"`]|['"`]$/g, '') || '';
|
|
114
|
+
if (content.length < 2) return { name: null, text: null, skip: true };
|
|
115
|
+
return { name: truncate(content, 100), text: truncate(node.text) };
|
|
116
|
+
},
|
|
117
|
+
regex: (node) => ({ name: node.text || '?', text: truncate(node.text) }),
|
|
118
|
+
};
|
|
119
|
+
const defaultResult: NameTextResult = { name: undefined, text: null };
|
|
120
|
+
|
|
121
|
+
function resolveNameAndText(node: TreeSitterNode, kind: string): NameTextResult {
|
|
122
|
+
const handler = kindHandlers[kind];
|
|
123
|
+
return handler ? handler(node) : defaultResult;
|
|
126
124
|
}
|
|
127
125
|
|
|
128
126
|
function collectNode(node: TreeSitterNode, kind: string): void {
|
|
@@ -40,6 +40,32 @@ function classifyHalstead(node: TreeSitterNode, hRules: AnyRules, acc: Complexit
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Detect whether a branch node is an else-if that the DFS walk would NOT
|
|
45
|
+
* increment nesting for. Returns true for:
|
|
46
|
+
* - Pattern A (JS/C#/Rust): if_statement whose parent is else_clause
|
|
47
|
+
* - Pattern C (Go/Java): if_statement that is the alternative of parent if
|
|
48
|
+
*
|
|
49
|
+
* Pattern B (Python elif_clause) is not an issue because elif_clause is
|
|
50
|
+
* never in nestingNodes.
|
|
51
|
+
*/
|
|
52
|
+
function isElseIfNonNesting(node: TreeSitterNode, type: string, cRules: AnyRules): boolean {
|
|
53
|
+
if (type !== cRules.ifNodeType) return false;
|
|
54
|
+
|
|
55
|
+
if (cRules.elseViaAlternative) {
|
|
56
|
+
// Pattern C
|
|
57
|
+
return (
|
|
58
|
+
node.parent?.type === cRules.ifNodeType &&
|
|
59
|
+
node.parent?.childForFieldName('alternative')?.id === node.id
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
if (cRules.elseNodeType) {
|
|
63
|
+
// Pattern A
|
|
64
|
+
return node.parent?.type === cRules.elseNodeType;
|
|
65
|
+
}
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
43
69
|
function classifyBranchNode(
|
|
44
70
|
node: TreeSitterNode,
|
|
45
71
|
type: string,
|
|
@@ -190,6 +216,13 @@ export function createComplexityVisitor(
|
|
|
190
216
|
let funcDepth = 0;
|
|
191
217
|
const results: PerFunctionResult[] = [];
|
|
192
218
|
|
|
219
|
+
// The walker increments context.nestingLevel for ALL nodes in nestingNodeTypes
|
|
220
|
+
// (including if_statement). But the DFS engine does NOT increment nesting for
|
|
221
|
+
// else-if if_statement nodes. Track a correction counter so children of else-if
|
|
222
|
+
// nodes see the correct (non-inflated) nesting level.
|
|
223
|
+
let nestingAdjust = 0;
|
|
224
|
+
const adjustNodeIds = new Set<number>();
|
|
225
|
+
|
|
193
226
|
return {
|
|
194
227
|
name: 'complexity',
|
|
195
228
|
functionNodeTypes: cRules.functionNodes,
|
|
@@ -204,6 +237,8 @@ export function createComplexityVisitor(
|
|
|
204
237
|
activeFuncNode = funcNode;
|
|
205
238
|
activeFuncName = funcName;
|
|
206
239
|
funcDepth = 0;
|
|
240
|
+
nestingAdjust = 0;
|
|
241
|
+
adjustNodeIds.clear();
|
|
207
242
|
} else {
|
|
208
243
|
funcDepth++;
|
|
209
244
|
}
|
|
@@ -230,11 +265,30 @@ export function createComplexityVisitor(
|
|
|
230
265
|
enterNode(node: TreeSitterNode, context: VisitorContext): EnterNodeResult | undefined {
|
|
231
266
|
if (fileLevelWalk && !activeFuncNode) return;
|
|
232
267
|
|
|
233
|
-
|
|
268
|
+
// In file-level mode, funcDepth starts at 0 for the active function.
|
|
269
|
+
// In function-level mode, funcDepth starts at 1 for the root function
|
|
270
|
+
// (since enterFunction always increments it). Nested functions add +1
|
|
271
|
+
// each level — subtract 1 so the root function contributes 0 nesting
|
|
272
|
+
// and each nested level adds +1, matching the Rust engine's behavior.
|
|
273
|
+
const funcNesting = fileLevelWalk ? funcDepth : Math.max(0, funcDepth - 1);
|
|
274
|
+
const nestingLevel = context.nestingLevel + funcNesting - nestingAdjust;
|
|
234
275
|
classifyNode(node, nestingLevel, cRules, hRules, acc);
|
|
276
|
+
|
|
277
|
+
// If this is an else-if if_statement that the walker will treat as a
|
|
278
|
+
// nesting node (incrementing context.nestingLevel for children), but
|
|
279
|
+
// the DFS walk would NOT increment nesting for, compensate by bumping
|
|
280
|
+
// nestingAdjust so children see the correct level.
|
|
281
|
+
if (cRules.nestingNodes.has(node.type) && isElseIfNonNesting(node, node.type, cRules)) {
|
|
282
|
+
nestingAdjust++;
|
|
283
|
+
adjustNodeIds.add(node.id);
|
|
284
|
+
}
|
|
235
285
|
},
|
|
236
286
|
|
|
237
287
|
exitNode(node: TreeSitterNode): void {
|
|
288
|
+
if (adjustNodeIds.has(node.id)) {
|
|
289
|
+
nestingAdjust--;
|
|
290
|
+
adjustNodeIds.delete(node.id);
|
|
291
|
+
}
|
|
238
292
|
if (hRules?.skipTypes.has(node.type)) acc.halsteadSkipDepth--;
|
|
239
293
|
},
|
|
240
294
|
|
|
@@ -5,6 +5,7 @@ export const command: CommandDefinition = {
|
|
|
5
5
|
description: 'Compare code structure between two branches/refs',
|
|
6
6
|
options: [
|
|
7
7
|
['--depth <n>', 'Max transitive caller depth', '3'],
|
|
8
|
+
['-d, --db <path>', 'Path to graph.db'],
|
|
8
9
|
['-T, --no-tests', 'Exclude test/spec files'],
|
|
9
10
|
['--include-tests', 'Include test/spec files (overrides excludeTests config)'],
|
|
10
11
|
['-j, --json', 'Output as JSON'],
|
|
@@ -12,12 +13,15 @@ export const command: CommandDefinition = {
|
|
|
12
13
|
],
|
|
13
14
|
async execute([base, target], opts, ctx) {
|
|
14
15
|
const { branchCompare } = await import('../../presentation/branch-compare.js');
|
|
16
|
+
const path = await import('node:path');
|
|
17
|
+
const repoRoot = opts.db ? path.resolve(path.dirname(opts.db as string), '..') : undefined;
|
|
15
18
|
await branchCompare(base!, target!, {
|
|
16
19
|
engine: ctx.program.opts().engine,
|
|
17
20
|
depth: parseInt(opts.depth as string, 10),
|
|
18
21
|
noTests: ctx.resolveNoTests(opts),
|
|
19
22
|
json: opts.json,
|
|
20
23
|
format: opts.format,
|
|
24
|
+
repoRoot,
|
|
21
25
|
});
|
|
22
26
|
},
|
|
23
27
|
};
|
|
@@ -13,6 +13,7 @@ export const command: CommandDefinition = {
|
|
|
13
13
|
['--ndjson', 'Newline-delimited JSON output'],
|
|
14
14
|
['--staged', 'Analyze staged changes instead of unstaged'],
|
|
15
15
|
['--depth <n>', 'Max transitive caller depth', '3'],
|
|
16
|
+
['-j, --json', 'Output as JSON (shorthand for -f json)'],
|
|
16
17
|
['-f, --format <format>', 'Output format: text, mermaid, json', 'text'],
|
|
17
18
|
['--no-implementations', 'Exclude interface/trait implementors from blast radius'],
|
|
18
19
|
],
|
|
@@ -21,7 +22,7 @@ export const command: CommandDefinition = {
|
|
|
21
22
|
ref,
|
|
22
23
|
staged: opts.staged,
|
|
23
24
|
depth: parseInt(opts.depth as string, 10),
|
|
24
|
-
format: opts.format,
|
|
25
|
+
format: opts.json ? 'json' : opts.format,
|
|
25
26
|
includeImplementors: opts.implementations !== false,
|
|
26
27
|
...ctx.resolveQueryOpts(opts),
|
|
27
28
|
});
|
package/src/cli/commands/info.ts
CHANGED
|
@@ -3,7 +3,8 @@ import type { CommandDefinition } from '../types.js';
|
|
|
3
3
|
export const command: CommandDefinition = {
|
|
4
4
|
name: 'info',
|
|
5
5
|
description: 'Show codegraph engine info and diagnostics',
|
|
6
|
-
|
|
6
|
+
options: [['-d, --db <path>', 'Path to graph.db']],
|
|
7
|
+
async execute(_args, opts, ctx) {
|
|
7
8
|
const { getNativePackageVersion, isNativeAvailable, loadNative } = await import(
|
|
8
9
|
'../../infrastructure/native.js'
|
|
9
10
|
);
|
|
@@ -40,7 +41,7 @@ export const command: CommandDefinition = {
|
|
|
40
41
|
try {
|
|
41
42
|
const { findDbPath, getBuildMeta } = await import('../../db/index.js');
|
|
42
43
|
const Database = (await import('better-sqlite3')).default;
|
|
43
|
-
const dbPath = findDbPath();
|
|
44
|
+
const dbPath = findDbPath(opts.db as string | undefined);
|
|
44
45
|
const fs = await import('node:fs');
|
|
45
46
|
if (fs.existsSync(dbPath)) {
|
|
46
47
|
const db = new Database(dbPath, { readonly: true });
|
package/src/db/connection.ts
CHANGED
|
@@ -311,6 +311,7 @@ export function openReadonlyOrFail(customPath?: string): BetterSqlite3Database {
|
|
|
311
311
|
}
|
|
312
312
|
const Database = getDatabase();
|
|
313
313
|
const db = new Database(dbPath, { readonly: true }) as unknown as BetterSqlite3Database;
|
|
314
|
+
db.pragma('busy_timeout = 5000');
|
|
314
315
|
|
|
315
316
|
warnOnVersionMismatch(() => {
|
|
316
317
|
const row = db
|
|
@@ -359,7 +360,7 @@ function openRepoNative(customDbPath?: string): { repo: Repository; close(): voi
|
|
|
359
360
|
*/
|
|
360
361
|
export function openRepo(
|
|
361
362
|
customDbPath?: string,
|
|
362
|
-
opts: { repo?: Repository } = {},
|
|
363
|
+
opts: { repo?: Repository; engine?: 'native' | 'wasm' | 'auto' } = {},
|
|
363
364
|
): { repo: Repository; close(): void } {
|
|
364
365
|
if (opts.repo != null) {
|
|
365
366
|
if (!(opts.repo instanceof Repository)) {
|
|
@@ -370,15 +371,25 @@ export function openRepo(
|
|
|
370
371
|
return { repo: opts.repo, close() {} };
|
|
371
372
|
}
|
|
372
373
|
|
|
374
|
+
// Respect explicit engine selection: opts.engine > CODEGRAPH_ENGINE env > auto.
|
|
375
|
+
// This ensures --engine wasm and benchmark workers bypass the native path.
|
|
376
|
+
const engine = opts.engine || process.env.CODEGRAPH_ENGINE || 'auto';
|
|
377
|
+
|
|
373
378
|
// Try native rusqlite path first (Phase 6.14)
|
|
374
|
-
if (isNativeAvailable()) {
|
|
379
|
+
if (engine !== 'wasm' && isNativeAvailable()) {
|
|
375
380
|
try {
|
|
376
381
|
return openRepoNative(customDbPath);
|
|
377
382
|
} catch (e) {
|
|
378
383
|
// Re-throw user-visible errors (e.g. DB not found) — only silently
|
|
379
384
|
// fall back for native-engine failures (e.g. incompatible native binary).
|
|
380
385
|
if (e instanceof DbError) throw e;
|
|
381
|
-
|
|
386
|
+
// Re-throw locking/busy errors — falling back to better-sqlite3 would
|
|
387
|
+
// hit the same contention (and potentially hang without busy_timeout).
|
|
388
|
+
const msg = toErrorMessage(e);
|
|
389
|
+
if (/\b(busy|locked|SQLITE_BUSY|SQLITE_LOCKED)\b/i.test(msg)) {
|
|
390
|
+
throw new DbError(`Database is busy (another process may be writing): ${msg}`, {});
|
|
391
|
+
}
|
|
392
|
+
debug(`openRepo: native path failed, falling back to better-sqlite3: ${msg}`);
|
|
382
393
|
}
|
|
383
394
|
}
|
|
384
395
|
|
|
@@ -405,14 +416,22 @@ export function openReadonlyWithNative(customPath?: string): {
|
|
|
405
416
|
} {
|
|
406
417
|
const db = openReadonlyOrFail(customPath);
|
|
407
418
|
|
|
419
|
+
// Respect explicit engine selection, consistent with openRepo().
|
|
420
|
+
const engine = process.env.CODEGRAPH_ENGINE || 'auto';
|
|
421
|
+
|
|
408
422
|
let nativeDb: NativeDatabase | undefined;
|
|
409
|
-
if (isNativeAvailable()) {
|
|
423
|
+
if (engine !== 'wasm' && isNativeAvailable()) {
|
|
410
424
|
try {
|
|
411
425
|
const dbPath = findDbPath(customPath);
|
|
412
426
|
const native = getNative();
|
|
413
427
|
nativeDb = native.NativeDatabase.openReadonly(dbPath);
|
|
414
428
|
} catch (e) {
|
|
415
|
-
|
|
429
|
+
const msg = toErrorMessage(e);
|
|
430
|
+
if (/\b(busy|locked|SQLITE_BUSY|SQLITE_LOCKED)\b/i.test(msg)) {
|
|
431
|
+
debug(`openReadonlyWithNative: native path busy, skipping native DB: ${msg}`);
|
|
432
|
+
} else {
|
|
433
|
+
debug(`openReadonlyWithNative: native path failed: ${msg}`);
|
|
434
|
+
}
|
|
416
435
|
}
|
|
417
436
|
}
|
|
418
437
|
|
|
@@ -100,6 +100,20 @@ export class Repository implements IRepository {
|
|
|
100
100
|
throw new Error('not implemented');
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Batch version of findCallers — returns callers for multiple node IDs in a
|
|
105
|
+
* single query. Default implementation loops; subclasses override with SQL
|
|
106
|
+
* `IN (...)` for efficiency.
|
|
107
|
+
*/
|
|
108
|
+
findCallersBatch(nodeIds: number[]): Map<number, RelatedNodeRow[]> {
|
|
109
|
+
const result = new Map<number, RelatedNodeRow[]>();
|
|
110
|
+
for (const id of nodeIds) {
|
|
111
|
+
const callers = this.findCallers(id);
|
|
112
|
+
if (callers.length > 0) result.set(id, callers);
|
|
113
|
+
}
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
|
|
103
117
|
findDistinctCallers(_nodeId: number): RelatedNodeRow[] {
|
|
104
118
|
throw new Error('not implemented');
|
|
105
119
|
}
|
|
@@ -209,4 +223,47 @@ export class Repository implements IRepository {
|
|
|
209
223
|
hasCoChangesTable(): boolean {
|
|
210
224
|
return false;
|
|
211
225
|
}
|
|
226
|
+
|
|
227
|
+
// ── Composite queries ──────────────────────────────────────────────
|
|
228
|
+
/**
|
|
229
|
+
* Complete fnDeps query in a single call. Returns null when not natively
|
|
230
|
+
* supported — callers should fall back to the JS-orchestrated path.
|
|
231
|
+
*/
|
|
232
|
+
fnDeps(
|
|
233
|
+
_name: string,
|
|
234
|
+
_opts?: { depth?: number; noTests?: boolean; file?: string; kind?: string },
|
|
235
|
+
): FnDepsResult | null {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ── Composite query result types ────────────────────────────────────────
|
|
241
|
+
|
|
242
|
+
export interface FnDepsNode {
|
|
243
|
+
name: string;
|
|
244
|
+
kind: string;
|
|
245
|
+
file: string;
|
|
246
|
+
line: number | null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export interface FnDepsCallerNode extends FnDepsNode {
|
|
250
|
+
viaHierarchy?: string;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export interface FnDepsEntry {
|
|
254
|
+
name: string;
|
|
255
|
+
kind: string;
|
|
256
|
+
file: string;
|
|
257
|
+
line: number | null;
|
|
258
|
+
endLine: number | null;
|
|
259
|
+
role: string | null;
|
|
260
|
+
fileHash: string | null;
|
|
261
|
+
callees: FnDepsNode[];
|
|
262
|
+
callers: FnDepsCallerNode[];
|
|
263
|
+
transitiveCallers: Record<number, FnDepsNode[]>;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export interface FnDepsResult {
|
|
267
|
+
name: string;
|
|
268
|
+
results: FnDepsEntry[];
|
|
212
269
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Barrel re-export for repository/ modules.
|
|
2
2
|
|
|
3
|
+
export type { FnDepsCallerNode, FnDepsEntry, FnDepsNode, FnDepsResult } from './base.js';
|
|
3
4
|
export { Repository } from './base.js';
|
|
4
5
|
export { purgeFileData, purgeFilesData } from './build-stmts.js';
|
|
5
6
|
export { cachedStmt } from './cached-stmt.js';
|