@theihtisham/ai-testgen 1.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.
Files changed (149) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +383 -0
  3. package/dist/analyzers/analyzer.d.ts +10 -0
  4. package/dist/analyzers/analyzer.d.ts.map +1 -0
  5. package/dist/analyzers/analyzer.js +131 -0
  6. package/dist/analyzers/analyzer.js.map +1 -0
  7. package/dist/analyzers/go-analyzer.d.ts +3 -0
  8. package/dist/analyzers/go-analyzer.d.ts.map +1 -0
  9. package/dist/analyzers/go-analyzer.js +244 -0
  10. package/dist/analyzers/go-analyzer.js.map +1 -0
  11. package/dist/analyzers/index.d.ts +5 -0
  12. package/dist/analyzers/index.d.ts.map +1 -0
  13. package/dist/analyzers/index.js +15 -0
  14. package/dist/analyzers/index.js.map +1 -0
  15. package/dist/analyzers/js-ts-analyzer.d.ts +3 -0
  16. package/dist/analyzers/js-ts-analyzer.d.ts.map +1 -0
  17. package/dist/analyzers/js-ts-analyzer.js +299 -0
  18. package/dist/analyzers/js-ts-analyzer.js.map +1 -0
  19. package/dist/analyzers/python-analyzer.d.ts +3 -0
  20. package/dist/analyzers/python-analyzer.d.ts.map +1 -0
  21. package/dist/analyzers/python-analyzer.js +306 -0
  22. package/dist/analyzers/python-analyzer.js.map +1 -0
  23. package/dist/cli.d.ts +3 -0
  24. package/dist/cli.d.ts.map +1 -0
  25. package/dist/cli.js +381 -0
  26. package/dist/cli.js.map +1 -0
  27. package/dist/config/defaults.d.ts +6 -0
  28. package/dist/config/defaults.d.ts.map +1 -0
  29. package/dist/config/defaults.js +80 -0
  30. package/dist/config/defaults.js.map +1 -0
  31. package/dist/config/index.d.ts +3 -0
  32. package/dist/config/index.d.ts.map +1 -0
  33. package/dist/config/index.js +14 -0
  34. package/dist/config/index.js.map +1 -0
  35. package/dist/config/loader.d.ts +6 -0
  36. package/dist/config/loader.d.ts.map +1 -0
  37. package/dist/config/loader.js +126 -0
  38. package/dist/config/loader.js.map +1 -0
  39. package/dist/coverage.d.ts +4 -0
  40. package/dist/coverage.d.ts.map +1 -0
  41. package/dist/coverage.js +108 -0
  42. package/dist/coverage.js.map +1 -0
  43. package/dist/generators/ai-generator.d.ts +4 -0
  44. package/dist/generators/ai-generator.d.ts.map +1 -0
  45. package/dist/generators/ai-generator.js +175 -0
  46. package/dist/generators/ai-generator.js.map +1 -0
  47. package/dist/generators/generator.d.ts +4 -0
  48. package/dist/generators/generator.d.ts.map +1 -0
  49. package/dist/generators/generator.js +121 -0
  50. package/dist/generators/generator.js.map +1 -0
  51. package/dist/generators/go-generator.d.ts +3 -0
  52. package/dist/generators/go-generator.d.ts.map +1 -0
  53. package/dist/generators/go-generator.js +175 -0
  54. package/dist/generators/go-generator.js.map +1 -0
  55. package/dist/generators/index.d.ts +6 -0
  56. package/dist/generators/index.d.ts.map +1 -0
  57. package/dist/generators/index.js +16 -0
  58. package/dist/generators/index.js.map +1 -0
  59. package/dist/generators/js-ts-generator.d.ts +3 -0
  60. package/dist/generators/js-ts-generator.d.ts.map +1 -0
  61. package/dist/generators/js-ts-generator.js +331 -0
  62. package/dist/generators/js-ts-generator.js.map +1 -0
  63. package/dist/generators/python-generator.d.ts +3 -0
  64. package/dist/generators/python-generator.d.ts.map +1 -0
  65. package/dist/generators/python-generator.js +180 -0
  66. package/dist/generators/python-generator.js.map +1 -0
  67. package/dist/incremental.d.ts +16 -0
  68. package/dist/incremental.d.ts.map +1 -0
  69. package/dist/incremental.js +146 -0
  70. package/dist/incremental.js.map +1 -0
  71. package/dist/index.d.ts +9 -0
  72. package/dist/index.d.ts.map +1 -0
  73. package/dist/index.js +44 -0
  74. package/dist/index.js.map +1 -0
  75. package/dist/mutation/index.d.ts +2 -0
  76. package/dist/mutation/index.d.ts.map +1 -0
  77. package/dist/mutation/index.js +9 -0
  78. package/dist/mutation/index.js.map +1 -0
  79. package/dist/mutation/mutator.d.ts +6 -0
  80. package/dist/mutation/mutator.d.ts.map +1 -0
  81. package/dist/mutation/mutator.js +237 -0
  82. package/dist/mutation/mutator.js.map +1 -0
  83. package/dist/types.d.ts +199 -0
  84. package/dist/types.d.ts.map +1 -0
  85. package/dist/types.js +4 -0
  86. package/dist/types.js.map +1 -0
  87. package/dist/utils/file.d.ts +10 -0
  88. package/dist/utils/file.d.ts.map +1 -0
  89. package/dist/utils/file.js +108 -0
  90. package/dist/utils/file.js.map +1 -0
  91. package/dist/utils/index.d.ts +4 -0
  92. package/dist/utils/index.d.ts.map +1 -0
  93. package/dist/utils/index.js +24 -0
  94. package/dist/utils/index.js.map +1 -0
  95. package/dist/utils/language.d.ts +8 -0
  96. package/dist/utils/language.d.ts.map +1 -0
  97. package/dist/utils/language.js +137 -0
  98. package/dist/utils/language.js.map +1 -0
  99. package/dist/utils/logger.d.ts +13 -0
  100. package/dist/utils/logger.d.ts.map +1 -0
  101. package/dist/utils/logger.js +57 -0
  102. package/dist/utils/logger.js.map +1 -0
  103. package/dist/watcher/index.d.ts +2 -0
  104. package/dist/watcher/index.d.ts.map +1 -0
  105. package/dist/watcher/index.js +6 -0
  106. package/dist/watcher/index.js.map +1 -0
  107. package/dist/watcher/watcher.d.ts +19 -0
  108. package/dist/watcher/watcher.d.ts.map +1 -0
  109. package/dist/watcher/watcher.js +122 -0
  110. package/dist/watcher/watcher.js.map +1 -0
  111. package/package.json +63 -0
  112. package/src/analyzers/analyzer.ts +180 -0
  113. package/src/analyzers/go-analyzer.ts +235 -0
  114. package/src/analyzers/index.ts +4 -0
  115. package/src/analyzers/js-ts-analyzer.ts +324 -0
  116. package/src/analyzers/python-analyzer.ts +306 -0
  117. package/src/cli.ts +416 -0
  118. package/src/config/defaults.ts +81 -0
  119. package/src/config/index.ts +2 -0
  120. package/src/config/loader.ts +114 -0
  121. package/src/coverage.ts +128 -0
  122. package/src/generators/ai-generator.ts +170 -0
  123. package/src/generators/generator.ts +117 -0
  124. package/src/generators/go-generator.ts +183 -0
  125. package/src/generators/index.ts +5 -0
  126. package/src/generators/js-ts-generator.ts +379 -0
  127. package/src/generators/python-generator.ts +201 -0
  128. package/src/incremental.ts +131 -0
  129. package/src/index.ts +8 -0
  130. package/src/mutation/index.ts +1 -0
  131. package/src/mutation/mutator.ts +314 -0
  132. package/src/types.ts +240 -0
  133. package/src/utils/file.ts +73 -0
  134. package/src/utils/index.ts +3 -0
  135. package/src/utils/language.ts +114 -0
  136. package/src/utils/logger.ts +61 -0
  137. package/src/watcher/index.ts +1 -0
  138. package/src/watcher/watcher.ts +103 -0
  139. package/tests/analyzer.test.ts +429 -0
  140. package/tests/config.test.ts +171 -0
  141. package/tests/coverage.test.ts +197 -0
  142. package/tests/file-utils.test.ts +121 -0
  143. package/tests/generators.test.ts +383 -0
  144. package/tests/incremental.test.ts +108 -0
  145. package/tests/language.test.ts +90 -0
  146. package/tests/mutation.test.ts +286 -0
  147. package/tests/watcher.test.ts +35 -0
  148. package/tsconfig.json +26 -0
  149. package/vitest.config.ts +25 -0
