@optave/codegraph 3.0.3 → 3.1.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 +60 -53
- package/package.json +9 -6
- package/src/ast.js +24 -24
- package/src/builder.js +318 -163
- package/src/cfg.js +39 -18
- package/src/cli.js +35 -0
- package/src/dataflow.js +42 -36
- package/src/db.js +7 -0
- package/src/flow.js +3 -70
- package/src/index.js +2 -1
- package/src/mcp.js +60 -0
- package/src/native.js +23 -3
- package/src/parser.js +58 -75
- package/src/queries.js +60 -21
- package/src/resolve.js +11 -2
- package/src/sequence.js +369 -0
package/src/builder.js
CHANGED
|
@@ -7,6 +7,7 @@ import { EXTENSIONS, IGNORE_DIRS, normalizePath } from './constants.js';
|
|
|
7
7
|
import { closeDb, getBuildMeta, initSchema, MIGRATIONS, openDb, setBuildMeta } from './db.js';
|
|
8
8
|
import { readJournal, writeJournalHeader } from './journal.js';
|
|
9
9
|
import { debug, info, warn } from './logger.js';
|
|
10
|
+
import { loadNative } from './native.js';
|
|
10
11
|
import { getActiveEngine, parseFilesAuto } from './parser.js';
|
|
11
12
|
import { computeConfidence, resolveImportPath, resolveImportsBatch } from './resolve.js';
|
|
12
13
|
|
|
@@ -444,7 +445,11 @@ export async function buildGraph(rootDir, opts = {}) {
|
|
|
444
445
|
opts.incremental !== false && config.build && config.build.incremental !== false;
|
|
445
446
|
|
|
446
447
|
// Engine selection: 'native', 'wasm', or 'auto' (default)
|
|
447
|
-
const engineOpts = {
|
|
448
|
+
const engineOpts = {
|
|
449
|
+
engine: opts.engine || 'auto',
|
|
450
|
+
dataflow: opts.dataflow !== false,
|
|
451
|
+
ast: opts.ast !== false,
|
|
452
|
+
};
|
|
448
453
|
const { name: engineName, version: engineVersion } = getActiveEngine(engineOpts);
|
|
449
454
|
info(`Using ${engineName} engine${engineVersion ? ` (v${engineVersion})` : ''}`);
|
|
450
455
|
|
|
@@ -548,7 +553,11 @@ export async function buildGraph(rootDir, opts = {}) {
|
|
|
548
553
|
|
|
549
554
|
if (needsCfg || needsDataflow) {
|
|
550
555
|
info('No file changes. Running pending analysis pass...');
|
|
551
|
-
const
|
|
556
|
+
const analysisOpts = {
|
|
557
|
+
...engineOpts,
|
|
558
|
+
dataflow: needsDataflow && opts.dataflow !== false,
|
|
559
|
+
};
|
|
560
|
+
const analysisSymbols = await parseFilesAuto(files, rootDir, analysisOpts);
|
|
552
561
|
if (needsCfg) {
|
|
553
562
|
const { buildCFGData } = await import('./cfg.js');
|
|
554
563
|
await buildCFGData(db, analysisSymbols, rootDir, engineOpts);
|
|
@@ -668,15 +677,38 @@ export async function buildGraph(rootDir, opts = {}) {
|
|
|
668
677
|
}
|
|
669
678
|
}
|
|
670
679
|
|
|
671
|
-
const insertNode = db.prepare(
|
|
672
|
-
'INSERT OR IGNORE INTO nodes (name, kind, file, line, end_line, parent_id) VALUES (?, ?, ?, ?, ?, ?)',
|
|
673
|
-
);
|
|
674
680
|
const getNodeId = db.prepare(
|
|
675
681
|
'SELECT id FROM nodes WHERE name = ? AND kind = ? AND file = ? AND line = ?',
|
|
676
682
|
);
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
683
|
+
|
|
684
|
+
// Batch INSERT helpers — multi-value INSERTs reduce SQLite round-trips
|
|
685
|
+
const BATCH_CHUNK = 200;
|
|
686
|
+
function batchInsertNodes(rows) {
|
|
687
|
+
if (!rows.length) return;
|
|
688
|
+
const ph = '(?,?,?,?,?,?)';
|
|
689
|
+
for (let i = 0; i < rows.length; i += BATCH_CHUNK) {
|
|
690
|
+
const chunk = rows.slice(i, i + BATCH_CHUNK);
|
|
691
|
+
const vals = [];
|
|
692
|
+
for (const r of chunk) vals.push(r[0], r[1], r[2], r[3], r[4], r[5]);
|
|
693
|
+
db.prepare(
|
|
694
|
+
'INSERT OR IGNORE INTO nodes (name,kind,file,line,end_line,parent_id) VALUES ' +
|
|
695
|
+
chunk.map(() => ph).join(','),
|
|
696
|
+
).run(...vals);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
function batchInsertEdges(rows) {
|
|
700
|
+
if (!rows.length) return;
|
|
701
|
+
const ph = '(?,?,?,?,?)';
|
|
702
|
+
for (let i = 0; i < rows.length; i += BATCH_CHUNK) {
|
|
703
|
+
const chunk = rows.slice(i, i + BATCH_CHUNK);
|
|
704
|
+
const vals = [];
|
|
705
|
+
for (const r of chunk) vals.push(r[0], r[1], r[2], r[3], r[4]);
|
|
706
|
+
db.prepare(
|
|
707
|
+
'INSERT INTO edges (source_id,target_id,kind,confidence,dynamic) VALUES ' +
|
|
708
|
+
chunk.map(() => ph).join(','),
|
|
709
|
+
).run(...vals);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
680
712
|
|
|
681
713
|
// Prepare hash upsert (with size column from migration v4)
|
|
682
714
|
let upsertHash;
|
|
@@ -723,57 +755,76 @@ export async function buildGraph(rootDir, opts = {}) {
|
|
|
723
755
|
const bulkGetNodeIds = db.prepare('SELECT id, name, kind, line FROM nodes WHERE file = ?');
|
|
724
756
|
|
|
725
757
|
const insertAll = db.transaction(() => {
|
|
758
|
+
// Phase 1: Batch insert all file nodes + definitions + exports
|
|
759
|
+
const phase1Rows = [];
|
|
726
760
|
for (const [relPath, symbols] of allSymbols) {
|
|
727
761
|
fileSymbols.set(relPath, symbols);
|
|
728
|
-
|
|
729
|
-
// Phase 1: Insert file node + definitions + exports (no children yet)
|
|
730
|
-
insertNode.run(relPath, 'file', relPath, 0, null, null);
|
|
762
|
+
phase1Rows.push([relPath, 'file', relPath, 0, null, null]);
|
|
731
763
|
for (const def of symbols.definitions) {
|
|
732
|
-
|
|
764
|
+
phase1Rows.push([def.name, def.kind, relPath, def.line, def.endLine || null, null]);
|
|
765
|
+
}
|
|
766
|
+
for (const exp of symbols.exports) {
|
|
767
|
+
phase1Rows.push([exp.name, exp.kind, relPath, exp.line, null, null]);
|
|
733
768
|
}
|
|
769
|
+
}
|
|
770
|
+
batchInsertNodes(phase1Rows);
|
|
771
|
+
|
|
772
|
+
// Phase 1b: Mark exported symbols
|
|
773
|
+
const markExported = db.prepare(
|
|
774
|
+
'UPDATE nodes SET exported = 1 WHERE name = ? AND kind = ? AND file = ? AND line = ?',
|
|
775
|
+
);
|
|
776
|
+
for (const [relPath, symbols] of allSymbols) {
|
|
734
777
|
for (const exp of symbols.exports) {
|
|
735
|
-
|
|
778
|
+
markExported.run(exp.name, exp.kind, relPath, exp.line);
|
|
736
779
|
}
|
|
780
|
+
}
|
|
737
781
|
|
|
738
|
-
|
|
782
|
+
// Phase 3: Batch insert children (needs parent IDs from Phase 2)
|
|
783
|
+
const childRows = [];
|
|
784
|
+
for (const [relPath, symbols] of allSymbols) {
|
|
739
785
|
const nodeIdMap = new Map();
|
|
740
786
|
for (const row of bulkGetNodeIds.all(relPath)) {
|
|
741
787
|
nodeIdMap.set(`${row.name}|${row.kind}|${row.line}`, row.id);
|
|
742
788
|
}
|
|
743
|
-
|
|
744
|
-
// Phase 3: Insert children with parent_id from the map
|
|
745
789
|
for (const def of symbols.definitions) {
|
|
746
790
|
if (!def.children?.length) continue;
|
|
747
791
|
const defId = nodeIdMap.get(`${def.name}|${def.kind}|${def.line}`);
|
|
748
792
|
if (!defId) continue;
|
|
749
793
|
for (const child of def.children) {
|
|
750
|
-
|
|
794
|
+
childRows.push([
|
|
795
|
+
child.name,
|
|
796
|
+
child.kind,
|
|
797
|
+
relPath,
|
|
798
|
+
child.line,
|
|
799
|
+
child.endLine || null,
|
|
800
|
+
defId,
|
|
801
|
+
]);
|
|
751
802
|
}
|
|
752
803
|
}
|
|
804
|
+
}
|
|
805
|
+
batchInsertNodes(childRows);
|
|
753
806
|
|
|
754
|
-
|
|
755
|
-
|
|
807
|
+
// Phase 5: Batch insert contains/parameter_of edges
|
|
808
|
+
const edgeRows = [];
|
|
809
|
+
for (const [relPath, symbols] of allSymbols) {
|
|
810
|
+
// Re-fetch to include children IDs
|
|
811
|
+
const nodeIdMap = new Map();
|
|
756
812
|
for (const row of bulkGetNodeIds.all(relPath)) {
|
|
757
813
|
nodeIdMap.set(`${row.name}|${row.kind}|${row.line}`, row.id);
|
|
758
814
|
}
|
|
759
|
-
|
|
760
|
-
// Phase 5: Insert edges using the cached ID map
|
|
761
815
|
const fileId = nodeIdMap.get(`${relPath}|file|0`);
|
|
762
816
|
for (const def of symbols.definitions) {
|
|
763
817
|
const defId = nodeIdMap.get(`${def.name}|${def.kind}|${def.line}`);
|
|
764
|
-
// File → top-level definition contains edge
|
|
765
818
|
if (fileId && defId) {
|
|
766
|
-
|
|
819
|
+
edgeRows.push([fileId, defId, 'contains', 1.0, 0]);
|
|
767
820
|
}
|
|
768
821
|
if (def.children?.length && defId) {
|
|
769
822
|
for (const child of def.children) {
|
|
770
823
|
const childId = nodeIdMap.get(`${child.name}|${child.kind}|${child.line}`);
|
|
771
824
|
if (childId) {
|
|
772
|
-
|
|
773
|
-
insertEdge.run(defId, childId, 'contains', 1.0, 0);
|
|
774
|
-
// Parameter → parent parameter_of edge (inverse direction)
|
|
825
|
+
edgeRows.push([defId, childId, 'contains', 1.0, 0]);
|
|
775
826
|
if (child.kind === 'parameter') {
|
|
776
|
-
|
|
827
|
+
edgeRows.push([childId, defId, 'parameter_of', 1.0, 0]);
|
|
777
828
|
}
|
|
778
829
|
}
|
|
779
830
|
}
|
|
@@ -808,6 +859,7 @@ export async function buildGraph(rootDir, opts = {}) {
|
|
|
808
859
|
}
|
|
809
860
|
}
|
|
810
861
|
}
|
|
862
|
+
batchInsertEdges(edgeRows);
|
|
811
863
|
|
|
812
864
|
// Also update metadata-only entries (self-heal mtime/size without re-parse)
|
|
813
865
|
if (upsertHash) {
|
|
@@ -844,7 +896,7 @@ export async function buildGraph(rootDir, opts = {}) {
|
|
|
844
896
|
batchInputs.push({ fromFile: absFile, importSource: imp.source });
|
|
845
897
|
}
|
|
846
898
|
}
|
|
847
|
-
const batchResolved = resolveImportsBatch(batchInputs, rootDir, aliases);
|
|
899
|
+
const batchResolved = resolveImportsBatch(batchInputs, rootDir, aliases, files);
|
|
848
900
|
_t.resolveMs = performance.now() - _t.resolve0;
|
|
849
901
|
|
|
850
902
|
function getResolved(absFile, importSource) {
|
|
@@ -957,7 +1009,7 @@ export async function buildGraph(rootDir, opts = {}) {
|
|
|
957
1009
|
// N+1 optimization: pre-load all nodes into a lookup map for edge building
|
|
958
1010
|
const allNodes = db
|
|
959
1011
|
.prepare(
|
|
960
|
-
`SELECT id, name, kind, file FROM nodes WHERE kind IN ('function','method','class','interface','struct','type','module','enum','trait')`,
|
|
1012
|
+
`SELECT id, name, kind, file, line FROM nodes WHERE kind IN ('function','method','class','interface','struct','type','module','enum','trait')`,
|
|
961
1013
|
)
|
|
962
1014
|
.all();
|
|
963
1015
|
const nodesByName = new Map();
|
|
@@ -972,9 +1024,11 @@ export async function buildGraph(rootDir, opts = {}) {
|
|
|
972
1024
|
nodesByNameAndFile.get(key).push(node);
|
|
973
1025
|
}
|
|
974
1026
|
|
|
975
|
-
// Second pass: build edges
|
|
1027
|
+
// Second pass: build edges (accumulated and batch-inserted)
|
|
976
1028
|
_t.edges0 = performance.now();
|
|
977
1029
|
const buildEdges = db.transaction(() => {
|
|
1030
|
+
const allEdgeRows = [];
|
|
1031
|
+
|
|
978
1032
|
for (const [relPath, symbols] of fileSymbols) {
|
|
979
1033
|
// Skip barrel-only files — loaded for resolution, edges already in DB
|
|
980
1034
|
if (barrelOnlyFiles.has(relPath)) continue;
|
|
@@ -988,7 +1042,7 @@ export async function buildGraph(rootDir, opts = {}) {
|
|
|
988
1042
|
const targetRow = getNodeId.get(resolvedPath, 'file', resolvedPath, 0);
|
|
989
1043
|
if (targetRow) {
|
|
990
1044
|
const edgeKind = imp.reexport ? 'reexports' : imp.typeOnly ? 'imports-type' : 'imports';
|
|
991
|
-
|
|
1045
|
+
allEdgeRows.push([fileNodeId, targetRow.id, edgeKind, 1.0, 0]);
|
|
992
1046
|
|
|
993
1047
|
if (!imp.reexport && isBarrelFile(resolvedPath)) {
|
|
994
1048
|
const resolvedSources = new Set();
|
|
@@ -1003,171 +1057,218 @@ export async function buildGraph(rootDir, opts = {}) {
|
|
|
1003
1057
|
resolvedSources.add(actualSource);
|
|
1004
1058
|
const actualRow = getNodeId.get(actualSource, 'file', actualSource, 0);
|
|
1005
1059
|
if (actualRow) {
|
|
1006
|
-
|
|
1060
|
+
allEdgeRows.push([
|
|
1007
1061
|
fileNodeId,
|
|
1008
1062
|
actualRow.id,
|
|
1009
1063
|
edgeKind === 'imports-type' ? 'imports-type' : 'imports',
|
|
1010
1064
|
0.9,
|
|
1011
1065
|
0,
|
|
1012
|
-
);
|
|
1066
|
+
]);
|
|
1013
1067
|
}
|
|
1014
1068
|
}
|
|
1015
1069
|
}
|
|
1016
1070
|
}
|
|
1017
1071
|
}
|
|
1018
1072
|
}
|
|
1073
|
+
}
|
|
1019
1074
|
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1075
|
+
// Call/receiver/extends/implements edges — native when available
|
|
1076
|
+
const native = engineName === 'native' ? loadNative() : null;
|
|
1077
|
+
if (native?.buildCallEdges) {
|
|
1078
|
+
const nativeFiles = [];
|
|
1079
|
+
for (const [relPath, symbols] of fileSymbols) {
|
|
1080
|
+
if (barrelOnlyFiles.has(relPath)) continue;
|
|
1081
|
+
const fileNodeRow = getNodeId.get(relPath, 'file', relPath, 0);
|
|
1082
|
+
if (!fileNodeRow) continue;
|
|
1083
|
+
|
|
1084
|
+
// Pre-resolve imported names (including barrel resolution)
|
|
1085
|
+
const importedNames = [];
|
|
1086
|
+
for (const imp of symbols.imports) {
|
|
1087
|
+
const resolvedPath = getResolved(path.join(rootDir, relPath), imp.source);
|
|
1088
|
+
for (const name of imp.names) {
|
|
1089
|
+
const cleanName = name.replace(/^\*\s+as\s+/, '');
|
|
1090
|
+
let targetFile = resolvedPath;
|
|
1091
|
+
if (isBarrelFile(resolvedPath)) {
|
|
1092
|
+
const actual = resolveBarrelExport(resolvedPath, cleanName);
|
|
1093
|
+
if (actual) targetFile = actual;
|
|
1094
|
+
}
|
|
1095
|
+
importedNames.push({ name: cleanName, file: targetFile });
|
|
1096
|
+
}
|
|
1027
1097
|
}
|
|
1098
|
+
|
|
1099
|
+
nativeFiles.push({
|
|
1100
|
+
file: relPath,
|
|
1101
|
+
fileNodeId: fileNodeRow.id,
|
|
1102
|
+
definitions: symbols.definitions.map((d) => ({
|
|
1103
|
+
name: d.name,
|
|
1104
|
+
kind: d.kind,
|
|
1105
|
+
line: d.line,
|
|
1106
|
+
endLine: d.endLine ?? null,
|
|
1107
|
+
})),
|
|
1108
|
+
calls: symbols.calls,
|
|
1109
|
+
importedNames,
|
|
1110
|
+
classes: symbols.classes,
|
|
1111
|
+
});
|
|
1028
1112
|
}
|
|
1029
1113
|
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
for (const
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1114
|
+
const nativeEdges = native.buildCallEdges(nativeFiles, allNodes, [...BUILTIN_RECEIVERS]);
|
|
1115
|
+
|
|
1116
|
+
for (const e of nativeEdges) {
|
|
1117
|
+
allEdgeRows.push([e.sourceId, e.targetId, e.kind, e.confidence, e.dynamic]);
|
|
1118
|
+
}
|
|
1119
|
+
} else {
|
|
1120
|
+
// JS fallback — call/receiver/extends/implements edges
|
|
1121
|
+
for (const [relPath, symbols] of fileSymbols) {
|
|
1122
|
+
if (barrelOnlyFiles.has(relPath)) continue;
|
|
1123
|
+
const fileNodeRow = getNodeId.get(relPath, 'file', relPath, 0);
|
|
1124
|
+
if (!fileNodeRow) continue;
|
|
1125
|
+
|
|
1126
|
+
// Build import name -> target file mapping
|
|
1127
|
+
const importedNames = new Map();
|
|
1128
|
+
for (const imp of symbols.imports) {
|
|
1129
|
+
const resolvedPath = getResolved(path.join(rootDir, relPath), imp.source);
|
|
1130
|
+
for (const name of imp.names) {
|
|
1131
|
+
const cleanName = name.replace(/^\*\s+as\s+/, '');
|
|
1132
|
+
importedNames.set(cleanName, resolvedPath);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// Call edges with confidence scoring — using pre-loaded lookup maps (N+1 fix)
|
|
1137
|
+
const seenCallEdges = new Set();
|
|
1138
|
+
for (const call of symbols.calls) {
|
|
1139
|
+
if (call.receiver && BUILTIN_RECEIVERS.has(call.receiver)) continue;
|
|
1140
|
+
let caller = null;
|
|
1141
|
+
let callerSpan = Infinity;
|
|
1142
|
+
for (const def of symbols.definitions) {
|
|
1143
|
+
if (def.line <= call.line) {
|
|
1144
|
+
const end = def.endLine || Infinity;
|
|
1145
|
+
if (call.line <= end) {
|
|
1146
|
+
const span = end - def.line;
|
|
1147
|
+
if (span < callerSpan) {
|
|
1148
|
+
const row = getNodeId.get(def.name, def.kind, relPath, def.line);
|
|
1149
|
+
if (row) {
|
|
1150
|
+
caller = row;
|
|
1151
|
+
callerSpan = span;
|
|
1152
|
+
}
|
|
1047
1153
|
}
|
|
1154
|
+
} else if (!caller) {
|
|
1155
|
+
const row = getNodeId.get(def.name, def.kind, relPath, def.line);
|
|
1156
|
+
if (row) caller = row;
|
|
1048
1157
|
}
|
|
1049
|
-
} else if (!caller) {
|
|
1050
|
-
// Fallback: def starts before call but call is past end
|
|
1051
|
-
// Only use if we haven't found an enclosing scope yet
|
|
1052
|
-
const row = getNodeId.get(def.name, def.kind, relPath, def.line);
|
|
1053
|
-
if (row) caller = row;
|
|
1054
1158
|
}
|
|
1055
1159
|
}
|
|
1056
|
-
|
|
1057
|
-
if (!caller) caller = fileNodeRow;
|
|
1160
|
+
if (!caller) caller = fileNodeRow;
|
|
1058
1161
|
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1162
|
+
const isDynamic = call.dynamic ? 1 : 0;
|
|
1163
|
+
let targets;
|
|
1164
|
+
const importedFrom = importedNames.get(call.name);
|
|
1062
1165
|
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
targets = nodesByNameAndFile.get(`${call.name}|${importedFrom}`) || [];
|
|
1166
|
+
if (importedFrom) {
|
|
1167
|
+
targets = nodesByNameAndFile.get(`${call.name}|${importedFrom}`) || [];
|
|
1066
1168
|
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1169
|
+
if (targets.length === 0 && isBarrelFile(importedFrom)) {
|
|
1170
|
+
const actualSource = resolveBarrelExport(importedFrom, call.name);
|
|
1171
|
+
if (actualSource) {
|
|
1172
|
+
targets = nodesByNameAndFile.get(`${call.name}|${actualSource}`) || [];
|
|
1173
|
+
}
|
|
1071
1174
|
}
|
|
1072
1175
|
}
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
// Method name match (e.g. ClassName.methodName)
|
|
1079
|
-
const methodCandidates = (nodesByName.get(call.name) || []).filter(
|
|
1080
|
-
(n) => n.name.endsWith(`.${call.name}`) && n.kind === 'method',
|
|
1081
|
-
);
|
|
1082
|
-
if (methodCandidates.length > 0) {
|
|
1083
|
-
targets = methodCandidates;
|
|
1084
|
-
} else if (
|
|
1085
|
-
!call.receiver ||
|
|
1086
|
-
call.receiver === 'this' ||
|
|
1087
|
-
call.receiver === 'self' ||
|
|
1088
|
-
call.receiver === 'super'
|
|
1089
|
-
) {
|
|
1090
|
-
// Scoped fallback — same-dir or parent-dir only, not global
|
|
1091
|
-
targets = (nodesByName.get(call.name) || []).filter(
|
|
1092
|
-
(n) => computeConfidence(relPath, n.file, null) >= 0.5,
|
|
1176
|
+
if (!targets || targets.length === 0) {
|
|
1177
|
+
targets = nodesByNameAndFile.get(`${call.name}|${relPath}`) || [];
|
|
1178
|
+
if (targets.length === 0) {
|
|
1179
|
+
const methodCandidates = (nodesByName.get(call.name) || []).filter(
|
|
1180
|
+
(n) => n.name.endsWith(`.${call.name}`) && n.kind === 'method',
|
|
1093
1181
|
);
|
|
1182
|
+
if (methodCandidates.length > 0) {
|
|
1183
|
+
targets = methodCandidates;
|
|
1184
|
+
} else if (
|
|
1185
|
+
!call.receiver ||
|
|
1186
|
+
call.receiver === 'this' ||
|
|
1187
|
+
call.receiver === 'self' ||
|
|
1188
|
+
call.receiver === 'super'
|
|
1189
|
+
) {
|
|
1190
|
+
targets = (nodesByName.get(call.name) || []).filter(
|
|
1191
|
+
(n) => computeConfidence(relPath, n.file, null) >= 0.5,
|
|
1192
|
+
);
|
|
1193
|
+
}
|
|
1094
1194
|
}
|
|
1095
|
-
// else: method call on a receiver — skip global fallback entirely
|
|
1096
1195
|
}
|
|
1097
|
-
}
|
|
1098
1196
|
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1197
|
+
if (targets.length > 1) {
|
|
1198
|
+
targets.sort((a, b) => {
|
|
1199
|
+
const confA = computeConfidence(relPath, a.file, importedFrom);
|
|
1200
|
+
const confB = computeConfidence(relPath, b.file, importedFrom);
|
|
1201
|
+
return confB - confA;
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1106
1204
|
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1205
|
+
for (const t of targets) {
|
|
1206
|
+
const edgeKey = `${caller.id}|${t.id}`;
|
|
1207
|
+
if (t.id !== caller.id && !seenCallEdges.has(edgeKey)) {
|
|
1208
|
+
seenCallEdges.add(edgeKey);
|
|
1209
|
+
const confidence = computeConfidence(relPath, t.file, importedFrom);
|
|
1210
|
+
allEdgeRows.push([caller.id, t.id, 'calls', confidence, isDynamic]);
|
|
1211
|
+
}
|
|
1113
1212
|
}
|
|
1114
|
-
}
|
|
1115
1213
|
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1214
|
+
// Receiver edge: caller → receiver type node
|
|
1215
|
+
if (
|
|
1216
|
+
call.receiver &&
|
|
1217
|
+
!BUILTIN_RECEIVERS.has(call.receiver) &&
|
|
1218
|
+
call.receiver !== 'this' &&
|
|
1219
|
+
call.receiver !== 'self' &&
|
|
1220
|
+
call.receiver !== 'super'
|
|
1221
|
+
) {
|
|
1222
|
+
const receiverKinds = new Set(['class', 'struct', 'interface', 'type', 'module']);
|
|
1223
|
+
const samefile = nodesByNameAndFile.get(`${call.receiver}|${relPath}`) || [];
|
|
1224
|
+
const candidates =
|
|
1225
|
+
samefile.length > 0 ? samefile : nodesByName.get(call.receiver) || [];
|
|
1226
|
+
const receiverNodes = candidates.filter((n) => receiverKinds.has(n.kind));
|
|
1227
|
+
if (receiverNodes.length > 0 && caller) {
|
|
1228
|
+
const recvTarget = receiverNodes[0];
|
|
1229
|
+
const recvKey = `recv|${caller.id}|${recvTarget.id}`;
|
|
1230
|
+
if (!seenCallEdges.has(recvKey)) {
|
|
1231
|
+
seenCallEdges.add(recvKey);
|
|
1232
|
+
allEdgeRows.push([caller.id, recvTarget.id, 'receiver', 0.7, 0]);
|
|
1233
|
+
}
|
|
1135
1234
|
}
|
|
1136
1235
|
}
|
|
1137
1236
|
}
|
|
1138
|
-
}
|
|
1139
1237
|
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1238
|
+
// Class extends edges
|
|
1239
|
+
for (const cls of symbols.classes) {
|
|
1240
|
+
if (cls.extends) {
|
|
1241
|
+
const sourceRow = (nodesByNameAndFile.get(`${cls.name}|${relPath}`) || []).find(
|
|
1242
|
+
(n) => n.kind === 'class',
|
|
1243
|
+
);
|
|
1244
|
+
const targetCandidates = nodesByName.get(cls.extends) || [];
|
|
1245
|
+
const targetRows = targetCandidates.filter((n) => n.kind === 'class');
|
|
1246
|
+
if (sourceRow) {
|
|
1247
|
+
for (const t of targetRows) {
|
|
1248
|
+
allEdgeRows.push([sourceRow.id, t.id, 'extends', 1.0, 0]);
|
|
1249
|
+
}
|
|
1151
1250
|
}
|
|
1152
1251
|
}
|
|
1153
|
-
}
|
|
1154
1252
|
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1253
|
+
if (cls.implements) {
|
|
1254
|
+
const sourceRow = (nodesByNameAndFile.get(`${cls.name}|${relPath}`) || []).find(
|
|
1255
|
+
(n) => n.kind === 'class',
|
|
1256
|
+
);
|
|
1257
|
+
const targetCandidates = nodesByName.get(cls.implements) || [];
|
|
1258
|
+
const targetRows = targetCandidates.filter(
|
|
1259
|
+
(n) => n.kind === 'interface' || n.kind === 'class',
|
|
1260
|
+
);
|
|
1261
|
+
if (sourceRow) {
|
|
1262
|
+
for (const t of targetRows) {
|
|
1263
|
+
allEdgeRows.push([sourceRow.id, t.id, 'implements', 1.0, 0]);
|
|
1264
|
+
}
|
|
1166
1265
|
}
|
|
1167
1266
|
}
|
|
1168
1267
|
}
|
|
1169
1268
|
}
|
|
1170
1269
|
}
|
|
1270
|
+
|
|
1271
|
+
batchInsertEdges(allEdgeRows);
|
|
1171
1272
|
});
|
|
1172
1273
|
buildEdges();
|
|
1173
1274
|
_t.edgesMs = performance.now() - _t.edges0;
|
|
@@ -1175,8 +1276,8 @@ export async function buildGraph(rootDir, opts = {}) {
|
|
|
1175
1276
|
// Build line count map for structure metrics (prefer cached _lineCount from parser)
|
|
1176
1277
|
const lineCountMap = new Map();
|
|
1177
1278
|
for (const [relPath, symbols] of fileSymbols) {
|
|
1178
|
-
if (symbols._lineCount) {
|
|
1179
|
-
lineCountMap.set(relPath, symbols._lineCount);
|
|
1279
|
+
if (symbols.lineCount ?? symbols._lineCount) {
|
|
1280
|
+
lineCountMap.set(relPath, symbols.lineCount ?? symbols._lineCount);
|
|
1180
1281
|
} else {
|
|
1181
1282
|
const absPath = path.join(rootDir, relPath);
|
|
1182
1283
|
try {
|
|
@@ -1317,16 +1418,47 @@ export async function buildGraph(rootDir, opts = {}) {
|
|
|
1317
1418
|
_t.complexityMs = performance.now() - _t.complexity0;
|
|
1318
1419
|
|
|
1319
1420
|
// Pre-parse files missing WASM trees (native builds) so CFG + dataflow
|
|
1320
|
-
// share a single parse pass instead of each creating parsers independently
|
|
1421
|
+
// share a single parse pass instead of each creating parsers independently.
|
|
1422
|
+
// Skip entirely when native engine already provides CFG + dataflow data.
|
|
1321
1423
|
if (opts.cfg !== false || opts.dataflow !== false) {
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1424
|
+
const needsCfg = opts.cfg !== false;
|
|
1425
|
+
const needsDataflow = opts.dataflow !== false;
|
|
1426
|
+
|
|
1427
|
+
let needsWasmTrees = false;
|
|
1428
|
+
for (const [, symbols] of astComplexitySymbols) {
|
|
1429
|
+
if (symbols._tree) continue; // already has a tree
|
|
1430
|
+
// CFG: need tree if any function/method def lacks native CFG
|
|
1431
|
+
if (needsCfg) {
|
|
1432
|
+
const fnDefs = (symbols.definitions || []).filter(
|
|
1433
|
+
(d) => (d.kind === 'function' || d.kind === 'method') && d.line,
|
|
1434
|
+
);
|
|
1435
|
+
if (
|
|
1436
|
+
fnDefs.length > 0 &&
|
|
1437
|
+
!fnDefs.every((d) => d.cfg === null || Array.isArray(d.cfg?.blocks))
|
|
1438
|
+
) {
|
|
1439
|
+
needsWasmTrees = true;
|
|
1440
|
+
break;
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
// Dataflow: need tree if file lacks native dataflow
|
|
1444
|
+
if (needsDataflow && !symbols.dataflow) {
|
|
1445
|
+
needsWasmTrees = true;
|
|
1446
|
+
break;
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
if (needsWasmTrees) {
|
|
1451
|
+
_t.wasmPre0 = performance.now();
|
|
1452
|
+
try {
|
|
1453
|
+
const { ensureWasmTrees } = await import('./parser.js');
|
|
1454
|
+
await ensureWasmTrees(astComplexitySymbols, rootDir);
|
|
1455
|
+
} catch (err) {
|
|
1456
|
+
debug(`WASM pre-parse failed: ${err.message}`);
|
|
1457
|
+
}
|
|
1458
|
+
_t.wasmPreMs = performance.now() - _t.wasmPre0;
|
|
1459
|
+
} else {
|
|
1460
|
+
_t.wasmPreMs = 0;
|
|
1328
1461
|
}
|
|
1329
|
-
_t.wasmPreMs = performance.now() - _t.wasmPre0;
|
|
1330
1462
|
}
|
|
1331
1463
|
|
|
1332
1464
|
// CFG analysis (skip with --no-cfg)
|
|
@@ -1400,6 +1532,29 @@ export async function buildGraph(rootDir, opts = {}) {
|
|
|
1400
1532
|
}
|
|
1401
1533
|
}
|
|
1402
1534
|
|
|
1535
|
+
// Warn about unused exports (exported but zero cross-file consumers)
|
|
1536
|
+
try {
|
|
1537
|
+
const unusedCount = db
|
|
1538
|
+
.prepare(
|
|
1539
|
+
`SELECT COUNT(*) as c FROM nodes
|
|
1540
|
+
WHERE exported = 1 AND kind != 'file'
|
|
1541
|
+
AND id NOT IN (
|
|
1542
|
+
SELECT DISTINCT e.target_id FROM edges e
|
|
1543
|
+
JOIN nodes caller ON e.source_id = caller.id
|
|
1544
|
+
JOIN nodes target ON e.target_id = target.id
|
|
1545
|
+
WHERE e.kind = 'calls' AND caller.file != target.file
|
|
1546
|
+
)`,
|
|
1547
|
+
)
|
|
1548
|
+
.get().c;
|
|
1549
|
+
if (unusedCount > 0) {
|
|
1550
|
+
warn(
|
|
1551
|
+
`${unusedCount} exported symbol${unusedCount > 1 ? 's have' : ' has'} zero cross-file consumers. Run "codegraph exports <file> --unused" to inspect.`,
|
|
1552
|
+
);
|
|
1553
|
+
}
|
|
1554
|
+
} catch {
|
|
1555
|
+
/* exported column may not exist on older DBs */
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1403
1558
|
// Persist build metadata for mismatch detection
|
|
1404
1559
|
try {
|
|
1405
1560
|
setBuildMeta(db, {
|