@optave/codegraph 3.8.1 → 3.9.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 +11 -7
- package/dist/ast-analysis/engine.d.ts.map +1 -1
- package/dist/ast-analysis/engine.js +43 -0
- package/dist/ast-analysis/engine.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/repository/base.d.ts +6 -0
- package/dist/db/repository/base.d.ts.map +1 -1
- package/dist/db/repository/base.js +14 -0
- package/dist/db/repository/base.js.map +1 -1
- package/dist/db/repository/native-repository.d.ts +1 -0
- package/dist/db/repository/native-repository.d.ts.map +1 -1
- package/dist/db/repository/native-repository.js +23 -0
- 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.map +1 -1
- package/dist/domain/analysis/dependencies.js +12 -8
- package/dist/domain/analysis/dependencies.js.map +1 -1
- package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
- package/dist/domain/graph/builder/pipeline.js +154 -59
- 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 +27 -1
- package/dist/domain/graph/builder/stages/build-edges.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 +128 -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/javascript.js +19 -9
- package/dist/extractors/javascript.js.map +1 -1
- package/dist/types.d.ts +2 -1
- 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 +51 -0
- 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/repository/base.ts +14 -0
- package/src/db/repository/native-repository.ts +25 -0
- package/src/db/repository/sqlite-repository.ts +26 -0
- package/src/domain/analysis/dependencies.ts +11 -6
- package/src/domain/graph/builder/pipeline.ts +185 -64
- package/src/domain/graph/builder/stages/build-edges.ts +23 -1
- package/src/domain/parser.ts +129 -63
- package/src/domain/search/models.ts +11 -5
- package/src/extractors/javascript.ts +21 -8
- package/src/types.ts +2 -1
|
@@ -75,14 +75,20 @@ function buildTransitiveCallers(
|
|
|
75
75
|
let frontier = callers;
|
|
76
76
|
|
|
77
77
|
for (let d = 2; d <= depth; d++) {
|
|
78
|
+
// Collect unvisited frontier IDs for a single batched query per depth
|
|
79
|
+
const unvisited = frontier.filter((f) => !visited.has(f.id));
|
|
80
|
+
for (const f of unvisited) visited.add(f.id);
|
|
81
|
+
if (unvisited.length === 0) break;
|
|
82
|
+
|
|
83
|
+
const batchCallers = repo.findCallersBatch(unvisited.map((f) => f.id));
|
|
78
84
|
const nextFrontier: typeof frontier = [];
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const upstream = repo.findCallers(f.id) as RelatedNodeRow[];
|
|
85
|
+
const nextFrontierIds = new Set<number>();
|
|
86
|
+
for (const f of unvisited) {
|
|
87
|
+
const upstream = batchCallers.get(f.id) || [];
|
|
83
88
|
for (const u of upstream) {
|
|
84
89
|
if (noTests && isTestFile(u.file)) continue;
|
|
85
|
-
if (!visited.has(u.id)) {
|
|
90
|
+
if (!visited.has(u.id) && !nextFrontierIds.has(u.id)) {
|
|
91
|
+
nextFrontierIds.add(u.id);
|
|
86
92
|
nextFrontier.push(u);
|
|
87
93
|
}
|
|
88
94
|
}
|
|
@@ -96,7 +102,6 @@ function buildTransitiveCallers(
|
|
|
96
102
|
}));
|
|
97
103
|
}
|
|
98
104
|
frontier = nextFrontier;
|
|
99
|
-
if (frontier.length === 0) break;
|
|
100
105
|
}
|
|
101
106
|
|
|
102
107
|
return transitiveCallers;
|
|
@@ -134,7 +134,9 @@ function setupPipeline(ctx: PipelineContext): void {
|
|
|
134
134
|
// Use NativeDatabase for schema init when native engine is available (Phase 6.13).
|
|
135
135
|
// better-sqlite3 (ctx.db) is still always opened — needed for queries and stages
|
|
136
136
|
// that haven't been migrated to rusqlite yet.
|
|
137
|
-
|
|
137
|
+
// Skip native DB entirely when user explicitly requested --engine wasm.
|
|
138
|
+
const enginePref = ctx.opts.engine || 'auto';
|
|
139
|
+
const native = enginePref !== 'wasm' ? loadNative() : null;
|
|
138
140
|
if (native?.NativeDatabase) {
|
|
139
141
|
try {
|
|
140
142
|
ctx.nativeDb = native.NativeDatabase.openReadWrite(ctx.dbPath);
|
|
@@ -216,6 +218,7 @@ function closeNativeDb(ctx: PipelineContext, label: string): void {
|
|
|
216
218
|
|
|
217
219
|
/** Try to reopen the native connection for a given pipeline phase. */
|
|
218
220
|
function reopenNativeDb(ctx: PipelineContext, label: string): void {
|
|
221
|
+
if ((ctx.opts.engine ?? 'auto') === 'wasm') return;
|
|
219
222
|
const native = loadNative();
|
|
220
223
|
if (!native?.NativeDatabase) return;
|
|
221
224
|
try {
|
|
@@ -336,10 +339,14 @@ export async function buildGraph(
|
|
|
336
339
|
// napi crossings (eliminates WAL dual-connection dance). Falls back
|
|
337
340
|
// to the JS pipeline on failure or when native is unavailable.
|
|
338
341
|
//
|
|
339
|
-
// Native addon 3.8.0 has a path bug: file_symbols keys are absolute
|
|
342
|
+
// Native addon ≤3.8.0 has a path bug: file_symbols keys are absolute
|
|
340
343
|
// paths but known_files are relative, causing zero import/call edges.
|
|
344
|
+
// Native addon ≤3.8.1 has an incremental barrel bug: the Rust pipeline
|
|
345
|
+
// doesn't re-parse barrel files that are imported by changed files,
|
|
346
|
+
// causing missing barrel import edges and lost analysis data for
|
|
347
|
+
// reverse-dep files during incremental builds.
|
|
341
348
|
// Skip the orchestrator for affected versions (fixed in 3.9.0+).
|
|
342
|
-
const orchestratorBuggy = !!ctx.engineVersion && semverCompare(ctx.engineVersion, '3.8.
|
|
349
|
+
const orchestratorBuggy = !!ctx.engineVersion && semverCompare(ctx.engineVersion, '3.8.1') <= 0;
|
|
343
350
|
const forceJs =
|
|
344
351
|
process.env.CODEGRAPH_FORCE_JS_PIPELINE === '1' ||
|
|
345
352
|
ctx.forceFullRebuild ||
|
|
@@ -413,18 +420,32 @@ export async function buildGraph(
|
|
|
413
420
|
`Native build orchestrator completed: ${result.nodeCount ?? 0} nodes, ${result.edgeCount ?? 0} edges, ${result.fileCount ?? 0} files`,
|
|
414
421
|
);
|
|
415
422
|
|
|
416
|
-
// ── Run analysis phases
|
|
417
|
-
//
|
|
418
|
-
//
|
|
419
|
-
//
|
|
423
|
+
// ── Run structure + analysis phases after native orchestrator ──
|
|
424
|
+
// Structure (directory nodes, contains edges, metrics) is not fully
|
|
425
|
+
// ported to Rust — the native pipeline only handles the small
|
|
426
|
+
// incremental fast path (≤5 changed files). For full builds and
|
|
427
|
+
// larger incremental builds, run JS buildStructure() to fill the gap.
|
|
428
|
+
// Analysis phases (AST, complexity, CFG, dataflow) are also not yet
|
|
429
|
+
// ported; run via JS engine after reconstructing fileSymbols from DB.
|
|
420
430
|
let analysisTiming = { astMs: 0, complexityMs: 0, cfgMs: 0, dataflowMs: 0 };
|
|
431
|
+
let structurePatchMs = 0;
|
|
421
432
|
const needsAnalysis =
|
|
422
433
|
opts.ast !== false ||
|
|
423
434
|
opts.complexity !== false ||
|
|
424
435
|
opts.cfg !== false ||
|
|
425
436
|
opts.dataflow !== false;
|
|
426
437
|
|
|
427
|
-
|
|
438
|
+
// The native fast path only runs structure for small incremental
|
|
439
|
+
// builds: !isFullBuild && changedCount <= 5 && existingFileCount > 20.
|
|
440
|
+
// For all other cases (full builds, large incrementals), we must
|
|
441
|
+
// run JS buildStructure() to create directory nodes + contains edges (#804).
|
|
442
|
+
// Always run JS structure — the native fast-path has an additional
|
|
443
|
+
// existingFileCount > 20 guard that isn't reflected in the result JSON,
|
|
444
|
+
// so we can't reliably detect whether native actually ran structure.
|
|
445
|
+
const nativeHandledStructure = false;
|
|
446
|
+
const needsStructure = !nativeHandledStructure;
|
|
447
|
+
|
|
448
|
+
if (needsAnalysis || needsStructure) {
|
|
428
449
|
// WAL handoff: checkpoint through rusqlite, close nativeDb,
|
|
429
450
|
// reopen better-sqlite3 with a fresh page cache (#715, #736).
|
|
430
451
|
try {
|
|
@@ -447,10 +468,8 @@ export async function buildGraph(
|
|
|
447
468
|
try {
|
|
448
469
|
ctx.db = openDb(ctx.dbPath);
|
|
449
470
|
} catch (reopenErr) {
|
|
450
|
-
warn(
|
|
451
|
-
|
|
452
|
-
);
|
|
453
|
-
// Native build succeeded but we can't run analyses — return partial result
|
|
471
|
+
warn(`Failed to reopen DB after native build: ${(reopenErr as Error).message}`);
|
|
472
|
+
// Native build succeeded but we can't run post-processing — return partial result
|
|
454
473
|
return {
|
|
455
474
|
phases: {
|
|
456
475
|
setupMs: +((p.setupMs ?? 0) + (p.collectMs ?? 0) + (p.detectMs ?? 0)).toFixed(1),
|
|
@@ -469,22 +488,14 @@ export async function buildGraph(
|
|
|
469
488
|
};
|
|
470
489
|
}
|
|
471
490
|
|
|
472
|
-
// Reconstruct
|
|
473
|
-
//
|
|
474
|
-
//
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
const params: string[] = [];
|
|
481
|
-
if (changedFiles && changedFiles.length > 0) {
|
|
482
|
-
const placeholders = changedFiles.map(() => '?').join(',');
|
|
483
|
-
query += ` AND file IN (${placeholders})`;
|
|
484
|
-
params.push(...changedFiles);
|
|
485
|
-
}
|
|
486
|
-
query += ' ORDER BY file, line';
|
|
487
|
-
const rows = ctx.db.prepare(query).all(...params) as {
|
|
491
|
+
// Reconstruct fileSymbols from DB. For structure we need ALL files
|
|
492
|
+
// (to build complete directory tree); for analysis we scope to
|
|
493
|
+
// changed files only. Load all files, then scope analysis later.
|
|
494
|
+
const allFileRows = ctx.db
|
|
495
|
+
.prepare(
|
|
496
|
+
'SELECT file, name, kind, line, end_line as endLine FROM nodes WHERE file IS NOT NULL ORDER BY file, line',
|
|
497
|
+
)
|
|
498
|
+
.all() as {
|
|
488
499
|
file: string;
|
|
489
500
|
name: string;
|
|
490
501
|
kind: string;
|
|
@@ -492,9 +503,9 @@ export async function buildGraph(
|
|
|
492
503
|
endLine: number | null;
|
|
493
504
|
}[];
|
|
494
505
|
|
|
495
|
-
const
|
|
496
|
-
for (const row of
|
|
497
|
-
let entry =
|
|
506
|
+
const allFileSymbols = new Map<string, ExtractorOutput>();
|
|
507
|
+
for (const row of allFileRows) {
|
|
508
|
+
let entry = allFileSymbols.get(row.file);
|
|
498
509
|
if (!entry) {
|
|
499
510
|
entry = {
|
|
500
511
|
definitions: [],
|
|
@@ -504,7 +515,7 @@ export async function buildGraph(
|
|
|
504
515
|
exports: [],
|
|
505
516
|
typeMap: new Map(),
|
|
506
517
|
};
|
|
507
|
-
|
|
518
|
+
allFileSymbols.set(row.file, entry);
|
|
508
519
|
}
|
|
509
520
|
entry.definitions.push({
|
|
510
521
|
name: row.name,
|
|
@@ -514,45 +525,155 @@ export async function buildGraph(
|
|
|
514
525
|
});
|
|
515
526
|
}
|
|
516
527
|
|
|
517
|
-
//
|
|
518
|
-
|
|
519
|
-
|
|
528
|
+
// Populate import/export counts from DB edges so buildStructure
|
|
529
|
+
// computes correct import_count/export_count in node_metrics.
|
|
530
|
+
// The extractor arrays aren't persisted to the DB, so we derive
|
|
531
|
+
// counts from edge data instead (#804).
|
|
532
|
+
const importCountRows = ctx.db
|
|
533
|
+
.prepare(
|
|
534
|
+
`SELECT n.file, COUNT(*) AS cnt
|
|
535
|
+
FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
536
|
+
WHERE e.kind IN ('imports', 'imports-type', 'dynamic-imports')
|
|
537
|
+
AND n.file IS NOT NULL
|
|
538
|
+
GROUP BY n.file`,
|
|
539
|
+
)
|
|
540
|
+
.all() as { file: string; cnt: number }[];
|
|
541
|
+
for (const row of importCountRows) {
|
|
542
|
+
const entry = allFileSymbols.get(row.file);
|
|
543
|
+
if (entry) entry.imports = new Array(row.cnt) as ExtractorOutput['imports'];
|
|
544
|
+
}
|
|
545
|
+
// Export count: definitions in this file that are imported by other files
|
|
546
|
+
const exportCountRows = ctx.db
|
|
547
|
+
.prepare(
|
|
548
|
+
`SELECT n_tgt.file, COUNT(DISTINCT n_tgt.id) AS cnt
|
|
549
|
+
FROM edges e
|
|
550
|
+
JOIN nodes n_tgt ON e.target_id = n_tgt.id
|
|
551
|
+
JOIN nodes n_src ON e.source_id = n_src.id
|
|
552
|
+
WHERE e.kind IN ('imports', 'imports-type', 'reexports')
|
|
553
|
+
AND n_tgt.file IS NOT NULL
|
|
554
|
+
AND n_src.file != n_tgt.file
|
|
555
|
+
GROUP BY n_tgt.file`,
|
|
556
|
+
)
|
|
557
|
+
.all() as { file: string; cnt: number }[];
|
|
558
|
+
for (const row of exportCountRows) {
|
|
559
|
+
const entry = allFileSymbols.get(row.file);
|
|
560
|
+
if (entry) entry.exports = new Array(row.cnt) as ExtractorOutput['exports'];
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// ── Structure phase: directory nodes + contains edges (#804) ──
|
|
564
|
+
if (needsStructure) {
|
|
565
|
+
const structureStart = performance.now();
|
|
520
566
|
try {
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
567
|
+
// Derive directories from file paths
|
|
568
|
+
const directories = new Set<string>();
|
|
569
|
+
for (const relPath of allFileSymbols.keys()) {
|
|
570
|
+
const parts = relPath.split('/');
|
|
571
|
+
for (let i = 1; i < parts.length; i++) {
|
|
572
|
+
directories.add(parts.slice(0, i).join('/'));
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Build line count map from DB metrics or file content
|
|
577
|
+
const lineCountMap = new Map<string, number>();
|
|
578
|
+
const cachedLineCounts = ctx.db
|
|
579
|
+
.prepare(
|
|
580
|
+
`SELECT n.name AS file, m.line_count
|
|
581
|
+
FROM node_metrics m JOIN nodes n ON m.node_id = n.id
|
|
582
|
+
WHERE n.kind = 'file'`,
|
|
583
|
+
)
|
|
584
|
+
.all() as Array<{ file: string; line_count: number }>;
|
|
585
|
+
for (const row of cachedLineCounts) {
|
|
586
|
+
lineCountMap.set(row.file, row.line_count);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Native ran no structure at all — always do a full rebuild so
|
|
590
|
+
// every directory gets nodes + contains edges (#804).
|
|
591
|
+
const changedFilePaths = null;
|
|
592
|
+
|
|
593
|
+
const { buildStructure: buildStructureFn } = (await import(
|
|
594
|
+
'../../../features/structure.js'
|
|
595
|
+
)) as {
|
|
596
|
+
buildStructure: (
|
|
597
|
+
db: typeof ctx.db,
|
|
598
|
+
fileSymbols: Map<string, ExtractorOutput>,
|
|
599
|
+
rootDir: string,
|
|
600
|
+
lineCountMap: Map<string, number>,
|
|
601
|
+
directories: Set<string>,
|
|
602
|
+
changedFiles: string[] | null,
|
|
603
|
+
) => void;
|
|
604
|
+
};
|
|
605
|
+
buildStructureFn(
|
|
606
|
+
ctx.db,
|
|
607
|
+
allFileSymbols,
|
|
608
|
+
ctx.rootDir,
|
|
609
|
+
lineCountMap,
|
|
610
|
+
directories,
|
|
611
|
+
changedFilePaths,
|
|
612
|
+
);
|
|
613
|
+
debug('Structure phase completed after native orchestrator');
|
|
614
|
+
} catch (err) {
|
|
615
|
+
warn(`Structure phase failed after native build: ${toErrorMessage(err)}`);
|
|
526
616
|
}
|
|
617
|
+
structurePatchMs = performance.now() - structureStart;
|
|
527
618
|
}
|
|
528
619
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
620
|
+
// ── Analysis phase ──
|
|
621
|
+
if (needsAnalysis) {
|
|
622
|
+
// Scope analysis fileSymbols to changed files only
|
|
623
|
+
const changedFiles = result.changedFiles;
|
|
624
|
+
let analysisFileSymbols: Map<string, ExtractorOutput>;
|
|
625
|
+
if (changedFiles && changedFiles.length > 0) {
|
|
626
|
+
analysisFileSymbols = new Map();
|
|
627
|
+
for (const f of changedFiles) {
|
|
628
|
+
const entry = allFileSymbols.get(f);
|
|
629
|
+
if (entry) analysisFileSymbols.set(f, entry);
|
|
630
|
+
}
|
|
631
|
+
} else {
|
|
632
|
+
analysisFileSymbols = allFileSymbols;
|
|
633
|
+
}
|
|
541
634
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
635
|
+
// Reopen nativeDb for analysis features (suspend/resume WAL pattern).
|
|
636
|
+
const native = loadNative();
|
|
637
|
+
if (native?.NativeDatabase) {
|
|
638
|
+
try {
|
|
639
|
+
ctx.nativeDb = native.NativeDatabase.openReadWrite(ctx.dbPath);
|
|
640
|
+
if (ctx.engineOpts) ctx.engineOpts.nativeDb = ctx.nativeDb;
|
|
641
|
+
} catch {
|
|
642
|
+
ctx.nativeDb = undefined;
|
|
643
|
+
if (ctx.engineOpts) ctx.engineOpts.nativeDb = undefined;
|
|
644
|
+
}
|
|
548
645
|
}
|
|
646
|
+
|
|
549
647
|
try {
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
648
|
+
const { runAnalyses: runAnalysesFn } = await import(
|
|
649
|
+
'../../../ast-analysis/engine.js'
|
|
650
|
+
);
|
|
651
|
+
analysisTiming = await runAnalysesFn(
|
|
652
|
+
ctx.db,
|
|
653
|
+
analysisFileSymbols,
|
|
654
|
+
ctx.rootDir,
|
|
655
|
+
opts,
|
|
656
|
+
ctx.engineOpts,
|
|
657
|
+
);
|
|
658
|
+
} catch (err) {
|
|
659
|
+
warn(`Analysis phases failed after native build: ${toErrorMessage(err)}`);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// Close nativeDb after analyses
|
|
663
|
+
if (ctx.nativeDb) {
|
|
664
|
+
try {
|
|
665
|
+
ctx.nativeDb.exec('PRAGMA wal_checkpoint(TRUNCATE)');
|
|
666
|
+
} catch {
|
|
667
|
+
/* ignore checkpoint errors */
|
|
668
|
+
}
|
|
669
|
+
try {
|
|
670
|
+
ctx.nativeDb.close();
|
|
671
|
+
} catch {
|
|
672
|
+
/* ignore close errors */
|
|
673
|
+
}
|
|
674
|
+
ctx.nativeDb = undefined;
|
|
675
|
+
if (ctx.engineOpts) ctx.engineOpts.nativeDb = undefined;
|
|
553
676
|
}
|
|
554
|
-
ctx.nativeDb = undefined;
|
|
555
|
-
if (ctx.engineOpts) ctx.engineOpts.nativeDb = undefined;
|
|
556
677
|
}
|
|
557
678
|
}
|
|
558
679
|
|
|
@@ -564,7 +685,7 @@ export async function buildGraph(
|
|
|
564
685
|
insertMs: +(p.insertMs ?? 0).toFixed(1),
|
|
565
686
|
resolveMs: +(p.resolveMs ?? 0).toFixed(1),
|
|
566
687
|
edgesMs: +(p.edgesMs ?? 0).toFixed(1),
|
|
567
|
-
structureMs: +(p.structureMs ?? 0).toFixed(1),
|
|
688
|
+
structureMs: +((p.structureMs ?? 0) + structurePatchMs).toFixed(1),
|
|
568
689
|
rolesMs: +(p.rolesMs ?? 0).toFixed(1),
|
|
569
690
|
astMs: +(analysisTiming.astMs ?? 0).toFixed(1),
|
|
570
691
|
complexityMs: +(analysisTiming.complexityMs ?? 0).toFixed(1),
|
|
@@ -354,7 +354,10 @@ function buildImportedNamesForNative(
|
|
|
354
354
|
rootDir: string,
|
|
355
355
|
): Array<{ name: string; file: string }> {
|
|
356
356
|
const importedNames: Array<{ name: string; file: string }> = [];
|
|
357
|
-
|
|
357
|
+
// Process dynamic imports first (lower priority), then static imports
|
|
358
|
+
// (higher priority). Rust HashMap::collect keeps the last entry per key,
|
|
359
|
+
// so static imports win when both contribute the same name.
|
|
360
|
+
const addImports = (imp: (typeof symbols.imports)[number]) => {
|
|
358
361
|
const resolvedPath = getResolved(ctx, path.join(rootDir, relPath), imp.source);
|
|
359
362
|
for (const name of imp.names) {
|
|
360
363
|
const cleanName = name.replace(/^\*\s+as\s+/, '');
|
|
@@ -365,6 +368,12 @@ function buildImportedNamesForNative(
|
|
|
365
368
|
}
|
|
366
369
|
importedNames.push({ name: cleanName, file: targetFile });
|
|
367
370
|
}
|
|
371
|
+
};
|
|
372
|
+
for (const imp of symbols.imports) {
|
|
373
|
+
if (imp.dynamicImport) addImports(imp);
|
|
374
|
+
}
|
|
375
|
+
for (const imp of symbols.imports) {
|
|
376
|
+
if (!imp.dynamicImport) addImports(imp);
|
|
368
377
|
}
|
|
369
378
|
return importedNames;
|
|
370
379
|
}
|
|
@@ -409,12 +418,25 @@ function buildImportedNamesMap(
|
|
|
409
418
|
rootDir: string,
|
|
410
419
|
): Map<string, string> {
|
|
411
420
|
const importedNames = new Map<string, string>();
|
|
421
|
+
// Process dynamic imports first (lower priority), then static imports
|
|
422
|
+
// (higher priority). Static imports represent direct bindings while dynamic
|
|
423
|
+
// imports often use aliased destructuring (`{ foo: bar } = await import(…)`).
|
|
424
|
+
// When both contribute the same name, the static binding is authoritative.
|
|
412
425
|
for (const imp of symbols.imports) {
|
|
426
|
+
if (!imp.dynamicImport) continue;
|
|
413
427
|
const resolvedPath = getResolved(ctx, path.join(rootDir, relPath), imp.source);
|
|
414
428
|
for (const name of imp.names) {
|
|
415
429
|
importedNames.set(name.replace(/^\*\s+as\s+/, ''), resolvedPath);
|
|
416
430
|
}
|
|
417
431
|
}
|
|
432
|
+
for (const imp of symbols.imports) {
|
|
433
|
+
if (imp.dynamicImport) continue;
|
|
434
|
+
const resolvedPath = getResolved(ctx, path.join(rootDir, relPath), imp.source);
|
|
435
|
+
for (const name of imp.names) {
|
|
436
|
+
const cleanName = name.replace(/^\*\s+as\s+/, '');
|
|
437
|
+
importedNames.set(cleanName, resolvedPath);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
418
440
|
return importedNames;
|
|
419
441
|
}
|
|
420
442
|
|