@@ -0,0 +1,235 @@
1
+ import * as fs from 'fs';
2
+ import {
3
+ SourceAnalysis,
4
+ AnalyzedFunction,
5
+ AnalyzedInterface,
6
+ AnalyzedExport,
7
+ ImportInfo,
8
+ FunctionParam,
9
+ } from '../types.js';
10
+
11
+ export function analyzeGoSource(filePath: string): SourceAnalysis {
12
+ const content = fs.readFileSync(filePath, 'utf-8');
13
+ const lines = content.split('\n');
14
+
15
+ return {
16
+ filePath,
17
+ language: 'go',
18
+ functions: extractGoFunctions(content),
19
+ classes: [],
20
+ interfaces: extractGoInterfaces(content),
21
+ exports: extractGoExports(content),
22
+ imports: extractGoImports(content),
23
+ dependencies: extractGoImports(content).map((imp) => imp.modulePath),
24
+ linesOfCode: lines.length,
25
+ cyclomaticComplexity: calculateGoComplexity(content),
26
+ };
27
+ }
28
+
29
+ function extractGoFunctions(content: string): AnalyzedFunction[] {
30
+ const functions: AnalyzedFunction[] = [];
31
+ // Match: func [receiver] name(params) [returnType] {
32
+ const funcRegex = /^func\s+(?:\(\w+\s+\*?\*?\w+\)\s+)?(\w+)\s*\(([^)]*)\)\s*(.*?)\s*\{/gm;
33
+ let match: RegExpExecArray | null;
34
+
35
+ while ((match = funcRegex.exec(content)) !== null) {
36
+ const name = match[1]!;
37
+ const paramsStr = match[2] ?? '';
38
+ const returnTypeRaw = match[3]?.trim() ?? '';
39
+ // Strip surrounding parens from return type if present
40
+ const returnTypes = returnTypeRaw
41
+ ? (returnTypeRaw.startsWith('(') && returnTypeRaw.endsWith(')')
42
+ ? returnTypeRaw.slice(1, -1).trim()
43
+ : returnTypeRaw)
44
+ : null;
45
+
46
+ const params = parseGoParams(paramsStr);
47
+ const startLine = content.substring(0, match.index).split('\n').length;
48
+ const body = extractGoBody(content, match.index);
49
+ const endLine = startLine + body.split('\n').length - 1;
50
+ const isExported = name[0] === name[0]!.toUpperCase();
51
+
52
+ functions.push({
53
+ name,
54
+ isAsync: false,
55
+ isExported,
56
+ params,
57
+ returnType: returnTypes,
58
+ throws: [],
59
+ complexity: calculateGoComplexity(body),
60
+ hasSideEffects: detectGoSideEffects(body),
61
+ startLine,
62
+ endLine,
63
+ });
64
+ }
65
+
66
+ return functions;
67
+ }
68
+
69
+ function parseGoParams(paramsStr: string): FunctionParam[] {
70
+ if (!paramsStr.trim()) return [];
71
+
72
+ const params: FunctionParam[] = [];
73
+ const parts = paramsStr.split(',');
74
+ let pendingNames: string[] = [];
75
+ let pendingType: string | null = null;
76
+
77
+ for (const part of parts) {
78
+ const trimmed = part.trim();
79
+ if (!trimmed) continue;
80
+
81
+ const tokens = trimmed.split(/\s+/);
82
+ if (tokens.length === 1) {
83
+ pendingNames.push(tokens[0]!);
84
+ } else {
85
+ pendingNames.push(tokens[0]!);
86
+ pendingType = tokens.slice(1).join(' ');
87
+ for (const name of pendingNames) {
88
+ params.push({ name, type: pendingType, optional: false, defaultValue: null });
89
+ }
90
+ pendingNames = [];
91
+ pendingType = null;
92
+ }
93
+ }
94
+
95
+ if (pendingNames.length > 0) {
96
+ for (const name of pendingNames) {
97
+ params.push({ name, type: pendingType, optional: false, defaultValue: null });
98
+ }
99
+ }
100
+
101
+ return params;
102
+ }
103
+
104
+ function extractGoBody(content: string, startIndex: number): string {
105
+ const afterMatch = content.substring(startIndex);
106
+ let braceCount = 0;
107
+ let bodyStart = -1;
108
+
109
+ for (let i = 0; i < afterMatch.length; i++) {
110
+ if (afterMatch[i] === '{') {
111
+ if (braceCount === 0) bodyStart = i + 1;
112
+ braceCount++;
113
+ } else if (afterMatch[i] === '}') {
114
+ braceCount--;
115
+ if (braceCount === 0) {
116
+ return afterMatch.substring(bodyStart > 0 ? bodyStart : 0, i);
117
+ }
118
+ }
119
+ }
120
+
121
+ return afterMatch.substring(bodyStart > 0 ? bodyStart : 0);
122
+ }
123
+
124
+ function extractGoInterfaces(content: string): AnalyzedInterface[] {
125
+ const interfaces: AnalyzedInterface[] = [];
126
+ const ifaceRegex = /type\s+(\w+)\s+interface\s*\{([^}]*)\}/g;
127
+ let match: RegExpExecArray | null;
128
+
129
+ while ((match = ifaceRegex.exec(content)) !== null) {
130
+ const name = match[1]!;
131
+ const body = match[2] ?? '';
132
+ const methods: { name: string; params: FunctionParam[]; returnType: string | null }[] = [];
133
+
134
+ const methodRegex = /(\w+)\s*\(([^)]*)\)(?:\s*\(([^)]*)\))?/g;
135
+ let m: RegExpExecArray | null;
136
+ while ((m = methodRegex.exec(body)) !== null) {
137
+ methods.push({
138
+ name: m[1]!,
139
+ params: parseGoParams(m[2] ?? ''),
140
+ returnType: m[3]?.trim() ?? null,
141
+ });
142
+ }
143
+
144
+ interfaces.push({
145
+ name,
146
+ isExported: name[0] === name[0]!.toUpperCase(),
147
+ properties: [],
148
+ methods,
149
+ });
150
+ }
151
+
152
+ return interfaces;
153
+ }
154
+
155
+ function extractGoExports(content: string): AnalyzedExport[] {
156
+ const exports: AnalyzedExport[] = [];
157
+ const funcRegex = /^func\s+(?:\(\w+\s+\*?\w+\)\s+)?(\w+)/gm;
158
+ let match: RegExpExecArray | null;
159
+ while ((match = funcRegex.exec(content)) !== null) {
160
+ const name = match[1]!;
161
+ if (name[0] === name[0]!.toUpperCase()) {
162
+ exports.push({ name, type: 'function', filePath: '' });
163
+ }
164
+ }
165
+ return exports;
166
+ }
167
+
168
+ function extractGoImports(content: string): ImportInfo[] {
169
+ const imports: ImportInfo[] = [];
170
+ const singleImport = /^import\s+"([^"]+)"/gm;
171
+ let match: RegExpExecArray | null;
172
+ while ((match = singleImport.exec(content)) !== null) {
173
+ imports.push({
174
+ modulePath: match[1]!,
175
+ namedImports: [],
176
+ defaultImport: null,
177
+ isTypeOnly: false,
178
+ });
179
+ }
180
+
181
+ const multiImport = /import\s*\(([^)]+)\)/gs;
182
+ while ((match = multiImport.exec(content)) !== null) {
183
+ const block = match[1] ?? '';
184
+ const blockLines = block.split('\n');
185
+ for (const line of blockLines) {
186
+ const trimmed = line.trim();
187
+ if (!trimmed || trimmed.startsWith('//')) continue;
188
+ const importMatch = trimmed.match(/"([^"]+)"/);
189
+ if (importMatch) {
190
+ imports.push({
191
+ modulePath: importMatch[1]!,
192
+ namedImports: [],
193
+ defaultImport: null,
194
+ isTypeOnly: false,
195
+ });
196
+ }
197
+ }
198
+ }
199
+
200
+ return imports;
201
+ }
202
+
203
+ function calculateGoComplexity(code: string): number {
204
+ let complexity = 1;
205
+ const patterns = [
206
+ /\bif\b/g,
207
+ /\belse\b/g,
208
+ /\bfor\b/g,
209
+ /\bcase\b/g,
210
+ /\bswitch\b/g,
211
+ /\bselect\b/g,
212
+ /&&/g,
213
+ /\|\|/g,
214
+ /\brange\b/g,
215
+ ];
216
+ for (const pattern of patterns) {
217
+ const matches = code.match(pattern);
218
+ if (matches) complexity += matches.length;
219
+ }
220
+ return complexity;
221
+ }
222
+
223
+ function detectGoSideEffects(code: string): boolean {
224
+ const patterns = [
225
+ /\bfmt\.\w+\(/,
226
+ /\bos\.\w+/,
227
+ /\bio\.\w+/,
228
+ /\bhttp\.\w+/,
229
+ /\brand\.\w+/,
230
+ /\btime\.\w+/,
231
+ /\blog\.\w+/,
232
+ /\bappend\(/,
233
+ ];
234
+ return patterns.some((p) => p.test(code));
235
+ }
@@ -0,0 +1,4 @@
1
+ export { analyzeSource, detectEdgeCases, detectMocks, buildDependencyGraph } from './analyzer.js';
2
+ export { analyzeTsSource } from './js-ts-analyzer.js';
3
+ export { analyzePythonSource } from './python-analyzer.js';
4
+ export { analyzeGoSource } from './go-analyzer.js';
@@ -0,0 +1,324 @@
1
+ import * as fs from 'fs';
2
+ import {
3
+ Project,
4
+ SourceFile,
5
+ SyntaxKind,
6
+ FunctionDeclaration,
7
+ MethodDeclaration,
8
+ VariableDeclaration,
9
+ ArrowFunction,
10
+ FunctionExpression,
11
+ Block,
12
+ Node,
13
+ } from 'ts-morph';
14
+ import {
15
+ SourceAnalysis,
16
+ AnalyzedFunction,
17
+ AnalyzedClass,
18
+ AnalyzedInterface,
19
+ AnalyzedExport,
20
+ ImportInfo,
21
+ FunctionParam,
22
+ ClassProperty,
23
+ SupportedLanguage,
24
+ } from '../types.js';
25
+
26
+ export function analyzeTsSource(
27
+ filePath: string,
28
+ language: SupportedLanguage,
29
+ ): SourceAnalysis {
30
+ const project = new Project({
31
+ useInMemoryFileSystem: true,
32
+ compilerOptions: {
33
+ strict: true,
34
+ noEmit: true,
35
+ },
36
+ });
37
+
38
+ let sourceFile: SourceFile;
39
+ try {
40
+ const content = fs.readFileSync(filePath, 'utf-8');
41
+ sourceFile = project.createSourceFile(filePath, content);
42
+ } catch {
43
+ sourceFile = project.createSourceFile(filePath, '');
44
+ }
45
+
46
+ const functions = extractFunctions(sourceFile);
47
+ const classes = extractClasses(sourceFile);
48
+ const interfaces = extractInterfaces(sourceFile);
49
+ const exports = extractExports(sourceFile);
50
+ const imports = extractImports(sourceFile);
51
+ const result = {
52
+ filePath,
53
+ language,
54
+ functions,
55
+ classes,
56
+ interfaces,
57
+ exports,
58
+ imports,
59
+ dependencies: imports.map((imp) => imp.modulePath),
60
+ linesOfCode: sourceFile.getEndLineNumber() - sourceFile.getStartLineNumber() + 1,
61
+ cyclomaticComplexity: calculateFileComplexity(sourceFile),
62
+ };
63
+
64
+ // Free memory
65
+ project.removeSourceFile(sourceFile);
66
+
67
+ return result;
68
+ }
69
+
70
+ function extractFunctions(sourceFile: SourceFile): AnalyzedFunction[] {
71
+ const functions: AnalyzedFunction[] = [];
72
+
73
+ sourceFile.getFunctions().forEach((fn) => {
74
+ functions.push(analyzeFunctionDeclaration(fn));
75
+ });
76
+
77
+ sourceFile.getVariableDeclarations().forEach((varDecl) => {
78
+ const initializer = varDecl.getInitializer();
79
+ if (initializer) {
80
+ if (
81
+ initializer.getKind() === SyntaxKind.ArrowFunction ||
82
+ initializer.getKind() === SyntaxKind.FunctionExpression
83
+ ) {
84
+ functions.push(analyzeVariableFunction(varDecl, initializer as ArrowFunction | FunctionExpression));
85
+ }
86
+ }
87
+ });
88
+
89
+ return functions;
90
+ }
91
+
92
+ function analyzeFunctionDeclaration(fn: FunctionDeclaration): AnalyzedFunction {
93
+ const params = fn.getParameters().map(paramToInfo);
94
+ const returnType = fn.getReturnTypeNode()?.getText() ?? null;
95
+ const isAsync = fn.isAsync();
96
+ const isExported = fn.isExported();
97
+ const body = fn.getBody();
98
+ const bodyText = getBodyText(body);
99
+
100
+ return {
101
+ name: fn.getName() ?? 'anonymous',
102
+ isAsync,
103
+ isExported,
104
+ params,
105
+ returnType,
106
+ throws: extractThrowsFromBody(body),
107
+ complexity: calculateComplexity(bodyText),
108
+ hasSideEffects: detectSideEffects(bodyText),
109
+ startLine: fn.getStartLineNumber(),
110
+ endLine: fn.getEndLineNumber(),
111
+ };
112
+ }
113
+
114
+ function analyzeVariableFunction(
115
+ varDecl: VariableDeclaration,
116
+ fn: ArrowFunction | FunctionExpression,
117
+ ): AnalyzedFunction {
118
+ const params = fn.getParameters().map(paramToInfo);
119
+ const returnType = fn.getReturnTypeNode()?.getText() ?? null;
120
+ const isAsync = fn.isAsync();
121
+ const body = fn.getBody();
122
+ const bodyText = getBodyText(body);
123
+
124
+ return {
125
+ name: varDecl.getName(),
126
+ isAsync,
127
+ isExported: isVarExported(varDecl),
128
+ params,
129
+ returnType,
130
+ throws: extractThrowsFromBody(body),
131
+ complexity: calculateComplexity(bodyText),
132
+ hasSideEffects: detectSideEffects(bodyText),
133
+ startLine: fn.getStartLineNumber(),
134
+ endLine: fn.getEndLineNumber(),
135
+ };
136
+ }
137
+
138
+ function paramToInfo(param: import('ts-morph').ParameterDeclaration): FunctionParam {
139
+ return {
140
+ name: param.getName(),
141
+ type: param.getTypeNode()?.getText() ?? null,
142
+ optional: param.isOptional(),
143
+ defaultValue: param.getInitializer()?.getText() ?? null,
144
+ };
145
+ }
146
+
147
+ function isVarExported(varDecl: VariableDeclaration): boolean {
148
+ const statement = varDecl.getVariableStatement();
149
+ return statement?.isExported() ?? false;
150
+ }
151
+
152
+ function extractClasses(sourceFile: SourceFile): AnalyzedClass[] {
153
+ return sourceFile.getClasses().map((cls) => {
154
+ const name = cls.getName() ?? 'AnonymousClass';
155
+ const isExported = cls.isExported();
156
+ const extendsClass = cls.getExtends()?.getText() ?? null;
157
+ const implementsInterfaces = cls.getImplements().map((imp) => imp.getText());
158
+
159
+ const constructorParams = cls.getConstructors().flatMap((ctor) =>
160
+ ctor.getParameters().map(paramToInfo),
161
+ );
162
+
163
+ const methods = cls.getMethods().map((method) => analyzeMethod(method));
164
+
165
+ const properties: ClassProperty[] = cls.getProperties().map((prop) => ({
166
+ name: prop.getName(),
167
+ type: prop.getTypeNode()?.getText() ?? null,
168
+ visibility: (prop.getScope() as 'public' | 'private' | 'protected') ?? 'public',
169
+ isStatic: prop.isStatic(),
170
+ isReadonly: prop.isReadonly(),
171
+ }));
172
+
173
+ return {
174
+ name,
175
+ isExported,
176
+ constructorParams,
177
+ methods,
178
+ properties,
179
+ implements: implementsInterfaces,
180
+ extends: extendsClass,
181
+ };
182
+ });
183
+ }
184
+
185
+ function analyzeMethod(method: MethodDeclaration): AnalyzedFunction {
186
+ const params = method.getParameters().map(paramToInfo);
187
+ const returnType = method.getReturnTypeNode()?.getText() ?? null;
188
+ const body = method.getBody();
189
+ const bodyText = body?.getText() ?? '';
190
+
191
+ return {
192
+ name: method.getName(),
193
+ isAsync: method.isAsync(),
194
+ isExported: false,
195
+ params,
196
+ returnType,
197
+ throws: extractThrowsFromBody(body),
198
+ complexity: calculateComplexity(bodyText),
199
+ hasSideEffects: detectSideEffects(bodyText),
200
+ startLine: method.getStartLineNumber(),
201
+ endLine: method.getEndLineNumber(),
202
+ };
203
+ }
204
+
205
+ function extractInterfaces(sourceFile: SourceFile): AnalyzedInterface[] {
206
+ return sourceFile.getInterfaces().map((iface) => ({
207
+ name: iface.getName(),
208
+ isExported: iface.isExported(),
209
+ properties: iface.getProperties().map((prop) => ({
210
+ name: prop.getName(),
211
+ type: prop.getTypeNode()?.getText() ?? null,
212
+ optional: prop.hasQuestionToken(),
213
+ })),
214
+ methods: iface.getMethods().map((method) => ({
215
+ name: method.getName(),
216
+ params: method.getParameters().map(paramToInfo),
217
+ returnType: method.getReturnTypeNode()?.getText() ?? null,
218
+ })),
219
+ }));
220
+ }
221
+
222
+ function extractExports(sourceFile: SourceFile): AnalyzedExport[] {
223
+ const exports: AnalyzedExport[] = [];
224
+
225
+ sourceFile.getExportDeclarations().forEach((expDecl) => {
226
+ const moduleSpecifier = expDecl.getModuleSpecifierValue();
227
+ if (!moduleSpecifier) return;
228
+ expDecl.getNamedExports().forEach((named) => {
229
+ exports.push({
230
+ name: named.getName(),
231
+ type: 'constant',
232
+ filePath: moduleSpecifier,
233
+ });
234
+ });
235
+ });
236
+
237
+ const defaultExport = sourceFile.getDefaultExportSymbol();
238
+ if (defaultExport) {
239
+ exports.push({
240
+ name: defaultExport.getName() ?? 'default',
241
+ type: 'default',
242
+ filePath: sourceFile.getFilePath().toString(),
243
+ });
244
+ }
245
+
246
+ return exports;
247
+ }
248
+
249
+ function extractImports(sourceFile: SourceFile): ImportInfo[] {
250
+ return sourceFile.getImportDeclarations().map((imp) => ({
251
+ modulePath: imp.getModuleSpecifierValue(),
252
+ namedImports: imp.getNamedImports().map((ni) => ni.getName()),
253
+ defaultImport: imp.getDefaultImport()?.getText() ?? null,
254
+ isTypeOnly: imp.isTypeOnly(),
255
+ }));
256
+ }
257
+
258
+ function getBodyText(body: Node | undefined): string {
259
+ if (!body) return '';
260
+ if (body instanceof Block) {
261
+ return body.getText();
262
+ }
263
+ return body.getText();
264
+ }
265
+
266
+ function extractThrowsFromBody(body: Node | undefined): string[] {
267
+ if (!body) return [];
268
+ const throws: string[] = [];
269
+
270
+ body.getDescendantsOfKind(SyntaxKind.ThrowStatement).forEach((throwStmt) => {
271
+ const expr = throwStmt.getExpression();
272
+ if (expr) {
273
+ throws.push(expr.getText());
274
+ }
275
+ });
276
+
277
+ return throws;
278
+ }
279
+
280
+ function calculateComplexity(code: string): number {
281
+ let complexity = 1;
282
+ const patterns = [
283
+ /\bif\b/g,
284
+ /\belse\b/g,
285
+ /\bfor\b/g,
286
+ /\bwhile\b/g,
287
+ /\bcase\b/g,
288
+ /\bcatch\b/g,
289
+ /&&/g,
290
+ /\|\|/g,
291
+ /\?\?/g,
292
+ /\?\./g,
293
+ /\?[^.]/g,
294
+ ];
295
+ for (const pattern of patterns) {
296
+ const matches = code.match(pattern);
297
+ if (matches) complexity += matches.length;
298
+ }
299
+ return complexity;
300
+ }
301
+
302
+ function calculateFileComplexity(sourceFile: SourceFile): number {
303
+ const text = sourceFile.getFullText();
304
+ return calculateComplexity(text);
305
+ }
306
+
307
+ function detectSideEffects(code: string): boolean {
308
+ const sideEffectPatterns = [
309
+ /\bconsole\.\w+\(/,
310
+ /\bprocess\.\w+/,
311
+ /\bfs\.\w+\(/,
312
+ /\bfetch\(/,
313
+ /\baxios\.\w+\(/,
314
+ /\bMath\.random\(/,
315
+ /\bDate\.now\(/,
316
+ /\bnew Date\(/,
317
+ /\.push\(/,
318
+ /\.splice\(/,
319
+ /\.sort\(/,
320
+ /\bdocument\.\w+/,
321
+ /\bwindow\.\w+/,
322
+ ];
323
+ return sideEffectPatterns.some((pattern) => pattern.test(code));
324
+ }