@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.
- package/dist/extraction/BubbleParser.d.ts +187 -8
- package/dist/extraction/BubbleParser.d.ts.map +1 -1
- package/dist/extraction/BubbleParser.js +2271 -117
- package/dist/extraction/BubbleParser.js.map +1 -1
- package/dist/extraction/index.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/injection/BubbleInjector.d.ts +27 -2
- package/dist/injection/BubbleInjector.d.ts.map +1 -1
- package/dist/injection/BubbleInjector.js +343 -35
- package/dist/injection/BubbleInjector.js.map +1 -1
- package/dist/injection/LoggerInjector.d.ts +12 -1
- package/dist/injection/LoggerInjector.d.ts.map +1 -1
- package/dist/injection/LoggerInjector.js +301 -13
- package/dist/injection/LoggerInjector.js.map +1 -1
- package/dist/injection/index.js +1 -0
- package/dist/parse/BubbleScript.d.ts +60 -3
- package/dist/parse/BubbleScript.d.ts.map +1 -1
- package/dist/parse/BubbleScript.js +133 -15
- package/dist/parse/BubbleScript.js.map +1 -1
- package/dist/parse/index.d.ts +0 -1
- package/dist/parse/index.d.ts.map +1 -1
- package/dist/parse/index.js +1 -1
- package/dist/parse/index.js.map +1 -1
- package/dist/runtime/BubbleRunner.d.ts +8 -2
- package/dist/runtime/BubbleRunner.d.ts.map +1 -1
- package/dist/runtime/BubbleRunner.js +41 -30
- package/dist/runtime/BubbleRunner.js.map +1 -1
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +1 -0
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/types.js +1 -0
- package/dist/types/index.js +1 -0
- package/dist/utils/bubble-helper.d.ts +2 -2
- package/dist/utils/bubble-helper.d.ts.map +1 -1
- package/dist/utils/bubble-helper.js +6 -1
- package/dist/utils/bubble-helper.js.map +1 -1
- package/dist/utils/normalize-control-flow.d.ts +14 -0
- package/dist/utils/normalize-control-flow.d.ts.map +1 -0
- package/dist/utils/normalize-control-flow.js +179 -0
- package/dist/utils/normalize-control-flow.js.map +1 -0
- package/dist/utils/parameter-formatter.d.ts +14 -5
- package/dist/utils/parameter-formatter.d.ts.map +1 -1
- package/dist/utils/parameter-formatter.js +164 -45
- package/dist/utils/parameter-formatter.js.map +1 -1
- package/dist/utils/sanitize-script.d.ts +11 -0
- package/dist/utils/sanitize-script.d.ts.map +1 -0
- package/dist/utils/sanitize-script.js +43 -0
- package/dist/utils/sanitize-script.js.map +1 -0
- package/dist/validation/BubbleValidator.d.ts +15 -0
- package/dist/validation/BubbleValidator.d.ts.map +1 -1
- package/dist/validation/BubbleValidator.js +168 -1
- package/dist/validation/BubbleValidator.js.map +1 -1
- package/dist/validation/index.d.ts +6 -3
- package/dist/validation/index.d.ts.map +1 -1
- package/dist/validation/index.js +33 -9
- package/dist/validation/index.js.map +1 -1
- package/dist/validation/lint-rules.d.ts +91 -0
- package/dist/validation/lint-rules.d.ts.map +1 -0
- package/dist/validation/lint-rules.js +755 -0
- package/dist/validation/lint-rules.js.map +1 -0
- package/package.json +4 -4
- package/dist/parse/traceDependencies.d.ts +0 -18
- package/dist/parse/traceDependencies.d.ts.map +0 -1
- package/dist/parse/traceDependencies.js +0 -195
- 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
|