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

Create parser plugin "topLevelAwait" #10449

Merged
merged 8 commits into from Oct 29, 2019
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
25 changes: 21 additions & 4 deletions packages/babel-parser/src/index.js
Expand Up @@ -25,15 +25,32 @@ export function parse(input: string, options?: Options): File {
const parser = getParser(options, input);
const ast = parser.parse();

// Rather than try to parse as a script first, we opt to parse as a module and convert back
// to a script where possible to avoid having to do a full re-parse of the input content.
if (!parser.sawUnambiguousESM) ast.program.sourceType = "script";
if (parser.sawUnambiguousESM) {
return ast;
}

if (parser.ambiguousScriptDifferentAst) {
// Top level await introduces code which can be both a valid script and
// a valid module, but which produces different ASTs:
// await
// 0
// can be parser either as an AwaitExpression, or as two ExpressionStatements.
nicolo-ribaudo marked this conversation as resolved.
Show resolved Hide resolved
try {
options.sourceType = "script";
return getParser(options, input).parse();
} catch {}
} else {
// This is both a valid module and a valid script, but
// we parse it as a script by default
ast.program.sourceType = "script";
}

return ast;
} catch (moduleError) {
try {
options.sourceType = "script";
return getParser(options, input).parse();
} catch (scriptError) {}
} catch {}

throw moduleError;
}
Expand Down
1 change: 1 addition & 0 deletions packages/babel-parser/src/parser/base.js
Expand Up @@ -13,6 +13,7 @@ export default class BaseParser {
plugins: PluginsMap;
filename: ?string;
sawUnambiguousESM: boolean = false;
ambiguousScriptDifferentAst: boolean = false;

// Initialized by Tokenizer
state: State;
Expand Down
10 changes: 10 additions & 0 deletions packages/babel-parser/src/parser/expression.js
Expand Up @@ -2208,6 +2208,7 @@ export default class ExpressionParser extends LValParser {
isAwaitAllowed(): boolean {
if (this.scope.inFunction) return this.scope.inAsync;
if (this.options.allowAwaitOutsideFunction) return true;
if (this.hasPlugin("topLevelAwait")) return this.inModule;
Copy link
Member

Choose a reason for hiding this comment

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

If top-level await is only valid in modules, should seeing one set sawUnambiguousESM? In other words, how should this behave with a sourceType of unambiguous? Currently, parsing await 0 with unambiguous yields an AST with sourceType of script, even though if you'd specified script it would not have parsed:

> require('./').parse('await 0', { plugins: ['topLevelAwait'], sourceType: 'unambiguous' }).program.sourceType
'script'
> require('./').parse('await 0', { plugins: ['topLevelAwait'], sourceType: 'script' }).program.sourceType
Thrown:
{ [SyntaxError: Unexpected token, expected ";" (1:6)
] pos: 6, loc: Position { line: 1, column: 6 } }

Copy link
Member Author

Choose a reason for hiding this comment

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

This is tricky because, for exmaple, this is both a valid script and a valid module:

await
0

I can set it to a module when await the next invalid token is on the same line.

return false;
}

Expand All @@ -2234,9 +2235,18 @@ export default class ExpressionParser extends LValParser {
);
}

if (!this.scope.inFunction && !this.options.allowAwaitOutsideFunction) {
if (this.hasPrecedingLineBreak()) {
Copy link
Contributor

@JLHwung JLHwung Oct 21, 2019

Choose a reason for hiding this comment

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

I think the following examples are also ambiguous

await + 0
await - 0
await ( 0 )
await [ 0 ]
await / 0 /u

I am not sure if they are complete but we can always lookahead charcode to fix them.

this.ambiguousScriptDifferentAst = true;
} else {
this.sawUnambiguousESM = true;
}
}

if (!this.state.soloAwait) {
node.argument = this.parseMaybeUnary();
}

return this.finishNode(node, "AwaitExpression");
}

