-
Notifications
You must be signed in to change notification settings - Fork 12
Expand file tree
/
Copy pathincremental.js
More file actions
178 lines (161 loc) · 5.57 KB
/
incremental.js
File metadata and controls
178 lines (161 loc) · 5.57 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
/**
* Incremental single-file rebuild — used by watch mode.
*
* Reuses pipeline helpers instead of duplicating node insertion and edge building
* logic from the main builder. This eliminates the watcher.js divergence (ROADMAP 3.9).
*/
import fs from 'node:fs';
import path from 'node:path';
import { normalizePath } from '../constants.js';
import { warn } from '../logger.js';
import { parseFileIncremental } from '../parser.js';
import { computeConfidence, resolveImportPath } from '../resolve.js';
import { BUILTIN_RECEIVERS, readFileSafe } from './helpers.js';
/**
* Parse a single file and update the database incrementally.
*
* @param {import('better-sqlite3').Database} db
* @param {string} rootDir - Absolute root directory
* @param {string} filePath - Absolute file path
* @param {object} stmts - Prepared DB statements
* @param {object} engineOpts - Engine options
* @param {object|null} cache - Parse tree cache (native only)
* @param {object} [options]
* @param {Function} [options.diffSymbols] - Symbol diff function
* @returns {Promise<object|null>} Update result or null on failure
*/
export async function rebuildFile(_db, rootDir, filePath, stmts, engineOpts, cache, options = {}) {
const { diffSymbols } = options;
const relPath = normalizePath(path.relative(rootDir, filePath));
const oldNodes = stmts.countNodes.get(relPath)?.c || 0;
const oldSymbols = diffSymbols ? stmts.listSymbols.all(relPath) : [];
stmts.deleteEdgesForFile.run(relPath);
stmts.deleteNodes.run(relPath);
if (!fs.existsSync(filePath)) {
if (cache) cache.remove(filePath);
const symbolDiff = diffSymbols ? diffSymbols(oldSymbols, []) : null;
return {
file: relPath,
nodesAdded: 0,
nodesRemoved: oldNodes,
edgesAdded: 0,
deleted: true,
event: 'deleted',
symbolDiff,
nodesBefore: oldNodes,
nodesAfter: 0,
};
}
let code;
try {
code = readFileSafe(filePath);
} catch (err) {
warn(`Cannot read ${relPath}: ${err.message}`);
return null;
}
const symbols = await parseFileIncremental(cache, filePath, code, engineOpts);
if (!symbols) return null;
// Insert nodes
stmts.insertNode.run(relPath, 'file', relPath, 0, null);
for (const def of symbols.definitions) {
stmts.insertNode.run(def.name, def.kind, relPath, def.line, def.endLine || null);
}
for (const exp of symbols.exports) {
stmts.insertNode.run(exp.name, exp.kind, relPath, exp.line, null);
}
const newNodes = stmts.countNodes.get(relPath)?.c || 0;
const newSymbols = diffSymbols ? stmts.listSymbols.all(relPath) : [];
let edgesAdded = 0;
const fileNodeRow = stmts.getNodeId.get(relPath, 'file', relPath, 0);
if (!fileNodeRow)
return { file: relPath, nodesAdded: newNodes, nodesRemoved: oldNodes, edgesAdded: 0 };
const fileNodeId = fileNodeRow.id;
// Load aliases for import resolution
const aliases = { baseUrl: null, paths: {} };
// Import edges
for (const imp of symbols.imports) {
const resolvedPath = resolveImportPath(
path.join(rootDir, relPath),
imp.source,
rootDir,
aliases,
);
const targetRow = stmts.getNodeId.get(resolvedPath, 'file', resolvedPath, 0);
if (targetRow) {
const edgeKind = imp.reexport ? 'reexports' : imp.typeOnly ? 'imports-type' : 'imports';
stmts.insertEdge.run(fileNodeId, targetRow.id, edgeKind, 1.0, 0);
edgesAdded++;
}
}
// Build import name → resolved file mapping
const importedNames = new Map();
for (const imp of symbols.imports) {
const resolvedPath = resolveImportPath(
path.join(rootDir, relPath),
imp.source,
rootDir,
aliases,
);
for (const name of imp.names) {
importedNames.set(name.replace(/^\*\s+as\s+/, ''), resolvedPath);
}
}
// Call edges
for (const call of symbols.calls) {
if (call.receiver && BUILTIN_RECEIVERS.has(call.receiver)) continue;
let caller = null;
let callerSpan = Infinity;
for (const def of symbols.definitions) {
if (def.line <= call.line) {
const end = def.endLine || Infinity;
if (call.line <= end) {
const span = end - def.line;
if (span < callerSpan) {
const row = stmts.getNodeId.get(def.name, def.kind, relPath, def.line);
if (row) {
caller = row;
callerSpan = span;
}
}
} else if (!caller) {
const row = stmts.getNodeId.get(def.name, def.kind, relPath, def.line);
if (row) caller = row;
}
}
}
if (!caller) caller = fileNodeRow;
const importedFrom = importedNames.get(call.name);
let targets;
if (importedFrom) {
targets = stmts.findNodeInFile.all(call.name, importedFrom);
}
if (!targets || targets.length === 0) {
targets = stmts.findNodeInFile.all(call.name, relPath);
if (targets.length === 0) {
targets = stmts.findNodeByName.all(call.name);
}
}
for (const t of targets) {
if (t.id !== caller.id) {
const confidence = importedFrom
? computeConfidence(relPath, t.file, importedFrom)
: computeConfidence(relPath, t.file, null);
stmts.insertEdge.run(caller.id, t.id, 'calls', confidence, call.dynamic ? 1 : 0);
edgesAdded++;
}
}
}
const symbolDiff = diffSymbols ? diffSymbols(oldSymbols, newSymbols) : null;
const event = oldNodes === 0 ? 'added' : 'modified';
return {
file: relPath,
nodesAdded: newNodes,
nodesRemoved: oldNodes,
edgesAdded,
deleted: false,
event,
symbolDiff,
nodesBefore: oldNodes,
nodesAfter: newNodes,
};
}