@optave/codegraph 2.5.1 → 3.0.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 +216 -89
- package/package.json +8 -7
- package/src/ast.js +392 -0
- package/src/audit.js +423 -0
- package/src/batch.js +180 -0
- package/src/boundaries.js +346 -0
- package/src/builder.js +375 -92
- package/src/cfg.js +1451 -0
- package/src/change-journal.js +130 -0
- package/src/check.js +432 -0
- package/src/cli.js +734 -107
- package/src/cochange.js +5 -2
- package/src/communities.js +7 -1
- package/src/complexity.js +124 -17
- package/src/config.js +10 -0
- package/src/dataflow.js +1187 -0
- package/src/db.js +96 -0
- package/src/embedder.js +359 -47
- package/src/export.js +305 -0
- package/src/extractors/csharp.js +64 -1
- package/src/extractors/go.js +66 -1
- package/src/extractors/hcl.js +22 -0
- package/src/extractors/java.js +61 -1
- package/src/extractors/javascript.js +142 -0
- package/src/extractors/php.js +79 -0
- package/src/extractors/python.js +134 -0
- package/src/extractors/ruby.js +89 -0
- package/src/extractors/rust.js +71 -1
- package/src/flow.js +4 -4
- package/src/index.js +78 -3
- package/src/manifesto.js +69 -1
- package/src/mcp.js +702 -193
- package/src/owners.js +359 -0
- package/src/paginate.js +37 -2
- package/src/parser.js +8 -0
- package/src/queries.js +590 -50
- package/src/snapshot.js +149 -0
- package/src/structure.js +9 -3
- package/src/triage.js +273 -0
- package/src/viewer.js +948 -0
- package/src/watcher.js +36 -1
package/src/owners.js
ADDED
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { findDbPath, openReadonlyOrFail } from './db.js';
|
|
4
|
+
import { isTestFile } from './queries.js';
|
|
5
|
+
|
|
6
|
+
// ─── CODEOWNERS Parsing ──────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
const CODEOWNERS_PATHS = ['CODEOWNERS', '.github/CODEOWNERS', 'docs/CODEOWNERS'];
|
|
9
|
+
|
|
10
|
+
/** @type {Map<string, { rules: Array, path: string, mtime: number }>} */
|
|
11
|
+
const codeownersCache = new Map();
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Find and parse a CODEOWNERS file from the standard locations.
|
|
15
|
+
* Results are cached per rootDir and invalidated when the file's mtime changes.
|
|
16
|
+
* @param {string} rootDir - Repository root directory
|
|
17
|
+
* @returns {{ rules: Array<{pattern: string, owners: string[], regex: RegExp}>, path: string } | null}
|
|
18
|
+
*/
|
|
19
|
+
export function parseCodeowners(rootDir) {
|
|
20
|
+
const cached = codeownersCache.get(rootDir);
|
|
21
|
+
|
|
22
|
+
for (const rel of CODEOWNERS_PATHS) {
|
|
23
|
+
const fullPath = path.join(rootDir, rel);
|
|
24
|
+
if (fs.existsSync(fullPath)) {
|
|
25
|
+
const mtime = fs.statSync(fullPath).mtimeMs;
|
|
26
|
+
if (cached && cached.path === rel && cached.mtime === mtime) {
|
|
27
|
+
return { rules: cached.rules, path: cached.path };
|
|
28
|
+
}
|
|
29
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
30
|
+
const rules = parseCodeownersContent(content);
|
|
31
|
+
codeownersCache.set(rootDir, { rules, path: rel, mtime });
|
|
32
|
+
return { rules, path: rel };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
codeownersCache.delete(rootDir);
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Clear the parseCodeowners cache (for testing). */
|
|
40
|
+
export function clearCodeownersCache() {
|
|
41
|
+
codeownersCache.clear();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Parse CODEOWNERS file content into rules.
|
|
46
|
+
* @param {string} content - Raw CODEOWNERS file content
|
|
47
|
+
* @returns {Array<{pattern: string, owners: string[], regex: RegExp}>}
|
|
48
|
+
*/
|
|
49
|
+
export function parseCodeownersContent(content) {
|
|
50
|
+
const rules = [];
|
|
51
|
+
for (const raw of content.split('\n')) {
|
|
52
|
+
const line = raw.trim();
|
|
53
|
+
if (!line || line.startsWith('#')) continue;
|
|
54
|
+
const parts = line.split(/\s+/);
|
|
55
|
+
if (parts.length < 2) continue;
|
|
56
|
+
const pattern = parts[0];
|
|
57
|
+
const owners = parts.slice(1).filter((p) => p.startsWith('@') || /^[^@\s]+@[^@\s]+$/.test(p));
|
|
58
|
+
if (owners.length === 0) continue;
|
|
59
|
+
rules.push({ pattern, owners, regex: patternToRegex(pattern) });
|
|
60
|
+
}
|
|
61
|
+
return rules;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Convert a CODEOWNERS glob pattern to a RegExp.
|
|
66
|
+
*
|
|
67
|
+
* CODEOWNERS semantics:
|
|
68
|
+
* - Leading `/` anchors to repo root; without it, matches anywhere
|
|
69
|
+
* - `*` matches anything except `/`
|
|
70
|
+
* - `**` matches everything including `/`
|
|
71
|
+
* - Trailing `/` matches directory contents
|
|
72
|
+
* - A bare filename like `Makefile` matches anywhere
|
|
73
|
+
*/
|
|
74
|
+
export function patternToRegex(pattern) {
|
|
75
|
+
let p = pattern;
|
|
76
|
+
const anchored = p.startsWith('/');
|
|
77
|
+
if (anchored) p = p.slice(1);
|
|
78
|
+
|
|
79
|
+
const dirMatch = p.endsWith('/');
|
|
80
|
+
if (dirMatch) p = p.slice(0, -1);
|
|
81
|
+
|
|
82
|
+
// Escape regex specials except * and ?
|
|
83
|
+
let regex = p.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
84
|
+
|
|
85
|
+
// Replace ? first (single non-slash char) before ** handling
|
|
86
|
+
regex = regex.replace(/\?/g, '[^/]');
|
|
87
|
+
|
|
88
|
+
// Handle **/ (zero or more directories) and /** (everything below)
|
|
89
|
+
// Use placeholders to prevent single-* replacement from clobbering
|
|
90
|
+
regex = regex.replace(/\*\*\//g, '<DSS>');
|
|
91
|
+
regex = regex.replace(/\/\*\*/g, '<DSE>');
|
|
92
|
+
regex = regex.replace(/\*\*/g, '<DS>');
|
|
93
|
+
regex = regex.replace(/\*/g, '[^/]*');
|
|
94
|
+
regex = regex.replace(/<DSS>/g, '(.*/)?');
|
|
95
|
+
regex = regex.replace(/<DSE>/g, '/.*');
|
|
96
|
+
regex = regex.replace(/<DS>/g, '.*');
|
|
97
|
+
|
|
98
|
+
if (dirMatch) {
|
|
99
|
+
// Pattern like `docs/` matches everything under docs/
|
|
100
|
+
regex = anchored ? `^${regex}/` : `(?:^|/)${regex}/`;
|
|
101
|
+
} else if (anchored) {
|
|
102
|
+
regex = `^${regex}$`;
|
|
103
|
+
} else if (!regex.includes('/')) {
|
|
104
|
+
// Bare filename like `Makefile` or `*.js` — match anywhere
|
|
105
|
+
regex = `(?:^|/)${regex}$`;
|
|
106
|
+
} else {
|
|
107
|
+
// Pattern with path separators but not anchored — match at start or after /
|
|
108
|
+
regex = `(?:^|/)${regex}$`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return new RegExp(regex);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Find the owners for a file path. CODEOWNERS uses last-match-wins semantics.
|
|
116
|
+
* @param {string} filePath - Relative file path (forward slashes)
|
|
117
|
+
* @param {Array<{pattern: string, owners: string[], regex: RegExp}>} rules
|
|
118
|
+
* @returns {string[]}
|
|
119
|
+
*/
|
|
120
|
+
export function matchOwners(filePath, rules) {
|
|
121
|
+
let owners = [];
|
|
122
|
+
for (const rule of rules) {
|
|
123
|
+
if (rule.regex.test(filePath)) {
|
|
124
|
+
owners = rule.owners;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return owners;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ─── Data Functions ──────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Lightweight helper for diff-impact integration.
|
|
134
|
+
* Returns owner mapping for a list of file paths.
|
|
135
|
+
* @param {string[]} filePaths - Relative file paths
|
|
136
|
+
* @param {string} repoRoot - Repository root directory
|
|
137
|
+
* @returns {{ owners: Map<string, string[]>, affectedOwners: string[], suggestedReviewers: string[] }}
|
|
138
|
+
*/
|
|
139
|
+
export function ownersForFiles(filePaths, repoRoot) {
|
|
140
|
+
const parsed = parseCodeowners(repoRoot);
|
|
141
|
+
if (!parsed) return { owners: new Map(), affectedOwners: [], suggestedReviewers: [] };
|
|
142
|
+
|
|
143
|
+
const ownersMap = new Map();
|
|
144
|
+
const ownerSet = new Set();
|
|
145
|
+
for (const file of filePaths) {
|
|
146
|
+
const fileOwners = matchOwners(file, parsed.rules);
|
|
147
|
+
ownersMap.set(file, fileOwners);
|
|
148
|
+
for (const o of fileOwners) ownerSet.add(o);
|
|
149
|
+
}
|
|
150
|
+
const affectedOwners = [...ownerSet].sort();
|
|
151
|
+
return { owners: ownersMap, affectedOwners, suggestedReviewers: affectedOwners };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Full ownership data for the graph.
|
|
156
|
+
* @param {string} [customDbPath]
|
|
157
|
+
* @param {object} [opts]
|
|
158
|
+
* @param {string} [opts.owner] - Filter to a specific owner
|
|
159
|
+
* @param {string} [opts.file] - Filter by partial file path
|
|
160
|
+
* @param {string} [opts.kind] - Filter by symbol kind
|
|
161
|
+
* @param {boolean} [opts.noTests] - Exclude test files
|
|
162
|
+
* @param {boolean} [opts.boundary] - Show cross-owner boundary edges
|
|
163
|
+
*/
|
|
164
|
+
export function ownersData(customDbPath, opts = {}) {
|
|
165
|
+
const db = openReadonlyOrFail(customDbPath);
|
|
166
|
+
const dbPath = findDbPath(customDbPath);
|
|
167
|
+
const repoRoot = path.resolve(path.dirname(dbPath), '..');
|
|
168
|
+
|
|
169
|
+
const parsed = parseCodeowners(repoRoot);
|
|
170
|
+
if (!parsed) {
|
|
171
|
+
db.close();
|
|
172
|
+
return {
|
|
173
|
+
codeownersFile: null,
|
|
174
|
+
files: [],
|
|
175
|
+
symbols: [],
|
|
176
|
+
boundaries: [],
|
|
177
|
+
summary: {
|
|
178
|
+
totalFiles: 0,
|
|
179
|
+
ownedFiles: 0,
|
|
180
|
+
unownedFiles: 0,
|
|
181
|
+
coveragePercent: 0,
|
|
182
|
+
ownerCount: 0,
|
|
183
|
+
byOwner: [],
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Get all distinct files from nodes
|
|
189
|
+
let allFiles = db
|
|
190
|
+
.prepare('SELECT DISTINCT file FROM nodes')
|
|
191
|
+
.all()
|
|
192
|
+
.map((r) => r.file);
|
|
193
|
+
|
|
194
|
+
if (opts.noTests) allFiles = allFiles.filter((f) => !isTestFile(f));
|
|
195
|
+
if (opts.file) {
|
|
196
|
+
const filter = opts.file;
|
|
197
|
+
allFiles = allFiles.filter((f) => f.includes(filter));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Map files to owners
|
|
201
|
+
const fileOwners = allFiles.map((file) => ({
|
|
202
|
+
file,
|
|
203
|
+
owners: matchOwners(file, parsed.rules),
|
|
204
|
+
}));
|
|
205
|
+
|
|
206
|
+
// Build owner-to-files index
|
|
207
|
+
const ownerIndex = new Map();
|
|
208
|
+
let ownedCount = 0;
|
|
209
|
+
for (const fo of fileOwners) {
|
|
210
|
+
if (fo.owners.length > 0) ownedCount++;
|
|
211
|
+
for (const o of fo.owners) {
|
|
212
|
+
if (!ownerIndex.has(o)) ownerIndex.set(o, []);
|
|
213
|
+
ownerIndex.get(o).push(fo.file);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Filter files if --owner specified
|
|
218
|
+
let filteredFiles = fileOwners;
|
|
219
|
+
if (opts.owner) {
|
|
220
|
+
filteredFiles = fileOwners.filter((fo) => fo.owners.includes(opts.owner));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Get symbols for filtered files
|
|
224
|
+
const fileSet = new Set(filteredFiles.map((fo) => fo.file));
|
|
225
|
+
let symbols = db
|
|
226
|
+
.prepare('SELECT name, kind, file, line FROM nodes')
|
|
227
|
+
.all()
|
|
228
|
+
.filter((n) => fileSet.has(n.file));
|
|
229
|
+
|
|
230
|
+
if (opts.noTests) symbols = symbols.filter((s) => !isTestFile(s.file));
|
|
231
|
+
if (opts.kind) symbols = symbols.filter((s) => s.kind === opts.kind);
|
|
232
|
+
|
|
233
|
+
const symbolsWithOwners = symbols.map((s) => ({
|
|
234
|
+
...s,
|
|
235
|
+
owners: matchOwners(s.file, parsed.rules),
|
|
236
|
+
}));
|
|
237
|
+
|
|
238
|
+
// Boundary analysis — cross-owner call edges
|
|
239
|
+
const boundaries = [];
|
|
240
|
+
if (opts.boundary) {
|
|
241
|
+
const edges = db
|
|
242
|
+
.prepare(
|
|
243
|
+
`SELECT e.id, e.kind AS edgeKind,
|
|
244
|
+
s.name AS srcName, s.kind AS srcKind, s.file AS srcFile, s.line AS srcLine,
|
|
245
|
+
t.name AS tgtName, t.kind AS tgtKind, t.file AS tgtFile, t.line AS tgtLine
|
|
246
|
+
FROM edges e
|
|
247
|
+
JOIN nodes s ON e.source_id = s.id
|
|
248
|
+
JOIN nodes t ON e.target_id = t.id
|
|
249
|
+
WHERE e.kind = 'calls'`,
|
|
250
|
+
)
|
|
251
|
+
.all();
|
|
252
|
+
|
|
253
|
+
for (const e of edges) {
|
|
254
|
+
if (opts.noTests && (isTestFile(e.srcFile) || isTestFile(e.tgtFile))) continue;
|
|
255
|
+
const srcOwners = matchOwners(e.srcFile, parsed.rules);
|
|
256
|
+
const tgtOwners = matchOwners(e.tgtFile, parsed.rules);
|
|
257
|
+
// Cross-boundary: different owner sets
|
|
258
|
+
const srcKey = srcOwners.sort().join(',');
|
|
259
|
+
const tgtKey = tgtOwners.sort().join(',');
|
|
260
|
+
if (srcKey !== tgtKey) {
|
|
261
|
+
boundaries.push({
|
|
262
|
+
from: {
|
|
263
|
+
name: e.srcName,
|
|
264
|
+
kind: e.srcKind,
|
|
265
|
+
file: e.srcFile,
|
|
266
|
+
line: e.srcLine,
|
|
267
|
+
owners: srcOwners,
|
|
268
|
+
},
|
|
269
|
+
to: {
|
|
270
|
+
name: e.tgtName,
|
|
271
|
+
kind: e.tgtKind,
|
|
272
|
+
file: e.tgtFile,
|
|
273
|
+
line: e.tgtLine,
|
|
274
|
+
owners: tgtOwners,
|
|
275
|
+
},
|
|
276
|
+
edgeKind: e.edgeKind,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Summary
|
|
283
|
+
const byOwner = [...ownerIndex.entries()]
|
|
284
|
+
.map(([owner, files]) => ({ owner, fileCount: files.length }))
|
|
285
|
+
.sort((a, b) => b.fileCount - a.fileCount);
|
|
286
|
+
|
|
287
|
+
db.close();
|
|
288
|
+
return {
|
|
289
|
+
codeownersFile: parsed.path,
|
|
290
|
+
files: filteredFiles,
|
|
291
|
+
symbols: symbolsWithOwners,
|
|
292
|
+
boundaries,
|
|
293
|
+
summary: {
|
|
294
|
+
totalFiles: allFiles.length,
|
|
295
|
+
ownedFiles: ownedCount,
|
|
296
|
+
unownedFiles: allFiles.length - ownedCount,
|
|
297
|
+
coveragePercent: allFiles.length > 0 ? Math.round((ownedCount / allFiles.length) * 100) : 0,
|
|
298
|
+
ownerCount: ownerIndex.size,
|
|
299
|
+
byOwner,
|
|
300
|
+
},
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ─── CLI Display ─────────────────────────────────────────────────────
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* CLI display function for the `owners` command.
|
|
308
|
+
* @param {string} [customDbPath]
|
|
309
|
+
* @param {object} [opts]
|
|
310
|
+
*/
|
|
311
|
+
export function owners(customDbPath, opts = {}) {
|
|
312
|
+
const data = ownersData(customDbPath, opts);
|
|
313
|
+
if (opts.json) {
|
|
314
|
+
console.log(JSON.stringify(data, null, 2));
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (!data.codeownersFile) {
|
|
319
|
+
console.log('No CODEOWNERS file found.');
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
console.log(`\nCODEOWNERS: ${data.codeownersFile}\n`);
|
|
324
|
+
|
|
325
|
+
const s = data.summary;
|
|
326
|
+
console.log(
|
|
327
|
+
` Coverage: ${s.coveragePercent}% (${s.ownedFiles}/${s.totalFiles} files owned, ${s.ownerCount} owners)\n`,
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
if (s.byOwner.length > 0) {
|
|
331
|
+
console.log(' Owners:\n');
|
|
332
|
+
for (const o of s.byOwner) {
|
|
333
|
+
console.log(` ${o.owner} ${o.fileCount} files`);
|
|
334
|
+
}
|
|
335
|
+
console.log();
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (data.files.length > 0 && opts.owner) {
|
|
339
|
+
console.log(` Files owned by ${opts.owner}:\n`);
|
|
340
|
+
for (const f of data.files) {
|
|
341
|
+
console.log(` ${f.file}`);
|
|
342
|
+
}
|
|
343
|
+
console.log();
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (data.boundaries.length > 0) {
|
|
347
|
+
console.log(` Cross-owner boundaries: ${data.boundaries.length} edges\n`);
|
|
348
|
+
const shown = data.boundaries.slice(0, 30);
|
|
349
|
+
for (const b of shown) {
|
|
350
|
+
const srcOwner = b.from.owners.join(', ') || '(unowned)';
|
|
351
|
+
const tgtOwner = b.to.owners.join(', ') || '(unowned)';
|
|
352
|
+
console.log(` ${b.from.name} [${srcOwner}] -> ${b.to.name} [${tgtOwner}]`);
|
|
353
|
+
}
|
|
354
|
+
if (data.boundaries.length > 30) {
|
|
355
|
+
console.log(` ... and ${data.boundaries.length - 30} more`);
|
|
356
|
+
}
|
|
357
|
+
console.log();
|
|
358
|
+
}
|
|
359
|
+
}
|
package/src/paginate.js
CHANGED
|
@@ -7,12 +7,30 @@
|
|
|
7
7
|
|
|
8
8
|
/** Default limits applied by MCP tool handlers (not by the programmatic API). */
|
|
9
9
|
export const MCP_DEFAULTS = {
|
|
10
|
+
// Existing
|
|
10
11
|
list_functions: 100,
|
|
11
|
-
|
|
12
|
+
query: 10,
|
|
12
13
|
where: 50,
|
|
13
14
|
node_roles: 100,
|
|
14
|
-
list_entry_points: 100,
|
|
15
15
|
export_graph: 500,
|
|
16
|
+
// Smaller defaults for rich/nested results
|
|
17
|
+
fn_impact: 5,
|
|
18
|
+
context: 5,
|
|
19
|
+
explain: 10,
|
|
20
|
+
file_deps: 20,
|
|
21
|
+
file_exports: 20,
|
|
22
|
+
diff_impact: 30,
|
|
23
|
+
impact_analysis: 20,
|
|
24
|
+
semantic_search: 20,
|
|
25
|
+
execution_flow: 50,
|
|
26
|
+
hotspots: 20,
|
|
27
|
+
co_changes: 20,
|
|
28
|
+
complexity: 30,
|
|
29
|
+
manifesto: 50,
|
|
30
|
+
communities: 20,
|
|
31
|
+
structure: 30,
|
|
32
|
+
triage: 20,
|
|
33
|
+
ast_query: 50,
|
|
16
34
|
};
|
|
17
35
|
|
|
18
36
|
/** Hard cap to prevent abuse via MCP. */
|
|
@@ -68,3 +86,20 @@ export function paginateResult(result, field, { limit, offset } = {}) {
|
|
|
68
86
|
const { items, pagination } = paginate(arr, { limit, offset });
|
|
69
87
|
return { ...result, [field]: items, _pagination: pagination };
|
|
70
88
|
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Print data as newline-delimited JSON (NDJSON).
|
|
92
|
+
*
|
|
93
|
+
* Emits a `_meta` line with pagination info (if present), then one JSON
|
|
94
|
+
* line per item in the named array field.
|
|
95
|
+
*
|
|
96
|
+
* @param {object} data - Result object (may contain `_pagination`)
|
|
97
|
+
* @param {string} field - Array field name to stream (e.g. `'results'`)
|
|
98
|
+
*/
|
|
99
|
+
export function printNdjson(data, field) {
|
|
100
|
+
if (data._pagination) console.log(JSON.stringify({ _meta: data._pagination }));
|
|
101
|
+
const items = data[field];
|
|
102
|
+
if (Array.isArray(items)) {
|
|
103
|
+
for (const item of items) console.log(JSON.stringify(item));
|
|
104
|
+
}
|
|
105
|
+
}
|
package/src/parser.js
CHANGED
|
@@ -142,6 +142,14 @@ function normalizeNativeSymbols(result) {
|
|
|
142
142
|
maintainabilityIndex: d.complexity.maintainabilityIndex ?? null,
|
|
143
143
|
}
|
|
144
144
|
: null,
|
|
145
|
+
children: d.children?.length
|
|
146
|
+
? d.children.map((c) => ({
|
|
147
|
+
name: c.name,
|
|
148
|
+
kind: c.kind,
|
|
149
|
+
line: c.line,
|
|
150
|
+
endLine: c.endLine ?? c.end_line ?? null,
|
|
151
|
+
}))
|
|
152
|
+
: undefined,
|
|
145
153
|
})),
|
|
146
154
|
calls: (result.calls || []).map((c) => ({
|
|
147
155
|
name: c.name,
|