Expand Down
6 changes: 1 addition & 5 deletions packages/babel-parser/src/parser/statement.js
Expand Up @@ -495,11 +495,7 @@ export default class StatementParser extends ExpressionParser {
this.state.labels.push(loopLabel);

let awaitAt = -1;
if (
(this.scope.inAsync ||
(!this.scope.inFunction && this.options.allowAwaitOutsideFunction)) &&
this.eatContextual("await")
) {
if (this.isAwaitAllowed() && this.eatContextual("await")) {
awaitAt = this.state.lastTokStart;
}
this.scope.enter(SCOPE_OTHER);
Expand Down
@@ -0,0 +1 @@
export default await 0;
@@ -0,0 +1,4 @@
{
"plugins": ["topLevelAwait"],
"sourceType": "module"
}
@@ -0,0 +1,85 @@
{
"type": "File",
"start": 0,
"end": 23,
"loc": {
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 1,
"column": 23
}
},
"program": {
"type": "Program",
"start": 0,
"end": 23,
"loc": {
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 1,
"column": 23
}
},
"sourceType": "module",
"interpreter": null,
"body": [
{
"type": "ExportDefaultDeclaration",
"start": 0,
"end": 23,
"loc": {
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 1,
"column": 23
}
},
"declaration": {
"type": "AwaitExpression",
"start": 15,
"end": 22,
"loc": {
"start": {
"line": 1,
"column": 15
},
"end": {
"line": 1,
"column": 22
}
},
"argument": {
"type": "NumericLiteral",
"start": 21,
"end": 22,
"loc": {
"start": {
"line": 1,
"column": 21
},
"end": {
"line": 1,
"column": 22
}
},
"extra": {
"rawValue": 0,
"raw": "0"
},
"value": 0
}
}
}
],
"directives": []
}
}
@@ -0,0 +1 @@
for await (const a of b);
@@ -0,0 +1,4 @@
{
"plugins": ["topLevelAwait"],
"sourceType": "module"
}
@@ -0,0 +1,134 @@
{
"type": "File",
"start": 0,
"end": 25,
"loc": {
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 1,
"column": 25
}
},
"program": {
"type": "Program",
"start": 0,
"end": 25,
"loc": {
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 1,
"column": 25
}
},
"sourceType": "module",
"interpreter": null,
"body": [
{
"type": "ForOfStatement",
"start": 0,
"end": 25,
"loc": {
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 1,
"column": 25
}
},
"await": true,
"left": {
"type": "VariableDeclaration",
"start": 11,
"end": 18,
"loc": {
"start": {
"line": 1,
"column": 11
},
"end": {
"line": 1,
"column": 18
}
},
"declarations": [
{
"type": "VariableDeclarator",
"start": 17,
"end": 18,
"loc": {
"start": {
"line": 1,
"column": 17
},
"end": {
"line": 1,
"column": 18
}
},
"id": {
"type": "Identifier",
"start": 17,
"end": 18,
"loc": {
"start": {
"line": 1,
"column": 17
},
"end": {
"line": 1,
"column": 18
},
"identifierName": "a"
},
"name": "a"
},
"init": null
}
],
"kind": "const"
},
"right": {
"type": "Identifier",
"start": 22,
"end": 23,
"loc": {
"start": {
"line": 1,
"column": 22
},
"end": {
"line": 1,
"column": 23
},
"identifierName": "b"
},
"name": "b"
},
"body": {
"type": "EmptyStatement",
"start": 24,
"end": 25,
"loc": {
"start": {
"line": 1,
"column": 24
},
"end": {
"line": 1,
"column": 25
}
}
}
}
],
"directives": []
}
}
@@ -0,0 +1 @@
for await (const a of b);
@@ -0,0 +1,5 @@
{
"plugins": ["topLevelAwait"],
"sourceType": "script",
"throws": "Unexpected token, expected \"(\" (1:4)"
}
@@ -0,0 +1 @@
() => await 0;
@@ -0,0 +1,5 @@
{
"plugins": ["topLevelAwait"],
"sourceType": "module",
"throws": "Can not use keyword 'await' outside an async function (1:6)"
}
@@ -0,0 +1,3 @@
if (true) {
await 0;
}
@@ -0,0 +1,4 @@
{
"plugins": ["topLevelAwait"],
"sourceType": "module"
}