@optave/codegraph 3.0.4 → 3.1.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 +59 -52
- package/grammars/tree-sitter-go.wasm +0 -0
- package/package.json +9 -10
- package/src/ast-analysis/rules/csharp.js +201 -0
- package/src/ast-analysis/rules/go.js +182 -0
- package/src/ast-analysis/rules/index.js +82 -0
- package/src/ast-analysis/rules/java.js +175 -0
- package/src/ast-analysis/rules/javascript.js +246 -0
- package/src/ast-analysis/rules/php.js +219 -0
- package/src/ast-analysis/rules/python.js +196 -0
- package/src/ast-analysis/rules/ruby.js +204 -0
- package/src/ast-analysis/rules/rust.js +173 -0
- package/src/ast-analysis/shared.js +223 -0
- package/src/ast.js +15 -28
- package/src/audit.js +4 -5
- package/src/boundaries.js +1 -1
- package/src/branch-compare.js +84 -79
- package/src/builder.js +274 -159
- package/src/cfg.js +111 -341
- package/src/check.js +3 -3
- package/src/cli.js +122 -167
- package/src/cochange.js +1 -1
- package/src/communities.js +13 -16
- package/src/complexity.js +196 -1239
- package/src/cycles.js +1 -1
- package/src/dataflow.js +274 -697
- package/src/db/connection.js +88 -0
- package/src/db/migrations.js +312 -0
- package/src/db/query-builder.js +280 -0
- package/src/db/repository.js +134 -0
- package/src/db.js +19 -392
- package/src/embedder.js +145 -141
- package/src/export.js +1 -1
- package/src/flow.js +160 -228
- package/src/index.js +36 -2
- package/src/kinds.js +49 -0
- package/src/manifesto.js +3 -8
- package/src/mcp.js +97 -20
- package/src/owners.js +132 -132
- package/src/parser.js +58 -131
- package/src/queries-cli.js +866 -0
- package/src/queries.js +1356 -2261
- package/src/resolve.js +11 -2
- package/src/result-formatter.js +21 -0
- package/src/sequence.js +364 -0
- package/src/structure.js +200 -199
- package/src/test-filter.js +7 -0
- package/src/triage.js +120 -162
- package/src/viewer.js +1 -1
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
|
|
|
@@ -672,15 +677,38 @@ export async function buildGraph(rootDir, opts = {}) {
|
|
|
672
677
|
}
|
|
673
678
|
}
|
|
674
679
|
|
|
675
|
-
const insertNode = db.prepare(
|
|
676
|
-
'INSERT OR IGNORE INTO nodes (name, kind, file, line, end_line, parent_id) VALUES (?, ?, ?, ?, ?, ?)',
|
|
677
|
-
);
|
|
678
680
|
const getNodeId = db.prepare(
|
|
679
681
|
'SELECT id FROM nodes WHERE name = ? AND kind = ? AND file = ? AND line = ?',
|
|
680
682
|
);
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
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
|
+
}
|
|
684
712
|
|
|
685
713
|
// Prepare hash upsert (with size column from migration v4)
|
|
686
714
|
let upsertHash;
|
|
@@ -727,57 +755,76 @@ export async function buildGraph(rootDir, opts = {}) {
|
|
|
727
755
|
const bulkGetNodeIds = db.prepare('SELECT id, name, kind, line FROM nodes WHERE file = ?');
|
|
728
756
|
|
|
729
757
|
const insertAll = db.transaction(() => {
|
|
758
|
+
// Phase 1: Batch insert all file nodes + definitions + exports
|
|
759
|
+
const phase1Rows = [];
|
|
730
760
|
for (const [relPath, symbols] of allSymbols) {
|
|
731
761
|
fileSymbols.set(relPath, symbols);
|
|
732
|
-
|
|
733
|
-
// Phase 1: Insert file node + definitions + exports (no children yet)
|
|
734
|
-
insertNode.run(relPath, 'file', relPath, 0, null, null);
|
|
762
|
+
phase1Rows.push([relPath, 'file', relPath, 0, null, null]);
|
|
735
763
|
for (const def of symbols.definitions) {
|
|
736
|
-
|
|
764
|
+
phase1Rows.push([def.name, def.kind, relPath, def.line, def.endLine || null, null]);
|
|
737
765
|
}
|
|
738
766
|
for (const exp of symbols.exports) {
|
|
739
|
-
|
|
767
|
+
phase1Rows.push([exp.name, exp.kind, relPath, exp.line, null, null]);
|
|
740
768
|
}
|
|
769
|
+
}
|
|
770
|
+
batchInsertNodes(phase1Rows);
|
|
741
771
|
|
|
742
|
-
|
|
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) {
|
|
777
|
+
for (const exp of symbols.exports) {
|
|
778
|
+
markExported.run(exp.name, exp.kind, relPath, exp.line);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// Phase 3: Batch insert children (needs parent IDs from Phase 2)
|
|
783
|
+
const childRows = [];
|
|
784
|
+
for (const [relPath, symbols] of allSymbols) {
|
|
743
785
|
const nodeIdMap = new Map();
|
|
744
786
|
for (const row of bulkGetNodeIds.all(relPath)) {
|
|
745
787
|
nodeIdMap.set(`${row.name}|${row.kind}|${row.line}`, row.id);
|
|
746
788
|
}
|
|
747
|
-
|
|
748
|
-
// Phase 3: Insert children with parent_id from the map
|
|
749
789
|
for (const def of symbols.definitions) {
|
|
750
790
|
if (!def.children?.length) continue;
|
|
751
791
|
const defId = nodeIdMap.get(`${def.name}|${def.kind}|${def.line}`);
|
|
752
792
|
if (!defId) continue;
|
|
753
793
|
for (const child of def.children) {
|
|
754
|
-
|
|
794
|
+
childRows.push([
|
|
795
|
+
child.name,
|
|
796
|
+
child.kind,
|
|
797
|
+
relPath,
|
|
798
|
+
child.line,
|
|
799
|
+
child.endLine || null,
|
|
800
|
+
defId,
|
|
801
|
+
]);
|
|
755
802
|
}
|
|
756
803
|
}
|
|
804
|
+
}
|
|
805
|
+
batchInsertNodes(childRows);
|
|
757
806
|
|
|
758
|
-
|
|
759
|
-
|
|
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();
|
|
760
812
|
for (const row of bulkGetNodeIds.all(relPath)) {
|
|
761
813
|
nodeIdMap.set(`${row.name}|${row.kind}|${row.line}`, row.id);
|
|
762
814
|
}
|
|
763
|
-
|
|
764
|
-
// Phase 5: Insert edges using the cached ID map
|
|
765
815
|
const fileId = nodeIdMap.get(`${relPath}|file|0`);
|
|
766
816
|
for (const def of symbols.definitions) {
|
|
767
817
|
const defId = nodeIdMap.get(`${def.name}|${def.kind}|${def.line}`);
|
|
768
|
-
// File → top-level definition contains edge
|
|
769
818
|
if (fileId && defId) {
|
|
770
|
-
|
|
819
|
+
edgeRows.push([fileId, defId, 'contains', 1.0, 0]);
|
|
771
820
|
}
|
|
772
821
|
if (def.children?.length && defId) {
|
|
773
822
|
for (const child of def.children) {
|
|
774
823
|
const childId = nodeIdMap.get(`${child.name}|${child.kind}|${child.line}`);
|
|
775
824
|
if (childId) {
|
|
776
|
-
|
|
777
|
-
insertEdge.run(defId, childId, 'contains', 1.0, 0);
|
|
778
|
-
// Parameter → parent parameter_of edge (inverse direction)
|
|
825
|
+
edgeRows.push([defId, childId, 'contains', 1.0, 0]);
|
|
779
826
|
if (child.kind === 'parameter') {
|
|
780
|
-
|
|
827
|
+
edgeRows.push([childId, defId, 'parameter_of', 1.0, 0]);
|
|
781
828
|
}
|
|
782
829
|
}
|
|
783
830
|
}
|
|
@@ -812,6 +859,7 @@ export async function buildGraph(rootDir, opts = {}) {
|
|
|
812
859
|
}
|
|
813
860
|
}
|
|
814
861
|
}
|
|
862
|
+
batchInsertEdges(edgeRows);
|
|
815
863
|
|
|
816
864
|
// Also update metadata-only entries (self-heal mtime/size without re-parse)
|
|
817
865
|
if (upsertHash) {
|
|
@@ -848,7 +896,7 @@ export async function buildGraph(rootDir, opts = {}) {
|
|
|
848
896
|
batchInputs.push({ fromFile: absFile, importSource: imp.source });
|
|
849
897
|
}
|
|
850
898
|
}
|
|
851
|
-
const batchResolved = resolveImportsBatch(batchInputs, rootDir, aliases);
|
|
899
|
+
const batchResolved = resolveImportsBatch(batchInputs, rootDir, aliases, files);
|
|
852
900
|
_t.resolveMs = performance.now() - _t.resolve0;
|
|
853
901
|
|
|
854
902
|
function getResolved(absFile, importSource) {
|
|
@@ -961,7 +1009,7 @@ export async function buildGraph(rootDir, opts = {}) {
|
|
|
961
1009
|
// N+1 optimization: pre-load all nodes into a lookup map for edge building
|
|
962
1010
|
const allNodes = db
|
|
963
1011
|
.prepare(
|
|
964
|
-
`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')`,
|
|
965
1013
|
)
|
|
966
1014
|
.all();
|
|
967
1015
|
const nodesByName = new Map();
|
|
@@ -976,9 +1024,11 @@ export async function buildGraph(rootDir, opts = {}) {
|
|
|
976
1024
|
nodesByNameAndFile.get(key).push(node);
|
|
977
1025
|
}
|
|
978
1026
|
|
|
979
|
-
// Second pass: build edges
|
|
1027
|
+
// Second pass: build edges (accumulated and batch-inserted)
|
|
980
1028
|
_t.edges0 = performance.now();
|
|
981
1029
|
const buildEdges = db.transaction(() => {
|
|
1030
|
+
const allEdgeRows = [];
|
|
1031
|
+
|
|
982
1032
|
for (const [relPath, symbols] of fileSymbols) {
|
|
983
1033
|
// Skip barrel-only files — loaded for resolution, edges already in DB
|
|
984
1034
|
if (barrelOnlyFiles.has(relPath)) continue;
|
|
@@ -992,7 +1042,7 @@ export async function buildGraph(rootDir, opts = {}) {
|
|
|
992
1042
|
const targetRow = getNodeId.get(resolvedPath, 'file', resolvedPath, 0);
|
|
993
1043
|
if (targetRow) {
|
|
994
1044
|
const edgeKind = imp.reexport ? 'reexports' : imp.typeOnly ? 'imports-type' : 'imports';
|
|
995
|
-
|
|
1045
|
+
allEdgeRows.push([fileNodeId, targetRow.id, edgeKind, 1.0, 0]);
|
|
996
1046
|
|
|
997
1047
|
if (!imp.reexport && isBarrelFile(resolvedPath)) {
|
|
998
1048
|
const resolvedSources = new Set();
|
|
@@ -1007,171 +1057,218 @@ export async function buildGraph(rootDir, opts = {}) {
|
|
|
1007
1057
|
resolvedSources.add(actualSource);
|
|
1008
1058
|
const actualRow = getNodeId.get(actualSource, 'file', actualSource, 0);
|
|
1009
1059
|
if (actualRow) {
|
|
1010
|
-
|
|
1060
|
+
allEdgeRows.push([
|
|
1011
1061
|
fileNodeId,
|
|
1012
1062
|
actualRow.id,
|
|
1013
1063
|
edgeKind === 'imports-type' ? 'imports-type' : 'imports',
|
|
1014
1064
|
0.9,
|
|
1015
1065
|
0,
|
|
1016
|
-
);
|
|
1066
|
+
]);
|
|
1017
1067
|
}
|
|
1018
1068
|
}
|
|
1019
1069
|
}
|
|
1020
1070
|
}
|
|
1021
1071
|
}
|
|
1022
1072
|
}
|
|
1073
|
+
}
|
|
1023
1074
|
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
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
|
+
}
|
|
1031
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
|
+
});
|
|
1032
1112
|
}
|
|
1033
1113
|
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
for (const
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
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
|
+
}
|
|
1051
1153
|
}
|
|
1154
|
+
} else if (!caller) {
|
|
1155
|
+
const row = getNodeId.get(def.name, def.kind, relPath, def.line);
|
|
1156
|
+
if (row) caller = row;
|
|
1052
1157
|
}
|
|
1053
|
-
} else if (!caller) {
|
|
1054
|
-
// Fallback: def starts before call but call is past end
|
|
1055
|
-
// Only use if we haven't found an enclosing scope yet
|
|
1056
|
-
const row = getNodeId.get(def.name, def.kind, relPath, def.line);
|
|
1057
|
-
if (row) caller = row;
|
|
1058
1158
|
}
|
|
1059
1159
|
}
|
|
1060
|
-
|
|
1061
|
-
if (!caller) caller = fileNodeRow;
|
|
1160
|
+
if (!caller) caller = fileNodeRow;
|
|
1062
1161
|
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1162
|
+
const isDynamic = call.dynamic ? 1 : 0;
|
|
1163
|
+
let targets;
|
|
1164
|
+
const importedFrom = importedNames.get(call.name);
|
|
1066
1165
|
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
targets = nodesByNameAndFile.get(`${call.name}|${importedFrom}`) || [];
|
|
1166
|
+
if (importedFrom) {
|
|
1167
|
+
targets = nodesByNameAndFile.get(`${call.name}|${importedFrom}`) || [];
|
|
1070
1168
|
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
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
|
+
}
|
|
1075
1174
|
}
|
|
1076
1175
|
}
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
// Method name match (e.g. ClassName.methodName)
|
|
1083
|
-
const methodCandidates = (nodesByName.get(call.name) || []).filter(
|
|
1084
|
-
(n) => n.name.endsWith(`.${call.name}`) && n.kind === 'method',
|
|
1085
|
-
);
|
|
1086
|
-
if (methodCandidates.length > 0) {
|
|
1087
|
-
targets = methodCandidates;
|
|
1088
|
-
} else if (
|
|
1089
|
-
!call.receiver ||
|
|
1090
|
-
call.receiver === 'this' ||
|
|
1091
|
-
call.receiver === 'self' ||
|
|
1092
|
-
call.receiver === 'super'
|
|
1093
|
-
) {
|
|
1094
|
-
// Scoped fallback — same-dir or parent-dir only, not global
|
|
1095
|
-
targets = (nodesByName.get(call.name) || []).filter(
|
|
1096
|
-
(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',
|
|
1097
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
|
+
}
|
|
1098
1194
|
}
|
|
1099
|
-
// else: method call on a receiver — skip global fallback entirely
|
|
1100
1195
|
}
|
|
1101
|
-
}
|
|
1102
1196
|
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
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
|
+
}
|
|
1110
1204
|
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
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
|
+
}
|
|
1117
1212
|
}
|
|
1118
|
-
}
|
|
1119
1213
|
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
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
|
+
}
|
|
1139
1234
|
}
|
|
1140
1235
|
}
|
|
1141
1236
|
}
|
|
1142
|
-
}
|
|
1143
1237
|
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
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
|
+
}
|
|
1155
1250
|
}
|
|
1156
1251
|
}
|
|
1157
|
-
}
|
|
1158
1252
|
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
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
|
+
}
|
|
1170
1265
|
}
|
|
1171
1266
|
}
|
|
1172
1267
|
}
|
|
1173
1268
|
}
|
|
1174
1269
|
}
|
|
1270
|
+
|
|
1271
|
+
batchInsertEdges(allEdgeRows);
|
|
1175
1272
|
});
|
|
1176
1273
|
buildEdges();
|
|
1177
1274
|
_t.edgesMs = performance.now() - _t.edges0;
|
|
@@ -1179,8 +1276,8 @@ export async function buildGraph(rootDir, opts = {}) {
|
|
|
1179
1276
|
// Build line count map for structure metrics (prefer cached _lineCount from parser)
|
|
1180
1277
|
const lineCountMap = new Map();
|
|
1181
1278
|
for (const [relPath, symbols] of fileSymbols) {
|
|
1182
|
-
if (symbols._lineCount) {
|
|
1183
|
-
lineCountMap.set(relPath, symbols._lineCount);
|
|
1279
|
+
if (symbols.lineCount ?? symbols._lineCount) {
|
|
1280
|
+
lineCountMap.set(relPath, symbols.lineCount ?? symbols._lineCount);
|
|
1184
1281
|
} else {
|
|
1185
1282
|
const absPath = path.join(rootDir, relPath);
|
|
1186
1283
|
try {
|
|
@@ -1351,16 +1448,12 @@ export async function buildGraph(rootDir, opts = {}) {
|
|
|
1351
1448
|
}
|
|
1352
1449
|
|
|
1353
1450
|
if (needsWasmTrees) {
|
|
1354
|
-
_t.wasmPre0 = performance.now();
|
|
1355
1451
|
try {
|
|
1356
1452
|
const { ensureWasmTrees } = await import('./parser.js');
|
|
1357
1453
|
await ensureWasmTrees(astComplexitySymbols, rootDir);
|
|
1358
1454
|
} catch (err) {
|
|
1359
1455
|
debug(`WASM pre-parse failed: ${err.message}`);
|
|
1360
1456
|
}
|
|
1361
|
-
_t.wasmPreMs = performance.now() - _t.wasmPre0;
|
|
1362
|
-
} else {
|
|
1363
|
-
_t.wasmPreMs = 0;
|
|
1364
1457
|
}
|
|
1365
1458
|
}
|
|
1366
1459
|
|
|
@@ -1435,6 +1528,29 @@ export async function buildGraph(rootDir, opts = {}) {
|
|
|
1435
1528
|
}
|
|
1436
1529
|
}
|
|
1437
1530
|
|
|
1531
|
+
// Warn about unused exports (exported but zero cross-file consumers)
|
|
1532
|
+
try {
|
|
1533
|
+
const unusedCount = db
|
|
1534
|
+
.prepare(
|
|
1535
|
+
`SELECT COUNT(*) as c FROM nodes
|
|
1536
|
+
WHERE exported = 1 AND kind != 'file'
|
|
1537
|
+
AND id NOT IN (
|
|
1538
|
+
SELECT DISTINCT e.target_id FROM edges e
|
|
1539
|
+
JOIN nodes caller ON e.source_id = caller.id
|
|
1540
|
+
JOIN nodes target ON e.target_id = target.id
|
|
1541
|
+
WHERE e.kind = 'calls' AND caller.file != target.file
|
|
1542
|
+
)`,
|
|
1543
|
+
)
|
|
1544
|
+
.get().c;
|
|
1545
|
+
if (unusedCount > 0) {
|
|
1546
|
+
warn(
|
|
1547
|
+
`${unusedCount} exported symbol${unusedCount > 1 ? 's have' : ' has'} zero cross-file consumers. Run "codegraph exports <file> --unused" to inspect.`,
|
|
1548
|
+
);
|
|
1549
|
+
}
|
|
1550
|
+
} catch {
|
|
1551
|
+
/* exported column may not exist on older DBs */
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1438
1554
|
// Persist build metadata for mismatch detection
|
|
1439
1555
|
try {
|
|
1440
1556
|
setBuildMeta(db, {
|
|
@@ -1481,7 +1597,6 @@ export async function buildGraph(rootDir, opts = {}) {
|
|
|
1481
1597
|
rolesMs: +_t.rolesMs.toFixed(1),
|
|
1482
1598
|
astMs: +_t.astMs.toFixed(1),
|
|
1483
1599
|
complexityMs: +_t.complexityMs.toFixed(1),
|
|
1484
|
-
...(_t.wasmPreMs != null && { wasmPreMs: +_t.wasmPreMs.toFixed(1) }),
|
|
1485
1600
|
...(_t.cfgMs != null && { cfgMs: +_t.cfgMs.toFixed(1) }),
|
|
1486
1601
|
...(_t.dataflowMs != null && { dataflowMs: +_t.dataflowMs.toFixed(1) }),
|
|
1487
1602
|
},
|