@kridaydave/code-mapper 1.0.0 → 1.0.1
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/CHANGELOG.md +31 -0
- package/README.md +1 -0
- package/bin/code-mapper.mjs +86 -0
- package/dist/graph/GraphAnalyzer.js +32 -65
- package/dist/graph/GraphAnalyzer.js.map +1 -1
- package/dist/graph/GraphBuilder.js +18 -45
- package/dist/graph/GraphBuilder.js.map +1 -1
- package/dist/index.js +100 -23
- package/dist/index.js.map +1 -1
- package/dist/mcp/cache.js +8 -17
- package/dist/mcp/cache.js.map +1 -1
- package/dist/mcp/resources.js +5 -1
- package/dist/mcp/resources.js.map +1 -1
- package/dist/mcp/tools.js +190 -35
- package/dist/mcp/tools.js.map +1 -1
- package/dist/parser/ComplexityAnalyzer.js +19 -2
- package/dist/parser/ComplexityAnalyzer.js.map +1 -1
- package/dist/parser/FileAnalyzer.js +8 -30
- package/dist/parser/FileAnalyzer.js.map +1 -1
- package/dist/parser/ProjectParser.js +8 -5
- package/dist/parser/ProjectParser.js.map +1 -1
- package/dist/parser/ProjectParser.test.js +1 -17
- package/dist/parser/ProjectParser.test.js.map +1 -1
- package/dist/tui/index.js +239 -0
- package/dist/tui/index.js.map +1 -0
- package/package.json +82 -35
- package/AGENTS.md +0 -174
- package/docs/PHASE2_PLAN.md +0 -435
- package/fixtures/test-project/calculator.ts +0 -28
- package/fixtures/test-project/index.ts +0 -2
- package/fixtures/test-project/math.ts +0 -11
- package/src/graph/Graph.test.ts +0 -222
- package/src/graph/GraphAnalyzer.ts +0 -502
- package/src/graph/GraphBuilder.ts +0 -258
- package/src/graph/types.ts +0 -42
- package/src/index.ts +0 -38
- package/src/mcp/cache.ts +0 -89
- package/src/mcp/resources.ts +0 -137
- package/src/mcp/tools.test.ts +0 -104
- package/src/mcp/tools.ts +0 -529
- package/src/parser/ComplexityAnalyzer.ts +0 -275
- package/src/parser/FileAnalyzer.ts +0 -215
- package/src/parser/ProjectParser.test.ts +0 -96
- package/src/parser/ProjectParser.ts +0 -172
- package/src/parser/types.ts +0 -77
- package/src/types/graphology-pagerank.d.ts +0 -20
- package/tsconfig.json +0 -17
- package/vitest.config.ts +0 -15
|
@@ -1,275 +0,0 @@
|
|
|
1
|
-
import { SourceFile, Project, SyntaxKind } from "ts-morph";
|
|
2
|
-
import { ParseResult } from "./types.js";
|
|
3
|
-
|
|
4
|
-
export interface ComplexityResult {
|
|
5
|
-
filePath: string;
|
|
6
|
-
relativePath: string;
|
|
7
|
-
cyclomaticComplexity: number;
|
|
8
|
-
cognitiveComplexity: number;
|
|
9
|
-
nestingDepth: number;
|
|
10
|
-
linesOfCode: number;
|
|
11
|
-
functionCount: number;
|
|
12
|
-
classCount: number;
|
|
13
|
-
overallScore: number;
|
|
14
|
-
issues: string[];
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export class ComplexityAnalyzer {
|
|
18
|
-
private project: Project;
|
|
19
|
-
private baseDirectory: string;
|
|
20
|
-
|
|
21
|
-
constructor(project: Project, baseDirectory: string) {
|
|
22
|
-
this.project = project;
|
|
23
|
-
this.baseDirectory = baseDirectory;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
analyze(sourceFile: SourceFile): ComplexityResult {
|
|
27
|
-
const filePath = sourceFile.getFilePath();
|
|
28
|
-
const relativePath = this.getRelativePath(filePath);
|
|
29
|
-
const linesOfCode = sourceFile.getEndLineNumber() ?? 0;
|
|
30
|
-
const functionCount = sourceFile.getFunctions().length;
|
|
31
|
-
const classCount = sourceFile.getClasses().length;
|
|
32
|
-
|
|
33
|
-
const cyclomaticComplexity = this.calculateCyclomaticComplexity(sourceFile);
|
|
34
|
-
const cognitiveComplexity = this.calculateCognitiveComplexity(sourceFile);
|
|
35
|
-
const nestingDepth = this.calculateNestingDepth(sourceFile);
|
|
36
|
-
|
|
37
|
-
const issues = this.identifyIssues({
|
|
38
|
-
cyclomaticComplexity,
|
|
39
|
-
cognitiveComplexity,
|
|
40
|
-
nestingDepth,
|
|
41
|
-
linesOfCode,
|
|
42
|
-
functionCount,
|
|
43
|
-
classCount,
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
const overallScore = this.calculateOverallScore({
|
|
47
|
-
cyclomaticComplexity,
|
|
48
|
-
cognitiveComplexity,
|
|
49
|
-
nestingDepth,
|
|
50
|
-
linesOfCode,
|
|
51
|
-
functionCount,
|
|
52
|
-
classCount,
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
return {
|
|
56
|
-
filePath,
|
|
57
|
-
relativePath,
|
|
58
|
-
cyclomaticComplexity,
|
|
59
|
-
cognitiveComplexity,
|
|
60
|
-
nestingDepth,
|
|
61
|
-
linesOfCode,
|
|
62
|
-
functionCount,
|
|
63
|
-
classCount,
|
|
64
|
-
overallScore,
|
|
65
|
-
issues,
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
analyzeProject(parseResult: ParseResult): ComplexityResult[] {
|
|
70
|
-
const results: ComplexityResult[] = [];
|
|
71
|
-
|
|
72
|
-
for (const fileInfo of parseResult.files) {
|
|
73
|
-
try {
|
|
74
|
-
const sourceFile = this.project.getSourceFile(fileInfo.filePath);
|
|
75
|
-
if (sourceFile) {
|
|
76
|
-
results.push(this.analyze(sourceFile));
|
|
77
|
-
}
|
|
78
|
-
} catch {
|
|
79
|
-
// Skip files that can't be parsed
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return results;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
getTopComplexFiles(n: number): ComplexityResult[] {
|
|
87
|
-
const sourceFiles = this.project.getSourceFiles();
|
|
88
|
-
|
|
89
|
-
const results: ComplexityResult[] = [];
|
|
90
|
-
for (const sourceFile of sourceFiles) {
|
|
91
|
-
results.push(this.analyze(sourceFile));
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return results
|
|
95
|
-
.sort((a, b) => b.overallScore - a.overallScore)
|
|
96
|
-
.slice(0, n);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
private getRelativePath(target: string): string {
|
|
100
|
-
const baseParts = this.baseDirectory.replace(/\\/g, "/").split("/").filter(Boolean);
|
|
101
|
-
const targetParts = target.replace(/\\/g, "/").split("/").filter(Boolean);
|
|
102
|
-
|
|
103
|
-
let i = 0;
|
|
104
|
-
while (i < baseParts.length && i < targetParts.length && baseParts[i] === targetParts[i]) {
|
|
105
|
-
i++;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if (i < baseParts.length) {
|
|
109
|
-
return targetParts.slice(i).join("/");
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const up = baseParts.length - i;
|
|
113
|
-
const down = targetParts.slice(i);
|
|
114
|
-
return [...Array(up).fill(".."), ...down].join("/");
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
private calculateCyclomaticComplexity(sourceFile: SourceFile): number {
|
|
118
|
-
let complexity = 1;
|
|
119
|
-
|
|
120
|
-
sourceFile.forEachDescendant(node => {
|
|
121
|
-
const kind = node.getKind();
|
|
122
|
-
if (
|
|
123
|
-
kind === SyntaxKind.IfStatement ||
|
|
124
|
-
kind === SyntaxKind.ForStatement ||
|
|
125
|
-
kind === SyntaxKind.ForInStatement ||
|
|
126
|
-
kind === SyntaxKind.ForOfStatement ||
|
|
127
|
-
kind === SyntaxKind.WhileStatement ||
|
|
128
|
-
kind === SyntaxKind.SwitchStatement ||
|
|
129
|
-
kind === SyntaxKind.CatchClause ||
|
|
130
|
-
kind === SyntaxKind.BinaryExpression
|
|
131
|
-
) {
|
|
132
|
-
if (kind === SyntaxKind.BinaryExpression) {
|
|
133
|
-
const text = node.getText();
|
|
134
|
-
if (text.includes("&&") || text.includes("||")) {
|
|
135
|
-
complexity++;
|
|
136
|
-
}
|
|
137
|
-
} else {
|
|
138
|
-
complexity++;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
return complexity;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
private calculateCognitiveComplexity(sourceFile: SourceFile): number {
|
|
147
|
-
let complexity = 0;
|
|
148
|
-
let nestingLevel = 0;
|
|
149
|
-
|
|
150
|
-
const incrementComplexity = (kind: SyntaxKind, text?: string) => {
|
|
151
|
-
complexity++;
|
|
152
|
-
if (kind === SyntaxKind.IfStatement ||
|
|
153
|
-
kind === SyntaxKind.ForStatement ||
|
|
154
|
-
kind === SyntaxKind.ForInStatement ||
|
|
155
|
-
kind === SyntaxKind.ForOfStatement ||
|
|
156
|
-
kind === SyntaxKind.WhileStatement ||
|
|
157
|
-
kind === SyntaxKind.SwitchStatement ||
|
|
158
|
-
kind === SyntaxKind.CatchClause ||
|
|
159
|
-
kind === SyntaxKind.TryStatement ||
|
|
160
|
-
kind === SyntaxKind.ConditionalExpression) {
|
|
161
|
-
nestingLevel++;
|
|
162
|
-
}
|
|
163
|
-
if (text) {
|
|
164
|
-
if (text.includes("&&") || text.includes("||")) {
|
|
165
|
-
complexity += nestingLevel;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
const decrementNesting = (kind: SyntaxKind) => {
|
|
171
|
-
if (kind === SyntaxKind.IfStatement ||
|
|
172
|
-
kind === SyntaxKind.ForStatement ||
|
|
173
|
-
kind === SyntaxKind.ForInStatement ||
|
|
174
|
-
kind === SyntaxKind.ForOfStatement ||
|
|
175
|
-
kind === SyntaxKind.WhileStatement ||
|
|
176
|
-
kind === SyntaxKind.SwitchStatement ||
|
|
177
|
-
kind === SyntaxKind.CatchClause ||
|
|
178
|
-
kind === SyntaxKind.TryStatement ||
|
|
179
|
-
kind === SyntaxKind.ConditionalExpression) {
|
|
180
|
-
nestingLevel = Math.max(0, nestingLevel - 1);
|
|
181
|
-
}
|
|
182
|
-
};
|
|
183
|
-
|
|
184
|
-
sourceFile.forEachDescendant(node => {
|
|
185
|
-
const kind = node.getKind();
|
|
186
|
-
const text = kind === SyntaxKind.BinaryExpression ? node.getText() : undefined;
|
|
187
|
-
incrementComplexity(kind, text);
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
return complexity;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
private calculateNestingDepth(sourceFile: SourceFile): number {
|
|
194
|
-
let maxNesting = 0;
|
|
195
|
-
let currentNesting = 0;
|
|
196
|
-
|
|
197
|
-
sourceFile.forEachDescendant(node => {
|
|
198
|
-
const kind = node.getKind();
|
|
199
|
-
if (this.isControlStructure(kind)) {
|
|
200
|
-
currentNesting++;
|
|
201
|
-
maxNesting = Math.max(maxNesting, currentNesting);
|
|
202
|
-
}
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
return maxNesting;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
private isControlStructure(kind: SyntaxKind): boolean {
|
|
209
|
-
return kind === SyntaxKind.IfStatement ||
|
|
210
|
-
kind === SyntaxKind.ForStatement ||
|
|
211
|
-
kind === SyntaxKind.ForInStatement ||
|
|
212
|
-
kind === SyntaxKind.ForOfStatement ||
|
|
213
|
-
kind === SyntaxKind.WhileStatement ||
|
|
214
|
-
kind === SyntaxKind.SwitchStatement ||
|
|
215
|
-
kind === SyntaxKind.TryStatement ||
|
|
216
|
-
kind === SyntaxKind.CatchClause;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
private identifyIssues(metrics: {
|
|
220
|
-
cyclomaticComplexity: number;
|
|
221
|
-
cognitiveComplexity: number;
|
|
222
|
-
nestingDepth: number;
|
|
223
|
-
linesOfCode: number;
|
|
224
|
-
functionCount: number;
|
|
225
|
-
classCount: number;
|
|
226
|
-
}): string[] {
|
|
227
|
-
const issues: string[] = [];
|
|
228
|
-
|
|
229
|
-
if (metrics.cyclomaticComplexity > 10) {
|
|
230
|
-
issues.push(`Cyclomatic complexity (${metrics.cyclomaticComplexity}) exceeds threshold of 10`);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
if (metrics.cognitiveComplexity > 15) {
|
|
234
|
-
issues.push(`Cognitive complexity (${metrics.cognitiveComplexity}) exceeds threshold of 15`);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
if (metrics.nestingDepth > 4) {
|
|
238
|
-
issues.push(`Nesting depth (${metrics.nestingDepth}) exceeds threshold of 4`);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
if (metrics.linesOfCode > 500) {
|
|
242
|
-
issues.push(`Lines of code (${metrics.linesOfCode}) exceeds 500 - file is large`);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
if (metrics.functionCount > 20) {
|
|
246
|
-
issues.push(`Function count (${metrics.functionCount}) exceeds 20 - too many functions`);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
if (metrics.classCount > 10) {
|
|
250
|
-
issues.push(`Class count (${metrics.classCount}) exceeds 10 - too many classes`);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
return issues;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
private calculateOverallScore(metrics: {
|
|
257
|
-
cyclomaticComplexity: number;
|
|
258
|
-
cognitiveComplexity: number;
|
|
259
|
-
nestingDepth: number;
|
|
260
|
-
linesOfCode: number;
|
|
261
|
-
functionCount: number;
|
|
262
|
-
classCount: number;
|
|
263
|
-
}): number {
|
|
264
|
-
let score = 0;
|
|
265
|
-
|
|
266
|
-
score += Math.min(metrics.cyclomaticComplexity * 4, 25);
|
|
267
|
-
score += Math.min(metrics.cognitiveComplexity * 3, 25);
|
|
268
|
-
score += Math.min(metrics.nestingDepth * 5, 25);
|
|
269
|
-
score += Math.min((metrics.linesOfCode / 500) * 15, 15);
|
|
270
|
-
score += Math.min((metrics.functionCount / 20) * 5, 5);
|
|
271
|
-
score += Math.min((metrics.classCount / 10) * 5, 5);
|
|
272
|
-
|
|
273
|
-
return Math.min(Math.round(score), 100);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
import { SourceFile, Project } from "ts-morph";
|
|
2
|
-
import { FileInfo, FunctionInfo, ClassInfo, ImportInfo, ExportInfo, MethodInfo, PropertyInfo } from "./types.js";
|
|
3
|
-
|
|
4
|
-
export class FileAnalyzer {
|
|
5
|
-
private project: Project;
|
|
6
|
-
private baseDirectory: string;
|
|
7
|
-
|
|
8
|
-
constructor(project: Project, baseDirectory: string) {
|
|
9
|
-
this.project = project;
|
|
10
|
-
this.baseDirectory = baseDirectory;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
analyze(sourceFile: SourceFile): FileInfo {
|
|
14
|
-
const filePath = sourceFile.getFilePath();
|
|
15
|
-
const relativePath = this.relativePath(this.baseDirectory, filePath);
|
|
16
|
-
|
|
17
|
-
return {
|
|
18
|
-
filePath,
|
|
19
|
-
relativePath,
|
|
20
|
-
functions: this.extractFunctions(sourceFile),
|
|
21
|
-
classes: this.extractClasses(sourceFile),
|
|
22
|
-
imports: this.extractImports(sourceFile),
|
|
23
|
-
exports: this.extractExports(sourceFile),
|
|
24
|
-
totalLines: sourceFile.getEndLineNumber() ?? 0,
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
private relativePath(base: string, target: string): string {
|
|
29
|
-
const baseParts = base.replace(/\\/g, "/").split("/").filter(Boolean);
|
|
30
|
-
const targetParts = target.replace(/\\/g, "/").split("/").filter(Boolean);
|
|
31
|
-
|
|
32
|
-
// Find common prefix
|
|
33
|
-
let i = 0;
|
|
34
|
-
while (i < baseParts.length && i < targetParts.length && baseParts[i] === targetParts[i]) {
|
|
35
|
-
i++;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Verify target is actually within base directory
|
|
39
|
-
if (i < baseParts.length) {
|
|
40
|
-
throw new Error(`Target path is not within base directory: ${target} not in ${base}`);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const up = baseParts.length - i;
|
|
44
|
-
const down = targetParts.slice(i);
|
|
45
|
-
return [...Array(up).fill(".."), ...down].join("/");
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
private extractFunctions(sourceFile: SourceFile): FunctionInfo[] {
|
|
49
|
-
return sourceFile.getFunctions().map(fn => {
|
|
50
|
-
const name = fn.getName() ?? "anonymous";
|
|
51
|
-
const params = fn.getParameters().map(p => {
|
|
52
|
-
const paramName = p.getName();
|
|
53
|
-
const paramType = p.getTypeNode()?.getText() ?? "unknown";
|
|
54
|
-
return `${paramName}: ${paramType}`;
|
|
55
|
-
});
|
|
56
|
-
const returnType = fn.getReturnTypeNode()?.getText() ?? "void";
|
|
57
|
-
const body = fn.getBody()?.getText() ?? "";
|
|
58
|
-
const truncatedBody = [...body].slice(0, 200).join("");
|
|
59
|
-
|
|
60
|
-
return {
|
|
61
|
-
name,
|
|
62
|
-
filePath: sourceFile.getFilePath(),
|
|
63
|
-
lineNumber: fn.getStartLineNumber(),
|
|
64
|
-
parameters: params,
|
|
65
|
-
returnType,
|
|
66
|
-
isAsync: fn.isAsync(),
|
|
67
|
-
isExported: fn.isExported(),
|
|
68
|
-
isDefaultExport: fn.isDefaultExport(),
|
|
69
|
-
body: truncatedBody,
|
|
70
|
-
};
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
private extractClasses(sourceFile: SourceFile): ClassInfo[] {
|
|
75
|
-
return sourceFile.getClasses().map(cls => {
|
|
76
|
-
const methods: MethodInfo[] = cls.getMethods().map(m => ({
|
|
77
|
-
name: m.getName() ?? "anonymous",
|
|
78
|
-
parameters: m.getParameters().map(p => `${p.getName()}: ${p.getTypeNode()?.getText() ?? "unknown"}`),
|
|
79
|
-
returnType: m.getReturnTypeNode()?.getText() ?? "void",
|
|
80
|
-
isStatic: m.isStatic(),
|
|
81
|
-
isAsync: m.isAsync(),
|
|
82
|
-
lineNumber: m.getStartLineNumber(),
|
|
83
|
-
}));
|
|
84
|
-
|
|
85
|
-
const properties: PropertyInfo[] = cls.getProperties().map(p => ({
|
|
86
|
-
name: p.getName(),
|
|
87
|
-
type: p.getTypeNode()?.getText() ?? "unknown",
|
|
88
|
-
isStatic: p.isStatic(),
|
|
89
|
-
isReadonly: p.isReadonly(),
|
|
90
|
-
lineNumber: p.getStartLineNumber(),
|
|
91
|
-
}));
|
|
92
|
-
|
|
93
|
-
const extendsClause = cls.getExtends();
|
|
94
|
-
const implementsClauses = cls.getImplements();
|
|
95
|
-
|
|
96
|
-
return {
|
|
97
|
-
name: cls.getName() ?? "anonymous",
|
|
98
|
-
filePath: sourceFile.getFilePath(),
|
|
99
|
-
lineNumber: cls.getStartLineNumber(),
|
|
100
|
-
isExported: cls.isExported(),
|
|
101
|
-
isDefaultExport: cls.isDefaultExport(),
|
|
102
|
-
extends: extendsClause?.getText() ?? null,
|
|
103
|
-
implements: implementsClauses.map(i => i.getText()),
|
|
104
|
-
methods,
|
|
105
|
-
properties,
|
|
106
|
-
};
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
private extractImports(sourceFile: SourceFile): ImportInfo[] {
|
|
111
|
-
return sourceFile.getImportDeclarations().map(imp => {
|
|
112
|
-
const namedImports = imp.getNamedImports().map(n => n.getName());
|
|
113
|
-
const defaultImport = imp.getDefaultImport()?.getText() ?? null;
|
|
114
|
-
const namespaceImport = imp.getNamespaceImport()?.getText() ?? null;
|
|
115
|
-
|
|
116
|
-
return {
|
|
117
|
-
namedImports,
|
|
118
|
-
defaultImport,
|
|
119
|
-
namespaceImport,
|
|
120
|
-
moduleSpecifier: imp.getModuleSpecifierValue() ?? null,
|
|
121
|
-
filePath: sourceFile.getFilePath(),
|
|
122
|
-
lineNumber: imp.getStartLineNumber(),
|
|
123
|
-
};
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
private extractExports(sourceFile: SourceFile): ExportInfo[] {
|
|
128
|
-
const exports: ExportInfo[] = [];
|
|
129
|
-
|
|
130
|
-
sourceFile.getFunctions().forEach(fn => {
|
|
131
|
-
if (fn.isExported()) {
|
|
132
|
-
exports.push({
|
|
133
|
-
name: fn.getName() ?? "anonymous",
|
|
134
|
-
kind: "function",
|
|
135
|
-
isDefault: fn.isDefaultExport(),
|
|
136
|
-
filePath: sourceFile.getFilePath(),
|
|
137
|
-
lineNumber: fn.getStartLineNumber(),
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
sourceFile.getClasses().forEach(cls => {
|
|
143
|
-
if (cls.isExported()) {
|
|
144
|
-
exports.push({
|
|
145
|
-
name: cls.getName() ?? "anonymous",
|
|
146
|
-
kind: "class",
|
|
147
|
-
isDefault: cls.isDefaultExport(),
|
|
148
|
-
filePath: sourceFile.getFilePath(),
|
|
149
|
-
lineNumber: cls.getStartLineNumber(),
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
sourceFile.getExportDeclarations().forEach(exp => {
|
|
155
|
-
const namedExports = exp.getNamedExports();
|
|
156
|
-
if (namedExports.length > 0) {
|
|
157
|
-
namedExports.forEach(ne => {
|
|
158
|
-
exports.push({
|
|
159
|
-
name: ne.getName(),
|
|
160
|
-
kind: "re-export",
|
|
161
|
-
isDefault: false,
|
|
162
|
-
filePath: sourceFile.getFilePath(),
|
|
163
|
-
lineNumber: exp.getStartLineNumber(),
|
|
164
|
-
});
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
sourceFile.getVariableStatements().forEach(stmt => {
|
|
170
|
-
if (stmt.isExported()) {
|
|
171
|
-
stmt.getDeclarations().forEach(decl => {
|
|
172
|
-
exports.push({
|
|
173
|
-
name: decl.getName(),
|
|
174
|
-
kind: "variable" as const,
|
|
175
|
-
isDefault: false,
|
|
176
|
-
filePath: sourceFile.getFilePath(),
|
|
177
|
-
lineNumber: stmt.getStartLineNumber(),
|
|
178
|
-
});
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
sourceFile.getTypeAliases().forEach(ta => {
|
|
184
|
-
if (ta.isExported()) {
|
|
185
|
-
exports.push({
|
|
186
|
-
name: ta.getName(),
|
|
187
|
-
kind: "type" as const,
|
|
188
|
-
isDefault: false,
|
|
189
|
-
filePath: sourceFile.getFilePath(),
|
|
190
|
-
lineNumber: ta.getStartLineNumber(),
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
sourceFile.getInterfaces().forEach(iface => {
|
|
196
|
-
if (iface.isExported()) {
|
|
197
|
-
exports.push({
|
|
198
|
-
name: iface.getName(),
|
|
199
|
-
kind: "interface" as const,
|
|
200
|
-
isDefault: false,
|
|
201
|
-
filePath: sourceFile.getFilePath(),
|
|
202
|
-
lineNumber: iface.getStartLineNumber(),
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
const seen = new Set<string>();
|
|
208
|
-
return exports.filter(exp => {
|
|
209
|
-
const key = `${exp.name}|${exp.kind}|${exp.lineNumber}`;
|
|
210
|
-
if (seen.has(key)) return false;
|
|
211
|
-
seen.add(key);
|
|
212
|
-
return true;
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
}
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
-
import { ProjectParser } from "../parser/ProjectParser.js";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
|
|
5
|
-
describe("ProjectParser", () => {
|
|
6
|
-
let parser: ProjectParser;
|
|
7
|
-
|
|
8
|
-
beforeEach(() => {
|
|
9
|
-
parser = new ProjectParser();
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
describe("parse", () => {
|
|
13
|
-
it("should parse a directory and return file info", async () => {
|
|
14
|
-
const testDir = path.resolve("./fixtures/test-project");
|
|
15
|
-
const result = await parser.parse(testDir);
|
|
16
|
-
|
|
17
|
-
expect(result.totalFiles).toBe(3);
|
|
18
|
-
expect(result.totalFunctions).toBeGreaterThan(0);
|
|
19
|
-
expect(result.files.length).toBe(3);
|
|
20
|
-
}, 30000);
|
|
21
|
-
|
|
22
|
-
it("should count functions correctly", async () => {
|
|
23
|
-
const testDir = path.resolve("./fixtures/test-project");
|
|
24
|
-
const result = await parser.parse(testDir);
|
|
25
|
-
|
|
26
|
-
expect(result.totalFunctions).toBe(3);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it("should count classes correctly", async () => {
|
|
30
|
-
const testDir = path.resolve("./fixtures/test-project");
|
|
31
|
-
const result = await parser.parse(testDir);
|
|
32
|
-
|
|
33
|
-
expect(result.totalClasses).toBe(1);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it("should count imports correctly", async () => {
|
|
37
|
-
const testDir = path.resolve("./fixtures/test-project");
|
|
38
|
-
const result = await parser.parse(testDir);
|
|
39
|
-
|
|
40
|
-
expect(result.totalImports).toBeGreaterThan(0);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("should count exports correctly", async () => {
|
|
44
|
-
const testDir = path.resolve("./fixtures/test-project");
|
|
45
|
-
const result = await parser.parse(testDir);
|
|
46
|
-
|
|
47
|
-
expect(result.totalExports).toBeGreaterThan(0);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("should cache results for same directory", async () => {
|
|
51
|
-
const testDir = path.resolve("./fixtures/test-project");
|
|
52
|
-
const result1 = await parser.parse(testDir);
|
|
53
|
-
const result2 = await parser.parse(testDir);
|
|
54
|
-
|
|
55
|
-
expect(result1).toBe(result2);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it("should throw for non-existent directory", async () => {
|
|
59
|
-
await expect(parser.parse("/non/existent/path")).rejects.toThrow();
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it("should return empty result for empty directory", async () => {
|
|
63
|
-
const emptyDir = path.resolve("./fixtures/empty-project");
|
|
64
|
-
const fs = await import("node:fs");
|
|
65
|
-
fs.mkdirSync(emptyDir, { recursive: true });
|
|
66
|
-
|
|
67
|
-
try {
|
|
68
|
-
const result = await parser.parse(emptyDir);
|
|
69
|
-
expect(result.totalFiles).toBe(0);
|
|
70
|
-
expect(result.files).toEqual([]);
|
|
71
|
-
} finally {
|
|
72
|
-
fs.rmSync(emptyDir, { recursive: true, force: true });
|
|
73
|
-
}
|
|
74
|
-
});
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
describe("clearCache", () => {
|
|
78
|
-
it("should clear cache for specific directory", async () => {
|
|
79
|
-
const testDir = path.resolve("./fixtures/test-project");
|
|
80
|
-
await parser.parse(testDir);
|
|
81
|
-
|
|
82
|
-
parser.clearCache(testDir);
|
|
83
|
-
const result = await parser.parse(testDir);
|
|
84
|
-
expect(result.totalFiles).toBe(3);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it("should clear all caches when no directory specified", async () => {
|
|
88
|
-
const testDir = path.resolve("./fixtures/test-project");
|
|
89
|
-
await parser.parse(testDir);
|
|
90
|
-
|
|
91
|
-
parser.clearCache();
|
|
92
|
-
const result = await parser.parse(testDir);
|
|
93
|
-
expect(result.totalFiles).toBe(3);
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
});
|