project-graph-mcp 1.2.4 → 1.5.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/AGENT_ROLE.md +105 -29
- package/AGENT_ROLE_MINIMAL.md +23 -8
- package/README.md +100 -14
- package/package.json +1 -1
- package/src/.project-graph-cache.json +1 -1
- package/src/ai-context.js +113 -0
- package/src/analysis-cache.js +155 -0
- package/src/cli-handlers.js +131 -0
- package/src/cli.js +14 -2
- package/src/compact.js +207 -0
- package/src/complexity.js +21 -7
- package/src/compress.js +319 -0
- package/src/ctx-to-jsdoc.js +514 -0
- package/src/custom-rules.js +1 -0
- package/src/db-analysis.js +194 -0
- package/src/doc-dialect.js +716 -0
- package/src/full-analysis.js +322 -11
- package/src/graph-builder.js +32 -2
- package/src/instructions.js +3 -105
- package/src/jsdoc-checker.js +351 -0
- package/src/jsdoc-generator.js +0 -11
- package/src/lang-sql.js +309 -0
- package/src/large-files.js +1 -0
- package/src/mcp-server.js +236 -1
- package/src/mode-config.js +127 -0
- package/src/outdated-patterns.js +1 -0
- package/src/parser.js +364 -34
- package/src/similar-functions.js +1 -0
- package/src/test-annotations.js +203 -181
- package/src/tool-defs.js +318 -2
- package/src/tools.js +1 -1
- package/src/type-checker.js +188 -0
- package/src/undocumented.js +11 -12
- package/src/workspace.js +1 -1
- package/vendor/terser.mjs +49 -0
package/src/parser.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* AST Parser for JavaScript files using Acorn
|
|
3
|
-
* Extracts classes, functions, methods, properties, imports, and
|
|
3
|
+
* Extracts classes, functions, methods, properties, imports, calls, and SQL queries
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { readFileSync, readdirSync, statSync } from 'fs';
|
|
6
|
+
import { readFileSync, readdirSync, statSync, existsSync } from 'fs';
|
|
7
7
|
import { join, relative, resolve } from 'path';
|
|
8
8
|
import { parse } from '../vendor/acorn.mjs';
|
|
9
9
|
import * as walk from '../vendor/walk.mjs';
|
|
@@ -11,9 +11,10 @@ import { shouldExcludeDir, shouldExcludeFile, parseGitignore } from './filters.j
|
|
|
11
11
|
import { parseTypeScript } from './lang-typescript.js';
|
|
12
12
|
import { parsePython } from './lang-python.js';
|
|
13
13
|
import { parseGo } from './lang-go.js';
|
|
14
|
+
import { parseSQL, extractSQLFromString, isSQLString } from './lang-sql.js';
|
|
14
15
|
|
|
15
16
|
/** Supported source file extensions */
|
|
16
|
-
const SOURCE_EXTENSIONS = ['.js', '.ts', '.tsx', '.py', '.go'];
|
|
17
|
+
const SOURCE_EXTENSIONS = ['.js', '.ts', '.tsx', '.py', '.go', '.sql'];
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* @typedef {Object} ClassInfo
|
|
@@ -22,6 +23,8 @@ const SOURCE_EXTENSIONS = ['.js', '.ts', '.tsx', '.py', '.go'];
|
|
|
22
23
|
* @property {string[]} methods
|
|
23
24
|
* @property {string[]} properties
|
|
24
25
|
* @property {string[]} calls
|
|
26
|
+
* @property {string[]} [dbReads] - Tables read by SQL queries
|
|
27
|
+
* @property {string[]} [dbWrites] - Tables written by SQL queries
|
|
25
28
|
* @property {string} file
|
|
26
29
|
* @property {number} line
|
|
27
30
|
*/
|
|
@@ -31,6 +34,8 @@ const SOURCE_EXTENSIONS = ['.js', '.ts', '.tsx', '.py', '.go'];
|
|
|
31
34
|
* @property {string} name
|
|
32
35
|
* @property {boolean} exported
|
|
33
36
|
* @property {string[]} calls
|
|
37
|
+
* @property {string[]} [dbReads] - Tables read by SQL queries
|
|
38
|
+
* @property {string[]} [dbWrites] - Tables written by SQL queries
|
|
34
39
|
* @property {string} file
|
|
35
40
|
* @property {number} line
|
|
36
41
|
*/
|
|
@@ -59,12 +64,15 @@ export async function parseFile(code, filename) {
|
|
|
59
64
|
exports: [],
|
|
60
65
|
};
|
|
61
66
|
|
|
67
|
+
// Collect JSDoc comments for type extraction
|
|
68
|
+
const comments = [];
|
|
62
69
|
let ast;
|
|
63
70
|
try {
|
|
64
71
|
ast = parse(code, {
|
|
65
72
|
ecmaVersion: 'latest',
|
|
66
73
|
sourceType: 'module',
|
|
67
74
|
locations: true,
|
|
75
|
+
onComment: comments,
|
|
68
76
|
});
|
|
69
77
|
} catch (e) {
|
|
70
78
|
// If parsing fails, return empty result
|
|
@@ -72,6 +80,9 @@ export async function parseFile(code, filename) {
|
|
|
72
80
|
return result;
|
|
73
81
|
}
|
|
74
82
|
|
|
83
|
+
// Build JSDoc type map: endLine → { params: [{name, type}], returns: string }
|
|
84
|
+
const jsdocMap = buildJSDocTypeMap(comments, code);
|
|
85
|
+
|
|
75
86
|
// Track exported names
|
|
76
87
|
const exportedNames = new Set();
|
|
77
88
|
|
|
@@ -120,6 +131,8 @@ export async function parseFile(code, filename) {
|
|
|
120
131
|
methods: [],
|
|
121
132
|
properties: [],
|
|
122
133
|
calls: [],
|
|
134
|
+
dbReads: [],
|
|
135
|
+
dbWrites: [],
|
|
123
136
|
file: filename,
|
|
124
137
|
line: node.loc.start.line,
|
|
125
138
|
};
|
|
@@ -129,8 +142,8 @@ export async function parseFile(code, filename) {
|
|
|
129
142
|
if (element.type === 'MethodDefinition' && element.key.name !== 'constructor') {
|
|
130
143
|
classInfo.methods.push(element.key.name);
|
|
131
144
|
|
|
132
|
-
// Extract calls from method body
|
|
133
|
-
|
|
145
|
+
// Extract calls and SQL from method body
|
|
146
|
+
extractCallsAndSQL(element.value.body, classInfo.calls, classInfo.dbReads, classInfo.dbWrites);
|
|
134
147
|
} else if (element.type === 'PropertyDefinition') {
|
|
135
148
|
const propName = element.key.name;
|
|
136
149
|
|
|
@@ -151,15 +164,32 @@ export async function parseFile(code, filename) {
|
|
|
151
164
|
// Standalone function declarations
|
|
152
165
|
FunctionDeclaration(node) {
|
|
153
166
|
if (node.id) {
|
|
167
|
+
const rawParams = node.params.map(p => {
|
|
168
|
+
if (p.type === 'Identifier') return p.name;
|
|
169
|
+
if (p.type === 'AssignmentPattern' && p.left.type === 'Identifier') return p.left.name + '=';
|
|
170
|
+
if (p.type === 'RestElement' && p.argument.type === 'Identifier') return '...' + p.argument.name;
|
|
171
|
+
if (p.type === 'ObjectPattern') return 'options';
|
|
172
|
+
return '?';
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// Enrich params with JSDoc types
|
|
176
|
+
const jsdoc = findJSDocForNode(jsdocMap, node.loc.start.line);
|
|
177
|
+
const typedParams = enrichParamsWithTypes(rawParams, jsdoc);
|
|
178
|
+
|
|
154
179
|
const funcInfo = {
|
|
155
180
|
name: node.id.name,
|
|
156
181
|
exported: false, // Will be updated later
|
|
182
|
+
params: typedParams,
|
|
183
|
+
async: node.async || false,
|
|
184
|
+
returns: jsdoc?.returns || null,
|
|
157
185
|
calls: [],
|
|
186
|
+
dbReads: [],
|
|
187
|
+
dbWrites: [],
|
|
158
188
|
file: filename,
|
|
159
189
|
line: node.loc.start.line,
|
|
160
190
|
};
|
|
161
191
|
|
|
162
|
-
|
|
192
|
+
extractCallsAndSQL(node.body, funcInfo.calls, funcInfo.dbReads, funcInfo.dbWrites);
|
|
163
193
|
result.functions.push(funcInfo);
|
|
164
194
|
}
|
|
165
195
|
},
|
|
@@ -176,82 +206,262 @@ export async function parseFile(code, filename) {
|
|
|
176
206
|
return result;
|
|
177
207
|
}
|
|
178
208
|
|
|
209
|
+
/** DB client method names that accept SQL as first argument */
|
|
210
|
+
const DB_METHODS = new Set(['query', 'execute', 'raw', 'exec', 'queryFile', 'none', 'one', 'many', 'any', 'oneOrNone', 'manyOrNone', 'result']);
|
|
211
|
+
|
|
179
212
|
/**
|
|
180
|
-
* Extract method calls from AST node
|
|
213
|
+
* Extract method calls AND SQL queries from AST node in a single walk.
|
|
214
|
+
* Combines what was previously two separate walk.simple() calls.
|
|
181
215
|
* @param {Object} node
|
|
182
216
|
* @param {string[]} calls
|
|
217
|
+
* @param {string[]} [dbReads]
|
|
218
|
+
* @param {string[]} [dbWrites]
|
|
183
219
|
*/
|
|
184
|
-
function
|
|
220
|
+
function extractCallsAndSQL(node, calls, dbReads, dbWrites) {
|
|
185
221
|
if (!node) return;
|
|
186
222
|
|
|
187
223
|
walk.simple(node, {
|
|
188
224
|
CallExpression(callNode) {
|
|
189
225
|
const callee = callNode.callee;
|
|
190
226
|
|
|
227
|
+
// === Call extraction ===
|
|
191
228
|
if (callee.type === 'MemberExpression') {
|
|
192
|
-
// obj.method() or this.method()
|
|
193
229
|
const object = callee.object;
|
|
194
230
|
const property = callee.property;
|
|
195
231
|
|
|
196
232
|
if (property.type === 'Identifier') {
|
|
197
233
|
if (object.type === 'Identifier') {
|
|
198
|
-
// Class.method() or obj.method()
|
|
199
234
|
const call = `${object.name}.${property.name}`;
|
|
200
|
-
if (!calls.includes(call))
|
|
201
|
-
calls.push(call);
|
|
202
|
-
}
|
|
235
|
+
if (!calls.includes(call)) calls.push(call);
|
|
203
236
|
} else if (object.type === 'MemberExpression' && object.property.type === 'Identifier') {
|
|
204
|
-
// this.obj.method()
|
|
205
237
|
const call = `${object.property.name}.${property.name}`;
|
|
206
|
-
if (!calls.includes(call))
|
|
207
|
-
calls.push(call);
|
|
208
|
-
}
|
|
238
|
+
if (!calls.includes(call)) calls.push(call);
|
|
209
239
|
} else if (object.type === 'ThisExpression') {
|
|
210
|
-
// this.method() - internal call
|
|
211
240
|
const call = property.name;
|
|
212
|
-
if (!calls.includes(call))
|
|
213
|
-
calls.push(call);
|
|
214
|
-
}
|
|
241
|
+
if (!calls.includes(call)) calls.push(call);
|
|
215
242
|
}
|
|
216
243
|
}
|
|
217
244
|
} else if (callee.type === 'Identifier') {
|
|
218
|
-
// Direct function call: funcName()
|
|
219
245
|
const call = callee.name;
|
|
220
|
-
if (!calls.includes(call))
|
|
221
|
-
|
|
246
|
+
if (!calls.includes(call)) calls.push(call);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// === SQL extraction from DB client calls ===
|
|
250
|
+
if (dbReads && dbWrites) {
|
|
251
|
+
const methodName = getCallMethodName(callNode);
|
|
252
|
+
if (methodName && DB_METHODS.has(methodName) && callNode.arguments.length > 0) {
|
|
253
|
+
const sqlStr = extractStringValue(callNode.arguments[0]);
|
|
254
|
+
if (sqlStr && isSQLString(sqlStr)) {
|
|
255
|
+
const ext = extractSQLFromString(sqlStr);
|
|
256
|
+
ext.reads.forEach(t => { if (!dbReads.includes(t)) dbReads.push(t); });
|
|
257
|
+
ext.writes.forEach(t => { if (!dbWrites.includes(t)) dbWrites.push(t); });
|
|
258
|
+
}
|
|
222
259
|
}
|
|
223
260
|
}
|
|
224
261
|
},
|
|
262
|
+
|
|
263
|
+
// === SQL: Tagged templates ===
|
|
264
|
+
TaggedTemplateExpression(tagNode) {
|
|
265
|
+
if (!dbReads || !dbWrites) return;
|
|
266
|
+
const tagName = getTagName(tagNode.tag);
|
|
267
|
+
if (tagName && /sql/i.test(tagName)) {
|
|
268
|
+
const sqlStr = templateToString(tagNode.quasi);
|
|
269
|
+
if (sqlStr) {
|
|
270
|
+
const ext = extractSQLFromString(sqlStr);
|
|
271
|
+
ext.reads.forEach(t => { if (!dbReads.includes(t)) dbReads.push(t); });
|
|
272
|
+
ext.writes.forEach(t => { if (!dbWrites.includes(t)) dbWrites.push(t); });
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
|
|
277
|
+
// === SQL: Standalone template literals ===
|
|
278
|
+
TemplateLiteral(tplNode) {
|
|
279
|
+
if (!dbReads || !dbWrites) return;
|
|
280
|
+
const sqlStr = templateToString(tplNode);
|
|
281
|
+
if (sqlStr && isSQLString(sqlStr)) {
|
|
282
|
+
const ext = extractSQLFromString(sqlStr);
|
|
283
|
+
ext.reads.forEach(t => { if (!dbReads.includes(t)) dbReads.push(t); });
|
|
284
|
+
ext.writes.forEach(t => { if (!dbWrites.includes(t)) dbWrites.push(t); });
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
|
|
288
|
+
// === SQL: String literals ===
|
|
289
|
+
Literal(litNode) {
|
|
290
|
+
if (!dbReads || !dbWrites) return;
|
|
291
|
+
if (typeof litNode.value === 'string' && isSQLString(litNode.value)) {
|
|
292
|
+
const ext = extractSQLFromString(litNode.value);
|
|
293
|
+
ext.reads.forEach(t => { if (!dbReads.includes(t)) dbReads.push(t); });
|
|
294
|
+
ext.writes.forEach(t => { if (!dbWrites.includes(t)) dbWrites.push(t); });
|
|
295
|
+
}
|
|
296
|
+
},
|
|
225
297
|
});
|
|
226
298
|
}
|
|
227
299
|
|
|
300
|
+
/**
|
|
301
|
+
* Get tag name from tagged template expression.
|
|
302
|
+
* Handles: sql`...`, Prisma.sql`...`, db.sql`...`
|
|
303
|
+
* @param {Object} tag - AST node
|
|
304
|
+
* @returns {string|null}
|
|
305
|
+
*/
|
|
306
|
+
function getTagName(tag) {
|
|
307
|
+
if (tag.type === 'Identifier') return tag.name;
|
|
308
|
+
if (tag.type === 'MemberExpression' && tag.property.type === 'Identifier') {
|
|
309
|
+
return tag.property.name;
|
|
310
|
+
}
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Get method name from a CallExpression callee.
|
|
316
|
+
* @param {Object} callNode
|
|
317
|
+
* @returns {string|null}
|
|
318
|
+
*/
|
|
319
|
+
function getCallMethodName(callNode) {
|
|
320
|
+
const callee = callNode.callee;
|
|
321
|
+
if (callee.type === 'MemberExpression' && callee.property.type === 'Identifier') {
|
|
322
|
+
return callee.property.name;
|
|
323
|
+
}
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Extract string value from AST node (Literal or TemplateLiteral).
|
|
329
|
+
* For templates with expressions, substitutes $N placeholders.
|
|
330
|
+
* @param {Object} node
|
|
331
|
+
* @returns {string|null}
|
|
332
|
+
*/
|
|
333
|
+
function extractStringValue(node) {
|
|
334
|
+
if (!node) return null;
|
|
335
|
+
if (node.type === 'Literal' && typeof node.value === 'string') {
|
|
336
|
+
return node.value;
|
|
337
|
+
}
|
|
338
|
+
if (node.type === 'TemplateLiteral') {
|
|
339
|
+
return templateToString(node);
|
|
340
|
+
}
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Convert TemplateLiteral AST node to string.
|
|
346
|
+
* Expressions are replaced with $N placeholders.
|
|
347
|
+
* @param {Object} tplNode
|
|
348
|
+
* @returns {string}
|
|
349
|
+
*/
|
|
350
|
+
function templateToString(tplNode) {
|
|
351
|
+
if (!tplNode || !tplNode.quasis) return '';
|
|
352
|
+
let result = '';
|
|
353
|
+
for (let i = 0; i < tplNode.quasis.length; i++) {
|
|
354
|
+
result += tplNode.quasis[i].value.cooked || tplNode.quasis[i].value.raw || '';
|
|
355
|
+
if (i < tplNode.expressions?.length) {
|
|
356
|
+
result += '$' + (i + 1);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return result;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Discover sub-projects in a monorepo directory structure
|
|
364
|
+
* @param {string} rootDir
|
|
365
|
+
* @returns {Array<{name: string, path: string, absolutePath: string}>}
|
|
366
|
+
*/
|
|
367
|
+
export function discoverSubProjects(rootDir) {
|
|
368
|
+
const resolvedRoot = resolve(rootDir);
|
|
369
|
+
const subProjects = [];
|
|
370
|
+
|
|
371
|
+
// Known monorepo directory conventions
|
|
372
|
+
const MONO_DIRS = ['packages', 'apps', 'services', 'modules', 'libs', 'plugins'];
|
|
373
|
+
|
|
374
|
+
for (const monoDir of MONO_DIRS) {
|
|
375
|
+
const monoPath = join(resolvedRoot, monoDir);
|
|
376
|
+
if (!existsSync(monoPath)) continue;
|
|
377
|
+
|
|
378
|
+
try {
|
|
379
|
+
for (const entry of readdirSync(monoPath)) {
|
|
380
|
+
const entryPath = join(monoPath, entry);
|
|
381
|
+
const pkgPath = join(entryPath, 'package.json');
|
|
382
|
+
if (statSync(entryPath).isDirectory() && existsSync(pkgPath)) {
|
|
383
|
+
try {
|
|
384
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
385
|
+
subProjects.push({
|
|
386
|
+
name: pkg.name || entry,
|
|
387
|
+
path: relative(resolvedRoot, entryPath),
|
|
388
|
+
absolutePath: entryPath,
|
|
389
|
+
});
|
|
390
|
+
} catch {
|
|
391
|
+
subProjects.push({ name: entry, path: relative(resolvedRoot, entryPath), absolutePath: entryPath });
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
} catch { /* dir not readable */ }
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return subProjects;
|
|
399
|
+
}
|
|
400
|
+
|
|
228
401
|
/**
|
|
229
402
|
* Parse all JS files in a directory
|
|
230
403
|
* @param {string} dir
|
|
404
|
+
* @param {Object} [options={}]
|
|
405
|
+
* @param {boolean} [options.recursive=false]
|
|
231
406
|
* @returns {Promise<ParseResult>}
|
|
232
407
|
*/
|
|
233
|
-
export async function parseProject(dir) {
|
|
408
|
+
export async function parseProject(dir, options = {}) {
|
|
234
409
|
const result = {
|
|
235
410
|
files: [],
|
|
236
411
|
classes: [],
|
|
237
412
|
functions: [],
|
|
238
413
|
imports: [],
|
|
239
414
|
exports: [],
|
|
415
|
+
tables: [],
|
|
240
416
|
};
|
|
241
417
|
|
|
242
418
|
const resolvedDir = resolve(dir);
|
|
243
419
|
const files = findJSFiles(dir);
|
|
244
420
|
|
|
245
421
|
for (const file of files) {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
422
|
+
try {
|
|
423
|
+
const content = readFileSync(file, 'utf-8');
|
|
424
|
+
const relPath = relative(resolvedDir, file);
|
|
425
|
+
const parsed = await parseFileByExtension(content, relPath);
|
|
426
|
+
|
|
427
|
+
result.files.push(relPath);
|
|
428
|
+
result.classes.push(...parsed.classes);
|
|
429
|
+
result.functions.push(...parsed.functions);
|
|
430
|
+
result.imports.push(...parsed.imports);
|
|
431
|
+
result.exports.push(...parsed.exports);
|
|
432
|
+
if (parsed.tables?.length) {
|
|
433
|
+
result.tables.push(...parsed.tables);
|
|
434
|
+
}
|
|
435
|
+
} catch (e) {
|
|
436
|
+
// Ignore unreadable files
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Recursive monorepo support
|
|
441
|
+
if (options.recursive) {
|
|
442
|
+
const subs = discoverSubProjects(dir);
|
|
443
|
+
result.subProjects = [];
|
|
444
|
+
for (const sub of subs) {
|
|
445
|
+
try {
|
|
446
|
+
const subResult = await parseProject(sub.absolutePath);
|
|
447
|
+
// Prefix all file paths with sub-project path
|
|
448
|
+
for (const f of subResult.files) {
|
|
449
|
+
result.files.push(join(sub.path, f));
|
|
450
|
+
}
|
|
451
|
+
for (const c of subResult.classes) {
|
|
452
|
+
c.file = join(sub.path, c.file);
|
|
453
|
+
result.classes.push(c);
|
|
454
|
+
}
|
|
455
|
+
for (const fn of subResult.functions) {
|
|
456
|
+
fn.file = join(sub.path, fn.file);
|
|
457
|
+
result.functions.push(fn);
|
|
458
|
+
}
|
|
459
|
+
result.imports.push(...subResult.imports);
|
|
460
|
+
result.exports.push(...subResult.exports);
|
|
461
|
+
if (subResult.tables?.length) result.tables.push(...subResult.tables);
|
|
462
|
+
result.subProjects.push({ name: sub.name, path: sub.path, files: subResult.files.length });
|
|
463
|
+
} catch { /* sub-project parse failure is non-fatal */ }
|
|
464
|
+
}
|
|
255
465
|
}
|
|
256
466
|
|
|
257
467
|
// Dedupe imports/exports
|
|
@@ -268,6 +478,9 @@ export async function parseProject(dir) {
|
|
|
268
478
|
* @returns {Promise<ParseResult>}
|
|
269
479
|
*/
|
|
270
480
|
async function parseFileByExtension(code, filename) {
|
|
481
|
+
if (filename.endsWith('.sql')) {
|
|
482
|
+
return parseSQL(code, filename);
|
|
483
|
+
}
|
|
271
484
|
if (filename.endsWith('.py')) {
|
|
272
485
|
return parsePython(code, filename);
|
|
273
486
|
}
|
|
@@ -330,3 +543,120 @@ export function findJSFiles(dir, rootDir = dir) {
|
|
|
330
543
|
|
|
331
544
|
return files;
|
|
332
545
|
}
|
|
546
|
+
|
|
547
|
+
// ============================
|
|
548
|
+
// JSDoc Type Extraction
|
|
549
|
+
// ============================
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Build a map of JSDoc comment end-lines to their extracted type info.
|
|
553
|
+
* @param {Array} comments - Acorn onComment array
|
|
554
|
+
* @param {string} code - Full source code
|
|
555
|
+
* @returns {Map<number, {params: Array<{name: string, type: string}>, returns: string|null}>}
|
|
556
|
+
*/
|
|
557
|
+
function buildJSDocTypeMap(comments, code) {
|
|
558
|
+
const map = new Map();
|
|
559
|
+
|
|
560
|
+
for (const comment of comments) {
|
|
561
|
+
// Only process JSDoc blocks (/** ... */)
|
|
562
|
+
if (comment.type !== 'Block' || !comment.value.startsWith('*')) continue;
|
|
563
|
+
|
|
564
|
+
const text = '/*' + comment.value + '*/';
|
|
565
|
+
const endLine = code.slice(0, comment.end).split('\n').length;
|
|
566
|
+
|
|
567
|
+
// Parse @param tags with balanced brace matching
|
|
568
|
+
const params = [];
|
|
569
|
+
const paramStartRegex = /@param\s+\{/g;
|
|
570
|
+
let paramStart;
|
|
571
|
+
while ((paramStart = paramStartRegex.exec(text)) !== null) {
|
|
572
|
+
// Find matching closing brace (balanced — handles {Array<{text: string}>})
|
|
573
|
+
let depth = 1;
|
|
574
|
+
let i = paramStart.index + paramStart[0].length;
|
|
575
|
+
while (i < text.length && depth > 0) {
|
|
576
|
+
if (text[i] === '{') depth++;
|
|
577
|
+
else if (text[i] === '}') depth--;
|
|
578
|
+
i++;
|
|
579
|
+
}
|
|
580
|
+
if (depth !== 0) continue;
|
|
581
|
+
const type = text.slice(paramStart.index + paramStart[0].length, i - 1);
|
|
582
|
+
// Extract param name after the closing brace
|
|
583
|
+
const afterType = text.slice(i);
|
|
584
|
+
const nameMatch = afterType.match(/^\s+(\[?\w+(?:\.\w+)*\]?)/);
|
|
585
|
+
if (!nameMatch) continue;
|
|
586
|
+
let name = nameMatch[1];
|
|
587
|
+
// Strip [] from optional params: [opts] → opts
|
|
588
|
+
if (name.startsWith('[')) name = name.slice(1);
|
|
589
|
+
if (name.endsWith(']')) name = name.slice(0, -1);
|
|
590
|
+
// Skip dotted paths (options.x)
|
|
591
|
+
if (name.includes('.')) continue;
|
|
592
|
+
params.push({ name, type });
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Parse @returns {Type}
|
|
596
|
+
let returns = null;
|
|
597
|
+
const returnsMatch = text.match(/@returns?\s+\{([^}]+)\}/);
|
|
598
|
+
if (returnsMatch) {
|
|
599
|
+
returns = returnsMatch[1];
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if (params.length > 0 || returns) {
|
|
603
|
+
map.set(endLine, { params, returns });
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
return map;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Find the JSDoc entry that applies to a function at the given line.
|
|
612
|
+
* JSDoc must end within 2 lines above the function declaration.
|
|
613
|
+
* @param {Map} jsdocMap
|
|
614
|
+
* @param {number} funcLine - Function start line
|
|
615
|
+
* @returns {{ params: Array<{name: string, type: string}>, returns: string|null }|null}
|
|
616
|
+
*/
|
|
617
|
+
function findJSDocForNode(jsdocMap, funcLine) {
|
|
618
|
+
// JSDoc can end 1 or 2 lines above (direct or with blank line)
|
|
619
|
+
for (let offset = 1; offset <= 3; offset++) {
|
|
620
|
+
const entry = jsdocMap.get(funcLine - offset);
|
|
621
|
+
if (entry) return entry;
|
|
622
|
+
}
|
|
623
|
+
return null;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Enrich AST-extracted param names with types from JSDoc.
|
|
628
|
+
* Input: ['filePath', 'options='] + jsdoc.params: [{name:'filePath', type:'string'}, {name:'options', type:'Object'}]
|
|
629
|
+
* Output: ['filePath:string', 'options:Object=']
|
|
630
|
+
* @param {string[]} rawParams
|
|
631
|
+
* @param {Object|null} jsdoc
|
|
632
|
+
* @returns {string[]}
|
|
633
|
+
*/
|
|
634
|
+
function enrichParamsWithTypes(rawParams, jsdoc) {
|
|
635
|
+
if (!jsdoc || jsdoc.params.length === 0) return rawParams;
|
|
636
|
+
|
|
637
|
+
// Build name→type lookup from JSDoc
|
|
638
|
+
const typeMap = new Map();
|
|
639
|
+
for (const p of jsdoc.params) {
|
|
640
|
+
typeMap.set(p.name, p.type);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
return rawParams.map(param => {
|
|
644
|
+
// Parse: '...name', 'name=', 'name', 'options'
|
|
645
|
+
const isRest = param.startsWith('...');
|
|
646
|
+
const hasDefault = param.endsWith('=');
|
|
647
|
+
let cleanName = param;
|
|
648
|
+
if (isRest) cleanName = cleanName.slice(3);
|
|
649
|
+
if (hasDefault) cleanName = cleanName.slice(0, -1);
|
|
650
|
+
|
|
651
|
+
let type = typeMap.get(cleanName);
|
|
652
|
+
if (!type) return param; // No JSDoc type found
|
|
653
|
+
|
|
654
|
+
// Strip JSDoc rest indicator {...Type} — rest is already from AST
|
|
655
|
+
if (type.startsWith('...')) type = type.slice(3);
|
|
656
|
+
|
|
657
|
+
// Reconstruct: ...name:Type, name:Type=, name:Type
|
|
658
|
+
const prefix = isRest ? '...' : '';
|
|
659
|
+
const suffix = hasDefault ? '=' : '';
|
|
660
|
+
return `${prefix}${cleanName}:${type}${suffix}`;
|
|
661
|
+
});
|
|
662
|
+
}
|
package/src/similar-functions.js
CHANGED
|
@@ -63,6 +63,7 @@ function findJSFiles(dir, rootDir = dir) {
|
|
|
63
63
|
/**
|
|
64
64
|
* Extract function signatures from a file
|
|
65
65
|
* @param {string} filePath
|
|
66
|
+
* @param {string} rootDir - Root directory for relative path calculation
|
|
66
67
|
* @returns {FunctionSignature[]}
|
|
67
68
|
*/
|
|
68
69
|
function extractSignatures(filePath, rootDir) {
|