Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parse import-assertions #12139

Merged
6 changes: 3 additions & 3 deletions packages/babel-core/src/parser/util/missing-plugin-helper.js
Expand Up @@ -129,10 +129,10 @@ const pluginNameMap = {
url: "https://git.io/JfeDR",
},
},
moduleAttributes: {
importAssertions: {
syntax: {
name: "@babel/plugin-syntax-module-attributes",
url: "https://git.io/JfK3k",
name: "@babel/plugin-syntax-import-assertions",
url: "https://git.io/JUbkv",
},
},
moduleStringNames: {
Expand Down
14 changes: 13 additions & 1 deletion packages/babel-generator/src/generators/modules.js
Expand Up @@ -180,7 +180,19 @@ export function ImportDeclaration(node: Object) {

this.print(node.source, node);

if (node.attributes?.length) {
if (node.assertions?.length) {
Copy link
Contributor

@JLHwung JLHwung Oct 7, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: It seems that import foo from "foo.json" assert {} are now generated as import foo from "foo.json". Although it is semantically equivalent. I slightly prefer to reserve asserts {} if node.assertions is not nullish.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realized that current AST design does not differentiate between import foo from "foo.json" assert {} and import foo from "foo.json". Both of them have assertions set to [].

this.space();
this.word("assert");
this.space();
this.token("{");
this.space();
this.printList(node.assertions, node);
this.space();
this.token("}");
}
// todo(Babel 8): remove this if branch
// `module-attributes` support is discontinued, use `import-assertions` instead.
else if (node.attributes?.length) {
this.space();
this.word("with");
this.space();
Expand Down
@@ -0,0 +1 @@
import foo from "foo.json" assert { type: "json" };
@@ -0,0 +1,8 @@
{
"plugins": [
[
"importAssertions"
]
],
"sourceType": "module"
}
@@ -0,0 +1 @@
import foo from "foo.json" assert { type: "json" };

This file was deleted.

This file was deleted.

This file was deleted.

@@ -1 +1,3 @@
import "foo.json" with type: "json";
import("foo.json", { with: { type: "json" } },);

@@ -1,3 +1,4 @@
import "foo.json" with type: "json";
import("foo.json", {
with: {
type: "json"
Expand Down
2 changes: 1 addition & 1 deletion packages/babel-parser/ast/spec.md
Expand Up @@ -1292,7 +1292,7 @@ interface ImportDeclaration <: ModuleDeclaration {
importKind: null | "type" | "typeof" | "value";
specifiers: [ ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier ];
source: StringLiteral;
attributes?: [ ImportAttribute ];
assertions?: [ ImportAttribute ];
}
```

Expand Down
15 changes: 12 additions & 3 deletions packages/babel-parser/src/parser/expression.js
Expand Up @@ -829,13 +829,18 @@ export default class ExpressionParser extends LValParser {
): N.Expression {
if (node.callee.type === "Import") {
if (node.arguments.length === 2) {
this.expectPlugin("moduleAttributes");
// todo(Babel 8): remove the if condition,
// moduleAttributes is renamed to importAssertions
if (!this.hasPlugin("moduleAttributes")) {
this.expectPlugin("importAssertions");
}
}
if (node.arguments.length === 0 || node.arguments.length > 2) {
this.raise(
node.start,
Errors.ImportCallArity,
this.hasPlugin("moduleAttributes")
this.hasPlugin("importAssertions") ||
this.hasPlugin("moduleAttributes")
? "one or two arguments"
: "one argument",
);
Expand Down Expand Up @@ -872,7 +877,11 @@ export default class ExpressionParser extends LValParser {
} else {
this.expect(tt.comma);
if (this.match(close)) {
if (dynamicImport && !this.hasPlugin("moduleAttributes")) {
if (
dynamicImport &&
!this.hasPlugin("importAssertions") &&
!this.hasPlugin("moduleAttributes")
) {
this.raise(
this.state.lastTokStart,
Errors.ImportCallArgumentTrailingComma,
Expand Down
110 changes: 97 additions & 13 deletions packages/babel-parser/src/parser/statement.js
Expand Up @@ -2109,12 +2109,21 @@ export default class StatementParser extends ExpressionParser {
this.expectContextual("from");
}
node.source = this.parseImportSource();
// https://github.com/tc39/proposal-module-attributes
// parse module attributes if the next token is `with` or ignore and finish the ImportDeclaration node.
const attributes = this.maybeParseModuleAttributes();
if (attributes) {
node.attributes = attributes;
// https://github.com/tc39/proposal-import-assertions
// parse module import assertions if the next token is `assert` or ignore
// and finish the ImportDeclaration node.
const assertions = this.maybeParseImportAssertions();
if (assertions) {
node.assertions = assertions;
}
// todo(Babel 8): remove module attributes support
else {
const attributes = this.maybeParseModuleAttributes();
if (attributes) {
node.attributes = attributes;
}
}

this.semicolon();
return this.finishNode(node, "ImportDeclaration");
}
Expand Down Expand Up @@ -2145,6 +2154,69 @@ export default class StatementParser extends ExpressionParser {
node.specifiers.push(this.finishNode(specifier, type));
}

parseAssertEntries() {
JLHwung marked this conversation as resolved.
Show resolved Hide resolved
this.expectPlugin("importAssertions");

const attrs = [];
const attrNames = new Set();

do {
if (this.match(tt.braceR)) {
break;
}

const node = this.startNode();

// parse AssertionKey : IdentifierName, StringLiteral
let assertionKeyNode;
if (this.match(tt.string)) {
assertionKeyNode = this.parseLiteral(this.state.value, "StringLiteral");
} else {
assertionKeyNode = this.parseIdentifier(true);
}
this.next();
node.key = assertionKeyNode;

xtuc marked this conversation as resolved.
Show resolved Hide resolved
// for now we are only allowing `type` as the only allowed module attribute
if (node.key.name !== "type") {
this.raise(
node.key.start,
Errors.ModuleAttributeDifferentFromType,
node.key.name,
);
}
// check if we already have an entry for an attribute
// if a duplicate entry is found, throw an error
// for now this logic will come into play only when someone declares `type` twice
if (attrNames.has(node.key.name)) {
this.raise(
node.key.start,
Errors.ModuleAttributesWithDuplicateKeys,
node.key.name,
);
}
attrNames.add(node.key.name);

if (!this.match(tt.string)) {
throw this.unexpected(
this.state.start,
Errors.ModuleAttributeInvalidValue,
);
}
node.value = this.parseLiteral(this.state.value, "StringLiteral");
this.finishNode(node, "ImportAttribute");
attrs.push(node);
} while (this.eat(tt.comma));

return attrs;
}

/**
* parse module attributes
* @deprecated It will be removed in Babel 8
* @returns
* @memberof StatementParser
*/
maybeParseModuleAttributes() {
if (this.match(tt._with) && !this.hasPrecedingLineBreak()) {
this.expectPlugin("moduleAttributes");
Expand All @@ -2156,13 +2228,9 @@ export default class StatementParser extends ExpressionParser {
const attrs = [];
const attributes = new Set();
do {
// we are trying to parse a node which has the following syntax
// with type: "json"
// [with -> keyword], [type -> Identifier], [":" -> token for colon], ["json" -> StringLiteral]
const node = this.startNode();
node.key = this.parseIdentifier(true);

// for now we are only allowing `type` as the only allowed module attribute
if (node.key.name !== "type") {
this.raise(
node.key.start,
Expand All @@ -2171,9 +2239,6 @@ export default class StatementParser extends ExpressionParser {
);
}

// check if we already have an entry for an attribute
// if a duplicate entry is found, throw an error
// for now this logic will come into play only when someone declares `type` twice
if (attributes.has(node.key.name)) {
this.raise(
node.key.start,
Expand All @@ -2183,7 +2248,6 @@ export default class StatementParser extends ExpressionParser {
}
attributes.add(node.key.name);
this.expect(tt.colon);
// check if the value set to the module attribute is a string as we only allow string literals
if (!this.match(tt.string)) {
throw this.unexpected(
this.state.start,
Expand All @@ -2198,6 +2262,26 @@ export default class StatementParser extends ExpressionParser {
return attrs;
}

maybeParseImportAssertions() {
if (
this.match(tt.name) &&
this.state.value === "assert" &&
!this.hasPrecedingLineBreak()
) {
this.expectPlugin("importAssertions");
this.next();
} else {
if (this.hasPlugin("importAssertions")) return [];
return null;
}

this.eat(tt.braceL);
const attrs = this.parseAssertEntries();
this.eat(tt.braceR);

return attrs;
}

maybeParseDefaultImportSpecifier(node: N.ImportDeclaration): boolean {
if (this.shouldParseDefaultImport(node)) {
// import defaultObj, { x, y as z } from '...'
Expand Down
6 changes: 6 additions & 0 deletions packages/babel-parser/src/plugin-utils.js
Expand Up @@ -87,6 +87,11 @@ export function validatePlugins(plugins: PluginList) {
}

if (hasPlugin(plugins, "moduleAttributes")) {
if (hasPlugin(plugins, "importAssertions")) {
throw new Error(
"Cannot combine importAssertions and moduleAttributes plugins.",
);
}
const moduleAttributesVerionPluginOption = getPluginOption(
plugins,
"moduleAttributes",
Expand All @@ -100,6 +105,7 @@ export function validatePlugins(plugins: PluginList) {
);
}
}

if (
hasPlugin(plugins, "recordAndTuple") &&
!RECORD_AND_TUPLE_SYNTAX_TYPES.includes(
Expand Down
@@ -1,5 +1,5 @@
{
"throws": "This experimental syntax requires enabling the parser plugin: 'moduleAttributes' (1:24)",
"throws": "This experimental syntax requires enabling the parser plugin: 'importAssertions' (1:24)",
"sourceType": "module",
"plugins": []
}
@@ -0,0 +1 @@
import foo from "foo.json" assert { type: "json" };
@@ -0,0 +1,5 @@
{
"throws": "This experimental syntax requires enabling the parser plugin: 'importAssertions' (1:27)",
"sourceType": "module",
"plugins": []
}
@@ -1,5 +1,3 @@
{
"throws": "This experimental syntax requires enabling the parser plugin: 'moduleAttributes' (1:27)",
"sourceType": "module",
"plugins": []
}
"throws": "This experimental syntax requires enabling the parser plugin: 'moduleAttributes' (1:27)"
}
@@ -0,0 +1 @@
import("foo.json", { assert: { type: "json" } })