-
Notifications
You must be signed in to change notification settings - Fork 12
Expand file tree
/
Copy pathnodes.js
More file actions
333 lines (304 loc) · 10 KB
/
nodes.js
File metadata and controls
333 lines (304 loc) · 10 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
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
import { ConfigError } from '../../shared/errors.js';
import { EVERY_SYMBOL_KIND, VALID_ROLES } from '../../shared/kinds.js';
import { buildFileConditionSQL, NodeQuery } from '../query-builder.js';
import { cachedStmt } from './cached-stmt.js';
// ─── Query-builder based lookups (moved from src/db/repository.js) ─────
/**
* Find nodes matching a name pattern, with fan-in count.
* @param {object} db
* @param {string} namePattern - LIKE pattern (already wrapped with %)
* @param {object} [opts]
* @param {string[]} [opts.kinds]
* @param {string} [opts.file]
* @returns {object[]}
*/
export function findNodesWithFanIn(db, namePattern, opts = {}) {
const q = new NodeQuery()
.select('n.*, COALESCE(fi.cnt, 0) AS fan_in')
.withFanIn()
.where('n.name LIKE ?', namePattern);
if (opts.kinds) {
q.kinds(opts.kinds);
}
if (opts.file) {
q.fileFilter(opts.file);
}
return q.all(db);
}
/**
* Fetch nodes for triage scoring: fan-in + complexity + churn.
* @param {object} db
* @param {object} [opts]
* @returns {object[]}
*/
export function findNodesForTriage(db, opts = {}) {
if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
throw new ConfigError(
`Invalid kind: ${opts.kind} (expected one of ${EVERY_SYMBOL_KIND.join(', ')})`,
);
}
if (opts.role && !VALID_ROLES.includes(opts.role)) {
throw new ConfigError(`Invalid role: ${opts.role} (expected one of ${VALID_ROLES.join(', ')})`);
}
const kindsToUse = opts.kind ? [opts.kind] : ['function', 'method', 'class'];
const q = new NodeQuery()
.select(
`n.id, n.name, n.kind, n.file, n.line, n.end_line, n.role,
COALESCE(fi.cnt, 0) AS fan_in,
COALESCE(fc.cognitive, 0) AS cognitive,
COALESCE(fc.maintainability_index, 0) AS mi,
COALESCE(fc.cyclomatic, 0) AS cyclomatic,
COALESCE(fc.max_nesting, 0) AS max_nesting,
COALESCE(fcc.commit_count, 0) AS churn`,
)
.kinds(kindsToUse)
.withFanIn()
.withComplexity()
.withChurn()
.excludeTests(opts.noTests)
.fileFilter(opts.file)
.roleFilter(opts.role)
.orderBy('n.file, n.line');
return q.all(db);
}
/**
* Shared query builder for function/method/class node listing.
* @param {object} [opts]
* @returns {NodeQuery}
*/
function _functionNodeQuery(opts = {}) {
return new NodeQuery()
.select('name, kind, file, line, end_line, role')
.kinds(['function', 'method', 'class'])
.fileFilter(opts.file)
.nameLike(opts.pattern)
.excludeTests(opts.noTests)
.orderBy('file, line');
}
/**
* List function/method/class nodes with basic info.
* @param {object} db
* @param {object} [opts]
* @returns {object[]}
*/
export function listFunctionNodes(db, opts = {}) {
return _functionNodeQuery(opts).all(db);
}
/**
* Iterator version of listFunctionNodes for memory efficiency.
* @param {object} db
* @param {object} [opts]
* @returns {IterableIterator}
*/
export function iterateFunctionNodes(db, opts = {}) {
return _functionNodeQuery(opts).iterate(db);
}
// ─── Statement caches (one prepared statement per db instance) ────────────
// WeakMap keys on the db object so statements are GC'd when the db closes.
const _countNodesStmt = new WeakMap();
const _countEdgesStmt = new WeakMap();
const _countFilesStmt = new WeakMap();
const _findNodeByIdStmt = new WeakMap();
const _findNodesByFileStmt = new WeakMap();
const _findFileNodesStmt = new WeakMap();
const _getNodeIdStmt = new WeakMap();
const _getFunctionNodeIdStmt = new WeakMap();
const _bulkNodeIdsByFileStmt = new WeakMap();
const _findNodeChildrenStmt = new WeakMap();
const _findNodeByQualifiedNameStmt = new WeakMap();
/**
* Count total nodes.
* @param {object} db
* @returns {number}
*/
export function countNodes(db) {
return cachedStmt(_countNodesStmt, db, 'SELECT COUNT(*) AS cnt FROM nodes').get().cnt;
}
/**
* Count total edges.
* @param {object} db
* @returns {number}
*/
export function countEdges(db) {
return cachedStmt(_countEdgesStmt, db, 'SELECT COUNT(*) AS cnt FROM edges').get().cnt;
}
/**
* Count distinct files.
* @param {object} db
* @returns {number}
*/
export function countFiles(db) {
return cachedStmt(_countFilesStmt, db, 'SELECT COUNT(DISTINCT file) AS cnt FROM nodes').get().cnt;
}
// ─── Shared node lookups ───────────────────────────────────────────────
/**
* Find a single node by ID.
* @param {object} db
* @param {number} id
* @returns {object|undefined}
*/
export function findNodeById(db, id) {
return cachedStmt(_findNodeByIdStmt, db, 'SELECT * FROM nodes WHERE id = ?').get(id);
}
/**
* Find non-file nodes for a given file path (exact match), ordered by line.
* @param {object} db
* @param {string} file - Exact file path
* @returns {object[]}
*/
export function findNodesByFile(db, file) {
return cachedStmt(
_findNodesByFileStmt,
db,
"SELECT * FROM nodes WHERE file = ? AND kind != 'file' ORDER BY line",
).all(file);
}
/**
* Find file-kind nodes matching a LIKE pattern.
* @param {object} db
* @param {string} fileLike - LIKE pattern (caller wraps with %)
* @returns {object[]}
*/
export function findFileNodes(db, fileLike) {
return cachedStmt(
_findFileNodesStmt,
db,
"SELECT * FROM nodes WHERE file LIKE ? AND kind = 'file'",
).all(fileLike);
}
/**
* Look up a node's ID by its unique (name, kind, file, line) tuple.
* Shared by builder, watcher, structure, complexity, cfg, engine.
* @param {object} db
* @param {string} name
* @param {string} kind
* @param {string} file
* @param {number} line
* @returns {number|undefined}
*/
export function getNodeId(db, name, kind, file, line) {
return cachedStmt(
_getNodeIdStmt,
db,
'SELECT id FROM nodes WHERE name = ? AND kind = ? AND file = ? AND line = ?',
).get(name, kind, file, line)?.id;
}
/**
* Look up a function/method node's ID (kind-restricted variant of getNodeId).
* Used by complexity.js, cfg.js where only function/method kinds are expected.
* @param {object} db
* @param {string} name
* @param {string} file
* @param {number} line
* @returns {number|undefined}
*/
export function getFunctionNodeId(db, name, file, line) {
return cachedStmt(
_getFunctionNodeIdStmt,
db,
"SELECT id FROM nodes WHERE name = ? AND kind IN ('function','method') AND file = ? AND line = ?",
).get(name, file, line)?.id;
}
/**
* Bulk-fetch all node IDs for a file in one query.
* Returns rows suitable for building a `name|kind|line -> id` lookup map.
* Shared by builder, ast.js, ast-analysis/engine.js.
* @param {object} db
* @param {string} file
* @returns {{ id: number, name: string, kind: string, line: number }[]}
*/
export function bulkNodeIdsByFile(db, file) {
return cachedStmt(
_bulkNodeIdsByFileStmt,
db,
'SELECT id, name, kind, line FROM nodes WHERE file = ?',
).all(file);
}
/**
* Find child nodes (parameters, properties, constants) of a parent.
* @param {object} db
* @param {number} parentId
* @returns {{ name: string, kind: string, line: number, end_line: number|null, qualified_name: string|null, scope: string|null, visibility: string|null }[]}
*/
export function findNodeChildren(db, parentId) {
return cachedStmt(
_findNodeChildrenStmt,
db,
'SELECT name, kind, line, end_line, qualified_name, scope, visibility FROM nodes WHERE parent_id = ? ORDER BY line',
).all(parentId);
}
/**
* Find all nodes that belong to a given scope (by scope column).
* Enables "all methods of class X" without traversing edges.
* @param {object} db
* @param {string} scopeName - The scope to search for (e.g., class name)
* @param {object} [opts]
* @param {string} [opts.kind] - Filter by node kind
* @param {string} [opts.file] - Filter by file path (LIKE match)
* @returns {object[]}
*/
export function findNodesByScope(db, scopeName, opts = {}) {
let sql = 'SELECT * FROM nodes WHERE scope = ?';
const params = [scopeName];
if (opts.kind) {
sql += ' AND kind = ?';
params.push(opts.kind);
}
const fc = buildFileConditionSQL(opts.file, 'file');
sql += fc.sql;
params.push(...fc.params);
sql += ' ORDER BY file, line';
return db.prepare(sql).all(...params);
}
/**
* Find nodes by qualified name. Returns all matches since the same
* qualified_name can exist in different files (e.g., two classes named
* `DateHelper.format` in separate modules). Pass `opts.file` to narrow.
* @param {object} db
* @param {string} qualifiedName - e.g., 'DateHelper.format'
* @param {object} [opts]
* @param {string} [opts.file] - Filter by file path (LIKE match)
* @returns {object[]}
*/
export function findNodeByQualifiedName(db, qualifiedName, opts = {}) {
const fc = buildFileConditionSQL(opts.file, 'file');
if (fc.sql) {
return db
.prepare(`SELECT * FROM nodes WHERE qualified_name = ?${fc.sql} ORDER BY file, line`)
.all(qualifiedName, ...fc.params);
}
return cachedStmt(
_findNodeByQualifiedNameStmt,
db,
'SELECT * FROM nodes WHERE qualified_name = ? ORDER BY file, line',
).all(qualifiedName);
}
// ─── Metric helpers ──────────────────────────────────────────────────────
const _getLineCountForNodeStmt = new WeakMap();
/**
* Get line_count from node_metrics for a given node.
* @param {object} db
* @param {number} nodeId
* @returns {{ line_count: number } | undefined}
*/
export function getLineCountForNode(db, nodeId) {
return cachedStmt(
_getLineCountForNodeStmt,
db,
'SELECT line_count FROM node_metrics WHERE node_id = ?',
).get(nodeId);
}
const _getMaxEndLineForFileStmt = new WeakMap();
/**
* Get the maximum end_line across all nodes in a file.
* @param {object} db
* @param {string} file
* @returns {{ max_end: number | null } | undefined}
*/
export function getMaxEndLineForFile(db, file) {
return cachedStmt(
_getMaxEndLineForFileStmt,
db,
'SELECT MAX(end_line) as max_end FROM nodes WHERE file = ?',
).get(file);
}