@forgehive/forge-cli 0.3.6 → 0.3.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.
- package/dist/runner.js +8 -1
- package/dist/tasks/bundle/fingerprint.d.ts +12 -2
- package/dist/tasks/bundle/fingerprint.js +26 -12
- package/dist/tasks/conf/info.d.ts +12 -1
- package/dist/tasks/conf/info.js +36 -12
- package/dist/tasks/init.js +1 -0
- package/dist/tasks/task/fingerprint.d.ts +51 -8
- package/dist/tasks/task/fingerprint.js +12 -35
- package/dist/tasks/task/publish.d.ts +60 -4
- package/dist/tasks/task/publish.js +30 -8
- package/dist/tasks/types.d.ts +1 -0
- package/dist/utils/taskAnalysis.d.ts +27 -1
- package/dist/utils/taskAnalysis.js +760 -107
- package/forge.json +1 -0
- package/logs/bundle:fingerprint.log +1 -0
- package/package.json +9 -9
- package/specs/fingerprint.md +324 -60
- package/src/runner.ts +11 -1
- package/src/tasks/bundle/fingerprint.ts +32 -13
- package/src/tasks/conf/info.ts +36 -10
- package/src/tasks/init.ts +1 -0
- package/src/tasks/task/fingerprint.ts +13 -42
- package/src/tasks/task/publish.ts +35 -10
- package/src/tasks/types.ts +1 -0
- package/src/utils/taskAnalysis.ts +833 -118
|
@@ -7,9 +7,11 @@ interface TaskLocation {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
interface SchemaProperty {
|
|
10
|
+
name?: string
|
|
10
11
|
type: string
|
|
11
12
|
optional?: boolean
|
|
12
13
|
default?: string
|
|
14
|
+
properties?: Record<string, SchemaProperty>
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
interface InputSchema {
|
|
@@ -20,6 +22,18 @@ interface InputSchema {
|
|
|
20
22
|
interface OutputType {
|
|
21
23
|
type: string
|
|
22
24
|
properties?: Record<string, SchemaProperty>
|
|
25
|
+
elementType?: OutputType
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface FingerprintError {
|
|
29
|
+
type: 'parsing' | 'analysis' | 'boundary' | 'schema'
|
|
30
|
+
message: string
|
|
31
|
+
location?: {
|
|
32
|
+
file: string
|
|
33
|
+
line?: number
|
|
34
|
+
column?: number
|
|
35
|
+
}
|
|
36
|
+
details?: Record<string, unknown>
|
|
23
37
|
}
|
|
24
38
|
|
|
25
39
|
interface TaskFingerprint {
|
|
@@ -28,16 +42,30 @@ interface TaskFingerprint {
|
|
|
28
42
|
location: TaskLocation
|
|
29
43
|
inputSchema: InputSchema
|
|
30
44
|
outputType: OutputType
|
|
31
|
-
boundaries:
|
|
45
|
+
boundaries: BoundaryFingerprint[]
|
|
32
46
|
hash: string
|
|
33
47
|
}
|
|
34
48
|
|
|
49
|
+
interface BoundaryFingerprint {
|
|
50
|
+
name: string
|
|
51
|
+
input: SchemaProperty[]
|
|
52
|
+
output: OutputType
|
|
53
|
+
errors: FingerprintError[]
|
|
54
|
+
}
|
|
55
|
+
|
|
35
56
|
// Simplified interface for filesystem output (excludes name, location, hash)
|
|
36
57
|
export interface TaskFingerprintOutput {
|
|
37
58
|
description?: string
|
|
38
59
|
inputSchema: InputSchema
|
|
39
60
|
outputType: OutputType
|
|
40
|
-
boundaries:
|
|
61
|
+
boundaries: BoundaryFingerprint[]
|
|
62
|
+
errors: FingerprintError[]
|
|
63
|
+
analysisMetadata: {
|
|
64
|
+
timestamp: string
|
|
65
|
+
filePath: string
|
|
66
|
+
success: boolean
|
|
67
|
+
analysisVersion: string
|
|
68
|
+
}
|
|
41
69
|
}
|
|
42
70
|
|
|
43
71
|
// Hash generation function
|
|
@@ -51,8 +79,23 @@ function generateHash(input: string): string {
|
|
|
51
79
|
return Math.abs(hash).toString(36)
|
|
52
80
|
}
|
|
53
81
|
|
|
54
|
-
// TypeScript AST analysis function
|
|
55
|
-
function
|
|
82
|
+
// TypeScript AST analysis function with error collection
|
|
83
|
+
function extractTaskFingerprintsWithErrors(sourceCode: string, filePath: string, errors: FingerprintError[]): TaskFingerprint[] {
|
|
84
|
+
try {
|
|
85
|
+
return extractTaskFingerprintsInternal(sourceCode, filePath, errors)
|
|
86
|
+
} catch (error) {
|
|
87
|
+
errors.push({
|
|
88
|
+
type: 'parsing',
|
|
89
|
+
message: error instanceof Error ? error.message : 'TypeScript parsing failed',
|
|
90
|
+
location: { file: filePath },
|
|
91
|
+
details: { error: error instanceof Error ? error.stack : String(error) }
|
|
92
|
+
})
|
|
93
|
+
return []
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// TypeScript AST analysis function with error collection
|
|
98
|
+
function extractTaskFingerprintsInternal(sourceCode: string, filePath: string, errors: FingerprintError[]): TaskFingerprint[] {
|
|
56
99
|
const sourceFile = ts.createSourceFile(
|
|
57
100
|
filePath,
|
|
58
101
|
sourceCode,
|
|
@@ -61,6 +104,7 @@ function extractTaskFingerprints(sourceCode: string, filePath: string): TaskFing
|
|
|
61
104
|
)
|
|
62
105
|
|
|
63
106
|
const fingerprints: TaskFingerprint[] = []
|
|
107
|
+
const processedNodes = new Set<ts.Node>() // Track processed createTask nodes to prevent duplicates
|
|
64
108
|
let schemaNode: ts.Expression | null = null
|
|
65
109
|
let boundariesNode: ts.Expression | null = null
|
|
66
110
|
|
|
@@ -84,14 +128,17 @@ function extractTaskFingerprints(sourceCode: string, filePath: string): TaskFing
|
|
|
84
128
|
function findCreateTask(node: ts.Node): void {
|
|
85
129
|
// Look for createTask calls
|
|
86
130
|
if (ts.isCallExpression(node) &&
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
if (
|
|
94
|
-
|
|
131
|
+
ts.isIdentifier(node.expression) &&
|
|
132
|
+
node.expression.text === 'createTask') {
|
|
133
|
+
|
|
134
|
+
if (!processedNodes.has(node)) {
|
|
135
|
+
processedNodes.add(node)
|
|
136
|
+
const taskName = extractTaskName(node, sourceFile)
|
|
137
|
+
if (taskName) {
|
|
138
|
+
const fingerprint = analyzeCreateTaskCall(node, sourceFile, filePath, taskName, schemaNode, boundariesNode, errors)
|
|
139
|
+
if (fingerprint) {
|
|
140
|
+
fingerprints.push(fingerprint)
|
|
141
|
+
}
|
|
95
142
|
}
|
|
96
143
|
}
|
|
97
144
|
}
|
|
@@ -100,15 +147,18 @@ function extractTaskFingerprints(sourceCode: string, filePath: string): TaskFing
|
|
|
100
147
|
if (ts.isVariableStatement(node) && node.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword)) {
|
|
101
148
|
node.declarationList.declarations.forEach(decl => {
|
|
102
149
|
if (ts.isVariableDeclaration(decl) &&
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
150
|
+
decl.initializer &&
|
|
151
|
+
ts.isCallExpression(decl.initializer) &&
|
|
152
|
+
ts.isIdentifier(decl.initializer.expression) &&
|
|
153
|
+
decl.initializer.expression.text === 'createTask') {
|
|
107
154
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
155
|
+
if (!processedNodes.has(decl.initializer)) {
|
|
156
|
+
processedNodes.add(decl.initializer)
|
|
157
|
+
const taskName = ts.isIdentifier(decl.name) ? decl.name.text : 'unknown'
|
|
158
|
+
const fingerprint = analyzeCreateTaskCall(decl.initializer, sourceFile, filePath, taskName, schemaNode, boundariesNode, errors)
|
|
159
|
+
if (fingerprint) {
|
|
160
|
+
fingerprints.push(fingerprint)
|
|
161
|
+
}
|
|
112
162
|
}
|
|
113
163
|
}
|
|
114
164
|
})
|
|
@@ -142,47 +192,119 @@ function analyzeCreateTaskCall(
|
|
|
142
192
|
filePath: string,
|
|
143
193
|
taskName: string,
|
|
144
194
|
schemaNode: ts.Expression | null = null,
|
|
145
|
-
boundariesNode: ts.Expression | null = null
|
|
195
|
+
boundariesNode: ts.Expression | null = null,
|
|
196
|
+
errors: FingerprintError[] = []
|
|
146
197
|
): TaskFingerprint | null {
|
|
147
198
|
try {
|
|
148
199
|
const position = sourceFile.getLineAndCharacterOfPosition(node.getStart())
|
|
149
200
|
const args = node.arguments
|
|
150
201
|
|
|
151
|
-
//
|
|
202
|
+
// Analyze createTask({ schema, boundaries, fn }) structure
|
|
152
203
|
let inputSchema: InputSchema = { type: 'object', properties: {} }
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
204
|
+
let boundaries: BoundaryFingerprint[] = []
|
|
205
|
+
let boundaryTypes: Map<string, OutputType> = new Map()
|
|
206
|
+
|
|
207
|
+
if (args[0] && ts.isObjectLiteralExpression(args[0])) {
|
|
208
|
+
const schemaProperty = args[0].properties.find(prop =>
|
|
209
|
+
ts.isPropertyAssignment(prop) &&
|
|
210
|
+
ts.isIdentifier(prop.name) &&
|
|
211
|
+
prop.name.text === 'schema'
|
|
212
|
+
)
|
|
213
|
+
const boundariesProperty = args[0].properties.find(prop =>
|
|
214
|
+
ts.isPropertyAssignment(prop) &&
|
|
215
|
+
ts.isIdentifier(prop.name) &&
|
|
216
|
+
prop.name.text === 'boundaries'
|
|
217
|
+
)
|
|
158
218
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
219
|
+
if (schemaNode) {
|
|
220
|
+
try {
|
|
221
|
+
inputSchema = analyzeSchemaArg(schemaNode, sourceFile)
|
|
222
|
+
} catch (error) {
|
|
223
|
+
errors.push({
|
|
224
|
+
type: 'schema',
|
|
225
|
+
message: error instanceof Error ? error.message : 'Schema analysis failed',
|
|
226
|
+
location: { file: filePath, line: position.line + 1, column: position.character + 1 },
|
|
227
|
+
details: { taskName, schemaSource: 'variable' }
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
} else if (schemaProperty && ts.isPropertyAssignment(schemaProperty)) {
|
|
231
|
+
try {
|
|
232
|
+
inputSchema = analyzeSchemaArg(schemaProperty.initializer, sourceFile)
|
|
233
|
+
} catch (error) {
|
|
234
|
+
errors.push({
|
|
235
|
+
type: 'schema',
|
|
236
|
+
message: error instanceof Error ? error.message : 'Schema analysis failed',
|
|
237
|
+
location: { file: filePath, line: position.line + 1, column: position.character + 1 },
|
|
238
|
+
details: { taskName, schemaSource: 'property' }
|
|
239
|
+
})
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (boundariesNode) {
|
|
244
|
+
try {
|
|
245
|
+
const boundaryInfo = analyzeBoundariesWithTypes(boundariesNode, sourceFile)
|
|
246
|
+
boundaries = boundaryInfo.boundaries
|
|
247
|
+
boundaryTypes = boundaryInfo.types
|
|
248
|
+
} catch (error) {
|
|
249
|
+
errors.push({
|
|
250
|
+
type: 'boundary',
|
|
251
|
+
message: error instanceof Error ? error.message : 'Boundary analysis failed',
|
|
252
|
+
location: { file: filePath, line: position.line + 1, column: position.character + 1 },
|
|
253
|
+
details: { taskName, boundarySource: 'variable' }
|
|
254
|
+
})
|
|
255
|
+
}
|
|
256
|
+
} else if (boundariesProperty && ts.isPropertyAssignment(boundariesProperty)) {
|
|
257
|
+
try {
|
|
258
|
+
const boundaryInfo = analyzeBoundariesWithTypes(boundariesProperty.initializer, sourceFile)
|
|
259
|
+
boundaries = boundaryInfo.boundaries
|
|
260
|
+
boundaryTypes = boundaryInfo.types
|
|
261
|
+
} catch (error) {
|
|
262
|
+
errors.push({
|
|
263
|
+
type: 'boundary',
|
|
264
|
+
message: error instanceof Error ? error.message : 'Boundary analysis failed',
|
|
265
|
+
location: { file: filePath, line: position.line + 1, column: position.character + 1 },
|
|
266
|
+
details: { taskName, boundarySource: 'property' }
|
|
267
|
+
})
|
|
268
|
+
}
|
|
269
|
+
}
|
|
165
270
|
}
|
|
166
271
|
|
|
167
272
|
// Extract function output type with better detection
|
|
168
273
|
let outputType: OutputType = { type: 'unknown' }
|
|
169
|
-
|
|
274
|
+
let functionArg: ts.Expression | undefined
|
|
275
|
+
|
|
276
|
+
// Extract function from createTask({ fn }) structure
|
|
277
|
+
if (args[0] && ts.isObjectLiteralExpression(args[0])) {
|
|
278
|
+
const fnProperty = args[0].properties.find(prop =>
|
|
279
|
+
ts.isPropertyAssignment(prop) &&
|
|
280
|
+
ts.isIdentifier(prop.name) &&
|
|
281
|
+
prop.name.text === 'fn'
|
|
282
|
+
)
|
|
283
|
+
if (fnProperty && ts.isPropertyAssignment(fnProperty)) {
|
|
284
|
+
functionArg = fnProperty.initializer
|
|
285
|
+
}
|
|
286
|
+
}
|
|
170
287
|
|
|
171
288
|
if (functionArg) {
|
|
172
289
|
if (ts.isFunctionExpression(functionArg) || ts.isArrowFunction(functionArg)) {
|
|
290
|
+
// Collect errors from main task function
|
|
291
|
+
const mainFunctionErrors = analyzeMainTaskFunctionErrors(functionArg, sourceFile, taskName)
|
|
292
|
+
errors.push(...mainFunctionErrors)
|
|
293
|
+
|
|
173
294
|
// Better return type extraction
|
|
174
295
|
if (functionArg.type) {
|
|
175
296
|
const typeString = cleanTypeString(functionArg.type.getText(sourceFile))
|
|
176
297
|
outputType = { type: typeString }
|
|
177
298
|
} else {
|
|
178
|
-
// Try to infer from return statements with
|
|
179
|
-
outputType = inferDetailedReturnType(functionArg, sourceFile)
|
|
299
|
+
// Try to infer from return statements with boundary type information
|
|
300
|
+
outputType = inferDetailedReturnType(functionArg, sourceFile, boundaryTypes)
|
|
180
301
|
}
|
|
181
302
|
}
|
|
182
303
|
}
|
|
183
304
|
|
|
184
305
|
// Generate hash from task signature
|
|
185
|
-
const
|
|
306
|
+
const boundaryNames = boundaries.map(b => b.name)
|
|
307
|
+
const hashInput = `${taskName}:${JSON.stringify(inputSchema)}:${JSON.stringify(boundaryNames)}`
|
|
186
308
|
const hash = generateHash(hashInput)
|
|
187
309
|
|
|
188
310
|
return {
|
|
@@ -204,9 +326,39 @@ function analyzeCreateTaskCall(
|
|
|
204
326
|
}
|
|
205
327
|
|
|
206
328
|
// Enhanced return type inference with detailed object analysis
|
|
207
|
-
function inferDetailedReturnType(func: ts.FunctionExpression | ts.ArrowFunction,
|
|
329
|
+
function inferDetailedReturnType(func: ts.FunctionExpression | ts.ArrowFunction, sourceFile: ts.SourceFile, boundaryTypes: Map<string, OutputType> = new Map()): OutputType {
|
|
208
330
|
let returnType: OutputType = { type: 'unknown' }
|
|
209
331
|
|
|
332
|
+
// First, collect variable declarations and their types within the function
|
|
333
|
+
const variableTypes = new Map<string, OutputType>()
|
|
334
|
+
|
|
335
|
+
function collectVariableDeclarations(node: ts.Node): void {
|
|
336
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
|
|
337
|
+
const varName = node.name.text
|
|
338
|
+
|
|
339
|
+
// Handle await expressions specially for boundary calls
|
|
340
|
+
if (ts.isAwaitExpression(node.initializer) &&
|
|
341
|
+
ts.isCallExpression(node.initializer.expression) &&
|
|
342
|
+
ts.isIdentifier(node.initializer.expression.expression)) {
|
|
343
|
+
|
|
344
|
+
const boundaryName = node.initializer.expression.expression.text
|
|
345
|
+
const boundaryType = boundaryTypes.get(boundaryName)
|
|
346
|
+
|
|
347
|
+
if (boundaryType && typeof boundaryType === 'object') {
|
|
348
|
+
// Store the detailed type information
|
|
349
|
+
variableTypes.set(varName, boundaryType)
|
|
350
|
+
} else {
|
|
351
|
+
const varType = inferTypeFromExpression(node.initializer, sourceFile, variableTypes, boundaryTypes)
|
|
352
|
+
variableTypes.set(varName, { type: varType })
|
|
353
|
+
}
|
|
354
|
+
} else {
|
|
355
|
+
const varType = inferTypeFromExpression(node.initializer, sourceFile, variableTypes, boundaryTypes)
|
|
356
|
+
variableTypes.set(varName, { type: varType })
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
ts.forEachChild(node, collectVariableDeclarations)
|
|
360
|
+
}
|
|
361
|
+
|
|
210
362
|
function visitReturnStatements(node: ts.Node): void {
|
|
211
363
|
if (ts.isReturnStatement(node) && node.expression) {
|
|
212
364
|
if (ts.isObjectLiteralExpression(node.expression)) {
|
|
@@ -216,31 +368,61 @@ function inferDetailedReturnType(func: ts.FunctionExpression | ts.ArrowFunction,
|
|
|
216
368
|
if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
|
|
217
369
|
// Handle explicit property assignments: { propName: value }
|
|
218
370
|
const propName = prop.name.text
|
|
219
|
-
let propType = 'unknown'
|
|
220
|
-
|
|
221
|
-
// Try to infer property type from the initializer
|
|
222
|
-
if (ts.isStringLiteral(prop.initializer)) {
|
|
223
|
-
propType = 'string'
|
|
224
|
-
} else if (ts.isNumericLiteral(prop.initializer)) {
|
|
225
|
-
propType = 'number'
|
|
226
|
-
} else if (prop.initializer.kind === ts.SyntaxKind.TrueKeyword ||
|
|
227
|
-
prop.initializer.kind === ts.SyntaxKind.FalseKeyword) {
|
|
228
|
-
propType = 'boolean'
|
|
229
|
-
} else if (ts.isIdentifier(prop.initializer)) {
|
|
230
|
-
// Variable reference - try to infer from name
|
|
231
|
-
const varName = prop.initializer.text
|
|
232
|
-
propType = inferTypeFromVariableName(varName)
|
|
233
|
-
} else if (ts.isPropertyAccessExpression(prop.initializer)) {
|
|
234
|
-
// Property access like response.handler
|
|
235
|
-
propType = 'unknown'
|
|
236
|
-
}
|
|
237
371
|
|
|
238
|
-
|
|
372
|
+
// Check if it's a property access like result1.result
|
|
373
|
+
if (ts.isPropertyAccessExpression(prop.initializer)) {
|
|
374
|
+
const baseExpr = prop.initializer.expression
|
|
375
|
+
const propertyName = prop.initializer.name.text
|
|
376
|
+
|
|
377
|
+
if (ts.isIdentifier(baseExpr)) {
|
|
378
|
+
const baseVarType = variableTypes.get(baseExpr.text)
|
|
379
|
+
if (baseVarType && typeof baseVarType === 'object' && baseVarType.properties) {
|
|
380
|
+
const propertyType = baseVarType.properties[propertyName]
|
|
381
|
+
if (propertyType) {
|
|
382
|
+
properties[propName] = propertyType
|
|
383
|
+
} else {
|
|
384
|
+
properties[propName] = { type: 'unknown' }
|
|
385
|
+
}
|
|
386
|
+
} else {
|
|
387
|
+
properties[propName] = { type: 'unknown' }
|
|
388
|
+
}
|
|
389
|
+
} else {
|
|
390
|
+
properties[propName] = { type: 'unknown' }
|
|
391
|
+
}
|
|
392
|
+
} else {
|
|
393
|
+
const propType = inferTypeFromExpression(prop.initializer, sourceFile, variableTypes, boundaryTypes)
|
|
394
|
+
|
|
395
|
+
// Handle identifiers that might reference boundary call results
|
|
396
|
+
if (ts.isIdentifier(prop.initializer)) {
|
|
397
|
+
const varType = variableTypes.get(prop.initializer.text)
|
|
398
|
+
if (varType && typeof varType === 'object' && varType.type) {
|
|
399
|
+
properties[propName] = { type: varType.type }
|
|
400
|
+
} else {
|
|
401
|
+
properties[propName] = { type: propType }
|
|
402
|
+
}
|
|
403
|
+
} else {
|
|
404
|
+
properties[propName] = { type: propType }
|
|
405
|
+
}
|
|
406
|
+
}
|
|
239
407
|
} else if (ts.isShorthandPropertyAssignment(prop)) {
|
|
240
408
|
// Handle shorthand properties: { propName } (equivalent to { propName: propName })
|
|
241
409
|
const propName = prop.name.text
|
|
242
|
-
const
|
|
243
|
-
|
|
410
|
+
const varType = variableTypes.get(propName)
|
|
411
|
+
|
|
412
|
+
if (varType && typeof varType === 'object' && varType.type) {
|
|
413
|
+
// If we have detailed type information from boundary calls
|
|
414
|
+
if (varType.type === 'object' && varType.properties) {
|
|
415
|
+
properties[propName] = varType
|
|
416
|
+
} else if (varType.type === 'array' && varType.elementType) {
|
|
417
|
+
// Handle arrays with elementType information
|
|
418
|
+
properties[propName] = varType
|
|
419
|
+
} else {
|
|
420
|
+
properties[propName] = { type: varType.type }
|
|
421
|
+
}
|
|
422
|
+
} else {
|
|
423
|
+
const propType = varType ? (varType.type || 'unknown') : inferTypeFromIdentifier(prop.name.text, boundaryTypes)
|
|
424
|
+
properties[propName] = { type: propType }
|
|
425
|
+
}
|
|
244
426
|
}
|
|
245
427
|
})
|
|
246
428
|
|
|
@@ -257,53 +439,103 @@ function inferDetailedReturnType(func: ts.FunctionExpression | ts.ArrowFunction,
|
|
|
257
439
|
} else if (ts.isNumericLiteral(node.expression)) {
|
|
258
440
|
returnType = { type: 'number' }
|
|
259
441
|
} else if (node.expression.kind === ts.SyntaxKind.TrueKeyword ||
|
|
260
|
-
|
|
442
|
+
node.expression.kind === ts.SyntaxKind.FalseKeyword) {
|
|
261
443
|
returnType = { type: 'boolean' }
|
|
262
444
|
} else if (ts.isIdentifier(node.expression)) {
|
|
263
445
|
// Single variable return
|
|
264
|
-
const varType =
|
|
265
|
-
|
|
446
|
+
const varType = variableTypes.get(node.expression.text)
|
|
447
|
+
if (varType) {
|
|
448
|
+
returnType = varType
|
|
449
|
+
} else {
|
|
450
|
+
const inferredType = inferTypeFromIdentifier(node.expression.text, boundaryTypes)
|
|
451
|
+
returnType = { type: inferredType }
|
|
452
|
+
}
|
|
266
453
|
}
|
|
267
454
|
}
|
|
268
455
|
ts.forEachChild(node, visitReturnStatements)
|
|
269
456
|
}
|
|
270
457
|
|
|
271
458
|
if (func.body) {
|
|
459
|
+
// First pass: collect variable declarations
|
|
460
|
+
collectVariableDeclarations(func.body)
|
|
461
|
+
// Second pass: analyze return statements
|
|
272
462
|
visitReturnStatements(func.body)
|
|
273
463
|
}
|
|
274
464
|
|
|
275
465
|
return returnType
|
|
276
466
|
}
|
|
277
467
|
|
|
278
|
-
// Helper function to infer
|
|
279
|
-
function
|
|
280
|
-
|
|
281
|
-
if (varName.includes('path') || varName.includes('Path') ||
|
|
282
|
-
varName.includes('name') || varName.includes('Name') ||
|
|
283
|
-
varName.includes('descriptor') || varName.includes('Descriptor') ||
|
|
284
|
-
varName.includes('fileName') || varName.includes('handler') ||
|
|
285
|
-
varName.includes('url') || varName.includes('id') ||
|
|
286
|
-
varName.includes('uuid') || varName.includes('token')) {
|
|
468
|
+
// Helper function to infer type from any expression
|
|
469
|
+
function inferTypeFromExpression(expr: ts.Expression, sourceFile: ts.SourceFile, variableTypes: Map<string, OutputType>, boundaryTypes: Map<string, OutputType> = new Map()): string {
|
|
470
|
+
if (ts.isStringLiteral(expr)) {
|
|
287
471
|
return 'string'
|
|
288
|
-
} else if (
|
|
289
|
-
varName.includes('size') || varName.includes('Size') ||
|
|
290
|
-
varName.includes('length') || varName.includes('Length') ||
|
|
291
|
-
varName.includes('index') || varName.includes('Index')) {
|
|
472
|
+
} else if (ts.isNumericLiteral(expr)) {
|
|
292
473
|
return 'number'
|
|
293
|
-
} else if (
|
|
294
|
-
varName.includes('can') || varName.includes('should') ||
|
|
295
|
-
varName.includes('enabled') || varName.includes('success')) {
|
|
474
|
+
} else if (expr.kind === ts.SyntaxKind.TrueKeyword || expr.kind === ts.SyntaxKind.FalseKeyword) {
|
|
296
475
|
return 'boolean'
|
|
297
|
-
} else if (
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
476
|
+
} else if (ts.isArrayLiteralExpression(expr)) {
|
|
477
|
+
return 'array'
|
|
478
|
+
} else if (ts.isObjectLiteralExpression(expr)) {
|
|
479
|
+
return 'object'
|
|
480
|
+
} else if (ts.isIdentifier(expr)) {
|
|
481
|
+
// Check if we know the type from variable declarations
|
|
482
|
+
const varType = variableTypes.get(expr.text)
|
|
483
|
+
if (varType) {
|
|
484
|
+
return varType.type || 'unknown'
|
|
485
|
+
}
|
|
486
|
+
return inferTypeFromIdentifier(expr.text, boundaryTypes)
|
|
487
|
+
} else if (ts.isCallExpression(expr)) {
|
|
488
|
+
// Handle method calls like array.reduce(), boundary calls, etc.
|
|
489
|
+
if (ts.isPropertyAccessExpression(expr.expression)) {
|
|
490
|
+
const methodName = expr.expression.name.text
|
|
491
|
+
if (methodName === 'reduce') {
|
|
492
|
+
// For reduce, infer type from initial value (second argument)
|
|
493
|
+
if (expr.arguments.length > 1) {
|
|
494
|
+
const initialValue = expr.arguments[1]
|
|
495
|
+
return inferTypeFromExpression(initialValue, sourceFile, variableTypes, boundaryTypes)
|
|
496
|
+
}
|
|
497
|
+
return 'number' // Common case for reduce operations
|
|
498
|
+
} else if (methodName === 'map' || methodName === 'filter') {
|
|
499
|
+
// These typically return arrays
|
|
500
|
+
return 'array'
|
|
501
|
+
} else if (methodName === 'join' || methodName === 'toString') {
|
|
502
|
+
// These return strings
|
|
503
|
+
return 'string'
|
|
504
|
+
} else if (methodName === 'length' || methodName === 'indexOf' || methodName === 'findIndex') {
|
|
505
|
+
// These return numbers
|
|
506
|
+
return 'number'
|
|
507
|
+
}
|
|
508
|
+
} else if (ts.isIdentifier(expr.expression)) {
|
|
509
|
+
// Direct function calls - could be boundary functions
|
|
510
|
+
const functionName = expr.expression.text
|
|
511
|
+
const boundaryType = boundaryTypes.get(functionName)
|
|
512
|
+
if (boundaryType) {
|
|
513
|
+
// If boundaryType is an object with type info, return the type
|
|
514
|
+
if (typeof boundaryType === 'object' && boundaryType.type) {
|
|
515
|
+
return boundaryType.type
|
|
516
|
+
}
|
|
517
|
+
return typeof boundaryType === 'string' ? boundaryType : 'unknown'
|
|
518
|
+
}
|
|
519
|
+
return 'unknown'
|
|
520
|
+
}
|
|
521
|
+
return 'unknown'
|
|
522
|
+
} else if (ts.isAwaitExpression(expr)) {
|
|
523
|
+
// Handle await expressions - analyze the awaited expression
|
|
524
|
+
return inferTypeFromExpression(expr.expression, sourceFile, variableTypes, boundaryTypes)
|
|
525
|
+
} else if (ts.isPropertyAccessExpression(expr)) {
|
|
526
|
+
// Handle property access like obj.prop - try to infer from base object
|
|
527
|
+
const baseType = inferTypeFromExpression(expr.expression, sourceFile, variableTypes, boundaryTypes)
|
|
528
|
+
if (baseType === 'object') {
|
|
529
|
+
return 'unknown' // Could be any property type
|
|
530
|
+
}
|
|
301
531
|
return 'unknown'
|
|
302
532
|
}
|
|
303
533
|
|
|
304
534
|
return 'unknown'
|
|
305
535
|
}
|
|
306
536
|
|
|
537
|
+
|
|
538
|
+
|
|
307
539
|
// Clean up type strings (remove Promise wrappers for boundaries)
|
|
308
540
|
function cleanTypeString(typeString: string): string {
|
|
309
541
|
// Remove Promise wrapper for boundary functions
|
|
@@ -340,33 +572,43 @@ function analyzeSchemaArg(node: ts.Expression, sourceFile: ts.SourceFile): Input
|
|
|
340
572
|
}
|
|
341
573
|
|
|
342
574
|
// Enhanced schema property analysis
|
|
343
|
-
function analyzeSchemaProp(node: ts.Expression,
|
|
575
|
+
function analyzeSchemaProp(node: ts.Expression, sourceFile: ts.SourceFile): SchemaProperty {
|
|
344
576
|
// Analyze Schema.string(), Schema.number(), etc.
|
|
345
577
|
if (ts.isCallExpression(node)) {
|
|
346
578
|
if (ts.isPropertyAccessExpression(node.expression) &&
|
|
347
|
-
|
|
348
|
-
|
|
579
|
+
ts.isIdentifier(node.expression.expression) &&
|
|
580
|
+
node.expression.expression.text === 'Schema') {
|
|
349
581
|
|
|
350
582
|
const methodName = node.expression.name.text
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
// Check for chained methods like .optional() or .default()
|
|
354
|
-
let current = node
|
|
355
|
-
while (current.parent && ts.isCallExpression(current.parent)) {
|
|
356
|
-
current = current.parent
|
|
357
|
-
if (ts.isPropertyAccessExpression(current.expression)) {
|
|
358
|
-
const chainedMethod = current.expression.name.text
|
|
359
|
-
if (chainedMethod === 'optional') {
|
|
360
|
-
baseType = { ...baseType, optional: true }
|
|
361
|
-
} else if (chainedMethod === 'default' && current.arguments[0]) {
|
|
362
|
-
baseType = { ...baseType, default: current.arguments[0].getText() }
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
}
|
|
583
|
+
const baseType: SchemaProperty = { type: getSchemaTypeFromMethod(methodName) }
|
|
366
584
|
|
|
367
585
|
return baseType
|
|
368
586
|
}
|
|
369
587
|
}
|
|
588
|
+
|
|
589
|
+
// Handle chained calls like Schema.number().optional()
|
|
590
|
+
if (ts.isCallExpression(node)) {
|
|
591
|
+
if (ts.isPropertyAccessExpression(node.expression)) {
|
|
592
|
+
const chainedMethod = node.expression.name.text
|
|
593
|
+
if (chainedMethod === 'optional') {
|
|
594
|
+
// This is a .optional() call, get the base type
|
|
595
|
+
const baseCall = node.expression.expression
|
|
596
|
+
if (ts.isCallExpression(baseCall)) {
|
|
597
|
+
const baseType = analyzeSchemaProp(baseCall, sourceFile)
|
|
598
|
+
return { ...baseType, optional: true }
|
|
599
|
+
}
|
|
600
|
+
} else if (chainedMethod === 'default') {
|
|
601
|
+
// This is a .default() call, get the base type
|
|
602
|
+
const baseCall = node.expression.expression
|
|
603
|
+
if (ts.isCallExpression(baseCall)) {
|
|
604
|
+
const baseType = analyzeSchemaProp(baseCall, sourceFile)
|
|
605
|
+
const defaultValue = node.arguments[0]?.getText() || 'undefined'
|
|
606
|
+
return { ...baseType, default: defaultValue }
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
370
612
|
return { type: 'unknown' }
|
|
371
613
|
}
|
|
372
614
|
|
|
@@ -381,39 +623,512 @@ function getSchemaTypeFromMethod(methodName: string): string {
|
|
|
381
623
|
return typeMap[methodName] || 'unknown'
|
|
382
624
|
}
|
|
383
625
|
|
|
384
|
-
function analyzeBoundariesArg(node: ts.Expression, _sourceFile: ts.SourceFile): string[] {
|
|
385
|
-
const boundaries: string[] = []
|
|
386
626
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
627
|
+
// Enhanced boundary analysis that extracts detailed boundary information
|
|
628
|
+
function analyzeBoundariesWithTypes(node: ts.Expression, sourceFile: ts.SourceFile): { names: string[], types: Map<string, OutputType>, boundaries: BoundaryFingerprint[] } {
|
|
629
|
+
const names: string[] = []
|
|
630
|
+
const types = new Map<string, OutputType>()
|
|
631
|
+
const boundaries: BoundaryFingerprint[] = []
|
|
392
632
|
|
|
393
633
|
if (ts.isObjectLiteralExpression(node)) {
|
|
394
634
|
node.properties.forEach(prop => {
|
|
395
635
|
if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
|
|
396
636
|
const boundaryName = prop.name.text
|
|
397
|
-
|
|
637
|
+
names.push(boundaryName)
|
|
638
|
+
|
|
639
|
+
const boundaryErrors: FingerprintError[] = []
|
|
640
|
+
|
|
641
|
+
// Try to analyze the boundary function to extract input and output types
|
|
642
|
+
if (ts.isArrowFunction(prop.initializer) || ts.isFunctionExpression(prop.initializer)) {
|
|
643
|
+
try {
|
|
644
|
+
// Analyze input parameters with error collection
|
|
645
|
+
const inputAnalysis = analyzeBoundaryInputTypesWithErrors(prop.initializer, sourceFile, boundaryName)
|
|
646
|
+
boundaryErrors.push(...inputAnalysis.errors)
|
|
647
|
+
|
|
648
|
+
// Analyze return type with error collection
|
|
649
|
+
const returnAnalysis = analyzeBoundaryReturnTypeWithErrors(prop.initializer, sourceFile, boundaryName)
|
|
650
|
+
boundaryErrors.push(...returnAnalysis.errors)
|
|
651
|
+
|
|
652
|
+
types.set(boundaryName, returnAnalysis.returnType)
|
|
653
|
+
|
|
654
|
+
// Validate boundary structure and collect structural errors
|
|
655
|
+
const structuralErrors = validateBoundaryStructure(prop.initializer, sourceFile, boundaryName)
|
|
656
|
+
boundaryErrors.push(...structuralErrors)
|
|
657
|
+
|
|
658
|
+
// Create boundary fingerprint
|
|
659
|
+
const boundaryFingerprint: BoundaryFingerprint = {
|
|
660
|
+
name: boundaryName,
|
|
661
|
+
input: inputAnalysis.inputTypes,
|
|
662
|
+
output: returnAnalysis.returnType,
|
|
663
|
+
errors: boundaryErrors
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
boundaries.push(boundaryFingerprint)
|
|
667
|
+
} catch (error) {
|
|
668
|
+
boundaryErrors.push({
|
|
669
|
+
type: 'boundary',
|
|
670
|
+
message: error instanceof Error ? error.message : 'Boundary analysis failed',
|
|
671
|
+
location: { file: sourceFile.fileName },
|
|
672
|
+
details: { boundaryName, errorType: 'critical_analysis_failure' }
|
|
673
|
+
})
|
|
674
|
+
|
|
675
|
+
// Create boundary fingerprint with error
|
|
676
|
+
const boundaryFingerprint: BoundaryFingerprint = {
|
|
677
|
+
name: boundaryName,
|
|
678
|
+
input: [],
|
|
679
|
+
output: { type: 'unknown' },
|
|
680
|
+
errors: boundaryErrors
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
boundaries.push(boundaryFingerprint)
|
|
684
|
+
}
|
|
685
|
+
} else {
|
|
686
|
+
// Not a function - this is an error
|
|
687
|
+
boundaryErrors.push({
|
|
688
|
+
type: 'boundary',
|
|
689
|
+
message: 'Boundary is not a function',
|
|
690
|
+
location: { file: sourceFile.fileName },
|
|
691
|
+
details: { boundaryName, nodeType: ts.SyntaxKind[prop.initializer.kind] }
|
|
692
|
+
})
|
|
693
|
+
|
|
694
|
+
const boundaryFingerprint: BoundaryFingerprint = {
|
|
695
|
+
name: boundaryName,
|
|
696
|
+
input: [],
|
|
697
|
+
output: { type: 'unknown' },
|
|
698
|
+
errors: boundaryErrors
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
boundaries.push(boundaryFingerprint)
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
})
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
return { names, types, boundaries }
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// Analyze boundary function input parameter types with error collection
|
|
711
|
+
function analyzeBoundaryInputTypesWithErrors(func: ts.ArrowFunction | ts.FunctionExpression, sourceFile: ts.SourceFile, boundaryName: string): { inputTypes: SchemaProperty[], errors: FingerprintError[] } {
|
|
712
|
+
const inputTypes: SchemaProperty[] = []
|
|
713
|
+
const errors: FingerprintError[] = []
|
|
714
|
+
|
|
715
|
+
if (func.parameters) {
|
|
716
|
+
func.parameters.forEach((param, index) => {
|
|
717
|
+
if (ts.isIdentifier(param.name)) {
|
|
718
|
+
const paramName = param.name.text
|
|
719
|
+
|
|
720
|
+
if (param.type) {
|
|
721
|
+
try {
|
|
722
|
+
const typeText = param.type.getText(sourceFile)
|
|
723
|
+
const schemaProperty = parseTypeToSchemaProperty(typeText)
|
|
724
|
+
inputTypes.push({ ...schemaProperty, name: paramName })
|
|
725
|
+
} catch (error) {
|
|
726
|
+
errors.push({
|
|
727
|
+
type: 'boundary',
|
|
728
|
+
message: `Failed to parse parameter type for '${paramName}': ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
729
|
+
location: { file: sourceFile.fileName },
|
|
730
|
+
details: { boundaryName, parameterName: paramName, parameterIndex: index }
|
|
731
|
+
})
|
|
732
|
+
inputTypes.push({ name: paramName, type: 'unknown' })
|
|
733
|
+
}
|
|
734
|
+
} else {
|
|
735
|
+
// No type annotation - just use unknown
|
|
736
|
+
inputTypes.push({ name: paramName, type: 'unknown' })
|
|
737
|
+
}
|
|
738
|
+
} else {
|
|
739
|
+
// Complex parameter patterns (destructuring, etc.) - just use unknown
|
|
740
|
+
inputTypes.push({ name: `param${index}`, type: 'unknown' })
|
|
398
741
|
}
|
|
399
742
|
})
|
|
743
|
+
|
|
744
|
+
// Only check for obvious parameter issues
|
|
745
|
+
if (func.parameters.length === 0) {
|
|
746
|
+
errors.push({
|
|
747
|
+
type: 'boundary',
|
|
748
|
+
message: 'Boundary function has no parameters',
|
|
749
|
+
location: { file: sourceFile.fileName },
|
|
750
|
+
details: { boundaryName, issue: 'no_parameters' }
|
|
751
|
+
})
|
|
752
|
+
}
|
|
400
753
|
}
|
|
401
754
|
|
|
402
|
-
return
|
|
755
|
+
return { inputTypes, errors }
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
// Helper function to convert TypeScript type text to SchemaProperty
|
|
760
|
+
function parseTypeToSchemaProperty(typeText: string): SchemaProperty {
|
|
761
|
+
// Remove whitespace
|
|
762
|
+
const cleanType = typeText.trim()
|
|
763
|
+
|
|
764
|
+
if (cleanType === 'string') {
|
|
765
|
+
return { type: 'string' }
|
|
766
|
+
} else if (cleanType === 'number') {
|
|
767
|
+
return { type: 'number' }
|
|
768
|
+
} else if (cleanType === 'boolean') {
|
|
769
|
+
return { type: 'boolean' }
|
|
770
|
+
} else if (cleanType.includes('[]') || cleanType.includes('Array<')) {
|
|
771
|
+
return { type: 'array' }
|
|
772
|
+
} else if (cleanType.includes('{') && cleanType.includes('}')) {
|
|
773
|
+
// Parse object type structure
|
|
774
|
+
const objectMatch = cleanType.match(/^\s*\{\s*(.+)\s*\}\s*$/)
|
|
775
|
+
if (objectMatch) {
|
|
776
|
+
const properties: Record<string, SchemaProperty> = {}
|
|
777
|
+
const propsString = objectMatch[1]
|
|
778
|
+
|
|
779
|
+
// Split by commas and semicolons
|
|
780
|
+
const propPairs = propsString.split(/[,;]/).map(s => s.trim())
|
|
781
|
+
|
|
782
|
+
for (const propPair of propPairs) {
|
|
783
|
+
const colonIndex = propPair.indexOf(':')
|
|
784
|
+
if (colonIndex > 0) {
|
|
785
|
+
const propName = propPair.substring(0, colonIndex).trim()
|
|
786
|
+
const propType = propPair.substring(colonIndex + 1).trim()
|
|
787
|
+
|
|
788
|
+
properties[propName] = parseTypeToSchemaProperty(propType)
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
return {
|
|
793
|
+
type: 'object',
|
|
794
|
+
properties
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
return { type: 'object' }
|
|
798
|
+
} else {
|
|
799
|
+
return { type: cleanType }
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// Helper function to infer type from variable names using TypeScript compiler analysis
|
|
804
|
+
function inferTypeFromIdentifier(identifierText: string, boundaryTypes: Map<string, OutputType>): string {
|
|
805
|
+
// Check boundary types first
|
|
806
|
+
if (boundaryTypes.has(identifierText)) {
|
|
807
|
+
const boundaryType = boundaryTypes.get(identifierText)
|
|
808
|
+
if (typeof boundaryType === 'object' && boundaryType.type) {
|
|
809
|
+
return boundaryType.type
|
|
810
|
+
}
|
|
811
|
+
return (typeof boundaryType === 'object' && boundaryType?.type) ? boundaryType.type : 'unknown'
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// Use TypeScript's built-in type inference instead of hardcoded patterns
|
|
815
|
+
// For now, return unknown and let the compiler handle it
|
|
816
|
+
return 'unknown'
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// Analyze boundary function return type with error collection
|
|
820
|
+
function analyzeBoundaryReturnTypeWithErrors(func: ts.ArrowFunction | ts.FunctionExpression, sourceFile: ts.SourceFile, boundaryName: string): { returnType: OutputType, errors: FingerprintError[] } {
|
|
821
|
+
const errors: FingerprintError[] = []
|
|
822
|
+
|
|
823
|
+
// Check if function has explicit return type annotation
|
|
824
|
+
if (func.type) {
|
|
825
|
+
try {
|
|
826
|
+
const typeText = func.type.getText(sourceFile)
|
|
827
|
+
|
|
828
|
+
// Handle Promise<T> types - extract T
|
|
829
|
+
const promiseMatch = typeText.match(/Promise<(.+)>/)
|
|
830
|
+
if (promiseMatch) {
|
|
831
|
+
const innerType = promiseMatch[1]
|
|
832
|
+
|
|
833
|
+
// Just process the type without validation
|
|
834
|
+
|
|
835
|
+
// Parse detailed type patterns
|
|
836
|
+
if (innerType.includes('[]') || innerType.includes('Array<')) {
|
|
837
|
+
// Extract array element type
|
|
838
|
+
let elementType = 'unknown'
|
|
839
|
+
|
|
840
|
+
if (innerType.includes('[]')) {
|
|
841
|
+
// Handle T[] format
|
|
842
|
+
const elementTypeMatch = innerType.match(/^(.+)\[\]$/)
|
|
843
|
+
if (elementTypeMatch) {
|
|
844
|
+
const rawElementType = elementTypeMatch[1].trim()
|
|
845
|
+
|
|
846
|
+
// Parse object element types
|
|
847
|
+
if (rawElementType.includes('{') && rawElementType.includes('}')) {
|
|
848
|
+
const objectType = parseObjectTypeFromString(rawElementType)
|
|
849
|
+
if (objectType && objectType.properties) {
|
|
850
|
+
return {
|
|
851
|
+
returnType: {
|
|
852
|
+
type: 'array',
|
|
853
|
+
elementType: {
|
|
854
|
+
type: 'object',
|
|
855
|
+
properties: objectType.properties
|
|
856
|
+
}
|
|
857
|
+
},
|
|
858
|
+
errors
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
elementType = rawElementType
|
|
864
|
+
}
|
|
865
|
+
} else if (innerType.includes('Array<')) {
|
|
866
|
+
// Handle Array<T> format
|
|
867
|
+
const elementTypeMatch = innerType.match(/Array<(.+)>/)
|
|
868
|
+
if (elementTypeMatch) {
|
|
869
|
+
elementType = elementTypeMatch[1].trim()
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
return {
|
|
874
|
+
returnType: {
|
|
875
|
+
type: 'array',
|
|
876
|
+
elementType: elementType === 'unknown' ? undefined : { type: elementType }
|
|
877
|
+
},
|
|
878
|
+
errors
|
|
879
|
+
}
|
|
880
|
+
} else if (innerType === 'string') {
|
|
881
|
+
return { returnType: { type: 'string' }, errors }
|
|
882
|
+
} else if (innerType === 'number') {
|
|
883
|
+
return { returnType: { type: 'number' }, errors }
|
|
884
|
+
} else if (innerType === 'boolean') {
|
|
885
|
+
return { returnType: { type: 'boolean' }, errors }
|
|
886
|
+
} else if (innerType.includes('{') && innerType.includes('}')) {
|
|
887
|
+
// Try to parse object type structure from string
|
|
888
|
+
try {
|
|
889
|
+
const parsedType = parseObjectTypeFromString(innerType)
|
|
890
|
+
return { returnType: parsedType, errors }
|
|
891
|
+
} catch (parseError) {
|
|
892
|
+
errors.push({
|
|
893
|
+
type: 'boundary',
|
|
894
|
+
message: `Failed to parse return type object structure: ${parseError instanceof Error ? parseError.message : 'Unknown error'}`,
|
|
895
|
+
location: { file: sourceFile.fileName },
|
|
896
|
+
details: { boundaryName, returnType: innerType, issue: 'object_parse_failure' }
|
|
897
|
+
})
|
|
898
|
+
return { returnType: { type: 'object' }, errors }
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
} else {
|
|
902
|
+
// Not a Promise type - just use the type as-is
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
return { returnType: { type: cleanTypeString(typeText) }, errors }
|
|
906
|
+
} catch (error) {
|
|
907
|
+
errors.push({
|
|
908
|
+
type: 'boundary',
|
|
909
|
+
message: `Failed to analyze return type: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
910
|
+
location: { file: sourceFile.fileName },
|
|
911
|
+
details: { boundaryName, issue: 'return_type_analysis_failure' }
|
|
912
|
+
})
|
|
913
|
+
return { returnType: { type: 'unknown' }, errors }
|
|
914
|
+
}
|
|
915
|
+
} else {
|
|
916
|
+
// No return type annotation - will try to infer
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// If no explicit type, try to infer from return statements
|
|
920
|
+
if (func.body) {
|
|
921
|
+
try {
|
|
922
|
+
const returnType = inferDetailedReturnType(func, sourceFile, new Map())
|
|
923
|
+
return { returnType, errors }
|
|
924
|
+
} catch (error) {
|
|
925
|
+
errors.push({
|
|
926
|
+
type: 'boundary',
|
|
927
|
+
message: `Failed to infer return type from function body: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
928
|
+
location: { file: sourceFile.fileName },
|
|
929
|
+
details: { boundaryName, issue: 'return_type_inference_failure' }
|
|
930
|
+
})
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
return { returnType: { type: 'unknown' }, errors }
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
|
|
938
|
+
// Helper function to check for throw statements in boundary functions
|
|
939
|
+
function checkForBoundaryThrowStatements(node: ts.Node, sourceFile: ts.SourceFile, errors: FingerprintError[]): void {
|
|
940
|
+
if (ts.isThrowStatement(node)) {
|
|
941
|
+
let errorMessage = 'Boundary function contains throw statement'
|
|
942
|
+
|
|
943
|
+
// Try to extract error message if it's a simple throw new Error('message')
|
|
944
|
+
if (node.expression && ts.isNewExpression(node.expression)) {
|
|
945
|
+
if (ts.isIdentifier(node.expression.expression) &&
|
|
946
|
+
node.expression.expression.text === 'Error' &&
|
|
947
|
+
node.expression.arguments &&
|
|
948
|
+
node.expression.arguments.length > 0) {
|
|
949
|
+
const errorArg = node.expression.arguments[0]
|
|
950
|
+
if (ts.isStringLiteral(errorArg)) {
|
|
951
|
+
errorMessage = `Boundary function throws: ${errorArg.text}`
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
const position = sourceFile.getLineAndCharacterOfPosition(node.getStart())
|
|
957
|
+
errors.push({
|
|
958
|
+
type: 'boundary',
|
|
959
|
+
message: errorMessage,
|
|
960
|
+
location: {
|
|
961
|
+
file: sourceFile.fileName,
|
|
962
|
+
line: position.line + 1,
|
|
963
|
+
column: position.character + 1
|
|
964
|
+
}
|
|
965
|
+
})
|
|
966
|
+
}
|
|
967
|
+
ts.forEachChild(node, (childNode) => checkForBoundaryThrowStatements(childNode, sourceFile, errors))
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// Validate boundary structure and collect structural errors (simplified to focus on runtime errors)
|
|
971
|
+
function validateBoundaryStructure(func: ts.ArrowFunction | ts.FunctionExpression, sourceFile: ts.SourceFile, _boundaryName: string): FingerprintError[] {
|
|
972
|
+
const errors: FingerprintError[] = []
|
|
973
|
+
|
|
974
|
+
// Check function body for potential runtime errors
|
|
975
|
+
if (func.body) {
|
|
976
|
+
if (ts.isBlock(func.body)) {
|
|
977
|
+
checkForBoundaryThrowStatements(func.body, sourceFile, errors)
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
return errors
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
// Helper function to check for throw statements in main task functions
|
|
985
|
+
function checkForMainTaskThrowStatements(node: ts.Node, sourceFile: ts.SourceFile, errors: FingerprintError[]): void {
|
|
986
|
+
if (ts.isThrowStatement(node)) {
|
|
987
|
+
let errorMessage = 'Main task function contains throw statement'
|
|
988
|
+
|
|
989
|
+
// Try to extract error message if it's a simple throw new Error('message')
|
|
990
|
+
if (node.expression && ts.isNewExpression(node.expression)) {
|
|
991
|
+
if (ts.isIdentifier(node.expression.expression) &&
|
|
992
|
+
node.expression.expression.text === 'Error' &&
|
|
993
|
+
node.expression.arguments &&
|
|
994
|
+
node.expression.arguments.length > 0) {
|
|
995
|
+
const errorArg = node.expression.arguments[0]
|
|
996
|
+
if (ts.isStringLiteral(errorArg)) {
|
|
997
|
+
errorMessage = `Main task function throws: ${errorArg.text}`
|
|
998
|
+
} else if (ts.isTemplateExpression(errorArg)) {
|
|
999
|
+
// Handle template literals like `User with ID ${userId} not found`
|
|
1000
|
+
const templateText = errorArg.getText(sourceFile)
|
|
1001
|
+
errorMessage = `Main task function throws: ${templateText.replace(/`/g, '')}`
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
const position = sourceFile.getLineAndCharacterOfPosition(node.getStart())
|
|
1007
|
+
errors.push({
|
|
1008
|
+
type: 'analysis',
|
|
1009
|
+
message: errorMessage,
|
|
1010
|
+
location: {
|
|
1011
|
+
file: sourceFile.fileName,
|
|
1012
|
+
line: position.line + 1,
|
|
1013
|
+
column: position.character + 1
|
|
1014
|
+
}
|
|
1015
|
+
})
|
|
1016
|
+
}
|
|
1017
|
+
ts.forEachChild(node, (childNode) => checkForMainTaskThrowStatements(childNode, sourceFile, errors))
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
// Analyze main task function for throw statements and runtime errors
|
|
1021
|
+
function analyzeMainTaskFunctionErrors(func: ts.ArrowFunction | ts.FunctionExpression, sourceFile: ts.SourceFile, _taskName: string): FingerprintError[] {
|
|
1022
|
+
const errors: FingerprintError[] = []
|
|
1023
|
+
|
|
1024
|
+
// Check function body for potential runtime errors
|
|
1025
|
+
if (func.body) {
|
|
1026
|
+
if (ts.isBlock(func.body)) {
|
|
1027
|
+
checkForMainTaskThrowStatements(func.body, sourceFile, errors)
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
return errors
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// Helper function to parse object type structure from type string
|
|
1035
|
+
function parseObjectTypeFromString(typeString: string): OutputType {
|
|
1036
|
+
// Simple parsing for common object patterns like "{ result: string }"
|
|
1037
|
+
const objectMatch = typeString.match(/^\s*\{\s*(.+)\s*\}\s*$/)
|
|
1038
|
+
if (objectMatch) {
|
|
1039
|
+
const properties: Record<string, SchemaProperty> = {}
|
|
1040
|
+
const propsString = objectMatch[1]
|
|
1041
|
+
|
|
1042
|
+
// Split by commas (simple approach - doesn't handle nested objects)
|
|
1043
|
+
const propPairs = propsString.split(',').map(s => s.trim())
|
|
1044
|
+
|
|
1045
|
+
for (const propPair of propPairs) {
|
|
1046
|
+
const colonIndex = propPair.indexOf(':')
|
|
1047
|
+
if (colonIndex > 0) {
|
|
1048
|
+
const propName = propPair.substring(0, colonIndex).trim()
|
|
1049
|
+
const propType = propPair.substring(colonIndex + 1).trim()
|
|
1050
|
+
|
|
1051
|
+
if (propType === 'string') {
|
|
1052
|
+
properties[propName] = { type: 'string' }
|
|
1053
|
+
} else if (propType === 'number') {
|
|
1054
|
+
properties[propName] = { type: 'number' }
|
|
1055
|
+
} else if (propType === 'boolean') {
|
|
1056
|
+
properties[propName] = { type: 'boolean' }
|
|
1057
|
+
} else if (propType.includes('[]')) {
|
|
1058
|
+
properties[propName] = { type: 'array' }
|
|
1059
|
+
} else {
|
|
1060
|
+
properties[propName] = { type: propType }
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
return {
|
|
1066
|
+
type: 'object',
|
|
1067
|
+
properties
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
return { type: 'object' }
|
|
403
1072
|
}
|
|
404
1073
|
|
|
405
1074
|
// Export the core analysis function for reuse
|
|
406
1075
|
export function analyzeTaskFile(sourceCode: string, filePath: string, _expectedTaskName?: string): TaskFingerprintOutput | null {
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
|
|
1076
|
+
const errors: FingerprintError[] = []
|
|
1077
|
+
const analysisMetadata = {
|
|
1078
|
+
timestamp: new Date().toISOString(),
|
|
1079
|
+
filePath,
|
|
1080
|
+
success: true,
|
|
1081
|
+
analysisVersion: '1.0.0'
|
|
410
1082
|
}
|
|
411
1083
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
1084
|
+
try {
|
|
1085
|
+
const taskFingerprints = extractTaskFingerprintsWithErrors(sourceCode, filePath, errors)
|
|
1086
|
+
const taskFingerprint = taskFingerprints[0]
|
|
1087
|
+
|
|
1088
|
+
if (!taskFingerprint) {
|
|
1089
|
+
errors.push({
|
|
1090
|
+
type: 'analysis',
|
|
1091
|
+
message: 'No task fingerprint found in file',
|
|
1092
|
+
location: { file: filePath },
|
|
1093
|
+
details: { reason: 'No createTask calls detected' }
|
|
1094
|
+
})
|
|
1095
|
+
analysisMetadata.success = false
|
|
1096
|
+
|
|
1097
|
+
return {
|
|
1098
|
+
description: undefined,
|
|
1099
|
+
inputSchema: { type: 'object', properties: {} },
|
|
1100
|
+
outputType: { type: 'unknown' },
|
|
1101
|
+
boundaries: [],
|
|
1102
|
+
errors,
|
|
1103
|
+
analysisMetadata
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
// Return simplified output without name, location, hash
|
|
1108
|
+
return {
|
|
1109
|
+
description: taskFingerprint.description,
|
|
1110
|
+
inputSchema: taskFingerprint.inputSchema,
|
|
1111
|
+
outputType: taskFingerprint.outputType,
|
|
1112
|
+
boundaries: taskFingerprint.boundaries,
|
|
1113
|
+
errors,
|
|
1114
|
+
analysisMetadata
|
|
1115
|
+
}
|
|
1116
|
+
} catch (error) {
|
|
1117
|
+
errors.push({
|
|
1118
|
+
type: 'parsing',
|
|
1119
|
+
message: error instanceof Error ? error.message : 'Unknown parsing error',
|
|
1120
|
+
location: { file: filePath },
|
|
1121
|
+
details: { error: error instanceof Error ? error.stack : String(error) }
|
|
1122
|
+
})
|
|
1123
|
+
analysisMetadata.success = false
|
|
1124
|
+
|
|
1125
|
+
return {
|
|
1126
|
+
description: undefined,
|
|
1127
|
+
inputSchema: { type: 'object', properties: {} },
|
|
1128
|
+
outputType: { type: 'unknown' },
|
|
1129
|
+
boundaries: [],
|
|
1130
|
+
errors,
|
|
1131
|
+
analysisMetadata
|
|
1132
|
+
}
|
|
418
1133
|
}
|
|
419
1134
|
}
|