@optave/codegraph 3.9.2 → 3.9.4
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 +93 -10
- package/dist/ast-analysis/engine.d.ts.map +1 -1
- package/dist/ast-analysis/engine.js +64 -0
- package/dist/ast-analysis/engine.js.map +1 -1
- package/dist/ast-analysis/visitor.d.ts.map +1 -1
- package/dist/ast-analysis/visitor.js +14 -0
- package/dist/ast-analysis/visitor.js.map +1 -1
- package/dist/domain/analysis/diff-impact.d.ts +12 -0
- package/dist/domain/analysis/diff-impact.d.ts.map +1 -1
- package/dist/domain/analysis/diff-impact.js +20 -1
- package/dist/domain/analysis/diff-impact.js.map +1 -1
- package/dist/domain/graph/builder/context.d.ts +15 -0
- package/dist/domain/graph/builder/context.d.ts.map +1 -1
- package/dist/domain/graph/builder/context.js +7 -0
- package/dist/domain/graph/builder/context.js.map +1 -1
- package/dist/domain/graph/builder/native-db-proxy.d.ts.map +1 -1
- package/dist/domain/graph/builder/native-db-proxy.js +8 -4
- package/dist/domain/graph/builder/native-db-proxy.js.map +1 -1
- package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
- package/dist/domain/graph/builder/pipeline.js +176 -127
- 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 +67 -6
- package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
- package/dist/domain/graph/builder/stages/build-structure.js +2 -2
- package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/detect-changes.js +51 -10
- 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 +10 -4
- package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
- package/dist/domain/graph/builder/stages/run-analyses.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/run-analyses.js +5 -20
- package/dist/domain/graph/builder/stages/run-analyses.js.map +1 -1
- package/dist/domain/parser.d.ts.map +1 -1
- package/dist/domain/parser.js +6 -2
- package/dist/domain/parser.js.map +1 -1
- package/dist/extractors/javascript.js +120 -0
- package/dist/extractors/javascript.js.map +1 -1
- package/dist/features/ast.js +2 -2
- package/dist/features/ast.js.map +1 -1
- package/dist/features/cfg.d.ts +1 -1
- package/dist/features/cfg.d.ts.map +1 -1
- package/dist/features/cfg.js +52 -6
- package/dist/features/cfg.js.map +1 -1
- package/dist/features/complexity.d.ts.map +1 -1
- package/dist/features/complexity.js +7 -0
- package/dist/features/complexity.js.map +1 -1
- package/dist/features/structure.d.ts.map +1 -1
- package/dist/features/structure.js +14 -1
- package/dist/features/structure.js.map +1 -1
- package/dist/infrastructure/update-check.d.ts +1 -1
- package/dist/infrastructure/update-check.js +3 -3
- package/dist/infrastructure/update-check.js.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +7 -7
- package/src/ast-analysis/engine.ts +83 -0
- package/src/ast-analysis/visitor.ts +15 -0
- package/src/domain/analysis/diff-impact.ts +28 -1
- package/src/domain/graph/builder/context.ts +17 -0
- package/src/domain/graph/builder/native-db-proxy.ts +10 -4
- package/src/domain/graph/builder/pipeline.ts +196 -130
- package/src/domain/graph/builder/stages/build-edges.ts +80 -6
- package/src/domain/graph/builder/stages/build-structure.ts +2 -2
- package/src/domain/graph/builder/stages/detect-changes.ts +61 -12
- package/src/domain/graph/builder/stages/finalize.ts +11 -4
- package/src/domain/graph/builder/stages/run-analyses.ts +5 -26
- package/src/domain/parser.ts +6 -2
- package/src/extractors/javascript.ts +142 -0
- package/src/features/ast.ts +2 -2
- package/src/features/cfg.ts +51 -6
- package/src/features/complexity.ts +7 -0
- package/src/features/structure.ts +17 -1
- package/src/infrastructure/update-check.ts +3 -3
- package/src/types.ts +1 -0
|
@@ -374,24 +374,73 @@ function purgeAndAddReverseDeps(
|
|
|
374
374
|
// Prefer NativeDatabase: purge + reverse-dep edge deletion in one transaction (#670)
|
|
375
375
|
if (ctx.engineName === 'native' && ctx.nativeDb?.purgeFilesData) {
|
|
376
376
|
ctx.nativeDb.purgeFilesData(filesToPurge, false, hasReverseDeps ? reverseDepList : undefined);
|
|
377
|
+
// Native path still reparses reverse-deps (works correctly with native edge builder)
|
|
378
|
+
for (const relPath of reverseDeps) {
|
|
379
|
+
const absPath = path.join(rootDir, relPath);
|
|
380
|
+
ctx.parseChanges.push({ file: absPath, relPath, _reverseDepOnly: true });
|
|
381
|
+
}
|
|
377
382
|
} else {
|
|
383
|
+
// WASM/JS path: save edges from reverse-dep files → changed files BEFORE
|
|
384
|
+
// purge, then reconnect them to new node IDs after insertNodes (#932, #933).
|
|
385
|
+
//
|
|
386
|
+
// purgeFilesFromGraph deletes edges in BOTH directions for changed files,
|
|
387
|
+
// which already removes the reverse-dep → changed-file edges. The old
|
|
388
|
+
// approach then over-deleted ALL outgoing edges from reverse-dep files and
|
|
389
|
+
// reparsed them to rebuild everything — expensive (87 extra parses) and
|
|
390
|
+
// lossy (442 missing edges due to imperfect resolution on rebuild).
|
|
391
|
+
//
|
|
392
|
+
// New approach: save the edge topology, let purge handle deletion, then
|
|
393
|
+
// reconnect using new node IDs. No reparse needed.
|
|
394
|
+
if (hasReverseDeps && hasPurge) {
|
|
395
|
+
const changePathSet = new Set(changePaths);
|
|
396
|
+
const saveEdgesStmt = db.prepare(`
|
|
397
|
+
SELECT e.source_id, n_tgt.name AS tgt_name, n_tgt.kind AS tgt_kind,
|
|
398
|
+
n_tgt.file AS tgt_file, n_tgt.line AS tgt_line,
|
|
399
|
+
e.kind AS edge_kind, e.confidence, e.dynamic,
|
|
400
|
+
n_src.file AS src_file
|
|
401
|
+
FROM edges e
|
|
402
|
+
JOIN nodes n_src ON e.source_id = n_src.id
|
|
403
|
+
JOIN nodes n_tgt ON e.target_id = n_tgt.id
|
|
404
|
+
WHERE n_tgt.file = ? AND n_src.file != n_tgt.file
|
|
405
|
+
`);
|
|
406
|
+
for (const changedPath of changePaths) {
|
|
407
|
+
for (const row of saveEdgesStmt.all(changedPath) as Array<{
|
|
408
|
+
source_id: number;
|
|
409
|
+
tgt_name: string;
|
|
410
|
+
tgt_kind: string;
|
|
411
|
+
tgt_file: string;
|
|
412
|
+
tgt_line: number;
|
|
413
|
+
edge_kind: string;
|
|
414
|
+
confidence: number;
|
|
415
|
+
dynamic: number;
|
|
416
|
+
src_file: string;
|
|
417
|
+
}>) {
|
|
418
|
+
// Skip edges whose source is also being purged — buildEdges will
|
|
419
|
+
// re-create them with correct new IDs.
|
|
420
|
+
if (changePathSet.has(row.src_file)) continue;
|
|
421
|
+
ctx.savedReverseDepEdges.push({
|
|
422
|
+
sourceId: row.source_id,
|
|
423
|
+
tgtName: row.tgt_name,
|
|
424
|
+
tgtKind: row.tgt_kind,
|
|
425
|
+
tgtFile: row.tgt_file,
|
|
426
|
+
tgtLine: row.tgt_line,
|
|
427
|
+
edgeKind: row.edge_kind,
|
|
428
|
+
confidence: row.confidence,
|
|
429
|
+
dynamic: row.dynamic,
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
debug(`Saved ${ctx.savedReverseDepEdges.length} reverse-dep edges for reconnection`);
|
|
434
|
+
}
|
|
435
|
+
|
|
378
436
|
if (hasPurge) {
|
|
379
437
|
purgeFilesFromGraph(db, filesToPurge, { purgeHashes: false });
|
|
380
438
|
}
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
);
|
|
385
|
-
for (const relPath of reverseDepList) {
|
|
386
|
-
deleteOutgoingEdgesForFile.run(relPath);
|
|
387
|
-
}
|
|
388
|
-
}
|
|
439
|
+
// No outgoing-edge deletion for reverse-deps — purge already removed
|
|
440
|
+
// edges targeting the changed files, and other outgoing edges are valid.
|
|
441
|
+
// No reverse-deps added to parseChanges — no reparse needed.
|
|
389
442
|
}
|
|
390
443
|
}
|
|
391
|
-
for (const relPath of reverseDeps) {
|
|
392
|
-
const absPath = path.join(rootDir, relPath);
|
|
393
|
-
ctx.parseChanges.push({ file: absPath, relPath, _reverseDepOnly: true });
|
|
394
|
-
}
|
|
395
444
|
}
|
|
396
445
|
|
|
397
446
|
function detectHasEmbeddings(db: BetterSqlite3Database, nativeDb?: NativeDatabase): boolean {
|
|
@@ -81,13 +81,20 @@ function persistBuildMetadata(
|
|
|
81
81
|
): void {
|
|
82
82
|
const useNativeDb = ctx.engineName === 'native' && !!ctx.nativeDb;
|
|
83
83
|
if (!ctx.isFullBuild && ctx.allSymbols.size <= 3) return;
|
|
84
|
+
// When the native engine is active, persist the Rust addon version so that
|
|
85
|
+
// checkEngineSchemaMismatch compares against the same value on the next build.
|
|
86
|
+
// Writing CODEGRAPH_VERSION (the npm package version) here would create a
|
|
87
|
+
// permanent mismatch whenever npm and crate versions diverge, forcing every
|
|
88
|
+
// subsequent build to be a full rebuild.
|
|
89
|
+
const codeVersionToWrite =
|
|
90
|
+
ctx.engineName === 'native' && ctx.engineVersion ? ctx.engineVersion : CODEGRAPH_VERSION;
|
|
84
91
|
try {
|
|
85
92
|
if (useNativeDb) {
|
|
86
93
|
ctx.nativeDb!.setBuildMeta(
|
|
87
94
|
Object.entries({
|
|
88
95
|
engine: ctx.engineName,
|
|
89
|
-
engine_version:
|
|
90
|
-
codegraph_version:
|
|
96
|
+
engine_version: codeVersionToWrite,
|
|
97
|
+
codegraph_version: codeVersionToWrite,
|
|
91
98
|
schema_version: String(ctx.schemaVersion),
|
|
92
99
|
built_at: buildNow.toISOString(),
|
|
93
100
|
node_count: String(nodeCount),
|
|
@@ -97,8 +104,8 @@ function persistBuildMetadata(
|
|
|
97
104
|
} else {
|
|
98
105
|
setBuildMeta(ctx.db, {
|
|
99
106
|
engine: ctx.engineName,
|
|
100
|
-
engine_version:
|
|
101
|
-
codegraph_version:
|
|
107
|
+
engine_version: codeVersionToWrite,
|
|
108
|
+
codegraph_version: codeVersionToWrite,
|
|
102
109
|
schema_version: String(ctx.schemaVersion),
|
|
103
110
|
built_at: buildNow.toISOString(),
|
|
104
111
|
node_count: nodeCount,
|
|
@@ -2,39 +2,18 @@
|
|
|
2
2
|
* Stage: runAnalyses
|
|
3
3
|
*
|
|
4
4
|
* Dispatches to the unified AST analysis engine (AST nodes, complexity, CFG, dataflow).
|
|
5
|
-
*
|
|
5
|
+
* Reverse-dep files are no longer in allSymbols (they are not reparsed since #932/#933),
|
|
6
|
+
* so no filtering is needed here.
|
|
6
7
|
*/
|
|
7
|
-
import {
|
|
8
|
-
import type { ExtractorOutput } from '../../../../types.js';
|
|
8
|
+
import { warn } from '../../../../infrastructure/logger.js';
|
|
9
9
|
import type { PipelineContext } from '../context.js';
|
|
10
10
|
|
|
11
11
|
export async function runAnalyses(ctx: PipelineContext): Promise<void> {
|
|
12
|
-
const { db, allSymbols, rootDir, opts, engineOpts
|
|
13
|
-
|
|
14
|
-
// For incremental builds, exclude reverse-dep-only files
|
|
15
|
-
let astComplexitySymbols: Map<string, ExtractorOutput> = allSymbols;
|
|
16
|
-
if (!isFullBuild) {
|
|
17
|
-
const reverseDepFiles = new Set(
|
|
18
|
-
filesToParse
|
|
19
|
-
.filter((item) => (item as { _reverseDepOnly?: boolean })._reverseDepOnly)
|
|
20
|
-
.map((item) => item.relPath),
|
|
21
|
-
);
|
|
22
|
-
if (reverseDepFiles.size > 0) {
|
|
23
|
-
astComplexitySymbols = new Map();
|
|
24
|
-
for (const [relPath, symbols] of allSymbols) {
|
|
25
|
-
if (!reverseDepFiles.has(relPath)) {
|
|
26
|
-
astComplexitySymbols.set(relPath, symbols);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
debug(
|
|
30
|
-
`AST/complexity/CFG/dataflow: processing ${astComplexitySymbols.size} changed files (skipping ${reverseDepFiles.size} reverse-deps)`,
|
|
31
|
-
);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
12
|
+
const { db, allSymbols, rootDir, opts, engineOpts } = ctx;
|
|
34
13
|
|
|
35
14
|
const { runAnalyses: runAnalysesFn } = await import('../../../../ast-analysis/engine.js');
|
|
36
15
|
try {
|
|
37
|
-
const analysisTiming = await runAnalysesFn(db,
|
|
16
|
+
const analysisTiming = await runAnalysesFn(db, allSymbols, rootDir, opts, engineOpts);
|
|
38
17
|
ctx.timing.astMs = analysisTiming.astMs;
|
|
39
18
|
ctx.timing.complexityMs = analysisTiming.complexityMs;
|
|
40
19
|
ctx.timing.cfgMs = analysisTiming.cfgMs;
|
package/src/domain/parser.ts
CHANGED
|
@@ -780,7 +780,7 @@ export async function parseFileAuto(
|
|
|
780
780
|
const { native } = resolveEngine(opts);
|
|
781
781
|
|
|
782
782
|
if (native) {
|
|
783
|
-
const result = native.parseFile(filePath, source,
|
|
783
|
+
const result = native.parseFile(filePath, source, true, true);
|
|
784
784
|
if (!result) return null;
|
|
785
785
|
const patched = patchNativeResult(result);
|
|
786
786
|
// Always backfill typeMap for TS/TSX from WASM — native parser's type
|
|
@@ -878,7 +878,11 @@ export async function parseFilesAuto(
|
|
|
878
878
|
if (!native) return parseFilesWasm(filePaths, rootDir);
|
|
879
879
|
|
|
880
880
|
const result = new Map<string, ExtractorOutput>();
|
|
881
|
-
|
|
881
|
+
// Always extract all analysis data (dataflow + AST nodes) during native parse.
|
|
882
|
+
// This eliminates the need for any downstream WASM re-parse or native standalone calls.
|
|
883
|
+
const nativeResults = native.parseFilesFull
|
|
884
|
+
? native.parseFilesFull(filePaths, rootDir)
|
|
885
|
+
: native.parseFiles(filePaths, rootDir, true, true);
|
|
882
886
|
const needsTypeMap: { filePath: string; relPath: string }[] = [];
|
|
883
887
|
for (const r of nativeResults) {
|
|
884
888
|
if (!r) continue;
|
|
@@ -274,14 +274,17 @@ function dispatchQueryMatch(
|
|
|
274
274
|
name: c.callfn_name!.text,
|
|
275
275
|
line: c.callfn_node.startPosition.row + 1,
|
|
276
276
|
});
|
|
277
|
+
calls.push(...extractCallbackReferenceCalls(c.callfn_node));
|
|
277
278
|
} else if (c.callmem_node) {
|
|
278
279
|
const callInfo = extractCallInfo(c.callmem_fn!, c.callmem_node);
|
|
279
280
|
if (callInfo) calls.push(callInfo);
|
|
280
281
|
const cbDef = extractCallbackDefinition(c.callmem_node, c.callmem_fn);
|
|
281
282
|
if (cbDef) definitions.push(cbDef);
|
|
283
|
+
calls.push(...extractCallbackReferenceCalls(c.callmem_node));
|
|
282
284
|
} else if (c.callsub_node) {
|
|
283
285
|
const callInfo = extractCallInfo(c.callsub_fn!, c.callsub_node);
|
|
284
286
|
if (callInfo) calls.push(callInfo);
|
|
287
|
+
calls.push(...extractCallbackReferenceCalls(c.callsub_node));
|
|
285
288
|
} else if (c.newfn_node) {
|
|
286
289
|
calls.push({
|
|
287
290
|
name: c.newfn_name!.text,
|
|
@@ -321,6 +324,9 @@ function extractSymbolsQuery(tree: TreeSitterTree, query: TreeSitterQuery): Extr
|
|
|
321
324
|
// Extract typeMap from type annotations and new expressions
|
|
322
325
|
extractTypeMapWalk(tree.rootNode, typeMap);
|
|
323
326
|
|
|
327
|
+
// Extract definitions from destructured bindings (query patterns don't match object_pattern)
|
|
328
|
+
extractDestructuredBindingsWalk(tree.rootNode, definitions);
|
|
329
|
+
|
|
324
330
|
return { definitions, calls, imports, classes, exports: exps, typeMap };
|
|
325
331
|
}
|
|
326
332
|
|
|
@@ -334,6 +340,20 @@ const FUNCTION_SCOPE_TYPES = new Set([
|
|
|
334
340
|
'generator_function',
|
|
335
341
|
]);
|
|
336
342
|
|
|
343
|
+
/**
|
|
344
|
+
* Return true when `node` has an ancestor whose type is in FUNCTION_SCOPE_TYPES.
|
|
345
|
+
* Used by the walk path to skip declarations inside function bodies, matching
|
|
346
|
+
* the query path's top-down FUNCTION_SCOPE_TYPES filter.
|
|
347
|
+
*/
|
|
348
|
+
function hasFunctionScopeAncestor(node: TreeSitterNode): boolean {
|
|
349
|
+
let p: TreeSitterNode | null = node.parent ?? null;
|
|
350
|
+
while (p) {
|
|
351
|
+
if (FUNCTION_SCOPE_TYPES.has(p.type)) return true;
|
|
352
|
+
p = p.parent ?? null;
|
|
353
|
+
}
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
|
|
337
357
|
/**
|
|
338
358
|
* Recursively walk the AST to extract `const x = <literal>` as constants.
|
|
339
359
|
* Skips nodes inside function scopes so only file-level / block-level constants
|
|
@@ -363,6 +383,48 @@ function extractConstantsWalk(node: TreeSitterNode, definitions: Definition[]):
|
|
|
363
383
|
}
|
|
364
384
|
}
|
|
365
385
|
|
|
386
|
+
/**
|
|
387
|
+
* Walk the AST to find destructured const bindings (query patterns don't match object_pattern).
|
|
388
|
+
* e.g. `const { handleToken, checkPermissions } = initAuth(config)`
|
|
389
|
+
*/
|
|
390
|
+
function extractDestructuredBindingsWalk(node: TreeSitterNode, definitions: Definition[]): void {
|
|
391
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
392
|
+
const child = node.child(i);
|
|
393
|
+
if (!child) continue;
|
|
394
|
+
if (FUNCTION_SCOPE_TYPES.has(child.type)) continue;
|
|
395
|
+
|
|
396
|
+
let declNode = child;
|
|
397
|
+
if (child.type === 'export_statement') {
|
|
398
|
+
const inner = child.childForFieldName('declaration');
|
|
399
|
+
if (inner) declNode = inner;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const t = declNode.type;
|
|
403
|
+
if (
|
|
404
|
+
(t === 'lexical_declaration' || t === 'variable_declaration') &&
|
|
405
|
+
declNode.text.startsWith('const ')
|
|
406
|
+
) {
|
|
407
|
+
for (let j = 0; j < declNode.childCount; j++) {
|
|
408
|
+
const declarator = declNode.child(j);
|
|
409
|
+
if (!declarator || declarator.type !== 'variable_declarator') continue;
|
|
410
|
+
const nameN = declarator.childForFieldName('name');
|
|
411
|
+
if (nameN && nameN.type === 'object_pattern') {
|
|
412
|
+
extractDestructuredBindings(
|
|
413
|
+
nameN,
|
|
414
|
+
declNode.startPosition.row + 1,
|
|
415
|
+
nodeEndLine(declNode),
|
|
416
|
+
definitions,
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (child.type !== 'export_statement') {
|
|
423
|
+
extractDestructuredBindingsWalk(child, definitions);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
366
428
|
/** Extract constant definitions from a `const` declaration node. */
|
|
367
429
|
function extractConstDeclarators(declNode: TreeSitterNode, definitions: Definition[]): void {
|
|
368
430
|
const t = declNode.type;
|
|
@@ -637,6 +699,39 @@ function handleTypeAliasDecl(node: TreeSitterNode, ctx: ExtractorOutput): void {
|
|
|
637
699
|
}
|
|
638
700
|
}
|
|
639
701
|
|
|
702
|
+
/**
|
|
703
|
+
* Extract definitions from destructured object bindings.
|
|
704
|
+
* `const { handleToken, checkPermissions } = initAuth(...)` creates definitions
|
|
705
|
+
* for handleToken and checkPermissions so they can be resolved as call targets.
|
|
706
|
+
*/
|
|
707
|
+
function extractDestructuredBindings(
|
|
708
|
+
pattern: TreeSitterNode,
|
|
709
|
+
line: number,
|
|
710
|
+
endLine: number,
|
|
711
|
+
definitions: Definition[],
|
|
712
|
+
): void {
|
|
713
|
+
for (let i = 0; i < pattern.childCount; i++) {
|
|
714
|
+
const child = pattern.child(i);
|
|
715
|
+
if (!child) continue;
|
|
716
|
+
if (
|
|
717
|
+
child.type === 'shorthand_property_identifier_pattern' ||
|
|
718
|
+
child.type === 'shorthand_property_identifier'
|
|
719
|
+
) {
|
|
720
|
+
// { handleToken } — shorthand binding
|
|
721
|
+
definitions.push({ name: child.text, kind: 'function', line, endLine });
|
|
722
|
+
} else if (child.type === 'pair_pattern' || child.type === 'pair') {
|
|
723
|
+
// { original: renamed } — renamed binding, use the local alias
|
|
724
|
+
const value = child.childForFieldName('value');
|
|
725
|
+
if (
|
|
726
|
+
value &&
|
|
727
|
+
(value.type === 'identifier' || value.type === 'shorthand_property_identifier_pattern')
|
|
728
|
+
) {
|
|
729
|
+
definitions.push({ name: value.text, kind: 'function', line, endLine });
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
640
735
|
function handleVariableDecl(node: TreeSitterNode, ctx: ExtractorOutput): void {
|
|
641
736
|
const isConst = node.text.startsWith('const ');
|
|
642
737
|
for (let i = 0; i < node.childCount; i++) {
|
|
@@ -667,6 +762,20 @@ function handleVariableDecl(node: TreeSitterNode, ctx: ExtractorOutput): void {
|
|
|
667
762
|
line: node.startPosition.row + 1,
|
|
668
763
|
endLine: nodeEndLine(node),
|
|
669
764
|
});
|
|
765
|
+
} else if (isConst && nameN.type === 'object_pattern' && !hasFunctionScopeAncestor(node)) {
|
|
766
|
+
// Destructured bindings: const { handleToken, checkPermissions } = initAuth(...)
|
|
767
|
+
// Each destructured property becomes a function definition so it can be
|
|
768
|
+
// resolved when passed as a callback (e.g. router.use(handleToken)).
|
|
769
|
+
// Restricted to const to avoid creating spurious definitions for
|
|
770
|
+
// transient let/var destructuring (e.g. let { userId } = parseRequest(req)).
|
|
771
|
+
// Scope guard mirrors extractDestructuredBindingsWalk (query path) and
|
|
772
|
+
// handle_var_decl (Rust path) — skips bindings inside function bodies.
|
|
773
|
+
extractDestructuredBindings(
|
|
774
|
+
nameN,
|
|
775
|
+
node.startPosition.row + 1,
|
|
776
|
+
nodeEndLine(node),
|
|
777
|
+
ctx.definitions,
|
|
778
|
+
);
|
|
670
779
|
}
|
|
671
780
|
}
|
|
672
781
|
}
|
|
@@ -715,6 +824,7 @@ function handleCallExpr(node: TreeSitterNode, ctx: ExtractorOutput): void {
|
|
|
715
824
|
const cbDef = extractCallbackDefinition(node, fn);
|
|
716
825
|
if (cbDef) ctx.definitions.push(cbDef);
|
|
717
826
|
}
|
|
827
|
+
ctx.calls.push(...extractCallbackReferenceCalls(node));
|
|
718
828
|
}
|
|
719
829
|
}
|
|
720
830
|
|
|
@@ -1167,6 +1277,38 @@ function extractSubscriptCallInfo(fn: TreeSitterNode, callNode: TreeSitterNode):
|
|
|
1167
1277
|
return null;
|
|
1168
1278
|
}
|
|
1169
1279
|
|
|
1280
|
+
/**
|
|
1281
|
+
* Extract Call entries for named function references passed as arguments.
|
|
1282
|
+
* e.g. `router.use(handleToken, checkAuth)` yields calls to handleToken and checkAuth.
|
|
1283
|
+
* `app.use(auth.validate)` yields a call to validate with receiver auth.
|
|
1284
|
+
* Skips literals, objects, arrays, anonymous functions, and call expressions (already handled).
|
|
1285
|
+
*/
|
|
1286
|
+
function extractCallbackReferenceCalls(callNode: TreeSitterNode): Call[] {
|
|
1287
|
+
const args = callNode.childForFieldName('arguments') || findChild(callNode, 'arguments');
|
|
1288
|
+
if (!args) return [];
|
|
1289
|
+
|
|
1290
|
+
const result: Call[] = [];
|
|
1291
|
+
const callLine = callNode.startPosition.row + 1;
|
|
1292
|
+
|
|
1293
|
+
for (let i = 0; i < args.childCount; i++) {
|
|
1294
|
+
const child = args.child(i);
|
|
1295
|
+
if (!child) continue;
|
|
1296
|
+
|
|
1297
|
+
if (child.type === 'identifier') {
|
|
1298
|
+
result.push({ name: child.text, line: callLine, dynamic: true });
|
|
1299
|
+
} else if (child.type === 'member_expression') {
|
|
1300
|
+
const prop = child.childForFieldName('property');
|
|
1301
|
+
const obj = child.childForFieldName('object');
|
|
1302
|
+
if (prop) {
|
|
1303
|
+
const receiver = extractReceiverName(obj);
|
|
1304
|
+
result.push({ name: prop.text, line: callLine, dynamic: true, receiver });
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
return result;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1170
1312
|
function findAnonymousCallback(argsNode: TreeSitterNode): TreeSitterNode | null {
|
|
1171
1313
|
for (let i = 0; i < argsNode.childCount; i++) {
|
|
1172
1314
|
const child = argsNode.child(i);
|
package/src/features/ast.ts
CHANGED
|
@@ -115,8 +115,8 @@ function tryNativeBulkInsert(
|
|
|
115
115
|
receiver: n.receiver ?? '',
|
|
116
116
|
})),
|
|
117
117
|
});
|
|
118
|
-
} else if (symbols.
|
|
119
|
-
return false; // needs JS fallback
|
|
118
|
+
} else if (symbols._tree) {
|
|
119
|
+
return false; // has WASM tree not yet processed — needs JS fallback
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
122
|
|
package/src/features/cfg.ts
CHANGED
|
@@ -369,7 +369,7 @@ export async function buildCFGData(
|
|
|
369
369
|
db: BetterSqlite3Database,
|
|
370
370
|
fileSymbols: Map<string, FileSymbols>,
|
|
371
371
|
rootDir: string,
|
|
372
|
-
|
|
372
|
+
engineOpts?: {
|
|
373
373
|
nativeDb?: { bulkInsertCfg?(entries: Array<Record<string, unknown>>): number };
|
|
374
374
|
suspendJsDb?: () => void;
|
|
375
375
|
resumeJsDb?: () => void;
|
|
@@ -379,11 +379,56 @@ export async function buildCFGData(
|
|
|
379
379
|
// skip WASM parser init, tree parsing, and JS visitor entirely — just persist.
|
|
380
380
|
const allNative = allCfgNative(fileSymbols);
|
|
381
381
|
|
|
382
|
-
//
|
|
383
|
-
// The
|
|
384
|
-
//
|
|
385
|
-
|
|
386
|
-
|
|
382
|
+
// ── Native bulk-insert fast path ──────────────────────────────────────
|
|
383
|
+
// The Rust bulkInsertCfg handles delete-before-insert atomically on a
|
|
384
|
+
// single rusqlite connection, so there is no dual-connection WAL conflict.
|
|
385
|
+
const nativeDb = engineOpts?.nativeDb;
|
|
386
|
+
if (allNative && nativeDb?.bulkInsertCfg) {
|
|
387
|
+
const entries: Array<Record<string, unknown>> = [];
|
|
388
|
+
for (const [relPath, symbols] of fileSymbols) {
|
|
389
|
+
const ext = path.extname(relPath).toLowerCase();
|
|
390
|
+
if (!CFG_EXTENSIONS.has(ext)) continue;
|
|
391
|
+
|
|
392
|
+
for (const def of symbols.definitions) {
|
|
393
|
+
if (def.kind !== 'function' && def.kind !== 'method') continue;
|
|
394
|
+
if (!def.line) continue;
|
|
395
|
+
|
|
396
|
+
const nodeId = getFunctionNodeId(db, def.name, relPath, def.line);
|
|
397
|
+
if (!nodeId) continue;
|
|
398
|
+
|
|
399
|
+
const cfg = def.cfg as { blocks?: CfgBuildBlock[]; edges?: CfgBuildEdge[] } | undefined;
|
|
400
|
+
if (!cfg?.blocks?.length) continue;
|
|
401
|
+
|
|
402
|
+
entries.push({
|
|
403
|
+
nodeId,
|
|
404
|
+
blocks: cfg.blocks.map((b) => ({
|
|
405
|
+
index: b.index,
|
|
406
|
+
blockType: b.type,
|
|
407
|
+
startLine: b.startLine ?? undefined,
|
|
408
|
+
endLine: b.endLine ?? undefined,
|
|
409
|
+
label: b.label ?? undefined,
|
|
410
|
+
})),
|
|
411
|
+
edges: (cfg.edges || []).map((e) => ({
|
|
412
|
+
sourceIndex: e.sourceIndex,
|
|
413
|
+
targetIndex: e.targetIndex,
|
|
414
|
+
kind: e.kind,
|
|
415
|
+
})),
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (entries.length > 0) {
|
|
421
|
+
let inserted = 0;
|
|
422
|
+
try {
|
|
423
|
+
engineOpts?.suspendJsDb?.();
|
|
424
|
+
inserted = nativeDb.bulkInsertCfg(entries);
|
|
425
|
+
} finally {
|
|
426
|
+
engineOpts?.resumeJsDb?.();
|
|
427
|
+
}
|
|
428
|
+
info(`CFG (native bulk): ${inserted} functions analyzed`);
|
|
429
|
+
}
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
387
432
|
|
|
388
433
|
const extToLang = buildExtToLangMap();
|
|
389
434
|
let parsers: unknown = null;
|
|
@@ -545,6 +545,10 @@ function collectNativeBulkRows(
|
|
|
545
545
|
const rows: Array<Record<string, unknown>> = [];
|
|
546
546
|
|
|
547
547
|
for (const [relPath, symbols] of fileSymbols) {
|
|
548
|
+
const ext = path.extname(relPath).toLowerCase();
|
|
549
|
+
const langId = symbols._langId || '';
|
|
550
|
+
const langSupported = COMPLEXITY_EXTENSIONS.has(ext) || COMPLEXITY_RULES.has(langId);
|
|
551
|
+
|
|
548
552
|
for (const def of symbols.definitions) {
|
|
549
553
|
if (def.kind !== 'function' && def.kind !== 'method') continue;
|
|
550
554
|
if (!def.line) continue;
|
|
@@ -554,6 +558,9 @@ function collectNativeBulkRows(
|
|
|
554
558
|
// of the native bulk-insert path for every TypeScript codebase (#846).
|
|
555
559
|
if (!def.complexity) {
|
|
556
560
|
if (def.name.includes('.') || !def.endLine || def.endLine <= def.line) continue;
|
|
561
|
+
// Languages without complexity rules will never have data — skip them
|
|
562
|
+
// rather than bailing out of the entire native bulk path.
|
|
563
|
+
if (!langSupported) continue;
|
|
557
564
|
return null; // genuine function body missing complexity — needs JS fallback
|
|
558
565
|
}
|
|
559
566
|
const nodeId = getFunctionNodeId(db, def.name, relPath, def.line);
|
|
@@ -166,6 +166,22 @@ function computeFileMetrics(
|
|
|
166
166
|
fanOutMap: Map<string, number>,
|
|
167
167
|
): void {
|
|
168
168
|
db.transaction(() => {
|
|
169
|
+
// Batch-load import counts per file (distinct imported files,
|
|
170
|
+
// matching the fast-path semantics in updateChangedFileMetrics).
|
|
171
|
+
// Runs inside the transaction for parity with the Rust path.
|
|
172
|
+
const importCountMap = new Map<string, number>();
|
|
173
|
+
for (const row of db
|
|
174
|
+
.prepare(
|
|
175
|
+
`SELECT n1.file AS src, COUNT(DISTINCT n2.file) AS cnt FROM edges e
|
|
176
|
+
JOIN nodes n1 ON e.source_id = n1.id
|
|
177
|
+
JOIN nodes n2 ON e.target_id = n2.id
|
|
178
|
+
WHERE e.kind = 'imports'
|
|
179
|
+
GROUP BY n1.file`,
|
|
180
|
+
)
|
|
181
|
+
.all() as { src: string; cnt: number }[]) {
|
|
182
|
+
importCountMap.set(row.src, row.cnt);
|
|
183
|
+
}
|
|
184
|
+
|
|
169
185
|
for (const [relPath, symbols] of fileSymbols) {
|
|
170
186
|
const fileRow = getNodeIdStmt.get(relPath, 'file', relPath, 0);
|
|
171
187
|
if (!fileRow) continue;
|
|
@@ -180,7 +196,7 @@ function computeFileMetrics(
|
|
|
180
196
|
symbolCount++;
|
|
181
197
|
}
|
|
182
198
|
}
|
|
183
|
-
const importCount =
|
|
199
|
+
const importCount = importCountMap.get(relPath) || 0;
|
|
184
200
|
const exportCount = symbols.exports.length;
|
|
185
201
|
const fanIn = fanInMap.get(relPath) || 0;
|
|
186
202
|
const fanOut = fanOutMap.get(relPath) || 0;
|
|
@@ -18,11 +18,11 @@ interface UpdateCache {
|
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Minimal semver comparison. Returns -1, 0, or 1.
|
|
21
|
-
*
|
|
21
|
+
* Strips pre-release suffixes (e.g. "3.9.3-dev.6" → "3.9.3") before comparing.
|
|
22
22
|
*/
|
|
23
23
|
export function semverCompare(a: string, b: string): -1 | 0 | 1 {
|
|
24
|
-
const pa = a.split('.').map(Number);
|
|
25
|
-
const pb = b.split('.').map(Number);
|
|
24
|
+
const pa = a.replace(/-.*$/, '').split('.').map(Number);
|
|
25
|
+
const pb = b.replace(/-.*$/, '').split('.').map(Number);
|
|
26
26
|
for (let i = 0; i < 3; i++) {
|
|
27
27
|
const na = pa[i] || 0;
|
|
28
28
|
const nb = pb[i] || 0;
|
package/src/types.ts
CHANGED
|
@@ -1874,6 +1874,7 @@ export type StmtCache<TRow = unknown> = WeakMap<BetterSqlite3Database, SqliteSta
|
|
|
1874
1874
|
export interface NativeAddon {
|
|
1875
1875
|
parseFile(filePath: string, source: string, dataflow: boolean, ast: boolean): unknown;
|
|
1876
1876
|
parseFiles(files: string[], rootDir: string, dataflow: boolean, ast: boolean): unknown[];
|
|
1877
|
+
parseFilesFull?(files: string[], rootDir: string): unknown[];
|
|
1877
1878
|
resolveImport(fromFile: string, importSource: string, rootDir: string, aliases: unknown): string;
|
|
1878
1879
|
resolveImports(
|
|
1879
1880
|
items: Array<{ fromFile: string; importSource: string }>,
|