@optave/codegraph 3.1.5 → 3.2.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 +3 -2
- package/package.json +7 -7
- package/src/ast-analysis/engine.js +252 -258
- package/src/ast-analysis/shared.js +0 -12
- package/src/ast-analysis/visitors/cfg-visitor.js +635 -649
- package/src/ast-analysis/visitors/complexity-visitor.js +135 -139
- package/src/ast-analysis/visitors/dataflow-visitor.js +230 -224
- package/src/cli/commands/ast.js +2 -1
- package/src/cli/commands/audit.js +2 -1
- package/src/cli/commands/batch.js +2 -1
- package/src/cli/commands/brief.js +12 -0
- package/src/cli/commands/cfg.js +2 -1
- package/src/cli/commands/check.js +20 -23
- package/src/cli/commands/children.js +6 -1
- package/src/cli/commands/complexity.js +2 -1
- package/src/cli/commands/context.js +6 -1
- package/src/cli/commands/dataflow.js +2 -1
- package/src/cli/commands/deps.js +8 -3
- package/src/cli/commands/flow.js +2 -1
- package/src/cli/commands/fn-impact.js +6 -1
- package/src/cli/commands/owners.js +4 -2
- package/src/cli/commands/query.js +6 -1
- package/src/cli/commands/roles.js +2 -1
- package/src/cli/commands/search.js +8 -2
- package/src/cli/commands/sequence.js +2 -1
- package/src/cli/commands/triage.js +38 -27
- package/src/db/connection.js +18 -12
- package/src/db/migrations.js +41 -64
- package/src/db/query-builder.js +60 -4
- package/src/db/repository/in-memory-repository.js +27 -16
- package/src/db/repository/nodes.js +8 -10
- package/src/domain/analysis/brief.js +155 -0
- package/src/domain/analysis/context.js +174 -190
- package/src/domain/analysis/dependencies.js +200 -146
- package/src/domain/analysis/exports.js +3 -2
- package/src/domain/analysis/impact.js +267 -152
- package/src/domain/analysis/module-map.js +247 -221
- package/src/domain/analysis/roles.js +8 -5
- package/src/domain/analysis/symbol-lookup.js +7 -5
- package/src/domain/graph/builder/helpers.js +1 -1
- package/src/domain/graph/builder/incremental.js +116 -90
- package/src/domain/graph/builder/pipeline.js +106 -80
- package/src/domain/graph/builder/stages/build-edges.js +318 -239
- package/src/domain/graph/builder/stages/detect-changes.js +198 -177
- package/src/domain/graph/builder/stages/insert-nodes.js +147 -139
- package/src/domain/graph/watcher.js +2 -2
- package/src/domain/parser.js +20 -11
- package/src/domain/queries.js +1 -0
- package/src/domain/search/search/filters.js +9 -5
- package/src/domain/search/search/keyword.js +12 -5
- package/src/domain/search/search/prepare.js +13 -5
- package/src/extractors/csharp.js +224 -207
- package/src/extractors/go.js +176 -172
- package/src/extractors/hcl.js +94 -78
- package/src/extractors/java.js +213 -207
- package/src/extractors/javascript.js +274 -304
- package/src/extractors/php.js +234 -221
- package/src/extractors/python.js +252 -250
- package/src/extractors/ruby.js +192 -185
- package/src/extractors/rust.js +182 -167
- package/src/features/ast.js +5 -3
- package/src/features/audit.js +4 -2
- package/src/features/boundaries.js +98 -83
- package/src/features/cfg.js +134 -143
- package/src/features/communities.js +68 -53
- package/src/features/complexity.js +143 -132
- package/src/features/dataflow.js +146 -149
- package/src/features/export.js +3 -3
- package/src/features/graph-enrichment.js +2 -2
- package/src/features/manifesto.js +9 -6
- package/src/features/owners.js +4 -3
- package/src/features/sequence.js +152 -141
- package/src/features/shared/find-nodes.js +31 -0
- package/src/features/structure.js +130 -99
- package/src/features/triage.js +83 -68
- package/src/graph/classifiers/risk.js +3 -2
- package/src/graph/classifiers/roles.js +6 -3
- package/src/index.js +1 -0
- package/src/mcp/server.js +65 -56
- package/src/mcp/tool-registry.js +13 -0
- package/src/mcp/tools/brief.js +8 -0
- package/src/mcp/tools/index.js +2 -0
- package/src/presentation/brief.js +51 -0
- package/src/presentation/queries-cli/exports.js +21 -14
- package/src/presentation/queries-cli/impact.js +55 -39
- package/src/presentation/queries-cli/inspect.js +184 -189
- package/src/presentation/queries-cli/overview.js +57 -58
- package/src/presentation/queries-cli/path.js +36 -29
- package/src/presentation/table.js +0 -8
- package/src/shared/generators.js +7 -3
- package/src/shared/kinds.js +1 -1
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
getComplexityForNode,
|
|
14
14
|
openReadonlyOrFail,
|
|
15
15
|
} from '../../db/index.js';
|
|
16
|
+
import { debug } from '../../infrastructure/logger.js';
|
|
16
17
|
import { isTestFile } from '../../infrastructure/test-filter.js';
|
|
17
18
|
import {
|
|
18
19
|
createFileLinesReader,
|
|
@@ -26,6 +27,149 @@ import { normalizeSymbol } from '../../shared/normalize.js';
|
|
|
26
27
|
import { paginateResult } from '../../shared/paginate.js';
|
|
27
28
|
import { findMatchingNodes } from './symbol-lookup.js';
|
|
28
29
|
|
|
30
|
+
function buildCallees(db, node, repoRoot, getFileLines, opts) {
|
|
31
|
+
const { noTests, depth } = opts;
|
|
32
|
+
const calleeRows = findCallees(db, node.id);
|
|
33
|
+
const filteredCallees = noTests ? calleeRows.filter((c) => !isTestFile(c.file)) : calleeRows;
|
|
34
|
+
|
|
35
|
+
const callees = filteredCallees.map((c) => {
|
|
36
|
+
const cLines = getFileLines(c.file);
|
|
37
|
+
const summary = cLines ? extractSummary(cLines, c.line) : null;
|
|
38
|
+
let calleeSource = null;
|
|
39
|
+
if (depth >= 1) {
|
|
40
|
+
calleeSource = readSourceRange(repoRoot, c.file, c.line, c.end_line);
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
name: c.name,
|
|
44
|
+
kind: c.kind,
|
|
45
|
+
file: c.file,
|
|
46
|
+
line: c.line,
|
|
47
|
+
endLine: c.end_line || null,
|
|
48
|
+
summary,
|
|
49
|
+
source: calleeSource,
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
if (depth > 1) {
|
|
54
|
+
const visited = new Set(filteredCallees.map((c) => c.id));
|
|
55
|
+
visited.add(node.id);
|
|
56
|
+
let frontier = filteredCallees.map((c) => c.id);
|
|
57
|
+
const maxDepth = Math.min(depth, 5);
|
|
58
|
+
for (let d = 2; d <= maxDepth; d++) {
|
|
59
|
+
const nextFrontier = [];
|
|
60
|
+
for (const fid of frontier) {
|
|
61
|
+
const deeper = findCallees(db, fid);
|
|
62
|
+
for (const c of deeper) {
|
|
63
|
+
if (!visited.has(c.id) && (!noTests || !isTestFile(c.file))) {
|
|
64
|
+
visited.add(c.id);
|
|
65
|
+
nextFrontier.push(c.id);
|
|
66
|
+
const cLines = getFileLines(c.file);
|
|
67
|
+
callees.push({
|
|
68
|
+
name: c.name,
|
|
69
|
+
kind: c.kind,
|
|
70
|
+
file: c.file,
|
|
71
|
+
line: c.line,
|
|
72
|
+
endLine: c.end_line || null,
|
|
73
|
+
summary: cLines ? extractSummary(cLines, c.line) : null,
|
|
74
|
+
source: readSourceRange(repoRoot, c.file, c.line, c.end_line),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
frontier = nextFrontier;
|
|
80
|
+
if (frontier.length === 0) break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return callees;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function buildCallers(db, node, noTests) {
|
|
88
|
+
let callerRows = findCallers(db, node.id);
|
|
89
|
+
|
|
90
|
+
if (node.kind === 'method' && node.name.includes('.')) {
|
|
91
|
+
const methodName = node.name.split('.').pop();
|
|
92
|
+
const relatedMethods = resolveMethodViaHierarchy(db, methodName);
|
|
93
|
+
for (const rm of relatedMethods) {
|
|
94
|
+
if (rm.id === node.id) continue;
|
|
95
|
+
const extraCallers = findCallers(db, rm.id);
|
|
96
|
+
callerRows.push(...extraCallers.map((c) => ({ ...c, viaHierarchy: rm.name })));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (noTests) callerRows = callerRows.filter((c) => !isTestFile(c.file));
|
|
100
|
+
|
|
101
|
+
return callerRows.map((c) => ({
|
|
102
|
+
name: c.name,
|
|
103
|
+
kind: c.kind,
|
|
104
|
+
file: c.file,
|
|
105
|
+
line: c.line,
|
|
106
|
+
viaHierarchy: c.viaHierarchy || undefined,
|
|
107
|
+
}));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function buildRelatedTests(db, node, getFileLines, includeTests) {
|
|
111
|
+
const testCallerRows = findCallers(db, node.id);
|
|
112
|
+
const testCallers = testCallerRows.filter((c) => isTestFile(c.file));
|
|
113
|
+
|
|
114
|
+
const testsByFile = new Map();
|
|
115
|
+
for (const tc of testCallers) {
|
|
116
|
+
if (!testsByFile.has(tc.file)) testsByFile.set(tc.file, []);
|
|
117
|
+
testsByFile.get(tc.file).push(tc);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const relatedTests = [];
|
|
121
|
+
for (const [file] of testsByFile) {
|
|
122
|
+
const tLines = getFileLines(file);
|
|
123
|
+
const testNames = [];
|
|
124
|
+
if (tLines) {
|
|
125
|
+
for (const tl of tLines) {
|
|
126
|
+
const tm = tl.match(/(?:it|test|describe)\s*\(\s*['"`]([^'"`]+)['"`]/);
|
|
127
|
+
if (tm) testNames.push(tm[1]);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const testSource = includeTests && tLines ? tLines.join('\n') : undefined;
|
|
131
|
+
relatedTests.push({
|
|
132
|
+
file,
|
|
133
|
+
testCount: testNames.length,
|
|
134
|
+
testNames,
|
|
135
|
+
source: testSource,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return relatedTests;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function getComplexityMetrics(db, nodeId) {
|
|
143
|
+
try {
|
|
144
|
+
const cRow = getComplexityForNode(db, nodeId);
|
|
145
|
+
if (!cRow) return null;
|
|
146
|
+
return {
|
|
147
|
+
cognitive: cRow.cognitive,
|
|
148
|
+
cyclomatic: cRow.cyclomatic,
|
|
149
|
+
maxNesting: cRow.max_nesting,
|
|
150
|
+
maintainabilityIndex: cRow.maintainability_index || 0,
|
|
151
|
+
halsteadVolume: cRow.halstead_volume || 0,
|
|
152
|
+
};
|
|
153
|
+
} catch (e) {
|
|
154
|
+
debug(`complexity lookup failed for node ${nodeId}: ${e.message}`);
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function getNodeChildrenSafe(db, nodeId) {
|
|
160
|
+
try {
|
|
161
|
+
return findNodeChildren(db, nodeId).map((c) => ({
|
|
162
|
+
name: c.name,
|
|
163
|
+
kind: c.kind,
|
|
164
|
+
line: c.line,
|
|
165
|
+
endLine: c.end_line || null,
|
|
166
|
+
}));
|
|
167
|
+
} catch (e) {
|
|
168
|
+
debug(`findNodeChildren failed for node ${nodeId}: ${e.message}`);
|
|
169
|
+
return [];
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
29
173
|
function explainFileImpl(db, target, getFileLines) {
|
|
30
174
|
const fileNodes = findFileNodes(db, `%${target}%`);
|
|
31
175
|
if (fileNodes.length === 0) return [];
|
|
@@ -49,14 +193,10 @@ function explainFileImpl(db, target, getFileLines) {
|
|
|
49
193
|
const publicApi = symbols.filter((s) => publicIds.has(s.id)).map(mapSymbol);
|
|
50
194
|
const internal = symbols.filter((s) => !publicIds.has(s.id)).map(mapSymbol);
|
|
51
195
|
|
|
52
|
-
// Imports / importedBy
|
|
53
196
|
const imports = findImportTargets(db, fn.id).map((r) => ({ file: r.file }));
|
|
54
|
-
|
|
55
197
|
const importedBy = findImportSources(db, fn.id).map((r) => ({ file: r.file }));
|
|
56
198
|
|
|
57
|
-
// Intra-file data flow
|
|
58
199
|
const intraEdges = findIntraFileCallEdges(db, fn.file);
|
|
59
|
-
|
|
60
200
|
const dataFlowMap = new Map();
|
|
61
201
|
for (const edge of intraEdges) {
|
|
62
202
|
if (!dataFlowMap.has(edge.caller_name)) dataFlowMap.set(edge.caller_name, []);
|
|
@@ -67,7 +207,6 @@ function explainFileImpl(db, target, getFileLines) {
|
|
|
67
207
|
callees,
|
|
68
208
|
}));
|
|
69
209
|
|
|
70
|
-
// Line count: prefer node_metrics (actual), fall back to MAX(end_line)
|
|
71
210
|
const metric = db
|
|
72
211
|
.prepare(`SELECT nm.line_count FROM node_metrics nm WHERE nm.node_id = ?`)
|
|
73
212
|
.get(fn.id);
|
|
@@ -95,7 +234,7 @@ function explainFileImpl(db, target, getFileLines) {
|
|
|
95
234
|
function explainFunctionImpl(db, target, noTests, getFileLines) {
|
|
96
235
|
let nodes = db
|
|
97
236
|
.prepare(
|
|
98
|
-
`SELECT * FROM nodes WHERE name LIKE ? AND kind IN ('function','method','class','interface','type','struct','enum','trait','record','module') ORDER BY file, line`,
|
|
237
|
+
`SELECT * FROM nodes WHERE name LIKE ? AND kind IN ('function','method','class','interface','type','struct','enum','trait','record','module','constant') ORDER BY file, line`,
|
|
99
238
|
)
|
|
100
239
|
.all(`%${target}%`);
|
|
101
240
|
if (noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
|
|
@@ -129,29 +268,12 @@ function explainFunctionImpl(db, target, noTests, getFileLines) {
|
|
|
129
268
|
.filter((r) => isTestFile(r.file) && !seenFiles.has(r.file) && seenFiles.add(r.file))
|
|
130
269
|
.map((r) => ({ file: r.file }));
|
|
131
270
|
|
|
132
|
-
// Complexity metrics
|
|
133
|
-
let complexityMetrics = null;
|
|
134
|
-
try {
|
|
135
|
-
const cRow = getComplexityForNode(db, node.id);
|
|
136
|
-
if (cRow) {
|
|
137
|
-
complexityMetrics = {
|
|
138
|
-
cognitive: cRow.cognitive,
|
|
139
|
-
cyclomatic: cRow.cyclomatic,
|
|
140
|
-
maxNesting: cRow.max_nesting,
|
|
141
|
-
maintainabilityIndex: cRow.maintainability_index || 0,
|
|
142
|
-
halsteadVolume: cRow.halstead_volume || 0,
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
} catch {
|
|
146
|
-
/* table may not exist */
|
|
147
|
-
}
|
|
148
|
-
|
|
149
271
|
return {
|
|
150
272
|
...normalizeSymbol(node, db, hc),
|
|
151
273
|
lineCount,
|
|
152
274
|
summary,
|
|
153
275
|
signature,
|
|
154
|
-
complexity:
|
|
276
|
+
complexity: getComplexityMetrics(db, node.id),
|
|
155
277
|
callees,
|
|
156
278
|
callers,
|
|
157
279
|
relatedTests,
|
|
@@ -159,6 +281,28 @@ function explainFunctionImpl(db, target, noTests, getFileLines) {
|
|
|
159
281
|
});
|
|
160
282
|
}
|
|
161
283
|
|
|
284
|
+
function explainCallees(parentResults, currentDepth, visited, db, noTests, getFileLines) {
|
|
285
|
+
if (currentDepth <= 0) return;
|
|
286
|
+
for (const r of parentResults) {
|
|
287
|
+
const newCallees = [];
|
|
288
|
+
for (const callee of r.callees) {
|
|
289
|
+
const key = `${callee.name}:${callee.file}:${callee.line}`;
|
|
290
|
+
if (visited.has(key)) continue;
|
|
291
|
+
visited.add(key);
|
|
292
|
+
const calleeResults = explainFunctionImpl(db, callee.name, noTests, getFileLines);
|
|
293
|
+
const exact = calleeResults.find((cr) => cr.file === callee.file && cr.line === callee.line);
|
|
294
|
+
if (exact) {
|
|
295
|
+
exact._depth = (r._depth || 0) + 1;
|
|
296
|
+
newCallees.push(exact);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (newCallees.length > 0) {
|
|
300
|
+
r.depDetails = newCallees;
|
|
301
|
+
explainCallees(newCallees, currentDepth - 1, visited, db, noTests, getFileLines);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
162
306
|
// ─── Exported functions ──────────────────────────────────────────────────
|
|
163
307
|
|
|
164
308
|
export function contextData(name, customDbPath, opts = {}) {
|
|
@@ -177,156 +321,22 @@ export function contextData(name, customDbPath, opts = {}) {
|
|
|
177
321
|
return { name, results: [] };
|
|
178
322
|
}
|
|
179
323
|
|
|
180
|
-
// No hardcoded slice — pagination handles bounding via limit/offset
|
|
181
|
-
|
|
182
324
|
const getFileLines = createFileLinesReader(repoRoot);
|
|
183
325
|
|
|
184
326
|
const results = nodes.map((node) => {
|
|
185
327
|
const fileLines = getFileLines(node.file);
|
|
186
328
|
|
|
187
|
-
// Source
|
|
188
329
|
const source = noSource
|
|
189
330
|
? null
|
|
190
331
|
: readSourceRange(repoRoot, node.file, node.line, node.end_line);
|
|
191
332
|
|
|
192
|
-
// Signature
|
|
193
333
|
const signature = fileLines ? extractSignature(fileLines, node.line) : null;
|
|
194
334
|
|
|
195
|
-
|
|
196
|
-
const
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
const
|
|
200
|
-
const cLines = getFileLines(c.file);
|
|
201
|
-
const summary = cLines ? extractSummary(cLines, c.line) : null;
|
|
202
|
-
let calleeSource = null;
|
|
203
|
-
if (depth >= 1) {
|
|
204
|
-
calleeSource = readSourceRange(repoRoot, c.file, c.line, c.end_line);
|
|
205
|
-
}
|
|
206
|
-
return {
|
|
207
|
-
name: c.name,
|
|
208
|
-
kind: c.kind,
|
|
209
|
-
file: c.file,
|
|
210
|
-
line: c.line,
|
|
211
|
-
endLine: c.end_line || null,
|
|
212
|
-
summary,
|
|
213
|
-
source: calleeSource,
|
|
214
|
-
};
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
// Deep callee expansion via BFS (depth > 1, capped at 5)
|
|
218
|
-
if (depth > 1) {
|
|
219
|
-
const visited = new Set(filteredCallees.map((c) => c.id));
|
|
220
|
-
visited.add(node.id);
|
|
221
|
-
let frontier = filteredCallees.map((c) => c.id);
|
|
222
|
-
const maxDepth = Math.min(depth, 5);
|
|
223
|
-
for (let d = 2; d <= maxDepth; d++) {
|
|
224
|
-
const nextFrontier = [];
|
|
225
|
-
for (const fid of frontier) {
|
|
226
|
-
const deeper = findCallees(db, fid);
|
|
227
|
-
for (const c of deeper) {
|
|
228
|
-
if (!visited.has(c.id) && (!noTests || !isTestFile(c.file))) {
|
|
229
|
-
visited.add(c.id);
|
|
230
|
-
nextFrontier.push(c.id);
|
|
231
|
-
const cLines = getFileLines(c.file);
|
|
232
|
-
callees.push({
|
|
233
|
-
name: c.name,
|
|
234
|
-
kind: c.kind,
|
|
235
|
-
file: c.file,
|
|
236
|
-
line: c.line,
|
|
237
|
-
endLine: c.end_line || null,
|
|
238
|
-
summary: cLines ? extractSummary(cLines, c.line) : null,
|
|
239
|
-
source: readSourceRange(repoRoot, c.file, c.line, c.end_line),
|
|
240
|
-
});
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
frontier = nextFrontier;
|
|
245
|
-
if (frontier.length === 0) break;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// Callers
|
|
250
|
-
let callerRows = findCallers(db, node.id);
|
|
251
|
-
|
|
252
|
-
// Method hierarchy resolution
|
|
253
|
-
if (node.kind === 'method' && node.name.includes('.')) {
|
|
254
|
-
const methodName = node.name.split('.').pop();
|
|
255
|
-
const relatedMethods = resolveMethodViaHierarchy(db, methodName);
|
|
256
|
-
for (const rm of relatedMethods) {
|
|
257
|
-
if (rm.id === node.id) continue;
|
|
258
|
-
const extraCallers = findCallers(db, rm.id);
|
|
259
|
-
callerRows.push(...extraCallers.map((c) => ({ ...c, viaHierarchy: rm.name })));
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
if (noTests) callerRows = callerRows.filter((c) => !isTestFile(c.file));
|
|
263
|
-
|
|
264
|
-
const callers = callerRows.map((c) => ({
|
|
265
|
-
name: c.name,
|
|
266
|
-
kind: c.kind,
|
|
267
|
-
file: c.file,
|
|
268
|
-
line: c.line,
|
|
269
|
-
viaHierarchy: c.viaHierarchy || undefined,
|
|
270
|
-
}));
|
|
271
|
-
|
|
272
|
-
// Related tests: callers that live in test files
|
|
273
|
-
const testCallerRows = findCallers(db, node.id);
|
|
274
|
-
const testCallers = testCallerRows.filter((c) => isTestFile(c.file));
|
|
275
|
-
|
|
276
|
-
const testsByFile = new Map();
|
|
277
|
-
for (const tc of testCallers) {
|
|
278
|
-
if (!testsByFile.has(tc.file)) testsByFile.set(tc.file, []);
|
|
279
|
-
testsByFile.get(tc.file).push(tc);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
const relatedTests = [];
|
|
283
|
-
for (const [file] of testsByFile) {
|
|
284
|
-
const tLines = getFileLines(file);
|
|
285
|
-
const testNames = [];
|
|
286
|
-
if (tLines) {
|
|
287
|
-
for (const tl of tLines) {
|
|
288
|
-
const tm = tl.match(/(?:it|test|describe)\s*\(\s*['"`]([^'"`]+)['"`]/);
|
|
289
|
-
if (tm) testNames.push(tm[1]);
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
const testSource = includeTests && tLines ? tLines.join('\n') : undefined;
|
|
293
|
-
relatedTests.push({
|
|
294
|
-
file,
|
|
295
|
-
testCount: testNames.length,
|
|
296
|
-
testNames,
|
|
297
|
-
source: testSource,
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// Complexity metrics
|
|
302
|
-
let complexityMetrics = null;
|
|
303
|
-
try {
|
|
304
|
-
const cRow = getComplexityForNode(db, node.id);
|
|
305
|
-
if (cRow) {
|
|
306
|
-
complexityMetrics = {
|
|
307
|
-
cognitive: cRow.cognitive,
|
|
308
|
-
cyclomatic: cRow.cyclomatic,
|
|
309
|
-
maxNesting: cRow.max_nesting,
|
|
310
|
-
maintainabilityIndex: cRow.maintainability_index || 0,
|
|
311
|
-
halsteadVolume: cRow.halstead_volume || 0,
|
|
312
|
-
};
|
|
313
|
-
}
|
|
314
|
-
} catch {
|
|
315
|
-
/* table may not exist */
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// Children (parameters, properties, constants)
|
|
319
|
-
let nodeChildren = [];
|
|
320
|
-
try {
|
|
321
|
-
nodeChildren = findNodeChildren(db, node.id).map((c) => ({
|
|
322
|
-
name: c.name,
|
|
323
|
-
kind: c.kind,
|
|
324
|
-
line: c.line,
|
|
325
|
-
endLine: c.end_line || null,
|
|
326
|
-
}));
|
|
327
|
-
} catch {
|
|
328
|
-
/* parent_id column may not exist */
|
|
329
|
-
}
|
|
335
|
+
const callees = buildCallees(db, node, repoRoot, getFileLines, { noTests, depth });
|
|
336
|
+
const callers = buildCallers(db, node, noTests);
|
|
337
|
+
const relatedTests = buildRelatedTests(db, node, getFileLines, includeTests);
|
|
338
|
+
const complexityMetrics = getComplexityMetrics(db, node.id);
|
|
339
|
+
const nodeChildren = getNodeChildrenSafe(db, node.id);
|
|
330
340
|
|
|
331
341
|
return {
|
|
332
342
|
name: node.name,
|
|
@@ -369,35 +379,9 @@ export function explainData(target, customDbPath, opts = {}) {
|
|
|
369
379
|
? explainFileImpl(db, target, getFileLines)
|
|
370
380
|
: explainFunctionImpl(db, target, noTests, getFileLines);
|
|
371
381
|
|
|
372
|
-
// Recursive dependency explanation for function targets
|
|
373
382
|
if (kind === 'function' && depth > 0 && results.length > 0) {
|
|
374
383
|
const visited = new Set(results.map((r) => `${r.name}:${r.file}:${r.line}`));
|
|
375
|
-
|
|
376
|
-
function explainCallees(parentResults, currentDepth) {
|
|
377
|
-
if (currentDepth <= 0) return;
|
|
378
|
-
for (const r of parentResults) {
|
|
379
|
-
const newCallees = [];
|
|
380
|
-
for (const callee of r.callees) {
|
|
381
|
-
const key = `${callee.name}:${callee.file}:${callee.line}`;
|
|
382
|
-
if (visited.has(key)) continue;
|
|
383
|
-
visited.add(key);
|
|
384
|
-
const calleeResults = explainFunctionImpl(db, callee.name, noTests, getFileLines);
|
|
385
|
-
const exact = calleeResults.find(
|
|
386
|
-
(cr) => cr.file === callee.file && cr.line === callee.line,
|
|
387
|
-
);
|
|
388
|
-
if (exact) {
|
|
389
|
-
exact._depth = (r._depth || 0) + 1;
|
|
390
|
-
newCallees.push(exact);
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
if (newCallees.length > 0) {
|
|
394
|
-
r.depDetails = newCallees;
|
|
395
|
-
explainCallees(newCallees, currentDepth - 1);
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
explainCallees(results, depth);
|
|
384
|
+
explainCallees(results, depth, visited, db, noTests, getFileLines);
|
|
401
385
|
}
|
|
402
386
|
|
|
403
387
|
const base = { target, kind, results };
|