@bubblelab/bubble-runtime 0.1.14 → 0.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/dist/extraction/BubbleParser.d.ts +187 -8
  2. package/dist/extraction/BubbleParser.d.ts.map +1 -1
  3. package/dist/extraction/BubbleParser.js +2271 -117
  4. package/dist/extraction/BubbleParser.js.map +1 -1
  5. package/dist/extraction/index.js +1 -0
  6. package/dist/index.d.ts +1 -0
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +2 -0
  9. package/dist/index.js.map +1 -1
  10. package/dist/injection/BubbleInjector.d.ts +27 -2
  11. package/dist/injection/BubbleInjector.d.ts.map +1 -1
  12. package/dist/injection/BubbleInjector.js +343 -35
  13. package/dist/injection/BubbleInjector.js.map +1 -1
  14. package/dist/injection/LoggerInjector.d.ts +12 -1
  15. package/dist/injection/LoggerInjector.d.ts.map +1 -1
  16. package/dist/injection/LoggerInjector.js +301 -13
  17. package/dist/injection/LoggerInjector.js.map +1 -1
  18. package/dist/injection/index.js +1 -0
  19. package/dist/parse/BubbleScript.d.ts +60 -3
  20. package/dist/parse/BubbleScript.d.ts.map +1 -1
  21. package/dist/parse/BubbleScript.js +133 -15
  22. package/dist/parse/BubbleScript.js.map +1 -1
  23. package/dist/parse/index.d.ts +0 -1
  24. package/dist/parse/index.d.ts.map +1 -1
  25. package/dist/parse/index.js +1 -1
  26. package/dist/parse/index.js.map +1 -1
  27. package/dist/runtime/BubbleRunner.d.ts +8 -2
  28. package/dist/runtime/BubbleRunner.d.ts.map +1 -1
  29. package/dist/runtime/BubbleRunner.js +41 -30
  30. package/dist/runtime/BubbleRunner.js.map +1 -1
  31. package/dist/runtime/index.d.ts +1 -1
  32. package/dist/runtime/index.d.ts.map +1 -1
  33. package/dist/runtime/index.js +1 -0
  34. package/dist/runtime/index.js.map +1 -1
  35. package/dist/runtime/types.js +1 -0
  36. package/dist/types/index.js +1 -0
  37. package/dist/utils/bubble-helper.d.ts +2 -2
  38. package/dist/utils/bubble-helper.d.ts.map +1 -1
  39. package/dist/utils/bubble-helper.js +6 -1
  40. package/dist/utils/bubble-helper.js.map +1 -1
  41. package/dist/utils/normalize-control-flow.d.ts +14 -0
  42. package/dist/utils/normalize-control-flow.d.ts.map +1 -0
  43. package/dist/utils/normalize-control-flow.js +179 -0
  44. package/dist/utils/normalize-control-flow.js.map +1 -0
  45. package/dist/utils/parameter-formatter.d.ts +14 -5
  46. package/dist/utils/parameter-formatter.d.ts.map +1 -1
  47. package/dist/utils/parameter-formatter.js +164 -45
  48. package/dist/utils/parameter-formatter.js.map +1 -1
  49. package/dist/utils/sanitize-script.d.ts +11 -0
  50. package/dist/utils/sanitize-script.d.ts.map +1 -0
  51. package/dist/utils/sanitize-script.js +43 -0
  52. package/dist/utils/sanitize-script.js.map +1 -0
  53. package/dist/validation/BubbleValidator.d.ts +15 -0
  54. package/dist/validation/BubbleValidator.d.ts.map +1 -1
  55. package/dist/validation/BubbleValidator.js +168 -1
  56. package/dist/validation/BubbleValidator.js.map +1 -1
  57. package/dist/validation/index.d.ts +6 -3
  58. package/dist/validation/index.d.ts.map +1 -1
  59. package/dist/validation/index.js +33 -9
  60. package/dist/validation/index.js.map +1 -1
  61. package/dist/validation/lint-rules.d.ts +91 -0
  62. package/dist/validation/lint-rules.d.ts.map +1 -0
  63. package/dist/validation/lint-rules.js +755 -0
  64. package/dist/validation/lint-rules.js.map +1 -0
  65. package/package.json +4 -4
  66. package/dist/parse/traceDependencies.d.ts +0 -18
  67. package/dist/parse/traceDependencies.d.ts.map +0 -1
  68. package/dist/parse/traceDependencies.js +0 -195
  69. package/dist/parse/traceDependencies.js.map +0 -1
