@deplens/mcp 0.1.7 → 0.1.8

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.
@@ -0,0 +1,524 @@
1
+ /**
2
+ * parse-source.mjs - Source code analysis for .ts/.js files
3
+ * Extracts implementation details beyond type signatures
4
+ */
5
+
6
+ import ts from "typescript";
7
+ import fs from "fs";
8
+ import path from "path";
9
+
10
+ /**
11
+ * Calculate cyclomatic complexity of a function
12
+ * Counts decision points: if, for, while, case, catch, &&, ||, ?:
13
+ */
14
+ function calculateComplexity(node) {
15
+ let complexity = 1; // Base complexity
16
+
17
+ function visit(n) {
18
+ switch (n.kind) {
19
+ case ts.SyntaxKind.IfStatement:
20
+ case ts.SyntaxKind.ForStatement:
21
+ case ts.SyntaxKind.ForInStatement:
22
+ case ts.SyntaxKind.ForOfStatement:
23
+ case ts.SyntaxKind.WhileStatement:
24
+ case ts.SyntaxKind.DoStatement:
25
+ case ts.SyntaxKind.CaseClause:
26
+ case ts.SyntaxKind.CatchClause:
27
+ case ts.SyntaxKind.ConditionalExpression: // ternary ?:
28
+ complexity++;
29
+ break;
30
+ case ts.SyntaxKind.BinaryExpression:
31
+ const op = n.operatorToken.kind;
32
+ if (
33
+ op === ts.SyntaxKind.AmpersandAmpersandToken ||
34
+ op === ts.SyntaxKind.BarBarToken ||
35
+ op === ts.SyntaxKind.QuestionQuestionToken
36
+ ) {
37
+ complexity++;
38
+ }
39
+ break;
40
+ }
41
+ ts.forEachChild(n, visit);
42
+ }
43
+
44
+ ts.forEachChild(node, visit);
45
+ return complexity;
46
+ }
47
+
48
+ /**
49
+ * Extract imports from source file
50
+ */
51
+ function extractImports(sourceFile) {
52
+ const imports = [];
53
+
54
+ ts.forEachChild(sourceFile, (node) => {
55
+ if (ts.isImportDeclaration(node)) {
56
+ const moduleSpecifier = node.moduleSpecifier.text;
57
+ const importClause = node.importClause;
58
+
59
+ const importInfo = {
60
+ module: moduleSpecifier,
61
+ default: null,
62
+ named: [],
63
+ namespace: null,
64
+ };
65
+
66
+ if (importClause) {
67
+ // Default import
68
+ if (importClause.name) {
69
+ importInfo.default = importClause.name.text;
70
+ }
71
+ // Named imports or namespace
72
+ if (importClause.namedBindings) {
73
+ if (ts.isNamespaceImport(importClause.namedBindings)) {
74
+ importInfo.namespace = importClause.namedBindings.name.text;
75
+ } else if (ts.isNamedImports(importClause.namedBindings)) {
76
+ importClause.namedBindings.elements.forEach((el) => {
77
+ importInfo.named.push(el.name.text);
78
+ });
79
+ }
80
+ }
81
+ }
82
+
83
+ imports.push(importInfo);
84
+ }
85
+ });
86
+
87
+ return imports;
88
+ }
89
+
90
+ /**
91
+ * Extract dependencies used within a function body
92
+ */
93
+ function extractDependencies(node, imports) {
94
+ const deps = new Set();
95
+ const builtins = new Set([
96
+ "console",
97
+ "setTimeout",
98
+ "clearTimeout",
99
+ "setInterval",
100
+ "clearInterval",
101
+ "Promise",
102
+ "Array",
103
+ "Object",
104
+ "String",
105
+ "Number",
106
+ "Boolean",
107
+ "Date",
108
+ "Math",
109
+ "JSON",
110
+ "Error",
111
+ "Map",
112
+ "Set",
113
+ "WeakMap",
114
+ "WeakSet",
115
+ "Symbol",
116
+ "Proxy",
117
+ "Reflect",
118
+ ]);
119
+
120
+ const importedNames = new Set();
121
+ imports.forEach((imp) => {
122
+ if (imp.default) importedNames.add(imp.default);
123
+ if (imp.namespace) importedNames.add(imp.namespace);
124
+ imp.named.forEach((n) => importedNames.add(n));
125
+ });
126
+
127
+ function visit(n) {
128
+ if (ts.isIdentifier(n)) {
129
+ const name = n.text;
130
+ if (importedNames.has(name)) {
131
+ deps.add(name);
132
+ } else if (builtins.has(name)) {
133
+ deps.add(`[builtin] ${name}`);
134
+ }
135
+ }
136
+ ts.forEachChild(n, visit);
137
+ }
138
+
139
+ ts.forEachChild(node, visit);
140
+ return Array.from(deps);
141
+ }
142
+
143
+ /**
144
+ * Detect patterns and edge case handling
145
+ */
146
+ function detectPatterns(node) {
147
+ const patterns = [];
148
+
149
+ function visit(n) {
150
+ // Try-catch
151
+ if (ts.isTryStatement(n)) {
152
+ patterns.push("error-handling");
153
+ }
154
+ // Null checks
155
+ if (ts.isBinaryExpression(n)) {
156
+ const op = n.operatorToken.kind;
157
+ if (
158
+ op === ts.SyntaxKind.EqualsEqualsEqualsToken ||
159
+ op === ts.SyntaxKind.ExclamationEqualsEqualsToken
160
+ ) {
161
+ const left = n.left.getText ? n.left.getText() : "";
162
+ const right = n.right.getText ? n.right.getText() : "";
163
+ if (
164
+ left === "null" ||
165
+ right === "null" ||
166
+ left === "undefined" ||
167
+ right === "undefined"
168
+ ) {
169
+ patterns.push("null-check");
170
+ }
171
+ }
172
+ // Optional chaining check via ??
173
+ if (op === ts.SyntaxKind.QuestionQuestionToken) {
174
+ patterns.push("nullish-coalescing");
175
+ }
176
+ }
177
+ // Optional chaining
178
+ if (
179
+ n.kind === ts.SyntaxKind.PropertyAccessExpression &&
180
+ n.questionDotToken
181
+ ) {
182
+ patterns.push("optional-chaining");
183
+ }
184
+ // Type guards
185
+ if (
186
+ ts.isTypeOfExpression(n) ||
187
+ (ts.isCallExpression(n) && n.expression.getText?.() === "typeof")
188
+ ) {
189
+ patterns.push("type-guard");
190
+ }
191
+ // Async/await
192
+ if (ts.isAwaitExpression(n)) {
193
+ patterns.push("async-await");
194
+ }
195
+ // Generators
196
+ if (ts.isYieldExpression(n)) {
197
+ patterns.push("generator");
198
+ }
199
+ // Closures (arrow functions or function expressions inside)
200
+ if (ts.isArrowFunction(n) || ts.isFunctionExpression(n)) {
201
+ patterns.push("closure");
202
+ }
203
+ // Recursion detection (function calling itself)
204
+ if (ts.isCallExpression(n) && ts.isIdentifier(n.expression)) {
205
+ // Will be checked later with function name context
206
+ }
207
+
208
+ ts.forEachChild(n, visit);
209
+ }
210
+
211
+ ts.forEachChild(node, visit);
212
+ return [...new Set(patterns)]; // Dedupe
213
+ }
214
+
215
+ /**
216
+ * Get function body as string (truncated)
217
+ */
218
+ function getFunctionBody(node, sourceFile, maxLines = 15) {
219
+ if (!node.body) return null;
220
+
221
+ const bodyText = node.body.getText(sourceFile);
222
+ const lines = bodyText.split("\n");
223
+
224
+ if (lines.length <= maxLines) {
225
+ return bodyText;
226
+ }
227
+
228
+ return (
229
+ lines.slice(0, maxLines).join("\n") +
230
+ `\n... (${lines.length - maxLines} more lines)`
231
+ );
232
+ }
233
+
234
+ /**
235
+ * Count lines of code in a node
236
+ */
237
+ function countLines(node, sourceFile) {
238
+ const text = node.getText(sourceFile);
239
+ return text.split("\n").length;
240
+ }
241
+
242
+ /**
243
+ * Parse source file and extract detailed analysis
244
+ */
245
+ export function parseSourceFile(filePath, options = {}) {
246
+ const { filter, maxBodyLines = 15, includeBody = true } = options;
247
+
248
+ if (!fs.existsSync(filePath)) {
249
+ return { error: `File not found: ${filePath}`, functions: {} };
250
+ }
251
+
252
+ const content = fs.readFileSync(filePath, "utf-8");
253
+ const sourceFile = ts.createSourceFile(
254
+ path.basename(filePath),
255
+ content,
256
+ ts.ScriptTarget.Latest,
257
+ true,
258
+ filePath.endsWith(".tsx") || filePath.endsWith(".jsx")
259
+ ? ts.ScriptKind.TSX
260
+ : filePath.endsWith(".ts")
261
+ ? ts.ScriptKind.TS
262
+ : ts.ScriptKind.JS,
263
+ );
264
+
265
+ const imports = extractImports(sourceFile);
266
+ const functions = {};
267
+
268
+ function shouldInclude(name) {
269
+ if (!filter) return true;
270
+ return name.toLowerCase().includes(filter.toLowerCase());
271
+ }
272
+
273
+ function analyzeFunction(node, name, isExported, isAsync) {
274
+ if (!shouldInclude(name)) return;
275
+
276
+ const complexity = calculateComplexity(node);
277
+ const lines = countLines(node, sourceFile);
278
+ const deps = extractDependencies(node, imports);
279
+ const patterns = detectPatterns(node);
280
+ const body = includeBody
281
+ ? getFunctionBody(node, sourceFile, maxBodyLines)
282
+ : null;
283
+
284
+ // Get parameters
285
+ const params = node.parameters
286
+ ? node.parameters.map((p) => {
287
+ const paramName = p.name.getText(sourceFile);
288
+ const paramType = p.type ? p.type.getText(sourceFile) : "any";
289
+ const optional = !!p.questionToken;
290
+ const defaultValue = p.initializer
291
+ ? p.initializer.getText(sourceFile)
292
+ : null;
293
+ return {
294
+ name: paramName,
295
+ type: paramType,
296
+ optional,
297
+ default: defaultValue,
298
+ };
299
+ })
300
+ : [];
301
+
302
+ // Get return type
303
+ const returnType = node.type ? node.type.getText(sourceFile) : "inferred";
304
+
305
+ functions[name] = {
306
+ exported: isExported,
307
+ async: isAsync,
308
+ params,
309
+ returnType,
310
+ complexity,
311
+ lines,
312
+ dependencies: deps,
313
+ patterns,
314
+ ...(body && { body }),
315
+ };
316
+ }
317
+
318
+ function visit(node) {
319
+ // Function declarations
320
+ if (ts.isFunctionDeclaration(node) && node.name) {
321
+ const name = node.name.text;
322
+ const isExported = node.modifiers?.some(
323
+ (m) => m.kind === ts.SyntaxKind.ExportKeyword,
324
+ );
325
+ const isAsync = node.modifiers?.some(
326
+ (m) => m.kind === ts.SyntaxKind.AsyncKeyword,
327
+ );
328
+ analyzeFunction(node, name, isExported, isAsync);
329
+ }
330
+
331
+ // Arrow functions assigned to variables
332
+ if (ts.isVariableStatement(node)) {
333
+ const isExported = node.modifiers?.some(
334
+ (m) => m.kind === ts.SyntaxKind.ExportKeyword,
335
+ );
336
+
337
+ node.declarationList.declarations.forEach((decl) => {
338
+ if (decl.initializer && ts.isArrowFunction(decl.initializer)) {
339
+ const name = decl.name.getText(sourceFile);
340
+ const isAsync = decl.initializer.modifiers?.some(
341
+ (m) => m.kind === ts.SyntaxKind.AsyncKeyword,
342
+ );
343
+ analyzeFunction(decl.initializer, name, isExported, isAsync);
344
+ }
345
+ // Function expressions
346
+ if (decl.initializer && ts.isFunctionExpression(decl.initializer)) {
347
+ const name = decl.name.getText(sourceFile);
348
+ const isAsync = decl.initializer.modifiers?.some(
349
+ (m) => m.kind === ts.SyntaxKind.AsyncKeyword,
350
+ );
351
+ analyzeFunction(decl.initializer, name, isExported, isAsync);
352
+ }
353
+ });
354
+ }
355
+
356
+ // Class methods
357
+ if (ts.isClassDeclaration(node) && node.name) {
358
+ const className = node.name.text;
359
+ const isClassExported = node.modifiers?.some(
360
+ (m) => m.kind === ts.SyntaxKind.ExportKeyword,
361
+ );
362
+
363
+ node.members.forEach((member) => {
364
+ if (ts.isMethodDeclaration(member) && member.name) {
365
+ const methodName = `${className}.${member.name.getText(sourceFile)}`;
366
+ const isAsync = member.modifiers?.some(
367
+ (m) => m.kind === ts.SyntaxKind.AsyncKeyword,
368
+ );
369
+ analyzeFunction(member, methodName, isClassExported, isAsync);
370
+ }
371
+ });
372
+ }
373
+
374
+ ts.forEachChild(node, visit);
375
+ }
376
+
377
+ visit(sourceFile);
378
+
379
+ return {
380
+ file: filePath,
381
+ imports,
382
+ functions,
383
+ totalFunctions: Object.keys(functions).length,
384
+ };
385
+ }
386
+
387
+ /**
388
+ * Find source files for a package
389
+ */
390
+ export function findSourceFiles(pkgDir, options = {}) {
391
+ const {
392
+ maxFiles = 10,
393
+ include = [
394
+ "src/**/*.ts",
395
+ "src/**/*.js",
396
+ "lib/**/*.ts",
397
+ "lib/**/*.js",
398
+ "dist/**/*.js",
399
+ "*.ts",
400
+ "*.js",
401
+ ],
402
+ } = options;
403
+
404
+ const sourceFiles = [];
405
+
406
+ for (const pattern of include) {
407
+ const fullPattern = path.join(pkgDir, pattern);
408
+ // Simple glob-like matching
409
+ const baseDir = path.dirname(fullPattern.split("*")[0]);
410
+ if (!fs.existsSync(baseDir)) continue;
411
+
412
+ function walkDir(dir, depth = 0) {
413
+ if (depth > 5 || sourceFiles.length >= maxFiles) return;
414
+
415
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
416
+ for (const entry of entries) {
417
+ if (sourceFiles.length >= maxFiles) break;
418
+
419
+ const fullPath = path.join(dir, entry.name);
420
+ if (
421
+ entry.isDirectory() &&
422
+ !entry.name.startsWith(".") &&
423
+ entry.name !== "node_modules"
424
+ ) {
425
+ walkDir(fullPath, depth + 1);
426
+ } else if (
427
+ entry.isFile() &&
428
+ (entry.name.endsWith(".ts") ||
429
+ entry.name.endsWith(".tsx") ||
430
+ entry.name.endsWith(".js") ||
431
+ entry.name.endsWith(".jsx"))
432
+ ) {
433
+ // Skip declaration files
434
+ if (!entry.name.endsWith(".d.ts")) {
435
+ sourceFiles.push(fullPath);
436
+ }
437
+ }
438
+ }
439
+ }
440
+
441
+ if (fs.existsSync(baseDir)) {
442
+ walkDir(baseDir);
443
+ }
444
+ }
445
+
446
+ return sourceFiles;
447
+ }
448
+
449
+ /**
450
+ * Analyze a package's source code
451
+ */
452
+ export function analyzePackageSource(pkgDir, options = {}) {
453
+ const {
454
+ filter,
455
+ maxFiles = 5,
456
+ maxBodyLines = 10,
457
+ includeBody = false,
458
+ } = options;
459
+
460
+ const sourceFiles = findSourceFiles(pkgDir, { maxFiles });
461
+
462
+ if (sourceFiles.length === 0) {
463
+ return { error: "No source files found", files: [] };
464
+ }
465
+
466
+ const results = {
467
+ files: [],
468
+ summary: {
469
+ totalFiles: sourceFiles.length,
470
+ totalFunctions: 0,
471
+ avgComplexity: 0,
472
+ highComplexityFunctions: [],
473
+ },
474
+ };
475
+
476
+ let totalComplexity = 0;
477
+ let functionCount = 0;
478
+
479
+ for (const file of sourceFiles) {
480
+ const analysis = parseSourceFile(file, {
481
+ filter,
482
+ maxBodyLines,
483
+ includeBody,
484
+ });
485
+
486
+ if (analysis.error) continue;
487
+
488
+ results.files.push({
489
+ path: path.relative(pkgDir, file),
490
+ functions: analysis.functions,
491
+ imports: analysis.imports,
492
+ });
493
+
494
+ for (const [name, info] of Object.entries(analysis.functions)) {
495
+ functionCount++;
496
+ totalComplexity += info.complexity;
497
+
498
+ if (info.complexity >= 10) {
499
+ results.summary.highComplexityFunctions.push({
500
+ name,
501
+ file: path.relative(pkgDir, file),
502
+ complexity: info.complexity,
503
+ });
504
+ }
505
+ }
506
+ }
507
+
508
+ results.summary.totalFunctions = functionCount;
509
+ results.summary.avgComplexity =
510
+ functionCount > 0
511
+ ? Math.round((totalComplexity / functionCount) * 10) / 10
512
+ : 0;
513
+ results.summary.highComplexityFunctions.sort(
514
+ (a, b) => b.complexity - a.complexity,
515
+ );
516
+
517
+ return results;
518
+ }
519
+
520
+ export default {
521
+ parseSourceFile,
522
+ findSourceFiles,
523
+ analyzePackageSource,
524
+ };