From eb68e720a4581ee232f09a4d583bbc5d8891d4d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Ribaudo?= Date: Tue, 16 May 2023 00:53:34 +0200 Subject: [PATCH 1/8] Generic import phases parsing --- packages/babel-parser/src/parser/statement.ts | 184 ++++++++++++------ .../babel-parser/src/plugins/flow/index.ts | 61 ++---- .../src/plugins/typescript/index.ts | 82 +++++--- .../invalid-flow-type-import-2/options.json | 8 +- .../invalid-flow-type-import-2/output.json | 42 ---- .../invalid-flow-typeof-import-2/options.json | 8 +- .../invalid-flow-typeof-import-2/output.json | 42 ---- .../invalid-ts-type-import-1/options.json | 8 +- .../invalid-ts-type-import-1/output.json | 42 ---- .../options.json | 4 +- 10 files changed, 217 insertions(+), 264 deletions(-) delete mode 100644 packages/babel-parser/test/fixtures/experimental/import-reflection/invalid-flow-type-import-2/output.json delete mode 100644 packages/babel-parser/test/fixtures/experimental/import-reflection/invalid-flow-typeof-import-2/output.json delete mode 100644 packages/babel-parser/test/fixtures/experimental/import-reflection/invalid-ts-type-import-1/output.json diff --git a/packages/babel-parser/src/parser/statement.ts b/packages/babel-parser/src/parser/statement.ts index d5c2e0492345..afc6f1778921 100644 --- a/packages/babel-parser/src/parser/statement.ts +++ b/packages/babel-parser/src/parser/statement.ts @@ -2918,67 +2918,128 @@ export default abstract class StatementParser extends ExpressionParser { } } - parseMaybeImportReflection(node: Undone) { - let isImportReflection = false; - if (this.isContextual(tt._module)) { - const lookahead = this.lookahead(); - const nextType = lookahead.type; - if (tokenIsIdentifier(nextType)) { - if (nextType !== tt._from) { - // import module x - isImportReflection = true; - } else { - const nextNextTokenFirstChar = this.input.charCodeAt( - this.nextTokenStartSince(lookahead.end), - ); - if (nextNextTokenFirstChar === charCodes.lowercaseF) { - // import module from from ... - isImportReflection = true; - } - } - } else if (nextType !== tt.comma) { - // import module { x } ... - // import module "foo" - // They are invalid, we will continue parsing and throw - // a recoverable error later - isImportReflection = true; - } - } - if (isImportReflection) { - this.expectPlugin("importReflection"); - this.next(); // eat tt._module; + isPotentialImportPhase(): boolean { + return this.isContextual(tt._module); + } + + applyImportPhase( + node: Undone, + phase: string | null, + loc?: Position, + ): void { + if (phase === "module") { + this.expectPlugin("importReflection", loc); node.module = true; } else if (this.hasPlugin("importReflection")) { node.module = false; } } + /* + * Parse `module` in `import module x fro "x"`, disambiguating + * `import module from "x"` and `import module from from "x"`. + * + * This function might return an identifier representing the `module` + * if it eats `module` and then discovers that it was the default import + * binding and not the import reflection. + * + * This function is also used to parse `import type` and `import typeof` + * in the TS and Flow plugins. + * + * Note: the proposal has been updated to use `source` instead of `module`, + * but it has not been implemented yet. + */ + parseMaybeImportPhase( + node: Undone, + ): N.Identifier | null { + if (!this.isPotentialImportPhase()) { + this.applyImportPhase(node as Undone, null); + return null; + } + + const phaseIdentifier = this.parseIdentifier(true); + const { type } = this.state; + const isImportPhase = tokenIsIdentifier(type) + ? // `import x` + type !== tt._from || + // `import module from from ...` + // We can safely assume that `f` is `from` because otherwise it + // will be a syntax error anyway. This will change with the + // module declarations proposal, and we will neet to disambiguate + // after parsing a second identifier. + this.lookaheadCharCode() === charCodes.lowercaseF + : // import { x } ... + // import "foo" + // The second one is invalid, we will continue parsing and throw + // a recoverable error later + // tt.eq is needed for TS `import type = require(...)`. + type !== tt.comma && type !== tt.eq; + + if (isImportPhase) { + this.applyImportPhase( + node as Undone, + phaseIdentifier.name, + phaseIdentifier.loc.start, + ); + return null; + } else { + this.applyImportPhase(node as Undone, null); + // `` is a default binding, return it to the main import declaration parser + return phaseIdentifier; + } + } + // Parses import declaration. // https://tc39.es/ecma262/#prod-ImportDeclaration parseImport(this: Parser, node: Undone): N.AnyImport { - // import '...' - node.specifiers = []; - if (!this.match(tt.string)) { - this.parseMaybeImportReflection(node); - // check if we have a default import like - // import React from "react"; - const hasDefault = this.maybeParseDefaultImportSpecifier(node); - /* we are checking if we do not have a default import, then it is obvious that we need named imports - * import { get } from "axios"; - * but if we do have a default import - * we need to check if we have a comma after that and - * that is where this `|| this.eat` condition comes into play - */ - const parseNext = !hasDefault || this.eat(tt.comma); - // if we do have to parse the next set of specifiers, we first check for star imports - // import React, * from "react"; - const hasStar = parseNext && this.maybeParseStarImportSpecifier(node); - // now we check if we need to parse the next imports - // but only if they are not importing * (everything) - if (parseNext && !hasStar) this.parseNamedImportSpecifiers(node); - this.expectContextual(tt._from); + if (this.match(tt.string)) { + // import '...' + return this.parseImportSourceAndAttributes(node); } + + return this.parseImportSpecifiersAndAfter( + node, + this.parseMaybeImportPhase(node), + ); + } + + parseImportSpecifiersAndAfter( + this: Parser, + node: Undone, + maybeDefaultIdentifier: N.Identifier | null, + ): N.AnyImport { + node.specifiers = []; + + // check if we have a default import like + // import React from "react"; + const hasDefault = this.maybeParseDefaultImportSpecifier( + node, + maybeDefaultIdentifier, + ); + /* we are checking if we do not have a default import, then it is obvious that we need named imports + * import { get } from "axios"; + * but if we do have a default import + * we need to check if we have a comma after that and + * that is where this `|| this.eat` condition comes into play + */ + const parseNext = !hasDefault || this.eat(tt.comma); + // if we do have to parse the next set of specifiers, we first check for star imports + // import React, * from "react"; + const hasStar = parseNext && this.maybeParseStarImportSpecifier(node); + // now we check if we need to parse the next imports + // but only if they are not importing * (everything) + if (parseNext && !hasStar) this.parseNamedImportSpecifiers(node); + this.expectContextual(tt._from); + + return this.parseImportSourceAndAttributes(node); + } + + parseImportSourceAndAttributes( + this: Parser, + node: Undone, + ): N.AnyImport { + node.specifiers ??= []; node.source = this.parseImportSource(); this.maybeParseImportAttributes(node); this.checkImportReflection(node); @@ -2993,11 +3054,6 @@ export default abstract class StatementParser extends ExpressionParser { return this.parseExprAtom() as N.StringLiteral; } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - shouldParseDefaultImport(node: Undone): boolean { - return tokenIsIdentifier(this.state.type); - } - parseImportSpecifierLocal< T extends | N.ImportSpecifier @@ -3177,9 +3233,21 @@ export default abstract class StatementParser extends ExpressionParser { } } - maybeParseDefaultImportSpecifier(node: Undone): boolean { - if (this.shouldParseDefaultImport(node)) { - // import defaultObj, { x, y as z } from '...' + maybeParseDefaultImportSpecifier( + node: Undone, + maybeDefaultIdentifier: N.Identifier | null, + ): boolean { + // import defaultObj, { x, y as z } from '...' + if (maybeDefaultIdentifier) { + const specifier = this.startNodeAtNode( + maybeDefaultIdentifier, + ); + specifier.local = maybeDefaultIdentifier; + node.specifiers.push( + this.finishImportSpecifier(specifier, "ImportDefaultSpecifier"), + ); + return true; + } else if (tokenIsIdentifier(this.state.type)) { this.parseImportSpecifierLocal( node, this.startNode(), diff --git a/packages/babel-parser/src/plugins/flow/index.ts b/packages/babel-parser/src/plugins/flow/index.ts index 5ff1a7f0d512..ddb91ba98464 100644 --- a/packages/babel-parser/src/plugins/flow/index.ts +++ b/packages/babel-parser/src/plugins/flow/index.ts @@ -247,10 +247,6 @@ function hasTypeImportKind(node: N.Node): boolean { return node.importKind === "type" || node.importKind === "typeof"; } -function isMaybeDefaultImport(type: TokenType): boolean { - return tokenIsKeywordOrIdentifier(type) && type !== tt._from; -} - const exportSuggestions = { const: "declare export var", let: "declare export var", @@ -2713,14 +2709,6 @@ export default (superClass: typeof Parser) => return node; } - shouldParseDefaultImport(node: N.ImportDeclaration): boolean { - if (!hasTypeImportKind(node)) { - return super.shouldParseDefaultImport(node); - } - - return isMaybeDefaultImport(this.state.type); - } - checkImportReflection(node: Undone) { super.checkImportReflection(node); if (node.module && node.importKind !== "value") { @@ -2746,37 +2734,28 @@ export default (superClass: typeof Parser) => node.specifiers.push(this.finishImportSpecifier(specifier, type)); } - // parse typeof and type imports - maybeParseDefaultImportSpecifier(node: N.ImportDeclaration): boolean { - node.importKind = "value"; - - let kind = null; - if (this.match(tt._typeof)) { - kind = "typeof" as const; - } else if (this.isContextual(tt._type)) { - kind = "type" as const; - } - if (kind) { - const lh = this.lookahead(); - const { type } = lh; - - // import type * is not allowed - if (kind === "type" && type === tt.star) { - // FIXME: lh.start? - this.unexpected(null, lh.type); - } + isPotentialImportPhase(): boolean { + return ( + super.isPotentialImportPhase() || + this.isContextual(tt._type) || + this.isContextual(tt._typeof) + ); + } - if ( - isMaybeDefaultImport(type) || - type === tt.braceL || - type === tt.star - ) { - this.next(); - node.importKind = kind; - } + applyImportPhase( + node: Undone, + phase: string | null, + loc?: Position, + ): void { + super.applyImportPhase(node, phase, loc); + if (phase === "type") { + node.importKind = "type"; + if (this.match(tt.star)) this.unexpected(); + } else if (phase === "typeof") { + node.importKind = "typeof"; + } else { + node.importKind = "value"; } - - return super.maybeParseDefaultImportSpecifier(node); } // parse import-type/typeof shorthand diff --git a/packages/babel-parser/src/plugins/typescript/index.ts b/packages/babel-parser/src/plugins/typescript/index.ts index 1a47e8c62762..c9d3a9a58ca3 100644 --- a/packages/babel-parser/src/plugins/typescript/index.ts +++ b/packages/babel-parser/src/plugins/typescript/index.ts @@ -1971,10 +1971,11 @@ export default (superClass: ClassWithMixin) => tsParseImportEqualsDeclaration( node: Undone, + maybeDefaultIdentifier?: N.Identifier | null, isExport?: boolean, ): N.TsImportEqualsDeclaration { node.isExport = isExport || false; - node.id = this.parseIdentifier(); + node.id = maybeDefaultIdentifier || this.parseIdentifier(); this.checkIdentifier(node.id, BIND_FLAGS_TS_IMPORT); this.expect(tt.eq); const moduleReference = this.tsParseModuleReference(); @@ -2701,41 +2702,60 @@ export default (superClass: ClassWithMixin) => */ checkDuplicateExports() {} + isPotentialImportPhase(): boolean { + return ( + super.isPotentialImportPhase() || + this.isContextual(tt._type) || + this.isContextual(tt._typeof) + ); + } + + applyImportPhase( + node: Undone, + phase: string | null, + loc?: Position, + ): void { + super.applyImportPhase(node, phase, loc); + node.importKind = + phase === "type" || phase === "typeof" ? phase : "value"; + } + parseImport( node: Undone, ): N.AnyImport { - node.importKind = "value"; + if (this.match(tt.string)) { + node.importKind = "value"; + return super.parseImport(node as Undone); + } + + let importNode; if ( - tokenIsIdentifier(this.state.type) || - this.match(tt.star) || - this.match(tt.braceL) + tokenIsIdentifier(this.state.type) && + this.lookaheadCharCode() === charCodes.equalsTo ) { - let ahead = this.lookahead(); - - if ( - this.isContextual(tt._type) && - // import type, { a } from "b"; - ahead.type !== tt.comma && - // import type from "a"; - ahead.type !== tt._from && - // import type = require("a"); - ahead.type !== tt.eq - ) { - node.importKind = "type"; - this.next(); - ahead = this.lookahead(); - } - - if (tokenIsIdentifier(this.state.type) && ahead.type === tt.eq) { + node.importKind = "value"; + return this.tsParseImportEqualsDeclaration( + node as Undone, + ); + } else if (this.isContextual(tt._type)) { + const maybeDefaultIdentifier = this.parseMaybeImportPhase( + node as Undone, + ); + if (this.lookaheadCharCode() === charCodes.equalsTo) { return this.tsParseImportEqualsDeclaration( node as Undone, + maybeDefaultIdentifier, + ); + } else { + importNode = super.parseImportSpecifiersAndAfter( + node as Undone, + maybeDefaultIdentifier, ); } + } else { + importNode = super.parseImport(node as Undone); } - const importNode = super.parseImport(node as Undone); - /*:: invariant(importNode.type !== "TSImportEqualsDeclaration") */ - // `import type` can only be used on imports with named imports or with a // default import - but not both if ( @@ -2760,17 +2780,17 @@ export default (superClass: ClassWithMixin) => if (this.match(tt._import)) { // `export import A = B;` this.next(); // eat `tt._import` - if ( - this.isContextual(tt._type) && - this.lookaheadCharCode() !== charCodes.equalsTo - ) { - node.importKind = "type"; - this.next(); // eat "type" + let maybeDefaultIdentifier: N.Identifier | null = null; + if (this.isContextual(tt._type)) { + maybeDefaultIdentifier = this.parseMaybeImportPhase( + node as Undone, + ); } else { node.importKind = "value"; } return this.tsParseImportEqualsDeclaration( node as Undone, + maybeDefaultIdentifier, /* isExport */ true, ); } else if (this.eat(tt.eq)) { diff --git a/packages/babel-parser/test/fixtures/experimental/import-reflection/invalid-flow-type-import-2/options.json b/packages/babel-parser/test/fixtures/experimental/import-reflection/invalid-flow-type-import-2/options.json index 2dd1ff29f752..2360f761dd93 100644 --- a/packages/babel-parser/test/fixtures/experimental/import-reflection/invalid-flow-type-import-2/options.json +++ b/packages/babel-parser/test/fixtures/experimental/import-reflection/invalid-flow-type-import-2/options.json @@ -1,4 +1,8 @@ { - "plugins": ["importReflection", "flow"], - "sourceType": "module" + "plugins": [ + "importReflection", + "flow" + ], + "sourceType": "module", + "throws": "Unexpected token, expected \"from\" (1:19)" } diff --git a/packages/babel-parser/test/fixtures/experimental/import-reflection/invalid-flow-type-import-2/output.json b/packages/babel-parser/test/fixtures/experimental/import-reflection/invalid-flow-type-import-2/output.json deleted file mode 100644 index 1e45993a703d..000000000000 --- a/packages/babel-parser/test/fixtures/experimental/import-reflection/invalid-flow-type-import-2/output.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "type": "File", - "start":0,"end":44,"loc":{"start":{"line":1,"column":0,"index":0},"end":{"line":1,"column":44,"index":44}}, - "errors": [ - "SyntaxError: An `import module` declaration can not use `type` or `typeof` keyword. (1:19)" - ], - "program": { - "type": "Program", - "start":0,"end":44,"loc":{"start":{"line":1,"column":0,"index":0},"end":{"line":1,"column":44,"index":44}}, - "sourceType": "module", - "interpreter": null, - "body": [ - { - "type": "ImportDeclaration", - "start":0,"end":44,"loc":{"start":{"line":1,"column":0,"index":0},"end":{"line":1,"column":44,"index":44}}, - "specifiers": [ - { - "type": "ImportDefaultSpecifier", - "start":19,"end":22,"loc":{"start":{"line":1,"column":19,"index":19},"end":{"line":1,"column":22,"index":22}}, - "local": { - "type": "Identifier", - "start":19,"end":22,"loc":{"start":{"line":1,"column":19,"index":19},"end":{"line":1,"column":22,"index":22},"identifierName":"foo"}, - "name": "foo" - } - } - ], - "module": true, - "importKind": "type", - "source": { - "type": "StringLiteral", - "start":28,"end":43,"loc":{"start":{"line":1,"column":28,"index":28},"end":{"line":1,"column":43,"index":43}}, - "extra": { - "rawValue": "./module.wasm", - "raw": "\"./module.wasm\"" - }, - "value": "./module.wasm" - } - } - ], - "directives": [] - } -} diff --git a/packages/babel-parser/test/fixtures/experimental/import-reflection/invalid-flow-typeof-import-2/options.json b/packages/babel-parser/test/fixtures/experimental/import-reflection/invalid-flow-typeof-import-2/options.json index 2dd1ff29f752..a47f856f23a1 100644 --- a/packages/babel-parser/test/fixtures/experimental/import-reflection/invalid-flow-typeof-import-2/options.json +++ b/packages/babel-parser/test/fixtures/experimental/import-reflection/invalid-flow-typeof-import-2/options.json @@ -1,4 +1,8 @@ { - "plugins": ["importReflection", "flow"], - "sourceType": "module" + "plugins": [ + "importReflection", + "flow" + ], + "sourceType": "module", + "throws": "Unexpected token, expected \"{\" (1:14)" } diff --git a/packages/babel-parser/test/fixtures/experimental/import-reflection/invalid-flow-typeof-import-2/output.json b/packages/babel-parser/test/fixtures/experimental/import-reflection/invalid-flow-typeof-import-2/output.json deleted file mode 100644 index fffe18921787..000000000000 --- a/packages/babel-parser/test/fixtures/experimental/import-reflection/invalid-flow-typeof-import-2/output.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "type": "File", - "start":0,"end":46,"loc":{"start":{"line":1,"column":0,"index":0},"end":{"line":1,"column":46,"index":46}}, - "errors": [ - "SyntaxError: An `import module` declaration can not use `type` or `typeof` keyword. (1:21)" - ], - "program": { - "type": "Program", - "start":0,"end":46,"loc":{"start":{"line":1,"column":0,"index":0},"end":{"line":1,"column":46,"index":46}}, - "sourceType": "module", - "interpreter": null, - "body": [ - { - "type": "ImportDeclaration", - "start":0,"end":46,"loc":{"start":{"line":1,"column":0,"index":0},"end":{"line":1,"column":46,"index":46}}, - "specifiers": [ - { - "type": "ImportDefaultSpecifier", - "start":21,"end":24,"loc":{"start":{"line":1,"column":21,"index":21},"end":{"line":1,"column":24,"index":24}}, - "local": { - "type": "Identifier", - "start":21,"end":24,"loc":{"start":{"line":1,"column":21,"index":21},"end":{"line":1,"column":24,"index":24},"identifierName":"foo"}, - "name": "foo" - } - } - ], - "module": true, - "importKind": "typeof", - "source": { - "type": "StringLiteral", - "start":30,"end":45,"loc":{"start":{"line":1,"column":30,"index":30},"end":{"line":1,"column":45,"index":45}}, - "extra": { - "rawValue": "./module.wasm", - "raw": "\"./module.wasm\"" - }, - "value": "./module.wasm" - } - } - ], - "directives": [] - } -} diff --git a/packages/babel-parser/test/fixtures/experimental/import-reflection/invalid-ts-type-import-1/options.json b/packages/babel-parser/test/fixtures/experimental/import-reflection/invalid-ts-type-import-1/options.json index 43d760edb3d7..d2768d790303 100644 --- a/packages/babel-parser/test/fixtures/experimental/import-reflection/invalid-ts-type-import-1/options.json +++ b/packages/babel-parser/test/fixtures/experimental/import-reflection/invalid-ts-type-import-1/options.json @@ -1,4 +1,8 @@ { - "plugins": ["importReflection", "typescript"], - "sourceType": "module" + "plugins": [ + "importReflection", + "typescript" + ], + "sourceType": "module", + "throws": "Unexpected token, expected \"from\" (1:19)" } diff --git a/packages/babel-parser/test/fixtures/experimental/import-reflection/invalid-ts-type-import-1/output.json b/packages/babel-parser/test/fixtures/experimental/import-reflection/invalid-ts-type-import-1/output.json deleted file mode 100644 index c443420a3c87..000000000000 --- a/packages/babel-parser/test/fixtures/experimental/import-reflection/invalid-ts-type-import-1/output.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "type": "File", - "start":0,"end":44,"loc":{"start":{"line":1,"column":0,"index":0},"end":{"line":1,"column":44,"index":44}}, - "errors": [ - "SyntaxError: An `import module` declaration can not use `type` modifier (1:19)" - ], - "program": { - "type": "Program", - "start":0,"end":44,"loc":{"start":{"line":1,"column":0,"index":0},"end":{"line":1,"column":44,"index":44}}, - "sourceType": "module", - "interpreter": null, - "body": [ - { - "type": "ImportDeclaration", - "start":0,"end":44,"loc":{"start":{"line":1,"column":0,"index":0},"end":{"line":1,"column":44,"index":44}}, - "importKind": "type", - "specifiers": [ - { - "type": "ImportDefaultSpecifier", - "start":19,"end":22,"loc":{"start":{"line":1,"column":19,"index":19},"end":{"line":1,"column":22,"index":22}}, - "local": { - "type": "Identifier", - "start":19,"end":22,"loc":{"start":{"line":1,"column":19,"index":19},"end":{"line":1,"column":22,"index":22},"identifierName":"foo"}, - "name": "foo" - } - } - ], - "module": true, - "source": { - "type": "StringLiteral", - "start":28,"end":43,"loc":{"start":{"line":1,"column":28,"index":28},"end":{"line":1,"column":43,"index":43}}, - "extra": { - "rawValue": "./module.wasm", - "raw": "\"./module.wasm\"" - }, - "value": "./module.wasm" - } - } - ], - "directives": [] - } -} diff --git a/packages/babel-parser/test/fixtures/flow/type-imports/invalid-import-type-namespace/options.json b/packages/babel-parser/test/fixtures/flow/type-imports/invalid-import-type-namespace/options.json index ac0cbbd262eb..a740d0c03e4f 100644 --- a/packages/babel-parser/test/fixtures/flow/type-imports/invalid-import-type-namespace/options.json +++ b/packages/babel-parser/test/fixtures/flow/type-imports/invalid-import-type-namespace/options.json @@ -4,5 +4,5 @@ "jsx", "flow" ], - "throws": "Unexpected token, expected \"*\" (1:7)" -} \ No newline at end of file + "throws": "Unexpected token (1:12)" +} From d7af687d33b7fd8f7546618b36a3f310cf4af043 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Ribaudo?= Date: Tue, 16 May 2023 16:40:22 +0200 Subject: [PATCH 2/8] Also use it for exports --- packages/babel-parser/src/parser/statement.ts | 104 ++++++++++++------ .../babel-parser/src/plugins/flow/index.ts | 53 ++++----- .../src/plugins/typescript/index.ts | 67 +++++++---- 3 files changed, 143 insertions(+), 81 deletions(-) diff --git a/packages/babel-parser/src/parser/statement.ts b/packages/babel-parser/src/parser/statement.ts index afc6f1778921..34827d845bcb 100644 --- a/packages/babel-parser/src/parser/statement.ts +++ b/packages/babel-parser/src/parser/statement.ts @@ -2345,9 +2345,13 @@ export default abstract class StatementParser extends ExpressionParser { >, decorators: N.Decorator[] | null, ): N.AnyExport { + const maybeDefaultIdentifier = this.parseMaybeImportPhase( + node, + /* isExport */ true, + ); const hasDefault = this.maybeParseExportDefaultSpecifier( - // @ts-expect-error todo(flow->ts) node, + maybeDefaultIdentifier, ); const parseAfterDefault = !hasDefault || this.eat(tt.comma); const hasStar = @@ -2441,13 +2445,23 @@ export default abstract class StatementParser extends ExpressionParser { return this.eat(tt.star); } - maybeParseExportDefaultSpecifier(node: N.Node): boolean { - if (this.isExportDefaultSpecifier()) { + maybeParseExportDefaultSpecifier( + node: Undone< + | N.ExportDefaultDeclaration + | N.ExportAllDeclaration + | N.ExportNamedDeclaration + >, + maybeDefaultIdentifier: N.Identifier | null, + ): node is Undone { + if (maybeDefaultIdentifier || this.isExportDefaultSpecifier()) { // export defaultObj ... - this.expectPlugin("exportDefaultFrom"); - const specifier = this.startNode(); - specifier.exported = this.parseIdentifier(true); - node.specifiers = [this.finishNode(specifier, "ExportDefaultSpecifier")]; + this.expectPlugin("exportDefaultFrom", maybeDefaultIdentifier?.loc.start); + const id = maybeDefaultIdentifier || this.parseIdentifier(true); + const specifier = this.startNodeAtNode(id); + specifier.exported = id; + (node as Undone).specifiers = [ + this.finishNode(specifier, "ExportDefaultSpecifier"), + ]; return true; } return false; @@ -2918,20 +2932,25 @@ export default abstract class StatementParser extends ExpressionParser { } } - isPotentialImportPhase(): boolean { - return this.isContextual(tt._module); + isPotentialImportPhase(isExport: boolean): boolean { + return !isExport && this.isContextual(tt._module); } applyImportPhase( - node: Undone, + node: Undone, + isExport: boolean, phase: string | null, loc?: Position, ): void { + if (isExport) { + // This will never happen + return; + } if (phase === "module") { this.expectPlugin("importReflection", loc); - node.module = true; + (node as N.ImportDeclaration).module = true; } else if (this.hasPlugin("importReflection")) { - node.module = false; + (node as N.ImportDeclaration).module = false; } } @@ -2951,44 +2970,61 @@ export default abstract class StatementParser extends ExpressionParser { */ parseMaybeImportPhase( node: Undone, + isExport: boolean, ): N.Identifier | null { - if (!this.isPotentialImportPhase()) { - this.applyImportPhase(node as Undone, null); + if (!this.isPotentialImportPhase(isExport)) { + this.applyImportPhase( + node as Undone, + isExport, + null, + ); return null; } const phaseIdentifier = this.parseIdentifier(true); - const { type } = this.state; - const isImportPhase = tokenIsIdentifier(type) - ? // `import x` - type !== tt._from || - // `import module from from ...` - // We can safely assume that `f` is `from` because otherwise it - // will be a syntax error anyway. This will change with the - // module declarations proposal, and we will neet to disambiguate - // after parsing a second identifier. - this.lookaheadCharCode() === charCodes.lowercaseF - : // import { x } ... - // import "foo" - // The second one is invalid, we will continue parsing and throw - // a recoverable error later - // tt.eq is needed for TS `import type = require(...)`. - type !== tt.comma && type !== tt.eq; - - if (isImportPhase) { + + if (this.isPrecedingIdImportPhase(phaseIdentifier.name)) { this.applyImportPhase( node as Undone, + isExport, phaseIdentifier.name, phaseIdentifier.loc.start, ); return null; } else { - this.applyImportPhase(node as Undone, null); + this.applyImportPhase( + node as Undone, + isExport, + null, + ); // `` is a default binding, return it to the main import declaration parser return phaseIdentifier; } } + isPrecedingIdImportPhase( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + phase: string, + ) { + const { type } = this.state; + return tokenIsIdentifier(type) + ? // OK: import x from "foo"; + // OK: import from from "foo"; + // NO: import from "foo"; + // NO: import from 'foo'; + // With the module declarations proposals, we will need further disambiguation + // for `import module from from;`. + type !== tt._from || this.lookaheadCharCode() === charCodes.lowercaseF + : // OK: import { x } from "foo"; + // OK: import x from "foo"; + // OK: import * as T from "foo"; + // NO: import from "foo"; + // OK: import "foo"; + // The last one is invalid, we will continue parsing and throw + // an error later + type !== tt.comma; + } + // Parses import declaration. // https://tc39.es/ecma262/#prod-ImportDeclaration @@ -3000,7 +3036,7 @@ export default abstract class StatementParser extends ExpressionParser { return this.parseImportSpecifiersAndAfter( node, - this.parseMaybeImportPhase(node), + this.parseMaybeImportPhase(node, /* isExport */ false), ); } diff --git a/packages/babel-parser/src/plugins/flow/index.ts b/packages/babel-parser/src/plugins/flow/index.ts index ddb91ba98464..0b6233723795 100644 --- a/packages/babel-parser/src/plugins/flow/index.ts +++ b/packages/babel-parser/src/plugins/flow/index.ts @@ -2226,20 +2226,6 @@ export default (superClass: typeof Parser) => super.assertModuleNodeAllowed(node); } - parseExport( - node: Undone, - decorators: N.Decorator[] | null, - ): N.AnyExport { - const decl = super.parseExport(node, decorators); - if ( - decl.type === "ExportNamedDeclaration" || - decl.type === "ExportAllDeclaration" - ) { - decl.exportKind = decl.exportKind || "value"; - } - return decl; - } - parseExportDeclaration( node: N.ExportNamedDeclaration, ): N.Declaration | undefined | null { @@ -2734,27 +2720,42 @@ export default (superClass: typeof Parser) => node.specifiers.push(this.finishImportSpecifier(specifier, type)); } - isPotentialImportPhase(): boolean { + isPotentialImportPhase(isExport: boolean): boolean { + if (super.isPotentialImportPhase(isExport)) return true; + if (this.isContextual(tt._type)) { + if (!isExport) return true; + const ch = this.lookaheadCharCode(); + return ch === charCodes.leftCurlyBrace || ch === charCodes.asterisk; + } + return !isExport && this.isContextual(tt._typeof); + } + + isPrecedingIdImportPhase(phase: string): boolean { return ( - super.isPotentialImportPhase() || - this.isContextual(tt._type) || - this.isContextual(tt._typeof) + super.isPrecedingIdImportPhase(phase) && + // `export type x = ...` + (phase !== "type" || this.lookaheadCharCode() !== tt.eq) ); } applyImportPhase( - node: Undone, + node: Undone, + isExport: boolean, phase: string | null, loc?: Position, ): void { - super.applyImportPhase(node, phase, loc); - if (phase === "type") { - node.importKind = "type"; - if (this.match(tt.star)) this.unexpected(); - } else if (phase === "typeof") { - node.importKind = "typeof"; + super.applyImportPhase(node, isExport, phase, loc); + if (isExport) { + if (!phase && this.match(tt._default)) { + // TODO: Align with our TS AST and always add .exportKind + return; + } + (node as N.ExportNamedDeclaration).exportKind = + phase === "type" ? phase : "value"; } else { - node.importKind = "value"; + if (phase === "type" && this.match(tt.star)) this.unexpected(); + (node as N.ImportDeclaration).importKind = + phase === "type" || phase === "typeof" ? phase : "value"; } } diff --git a/packages/babel-parser/src/plugins/typescript/index.ts b/packages/babel-parser/src/plugins/typescript/index.ts index c9d3a9a58ca3..3643bee68492 100644 --- a/packages/babel-parser/src/plugins/typescript/index.ts +++ b/packages/babel-parser/src/plugins/typescript/index.ts @@ -2702,22 +2702,51 @@ export default (superClass: ClassWithMixin) => */ checkDuplicateExports() {} - isPotentialImportPhase(): boolean { - return ( - super.isPotentialImportPhase() || - this.isContextual(tt._type) || - this.isContextual(tt._typeof) - ); + isPotentialImportPhase(isExport: boolean): boolean { + if (super.isPotentialImportPhase(isExport)) return true; + if (this.isContextual(tt._type)) { + if (!isExport) return true; + const ch = this.lookaheadCharCode(); + return ch === charCodes.leftCurlyBrace || ch === charCodes.asterisk; + } + return !isExport && this.isContextual(tt._typeof); + } + + isPrecedingIdImportPhase(phase: string): boolean { + if (phase !== "type") return super.isPrecedingIdImportPhase(phase); + const { type } = this.state; + return tokenIsIdentifier(type) + ? // OK: import type x from "foo"; + // OK: import type from from "foo"; + // NO: import type from "foo"; + // NO: import type from 'foo'; + // OK: import type x = require("foo"); + // OK: import type from = require("foo"); + type !== tt._from || + this.lookaheadCharCode() === charCodes.lowercaseF || + this.lookaheadCharCode() === tt.eq + : // OK: import type { x } from "foo"; + // OK: import type x from "foo"; + // OK: import type * as T from "foo"; + // NO: import type, {} from "foo"; + // NO: import type = require("foo"); + type !== tt.comma && type !== tt.eq; } applyImportPhase( - node: Undone, + node: Undone, + isExport: boolean, phase: string | null, loc?: Position, ): void { - super.applyImportPhase(node, phase, loc); - node.importKind = - phase === "type" || phase === "typeof" ? phase : "value"; + super.applyImportPhase(node, isExport, phase, loc); + if (isExport) { + (node as N.ExportNamedDeclaration).exportKind = + phase === "type" ? "type" : "value"; + } else { + (node as N.ImportDeclaration).importKind = + phase === "type" || phase === "typeof" ? phase : "value"; + } } parseImport( @@ -2740,6 +2769,7 @@ export default (superClass: ClassWithMixin) => } else if (this.isContextual(tt._type)) { const maybeDefaultIdentifier = this.parseMaybeImportPhase( node as Undone, + /* isExport */ false, ); if (this.lookaheadCharCode() === charCodes.equalsTo) { return this.tsParseImportEqualsDeclaration( @@ -2781,9 +2811,14 @@ export default (superClass: ClassWithMixin) => // `export import A = B;` this.next(); // eat `tt._import` let maybeDefaultIdentifier: N.Identifier | null = null; - if (this.isContextual(tt._type)) { + if ( + this.isContextual(tt._type) && + // We pass false here, because we are parsing an `import ... =` + this.isPotentialImportPhase(/* isExport */ false) + ) { maybeDefaultIdentifier = this.parseMaybeImportPhase( node as Undone, + /* isExport */ false, ); } else { node.importKind = "value"; @@ -2808,16 +2843,6 @@ export default (superClass: ClassWithMixin) => this.semicolon(); return this.finishNode(decl, "TSNamespaceExportDeclaration"); } else { - node.exportKind = "value"; - - if (this.isContextual(tt._type)) { - const ch = this.lookaheadCharCode(); - if (ch === charCodes.leftCurlyBrace || ch === charCodes.asterisk) { - this.next(); - node.exportKind = "type"; - } - } - return super.parseExport( node as Undone, decorators, From 6a4789a184d8fe68fe02d21d960677ab550c6099 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Ribaudo?= Date: Tue, 16 May 2023 20:18:33 +0200 Subject: [PATCH 3/8] Inline `isPrecedingIdImportPhase` --- packages/babel-parser/src/parser/statement.ts | 20 +++++++++++++- .../babel-parser/src/plugins/flow/index.ts | 8 ------ .../src/plugins/typescript/index.ts | 26 +++---------------- 3 files changed, 22 insertions(+), 32 deletions(-) diff --git a/packages/babel-parser/src/parser/statement.ts b/packages/babel-parser/src/parser/statement.ts index 34827d845bcb..1936820444da 100644 --- a/packages/babel-parser/src/parser/statement.ts +++ b/packages/babel-parser/src/parser/statement.ts @@ -2983,7 +2983,25 @@ export default abstract class StatementParser extends ExpressionParser { const phaseIdentifier = this.parseIdentifier(true); - if (this.isPrecedingIdImportPhase(phaseIdentifier.name)) { + const { type } = this.state; + const isImportPhase = tokenIsIdentifier(type) + ? // OK: import x from "foo"; + // OK: import from from "foo"; + // NO: import from "foo"; + // NO: import from 'foo'; + // With the module declarations proposals, we will need further disambiguation + // for `import module from from;`. + type !== tt._from || this.lookaheadCharCode() === charCodes.lowercaseF + : // OK: import { x } from "foo"; + // OK: import x from "foo"; + // OK: import * as T from "foo"; + // NO: import from "foo"; + // OK: import "foo"; + // The last one is invalid, we will continue parsing and throw + // an error later + type !== tt.comma; + + if (isImportPhase) { this.applyImportPhase( node as Undone, isExport, diff --git a/packages/babel-parser/src/plugins/flow/index.ts b/packages/babel-parser/src/plugins/flow/index.ts index 0b6233723795..c912c0bc0277 100644 --- a/packages/babel-parser/src/plugins/flow/index.ts +++ b/packages/babel-parser/src/plugins/flow/index.ts @@ -2730,14 +2730,6 @@ export default (superClass: typeof Parser) => return !isExport && this.isContextual(tt._typeof); } - isPrecedingIdImportPhase(phase: string): boolean { - return ( - super.isPrecedingIdImportPhase(phase) && - // `export type x = ...` - (phase !== "type" || this.lookaheadCharCode() !== tt.eq) - ); - } - applyImportPhase( node: Undone, isExport: boolean, diff --git a/packages/babel-parser/src/plugins/typescript/index.ts b/packages/babel-parser/src/plugins/typescript/index.ts index 3643bee68492..66fe6e360459 100644 --- a/packages/babel-parser/src/plugins/typescript/index.ts +++ b/packages/babel-parser/src/plugins/typescript/index.ts @@ -2705,34 +2705,14 @@ export default (superClass: ClassWithMixin) => isPotentialImportPhase(isExport: boolean): boolean { if (super.isPotentialImportPhase(isExport)) return true; if (this.isContextual(tt._type)) { - if (!isExport) return true; const ch = this.lookaheadCharCode(); - return ch === charCodes.leftCurlyBrace || ch === charCodes.asterisk; + return isExport + ? ch === charCodes.leftCurlyBrace || ch === charCodes.asterisk + : ch !== charCodes.equalsTo; } return !isExport && this.isContextual(tt._typeof); } - isPrecedingIdImportPhase(phase: string): boolean { - if (phase !== "type") return super.isPrecedingIdImportPhase(phase); - const { type } = this.state; - return tokenIsIdentifier(type) - ? // OK: import type x from "foo"; - // OK: import type from from "foo"; - // NO: import type from "foo"; - // NO: import type from 'foo'; - // OK: import type x = require("foo"); - // OK: import type from = require("foo"); - type !== tt._from || - this.lookaheadCharCode() === charCodes.lowercaseF || - this.lookaheadCharCode() === tt.eq - : // OK: import type { x } from "foo"; - // OK: import type x from "foo"; - // OK: import type * as T from "foo"; - // NO: import type, {} from "foo"; - // NO: import type = require("foo"); - type !== tt.comma && type !== tt.eq; - } - applyImportPhase( node: Undone, isExport: boolean, From 88dda08397f77053cd69ceec94cd65295919263d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Ribaudo?= Date: Tue, 16 May 2023 20:32:39 +0200 Subject: [PATCH 4/8] Fix tscheck --- .../babel-parser/src/plugins/placeholders.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/babel-parser/src/plugins/placeholders.ts b/packages/babel-parser/src/plugins/placeholders.ts index c030877fffcd..4d460d40d422 100644 --- a/packages/babel-parser/src/plugins/placeholders.ts +++ b/packages/babel-parser/src/plugins/placeholders.ts @@ -302,12 +302,25 @@ export default (superClass: typeof Parser) => return super.isExportDefaultSpecifier(); } - maybeParseExportDefaultSpecifier(node: N.Node): boolean { - if (node.specifiers && node.specifiers.length > 0) { + maybeParseExportDefaultSpecifier( + node: Undone< + | N.ExportDefaultDeclaration + | N.ExportAllDeclaration + | N.ExportNamedDeclaration + >, + maybeDefaultIdentifier: N.Identifier | null, + ): node is Undone { + if ( + (node as N.ExportNamedDeclaration).specifiers && + (node as N.ExportNamedDeclaration).specifiers.length > 0 + ) { // "export %%NAME%%" has already been parsed by #parseExport. return true; } - return super.maybeParseExportDefaultSpecifier(node); + return super.maybeParseExportDefaultSpecifier( + node, + maybeDefaultIdentifier, + ); } checkExport(node: N.ExportNamedDeclaration): void { From 62868ff2f903fb4298daff08f05ed7e04160982a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Ribaudo?= Date: Wed, 17 May 2023 11:02:19 +0300 Subject: [PATCH 5/8] Fix comments attachment --- packages/babel-parser/src/parser/comments.ts | 34 ++++++ packages/babel-parser/src/parser/statement.ts | 1 + .../import-reflection/comments/input.mjs | 1 + .../import-reflection/comments/output.json | 112 ++++++++++++++++++ 4 files changed, 148 insertions(+) create mode 100644 packages/babel-parser/test/fixtures/experimental/import-reflection/comments/input.mjs create mode 100644 packages/babel-parser/test/fixtures/experimental/import-reflection/comments/output.json diff --git a/packages/babel-parser/src/parser/comments.ts b/packages/babel-parser/src/parser/comments.ts index 03e2db053db1..a81dfd6d58af 100644 --- a/packages/babel-parser/src/parser/comments.ts +++ b/packages/babel-parser/src/parser/comments.ts @@ -246,6 +246,40 @@ export default class CommentsParser extends BaseParser { } } + /* eslint-disable no-irregular-whitespace */ + /** + * Reset previous node leading comments. Used in import phase modifiers + * parsing. We parse `module` in `import module foo from ...` as an + * identifier but may reinterpret it into a phase modifier later. In this + * case the identifier is not part of the AST and we should sync the + * knowledge to commentStacks + * + * For example, when parsing + * ``` + * import /* 1 *​/ module a from "a"; + * ``` + * the comment whitespace `/* 1 *​/` has trailing node Identifier(module). When + * we see that `module` is not a default import binding, we mark `/* 1 *​/` as + * inner comments of the ImportDeclaration. So `/* 1 *​/` should be detached from + * the Identifier node. + * + * @param node the last finished AST node _before_ current token + */ + /* eslint-enable no-irregular-whitespace */ + resetPreviousNodeLeadingComments(node: Node) { + const { commentStack } = this.state; + const { length } = commentStack; + if (length === 0) return; + + for (let i = length - 1; i >= 0; i--) { + const commentWS = commentStack[i]; + if (commentWS.trailingNode === node) { + commentWS.trailingNode = null; + } + if (commentWS.end < node.start) break; + } + } + /** * Attach a node to the comment whitespaces right before/after * the given range. diff --git a/packages/babel-parser/src/parser/statement.ts b/packages/babel-parser/src/parser/statement.ts index 1936820444da..87afb26c6a21 100644 --- a/packages/babel-parser/src/parser/statement.ts +++ b/packages/babel-parser/src/parser/statement.ts @@ -3002,6 +3002,7 @@ export default abstract class StatementParser extends ExpressionParser { type !== tt.comma; if (isImportPhase) { + this.resetPreviousNodeLeadingComments(phaseIdentifier); this.applyImportPhase( node as Undone, isExport, diff --git a/packages/babel-parser/test/fixtures/experimental/import-reflection/comments/input.mjs b/packages/babel-parser/test/fixtures/experimental/import-reflection/comments/input.mjs new file mode 100644 index 000000000000..9726c2258813 --- /dev/null +++ b/packages/babel-parser/test/fixtures/experimental/import-reflection/comments/input.mjs @@ -0,0 +1 @@ +/* 0 */ import /* 1 */ module /* 2 */ Foo /* 3 */ from /* 4 */ "foo" /* 5 */; \ No newline at end of file diff --git a/packages/babel-parser/test/fixtures/experimental/import-reflection/comments/output.json b/packages/babel-parser/test/fixtures/experimental/import-reflection/comments/output.json new file mode 100644 index 000000000000..d96d9c5124d9 --- /dev/null +++ b/packages/babel-parser/test/fixtures/experimental/import-reflection/comments/output.json @@ -0,0 +1,112 @@ +{ + "type": "File", + "start":0,"end":77,"loc":{"start":{"line":1,"column":0,"index":0},"end":{"line":1,"column":77,"index":77}}, + "program": { + "type": "Program", + "start":0,"end":77,"loc":{"start":{"line":1,"column":0,"index":0},"end":{"line":1,"column":77,"index":77}}, + "sourceType": "module", + "interpreter": null, + "body": [ + { + "type": "ImportDeclaration", + "start":8,"end":77,"loc":{"start":{"line":1,"column":8,"index":8},"end":{"line":1,"column":77,"index":77}}, + "module": true, + "specifiers": [ + { + "type": "ImportDefaultSpecifier", + "start":38,"end":41,"loc":{"start":{"line":1,"column":38,"index":38},"end":{"line":1,"column":41,"index":41}}, + "local": { + "type": "Identifier", + "start":38,"end":41,"loc":{"start":{"line":1,"column":38,"index":38},"end":{"line":1,"column":41,"index":41},"identifierName":"Foo"}, + "name": "Foo" + }, + "trailingComments": [ + { + "type": "CommentBlock", + "value": " 3 ", + "start":42,"end":49,"loc":{"start":{"line":1,"column":42,"index":42},"end":{"line":1,"column":49,"index":49}} + } + ], + "leadingComments": [ + { + "type": "CommentBlock", + "value": " 2 ", + "start":30,"end":37,"loc":{"start":{"line":1,"column":30,"index":30},"end":{"line":1,"column":37,"index":37}} + } + ] + } + ], + "source": { + "type": "StringLiteral", + "start":63,"end":68,"loc":{"start":{"line":1,"column":63,"index":63},"end":{"line":1,"column":68,"index":68}}, + "extra": { + "rawValue": "foo", + "raw": "\"foo\"" + }, + "value": "foo", + "trailingComments": [ + { + "type": "CommentBlock", + "value": " 5 ", + "start":69,"end":76,"loc":{"start":{"line":1,"column":69,"index":69},"end":{"line":1,"column":76,"index":76}} + } + ], + "leadingComments": [ + { + "type": "CommentBlock", + "value": " 4 ", + "start":55,"end":62,"loc":{"start":{"line":1,"column":55,"index":55},"end":{"line":1,"column":62,"index":62}} + } + ] + }, + "innerComments": [ + { + "type": "CommentBlock", + "value": " 1 ", + "start":15,"end":22,"loc":{"start":{"line":1,"column":15,"index":15},"end":{"line":1,"column":22,"index":22}} + } + ], + "leadingComments": [ + { + "type": "CommentBlock", + "value": " 0 ", + "start":0,"end":7,"loc":{"start":{"line":1,"column":0,"index":0},"end":{"line":1,"column":7,"index":7}} + } + ] + } + ], + "directives": [] + }, + "comments": [ + { + "type": "CommentBlock", + "value": " 0 ", + "start":0,"end":7,"loc":{"start":{"line":1,"column":0,"index":0},"end":{"line":1,"column":7,"index":7}} + }, + { + "type": "CommentBlock", + "value": " 1 ", + "start":15,"end":22,"loc":{"start":{"line":1,"column":15,"index":15},"end":{"line":1,"column":22,"index":22}} + }, + { + "type": "CommentBlock", + "value": " 2 ", + "start":30,"end":37,"loc":{"start":{"line":1,"column":30,"index":30},"end":{"line":1,"column":37,"index":37}} + }, + { + "type": "CommentBlock", + "value": " 3 ", + "start":42,"end":49,"loc":{"start":{"line":1,"column":42,"index":42},"end":{"line":1,"column":49,"index":49}} + }, + { + "type": "CommentBlock", + "value": " 4 ", + "start":55,"end":62,"loc":{"start":{"line":1,"column":55,"index":55},"end":{"line":1,"column":62,"index":62}} + }, + { + "type": "CommentBlock", + "value": " 5 ", + "start":69,"end":76,"loc":{"start":{"line":1,"column":69,"index":69},"end":{"line":1,"column":76,"index":76}} + } + ] +} From fc765b841503f563ccb36113d1852e1f8c5b8e5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Ribaudo?= Date: Wed, 17 May 2023 11:32:24 +0300 Subject: [PATCH 6/8] Fix parsing of `import type ` with flow --- packages/babel-parser/src/parser/statement.ts | 8 +++- .../es2015/uncategorised/96/options.json | 3 +- .../es2015/uncategorised/96/output.json | 40 +++++++++++++++++++ .../invalid-import-default/options.json | 3 -- .../invalid-import-default/output.json | 40 +++++++++++++++++++ .../invalid-flow-typeof-import-2/options.json | 2 +- .../flow/imports/import-type-keyword/input.js | 1 + .../imports/import-type-keyword/output.json | 38 ++++++++++++++++++ 8 files changed, 127 insertions(+), 8 deletions(-) create mode 100644 packages/babel-parser/test/fixtures/es2015/uncategorised/96/output.json delete mode 100644 packages/babel-parser/test/fixtures/esprima/es2015-import-declaration/invalid-import-default/options.json create mode 100644 packages/babel-parser/test/fixtures/esprima/es2015-import-declaration/invalid-import-default/output.json create mode 100644 packages/babel-parser/test/fixtures/flow/imports/import-type-keyword/input.js create mode 100644 packages/babel-parser/test/fixtures/flow/imports/import-type-keyword/output.json diff --git a/packages/babel-parser/src/parser/statement.ts b/packages/babel-parser/src/parser/statement.ts index 87afb26c6a21..2e2a24a89e6b 100644 --- a/packages/babel-parser/src/parser/statement.ts +++ b/packages/babel-parser/src/parser/statement.ts @@ -1,6 +1,7 @@ import type * as N from "../types"; import { tokenIsIdentifier, + tokenIsKeywordOrIdentifier, tokenIsLoop, tokenIsTemplate, tt, @@ -2984,7 +2985,7 @@ export default abstract class StatementParser extends ExpressionParser { const phaseIdentifier = this.parseIdentifier(true); const { type } = this.state; - const isImportPhase = tokenIsIdentifier(type) + const isImportPhase = tokenIsKeywordOrIdentifier(type) ? // OK: import x from "foo"; // OK: import from from "foo"; // NO: import from "foo"; @@ -3302,7 +3303,10 @@ export default abstract class StatementParser extends ExpressionParser { this.finishImportSpecifier(specifier, "ImportDefaultSpecifier"), ); return true; - } else if (tokenIsIdentifier(this.state.type)) { + } else if ( + // We allow keywords, and parseImportSpecifierLocal will report a recoverable error + tokenIsKeywordOrIdentifier(this.state.type) + ) { this.parseImportSpecifierLocal( node, this.startNode(), diff --git a/packages/babel-parser/test/fixtures/es2015/uncategorised/96/options.json b/packages/babel-parser/test/fixtures/es2015/uncategorised/96/options.json index 9b76c337e1c3..2104ca43283f 100644 --- a/packages/babel-parser/test/fixtures/es2015/uncategorised/96/options.json +++ b/packages/babel-parser/test/fixtures/es2015/uncategorised/96/options.json @@ -1,4 +1,3 @@ { - "sourceType": "module", - "throws": "Unexpected token, expected \"{\" (1:7)" + "sourceType": "module" } diff --git a/packages/babel-parser/test/fixtures/es2015/uncategorised/96/output.json b/packages/babel-parser/test/fixtures/es2015/uncategorised/96/output.json new file mode 100644 index 000000000000..494c6112d903 --- /dev/null +++ b/packages/babel-parser/test/fixtures/es2015/uncategorised/96/output.json @@ -0,0 +1,40 @@ +{ + "type": "File", + "start":0,"end":25,"loc":{"start":{"line":1,"column":0,"index":0},"end":{"line":1,"column":25,"index":25}}, + "errors": [ + "SyntaxError: Unexpected keyword 'default'. (1:7)" + ], + "program": { + "type": "Program", + "start":0,"end":25,"loc":{"start":{"line":1,"column":0,"index":0},"end":{"line":1,"column":25,"index":25}}, + "sourceType": "module", + "interpreter": null, + "body": [ + { + "type": "ImportDeclaration", + "start":0,"end":25,"loc":{"start":{"line":1,"column":0,"index":0},"end":{"line":1,"column":25,"index":25}}, + "specifiers": [ + { + "type": "ImportDefaultSpecifier", + "start":7,"end":14,"loc":{"start":{"line":1,"column":7,"index":7},"end":{"line":1,"column":14,"index":14}}, + "local": { + "type": "Identifier", + "start":7,"end":14,"loc":{"start":{"line":1,"column":7,"index":7},"end":{"line":1,"column":14,"index":14},"identifierName":"default"}, + "name": "default" + } + } + ], + "source": { + "type": "StringLiteral", + "start":20,"end":25,"loc":{"start":{"line":1,"column":20,"index":20},"end":{"line":1,"column":25,"index":25}}, + "extra": { + "rawValue": "foo", + "raw": "\"foo\"" + }, + "value": "foo" + } + } + ], + "directives": [] + } +} diff --git a/packages/babel-parser/test/fixtures/esprima/es2015-import-declaration/invalid-import-default/options.json b/packages/babel-parser/test/fixtures/esprima/es2015-import-declaration/invalid-import-default/options.json deleted file mode 100644 index f2ecbd9741e7..000000000000 --- a/packages/babel-parser/test/fixtures/esprima/es2015-import-declaration/invalid-import-default/options.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "throws": "Unexpected token, expected \"{\" (1:7)" -} diff --git a/packages/babel-parser/test/fixtures/esprima/es2015-import-declaration/invalid-import-default/output.json b/packages/babel-parser/test/fixtures/esprima/es2015-import-declaration/invalid-import-default/output.json new file mode 100644 index 000000000000..494c6112d903 --- /dev/null +++ b/packages/babel-parser/test/fixtures/esprima/es2015-import-declaration/invalid-import-default/output.json @@ -0,0 +1,40 @@ +{ + "type": "File", + "start":0,"end":25,"loc":{"start":{"line":1,"column":0,"index":0},"end":{"line":1,"column":25,"index":25}}, + "errors": [ + "SyntaxError: Unexpected keyword 'default'. (1:7)" + ], + "program": { + "type": "Program", + "start":0,"end":25,"loc":{"start":{"line":1,"column":0,"index":0},"end":{"line":1,"column":25,"index":25}}, + "sourceType": "module", + "interpreter": null, + "body": [ + { + "type": "ImportDeclaration", + "start":0,"end":25,"loc":{"start":{"line":1,"column":0,"index":0},"end":{"line":1,"column":25,"index":25}}, + "specifiers": [ + { + "type": "ImportDefaultSpecifier", + "start":7,"end":14,"loc":{"start":{"line":1,"column":7,"index":7},"end":{"line":1,"column":14,"index":14}}, + "local": { + "type": "Identifier", + "start":7,"end":14,"loc":{"start":{"line":1,"column":7,"index":7},"end":{"line":1,"column":14,"index":14},"identifierName":"default"}, + "name": "default" + } + } + ], + "source": { + "type": "StringLiteral", + "start":20,"end":25,"loc":{"start":{"line":1,"column":20,"index":20},"end":{"line":1,"column":25,"index":25}}, + "extra": { + "rawValue": "foo", + "raw": "\"foo\"" + }, + "value": "foo" + } + } + ], + "directives": [] + } +} diff --git a/packages/babel-parser/test/fixtures/experimental/import-reflection/invalid-flow-typeof-import-2/options.json b/packages/babel-parser/test/fixtures/experimental/import-reflection/invalid-flow-typeof-import-2/options.json index a47f856f23a1..bfd6a7a73ddd 100644 --- a/packages/babel-parser/test/fixtures/experimental/import-reflection/invalid-flow-typeof-import-2/options.json +++ b/packages/babel-parser/test/fixtures/experimental/import-reflection/invalid-flow-typeof-import-2/options.json @@ -4,5 +4,5 @@ "flow" ], "sourceType": "module", - "throws": "Unexpected token, expected \"{\" (1:14)" + "throws": "Unexpected token, expected \"from\" (1:21)" } diff --git a/packages/babel-parser/test/fixtures/flow/imports/import-type-keyword/input.js b/packages/babel-parser/test/fixtures/flow/imports/import-type-keyword/input.js new file mode 100644 index 000000000000..0da6d5f06a16 --- /dev/null +++ b/packages/babel-parser/test/fixtures/flow/imports/import-type-keyword/input.js @@ -0,0 +1 @@ +import type switch from 'foo'; diff --git a/packages/babel-parser/test/fixtures/flow/imports/import-type-keyword/output.json b/packages/babel-parser/test/fixtures/flow/imports/import-type-keyword/output.json new file mode 100644 index 000000000000..17b2d427b043 --- /dev/null +++ b/packages/babel-parser/test/fixtures/flow/imports/import-type-keyword/output.json @@ -0,0 +1,38 @@ +{ + "type": "File", + "start":0,"end":30,"loc":{"start":{"line":1,"column":0,"index":0},"end":{"line":1,"column":30,"index":30}}, + "program": { + "type": "Program", + "start":0,"end":30,"loc":{"start":{"line":1,"column":0,"index":0},"end":{"line":1,"column":30,"index":30}}, + "sourceType": "module", + "interpreter": null, + "body": [ + { + "type": "ImportDeclaration", + "start":0,"end":30,"loc":{"start":{"line":1,"column":0,"index":0},"end":{"line":1,"column":30,"index":30}}, + "importKind": "type", + "specifiers": [ + { + "type": "ImportDefaultSpecifier", + "start":12,"end":18,"loc":{"start":{"line":1,"column":12,"index":12},"end":{"line":1,"column":18,"index":18}}, + "local": { + "type": "Identifier", + "start":12,"end":18,"loc":{"start":{"line":1,"column":12,"index":12},"end":{"line":1,"column":18,"index":18},"identifierName":"switch"}, + "name": "switch" + } + } + ], + "source": { + "type": "StringLiteral", + "start":24,"end":29,"loc":{"start":{"line":1,"column":24,"index":24},"end":{"line":1,"column":29,"index":29}}, + "extra": { + "rawValue": "foo", + "raw": "'foo'" + }, + "value": "foo" + } + } + ], + "directives": [] + } +} From 0cbe9fca42f55493705416edf5d8166e2cc20182 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Ribaudo?= Date: Mon, 22 May 2023 16:48:49 +0200 Subject: [PATCH 7/8] Review --- packages/babel-parser/src/parser/comments.ts | 24 +++++++++---------- packages/babel-parser/src/parser/statement.ts | 2 +- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/packages/babel-parser/src/parser/comments.ts b/packages/babel-parser/src/parser/comments.ts index a81dfd6d58af..44022899d492 100644 --- a/packages/babel-parser/src/parser/comments.ts +++ b/packages/babel-parser/src/parser/comments.ts @@ -1,7 +1,7 @@ /*:: declare var invariant; */ import BaseParser from "./base"; -import type { Comment, Node } from "../types"; +import type { Comment, Node, Identifier } from "../types"; import * as charCodes from "charcodes"; import type { Undone } from "./node"; @@ -248,11 +248,11 @@ export default class CommentsParser extends BaseParser { /* eslint-disable no-irregular-whitespace */ /** - * Reset previous node leading comments. Used in import phase modifiers - * parsing. We parse `module` in `import module foo from ...` as an - * identifier but may reinterpret it into a phase modifier later. In this - * case the identifier is not part of the AST and we should sync the - * knowledge to commentStacks + * Reset previous node leading comments, assuming that `node` is a + * single-token node. Used in import phase modifiers parsing. We parse + * `module` in `import module foo from ...` as an identifier but may + * reinterpret it into a phase modifier later. In this case the identifier is + * not part of the AST and we should sync the knowledge to commentStacks * * For example, when parsing * ``` @@ -266,17 +266,15 @@ export default class CommentsParser extends BaseParser { * @param node the last finished AST node _before_ current token */ /* eslint-enable no-irregular-whitespace */ - resetPreviousNodeLeadingComments(node: Node) { + resetPreviousIdentifierLeadingComments(node: Identifier) { const { commentStack } = this.state; const { length } = commentStack; if (length === 0) return; - for (let i = length - 1; i >= 0; i--) { - const commentWS = commentStack[i]; - if (commentWS.trailingNode === node) { - commentWS.trailingNode = null; - } - if (commentWS.end < node.start) break; + if (commentStack[length - 1].trailingNode === node) { + commentStack[length - 1].trailingNode = null; + } else if (length >= 2 && commentStack[length - 2].trailingNode === node) { + commentStack[length - 2].trailingNode = null; } } diff --git a/packages/babel-parser/src/parser/statement.ts b/packages/babel-parser/src/parser/statement.ts index 2e2a24a89e6b..6bbab77998ed 100644 --- a/packages/babel-parser/src/parser/statement.ts +++ b/packages/babel-parser/src/parser/statement.ts @@ -3003,7 +3003,7 @@ export default abstract class StatementParser extends ExpressionParser { type !== tt.comma; if (isImportPhase) { - this.resetPreviousNodeLeadingComments(phaseIdentifier); + this.resetPreviousIdentifierLeadingComments(phaseIdentifier); this.applyImportPhase( node as Undone, isExport, From db3a97a345b78e274ed2c07608e5dfe13473cb08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Ribaudo?= Date: Wed, 24 May 2023 15:07:50 +0200 Subject: [PATCH 8/8] Use a proper assertion --- packages/babel-parser/src/parser/statement.ts | 8 +++++++- packages/babel-parser/src/plugins/placeholders.ts | 5 +---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/babel-parser/src/parser/statement.ts b/packages/babel-parser/src/parser/statement.ts index 6bbab77998ed..5d760bde35a3 100644 --- a/packages/babel-parser/src/parser/statement.ts +++ b/packages/babel-parser/src/parser/statement.ts @@ -2944,7 +2944,13 @@ export default abstract class StatementParser extends ExpressionParser { loc?: Position, ): void { if (isExport) { - // This will never happen + if (!process.env.IS_PUBLISH) { + if (phase === "module") { + throw new Error( + "Assertion failure: export declarations do not support the 'module' phase.", + ); + } + } return; } if (phase === "module") { diff --git a/packages/babel-parser/src/plugins/placeholders.ts b/packages/babel-parser/src/plugins/placeholders.ts index 4d460d40d422..f349bd32a9d7 100644 --- a/packages/babel-parser/src/plugins/placeholders.ts +++ b/packages/babel-parser/src/plugins/placeholders.ts @@ -310,10 +310,7 @@ export default (superClass: typeof Parser) => >, maybeDefaultIdentifier: N.Identifier | null, ): node is Undone { - if ( - (node as N.ExportNamedDeclaration).specifiers && - (node as N.ExportNamedDeclaration).specifiers.length > 0 - ) { + if ((node as N.ExportNamedDeclaration).specifiers?.length) { // "export %%NAME%%" has already been parsed by #parseExport. return true; }