@getmikk/core 1.8.2 → 1.9.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/package.json +3 -1
- package/src/constants.ts +285 -0
- package/src/contract/contract-generator.ts +7 -0
- package/src/contract/index.ts +2 -3
- package/src/contract/lock-compiler.ts +74 -42
- package/src/contract/lock-reader.ts +24 -4
- package/src/contract/schema.ts +27 -1
- package/src/error-handler.ts +430 -0
- package/src/graph/cluster-detector.ts +45 -20
- package/src/graph/confidence-engine.ts +60 -0
- package/src/graph/dead-code-detector.ts +27 -5
- package/src/graph/graph-builder.ts +298 -238
- package/src/graph/impact-analyzer.ts +131 -114
- package/src/graph/index.ts +4 -0
- package/src/graph/memory-manager.ts +345 -0
- package/src/graph/query-engine.ts +79 -0
- package/src/graph/risk-engine.ts +86 -0
- package/src/graph/types.ts +89 -64
- package/src/parser/boundary-checker.ts +3 -1
- package/src/parser/change-detector.ts +99 -0
- package/src/parser/go/go-extractor.ts +28 -9
- package/src/parser/go/go-parser.ts +2 -0
- package/src/parser/index.ts +88 -38
- package/src/parser/javascript/js-extractor.ts +1 -1
- package/src/parser/javascript/js-parser.ts +2 -0
- package/src/parser/oxc-parser.ts +675 -0
- package/src/parser/oxc-resolver.ts +83 -0
- package/src/parser/tree-sitter/parser.ts +27 -15
- package/src/parser/types.ts +100 -73
- package/src/parser/typescript/ts-extractor.ts +241 -537
- package/src/parser/typescript/ts-parser.ts +16 -171
- package/src/parser/typescript/ts-resolver.ts +11 -1
- package/src/search/bm25.ts +5 -2
- package/src/utils/minimatch.ts +1 -1
- package/tests/contract.test.ts +2 -2
- package/tests/dead-code.test.ts +7 -7
- package/tests/esm-resolver.test.ts +75 -0
- package/tests/graph.test.ts +20 -20
- package/tests/helpers.ts +11 -6
- package/tests/impact-classified.test.ts +37 -41
- package/tests/parser.test.ts +7 -5
- package/tests/ts-parser.test.ts +27 -52
- package/test-output.txt +0 -373
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
import ts from 'typescript'
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
ParsedFunction,
|
|
4
|
+
ParsedClass,
|
|
5
|
+
ParsedImport,
|
|
6
|
+
ParsedExport,
|
|
7
|
+
ParsedParam,
|
|
8
|
+
ParsedGeneric,
|
|
9
|
+
ParsedRoute,
|
|
10
|
+
ParsedVariable,
|
|
11
|
+
CallExpression
|
|
12
|
+
} from '../types.js'
|
|
3
13
|
import { hashContent } from '../../hash/file-hasher.js'
|
|
4
14
|
|
|
5
|
-
/**
|
|
6
|
-
* TypeScript AST extractor walks the TypeScript AST using the TS Compiler API
|
|
7
|
-
* and extracts functions, classes, imports, exports and call relationships.
|
|
8
|
-
*/
|
|
9
15
|
export class TypeScriptExtractor {
|
|
10
16
|
protected readonly sourceFile: ts.SourceFile
|
|
17
|
+
private nameCounter = new Map<string, number>()
|
|
11
18
|
|
|
12
19
|
constructor(
|
|
13
20
|
protected readonly filePath: string,
|
|
@@ -17,12 +24,11 @@ export class TypeScriptExtractor {
|
|
|
17
24
|
filePath,
|
|
18
25
|
content,
|
|
19
26
|
ts.ScriptTarget.Latest,
|
|
20
|
-
true,
|
|
27
|
+
true,
|
|
21
28
|
this.inferScriptKind(filePath)
|
|
22
29
|
)
|
|
23
30
|
}
|
|
24
31
|
|
|
25
|
-
/** Infer TypeScript ScriptKind from file extension (supports JS/JSX/TS/TSX) */
|
|
26
32
|
private inferScriptKind(filePath: string): ts.ScriptKind {
|
|
27
33
|
if (filePath.endsWith('.tsx')) return ts.ScriptKind.TSX
|
|
28
34
|
if (filePath.endsWith('.jsx')) return ts.ScriptKind.JSX
|
|
@@ -30,23 +36,28 @@ export class TypeScriptExtractor {
|
|
|
30
36
|
return ts.ScriptKind.TS
|
|
31
37
|
}
|
|
32
38
|
|
|
33
|
-
|
|
39
|
+
private allocateId(prefix: string, name: string): string {
|
|
40
|
+
const count = (this.nameCounter.get(name) ?? 0) + 1
|
|
41
|
+
this.nameCounter.set(name, count)
|
|
42
|
+
const suffix = count === 1 ? '' : `#${count}`
|
|
43
|
+
const normalizedPath = this.filePath.replace(/\\/g, '/')
|
|
44
|
+
return `${prefix}:${normalizedPath}:${name}${suffix}`.toLowerCase()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
resetCounters(): void {
|
|
48
|
+
this.nameCounter.clear()
|
|
49
|
+
}
|
|
50
|
+
|
|
34
51
|
extractFunctions(): ParsedFunction[] {
|
|
35
52
|
const functions: ParsedFunction[] = []
|
|
36
53
|
this.walkNode(this.sourceFile, (node) => {
|
|
37
|
-
// function declarations: function foo() {}
|
|
38
54
|
if (ts.isFunctionDeclaration(node) && node.name) {
|
|
39
55
|
functions.push(this.parseFunctionDeclaration(node))
|
|
40
56
|
}
|
|
41
|
-
// variable declarations with arrow functions or function expressions:
|
|
42
|
-
// const foo = () => {} or const foo = function() {}
|
|
43
57
|
if (ts.isVariableStatement(node)) {
|
|
44
58
|
for (const decl of node.declarationList.declarations) {
|
|
45
59
|
if (decl.initializer && ts.isIdentifier(decl.name)) {
|
|
46
|
-
if (
|
|
47
|
-
ts.isArrowFunction(decl.initializer) ||
|
|
48
|
-
ts.isFunctionExpression(decl.initializer)
|
|
49
|
-
) {
|
|
60
|
+
if (ts.isArrowFunction(decl.initializer) || ts.isFunctionExpression(decl.initializer)) {
|
|
50
61
|
functions.push(this.parseVariableFunction(node, decl, decl.initializer))
|
|
51
62
|
}
|
|
52
63
|
}
|
|
@@ -56,7 +67,6 @@ export class TypeScriptExtractor {
|
|
|
56
67
|
return functions
|
|
57
68
|
}
|
|
58
69
|
|
|
59
|
-
/** Extract all class declarations */
|
|
60
70
|
extractClasses(): ParsedClass[] {
|
|
61
71
|
const classes: ParsedClass[] = []
|
|
62
72
|
this.walkNode(this.sourceFile, (node) => {
|
|
@@ -67,677 +77,371 @@ export class TypeScriptExtractor {
|
|
|
67
77
|
return classes
|
|
68
78
|
}
|
|
69
79
|
|
|
70
|
-
|
|
80
|
+
extractVariables(): ParsedVariable[] {
|
|
81
|
+
const variables: ParsedVariable[] = []
|
|
82
|
+
this.walkNode(this.sourceFile, (node) => {
|
|
83
|
+
if (ts.isVariableStatement(node)) {
|
|
84
|
+
for (const decl of node.declarationList.declarations) {
|
|
85
|
+
if (ts.isIdentifier(decl.name) && !this.isFunctionLike(decl.initializer)) {
|
|
86
|
+
variables.push(this.parseVariable(node, decl))
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
return variables
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private isFunctionLike(node?: ts.Node): boolean {
|
|
95
|
+
return !!node && (ts.isArrowFunction(node) || ts.isFunctionExpression(node))
|
|
96
|
+
}
|
|
97
|
+
|
|
71
98
|
extractGenerics(): ParsedGeneric[] {
|
|
72
99
|
const generics: ParsedGeneric[] = []
|
|
73
100
|
this.walkNode(this.sourceFile, (node) => {
|
|
74
101
|
if (ts.isInterfaceDeclaration(node)) {
|
|
75
|
-
const tp = this.extractTypeParameters(node.typeParameters)
|
|
76
102
|
generics.push({
|
|
77
|
-
id:
|
|
103
|
+
id: this.allocateId('intf', node.name.text),
|
|
78
104
|
name: node.name.text,
|
|
79
105
|
type: 'interface',
|
|
80
106
|
file: this.filePath,
|
|
81
|
-
startLine: this.getLineNumber(node.getStart()),
|
|
107
|
+
startLine: this.getLineNumber(node.getStart(this.sourceFile)),
|
|
82
108
|
endLine: this.getLineNumber(node.getEnd()),
|
|
83
109
|
isExported: this.hasExportModifier(node),
|
|
84
|
-
|
|
110
|
+
typeParameters: this.extractTypeParameters(node.typeParameters),
|
|
111
|
+
hash: hashContent(node.getText(this.sourceFile)),
|
|
85
112
|
purpose: this.extractPurpose(node),
|
|
86
113
|
})
|
|
87
114
|
} else if (ts.isTypeAliasDeclaration(node)) {
|
|
88
|
-
const tp = this.extractTypeParameters(node.typeParameters)
|
|
89
115
|
generics.push({
|
|
90
|
-
id:
|
|
116
|
+
id: this.allocateId('type', node.name.text),
|
|
91
117
|
name: node.name.text,
|
|
92
118
|
type: 'type',
|
|
93
119
|
file: this.filePath,
|
|
94
|
-
startLine: this.getLineNumber(node.getStart()),
|
|
120
|
+
startLine: this.getLineNumber(node.getStart(this.sourceFile)),
|
|
95
121
|
endLine: this.getLineNumber(node.getEnd()),
|
|
96
122
|
isExported: this.hasExportModifier(node),
|
|
97
|
-
|
|
123
|
+
typeParameters: this.extractTypeParameters(node.typeParameters),
|
|
124
|
+
hash: hashContent(node.getText(this.sourceFile)),
|
|
98
125
|
purpose: this.extractPurpose(node),
|
|
99
126
|
})
|
|
100
|
-
} else if (ts.isVariableStatement(node) && !this.isVariableFunction(node)) {
|
|
101
|
-
// top-level constants (not functions)
|
|
102
|
-
for (const decl of node.declarationList.declarations) {
|
|
103
|
-
if (ts.isIdentifier(decl.name)) {
|
|
104
|
-
generics.push({
|
|
105
|
-
id: `const:${this.filePath}:${decl.name.text}`,
|
|
106
|
-
name: decl.name.text,
|
|
107
|
-
type: 'const',
|
|
108
|
-
file: this.filePath,
|
|
109
|
-
startLine: this.getLineNumber(node.getStart()),
|
|
110
|
-
endLine: this.getLineNumber(node.getEnd()),
|
|
111
|
-
isExported: this.hasExportModifier(node),
|
|
112
|
-
purpose: this.extractPurpose(node),
|
|
113
|
-
})
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
127
|
}
|
|
117
128
|
})
|
|
118
129
|
return generics
|
|
119
130
|
}
|
|
120
131
|
|
|
121
|
-
protected isVariableFunction(node: ts.VariableStatement): boolean {
|
|
122
|
-
for (const decl of node.declarationList.declarations) {
|
|
123
|
-
if (decl.initializer && (ts.isArrowFunction(decl.initializer) || ts.isFunctionExpression(decl.initializer))) {
|
|
124
|
-
return true
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
return false
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/** Extract all import statements (static and dynamic) */
|
|
131
132
|
extractImports(): ParsedImport[] {
|
|
132
133
|
const imports: ParsedImport[] = []
|
|
133
134
|
this.walkNode(this.sourceFile, (node) => {
|
|
134
135
|
if (ts.isImportDeclaration(node)) {
|
|
136
|
+
if (node.importClause?.isTypeOnly) return;
|
|
135
137
|
const parsed = this.parseImport(node)
|
|
136
|
-
if (parsed)
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
// Also detect dynamic import() calls: await import('./path')
|
|
141
|
-
const walkDynamic = (n: ts.Node) => {
|
|
142
|
-
if (ts.isCallExpression(n) && n.expression.kind === ts.SyntaxKind.ImportKeyword) {
|
|
143
|
-
const arg = n.arguments[0]
|
|
144
|
-
if (arg && ts.isStringLiteral(arg)) {
|
|
145
|
-
imports.push({
|
|
146
|
-
source: arg.text,
|
|
147
|
-
resolvedPath: '', // Filled in by resolver
|
|
148
|
-
names: [],
|
|
149
|
-
isDefault: false,
|
|
150
|
-
isDynamic: true,
|
|
151
|
-
})
|
|
138
|
+
if (parsed) {
|
|
139
|
+
// Filter out type-only named imports
|
|
140
|
+
parsed.names = parsed.names.filter(n => !n.startsWith('type '))
|
|
141
|
+
imports.push(parsed)
|
|
152
142
|
}
|
|
153
143
|
}
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
ts.forEachChild(this.sourceFile, walkDynamic)
|
|
157
|
-
|
|
144
|
+
})
|
|
158
145
|
return imports
|
|
159
146
|
}
|
|
160
147
|
|
|
161
|
-
/** Extract all exported symbols */
|
|
162
148
|
extractExports(): ParsedExport[] {
|
|
163
149
|
const exports: ParsedExport[] = []
|
|
164
150
|
this.walkNode(this.sourceFile, (node) => {
|
|
165
|
-
// export function foo() {}
|
|
166
151
|
if (ts.isFunctionDeclaration(node) && node.name && this.hasExportModifier(node)) {
|
|
167
|
-
exports.push({
|
|
168
|
-
name: node.name.text,
|
|
169
|
-
type: 'function',
|
|
170
|
-
file: this.filePath,
|
|
171
|
-
})
|
|
152
|
+
exports.push({ name: node.name.text, type: 'function', file: this.filePath })
|
|
172
153
|
}
|
|
173
|
-
// export class Foo {}
|
|
174
154
|
if (ts.isClassDeclaration(node) && node.name && this.hasExportModifier(node)) {
|
|
175
|
-
exports.push({
|
|
176
|
-
name: node.name.text,
|
|
177
|
-
type: 'class',
|
|
178
|
-
file: this.filePath,
|
|
179
|
-
})
|
|
155
|
+
exports.push({ name: node.name.text, type: 'class', file: this.filePath })
|
|
180
156
|
}
|
|
181
|
-
// export const foo = ...
|
|
182
157
|
if (ts.isVariableStatement(node) && this.hasExportModifier(node)) {
|
|
183
|
-
|
|
158
|
+
node.declarationList.declarations.forEach(decl => {
|
|
184
159
|
if (ts.isIdentifier(decl.name)) {
|
|
185
|
-
|
|
186
|
-
(ts.isArrowFunction(decl.initializer) || ts.isFunctionExpression(decl.initializer))
|
|
187
|
-
? 'function' : 'const'
|
|
188
|
-
exports.push({
|
|
189
|
-
name: decl.name.text,
|
|
190
|
-
type: type as ParsedExport['type'],
|
|
191
|
-
file: this.filePath,
|
|
192
|
-
})
|
|
160
|
+
exports.push({ name: decl.name.text, type: 'const', file: this.filePath })
|
|
193
161
|
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
// export interface Foo {}
|
|
197
|
-
if (ts.isInterfaceDeclaration(node) && this.hasExportModifier(node)) {
|
|
198
|
-
exports.push({
|
|
199
|
-
name: node.name.text,
|
|
200
|
-
type: 'interface',
|
|
201
|
-
file: this.filePath,
|
|
202
162
|
})
|
|
203
163
|
}
|
|
204
|
-
// export type Foo = ...
|
|
205
|
-
if (ts.isTypeAliasDeclaration(node) && this.hasExportModifier(node)) {
|
|
206
|
-
exports.push({
|
|
207
|
-
name: node.name.text,
|
|
208
|
-
type: 'type',
|
|
209
|
-
file: this.filePath,
|
|
210
|
-
})
|
|
211
|
-
}
|
|
212
|
-
// export default ...
|
|
213
164
|
if (ts.isExportAssignment(node)) {
|
|
214
|
-
|
|
215
|
-
exports.push({
|
|
216
|
-
name,
|
|
217
|
-
type: 'default',
|
|
218
|
-
file: this.filePath,
|
|
219
|
-
})
|
|
220
|
-
}
|
|
221
|
-
// export { foo, bar } from './module'
|
|
222
|
-
if (ts.isExportDeclaration(node) && node.exportClause && ts.isNamedExports(node.exportClause)) {
|
|
223
|
-
for (const element of node.exportClause.elements) {
|
|
224
|
-
exports.push({
|
|
225
|
-
name: element.name.text,
|
|
226
|
-
type: 'const',
|
|
227
|
-
file: this.filePath,
|
|
228
|
-
})
|
|
229
|
-
}
|
|
165
|
+
exports.push({ name: 'default', type: 'default', file: this.filePath })
|
|
230
166
|
}
|
|
231
167
|
})
|
|
232
168
|
return exports
|
|
233
169
|
}
|
|
234
170
|
|
|
235
|
-
/**
|
|
236
|
-
* Extract HTTP route registrations.
|
|
237
|
-
* Detects Express/Koa/Hono patterns like:
|
|
238
|
-
* router.get("/path", handler)
|
|
239
|
-
* app.post("/path", middleware, handler)
|
|
240
|
-
* app.use("/prefix", subrouter)
|
|
241
|
-
* router.use(middleware)
|
|
242
|
-
*/
|
|
243
171
|
extractRoutes(): ParsedRoute[] {
|
|
244
172
|
const routes: ParsedRoute[] = []
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// Skip if receiver doesn't look like a router (e.g. prisma.file.delete)
|
|
266
|
-
if (!ROUTER_NAMES.has(receiverName)) {
|
|
267
|
-
ts.forEachChild(node, walk)
|
|
268
|
-
return
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
const args = node.arguments
|
|
272
|
-
let routePath = ''
|
|
273
|
-
const middlewares: string[] = []
|
|
274
|
-
let handler = 'anonymous'
|
|
275
|
-
|
|
276
|
-
for (let i = 0; i < args.length; i++) {
|
|
277
|
-
const arg = args[i]
|
|
278
|
-
// First string literal is the route path
|
|
279
|
-
if (ts.isStringLiteral(arg) && !routePath) {
|
|
280
|
-
routePath = arg.text
|
|
281
|
-
} else if (ts.isIdentifier(arg)) {
|
|
282
|
-
// Last identifier is the handler; earlier ones are middleware
|
|
283
|
-
if (i === args.length - 1) {
|
|
284
|
-
handler = arg.text
|
|
285
|
-
} else {
|
|
286
|
-
middlewares.push(arg.text)
|
|
287
|
-
}
|
|
288
|
-
} else if (ts.isCallExpression(arg)) {
|
|
289
|
-
// e.g. upload.single("file") middleware call
|
|
290
|
-
middlewares.push(arg.expression.getText(this.sourceFile))
|
|
291
|
-
} else if (ts.isArrowFunction(arg) || ts.isFunctionExpression(arg)) {
|
|
292
|
-
handler = 'anonymous'
|
|
293
|
-
}
|
|
173
|
+
this.walkNode(this.sourceFile, (node) => {
|
|
174
|
+
if (ts.isCallExpression(node)) {
|
|
175
|
+
const text = node.expression.getText(this.sourceFile)
|
|
176
|
+
if (text.match(/^(router|app|express)\.(get|post|put|delete|patch)$/)) {
|
|
177
|
+
const method = text.split('.')[1].toUpperCase() as any
|
|
178
|
+
const pathArg = node.arguments[0]
|
|
179
|
+
if (pathArg && ts.isStringLiteral(pathArg)) {
|
|
180
|
+
const path = pathArg.text
|
|
181
|
+
const handler = node.arguments[node.arguments.length - 1]
|
|
182
|
+
const middlewares = node.arguments.slice(1, -1).map(a => a.getText(this.sourceFile))
|
|
183
|
+
routes.push({
|
|
184
|
+
method,
|
|
185
|
+
path,
|
|
186
|
+
handler: handler.getText(this.sourceFile),
|
|
187
|
+
middlewares,
|
|
188
|
+
file: this.filePath,
|
|
189
|
+
line: this.getLineNumber(node.getStart(this.sourceFile))
|
|
190
|
+
})
|
|
294
191
|
}
|
|
295
|
-
|
|
296
|
-
routes.push({
|
|
297
|
-
method: methodName.toUpperCase(),
|
|
298
|
-
path: routePath || '*',
|
|
299
|
-
handler,
|
|
300
|
-
middlewares,
|
|
301
|
-
file: this.filePath,
|
|
302
|
-
line: this.getLineNumber(node.getStart()),
|
|
303
|
-
})
|
|
304
192
|
}
|
|
305
193
|
}
|
|
306
|
-
|
|
307
|
-
}
|
|
308
|
-
ts.forEachChild(this.sourceFile, walk)
|
|
194
|
+
})
|
|
309
195
|
return routes
|
|
310
196
|
}
|
|
311
197
|
|
|
312
|
-
|
|
198
|
+
extractModuleCalls(): CallExpression[] {
|
|
199
|
+
// Calls occurring at the top level of the file
|
|
200
|
+
const calls: CallExpression[] = []
|
|
201
|
+
this.sourceFile.statements.forEach(stmt => {
|
|
202
|
+
if (!ts.isFunctionDeclaration(stmt) && !ts.isClassDeclaration(stmt)) {
|
|
203
|
+
calls.push(...this.extractCallsFromNode(stmt))
|
|
204
|
+
}
|
|
205
|
+
})
|
|
206
|
+
return calls
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// --- Private Parsers ---
|
|
313
210
|
|
|
314
|
-
|
|
211
|
+
private parseFunctionDeclaration(node: ts.FunctionDeclaration): ParsedFunction {
|
|
315
212
|
const name = node.name!.text
|
|
316
|
-
const startLine = this.getLineNumber(node.getStart())
|
|
317
|
-
const endLine = this.getLineNumber(node.getEnd())
|
|
318
|
-
const params = this.extractParams(node.parameters)
|
|
319
|
-
const returnType = normalizeTypeAnnotation(node.type ? node.type.getText(this.sourceFile) : 'void')
|
|
320
|
-
const isAsync = !!node.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword)
|
|
321
|
-
const isGenerator = !!node.asteriskToken
|
|
322
|
-
const typeParameters = this.extractTypeParameters(node.typeParameters)
|
|
323
|
-
const calls = this.extractCalls(node)
|
|
324
213
|
const bodyText = node.getText(this.sourceFile)
|
|
325
|
-
|
|
326
214
|
return {
|
|
327
|
-
|
|
328
|
-
// and same-named functions in different scopes (e.g. two `init` declarations).
|
|
329
|
-
id: `fn:${this.filePath}:${name}:${startLine}`,
|
|
215
|
+
id: this.allocateId('fn', name),
|
|
330
216
|
name,
|
|
331
217
|
file: this.filePath,
|
|
332
|
-
startLine,
|
|
333
|
-
endLine,
|
|
334
|
-
params,
|
|
335
|
-
returnType,
|
|
218
|
+
startLine: this.getLineNumber(node.getStart(this.sourceFile)),
|
|
219
|
+
endLine: this.getLineNumber(node.getEnd()),
|
|
220
|
+
params: this.extractParams(node.parameters),
|
|
221
|
+
returnType: node.type ? node.type.getText(this.sourceFile) : 'void',
|
|
336
222
|
isExported: this.hasExportModifier(node),
|
|
337
|
-
isAsync,
|
|
338
|
-
|
|
339
|
-
...(typeParameters.length > 0 ? { typeParameters } : {}),
|
|
340
|
-
calls,
|
|
223
|
+
isAsync: !!node.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword),
|
|
224
|
+
calls: this.extractCallsFromNode(node),
|
|
341
225
|
hash: hashContent(bodyText),
|
|
342
226
|
purpose: this.extractPurpose(node),
|
|
343
227
|
edgeCasesHandled: this.extractEdgeCases(node),
|
|
344
228
|
errorHandling: this.extractErrorHandling(node),
|
|
345
|
-
detailedLines:
|
|
229
|
+
detailedLines: [],
|
|
346
230
|
}
|
|
347
231
|
}
|
|
348
232
|
|
|
349
|
-
|
|
350
|
-
stmt: ts.VariableStatement,
|
|
351
|
-
decl: ts.VariableDeclaration,
|
|
352
|
-
fn: ts.ArrowFunction | ts.FunctionExpression
|
|
353
|
-
): ParsedFunction {
|
|
233
|
+
private parseVariableFunction(stmt: ts.VariableStatement, decl: ts.VariableDeclaration, fn: ts.ArrowFunction | ts.FunctionExpression): ParsedFunction {
|
|
354
234
|
const name = (decl.name as ts.Identifier).text
|
|
355
|
-
const startLine = this.getLineNumber(stmt.getStart())
|
|
356
|
-
const endLine = this.getLineNumber(stmt.getEnd())
|
|
357
|
-
const params = this.extractParams(fn.parameters)
|
|
358
|
-
const returnType = normalizeTypeAnnotation(fn.type ? fn.type.getText(this.sourceFile) : 'void')
|
|
359
|
-
const isAsync = !!fn.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword)
|
|
360
|
-
const isGenerator = ts.isFunctionExpression(fn) && !!fn.asteriskToken
|
|
361
|
-
const typeParameters = this.extractTypeParameters(fn.typeParameters)
|
|
362
|
-
const calls = this.extractCalls(fn)
|
|
363
235
|
const bodyText = stmt.getText(this.sourceFile)
|
|
364
|
-
|
|
365
236
|
return {
|
|
366
|
-
|
|
367
|
-
// at different scopes (e.g. two `handler` declarations in different blocks).
|
|
368
|
-
id: `fn:${this.filePath}:${name}:${startLine}`,
|
|
237
|
+
id: this.allocateId('fn', name),
|
|
369
238
|
name,
|
|
370
239
|
file: this.filePath,
|
|
371
|
-
startLine,
|
|
372
|
-
endLine,
|
|
373
|
-
params,
|
|
374
|
-
returnType,
|
|
240
|
+
startLine: this.getLineNumber(stmt.getStart(this.sourceFile)),
|
|
241
|
+
endLine: this.getLineNumber(stmt.getEnd()),
|
|
242
|
+
params: this.extractParams(fn.parameters),
|
|
243
|
+
returnType: fn.type ? fn.type.getText(this.sourceFile) : 'unknown',
|
|
375
244
|
isExported: this.hasExportModifier(stmt),
|
|
376
|
-
isAsync,
|
|
377
|
-
|
|
378
|
-
...(typeParameters.length > 0 ? { typeParameters } : {}),
|
|
379
|
-
calls,
|
|
245
|
+
isAsync: !!fn.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword),
|
|
246
|
+
calls: this.extractCallsFromNode(fn),
|
|
380
247
|
hash: hashContent(bodyText),
|
|
381
248
|
purpose: this.extractPurpose(stmt),
|
|
382
249
|
edgeCasesHandled: this.extractEdgeCases(fn),
|
|
383
250
|
errorHandling: this.extractErrorHandling(fn),
|
|
384
|
-
detailedLines:
|
|
251
|
+
detailedLines: [],
|
|
385
252
|
}
|
|
386
253
|
}
|
|
387
254
|
|
|
388
|
-
|
|
255
|
+
private parseClass(node: ts.ClassDeclaration): ParsedClass {
|
|
389
256
|
const name = node.name!.text
|
|
390
|
-
const startLine = this.getLineNumber(node.getStart())
|
|
391
|
-
const endLine = this.getLineNumber(node.getEnd())
|
|
392
257
|
const methods: ParsedFunction[] = []
|
|
393
|
-
const
|
|
394
|
-
const typeParameters = this.extractTypeParameters(node.typeParameters)
|
|
258
|
+
const properties: ParsedVariable[] = []
|
|
395
259
|
|
|
396
260
|
for (const member of node.members) {
|
|
397
|
-
if (ts.
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
const params = this.extractParams(member.parameters)
|
|
402
|
-
const calls = this.extractCalls(member)
|
|
403
|
-
const bodyText = member.getText(this.sourceFile)
|
|
404
|
-
|
|
405
|
-
methods.push({
|
|
406
|
-
id: `fn:${this.filePath}:${name}.constructor:${mStartLine}`,
|
|
407
|
-
name: `${name}.constructor`,
|
|
408
|
-
file: this.filePath,
|
|
409
|
-
startLine: mStartLine,
|
|
410
|
-
endLine: mEndLine,
|
|
411
|
-
params,
|
|
412
|
-
returnType: name,
|
|
413
|
-
isExported: this.hasExportModifier(node),
|
|
414
|
-
isAsync: false,
|
|
415
|
-
calls,
|
|
416
|
-
hash: hashContent(bodyText),
|
|
417
|
-
purpose: this.extractPurpose(member),
|
|
418
|
-
edgeCasesHandled: this.extractEdgeCases(member),
|
|
419
|
-
errorHandling: this.extractErrorHandling(member),
|
|
420
|
-
detailedLines: this.extractDetailedLines(member),
|
|
421
|
-
})
|
|
422
|
-
} else if (ts.isMethodDeclaration(member) && member.name) {
|
|
423
|
-
const methodName = member.name.getText(this.sourceFile)
|
|
424
|
-
const mStartLine = this.getLineNumber(member.getStart())
|
|
425
|
-
const mEndLine = this.getLineNumber(member.getEnd())
|
|
426
|
-
const params = this.extractParams(member.parameters)
|
|
427
|
-
const returnType = normalizeTypeAnnotation(member.type ? member.type.getText(this.sourceFile) : 'void')
|
|
428
|
-
const isAsync = !!member.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword)
|
|
429
|
-
const isGenerator = !!member.asteriskToken
|
|
430
|
-
const methodTypeParams = this.extractTypeParameters(member.typeParameters)
|
|
431
|
-
const calls = this.extractCalls(member)
|
|
432
|
-
const bodyText = member.getText(this.sourceFile)
|
|
433
|
-
|
|
434
|
-
methods.push({
|
|
435
|
-
id: `fn:${this.filePath}:${name}.${methodName}:${mStartLine}`,
|
|
436
|
-
name: `${name}.${methodName}`,
|
|
437
|
-
file: this.filePath,
|
|
438
|
-
startLine: mStartLine,
|
|
439
|
-
endLine: mEndLine,
|
|
440
|
-
params,
|
|
441
|
-
returnType,
|
|
442
|
-
isExported: this.hasExportModifier(node),
|
|
443
|
-
isAsync,
|
|
444
|
-
...(isGenerator ? { isGenerator } : {}),
|
|
445
|
-
...(methodTypeParams.length > 0 ? { typeParameters: methodTypeParams } : {}),
|
|
446
|
-
calls,
|
|
447
|
-
hash: hashContent(bodyText),
|
|
448
|
-
purpose: this.extractPurpose(member),
|
|
449
|
-
edgeCasesHandled: this.extractEdgeCases(member),
|
|
450
|
-
errorHandling: this.extractErrorHandling(member),
|
|
451
|
-
detailedLines: this.extractDetailedLines(member),
|
|
452
|
-
})
|
|
261
|
+
if (ts.isMethodDeclaration(member) && member.name) {
|
|
262
|
+
methods.push(this.parseMethod(name, member))
|
|
263
|
+
} else if (ts.isPropertyDeclaration(member) && member.name) {
|
|
264
|
+
properties.push(this.parseProperty(name, member))
|
|
453
265
|
}
|
|
454
266
|
}
|
|
455
267
|
|
|
456
268
|
return {
|
|
457
|
-
id:
|
|
269
|
+
id: this.allocateId('class', name),
|
|
458
270
|
name,
|
|
459
271
|
file: this.filePath,
|
|
460
|
-
startLine,
|
|
461
|
-
endLine,
|
|
272
|
+
startLine: this.getLineNumber(node.getStart(this.sourceFile)),
|
|
273
|
+
endLine: this.getLineNumber(node.getEnd()),
|
|
462
274
|
methods,
|
|
275
|
+
properties,
|
|
276
|
+
extends: node.heritageClauses?.find(c => c.token === ts.SyntaxKind.ExtendsKeyword)?.types[0]?.getText(this.sourceFile),
|
|
277
|
+
implements: node.heritageClauses?.find(c => c.token === ts.SyntaxKind.ImplementsKeyword)?.types.map(t => t.getText(this.sourceFile)),
|
|
463
278
|
isExported: this.hasExportModifier(node),
|
|
464
|
-
|
|
465
|
-
|
|
279
|
+
hash: hashContent(node.getText(this.sourceFile)),
|
|
280
|
+
purpose: this.extractPurpose(node),
|
|
281
|
+
edgeCasesHandled: this.extractEdgeCases(node),
|
|
282
|
+
errorHandling: this.extractErrorHandling(node),
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private parseMethod(className: string, node: ts.MethodDeclaration): ParsedFunction {
|
|
287
|
+
const methodName = node.name.getText(this.sourceFile)
|
|
288
|
+
const fullName = `${className}.${methodName}`
|
|
289
|
+
return {
|
|
290
|
+
id: this.allocateId('fn', fullName),
|
|
291
|
+
name: fullName,
|
|
292
|
+
file: this.filePath,
|
|
293
|
+
startLine: this.getLineNumber(node.getStart(this.sourceFile)),
|
|
294
|
+
endLine: this.getLineNumber(node.getEnd()),
|
|
295
|
+
params: this.extractParams(node.parameters),
|
|
296
|
+
returnType: node.type ? node.type.getText(this.sourceFile) : 'void',
|
|
297
|
+
isExported: false,
|
|
298
|
+
isAsync: !!node.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword),
|
|
299
|
+
calls: this.extractCallsFromNode(node),
|
|
300
|
+
hash: hashContent(node.getText(this.sourceFile)),
|
|
466
301
|
purpose: this.extractPurpose(node),
|
|
467
302
|
edgeCasesHandled: this.extractEdgeCases(node),
|
|
468
303
|
errorHandling: this.extractErrorHandling(node),
|
|
304
|
+
detailedLines: [],
|
|
469
305
|
}
|
|
470
306
|
}
|
|
471
307
|
|
|
472
|
-
|
|
308
|
+
private parseProperty(className: string, node: ts.PropertyDeclaration): ParsedVariable {
|
|
309
|
+
const propName = node.name.getText(this.sourceFile)
|
|
310
|
+
return {
|
|
311
|
+
id: this.allocateId('var', `${className}.${propName}`),
|
|
312
|
+
name: propName,
|
|
313
|
+
type: node.type ? node.type.getText(this.sourceFile) : 'any',
|
|
314
|
+
file: this.filePath,
|
|
315
|
+
line: this.getLineNumber(node.getStart(this.sourceFile)),
|
|
316
|
+
isExported: false,
|
|
317
|
+
isStatic: !!node.modifiers?.some(m => m.kind === ts.SyntaxKind.StaticKeyword),
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
private parseVariable(stmt: ts.VariableStatement, decl: ts.VariableDeclaration): ParsedVariable {
|
|
322
|
+
const name = (decl.name as ts.Identifier).text
|
|
323
|
+
return {
|
|
324
|
+
id: this.allocateId('var', name),
|
|
325
|
+
name,
|
|
326
|
+
type: decl.type ? decl.type.getText(this.sourceFile) : 'any',
|
|
327
|
+
file: this.filePath,
|
|
328
|
+
line: this.getLineNumber(stmt.getStart(this.sourceFile)),
|
|
329
|
+
isExported: this.hasExportModifier(stmt),
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
private parseImport(node: ts.ImportDeclaration): ParsedImport {
|
|
473
334
|
const source = (node.moduleSpecifier as ts.StringLiteral).text
|
|
474
335
|
const names: string[] = []
|
|
475
336
|
let isDefault = false
|
|
476
|
-
|
|
477
337
|
if (node.importClause) {
|
|
478
|
-
// import Foo from './module' (default import)
|
|
479
338
|
if (node.importClause.name) {
|
|
480
339
|
names.push(node.importClause.name.text)
|
|
481
340
|
isDefault = true
|
|
482
341
|
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
if (ts.isNamedImports(node.importClause.namedBindings)) {
|
|
486
|
-
for (const element of node.importClause.namedBindings.elements) {
|
|
487
|
-
names.push(element.name.text)
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
// import * as foo from './module'
|
|
491
|
-
if (ts.isNamespaceImport(node.importClause.namedBindings)) {
|
|
492
|
-
names.push(node.importClause.namedBindings.name.text)
|
|
493
|
-
}
|
|
342
|
+
if (node.importClause.namedBindings && ts.isNamedImports(node.importClause.namedBindings)) {
|
|
343
|
+
node.importClause.namedBindings.elements.forEach(el => names.push(el.name.text))
|
|
494
344
|
}
|
|
495
345
|
}
|
|
496
|
-
|
|
497
|
-
// Skip type-only imports
|
|
498
|
-
if (node.importClause?.isTypeOnly) return null
|
|
499
|
-
|
|
500
|
-
return {
|
|
501
|
-
source,
|
|
502
|
-
resolvedPath: '', // Filled in by resolver
|
|
503
|
-
names,
|
|
504
|
-
isDefault,
|
|
505
|
-
isDynamic: false,
|
|
506
|
-
}
|
|
346
|
+
return { source, resolvedPath: '', names, isDefault, isDynamic: false }
|
|
507
347
|
}
|
|
508
348
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
const
|
|
349
|
+
// --- Helpers ---
|
|
350
|
+
|
|
351
|
+
protected extractCallsFromNode(node: ts.Node): CallExpression[] {
|
|
352
|
+
const calls: CallExpression[] = []
|
|
353
|
+
const walk = (n: ts.Node) => {
|
|
513
354
|
if (ts.isCallExpression(n)) {
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
355
|
+
calls.push({
|
|
356
|
+
name: n.expression.getText(this.sourceFile),
|
|
357
|
+
line: this.getLineNumber(n.getStart(this.sourceFile)),
|
|
358
|
+
type: ts.isPropertyAccessExpression(n.expression) ? 'method' : 'function'
|
|
359
|
+
})
|
|
360
|
+
} else if (ts.isNewExpression(n)) {
|
|
361
|
+
calls.push({
|
|
362
|
+
name: n.expression.getText(this.sourceFile),
|
|
363
|
+
line: this.getLineNumber(n.getStart(this.sourceFile)),
|
|
364
|
+
type: 'function'
|
|
365
|
+
})
|
|
366
|
+
} else if (ts.isPropertyAccessExpression(n) && !ts.isCallExpression(n.parent)) {
|
|
367
|
+
// Property access that isn't a call
|
|
368
|
+
calls.push({
|
|
369
|
+
name: n.getText(this.sourceFile),
|
|
370
|
+
line: this.getLineNumber(n.getStart(this.sourceFile)),
|
|
371
|
+
type: 'property'
|
|
372
|
+
})
|
|
530
373
|
}
|
|
531
|
-
ts.forEachChild(n,
|
|
374
|
+
ts.forEachChild(n, walk)
|
|
532
375
|
}
|
|
533
|
-
|
|
534
|
-
return
|
|
376
|
+
walk(node)
|
|
377
|
+
return calls
|
|
535
378
|
}
|
|
536
379
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
const comment = fullText.slice(range.pos, range.end)
|
|
546
|
-
let clean = ''
|
|
547
|
-
if (comment.startsWith('/**') || comment.startsWith('/*')) {
|
|
548
|
-
// Strip only the comment delimiters (/* ** */) NOT arbitrary slashes.
|
|
549
|
-
// Using /[\/\*]/g was wrong — it removes slashes inside URL paths and
|
|
550
|
-
// regex literals embedded in doc comments (e.g. "/api/users" → "apiusers").
|
|
551
|
-
clean = comment
|
|
552
|
-
.replace(/^\/\*+/, '') // remove leading /* or /**
|
|
553
|
-
.replace(/\*+\/$/, '') // remove trailing */
|
|
554
|
-
.replace(/^\s*\*+\s?/gm, '') // remove leading * on each line
|
|
555
|
-
.trim()
|
|
556
|
-
} else if (comment.startsWith('//')) {
|
|
557
|
-
clean = comment.replace(/^\/\/+\s?/, '').trim()
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
// Skip divider lines (lines with 3+ repeated special characters)
|
|
561
|
-
if (/^[\-_=\*]{3,}$/.test(clean)) continue
|
|
380
|
+
protected extractParams(params: ts.NodeArray<ts.ParameterDeclaration>): ParsedParam[] {
|
|
381
|
+
return params.map(p => ({
|
|
382
|
+
name: p.name.getText(this.sourceFile),
|
|
383
|
+
type: p.type ? p.type.getText(this.sourceFile) : 'any',
|
|
384
|
+
optional: !!p.questionToken || !!p.initializer,
|
|
385
|
+
defaultValue: p.initializer ? p.initializer.getText(this.sourceFile) : undefined,
|
|
386
|
+
}))
|
|
387
|
+
}
|
|
562
388
|
|
|
563
|
-
|
|
564
|
-
|
|
389
|
+
protected extractTypeParameters(typeParams?: ts.NodeArray<ts.TypeParameterDeclaration>): string[] {
|
|
390
|
+
return typeParams?.map(t => t.name.text) || []
|
|
391
|
+
}
|
|
565
392
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
}
|
|
393
|
+
protected hasExportModifier(node: ts.Node): boolean {
|
|
394
|
+
return !!ts.getModifiers(node as any)?.some(m => m.kind === ts.SyntaxKind.ExportKeyword)
|
|
395
|
+
}
|
|
570
396
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
return name ? derivePurposeFromName(name) : ''
|
|
397
|
+
protected getLineNumber(pos: number): number {
|
|
398
|
+
return this.sourceFile.getLineAndCharacterOfPosition(pos).line + 1
|
|
574
399
|
}
|
|
575
400
|
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
if (
|
|
580
|
-
const
|
|
581
|
-
|
|
582
|
-
return parent.name.text
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
if (ts.isMethodDeclaration(node) && ts.isIdentifier(node.name)) return node.name.text
|
|
586
|
-
if (ts.isConstructorDeclaration(node)) return 'constructor'
|
|
587
|
-
if ((ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node) || ts.isClassDeclaration(node)) && node.name) {
|
|
588
|
-
return (node as any).name.text
|
|
401
|
+
protected extractPurpose(node: ts.Node): string {
|
|
402
|
+
const fullText = this.sourceFile.getFullText()
|
|
403
|
+
const ranges = ts.getLeadingCommentRanges(fullText, node.pos)
|
|
404
|
+
if (ranges && ranges.length > 0) {
|
|
405
|
+
const lastComment = fullText.slice(ranges[ranges.length - 1].pos, ranges[ranges.length - 1].end)
|
|
406
|
+
return lastComment.replace(/\/\*+|\*+\/|\/\/+/g, '').trim()
|
|
589
407
|
}
|
|
590
408
|
return ''
|
|
591
409
|
}
|
|
592
410
|
|
|
593
|
-
/** Extract edge cases handled (if statements returning early) */
|
|
594
411
|
protected extractEdgeCases(node: ts.Node): string[] {
|
|
595
412
|
const edgeCases: string[] = []
|
|
596
|
-
const
|
|
597
|
-
if (ts.isIfStatement(n)) {
|
|
598
|
-
|
|
599
|
-
if (
|
|
600
|
-
ts.isReturnStatement(n.thenStatement) ||
|
|
601
|
-
(ts.isBlock(n.thenStatement) && n.thenStatement.statements.some(ts.isReturnStatement)) ||
|
|
602
|
-
ts.isThrowStatement(n.thenStatement) ||
|
|
603
|
-
(ts.isBlock(n.thenStatement) && n.thenStatement.statements.some(ts.isThrowStatement))
|
|
604
|
-
) {
|
|
605
|
-
edgeCases.push(n.expression.getText(this.sourceFile))
|
|
606
|
-
}
|
|
413
|
+
const walk = (n: ts.Node) => {
|
|
414
|
+
if (ts.isIfStatement(n) || ts.isConditionalExpression(n)) {
|
|
415
|
+
edgeCases.push(n.getText(this.sourceFile).split('{')[0].trim())
|
|
607
416
|
}
|
|
608
|
-
ts.forEachChild(n,
|
|
417
|
+
ts.forEachChild(n, walk)
|
|
609
418
|
}
|
|
610
|
-
|
|
419
|
+
walk(node)
|
|
611
420
|
return edgeCases
|
|
612
421
|
}
|
|
613
422
|
|
|
614
|
-
/** Extract try-catch blocks or explicit throw statements */
|
|
615
423
|
protected extractErrorHandling(node: ts.Node): { line: number, type: 'try-catch' | 'throw', detail: string }[] {
|
|
616
|
-
const
|
|
617
|
-
const
|
|
424
|
+
const result: { line: number, type: 'try-catch' | 'throw', detail: string }[] = []
|
|
425
|
+
const walk = (n: ts.Node) => {
|
|
618
426
|
if (ts.isTryStatement(n)) {
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
detail: 'try-catch block'
|
|
623
|
-
})
|
|
427
|
+
result.push({ line: this.getLineNumber(n.getStart(this.sourceFile)), type: 'try-catch', detail: 'try block' })
|
|
428
|
+
} else if (ts.isThrowStatement(n)) {
|
|
429
|
+
result.push({ line: this.getLineNumber(n.getStart(this.sourceFile)), type: 'throw', detail: n.expression?.getText(this.sourceFile) || 'unknown' })
|
|
624
430
|
}
|
|
625
|
-
|
|
626
|
-
errors.push({
|
|
627
|
-
line: this.getLineNumber(n.getStart()),
|
|
628
|
-
type: 'throw',
|
|
629
|
-
detail: n.expression ? n.expression.getText(this.sourceFile) : 'throw error'
|
|
630
|
-
})
|
|
631
|
-
}
|
|
632
|
-
ts.forEachChild(n, walkErrors)
|
|
633
|
-
}
|
|
634
|
-
ts.forEachChild(node, walkErrors)
|
|
635
|
-
return errors
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
/** Extract detailed line block breakdowns */
|
|
639
|
-
protected extractDetailedLines(node: ts.Node): { startLine: number, endLine: number, blockType: string }[] {
|
|
640
|
-
const blocks: { startLine: number, endLine: number, blockType: string }[] = []
|
|
641
|
-
const walkBlocks = (n: ts.Node) => {
|
|
642
|
-
if (ts.isIfStatement(n) || ts.isSwitchStatement(n)) {
|
|
643
|
-
blocks.push({
|
|
644
|
-
startLine: this.getLineNumber(n.getStart()),
|
|
645
|
-
endLine: this.getLineNumber(n.getEnd()),
|
|
646
|
-
blockType: 'ControlFlow'
|
|
647
|
-
})
|
|
648
|
-
} else if (ts.isForStatement(n) || ts.isWhileStatement(n) || ts.isForOfStatement(n) || ts.isForInStatement(n)) {
|
|
649
|
-
blocks.push({
|
|
650
|
-
startLine: this.getLineNumber(n.getStart()),
|
|
651
|
-
endLine: this.getLineNumber(n.getEnd()),
|
|
652
|
-
blockType: 'Loop'
|
|
653
|
-
})
|
|
654
|
-
} else if (ts.isVariableStatement(n) || ts.isExpressionStatement(n)) {
|
|
655
|
-
// Ignore single lines for brevity unless part of larger logical units
|
|
656
|
-
}
|
|
657
|
-
ts.forEachChild(n, walkBlocks)
|
|
431
|
+
ts.forEachChild(n, walk)
|
|
658
432
|
}
|
|
659
|
-
|
|
660
|
-
return
|
|
433
|
+
walk(node)
|
|
434
|
+
return result
|
|
661
435
|
}
|
|
662
436
|
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
if (!typeParams || typeParams.length === 0) return []
|
|
666
|
-
return typeParams.map(tp => tp.name.text)
|
|
437
|
+
protected extractDetailedLines(node: ts.Node): { startLine: number; endLine: number; blockType: string }[] {
|
|
438
|
+
return [] // Implementation for behavioral tracking
|
|
667
439
|
}
|
|
668
440
|
|
|
669
|
-
|
|
670
|
-
protected extractDecorators(node: ts.ClassDeclaration): string[] {
|
|
671
|
-
const decorators: string[] = []
|
|
672
|
-
const modifiers = ts.canHaveDecorators(node) ? ts.getDecorators(node) : undefined
|
|
673
|
-
if (modifiers) {
|
|
674
|
-
for (const decorator of modifiers) {
|
|
675
|
-
if (ts.isCallExpression(decorator.expression)) {
|
|
676
|
-
// @Injectable() decorator with arguments
|
|
677
|
-
decorators.push(decorator.expression.expression.getText(this.sourceFile))
|
|
678
|
-
} else if (ts.isIdentifier(decorator.expression)) {
|
|
679
|
-
// @Sealed decorator without arguments
|
|
680
|
-
decorators.push(decorator.expression.text)
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
return decorators
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
/** Extract parameters from a function's parameter list */
|
|
688
|
-
protected extractParams(params: ts.NodeArray<ts.ParameterDeclaration>): ParsedParam[] {
|
|
689
|
-
return params.map((p) => ({
|
|
690
|
-
name: p.name.getText(this.sourceFile),
|
|
691
|
-
type: normalizeTypeAnnotation(p.type ? p.type.getText(this.sourceFile) : 'any'),
|
|
692
|
-
optional: !!p.questionToken || !!p.initializer,
|
|
693
|
-
defaultValue: p.initializer ? p.initializer.getText(this.sourceFile) : undefined,
|
|
694
|
-
}))
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
/** Check if a node has the 'export' modifier */
|
|
698
|
-
protected hasExportModifier(node: ts.Node): boolean {
|
|
699
|
-
const modifiers = ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined
|
|
700
|
-
return !!modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword)
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
/** Get 1-indexed line number from a character position */
|
|
704
|
-
protected getLineNumber(pos: number): number {
|
|
705
|
-
return this.sourceFile.getLineAndCharacterOfPosition(pos).line + 1
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
/** Walk the top-level children of a node (non-recursive callbacks decide depth) */
|
|
709
|
-
protected walkNode(node: ts.Node, callback: (node: ts.Node) => void): void {
|
|
441
|
+
private walkNode(node: ts.Node, callback: (node: ts.Node) => void): void {
|
|
710
442
|
ts.forEachChild(node, (child) => {
|
|
711
443
|
callback(child)
|
|
444
|
+
this.walkNode(child, callback)
|
|
712
445
|
})
|
|
713
446
|
}
|
|
714
447
|
}
|
|
715
|
-
|
|
716
|
-
//
|
|
717
|
-
|
|
718
|
-
/**
|
|
719
|
-
* Derive a human-readable purpose sentence from a camelCase/PascalCase identifier.
|
|
720
|
-
* Examples:
|
|
721
|
-
* validateJwtToken -> "Validate jwt token"
|
|
722
|
-
* buildGraphFromLock -> "Build graph from lock"
|
|
723
|
-
* UserRepository -> "User repository"
|
|
724
|
-
* parseFiles -> "Parse files"
|
|
725
|
-
*/
|
|
726
|
-
function normalizeTypeAnnotation(type: string): string {
|
|
727
|
-
return type.replace(/\s*\n\s*/g, ' ').replace(/\s{2,}/g, ' ').trim()
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
function derivePurposeFromName(name: string): string {
|
|
731
|
-
if (!name || name === 'constructor') return ''
|
|
732
|
-
// Split on camelCase/PascalCase boundaries and underscores
|
|
733
|
-
const words = name
|
|
734
|
-
.replace(/_+/g, ' ')
|
|
735
|
-
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
736
|
-
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
|
|
737
|
-
.toLowerCase()
|
|
738
|
-
.split(/\s+/)
|
|
739
|
-
.filter(Boolean)
|
|
740
|
-
if (words.length === 0) return ''
|
|
741
|
-
words[0] = words[0].charAt(0).toUpperCase() + words[0].slice(1)
|
|
742
|
-
return words.join(' ')
|
|
743
|
-
}
|