diff --git a/src/asl.ts b/src/asl.ts index 35039ced..ab3a90e7 100644 --- a/src/asl.ts +++ b/src/asl.ts @@ -17,6 +17,7 @@ import { Identifier, NullLiteralExpr, PropAccessExpr, + QuasiString, } from "./expression"; import { isArgument, @@ -106,6 +107,7 @@ import { isTaggedTemplateExpr, isOmittedExpr, isFunctionLike, + isQuasiString, } from "./guards"; import { Integration, @@ -1576,7 +1578,7 @@ export class ASL { } throw new SynthError( ErrorCodes.Integration_must_be_immediately_awaited_or_returned, - `Integration must be immediately awaited or returned ${exprToString( + `Integration must be immediately awaited or returned ${nodeToString( expr )}` ); @@ -1594,7 +1596,11 @@ export class ASL { ); } else if (isTemplateExpr(expr)) { return this.evalContext(expr, (evalExpr) => { - const elementOutputs = expr.exprs.map(evalExpr); + const elementOutputs = expr.spans.map((span) => + isQuasiString(span) + ? { value: span.value, containsJsonPath: false } + : evalExpr(span) + ); /** * Step Functions `States.Format` has a bug which fails when a jsonPath does not start with a @@ -1861,7 +1867,7 @@ export class ASL { ); } throw new Error( - `call must be a service call or list .slice, .map, .forEach or .filter: ${exprToString( + `call must be a service call or list .slice, .map, .forEach or .filter: ${nodeToString( expr )}` ); @@ -2878,7 +2884,7 @@ export class ASL { throw new SynthError( ErrorCodes.StepFunction_invalid_filter_syntax, - `JSONPath's filter expression does not support '${exprToString(expr)}'` + `JSONPath's filter expression does not support '${nodeToString(expr)}'` ); }; @@ -3439,7 +3445,7 @@ export class ASL { } throw new SynthError( ErrorCodes.Integration_must_be_immediately_awaited_or_returned, - `Integration must be immediately awaited or returned ${exprToString( + `Integration must be immediately awaited or returned ${nodeToString( expr )}` ); @@ -4654,30 +4660,30 @@ function toStateName(node: FunctionlessNode): string { } } function inner(node: Exclude): string { - if (isExpr(node)) { - return exprToString(node); + if (isExpr(node) || isQuasiString(node)) { + return nodeToString(node); } else if (isIfStmt(node)) { - return `if(${exprToString(node.when)})`; + return `if(${nodeToString(node.when)})`; } else if (isExprStmt(node)) { - return exprToString(node.expr); + return nodeToString(node.expr); } else if (isBreakStmt(node)) { return "break"; } else if (isContinueStmt(node)) { return "continue"; } else if (isCatchClause(node)) { return `catch${ - node.variableDecl ? `(${exprToString(node.variableDecl)})` : "" + node.variableDecl ? `(${nodeToString(node.variableDecl)})` : "" }`; } else if (isDoStmt(node)) { - return `while (${exprToString(node.condition)})`; + return `while (${nodeToString(node.condition)})`; } else if (isForInStmt(node)) { return `for(${ isIdentifier(node.initializer) - ? exprToString(node.initializer) - : exprToString(node.initializer.name) - } in ${exprToString(node.expr)})`; + ? nodeToString(node.initializer) + : nodeToString(node.initializer.name) + } in ${nodeToString(node.expr)})`; } else if (isForOfStmt(node)) { - return `for(${exprToString(node.initializer)} of ${exprToString( + return `for(${nodeToString(node.initializer)} of ${nodeToString( node.expr )})`; } else if (isForStmt(node)) { @@ -4685,16 +4691,16 @@ function toStateName(node: FunctionlessNode): string { return `for(${ node.initializer && isVariableDeclList(node.initializer) ? inner(node.initializer) - : exprToString(node.initializer) - };${exprToString(node.condition)};${exprToString(node.incrementor)})`; + : nodeToString(node.initializer) + };${nodeToString(node.condition)};${nodeToString(node.incrementor)})`; } else if (isReturnStmt(node)) { if (node.expr) { - return `return ${exprToString(node.expr)}`; + return `return ${nodeToString(node.expr)}`; } else { return "return"; } } else if (isThrowStmt(node)) { - return `throw ${exprToString(node.expr)}`; + return `throw ${nodeToString(node.expr)}`; } else if (isTryStmt(node)) { return "try"; } else if (isVariableStmt(node)) { @@ -4703,16 +4709,16 @@ function toStateName(node: FunctionlessNode): string { return `${node.decls.map((v) => inner(v)).join(",")}`; } else if (isVariableDecl(node)) { return node.initializer - ? `${exprToString(node.name)} = ${exprToString(node.initializer)}` - : exprToString(node.name); + ? `${nodeToString(node.name)} = ${nodeToString(node.initializer)}` + : nodeToString(node.name); } else if (isWhileStmt(node)) { - return `while (${exprToString(node.condition)})`; + return `while (${nodeToString(node.condition)})`; } else if (isBindingElem(node)) { const binding = node.propertyName ? `${inner(node.propertyName)}: ${inner(node.name)}` : `${inner(node.name)}`; return node.initializer - ? `${binding} = ${exprToString(node.initializer)}` + ? `${binding} = ${nodeToString(node.initializer)}` : binding; } else if (isObjectBinding(node)) { return `{ ${node.bindings.map(inner).join(", ")} }`; @@ -4757,42 +4763,48 @@ function toStateName(node: FunctionlessNode): string { return inner(node); } -function exprToString( - expr?: Expr | ParameterDecl | BindingName | BindingElem | VariableDecl +function nodeToString( + expr?: + | Expr + | ParameterDecl + | BindingName + | BindingElem + | VariableDecl + | QuasiString ): string { if (!expr) { return ""; } else if (isArgument(expr)) { - return exprToString(expr.expr); + return nodeToString(expr.expr); } else if (isArrayLiteralExpr(expr)) { return `[${expr.items - .map((item) => (item ? exprToString(item) : "null")) + .map((item) => (item ? nodeToString(item) : "null")) .join(", ")}]`; } else if (isBigIntExpr(expr)) { return expr.value.toString(10); } else if (isBinaryExpr(expr)) { - return `${exprToString(expr.left)} ${expr.op} ${exprToString(expr.right)}`; + return `${nodeToString(expr.left)} ${expr.op} ${nodeToString(expr.right)}`; } else if (isBooleanLiteralExpr(expr)) { return `${expr.value}`; } else if (isCallExpr(expr) || isNewExpr(expr)) { if (isSuperKeyword(expr.expr) || isImportKeyword(expr.expr)) { throw new Error(`calling ${expr.expr.kindName} is unsupported in ASL`); } - return `${isNewExpr(expr) ? "new " : ""}${exprToString( + return `${isNewExpr(expr) ? "new " : ""}${nodeToString( expr.expr )}(${expr.args // Assume that undefined args are in order. .filter((arg) => arg && !isUndefinedLiteralExpr(arg)) - .map(exprToString) + .map(nodeToString) .join(", ")})`; } else if (isConditionExpr(expr)) { - return `if(${exprToString(expr.when)})`; + return `if(${nodeToString(expr.when)})`; } else if (isComputedPropertyNameExpr(expr)) { - return `[${exprToString(expr.expr)}]`; + return `[${nodeToString(expr.expr)}]`; } else if (isElementAccessExpr(expr)) { - return `${exprToString(expr.expr)}[${exprToString(expr.element)}]`; + return `${nodeToString(expr.expr)}[${nodeToString(expr.element)}]`; } else if (isFunctionExpr(expr) || isArrowFunctionExpr(expr)) { - return `function(${expr.parameters.map(exprToString).join(", ")})`; + return `function(${expr.parameters.map(nodeToString).join(", ")})`; } else if (isIdentifier(expr)) { return expr.name; } else if (isNullLiteralExpr(expr)) { @@ -4813,11 +4825,11 @@ function exprToString( ); } - return exprToString(prop); + return nodeToString(prop); }) .join(", ")}}`; } else if (isPropAccessExpr(expr)) { - return `${exprToString(expr.expr)}.${expr.name.name}`; + return `${nodeToString(expr.expr)}.${expr.name.name}`; } else if (isPropAssignExpr(expr)) { return `${ isIdentifier(expr.name) || isPrivateIdentifier(expr.name) @@ -4829,33 +4841,33 @@ function exprToString( : isComputedPropertyNameExpr(expr.name) ? isStringLiteralExpr(expr.name.expr) ? expr.name.expr.value - : exprToString(expr.name.expr) + : nodeToString(expr.name.expr) : assertNever(expr.name) - }: ${exprToString(expr.expr)}`; + }: ${nodeToString(expr.expr)}`; } else if (isReferenceExpr(expr)) { return expr.name; } else if (isSpreadAssignExpr(expr)) { - return `...${exprToString(expr.expr)}`; + return `...${nodeToString(expr.expr)}`; } else if (isSpreadElementExpr(expr)) { - return `...${exprToString(expr.expr)}`; + return `...${nodeToString(expr.expr)}`; } else if (isStringLiteralExpr(expr)) { return `"${expr.value}"`; } else if (isTemplateExpr(expr)) { - return `\`${expr.exprs - .map((e) => (isStringLiteralExpr(e) ? e.value : exprToString(e))) + return `\`${expr.spans + .map((e) => (isStringLiteralExpr(e) ? e.value : nodeToString(e))) .join("")}\``; } else if (isTypeOfExpr(expr)) { - return `typeof ${exprToString(expr.expr)}`; + return `typeof ${nodeToString(expr.expr)}`; } else if (isUnaryExpr(expr)) { - return `${expr.op}${exprToString(expr.expr)}`; + return `${expr.op}${nodeToString(expr.expr)}`; } else if (isPostfixUnaryExpr(expr)) { - return `${exprToString(expr.expr)}${expr.op}`; + return `${nodeToString(expr.expr)}${expr.op}`; } else if (isUndefinedLiteralExpr(expr)) { return "undefined"; } else if (isAwaitExpr(expr)) { - return `await ${exprToString(expr.expr)}`; + return `await ${nodeToString(expr.expr)}`; } else if (isPromiseExpr(expr) || isPromiseArrayExpr(expr)) { - return exprToString(expr.expr); + return nodeToString(expr.expr); } else if (isThisExpr(expr)) { return "this"; } else if (isClassExpr(expr)) { @@ -4866,31 +4878,31 @@ function exprToString( } else if (isPrivateIdentifier(expr)) { return expr.name; } else if (isYieldExpr(expr)) { - return `yield${expr.delegate ? "*" : ""} ${exprToString(expr.expr)}`; + return `yield${expr.delegate ? "*" : ""} ${nodeToString(expr.expr)}`; } else if (isRegexExpr(expr)) { return expr.regex.source; } else if (isDeleteExpr(expr)) { - return `delete ${exprToString(expr.expr)}`; + return `delete ${nodeToString(expr.expr)}`; } else if (isVoidExpr(expr)) { - return `void ${exprToString(expr.expr)}`; + return `void ${nodeToString(expr.expr)}`; } else if (isParenthesizedExpr(expr)) { - return exprToString(expr.expr); + return nodeToString(expr.expr); } else if (isObjectBinding(expr)) { - return `{${expr.bindings.map(exprToString).join(",")}}`; + return `{${expr.bindings.map(nodeToString).join(",")}}`; } else if (isArrayBinding(expr)) { - return `[${expr.bindings.map(exprToString).join(",")}]`; + return `[${expr.bindings.map(nodeToString).join(",")}]`; } else if (isBindingElem(expr)) { return `${expr.rest ? "..." : ""}${ expr.propertyName - ? `${exprToString(expr.propertyName)}:${exprToString(expr.name)}` - : `${exprToString(expr.name)}` + ? `${nodeToString(expr.propertyName)}:${nodeToString(expr.name)}` + : `${nodeToString(expr.name)}` }`; } else if (isVariableDecl(expr)) { - return `${exprToString(expr.name)}${ - expr.initializer ? ` = ${exprToString(expr.initializer)}` : "" + return `${nodeToString(expr.name)}${ + expr.initializer ? ` = ${nodeToString(expr.initializer)}` : "" }`; } else if (isParameterDecl(expr)) { - return exprToString(expr.name); + return nodeToString(expr.name); } else if (isTaggedTemplateExpr(expr)) { throw new SynthError( ErrorCodes.Unsupported_Feature, @@ -4898,6 +4910,8 @@ function exprToString( ); } else if (isOmittedExpr(expr)) { return "undefined"; + } else if (isQuasiString(expr)) { + return expr.value; } else { return assertNever(expr); } diff --git a/src/checker.ts b/src/checker.ts index 7c6923ed..fa653cd2 100644 --- a/src/checker.ts +++ b/src/checker.ts @@ -847,6 +847,9 @@ export function makeFunctionlessChecker( if ( ts.isStringLiteral(node) || ts.isNumericLiteral(node) || + ts.isTemplateHead(node) || + ts.isTemplateMiddle(node) || + ts.isTemplateTail(node) || node.kind === ts.SyntaxKind.TrueKeyword || node.kind === ts.SyntaxKind.FalseKeyword || node.kind === ts.SyntaxKind.NullKeyword || diff --git a/src/compile.ts b/src/compile.ts index f07633ac..2ae7506c 100644 --- a/src/compile.ts +++ b/src/compile.ts @@ -650,20 +650,30 @@ export function compile( toExpr(node.condition, scope), toExpr(node.incrementor, scope), ]); - } else if (ts.isTemplateExpression(node)) { + } else if ( + ts.isTemplateExpression(node) || + ts.isTaggedTemplateExpression(node) + ) { + const template = ts.isTemplateExpression(node) ? node : node.template; const exprs = []; - if (node.head.text) { - exprs.push(string(node.head.text)); + if (ts.isNoSubstitutionTemplateLiteral(template)) { + return newExpr(NodeKind.TaggedTemplateExpr, [quasi(template.text)]); + } + if (template.head.text) { + exprs.push(quasi(template.head.text)); } - for (const span of node.templateSpans) { + for (const span of template.templateSpans) { exprs.push(toExpr(span.expression, scope)); if (span.literal.text) { - exprs.push(string(span.literal.text)); + exprs.push(quasi(span.literal.text)); } } - return newExpr(NodeKind.TemplateExpr, [ - ts.factory.createArrayLiteralExpression(exprs), - ]); + return newExpr( + ts.isTemplateExpression(node) + ? NodeKind.TemplateExpr + : NodeKind.TaggedTemplateExpr, + [ts.factory.createArrayLiteralExpression(exprs)] + ); } else if (ts.isBreakStatement(node)) { return newExpr(NodeKind.BreakStmt, []); } else if (ts.isContinueStatement(node)) { @@ -856,6 +866,12 @@ export function compile( } } + function quasi(literal: string): ts.Expression { + return newExpr(NodeKind.QuasiString, [ + ts.factory.createStringLiteral(literal), + ]); + } + function string(literal: string): ts.Expression { return newExpr(NodeKind.StringLiteralExpr, [ ts.factory.createStringLiteral(literal), diff --git a/src/event-bridge/target-input.ts b/src/event-bridge/target-input.ts index 316e2f70..ef40d2ea 100644 --- a/src/event-bridge/target-input.ts +++ b/src/event-bridge/target-input.ts @@ -22,6 +22,7 @@ import { isPromiseExpr, isPropAccessExpr, isPropAssignExpr, + isQuasiString, isReferenceExpr, isStringLiteralExpr, isTemplateExpr, @@ -197,7 +198,9 @@ export const synthesizeEventBridgeTargets = ( } } else if (isTemplateExpr(expr)) { return { - value: expr.exprs.map((x) => exprToStringLiteral(x)).join(""), + value: expr.spans + .map((x) => (isQuasiString(x) ? x.value : exprToStringLiteral(x))) + .join(""), type: "string", }; } else if (isObjectLiteralExpr(expr) || isArrayLiteralExpr(expr)) { diff --git a/src/event-bridge/utils.ts b/src/event-bridge/utils.ts index 19eceaa3..93692fa5 100644 --- a/src/event-bridge/utils.ts +++ b/src/event-bridge/utils.ts @@ -28,6 +28,7 @@ import { isParenthesizedExpr, isPropAccessExpr, isPropAssignExpr, + isQuasiString, isReturnStmt, isSetAccessorDecl, isSpreadElementExpr, @@ -244,8 +245,8 @@ export const flattenExpression = (expr: Expr, scope: EventScope): Expr => { }, [] as PropAssignExpr[]) ); } else if (isTemplateExpr(expr)) { - const flattenedExpressions = expr.exprs.map((x) => - flattenExpression(x, scope) + const flattenedExpressions = expr.spans.map((x) => + isQuasiString(x) ? x : flattenExpression(x, scope) ); const flattenedConstants = flattenedExpressions.map((e) => @@ -258,7 +259,11 @@ export const flattenExpression = (expr: Expr, scope: EventScope): Expr => { ? new StringLiteralExpr( (flattenedConstants).map((e) => e.constant).join("") ) - : new TemplateExpr(expr.exprs.map((x) => flattenExpression(x, scope))); + : new TemplateExpr( + expr.spans.map((x) => + isQuasiString(x) ? x : flattenExpression(x, scope) + ) + ); } else { return expr; } diff --git a/src/expression.ts b/src/expression.ts index bb97eda4..3dd8769b 100644 --- a/src/expression.ts +++ b/src/expression.ts @@ -387,20 +387,47 @@ export class SpreadElementExpr extends BaseExpr< } } +/** + * A quasi string in a {@link TemplateExpr} or {@link TaggedTemplateExpr}. + * + * ```ts + * const s = `abc${def}` + * // ^ quasi + * ``` + */ +export class QuasiString extends BaseNode { + readonly nodeKind = "Node"; + constructor(readonly value: string) { + super(NodeKind.QuasiString, arguments); + } +} + +/** + * A span of text within a {@link TemplateExpr} or {@link TaggedTemplateExpr}. + * + * ```ts + * const s = `quasi ${expr}` + * // ^ Quasi string + * const s = `quasi ${expr}` + * // ^ expression to splice + * ``` + */ +export type TemplateSpan = QuasiString | Expr; + /** * Interpolates a TemplateExpr to a string `this ${is} a template expression` */ export class TemplateExpr extends BaseExpr { - constructor(readonly exprs: Expr[]) { + constructor(readonly spans: TemplateSpan[]) { super(NodeKind.TemplateExpr, arguments); - this.ensureArrayOf(exprs, "expr", ["Expr"]); + this.ensureArrayOf(spans, "span", [NodeKind.QuasiString, "Expr"]); } } export class TaggedTemplateExpr extends BaseExpr { - constructor(readonly tag: Expr, readonly exprs: Expr[]) { + constructor(readonly tag: Expr, readonly spans: TemplateSpan[]) { super(NodeKind.TaggedTemplateExpr, arguments); - this.ensureArrayOf(exprs, "expr", ["Expr"]); + this.ensureArrayOf(spans, "span", [NodeKind.QuasiString, "Expr"]); } } diff --git a/src/guards.ts b/src/guards.ts index 97f184f6..f96e0b7c 100644 --- a/src/guards.ts +++ b/src/guards.ts @@ -44,6 +44,7 @@ export const isPromiseArrayExpr = typeGuard(NodeKind.PromiseArrayExpr); export const isPromiseExpr = typeGuard(NodeKind.PromiseExpr); export const isPropAccessExpr = typeGuard(NodeKind.PropAccessExpr); export const isPropAssignExpr = typeGuard(NodeKind.PropAssignExpr); +export const isQuasiString = typeGuard(NodeKind.QuasiString); export const isReferenceExpr = typeGuard(NodeKind.ReferenceExpr); export const isRegexExpr = typeGuard(NodeKind.RegexExpr); export const isSpreadAssignExpr = typeGuard(NodeKind.SpreadAssignExpr); diff --git a/src/node-ctor.ts b/src/node-ctor.ts index 60100246..e4306302 100644 --- a/src/node-ctor.ts +++ b/src/node-ctor.ts @@ -44,6 +44,7 @@ import { PromiseExpr, PropAccessExpr, PropAssignExpr, + QuasiString, ReferenceExpr, RegexExpr, SpreadAssignExpr, @@ -111,6 +112,7 @@ export const declarations = { [NodeKind.SetAccessorDecl]: SetAccessorDecl, [NodeKind.VariableDecl]: VariableDecl, [NodeKind.VariableDeclList]: VariableDeclList, + [NodeKind.QuasiString]: QuasiString, } as const; export const error = { diff --git a/src/node-kind.ts b/src/node-kind.ts index d9692071..0811bf14 100644 --- a/src/node-kind.ts +++ b/src/node-kind.ts @@ -78,6 +78,7 @@ export enum NodeKind { WhileStmt = 76, WithStmt = 77, YieldExpr = 78, + QuasiString = 79, } export namespace NodeKind { diff --git a/src/node.ts b/src/node.ts index a8fbfbc9..1c040fcc 100644 --- a/src/node.ts +++ b/src/node.ts @@ -13,7 +13,12 @@ import { ensureArrayOf, } from "./ensure"; import type { Err } from "./error"; -import type { Expr, ImportKeyword, SuperKeyword } from "./expression"; +import type { + Expr, + ImportKeyword, + QuasiString, + SuperKeyword, +} from "./expression"; import { isBindingElem, isBindingPattern, @@ -44,7 +49,8 @@ export type FunctionlessNode = | SuperKeyword | ImportKeyword | BindingPattern - | VariableDeclList; + | VariableDeclList + | QuasiString; export interface HasParent { get parent(): Parent; diff --git a/src/util.ts b/src/util.ts index 6be45789..4dbf2b4b 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,6 +1,12 @@ import { Construct } from "constructs"; import ts from "typescript"; -import { BinaryOp, CallExpr, Expr, PropAccessExpr } from "./expression"; +import { + BinaryOp, + CallExpr, + Expr, + PropAccessExpr, + QuasiString, +} from "./expression"; import { isArrayLiteralExpr, isBinaryExpr, @@ -16,6 +22,7 @@ import { isPrivateIdentifier, isPropAccessExpr, isPropAssignExpr, + isQuasiString, isReferenceExpr, isSpreadAssignExpr, isSpreadElementExpr, @@ -199,13 +206,16 @@ export function isConstant(x: any): x is Constant { * const obj = { val: "hello" }; * obj.val -> { constant: "hello" } */ -export const evalToConstant = (expr: Expr): Constant | undefined => { +export const evalToConstant = ( + expr: Expr | QuasiString +): Constant | undefined => { if ( isStringLiteralExpr(expr) || isNumberLiteralExpr(expr) || isBooleanLiteralExpr(expr) || isNullLiteralExpr(expr) || - isUndefinedLiteralExpr(expr) + isUndefinedLiteralExpr(expr) || + isQuasiString(expr) ) { return { constant: expr.value }; } else if (isArrayLiteralExpr(expr)) { @@ -311,7 +321,7 @@ export const evalToConstant = (expr: Expr): Constant | undefined => { } } } else if (isTemplateExpr(expr)) { - const values = expr.exprs.map(evalToConstant); + const values = expr.spans.map(evalToConstant); if (values.every((v): v is Constant => !!v)) { return { constant: values.map((v) => v.constant).join("") }; } diff --git a/src/vtl.ts b/src/vtl.ts index 5cf6b39b..d4c57500 100644 --- a/src/vtl.ts +++ b/src/vtl.ts @@ -69,6 +69,7 @@ import { isPropAccessExpr, isPropAssignExpr, isPropDecl, + isQuasiString, isReferenceExpr, isRegexExpr, isReturnStmt, @@ -634,9 +635,9 @@ export abstract class VTL { } else if (isStringLiteralExpr(node)) { return this.str(node.value); } else if (isTemplateExpr(node)) { - return `"${node.exprs + return `"${node.spans .map((expr) => { - if (isStringLiteralExpr(expr)) { + if (isQuasiString(expr) || isStringLiteralExpr(expr)) { return expr.value; } const text = this.eval(expr, returnVar);