diff --git a/src/api.ts b/src/api.ts index 9ac076c4..1490f539 100644 --- a/src/api.ts +++ b/src/api.ts @@ -6,7 +6,13 @@ import { } from "aws-lambda"; import { FunctionDecl, validateFunctionDecl } from "./declaration"; import { ErrorCodes, SynthError } from "./error-code"; -import { CallExpr, Expr, Identifier, ReferenceExpr } from "./expression"; +import { + CallExpr, + Expr, + Identifier, + ReferenceExpr, + ThisExpr, +} from "./expression"; import { Function } from "./function"; import { isReturnStmt, @@ -28,7 +34,9 @@ import { isReferenceExpr, isAwaitExpr, isPromiseExpr, + isThisExpr, isVariableDecl, + isParenthesizedExpr, } from "./guards"; import { Integration, IntegrationImpl, isIntegration } from "./integration"; import { Stmt } from "./statement"; @@ -643,8 +651,9 @@ export class APIGatewayVTL extends VTL { return this.add(this.exprToJson(node.expr)); } else if ( isPropAccessExpr(node) && + isIdentifier(node.name) && isInputBody(node.expr) && - node.name === "data" + node.name.name === "data" ) { // $input.data maps to `$input.path('$')` // this returns a VTL object representing the root payload data @@ -695,6 +704,8 @@ export class APIGatewayVTL extends VTL { const jsonPath = toJsonPath(expr); if (jsonPath) { return `$input.json('${jsonPath}')`; + } else if (isParenthesizedExpr(expr)) { + return this.exprToJson(expr.expr); } else if (isNullLiteralExpr(expr) || isUndefinedLiteralExpr(expr)) { // Undefined is not the same as null. In JSON terms, `undefined` is the absence of a value where-as `null` is a present null value. return "null"; @@ -715,7 +726,7 @@ export class APIGatewayVTL extends VTL { return this.exprToJson(expr.expr); } } else if (isCallExpr(expr)) { - if (isReferenceExpr(expr.expr)) { + if (isReferenceExpr(expr.expr) || isThisExpr(expr.expr)) { const ref = expr.expr.ref(); if (isIntegration(ref)) { const serviceCall = new IntegrationImpl(ref); @@ -730,7 +741,11 @@ export class APIGatewayVTL extends VTL { } } else if (isIdentifier(expr.expr) && expr.expr.name === "Number") { return this.exprToJson(expr.args[0]); - } else if (isPropAccessExpr(expr.expr) && expr.expr.name === "params") { + } else if ( + isPropAccessExpr(expr.expr) && + isIdentifier(expr.expr.name) && + expr.expr.name.name === "params" + ) { if (isIdentifier(expr.expr.expr)) { const ref = expr.expr.expr.lookup(); if ( @@ -740,7 +755,7 @@ export class APIGatewayVTL extends VTL { ref.parent.parameters.findIndex((param) => param === ref) === 0 ) { // the first argument of the FunctionDecl is the `$input`, regardless of what it is named - if (expr.args.length === 0 || expr.args[0]?.expr === undefined) { + if (expr.args.length === 0 || expr.args[0].expr === undefined) { const key = this.newLocalVarName(); return `{#foreach(${key} in $input.params().keySet())"${key}": "$input.params("${key}")"#if($foreach.hasNext),#end#end}`; } else if (expr.args.length === 1) { @@ -819,18 +834,24 @@ export class APIGatewayVTL extends VTL { * @returns a JSON Path `string` if this {@link Expr} can be evaluated as a JSON Path from the `$input`, otherwise `undefined`. */ function toJsonPath(expr: Expr): string | undefined { - if (isInputBody(expr)) { + if (isParenthesizedExpr(expr)) { + return toJsonPath(expr.expr); + } else if (isInputBody(expr)) { return "$"; } else if (isIdentifier(expr)) { // this is a reference to an intermediate value, cannot be expressed as JSON Path return undefined; } else if (isPropAccessExpr(expr)) { - if (expr.name === "data" && isInputBody(expr.expr)) { + if ( + isIdentifier(expr.name) && + expr.name.name === "data" && + isInputBody(expr.expr) + ) { return "$"; } const exprJsonPath = toJsonPath(expr.expr); if (exprJsonPath !== undefined) { - return `${exprJsonPath}.${expr.name}`; + return `${exprJsonPath}.${expr.name.name}`; } } else if ( isElementAccessExpr(expr) && @@ -883,8 +904,10 @@ ${reference} * @param id the {@link Identifier} expression. * @returns a VTL string that points to the value at runtime. */ - public override dereference(id: Identifier | ReferenceExpr): string { - if (isReferenceExpr(id)) { + public override dereference( + id: Identifier | ReferenceExpr | ThisExpr + ): string { + if (isReferenceExpr(id) || isThisExpr(id)) { throw new SynthError(ErrorCodes.ApiGateway_Unsupported_Reference); } else { const ref = id.lookup(); diff --git a/src/appsync.ts b/src/appsync.ts index ffc436c3..a1737ded 100644 --- a/src/appsync.ts +++ b/src/appsync.ts @@ -9,6 +9,7 @@ import { import { FunctionDecl, validateFunctionDecl, + VariableDecl, VariableDeclList, } from "./declaration"; import { ErrorCodes, SynthError } from "./error-code"; @@ -22,6 +23,7 @@ import { PropAccessExpr, ReferenceExpr, StringLiteralExpr, + ThisExpr, } from "./expression"; import { isVariableStmt, @@ -43,7 +45,9 @@ import { isBindingElem, isBindingPattern, isReferenceExpr, + isThisExpr, isVariableDecl, + isIdentifier, } from "./guards"; import { findDeepIntegrations, @@ -146,8 +150,8 @@ export class AppsyncVTL extends VTL { } } - protected dereference(id: Identifier | ReferenceExpr): string { - if (isReferenceExpr(id)) { + protected dereference(id: Identifier | ReferenceExpr | ThisExpr): string { + if (isReferenceExpr(id) || isThisExpr(id)) { const ref = id.ref(); if (ref === $util) { return "$util"; @@ -585,13 +589,21 @@ function synthesizeFunctions(api: appsync.GraphqlApi, decl: FunctionDecl) { stmt.declList.decls[0].initializer && isIntegrationCallPattern(stmt.declList.decls[0].initializer) ) { - const decl = stmt.declList.decls[0]; - + const decl: VariableDecl | undefined = stmt.declList.decls[0]; + const varName = isIdentifier(decl?.name) ? decl.name.name : undefined; + if (varName === undefined) { + throw new SynthError( + ErrorCodes.Unsupported_Feature, + "Destructured parameter declarations are not yet supported by Appsync. https://github.com/functionless/functionless/issues/364" + ); + } return createStage( service, - `${pre ? `${pre}\n` : ""}#set( $context.stash.${ - decl.name - } = ${getResult(decl.initializer)} )\n{}` + `${ + pre ? `${pre}\n` : "" + }#set( $context.stash.${varName} = ${getResult( + decl.initializer + )} )\n{}` ); } else { throw new SynthError( @@ -621,7 +633,7 @@ function synthesizeFunctions(api: appsync.GraphqlApi, decl: FunctionDecl) { template.call(expr); return returnValName; } else if (isPropAccessExpr(expr)) { - return `${getResult(expr.expr)}.${expr.name}`; + return `${getResult(expr.expr)}.${expr.name.name}`; } else if (isElementAccessExpr(expr)) { return `${getResult(expr.expr)}[${getResult(expr.element)}]`; } else if (isPromiseExpr(expr)) { diff --git a/src/asl.ts b/src/asl.ts index 3520339c..002077b0 100644 --- a/src/asl.ts +++ b/src/asl.ts @@ -1,75 +1,105 @@ import { aws_iam } from "aws-cdk-lib"; import { Construct } from "constructs"; import { assertNever } from "./assert"; -import { Decl, FunctionDecl, VariableDecl } from "./declaration"; +import { + BindingElem, + BindingName, + Decl, + FunctionDecl, + ParameterDecl, + VariableDecl, +} from "./declaration"; import { ErrorCodes, SynthError } from "./error-code"; import { - Argument, CallExpr, ElementAccessExpr, Expr, - Identifier, NullLiteralExpr, PropAccessExpr, } from "./expression"; import { + isArgument, + isArrayBinding, + isArrayLiteralExpr, + isArrowFunctionExpr, + isAwaitExpr, + isBinaryExpr, + isBindingElem, + isBindingPattern, isBlockStmt, - isFunctionExpr, - isFunctionDecl, - isExprStmt, - isVariableStmt, - isReturnStmt, - isCallExpr, + isBooleanLiteralExpr, isBreakStmt, - isForInStmt, - isDoStmt, + isCallExpr, + isCaseClause, + isCatchClause, + isClassDecl, + isClassExpr, + isClassStaticBlockDecl, + isComputedPropertyNameExpr, + isConditionExpr, + isConstructorDecl, isContinueStmt, + isDebuggerStmt, + isDefaultClause, + isDoStmt, + isElementAccessExpr, + isEmptyStmt, + isErr, + isExpr, + isExprStmt, + isForInStmt, + isForOfStmt, + isFunctionDecl, + isFunctionExpr, + isIdentifier, isIfStmt, - isNullLiteralExpr, - isUndefinedLiteralExpr, - isThrowStmt, - isNewExpr, - isTryStmt, - isPropAccessExpr, + isLabelledStmt, isLiteralExpr, + isMethodDecl, + isNewExpr, + isNode, + isNullLiteralExpr, + isNumberLiteralExpr, + isObjectBinding, isObjectLiteralExpr, - isBinaryExpr, - isUnaryExpr, - isArgument, - isElementAccessExpr, - isArrayLiteralExpr, - isPropAssignExpr, - isComputedPropertyNameExpr, - isStringLiteralExpr, - isTemplateExpr, isParameterDecl, - isBooleanLiteralExpr, - isNumberLiteralExpr, - isTypeOfExpr, - isConditionExpr, - isSpreadAssignExpr, - isSpreadElementExpr, - isCatchClause, - isIdentifier, - isAwaitExpr, - isForOfStmt, + isPostfixUnaryExpr, isPromiseArrayExpr, isPromiseExpr, - isWhileStmt, + isPropAccessExpr, + isPropAssignExpr, + isPropDecl, isReferenceExpr, + isReturnStmt, + isSpreadAssignExpr, + isSpreadElementExpr, isStmt, - isPostfixUnaryExpr, + isStringLiteralExpr, + isSuperKeyword, + isSwitchStmt, + isTemplateExpr, + isThisExpr, + isThrowStmt, + isTryStmt, + isTypeOfExpr, + isUnaryExpr, + isUndefinedLiteralExpr, isVariableReference, - isBindingPattern, - isExpr, - isBindingElem, - isObjectBinding, - isArrayBinding, - isErr, - isNode, + isVariableStmt, + isWhileStmt, + isWithStmt, isForStmt, isVariableDeclList, isVariableDecl, + isClassMember, + isPrivateIdentifier, + isYieldExpr, + isBigIntExpr, + isRegexExpr, + isDeleteExpr, + isVoidExpr, + isParenthesizedExpr, + isImportKeyword, } from "./guards"; import { Integration, @@ -87,6 +117,7 @@ import { ReturnStmt, Stmt, } from "./statement"; +import { StepFunctionError } from "./step-function"; import { anyOf, DeterministicNameGenerator, @@ -484,6 +515,12 @@ export class ASL { }); const inputName = decl.parameters[0]?.name; + if (inputName && !isIdentifier(inputName)) { + throw new SynthError( + ErrorCodes.Unsupported_Feature, + "Destructured parameter declarations are not yet supported by Step Functions. https://github.com/functionless/functionless/issues/364" + ); + } const states = this.evalStmt(this.decl.body); @@ -500,7 +537,11 @@ export class ASL { Type: "Pass", Parameters: { [FUNCTIONLESS_CONTEXT_NAME]: { null: null }, - ...(inputName ? { [`${inputName}.$`]: "$" } : {}), + ...(inputName + ? { + [`${inputName.name}.$`]: "$", + } + : {}), }, ResultPath: "$", Next: ASLGraph.DeferNext, @@ -674,6 +715,20 @@ export class ASL { ResultPath: tempArrayPath, })!; + const initializerName = isIdentifier(stmt.initializer) + ? stmt.initializer.name + : isVariableDecl(stmt.initializer) && + isIdentifier(stmt.initializer.name) + ? stmt.initializer.name.name + : undefined; + + if (initializerName === undefined) { + throw new SynthError( + ErrorCodes.Unsupported_Feature, + "Destructured parameter declarations are not yet supported by Step Functions. https://github.com/functionless/functionless/issues/364" + ); + } + return { startState: "assignTemp", node: stmt, @@ -696,9 +751,9 @@ export class ASL { assign: isForOfStmt(stmt) ? { Type: "Pass", - node: stmt.variableDecl, + node: stmt.initializer, InputPath: `${tempArrayPath}[0]`, - ResultPath: `$.${stmt.variableDecl.name}`, + ResultPath: `$.${initializerName}`, Next: "body", } : /**ForInStmt @@ -707,18 +762,18 @@ export class ASL { */ { startState: "assignIndex", - node: stmt.variableDecl, + node: stmt.initializer, states: { assignIndex: { Type: "Pass", InputPath: `${tempArrayPath}[0].index`, - ResultPath: `$.${stmt.variableDecl.name}`, + ResultPath: `$.${initializerName}`, Next: "assignValue", }, assignValue: { Type: "Pass", InputPath: `${tempArrayPath}[0].item`, - ResultPath: `$.0__${stmt.variableDecl.name}`, + ResultPath: `$.0__${initializerName}`, Next: "body", }, }, @@ -758,10 +813,10 @@ export class ASL { const body = this.evalStmt(stmt.body); return this.evalContextToSubState(stmt, (evalExpr) => { - const initializers = stmt.variableDecl - ? isVariableDeclList(stmt.variableDecl) - ? stmt.variableDecl.decls.map((x) => this.evalStmt(x)) - : [evalExpr(stmt.variableDecl)] + const initializers = stmt.initializer + ? isVariableDeclList(stmt.initializer) + ? stmt.initializer.decls.map((x) => this.evalStmt(x)) + : [evalExpr(stmt.initializer)] : [undefined]; const [cond, condStates] = stmt.condition @@ -883,11 +938,19 @@ export class ASL { } return this.evalExprToSubState(stmt.initializer, (exprOutput) => { + const name = isIdentifier(stmt.name) ? stmt.name.name : undefined; + if (name === undefined) { + throw new SynthError( + ErrorCodes.Unsupported_Feature, + "Destructured parameter declarations are not yet supported by Step Functions. https://github.com/functionless/functionless/issues/364" + ); + } + return ASLGraph.passWithInput( { Type: "Pass" as const, // TODO support binding pattern - https://github.com/functionless/functionless/issues/302 - ResultPath: `$.${stmt.name}`, + ResultPath: `$.${name}`, }, exprOutput ); @@ -920,42 +983,125 @@ export class ASL { : stmt.expr.expr; const throwState = this.evalContextToSubState(updated, (evalExpr) => { - const error = Object.fromEntries( - updated.args - .filter((arg): arg is Argument & { expr: Expr } => !!arg.expr) - .map((arg) => { - const output = evalExpr(arg.expr); - // https://stackoverflow.com/questions/67794661/propogating-error-message-through-fail-state-in-aws-step-functions?answertab=trending#tab-top - if (!ASLGraph.isLiteralValue(output) || output.containsJsonPath) { - throw new SynthError( - ErrorCodes.StepFunctions_error_cause_must_be_a_constant - ); - } - return [arg.name!, output.value]; - }) - ); + const errorClassName = + // new StepFunctionError will be a ReferenceExpr with the name: Step + isReferenceExpr(updated.expr) && + StepFunctionError.isConstructor(updated.expr.ref()) + ? StepFunctionError.kind + : isReferenceExpr(updated.expr) || isIdentifier(updated.expr) + ? updated.expr.name + : isPropAccessExpr(updated.expr) + ? updated.expr.name.name + : undefined; + + // we support three ways of throwing errors within Step Functions + // throw new Error(msg) + // throw Error(msg) + // throw StepFunctionError(cause, message); + + const { errorName, causeJson } = resolveErrorNameAndCause(); const throwTransition = this.throw(stmt); if (throwTransition === undefined) { return { Type: "Fail", - Error: exprToString(updated.expr), - Cause: JSON.stringify(error), + Error: errorName, + Cause: JSON.stringify(causeJson), }; } else { return { Type: "Pass", - Result: error, + Result: causeJson, ...throwTransition, }; } + + function resolveErrorNameAndCause(): { + errorName: string; + causeJson: unknown; + } { + if (errorClassName === "Error") { + const errorMessage = updated.args[0]?.expr; + if ( + errorMessage === undefined || + isUndefinedLiteralExpr(errorMessage) + ) { + return { + errorName: "Error", + causeJson: { + message: null, + }, + }; + } else { + return { + errorName: "Error", + causeJson: { + message: toJson(errorMessage), + }, + }; + } + } else if (errorClassName === "StepFunctionError") { + const [error, cause] = updated.args.map(({ expr }) => expr); + if (error === undefined || cause === undefined) { + // this should never happen if typescript type checking is enabled + // hence why we don't add a new ErrorCode for it + throw new SynthError( + ErrorCodes.Unexpected_Error, + `Expected 'error' and 'cause' parameter in StepFunctionError` + ); + } + const errorName = toJson(error); + if (typeof errorName !== "string") { + // this should never happen if typescript type checking is enabled + // hence why we don't add a new ErrorCode for it + throw new SynthError( + ErrorCodes.Unexpected_Error, + `Expected 'error' parameter in StepFunctionError to be of type string, but got ${typeof errorName}` + ); + } + try { + return { + errorName, + causeJson: toJson(cause), + }; + } catch (err: any) { + throw new SynthError( + ErrorCodes.StepFunctions_error_cause_must_be_a_constant, + err.message + ); + } + } else { + throw new SynthError( + ErrorCodes.StepFunction_Throw_must_be_Error_or_StepFunctionError_class + ); + } + } + + /** + * Attempts to convert a Node into a JSON object. + * + * Only literal expression types are supported - no computation. + */ + function toJson(expr: Expr): unknown { + const val = evalExpr(expr); + if (!ASLGraph.isLiteralValue(val) || val.containsJsonPath) { + throw new SynthError( + ErrorCodes.StepFunctions_error_cause_must_be_a_constant + ); + } + return val.value; + } }); return { ...throwState, node: stmt }; } else if (isTryStmt(stmt)) { const tryFlow = analyzeFlow(stmt.tryBlock); - const errorVariableName = stmt.catchClause?.variableDecl?.name; + const errorVariableName = isIdentifier( + stmt.catchClause?.variableDecl?.name + ) + ? stmt.catchClause!.variableDecl!.name.name + : undefined; const tryState = { startState: "try", @@ -1145,6 +1291,25 @@ export class ASL { }, }; }); + } else if (isDebuggerStmt(stmt) || isEmptyStmt(stmt)) { + return undefined; + } else if (isLabelledStmt(stmt)) { + return this.evalStmt(stmt.stmt); + } else if (isWithStmt(stmt)) { + throw new SynthError( + ErrorCodes.Unsupported_Feature, + `with statements are not yet supported by ASL` + ); + } else if ( + isSwitchStmt(stmt) || + isCaseClause(stmt) || + isDefaultClause(stmt) + ) { + // see: https://github.com/functionless/functionless/issues/306 + throw new SynthError( + ErrorCodes.Unsupported_Feature, + `switch statements are not yet supported in Step Functions, see https://github.com/functionless/functionless/issues/306` + ); } return assertNever(stmt); } @@ -1547,7 +1712,7 @@ export class ASL { } else if (isMapOrForEach(expr)) { const throwTransition = this.throw(expr); - const callbackfn = expr.getArgument("callbackfn")?.expr; + const callbackfn = expr.args[0].expr; if (callbackfn !== undefined && isFunctionExpr(callbackfn)) { const callbackStates = this.evalStmt(callbackfn.body); @@ -1589,14 +1754,26 @@ export class ASL { Parameters: { ...this.cloneLexicalScopeParameters(expr), ...Object.fromEntries( - callbackfn.parameters.map((param, i) => [ - `${param.name}.$`, - i === 0 - ? "$$.Map.Item.Value" - : i == 1 - ? "$$.Map.Item.Index" - : listPath, - ]) + callbackfn.parameters.map((param, i) => { + const paramName = isIdentifier(param.name) + ? param.name.name + : undefined; + + if (paramName === undefined) { + throw new SynthError( + ErrorCodes.Unsupported_Feature, + "Destructured parameter declarations are not yet supported by Step Functions. https://github.com/functionless/functionless/issues/364" + ); + } + return [ + `${paramName}.$`, + i === 0 + ? "$$.Map.Item.Value" + : i == 1 + ? "$$.Map.Item.Index" + : listPath, + ]; + }) ), }, ...(throwTransition @@ -1622,23 +1799,24 @@ export class ASL { } else if (isJoin(expr)) { return this.joinToStateOutput(expr); } else if (isPromiseAll(expr)) { - const values = expr.getArgument("values"); + const values = expr.args[0]?.expr; // just validate Promise.all and continue, will validate the PromiseArray later. - if (values?.expr && isPromiseArrayExpr(values?.expr)) { - return this.eval(values.expr); + if (values && isPromiseArrayExpr(values)) { + return this.eval(values); } throw new SynthError(ErrorCodes.Unsupported_Use_of_Promises); } else if ( isPropAccessExpr(expr.expr) && + isIdentifier(expr.expr.name) && ((isIdentifier(expr.expr.expr) && expr.expr.expr.name === "JSON") || (isReferenceExpr(expr.expr.expr) && expr.expr.expr.ref() === JSON)) && - (expr.expr.name === "stringify" || expr.expr.name === "parse") + (expr.expr.name.name === "stringify" || expr.expr.name.name === "parse") ) { const heap = this.newHeapVariable(); - const objParamExpr = expr.args[0].expr; + const objParamExpr = expr.args[0]?.expr; if (!objParamExpr || isUndefinedLiteralExpr(objParamExpr)) { - if (expr.expr.name === "stringify") { + if (expr.expr.name.name === "stringify") { // return an undefined variable return { jsonPath: heap, @@ -1662,7 +1840,9 @@ export class ASL { // intrinsic functions cannot be used in InputPath and some other json path locations. // We compute the value and place it on the heap. "string.$": - (expr.expr).name === "stringify" + isPropAccessExpr(expr.expr) && + isIdentifier(expr.expr.name) && + expr.expr.name.name === "stringify" ? `States.JsonToString(${objectPath})` : `States.StringToJson(${objectPath})`, }, @@ -1699,9 +1879,13 @@ export class ASL { } return { jsonPath: `$.${expr.name}` }; } else if (isPropAccessExpr(expr)) { - return this.evalExpr(expr.expr, (output) => { - return this.accessConstant(output, expr.name, false); - }); + if (isIdentifier(expr.name)) { + return this.evalExpr(expr.expr, (output) => { + return this.accessConstant(output, expr.name.name, false); + }); + } else { + throw new SynthError(ErrorCodes.Classes_are_not_supported); + } } else if (isElementAccessExpr(expr)) { return this.elementAccessExprToJsonPath(expr); } @@ -1809,7 +1993,12 @@ export class ASL { return this.conditionState(cond); }); } - } else if (expr.op === "-" || expr.op === "++" || expr.op === "--") { + } else if ( + expr.op === "-" || + expr.op === "++" || + expr.op === "--" || + expr.op === "~" + ) { throw new SynthError( ErrorCodes.Cannot_perform_arithmetic_on_variables_in_Step_Function, `Step Function does not support operator ${expr.op}` @@ -2088,6 +2277,8 @@ export class ASL { }, }; }); + } else if (isParenthesizedExpr(expr)) { + return this.eval(expr.expr); } throw new Error(`cannot eval expression kind '${expr.kind}'`); } @@ -2284,19 +2475,37 @@ export class ASL { // the catch/finally handler is nearer than the surrounding Map/Parallel State return { Next: ASL.CatchState, - ResultPath: - isCatchClause(catchOrFinally) && catchOrFinally.variableDecl - ? `$.${catchOrFinally.variableDecl.name}` - : isBlockStmt(catchOrFinally) && - catchOrFinally.isFinallyBlock() && - catchOrFinally.parent.catchClause && - canThrow(catchOrFinally.parent.catchClause) && - // we only store the error thrown from the catchClause if the finallyBlock is not terminal - // by terminal, we mean that every branch returns a value - meaning that the re-throw - // behavior of a finally will never be triggered - the return within the finally intercepts it - !catchOrFinally.isTerminal() - ? `$.${this.generatedNames.generateOrGet(catchOrFinally)}` - : null, + ResultPath: (() => { + if (isCatchClause(catchOrFinally)) { + if (catchOrFinally.variableDecl) { + const varName = isIdentifier(catchOrFinally.variableDecl?.name) + ? catchOrFinally.variableDecl!.name.name + : undefined; + if (varName === undefined) { + throw new SynthError( + ErrorCodes.Unsupported_Feature, + "Destructured parameter declarations are not yet supported by Step Functions. https://github.com/functionless/functionless/issues/364" + ); + } + return `$.${varName}`; + } else { + return null; + } + } else if ( + isBlockStmt(catchOrFinally) && + catchOrFinally.isFinallyBlock() && + catchOrFinally.parent.catchClause && + canThrow(catchOrFinally.parent.catchClause) && + // we only store the error thrown from the catchClause if the finallyBlock is not terminal + // by terminal, we mean that every branch returns a value - meaning that the re-throw + // behavior of a finally will never be triggered - the return within the finally intercepts it + !catchOrFinally.isTerminal() + ) { + return `$.${this.generatedNames.generateOrGet(catchOrFinally)}`; + } else { + return null; + } + })(), }; } else { // the Map/Parallel tasks are closer than the catch/finally, so we use a Fail State @@ -2312,8 +2521,8 @@ export class ASL { private sliceToStateOutput( expr: CallExpr & { expr: PropAccessExpr } ): ASLGraph.NodeResults { - const startArg = expr.getArgument("start")?.expr; - const endArg = expr.getArgument("end")?.expr; + const startArg = expr.args[0]?.expr; + const endArg = expr.args[1]?.expr; const value = this.eval(expr.expr.expr); const valueOutput = ASLGraph.getAslStateOutput(value); if (startArg === undefined && endArg === undefined) { @@ -2377,7 +2586,7 @@ export class ASL { expr: CallExpr & { expr: PropAccessExpr } ): ASLGraph.NodeResults { return this.evalContext(expr, (evalExpr) => { - const separatorArg = expr.getArgument("separator")?.expr; + const separatorArg = expr.args[0]?.expr; const valueOutput = evalExpr(expr.expr.expr); const separatorOutput = separatorArg ? evalExpr(separatorArg) : undefined; const separator = @@ -2526,7 +2735,7 @@ export class ASL { private filterToJsonPath( expr: CallExpr & { expr: PropAccessExpr } ): ASLGraph.NodeResults { - const predicate = expr.getArgument("predicate")?.expr; + const predicate = expr.args[0]?.expr; if (!isFunctionExpr(predicate)) { throw new Error( "the 'predicate' argument of slice must be a FunctionExpr" @@ -2575,7 +2784,9 @@ export class ASL { } else if ( isVariableDecl(ref) || isBindingElem(ref) || - isFunctionDecl(ref) + isFunctionDecl(ref) || + isClassDecl(ref) || + isClassMember(ref) ) { throw new Error( `cannot reference a ${ref.kind} within a JSONPath .filter expression` @@ -2591,7 +2802,7 @@ export class ASL { ) { return `${expr.value}`; } else if (isPropAccessExpr(expr)) { - return `${toFilterCondition(expr.expr)}.${expr.name}`; + return `${toFilterCondition(expr.expr)}.${expr.name.name}`; } else if (isElementAccessExpr(expr)) { return `${toFilterCondition( expr.expr @@ -2654,17 +2865,20 @@ export class ASL { if (isIdentifier(access.element)) { const element = access.element.lookup(); if ( - access.findParent( - (parent): parent is ForInStmt => - isForInStmt(parent) && - // find the first forin parent which has an identifier with this name - parent.variableDecl.name === (access.element).name && - // if the variable decl is an identifier, it will have the same initializer. - ((isIdentifier(parent.variableDecl) && - element === parent.variableDecl.lookup()) || - // if the variable decl is an variable stmt, it will be the initializer of the element. - element === parent.variableDecl) - ) + isVariableDecl(element) && + access.findParent((parent): parent is ForInStmt => { + if (isForInStmt(parent)) { + if (isIdentifier(parent.initializer)) { + // let i; + // for (i in ..) + return element === parent.initializer.lookup(); + } else if (isVariableDecl(parent.initializer)) { + // for (let i in ..) + return parent.initializer === element; + } + } + return false; + }) ) { // the array element is assigned to $.0__[name] return { jsonPath: `$.0__${access.element.name}` }; @@ -2725,8 +2939,10 @@ export class ASL { subState && subStates.push(subState); return cond; }; - const internal = (): Condition => { - if (isBooleanLiteralExpr(expr)) { + const internal = (expr: Expr): Condition => { + if (isParenthesizedExpr(expr)) { + return internal(expr.expr); + } else if (isBooleanLiteralExpr(expr)) { return expr.value ? ASL.trueCondition() : ASL.falseCondition(); } else if (isUnaryExpr(expr) || isPostfixUnaryExpr(expr)) { // TODO: more than just unary not... - https://github.com/functionless/functionless/issues/232 @@ -2734,7 +2950,12 @@ export class ASL { return { Not: localToCondition(expr.expr), }; - } else if (expr.op === "++" || expr.op === "--" || expr.op === "-") { + } else if ( + expr.op === "++" || + expr.op === "--" || + expr.op === "-" || + expr.op === "~" + ) { throw new SynthError( ErrorCodes.Cannot_perform_arithmetic_on_variables_in_Step_Function, `Step Function does not support operator ${expr.op}` @@ -2887,7 +3108,7 @@ export class ASL { throw new Error(`cannot evaluate expression: '${expr.kind}`); }; - return [internal(), ASLGraph.joinSubStates(expr, ...subStates)]; + return [internal(expr), ASLGraph.joinSubStates(expr, ...subStates)]; } } @@ -2896,7 +3117,8 @@ export function isMapOrForEach(expr: CallExpr): expr is CallExpr & { } { return ( isPropAccessExpr(expr.expr) && - (expr.expr.name === "map" || expr.expr.name === "forEach") + isIdentifier(expr.expr.name) && + (expr.expr.name.name === "map" || expr.expr.name.name === "forEach") ); } @@ -2905,7 +3127,11 @@ function isSlice(expr: CallExpr): expr is CallExpr & { name: "slice"; }; } { - return isPropAccessExpr(expr.expr) && expr.expr.name === "slice"; + return ( + isPropAccessExpr(expr.expr) && + isIdentifier(expr.expr.name) && + expr.expr.name.name === "slice" + ); } function isJoin(expr: CallExpr): expr is CallExpr & { @@ -2913,7 +3139,11 @@ function isJoin(expr: CallExpr): expr is CallExpr & { name: "join"; }; } { - return isPropAccessExpr(expr.expr) && expr.expr.name === "join"; + return ( + isPropAccessExpr(expr.expr) && + isIdentifier(expr.expr.name) && + expr.expr.name.name === "join" + ); } function isFilter(expr: CallExpr): expr is CallExpr & { @@ -2921,7 +3151,11 @@ function isFilter(expr: CallExpr): expr is CallExpr & { name: "filter"; }; } { - return isPropAccessExpr(expr.expr) && expr.expr.name === "filter"; + return ( + isPropAccessExpr(expr.expr) && + isIdentifier(expr.expr.name) && + expr.expr.name.name === "filter" + ); } function canThrow(node: FunctionlessNode): boolean { @@ -4081,20 +4315,28 @@ function toStateName(node: FunctionlessNode): string { return "continue"; } else if (isCatchClause(node)) { return `catch${ - node.variableDecl?.name ? `(${node.variableDecl?.name})` : "" + node.variableDecl?.name + ? `(${exprToString(node.variableDecl.name)})` + : "" }`; } else if (isDoStmt(node)) { return `while (${exprToString(node.condition)})`; } else if (isForInStmt(node)) { - return `for(${node.variableDecl.name} in ${exprToString(node.expr)})`; + return `for(${ + isIdentifier(node.initializer) + ? exprToString(node.initializer) + : exprToString(node.initializer.name) + } in ${exprToString(node.expr)})`; } else if (isForOfStmt(node)) { - return `for(${node.variableDecl.name} of ${exprToString(node.expr)})`; + return `for(${exprToString(node.initializer)} of ${exprToString( + node.expr + )})`; } else if (isForStmt(node)) { // for(;;) return `for(${ - node.variableDecl && isVariableDeclList(node.variableDecl) - ? inner(node.variableDecl) - : exprToString(node.variableDecl) + node.initializer && isVariableDeclList(node.initializer) + ? inner(node.initializer) + : exprToString(node.initializer) };${exprToString(node.condition)};${exprToString(node.incrementor)})`; } else if (isReturnStmt(node)) { if (node.expr) { @@ -4108,11 +4350,13 @@ function toStateName(node: FunctionlessNode): string { return "try"; } else if (isVariableStmt(node)) { return inner(node.declList); + } else if (isVariableDeclList(node)) { + return `${node.decls.map((v) => inner(v)).join(",")}`; } else if (isVariableDecl(node)) { if (isCatchClause(node.parent)) { return `catch(${node.name})`; } else { - return `${node.name} = ${ + return `${exprToString(node.name)} = ${ node.initializer ? exprToString(node.initializer) : "undefined" }`; } @@ -4129,14 +4373,39 @@ function toStateName(node: FunctionlessNode): string { return `{ ${node.bindings.map(inner).join(", ")} }`; } else if (isArrayBinding(node)) { return `[ ${node.bindings.map((b) => (!b ? "" : inner(b))).join(", ")} ]`; - } else if (isFunctionDecl(node)) { + } else if ( + isFunctionDecl(node) || + isFunctionExpr(node) || + isArrowFunctionExpr(node) + ) { return `function (${node.parameters.map(inner).join(",")})`; } else if (isParameterDecl(node)) { - return isBindingPattern(node.name) ? inner(node.name) : node.name; + return inner(node.name); } else if (isErr(node)) { throw node.error; - } else if (isVariableDeclList(node)) { - return `${node.decls.map((v) => inner(v)).join(",")}`; + } else if (isEmptyStmt(node)) { + return ";"; + } else if ( + isCaseClause(node) || + isClassDecl(node) || + isClassStaticBlockDecl(node) || + isConstructorDecl(node) || + isDebuggerStmt(node) || + isDefaultClause(node) || + isLabelledStmt(node) || + isMethodDecl(node) || + isPropDecl(node) || + isSuperKeyword(node) || + isSwitchStmt(node) || + isWithStmt(node) || + isYieldExpr(node) || + isSuperKeyword(node) || + isImportKeyword(node) + ) { + throw new SynthError( + ErrorCodes.Unsupported_Feature, + `Unsupported kind: ${node.kind}` + ); } else { return assertNever(node); } @@ -4145,28 +4414,31 @@ function toStateName(node: FunctionlessNode): string { return inner(node); } -function exprToString(expr?: Expr): string { +function exprToString( + expr?: Expr | ParameterDecl | BindingName | BindingElem | VariableDecl +): string { if (!expr) { return ""; } else if (isArgument(expr)) { return exprToString(expr.expr); } else if (isArrayLiteralExpr(expr)) { return `[${expr.items.map(exprToString).join(", ")}]`; + } else if (isBigIntExpr(expr)) { + return expr.value.toString(10); } else if (isBinaryExpr(expr)) { return `${exprToString(expr.left)} ${expr.op} ${exprToString(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.kind} is unsupported in ASL`); + } return `${isNewExpr(expr) ? "new " : ""}${exprToString( expr.expr )}(${expr.args // Assume that undefined args are in order. - .filter( - (arg) => - arg.expr && - !(arg.name === "thisArg" && isUndefinedLiteralExpr(arg.expr)) - ) - .map((arg) => exprToString(arg.expr)) + .filter((arg) => arg && !isUndefinedLiteralExpr(arg)) + .map(exprToString) .join(", ")})`; } else if (isConditionExpr(expr)) { return `if(${exprToString(expr.when)})`; @@ -4174,8 +4446,8 @@ function exprToString(expr?: Expr): string { return `[${exprToString(expr.expr)}]`; } else if (isElementAccessExpr(expr)) { return `${exprToString(expr.expr)}[${exprToString(expr.element)}]`; - } else if (isFunctionExpr(expr)) { - return `function(${expr.parameters.map((param) => param.name).join(", ")})`; + } else if (isFunctionExpr(expr) || isArrowFunctionExpr(expr)) { + return `function(${expr.parameters.map(exprToString).join(", ")})`; } else if (isIdentifier(expr)) { return expr.name; } else if (isNullLiteralExpr(expr)) { @@ -4185,13 +4457,15 @@ function exprToString(expr?: Expr): string { } else if (isObjectLiteralExpr(expr)) { return `{${expr.properties.map(exprToString).join(", ")}}`; } else if (isPropAccessExpr(expr)) { - return `${exprToString(expr.expr)}.${expr.name}`; + return `${exprToString(expr.expr)}.${expr.name.name}`; } else if (isPropAssignExpr(expr)) { return `${ - isIdentifier(expr.name) + isIdentifier(expr.name) || isPrivateIdentifier(expr.name) ? expr.name.name : isStringLiteralExpr(expr.name) ? expr.name.value + : isNumberLiteralExpr(expr.name) + ? expr.name.value : isComputedPropertyNameExpr(expr.name) ? isStringLiteralExpr(expr.name.expr) ? expr.name.expr.value @@ -4222,6 +4496,41 @@ function exprToString(expr?: Expr): string { return `await ${exprToString(expr.expr)}`; } else if (isPromiseExpr(expr) || isPromiseArrayExpr(expr)) { return exprToString(expr.expr); + } else if (isThisExpr(expr)) { + return "this"; + } else if (isClassExpr(expr)) { + throw new SynthError( + ErrorCodes.Unsupported_Feature, + `ClassDecl is not supported in StepFunctions` + ); + } else if (isPrivateIdentifier(expr)) { + return expr.name; + } else if (isYieldExpr(expr)) { + return `yield${expr.delegate ? "*" : ""} ${exprToString(expr.expr)}`; + } else if (isRegexExpr(expr)) { + return expr.regex.source; + } else if (isDeleteExpr(expr)) { + return `delete ${exprToString(expr.expr)}`; + } else if (isVoidExpr(expr)) { + return `void ${exprToString(expr.expr)}`; + } else if (isParenthesizedExpr(expr)) { + return exprToString(expr.expr); + } else if (isObjectBinding(expr)) { + return `{${expr.bindings.map(exprToString).join(",")}}`; + } else if (isArrayBinding(expr)) { + return `[${expr.bindings.map(exprToString).join(",")}]`; + } else if (isBindingElem(expr)) { + return `${expr.rest ? "..." : ""}${ + expr.propertyName + ? `${exprToString(expr.propertyName)}:${exprToString(expr.name)}` + : `${exprToString(expr.name)}` + }`; + } else if (isVariableDecl(expr)) { + return `${exprToString(expr.name)}${ + expr.initializer ? ` = ${exprToString(expr.initializer)}` : "" + }`; + } else if (isParameterDecl(expr)) { + return exprToString(expr.name); } else { return assertNever(expr); } diff --git a/src/aws.ts b/src/aws.ts index ef19ef48..9164e51a 100644 --- a/src/aws.ts +++ b/src/aws.ts @@ -574,7 +574,7 @@ function makeDynamoIntegration< }, }, asl(call, context) { - const input = call.getArgument("input")?.expr; + const input = call.args[0]?.expr; if (!isObjectLiteralExpr(input)) { throw new SynthError( ErrorCodes.Expected_an_object_literal, diff --git a/src/compile.ts b/src/compile.ts index e483bebe..a40d32a1 100644 --- a/src/compile.ts +++ b/src/compile.ts @@ -11,13 +11,14 @@ import { FunctionInterface, makeFunctionlessChecker, } from "./checker"; -import type { FunctionDecl } from "./declaration"; +import type { ConstructorDecl, FunctionDecl, MethodDecl } from "./declaration"; import { ErrorCodes, SynthError } from "./error-code"; import type { FunctionExpr, BinaryOp, UnaryOp, PostfixUnaryOp, + ArrowFunctionExpr, } from "./expression"; import { FunctionlessNode } from "./node"; @@ -297,8 +298,13 @@ export function compile( } function toFunction( - type: FunctionDecl["kind"] | FunctionExpr["kind"], - impl: ts.Expression, + type: + | FunctionDecl["kind"] + | ArrowFunctionExpr["kind"] + | FunctionExpr["kind"] + | ConstructorDecl["kind"] + | MethodDecl["kind"], + impl: ts.Node, /** * Scope used to determine the accessability of identifiers. * Functionless considers identifiers in a closure and all nested closures to be in scope. @@ -322,7 +328,9 @@ export function compile( if ( !ts.isFunctionDeclaration(impl) && !ts.isArrowFunction(impl) && - !ts.isFunctionExpression(impl) + !ts.isFunctionExpression(impl) && + !ts.isConstructorDeclaration(impl) && + !ts.isMethodDeclaration(impl) ) { throw new Error( `Functionless reflection only supports function parameters with bodies, no signature only declarations or references. Found ${impl.getText()}.` @@ -343,17 +351,37 @@ export function compile( ]); return newExpr(type, [ + ...resolveFunctionName(), ts.factory.createArrayLiteralExpression( impl.parameters.map((param) => newExpr("ParameterDecl", [ - ts.isIdentifier(param.name) - ? ts.factory.createStringLiteral(param.name.text) - : toExpr(param.name, scope ?? impl), + toExpr(param.name, scope ?? impl), + ...(param.initializer + ? [toExpr(param.initializer, scope ?? impl)] + : []), ]) ) ), body, ]); + + function resolveFunctionName(): [ts.Expression] | [] { + if (type === "MethodDecl") { + // methods can be any valid PropertyName expression + return [toExpr((impl).name!, scope ?? impl)]; + } else if (type === "FunctionDecl" || type === "FunctionExpr") { + if ( + (ts.isFunctionDeclaration(impl) || + ts.isFunctionExpression(impl)) && + impl.name + ) { + return [ts.factory.createStringLiteral(impl.name.text)]; + } else { + return [ts.factory.createIdentifier("undefined")]; + } + } + return []; + } } function visitApiIntegration(node: ts.NewExpression): ts.Node { @@ -410,7 +438,7 @@ export function compile( const newType = checker.getTypeAtLocation(node); // cannot create new resources in native runtime code. const functionlessKind = checker.getFunctionlessTypeKind(newType); - if (checker.getFunctionlessTypeKind(newType)) { + if (functionlessKind) { throw new SynthError( ErrorCodes.Unsupported_initialization_of_resources, `Cannot initialize new resources in a runtime function, found ${functionlessKind}.` @@ -425,69 +453,17 @@ export function compile( } } - const getCall = () => { - const exprType = checker.getTypeAtLocation(node.expression); - const functionBrand = exprType.getProperty("__functionBrand"); - let signature: ts.Signature | undefined; - if (functionBrand !== undefined) { - const functionType = checker.getTypeOfSymbolAtLocation( - functionBrand, - node.expression - ); - const signatures = checker.getSignaturesOfType( - functionType, - ts.SignatureKind.Call - ); - - if (signatures.length === 1) { - signature = signatures[0]; - } else { - // If the function brand has multiple signatures, try the resolved signature. - signature = checker.getResolvedSignature(node); - } - } else { - signature = checker.getResolvedSignature(node); - } - if (signature && signature.parameters.length > 0) { - return newExpr( - ts.isCallExpression(node) ? "CallExpr" : "NewExpr", - [ - toExpr(node.expression, scope), - ts.factory.createArrayLiteralExpression( - signature.parameters.map((parameter, i) => - newExpr("Argument", [ - (parameter.declarations?.[0] as ts.ParameterDeclaration) - ?.dotDotDotToken - ? newExpr("ArrayLiteralExpr", [ - ts.factory.createArrayLiteralExpression( - node.arguments - ?.slice(i) - .map((x) => toExpr(x, scope)) ?? [] - ), - ]) - : toExpr(node.arguments?.[i], scope), - ts.factory.createStringLiteral(parameter.name), - ]) - ) - ), - ] - ); - } else { - return newExpr("CallExpr", [ - toExpr(node.expression, scope), - ts.factory.createArrayLiteralExpression( - node.arguments?.map((arg) => - newExpr("Argument", [ - toExpr(arg, scope), - ts.factory.createIdentifier("undefined"), - ]) - ) ?? [] - ), - ]); - } - }; - - const call = getCall(); + const call = newExpr( + ts.isCallExpression(node) ? "CallExpr" : "NewExpr", + [ + toExpr(node.expression, scope), + ts.factory.createArrayLiteralExpression( + node.arguments?.map((arg) => + newExpr("Argument", [toExpr(arg, scope)]) + ) ?? [] + ), + ] + ); const type = checker.getTypeAtLocation(node); const typeSymbol = type.getSymbol(); @@ -579,17 +555,14 @@ export function compile( ), ]); } else if (ts.isVariableDeclaration(node)) { - if (ts.isIdentifier(node.name)) { - return newExpr("VariableDecl", [ - ts.factory.createStringLiteral(node.name.getText()), - ...(node.initializer ? [toExpr(node.initializer, scope)] : []), - ]); - } else { - return newExpr("VariableDecl", [ - toExpr(node.name, scope), - toExpr(node.initializer, scope), - ]); - } + return newExpr("VariableDecl", [ + ts.isIdentifier(node.name) + ? newExpr("Identifier", [ + ts.factory.createStringLiteral(node.name.text), + ]) + : toExpr(node.name, scope), + ...(node.initializer ? [toExpr(node.initializer, scope)] : []), + ]); } else if (ts.isIfStatement(node)) { return newExpr("IfStmt", [ // when @@ -712,6 +685,10 @@ export function compile( ]); } else if (ts.isNumericLiteral(node)) { return newExpr("NumberLiteralExpr", [node]); + } else if (ts.isBigIntLiteral(node)) { + return newExpr("BigIntExpr", [node]); + } else if (ts.isRegularExpressionLiteral(node)) { + return newExpr("RegexExpr", [node]); } else if ( ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) @@ -820,7 +797,7 @@ export function compile( toExpr(node.expression, scope), ]); } else if (ts.isParenthesizedExpression(node)) { - return toExpr(node.expression, scope); + return newExpr("ParenthesizedExpr", [toExpr(node.expression, scope)]); } else if (ts.isAsExpression(node)) { return toExpr(node.expression, scope); } else if (ts.isTypeAssertionExpression(node)) { @@ -828,10 +805,95 @@ export function compile( } else if (ts.isNonNullExpression(node)) { return toExpr(node.expression, scope); } else if (node.kind === ts.SyntaxKind.ThisKeyword) { - // assuming that this is used in a valid location, create a closure around that instance. - return ref(ts.factory.createIdentifier("this")); + return newExpr("ThisExpr", [ + ts.factory.createArrowFunction( + undefined, + undefined, + [], + undefined, + undefined, + ts.factory.createIdentifier("this") + ), + ]); + } else if ( + ts.isToken(node) && + node.kind === ts.SyntaxKind.SuperKeyword + ) { + return newExpr("SuperKeyword", []); } else if (ts.isAwaitExpression(node)) { return newExpr("AwaitExpr", [toExpr(node.expression, scope)]); + } else if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) { + return newExpr( + `Class${ts.isClassDeclaration(node) ? "Decl" : "Expr"}`, + [ + // name + node.name ?? ts.factory.createIdentifier("undefined"), + // extends + node.heritageClauses?.flatMap((clause) => + clause.token === ts.SyntaxKind.ExtendsKeyword && + clause.types[0].expression !== undefined + ? [toExpr(clause.types[0].expression, scope)] + : [] + )[0] ?? ts.factory.createIdentifier("undefined"), + // members + ts.factory.createArrayLiteralExpression( + node.members.map((member) => toExpr(member, scope)) + ), + ] + ); + } else if (ts.isClassStaticBlockDeclaration(node)) { + return newExpr("ClassStaticBlockDecl", [toExpr(node.body, scope)]); + } else if (ts.isConstructorDeclaration(node)) { + return toFunction("ConstructorDecl", node); + } else if (ts.isMethodDeclaration(node)) { + return toFunction("MethodDecl", node); + } else if (ts.isPropertyDeclaration(node)) { + return newExpr("PropDecl", [ + toExpr(node.name, scope), + node.initializer + ? toExpr(node.initializer, scope) + : ts.factory.createIdentifier("undefined"), + ]); + } else if (ts.isDebuggerStatement(node)) { + return newExpr("DebuggerStmt", []); + } else if (ts.isLabeledStatement(node)) { + return newExpr("LabelledStmt", [toExpr(node.statement, scope)]); + } else if (ts.isSwitchStatement(node)) { + return newExpr("SwitchStmt", [ + ts.factory.createArrayLiteralExpression( + node.caseBlock.clauses.map((clause) => toExpr(clause, scope)) + ), + ]); + } else if (ts.isCaseClause(node)) { + return newExpr("CaseClause", [ + toExpr(node.expression, scope), + ts.factory.createArrayLiteralExpression( + node.statements.map((stmt) => toExpr(stmt, scope)) + ), + ]); + } else if (ts.isDefaultClause(node)) { + return newExpr("DefaultClause", [ + ts.factory.createArrayLiteralExpression( + node.statements.map((stmt) => toExpr(stmt, scope)) + ), + ]); + } else if (ts.isWithStatement(node)) { + return newExpr("WithStmt", []); + } else if (ts.isPrivateIdentifier(node)) { + return newExpr("PrivateIdentifier", [ + ts.factory.createStringLiteral(node.getText()), + ]); + } else if (ts.isVoidExpression(node)) { + return newExpr("VoidExpr", [toExpr(node.expression, scope)]); + } else if (ts.isDeleteExpression(node)) { + return newExpr("DeleteExpr", [toExpr(node.expression, scope)]); + } else if (ts.isYieldExpression(node)) { + return newExpr("YieldExpr", [ + toExpr(node.expression, scope), + node.asteriskToken + ? ts.factory.createTrue() + : ts.factory.createFalse(), + ]); } throw new Error( diff --git a/src/declaration.ts b/src/declaration.ts index be3e129a..d0d32112 100644 --- a/src/declaration.ts +++ b/src/declaration.ts @@ -1,19 +1,13 @@ import { ErrorCodes, SynthError } from "./error-code"; import { - Argument, ComputedPropertyNameExpr, Expr, FunctionExpr, Identifier, + PropName, StringLiteralExpr, } from "./expression"; -import { - isBindingPattern, - isErr, - isFunctionDecl, - isNode, - isParameterDecl, -} from "./guards"; +import { isBindingPattern, isErr, isFunctionDecl } from "./guards"; import { Integration } from "./integration"; import { BaseNode, FunctionlessNode } from "./node"; import type { @@ -24,34 +18,115 @@ import type { ForStmt, VariableStmt, } from "./statement"; -import { AnyFunction, anyOf } from "./util"; - -export type Decl = FunctionDecl | ParameterDecl | BindingElem | VariableDecl; +import { AnyClass, AnyFunction, anyOf } from "./util"; -export function isDecl(a: any): a is Decl { - return isNode(a) && (isFunctionDecl(a) || isParameterDecl(a)); -} +export type Decl = + | BindingElem + | ClassDecl + | ClassMember + | FunctionDecl + | ParameterDecl + | VariableDecl; abstract class BaseDecl< Kind extends FunctionlessNode["kind"], - Parent extends FunctionlessNode | undefined + Parent extends FunctionlessNode | undefined = FunctionlessNode | undefined > extends BaseNode { readonly nodeKind: "Decl" = "Decl"; } -export class FunctionDecl extends BaseDecl< - "FunctionDecl", +export class ClassDecl extends BaseDecl< + "ClassDecl", undefined > { - readonly _functionBrand?: F; + readonly _classBrand?: C; + constructor( + readonly name: string, + readonly heritage: Expr | undefined, + readonly members: ClassMember[] + ) { + super("ClassDecl", arguments); + } + public clone(): this { + return new ClassDecl( + this.name, + this.heritage?.clone(), + this.members.map((m) => m.clone()) + ) as this; + } +} + +export type ClassMember = + | ClassStaticBlockDecl + | ConstructorDecl + | MethodDecl + | PropDecl; + +export class ClassStaticBlockDecl extends BaseDecl<"ClassStaticBlockDecl"> { + constructor(readonly block: BlockStmt) { + super("ClassStaticBlockDecl", arguments); + } + + public clone(): this { + return new ClassStaticBlockDecl(this.block.clone()) as this; + } +} + +export class ConstructorDecl extends BaseDecl<"ConstructorDecl"> { constructor(readonly parameters: ParameterDecl[], readonly body: BlockStmt) { - super("FunctionDecl"); - parameters.forEach((param) => param.setParent(this)); - body.setParent(this); + super("ConstructorDecl", arguments); + } + + public clone(): this { + return new ConstructorDecl( + this.parameters.map((p) => p.clone()), + this.body.clone() + ) as this; + } +} + +export class MethodDecl extends BaseDecl<"MethodDecl"> { + constructor( + readonly name: PropName, + readonly parameters: ParameterDecl[], + readonly body: BlockStmt + ) { + super("MethodDecl", arguments); + } + + public clone(): this { + return new MethodDecl( + this.name.clone(), + this.parameters.map((p) => p.clone()), + this.body.clone() + ) as this; + } +} + +export class PropDecl extends BaseDecl<"PropDecl"> { + constructor(readonly name: PropName, readonly initializer?: Expr) { + super("PropDecl", arguments); + } + public clone(): this { + return new PropDecl(this.name.clone(), this.initializer?.clone()) as this; + } +} + +export class FunctionDecl< + F extends AnyFunction = AnyFunction +> extends BaseDecl<"FunctionDecl"> { + readonly _functionBrand?: F; + constructor( + readonly name: string, + readonly parameters: ParameterDecl[], + readonly body: BlockStmt + ) { + super("FunctionDecl", arguments); } public clone(): this { return new FunctionDecl( + this.name, this.parameters.map((param) => param.clone()), this.body.clone() ) as this; @@ -60,15 +135,17 @@ export class FunctionDecl extends BaseDecl< export interface IntegrationInvocation { integration: Integration; - args: Argument[]; + args: Expr[]; } +export type BindingName = Identifier | BindingPattern; + export class ParameterDecl extends BaseDecl< "ParameterDecl", FunctionDecl | FunctionExpr > { - constructor(readonly name: string | BindingPattern) { - super("ParameterDecl"); + constructor(readonly name: BindingName, readonly initializer?: Expr) { + super("ParameterDecl", arguments); } public clone(): this { @@ -120,7 +197,7 @@ export type BindingPattern = ObjectBinding | ArrayBinding; * ``` * * * `a` - creates a variable called a with the value of the right side (`const a = right.a`). - * * `a: b` - creates a variable called b with the value of the right side (`const b = right.b`). + * * `a: b` - creates a variable called b with the value of the right side (`const b = right.a`). * * `a = "value"` - creates a variable called a with the value of the right side or "value" when the right side is undefined (`const a = right.a ?? "value"`) * * `a: { b }` - creates a variable called b with the value of the right side's a value. (`const b = right.a.b`) * * `...rest`- creates a variable called rest with all of the unused keys in the right side @@ -146,10 +223,7 @@ export class BindingElem extends BaseDecl<"BindingElem", BindingPattern> { | StringLiteralExpr, readonly initializer?: Expr ) { - super("BindingElem"); - name.setParent(this); - propertyName?.setParent(this); - initializer?.setParent(this); + super("BindingElem", arguments); } public clone(): this { @@ -187,8 +261,7 @@ export class ObjectBinding extends BaseNode<"ObjectBinding", VariableDecl> { readonly nodeKind: "Node" = "Node"; constructor(readonly bindings: BindingElem[]) { - super("ObjectBinding"); - bindings.forEach((b) => b.setParent(this)); + super("ObjectBinding", arguments); } public clone(): this { @@ -220,8 +293,7 @@ export class ArrayBinding extends BaseNode<"ArrayBinding", VariableDecl> { readonly nodeKind: "Node" = "Node"; constructor(readonly bindings: (BindingElem | undefined)[]) { - super("ArrayBinding"); - bindings.forEach((b) => b?.setParent(this)); + super("ArrayBinding", arguments); } public clone(): this { @@ -238,12 +310,8 @@ export type VariableDeclParent = export class VariableDecl< E extends Expr | undefined = Expr | undefined > extends BaseDecl<"VariableDecl", VariableDeclParent> { - constructor(readonly name: string | BindingPattern, readonly initializer: E) { - super("VariableDecl"); - if (isBindingPattern(name)) { - name.setParent(this); - } - initializer?.setParent(this); + constructor(readonly name: BindingName, readonly initializer: E) { + super("VariableDecl", arguments); } public clone(): this { @@ -263,8 +331,7 @@ export class VariableDeclList extends BaseNode< readonly nodeKind: "Node" = "Node"; constructor(readonly decls: VariableDecl[]) { - super("VariableDeclList"); - decls.map((decl) => decl.setParent(this)); + super("VariableDeclList", arguments); } public clone(): this { diff --git a/src/error-code.ts b/src/error-code.ts index fdc35a2b..b5536464 100644 --- a/src/error-code.ts +++ b/src/error-code.ts @@ -1104,6 +1104,44 @@ export namespace ErrorCodes { type: ErrorType.ERROR, title: "ApiGateway Unsupported Reference", }; + + /** + * Errors in Step Functions can only be thrown in one of two ways: + * + * 1. by throwing javascript's `Error` class + * ```ts + * throw Error("message"); + * throw new Error("message"); + * ``` + * 2. by throwing the `StepFunctionError` class + * ```ts + * throw new StepFunctionError("CustomErrorName", { error: "data" }) + * ``` + */ + export const StepFunction_Throw_must_be_Error_or_StepFunctionError_class: ErrorCode = + { + code: 10030, + type: ErrorType.ERROR, + title: "StepFunction throw must be Error or StepFunctionError class", + }; + + /** + * Classes, methods and private identifiers are not yet supported by Functionless. + * + * To workaround, use Functions. + * + * ```ts + * function foo () { .. } + * const foo = () => { .. } + * ``` + * + * @see https://github.com/functionless/functionless/issues/362 + */ + export const Classes_are_not_supported: ErrorCode = { + code: 10031, + type: ErrorType.ERROR, + title: "Classes are not yet supported by Functionless", + }; } // to prevent the closure serializer from trying to import all of functionless. diff --git a/src/error.ts b/src/error.ts index 05bfaf03..7df88964 100644 --- a/src/error.ts +++ b/src/error.ts @@ -4,7 +4,7 @@ export class Err extends BaseNode<"Err"> { readonly nodeKind: "Err" = "Err"; constructor(readonly error: Error) { - super("Err"); + super("Err", arguments); } public clone(): this { diff --git a/src/event-bridge/event-bus.ts b/src/event-bridge/event-bus.ts index ac68113b..3e4d4e35 100644 --- a/src/event-bridge/event-bus.ts +++ b/src/event-bridge/event-bus.ts @@ -256,16 +256,16 @@ abstract class EventBusBase // Validate that the events are object literals. // Then normalize nested arrays of events into a single list of events. // TODO Relax these restrictions: https://github.com/functionless/functionless/issues/101 - const eventObjs = call.args.flatMap((arg) => { - if (isArrayLiteralExpr(arg.expr)) { - if (!arg.expr.items.every(isObjectLiteralExpr)) { + const eventObjs = call.args.flatMap(({ expr: arg }) => { + if (isArrayLiteralExpr(arg)) { + if (!arg.items.every(isObjectLiteralExpr)) { throw new SynthError( ErrorCodes.StepFunctions_calls_to_EventBus_PutEvents_must_use_object_literals ); } - return arg.expr.items; - } else if (isObjectLiteralExpr(arg.expr)) { - return [arg.expr]; + return arg.items; + } else if (isObjectLiteralExpr(arg)) { + return [arg]; } throw new SynthError( ErrorCodes.StepFunctions_calls_to_EventBus_PutEvents_must_use_object_literals diff --git a/src/event-bridge/event-pattern/synth.ts b/src/event-bridge/event-pattern/synth.ts index 79403ecd..64998a09 100644 --- a/src/event-bridge/event-pattern/synth.ts +++ b/src/event-bridge/event-pattern/synth.ts @@ -25,6 +25,7 @@ import { isErr, isFunctionDecl, isNullLiteralExpr, + isParenthesizedExpr, isPropAccessExpr, isUnaryExpr, isUndefinedLiteralExpr, @@ -132,7 +133,7 @@ export const synthesizePatternDocument = ( "Expected parameter to synthesizeEventPattern to be compiled by functionless." ); } - const [eventDecl = undefined] = predicate.parameters; + const [eventDecl = undefined] = (predicate).parameters; const evalExpr = (expr: Expr): PatternDocument => { if (isBinaryExpr(expr)) { @@ -149,6 +150,8 @@ export const synthesizePatternDocument = ( return evalCall(expr); } else if (isBooleanLiteralExpr(expr)) { return { doc: {} }; + } else if (isParenthesizedExpr(expr)) { + return evalExpr(expr.expr); } else { throw new Error(`${expr.kind} is unsupported`); } @@ -389,22 +392,20 @@ export const synthesizePatternDocument = ( ): PatternDocument => { const searchElement = evalToConstant( assertDefined( - expr.args[0].expr, + expr.args[0]?.expr, `Includes must have a single string argument ${INCLUDES_SEARCH_ELEMENT}.` ) )?.constant; if ( - expr.args - .map((e) => e.expr) - .filter( - (e) => - !( - e === undefined || - isNullLiteralExpr(e) || - isUndefinedLiteralExpr(e) - ) - ).length > 1 + expr.args.filter( + (e) => + !( + e?.expr === undefined || + isNullLiteralExpr(e.expr) || + isUndefinedLiteralExpr(e.expr) + ) + ).length > 1 ) { throw new Error("Includes only supports the searchElement argument"); } @@ -459,22 +460,20 @@ export const synthesizePatternDocument = ( expr: CallExpr & { expr: PropAccessExpr | ElementAccessExpr } ): PatternDocument => { const arg = assertDefined( - expr.args[0].expr, + expr.args[0]?.expr, `StartsWith must contain a single string argument ${STARTS_WITH_SEARCH_STRING}` ); const searchString = assertString(evalToConstant(arg)?.constant); if ( - expr.args - .map((e) => e.expr) - .filter( - (e) => - !( - e === undefined || - isNullLiteralExpr(e) || - isUndefinedLiteralExpr(e) - ) - ).length > 1 + expr.args.filter( + (e) => + !( + e?.expr === undefined || + isNullLiteralExpr(e.expr) || + isUndefinedLiteralExpr(e.expr) + ) + ).length > 1 ) { throw new Error("Includes only supports the searchString argument"); } @@ -488,11 +487,12 @@ export const synthesizePatternDocument = ( ); } - if ( - isPropAccessExpr(expr.expr.expr) || - isElementAccessExpr(expr.expr.expr) - ) { - if (expr.expr.expr.type === "string") { + const e = isParenthesizedExpr(expr.expr.expr) + ? expr.expr.expr.unwrap() + : expr.expr.expr; + + if (isPropAccessExpr(e) || isElementAccessExpr(e)) { + if (e.type === "string") { assertValidEventReference(eventReference, eventDecl); return eventReferenceToPatternDocument(eventReference, { prefix: searchString, @@ -501,7 +501,7 @@ export const synthesizePatternDocument = ( // TODO: support for strings throw new Error( - `Starts With operation only supported on strings, found ${expr.expr.expr.type}.` + `Starts With operation only supported on strings, found ${e.type}.` ); } diff --git a/src/event-bridge/target-input.ts b/src/event-bridge/target-input.ts index dcaa2f81..2b1897cd 100644 --- a/src/event-bridge/target-input.ts +++ b/src/event-bridge/target-input.ts @@ -4,7 +4,12 @@ import { assertConstantValue, assertString } from "../assert"; import { FunctionDecl, validateFunctionDecl } from "../declaration"; import { Err } from "../error"; import { ErrorCodes, SynthError } from "../error-code"; -import { ArrayLiteralExpr, Expr, ObjectLiteralExpr } from "../expression"; +import { + ArrayLiteralExpr, + Expr, + Identifier, + ObjectLiteralExpr, +} from "../expression"; import { isArrayLiteralExpr, isAwaitExpr, @@ -18,6 +23,7 @@ import { isReferenceExpr, isPromiseExpr, isTemplateExpr, + isParenthesizedExpr, } from "../guards"; import { isIntegration } from "../integration"; import { evalToConstant } from "../util"; @@ -103,7 +109,9 @@ export const synthesizeEventBridgeTargets = ( const exprToLiteral = (expr: Expr): LiteralType => { const constant = evalToConstant(expr); - if ( + if (isParenthesizedExpr(expr)) { + return exprToLiteral(expr.expr); + } else if ( constant && (constant.constant === null || typeof constant.constant !== "object") ) { @@ -122,7 +130,7 @@ export const synthesizeEventBridgeTargets = ( if ( eventDecl && ref.reference.length === 0 && - ref.identity === eventDecl.name + ref.identity === (eventDecl.name).name ) { return { value: "", @@ -130,7 +138,7 @@ export const synthesizeEventBridgeTargets = ( }; } // check to see if the value is a predefined value - if (utilsDecl && ref.identity === utilsDecl?.name) { + if (utilsDecl && ref.identity === (utilsDecl.name).name) { const [context = undefined, value = undefined] = ref.reference; if (context === "context") { if (value === "ruleName") { diff --git a/src/event-bridge/utils.ts b/src/event-bridge/utils.ts index a5f771f3..42650ff4 100644 --- a/src/event-bridge/utils.ts +++ b/src/event-bridge/utils.ts @@ -23,6 +23,7 @@ import { isElementAccessExpr, isIdentifier, isObjectLiteralExpr, + isParenthesizedExpr, isPropAccessExpr, isPropAssignExpr, isReturnStmt, @@ -49,7 +50,9 @@ import { Constant, evalToConstant } from "../util"; export const getReferencePath = ( expression: Expr ): ReferencePath | undefined => { - if (isIdentifier(expression)) { + if (isParenthesizedExpr(expression)) { + return getReferencePath(expression.expr); + } else if (isIdentifier(expression)) { return { reference: [], identity: expression.name }; } else if (isPropAccessExpr(expression) || isElementAccessExpr(expression)) { const key = getPropertyAccessKey(expression); @@ -88,7 +91,7 @@ export const getPropertyAccessKey = ( expr: PropAccessExpr | ElementAccessExpr ): string | number => { const key = isPropAccessExpr(expr) - ? expr.name + ? expr.name.name : evalToConstant(expr.element)?.constant; if (!(typeof key === "string" || typeof key === "number")) { @@ -139,7 +142,9 @@ export interface EventScope { * Also does some optimization like turning templated strings of all constants into a string constant. */ export const flattenExpression = (expr: Expr, scope: EventScope): Expr => { - if (isUnaryExpr(expr)) { + if (isParenthesizedExpr(expr)) { + return flattenExpression(expr.expr, scope); + } else if (isUnaryExpr(expr)) { return new UnaryExpr(expr.op, flattenExpression(expr.expr, scope)); } else if (isIdentifier(expr)) { // if this variable is in scope, return the expression it points to. @@ -281,7 +286,7 @@ export const flattenStatementsScope = ( return { ...scope, - [stmt.name]: flattened, + [stmt.name.name]: flattened, }; }, {}); }; @@ -310,7 +315,7 @@ export function assertValidEventReference( } const eName = eventName?.name; const uName = utilsName?.name; - if (eventReference.identity === eName) { + if (eventReference.identity === (eName)?.name) { if (eventReference.reference.length > 1) { const [first] = eventReference.reference; if (first !== "detail") { @@ -319,7 +324,10 @@ export function assertValidEventReference( )}`; } } - } else if (!utilsName || eventReference.identity !== uName) { + } else if ( + !utilsName || + eventReference.identity !== (uName)?.name + ) { throw Error( `Unresolved references can only reference the event parameter (${eventName}) or the $utils parameter (${utilsName}), but found ${eventReference.identity}` ); diff --git a/src/expression.ts b/src/expression.ts index d83bbd87..3aad2822 100644 --- a/src/expression.ts +++ b/src/expression.ts @@ -1,8 +1,21 @@ -import { BindingElem, Decl, ParameterDecl, VariableDecl } from "./declaration"; -import { isIdentifier, isPropAssignExpr, isStringLiteralExpr } from "./guards"; +import type { + BindingElem, + ClassMember, + Decl, + ParameterDecl, + VariableDecl, +} from "./declaration"; +import { + isIdentifier, + isNumberLiteralExpr, + isParenthesizedExpr, + isPrivateIdentifier, + isPropAssignExpr, + isStringLiteralExpr, +} from "./guards"; import { BaseNode, FunctionlessNode } from "./node"; import type { BlockStmt, Stmt } from "./statement"; -import type { AnyFunction } from "./util"; +import type { AnyClass, AnyFunction } from "./util"; /** * An {@link Expr} (Expression) is a Node that will be interpreted to a value. @@ -10,12 +23,16 @@ import type { AnyFunction } from "./util"; export type Expr = | Argument | ArrayLiteralExpr + | ArrowFunctionExpr | AwaitExpr + | BigIntExpr | BinaryExpr | BooleanLiteralExpr | CallExpr - | ConditionExpr + | ClassExpr | ComputedPropertyNameExpr + | ConditionExpr + | DeleteExpr | ElementAccessExpr | FunctionExpr | Identifier @@ -23,19 +40,25 @@ export type Expr = | NullLiteralExpr | NumberLiteralExpr | ObjectLiteralExpr - | PropAccessExpr - | PropAssignExpr + | ParenthesizedExpr + | PostfixUnaryExpr + | PrivateIdentifier | PromiseArrayExpr | PromiseExpr + | PropAccessExpr + | PropAssignExpr | ReferenceExpr + | RegexExpr | SpreadAssignExpr | SpreadElementExpr | StringLiteralExpr | TemplateExpr + | ThisExpr | TypeOfExpr | UnaryExpr - | PostfixUnaryExpr - | UndefinedLiteralExpr; + | UndefinedLiteralExpr + | VoidExpr + | YieldExpr; export abstract class BaseExpr< Kind extends FunctionlessNode["kind"], @@ -49,27 +72,67 @@ export abstract class BaseExpr< readonly nodeKind: "Expr" = "Expr"; } +export class ArrowFunctionExpr< + F extends AnyFunction = AnyFunction +> extends BaseExpr<"ArrowFunctionExpr"> { + readonly _functionBrand?: F; + constructor(readonly parameters: ParameterDecl[], readonly body: BlockStmt) { + super("ArrowFunctionExpr", arguments); + } + + public clone(): this { + return new ArrowFunctionExpr( + this.parameters.map((p) => p.clone()), + this.body.clone() + ) as this; + } +} + export class FunctionExpr< F extends AnyFunction = AnyFunction > extends BaseExpr<"FunctionExpr"> { readonly _functionBrand?: F; - constructor(readonly parameters: ParameterDecl[], readonly body: BlockStmt) { - super("FunctionExpr"); - parameters.forEach((param) => param.setParent(this)); - body.setParent(this); + constructor( + readonly name: string | undefined, + readonly parameters: ParameterDecl[], + readonly body: BlockStmt + ) { + super("FunctionExpr", arguments); } public clone(): this { return new FunctionExpr( + this.name, this.parameters.map((p) => p.clone()), this.body.clone() ) as this; } } +export class ClassExpr extends BaseExpr< + "ClassExpr", + undefined +> { + readonly _classBrand?: C; + constructor( + readonly name: string | undefined, + readonly heritage: Expr | undefined, + readonly members: ClassMember[] + ) { + super("ClassExpr", arguments); + } + public clone(): this { + return new ClassExpr( + this.name, + this.heritage?.clone(), + this.members.map((m) => m.clone()) + ) as this; + } +} + export class ReferenceExpr extends BaseExpr<"ReferenceExpr"> { constructor(readonly name: string, readonly ref: () => R) { - super("ReferenceExpr"); + super("ReferenceExpr", arguments); } public clone(): this { @@ -81,7 +144,7 @@ export type VariableReference = Identifier | PropAccessExpr | ElementAccessExpr; export class Identifier extends BaseExpr<"Identifier"> { constructor(readonly name: string) { - super("Identifier"); + super("Identifier", arguments); } public clone(): this { @@ -93,14 +156,29 @@ export class Identifier extends BaseExpr<"Identifier"> { } } +export class PrivateIdentifier extends BaseExpr<"PrivateIdentifier"> { + constructor(readonly name: `#${string}`) { + super("PrivateIdentifier", arguments); + } + + public clone(): this { + return new PrivateIdentifier(this.name) as this; + } + + public lookup(): Decl | undefined { + return this.getLexicalScope().get(this.name); + } +} + export class PropAccessExpr extends BaseExpr<"PropAccessExpr"> { + readonly name: Identifier | PrivateIdentifier; constructor( readonly expr: Expr, - readonly name: string, + name: string | Identifier | PrivateIdentifier, readonly type?: string ) { - super("PropAccessExpr"); - expr.setParent(this); + super("PropAccessExpr", arguments); + this.name = typeof name === "string" ? new Identifier(name) : name; } public clone(): this { @@ -114,9 +192,7 @@ export class ElementAccessExpr extends BaseExpr<"ElementAccessExpr"> { readonly element: Expr, readonly type?: string ) { - super("ElementAccessExpr"); - expr.setParent(this); - element.setParent(this); + super("ElementAccessExpr", arguments); } public clone(): this { @@ -129,25 +205,21 @@ export class ElementAccessExpr extends BaseExpr<"ElementAccessExpr"> { } export class Argument extends BaseExpr<"Argument", CallExpr | NewExpr> { - constructor(readonly expr?: Expr, readonly name?: string) { - super("Argument"); - expr?.setParent(this); + constructor(readonly expr?: Expr) { + super("Argument", arguments); } public clone(): this { - return new Argument(this.expr?.clone(), this.name) as this; + return new Argument(this.expr?.clone()) as this; } } export class CallExpr extends BaseExpr<"CallExpr"> { - constructor(readonly expr: Expr, readonly args: Argument[]) { - super("CallExpr"); - expr.setParent(this); - args.forEach((arg) => arg.setParent(this)); - } - - public getArgument(name: string): Argument | undefined { - return this.args.find((arg) => arg.name === name); + constructor( + readonly expr: Expr | SuperKeyword | ImportKeyword, + readonly args: Argument[] + ) { + super("CallExpr", arguments); } public clone(): this { @@ -160,11 +232,9 @@ export class CallExpr extends BaseExpr<"CallExpr"> { export class NewExpr extends BaseExpr<"NewExpr"> { constructor(readonly expr: Expr, readonly args: Argument[]) { - super("NewExpr"); - expr.setParent(this); + super("NewExpr", arguments); for (const arg of Object.values(args)) { if (arg) { - arg.setParent(this); } } } @@ -179,11 +249,8 @@ export class NewExpr extends BaseExpr<"NewExpr"> { export class ConditionExpr extends BaseExpr<"ConditionExpr"> { constructor(readonly when: Expr, readonly then: Expr, readonly _else: Expr) { - super("ConditionExpr"); - when.setParent(this); - then.setParent(this); + super("ConditionExpr", arguments); if (_else) { - _else.setParent(this); } } @@ -216,9 +283,7 @@ export class BinaryExpr extends BaseExpr<"BinaryExpr"> { readonly op: BinaryOp, readonly right: Expr ) { - super("BinaryExpr"); - left.setParent(this); - right.setParent(this); + super("BinaryExpr", arguments); } public clone(): this { @@ -231,12 +296,11 @@ export class BinaryExpr extends BaseExpr<"BinaryExpr"> { } export type PostfixUnaryOp = "--" | "++"; -export type UnaryOp = "!" | "-" | PostfixUnaryOp; +export type UnaryOp = "!" | "-" | "~" | PostfixUnaryOp; export class UnaryExpr extends BaseExpr<"UnaryExpr"> { constructor(readonly op: UnaryOp, readonly expr: Expr) { - super("UnaryExpr"); - expr.setParent(this); + super("UnaryExpr", arguments); } public clone(): this { @@ -246,8 +310,7 @@ export class UnaryExpr extends BaseExpr<"UnaryExpr"> { export class PostfixUnaryExpr extends BaseExpr<"PostfixUnaryExpr"> { constructor(readonly op: PostfixUnaryOp, readonly expr: Expr) { - super("PostfixUnaryExpr"); - expr.setParent(this); + super("PostfixUnaryExpr", arguments); } public clone(): this { @@ -260,7 +323,7 @@ export class PostfixUnaryExpr extends BaseExpr<"PostfixUnaryExpr"> { export class NullLiteralExpr extends BaseExpr<"NullLiteralExpr"> { readonly value = null; constructor() { - super("NullLiteralExpr"); + super("NullLiteralExpr", arguments); } public clone(): this { @@ -272,7 +335,7 @@ export class UndefinedLiteralExpr extends BaseExpr<"UndefinedLiteralExpr"> { readonly value = undefined; constructor() { - super("UndefinedLiteralExpr"); + super("UndefinedLiteralExpr", arguments); } public clone(): this { @@ -282,7 +345,7 @@ export class UndefinedLiteralExpr extends BaseExpr<"UndefinedLiteralExpr"> { export class BooleanLiteralExpr extends BaseExpr<"BooleanLiteralExpr"> { constructor(readonly value: boolean) { - super("BooleanLiteralExpr"); + super("BooleanLiteralExpr", arguments); } public clone(): this { @@ -290,9 +353,19 @@ export class BooleanLiteralExpr extends BaseExpr<"BooleanLiteralExpr"> { } } +export class BigIntExpr extends BaseExpr<"BigIntExpr"> { + constructor(readonly value: bigint) { + super("BigIntExpr", arguments); + } + + public clone(): this { + return new BigIntExpr(this.value) as this; + } +} + export class NumberLiteralExpr extends BaseExpr<"NumberLiteralExpr"> { constructor(readonly value: number) { - super("NumberLiteralExpr"); + super("NumberLiteralExpr", arguments); } public clone(): this { @@ -302,7 +375,7 @@ export class NumberLiteralExpr extends BaseExpr<"NumberLiteralExpr"> { export class StringLiteralExpr extends BaseExpr<"StringLiteralExpr"> { constructor(readonly value: string) { - super("StringLiteralExpr"); + super("StringLiteralExpr", arguments); } public clone(): this { @@ -312,8 +385,7 @@ export class StringLiteralExpr extends BaseExpr<"StringLiteralExpr"> { export class ArrayLiteralExpr extends BaseExpr<"ArrayLiteralExpr"> { constructor(readonly items: Expr[]) { - super("ArrayLiteralExpr"); - items.forEach((item) => item.setParent(this)); + super("ArrayLiteralExpr", arguments); } public clone(): this { @@ -325,8 +397,7 @@ export type ObjectElementExpr = PropAssignExpr | SpreadAssignExpr; export class ObjectLiteralExpr extends BaseExpr<"ObjectLiteralExpr"> { constructor(readonly properties: ObjectElementExpr[]) { - super("ObjectLiteralExpr"); - properties.forEach((prop) => prop.setParent(this)); + super("ObjectLiteralExpr", arguments); } public clone(): this { @@ -337,10 +408,13 @@ export class ObjectLiteralExpr extends BaseExpr<"ObjectLiteralExpr"> { public getProperty(name: string) { return this.properties.find((prop) => { if (isPropAssignExpr(prop)) { - if (isIdentifier(prop.name)) { + if (isIdentifier(prop.name) || isPrivateIdentifier(prop.name)) { return prop.name.name === name; } else if (isStringLiteralExpr(prop.name)) { return prop.name.value === name; + } else if (isNumberLiteralExpr(prop.name)) { + // compare by string + return prop.name.value.toString(10) === name; } else if (isStringLiteralExpr(prop.name.expr)) { return prop.name.expr.value === name; } @@ -350,17 +424,19 @@ export class ObjectLiteralExpr extends BaseExpr<"ObjectLiteralExpr"> { } } +export type PropName = + | Identifier + | PrivateIdentifier + | ComputedPropertyNameExpr + | StringLiteralExpr + | NumberLiteralExpr; + export class PropAssignExpr extends BaseExpr< "PropAssignExpr", ObjectLiteralExpr > { - constructor( - readonly name: Identifier | ComputedPropertyNameExpr | StringLiteralExpr, - readonly expr: Expr - ) { - super("PropAssignExpr"); - name.setParent(this); - expr.setParent(this); + constructor(readonly name: PropName, readonly expr: Expr) { + super("PropAssignExpr", arguments); } /** @@ -385,8 +461,7 @@ export class ComputedPropertyNameExpr extends BaseExpr< PropAssignExpr > { constructor(readonly expr: Expr) { - super("ComputedPropertyNameExpr"); - expr.setParent(this); + super("ComputedPropertyNameExpr", arguments); } public clone(): this { @@ -399,8 +474,7 @@ export class SpreadAssignExpr extends BaseExpr< ObjectLiteralExpr > { constructor(readonly expr: Expr) { - super("SpreadAssignExpr"); - expr.setParent(this); + super("SpreadAssignExpr", arguments); } public clone(): this { @@ -413,8 +487,7 @@ export class SpreadElementExpr extends BaseExpr< ObjectLiteralExpr > { constructor(readonly expr: Expr) { - super("SpreadElementExpr"); - expr.setParent(this); + super("SpreadElementExpr", arguments); } public clone(): this { @@ -427,8 +500,7 @@ export class SpreadElementExpr extends BaseExpr< */ export class TemplateExpr extends BaseExpr<"TemplateExpr"> { constructor(readonly exprs: Expr[]) { - super("TemplateExpr"); - exprs.forEach((expr) => expr.setParent(this)); + super("TemplateExpr", arguments); } public clone(): this { @@ -438,9 +510,7 @@ export class TemplateExpr extends BaseExpr<"TemplateExpr"> { export class TypeOfExpr extends BaseExpr<"TypeOfExpr"> { constructor(readonly expr: Expr) { - super("TypeOfExpr"); - - expr.setParent(this); + super("TypeOfExpr", arguments); } public clone(): this { @@ -450,9 +520,7 @@ export class TypeOfExpr extends BaseExpr<"TypeOfExpr"> { export class AwaitExpr extends BaseExpr<"AwaitExpr"> { constructor(readonly expr: Expr) { - super("AwaitExpr"); - - expr.setParent(this); + super("AwaitExpr", arguments); } public clone(): this { @@ -462,9 +530,7 @@ export class AwaitExpr extends BaseExpr<"AwaitExpr"> { export class PromiseExpr extends BaseExpr<"PromiseExpr"> { constructor(readonly expr: Expr) { - super("PromiseExpr"); - - expr.setParent(this); + super("PromiseExpr", arguments); } public clone(): this { @@ -474,9 +540,7 @@ export class PromiseExpr extends BaseExpr<"PromiseExpr"> { export class PromiseArrayExpr extends BaseExpr<"PromiseArrayExpr"> { constructor(readonly expr: Expr) { - super("PromiseArrayExpr"); - - expr.setParent(this); + super("PromiseArrayExpr", arguments); } public clone(): this { @@ -484,5 +548,111 @@ export class PromiseArrayExpr extends BaseExpr<"PromiseArrayExpr"> { } } +export class ThisExpr extends BaseExpr<"ThisExpr"> { + constructor( + /** + * Produce the value of `this` + */ + readonly ref: () => T + ) { + super("ThisExpr", arguments); + } + public clone(): this { + return new ThisExpr(this.ref) as this; + } +} + +export class SuperKeyword extends BaseNode<"SuperKeyword"> { + // `super` is not an expression - a reference to it does not yield a value + // it only supports the following interactions + // 1. call in a constructor - `super(..)` + // 2. call a method on it - `super.method(..)`. + readonly nodeKind = "Node"; + constructor() { + super("SuperKeyword", arguments); + } + public clone(): this { + return new SuperKeyword() as this; + } +} + +export class ImportKeyword extends BaseNode<"ImportKeyword"> { + readonly nodeKind = "Node"; + constructor() { + super("ImportKeyword", arguments); + } + public clone(): this { + return new ImportKeyword() as this; + } +} + +export class YieldExpr extends BaseExpr<"YieldExpr"> { + constructor( + /** + * The expression to yield (or delegate) to. + */ + readonly expr: Expr | undefined, + /** + * Is a `yield*` delegate expression. + */ + readonly delegate: boolean + ) { + super("YieldExpr", arguments); + } + public clone(): this { + return new YieldExpr(this.expr?.clone(), this.delegate) as this; + } +} + +export class RegexExpr extends BaseExpr<"RegexExpr"> { + constructor(readonly regex: RegExp) { + super("RegexExpr", arguments); + } + + public clone(): this { + return new RegexExpr(this.regex) as this; + } +} + +export class VoidExpr extends BaseExpr<"VoidExpr"> { + constructor( + /** + * The expression to yield (or delegate) to. + */ + readonly expr: Expr + ) { + super("VoidExpr", arguments); + } + public clone(): this { + return new VoidExpr(this.expr?.clone()) as this; + } +} + +export class DeleteExpr extends BaseExpr<"DeleteExpr"> { + constructor(readonly expr: PropAccessExpr | ElementAccessExpr) { + super("DeleteExpr", arguments); + } + public clone(): this { + return new DeleteExpr(this.expr?.clone()) as this; + } +} + +export class ParenthesizedExpr extends BaseExpr<"ParenthesizedExpr"> { + constructor(readonly expr: Expr) { + super("ParenthesizedExpr", arguments); + } + + public clone(): this { + return new ParenthesizedExpr(this.expr.clone()) as this; + } + + public unwrap(): Expr | undefined { + if (isParenthesizedExpr(this.expr)) { + return this.expr.unwrap(); + } + return this.expr; + } +} + // to prevent the closure serializer from trying to import all of functionless. export const deploymentOnlyModule = true; diff --git a/src/function.ts b/src/function.ts index a6c483e2..e721e77b 100644 --- a/src/function.ts +++ b/src/function.ts @@ -351,10 +351,8 @@ abstract class FunctionBase }); }, request(call, context) { - const payloadArg = call.getArgument("payload"); - const payload = payloadArg?.expr - ? context.eval(payloadArg.expr) - : "$null"; + const payloadArg = call.args[0]?.expr; + const payload = payloadArg ? context.eval(payloadArg) : "$null"; const request = context.var( `{"version": "2018-05-29", "operation": "Invoke", "payload": ${payload}}` @@ -365,8 +363,8 @@ abstract class FunctionBase this.apiGWVtl = { renderRequest: (call, context) => { - const payloadArg = call.getArgument("payload"); - return payloadArg?.expr ? context.exprToJson(payloadArg.expr) : "$null"; + const payloadArg = call.args[0]?.expr; + return payloadArg ? context.exprToJson(payloadArg) : "$null"; }, createIntegration: (options) => { @@ -381,7 +379,7 @@ abstract class FunctionBase } public asl(call: CallExpr, context: ASL) { - const payloadArg = call.getArgument("payload")?.expr; + const payloadArg = call.args[0]?.expr; this.resource.grantInvoke(context.role); return payloadArg diff --git a/src/guards.ts b/src/guards.ts index 85150d97..4379bdf0 100644 --- a/src/guards.ts +++ b/src/guards.ts @@ -1,4 +1,4 @@ -import { BindingPattern } from "./declaration"; +import type { BindingPattern, Decl } from "./declaration"; import type { Expr, VariableReference } from "./expression"; import type { FunctionlessNode } from "./node"; import type { Stmt } from "./statement"; @@ -10,66 +10,49 @@ export function isNode(a: any): a is FunctionlessNode { export const isErr = typeGuard("Err"); export function isExpr(a: any): a is Expr { - return ( - isNode(a) && - (isArgument(a) || - isArrayLiteralExpr(a) || - isAwaitExpr(a) || - isBinaryExpr(a) || - isBooleanLiteralExpr(a) || - isCallExpr(a) || - isConditionExpr(a) || - isComputedPropertyNameExpr(a) || - isFunctionExpr(a) || - isElementAccessExpr(a) || - isFunctionExpr(a) || - isIdentifier(a) || - isNewExpr(a) || - isNullLiteralExpr(a) || - isNumberLiteralExpr(a) || - isObjectLiteralExpr(a) || - isPromiseArrayExpr(a) || - isPromiseExpr(a) || - isPropAccessExpr(a) || - isPropAssignExpr(a) || - isReferenceExpr(a) || - isStringLiteralExpr(a) || - isTemplateExpr(a) || - isTypeOfExpr(a) || - isUnaryExpr(a) || - isPostfixUnaryExpr(a) || - isUndefinedLiteralExpr(a)) - ); + return isNode(a) && a.nodeKind === "Expr"; } -export const isFunctionExpr = typeGuard("FunctionExpr"); -export const isReferenceExpr = typeGuard("ReferenceExpr"); -export const isIdentifier = typeGuard("Identifier"); -export const isPropAccessExpr = typeGuard("PropAccessExpr"); -export const isElementAccessExpr = typeGuard("ElementAccessExpr"); export const isArgument = typeGuard("Argument"); +export const isArrayLiteralExpr = typeGuard("ArrayLiteralExpr"); +export const isArrowFunctionExpr = typeGuard("ArrowFunctionExpr"); +export const isAwaitExpr = typeGuard("AwaitExpr"); +export const isBigIntExpr = typeGuard("BigIntExpr"); +export const isBinaryExpr = typeGuard("BinaryExpr"); +export const isBooleanLiteralExpr = typeGuard("BooleanLiteralExpr"); export const isCallExpr = typeGuard("CallExpr"); -export const isNewExpr = typeGuard("NewExpr"); +export const isClassExpr = typeGuard("ClassExpr"); +export const isComputedPropertyNameExpr = typeGuard("ComputedPropertyNameExpr"); export const isConditionExpr = typeGuard("ConditionExpr"); -export const isBinaryExpr = typeGuard("BinaryExpr"); -export const isUnaryExpr = typeGuard("UnaryExpr"); -export const isPostfixUnaryExpr = typeGuard("PostfixUnaryExpr"); +export const isDeleteExpr = typeGuard("DeleteExpr"); +export const isElementAccessExpr = typeGuard("ElementAccessExpr"); +export const isFunctionExpr = typeGuard("FunctionExpr"); +export const isIdentifier = typeGuard("Identifier"); +export const isImportKeyword = typeGuard("ImportKeyword"); +export const isNewExpr = typeGuard("NewExpr"); export const isNullLiteralExpr = typeGuard("NullLiteralExpr"); -export const isUndefinedLiteralExpr = typeGuard("UndefinedLiteralExpr"); -export const isBooleanLiteralExpr = typeGuard("BooleanLiteralExpr"); export const isNumberLiteralExpr = typeGuard("NumberLiteralExpr"); -export const isStringLiteralExpr = typeGuard("StringLiteralExpr"); -export const isArrayLiteralExpr = typeGuard("ArrayLiteralExpr"); export const isObjectLiteralExpr = typeGuard("ObjectLiteralExpr"); +export const isParenthesizedExpr = typeGuard("ParenthesizedExpr"); +export const isPostfixUnaryExpr = typeGuard("PostfixUnaryExpr"); +export const isPrivateIdentifier = typeGuard("PrivateIdentifier"); +export const isPromiseArrayExpr = typeGuard("PromiseArrayExpr"); +export const isPromiseExpr = typeGuard("PromiseExpr"); +export const isPropAccessExpr = typeGuard("PropAccessExpr"); export const isPropAssignExpr = typeGuard("PropAssignExpr"); -export const isComputedPropertyNameExpr = typeGuard("ComputedPropertyNameExpr"); +export const isReferenceExpr = typeGuard("ReferenceExpr"); +export const isRegexExpr = typeGuard("RegexExpr"); export const isSpreadAssignExpr = typeGuard("SpreadAssignExpr"); export const isSpreadElementExpr = typeGuard("SpreadElementExpr"); +export const isStringLiteralExpr = typeGuard("StringLiteralExpr"); +export const isSuperKeyword = typeGuard("SuperKeyword"); export const isTemplateExpr = typeGuard("TemplateExpr"); +export const isThisExpr = typeGuard("ThisExpr"); export const isTypeOfExpr = typeGuard("TypeOfExpr"); -export const isPromiseExpr = typeGuard("PromiseExpr"); -export const isPromiseArrayExpr = typeGuard("PromiseArrayExpr"); -export const isAwaitExpr = typeGuard("AwaitExpr"); +export const isUnaryExpr = typeGuard("UnaryExpr"); +export const isUndefinedLiteralExpr = typeGuard("UndefinedLiteralExpr"); +export const isVoidExpr = typeGuard("VoidExpr"); +export const isYieldExpr = typeGuard("YieldExpr"); export const isObjectElementExpr = typeGuard( "PropAssignExpr", @@ -94,48 +77,61 @@ export const isLiteralPrimitiveExpr = typeGuard( ); export function isStmt(a: any): a is Stmt { - return ( - isNode(a) && - (isBreakStmt(a) || - isBlockStmt(a) || - isCatchClause(a) || - isContinueStmt(a) || - isDoStmt(a) || - isExprStmt(a) || - isForInStmt(a) || - isForOfStmt(a) || - isForStmt(a) || - isIfStmt(a) || - isReturnStmt(a) || - isThrowStmt(a) || - isTryStmt(a) || - isVariableStmt(a) || - isWhileStmt(a)) - ); + return isNode(a) && a.nodeKind === "Stmt"; } -export const isExprStmt = typeGuard("ExprStmt"); -export const isVariableStmt = typeGuard("VariableStmt"); export const isBlockStmt = typeGuard("BlockStmt"); -export const isReturnStmt = typeGuard("ReturnStmt"); -export const isIfStmt = typeGuard("IfStmt"); -export const isForOfStmt = typeGuard("ForOfStmt"); -export const isForInStmt = typeGuard("ForInStmt"); export const isForStmt = typeGuard("ForStmt"); export const isBreakStmt = typeGuard("BreakStmt"); -export const isContinueStmt = typeGuard("ContinueStmt"); -export const isTryStmt = typeGuard("TryStmt"); +export const isCaseClause = typeGuard("CaseClause"); export const isCatchClause = typeGuard("CatchClause"); +export const isContinueStmt = typeGuard("ContinueStmt"); +export const isDebuggerStmt = typeGuard("DebuggerStmt"); +export const isDefaultClause = typeGuard("DefaultClause"); +export const isDoStmt = typeGuard("DoStmt"); +export const isEmptyStmt = typeGuard("EmptyStmt"); +export const isExprStmt = typeGuard("ExprStmt"); +export const isForInStmt = typeGuard("ForInStmt"); +export const isForOfStmt = typeGuard("ForOfStmt"); +export const isIfStmt = typeGuard("IfStmt"); +export const isLabelledStmt = typeGuard("LabelledStmt"); +export const isReturnStmt = typeGuard("ReturnStmt"); +export const isSwitchStmt = typeGuard("SwitchStmt"); export const isThrowStmt = typeGuard("ThrowStmt"); +export const isTryStmt = typeGuard("TryStmt"); +export const isVariableStmt = typeGuard("VariableStmt"); export const isWhileStmt = typeGuard("WhileStmt"); -export const isDoStmt = typeGuard("DoStmt"); +export const isWithStmt = typeGuard("WithStmt"); + +export const isSwitchClause = typeGuard("CaseClause", "DefaultClause"); + +export function isDecl(a: any): a is Decl { + return isNode(a) && a.nodeKind === "Decl"; +} +export const isClassDecl = typeGuard("ClassDecl"); +export const isClassStaticBlockDecl = typeGuard("ClassStaticBlockDecl"); +export const isConstructorDecl = typeGuard("ConstructorDecl"); export const isFunctionDecl = typeGuard("FunctionDecl"); +export const isMethodDecl = typeGuard("MethodDecl"); export const isParameterDecl = typeGuard("ParameterDecl"); -export const isBindingElem = typeGuard("BindingElem"); +export const isPropDecl = typeGuard("PropDecl"); +export const isClassMember = typeGuard( + "ClassStaticBlockDecl", + "ConstructorDecl", + "MethodDecl", + "PropDecl" +); export const isVariableDecl = typeGuard("VariableDecl"); -export const isObjectBinding = typeGuard("ObjectBinding"); export const isArrayBinding = typeGuard("ArrayBinding"); +export const isBindingElem = typeGuard("BindingElem"); +export const isObjectBinding = typeGuard("ObjectBinding"); + +export const isPropName = typeGuard( + "Identifier", + "ComputedPropertyNameExpr", + "StringLiteralExpr" +); export const isBindingPattern = (a: any): a is BindingPattern => isNode(a) && (isObjectBinding(a) || isArrayBinding(a)); diff --git a/src/integration.ts b/src/integration.ts index 40631627..6129b148 100644 --- a/src/integration.ts +++ b/src/integration.ts @@ -9,6 +9,7 @@ import { isCallExpr, isPromiseExpr, isReferenceExpr, + isThisExpr, } from "./guards"; import { FunctionlessNode } from "./node"; import { AnyFunction } from "./util"; @@ -28,7 +29,7 @@ export function isIntegrationCallExpr( ): node is IntegrationCallExpr { return ( isCallExpr(node) && - isReferenceExpr(node.expr) && + (isReferenceExpr(node.expr) || isThisExpr(node.expr)) && isIntegration(node.expr.ref()) ); } diff --git a/src/node.ts b/src/node.ts index ff0484f9..e90966a6 100644 --- a/src/node.ts +++ b/src/node.ts @@ -1,4 +1,4 @@ -import { +import type { BindingElem, BindingPattern, Decl, @@ -7,7 +7,7 @@ import { VariableDeclList, } from "./declaration"; import type { Err } from "./error"; -import { Expr } from "./expression"; +import type { Expr, ImportKeyword, SuperKeyword } from "./expression"; import { isBlockStmt, isTryStmt, @@ -26,14 +26,17 @@ import { isBindingElem, isIdentifier, isVariableDecl, + isNode, } from "./guards"; -import { BlockStmt, CatchClause, Stmt } from "./statement"; +import type { BlockStmt, CatchClause, Stmt } from "./statement"; export type FunctionlessNode = | Decl | Expr | Stmt | Err + | SuperKeyword + | ImportKeyword | BindingPattern | VariableDeclList; @@ -56,7 +59,20 @@ export abstract class BaseNode< */ readonly children: FunctionlessNode[] = []; - constructor(readonly kind: Kind) {} + constructor(readonly kind: Kind, args: IArguments) { + const setParent = (node: any) => { + if (!node) { + return; + } else if (isNode(node)) { + node.setParent(this as FunctionlessNode); + } else if (Array.isArray(node)) { + node.forEach(setParent); + } + }; + for (const arg of args) { + setParent(arg); + } + } public abstract clone(): this; @@ -334,7 +350,7 @@ export abstract class BaseNode< if (isBindingPattern(node.name)) { return getNames(node.name); } - return [[node.name, node]]; + return [[node.name.name, node]]; } else if (isBindingElem(node)) { if (isIdentifier(node.name)) { return [[node.name.name, node]]; @@ -344,12 +360,12 @@ export abstract class BaseNode< return node.bindings.flatMap((b) => getNames(b)); } else if (isFunctionExpr(node) || isFunctionDecl(node)) { return node.parameters.flatMap((param) => - typeof param.name === "string" - ? [[param.name, param]] + isIdentifier(param.name) + ? [[param.name.name, param]] : getNames(param.name) ); } else if (isForInStmt(node) || isForOfStmt(node)) { - return getNames(node.variableDecl); + return getNames(node.initializer); } else if (isCatchClause(node) && node.variableDecl?.name) { return getNames(node.variableDecl); } else { diff --git a/src/statement.ts b/src/statement.ts index 6f4d2c3a..045bd63e 100644 --- a/src/statement.ts +++ b/src/statement.ts @@ -16,17 +16,23 @@ export type Stmt = | BlockStmt | CatchClause | ContinueStmt + | DebuggerStmt | DoStmt + | EmptyStmt | ExprStmt | ForInStmt | ForOfStmt | ForStmt | IfStmt + | LabelledStmt | ReturnStmt + | SwitchStmt + | SwitchClause | ThrowStmt | TryStmt | VariableStmt - | WhileStmt; + | WhileStmt + | WithStmt; export abstract class BaseStmt< Kind extends FunctionlessNode["kind"], @@ -46,8 +52,7 @@ export abstract class BaseStmt< export class ExprStmt extends BaseStmt<"ExprStmt"> { constructor(readonly expr: Expr) { - super("ExprStmt"); - expr.setParent(this); + super("ExprStmt", arguments); } public clone(): this { @@ -57,8 +62,7 @@ export class ExprStmt extends BaseStmt<"ExprStmt"> { export class VariableStmt extends BaseStmt<"VariableStmt"> { constructor(readonly declList: VariableDeclList) { - super("VariableStmt"); - declList.setParent(this); + super("VariableStmt", arguments); } public clone(): this { @@ -80,9 +84,8 @@ export type BlockStmtParent = export class BlockStmt extends BaseStmt<"BlockStmt", BlockStmtParent> { constructor(readonly statements: Stmt[]) { - super("BlockStmt"); + super("BlockStmt", arguments); statements.forEach((stmt, i) => { - stmt.setParent(this as never); stmt.prev = i > 0 ? statements[i - 1] : undefined; stmt.next = i + 1 < statements.length ? statements[i + 1] : undefined; }); @@ -123,8 +126,7 @@ export class BlockStmt extends BaseStmt<"BlockStmt", BlockStmtParent> { export class ReturnStmt extends BaseStmt<"ReturnStmt"> { constructor(readonly expr: Expr) { - super("ReturnStmt"); - expr.setParent(this); + super("ReturnStmt", arguments); } public clone(): this { @@ -134,11 +136,8 @@ export class ReturnStmt extends BaseStmt<"ReturnStmt"> { export class IfStmt extends BaseStmt<"IfStmt"> { constructor(readonly when: Expr, readonly then: Stmt, readonly _else?: Stmt) { - super("IfStmt"); - when.setParent(this as never); - then.setParent(this); + super("IfStmt", arguments); if (_else) { - _else.setParent(this); } } @@ -153,19 +152,16 @@ export class IfStmt extends BaseStmt<"IfStmt"> { export class ForOfStmt extends BaseStmt<"ForOfStmt"> { constructor( - readonly variableDecl: VariableDecl | Identifier, + readonly initializer: VariableDecl | Identifier, readonly expr: Expr, readonly body: BlockStmt ) { - super("ForOfStmt"); - variableDecl.setParent(this); - expr.setParent(this as never); - body.setParent(this); + super("ForOfStmt", arguments); } public clone(): this { return new ForOfStmt( - this.variableDecl.clone(), + this.initializer.clone(), this.expr.clone(), this.body.clone() ) as this; @@ -174,19 +170,16 @@ export class ForOfStmt extends BaseStmt<"ForOfStmt"> { export class ForInStmt extends BaseStmt<"ForInStmt"> { constructor( - readonly variableDecl: VariableDecl | Identifier, + readonly initializer: VariableDecl | Identifier, readonly expr: Expr, readonly body: BlockStmt ) { - super("ForInStmt"); - variableDecl.setParent(this); - expr.setParent(this as never); - body.setParent(this); + super("ForInStmt", arguments); } public clone(): this { return new ForInStmt( - this.variableDecl.clone(), + this.initializer.clone(), this.expr.clone(), this.body.clone() ) as this; @@ -196,21 +189,17 @@ export class ForInStmt extends BaseStmt<"ForInStmt"> { export class ForStmt extends BaseStmt<"ForStmt"> { constructor( readonly body: BlockStmt, - readonly variableDecl?: VariableDeclList | Expr, + readonly initializer?: VariableDeclList | Expr, readonly condition?: Expr, readonly incrementor?: Expr ) { - super("ForStmt"); - variableDecl?.setParent(this); - condition?.setParent(this); - incrementor?.setParent(this); - body.setParent(this); + super("ForStmt", arguments); } public clone(): this { return new ForStmt( this.body.clone(), - this.variableDecl?.clone(), + this.initializer?.clone(), this.condition?.clone(), this.incrementor?.clone() ) as this; @@ -219,7 +208,7 @@ export class ForStmt extends BaseStmt<"ForStmt"> { export class BreakStmt extends BaseStmt<"BreakStmt"> { constructor() { - super("BreakStmt"); + super("BreakStmt", arguments); } public clone(): this { @@ -229,7 +218,7 @@ export class BreakStmt extends BaseStmt<"BreakStmt"> { export class ContinueStmt extends BaseStmt<"ContinueStmt"> { constructor() { - super("ContinueStmt"); + super("ContinueStmt", arguments); } public clone(): this { @@ -249,13 +238,10 @@ export class TryStmt extends BaseStmt<"TryStmt"> { readonly catchClause?: CatchClause, readonly finallyBlock?: FinallyBlock ) { - super("TryStmt"); - tryBlock.setParent(this); + super("TryStmt", arguments); if (catchClause) { - catchClause.setParent(this); } if (finallyBlock) { - finallyBlock.setParent(this); } } @@ -273,11 +259,9 @@ export class CatchClause extends BaseStmt<"CatchClause", TryStmt> { readonly variableDecl: VariableDecl | undefined, readonly block: BlockStmt ) { - super("CatchClause"); + super("CatchClause", arguments); if (variableDecl) { - variableDecl.setParent(this); } - block.setParent(this); } public clone(): this { @@ -290,8 +274,7 @@ export class CatchClause extends BaseStmt<"CatchClause", TryStmt> { export class ThrowStmt extends BaseStmt<"ThrowStmt"> { constructor(readonly expr: Expr) { - super("ThrowStmt"); - expr.setParent(this as never); + super("ThrowStmt", arguments); } public clone(): this { @@ -301,9 +284,7 @@ export class ThrowStmt extends BaseStmt<"ThrowStmt"> { export class WhileStmt extends BaseStmt<"WhileStmt"> { constructor(readonly condition: Expr, readonly block: BlockStmt) { - super("WhileStmt"); - condition.setParent(this); - block.setParent(this); + super("WhileStmt", arguments); } public clone(): this { @@ -313,9 +294,7 @@ export class WhileStmt extends BaseStmt<"WhileStmt"> { export class DoStmt extends BaseStmt<"DoStmt"> { constructor(readonly block: BlockStmt, readonly condition: Expr) { - super("DoStmt"); - block.setParent(this); - condition.setParent(this); + super("DoStmt", arguments); } public clone(): this { @@ -323,5 +302,78 @@ export class DoStmt extends BaseStmt<"DoStmt"> { } } +export class LabelledStmt extends BaseStmt<"LabelledStmt"> { + constructor(readonly label: string, readonly stmt: Stmt) { + super("LabelledStmt", arguments); + } + + public clone(): this { + return new LabelledStmt(this.label, this.stmt.clone()) as this; + } +} + +export class DebuggerStmt extends BaseStmt<"DebuggerStmt"> { + constructor() { + super("DebuggerStmt", arguments); + } + public clone(): this { + return new DebuggerStmt() as this; + } +} + +export class SwitchStmt extends BaseStmt<"SwitchStmt"> { + constructor(readonly clauses: SwitchClause[]) { + super("SwitchStmt", arguments); + } + + public clone(): this { + return new SwitchStmt(this.clauses.map((clause) => clause.clone())) as this; + } +} + +export type SwitchClause = CaseClause | DefaultClause; + +export class CaseClause extends BaseStmt<"CaseClause"> { + constructor(readonly expr: Expr, readonly statements: Stmt[]) { + super("CaseClause", arguments); + } + public clone(): this { + return new CaseClause( + this.expr.clone(), + this.statements.map((stmt) => stmt.clone()) + ) as this; + } +} + +export class DefaultClause extends BaseStmt<"DefaultClause"> { + constructor(readonly statements: Stmt[]) { + super("DefaultClause", arguments); + } + public clone(): this { + return new DefaultClause( + this.statements.map((stmt) => stmt.clone()) + ) as this; + } +} + +export class EmptyStmt extends BaseStmt<"EmptyStmt"> { + constructor() { + super("EmptyStmt", arguments); + } + public clone(): this { + return new EmptyStmt() as this; + } +} + +export class WithStmt extends BaseStmt<"WithStmt"> { + constructor(readonly expr: Expr, readonly stmt: Stmt) { + super("WithStmt", arguments); + } + + public clone(): this { + return new WithStmt(this.expr.clone(), this.stmt.clone()) as this; + } +} + // to prevent the closure serializer from trying to import all of functionless. export const deploymentOnlyModule = true; diff --git a/src/step-function.ts b/src/step-function.ts index b7dbe63b..6d58ca80 100644 --- a/src/step-function.ts +++ b/src/step-function.ts @@ -28,6 +28,7 @@ import { isErr, isFunctionDecl, isFunctionExpr, + isIdentifier, isNumberLiteralExpr, isObjectLiteralExpr, isPropAssignExpr, @@ -139,7 +140,7 @@ export namespace $SFN { (seconds: number) => void >("waitFor", { asl(call, context) { - const seconds = call.args[0].expr; + const seconds = call.args[0]?.expr; if (seconds === undefined) { throw new Error("the 'seconds' argument is required"); } @@ -311,7 +312,8 @@ export namespace $SFN { }); function mapOrForEach(call: CallExpr, context: ASL) { - const callbackfn = call.getArgument("callbackfn")?.expr; + const callbackfn = + call.args.length === 3 ? call.args[2]?.expr : call.args[1]?.expr; if (callbackfn === undefined || callbackfn.kind !== "FunctionExpr") { throw new Error("missing callbackfn in $SFN.map"); } @@ -322,7 +324,7 @@ export namespace $SFN { `a $SFN.Map or $SFN.ForEach block must have at least one Stmt` ); } - const props = call.getArgument("props")?.expr; + const props = call.args.length === 3 ? call.args[1]?.expr : undefined; let maxConcurrency: number | undefined; if (props !== undefined) { if (isObjectLiteralExpr(props)) { @@ -344,7 +346,7 @@ export namespace $SFN { throw new Error("argument 'props' must be an ObjectLiteralExpr"); } } - const array = call.getArgument("array")?.expr; + const array = call.args[0]?.expr; if (array === undefined) { throw new Error("missing argument 'array'"); } @@ -365,14 +367,25 @@ export namespace $SFN { Parameters: { ...context.cloneLexicalScopeParameters(call), ...Object.fromEntries( - callbackfn.parameters.map((param, i) => [ - `${param.name}.$`, - i === 0 - ? "$$.Map.Item.Value" - : i == 1 - ? "$$.Map.Item.Index" - : arrayPath, - ]) + callbackfn.parameters.map((param, i) => { + const paramName = isIdentifier(param?.name) + ? param.name.name + : undefined; + if (paramName === undefined) { + throw new SynthError( + ErrorCodes.Unsupported_Feature, + "Destructured parameter declarations are not yet supported by Step Functions. https://github.com/functionless/functionless/issues/364" + ); + } + return [ + `${paramName}.$`, + i === 0 + ? "$$.Map.Item.Value" + : i == 1 + ? "$$.Map.Item.Index" + : arrayPath, + ]; + }) ), }, }, @@ -405,22 +418,17 @@ export namespace $SFN { }> >("parallel", { asl(call, context) { - const paths = call.getArgument("paths")?.expr; - if (paths === undefined) { - throw new Error("missing required argument 'paths'"); - } - if (paths.kind !== "ArrayLiteralExpr") { - throw new Error("invalid arguments to $SFN.parallel"); - } + const paths = call.args.map((arg) => arg.expr); + ensureItemOf( - paths.items, + paths, isFunctionExpr, "each parallel path must be an inline FunctionExpr" ); return context.stateWithHeapOutput({ Type: "Parallel", - Branches: paths.items.map((func) => { + Branches: paths.map((func) => { const funcBody = context.evalStmt(func.body); if (!funcBody) { @@ -437,6 +445,77 @@ export namespace $SFN { }); } +/** + * Data types allowed as a Step Function Cause. + */ +export type StepFunctionCause = + | null + | boolean + | number + | string + | StepFunctionCause[] + | { + [prop: string]: StepFunctionCause; + }; + +/** + * An Error that can be thrown from within a {@link StepFunction} or {@link ExpressStepFunction}. + * + * It encodes a `Fail` state in ASL. + * ```json + * { + * "Type": "Fail", + * "Error": , + * "Cause": JSON.stringify() + * } + * ``` + * + * For example: + * ```ts + * throw new StepFunctionError("MyError", { "key": "value"}); + * ``` + * + * Produces the following Fail state: + * ```json + * { + * "Type": "Fail", + * "Error": "MyError", + * "Cause": "{\"key\":\"value\""}" + * } + * ``` + */ +export class StepFunctionError extends Error { + static readonly kind = "StepFunctionError"; + + public static isConstructor(a: any): a is typeof StepFunctionError { + return a === StepFunctionError || a?.kind === StepFunctionError.kind; + } + + constructor( + /** + * The name of the Error to place in the Fail state. + */ + readonly error: string, + /* + * A JSON object to be encoded as the `Cause`. + * + * Due to limitations in Step Functions, all values in the {@link cause} must be + * a literal value - no references or calls are allowed. + * + * ```ts + * // valid + * new StepFunctionError("Error", { data: "prop" }) + * // invalid + * new StepFunctionError("Error", { data: ref }) + * new StepFunctionError("Error", { data: call() }) + * ``` + */ + readonly cause: StepFunctionCause + ) { + super(); + } +} + function makeStepFunctionIntegration( methodName: K, integration: Omit, "kind"> @@ -888,14 +967,14 @@ function retrieveMachineArgs(call: CallExpr) { // machine({ input: { ... } }) // Inline with reference // machine({ input: ref, name: "hi", traceHeader: "hi" }) - const arg = call.args[0]; + const arg = call.args[0]?.expr; - if (!arg.expr || !isObjectLiteralExpr(arg.expr)) { + if (!arg || !isObjectLiteralExpr(arg)) { throw Error( "Step function invocation must use a single, inline object parameter. Variable references are not supported currently." ); } else if ( - arg.expr.properties.some( + arg.properties.some( (x) => isSpreadAssignExpr(x) || isComputedPropertyNameExpr(x.name) ) ) { @@ -906,9 +985,9 @@ function retrieveMachineArgs(call: CallExpr) { // we know the keys cannot be computed, so it is safe to use getProperty return { - name: arg.expr.getProperty("name")?.expr, - traceHeader: arg.expr.getProperty("traceHeader")?.expr, - input: arg.expr.getProperty("input")?.expr, + name: arg.getProperty("name")?.expr, + traceHeader: arg.getProperty("traceHeader")?.expr, + input: arg.getProperty("input")?.expr, }; } @@ -1310,7 +1389,7 @@ class BaseStandardStepFunction< this.resource.grantRead(context.role); const executionArnExpr = assertDefined( - call.args[0].expr, + call.args[0]?.expr, "Describe Execution requires a single string argument." ); @@ -1513,7 +1592,7 @@ class ImportedStepFunction< } function getArgs(call: CallExpr) { - const executionArn = call.args[0]?.expr; + const executionArn = call.args[0]; if (executionArn === undefined) { throw new Error("missing argument 'executionArn'"); } diff --git a/src/table.ts b/src/table.ts index d02d9f55..384202a0 100644 --- a/src/table.ts +++ b/src/table.ts @@ -295,7 +295,7 @@ class BaseTable< request(call, vtl) { const input = vtl.eval( assertNodeKind( - call.getArgument("input")?.expr, + call.args[0]?.expr, "ObjectLiteralExpr" ) ); @@ -315,7 +315,7 @@ class BaseTable< request: (call, vtl) => { const input = vtl.eval( assertNodeKind( - call.getArgument("input")?.expr, + call.args[0]?.expr, "ObjectLiteralExpr" ) ); @@ -339,7 +339,7 @@ class BaseTable< request: (call, vtl) => { const input = vtl.eval( assertNodeKind( - call.getArgument("input")?.expr, + call.args[0]?.expr, "ObjectLiteralExpr" ) ); @@ -361,7 +361,7 @@ class BaseTable< request: (call, vtl) => { const input = vtl.eval( assertNodeKind( - call.getArgument("input")?.expr, + call.args[0]?.expr, "ObjectLiteralExpr" ) ); @@ -382,7 +382,7 @@ class BaseTable< request: (call, vtl) => { const input = vtl.eval( assertNodeKind( - call.getArgument("input")?.expr, + call.args[0]?.expr, "ObjectLiteralExpr" ) ); diff --git a/src/util.ts b/src/util.ts index 14476d37..b3f98d54 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,5 +1,6 @@ import { Construct } from "constructs"; import ts from "typescript"; +import { ErrorCodes, SynthError } from "./error-code"; import { BinaryOp, CallExpr, Expr, PropAccessExpr } from "./expression"; import { isArrayLiteralExpr, @@ -14,6 +15,7 @@ import { isNullLiteralExpr, isNumberLiteralExpr, isObjectLiteralExpr, + isPrivateIdentifier, isPropAccessExpr, isPropAssignExpr, isReferenceExpr, @@ -25,6 +27,7 @@ import { } from "./guards"; import { FunctionlessNode } from "./node"; +export type AnyClass = new (...args: any[]) => any; export type AnyFunction = (...args: any[]) => any; export type AnyAsyncFunction = (...args: any[]) => Promise; @@ -80,7 +83,7 @@ export function ensure( message: string ): asserts a is T { if (!is(a)) { - throw new Error(message); + throw new SynthError(ErrorCodes.Unexpected_Error, message); } } @@ -165,7 +168,8 @@ export function isPromiseAll(expr: CallExpr): expr is CallExpr & { } { return ( isPropAccessExpr(expr.expr) && - expr.expr.name === "all" && + isIdentifier(expr.expr.name) && + expr.expr.name.name === "all" && ((isIdentifier(expr.expr.expr) && expr.expr.expr.name === "Promise") || (isReferenceExpr(expr.expr.expr) && expr.expr.expr.ref() === Promise)) ); @@ -261,7 +265,10 @@ export const evalToConstant = (expr: Expr): Constant | undefined => { return undefined; } } else { - name = isIdentifier(prop.name) ? prop.name.name : prop.name.value; + name = + isIdentifier(prop.name) || isPrivateIdentifier(prop.name) + ? prop.name.name + : prop.name.value; } const val = evalToConstant(prop.expr); if (val === undefined) { @@ -293,8 +300,8 @@ export const evalToConstant = (expr: Expr): Constant | undefined => { } } else if (isPropAccessExpr(expr)) { const obj = evalToConstant(expr.expr)?.constant as any; - if (obj && expr.name in obj) { - return { constant: obj[expr.name] }; + if (obj && isIdentifier(expr.name) && expr.name.name in obj) { + return { constant: obj[expr.name.name] }; } return undefined; } else if (isReferenceExpr(expr)) { diff --git a/src/validate.ts b/src/validate.ts index b83ddfba..8e2c08df 100644 --- a/src/validate.ts +++ b/src/validate.ts @@ -1,4 +1,3 @@ -import * as typescript from "typescript"; import { EventBusMapInterface, EventBusWhenInterface, @@ -27,7 +26,7 @@ import { anyOf, hasOnlyAncestors } from "./util"; * @returns diagnostic errors for the file. */ export function validate( - ts: typeof typescript, + ts: typeof import("typescript"), checker: FunctionlessChecker, node: ts.Node, logger?: { @@ -36,7 +35,7 @@ export function validate( ): ts.Diagnostic[] { logger?.info("Beginning validation of Functionless semantics"); - function visit(node: typescript.Node): typescript.Diagnostic[] { + function visit(node: ts.Node): ts.Diagnostic[] { if (checker.isNewStepFunction(node)) { return validateNewStepFunctionNode(node); } else if (checker.isApiIntegration(node)) { @@ -91,7 +90,7 @@ export function validate( ]); } - function validateNodes(nodes: typescript.Node[]) { + function validateNodes(nodes: ts.Node[]) { return nodes.flatMap((arg) => collectEachChild(arg, visit)); } @@ -108,9 +107,7 @@ export function validate( ...collectEachChildRecursive(func, validateStepFunctionNode), ]; - function validateStepFunctionNode( - node: typescript.Node - ): typescript.Diagnostic[] { + function validateStepFunctionNode(node: ts.Node): ts.Diagnostic[] { const type = checker.getTypeAtLocation(node); if ( typeMatch( @@ -146,8 +143,7 @@ export function validate( ), ]; } - } - if ( + } else if ( ((ts.isBinaryExpression(node) && isArithmeticToken(node.operatorToken.kind)) || ((ts.isPrefixUnaryExpression(node) || @@ -208,8 +204,9 @@ export function validate( ts.isNewExpression(node.expression) || ts.isCallExpression(node.expression) ) { - return ( - node.expression.arguments?.flatMap((arg) => { + return [ + ...validateStepFunctionError(node.expression.expression), + ...(node.expression.arguments?.flatMap((arg) => { if (!checker.isConstant(arg)) { return [ newError( @@ -219,8 +216,8 @@ export function validate( ]; } return []; - }) ?? [] - ); + }) ?? []), + ]; } } else if (ts.isPropertyAssignment(node)) { if ( @@ -255,6 +252,32 @@ export function validate( } } + function validateStepFunctionError(expr: ts.Expression): ts.Diagnostic[] { + const callExprType = checker.getTypeAtLocation(expr); + if (checker.typeToString(callExprType) === "ErrorConstructor") { + // throw new Error + return []; + } + const kind = callExprType.getProperty("kind"); + if (kind !== undefined) { + const kindType = checker.getTypeOfSymbolAtLocation(kind, expr); + if ( + kindType.isStringLiteral() && + kindType.value === "StepFunctionError" + ) { + // throw new StepFunctionError + return []; + } + } + + return [ + newError( + expr, + ErrorCodes.StepFunction_Throw_must_be_Error_or_StepFunctionError_class + ), + ]; + } + /** * Validates that calls which return promises are immediately awaited and that calls * which return arrays fo promises are wrapped in `Promise.all` @@ -450,13 +473,22 @@ export function validate( * const v = x ? await func() : await func2() */ if ( - rootStatement && - (isControlFlowStatement(rootStatement) || - findParent( - node.expression, - ts.isConditionalExpression, - rootStatement - )) + (rootStatement && + (ts.isEmptyStatement(rootStatement) || + ts.isIfStatement(rootStatement) || + ts.isDoStatement(rootStatement) || + ts.isWhileStatement(rootStatement) || + ts.isForStatement(rootStatement) || + ts.isForInStatement(rootStatement) || + ts.isForOfStatement(rootStatement) || + ts.isSwitchStatement(rootStatement) || + ts.isLabeledStatement(rootStatement) || + ts.isTryStatement(rootStatement))) || + findParent( + node.expression, + ts.isConditionalExpression, + rootStatement + ) ) { return [ newError( @@ -658,9 +690,7 @@ export function validate( ...collectEachChildRecursive(func, validateFunctionClosureNode), ]; - function validateFunctionClosureNode( - node: typescript.Node - ): typescript.Diagnostic[] { + function validateFunctionClosureNode(node: ts.Node): ts.Diagnostic[] { if ( checker.isStepFunction(node) || checker.isTable(node) || @@ -668,7 +698,7 @@ export function validate( checker.isEventBus(node) ) { if ( - typescript.isPropertyAccessExpression(node.parent) && + ts.isPropertyAccessExpression(node.parent) && node.parent.name.text === "resource" ) { return [ @@ -708,9 +738,7 @@ export function validate( } } - function validateEventBusWhen( - node: EventBusWhenInterface - ): typescript.Diagnostic[] { + function validateEventBusWhen(node: EventBusWhenInterface): ts.Diagnostic[] { const func = node.arguments.length === 3 ? node.arguments[2] : node.arguments[1]; @@ -722,7 +750,7 @@ export function validate( ]; } - function validateRule(node: RuleInterface): typescript.Diagnostic[] { + function validateRule(node: RuleInterface): ts.Diagnostic[] { const func = node.arguments[3]; return [ @@ -748,7 +776,7 @@ export function validate( function validateEventTransform( node: EventTransformInterface - ): typescript.Diagnostic[] { + ): ts.Diagnostic[] { const func = node.arguments[1]; return [ @@ -759,9 +787,7 @@ export function validate( ]; } - function validateEventBusMap( - node: EventBusMapInterface - ): typescript.Diagnostic[] { + function validateEventBusMap(node: EventBusMapInterface): ts.Diagnostic[] { const func = node.arguments[0]; return [ @@ -805,21 +831,5 @@ export function validate( } } -/** - * Used by AppSync to determine of a root statement can be statically analyzed. - */ -const isControlFlowStatement = anyOf( - typescript.isEmptyStatement, - typescript.isIfStatement, - typescript.isDoStatement, - typescript.isWhileStatement, - typescript.isForStatement, - typescript.isForInStatement, - typescript.isForOfStatement, - typescript.isSwitchStatement, - typescript.isLabeledStatement, - typescript.isTryStatement -); - // to prevent the closure serializer from trying to import all of functionless. export const deploymentOnlyModule = true; diff --git a/src/visit.ts b/src/visit.ts index 95a02405..4c9d9e88 100644 --- a/src/visit.ts +++ b/src/visit.ts @@ -2,22 +2,31 @@ import { assertNever } from "./assert"; import { ArrayBinding, BindingElem, + ClassDecl, + ClassStaticBlockDecl, + MethodDecl, + PropDecl, FunctionDecl, ObjectBinding, ParameterDecl, + ConstructorDecl, VariableDecl, VariableDeclList, } from "./declaration"; import { Err } from "./error"; +import { ErrorCodes, SynthError } from "./error-code"; import { Argument, ArrayLiteralExpr, + ArrowFunctionExpr, AwaitExpr, BinaryExpr, BooleanLiteralExpr, CallExpr, + ClassExpr, ComputedPropertyNameExpr, ConditionExpr, + DeleteExpr, ElementAccessExpr, Expr, FunctionExpr, @@ -25,8 +34,12 @@ import { NewExpr, NullLiteralExpr, NumberLiteralExpr, - ObjectElementExpr, ObjectLiteralExpr, + ParenthesizedExpr, + PostfixUnaryExpr, + PrivateIdentifier, + PromiseArrayExpr, + PromiseExpr, PropAccessExpr, PropAssignExpr, ReferenceExpr, @@ -37,13 +50,13 @@ import { TypeOfExpr, UnaryExpr, UndefinedLiteralExpr, - PromiseArrayExpr, - PromiseExpr, - PostfixUnaryExpr, + VoidExpr, + YieldExpr, } from "./expression"; import { isArgument, isArrayLiteralExpr, + isArrowFunctionExpr, isAwaitExpr, isBinaryExpr, isBindingElem, @@ -52,11 +65,21 @@ import { isBooleanLiteralExpr, isBreakStmt, isCallExpr, + isCaseClause, isCatchClause, + isClassDecl, + isClassExpr, + isClassMember, + isClassStaticBlockDecl, isComputedPropertyNameExpr, isConditionExpr, + isConstructorDecl, isContinueStmt, + isDebuggerStmt, + isDefaultClause, isDoStmt, + isElementAccessExpr, + isEmptyStmt, isErr, isExpr, isExprStmt, @@ -66,6 +89,8 @@ import { isFunctionExpr, isIdentifier, isIfStmt, + isLabelledStmt, + isMethodDecl, isNewExpr, isNullLiteralExpr, isNumberLiteralExpr, @@ -73,37 +98,53 @@ import { isObjectElementExpr, isObjectLiteralExpr, isParameterDecl, + isPostfixUnaryExpr, isPromiseArrayExpr, isPromiseExpr, isPropAccessExpr, isPropAssignExpr, + isPropDecl, + isPropName, isReferenceExpr, isReturnStmt, isSpreadAssignExpr, isSpreadElementExpr, isStmt, isStringLiteralExpr, + isSuperKeyword, + isSwitchClause, + isSwitchStmt, isTemplateExpr, + isThisExpr, isThrowStmt, isTryStmt, isTypeOfExpr, isUnaryExpr, - isPostfixUnaryExpr, isUndefinedLiteralExpr, isVariableStmt, isWhileStmt, - isElementAccessExpr, + isWithStmt, isForStmt, isVariableDeclList, isVariableDecl, + isPrivateIdentifier, + isYieldExpr, + isBigIntExpr, + isRegexExpr, + isVoidExpr, + isDeleteExpr, + isParenthesizedExpr, + isImportKeyword, } from "./guards"; import { FunctionlessNode } from "./node"; import { BlockStmt, BreakStmt, + CaseClause, CatchClause, ContinueStmt, + DefaultClause, DoStmt, ExprStmt, FinallyBlock, @@ -111,12 +152,15 @@ import { ForOfStmt, ForStmt, IfStmt, + LabelledStmt, ReturnStmt, Stmt, + SwitchStmt, ThrowStmt, TryStmt, VariableStmt, WhileStmt, + WithStmt, } from "./statement"; import { anyOf, @@ -145,23 +189,16 @@ export function visitEachChild( } const expr = visitor(node.expr); ensure(expr, isExpr, "an Argument's expr must be an Expr"); - return new Argument(expr, node.name) as T; + return new Argument(expr) as T; } else if (isArrayLiteralExpr(node)) { return new ArrayLiteralExpr( - node.items.reduce((items: Expr[], item) => { - let result = visitor(item); - if (Array.isArray(result)) { - result = flatten(result); - ensureItemOf( - result, - isExpr, - "Items of an ArrayLiteralExpr must be Expr nodes" - ); - return items.concat(result as Expr[]); - } else { - return [...items, result] as any; - } - }, []) + node.items.flatMap((item) => + ensureSingleOrArray( + visitor(item), + isExpr, + "Items of an ArrayLiteralExpr must be Expr nodes" + ) + ) ) as T; } else if (isBinaryExpr(node)) { const left = visitor(node.left); @@ -169,31 +206,23 @@ export function visitEachChild( if (isExpr(left) && isExpr(right)) { return new BinaryExpr(left, node.op, right) as T; } else { - throw new Error( + throw new SynthError( + ErrorCodes.Unexpected_Error, "visitEachChild of BinaryExpr must return an Expr for both the left and right operands" ); } } else if (isBlockStmt(node)) { return new BlockStmt( - node.statements.reduce((stmts: Stmt[], stmt) => { - let result = visitor(stmt); - if (Array.isArray(result)) { - result = flatten(result); - ensureItemOf( - result, - isStmt, - "Statements in BlockStmt must be Stmt nodes" - ); - return stmts.concat(result); - } else if (isStmt(result)) { - return [...stmts, result]; - } else { - throw new Error( - "visitEachChild of a BlockStmt's child statements must return a Stmt" - ); - } - }, []) + node.statements.flatMap((stmt) => + ensureSingleOrArray( + visitor(stmt), + isStmt, + "Statements in BlockStmt must be Stmt nodes" + ) + ) ) as T; + } else if (isBigIntExpr(node)) { + return node.clone() as T; } else if (isBooleanLiteralExpr(node)) { return new BooleanLiteralExpr(node.value) as T; } else if (isBreakStmt(node)) { @@ -208,16 +237,13 @@ export function visitEachChild( `visitEachChild of a ${node.kind}'s expr must return a single Expr` ); const args = node.args.flatMap((arg) => { - if (!arg.expr) { - return arg.clone(); - } - const expr = visitor(arg.expr); + const expr = visitor(arg.expr!); ensure( expr, isExpr, `visitEachChild of a ${node.kind}'s argument must return a single Expr` ); - return new Argument(expr, arg.name); + return new Argument(expr); }); return ( isCallExpr(node) ? new CallExpr(expr, args) : new NewExpr(expr, args) @@ -236,6 +262,43 @@ export function visitEachChild( const block = visitBlockStmt(node.block, visitor); return new CatchClause(variableDecl, block) as T; + } else if (isClassDecl(node) || isClassExpr(node)) { + let heritage; + + if (node.heritage) { + heritage = visitor(node.heritage); + if (heritage) { + ensure( + heritage, + isExpr, + `A ${node.kind}'s Heritage Clause must be an Expr` + ); + } + } + + const classMembers = node.members.flatMap((classMember) => { + let updatedMember = visitor(classMember); + + return ensureSingleOrArray( + updatedMember, + isClassMember, + "A ClassDecl's ClassMembers must be ClassMember declarations" + ); + }); + + if (isClassDecl(node)) { + return new ClassDecl(node.name, heritage, classMembers) as T; + } else { + return new ClassExpr(node.name, heritage, classMembers) as T; + } + } else if (isClassStaticBlockDecl(node)) { + const block = visitor(node); + ensure( + block, + isBlockStmt, + "A ClassStaticBlockDecl's block must be a BlockStmt" + ); + return new ClassStaticBlockDecl(block) as T; } else if (isComputedPropertyNameExpr(node)) { const expr = visitor(node.expr); ensure( @@ -254,6 +317,8 @@ export function visitEachChild( ensure(_else, isExpr, "ConditionExpr's else must be an Expr"); return new ConditionExpr(when, then, _else) as T; + } else if (isDebuggerStmt(node)) { + return node.clone() as T; } else if (isDoStmt(node)) { const block = visitBlockStmt(node.block, visitor); const condition = visitor(node.condition); @@ -275,28 +340,10 @@ export function visitEachChild( const expr = visitor(node.expr); ensure(expr, isExpr, "The Expr in an ExprStmt must be an Expr"); return new ExprStmt(expr) as T; - } else if (isForInStmt(node) || isForOfStmt(node)) { - const variableDecl = visitor(node.variableDecl); - ensure( - variableDecl, - anyOf(isVariableDecl, isIdentifier), - `Initializer in ${node.kind} must be a VariableDecl or Identifier` - ); - - const expr = visitor(node.expr); - ensure(expr, isExpr, `Expr in ${node.kind} must be an Expr`); - - const body = visitBlockStmt(node.body, visitor); - - return ( - isForInStmt(node) - ? new ForInStmt(variableDecl, expr, body) - : new ForOfStmt(variableDecl, expr, body) - ) as T; } else if (isForStmt(node)) { const body = visitBlockStmt(node.body, visitor); - const variableDecl = node.variableDecl - ? visitor(node.variableDecl) + const variableDecl = node.initializer + ? visitor(node.initializer) : undefined; variableDecl && ensure( @@ -319,39 +366,56 @@ export function visitEachChild( condition as Expr, incrementor as Expr ) as T; - } else if (isFunctionDecl(node) || isFunctionExpr(node)) { - const parameters = node.parameters.reduce( - (params: ParameterDecl[], parameter) => { - let p = visitor(parameter); - if (Array.isArray(p)) { - p = flatten(p); - ensureItemOf( - p, - isParameterDecl, - `a ${node.kind}'s parameters must be ParameterDecl nodes` - ); - return params.concat(p); - } else { - ensure( - p, - isParameterDecl, - `a ${node.kind}'s parameters must be ParameterDecl nodes` - ); - return [...params, p]; - } - }, - [] + } else if (isForInStmt(node) || isForOfStmt(node)) { + const variableDecl = visitor(node.initializer); + ensure( + variableDecl, + anyOf(isVariableDecl, isIdentifier), + `Initializer in ${node.kind} must be a VariableDecl or Identifier` + ); + + const expr = visitor(node.expr); + ensure(expr, isExpr, `Expr in ${node.kind} must be an Expr`); + + const body = visitBlockStmt(node.body, visitor); + + return ( + isForInStmt(node) + ? new ForInStmt(variableDecl, expr, body) + : new ForOfStmt(variableDecl, expr, body) + ) as T; + } else if ( + isFunctionDecl(node) || + isArrowFunctionExpr(node) || + isFunctionExpr(node) || + isMethodDecl(node) || + isConstructorDecl(node) + ) { + const parameters = node.parameters.flatMap((parameter) => + ensureSingleOrArray( + visitor(parameter), + isParameterDecl, + `a ${node.kind}'s parameters must be ParameterDecl nodes` + ) ); const body = visitBlockStmt(node.body, visitor); return ( isFunctionDecl(node) - ? new FunctionDecl(parameters, body) - : new FunctionExpr(parameters, body) + ? new FunctionDecl(node.name, parameters, body) + : isArrowFunctionExpr(node) + ? new ArrowFunctionExpr(parameters, body) + : isFunctionExpr(node) + ? new FunctionExpr(node.name, parameters, body) + : isMethodDecl(node) + ? new MethodDecl(node.name, parameters, body) + : new ConstructorDecl(parameters, body) ) as T; } else if (isIdentifier(node)) { return new Identifier(node.name) as T; + } else if (isPrivateIdentifier(node)) { + return new PrivateIdentifier(node.name) as T; } else if (isIfStmt(node)) { const when = visitor(node.when); const then = visitor(node.then); @@ -370,25 +434,13 @@ export function visitEachChild( return new NumberLiteralExpr(node.value) as T; } else if (isObjectLiteralExpr(node)) { return new ObjectLiteralExpr( - node.properties.reduce((props: ObjectElementExpr[], prop) => { - let p = visitor(prop); - if (Array.isArray(p)) { - p = flatten(p); - ensureItemOf( - p, - isObjectElementExpr, - "an ObjectLiteralExpr's properties must be ObjectElementExpr nodes" - ); - return props.concat(p); - } else { - ensure( - p, - isObjectElementExpr, - "an ObjectLiteralExpr's properties must be ObjectElementExpr nodes" - ); - return [...props, p]; - } - }, []) + node.properties.flatMap((prop) => + ensureSingleOrArray( + visitor(prop), + isObjectElementExpr, + "an ObjectLiteralExpr's properties must be ObjectElementExpr nodes" + ) + ) ) as T; } else if (isParameterDecl(node)) { return new ParameterDecl(node.name) as T; @@ -414,6 +466,21 @@ export function visitEachChild( "a PropAssignExpr's expr property must be an Expr node type" ); return new PropAssignExpr(name, expr) as T; + } else if (isPropDecl(node)) { + const name = visitor(node.name); + const initializer = node.initializer + ? visitor(node.initializer) + : undefined; + ensure( + name, + isPropName, + "a PropDecl's name must be an Identifier, StringLiteralExpr or ComputedPropNameExpr" + ); + if (initializer) { + ensure(initializer, isExpr, "A PropDecl's initializer must be an Expr"); + } + + return new PropDecl(name, initializer) as T; } else if (isReferenceExpr(node)) { return new ReferenceExpr(node.name, node.ref) as T; } else if (isReturnStmt(node)) { @@ -434,30 +501,20 @@ export function visitEachChild( return new SpreadElementExpr(expr) as T; } else if (isStringLiteralExpr(node)) { return new StringLiteralExpr(node.value) as T; + } else if (isSuperKeyword(node)) { + return node.clone() as T; } else if (isTemplateExpr(node)) { return new TemplateExpr( - node.exprs.reduce((exprs: Expr[], expr) => { - let e = visitor(expr); - if (e === undefined) { - return exprs; - } else if (Array.isArray(e)) { - e = flatten(e); - ensureItemOf( - e, - isExpr, - "a TemplateExpr's expr property must only contain Expr node types" - ); - return exprs.concat(e); - } else { - ensure( - e, - isExpr, - "a TemplateExpr's expr property must only contain Expr node types" - ); - return [...exprs, e]; - } - }, []) + node.exprs.flatMap((expr) => + ensureSingleOrArray( + visitor(expr), + isExpr, + "a TemplateExpr's expr property must only contain Expr node types" + ) + ) ) as T; + } else if (isThisExpr(node)) { + return node.clone() as T; } else if (isThrowStmt(node)) { const expr = visitor(node.expr); ensure(expr, isExpr, "a ThrowStmt's expr must be an Expr node type"); @@ -499,12 +556,6 @@ export function visitEachChild( return new PostfixUnaryExpr(node.op, expr) as T; } else if (isUndefinedLiteralExpr(node)) { return new UndefinedLiteralExpr() as T; - } else if (isVariableDecl(node)) { - const expr = node.initializer ? visitor(node.initializer) : undefined; - if (expr) { - ensure(expr, isExpr, "a VariableDecl's expr property must be an Expr"); - } - return new VariableDecl(node.name, expr) as T; } else if (isVariableStmt(node)) { const declList = visitor(node.declList); ensure( @@ -513,6 +564,20 @@ export function visitEachChild( "a VariableStmt's declList property must be an VariableDeclList" ); return new VariableStmt(declList) as T; + } else if (isVariableDeclList(node)) { + const variables = node.decls.map(visitor); + ensureItemOf( + variables, + isVariableDecl, + "Variables in a VariableDeclList must be of type VariableDecl" + ); + return new VariableDeclList(variables) as T; + } else if (isVariableDecl(node)) { + const expr = node.initializer ? visitor(node.initializer) : undefined; + if (expr) { + ensure(expr, isExpr, "a VariableDecl's expr property must be an Expr"); + } + return new VariableDecl(node.name, expr) as T; } else if (isWhileStmt(node)) { const condition = visitor(node.condition); ensure(condition, isExpr, "a WhileStmt's condition must be an Expr"); @@ -576,14 +641,92 @@ export function visitEachChild( } else { return new ArrayBinding(bindings as ArrayBinding["bindings"]) as T; } - } else if (isVariableDeclList(node)) { - const variables = node.decls.map(visitor); - ensureItemOf( - variables, - isVariableDecl, - "Variables in a VariableDeclList must be of type VariableDecl" + } else if (isLabelledStmt(node)) { + const stmt = visitor(node.stmt); + ensure(stmt, isStmt, "LabelledStmt's stmt must be a Stmt"); + return new LabelledStmt(node.label, stmt) as T; + } else if (isSwitchStmt(node)) { + const clauses = node.clauses.flatMap((clause) => + ensureSingleOrArray( + visitor(clause), + isSwitchClause, + "must be a CaseClause or DefaultClause" + ) ); - return new VariableDeclList(variables) as T; + + const defaultClauses = clauses.filter(isDefaultClause); + if (defaultClauses.length === 1) { + if (!isDefaultClause(clauses[clauses.length - 1])) { + throw new SynthError( + ErrorCodes.Unexpected_Error, + `only the last SwitchClause can be a DefaultClause` + ); + } + } else if (defaultClauses.length > 1) { + throw new SynthError( + ErrorCodes.Unexpected_Error, + `there must be 0 or 1 DefaultClauses in a single SwitchStmt, but found ${defaultClauses.length}` + ); + } + + return new SwitchStmt(clauses) as T; + } else if (isCaseClause(node)) { + const expr = visitor(node.expr); + ensure(expr, isExpr, `the CaseClause's expr must be an Expr`); + const stmts = node.statements.flatMap((stmt) => + ensureSingleOrArray( + visitor(stmt), + isStmt, + `expected all items in a CaseClause's statements to be Stmt nodes` + ) + ); + + return new CaseClause(expr, stmts) as T; + } else if (isDefaultClause(node)) { + const stmts = node.statements.flatMap((stmt) => + ensureSingleOrArray( + visitor(stmt), + isStmt, + `expected all items in a DefaultClause's statements to be Stmt nodes` + ) + ); + + return new DefaultClause(stmts) as T; + } else if (isEmptyStmt(node)) { + return node.clone() as T; + } else if (isWithStmt(node)) { + const expr = visitor(node.expr); + const stmt = visitor(node.stmt); + ensure(expr, isExpr, "WithStmt's expr must be an Expr"); + ensure(stmt, isStmt, "WithStmt's stmt must be a Stmt"); + return new WithStmt(expr, stmt) as T; + } else if (isRegexExpr(node)) { + return node.clone() as T; + } else if (isDeleteExpr(node)) { + const expr = visitor(node.expr); + ensure( + expr, + anyOf(isPropAccessExpr, isElementAccessExpr), + "DeleteExpr's expr must be PropAccessExpr or ElementAccessExpr" + ); + return new DeleteExpr(expr) as T; + } else if (isVoidExpr(node)) { + const expr = visitor(node.expr); + ensure(expr, isExpr, "VoidExpr's expr must be an Expr"); + return new VoidExpr(expr) as T; + } else if (isYieldExpr(node)) { + let expr; + if (node.expr) { + expr = visitor(node.expr); + ensure(expr, isExpr, "YieldExpr's expr must be an Expr"); + } + return new YieldExpr(expr, node.delegate) as T; + } else if (isParenthesizedExpr(node)) { + const expr = visitor(node.expr); + ensure(expr, isExpr, "ParenthesizedExpr's expr must be an Expr"); + return new ParenthesizedExpr(expr) as T; + } else if (isImportKeyword(node)) { + return node.clone() as T; } return assertNever(node); } @@ -620,7 +763,7 @@ export function visitBlock( const id = new Identifier(nameGenerator.generateOrGet(expr)); nestedTasks.push( new VariableStmt( - new VariableDeclList([new VariableDecl(id.name, expr)]) + new VariableDeclList([new VariableDecl(id.clone(), expr)]) ) ); return id; @@ -634,6 +777,31 @@ export function visitBlock( }); } +/** + * Ensures that the {@link val} is either: + * 1. a "single" instance of {@link T} + * 2. an "array" of {@link T} + * + * @param val value to check + * @param assertion assertion function to apply to a single instance + * @param message error message to throw if the assertion is false + * @returns an array of {@link T} for folding back into a visitEachChild result + */ +function ensureSingleOrArray( + val: any, + assertion: (a: any) => a is T, + message: string +): T[] { + if (Array.isArray(val)) { + val = val.flat(); + ensureItemOf(val, assertion, message); + return val; + } else { + ensure(val, assertion, message); + return [val]; + } +} + /** * Starting at the root, explore the children without processing until one or more start nodes are found. * diff --git a/src/vtl.ts b/src/vtl.ts index fc9a3537..1e37eb70 100644 --- a/src/vtl.ts +++ b/src/vtl.ts @@ -1,6 +1,5 @@ import { assertNever, assertNodeKind } from "./assert"; import { BindingPattern, VariableDecl } from "./declaration"; -import {} from "./error"; import { ErrorCodes, SynthError } from "./error-code"; import { CallExpr, @@ -8,57 +7,82 @@ import { FunctionExpr, Identifier, ReferenceExpr, + ThisExpr, } from "./expression"; import { isArgument, isArrayBinding, isArrayLiteralExpr, + isArrowFunctionExpr, isAwaitExpr, + isBigIntExpr, isBinaryExpr, isBindingPattern, isBlockStmt, isBooleanLiteralExpr, isBreakStmt, isCallExpr, + isCaseClause, isCatchClause, + isClassDecl, + isClassExpr, + isClassStaticBlockDecl, isComputedPropertyNameExpr, isConditionExpr, + isConstructorDecl, isContinueStmt, + isDebuggerStmt, + isDefaultClause, + isDeleteExpr, isDoStmt, isElementAccessExpr, + isEmptyStmt, isExprStmt, isForInStmt, isForOfStmt, + isForStmt, isFunctionDecl, isFunctionExpr, isIdentifier, isIfStmt, + isImportKeyword, + isLabelledStmt, + isMethodDecl, isNewExpr, isNullLiteralExpr, isNumberLiteralExpr, isObjectLiteralExpr, isParameterDecl, + isParenthesizedExpr, + isPostfixUnaryExpr, + isPrivateIdentifier, isPromiseArrayExpr, isPromiseExpr, isPropAccessExpr, isPropAssignExpr, + isPropDecl, isReferenceExpr, + isRegexExpr, isReturnStmt, isSpreadAssignExpr, isSpreadElementExpr, isStmt, isStringLiteralExpr, + isSuperKeyword, + isSwitchStmt, isTemplateExpr, + isThisExpr, isThrowStmt, isTryStmt, isTypeOfExpr, isUnaryExpr, - isPostfixUnaryExpr, isUndefinedLiteralExpr, + isVariableDecl, isVariableStmt, + isVoidExpr, isWhileStmt, - isForStmt, - isVariableDecl, + isWithStmt, + isYieldExpr, } from "./guards"; import { Integration, IntegrationImpl, isIntegration } from "./integration"; import { Stmt } from "./statement"; @@ -230,7 +254,9 @@ export abstract class VTL { // deconstruct from the temp variable this.evaluateBindingPattern(iterVar.name, tempVar); } else { - this.add(`#foreach($${iterVar.name} in ${this.printExpr(iterValue)})`); + this.add( + `#foreach($${iterVar.name.name} in ${this.printExpr(iterValue)})` + ); } } else { this.add( @@ -269,7 +295,9 @@ export abstract class VTL { call: CallExpr ): string; - protected abstract dereference(id: Identifier | ReferenceExpr): string; + protected abstract dereference( + id: Identifier | ReferenceExpr | ThisExpr + ): string; /** * Evaluate an {@link Expr} or {@link Stmt} by emitting statements to this VTL template and @@ -284,23 +312,11 @@ export abstract class VTL { if (!node) { return "$null"; } - if (isArrayLiteralExpr(node)) { - if (node.items.find(isSpreadElementExpr) === undefined) { - return `[${node.items.map((item) => this.eval(item)).join(", ")}]`; - } else { - // contains a spread, e.g. [...i], so we will store in a variable - const list = this.var("[]"); - for (const item of node.items) { - if (isSpreadElementExpr(item)) { - this.qr(`${list}.addAll(${this.eval(item.expr)})`); - } else { - // we use addAll because `list.push(item)` is pared as `list.push(...[item])` - // - i.e. the compiler passes us an ArrayLiteralExpr even if there is one arg - this.qr(`${list}.add(${this.eval(item)})`); - } - } - return list; - } + if (isParenthesizedExpr(node)) { + // TODO: do we need to do anything to ensure precedence of parenthesis are maintained? + return this.eval(node.expr); + } else if (isArrayLiteralExpr(node)) { + return this.addAll(node.items); } else if (isBinaryExpr(node)) { if (node.op === "in") { throw new SynthError( @@ -345,8 +361,14 @@ export abstract class VTL { } else if (isBreakStmt(node)) { return this.add("#break"); } else if (isCallExpr(node)) { - if (isReferenceExpr(node.expr)) { - const ref = node.expr.ref(); + const expr = isParenthesizedExpr(node.expr) + ? node.expr.unwrap() + : node.expr; + + if (isSuperKeyword(expr) || isImportKeyword(expr)) { + throw new Error(`super and import are not supported by VTL`); + } else if (isReferenceExpr(expr)) { + const ref = expr.ref(); if (isIntegration(ref)) { const serviceCall = new IntegrationImpl(ref); return this.integrate(serviceCall, node); @@ -358,24 +380,26 @@ export abstract class VTL { } } else if ( // If the parent is a propAccessExpr - isPropAccessExpr(node.expr) && - (node.expr.name === "map" || - node.expr.name === "forEach" || - node.expr.name === "reduce") + isPropAccessExpr(expr) && + isIdentifier(expr.name) && + (expr.name.name === "map" || + expr.name.name === "forEach" || + expr.name.name === "reduce" || + expr.name.name === "push") ) { - if (node.expr.name === "map" || node.expr.name == "forEach") { + if (expr.name.name === "map" || expr.name.name == "forEach") { // list.map(item => ..) // list.map((item, idx) => ..) // list.forEach(item => ..) // list.forEach((item, idx) => ..) - const newList = node.expr.name === "map" ? this.var("[]") : undefined; + const newList = expr.name.name === "map" ? this.var("[]") : undefined; const [value, index, array] = getMapForEachArgs(node); // Try to flatten any maps before this operation // returns the first variable to be used in the foreach of this operation (may be the `value`) const list = this.flattenListMapOperations( - node.expr.expr, + expr.expr, value, (firstVariable, list) => { this.add(`#foreach(${firstVariable} in ${list})`); @@ -395,42 +419,46 @@ export abstract class VTL { ); // Add the final value to the array - if (node.expr.name === "map") { + if (expr.name.name === "map") { this.qr(`${newList}.add(${tmp})`); } this.add("#end"); return newList ?? "$null"; - } else if (node.expr.name === "reduce") { + } else if (expr.name.name === "reduce") { // list.reduce((result: string[], next) => [...result, next], []); // list.reduce((result, next) => [...result, next]); const fn = assertNodeKind( - node.getArgument("callbackfn")?.expr, + node.args[0]?.expr, "FunctionExpr" ); - const initialValue = node.getArgument("initialValue")?.expr; + const initialValue = node.args[1]; // (previousValue: string[], currentValue: string, currentIndex: number, array: string[]) - const previousValue = fn.parameters[0]?.name - ? `$${fn.parameters[0].name}` - : this.newLocalVarName(); - const currentValue = fn.parameters[1]?.name - ? `$${fn.parameters[1].name}` - : this.newLocalVarName(); - const currentIndex = fn.parameters[2]?.name - ? `$${fn.parameters[2].name}` - : undefined; - const array = fn.parameters[3]?.name - ? `$${fn.parameters[3].name}` - : undefined; + + const [ + previousValue = this.newLocalVarName(), + currentValue = this.newLocalVarName(), + currentIndex, + array, + ] = fn.parameters.map((param) => { + if (isIdentifier(param.name)) { + return `$${param.name.name}`; + } else { + throw new SynthError( + ErrorCodes.Unsupported_Feature, + "Binding variable assignment is not currently supported in VTL. https://github.com/functionless/functionless/issues/302" + ); + } + }); // create a new local variable name to hold the initial/previous value // this is because previousValue may not be unique and isn't contained within the loop const previousTmp = this.newLocalVarName(); const list = this.flattenListMapOperations( - node.expr.expr, + expr.expr, currentValue, (firstVariable, list) => { if (initialValue !== undefined) { @@ -481,18 +509,33 @@ export abstract class VTL { } return previousTmp; - } else if ( - isIdentifier(node.expr.expr) && - node.expr.expr.name === "Promise" - ) { + } else if (isIdentifier(expr.expr) && expr.expr.name === "Promise") { throw new SynthError( ErrorCodes.Unsupported_Use_of_Promises, "Appsync does not support concurrent integration invocation or methods on the `Promise` api." ); + } else if (expr.name.name === "push") { + if ( + node.args.length === 1 && + !isSpreadElementExpr(node.args[0].expr) + ) { + // use the .add for the case when we are pushing exactly one argument + return `${this.eval(expr.expr)}.add(${this.eval( + node.args[0].expr + )})`; + } else { + // for all other cases, use .addAll + // such as `.push(a, b, ...c)` or `.push(...a)` + return `${this.eval(expr.expr)}.addAll(${this.addAll( + node.args + .map((arg) => arg.expr) + .filter((e): e is Expr => e !== undefined) + )})`; + } } // this is an array map, forEach, reduce call } - return `${this.eval(node.expr)}(${Object.values(node.args) + return `${this.eval(expr)}(${Object.values(node.args) .map((arg) => this.eval(arg)) .join(", ")})`; } else if (isConditionExpr(node)) { @@ -513,32 +556,26 @@ export abstract class VTL { return this.qr(this.eval(node.expr)); } else if (isForInStmt(node) || isForOfStmt(node)) { this.foreach( - node.variableDecl, + node.initializer, `${this.eval(node.expr)}${isForInStmt(node) ? ".keySet()" : ""}`, node.body ); return undefined; } else if (isFunctionDecl(node)) { // there should never be nested functions - } else if (isFunctionExpr(node)) { + } else if (isFunctionExpr(node) || isArrowFunctionExpr(node)) { return this.eval(node.body); } else if (isIdentifier(node)) { return this.dereference(node); } else if (isNewExpr(node)) { throw new Error("NewExpr is not supported by Velocity Templates"); } else if (isPropAccessExpr(node)) { - let name = node.name; - if (name === "push" && isCallExpr(node.parent)) { - // this is a push to an array, rename to 'addAll' - // addAll because the var-args are converted to an ArrayLiteralExpr - name = "addAll"; - } - return `${this.eval(node.expr)}.${name}`; + return `${this.eval(node.expr)}.${node.name.name}`; } else if (isElementAccessExpr(node)) { return `${this.eval(node.expr)}[${this.eval(node.element)}]`; } else if (isNullLiteralExpr(node) || isUndefinedLiteralExpr(node)) { return "$null"; - } else if (isNumberLiteralExpr(node)) { + } else if (isNumberLiteralExpr(node) || isBigIntExpr(node)) { return node.value.toString(10); } else if (isObjectLiteralExpr(node)) { const obj = this.var("{}"); @@ -557,7 +594,7 @@ export abstract class VTL { return obj; } else if (isComputedPropertyNameExpr(node)) { return this.eval(node.expr); - } else if (isReferenceExpr(node)) { + } else if (isReferenceExpr(node) || isThisExpr(node)) { return this.dereference(node); } else if (isParameterDecl(node) || isPropAssignExpr(node)) { throw new Error(`cannot evaluate Expr kind: '${node.kind}'`); @@ -628,7 +665,7 @@ export abstract class VTL { // may generate may variables, return nothing. return undefined; } else { - const varName = `${variablePrefix}${decl.name}`; + const varName = `${variablePrefix}${decl.name.name}`; if (decl.initializer) { return this.set(varName, decl.initializer); @@ -667,12 +704,55 @@ export abstract class VTL { ErrorCodes.Unsupported_Feature, "Condition based for loops (for(;;)) are not currently supported. For in and for of loops may be supported based on the use case. https://github.com/functionless/functionless/issues/303" ); + } else if ( + isCaseClause(node) || + isClassDecl(node) || + isClassExpr(node) || + isClassStaticBlockDecl(node) || + isConstructorDecl(node) || + isDebuggerStmt(node) || + isDefaultClause(node) || + isDeleteExpr(node) || + isEmptyStmt(node) || + isLabelledStmt(node) || + isMethodDecl(node) || + isPrivateIdentifier(node) || + isPropDecl(node) || + isRegexExpr(node) || + isSuperKeyword(node) || + isSwitchStmt(node) || + isVoidExpr(node) || + isWithStmt(node) || + isYieldExpr(node) + ) { + throw new SynthError( + ErrorCodes.Unexpected_Error, + `${node.kind} is not yet supported in VTL` + ); } else { return assertNever(node); } throw new Error(`cannot evaluate Expr kind: '${node.kind}'`); } + public addAll(items: Expr[]) { + if (items.find(isSpreadElementExpr) === undefined) { + return `[${items.map((item) => this.eval(item)).join(", ")}]`; + } + // contains a spread, e.g. [...i], so we will store in a variable + const list = this.var("[]"); + for (const item of items) { + if (isSpreadElementExpr(item)) { + this.qr(`${list}.addAll(${this.eval(item.expr)})`); + } else { + // we use addAll because `list.push(item)` is pared as `list.push(...[item])` + // - i.e. the compiler passes us an ArrayLiteralExpr even if there is one arg + this.qr(`${list}.add(${this.eval(item)})`); + } + } + return list; + } + /** * Expands a destructure/binding declaration to separate variable declarations in velocity * @@ -876,10 +956,7 @@ export abstract class VTL { this.add(`#set(${array} = ${list})`); } - const fn = assertNodeKind( - call.getArgument("callbackfn")?.expr, - "FunctionExpr" - ); + const fn = assertNodeKind(call.args[0]?.expr, "FunctionExpr"); const tmp = returnVariable ? returnVariable : this.newLocalVarName(); @@ -908,7 +985,8 @@ export abstract class VTL { !alwaysEvaluate && isCallExpr(expr) && isPropAccessExpr(expr.expr) && - expr.expr.name === "map" + isIdentifier(expr.expr.name) && + expr.expr.name.name === "map" ) { const [value, index, array] = getMapForEachArgs(expr); @@ -941,11 +1019,17 @@ export abstract class VTL { * Returns the [value, index, array] arguments if this CallExpr is a `forEach` or `map` call. */ const getMapForEachArgs = (call: CallExpr) => { - const fn = assertNodeKind( - call.getArgument("callbackfn")?.expr, - "FunctionExpr" - ); - return fn.parameters.map((p) => (p.name ? `$${p.name}` : p.name)); + const fn = assertNodeKind(call.args[0].expr, "FunctionExpr"); + return fn.parameters.map((p) => { + if (isIdentifier(p.name)) { + return `$${p.name.name}`; + } else { + throw new SynthError( + ErrorCodes.Unsupported_Feature, + "Destructured parameter declarations are not yet supported by VTL. https://github.com/functionless/functionless/issues/364" + ); + } + }); }; // to prevent the closure serializer from trying to import all of functionless. diff --git a/test/__snapshots__/step-function.localstack.test.ts.snap b/test/__snapshots__/step-function.localstack.test.ts.snap index b4bb03d5..004008ae 100644 --- a/test/__snapshots__/step-function.localstack.test.ts.snap +++ b/test/__snapshots__/step-function.localstack.test.ts.snap @@ -4806,14 +4806,14 @@ exports[`call $SFN parallel 1`] = ` Object { "StartAt": "Initialize Functionless Context", "States": Object { - "1__return $SFN.parallel([function(), function()])": Object { + "1__return $SFN.parallel(function(), function())": Object { "End": true, "InputPath": "$.heap0", "ResultPath": "$", "Type": "Pass", }, "Initialize Functionless Context": Object { - "Next": "return $SFN.parallel([function(), function()])", + "Next": "return $SFN.parallel(function(), function())", "Parameters": Object { "fnl_context": Object { "null": null, @@ -4822,7 +4822,7 @@ Object { "ResultPath": "$", "Type": "Pass", }, - "return $SFN.parallel([function(), function()])": Object { + "return $SFN.parallel(function(), function())": Object { "Branches": Array [ Object { "StartAt": "return 1", @@ -4847,7 +4847,7 @@ Object { }, }, ], - "Next": "1__return $SFN.parallel([function(), function()])", + "Next": "1__return $SFN.parallel(function(), function())", "ResultPath": "$.heap0", "Type": "Parallel", }, @@ -10018,7 +10018,9 @@ Object { }, "try 2": Object { "Next": "catch__try 2", - "Result": Object {}, + "Result": Object { + "message": null, + }, "ResultPath": null, "Type": "Pass", }, diff --git a/test/__snapshots__/step-function.test.ts.snap b/test/__snapshots__/step-function.test.ts.snap index 50ac73c3..086c627e 100644 --- a/test/__snapshots__/step-function.test.ts.snap +++ b/test/__snapshots__/step-function.test.ts.snap @@ -8553,14 +8553,14 @@ exports[`return $SFN.parallel(() => "hello", () => "world")) 1`] = ` Object { "StartAt": "Initialize Functionless Context", "States": Object { - "1__return $SFN.parallel([function(), function()])": Object { + "1__return $SFN.parallel(function(), function())": Object { "End": true, "InputPath": "$.heap0", "ResultPath": "$", "Type": "Pass", }, "Initialize Functionless Context": Object { - "Next": "return $SFN.parallel([function(), function()])", + "Next": "return $SFN.parallel(function(), function())", "Parameters": Object { "fnl_context": Object { "null": null, @@ -8569,7 +8569,7 @@ Object { "ResultPath": "$", "Type": "Pass", }, - "return $SFN.parallel([function(), function()])": Object { + "return $SFN.parallel(function(), function())": Object { "Branches": Array [ Object { "StartAt": "return \\"hello\\"", @@ -8594,7 +8594,7 @@ Object { }, }, ], - "Next": "1__return $SFN.parallel([function(), function()])", + "Next": "1__return $SFN.parallel(function(), function())", "ResultPath": "$.heap0", "Type": "Parallel", }, @@ -8606,14 +8606,14 @@ exports[`return $SFN.parallel(() => {})) } 1`] = ` Object { "StartAt": "Initialize Functionless Context", "States": Object { - "1__return $SFN.parallel([function()])": Object { + "1__return $SFN.parallel(function())": Object { "End": true, "InputPath": "$.heap0", "ResultPath": "$", "Type": "Pass", }, "Initialize Functionless Context": Object { - "Next": "return $SFN.parallel([function()])", + "Next": "return $SFN.parallel(function())", "Parameters": Object { "fnl_context": Object { "null": null, @@ -8622,7 +8622,7 @@ Object { "ResultPath": "$", "Type": "Pass", }, - "return $SFN.parallel([function()])": Object { + "return $SFN.parallel(function())": Object { "Branches": Array [ Object { "StartAt": "return null", @@ -8636,7 +8636,7 @@ Object { }, }, ], - "Next": "1__return $SFN.parallel([function()])", + "Next": "1__return $SFN.parallel(function())", "ResultPath": "$.heap0", "Type": "Parallel", }, @@ -10393,12 +10393,35 @@ Object { } `; -exports[`throw new CustomError 1`] = ` +exports[`throw new Error 1`] = ` +Object { + "StartAt": "Initialize Functionless Context", + "States": Object { + "Initialize Functionless Context": Object { + "Next": "throw new Error(\\"cause\\")", + "Parameters": Object { + "fnl_context": Object { + "null": null, + }, + }, + "ResultPath": "$", + "Type": "Pass", + }, + "throw new Error(\\"cause\\")": Object { + "Cause": "{\\"message\\":\\"cause\\"}", + "Error": "Error", + "Type": "Fail", + }, + }, +} +`; + +exports[`throw new StepFunctionError 1`] = ` Object { "StartAt": "Initialize Functionless Context", "States": Object { "Initialize Functionless Context": Object { - "Next": "throw new CustomError(\\"cause\\")", + "Next": "throw new StepFunctionError(\\"CustomError\\", {property: \\"cause\\"})", "Parameters": Object { "fnl_context": Object { "null": null, @@ -10407,7 +10430,7 @@ Object { "ResultPath": "$", "Type": "Pass", }, - "throw new CustomError(\\"cause\\")": Object { + "throw new StepFunctionError(\\"CustomError\\", {property: \\"cause\\"})": Object { "Cause": "{\\"property\\":\\"cause\\"}", "Error": "CustomError", "Type": "Fail", @@ -10416,12 +10439,12 @@ Object { } `; -exports[`throw new Error 1`] = ` +exports[`throw new functionless.StepFunctionError 1`] = ` Object { "StartAt": "Initialize Functionless Context", "States": Object { "Initialize Functionless Context": Object { - "Next": "throw new Error(\\"cause\\")", + "Next": "throw new functionless.StepFunctionError(\\"CustomError\\", {property: \\"cause\\"}", "Parameters": Object { "fnl_context": Object { "null": null, @@ -10430,9 +10453,9 @@ Object { "ResultPath": "$", "Type": "Pass", }, - "throw new Error(\\"cause\\")": Object { - "Cause": "{\\"message\\":\\"cause\\"}", - "Error": "Error", + "throw new functionless.StepFunctionError(\\"CustomError\\", {property: \\"cause\\"}": Object { + "Cause": "{\\"property\\":\\"cause\\"}", + "Error": "CustomError", "Type": "Fail", }, }, @@ -12059,7 +12082,7 @@ Object { "StartAt": "Initialize Functionless Context", "States": Object { "Initialize Functionless Context": Object { - "Next": "throw new CustomError(\\"cause\\")", + "Next": "throw new StepFunctionError(\\"CustomError\\", {property: \\"cause\\"})", "Parameters": Object { "fnl_context": Object { "null": null, @@ -12074,7 +12097,7 @@ Object { "ResultPath": "$", "Type": "Pass", }, - "throw new CustomError(\\"cause\\")": Object { + "throw new StepFunctionError(\\"CustomError\\", {property: \\"cause\\"})": Object { "Next": "return null", "Result": Object { "property": "cause", diff --git a/test/__snapshots__/validate.test.ts.snap b/test/__snapshots__/validate.test.ts.snap index 45c70edb..1b78e121 100644 --- a/test/__snapshots__/validate.test.ts.snap +++ b/test/__snapshots__/validate.test.ts.snap @@ -917,5 +917,35 @@ https://functionless.org/docs/error-codes#stepfunction-property-names-must-be-co     509 [input.key]: \\"\\",   ~~~~~~~~~~~~~~~ +test/test-files/step-function.ts:563:23 - error Functionless(10024): StepFunctions error cause must be a constant + +https://functionless.org/docs/error-codes#stepfunctions-error-cause-must-be-a-constant + +563 throw new Error(input.key); +   ~~~~~~~~~ +test/test-files/step-function.ts:565:16 - error Functionless(10030): StepFunction throw must be Error or StepFunctionError class + +https://functionless.org/docs/error-codes#stepfunction-throw-must-be-error-or-stepfunctionerror-class + +565 throw new CustomError(\\"error\\"); +   ~~~~~~~~~~~~ +test/test-files/step-function.ts:568:35 - error Functionless(10024): StepFunctions error cause must be a constant + +https://functionless.org/docs/error-codes#stepfunctions-error-cause-must-be-a-constant + +568 throw new StepFunctionError(input.key, { reason: \\"reason\\" }); +   ~~~~~~~~~ +test/test-files/step-function.ts:571:47 - error Functionless(10024): StepFunctions error cause must be a constant + +https://functionless.org/docs/error-codes#stepfunctions-error-cause-must-be-a-constant + +571 throw new StepFunctionError(\\"ErrorName\\", { reason: input.key }); +   ~~~~~~~~~~~~~~~~~~~~~~ +test/test-files/step-function.ts:573:47 - error Functionless(10024): StepFunctions error cause must be a constant + +https://functionless.org/docs/error-codes#stepfunctions-error-cause-must-be-a-constant + +573 throw new StepFunctionError(\\"ErrorName\\", input.key); +   ~~~~~~~~~~ " `; diff --git a/test/__snapshots__/vtl.test.ts.snap b/test/__snapshots__/vtl.test.ts.snap index 15bbfab8..9a39aa26 100644 --- a/test/__snapshots__/vtl.test.ts.snap +++ b/test/__snapshots__/vtl.test.ts.snap @@ -20,7 +20,7 @@ Array [ $util.qr($v1.put('a', 1)) #set($v2 = {}) $util.qr($v2.put('b', 2)) -#return($util.log.error('hello world', [$v1, $v2]))", +#return($util.log.error('hello world', $v1, $v2))", ] `; @@ -44,7 +44,7 @@ Array [ $util.qr($v1.put('a', 1)) #set($v2 = {}) $util.qr($v2.put('b', 2)) -#return($util.log.info('hello world', [$v1, $v2]))", +#return($util.log.info('hello world', $v1, $v2))", ] `; @@ -523,7 +523,7 @@ Array [ #if($v1) #break #end -$util.qr($context.stash.newList.addAll([$item])) +$util.qr($context.stash.newList.add($item)) #end #return($context.stash.newList)", ] @@ -799,7 +799,7 @@ Array [ }", "#set($context.stash.newList = []) #foreach($key in $context.arguments.record.keySet()) -$util.qr($context.stash.newList.addAll([$context.arguments.record[$key]])) +$util.qr($context.stash.newList.add($context.arguments.record[$key])) #end #return($context.stash.newList)", ] @@ -813,7 +813,7 @@ Array [ }", "#set($context.stash.newList = []) #foreach($item in $context.arguments.list) -$util.qr($context.stash.newList.addAll([$item])) +$util.qr($context.stash.newList.add($item)) #end #return($context.stash.newList)", ] @@ -860,7 +860,7 @@ Array [ "#set($context.stash.newList = []) #foreach($item in $context.arguments.list) #set($i = $item) -$util.qr($context.stash.newList.addAll([$i])) +$util.qr($context.stash.newList.add($i)) #end #return($context.stash.newList)", ] @@ -1056,7 +1056,7 @@ Array [ \\"version\\": \\"2018-05-29\\", \\"payload\\": null }", - "$util.qr($context.arguments.list.addAll(['hello'])) + "$util.qr($context.arguments.list.add('hello')) #return($context.arguments.list)", ] `; diff --git a/test/node.test.ts b/test/node.test.ts index cf80b52e..37ac144e 100644 --- a/test/node.test.ts +++ b/test/node.test.ts @@ -15,7 +15,7 @@ import { test("node.exit() from catch surrounded by while", () => { const catchClause = new CatchClause( - new VariableDecl("var", new NullLiteralExpr()), + new VariableDecl(new Identifier("var"), new NullLiteralExpr()), new BlockStmt([new ExprStmt(new CallExpr(new Identifier("task"), []))]) ); @@ -24,7 +24,7 @@ test("node.exit() from catch surrounded by while", () => { new BlockStmt([new TryStmt(new BlockStmt([]), catchClause)]) ); - new FunctionDecl([], new BlockStmt([whileStmt])); + new FunctionDecl("name", [], new BlockStmt([whileStmt])); const exit = catchClause.exit(); diff --git a/test/reflect.test.ts b/test/reflect.test.ts index cf2e741e..a56f6ae3 100644 --- a/test/reflect.test.ts +++ b/test/reflect.test.ts @@ -7,6 +7,7 @@ import { NullLiteralExpr, NumberLiteralExpr, ObjectLiteralExpr, + ParenthesizedExpr, reflect, ReturnStmt, StringLiteralExpr, @@ -44,7 +45,11 @@ test("parenthesis", () => { ); const expr = assertNodeKind(fn.body.statements[0], "ExprStmt"); - assertNodeKind(expr.expr, "StringLiteralExpr"); + const parens = assertNodeKind( + expr.expr, + "ParenthesizedExpr" + ); + assertNodeKind(parens.expr, "StringLiteralExpr"); }); test("parenthesis are respected", () => { @@ -58,7 +63,11 @@ test("parenthesis are respected", () => { const expr = assertNodeKind(fn.body.statements[0], "ExprStmt"); const bin = assertNodeKind(expr.expr, "BinaryExpr"); assertNodeKind(bin.left, "NumberLiteralExpr"); - assertNodeKind(bin.right, "BinaryExpr"); + const parens = assertNodeKind( + bin.right, + "ParenthesizedExpr" + ); + assertNodeKind(parens.expr, "BinaryExpr"); }); test("parenthesis are respected inverted", () => { @@ -111,7 +120,6 @@ test("any function args", () => { const call = assertNodeKind(expr.expr, "CallExpr"); expect(call.args).toHaveLength(1); - expect(call.getArgument("searchString")).toBeUndefined(); }); test("named function args", () => { @@ -125,9 +133,7 @@ test("named function args", () => { const expr = assertNodeKind(result.body.statements[0], "ExprStmt"); const call = assertNodeKind(expr.expr, "CallExpr"); - expect(call.getArgument("searchString")?.expr?.kind).toEqual( - "StringLiteralExpr" - ); + expect(call.args[0]?.expr?.kind).toEqual("StringLiteralExpr"); }); test("null", () => { diff --git a/test/step-function.test.ts b/test/step-function.test.ts index 8792616b..907f3ba3 100644 --- a/test/step-function.test.ts +++ b/test/step-function.test.ts @@ -1,6 +1,7 @@ import { aws_stepfunctions, Stack } from "aws-cdk-lib"; import { Pass } from "aws-cdk-lib/aws-stepfunctions"; import "jest"; +import * as functionless from "../src"; import { $AWS, $SFN, @@ -11,6 +12,7 @@ import { SyncExecutionResult, ErrorCodes, SynthError, + StepFunctionError, } from "../src"; import { StateMachine, States, Task } from "../src/asl"; import { Function } from "../src/function"; @@ -952,6 +954,44 @@ test("for i in items, items[i]", () => { expect(normalizeDefinition(definition)).toMatchSnapshot(); }); +test("for i in items, items[i] (shadowed)", () => { + const { stack } = initStepFunctionApp(); + expect( + () => + new ExpressStepFunction<{ items: string[] }, void>( + stack, + "fn", + (input) => { + var i; + for (i in input.items) { + const i = ""; + // @ts-ignore + const a = items[i]; + } + } + ) + ).toThrow(); +}); + +test("for let i in items, items[i] (shadowed)", () => { + const { stack } = initStepFunctionApp(); + expect( + () => + new ExpressStepFunction<{ items: string[] }, void>( + stack, + "fn", + (input) => { + // @ts-ignore + for (let i in input.items) { + const i = ""; + // @ts-ignore + const a = items[i]; + } + } + ) + ).toThrow(); +}); + test("empty for", () => { const { stack, task } = initStepFunctionApp(); const definition = new ExpressStepFunction<{ items: string[] }, void>( @@ -1391,15 +1431,23 @@ test("throw Error", () => { expect(normalizeDefinition(definition)).toMatchSnapshot(); }); -class CustomError { - constructor(readonly property: string) {} -} +test("throw new StepFunctionError", () => { + const { stack } = initStepFunctionApp(); + + const definition = new ExpressStepFunction(stack, "fn", () => { + throw new StepFunctionError("CustomError", { property: "cause" }); + }).definition; -test("throw new CustomError", () => { + expect(normalizeDefinition(definition)).toMatchSnapshot(); +}); + +test("throw new functionless.StepFunctionError", () => { const { stack } = initStepFunctionApp(); const definition = new ExpressStepFunction(stack, "fn", () => { - throw new CustomError("cause"); + throw new functionless.StepFunctionError("CustomError", { + property: "cause", + }); }).definition; expect(normalizeDefinition(definition)).toMatchSnapshot(); @@ -1422,7 +1470,7 @@ test("try, throw, empty catch", () => { const definition = new ExpressStepFunction(stack, "fn", () => { try { - throw new CustomError("cause"); + throw new StepFunctionError("CustomError", { property: "cause" }); } catch {} }).definition; @@ -1451,7 +1499,7 @@ test("catch and throw new Error", () => { try { throw new Error("cause"); } catch (err: any) { - throw new CustomError("custom cause"); + throw new StepFunctionError("CustomError", { property: "custom cause" }); } }).definition; @@ -1465,7 +1513,7 @@ test("catch and throw Error", () => { try { throw Error("cause"); } catch (err: any) { - throw new CustomError("custom cause"); + throw new StepFunctionError("CustomError", { property: "custom cause" }); } }).definition; @@ -2172,7 +2220,9 @@ test("throw task(task())", () => { throw await task(await task(input)); } ) - ).toThrow("StepFunctions error cause must be a constant"); + ).toThrow( + "StepFunction throw must be Error or StepFunctionError class\n\nhttps://functionless.org/docs/error-codes#stepfunction-throw-must-be-error-or-stepfunctionerror-class" + ); }); test("input.b ? task() : task(input)", () => { diff --git a/test/test-files/step-function.ts b/test/test-files/step-function.ts index b5b4091a..3483dab9 100644 --- a/test/test-files/step-function.ts +++ b/test/test-files/step-function.ts @@ -5,11 +5,11 @@ import { StepFunction, Function, EventBus, - // @ts-ignore - for ts-docs ErrorCodes, AppsyncResolver, $AWS, Table, + StepFunctionError, } from "../../src"; import { Event } from "../../src/event-bridge"; import { PutEventInput } from "../../src/event-bridge/event-bus"; @@ -526,3 +526,55 @@ const objAssignFunc = new Function< new StepFunction(stack, "obj ref", async (input: { key: string }) => { return objAssignFunc({ obj: {}, key: input.key, value: "" }); }); + +// supported errors + +// eslint-disable-next-line import/order +import * as functionless from "../../src"; + +new StepFunction(stack, "supported errors", async (input: { key: string }) => { + if (input.key === "1") { + throw new Error(); + } else if (input.key === "2") { + throw Error(); + } else if (input.key === "3") { + throw new Error("message"); + } else if (input.key === "4") { + throw Error("message"); + } else if (input.key === "5") { + // import { StepFunctionError } from "functionless"; + throw new StepFunctionError("ErrorName", { reason: "you suck" }); + } else if (input.key === "6") { + // import * as functionless from "functionless"; + throw new functionless.StepFunctionError("ErrorName", { + reason: "you suck", + }); + } +}); + +// unsupported errors + +new StepFunction( + stack, + "unsupported errors", + async (input: { key: string }) => { + if (input.key === "1") { + // reference is not allowed + throw new Error(input.key); + } else if (input.key === "2") { + throw new CustomError("error"); + } else if (input.key === "3") { + // non-constant value as first arg + throw new StepFunctionError(input.key, { reason: "reason" }); + } else if (input.key === "4") { + // non-constant value as second arg + throw new StepFunctionError("ErrorName", { reason: input.key }); + } else { + throw new StepFunctionError("ErrorName", input.key); + } + } +); + +class CustomError { + constructor(readonly prop: string) {} +} diff --git a/test/validate.test.ts b/test/validate.test.ts index a483d02c..07cca3c6 100644 --- a/test/validate.test.ts +++ b/test/validate.test.ts @@ -46,6 +46,8 @@ const skipErrorCodes: ErrorCode[] = [ ErrorCodes.Unsupported_Feature, // generic ErrorCodes.Invalid_Input, + // hard to validate, will be supported later + ErrorCodes.Classes_are_not_supported, ]; /** @@ -68,7 +70,11 @@ describe("all error codes tested", () => { test.concurrent.each( Object.values(ErrorCodes).filter((code) => !skipErrorCodes.includes(code)) )("$code: $title", async (code) => { - expect(file!).toContain(`${code.code}`); + if (!file?.includes(`${code.code}`)) { + throw new Error( + `validate.test.ts does not emit any errors for ${code.title}` + ); + } }); test.skip.each(skipErrorCodes)("$code: $title", () => {}); diff --git a/website/docs/concepts/step-function/usage.md b/website/docs/concepts/step-function/usage.md index 617561b1..7c4d2d7d 100644 --- a/website/docs/concepts/step-function/usage.md +++ b/website/docs/concepts/step-function/usage.md @@ -239,3 +239,26 @@ new StepFunction(stack, "sfn", (input, context) => { :::info For more details on the Context Argument, see [Context Object](https://docs.aws.amazon.com/step-functions/latest/dg/input-output-contextobject.html). ::: + +## Throw Error + +When throwing errors from a Step Function, you have two options available: + +1. throw NodeJS's `Error` type + +```ts +throw new Error("message"); +``` + +2. throw Functionless's [`StepFunctionError`](../../api/classes/StepFunctionError.md) type + +```ts +throw new StepFunctionError("CustomErrorName", "cause"); +``` + +Due to limitations in AWS Step Functions, all of the arguments to `Error` and `StepFunctionError` must be constant values. + +```ts +// illegal: input.prop is not a constant value +throw new Error(input.prop); +```