diff --git a/.travis.yml b/.travis.yml index 440502be..7c9bc370 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,11 +24,12 @@ matrix: - yarn fast-build - yarn run-examples tslint apollo-client - script: - - yarn test - yarn build + - yarn test - yarn run-examples decaffeinate decaffeinate-parser coffee-lex - node_js: '8' script: + - yarn build - yarn test # Exclude the default build; we only want to run explicitly-included builds. exclude: diff --git a/script/lint.ts b/script/lint.ts index c7e09a58..2f7b2be2 100755 --- a/script/lint.ts +++ b/script/lint.ts @@ -39,4 +39,5 @@ async function checkIntegration(path: string): Promise { main().catch((e) => { console.error("Unhandled error:"); console.error(e); + process.exitCode = 1; }); diff --git a/src/parser/plugins/flow.ts b/src/parser/plugins/flow.ts index c79804eb..5f54bbda 100644 --- a/src/parser/plugins/flow.ts +++ b/src/parser/plugins/flow.ts @@ -242,7 +242,7 @@ function flowParseInterfaceish(isClass: boolean = false): void { } while (eat(tt.comma)); } - flowParseObjectType(true, false, isClass); + flowParseObjectType(isClass, false, isClass); } function flowParseInterfaceExtends(): void { @@ -338,7 +338,7 @@ function flowParseInterfaceType(): void { flowParseInterfaceExtends(); } while (eat(tt.comma)); } - flowParseObjectType(true, false, false); + flowParseObjectType(false, false, false); } function flowParseObjectPropertyKey(): void { diff --git a/src/parser/plugins/jsx/index.ts b/src/parser/plugins/jsx/index.ts index 579498d8..a9393a98 100644 --- a/src/parser/plugins/jsx/index.ts +++ b/src/parser/plugins/jsx/index.ts @@ -10,11 +10,12 @@ import { Token, } from "../../tokenizer/index"; import {TokenType as tt} from "../../tokenizer/types"; -import {input, raise, state} from "../../traverser/base"; +import {input, isTypeScriptEnabled, raise, state} from "../../traverser/base"; import {parseExpression, parseMaybeAssign} from "../../traverser/expression"; import {expect, unexpected} from "../../traverser/util"; import {charCodes} from "../../util/charcodes"; import {isIdentifierChar, isIdentifierStart} from "../../util/identifier"; +import {tsTryParseJSXTypeArgument} from "../typescript"; // Reads inline JSX contents token. function jsxReadToken(): void { @@ -178,6 +179,9 @@ function jsxParseOpeningElement(): boolean { return false; } jsxParseElementName(); + if (isTypeScriptEnabled) { + tsTryParseJSXTypeArgument(); + } while (!match(tt.slash) && !match(tt.jsxTagEnd)) { jsxParseAttribute(); } @@ -252,7 +256,7 @@ export function jsxParseElement(): void { // Overrides // ================================== -function nextJSXTagToken(): void { +export function nextJSXTagToken(): void { state.tokens.push(new Token()); skipSpace(); state.start = state.pos; diff --git a/src/parser/plugins/typescript.ts b/src/parser/plugins/typescript.ts index 1368da79..03aed6c4 100644 --- a/src/parser/plugins/typescript.ts +++ b/src/parser/plugins/typescript.ts @@ -23,6 +23,7 @@ import { parseMaybeAssign, parseMaybeUnary, parsePropertyName, + parseTemplate, StopState, } from "../traverser/expression"; import {parseBindingList} from "../traverser/lval"; @@ -49,6 +50,7 @@ import { semicolon, unexpected, } from "../traverser/util"; +import {nextJSXTagToken} from "./jsx"; function assert(x: boolean): void { if (!x) { @@ -673,17 +675,15 @@ export function tsParseTypeAssertion(): void { parseMaybeUnary(); } -// Returns true if parsing was successful. -function tsTryParseTypeArgumentsInExpression(): boolean { - return tsTryParseAndCatch(() => { - const oldIsType = pushTypeContext(0); - expect(tt.lessThan); +export function tsTryParseJSXTypeArgument(): void { + if (eat(tt.jsxTagStart)) { state.tokens[state.tokens.length - 1].type = tt.typeParameterStart; + const oldIsType = pushTypeContext(1); tsParseDelimitedList(ParsingContext.TypeParametersOrArguments, tsParseType); - expect(tt.greaterThan); + // Process >, but the one after needs to be parsed JSX-style. + nextJSXTagToken(); popTypeContext(oldIsType); - expect(tt.parenL); - }); + } } function tsParseHeritageClause(): void { @@ -1093,22 +1093,32 @@ export function tsParseSubscript( return; } - if (!noCalls && match(tt.lessThan)) { - if (atPossibleAsync()) { - // Almost certainly this is a generic async function `async () => ... - // But it might be a call with a type argument `async();` - const asyncArrowFn = tsTryParseGenericAsyncArrowFunction(); - if (asyncArrowFn) { + // There are number of things we are going to "maybe" parse, like type arguments on + // tagged template expressions. If any of them fail, walk it back and continue. + const success = tsTryParseAndCatch(() => { + if (match(tt.lessThan)) { + if (!noCalls && atPossibleAsync()) { + // Almost certainly this is a generic async function `async () => ... + // But it might be a call with a type argument `async();` + const asyncArrowFn = tsTryParseGenericAsyncArrowFunction(); + if (asyncArrowFn) { + return; + } + } + tsParseTypeArguments(); + if (!noCalls && eat(tt.parenL)) { + parseCallExpressionArguments(tt.parenR); + return; + } else if (match(tt.backQuote)) { + // Tagged template with a type argument. + parseTemplate(); return; } } - - // May be passing type arguments. But may just be the `<` operator. - const typeArguments = tsTryParseTypeArgumentsInExpression(); // Also eats the "(" - if (typeArguments) { - // possibleAsync always false here, because we would have handled it above. - parseCallExpressionArguments(tt.parenR); - } + unexpected(); + }); + if (success) { + return; } baseParseSubscript(startPos, noCalls, stopState); } @@ -1157,6 +1167,13 @@ export function tsTryParseExportDefaultExpression(): boolean { parseClass(true, true); return true; } + if (isContextual(ContextualKeyword._interface)) { + // Make sure "export default" are considered type tokens so the whole thing is removed. + const oldIsType = pushTypeContext(2); + tsParseDeclaration(ContextualKeyword._interface, true); + popTypeContext(oldIsType); + return true; + } return false; } diff --git a/src/parser/tokenizer/index.ts b/src/parser/tokenizer/index.ts index 9cd13514..a5e4c0eb 100644 --- a/src/parser/tokenizer/index.ts +++ b/src/parser/tokenizer/index.ts @@ -4,7 +4,7 @@ import {input, isFlowEnabled, raise, state} from "../traverser/base"; import {unexpected} from "../traverser/util"; import {charCodes} from "../util/charcodes"; import {isIdentifierChar, isIdentifierStart} from "../util/identifier"; -import {nonASCIIwhitespace} from "../util/whitespace"; +import {isWhitespace} from "../util/whitespace"; import readWord from "./readWord"; import {TokenType, TokenType as tt} from "./types"; @@ -267,11 +267,6 @@ export function skipSpace(): void { while (state.pos < input.length) { const ch = input.charCodeAt(state.pos); switch (ch) { - case charCodes.space: - case charCodes.nonBreakingSpace: - ++state.pos; - break; - case charCodes.carriageReturn: if (input.charCodeAt(state.pos + 1) === charCodes.lineFeed) { ++state.pos; @@ -299,10 +294,7 @@ export function skipSpace(): void { break; default: - if ( - (ch > charCodes.backSpace && ch < charCodes.shiftOut) || - (ch >= charCodes.oghamSpaceMark && nonASCIIwhitespace.test(String.fromCharCode(ch))) - ) { + if (isWhitespace(ch)) { ++state.pos; } else { return; diff --git a/src/parser/traverser/expression.ts b/src/parser/traverser/expression.ts index fe5f29e9..b9acdb6e 100644 --- a/src/parser/traverser/expression.ts +++ b/src/parser/traverser/expression.ts @@ -669,7 +669,7 @@ function parseNewArguments(): void { } } -function parseTemplate(): void { +export function parseTemplate(): void { // Finish `, read quasi nextTemplateToken(); // Finish quasi, read ${ diff --git a/src/parser/util/whitespace.ts b/src/parser/util/whitespace.ts index fb0484b6..1b621da7 100644 --- a/src/parser/util/whitespace.ts +++ b/src/parser/util/whitespace.ts @@ -1,5 +1,36 @@ // Matches a whole line break (where CRLF is considered a single // line break). Used to count lines. +import {charCodes} from "./charcodes"; + export const lineBreak = /\r\n?|\n|\u2028|\u2029/; -export const nonASCIIwhitespace = /[\u1680\u180e\u2000-\u200a\u202f\u205f\u3000\ufeff]/; +// https://tc39.github.io/ecma262/#sec-white-space +export function isWhitespace(code: number): boolean { + switch (code) { + case 0x0009: // CHARACTER TABULATION + case 0x000b: // LINE TABULATION + case 0x000c: // FORM FEED + case charCodes.space: + case charCodes.nonBreakingSpace: + case charCodes.oghamSpaceMark: + case 0x2000: // EN QUAD + case 0x2001: // EM QUAD + case 0x2002: // EN SPACE + case 0x2003: // EM SPACE + case 0x2004: // THREE-PER-EM SPACE + case 0x2005: // FOUR-PER-EM SPACE + case 0x2006: // SIX-PER-EM SPACE + case 0x2007: // FIGURE SPACE + case 0x2008: // PUNCTUATION SPACE + case 0x2009: // THIN SPACE + case 0x200a: // HAIR SPACE + case 0x202f: // NARROW NO-BREAK SPACE + case 0x205f: // MEDIUM MATHEMATICAL SPACE + case 0x3000: // IDEOGRAPHIC SPACE + case 0xfeff: // ZERO WIDTH NO-BREAK SPACE + return true; + + default: + return false; + } +} diff --git a/src/transformers/JSXTransformer.ts b/src/transformers/JSXTransformer.ts index 21c648d1..dd9dd0b7 100644 --- a/src/transformers/JSXTransformer.ts +++ b/src/transformers/JSXTransformer.ts @@ -150,15 +150,18 @@ export default class JSXTransformer extends Transformer { processTagIntro(): void { // Walk forward until we see one of these patterns: // jsxName to start the first prop, preceded by another jsxName to end the tag name. + // jsxName to start the first prop, preceded by greaterThan to end the type argument. // [open brace] to start the first prop. // [jsxTagEnd] to end the open-tag. // [slash, jsxTagEnd] to end the self-closing tag. let introEnd = this.tokens.currentIndex() + 1; while ( - !this.tokens.matchesAtIndex(introEnd - 1, [tt.jsxName, tt.jsxName]) && - !this.tokens.matchesAtIndex(introEnd, [tt.braceL]) && - !this.tokens.matchesAtIndex(introEnd, [tt.jsxTagEnd]) && - !this.tokens.matchesAtIndex(introEnd, [tt.slash, tt.jsxTagEnd]) + this.tokens.tokens[introEnd].isType || + (!this.tokens.matchesAtIndex(introEnd - 1, [tt.jsxName, tt.jsxName]) && + !this.tokens.matchesAtIndex(introEnd - 1, [tt.greaterThan, tt.jsxName]) && + !this.tokens.matchesAtIndex(introEnd, [tt.braceL]) && + !this.tokens.matchesAtIndex(introEnd, [tt.jsxTagEnd]) && + !this.tokens.matchesAtIndex(introEnd, [tt.slash, tt.jsxTagEnd])) ) { introEnd++; } diff --git a/test/flow-test.ts b/test/flow-test.ts index 2ba453a2..9b6a9935 100644 --- a/test/flow-test.ts +++ b/test/flow-test.ts @@ -298,4 +298,34 @@ describe("transform flow", () => { `, ); }); + + it("allows interface methods named 'static'", () => { + assertFlowResult( + ` + type T = interface { static(): number } + `, + `"use strict"; + + `, + ); + }); + + // Note that we don't actually transform private fields at the moment, this just makes sure it + // parses. + it("allows private properties with type annotations", () => { + assertFlowResult( + ` + class A { + #prop1: string; + #prop2: number = value; + } + `, + `"use strict";const __init = Symbol(); + class A {constructor() { this[__init](); } + + [__init]() {this.prop2 = value} + } + `, + ); + }); }); diff --git a/test/imports-test.ts b/test/imports-test.ts index a023b324..3d65480f 100644 --- a/test/imports-test.ts +++ b/test/imports-test.ts @@ -1043,4 +1043,20 @@ module.exports = exports.default; {transforms: ["imports", "typescript"]}, ); }); + + // Skipping since this is reasonably obscure and seems hard to implement right in Sucrase. + // Babel fix: https://github.com/babel/babel/pull/8698 + it.skip("treats 'export default async' as a complete statement", () => { + assertResult( + ` + export default async + function bar() {} + `, + `"use strict";${ESMODULE_PREFIX} + exports. default = async + function bar() {} + `, + {transforms: ["imports", "typescript"]}, + ); + }); }); diff --git a/test/typescript-test.ts b/test/typescript-test.ts index f13fc1ac..f9b906ed 100644 --- a/test/typescript-test.ts +++ b/test/typescript-test.ts @@ -1045,4 +1045,43 @@ describe("typescript transform", () => { `, ); }); + + it("allows type arguments in JSX elements", () => { + assertTypeScriptResult( + ` + const e1 = x="1" /> + const e2 = >Hello + `, + `"use strict";const _jsxFileName = ""; + const e1 = React.createElement(Foo, { x: "1", __self: this, __source: {fileName: _jsxFileName, lineNumber: 2}} ) + const e2 = React.createElement(Foo, {__self: this, __source: {fileName: _jsxFileName, lineNumber: 3}}, React.createElement('span', {__self: this, __source: {fileName: _jsxFileName, lineNumber: 3}}, "Hello")) + `, + ); + }); + + it("allows type arguments tagged templates", () => { + assertTypeScriptResult( + ` + f\`\`; + new C + \`\`; + `, + `"use strict"; + f\`\`; + new C + \`\`; + `, + ); + }); + + it("allows export default interface", () => { + assertTypeScriptResult( + ` + export default interface A {} + `, + `"use strict"; + + `, + ); + }); });