@@ -0,0 +1,755 @@
1
+ import ts from 'typescript';
2
+ /**
3
+ * Registry that manages and executes all lint rules
4
+ */
5
+ export class LintRuleRegistry {
6
+ rules = [];
7
+ /**
8
+ * Register a lint rule
9
+ */
10
+ register(rule) {
11
+ this.rules.push(rule);
12
+ }
13
+ /**
14
+ * Execute all registered rules on the given code
15
+ * Traverses AST once and shares context with all rules for efficiency
16
+ */
17
+ validateAll(sourceFile) {
18
+ // Parse AST once and create shared context
19
+ const context = parseLintRuleContext(sourceFile);
20
+ const errors = [];
21
+ for (const rule of this.rules) {
22
+ try {
23
+ const ruleErrors = rule.validate(context);
24
+ errors.push(...ruleErrors);
25
+ }
26
+ catch (error) {
27
+ // If a rule fails, log but don't stop other rules
28
+ console.error(`Error in lint rule ${rule.name}:`, error);
29
+ }
30
+ }
31
+ return errors;
32
+ }
33
+ /**
34
+ * Get all registered rule names
35
+ */
36
+ getRuleNames() {
37
+ return this.rules.map((r) => r.name);
38
+ }
39
+ }
40
+ /**
41
+ * Parses the AST once to create a shared context for all lint rules
42
+ * This avoids redundant AST traversals by doing a single pass
43
+ */
44
+ function parseLintRuleContext(sourceFile) {
45
+ let bubbleFlowClass = null;
46
+ let handleMethod = null;
47
+ const importedBubbleClasses = new Set();
48
+ // Single AST traversal to collect all needed information
49
+ const visit = (node) => {
50
+ // Find BubbleFlow class
51
+ if (ts.isClassDeclaration(node) && !bubbleFlowClass) {
52
+ if (node.heritageClauses) {
53
+ for (const clause of node.heritageClauses) {
54
+ if (clause.token === ts.SyntaxKind.ExtendsKeyword) {
55
+ for (const type of clause.types) {
56
+ if (ts.isIdentifier(type.expression)) {
57
+ if (type.expression.text === 'BubbleFlow') {
58
+ bubbleFlowClass = node;
59
+ // Find handle method in this class
60
+ if (node.members) {
61
+ for (const member of node.members) {
62
+ if (ts.isMethodDeclaration(member)) {
63
+ const name = member.name;
64
+ if (ts.isIdentifier(name) && name.text === 'handle') {
65
+ handleMethod = member;
66
+ break;
67
+ }
68
+ }
69
+ }
70
+ }
71
+ break;
72
+ }
73
+ }
74
+ }
75
+ }
76
+ }
77
+ }
78
+ }
79
+ // Collect imported bubble classes
80
+ if (ts.isImportDeclaration(node)) {
81
+ const moduleSpecifier = node.moduleSpecifier;
82
+ if (ts.isStringLiteral(moduleSpecifier) &&
83
+ (moduleSpecifier.text === '@bubblelab/bubble-core' ||
84
+ moduleSpecifier.text === '@nodex/bubble-core')) {
85
+ if (node.importClause && node.importClause.namedBindings) {
86
+ if (ts.isNamedImports(node.importClause.namedBindings)) {
87
+ for (const element of node.importClause.namedBindings.elements) {
88
+ const importedName = element.name
89
+ ? element.name.text
90
+ : element.propertyName?.text;
91
+ if (importedName) {
92
+ if ((importedName.endsWith('Bubble') ||
93
+ (importedName.endsWith('Tool') &&
94
+ !importedName.includes('Structured'))) &&
95
+ importedName !== 'BubbleFlow' &&
96
+ importedName !== 'BaseBubble') {
97
+ importedBubbleClasses.add(importedName);
98
+ }
99
+ }
100
+ }
101
+ }
102
+ }
103
+ }
104
+ }
105
+ ts.forEachChild(node, visit);
106
+ };
107
+ visit(sourceFile);
108
+ let handleMethodBody = null;
109
+ if (handleMethod !== null) {
110
+ const methodBody = handleMethod.body;
111
+ if (methodBody !== undefined && ts.isBlock(methodBody)) {
112
+ handleMethodBody = methodBody;
113
+ }
114
+ }
115
+ return {
116
+ sourceFile,
117
+ bubbleFlowClass,
118
+ handleMethod,
119
+ handleMethodBody,
120
+ importedBubbleClasses,
121
+ };
122
+ }
123
+ /**
124
+ * Lint rule that prevents throw statements directly in the handle method
125
+ */
126
+ export const noThrowInHandleRule = {
127
+ name: 'no-throw-in-handle',
128
+ validate(context) {
129
+ const errors = [];
130
+ // Use pre-parsed context
131
+ if (!context.handleMethodBody) {
132
+ return errors; // No handle method body found, skip this rule
133
+ }
134
+ // Check only direct statements in the method body (not nested)
135
+ for (const statement of context.handleMethodBody.statements) {
136
+ const throwError = checkStatementForThrow(statement, context.sourceFile);
137
+ if (throwError) {
138
+ errors.push(throwError);
139
+ }
140
+ }
141
+ return errors;
142
+ },
143
+ };
144
+ /**
145
+ * Checks if a statement is a throw statement or contains a throw at the top level
146
+ * Only checks direct statements, not nested blocks
147
+ */
148
+ function checkStatementForThrow(statement, sourceFile) {
149
+ // Direct throw statement
150
+ if (ts.isThrowStatement(statement)) {
151
+ const { line } = sourceFile.getLineAndCharacterOfPosition(statement.getStart(sourceFile));
152
+ return {
153
+ line: line + 1, // Convert to 1-based
154
+ message: 'throw statements are not allowed directly in handle method. Move error handling into another step.',
155
+ };
156
+ }
157
+ // Check for if statement with direct throw in then/else branches
158
+ // Note: We only check if the then/else is a direct throw statement, not if it's inside a block
159
+ if (ts.isIfStatement(statement)) {
160
+ // Check if the then branch is a direct throw statement (not inside a block)
161
+ if (ts.isThrowStatement(statement.thenStatement)) {
162
+ const { line } = sourceFile.getLineAndCharacterOfPosition(statement.thenStatement.getStart(sourceFile));
163
+ return {
164
+ line: line + 1,
165
+ message: 'throw statements are not allowed directly in handle method. Move error handling into another step.',
166
+ };
167
+ }
168
+ // Check if the else branch is a direct throw statement (not inside a block)
169
+ if (statement.elseStatement &&
170
+ ts.isThrowStatement(statement.elseStatement)) {
171
+ const { line } = sourceFile.getLineAndCharacterOfPosition(statement.elseStatement.getStart(sourceFile));
172
+ return {
173
+ line: line + 1,
174
+ message: 'throw statements are not allowed directly in handle method. Move error handling into another step.',
175
+ };
176
+ }
177
+ }
178
+ return null;
179
+ }
180
+ /**
181
+ * Lint rule that prevents direct bubble instantiation in the handle method
182
+ */
183
+ export const noDirectBubbleInstantiationInHandleRule = {
184
+ name: 'no-direct-bubble-instantiation-in-handle',
185
+ validate(context) {
186
+ const errors = [];
187
+ // Use pre-parsed context
188
+ if (!context.handleMethodBody) {
189
+ return errors; // No handle method body found, skip this rule
190
+ }
191
+ // Recursively check all statements in the method body, including nested blocks
192
+ for (const statement of context.handleMethodBody.statements) {
193
+ const bubbleErrors = checkStatementForBubbleInstantiation(statement, context.sourceFile, context.importedBubbleClasses);
194
+ errors.push(...bubbleErrors);
195
+ }
196
+ return errors;
197
+ },
198
+ };
199
+ /**
200
+ * Checks if a statement contains a direct bubble instantiation
201
+ * Recursively checks nested blocks to find all bubble instantiations
202
+ */
203
+ function checkStatementForBubbleInstantiation(statement, sourceFile, importedBubbleClasses) {
204
+ const errors = [];
205
+ // Check for variable declaration with bubble instantiation
206
+ if (ts.isVariableStatement(statement)) {
207
+ for (const declaration of statement.declarationList.declarations) {
208
+ if (declaration.initializer) {
209
+ const error = checkExpressionForBubbleInstantiation(declaration.initializer, sourceFile, importedBubbleClasses);
210
+ if (error) {
211
+ errors.push(error);
212
+ }
213
+ }
214
+ }
215
+ }
216
+ // Check for expression statement with bubble instantiation
217
+ if (ts.isExpressionStatement(statement)) {
218
+ const error = checkExpressionForBubbleInstantiation(statement.expression, sourceFile, importedBubbleClasses);
219
+ if (error) {
220
+ errors.push(error);
221
+ }
222
+ }
223
+ // Check for if statement - recursively check then/else branches
224
+ if (ts.isIfStatement(statement)) {
225
+ // Check the then branch
226
+ if (ts.isBlock(statement.thenStatement)) {
227
+ // Recursively check all statements inside the block
228
+ for (const nestedStatement of statement.thenStatement.statements) {
229
+ const nestedErrors = checkStatementForBubbleInstantiation(nestedStatement, sourceFile, importedBubbleClasses);
230
+ errors.push(...nestedErrors);
231
+ }
232
+ }
233
+ else {
234
+ // Single statement (not a block) - check it directly
235
+ const nestedErrors = checkStatementForBubbleInstantiation(statement.thenStatement, sourceFile, importedBubbleClasses);
236
+ errors.push(...nestedErrors);
237
+ }
238
+ // Check the else branch if it exists
239
+ if (statement.elseStatement) {
240
+ if (ts.isBlock(statement.elseStatement)) {
241
+ // Recursively check all statements inside the block
242
+ for (const nestedStatement of statement.elseStatement.statements) {
243
+ const nestedErrors = checkStatementForBubbleInstantiation(nestedStatement, sourceFile, importedBubbleClasses);
244
+ errors.push(...nestedErrors);
245
+ }
246
+ }
247
+ else {
248
+ // Single statement (not a block) - check it directly
249
+ const nestedErrors = checkStatementForBubbleInstantiation(statement.elseStatement, sourceFile, importedBubbleClasses);
250
+ errors.push(...nestedErrors);
251
+ }
252
+ }
253
+ }
254
+ // Check for other block statements (for, while, etc.)
255
+ if (ts.isForStatement(statement) ||
256
+ ts.isWhileStatement(statement) ||
257
+ ts.isForInStatement(statement) ||
258
+ ts.isForOfStatement(statement)) {
259
+ const block = statement.statement;
260
+ if (ts.isBlock(block)) {
261
+ for (const nestedStatement of block.statements) {
262
+ const nestedErrors = checkStatementForBubbleInstantiation(nestedStatement, sourceFile, importedBubbleClasses);
263
+ errors.push(...nestedErrors);
264
+ }
265
+ }
266
+ else {
267
+ // Single statement (not a block) - check it directly
268
+ const nestedErrors = checkStatementForBubbleInstantiation(block, sourceFile, importedBubbleClasses);
269
+ errors.push(...nestedErrors);
270
+ }
271
+ }
272
+ // Check for try-catch-finally statements
273
+ if (ts.isTryStatement(statement)) {
274
+ // Check try block
275
+ if (ts.isBlock(statement.tryBlock)) {
276
+ for (const nestedStatement of statement.tryBlock.statements) {
277
+ const nestedErrors = checkStatementForBubbleInstantiation(nestedStatement, sourceFile, importedBubbleClasses);
278
+ errors.push(...nestedErrors);
279
+ }
280
+ }
281
+ // Check catch clause
282
+ if (statement.catchClause && statement.catchClause.block) {
283
+ for (const nestedStatement of statement.catchClause.block.statements) {
284
+ const nestedErrors = checkStatementForBubbleInstantiation(nestedStatement, sourceFile, importedBubbleClasses);
285
+ errors.push(...nestedErrors);
286
+ }
287
+ }
288
+ // Check finally block
289
+ if (statement.finallyBlock && ts.isBlock(statement.finallyBlock)) {
290
+ for (const nestedStatement of statement.finallyBlock.statements) {
291
+ const nestedErrors = checkStatementForBubbleInstantiation(nestedStatement, sourceFile, importedBubbleClasses);
292
+ errors.push(...nestedErrors);
293
+ }
294
+ }
295
+ }
296
+ return errors;
297
+ }
298
+ /**
299
+ * Checks if an expression is a bubble instantiation (new BubbleClass(...))
300
+ */
301
+ function checkExpressionForBubbleInstantiation(expression, sourceFile, importedBubbleClasses) {
302
+ // Handle await expressions
303
+ if (ts.isAwaitExpression(expression)) {
304
+ return checkExpressionForBubbleInstantiation(expression.expression, sourceFile, importedBubbleClasses);
305
+ }
306
+ // Handle call expressions (e.g., new Bubble().action())
307
+ if (ts.isCallExpression(expression)) {
308
+ if (ts.isPropertyAccessExpression(expression.expression)) {
309
+ return checkExpressionForBubbleInstantiation(expression.expression.expression, sourceFile, importedBubbleClasses);
310
+ }
311
+ }
312
+ // Check for new expression
313
+ if (ts.isNewExpression(expression)) {
314
+ const className = getClassNameFromExpression(expression.expression);
315
+ if (className && isBubbleClass(className, importedBubbleClasses)) {
316
+ const { line } = sourceFile.getLineAndCharacterOfPosition(expression.getStart(sourceFile));
317
+ return {
318
+ line: line + 1,
319
+ message: 'Direct bubble instantiation is not allowed in handle method. Move bubble creation into another step.',
320
+ };
321
+ }
322
+ }
323
+ return null;
324
+ }
325
+ /**
326
+ * Gets the class name from an expression (handles identifiers and property access)
327
+ */
328
+ function getClassNameFromExpression(expression) {
329
+ if (ts.isIdentifier(expression)) {
330
+ return expression.text;
331
+ }
332
+ if (ts.isPropertyAccessExpression(expression)) {
333
+ return expression.name.text;
334
+ }
335
+ return null;
336
+ }
337
+ /**
338
+ * Checks if a class name represents a bubble class
339
+ */
340
+ function isBubbleClass(className, importedBubbleClasses) {
341
+ // Check if it's in the imported bubble classes
342
+ if (importedBubbleClasses.has(className)) {
343
+ return true;
344
+ }
345
+ // Fallback: check naming pattern
346
+ // Bubble classes typically end with "Bubble" or "Tool" (but not StructuredTool)
347
+ const endsWithBubble = className.endsWith('Bubble');
348
+ const endsWithTool = className.endsWith('Tool') && !className.includes('Structured');
349
+ return ((endsWithBubble || endsWithTool) &&
350
+ className !== 'BubbleFlow' &&
351
+ className !== 'BaseBubble' &&
352
+ !className.includes('Error') &&
353
+ !className.includes('Exception') &&
354
+ !className.includes('Validation'));
355
+ }
356
+ /**
357
+ * Lint rule that prevents credentials parameter from being used in bubble instantiations
358
+ */
359
+ export const noCredentialsParameterRule = {
360
+ name: 'no-credentials-parameter',
361
+ validate(context) {
362
+ const errors = [];
363
+ // Traverse entire source file to find all bubble instantiations
364
+ const visit = (node) => {
365
+ // Check for new expressions (bubble instantiations)
366
+ if (ts.isNewExpression(node)) {
367
+ const className = getClassNameFromExpression(node.expression);
368
+ if (className &&
369
+ isBubbleClass(className, context.importedBubbleClasses)) {
370
+ // Check constructor arguments for credentials parameter
371
+ if (node.arguments && node.arguments.length > 0) {
372
+ for (const arg of node.arguments) {
373
+ const credentialError = checkForCredentialsParameter(arg, context.sourceFile);
374
+ if (credentialError) {
375
+ errors.push(credentialError);
376
+ }
377
+ }
378
+ }
379
+ }
380
+ }
381
+ ts.forEachChild(node, visit);
382
+ };
383
+ visit(context.sourceFile);
384
+ return errors;
385
+ },
386
+ };
387
+ /**
388
+ * Checks if an expression (constructor argument) contains a credentials parameter
389
+ */
390
+ function checkForCredentialsParameter(expression, sourceFile) {
391
+ // Handle object literals: { credentials: {...} }
392
+ if (ts.isObjectLiteralExpression(expression)) {
393
+ for (const property of expression.properties) {
394
+ if (ts.isPropertyAssignment(property)) {
395
+ const name = property.name;
396
+ if ((ts.isIdentifier(name) && name.text === 'credentials') ||
397
+ (ts.isStringLiteral(name) && name.text === 'credentials')) {
398
+ const { line } = sourceFile.getLineAndCharacterOfPosition(property.getStart(sourceFile));
399
+ return {
400
+ line: line + 1,
401
+ message: 'credentials parameter is not allowed in bubble instantiation. Credentials should be injected at runtime, not passed as parameters.',
402
+ };
403
+ }
404
+ }
405
+ // Handle shorthand property: { credentials }
406
+ if (ts.isShorthandPropertyAssignment(property)) {
407
+ const name = property.name;
408
+ if (ts.isIdentifier(name) && name.text === 'credentials') {
409
+ const { line } = sourceFile.getLineAndCharacterOfPosition(property.getStart(sourceFile));
410
+ return {
411
+ line: line + 1,
412
+ message: 'credentials parameter is not allowed in bubble instantiation. Credentials should be injected at runtime, not passed as parameters.',
413
+ };
414
+ }
415
+ }
416
+ }
417
+ }
418
+ // Handle spread expressions that might contain credentials
419
+ if (ts.isSpreadElement(expression)) {
420
+ return checkForCredentialsParameter(expression.expression, sourceFile);
421
+ }
422
+ // Handle type assertions: { credentials: {...} } as Record<string, string>
423
+ if (ts.isAsExpression(expression)) {
424
+ return checkForCredentialsParameter(expression.expression, sourceFile);
425
+ }
426
+ // Handle parenthesized expressions: ({ credentials: {...} })
427
+ if (ts.isParenthesizedExpression(expression)) {
428
+ return checkForCredentialsParameter(expression.expression, sourceFile);
429
+ }
430
+ return null;
431
+ }
432
+ /**
433
+ * Lint rule that prevents usage of process.env
434
+ */
435
+ export const noProcessEnvRule = {
436
+ name: 'no-process-env',
437
+ validate(context) {
438
+ const errors = [];
439
+ // Traverse entire source file to find all process.env usages
440
+ const visit = (node) => {
441
+ // Check for property access expression: process.env
442
+ if (ts.isPropertyAccessExpression(node)) {
443
+ const object = node.expression;
444
+ const property = node.name;
445
+ // Check if it's process.env
446
+ if (ts.isIdentifier(object) &&
447
+ object.text === 'process' &&
448
+ ts.isIdentifier(property) &&
449
+ property.text === 'env') {
450
+ const { line, character } = context.sourceFile.getLineAndCharacterOfPosition(node.getStart(context.sourceFile));
451
+ errors.push({
452
+ line: line + 1,
453
+ column: character + 1,
454
+ message: 'process.env is not allowed. Put the credential inside payload if the integration is not supported yet (service is not an available bubble).',
455
+ });
456
+ }
457
+ }
458
+ ts.forEachChild(node, visit);
459
+ };
460
+ visit(context.sourceFile);
461
+ return errors;
462
+ },
463
+ };
464
+ /**
465
+ * Lint rule that prevents method invocations inside complex expressions
466
+ */
467
+ export const noMethodInvocationInComplexExpressionRule = {
468
+ name: 'no-method-invocation-in-complex-expression',
469
+ validate(context) {
470
+ const errors = [];
471
+ // Track parent nodes to detect complex expressions
472
+ const visitWithParents = (node, parents = []) => {
473
+ // Check for method calls: this.methodName()
474
+ if (ts.isCallExpression(node)) {
475
+ if (ts.isPropertyAccessExpression(node.expression)) {
476
+ const object = node.expression.expression;
477
+ // Check if it's 'this' keyword (SyntaxKind.ThisKeyword)
478
+ if (object.kind === ts.SyntaxKind.ThisKeyword) {
479
+ // This is a method invocation: this.methodName()
480
+ // Check if any parent is a complex expression
481
+ const complexParent = findComplexExpressionParent(parents, node);
482
+ if (complexParent) {
483
+ const { line, character } = context.sourceFile.getLineAndCharacterOfPosition(node.getStart(context.sourceFile));
484
+ const methodName = node.expression.name.text;
485
+ const parentType = getReadableParentType(complexParent);
486
+ errors.push({
487
+ line: line + 1,
488
+ column: character + 1,
489
+ message: `Method invocation 'this.${methodName}()' inside ${parentType} cannot be instrumented. Extract to a separate variable before using in ${parentType}.`,
490
+ });
491
+ }
492
+ }
493
+ }
494
+ }
495
+ // Recursively visit children with updated parent chain
496
+ ts.forEachChild(node, (child) => {
497
+ visitWithParents(child, [...parents, node]);
498
+ });
499
+ };
500
+ visitWithParents(context.sourceFile);
501
+ return errors;
502
+ },
503
+ };
504
+ /**
505
+ * Finds if any parent node is a complex expression that cannot contain instrumented calls
506
+ */
507
+ function findComplexExpressionParent(parents, node) {
508
+ // Walk through parents to find complex expressions
509
+ // Stop at statement boundaries (these are safe)
510
+ let currentChild = node;
511
+ for (let i = parents.length - 1; i >= 0; i--) {
512
+ const parent = parents[i];
513
+ // Stop at statement boundaries - these are safe contexts
514
+ if (ts.isVariableDeclaration(parent) ||
515
+ ts.isExpressionStatement(parent) ||
516
+ ts.isReturnStatement(parent) ||
517
+ ts.isBlock(parent)) {
518
+ return null;
519
+ }
520
+ // Check for complex expressions
521
+ if (ts.isConditionalExpression(parent)) {
522
+ return parent; // Ternary operator
523
+ }
524
+ if (ts.isObjectLiteralExpression(parent)) {
525
+ return parent; // Object literal
526
+ }
527
+ if (ts.isArrayLiteralExpression(parent)) {
528
+ if (currentChild &&
529
+ isPromiseAllArrayElement(parent, currentChild)) {
530
+ currentChild = parent;
531
+ continue;
532
+ }
533
+ return parent; // Array literal
534
+ }
535
+ if (ts.isPropertyAssignment(parent)) {
536
+ return parent; // Object property value
537
+ }
538
+ if (ts.isSpreadElement(parent)) {
539
+ return parent; // Spread expression
540
+ }
541
+ currentChild = parent;
542
+ }
543
+ return null;
544
+ }
545
+ /**
546
+ * Gets a human-readable description of the parent node type
547
+ */
548
+ function getReadableParentType(node) {
549
+ if (ts.isConditionalExpression(node)) {
550
+ return 'ternary operator';
551
+ }
552
+ if (ts.isObjectLiteralExpression(node)) {
553
+ return 'object literal';
554
+ }
555
+ if (ts.isArrayLiteralExpression(node)) {
556
+ return 'array literal';
557
+ }
558
+ if (ts.isPropertyAssignment(node)) {
559
+ return 'object property';
560
+ }
561
+ if (ts.isSpreadElement(node)) {
562
+ return 'spread expression';
563
+ }
564
+ return 'complex expression';
565
+ }
566
+ function isPromiseAllArrayElement(arrayNode, childNode) {
567
+ if (!arrayNode.elements.some((element) => element === childNode)) {
568
+ return false;
569
+ }
570
+ if (!arrayNode.parent || !ts.isCallExpression(arrayNode.parent)) {
571
+ return false;
572
+ }
573
+ const callExpr = arrayNode.parent;
574
+ const callee = callExpr.expression;
575
+ if (!ts.isPropertyAccessExpression(callee) ||
576
+ !ts.isIdentifier(callee.expression) ||
577
+ callee.expression.text !== 'Promise' ||
578
+ callee.name.text !== 'all') {
579
+ return false;
580
+ }
581
+ return callExpr.arguments.length > 0 && callExpr.arguments[0] === arrayNode;
582
+ }
583
+ /**
584
+ * Lint rule that prevents try-catch statements in the handle method
585
+ * Try-catch blocks interfere with runtime instrumentation and error handling
586
+ */
587
+ export const noTryCatchInHandleRule = {
588
+ name: 'no-try-catch-in-handle',
589
+ validate(context) {
590
+ const errors = [];
591
+ if (!context.handleMethodBody) {
592
+ return errors; // No handle method body found, skip this rule
593
+ }
594
+ // Recursively find all try statements in the handle method body
595
+ const findTryStatements = (node) => {
596
+ if (ts.isTryStatement(node)) {
597
+ const { line } = context.sourceFile.getLineAndCharacterOfPosition(node.getStart(context.sourceFile));
598
+ errors.push({
599
+ line: line + 1,
600
+ message: 'try-catch statements are not allowed in handle method. Error handling should be done in function steps.',
601
+ });
602
+ // Don't return early - continue checking nested content for multiple violations
603
+ }
604
+ ts.forEachChild(node, findTryStatements);
605
+ };
606
+ // Start recursive search from the handle method body
607
+ findTryStatements(context.handleMethodBody);
608
+ return errors;
609
+ },
610
+ };
611
+ /**
612
+ * Lint rule that prevents methods from calling other methods
613
+ * Methods should only be called from the handle method, not from other methods
614
+ */
615
+ export const noMethodCallingMethodRule = {
616
+ name: 'no-method-calling-method',
617
+ validate(context) {
618
+ const errors = [];
619
+ if (!context.bubbleFlowClass) {
620
+ return errors; // No BubbleFlow class found, skip this rule
621
+ }
622
+ // Find all methods in the BubbleFlow class
623
+ const methods = [];
624
+ if (context.bubbleFlowClass.members) {
625
+ for (const member of context.bubbleFlowClass.members) {
626
+ if (ts.isMethodDeclaration(member)) {
627
+ // Skip the handle method - it's allowed to call other methods
628
+ const methodName = member.name;
629
+ if (ts.isIdentifier(methodName) && methodName.text === 'handle') {
630
+ continue;
631
+ }
632
+ methods.push(member);
633
+ }
634
+ }
635
+ }
636
+ // For each method, check if it calls other methods
637
+ for (const method of methods) {
638
+ if (!method.body || !ts.isBlock(method.body)) {
639
+ continue;
640
+ }
641
+ const methodCallErrors = findMethodCallsInNode(method.body, context.sourceFile);
642
+ errors.push(...methodCallErrors);
643
+ }
644
+ return errors;
645
+ },
646
+ };
647
+ /**
648
+ * Recursively finds all method calls (this.methodName()) in a node
649
+ */
650
+ function findMethodCallsInNode(node, sourceFile) {
651
+ const errors = [];
652
+ const visit = (n) => {
653
+ // Check for call expressions: this.methodName()
654
+ if (ts.isCallExpression(n)) {
655
+ if (ts.isPropertyAccessExpression(n.expression)) {
656
+ const object = n.expression.expression;
657
+ // Check if it's 'this' keyword (SyntaxKind.ThisKeyword)
658
+ if (object.kind === ts.SyntaxKind.ThisKeyword) {
659
+ const methodName = n.expression.name.text;
660
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(n.getStart(sourceFile));
661
+ errors.push({
662
+ line: line + 1,
663
+ column: character + 1,
664
+ message: `Method 'this.${methodName}()' cannot be called from another method. Methods should only be called from the handle method.`,
665
+ });
666
+ }
667
+ }
668
+ }
669
+ ts.forEachChild(n, visit);
670
+ };
671
+ visit(node);
672
+ return errors;
673
+ }
674
+ /**
675
+ * Lint rule that prevents usage of 'any' type
676
+ * Using 'any' bypasses TypeScript's type checking and should be avoided
677
+ */
678
+ export const noAnyTypeRule = {
679
+ name: 'no-any-type',
680
+ validate(context) {
681
+ const errors = [];
682
+ const visit = (node) => {
683
+ // Check for 'any' keyword in type nodes
684
+ if (node.kind === ts.SyntaxKind.AnyKeyword) {
685
+ const { line, character } = context.sourceFile.getLineAndCharacterOfPosition(node.getStart(context.sourceFile));
686
+ errors.push({
687
+ line: line + 1,
688
+ column: character + 1,
689
+ message: "Type 'any' is not allowed. Use a specific type, 'unknown', or a generic type parameter instead.",
690
+ });
691
+ }
692
+ ts.forEachChild(node, visit);
693
+ };
694
+ visit(context.sourceFile);
695
+ return errors;
696
+ },
697
+ };
698
+ /**
699
+ * Lint rule that prevents multiple BubbleFlow classes in a single file
700
+ * Only one class extending BubbleFlow is allowed per file for proper runtime instrumentation
701
+ */
702
+ export const singleBubbleFlowClassRule = {
703
+ name: 'single-bubbleflow-class',
704
+ validate(context) {
705
+ const errors = [];
706
+ const bubbleFlowClasses = [];
707
+ // Traverse the entire source file to find all BubbleFlow class declarations
708
+ const visit = (node) => {
709
+ if (ts.isClassDeclaration(node)) {
710
+ if (node.heritageClauses) {
711
+ for (const clause of node.heritageClauses) {
712
+ if (clause.token === ts.SyntaxKind.ExtendsKeyword) {
713
+ for (const type of clause.types) {
714
+ if (ts.isIdentifier(type.expression)) {
715
+ if (type.expression.text === 'BubbleFlow') {
716
+ const { line } = context.sourceFile.getLineAndCharacterOfPosition(node.getStart(context.sourceFile));
717
+ const className = node.name?.text || 'Anonymous';
718
+ bubbleFlowClasses.push({ name: className, line: line + 1 });
719
+ }
720
+ }
721
+ }
722
+ }
723
+ }
724
+ }
725
+ }
726
+ ts.forEachChild(node, visit);
727
+ };
728
+ visit(context.sourceFile);
729
+ // If more than one BubbleFlow class found, report errors for all except the first
730
+ if (bubbleFlowClasses.length > 1) {
731
+ for (let i = 1; i < bubbleFlowClasses.length; i++) {
732
+ const cls = bubbleFlowClasses[i];
733
+ errors.push({
734
+ line: cls.line,
735
+ message: `Multiple BubbleFlow classes are not allowed. Found '${cls.name}' but '${bubbleFlowClasses[0].name}' already extends BubbleFlow. Remove the additional class or combine the flows into a single class.`,
736
+ });
737
+ }
738
+ }
739
+ return errors;
740
+ },
741
+ };
742
+ /**
743
+ * Default registry instance with all rules registered
744
+ */
745
+ export const defaultLintRuleRegistry = new LintRuleRegistry();
746
+ defaultLintRuleRegistry.register(noThrowInHandleRule);
747
+ defaultLintRuleRegistry.register(noDirectBubbleInstantiationInHandleRule);
748
+ defaultLintRuleRegistry.register(noCredentialsParameterRule);
749
+ defaultLintRuleRegistry.register(noMethodInvocationInComplexExpressionRule);
750
+ defaultLintRuleRegistry.register(noProcessEnvRule);
751
+ defaultLintRuleRegistry.register(noMethodCallingMethodRule);
752
+ defaultLintRuleRegistry.register(noTryCatchInHandleRule);
753
+ defaultLintRuleRegistry.register(noAnyTypeRule);
754
+ defaultLintRuleRegistry.register(singleBubbleFlowClassRule);
755
+ //# sourceMappingURL=lint-rules.js.map