diff --git a/src/transformation/visitors/errors.ts b/src/transformation/visitors/errors.ts index 77067dda8..a070caac6 100644 --- a/src/transformation/visitors/errors.ts +++ b/src/transformation/visitors/errors.ts @@ -165,6 +165,7 @@ export const transformTryStatement: FunctionVisitor = (statemen const tryResultIdentifier = lua.createIdentifier("____try"); const returnValueIdentifier = lua.createIdentifier("____returnValue"); + const rethrowIdentifier = lua.createIdentifier("____rethrow"); const result: lua.Statement[] = []; @@ -191,6 +192,9 @@ export const transformTryStatement: FunctionVisitor = (statemen } } result.push(lua.createVariableDeclarationStatement(tryReturnIdentifiers, tryCall)); + if (statement.finallyBlock) { + result.push(lua.createVariableDeclarationStatement(rethrowIdentifier)); + } const catchCall = lua.createCallExpression( catchIdentifier, @@ -203,8 +207,22 @@ export const transformTryStatement: FunctionVisitor = (statemen ) : lua.createExpressionStatement(catchCall); + const catchCallFunction = lua.createFunctionExpression(lua.createBlock([catchCallStatement])); + + const tryCatchCall = lua.createCallExpression(pCall, [catchCallFunction]); + + const tryCatchCallStatement = lua.createAssignmentStatement( + [lua.cloneIdentifier(tryResultIdentifier), lua.cloneIdentifier(rethrowIdentifier)], + tryCatchCall + ); + const notTryCondition = lua.createUnaryExpression(tryResultIdentifier, lua.SyntaxKind.NotOperator); - result.push(lua.createIfStatement(notTryCondition, lua.createBlock([catchCallStatement]))); + result.push( + lua.createIfStatement( + notTryCondition, + lua.createBlock(statement.finallyBlock ? [tryCatchCallStatement] : [catchCallStatement]) + ) + ); } else if (tryScope.functionReturned) { // try with return, but no catch // returnedIdentifier = lua.createIdentifier("____returned"); @@ -230,21 +248,33 @@ export const transformTryStatement: FunctionVisitor = (statemen result.push(...context.transformStatements(statement.finallyBlock)); } - // Re-throw error if try had no catch but had a finally. + // Re-throw error if try had a finally. // On pcall failure the error is the second return value, which lands in // ____hasReturned (when functionReturned) or ____error (otherwise). - if (!statement.catchClause && statement.finallyBlock) { + if (statement.finallyBlock) { const notTryCondition = lua.createUnaryExpression( lua.cloneIdentifier(tryResultIdentifier), lua.SyntaxKind.NotOperator ); - const errorIdentifier = tryScope.functionReturned - ? lua.cloneIdentifier(returnedIdentifier) - : lua.createIdentifier("____error"); - const rethrow = lua.createExpressionStatement( - lua.createCallExpression(lua.createIdentifier("error"), [errorIdentifier, lua.createNumericLiteral(0)]) - ); - result.push(lua.createIfStatement(notTryCondition, lua.createBlock([rethrow]))); + + if (!statement.catchClause) { + const errorIdentifier = tryScope.functionReturned + ? lua.cloneIdentifier(returnedIdentifier) + : lua.createIdentifier("____error"); + const rethrow = lua.createExpressionStatement( + lua.createCallExpression(lua.createIdentifier("error"), [errorIdentifier, lua.createNumericLiteral(0)]) + ); + result.push(lua.createIfStatement(notTryCondition, lua.createBlock([rethrow]))); + } else if (statement.catchClause.block.statements.length > 0) { + // Re-throw is possible only from non-empty blocks. + const rethrow = lua.createExpressionStatement( + lua.createCallExpression(lua.createIdentifier("error"), [ + rethrowIdentifier, + lua.createNumericLiteral(0), + ]) + ); + result.push(lua.createIfStatement(notTryCondition, lua.createBlock([rethrow]))); + } } if (returnCondition && returnedIdentifier) { diff --git a/test/unit/error.spec.ts b/test/unit/error.spec.ts index 8204cc36a..f7056655f 100644 --- a/test/unit/error.spec.ts +++ b/test/unit/error.spec.ts @@ -7,31 +7,40 @@ test("throwString", () => { `.expectToEqual(new util.ExecutionError("Some Error")); }); -// TODO: Finally does not behave like it should, see #1137 -// eslint-disable-next-line jest/no-disabled-tests -test.skip.each([0, 1, 2])("re-throw (%p)", i => { +test.each([ + { innerTry: false, innerFinally: false, outerFinally: false }, + { innerTry: false, innerFinally: false, outerFinally: true }, + { innerTry: false, innerFinally: true, outerFinally: false }, + { innerTry: false, innerFinally: true, outerFinally: true }, + { innerTry: true, innerFinally: false, outerFinally: false }, + { innerTry: true, innerFinally: false, outerFinally: true }, + { innerTry: true, innerFinally: true, outerFinally: false }, + { innerTry: true, innerFinally: true, outerFinally: true }, +])("re-throw (%j)", o => { util.testFunction` - const i: number = ${i}; + const innerTry: boolean = ${o.innerTry}; + const innerFinally: boolean = ${o.innerFinally}; + const outerFinally: boolean = ${o.outerFinally}; function foo() { try { try { - if (i === 0) { throw "z"; } + if (innerTry) { throw "inner.try"; } } catch (e) { - throw "a"; + throw (e as string) + "->" + "inner.catch"; } finally { - if (i === 1) { throw "b"; } + if (innerFinally) { throw "inner.finally"; } } } catch (e) { - throw (e as string).toUpperCase(); + throw (e as string) + "->" + "outer.catch"; } finally { - throw "C"; + if (outerFinally) { throw "outer.finally"; } } } let result: string = "x"; try { foo(); } catch (e) { - result = (e as string)[(e as string).length - 1]; + result = e as string; } return result; `.expectToMatchJsResult();