@optave/codegraph 3.1.1 → 3.1.3
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 +6 -6
- package/package.json +7 -7
- package/src/ast-analysis/engine.js +365 -0
- package/src/ast-analysis/metrics.js +118 -0
- package/src/ast-analysis/visitor-utils.js +176 -0
- package/src/ast-analysis/visitor.js +162 -0
- package/src/ast-analysis/visitors/ast-store-visitor.js +150 -0
- package/src/ast-analysis/visitors/cfg-visitor.js +792 -0
- package/src/ast-analysis/visitors/complexity-visitor.js +243 -0
- package/src/ast-analysis/visitors/dataflow-visitor.js +358 -0
- package/src/ast.js +13 -140
- package/src/audit.js +2 -87
- package/src/batch.js +0 -25
- package/src/boundaries.js +1 -1
- package/src/branch-compare.js +1 -96
- package/src/builder.js +60 -178
- package/src/cfg.js +89 -883
- package/src/check.js +1 -84
- package/src/cli.js +31 -22
- package/src/cochange.js +1 -39
- package/src/commands/audit.js +88 -0
- package/src/commands/batch.js +26 -0
- package/src/commands/branch-compare.js +97 -0
- package/src/commands/cfg.js +55 -0
- package/src/commands/check.js +82 -0
- package/src/commands/cochange.js +37 -0
- package/src/commands/communities.js +69 -0
- package/src/commands/complexity.js +77 -0
- package/src/commands/dataflow.js +110 -0
- package/src/commands/flow.js +70 -0
- package/src/commands/manifesto.js +77 -0
- package/src/commands/owners.js +52 -0
- package/src/commands/query.js +21 -0
- package/src/commands/sequence.js +33 -0
- package/src/commands/structure.js +64 -0
- package/src/commands/triage.js +49 -0
- package/src/communities.js +12 -83
- package/src/complexity.js +43 -357
- package/src/cycles.js +1 -1
- package/src/dataflow.js +12 -665
- package/src/db/repository/build-stmts.js +104 -0
- package/src/db/repository/cached-stmt.js +19 -0
- package/src/db/repository/cfg.js +72 -0
- package/src/db/repository/cochange.js +54 -0
- package/src/db/repository/complexity.js +20 -0
- package/src/db/repository/dataflow.js +17 -0
- package/src/db/repository/edges.js +281 -0
- package/src/db/repository/embeddings.js +51 -0
- package/src/db/repository/graph-read.js +59 -0
- package/src/db/repository/index.js +43 -0
- package/src/db/repository/nodes.js +247 -0
- package/src/db.js +40 -1
- package/src/embedder.js +14 -34
- package/src/export.js +1 -1
- package/src/extractors/javascript.js +130 -5
- package/src/flow.js +2 -70
- package/src/index.js +30 -20
- package/src/{result-formatter.js → infrastructure/result-formatter.js} +1 -1
- package/src/kinds.js +1 -0
- package/src/manifesto.js +0 -76
- package/src/native.js +31 -9
- package/src/owners.js +1 -56
- package/src/parser.js +53 -2
- package/src/queries-cli.js +1 -1
- package/src/queries.js +79 -280
- package/src/sequence.js +5 -44
- package/src/structure.js +16 -75
- package/src/triage.js +1 -54
- package/src/viewer.js +1 -1
- package/src/watcher.js +7 -4
- package/src/db/repository.js +0 -134
- /package/src/{test-filter.js → infrastructure/test-filter.js} +0 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Barrel re-export for repository/ modules.
|
|
2
|
+
|
|
3
|
+
export { purgeFileData, purgeFilesData } from './build-stmts.js';
|
|
4
|
+
export { cachedStmt } from './cached-stmt.js';
|
|
5
|
+
export { deleteCfgForNode, getCfgBlocks, getCfgEdges, hasCfgTables } from './cfg.js';
|
|
6
|
+
export { getCoChangeMeta, hasCoChanges, upsertCoChangeMeta } from './cochange.js';
|
|
7
|
+
|
|
8
|
+
export { getComplexityForNode } from './complexity.js';
|
|
9
|
+
export { hasDataflowTable } from './dataflow.js';
|
|
10
|
+
export {
|
|
11
|
+
countCrossFileCallers,
|
|
12
|
+
findAllIncomingEdges,
|
|
13
|
+
findAllOutgoingEdges,
|
|
14
|
+
findCalleeNames,
|
|
15
|
+
findCallees,
|
|
16
|
+
findCallerNames,
|
|
17
|
+
findCallers,
|
|
18
|
+
findCrossFileCallTargets,
|
|
19
|
+
findDistinctCallers,
|
|
20
|
+
findImportDependents,
|
|
21
|
+
findImportSources,
|
|
22
|
+
findImportTargets,
|
|
23
|
+
findIntraFileCallEdges,
|
|
24
|
+
getClassHierarchy,
|
|
25
|
+
} from './edges.js';
|
|
26
|
+
export { getEmbeddingCount, getEmbeddingMeta, hasEmbeddings } from './embeddings.js';
|
|
27
|
+
export { getCallableNodes, getCallEdges, getFileNodesAll, getImportEdges } from './graph-read.js';
|
|
28
|
+
export {
|
|
29
|
+
bulkNodeIdsByFile,
|
|
30
|
+
countEdges,
|
|
31
|
+
countFiles,
|
|
32
|
+
countNodes,
|
|
33
|
+
findFileNodes,
|
|
34
|
+
findNodeById,
|
|
35
|
+
findNodeChildren,
|
|
36
|
+
findNodesByFile,
|
|
37
|
+
findNodesForTriage,
|
|
38
|
+
findNodesWithFanIn,
|
|
39
|
+
getFunctionNodeId,
|
|
40
|
+
getNodeId,
|
|
41
|
+
iterateFunctionNodes,
|
|
42
|
+
listFunctionNodes,
|
|
43
|
+
} from './nodes.js';
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { EVERY_SYMBOL_KIND, VALID_ROLES } from '../../kinds.js';
|
|
2
|
+
import { NodeQuery } from '../query-builder.js';
|
|
3
|
+
import { cachedStmt } from './cached-stmt.js';
|
|
4
|
+
|
|
5
|
+
// ─── Query-builder based lookups (moved from src/db/repository.js) ─────
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Find nodes matching a name pattern, with fan-in count.
|
|
9
|
+
* @param {object} db
|
|
10
|
+
* @param {string} namePattern - LIKE pattern (already wrapped with %)
|
|
11
|
+
* @param {object} [opts]
|
|
12
|
+
* @param {string[]} [opts.kinds]
|
|
13
|
+
* @param {string} [opts.file]
|
|
14
|
+
* @returns {object[]}
|
|
15
|
+
*/
|
|
16
|
+
export function findNodesWithFanIn(db, namePattern, opts = {}) {
|
|
17
|
+
const q = new NodeQuery()
|
|
18
|
+
.select('n.*, COALESCE(fi.cnt, 0) AS fan_in')
|
|
19
|
+
.withFanIn()
|
|
20
|
+
.where('n.name LIKE ?', namePattern);
|
|
21
|
+
|
|
22
|
+
if (opts.kinds) {
|
|
23
|
+
q.kinds(opts.kinds);
|
|
24
|
+
}
|
|
25
|
+
if (opts.file) {
|
|
26
|
+
q.fileFilter(opts.file);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return q.all(db);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Fetch nodes for triage scoring: fan-in + complexity + churn.
|
|
34
|
+
* @param {object} db
|
|
35
|
+
* @param {object} [opts]
|
|
36
|
+
* @returns {object[]}
|
|
37
|
+
*/
|
|
38
|
+
export function findNodesForTriage(db, opts = {}) {
|
|
39
|
+
if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
|
|
40
|
+
throw new Error(`Invalid kind: ${opts.kind} (expected one of ${EVERY_SYMBOL_KIND.join(', ')})`);
|
|
41
|
+
}
|
|
42
|
+
if (opts.role && !VALID_ROLES.includes(opts.role)) {
|
|
43
|
+
throw new Error(`Invalid role: ${opts.role} (expected one of ${VALID_ROLES.join(', ')})`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const kindsToUse = opts.kind ? [opts.kind] : ['function', 'method', 'class'];
|
|
47
|
+
const q = new NodeQuery()
|
|
48
|
+
.select(
|
|
49
|
+
`n.id, n.name, n.kind, n.file, n.line, n.end_line, n.role,
|
|
50
|
+
COALESCE(fi.cnt, 0) AS fan_in,
|
|
51
|
+
COALESCE(fc.cognitive, 0) AS cognitive,
|
|
52
|
+
COALESCE(fc.maintainability_index, 0) AS mi,
|
|
53
|
+
COALESCE(fc.cyclomatic, 0) AS cyclomatic,
|
|
54
|
+
COALESCE(fc.max_nesting, 0) AS max_nesting,
|
|
55
|
+
COALESCE(fcc.commit_count, 0) AS churn`,
|
|
56
|
+
)
|
|
57
|
+
.kinds(kindsToUse)
|
|
58
|
+
.withFanIn()
|
|
59
|
+
.withComplexity()
|
|
60
|
+
.withChurn()
|
|
61
|
+
.excludeTests(opts.noTests)
|
|
62
|
+
.fileFilter(opts.file)
|
|
63
|
+
.roleFilter(opts.role)
|
|
64
|
+
.orderBy('n.file, n.line');
|
|
65
|
+
|
|
66
|
+
return q.all(db);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Shared query builder for function/method/class node listing.
|
|
71
|
+
* @param {object} [opts]
|
|
72
|
+
* @returns {NodeQuery}
|
|
73
|
+
*/
|
|
74
|
+
function _functionNodeQuery(opts = {}) {
|
|
75
|
+
return new NodeQuery()
|
|
76
|
+
.select('name, kind, file, line, end_line, role')
|
|
77
|
+
.kinds(['function', 'method', 'class'])
|
|
78
|
+
.fileFilter(opts.file)
|
|
79
|
+
.nameLike(opts.pattern)
|
|
80
|
+
.excludeTests(opts.noTests)
|
|
81
|
+
.orderBy('file, line');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* List function/method/class nodes with basic info.
|
|
86
|
+
* @param {object} db
|
|
87
|
+
* @param {object} [opts]
|
|
88
|
+
* @returns {object[]}
|
|
89
|
+
*/
|
|
90
|
+
export function listFunctionNodes(db, opts = {}) {
|
|
91
|
+
return _functionNodeQuery(opts).all(db);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Iterator version of listFunctionNodes for memory efficiency.
|
|
96
|
+
* @param {object} db
|
|
97
|
+
* @param {object} [opts]
|
|
98
|
+
* @returns {IterableIterator}
|
|
99
|
+
*/
|
|
100
|
+
export function iterateFunctionNodes(db, opts = {}) {
|
|
101
|
+
return _functionNodeQuery(opts).iterate(db);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ─── Statement caches (one prepared statement per db instance) ────────────
|
|
105
|
+
// WeakMap keys on the db object so statements are GC'd when the db closes.
|
|
106
|
+
const _countNodesStmt = new WeakMap();
|
|
107
|
+
const _countEdgesStmt = new WeakMap();
|
|
108
|
+
const _countFilesStmt = new WeakMap();
|
|
109
|
+
const _findNodeByIdStmt = new WeakMap();
|
|
110
|
+
const _findNodesByFileStmt = new WeakMap();
|
|
111
|
+
const _findFileNodesStmt = new WeakMap();
|
|
112
|
+
const _getNodeIdStmt = new WeakMap();
|
|
113
|
+
const _getFunctionNodeIdStmt = new WeakMap();
|
|
114
|
+
const _bulkNodeIdsByFileStmt = new WeakMap();
|
|
115
|
+
const _findNodeChildrenStmt = new WeakMap();
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Count total nodes.
|
|
119
|
+
* @param {object} db
|
|
120
|
+
* @returns {number}
|
|
121
|
+
*/
|
|
122
|
+
export function countNodes(db) {
|
|
123
|
+
return cachedStmt(_countNodesStmt, db, 'SELECT COUNT(*) AS cnt FROM nodes').get().cnt;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Count total edges.
|
|
128
|
+
* @param {object} db
|
|
129
|
+
* @returns {number}
|
|
130
|
+
*/
|
|
131
|
+
export function countEdges(db) {
|
|
132
|
+
return cachedStmt(_countEdgesStmt, db, 'SELECT COUNT(*) AS cnt FROM edges').get().cnt;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Count distinct files.
|
|
137
|
+
* @param {object} db
|
|
138
|
+
* @returns {number}
|
|
139
|
+
*/
|
|
140
|
+
export function countFiles(db) {
|
|
141
|
+
return cachedStmt(_countFilesStmt, db, 'SELECT COUNT(DISTINCT file) AS cnt FROM nodes').get().cnt;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ─── Shared node lookups ───────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Find a single node by ID.
|
|
148
|
+
* @param {object} db
|
|
149
|
+
* @param {number} id
|
|
150
|
+
* @returns {object|undefined}
|
|
151
|
+
*/
|
|
152
|
+
export function findNodeById(db, id) {
|
|
153
|
+
return cachedStmt(_findNodeByIdStmt, db, 'SELECT * FROM nodes WHERE id = ?').get(id);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Find non-file nodes for a given file path (exact match), ordered by line.
|
|
158
|
+
* @param {object} db
|
|
159
|
+
* @param {string} file - Exact file path
|
|
160
|
+
* @returns {object[]}
|
|
161
|
+
*/
|
|
162
|
+
export function findNodesByFile(db, file) {
|
|
163
|
+
return cachedStmt(
|
|
164
|
+
_findNodesByFileStmt,
|
|
165
|
+
db,
|
|
166
|
+
"SELECT * FROM nodes WHERE file = ? AND kind != 'file' ORDER BY line",
|
|
167
|
+
).all(file);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Find file-kind nodes matching a LIKE pattern.
|
|
172
|
+
* @param {object} db
|
|
173
|
+
* @param {string} fileLike - LIKE pattern (caller wraps with %)
|
|
174
|
+
* @returns {object[]}
|
|
175
|
+
*/
|
|
176
|
+
export function findFileNodes(db, fileLike) {
|
|
177
|
+
return cachedStmt(
|
|
178
|
+
_findFileNodesStmt,
|
|
179
|
+
db,
|
|
180
|
+
"SELECT * FROM nodes WHERE file LIKE ? AND kind = 'file'",
|
|
181
|
+
).all(fileLike);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Look up a node's ID by its unique (name, kind, file, line) tuple.
|
|
186
|
+
* Shared by builder, watcher, structure, complexity, cfg, engine.
|
|
187
|
+
* @param {object} db
|
|
188
|
+
* @param {string} name
|
|
189
|
+
* @param {string} kind
|
|
190
|
+
* @param {string} file
|
|
191
|
+
* @param {number} line
|
|
192
|
+
* @returns {number|undefined}
|
|
193
|
+
*/
|
|
194
|
+
export function getNodeId(db, name, kind, file, line) {
|
|
195
|
+
return cachedStmt(
|
|
196
|
+
_getNodeIdStmt,
|
|
197
|
+
db,
|
|
198
|
+
'SELECT id FROM nodes WHERE name = ? AND kind = ? AND file = ? AND line = ?',
|
|
199
|
+
).get(name, kind, file, line)?.id;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Look up a function/method node's ID (kind-restricted variant of getNodeId).
|
|
204
|
+
* Used by complexity.js, cfg.js where only function/method kinds are expected.
|
|
205
|
+
* @param {object} db
|
|
206
|
+
* @param {string} name
|
|
207
|
+
* @param {string} file
|
|
208
|
+
* @param {number} line
|
|
209
|
+
* @returns {number|undefined}
|
|
210
|
+
*/
|
|
211
|
+
export function getFunctionNodeId(db, name, file, line) {
|
|
212
|
+
return cachedStmt(
|
|
213
|
+
_getFunctionNodeIdStmt,
|
|
214
|
+
db,
|
|
215
|
+
"SELECT id FROM nodes WHERE name = ? AND kind IN ('function','method') AND file = ? AND line = ?",
|
|
216
|
+
).get(name, file, line)?.id;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Bulk-fetch all node IDs for a file in one query.
|
|
221
|
+
* Returns rows suitable for building a `name|kind|line -> id` lookup map.
|
|
222
|
+
* Shared by builder, ast.js, ast-analysis/engine.js.
|
|
223
|
+
* @param {object} db
|
|
224
|
+
* @param {string} file
|
|
225
|
+
* @returns {{ id: number, name: string, kind: string, line: number }[]}
|
|
226
|
+
*/
|
|
227
|
+
export function bulkNodeIdsByFile(db, file) {
|
|
228
|
+
return cachedStmt(
|
|
229
|
+
_bulkNodeIdsByFileStmt,
|
|
230
|
+
db,
|
|
231
|
+
'SELECT id, name, kind, line FROM nodes WHERE file = ?',
|
|
232
|
+
).all(file);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Find child nodes (parameters, properties, constants) of a parent.
|
|
237
|
+
* @param {object} db
|
|
238
|
+
* @param {number} parentId
|
|
239
|
+
* @returns {{ name: string, kind: string, line: number, end_line: number|null }[]}
|
|
240
|
+
*/
|
|
241
|
+
export function findNodeChildren(db, parentId) {
|
|
242
|
+
return cachedStmt(
|
|
243
|
+
_findNodeChildrenStmt,
|
|
244
|
+
db,
|
|
245
|
+
'SELECT name, kind, line, end_line FROM nodes WHERE parent_id = ? ORDER BY line',
|
|
246
|
+
).all(parentId);
|
|
247
|
+
}
|
package/src/db.js
CHANGED
|
@@ -9,11 +9,50 @@ export {
|
|
|
9
9
|
testFilterSQL,
|
|
10
10
|
} from './db/query-builder.js';
|
|
11
11
|
export {
|
|
12
|
+
bulkNodeIdsByFile,
|
|
13
|
+
countCrossFileCallers,
|
|
12
14
|
countEdges,
|
|
13
15
|
countFiles,
|
|
14
16
|
countNodes,
|
|
17
|
+
deleteCfgForNode,
|
|
18
|
+
findAllIncomingEdges,
|
|
19
|
+
findAllOutgoingEdges,
|
|
20
|
+
findCalleeNames,
|
|
21
|
+
findCallees,
|
|
22
|
+
findCallerNames,
|
|
23
|
+
findCallers,
|
|
24
|
+
findCrossFileCallTargets,
|
|
25
|
+
findDistinctCallers,
|
|
26
|
+
findFileNodes,
|
|
27
|
+
findImportDependents,
|
|
28
|
+
findImportSources,
|
|
29
|
+
findImportTargets,
|
|
30
|
+
findIntraFileCallEdges,
|
|
31
|
+
findNodeById,
|
|
32
|
+
findNodeChildren,
|
|
33
|
+
findNodesByFile,
|
|
15
34
|
findNodesForTriage,
|
|
16
35
|
findNodesWithFanIn,
|
|
36
|
+
getCallableNodes,
|
|
37
|
+
getCallEdges,
|
|
38
|
+
getCfgBlocks,
|
|
39
|
+
getCfgEdges,
|
|
40
|
+
getClassHierarchy,
|
|
41
|
+
getCoChangeMeta,
|
|
42
|
+
getComplexityForNode,
|
|
43
|
+
getEmbeddingCount,
|
|
44
|
+
getEmbeddingMeta,
|
|
45
|
+
getFileNodesAll,
|
|
46
|
+
getFunctionNodeId,
|
|
47
|
+
getImportEdges,
|
|
48
|
+
getNodeId,
|
|
49
|
+
hasCfgTables,
|
|
50
|
+
hasCoChanges,
|
|
51
|
+
hasDataflowTable,
|
|
52
|
+
hasEmbeddings,
|
|
17
53
|
iterateFunctionNodes,
|
|
18
54
|
listFunctionNodes,
|
|
19
|
-
|
|
55
|
+
purgeFileData,
|
|
56
|
+
purgeFilesData,
|
|
57
|
+
upsertCoChangeMeta,
|
|
58
|
+
} from './db/repository/index.js';
|
package/src/embedder.js
CHANGED
|
@@ -2,7 +2,14 @@ import { execFileSync } from 'node:child_process';
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { createInterface } from 'node:readline';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
closeDb,
|
|
7
|
+
findCalleeNames,
|
|
8
|
+
findCallerNames,
|
|
9
|
+
findDbPath,
|
|
10
|
+
openDb,
|
|
11
|
+
openReadonlyOrFail,
|
|
12
|
+
} from './db.js';
|
|
6
13
|
import { info, warn } from './logger.js';
|
|
7
14
|
import { normalizeSymbol } from './queries.js';
|
|
8
15
|
|
|
@@ -166,7 +173,7 @@ function extractLeadingComment(lines, fnLineIndex) {
|
|
|
166
173
|
* Build graph-enriched text for a symbol using dependency context.
|
|
167
174
|
* Produces compact, semantic text (~100 tokens) instead of full source code.
|
|
168
175
|
*/
|
|
169
|
-
function buildStructuredText(node, file, lines,
|
|
176
|
+
function buildStructuredText(node, file, lines, db) {
|
|
170
177
|
const readable = splitIdentifier(node.name);
|
|
171
178
|
const parts = [`${node.kind} ${node.name} (${readable}) in ${file}`];
|
|
172
179
|
const startLine = Math.max(0, node.line - 1);
|
|
@@ -179,25 +186,15 @@ function buildStructuredText(node, file, lines, calleesStmt, callersStmt) {
|
|
|
179
186
|
}
|
|
180
187
|
|
|
181
188
|
// Graph context: callees (capped at 10)
|
|
182
|
-
const callees =
|
|
189
|
+
const callees = findCalleeNames(db, node.id);
|
|
183
190
|
if (callees.length > 0) {
|
|
184
|
-
parts.push(
|
|
185
|
-
`Calls: ${callees
|
|
186
|
-
.slice(0, 10)
|
|
187
|
-
.map((c) => c.name)
|
|
188
|
-
.join(', ')}`,
|
|
189
|
-
);
|
|
191
|
+
parts.push(`Calls: ${callees.slice(0, 10).join(', ')}`);
|
|
190
192
|
}
|
|
191
193
|
|
|
192
194
|
// Graph context: callers (capped at 10)
|
|
193
|
-
const callers =
|
|
195
|
+
const callers = findCallerNames(db, node.id);
|
|
194
196
|
if (callers.length > 0) {
|
|
195
|
-
parts.push(
|
|
196
|
-
`Called by: ${callers
|
|
197
|
-
.slice(0, 10)
|
|
198
|
-
.map((c) => c.name)
|
|
199
|
-
.join(', ')}`,
|
|
200
|
-
);
|
|
197
|
+
parts.push(`Called by: ${callers.slice(0, 10).join(', ')}`);
|
|
201
198
|
}
|
|
202
199
|
|
|
203
200
|
// Leading comment (high semantic value) or first few lines of code
|
|
@@ -438,23 +435,6 @@ export async function buildEmbeddings(rootDir, modelKey, customDbPath, options =
|
|
|
438
435
|
|
|
439
436
|
console.log(`Building embeddings for ${nodes.length} symbols (strategy: ${strategy})...`);
|
|
440
437
|
|
|
441
|
-
// Prepare graph-context queries for structured strategy
|
|
442
|
-
let calleesStmt, callersStmt;
|
|
443
|
-
if (strategy === 'structured') {
|
|
444
|
-
calleesStmt = db.prepare(`
|
|
445
|
-
SELECT DISTINCT n.name FROM edges e
|
|
446
|
-
JOIN nodes n ON e.target_id = n.id
|
|
447
|
-
WHERE e.source_id = ? AND e.kind = 'calls'
|
|
448
|
-
ORDER BY n.name
|
|
449
|
-
`);
|
|
450
|
-
callersStmt = db.prepare(`
|
|
451
|
-
SELECT DISTINCT n.name FROM edges e
|
|
452
|
-
JOIN nodes n ON e.source_id = n.id
|
|
453
|
-
WHERE e.target_id = ? AND e.kind = 'calls'
|
|
454
|
-
ORDER BY n.name
|
|
455
|
-
`);
|
|
456
|
-
}
|
|
457
|
-
|
|
458
438
|
const byFile = new Map();
|
|
459
439
|
for (const node of nodes) {
|
|
460
440
|
if (!byFile.has(node.file)) byFile.set(node.file, []);
|
|
@@ -482,7 +462,7 @@ export async function buildEmbeddings(rootDir, modelKey, customDbPath, options =
|
|
|
482
462
|
for (const node of fileNodes) {
|
|
483
463
|
let text =
|
|
484
464
|
strategy === 'structured'
|
|
485
|
-
? buildStructuredText(node, file, lines,
|
|
465
|
+
? buildStructuredText(node, file, lines, db)
|
|
486
466
|
: buildSourceText(node, file, lines);
|
|
487
467
|
|
|
488
468
|
// Detect and handle context window overflow
|
package/src/export.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { debug } from '../logger.js';
|
|
1
2
|
import { findChild, nodeEndLine } from './helpers.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -173,6 +174,9 @@ function extractSymbolsQuery(tree, query) {
|
|
|
173
174
|
// Extract top-level constants via targeted walk (query patterns don't cover these)
|
|
174
175
|
extractConstantsWalk(tree.rootNode, definitions);
|
|
175
176
|
|
|
177
|
+
// Extract dynamic import() calls via targeted walk (query patterns don't match `import` function type)
|
|
178
|
+
extractDynamicImportsWalk(tree.rootNode, imports);
|
|
179
|
+
|
|
176
180
|
return { definitions, calls, imports, classes, exports: exps };
|
|
177
181
|
}
|
|
178
182
|
|
|
@@ -224,6 +228,41 @@ function extractConstantsWalk(rootNode, definitions) {
|
|
|
224
228
|
}
|
|
225
229
|
}
|
|
226
230
|
|
|
231
|
+
/**
|
|
232
|
+
* Recursive walk to find dynamic import() calls.
|
|
233
|
+
* Query patterns match call_expression with identifier/member_expression/subscript_expression
|
|
234
|
+
* functions, but import() has function type `import` which none of those patterns cover.
|
|
235
|
+
*/
|
|
236
|
+
function extractDynamicImportsWalk(node, imports) {
|
|
237
|
+
if (node.type === 'call_expression') {
|
|
238
|
+
const fn = node.childForFieldName('function');
|
|
239
|
+
if (fn && fn.type === 'import') {
|
|
240
|
+
const args = node.childForFieldName('arguments') || findChild(node, 'arguments');
|
|
241
|
+
if (args) {
|
|
242
|
+
const strArg = findChild(args, 'string');
|
|
243
|
+
if (strArg) {
|
|
244
|
+
const modPath = strArg.text.replace(/['"]/g, '');
|
|
245
|
+
const names = extractDynamicImportNames(node);
|
|
246
|
+
imports.push({
|
|
247
|
+
source: modPath,
|
|
248
|
+
names,
|
|
249
|
+
line: node.startPosition.row + 1,
|
|
250
|
+
dynamicImport: true,
|
|
251
|
+
});
|
|
252
|
+
} else {
|
|
253
|
+
debug(
|
|
254
|
+
`Skipping non-static dynamic import() at line ${node.startPosition.row + 1} (template literal or variable)`,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return; // no need to recurse into import() children
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
262
|
+
extractDynamicImportsWalk(node.child(i), imports);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
227
266
|
function handleCommonJSAssignment(left, right, node, imports) {
|
|
228
267
|
if (!left || !right) return;
|
|
229
268
|
const leftText = left.text;
|
|
@@ -455,11 +494,36 @@ function extractSymbolsWalk(tree) {
|
|
|
455
494
|
case 'call_expression': {
|
|
456
495
|
const fn = node.childForFieldName('function');
|
|
457
496
|
if (fn) {
|
|
458
|
-
|
|
459
|
-
if (
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
497
|
+
// Dynamic import(): import('./foo.js') → extract as an import entry
|
|
498
|
+
if (fn.type === 'import') {
|
|
499
|
+
const args = node.childForFieldName('arguments') || findChild(node, 'arguments');
|
|
500
|
+
if (args) {
|
|
501
|
+
const strArg = findChild(args, 'string');
|
|
502
|
+
if (strArg) {
|
|
503
|
+
const modPath = strArg.text.replace(/['"]/g, '');
|
|
504
|
+
// Extract destructured names from parent context:
|
|
505
|
+
// const { a, b } = await import('./foo.js')
|
|
506
|
+
// (standalone import('./foo.js').then(...) calls produce an edge with empty names)
|
|
507
|
+
const names = extractDynamicImportNames(node);
|
|
508
|
+
imports.push({
|
|
509
|
+
source: modPath,
|
|
510
|
+
names,
|
|
511
|
+
line: node.startPosition.row + 1,
|
|
512
|
+
dynamicImport: true,
|
|
513
|
+
});
|
|
514
|
+
} else {
|
|
515
|
+
debug(
|
|
516
|
+
`Skipping non-static dynamic import() at line ${node.startPosition.row + 1} (template literal or variable)`,
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
} else {
|
|
521
|
+
const callInfo = extractCallInfo(fn, node);
|
|
522
|
+
if (callInfo) calls.push(callInfo);
|
|
523
|
+
if (fn.type === 'member_expression') {
|
|
524
|
+
const cbDef = extractCallbackDefinition(node, fn);
|
|
525
|
+
if (cbDef) definitions.push(cbDef);
|
|
526
|
+
}
|
|
463
527
|
}
|
|
464
528
|
}
|
|
465
529
|
break;
|
|
@@ -941,3 +1005,64 @@ function extractImportNames(node) {
|
|
|
941
1005
|
scan(node);
|
|
942
1006
|
return names;
|
|
943
1007
|
}
|
|
1008
|
+
|
|
1009
|
+
/**
|
|
1010
|
+
* Extract destructured names from a dynamic import() call expression.
|
|
1011
|
+
*
|
|
1012
|
+
* Handles:
|
|
1013
|
+
* const { a, b } = await import('./foo.js') → ['a', 'b']
|
|
1014
|
+
* const mod = await import('./foo.js') → ['mod']
|
|
1015
|
+
* import('./foo.js') → [] (no names extractable)
|
|
1016
|
+
*
|
|
1017
|
+
* Walks up the AST from the call_expression to find the enclosing
|
|
1018
|
+
* variable_declarator and reads the name/object_pattern.
|
|
1019
|
+
*/
|
|
1020
|
+
function extractDynamicImportNames(callNode) {
|
|
1021
|
+
// Walk up: call_expression → await_expression → variable_declarator
|
|
1022
|
+
let current = callNode.parent;
|
|
1023
|
+
// Skip await_expression wrapper if present
|
|
1024
|
+
if (current && current.type === 'await_expression') current = current.parent;
|
|
1025
|
+
// We should now be at a variable_declarator (or not, if standalone import())
|
|
1026
|
+
if (!current || current.type !== 'variable_declarator') return [];
|
|
1027
|
+
|
|
1028
|
+
const nameNode = current.childForFieldName('name');
|
|
1029
|
+
if (!nameNode) return [];
|
|
1030
|
+
|
|
1031
|
+
// const { a, b } = await import(...) → object_pattern
|
|
1032
|
+
if (nameNode.type === 'object_pattern') {
|
|
1033
|
+
const names = [];
|
|
1034
|
+
for (let i = 0; i < nameNode.childCount; i++) {
|
|
1035
|
+
const child = nameNode.child(i);
|
|
1036
|
+
if (child.type === 'shorthand_property_identifier_pattern') {
|
|
1037
|
+
names.push(child.text);
|
|
1038
|
+
} else if (child.type === 'pair_pattern') {
|
|
1039
|
+
// { a: localName } → use localName (the alias) for the local binding,
|
|
1040
|
+
// but use the key (original name) for import resolution
|
|
1041
|
+
const key = child.childForFieldName('key');
|
|
1042
|
+
if (key) names.push(key.text);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
return names;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// const mod = await import(...) → identifier (namespace-like import)
|
|
1049
|
+
if (nameNode.type === 'identifier') {
|
|
1050
|
+
return [nameNode.text];
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
// const [a, b] = await import(...) → array_pattern (rare but possible)
|
|
1054
|
+
if (nameNode.type === 'array_pattern') {
|
|
1055
|
+
const names = [];
|
|
1056
|
+
for (let i = 0; i < nameNode.childCount; i++) {
|
|
1057
|
+
const child = nameNode.child(i);
|
|
1058
|
+
if (child.type === 'identifier') names.push(child.text);
|
|
1059
|
+
else if (child.type === 'rest_pattern') {
|
|
1060
|
+
const inner = child.child(0) || child.childForFieldName('name');
|
|
1061
|
+
if (inner && inner.type === 'identifier') names.push(inner.text);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
return names;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
return [];
|
|
1068
|
+
}
|
package/src/flow.js
CHANGED
|
@@ -6,11 +6,10 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { openReadonlyOrFail } from './db.js';
|
|
9
|
+
import { isTestFile } from './infrastructure/test-filter.js';
|
|
9
10
|
import { paginateResult } from './paginate.js';
|
|
10
|
-
import { CORE_SYMBOL_KINDS, findMatchingNodes
|
|
11
|
-
import { outputResult } from './result-formatter.js';
|
|
11
|
+
import { CORE_SYMBOL_KINDS, findMatchingNodes } from './queries.js';
|
|
12
12
|
import { FRAMEWORK_ENTRY_PREFIXES } from './structure.js';
|
|
13
|
-
import { isTestFile } from './test-filter.js';
|
|
14
13
|
|
|
15
14
|
/**
|
|
16
15
|
* Determine the entry point type from a node name based on framework prefixes.
|
|
@@ -227,70 +226,3 @@ export function flowData(name, dbPath, opts = {}) {
|
|
|
227
226
|
db.close();
|
|
228
227
|
}
|
|
229
228
|
}
|
|
230
|
-
|
|
231
|
-
/**
|
|
232
|
-
* CLI formatter — text or JSON output.
|
|
233
|
-
*/
|
|
234
|
-
export function flow(name, dbPath, opts = {}) {
|
|
235
|
-
if (opts.list) {
|
|
236
|
-
const data = listEntryPointsData(dbPath, {
|
|
237
|
-
noTests: opts.noTests,
|
|
238
|
-
limit: opts.limit,
|
|
239
|
-
offset: opts.offset,
|
|
240
|
-
});
|
|
241
|
-
if (outputResult(data, 'entries', opts)) return;
|
|
242
|
-
if (data.count === 0) {
|
|
243
|
-
console.log('No entry points found. Run "codegraph build" first.');
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
246
|
-
console.log(`\nEntry points (${data.count} total):\n`);
|
|
247
|
-
for (const [type, entries] of Object.entries(data.byType)) {
|
|
248
|
-
console.log(` ${type} (${entries.length}):`);
|
|
249
|
-
for (const e of entries) {
|
|
250
|
-
console.log(` [${kindIcon(e.kind)}] ${e.name} ${e.file}:${e.line}`);
|
|
251
|
-
}
|
|
252
|
-
console.log();
|
|
253
|
-
}
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
const data = flowData(name, dbPath, opts);
|
|
258
|
-
if (outputResult(data, 'steps', opts)) return;
|
|
259
|
-
|
|
260
|
-
if (!data.entry) {
|
|
261
|
-
console.log(`No matching entry point or function found for "${name}".`);
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
const e = data.entry;
|
|
266
|
-
const typeTag = e.type !== 'exported' ? ` (${e.type})` : '';
|
|
267
|
-
console.log(`\nFlow from: [${kindIcon(e.kind)}] ${e.name}${typeTag} ${e.file}:${e.line}`);
|
|
268
|
-
console.log(
|
|
269
|
-
`Depth: ${data.depth} Reached: ${data.totalReached} nodes Leaves: ${data.leaves.length}`,
|
|
270
|
-
);
|
|
271
|
-
if (data.truncated) {
|
|
272
|
-
console.log(` (truncated at depth ${data.depth})`);
|
|
273
|
-
}
|
|
274
|
-
console.log();
|
|
275
|
-
|
|
276
|
-
if (data.steps.length === 0) {
|
|
277
|
-
console.log(' (leaf node — no callees)');
|
|
278
|
-
return;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
for (const step of data.steps) {
|
|
282
|
-
console.log(` depth ${step.depth}:`);
|
|
283
|
-
for (const n of step.nodes) {
|
|
284
|
-
const isLeaf = data.leaves.some((l) => l.name === n.name && l.file === n.file);
|
|
285
|
-
const leafTag = isLeaf ? ' [leaf]' : '';
|
|
286
|
-
console.log(` [${kindIcon(n.kind)}] ${n.name} ${n.file}:${n.line}${leafTag}`);
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
if (data.cycles.length > 0) {
|
|
291
|
-
console.log('\n Cycles detected:');
|
|
292
|
-
for (const c of data.cycles) {
|
|
293
|
-
console.log(` ${c.from} -> ${c.to} (at depth ${c.depth})`);
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
}
|