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