From c1976a6ad42b9aacff638b8cab8ec74176629138 Mon Sep 17 00:00:00 2001 From: Alan Pierce Date: Mon, 13 Jan 2020 08:48:27 -0800 Subject: [PATCH] Add support for basic TS assertion signature syntax (#504) Fixes #503 The previous code didn't handle the basic case `asserts x`, which I just overlooked when backporting Babel parser changes. The new code has a special case for that and conditionally tells the caller whether to parse a type afterward. I also made the contract a little simpler; it either fully parses the return type or it doesn't. --- src/parser/plugins/typescript.ts | 32 ++++++++++++++++++++++++-------- test/typescript-test.ts | 19 +++++++++++++++++++ 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/src/parser/plugins/typescript.ts b/src/parser/plugins/typescript.ts index 0a9d14cf..4f111a6c 100644 --- a/src/parser/plugins/typescript.ts +++ b/src/parser/plugins/typescript.ts @@ -575,10 +575,10 @@ function tsIsUnambiguouslyStartOfFunctionType(): boolean { function tsParseTypeOrTypePredicateAnnotation(returnToken: TokenType): void { const oldIsType = pushTypeContext(0); expect(returnToken); - tsParseTypePredicateOrAssertsPrefix(); - // Regardless of whether we found an "asserts" or "is" token, there's now just a regular type in - // front of us. - tsParseType(); + const finishedReturn = tsParseTypePredicateOrAssertsPrefix(); + if (!finishedReturn) { + tsParseType(); + } popTypeContext(oldIsType); } @@ -600,33 +600,49 @@ function tsTryParseType(): void { } } -function tsParseTypePredicateOrAssertsPrefix(): void { +/** + * Detect a few special return syntax cases: `x is T`, `asserts x`, `asserts x is T`, + * `asserts this is T`. + * + * Returns true if we parsed the return type, false if there's still a type to be parsed. + */ +function tsParseTypePredicateOrAssertsPrefix(): boolean { const snapshot = state.snapshot(); if (isContextual(ContextualKeyword._asserts) && !hasPrecedingLineBreak()) { // Normally this is `asserts x is T`, but at this point, it might be `asserts is T` (a user- // defined type guard on the `asserts` variable) or just a type called `asserts`. next(); - if (isContextual(ContextualKeyword._is)) { + if (eatContextual(ContextualKeyword._is)) { // If we see `asserts is`, then this must be of the form `asserts is T`, since // `asserts is is T` isn't valid. - next(); + tsParseType(); + return true; } else if (tsIsIdentifier() || match(tt._this)) { next(); - expectContextual(ContextualKeyword._is); + if (eatContextual(ContextualKeyword._is)) { + // If we see `is`, then this is `asserts x is T`. Otherwise, it's `asserts x`. + tsParseType(); + } + return true; } else { // Regular type, so bail out and start type parsing from scratch. state.restoreFromSnapshot(snapshot); + return false; } } else if (tsIsIdentifier() || match(tt._this)) { // This is a regular identifier, which may or may not have "is" after it. next(); if (isContextual(ContextualKeyword._is) && !hasPrecedingLineBreak()) { next(); + tsParseType(); + return true; } else { // Regular type, so bail out and start type parsing from scratch. state.restoreFromSnapshot(snapshot); + return false; } } + return false; } export function tsParseTypeAnnotation(): void { diff --git a/test/typescript-test.ts b/test/typescript-test.ts index 1395ef60..c2c4cac0 100644 --- a/test/typescript-test.ts +++ b/test/typescript-test.ts @@ -1834,6 +1834,25 @@ describe("typescript transform", () => { }); it("allows assertion signature syntax", () => { + assertTypeScriptResult( + ` + function assert(condition: any, msg?: string): asserts condition { + if (!condition) { + throw new AssertionError(msg) + } + } + `, + `"use strict"; + function assert(condition, msg) { + if (!condition) { + throw new AssertionError(msg) + } + } + `, + ); + }); + + it("allows assertion signature syntax with is", () => { assertTypeScriptResult( ` function assertIsDefined(x: T): asserts x is NonNullable {