* Substitute all Ref(varName) nodes in an AST tree with a literal value. * This pre-resolves loop variables so deferred expressions (like Action steps) * don't lose scope when evaluated later at click time.
(node: ASTNode, varName: string, value: unknown)
| 369 | * don't lose scope when evaluated later at click time. |
| 370 | */ |
| 371 | function substituteRef(node: ASTNode, varName: string, value: unknown): ASTNode { |
| 372 | switch (node.k) { |
| 373 | case "Ref": |
| 374 | return node.n === varName ? toLiteralAST(value) : node; |
| 375 | case "Member": { |
| 376 | // Member access on the loop var: t.id → resolve t, then access .id |
| 377 | if (isASTNode(node.obj)) { |
| 378 | const subObj = substituteRef(node.obj as ASTNode, varName, value); |
| 379 | // If obj resolved to a literal, we can inline the member access result |
| 380 | if (subObj.k === "Obj") { |
| 381 | const entry = subObj.entries.find(([k]) => k === node.field); |
| 382 | if (entry) return entry[1]; |
| 383 | } |
| 384 | return { ...node, obj: subObj }; |
| 385 | } |
| 386 | return node; |
| 387 | } |
| 388 | case "Index": |
| 389 | return { |
| 390 | ...node, |
| 391 | obj: isASTNode(node.obj) ? substituteRef(node.obj as ASTNode, varName, value) : node.obj, |
| 392 | index: isASTNode(node.index) |
| 393 | ? substituteRef(node.index as ASTNode, varName, value) |
| 394 | : node.index, |
| 395 | }; |
| 396 | case "BinOp": |
| 397 | return { |
| 398 | ...node, |
| 399 | left: substituteRef(node.left, varName, value), |
| 400 | right: substituteRef(node.right, varName, value), |
| 401 | }; |
| 402 | case "UnaryOp": |
| 403 | return { ...node, operand: substituteRef(node.operand, varName, value) }; |
| 404 | case "Ternary": |
| 405 | return { |
| 406 | ...node, |
| 407 | cond: substituteRef(node.cond, varName, value), |
| 408 | then: substituteRef(node.then, varName, value), |
| 409 | else: substituteRef(node.else, varName, value), |
| 410 | }; |
| 411 | case "Arr": |
| 412 | return { ...node, els: node.els.map((e) => substituteRef(e, varName, value)) }; |
| 413 | case "Obj": |
| 414 | return { |
| 415 | ...node, |
| 416 | entries: node.entries.map( |
| 417 | ([k, v]) => [k, substituteRef(v, varName, value)] as [string, ASTNode], |
| 418 | ), |
| 419 | }; |
| 420 | case "Comp": { |
| 421 | const result = { ...node, args: node.args.map((a) => substituteRef(a, varName, value)) }; |
| 422 | // Also substitute in mappedProps (added by materializer for catalog components) |
| 423 | if (node.mappedProps) { |
| 424 | const subProps: Record<string, ASTNode> = {}; |
| 425 | for (const [k, v] of Object.entries(node.mappedProps)) { |
| 426 | subProps[k] = substituteRef(v, varName, value); |
| 427 | } |
| 428 | (result as any).mappedProps = subProps; |
no test coverage detected