@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
|
@@ -1,8 +1,22 @@
|
|
|
1
|
+
import { AST_NODE_TYPES } from '@typescript-eslint/typescript-estree';
|
|
1
2
|
import { buildClassNameLookup } from '../utils/bubble-helper';
|
|
2
|
-
import { BubbleParameterType } from '@bubblelab/shared-schemas';
|
|
3
|
+
import { BubbleParameterType, hashToVariableId, buildCallSiteKey, } from '@bubblelab/shared-schemas';
|
|
3
4
|
import { parseToolsParamValue } from '../utils/parameter-formatter';
|
|
4
5
|
export class BubbleParser {
|
|
5
6
|
bubbleScript;
|
|
7
|
+
cachedAST = null;
|
|
8
|
+
methodInvocationOrdinalMap = new Map();
|
|
9
|
+
invocationBubbleCloneCache = new Map();
|
|
10
|
+
/**
|
|
11
|
+
* Track which call expressions have been assigned an invocation index.
|
|
12
|
+
* Key: `methodName:startOffset` (using AST range start position)
|
|
13
|
+
* Value: the assigned invocation index
|
|
14
|
+
* This prevents double-counting when the same call site is processed multiple times
|
|
15
|
+
* (e.g., once in .map() callback processing, again in Promise.all resolution)
|
|
16
|
+
*/
|
|
17
|
+
processedCallSiteIndexes = new Map();
|
|
18
|
+
/** Custom tool func ranges for marking bubbles inside custom tools */
|
|
19
|
+
customToolFuncs = [];
|
|
6
20
|
constructor(bubbleScript) {
|
|
7
21
|
this.bubbleScript = bubbleScript;
|
|
8
22
|
}
|
|
@@ -17,13 +31,35 @@ export class BubbleParser {
|
|
|
17
31
|
}
|
|
18
32
|
const nodes = {};
|
|
19
33
|
const errors = [];
|
|
20
|
-
// Find
|
|
21
|
-
const
|
|
34
|
+
// Find main BubbleFlow class and all its instance methods
|
|
35
|
+
const mainClass = this.findMainBubbleFlowClass(ast);
|
|
36
|
+
const instanceMethodsLocation = {};
|
|
37
|
+
if (mainClass) {
|
|
38
|
+
const methods = this.findAllInstanceMethods(mainClass);
|
|
39
|
+
const methodNames = methods.map((m) => m.methodName);
|
|
40
|
+
const invocations = this.findMethodInvocations(ast, methodNames);
|
|
41
|
+
// Combine method locations with invocation lines
|
|
42
|
+
for (const method of methods) {
|
|
43
|
+
instanceMethodsLocation[method.methodName] = {
|
|
44
|
+
startLine: method.startLine,
|
|
45
|
+
endLine: method.endLine,
|
|
46
|
+
definitionStartLine: method.definitionStartLine,
|
|
47
|
+
bodyStartLine: method.bodyStartLine,
|
|
48
|
+
invocationLines: invocations[method.methodName] || [],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// Clear custom tool func tracking from previous runs
|
|
53
|
+
this.customToolFuncs = [];
|
|
22
54
|
// Visit AST nodes to find bubble instantiations
|
|
23
55
|
this.visitNode(ast, nodes, classNameToInfo, scopeManager);
|
|
24
56
|
if (errors.length > 0) {
|
|
25
57
|
throw new Error(`Failed to trace bubble dependencies: ${errors.join(', ')}`);
|
|
26
58
|
}
|
|
59
|
+
// Find custom tools in ai-agent bubbles and populate customToolFuncs
|
|
60
|
+
this.findCustomToolsInAIAgentBubbles(ast, nodes, classNameToInfo);
|
|
61
|
+
// Mark bubbles that are inside custom tool funcs
|
|
62
|
+
this.markBubblesInsideCustomTools(nodes);
|
|
27
63
|
// Build a set of used variable IDs to ensure uniqueness for any synthetic IDs we allocate
|
|
28
64
|
const usedVariableIds = new Set();
|
|
29
65
|
for (const [idStr, node] of Object.entries(nodes)) {
|
|
@@ -59,10 +95,69 @@ export class BubbleParser {
|
|
|
59
95
|
ordinalCounters, usedVariableIds, bubble.variableId, // Root variable id mirrors the parsed bubble's variable id
|
|
60
96
|
true, // suppress adding self segment for root
|
|
61
97
|
bubble.variableName);
|
|
98
|
+
// Add functionCallChildren for ai-agent bubbles with custom tools
|
|
99
|
+
if (bubble.bubbleName === 'ai-agent' &&
|
|
100
|
+
bubble.dependencyGraph &&
|
|
101
|
+
this.customToolFuncs.length > 0) {
|
|
102
|
+
const toolsForThisBubble = this.customToolFuncs.filter((t) => t.parentBubbleVariableId === bubble.variableId);
|
|
103
|
+
if (toolsForThisBubble.length > 0) {
|
|
104
|
+
const functionCallChildren = [];
|
|
105
|
+
for (const toolFunc of toolsForThisBubble) {
|
|
106
|
+
// Find all bubbles that are inside this custom tool func
|
|
107
|
+
const childBubbleNodes = [];
|
|
108
|
+
for (const [, bubbleInfo] of Object.entries(nodes)) {
|
|
109
|
+
if (bubbleInfo.containingCustomToolId ===
|
|
110
|
+
`${toolFunc.parentBubbleVariableId}.${toolFunc.toolName}`) {
|
|
111
|
+
childBubbleNodes.push({
|
|
112
|
+
type: 'bubble',
|
|
113
|
+
variableId: bubbleInfo.variableId,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// Create FunctionCallWorkflowNode for this custom tool
|
|
118
|
+
const funcCallNode = {
|
|
119
|
+
type: 'function_call',
|
|
120
|
+
functionName: toolFunc.toolName,
|
|
121
|
+
description: toolFunc.description,
|
|
122
|
+
isMethodCall: false,
|
|
123
|
+
code: `customTool:${toolFunc.toolName}`,
|
|
124
|
+
variableId: hashToVariableId(`${bubble.variableId}.customTool.${toolFunc.toolName}`),
|
|
125
|
+
location: {
|
|
126
|
+
startLine: toolFunc.startLine,
|
|
127
|
+
startCol: toolFunc.startCol,
|
|
128
|
+
endLine: toolFunc.endLine,
|
|
129
|
+
endCol: toolFunc.endCol,
|
|
130
|
+
},
|
|
131
|
+
methodDefinition: {
|
|
132
|
+
location: {
|
|
133
|
+
startLine: toolFunc.startLine,
|
|
134
|
+
endLine: toolFunc.endLine,
|
|
135
|
+
},
|
|
136
|
+
isAsync: toolFunc.isAsync,
|
|
137
|
+
parameters: ['input'], // Custom tools always have 'input' parameter
|
|
138
|
+
},
|
|
139
|
+
children: childBubbleNodes,
|
|
140
|
+
};
|
|
141
|
+
functionCallChildren.push(funcCallNode);
|
|
142
|
+
}
|
|
143
|
+
bubble.dependencyGraph.functionCallChildren = functionCallChildren;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// Store AST for method definition lookup
|
|
148
|
+
this.cachedAST = ast;
|
|
149
|
+
this.methodInvocationOrdinalMap.clear();
|
|
150
|
+
this.invocationBubbleCloneCache.clear();
|
|
151
|
+
this.processedCallSiteIndexes.clear();
|
|
152
|
+
// Build hierarchical workflow structure
|
|
153
|
+
const workflow = this.buildWorkflowTree(ast, nodes, scopeManager);
|
|
154
|
+
for (const clone of this.invocationBubbleCloneCache.values()) {
|
|
155
|
+
nodes[clone.variableId] = clone;
|
|
62
156
|
}
|
|
63
157
|
return {
|
|
64
158
|
bubbles: nodes,
|
|
65
|
-
|
|
159
|
+
workflow,
|
|
160
|
+
instanceMethodsLocation,
|
|
66
161
|
};
|
|
67
162
|
}
|
|
68
163
|
findDependenciesForBubble(currentDependencies, bubbleFactory, parameters, seen = new Set()) {
|
|
@@ -143,7 +238,7 @@ export class BubbleParser {
|
|
|
143
238
|
: `${bubbleName}#${nextOrdinal}`;
|
|
144
239
|
const variableId = typeof explicitVariableId === 'number'
|
|
145
240
|
? explicitVariableId
|
|
146
|
-
:
|
|
241
|
+
: hashToVariableId(uniqueId);
|
|
147
242
|
const metadata = bubbleFactory.getMetadata(bubbleName);
|
|
148
243
|
if (seen.has(bubbleName)) {
|
|
149
244
|
return {
|
|
@@ -180,7 +275,7 @@ export class BubbleParser {
|
|
|
180
275
|
const aiOrdinal = (ordinalCounters.get(aiCountKey) || 0) + 1;
|
|
181
276
|
ordinalCounters.set(aiCountKey, aiOrdinal);
|
|
182
277
|
const aiAgentUniqueId = `${uniqueId}.ai-agent#${aiOrdinal}`;
|
|
183
|
-
const aiAgentVarId =
|
|
278
|
+
const aiAgentVarId = hashToVariableId(aiAgentUniqueId);
|
|
184
279
|
const toolChildren = [];
|
|
185
280
|
for (const toolName of toolsForChild) {
|
|
186
281
|
toolChildren.push(this.buildDependencyGraph(toolName, bubbleFactory, nextSeen, undefined, aiAgentUniqueId, ordinalCounters, usedVariableIds, undefined, false, toolName));
|
|
@@ -225,17 +320,6 @@ export class BubbleParser {
|
|
|
225
320
|
};
|
|
226
321
|
return nodeObj;
|
|
227
322
|
}
|
|
228
|
-
// Deterministic non-negative integer ID from uniqueId string
|
|
229
|
-
hashUniqueIdToVarId(input) {
|
|
230
|
-
let hash = 2166136261; // FNV-1a 32-bit offset basis
|
|
231
|
-
for (let i = 0; i < input.length; i++) {
|
|
232
|
-
hash ^= input.charCodeAt(i);
|
|
233
|
-
hash = (hash * 16777619) >>> 0; // unsigned 32-bit
|
|
234
|
-
}
|
|
235
|
-
// Map to 6-digit range to avoid colliding with small AST ids while readable
|
|
236
|
-
const mapped = 100000 + (hash % 900000);
|
|
237
|
-
return mapped;
|
|
238
|
-
}
|
|
239
323
|
/**
|
|
240
324
|
* Build a JSON Schema object for the payload parameter of the top-level `handle` entrypoint.
|
|
241
325
|
* Supports primitives, arrays, unions (anyOf), intersections (allOf), type literals, and
|
|
@@ -549,8 +633,21 @@ export class BubbleParser {
|
|
|
549
633
|
if (!keyName)
|
|
550
634
|
continue;
|
|
551
635
|
const propSchema = m.typeAnnotation
|
|
552
|
-
? this.tsTypeToJsonSchema(m.typeAnnotation.typeAnnotation, ast)
|
|
636
|
+
? this.tsTypeToJsonSchema(m.typeAnnotation.typeAnnotation, ast) || {}
|
|
553
637
|
: {};
|
|
638
|
+
// Extract comment/description and JSDoc tags for this property
|
|
639
|
+
const jsDocInfo = this.extractJSDocForNode(m);
|
|
640
|
+
if (jsDocInfo.description) {
|
|
641
|
+
propSchema.description = jsDocInfo.description;
|
|
642
|
+
}
|
|
643
|
+
// Add canBeFile flag to schema if explicitly specified in JSDoc
|
|
644
|
+
if (jsDocInfo.canBeFile !== undefined) {
|
|
645
|
+
propSchema.canBeFile = jsDocInfo.canBeFile;
|
|
646
|
+
}
|
|
647
|
+
// Add canBeGoogleFile flag to schema if explicitly specified in JSDoc
|
|
648
|
+
if (jsDocInfo.canBeGoogleFile !== undefined) {
|
|
649
|
+
propSchema.canBeGoogleFile = jsDocInfo.canBeGoogleFile;
|
|
650
|
+
}
|
|
554
651
|
properties[keyName] = propSchema;
|
|
555
652
|
if (!m.optional)
|
|
556
653
|
required.push(keyName);
|
|
@@ -586,15 +683,6 @@ export class BubbleParser {
|
|
|
586
683
|
},
|
|
587
684
|
};
|
|
588
685
|
}
|
|
589
|
-
if (eventKey === 'gmail/email_received') {
|
|
590
|
-
return {
|
|
591
|
-
type: 'object',
|
|
592
|
-
properties: {
|
|
593
|
-
email: { type: 'string' },
|
|
594
|
-
},
|
|
595
|
-
required: ['email'],
|
|
596
|
-
};
|
|
597
|
-
}
|
|
598
686
|
if (eventKey === 'schedule/cron') {
|
|
599
687
|
return {
|
|
600
688
|
type: 'object',
|
|
@@ -641,122 +729,411 @@ export class BubbleParser {
|
|
|
641
729
|
return null;
|
|
642
730
|
}
|
|
643
731
|
/**
|
|
644
|
-
* Find the
|
|
732
|
+
* Find the main class that extends BubbleFlow
|
|
645
733
|
*/
|
|
646
|
-
|
|
734
|
+
findMainBubbleFlowClass(ast) {
|
|
647
735
|
for (const statement of ast.body) {
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
statement.id?.name === 'handle') {
|
|
651
|
-
const definitionStart = statement.loc?.start.line || -1;
|
|
652
|
-
const bodyStart = statement.body?.loc?.start.line || definitionStart;
|
|
653
|
-
return {
|
|
654
|
-
startLine: definitionStart,
|
|
655
|
-
endLine: statement.loc?.end.line || -1,
|
|
656
|
-
definitionStartLine: definitionStart,
|
|
657
|
-
bodyStartLine: bodyStart,
|
|
658
|
-
};
|
|
659
|
-
}
|
|
660
|
-
// Look for exported function declarations: export function handle() {}
|
|
736
|
+
let classDecl = null;
|
|
737
|
+
// Check exported class declarations
|
|
661
738
|
if (statement.type === 'ExportNamedDeclaration' &&
|
|
662
|
-
statement.declaration?.type === '
|
|
663
|
-
statement.declaration
|
|
664
|
-
const decl = statement.declaration;
|
|
665
|
-
const definitionStart = decl.loc?.start.line || -1;
|
|
666
|
-
const bodyStart = decl.body?.loc?.start.line || definitionStart;
|
|
667
|
-
return {
|
|
668
|
-
startLine: definitionStart,
|
|
669
|
-
endLine: decl.loc?.end.line || -1,
|
|
670
|
-
definitionStartLine: definitionStart,
|
|
671
|
-
bodyStartLine: bodyStart,
|
|
672
|
-
};
|
|
739
|
+
statement.declaration?.type === 'ClassDeclaration') {
|
|
740
|
+
classDecl = statement.declaration;
|
|
673
741
|
}
|
|
674
|
-
//
|
|
675
|
-
if (statement.type === '
|
|
676
|
-
|
|
677
|
-
if (declarator.type === 'VariableDeclarator' &&
|
|
678
|
-
declarator.id.type === 'Identifier' &&
|
|
679
|
-
declarator.id.name === 'handle' &&
|
|
680
|
-
(declarator.init?.type === 'FunctionExpression' ||
|
|
681
|
-
declarator.init?.type === 'ArrowFunctionExpression')) {
|
|
682
|
-
const init = declarator.init;
|
|
683
|
-
const definitionStart = declarator.loc?.start.line || -1;
|
|
684
|
-
const bodyStart = init.type === 'FunctionExpression'
|
|
685
|
-
? init.body?.loc?.start.line || definitionStart
|
|
686
|
-
: init.body?.loc?.start.line || definitionStart;
|
|
687
|
-
return {
|
|
688
|
-
startLine: definitionStart,
|
|
689
|
-
endLine: init.loc?.end.line || -1,
|
|
690
|
-
definitionStartLine: definitionStart,
|
|
691
|
-
bodyStartLine: bodyStart,
|
|
692
|
-
};
|
|
693
|
-
}
|
|
694
|
-
}
|
|
742
|
+
// Check non-exported class declarations
|
|
743
|
+
else if (statement.type === 'ClassDeclaration') {
|
|
744
|
+
classDecl = statement;
|
|
695
745
|
}
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
};
|
|
746
|
+
if (classDecl) {
|
|
747
|
+
// Check if this class extends BubbleFlow
|
|
748
|
+
if (classDecl.superClass) {
|
|
749
|
+
const superClass = classDecl.superClass;
|
|
750
|
+
// Handle simple identifier like extends BubbleFlow
|
|
751
|
+
if (superClass.type === 'Identifier' &&
|
|
752
|
+
superClass.name === 'BubbleFlow') {
|
|
753
|
+
return classDecl;
|
|
754
|
+
}
|
|
755
|
+
// Handle generic type like BubbleFlow<'webhook/http'>
|
|
756
|
+
// Check if it's a TSTypeReference with type parameters
|
|
757
|
+
// Use type assertion since TSESTree types may not fully expose this
|
|
758
|
+
if (superClass.type === 'TSTypeReference') {
|
|
759
|
+
const typeName = superClass.typeName;
|
|
760
|
+
if (typeName &&
|
|
761
|
+
typeName.type === 'Identifier' &&
|
|
762
|
+
typeName.name === 'BubbleFlow') {
|
|
763
|
+
return classDecl;
|
|
764
|
+
}
|
|
716
765
|
}
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
// Look for exported class declarations with handle method
|
|
720
|
-
if (statement.type === 'ExportNamedDeclaration' &&
|
|
721
|
-
statement.declaration?.type === 'ClassDeclaration') {
|
|
722
|
-
const handleMethod = this.findHandleMethodInClass(statement.declaration);
|
|
723
|
-
if (handleMethod) {
|
|
724
|
-
return handleMethod;
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
// Look for class declarations with handle method
|
|
728
|
-
if (statement.type === 'ClassDeclaration') {
|
|
729
|
-
const handleMethod = this.findHandleMethodInClass(statement);
|
|
730
|
-
if (handleMethod) {
|
|
731
|
-
return handleMethod;
|
|
732
766
|
}
|
|
733
767
|
}
|
|
734
768
|
}
|
|
735
|
-
return null;
|
|
769
|
+
return null;
|
|
736
770
|
}
|
|
737
771
|
/**
|
|
738
|
-
*
|
|
772
|
+
* Extract all instance methods from a class
|
|
739
773
|
*/
|
|
740
|
-
|
|
774
|
+
findAllInstanceMethods(classDeclaration) {
|
|
775
|
+
const methods = [];
|
|
741
776
|
if (!classDeclaration.body)
|
|
742
|
-
return
|
|
777
|
+
return methods;
|
|
743
778
|
for (const member of classDeclaration.body.body) {
|
|
779
|
+
// Only process instance methods (not static, not getters/setters)
|
|
744
780
|
if (member.type === 'MethodDefinition' &&
|
|
781
|
+
!member.static &&
|
|
782
|
+
member.kind === 'method' &&
|
|
745
783
|
member.key.type === 'Identifier' &&
|
|
746
|
-
member.key.name === 'handle' &&
|
|
747
784
|
member.value.type === 'FunctionExpression') {
|
|
785
|
+
const methodName = member.key.name;
|
|
748
786
|
const definitionStart = member.loc?.start.line || -1;
|
|
749
787
|
const bodyStart = member.value.body?.loc?.start.line || definitionStart;
|
|
750
788
|
const definitionEnd = member.loc?.end.line || -1;
|
|
751
|
-
|
|
789
|
+
methods.push({
|
|
790
|
+
methodName,
|
|
752
791
|
startLine: definitionStart,
|
|
753
792
|
endLine: definitionEnd,
|
|
754
793
|
definitionStartLine: definitionStart,
|
|
755
794
|
bodyStartLine: bodyStart,
|
|
756
|
-
};
|
|
795
|
+
});
|
|
757
796
|
}
|
|
758
797
|
}
|
|
759
|
-
return
|
|
798
|
+
return methods;
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* Find all method invocations in the AST with full details
|
|
802
|
+
*/
|
|
803
|
+
findMethodInvocations(ast, methodNames) {
|
|
804
|
+
const invocations = {};
|
|
805
|
+
const methodNameSet = new Set(methodNames);
|
|
806
|
+
const visitedNodes = new WeakSet();
|
|
807
|
+
const parentMap = new WeakMap();
|
|
808
|
+
const invocationCounters = new Map();
|
|
809
|
+
// Initialize invocations map
|
|
810
|
+
for (const methodName of methodNames) {
|
|
811
|
+
invocations[methodName] = [];
|
|
812
|
+
}
|
|
813
|
+
// First pass: Build parent map
|
|
814
|
+
const buildParentMap = (node, parent) => {
|
|
815
|
+
if (parent) {
|
|
816
|
+
parentMap.set(node, parent);
|
|
817
|
+
}
|
|
818
|
+
// Visit children
|
|
819
|
+
const visitValue = (value) => {
|
|
820
|
+
if (value && typeof value === 'object') {
|
|
821
|
+
if (Array.isArray(value)) {
|
|
822
|
+
value.forEach(visitValue);
|
|
823
|
+
}
|
|
824
|
+
else if ('type' in value && typeof value.type === 'string') {
|
|
825
|
+
buildParentMap(value, node);
|
|
826
|
+
}
|
|
827
|
+
else {
|
|
828
|
+
Object.values(value).forEach(visitValue);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
};
|
|
832
|
+
const nodeObj = node;
|
|
833
|
+
for (const [key, value] of Object.entries(nodeObj)) {
|
|
834
|
+
if (key === 'parent' || key === 'loc' || key === 'range') {
|
|
835
|
+
continue;
|
|
836
|
+
}
|
|
837
|
+
visitValue(value);
|
|
838
|
+
}
|
|
839
|
+
};
|
|
840
|
+
buildParentMap(ast);
|
|
841
|
+
const isPromiseAllArrayElement = (arrayNode, elementNode) => {
|
|
842
|
+
if (!elementNode || !arrayNode.elements.includes(elementNode)) {
|
|
843
|
+
return false;
|
|
844
|
+
}
|
|
845
|
+
const parentCall = parentMap.get(arrayNode);
|
|
846
|
+
if (!parentCall || parentCall.type !== 'CallExpression') {
|
|
847
|
+
return false;
|
|
848
|
+
}
|
|
849
|
+
const callee = parentCall.callee;
|
|
850
|
+
if (callee.type !== 'MemberExpression' ||
|
|
851
|
+
callee.object.type !== 'Identifier' ||
|
|
852
|
+
callee.object.name !== 'Promise' ||
|
|
853
|
+
callee.property.type !== 'Identifier' ||
|
|
854
|
+
callee.property.name !== 'all') {
|
|
855
|
+
return false;
|
|
856
|
+
}
|
|
857
|
+
return (parentCall.arguments.length > 0 && parentCall.arguments[0] === arrayNode);
|
|
858
|
+
};
|
|
859
|
+
const visitNode = (node, parent) => {
|
|
860
|
+
// Skip if already visited
|
|
861
|
+
if (visitedNodes.has(node)) {
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
visitedNodes.add(node);
|
|
865
|
+
// Look for CallExpression nodes
|
|
866
|
+
if (node.type === 'CallExpression') {
|
|
867
|
+
const callee = node.callee;
|
|
868
|
+
// Check if it's a method call: this.methodName()
|
|
869
|
+
if (callee.type === 'MemberExpression') {
|
|
870
|
+
const object = callee.object;
|
|
871
|
+
const property = callee.property;
|
|
872
|
+
if (object.type === 'ThisExpression' &&
|
|
873
|
+
property.type === 'Identifier' &&
|
|
874
|
+
methodNameSet.has(property.name)) {
|
|
875
|
+
const methodName = property.name;
|
|
876
|
+
const lineNumber = node.loc?.start.line;
|
|
877
|
+
const endLineNumber = node.loc?.end.line;
|
|
878
|
+
const columnNumber = node.loc?.start.column ?? -1;
|
|
879
|
+
if (!lineNumber || !endLineNumber)
|
|
880
|
+
return;
|
|
881
|
+
// Extract arguments
|
|
882
|
+
const args = node.arguments
|
|
883
|
+
.map((arg) => this.bubbleScript.substring(arg.range[0], arg.range[1]))
|
|
884
|
+
.join(', ');
|
|
885
|
+
// Determine statement type by looking at parent context
|
|
886
|
+
let statementType = 'simple';
|
|
887
|
+
let variableName;
|
|
888
|
+
let variableType;
|
|
889
|
+
let destructuringPattern;
|
|
890
|
+
let hasAwait = false;
|
|
891
|
+
// For condition_expression: track the containing statement info
|
|
892
|
+
let containingStatementLine;
|
|
893
|
+
let callRange;
|
|
894
|
+
let callText;
|
|
895
|
+
// Check if parent is AwaitExpression
|
|
896
|
+
if (parent?.type === 'AwaitExpression') {
|
|
897
|
+
hasAwait = true;
|
|
898
|
+
}
|
|
899
|
+
// Find the statement containing this call using parent map
|
|
900
|
+
// We need to check parent chain to identify:
|
|
901
|
+
// 1. VariableDeclarator -> VariableDeclaration (for const/let/var x = ...)
|
|
902
|
+
// 2. AssignmentExpression (for x = ...)
|
|
903
|
+
// 3. ReturnStatement (for return ...)
|
|
904
|
+
// Also check for complex expressions that should not be instrumented
|
|
905
|
+
let currentParent = parentMap.get(node);
|
|
906
|
+
let isInComplexExpression = false;
|
|
907
|
+
let invocationContext = 'default';
|
|
908
|
+
let currentChild = node;
|
|
909
|
+
while (currentParent) {
|
|
910
|
+
const parentIsPromiseAllElement = currentParent.type === 'ArrayExpression' &&
|
|
911
|
+
isPromiseAllArrayElement(currentParent, currentChild);
|
|
912
|
+
// Check if we're inside a complex expression before reaching the statement level
|
|
913
|
+
// These expressions cannot be instrumented inline without breaking syntax
|
|
914
|
+
if (currentParent.type === 'ConditionalExpression' || // Ternary: a ? b : c
|
|
915
|
+
currentParent.type === 'ObjectExpression' || // Object literal: { key: value }
|
|
916
|
+
(currentParent.type === 'ArrayExpression' &&
|
|
917
|
+
!parentIsPromiseAllElement) || // Array literal outside Promise.all
|
|
918
|
+
currentParent.type === 'Property' || // Object property
|
|
919
|
+
currentParent.type === 'SpreadElement' // Spread: ...expr
|
|
920
|
+
) {
|
|
921
|
+
isInComplexExpression = true;
|
|
922
|
+
break;
|
|
923
|
+
}
|
|
924
|
+
// Check if we're inside an arrow function expression body (no braces)
|
|
925
|
+
// e.g., arr.map((x) => this.method(x)) - the body is just an expression
|
|
926
|
+
// These need to be wrapped in an async IIFE, similar to promise_all_element
|
|
927
|
+
if (currentParent.type === 'ArrowFunctionExpression' &&
|
|
928
|
+
currentChild === currentParent.body &&
|
|
929
|
+
currentParent.body.type !== 'BlockStatement') {
|
|
930
|
+
invocationContext = 'promise_all_element';
|
|
931
|
+
// Capture call text for proper replacement
|
|
932
|
+
const callNode = hasAwait ? parent : node;
|
|
933
|
+
if (callNode?.range) {
|
|
934
|
+
callRange = {
|
|
935
|
+
start: callNode.range[0],
|
|
936
|
+
end: callNode.range[1],
|
|
937
|
+
};
|
|
938
|
+
callText = this.bubbleScript.substring(callRange.start, callRange.end);
|
|
939
|
+
}
|
|
940
|
+
// Don't break - continue to find the outer statement for line info
|
|
941
|
+
}
|
|
942
|
+
// Check if we're inside the condition/test part of a control flow statement
|
|
943
|
+
// These need special handling - extract call before the statement and replace in-place
|
|
944
|
+
// IMPORTANT: Only treat as condition_expression if the call is in the test/condition,
|
|
945
|
+
// not in the body (consequent/alternate/etc)
|
|
946
|
+
if (currentParent.type === 'IfStatement' ||
|
|
947
|
+
currentParent.type === 'WhileStatement' ||
|
|
948
|
+
currentParent.type === 'DoWhileStatement' ||
|
|
949
|
+
currentParent.type === 'ForStatement' ||
|
|
950
|
+
currentParent.type === 'SwitchStatement') {
|
|
951
|
+
// Check if currentChild is actually in the test/condition part
|
|
952
|
+
const isInCondition = this.isNodeInConditionPart(currentParent, currentChild);
|
|
953
|
+
if (isInCondition) {
|
|
954
|
+
statementType = 'condition_expression';
|
|
955
|
+
containingStatementLine = currentParent.loc?.start.line;
|
|
956
|
+
// Capture the call range - include await if present
|
|
957
|
+
const callNode = hasAwait ? parent : node;
|
|
958
|
+
if (callNode?.range) {
|
|
959
|
+
callRange = {
|
|
960
|
+
start: callNode.range[0],
|
|
961
|
+
end: callNode.range[1],
|
|
962
|
+
};
|
|
963
|
+
callText = this.bubbleScript.substring(callRange.start, callRange.end);
|
|
964
|
+
}
|
|
965
|
+
break;
|
|
966
|
+
}
|
|
967
|
+
// If not in condition part, continue walking up - the call is in the body
|
|
968
|
+
// and should be treated as a normal statement
|
|
969
|
+
}
|
|
970
|
+
if (parentIsPromiseAllElement) {
|
|
971
|
+
invocationContext = 'promise_all_element';
|
|
972
|
+
}
|
|
973
|
+
// Check if we're nested inside another CallExpression (e.g., arr.push(this.method()))
|
|
974
|
+
// This needs special handling similar to condition_expression - extract call before
|
|
975
|
+
// the statement and replace the call text inline
|
|
976
|
+
if (currentParent.type === 'CallExpression' &&
|
|
977
|
+
currentParent !== node) {
|
|
978
|
+
// This is a nested call - the tracked method is an argument to another call
|
|
979
|
+
// Find the containing ExpressionStatement line
|
|
980
|
+
let stmtParent = currentParent;
|
|
981
|
+
while (stmtParent &&
|
|
982
|
+
stmtParent.type !== 'ExpressionStatement') {
|
|
983
|
+
stmtParent = parentMap.get(stmtParent);
|
|
984
|
+
}
|
|
985
|
+
if (stmtParent?.type === 'ExpressionStatement') {
|
|
986
|
+
statementType = 'nested_call_expression';
|
|
987
|
+
containingStatementLine = stmtParent.loc?.start.line;
|
|
988
|
+
// Capture the call range - include await if present
|
|
989
|
+
const callNode = hasAwait ? parent : node;
|
|
990
|
+
if (callNode?.range) {
|
|
991
|
+
callRange = {
|
|
992
|
+
start: callNode.range[0],
|
|
993
|
+
end: callNode.range[1],
|
|
994
|
+
};
|
|
995
|
+
callText = this.bubbleScript.substring(callRange.start, callRange.end);
|
|
996
|
+
}
|
|
997
|
+
break;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
if (currentParent.type === 'VariableDeclarator') {
|
|
1001
|
+
statementType = 'variable_declaration';
|
|
1002
|
+
if (currentParent.id.type === 'Identifier') {
|
|
1003
|
+
variableName = currentParent.id.name;
|
|
1004
|
+
}
|
|
1005
|
+
else if (currentParent.id.type === 'ObjectPattern' ||
|
|
1006
|
+
currentParent.id.type === 'ArrayPattern') {
|
|
1007
|
+
// Extract the destructuring pattern from the source
|
|
1008
|
+
const declaratorNode = currentParent;
|
|
1009
|
+
const patternRange = declaratorNode.id.range;
|
|
1010
|
+
if (patternRange) {
|
|
1011
|
+
destructuringPattern = this.bubbleScript.substring(patternRange[0], patternRange[1]);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
// Continue to find the VariableDeclaration parent to get const/let/var
|
|
1015
|
+
}
|
|
1016
|
+
else if (currentParent.type === 'VariableDeclaration') {
|
|
1017
|
+
// This should only be reached if we found VariableDeclarator first
|
|
1018
|
+
if (statementType === 'variable_declaration') {
|
|
1019
|
+
variableType = currentParent.kind;
|
|
1020
|
+
break;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
else if (currentParent.type === 'AssignmentExpression') {
|
|
1024
|
+
statementType = 'assignment';
|
|
1025
|
+
if (currentParent.left.type === 'Identifier') {
|
|
1026
|
+
variableName = currentParent.left.name;
|
|
1027
|
+
}
|
|
1028
|
+
break;
|
|
1029
|
+
}
|
|
1030
|
+
else if (currentParent.type === 'ReturnStatement') {
|
|
1031
|
+
statementType = 'return';
|
|
1032
|
+
break;
|
|
1033
|
+
}
|
|
1034
|
+
// Move up the tree using parent map
|
|
1035
|
+
currentChild = currentParent;
|
|
1036
|
+
currentParent = parentMap.get(currentParent);
|
|
1037
|
+
}
|
|
1038
|
+
// Skip this invocation if it's inside a complex expression
|
|
1039
|
+
// Instrumenting these would break the syntax
|
|
1040
|
+
if (isInComplexExpression) {
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
const invocationIndex = (invocationCounters.get(methodName) ?? 0) + 1;
|
|
1044
|
+
invocationCounters.set(methodName, invocationIndex);
|
|
1045
|
+
invocations[methodName].push({
|
|
1046
|
+
lineNumber,
|
|
1047
|
+
endLineNumber,
|
|
1048
|
+
columnNumber,
|
|
1049
|
+
invocationIndex,
|
|
1050
|
+
hasAwait,
|
|
1051
|
+
arguments: args,
|
|
1052
|
+
statementType,
|
|
1053
|
+
variableName,
|
|
1054
|
+
variableType,
|
|
1055
|
+
destructuringPattern,
|
|
1056
|
+
context: invocationContext,
|
|
1057
|
+
containingStatementLine,
|
|
1058
|
+
callRange,
|
|
1059
|
+
callText,
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
// Check for await expressions - pass current node as parent
|
|
1065
|
+
if (node.type === 'AwaitExpression' && node.argument) {
|
|
1066
|
+
visitNode(node.argument, node);
|
|
1067
|
+
}
|
|
1068
|
+
// Recursively visit child nodes with parent context
|
|
1069
|
+
this.visitChildNodesForInvocations(node, (child) => visitNode(child, node));
|
|
1070
|
+
};
|
|
1071
|
+
visitNode(ast);
|
|
1072
|
+
return invocations;
|
|
1073
|
+
}
|
|
1074
|
+
/**
|
|
1075
|
+
* Check if a child node is in the condition/test part of a control flow statement
|
|
1076
|
+
* Returns true if the child is the test/discriminant expression, false if it's in the body
|
|
1077
|
+
*/
|
|
1078
|
+
isNodeInConditionPart(controlFlowNode, childNode) {
|
|
1079
|
+
if (!childNode)
|
|
1080
|
+
return false;
|
|
1081
|
+
switch (controlFlowNode.type) {
|
|
1082
|
+
case 'IfStatement': {
|
|
1083
|
+
const ifStmt = controlFlowNode;
|
|
1084
|
+
return childNode === ifStmt.test;
|
|
1085
|
+
}
|
|
1086
|
+
case 'WhileStatement': {
|
|
1087
|
+
const whileStmt = controlFlowNode;
|
|
1088
|
+
return childNode === whileStmt.test;
|
|
1089
|
+
}
|
|
1090
|
+
case 'DoWhileStatement': {
|
|
1091
|
+
const doWhileStmt = controlFlowNode;
|
|
1092
|
+
return childNode === doWhileStmt.test;
|
|
1093
|
+
}
|
|
1094
|
+
case 'ForStatement': {
|
|
1095
|
+
const forStmt = controlFlowNode;
|
|
1096
|
+
// ForStatement has init, test, and update - all are condition-like
|
|
1097
|
+
return (childNode === forStmt.init ||
|
|
1098
|
+
childNode === forStmt.test ||
|
|
1099
|
+
childNode === forStmt.update);
|
|
1100
|
+
}
|
|
1101
|
+
case 'SwitchStatement': {
|
|
1102
|
+
const switchStmt = controlFlowNode;
|
|
1103
|
+
return childNode === switchStmt.discriminant;
|
|
1104
|
+
}
|
|
1105
|
+
default:
|
|
1106
|
+
return false;
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
/**
|
|
1110
|
+
* Helper to recursively visit child nodes for finding invocations
|
|
1111
|
+
*/
|
|
1112
|
+
visitChildNodesForInvocations(node, visitor) {
|
|
1113
|
+
const visitValue = (value) => {
|
|
1114
|
+
if (value && typeof value === 'object') {
|
|
1115
|
+
if (Array.isArray(value)) {
|
|
1116
|
+
value.forEach(visitValue);
|
|
1117
|
+
}
|
|
1118
|
+
else if ('type' in value && typeof value.type === 'string') {
|
|
1119
|
+
// This is likely an AST node
|
|
1120
|
+
visitor(value);
|
|
1121
|
+
}
|
|
1122
|
+
else {
|
|
1123
|
+
// This is a regular object, recurse into its properties
|
|
1124
|
+
Object.values(value).forEach(visitValue);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
};
|
|
1128
|
+
// Get all property values of the node, excluding metadata properties
|
|
1129
|
+
const nodeObj = node;
|
|
1130
|
+
for (const [key, value] of Object.entries(nodeObj)) {
|
|
1131
|
+
// Skip metadata properties that aren't part of the AST structure
|
|
1132
|
+
if (key === 'parent' || key === 'loc' || key === 'range') {
|
|
1133
|
+
continue;
|
|
1134
|
+
}
|
|
1135
|
+
visitValue(value);
|
|
1136
|
+
}
|
|
760
1137
|
}
|
|
761
1138
|
/**
|
|
762
1139
|
* Recursively visit AST nodes to find bubble instantiations
|
|
@@ -772,6 +1149,11 @@ export class BubbleParser {
|
|
|
772
1149
|
const bubbleNode = this.extractBubbleFromExpression(declarator.init, classNameLookup);
|
|
773
1150
|
if (bubbleNode) {
|
|
774
1151
|
bubbleNode.variableName = nameText;
|
|
1152
|
+
// Extract comment for this bubble node
|
|
1153
|
+
const description = this.extractCommentForNode(node);
|
|
1154
|
+
if (description) {
|
|
1155
|
+
bubbleNode.description = description;
|
|
1156
|
+
}
|
|
775
1157
|
// Find the Variable object for this bubble declaration
|
|
776
1158
|
const variable = this.findVariableForBubble(nameText, node, scopeManager);
|
|
777
1159
|
if (variable) {
|
|
@@ -794,6 +1176,11 @@ export class BubbleParser {
|
|
|
794
1176
|
if (bubbleNode) {
|
|
795
1177
|
const synthetic = `_anonymous_${bubbleNode.className}_${Object.keys(nodes).length}`;
|
|
796
1178
|
bubbleNode.variableName = synthetic;
|
|
1179
|
+
// Extract comment for this bubble node
|
|
1180
|
+
const description = this.extractCommentForNode(node);
|
|
1181
|
+
if (description) {
|
|
1182
|
+
bubbleNode.description = description;
|
|
1183
|
+
}
|
|
797
1184
|
// For anonymous bubbles, use negative synthetic ID (no Variable object exists)
|
|
798
1185
|
const syntheticId = -1 * (Object.keys(nodes).length + 1);
|
|
799
1186
|
bubbleNode.variableId = syntheticId;
|
|
@@ -822,6 +1209,11 @@ export class BubbleParser {
|
|
|
822
1209
|
if (bubbleNode) {
|
|
823
1210
|
const synthetic = `_anonymous_${bubbleNode.className}_${Object.keys(nodes).length}`;
|
|
824
1211
|
bubbleNode.variableName = synthetic;
|
|
1212
|
+
// Extract comment for this bubble node
|
|
1213
|
+
const description = this.extractCommentForNode(node);
|
|
1214
|
+
if (description) {
|
|
1215
|
+
bubbleNode.description = description;
|
|
1216
|
+
}
|
|
825
1217
|
const syntheticId = -1 * (Object.keys(nodes).length + 1);
|
|
826
1218
|
bubbleNode.variableId = syntheticId;
|
|
827
1219
|
bubbleNode.parameters = this.addVariableReferencesToParameters(bubbleNode.parameters, node, scopeManager);
|
|
@@ -1020,6 +1412,7 @@ export class BubbleParser {
|
|
|
1020
1412
|
name,
|
|
1021
1413
|
...value,
|
|
1022
1414
|
location,
|
|
1415
|
+
source: 'object-property', // Parameter came from an object literal property
|
|
1023
1416
|
});
|
|
1024
1417
|
}
|
|
1025
1418
|
}
|
|
@@ -1041,6 +1434,7 @@ export class BubbleParser {
|
|
|
1041
1434
|
name: spreadName,
|
|
1042
1435
|
...value,
|
|
1043
1436
|
location,
|
|
1437
|
+
source: 'spread', // Changed from 'object-property' to 'spread'
|
|
1044
1438
|
});
|
|
1045
1439
|
}
|
|
1046
1440
|
}
|
|
@@ -1062,6 +1456,7 @@ export class BubbleParser {
|
|
|
1062
1456
|
name: argName,
|
|
1063
1457
|
...value,
|
|
1064
1458
|
location,
|
|
1459
|
+
source: 'first-arg', // Parameter represents the entire first argument
|
|
1065
1460
|
});
|
|
1066
1461
|
}
|
|
1067
1462
|
}
|
|
@@ -1148,5 +1543,1764 @@ export class BubbleParser {
|
|
|
1148
1543
|
// Fallback
|
|
1149
1544
|
return { value: valueText, type: BubbleParameterType.UNKNOWN };
|
|
1150
1545
|
}
|
|
1546
|
+
/**
|
|
1547
|
+
* Find custom tools in ai-agent bubbles and populate customToolFuncs.
|
|
1548
|
+
* This scans the AST for ai-agent instantiations and extracts custom tool func locations.
|
|
1549
|
+
*/
|
|
1550
|
+
findCustomToolsInAIAgentBubbles(ast, nodes, classNameLookup) {
|
|
1551
|
+
// Get all ai-agent bubbles from nodes
|
|
1552
|
+
const aiAgentBubbles = Object.values(nodes).filter((n) => n.bubbleName === 'ai-agent');
|
|
1553
|
+
if (aiAgentBubbles.length === 0)
|
|
1554
|
+
return;
|
|
1555
|
+
// Helper to find NewExpression nodes in the AST
|
|
1556
|
+
const findNewExpressions = (node, results) => {
|
|
1557
|
+
if (node.type === 'NewExpression') {
|
|
1558
|
+
results.push(node);
|
|
1559
|
+
}
|
|
1560
|
+
for (const key of Object.keys(node)) {
|
|
1561
|
+
if (key === 'parent' || key === 'range' || key === 'loc')
|
|
1562
|
+
continue;
|
|
1563
|
+
const child = node[key];
|
|
1564
|
+
if (Array.isArray(child)) {
|
|
1565
|
+
for (const item of child) {
|
|
1566
|
+
if (item && typeof item === 'object' && 'type' in item) {
|
|
1567
|
+
findNewExpressions(item, results);
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
else if (child && typeof child === 'object' && 'type' in child) {
|
|
1572
|
+
findNewExpressions(child, results);
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
};
|
|
1576
|
+
const allNewExprs = [];
|
|
1577
|
+
findNewExpressions(ast, allNewExprs);
|
|
1578
|
+
// Find NewExpressions that are AIAgentBubble instantiations
|
|
1579
|
+
for (const newExpr of allNewExprs) {
|
|
1580
|
+
if (newExpr.callee.type !== 'Identifier' ||
|
|
1581
|
+
!classNameLookup.has(newExpr.callee.name)) {
|
|
1582
|
+
continue;
|
|
1583
|
+
}
|
|
1584
|
+
const info = classNameLookup.get(newExpr.callee.name);
|
|
1585
|
+
if (!info || info.bubbleName !== 'ai-agent')
|
|
1586
|
+
continue;
|
|
1587
|
+
// Find the corresponding parsed bubble by location
|
|
1588
|
+
const matchingBubble = aiAgentBubbles.find((b) => b.location.startLine === (newExpr.loc?.start.line ?? 0) &&
|
|
1589
|
+
b.location.startCol === (newExpr.loc?.start.column ?? 0));
|
|
1590
|
+
if (!matchingBubble)
|
|
1591
|
+
continue;
|
|
1592
|
+
// Find the customTools property in the first argument
|
|
1593
|
+
if (!newExpr.arguments ||
|
|
1594
|
+
newExpr.arguments.length === 0 ||
|
|
1595
|
+
newExpr.arguments[0].type !== 'ObjectExpression') {
|
|
1596
|
+
continue;
|
|
1597
|
+
}
|
|
1598
|
+
const firstArg = newExpr.arguments[0];
|
|
1599
|
+
const customToolsProp = firstArg.properties.find((p) => p.type === 'Property' &&
|
|
1600
|
+
p.key.type === 'Identifier' &&
|
|
1601
|
+
p.key.name === 'customTools');
|
|
1602
|
+
if (!customToolsProp ||
|
|
1603
|
+
customToolsProp.value.type !== 'ArrayExpression') {
|
|
1604
|
+
continue;
|
|
1605
|
+
}
|
|
1606
|
+
const customToolsArray = customToolsProp.value;
|
|
1607
|
+
// Process each custom tool object
|
|
1608
|
+
for (const element of customToolsArray.elements) {
|
|
1609
|
+
if (!element || element.type !== 'ObjectExpression')
|
|
1610
|
+
continue;
|
|
1611
|
+
const toolObj = element;
|
|
1612
|
+
// Find name property
|
|
1613
|
+
const nameProp = toolObj.properties.find((p) => p.type === 'Property' &&
|
|
1614
|
+
p.key.type === 'Identifier' &&
|
|
1615
|
+
p.key.name === 'name');
|
|
1616
|
+
const toolName = nameProp?.value.type === 'Literal' &&
|
|
1617
|
+
typeof nameProp.value.value === 'string'
|
|
1618
|
+
? nameProp.value.value
|
|
1619
|
+
: undefined;
|
|
1620
|
+
if (!toolName)
|
|
1621
|
+
continue;
|
|
1622
|
+
// Find description property
|
|
1623
|
+
const descProp = toolObj.properties.find((p) => p.type === 'Property' &&
|
|
1624
|
+
p.key.type === 'Identifier' &&
|
|
1625
|
+
p.key.name === 'description');
|
|
1626
|
+
const description = descProp?.value.type === 'Literal' &&
|
|
1627
|
+
typeof descProp.value.value === 'string'
|
|
1628
|
+
? descProp.value.value
|
|
1629
|
+
: undefined;
|
|
1630
|
+
// Find func property
|
|
1631
|
+
const funcProp = toolObj.properties.find((p) => p.type === 'Property' &&
|
|
1632
|
+
p.key.type === 'Identifier' &&
|
|
1633
|
+
p.key.name === 'func');
|
|
1634
|
+
if (!funcProp)
|
|
1635
|
+
continue;
|
|
1636
|
+
const funcValue = funcProp.value;
|
|
1637
|
+
if (funcValue.type !== 'ArrowFunctionExpression' &&
|
|
1638
|
+
funcValue.type !== 'FunctionExpression') {
|
|
1639
|
+
continue;
|
|
1640
|
+
}
|
|
1641
|
+
const funcExpr = funcValue;
|
|
1642
|
+
// Store the custom tool func info
|
|
1643
|
+
this.customToolFuncs.push({
|
|
1644
|
+
toolName,
|
|
1645
|
+
description,
|
|
1646
|
+
isAsync: funcExpr.async,
|
|
1647
|
+
startLine: funcExpr.loc?.start.line ?? 0,
|
|
1648
|
+
endLine: funcExpr.loc?.end.line ?? 0,
|
|
1649
|
+
startCol: funcExpr.loc?.start.column ?? 0,
|
|
1650
|
+
endCol: funcExpr.loc?.end.column ?? 0,
|
|
1651
|
+
parentBubbleVariableId: matchingBubble.variableId,
|
|
1652
|
+
});
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
/**
|
|
1657
|
+
* Mark bubbles that are inside custom tool funcs with isInsideCustomTool flag.
|
|
1658
|
+
*/
|
|
1659
|
+
markBubblesInsideCustomTools(nodes) {
|
|
1660
|
+
for (const bubble of Object.values(nodes)) {
|
|
1661
|
+
// Skip ai-agent bubbles themselves
|
|
1662
|
+
if (bubble.bubbleName === 'ai-agent')
|
|
1663
|
+
continue;
|
|
1664
|
+
// Check if this bubble's location falls inside any custom tool func
|
|
1665
|
+
for (const toolFunc of this.customToolFuncs) {
|
|
1666
|
+
if (bubble.location.startLine >= toolFunc.startLine &&
|
|
1667
|
+
bubble.location.endLine <= toolFunc.endLine) {
|
|
1668
|
+
bubble.isInsideCustomTool = true;
|
|
1669
|
+
bubble.containingCustomToolId = `${toolFunc.parentBubbleVariableId}.${toolFunc.toolName}`;
|
|
1670
|
+
break;
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
/**
|
|
1676
|
+
* Extract comment/description for a node by looking at preceding comments
|
|
1677
|
+
**/
|
|
1678
|
+
extractCommentForNode(node) {
|
|
1679
|
+
// Get the line number where this node starts
|
|
1680
|
+
const nodeLine = node.loc?.start.line;
|
|
1681
|
+
if (!nodeLine)
|
|
1682
|
+
return undefined;
|
|
1683
|
+
// Split the script into lines to find comments
|
|
1684
|
+
const lines = this.bubbleScript.split('\n');
|
|
1685
|
+
// Look backwards from the node line to find comments
|
|
1686
|
+
const commentLines = [];
|
|
1687
|
+
let currentLine = nodeLine - 1; // Start from the line before the node (0-indexed, but node.loc is 1-indexed)
|
|
1688
|
+
let isBlockComment = false;
|
|
1689
|
+
// Scan backwards to collect comment lines
|
|
1690
|
+
while (currentLine > 0) {
|
|
1691
|
+
const line = lines[currentLine - 1]?.trim(); // Convert to 0-indexed
|
|
1692
|
+
if (!line) {
|
|
1693
|
+
// Empty line - if we already have comments, stop here
|
|
1694
|
+
if (commentLines.length > 0)
|
|
1695
|
+
break;
|
|
1696
|
+
currentLine--;
|
|
1697
|
+
continue;
|
|
1698
|
+
}
|
|
1699
|
+
// Check for single-line comment (//)
|
|
1700
|
+
if (line.startsWith('//')) {
|
|
1701
|
+
commentLines.unshift(line);
|
|
1702
|
+
currentLine--;
|
|
1703
|
+
continue;
|
|
1704
|
+
}
|
|
1705
|
+
// Check if this line is part of a block comment
|
|
1706
|
+
if (line.startsWith('*') ||
|
|
1707
|
+
line.startsWith('/**') ||
|
|
1708
|
+
line.startsWith('/*')) {
|
|
1709
|
+
commentLines.unshift(line);
|
|
1710
|
+
isBlockComment = true;
|
|
1711
|
+
currentLine--;
|
|
1712
|
+
continue;
|
|
1713
|
+
}
|
|
1714
|
+
// Check if this line ends a block comment
|
|
1715
|
+
if (line.endsWith('*/')) {
|
|
1716
|
+
commentLines.unshift(line);
|
|
1717
|
+
isBlockComment = true;
|
|
1718
|
+
currentLine--;
|
|
1719
|
+
// Continue collecting the rest of the comment block
|
|
1720
|
+
continue;
|
|
1721
|
+
}
|
|
1722
|
+
// If we've already collected some comment lines and hit a non-comment, stop
|
|
1723
|
+
if (commentLines.length > 0) {
|
|
1724
|
+
break;
|
|
1725
|
+
}
|
|
1726
|
+
// Otherwise, this might be code - stop looking
|
|
1727
|
+
break;
|
|
1728
|
+
}
|
|
1729
|
+
if (commentLines.length === 0)
|
|
1730
|
+
return undefined;
|
|
1731
|
+
// Join comment lines and extract the actual text
|
|
1732
|
+
const fullComment = commentLines.join('\n');
|
|
1733
|
+
let cleaned;
|
|
1734
|
+
if (isBlockComment) {
|
|
1735
|
+
// Extract text from JSDoc-style or block comments
|
|
1736
|
+
// Remove /** ... */ or /* ... */ wrappers and clean up
|
|
1737
|
+
cleaned = fullComment
|
|
1738
|
+
.replace(/^\/\*\*?\s*/, '') // Remove opening /** or /*
|
|
1739
|
+
.replace(/\s*\*\/\s*$/, '') // Remove closing */
|
|
1740
|
+
.split('\n')
|
|
1741
|
+
.map((line) => {
|
|
1742
|
+
// Remove leading * and whitespace from each line
|
|
1743
|
+
return line.replace(/^\s*\*\s?/, '').trim();
|
|
1744
|
+
})
|
|
1745
|
+
.filter((line) => line.length > 0) // Remove empty lines
|
|
1746
|
+
.join(' ') // Join into single line
|
|
1747
|
+
.trim();
|
|
1748
|
+
}
|
|
1749
|
+
else {
|
|
1750
|
+
// Handle single-line comments (//)
|
|
1751
|
+
cleaned = fullComment
|
|
1752
|
+
.split('\n')
|
|
1753
|
+
.map((line) => {
|
|
1754
|
+
// Remove leading // and whitespace from each line
|
|
1755
|
+
return line.replace(/^\/\/\s?/, '').trim();
|
|
1756
|
+
})
|
|
1757
|
+
.filter((line) => line.length > 0) // Remove empty lines
|
|
1758
|
+
.join(' ') // Join into single line
|
|
1759
|
+
.trim();
|
|
1760
|
+
}
|
|
1761
|
+
return cleaned || undefined;
|
|
1762
|
+
}
|
|
1763
|
+
/**
|
|
1764
|
+
* Extract JSDoc info including description, @canBeFile, and @canBeGoogleFile tags from a node's preceding comments.
|
|
1765
|
+
* The @canBeFile tag controls whether file upload is enabled for string fields in the UI.
|
|
1766
|
+
* The @canBeGoogleFile tag enables Google Picker UI for Google Drive file/folder ID fields.
|
|
1767
|
+
*/
|
|
1768
|
+
extractJSDocForNode(node) {
|
|
1769
|
+
// Get the line number where this node starts
|
|
1770
|
+
const nodeLine = node.loc?.start.line;
|
|
1771
|
+
if (!nodeLine)
|
|
1772
|
+
return {};
|
|
1773
|
+
// Split the script into lines to find comments
|
|
1774
|
+
const lines = this.bubbleScript.split('\n');
|
|
1775
|
+
// Look backwards from the node line to find comments
|
|
1776
|
+
const commentLines = [];
|
|
1777
|
+
let currentLine = nodeLine - 1;
|
|
1778
|
+
let isBlockComment = false;
|
|
1779
|
+
// Scan backwards to collect comment lines
|
|
1780
|
+
while (currentLine > 0) {
|
|
1781
|
+
const line = lines[currentLine - 1]?.trim();
|
|
1782
|
+
if (!line) {
|
|
1783
|
+
if (commentLines.length > 0)
|
|
1784
|
+
break;
|
|
1785
|
+
currentLine--;
|
|
1786
|
+
continue;
|
|
1787
|
+
}
|
|
1788
|
+
if (line.startsWith('//')) {
|
|
1789
|
+
commentLines.unshift(line);
|
|
1790
|
+
currentLine--;
|
|
1791
|
+
continue;
|
|
1792
|
+
}
|
|
1793
|
+
if (line.startsWith('*') ||
|
|
1794
|
+
line.startsWith('/**') ||
|
|
1795
|
+
line.startsWith('/*')) {
|
|
1796
|
+
commentLines.unshift(line);
|
|
1797
|
+
isBlockComment = true;
|
|
1798
|
+
currentLine--;
|
|
1799
|
+
continue;
|
|
1800
|
+
}
|
|
1801
|
+
if (line.endsWith('*/')) {
|
|
1802
|
+
commentLines.unshift(line);
|
|
1803
|
+
isBlockComment = true;
|
|
1804
|
+
currentLine--;
|
|
1805
|
+
continue;
|
|
1806
|
+
}
|
|
1807
|
+
if (commentLines.length > 0) {
|
|
1808
|
+
break;
|
|
1809
|
+
}
|
|
1810
|
+
break;
|
|
1811
|
+
}
|
|
1812
|
+
if (commentLines.length === 0)
|
|
1813
|
+
return {};
|
|
1814
|
+
const fullComment = commentLines.join('\n');
|
|
1815
|
+
let canBeFile;
|
|
1816
|
+
let canBeGoogleFile;
|
|
1817
|
+
// Parse @canBeFile tag from the raw comment
|
|
1818
|
+
const canBeFileMatch = fullComment.match(/@canBeFile\s+(true|false)/i);
|
|
1819
|
+
if (canBeFileMatch) {
|
|
1820
|
+
canBeFile = canBeFileMatch[1].toLowerCase() === 'true';
|
|
1821
|
+
}
|
|
1822
|
+
// Parse @canBeGoogleFile tag from the raw comment
|
|
1823
|
+
const canBeGoogleFileMatch = fullComment.match(/@canBeGoogleFile\s+(true|false)/i);
|
|
1824
|
+
if (canBeGoogleFileMatch) {
|
|
1825
|
+
canBeGoogleFile = canBeGoogleFileMatch[1].toLowerCase() === 'true';
|
|
1826
|
+
}
|
|
1827
|
+
let description;
|
|
1828
|
+
if (isBlockComment) {
|
|
1829
|
+
description = fullComment
|
|
1830
|
+
.replace(/^\/\*\*?\s*/, '')
|
|
1831
|
+
.replace(/\s*\*\/\s*$/, '')
|
|
1832
|
+
.split('\n')
|
|
1833
|
+
.map((line) => line.replace(/^\s*\*\s?/, '').trim())
|
|
1834
|
+
.filter((line) => line.length > 0 &&
|
|
1835
|
+
!line.startsWith('@canBeFile') &&
|
|
1836
|
+
!line.startsWith('@canBeGoogleFile'))
|
|
1837
|
+
.join(' ')
|
|
1838
|
+
.trim();
|
|
1839
|
+
}
|
|
1840
|
+
else {
|
|
1841
|
+
description = fullComment
|
|
1842
|
+
.split('\n')
|
|
1843
|
+
.map((line) => line.replace(/^\/\/\s?/, '').trim())
|
|
1844
|
+
.filter((line) => line.length > 0 &&
|
|
1845
|
+
!line.startsWith('@canBeFile') &&
|
|
1846
|
+
!line.startsWith('@canBeGoogleFile'))
|
|
1847
|
+
.join(' ')
|
|
1848
|
+
.trim();
|
|
1849
|
+
}
|
|
1850
|
+
return {
|
|
1851
|
+
description: description || undefined,
|
|
1852
|
+
canBeFile,
|
|
1853
|
+
canBeGoogleFile,
|
|
1854
|
+
};
|
|
1855
|
+
}
|
|
1856
|
+
/**
|
|
1857
|
+
* Check if a list of workflow nodes contains a terminating statement (return/throw)
|
|
1858
|
+
* A branch terminates if its last statement is a return or throw
|
|
1859
|
+
*/
|
|
1860
|
+
branchTerminates(nodes) {
|
|
1861
|
+
if (nodes.length === 0)
|
|
1862
|
+
return false;
|
|
1863
|
+
const lastNode = nodes[nodes.length - 1];
|
|
1864
|
+
// Check if last node is a return statement
|
|
1865
|
+
if (lastNode.type === 'return') {
|
|
1866
|
+
return true;
|
|
1867
|
+
}
|
|
1868
|
+
// Check if last node is a code block containing return/throw
|
|
1869
|
+
if (lastNode.type === 'code_block') {
|
|
1870
|
+
const code = lastNode.code.trim();
|
|
1871
|
+
return (code.startsWith('return ') ||
|
|
1872
|
+
code.startsWith('return;') ||
|
|
1873
|
+
code === 'return' ||
|
|
1874
|
+
code.startsWith('throw '));
|
|
1875
|
+
}
|
|
1876
|
+
// Check nested control flow - all branches must terminate
|
|
1877
|
+
if (lastNode.type === 'if') {
|
|
1878
|
+
const thenTerminates = this.branchTerminates(lastNode.children);
|
|
1879
|
+
const elseTerminates = lastNode.elseBranch
|
|
1880
|
+
? this.branchTerminates(lastNode.elseBranch)
|
|
1881
|
+
: false;
|
|
1882
|
+
// Both branches must terminate for the if to terminate
|
|
1883
|
+
return thenTerminates && elseTerminates;
|
|
1884
|
+
}
|
|
1885
|
+
if (lastNode.type === 'try_catch') {
|
|
1886
|
+
const tryTerminates = this.branchTerminates(lastNode.children);
|
|
1887
|
+
const catchTerminates = lastNode.catchBlock
|
|
1888
|
+
? this.branchTerminates(lastNode.catchBlock)
|
|
1889
|
+
: false;
|
|
1890
|
+
return tryTerminates && catchTerminates;
|
|
1891
|
+
}
|
|
1892
|
+
return false;
|
|
1893
|
+
}
|
|
1894
|
+
/**
|
|
1895
|
+
* Build hierarchical workflow structure from AST
|
|
1896
|
+
*/
|
|
1897
|
+
buildWorkflowTree(ast, bubbles, scopeManager) {
|
|
1898
|
+
const handleNode = this.findHandleFunctionNode(ast);
|
|
1899
|
+
if (!handleNode || handleNode.body.type !== 'BlockStatement') {
|
|
1900
|
+
// If no handle method or empty body, return empty workflow
|
|
1901
|
+
return {
|
|
1902
|
+
root: [],
|
|
1903
|
+
bubbles,
|
|
1904
|
+
};
|
|
1905
|
+
}
|
|
1906
|
+
const workflowNodes = [];
|
|
1907
|
+
const bubbleMap = new Map();
|
|
1908
|
+
for (const [id, bubble] of Object.entries(bubbles)) {
|
|
1909
|
+
bubbleMap.set(Number(id), bubble);
|
|
1910
|
+
}
|
|
1911
|
+
// Process statements in handle method body
|
|
1912
|
+
const statements = handleNode.body.body;
|
|
1913
|
+
for (let i = 0; i < statements.length; i++) {
|
|
1914
|
+
const stmt = statements[i];
|
|
1915
|
+
const node = this.buildWorkflowNodeFromStatement(stmt, bubbleMap, scopeManager);
|
|
1916
|
+
if (node) {
|
|
1917
|
+
// Check if this is an if with terminating then branch but no else
|
|
1918
|
+
// In this case, move subsequent statements into implicit else
|
|
1919
|
+
if (node.type === 'if' &&
|
|
1920
|
+
node.thenTerminates &&
|
|
1921
|
+
!node.elseBranch &&
|
|
1922
|
+
i < statements.length - 1) {
|
|
1923
|
+
// Collect remaining statements as implicit else branch
|
|
1924
|
+
const implicitElse = [];
|
|
1925
|
+
for (let j = i + 1; j < statements.length; j++) {
|
|
1926
|
+
const remainingNode = this.buildWorkflowNodeFromStatement(statements[j], bubbleMap, scopeManager);
|
|
1927
|
+
if (remainingNode) {
|
|
1928
|
+
implicitElse.push(remainingNode);
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
if (implicitElse.length > 0) {
|
|
1932
|
+
node.elseBranch = implicitElse;
|
|
1933
|
+
}
|
|
1934
|
+
workflowNodes.push(node);
|
|
1935
|
+
break; // All remaining statements have been moved to else branch
|
|
1936
|
+
}
|
|
1937
|
+
workflowNodes.push(node);
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
// Group consecutive nodes of the same type
|
|
1941
|
+
const groupedNodes = this.groupConsecutiveNodes(workflowNodes);
|
|
1942
|
+
return {
|
|
1943
|
+
root: groupedNodes,
|
|
1944
|
+
bubbles,
|
|
1945
|
+
};
|
|
1946
|
+
}
|
|
1947
|
+
/**
|
|
1948
|
+
* Group consecutive nodes of the same type
|
|
1949
|
+
* - Consecutive variable_declaration nodes → merge into one
|
|
1950
|
+
* - Consecutive code_block nodes → merge into one
|
|
1951
|
+
* - return nodes are NOT grouped (each is a distinct exit point)
|
|
1952
|
+
*/
|
|
1953
|
+
groupConsecutiveNodes(nodes) {
|
|
1954
|
+
if (nodes.length === 0)
|
|
1955
|
+
return [];
|
|
1956
|
+
const result = [];
|
|
1957
|
+
let currentGroup = [];
|
|
1958
|
+
let currentType = null;
|
|
1959
|
+
for (const node of nodes) {
|
|
1960
|
+
// Control flow nodes break grouping
|
|
1961
|
+
const isControlFlow = node.type === 'if' ||
|
|
1962
|
+
node.type === 'for' ||
|
|
1963
|
+
node.type === 'while' ||
|
|
1964
|
+
node.type === 'try_catch' ||
|
|
1965
|
+
node.type === 'bubble' ||
|
|
1966
|
+
node.type === 'function_call';
|
|
1967
|
+
// Return nodes are never grouped
|
|
1968
|
+
const isReturn = node.type === 'return';
|
|
1969
|
+
// If we hit a control flow node or return, flush current group
|
|
1970
|
+
if (isControlFlow || isReturn) {
|
|
1971
|
+
if (currentGroup.length > 0) {
|
|
1972
|
+
result.push(...this.mergeGroup(currentGroup, currentType));
|
|
1973
|
+
currentGroup = [];
|
|
1974
|
+
currentType = null;
|
|
1975
|
+
}
|
|
1976
|
+
result.push(node);
|
|
1977
|
+
continue;
|
|
1978
|
+
}
|
|
1979
|
+
// Check if this node can be grouped
|
|
1980
|
+
const groupableType = node.type === 'variable_declaration' || node.type === 'code_block';
|
|
1981
|
+
if (groupableType) {
|
|
1982
|
+
// Don't group if node has children (e.g., function calls)
|
|
1983
|
+
const hasChildren = node.children && node.children.length > 0;
|
|
1984
|
+
if (hasChildren) {
|
|
1985
|
+
// Flush current group and add this node as-is
|
|
1986
|
+
if (currentGroup.length > 0) {
|
|
1987
|
+
result.push(...this.mergeGroup(currentGroup, currentType));
|
|
1988
|
+
currentGroup = [];
|
|
1989
|
+
currentType = null;
|
|
1990
|
+
}
|
|
1991
|
+
result.push(node);
|
|
1992
|
+
}
|
|
1993
|
+
else if (currentType === node.type) {
|
|
1994
|
+
// Same type, no children, add to group
|
|
1995
|
+
currentGroup.push(node);
|
|
1996
|
+
}
|
|
1997
|
+
else {
|
|
1998
|
+
// Different type, flush current group and start new one
|
|
1999
|
+
if (currentGroup.length > 0) {
|
|
2000
|
+
result.push(...this.mergeGroup(currentGroup, currentType));
|
|
2001
|
+
}
|
|
2002
|
+
currentGroup = [node];
|
|
2003
|
+
currentType = node.type;
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
else {
|
|
2007
|
+
// Not groupable, flush and add as-is
|
|
2008
|
+
if (currentGroup.length > 0) {
|
|
2009
|
+
result.push(...this.mergeGroup(currentGroup, currentType));
|
|
2010
|
+
currentGroup = [];
|
|
2011
|
+
currentType = null;
|
|
2012
|
+
}
|
|
2013
|
+
result.push(node);
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
// Flush any remaining group
|
|
2017
|
+
if (currentGroup.length > 0) {
|
|
2018
|
+
result.push(...this.mergeGroup(currentGroup, currentType));
|
|
2019
|
+
}
|
|
2020
|
+
return result;
|
|
2021
|
+
}
|
|
2022
|
+
/**
|
|
2023
|
+
* Merge a group of nodes of the same type into a single node
|
|
2024
|
+
*/
|
|
2025
|
+
mergeGroup(group, type) {
|
|
2026
|
+
if (group.length === 0)
|
|
2027
|
+
return [];
|
|
2028
|
+
if (group.length === 1)
|
|
2029
|
+
return group;
|
|
2030
|
+
if (type === 'variable_declaration') {
|
|
2031
|
+
const first = group[0];
|
|
2032
|
+
const allVariables = first.variables.slice();
|
|
2033
|
+
let startLine = first.location.startLine;
|
|
2034
|
+
let startCol = first.location.startCol;
|
|
2035
|
+
let endLine = first.location.endLine;
|
|
2036
|
+
let endCol = first.location.endCol;
|
|
2037
|
+
let code = first.code;
|
|
2038
|
+
for (let i = 1; i < group.length; i++) {
|
|
2039
|
+
const node = group[i];
|
|
2040
|
+
allVariables.push(...node.variables);
|
|
2041
|
+
if (node.location.startLine < startLine) {
|
|
2042
|
+
startLine = node.location.startLine;
|
|
2043
|
+
startCol = node.location.startCol;
|
|
2044
|
+
}
|
|
2045
|
+
if (node.location.endLine > endLine) {
|
|
2046
|
+
endLine = node.location.endLine;
|
|
2047
|
+
endCol = node.location.endCol;
|
|
2048
|
+
}
|
|
2049
|
+
code += '\n' + node.code;
|
|
2050
|
+
}
|
|
2051
|
+
return [
|
|
2052
|
+
{
|
|
2053
|
+
type: 'variable_declaration',
|
|
2054
|
+
location: {
|
|
2055
|
+
startLine,
|
|
2056
|
+
startCol,
|
|
2057
|
+
endLine,
|
|
2058
|
+
endCol,
|
|
2059
|
+
},
|
|
2060
|
+
code,
|
|
2061
|
+
variables: allVariables,
|
|
2062
|
+
children: [],
|
|
2063
|
+
},
|
|
2064
|
+
];
|
|
2065
|
+
}
|
|
2066
|
+
if (type === 'code_block') {
|
|
2067
|
+
const first = group[0];
|
|
2068
|
+
let startLine = first.location.startLine;
|
|
2069
|
+
let startCol = first.location.startCol;
|
|
2070
|
+
let endLine = first.location.endLine;
|
|
2071
|
+
let endCol = first.location.endCol;
|
|
2072
|
+
let code = first.code;
|
|
2073
|
+
for (let i = 1; i < group.length; i++) {
|
|
2074
|
+
const node = group[i];
|
|
2075
|
+
if (node.location.startLine < startLine) {
|
|
2076
|
+
startLine = node.location.startLine;
|
|
2077
|
+
startCol = node.location.startCol;
|
|
2078
|
+
}
|
|
2079
|
+
if (node.location.endLine > endLine) {
|
|
2080
|
+
endLine = node.location.endLine;
|
|
2081
|
+
endCol = node.location.endCol;
|
|
2082
|
+
}
|
|
2083
|
+
code += '\n' + node.code;
|
|
2084
|
+
}
|
|
2085
|
+
return [
|
|
2086
|
+
{
|
|
2087
|
+
type: 'code_block',
|
|
2088
|
+
location: {
|
|
2089
|
+
startLine,
|
|
2090
|
+
startCol,
|
|
2091
|
+
endLine,
|
|
2092
|
+
endCol,
|
|
2093
|
+
},
|
|
2094
|
+
code,
|
|
2095
|
+
children: [],
|
|
2096
|
+
},
|
|
2097
|
+
];
|
|
2098
|
+
}
|
|
2099
|
+
// Fallback: return as-is
|
|
2100
|
+
return group;
|
|
2101
|
+
}
|
|
2102
|
+
/**
|
|
2103
|
+
* Build a workflow node from an AST statement
|
|
2104
|
+
*/
|
|
2105
|
+
buildWorkflowNodeFromStatement(stmt, bubbleMap, scopeManager) {
|
|
2106
|
+
// Handle IfStatement
|
|
2107
|
+
if (stmt.type === 'IfStatement') {
|
|
2108
|
+
return this.buildIfNode(stmt, bubbleMap, scopeManager);
|
|
2109
|
+
}
|
|
2110
|
+
// Handle ForStatement
|
|
2111
|
+
if (stmt.type === 'ForStatement' ||
|
|
2112
|
+
stmt.type === 'ForInStatement' ||
|
|
2113
|
+
stmt.type === 'ForOfStatement') {
|
|
2114
|
+
return this.buildForNode(stmt, bubbleMap, scopeManager);
|
|
2115
|
+
}
|
|
2116
|
+
// Handle WhileStatement
|
|
2117
|
+
if (stmt.type === 'WhileStatement') {
|
|
2118
|
+
return this.buildWhileNode(stmt, bubbleMap, scopeManager);
|
|
2119
|
+
}
|
|
2120
|
+
// Handle TryStatement
|
|
2121
|
+
if (stmt.type === 'TryStatement') {
|
|
2122
|
+
return this.buildTryCatchNode(stmt, bubbleMap, scopeManager);
|
|
2123
|
+
}
|
|
2124
|
+
// Handle VariableDeclaration - check if it's a bubble, Promise.all, or function call
|
|
2125
|
+
if (stmt.type === 'VariableDeclaration') {
|
|
2126
|
+
for (const decl of stmt.declarations) {
|
|
2127
|
+
if (decl.init) {
|
|
2128
|
+
// Check if it's Promise.all (supports array destructuring)
|
|
2129
|
+
const promiseAll = this.detectPromiseAll(decl.init);
|
|
2130
|
+
if (promiseAll) {
|
|
2131
|
+
return this.buildParallelExecutionNode(promiseAll, stmt, bubbleMap, scopeManager);
|
|
2132
|
+
}
|
|
2133
|
+
// Handle Identifier declarations (const foo = ...)
|
|
2134
|
+
if (decl.id.type === 'Identifier') {
|
|
2135
|
+
// Try to find bubble by variable name and location
|
|
2136
|
+
// This handles same-named variables in different scopes by matching location
|
|
2137
|
+
const variableName = decl.id.name;
|
|
2138
|
+
const stmtStartLine = stmt.loc?.start.line ?? 0;
|
|
2139
|
+
const stmtEndLine = stmt.loc?.end.line ?? 0;
|
|
2140
|
+
const bubble = Array.from(bubbleMap.values()).find((b) => b.variableName === variableName &&
|
|
2141
|
+
b.location.startLine >= stmtStartLine &&
|
|
2142
|
+
b.location.endLine <= stmtEndLine);
|
|
2143
|
+
if (bubble) {
|
|
2144
|
+
return {
|
|
2145
|
+
type: 'bubble',
|
|
2146
|
+
variableId: bubble.variableId,
|
|
2147
|
+
};
|
|
2148
|
+
}
|
|
2149
|
+
// Check if initializer is a function call
|
|
2150
|
+
const functionCall = this.detectFunctionCall(decl.init);
|
|
2151
|
+
if (functionCall) {
|
|
2152
|
+
// If variable declaration contains a function call, represent it as function_call or transformation_function
|
|
2153
|
+
// The function call node will contain the full statement code
|
|
2154
|
+
// Variable declaration is already handled inside buildFunctionCallNode
|
|
2155
|
+
return this.buildFunctionCallNode(functionCall, stmt, bubbleMap, scopeManager);
|
|
2156
|
+
}
|
|
2157
|
+
// Fallback to expression matching for bubbles
|
|
2158
|
+
const bubbleFromExpr = this.findBubbleInExpression(decl.init, bubbleMap);
|
|
2159
|
+
if (bubbleFromExpr) {
|
|
2160
|
+
return {
|
|
2161
|
+
type: 'bubble',
|
|
2162
|
+
variableId: bubbleFromExpr.variableId,
|
|
2163
|
+
};
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
else if (decl.id.type === 'ObjectPattern' ||
|
|
2167
|
+
decl.id.type === 'ArrayPattern') {
|
|
2168
|
+
// Handle destructuring declarations (const { a, b } = ... or const [a, b] = ...)
|
|
2169
|
+
// Check if initializer is a function call - transformation functions take precedence
|
|
2170
|
+
const functionCall = this.detectFunctionCall(decl.init);
|
|
2171
|
+
if (functionCall) {
|
|
2172
|
+
// If variable declaration contains a function call, represent it as function_call or transformation_function
|
|
2173
|
+
// The function call node will contain the full statement code
|
|
2174
|
+
// Variable declaration is already handled inside buildFunctionCallNode
|
|
2175
|
+
return this.buildFunctionCallNode(functionCall, stmt, bubbleMap, scopeManager);
|
|
2176
|
+
}
|
|
2177
|
+
// Check for bubbles in the expression
|
|
2178
|
+
const bubbleFromExpr = this.findBubbleInExpression(decl.init, bubbleMap);
|
|
2179
|
+
if (bubbleFromExpr) {
|
|
2180
|
+
return {
|
|
2181
|
+
type: 'bubble',
|
|
2182
|
+
variableId: bubbleFromExpr.variableId,
|
|
2183
|
+
};
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
// If not a bubble or function call, create variable declaration node
|
|
2189
|
+
return this.buildVariableDeclarationNode(stmt, bubbleMap, scopeManager);
|
|
2190
|
+
}
|
|
2191
|
+
// Handle ExpressionStatement - check if it's a bubble or function call
|
|
2192
|
+
if (stmt.type === 'ExpressionStatement') {
|
|
2193
|
+
// Handle AssignmentExpression (e.g., agentResponse = await this.method())
|
|
2194
|
+
if (stmt.expression.type === 'AssignmentExpression') {
|
|
2195
|
+
const assignExpr = stmt.expression;
|
|
2196
|
+
// Check if right-hand side is a bubble
|
|
2197
|
+
const bubble = this.findBubbleInExpression(assignExpr.right, bubbleMap);
|
|
2198
|
+
if (bubble) {
|
|
2199
|
+
return {
|
|
2200
|
+
type: 'bubble',
|
|
2201
|
+
variableId: bubble.variableId,
|
|
2202
|
+
};
|
|
2203
|
+
}
|
|
2204
|
+
// Check if right-hand side is a function call
|
|
2205
|
+
const functionCall = this.detectFunctionCall(assignExpr.right);
|
|
2206
|
+
if (functionCall) {
|
|
2207
|
+
return this.buildFunctionCallNode(functionCall, stmt, bubbleMap, scopeManager);
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
else {
|
|
2211
|
+
// Regular expression (not assignment)
|
|
2212
|
+
const bubble = this.findBubbleInExpression(stmt.expression, bubbleMap);
|
|
2213
|
+
if (bubble) {
|
|
2214
|
+
return {
|
|
2215
|
+
type: 'bubble',
|
|
2216
|
+
variableId: bubble.variableId,
|
|
2217
|
+
};
|
|
2218
|
+
}
|
|
2219
|
+
// Check for function calls
|
|
2220
|
+
const functionCall = this.detectFunctionCall(stmt.expression);
|
|
2221
|
+
if (functionCall) {
|
|
2222
|
+
return this.buildFunctionCallNode(functionCall, stmt, bubbleMap, scopeManager);
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
// If not a bubble or function call, treat as code block
|
|
2226
|
+
return this.buildCodeBlockNode(stmt, bubbleMap, scopeManager);
|
|
2227
|
+
}
|
|
2228
|
+
// Handle ReturnStatement
|
|
2229
|
+
if (stmt.type === 'ReturnStatement') {
|
|
2230
|
+
if (stmt.argument) {
|
|
2231
|
+
const bubble = this.findBubbleInExpression(stmt.argument, bubbleMap);
|
|
2232
|
+
if (bubble) {
|
|
2233
|
+
return {
|
|
2234
|
+
type: 'bubble',
|
|
2235
|
+
variableId: bubble.variableId,
|
|
2236
|
+
};
|
|
2237
|
+
}
|
|
2238
|
+
// Check if return value is a function call
|
|
2239
|
+
const functionCall = this.detectFunctionCall(stmt.argument);
|
|
2240
|
+
if (functionCall) {
|
|
2241
|
+
// Create return node with function call as child
|
|
2242
|
+
const returnNode = this.buildReturnNode(stmt, bubbleMap, scopeManager);
|
|
2243
|
+
if (returnNode) {
|
|
2244
|
+
const funcCallNode = this.buildFunctionCallNode(functionCall, stmt, bubbleMap, scopeManager);
|
|
2245
|
+
if (funcCallNode) {
|
|
2246
|
+
returnNode.children = [funcCallNode];
|
|
2247
|
+
}
|
|
2248
|
+
return returnNode;
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
return this.buildReturnNode(stmt, bubbleMap, scopeManager);
|
|
2253
|
+
}
|
|
2254
|
+
// Default: treat as code block
|
|
2255
|
+
return this.buildCodeBlockNode(stmt, bubbleMap, scopeManager);
|
|
2256
|
+
}
|
|
2257
|
+
/**
|
|
2258
|
+
* Build an if node from IfStatement
|
|
2259
|
+
*/
|
|
2260
|
+
buildIfNode(stmt, bubbleMap, scopeManager) {
|
|
2261
|
+
const condition = this.extractConditionString(stmt.test);
|
|
2262
|
+
const location = this.extractLocation(stmt);
|
|
2263
|
+
const children = [];
|
|
2264
|
+
if (stmt.consequent.type === 'BlockStatement') {
|
|
2265
|
+
for (const childStmt of stmt.consequent.body) {
|
|
2266
|
+
const node = this.buildWorkflowNodeFromStatement(childStmt, bubbleMap, scopeManager);
|
|
2267
|
+
if (node) {
|
|
2268
|
+
children.push(node);
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
else {
|
|
2273
|
+
// Single statement (no braces)
|
|
2274
|
+
const node = this.buildWorkflowNodeFromStatement(stmt.consequent, bubbleMap, scopeManager);
|
|
2275
|
+
if (node) {
|
|
2276
|
+
children.push(node);
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
const elseBranch = stmt.alternate
|
|
2280
|
+
? (() => {
|
|
2281
|
+
if (stmt.alternate.type === 'BlockStatement') {
|
|
2282
|
+
const nodes = [];
|
|
2283
|
+
for (const childStmt of stmt.alternate.body) {
|
|
2284
|
+
const node = this.buildWorkflowNodeFromStatement(childStmt, bubbleMap, scopeManager);
|
|
2285
|
+
if (node) {
|
|
2286
|
+
nodes.push(node);
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
return nodes;
|
|
2290
|
+
}
|
|
2291
|
+
else if (stmt.alternate.type === 'IfStatement') {
|
|
2292
|
+
// else if - treat as nested if
|
|
2293
|
+
const node = this.buildIfNode(stmt.alternate, bubbleMap, scopeManager);
|
|
2294
|
+
return [node];
|
|
2295
|
+
}
|
|
2296
|
+
else {
|
|
2297
|
+
// Single statement else
|
|
2298
|
+
const node = this.buildWorkflowNodeFromStatement(stmt.alternate, bubbleMap, scopeManager);
|
|
2299
|
+
return node ? [node] : [];
|
|
2300
|
+
}
|
|
2301
|
+
})()
|
|
2302
|
+
: undefined;
|
|
2303
|
+
// Check if branches terminate (contain return/throw)
|
|
2304
|
+
const thenTerminates = this.branchTerminates(children);
|
|
2305
|
+
const elseTerminates = elseBranch
|
|
2306
|
+
? this.branchTerminates(elseBranch)
|
|
2307
|
+
: false;
|
|
2308
|
+
return {
|
|
2309
|
+
type: 'if',
|
|
2310
|
+
location,
|
|
2311
|
+
condition,
|
|
2312
|
+
children,
|
|
2313
|
+
elseBranch,
|
|
2314
|
+
thenTerminates: thenTerminates || undefined,
|
|
2315
|
+
elseTerminates: elseTerminates || undefined,
|
|
2316
|
+
};
|
|
2317
|
+
}
|
|
2318
|
+
/**
|
|
2319
|
+
* Build a for node from ForStatement/ForInStatement/ForOfStatement
|
|
2320
|
+
*/
|
|
2321
|
+
buildForNode(stmt, bubbleMap, scopeManager) {
|
|
2322
|
+
const location = this.extractLocation(stmt);
|
|
2323
|
+
let condition;
|
|
2324
|
+
if (stmt.type === 'ForStatement') {
|
|
2325
|
+
const init = stmt.init
|
|
2326
|
+
? this.bubbleScript.substring(stmt.init.range[0], stmt.init.range[1])
|
|
2327
|
+
: '';
|
|
2328
|
+
const test = stmt.test
|
|
2329
|
+
? this.bubbleScript.substring(stmt.test.range[0], stmt.test.range[1])
|
|
2330
|
+
: '';
|
|
2331
|
+
const update = stmt.update
|
|
2332
|
+
? this.bubbleScript.substring(stmt.update.range[0], stmt.update.range[1])
|
|
2333
|
+
: '';
|
|
2334
|
+
condition = `${init}; ${test}; ${update}`.trim();
|
|
2335
|
+
}
|
|
2336
|
+
else if (stmt.type === 'ForInStatement') {
|
|
2337
|
+
const left = this.bubbleScript.substring(stmt.left.range[0], stmt.left.range[1]);
|
|
2338
|
+
const right = this.bubbleScript.substring(stmt.right.range[0], stmt.right.range[1]);
|
|
2339
|
+
condition = `${left} in ${right}`;
|
|
2340
|
+
}
|
|
2341
|
+
else if (stmt.type === 'ForOfStatement') {
|
|
2342
|
+
const left = this.bubbleScript.substring(stmt.left.range[0], stmt.left.range[1]);
|
|
2343
|
+
const right = this.bubbleScript.substring(stmt.right.range[0], stmt.right.range[1]);
|
|
2344
|
+
condition = `${left} of ${right}`;
|
|
2345
|
+
}
|
|
2346
|
+
const children = [];
|
|
2347
|
+
if (stmt.body.type === 'BlockStatement') {
|
|
2348
|
+
for (const childStmt of stmt.body.body) {
|
|
2349
|
+
const node = this.buildWorkflowNodeFromStatement(childStmt, bubbleMap, scopeManager);
|
|
2350
|
+
if (node) {
|
|
2351
|
+
children.push(node);
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
else {
|
|
2356
|
+
// Single statement (no braces)
|
|
2357
|
+
const node = this.buildWorkflowNodeFromStatement(stmt.body, bubbleMap, scopeManager);
|
|
2358
|
+
if (node) {
|
|
2359
|
+
children.push(node);
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
return {
|
|
2363
|
+
type: stmt.type === 'ForOfStatement' ? 'for' : 'for',
|
|
2364
|
+
location,
|
|
2365
|
+
condition,
|
|
2366
|
+
children,
|
|
2367
|
+
};
|
|
2368
|
+
}
|
|
2369
|
+
/**
|
|
2370
|
+
* Build a while node from WhileStatement
|
|
2371
|
+
*/
|
|
2372
|
+
buildWhileNode(stmt, bubbleMap, scopeManager) {
|
|
2373
|
+
const location = this.extractLocation(stmt);
|
|
2374
|
+
const condition = this.extractConditionString(stmt.test);
|
|
2375
|
+
const children = [];
|
|
2376
|
+
if (stmt.body.type === 'BlockStatement') {
|
|
2377
|
+
for (const childStmt of stmt.body.body) {
|
|
2378
|
+
const node = this.buildWorkflowNodeFromStatement(childStmt, bubbleMap, scopeManager);
|
|
2379
|
+
if (node) {
|
|
2380
|
+
children.push(node);
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
else {
|
|
2385
|
+
// Single statement (no braces)
|
|
2386
|
+
const node = this.buildWorkflowNodeFromStatement(stmt.body, bubbleMap, scopeManager);
|
|
2387
|
+
if (node) {
|
|
2388
|
+
children.push(node);
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
2391
|
+
return {
|
|
2392
|
+
type: 'while',
|
|
2393
|
+
location,
|
|
2394
|
+
condition,
|
|
2395
|
+
children,
|
|
2396
|
+
};
|
|
2397
|
+
}
|
|
2398
|
+
/**
|
|
2399
|
+
* Build a try-catch node from TryStatement
|
|
2400
|
+
*/
|
|
2401
|
+
buildTryCatchNode(stmt, bubbleMap, scopeManager) {
|
|
2402
|
+
const location = this.extractLocation(stmt);
|
|
2403
|
+
const children = [];
|
|
2404
|
+
if (stmt.block.type === 'BlockStatement') {
|
|
2405
|
+
for (const childStmt of stmt.block.body) {
|
|
2406
|
+
const node = this.buildWorkflowNodeFromStatement(childStmt, bubbleMap, scopeManager);
|
|
2407
|
+
if (node) {
|
|
2408
|
+
children.push(node);
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
const catchBlock = stmt.handler
|
|
2413
|
+
? (() => {
|
|
2414
|
+
if (stmt.handler.body.type === 'BlockStatement') {
|
|
2415
|
+
const nodes = [];
|
|
2416
|
+
for (const childStmt of stmt.handler.body.body) {
|
|
2417
|
+
const node = this.buildWorkflowNodeFromStatement(childStmt, bubbleMap, scopeManager);
|
|
2418
|
+
if (node) {
|
|
2419
|
+
nodes.push(node);
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
return nodes;
|
|
2423
|
+
}
|
|
2424
|
+
return [];
|
|
2425
|
+
})()
|
|
2426
|
+
: undefined;
|
|
2427
|
+
return {
|
|
2428
|
+
type: 'try_catch',
|
|
2429
|
+
location,
|
|
2430
|
+
children,
|
|
2431
|
+
catchBlock,
|
|
2432
|
+
};
|
|
2433
|
+
}
|
|
2434
|
+
/**
|
|
2435
|
+
* Build a code block node from a statement
|
|
2436
|
+
*/
|
|
2437
|
+
buildCodeBlockNode(stmt, bubbleMap, scopeManager) {
|
|
2438
|
+
const location = this.extractLocation(stmt);
|
|
2439
|
+
if (!location)
|
|
2440
|
+
return null;
|
|
2441
|
+
const code = this.bubbleScript.substring(stmt.range[0], stmt.range[1]);
|
|
2442
|
+
// Check for nested structures
|
|
2443
|
+
const children = [];
|
|
2444
|
+
if (stmt.type === 'BlockStatement') {
|
|
2445
|
+
for (const childStmt of stmt.body) {
|
|
2446
|
+
const node = this.buildWorkflowNodeFromStatement(childStmt, bubbleMap, scopeManager);
|
|
2447
|
+
if (node) {
|
|
2448
|
+
children.push(node);
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
return {
|
|
2453
|
+
type: 'code_block',
|
|
2454
|
+
location,
|
|
2455
|
+
code,
|
|
2456
|
+
children,
|
|
2457
|
+
};
|
|
2458
|
+
}
|
|
2459
|
+
/**
|
|
2460
|
+
* Find a bubble in an expression by checking if it matches any parsed bubble
|
|
2461
|
+
*/
|
|
2462
|
+
findBubbleInExpression(expr, bubbleMap) {
|
|
2463
|
+
if (!expr.loc)
|
|
2464
|
+
return null;
|
|
2465
|
+
// Extract the NewExpression from the expression (handles await, .action(), etc.)
|
|
2466
|
+
const newExpr = this.extractNewExpression(expr);
|
|
2467
|
+
if (!newExpr || !newExpr.loc)
|
|
2468
|
+
return null;
|
|
2469
|
+
// Match by NewExpression location (this is what bubbles are stored with)
|
|
2470
|
+
for (const bubble of bubbleMap.values()) {
|
|
2471
|
+
// Check if the NewExpression location overlaps with bubble location
|
|
2472
|
+
// Use a tolerance for column matching since the exact column might differ slightly
|
|
2473
|
+
if (bubble.location.startLine === newExpr.loc.start.line &&
|
|
2474
|
+
bubble.location.endLine === newExpr.loc.end.line &&
|
|
2475
|
+
Math.abs(bubble.location.startCol - newExpr.loc.start.column) <= 5) {
|
|
2476
|
+
return bubble;
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
return null;
|
|
2480
|
+
}
|
|
2481
|
+
/**
|
|
2482
|
+
* Extract the NewExpression from an expression, handling await, .action(), etc.
|
|
2483
|
+
*/
|
|
2484
|
+
extractNewExpression(expr) {
|
|
2485
|
+
// Handle await new X()
|
|
2486
|
+
if (expr.type === 'AwaitExpression' && expr.argument) {
|
|
2487
|
+
return this.extractNewExpression(expr.argument);
|
|
2488
|
+
}
|
|
2489
|
+
// Handle new X().action()
|
|
2490
|
+
if (expr.type === 'CallExpression' &&
|
|
2491
|
+
expr.callee.type === 'MemberExpression') {
|
|
2492
|
+
if (expr.callee.object) {
|
|
2493
|
+
return this.extractNewExpression(expr.callee.object);
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
// Direct NewExpression
|
|
2497
|
+
if (expr.type === 'NewExpression') {
|
|
2498
|
+
return expr;
|
|
2499
|
+
}
|
|
2500
|
+
return null;
|
|
2501
|
+
}
|
|
2502
|
+
/**
|
|
2503
|
+
* Build a variable declaration node from a VariableDeclaration statement
|
|
2504
|
+
*/
|
|
2505
|
+
buildVariableDeclarationNode(stmt, _bubbleMap, _scopeManager) {
|
|
2506
|
+
const location = this.extractLocation(stmt);
|
|
2507
|
+
if (!location)
|
|
2508
|
+
return null;
|
|
2509
|
+
const code = this.bubbleScript.substring(stmt.range[0], stmt.range[1]);
|
|
2510
|
+
const variables = [];
|
|
2511
|
+
for (const decl of stmt.declarations) {
|
|
2512
|
+
if (decl.id.type === 'Identifier') {
|
|
2513
|
+
variables.push({
|
|
2514
|
+
name: decl.id.name,
|
|
2515
|
+
type: stmt.kind,
|
|
2516
|
+
hasInitializer: decl.init !== null && decl.init !== undefined,
|
|
2517
|
+
});
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2520
|
+
return {
|
|
2521
|
+
type: 'variable_declaration',
|
|
2522
|
+
location,
|
|
2523
|
+
code,
|
|
2524
|
+
variables,
|
|
2525
|
+
children: [],
|
|
2526
|
+
};
|
|
2527
|
+
}
|
|
2528
|
+
/**
|
|
2529
|
+
* Build a return node from a ReturnStatement
|
|
2530
|
+
*/
|
|
2531
|
+
buildReturnNode(stmt, _bubbleMap, _scopeManager) {
|
|
2532
|
+
const location = this.extractLocation(stmt);
|
|
2533
|
+
if (!location)
|
|
2534
|
+
return null;
|
|
2535
|
+
const code = this.bubbleScript.substring(stmt.range[0], stmt.range[1]);
|
|
2536
|
+
const value = stmt.argument
|
|
2537
|
+
? this.bubbleScript.substring(stmt.argument.range[0], stmt.argument.range[1])
|
|
2538
|
+
: undefined;
|
|
2539
|
+
return {
|
|
2540
|
+
type: 'return',
|
|
2541
|
+
location,
|
|
2542
|
+
code,
|
|
2543
|
+
value,
|
|
2544
|
+
children: [],
|
|
2545
|
+
};
|
|
2546
|
+
}
|
|
2547
|
+
/**
|
|
2548
|
+
* Detect if an expression is Promise.all([...]) or Promise.all(variable)
|
|
2549
|
+
*/
|
|
2550
|
+
detectPromiseAll(expr) {
|
|
2551
|
+
// Handle await Promise.all([...])
|
|
2552
|
+
let callExpr = null;
|
|
2553
|
+
if (expr.type === 'AwaitExpression' && expr.argument) {
|
|
2554
|
+
if (expr.argument.type === 'CallExpression') {
|
|
2555
|
+
callExpr = expr.argument;
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
else if (expr.type === 'CallExpression') {
|
|
2559
|
+
callExpr = expr;
|
|
2560
|
+
}
|
|
2561
|
+
if (!callExpr)
|
|
2562
|
+
return null;
|
|
2563
|
+
// Check if it's Promise.all
|
|
2564
|
+
const callee = callExpr.callee;
|
|
2565
|
+
if (callee.type === 'MemberExpression' &&
|
|
2566
|
+
callee.object.type === 'Identifier' &&
|
|
2567
|
+
callee.object.name === 'Promise' &&
|
|
2568
|
+
callee.property.type === 'Identifier' &&
|
|
2569
|
+
callee.property.name === 'all') {
|
|
2570
|
+
// Check if the first argument is an array or variable
|
|
2571
|
+
if (callExpr.arguments.length > 0) {
|
|
2572
|
+
const arg = callExpr.arguments[0];
|
|
2573
|
+
if (arg.type === 'ArrayExpression') {
|
|
2574
|
+
return {
|
|
2575
|
+
callExpr,
|
|
2576
|
+
arrayExpr: arg,
|
|
2577
|
+
};
|
|
2578
|
+
}
|
|
2579
|
+
if (arg.type === 'Identifier') {
|
|
2580
|
+
return {
|
|
2581
|
+
callExpr,
|
|
2582
|
+
arrayExpr: arg,
|
|
2583
|
+
};
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
return null;
|
|
2588
|
+
}
|
|
2589
|
+
/**
|
|
2590
|
+
* Detect if an expression is a function call
|
|
2591
|
+
*/
|
|
2592
|
+
detectFunctionCall(expr) {
|
|
2593
|
+
// Handle await functionCall()
|
|
2594
|
+
let callExpr = null;
|
|
2595
|
+
if (expr.type === 'AwaitExpression' && expr.argument) {
|
|
2596
|
+
if (expr.argument.type === 'CallExpression') {
|
|
2597
|
+
callExpr = expr.argument;
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
else if (expr.type === 'CallExpression') {
|
|
2601
|
+
callExpr = expr;
|
|
2602
|
+
}
|
|
2603
|
+
if (!callExpr)
|
|
2604
|
+
return null;
|
|
2605
|
+
const callee = callExpr.callee;
|
|
2606
|
+
let functionName = null;
|
|
2607
|
+
let isMethodCall = false;
|
|
2608
|
+
if (callee.type === 'Identifier') {
|
|
2609
|
+
// Direct function call: functionName()
|
|
2610
|
+
functionName = callee.name;
|
|
2611
|
+
isMethodCall = false;
|
|
2612
|
+
}
|
|
2613
|
+
else if (callee.type === 'MemberExpression') {
|
|
2614
|
+
// Method call: this.methodName() or obj.methodName()
|
|
2615
|
+
if (callee.object.type === 'ThisExpression' &&
|
|
2616
|
+
callee.property.type === 'Identifier') {
|
|
2617
|
+
functionName = callee.property.name;
|
|
2618
|
+
isMethodCall = true;
|
|
2619
|
+
}
|
|
2620
|
+
else if (callee.property.type === 'Identifier') {
|
|
2621
|
+
functionName = callee.property.name;
|
|
2622
|
+
isMethodCall = false; // External method call, not this.method()
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
if (!functionName)
|
|
2626
|
+
return null;
|
|
2627
|
+
const args = callExpr.arguments
|
|
2628
|
+
.map((arg) => this.bubbleScript.substring(arg.range[0], arg.range[1]))
|
|
2629
|
+
.join(', ');
|
|
2630
|
+
return {
|
|
2631
|
+
functionName,
|
|
2632
|
+
isMethodCall,
|
|
2633
|
+
arguments: args,
|
|
2634
|
+
callExpr,
|
|
2635
|
+
};
|
|
2636
|
+
}
|
|
2637
|
+
/**
|
|
2638
|
+
* Find a method definition in the class by name
|
|
2639
|
+
*/
|
|
2640
|
+
findMethodDefinition(methodName, ast) {
|
|
2641
|
+
const mainClass = this.findMainBubbleFlowClass(ast);
|
|
2642
|
+
if (!mainClass || !mainClass.body)
|
|
2643
|
+
return null;
|
|
2644
|
+
for (const member of mainClass.body.body) {
|
|
2645
|
+
if (member.type === 'MethodDefinition' &&
|
|
2646
|
+
member.key.type === 'Identifier' &&
|
|
2647
|
+
member.key.name === methodName &&
|
|
2648
|
+
member.value.type === 'FunctionExpression') {
|
|
2649
|
+
const func = member.value;
|
|
2650
|
+
const body = func.body.type === 'BlockStatement' ? func.body : null;
|
|
2651
|
+
const isAsync = func.async || false;
|
|
2652
|
+
const parameters = func.params
|
|
2653
|
+
.map((param) => {
|
|
2654
|
+
if (param.type === 'Identifier')
|
|
2655
|
+
return param.name;
|
|
2656
|
+
if (param.type === 'AssignmentPattern' &&
|
|
2657
|
+
param.left.type === 'Identifier')
|
|
2658
|
+
return param.left.name;
|
|
2659
|
+
return '';
|
|
2660
|
+
})
|
|
2661
|
+
.filter((name) => name !== '');
|
|
2662
|
+
return {
|
|
2663
|
+
method: member,
|
|
2664
|
+
body,
|
|
2665
|
+
isAsync,
|
|
2666
|
+
parameters,
|
|
2667
|
+
};
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
return null;
|
|
2671
|
+
}
|
|
2672
|
+
/**
|
|
2673
|
+
* Check if a workflow node tree contains any bubbles (recursively)
|
|
2674
|
+
*/
|
|
2675
|
+
containsBubbles(nodes) {
|
|
2676
|
+
for (const node of nodes) {
|
|
2677
|
+
if (node.type === 'bubble') {
|
|
2678
|
+
return true;
|
|
2679
|
+
}
|
|
2680
|
+
// Check children recursively
|
|
2681
|
+
if ('children' in node && Array.isArray(node.children)) {
|
|
2682
|
+
if (this.containsBubbles(node.children)) {
|
|
2683
|
+
return true;
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
// Check elseBranch for if statements
|
|
2687
|
+
if (node.type === 'if' && node.elseBranch) {
|
|
2688
|
+
if (this.containsBubbles(node.elseBranch)) {
|
|
2689
|
+
return true;
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
// Check catchBlock for try_catch
|
|
2693
|
+
if (node.type === 'try_catch' && node.catchBlock) {
|
|
2694
|
+
if (this.containsBubbles(node.catchBlock)) {
|
|
2695
|
+
return true;
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2699
|
+
return false;
|
|
2700
|
+
}
|
|
2701
|
+
/**
|
|
2702
|
+
* Build a function call node from a function call expression
|
|
2703
|
+
*/
|
|
2704
|
+
buildFunctionCallNode(callInfo, stmt, bubbleMap, scopeManager) {
|
|
2705
|
+
const location = this.extractLocation(stmt);
|
|
2706
|
+
if (!location)
|
|
2707
|
+
return null;
|
|
2708
|
+
const code = this.bubbleScript.substring(stmt.range[0], stmt.range[1]);
|
|
2709
|
+
// Try to find method definition if it's a method call
|
|
2710
|
+
let methodDefinition = undefined;
|
|
2711
|
+
const methodChildren = [];
|
|
2712
|
+
const children = [];
|
|
2713
|
+
let description = undefined;
|
|
2714
|
+
const methodBubbleMap = new Map();
|
|
2715
|
+
if (callInfo.isMethodCall && this.cachedAST) {
|
|
2716
|
+
const methodDef = this.findMethodDefinition(callInfo.functionName, this.cachedAST);
|
|
2717
|
+
if (methodDef && methodDef.body) {
|
|
2718
|
+
methodDefinition = {
|
|
2719
|
+
location: {
|
|
2720
|
+
startLine: methodDef.method.loc?.start.line || 0,
|
|
2721
|
+
endLine: methodDef.method.loc?.end.line || 0,
|
|
2722
|
+
},
|
|
2723
|
+
isAsync: methodDef.isAsync,
|
|
2724
|
+
parameters: methodDef.parameters,
|
|
2725
|
+
};
|
|
2726
|
+
// Extract description from method comments
|
|
2727
|
+
description = this.extractCommentForNode(methodDef.method);
|
|
2728
|
+
// Filter bubbleMap to only include bubbles within this method's scope
|
|
2729
|
+
const methodStartLine = methodDef.method.loc?.start.line || 0;
|
|
2730
|
+
const methodEndLine = methodDef.method.loc?.end.line || 0;
|
|
2731
|
+
for (const [id, bubble] of bubbleMap.entries()) {
|
|
2732
|
+
// Include bubble if it's within the method's line range
|
|
2733
|
+
if (bubble.location.startLine >= methodStartLine &&
|
|
2734
|
+
bubble.location.endLine <= methodEndLine) {
|
|
2735
|
+
methodBubbleMap.set(id, bubble);
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
// Recursively build workflow nodes from method body
|
|
2739
|
+
for (const childStmt of methodDef.body.body) {
|
|
2740
|
+
const node = this.buildWorkflowNodeFromStatement(childStmt, methodBubbleMap, scopeManager);
|
|
2741
|
+
if (node) {
|
|
2742
|
+
methodChildren.push(node);
|
|
2743
|
+
}
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2747
|
+
const shouldTrackInvocation = callInfo.isMethodCall && !!methodDefinition;
|
|
2748
|
+
// Pass the call expression's start offset to deduplicate when the same call
|
|
2749
|
+
// is processed multiple times (e.g., .map() callback processing vs Promise.all resolution)
|
|
2750
|
+
const callExprStartOffset = callInfo.callExpr.range?.[0];
|
|
2751
|
+
const invocationIndex = shouldTrackInvocation
|
|
2752
|
+
? this.getNextInvocationIndex(callInfo.functionName, callExprStartOffset)
|
|
2753
|
+
: 0;
|
|
2754
|
+
const callSiteKey = shouldTrackInvocation && invocationIndex > 0
|
|
2755
|
+
? buildCallSiteKey(callInfo.functionName, invocationIndex)
|
|
2756
|
+
: null;
|
|
2757
|
+
const invocationCloneMap = callSiteKey !== null ? new Map() : null;
|
|
2758
|
+
const fallbackCallSiteKey = `${callInfo.functionName}:${location.startLine}:${location.startCol}`;
|
|
2759
|
+
if (methodChildren.length > 0) {
|
|
2760
|
+
if (callSiteKey && methodBubbleMap.size > 0 && invocationCloneMap) {
|
|
2761
|
+
const clonedMethodChildren = this.cloneWorkflowNodesForInvocation(methodChildren, callSiteKey, methodBubbleMap, invocationCloneMap);
|
|
2762
|
+
children.push(...clonedMethodChildren);
|
|
2763
|
+
}
|
|
2764
|
+
else {
|
|
2765
|
+
children.push(...methodChildren);
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
// After method definition processing, check for callback arguments
|
|
2769
|
+
if (callInfo.callExpr && callInfo.callExpr.arguments.length > 0) {
|
|
2770
|
+
for (const arg of callInfo.callExpr.arguments) {
|
|
2771
|
+
if (arg.type === 'ArrowFunctionExpression' ||
|
|
2772
|
+
arg.type === 'FunctionExpression') {
|
|
2773
|
+
const callbackBody = this.extractCallbackBody(arg);
|
|
2774
|
+
if (callbackBody && callbackBody.length > 0) {
|
|
2775
|
+
const callbackStartLine = arg.loc?.start.line || 0;
|
|
2776
|
+
const callbackEndLine = arg.loc?.end.line || 0;
|
|
2777
|
+
const callbackBubbleMap = new Map();
|
|
2778
|
+
for (const [id, bubble] of bubbleMap.entries()) {
|
|
2779
|
+
if (bubble.location.startLine >= callbackStartLine &&
|
|
2780
|
+
bubble.location.endLine <= callbackEndLine) {
|
|
2781
|
+
callbackBubbleMap.set(id, bubble);
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
const callbackNodes = [];
|
|
2785
|
+
for (const callbackStmt of callbackBody) {
|
|
2786
|
+
const node = this.buildWorkflowNodeFromStatement(callbackStmt, callbackBubbleMap, scopeManager);
|
|
2787
|
+
if (node) {
|
|
2788
|
+
callbackNodes.push(node);
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
if (callSiteKey && callbackNodes.length > 0 && invocationCloneMap) {
|
|
2792
|
+
const clonedCallbacks = this.cloneWorkflowNodesForInvocation(callbackNodes, callSiteKey, callbackBubbleMap, invocationCloneMap);
|
|
2793
|
+
children.push(...clonedCallbacks);
|
|
2794
|
+
}
|
|
2795
|
+
else {
|
|
2796
|
+
children.push(...callbackNodes);
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2802
|
+
// Check if this function call contains any bubbles
|
|
2803
|
+
// Only create transformation_function if:
|
|
2804
|
+
// 1. It's a method call (this.methodName())
|
|
2805
|
+
// 2. A method definition was found (method exists in class)
|
|
2806
|
+
// 3. It has no bubbles in its children
|
|
2807
|
+
if (callInfo.isMethodCall &&
|
|
2808
|
+
methodDefinition &&
|
|
2809
|
+
!this.containsBubbles(children)) {
|
|
2810
|
+
// Get the entire method body code
|
|
2811
|
+
let fullCode = code;
|
|
2812
|
+
if (this.cachedAST) {
|
|
2813
|
+
const methodDef = this.findMethodDefinition(callInfo.functionName, this.cachedAST);
|
|
2814
|
+
if (methodDef && methodDef.body) {
|
|
2815
|
+
// Extract the entire method body code
|
|
2816
|
+
fullCode = this.bubbleScript.substring(methodDef.body.range[0], methodDef.body.range[1]);
|
|
2817
|
+
}
|
|
2818
|
+
}
|
|
2819
|
+
const idKey = callSiteKey ?? fallbackCallSiteKey;
|
|
2820
|
+
const variableId = hashToVariableId(idKey);
|
|
2821
|
+
const transformationNode = {
|
|
2822
|
+
type: 'transformation_function',
|
|
2823
|
+
location,
|
|
2824
|
+
code: fullCode,
|
|
2825
|
+
functionName: callInfo.functionName,
|
|
2826
|
+
isMethodCall: callInfo.isMethodCall,
|
|
2827
|
+
description,
|
|
2828
|
+
arguments: callInfo.arguments,
|
|
2829
|
+
variableId,
|
|
2830
|
+
methodDefinition,
|
|
2831
|
+
};
|
|
2832
|
+
// Add variable declaration if present
|
|
2833
|
+
if (stmt.type === 'VariableDeclaration' && stmt.declarations.length > 0) {
|
|
2834
|
+
const decl = stmt.declarations[0];
|
|
2835
|
+
if (decl.id.type === 'Identifier') {
|
|
2836
|
+
transformationNode.variableDeclaration = {
|
|
2837
|
+
variableName: decl.id.name,
|
|
2838
|
+
variableType: stmt.kind,
|
|
2839
|
+
};
|
|
2840
|
+
}
|
|
2841
|
+
}
|
|
2842
|
+
else if (stmt.type === 'ExpressionStatement' &&
|
|
2843
|
+
stmt.expression.type === 'AssignmentExpression' &&
|
|
2844
|
+
stmt.expression.left.type === 'Identifier') {
|
|
2845
|
+
transformationNode.variableDeclaration = {
|
|
2846
|
+
variableName: stmt.expression.left.name,
|
|
2847
|
+
variableType: 'let', // Assignment implies let/var, default to let
|
|
2848
|
+
};
|
|
2849
|
+
}
|
|
2850
|
+
return transformationNode;
|
|
2851
|
+
}
|
|
2852
|
+
const variableId = hashToVariableId(callSiteKey ?? fallbackCallSiteKey);
|
|
2853
|
+
const functionCallNode = {
|
|
2854
|
+
type: 'function_call',
|
|
2855
|
+
location,
|
|
2856
|
+
functionName: callInfo.functionName,
|
|
2857
|
+
isMethodCall: callInfo.isMethodCall,
|
|
2858
|
+
description,
|
|
2859
|
+
arguments: callInfo.arguments,
|
|
2860
|
+
code,
|
|
2861
|
+
variableId,
|
|
2862
|
+
methodDefinition,
|
|
2863
|
+
children,
|
|
2864
|
+
};
|
|
2865
|
+
// Add variable declaration if present
|
|
2866
|
+
if (stmt.type === 'VariableDeclaration' && stmt.declarations.length > 0) {
|
|
2867
|
+
const decl = stmt.declarations[0];
|
|
2868
|
+
if (decl.id.type === 'Identifier') {
|
|
2869
|
+
functionCallNode.variableDeclaration = {
|
|
2870
|
+
variableName: decl.id.name,
|
|
2871
|
+
variableType: stmt.kind,
|
|
2872
|
+
};
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
else if (stmt.type === 'ExpressionStatement' &&
|
|
2876
|
+
stmt.expression.type === 'AssignmentExpression' &&
|
|
2877
|
+
stmt.expression.left.type === 'Identifier') {
|
|
2878
|
+
functionCallNode.variableDeclaration = {
|
|
2879
|
+
variableName: stmt.expression.left.name,
|
|
2880
|
+
variableType: 'let', // Assignment implies let/var, default to let
|
|
2881
|
+
};
|
|
2882
|
+
}
|
|
2883
|
+
return functionCallNode;
|
|
2884
|
+
}
|
|
2885
|
+
/**
|
|
2886
|
+
* Extract the body of a callback function (arrow or regular function expression)
|
|
2887
|
+
* Handles both block statements and concise arrow functions
|
|
2888
|
+
*/
|
|
2889
|
+
extractCallbackBody(func) {
|
|
2890
|
+
// Handle block statement body: (x) => { statements }
|
|
2891
|
+
if (func.body.type === 'BlockStatement') {
|
|
2892
|
+
return func.body.body;
|
|
2893
|
+
}
|
|
2894
|
+
// Handle concise arrow function: (x) => expression
|
|
2895
|
+
// Convert expression to a synthetic return statement
|
|
2896
|
+
const syntheticReturn = {
|
|
2897
|
+
type: AST_NODE_TYPES.ReturnStatement,
|
|
2898
|
+
argument: func.body,
|
|
2899
|
+
range: func.body.range,
|
|
2900
|
+
loc: func.body.loc,
|
|
2901
|
+
parent: func,
|
|
2902
|
+
};
|
|
2903
|
+
return [syntheticReturn];
|
|
2904
|
+
}
|
|
2905
|
+
/**
|
|
2906
|
+
* Find array elements from .push() calls or .map() callbacks
|
|
2907
|
+
* Handles both patterns:
|
|
2908
|
+
* - .push(): array.push(item1, item2, ...)
|
|
2909
|
+
* - .map(): const promises = items.map(item => this.processItem(item))
|
|
2910
|
+
*/
|
|
2911
|
+
findArrayElements(arrayVarName, ast, contextLine, scopeManager) {
|
|
2912
|
+
const elements = [];
|
|
2913
|
+
const varId = this.findVariableIdByName(arrayVarName, contextLine, scopeManager);
|
|
2914
|
+
if (varId === undefined)
|
|
2915
|
+
return elements;
|
|
2916
|
+
const walk = (node) => {
|
|
2917
|
+
// Handle .push() calls: array.push(item1, item2, ...)
|
|
2918
|
+
if (node.type === 'CallExpression' &&
|
|
2919
|
+
node.callee.type === 'MemberExpression' &&
|
|
2920
|
+
node.callee.property.type === 'Identifier' &&
|
|
2921
|
+
node.callee.property.name === 'push' &&
|
|
2922
|
+
node.callee.object.type === 'Identifier' &&
|
|
2923
|
+
node.callee.object.name === arrayVarName) {
|
|
2924
|
+
const callLine = node.loc?.start.line || 0;
|
|
2925
|
+
if (this.findVariableIdByName(arrayVarName, callLine, scopeManager) ===
|
|
2926
|
+
varId) {
|
|
2927
|
+
node.arguments.forEach((arg) => {
|
|
2928
|
+
elements.push(arg);
|
|
2929
|
+
});
|
|
2930
|
+
}
|
|
2931
|
+
}
|
|
2932
|
+
// Handle .map() calls: const promises = items.map(item => ...)
|
|
2933
|
+
if (node.type === 'VariableDeclaration') {
|
|
2934
|
+
for (const decl of node.declarations) {
|
|
2935
|
+
if (decl.id.type === 'Identifier' &&
|
|
2936
|
+
decl.id.name === arrayVarName &&
|
|
2937
|
+
decl.init &&
|
|
2938
|
+
decl.init.type === 'CallExpression' &&
|
|
2939
|
+
decl.init.callee.type === 'MemberExpression' &&
|
|
2940
|
+
decl.init.callee.property.type === 'Identifier' &&
|
|
2941
|
+
decl.init.callee.property.name === 'map') {
|
|
2942
|
+
const declLine = node.loc?.start.line || 0;
|
|
2943
|
+
if (this.findVariableIdByName(arrayVarName, declLine, scopeManager) === varId) {
|
|
2944
|
+
if (decl.init.arguments.length > 0) {
|
|
2945
|
+
const callback = decl.init.arguments[0];
|
|
2946
|
+
const callbackExpr = this.extractCallbackExpression(callback);
|
|
2947
|
+
if (callbackExpr) {
|
|
2948
|
+
const sourceArray = decl.init.callee.object;
|
|
2949
|
+
const sourceElements = this.getSourceArrayElements(sourceArray, ast, declLine, scopeManager);
|
|
2950
|
+
// Create one expression per source element, or single fallback
|
|
2951
|
+
const count = sourceElements?.length || 1;
|
|
2952
|
+
for (let i = 0; i < count; i++) {
|
|
2953
|
+
elements.push(callbackExpr);
|
|
2954
|
+
}
|
|
2955
|
+
}
|
|
2956
|
+
}
|
|
2957
|
+
}
|
|
2958
|
+
}
|
|
2959
|
+
}
|
|
2960
|
+
}
|
|
2961
|
+
for (const key in node) {
|
|
2962
|
+
const child = node[key];
|
|
2963
|
+
if (Array.isArray(child))
|
|
2964
|
+
child.forEach(walk);
|
|
2965
|
+
else if (child?.type)
|
|
2966
|
+
walk(child);
|
|
2967
|
+
}
|
|
2968
|
+
};
|
|
2969
|
+
walk(ast);
|
|
2970
|
+
return elements;
|
|
2971
|
+
}
|
|
2972
|
+
/**
|
|
2973
|
+
* Extract expression from callback function
|
|
2974
|
+
*/
|
|
2975
|
+
extractCallbackExpression(callback) {
|
|
2976
|
+
if (callback.type === 'ArrowFunctionExpression' &&
|
|
2977
|
+
callback.body.type !== 'BlockStatement') {
|
|
2978
|
+
return callback.body;
|
|
2979
|
+
}
|
|
2980
|
+
if ((callback.type === 'ArrowFunctionExpression' ||
|
|
2981
|
+
callback.type === 'FunctionExpression') &&
|
|
2982
|
+
callback.body.type === 'BlockStatement') {
|
|
2983
|
+
const returns = this.findReturnStatements(callback.body);
|
|
2984
|
+
return returns[0]?.argument;
|
|
2985
|
+
}
|
|
2986
|
+
return null;
|
|
2987
|
+
}
|
|
2988
|
+
/**
|
|
2989
|
+
* Get elements from source array (literal or variable)
|
|
2990
|
+
*/
|
|
2991
|
+
getSourceArrayElements(sourceArray, ast, contextLine, scopeManager) {
|
|
2992
|
+
if (sourceArray.type === 'ArrayExpression') {
|
|
2993
|
+
return sourceArray.elements.filter((el) => el !== null && el.type !== 'SpreadElement');
|
|
2994
|
+
}
|
|
2995
|
+
if (sourceArray.type === 'Identifier') {
|
|
2996
|
+
const varId = this.findVariableIdByName(sourceArray.name, contextLine, scopeManager);
|
|
2997
|
+
if (varId === undefined)
|
|
2998
|
+
return null;
|
|
2999
|
+
const walk = (node) => {
|
|
3000
|
+
if (node.type === 'VariableDeclaration') {
|
|
3001
|
+
for (const decl of node.declarations) {
|
|
3002
|
+
if (decl.id.type === 'Identifier' &&
|
|
3003
|
+
decl.id.name === sourceArray.name &&
|
|
3004
|
+
decl.init?.type === 'ArrayExpression' &&
|
|
3005
|
+
this.findVariableIdByName(sourceArray.name, node.loc?.start.line || 0, scopeManager) === varId) {
|
|
3006
|
+
return decl.init.elements.filter((el) => el !== null && el.type !== 'SpreadElement');
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
3009
|
+
}
|
|
3010
|
+
for (const key in node) {
|
|
3011
|
+
const child = node[key];
|
|
3012
|
+
if (Array.isArray(child)) {
|
|
3013
|
+
for (const c of child) {
|
|
3014
|
+
const result = walk(c);
|
|
3015
|
+
if (result)
|
|
3016
|
+
return result;
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
3019
|
+
else if (child?.type) {
|
|
3020
|
+
const result = walk(child);
|
|
3021
|
+
if (result)
|
|
3022
|
+
return result;
|
|
3023
|
+
}
|
|
3024
|
+
}
|
|
3025
|
+
return null;
|
|
3026
|
+
};
|
|
3027
|
+
return walk(ast);
|
|
3028
|
+
}
|
|
3029
|
+
return null;
|
|
3030
|
+
}
|
|
3031
|
+
/**
|
|
3032
|
+
* Find all return statements in a block statement
|
|
3033
|
+
*/
|
|
3034
|
+
findReturnStatements(block) {
|
|
3035
|
+
const returns = [];
|
|
3036
|
+
const walk = (node) => {
|
|
3037
|
+
if (node.type === 'ReturnStatement') {
|
|
3038
|
+
returns.push(node);
|
|
3039
|
+
}
|
|
3040
|
+
for (const key in node) {
|
|
3041
|
+
const child = node[key];
|
|
3042
|
+
if (Array.isArray(child))
|
|
3043
|
+
child.forEach(walk);
|
|
3044
|
+
else if (child?.type)
|
|
3045
|
+
walk(child);
|
|
3046
|
+
}
|
|
3047
|
+
};
|
|
3048
|
+
walk(block);
|
|
3049
|
+
return returns;
|
|
3050
|
+
}
|
|
3051
|
+
/**
|
|
3052
|
+
* Build a parallel execution node from Promise.all()
|
|
3053
|
+
*/
|
|
3054
|
+
buildParallelExecutionNode(promiseAllInfo, stmt, bubbleMap, scopeManager) {
|
|
3055
|
+
const location = this.extractLocation(stmt);
|
|
3056
|
+
if (!location)
|
|
3057
|
+
return null;
|
|
3058
|
+
const code = this.bubbleScript.substring(stmt.range[0], stmt.range[1]);
|
|
3059
|
+
const children = [];
|
|
3060
|
+
// Handle variable reference (e.g., Promise.all(exampleScrapers))
|
|
3061
|
+
if (promiseAllInfo.arrayExpr.type === 'Identifier' && this.cachedAST) {
|
|
3062
|
+
const arrayVarName = promiseAllInfo.arrayExpr.name;
|
|
3063
|
+
const contextLine = promiseAllInfo.arrayExpr.loc?.start.line || 0;
|
|
3064
|
+
const pushedArgs = this.findArrayElements(arrayVarName, this.cachedAST, contextLine, scopeManager);
|
|
3065
|
+
for (const arg of pushedArgs) {
|
|
3066
|
+
const methodCall = this.detectFunctionCall(arg);
|
|
3067
|
+
if (methodCall) {
|
|
3068
|
+
const syntheticStmt = {
|
|
3069
|
+
type: AST_NODE_TYPES.ExpressionStatement,
|
|
3070
|
+
expression: methodCall.callExpr,
|
|
3071
|
+
range: arg.range,
|
|
3072
|
+
loc: arg.loc,
|
|
3073
|
+
parent: stmt,
|
|
3074
|
+
};
|
|
3075
|
+
const funcCallNode = this.buildFunctionCallNode(methodCall, syntheticStmt, bubbleMap, scopeManager);
|
|
3076
|
+
if (funcCallNode)
|
|
3077
|
+
children.push(funcCallNode);
|
|
3078
|
+
}
|
|
3079
|
+
else {
|
|
3080
|
+
const bubble = this.findBubbleInExpression(arg, bubbleMap);
|
|
3081
|
+
if (bubble) {
|
|
3082
|
+
children.push({ type: 'bubble', variableId: bubble.variableId });
|
|
3083
|
+
}
|
|
3084
|
+
}
|
|
3085
|
+
}
|
|
3086
|
+
}
|
|
3087
|
+
else if (promiseAllInfo.arrayExpr.type === 'ArrayExpression') {
|
|
3088
|
+
// Handle direct array literal (existing logic)
|
|
3089
|
+
for (const element of promiseAllInfo.arrayExpr.elements) {
|
|
3090
|
+
if (!element || element.type === 'SpreadElement')
|
|
3091
|
+
continue;
|
|
3092
|
+
const bubble = this.findBubbleInExpression(element, bubbleMap);
|
|
3093
|
+
if (bubble) {
|
|
3094
|
+
children.push({ type: 'bubble', variableId: bubble.variableId });
|
|
3095
|
+
continue;
|
|
3096
|
+
}
|
|
3097
|
+
const functionCall = this.detectFunctionCall(element);
|
|
3098
|
+
if (functionCall) {
|
|
3099
|
+
const syntheticStmt = {
|
|
3100
|
+
type: AST_NODE_TYPES.ExpressionStatement,
|
|
3101
|
+
expression: functionCall.callExpr,
|
|
3102
|
+
range: element.range,
|
|
3103
|
+
loc: element.loc,
|
|
3104
|
+
parent: stmt,
|
|
3105
|
+
};
|
|
3106
|
+
const funcCallNode = this.buildFunctionCallNode(functionCall, syntheticStmt, bubbleMap, scopeManager);
|
|
3107
|
+
if (funcCallNode)
|
|
3108
|
+
children.push(funcCallNode);
|
|
3109
|
+
}
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
3112
|
+
// Extract variable declaration info if this is part of a variable declaration
|
|
3113
|
+
let variableDeclaration;
|
|
3114
|
+
if (stmt.type === 'VariableDeclaration' &&
|
|
3115
|
+
stmt.declarations.length > 0 &&
|
|
3116
|
+
stmt.declarations[0].id.type === 'ArrayPattern') {
|
|
3117
|
+
const arrayPattern = stmt.declarations[0].id;
|
|
3118
|
+
const variableNames = [];
|
|
3119
|
+
for (const element of arrayPattern.elements) {
|
|
3120
|
+
if (element && element.type === 'Identifier') {
|
|
3121
|
+
variableNames.push(element.name);
|
|
3122
|
+
}
|
|
3123
|
+
}
|
|
3124
|
+
variableDeclaration = {
|
|
3125
|
+
variableNames,
|
|
3126
|
+
variableType: stmt.kind,
|
|
3127
|
+
};
|
|
3128
|
+
}
|
|
3129
|
+
return {
|
|
3130
|
+
type: 'parallel_execution',
|
|
3131
|
+
location,
|
|
3132
|
+
code,
|
|
3133
|
+
variableDeclaration,
|
|
3134
|
+
children,
|
|
3135
|
+
};
|
|
3136
|
+
}
|
|
3137
|
+
/**
|
|
3138
|
+
* Get the invocation index for a method call.
|
|
3139
|
+
* If the same call expression (identified by its AST range) has been processed before,
|
|
3140
|
+
* return the same index to avoid double-counting.
|
|
3141
|
+
*
|
|
3142
|
+
* @param methodName - The name of the method being called
|
|
3143
|
+
* @param callExprStartOffset - Optional start offset of the CallExpression in the source.
|
|
3144
|
+
* Used to deduplicate when the same call is processed multiple times
|
|
3145
|
+
* (e.g., .map() callback processing vs Promise.all resolution)
|
|
3146
|
+
*/
|
|
3147
|
+
getNextInvocationIndex(methodName, callExprStartOffset) {
|
|
3148
|
+
// Check if this specific call site has already been indexed
|
|
3149
|
+
if (callExprStartOffset !== undefined) {
|
|
3150
|
+
const callSiteId = `${methodName}:${callExprStartOffset}`;
|
|
3151
|
+
const existingIndex = this.processedCallSiteIndexes.get(callSiteId);
|
|
3152
|
+
if (existingIndex !== undefined) {
|
|
3153
|
+
return existingIndex;
|
|
3154
|
+
}
|
|
3155
|
+
// New call site - assign next index and cache it
|
|
3156
|
+
const next = (this.methodInvocationOrdinalMap.get(methodName) ?? 0) + 1;
|
|
3157
|
+
this.methodInvocationOrdinalMap.set(methodName, next);
|
|
3158
|
+
this.processedCallSiteIndexes.set(callSiteId, next);
|
|
3159
|
+
return next;
|
|
3160
|
+
}
|
|
3161
|
+
// Fallback: no offset provided, just increment (legacy behavior)
|
|
3162
|
+
const next = (this.methodInvocationOrdinalMap.get(methodName) ?? 0) + 1;
|
|
3163
|
+
this.methodInvocationOrdinalMap.set(methodName, next);
|
|
3164
|
+
return next;
|
|
3165
|
+
}
|
|
3166
|
+
cloneWorkflowNodesForInvocation(nodes, callSiteKey, bubbleSourceMap, localCloneMap) {
|
|
3167
|
+
return nodes.map((node) => this.cloneWorkflowNodeForInvocation(node, callSiteKey, bubbleSourceMap, localCloneMap));
|
|
3168
|
+
}
|
|
3169
|
+
cloneWorkflowNodeForInvocation(node, callSiteKey, bubbleSourceMap, localCloneMap) {
|
|
3170
|
+
if (node.type === 'bubble') {
|
|
3171
|
+
const originalId = Number(node.variableId);
|
|
3172
|
+
const clonedId = this.ensureClonedBubbleForInvocation(originalId, callSiteKey, bubbleSourceMap, localCloneMap);
|
|
3173
|
+
return { ...node, variableId: clonedId };
|
|
3174
|
+
}
|
|
3175
|
+
const clonedNode = { ...node };
|
|
3176
|
+
if ('children' in node && Array.isArray(node.children)) {
|
|
3177
|
+
clonedNode.children = node.children.map((child) => this.cloneWorkflowNodeForInvocation(child, callSiteKey, bubbleSourceMap, localCloneMap));
|
|
3178
|
+
}
|
|
3179
|
+
if ('elseBranch' in node && Array.isArray(node.elseBranch)) {
|
|
3180
|
+
clonedNode.elseBranch = node.elseBranch.map((child) => this.cloneWorkflowNodeForInvocation(child, callSiteKey, bubbleSourceMap, localCloneMap));
|
|
3181
|
+
}
|
|
3182
|
+
if ('catchBlock' in node && Array.isArray(node.catchBlock)) {
|
|
3183
|
+
clonedNode.catchBlock = node.catchBlock.map((child) => this.cloneWorkflowNodeForInvocation(child, callSiteKey, bubbleSourceMap, localCloneMap));
|
|
3184
|
+
}
|
|
3185
|
+
return clonedNode;
|
|
3186
|
+
}
|
|
3187
|
+
ensureClonedBubbleForInvocation(originalId, callSiteKey, bubbleSourceMap, localCloneMap) {
|
|
3188
|
+
const existing = localCloneMap.get(originalId);
|
|
3189
|
+
if (existing) {
|
|
3190
|
+
return existing;
|
|
3191
|
+
}
|
|
3192
|
+
const sourceBubble = bubbleSourceMap.get(originalId);
|
|
3193
|
+
if (!sourceBubble) {
|
|
3194
|
+
return originalId;
|
|
3195
|
+
}
|
|
3196
|
+
const clonedId = this.cloneBubbleForInvocation(sourceBubble, callSiteKey, bubbleSourceMap);
|
|
3197
|
+
localCloneMap.set(originalId, clonedId);
|
|
3198
|
+
return clonedId;
|
|
3199
|
+
}
|
|
3200
|
+
cloneBubbleForInvocation(bubble, callSiteKey, bubbleSourceMap) {
|
|
3201
|
+
const cacheKey = `${bubble.variableId}:${callSiteKey}`;
|
|
3202
|
+
const existing = this.invocationBubbleCloneCache.get(cacheKey);
|
|
3203
|
+
if (existing) {
|
|
3204
|
+
return existing.variableId;
|
|
3205
|
+
}
|
|
3206
|
+
const clonedBubble = {
|
|
3207
|
+
...bubble,
|
|
3208
|
+
variableId: hashToVariableId(cacheKey),
|
|
3209
|
+
invocationCallSiteKey: callSiteKey,
|
|
3210
|
+
clonedFromVariableId: bubble.variableId,
|
|
3211
|
+
parameters: JSON.parse(JSON.stringify(bubble.parameters)),
|
|
3212
|
+
dependencyGraph: bubble.dependencyGraph
|
|
3213
|
+
? this.cloneDependencyGraphNodeForInvocation(bubble.dependencyGraph, callSiteKey)
|
|
3214
|
+
: undefined,
|
|
3215
|
+
};
|
|
3216
|
+
/**
|
|
3217
|
+
* Also clone any bubbles that are referenced inside this bubble's
|
|
3218
|
+
* dependencyGraph.functionCallChildren (e.g. bubbles instantiated
|
|
3219
|
+
* inside AI agent customTools).
|
|
3220
|
+
*
|
|
3221
|
+
* This ensures that nested bubbles inside custom tools participate
|
|
3222
|
+
* in per-invocation cloning just like top-level workflow bubbles:
|
|
3223
|
+
* - They get their own cloned ParsedBubbleWithInfo entry
|
|
3224
|
+
* - clonedFromVariableId points back to the original id
|
|
3225
|
+
* - invocationCallSiteKey is set
|
|
3226
|
+
* - Their dependencyGraph is cloned with per-invocation uniqueId/variableId
|
|
3227
|
+
*
|
|
3228
|
+
* We then rewrite the functionCallChildren children variableIds in the
|
|
3229
|
+
* cloned dependencyGraph to point at the cloned bubble ids so that
|
|
3230
|
+
* __bubbleInvocationDependencyGraphs[callSiteKey][originalId] contains
|
|
3231
|
+
* a fully self-consistent graph for this invocation.
|
|
3232
|
+
*/
|
|
3233
|
+
if (bubble.dependencyGraph &&
|
|
3234
|
+
bubble.dependencyGraph.functionCallChildren &&
|
|
3235
|
+
Array.isArray(bubble.dependencyGraph.functionCallChildren)) {
|
|
3236
|
+
const clonedDepGraph = clonedBubble.dependencyGraph;
|
|
3237
|
+
if (clonedDepGraph &&
|
|
3238
|
+
Array.isArray(clonedDepGraph.functionCallChildren)) {
|
|
3239
|
+
clonedDepGraph.functionCallChildren =
|
|
3240
|
+
clonedDepGraph.functionCallChildren.map((funcCallNode) => {
|
|
3241
|
+
if (!Array.isArray(funcCallNode.children)) {
|
|
3242
|
+
return funcCallNode;
|
|
3243
|
+
}
|
|
3244
|
+
const clonedChildren = funcCallNode.children.map((child) => {
|
|
3245
|
+
if (!child ||
|
|
3246
|
+
child.type !== 'bubble' ||
|
|
3247
|
+
typeof child.variableId !== 'number') {
|
|
3248
|
+
return child;
|
|
3249
|
+
}
|
|
3250
|
+
const originalChildId = child.variableId;
|
|
3251
|
+
const sourceChild = bubbleSourceMap.get(originalChildId);
|
|
3252
|
+
if (!sourceChild) {
|
|
3253
|
+
return child;
|
|
3254
|
+
}
|
|
3255
|
+
const clonedChildId = this.cloneBubbleForInvocation(sourceChild, callSiteKey, bubbleSourceMap);
|
|
3256
|
+
return {
|
|
3257
|
+
...child,
|
|
3258
|
+
variableId: clonedChildId,
|
|
3259
|
+
};
|
|
3260
|
+
});
|
|
3261
|
+
return {
|
|
3262
|
+
...funcCallNode,
|
|
3263
|
+
children: clonedChildren,
|
|
3264
|
+
};
|
|
3265
|
+
});
|
|
3266
|
+
}
|
|
3267
|
+
}
|
|
3268
|
+
this.invocationBubbleCloneCache.set(cacheKey, clonedBubble);
|
|
3269
|
+
return clonedBubble.variableId;
|
|
3270
|
+
}
|
|
3271
|
+
cloneDependencyGraphNodeForInvocation(node, callSiteKey) {
|
|
3272
|
+
const uniqueId = node.uniqueId
|
|
3273
|
+
? `${node.uniqueId}@${callSiteKey}`
|
|
3274
|
+
: undefined;
|
|
3275
|
+
const variableId = typeof node.variableId === 'number'
|
|
3276
|
+
? hashToVariableId(`${node.variableId}:${callSiteKey}`)
|
|
3277
|
+
: undefined;
|
|
3278
|
+
return {
|
|
3279
|
+
...node,
|
|
3280
|
+
uniqueId,
|
|
3281
|
+
variableId,
|
|
3282
|
+
dependencies: node.dependencies.map((child) => this.cloneDependencyGraphNodeForInvocation(child, callSiteKey)),
|
|
3283
|
+
};
|
|
3284
|
+
}
|
|
3285
|
+
/**
|
|
3286
|
+
* Extract condition string from a test expression
|
|
3287
|
+
*/
|
|
3288
|
+
extractConditionString(test) {
|
|
3289
|
+
return this.bubbleScript.substring(test.range[0], test.range[1]);
|
|
3290
|
+
}
|
|
3291
|
+
/**
|
|
3292
|
+
* Extract location from a node
|
|
3293
|
+
*/
|
|
3294
|
+
extractLocation(node) {
|
|
3295
|
+
if (!node.loc) {
|
|
3296
|
+
return { startLine: 0, startCol: 0, endLine: 0, endCol: 0 };
|
|
3297
|
+
}
|
|
3298
|
+
return {
|
|
3299
|
+
startLine: node.loc.start.line,
|
|
3300
|
+
startCol: node.loc.start.column,
|
|
3301
|
+
endLine: node.loc.end.line,
|
|
3302
|
+
endCol: node.loc.end.column,
|
|
3303
|
+
};
|
|
3304
|
+
}
|
|
1151
3305
|
}
|
|
1152
3306
|
//# sourceMappingURL=BubbleParser.js.map
|