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 JS Module Blocks proposal #12469

Merged
merged 24 commits into from Feb 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
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
13 changes: 13 additions & 0 deletions packages/babel-generator/src/generators/expressions.ts
Expand Up @@ -300,3 +300,16 @@ export function V8IntrinsicIdentifier(
this.token("%");
this.word(node.name);
}

export function ModuleExpression(node: t.ModuleExpression) {
this.word("module");
this.space();
this.token("{");
if (node.body.body.length === 0) {
this.token("}");
} else {
this.newline();
this.printSequence(node.body.body, node, { indent: true });
this.rightBrace();
}
}
@@ -0,0 +1,6 @@
const m = module { export const foo = "foo" };
module {
foo;
bar;
};
foo(module {});
@@ -0,0 +1,4 @@
{
"plugins": ["moduleBlocks"],
"sourceType": "module"
}
@@ -0,0 +1,8 @@
const m = module {
export const foo = "foo";
};
module {
foo;
bar;
};
foo(module {});
12 changes: 12 additions & 0 deletions packages/babel-parser/ast/spec.md
Expand Up @@ -87,6 +87,7 @@ These are the core @babel/parser (babylon) AST node types.
- [SequenceExpression](#sequenceexpression)
- [ParenthesizedExpression](#parenthesizedexpression)
- [DoExpression](#doexpression)
- [ModuleExpression](#moduleexpression)
- [Template Literals](#template-literals)
- [TemplateLiteral](#templateliteral)
- [TaggedTemplateExpression](#taggedtemplateexpression)
Expand Down Expand Up @@ -1086,6 +1087,17 @@ interface DoExpression <: Expression {
}
```

## ModuleExpression

```js
interface ModuleExpression <: Expression {
type: "ModuleExpression";
body: Program
}
```

A inline module expression proposed in https://github.com/tc39/proposal-js-module-blocks.

# Template Literals

## TemplateLiteral
Expand Down
42 changes: 41 additions & 1 deletion packages/babel-parser/src/parser/expression.js
Expand Up @@ -56,6 +56,10 @@ import {
} from "../util/expression-scope";
import { Errors } from "./error";

/*::
import type { SourceType } from "../options";
*/
Comment on lines +59 to +61
Copy link
Member

Choose a reason for hiding this comment

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

Nit: no need to comment this. It was only needed for class fields because Flow didn't support the declare keyword.

Suggested change
/*::
import type { SourceType } from "../options";
*/
import type { SourceType } from "../options";

Copy link
Member Author

Choose a reason for hiding this comment

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

If remove this comment, ESLint warns:

error: 'SourceType' is defined but never used (no-unused-vars) at packages/babel-parser/src/parser/expression.js:58:15:
56 | } from "../util/expression-scope";
57 | import { Errors } from "./error";
> 58 | import type { SourceType } from "../options";
   |               ^
59 | 
60 | export default class ExpressionParser extends LValParser {
61 |   // Forward-declaration: defined in statement.js

Sorry I'm not familiar with Flow, what should we do?

Copy link
Member

Choose a reason for hiding this comment

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

It's ok like this then.


export default class ExpressionParser extends LValParser {
// Forward-declaration: defined in statement.js
/*::
Expand All @@ -78,6 +82,16 @@ export default class ExpressionParser extends LValParser {
) => T;
+parseFunctionParams: (node: N.Function, allowModifiers?: boolean) => void;
+takeDecorators: (node: N.HasDecorators) => void;
+parseBlockOrModuleBlockBody: (
body: N.Statement[],
directives: ?(N.Directive[]),
topLevel: boolean,
end: TokenType,
afterBlockParse?: (hasStrictModeDirective: boolean) => void
) => void
+parseProgram: (
program: N.Program, end: TokenType, sourceType?: SourceType
) => N.Program
*/

// For object literal, check if property __proto__ has been used more than once.
Expand Down Expand Up @@ -500,7 +514,13 @@ export default class ExpressionParser extends LValParser {
this.next();
return this.parseAwait(startPos, startLoc);
}

if (
this.isContextual("module") &&
this.lookaheadCharCode() === charCodes.leftCurlyBrace &&
!this.hasFollowingLineBreak()
) {
return this.parseModuleExpression();
}
const update = this.match(tt.incDec);
const node = this.startNode();
if (this.state.type.prefix) {
Expand Down Expand Up @@ -2630,4 +2650,24 @@ export default class ExpressionParser extends LValParser {

return ret;
}

// https://github.com/tc39/proposal-js-module-blocks
parseModuleExpression(): N.ModuleExpression {
this.expectPlugin("moduleBlocks");
const node = this.startNode<N.ModuleExpression>();
this.next(); // eat "module"
this.eat(tt.braceL);
nicolo-ribaudo marked this conversation as resolved.
Show resolved Hide resolved

const revertScopes = this.initializeScopes(/** inModule */ true);
this.enterInitialScopes();

const program = this.startNode<N.Program>();
try {
node.body = this.parseProgram(program, tt.braceR, "module");
} finally {
revertScopes();
}
this.eat(tt.braceR);
return this.finishNode<N.ModuleExpression>(node, "ModuleExpression");
}
}
22 changes: 2 additions & 20 deletions packages/babel-parser/src/parser/index.js
Expand Up @@ -5,14 +5,7 @@ import type { File /*::, JSXOpeningElement */ } from "../types";
import type { PluginList } from "../plugin-utils";
import { getOptions } from "../options";
import StatementParser from "./statement";
import { SCOPE_PROGRAM } from "../util/scopeflags";
import ScopeHandler from "../util/scope";
import ClassScopeHandler from "../util/class-scope";
import ExpressionScopeHandler from "../util/expression-scope";
import ProductionParameterHandler, {
PARAM_AWAIT,
PARAM,
} from "../util/production-parameter";

export type PluginsMap = Map<string, { [string]: any }>;

Expand All @@ -28,14 +21,8 @@ export default class Parser extends StatementParser {
options = getOptions(options);
super(options, input);

const ScopeHandler = this.getScopeHandler();

this.options = options;
this.inModule = this.options.sourceType === "module";
this.scope = new ScopeHandler(this.raise.bind(this), this.inModule);
this.prodParam = new ProductionParameterHandler();
this.classScope = new ClassScopeHandler(this.raise.bind(this));
this.expressionScope = new ExpressionScopeHandler(this.raise.bind(this));
this.initializeScopes();
this.plugins = pluginsMap(this.options.plugins);
this.filename = options.sourceFilename;
}
Expand All @@ -46,12 +33,7 @@ export default class Parser extends StatementParser {
}

parse(): File {
let paramFlags = PARAM;
if (this.hasPlugin("topLevelAwait") && this.inModule) {
paramFlags |= PARAM_AWAIT;
}
this.scope.enter(SCOPE_PROGRAM);
this.prodParam.enter(paramFlags);
this.enterInitialScopes();
const file = this.startNode();
const program = this.startNode();
this.nextToken();
Expand Down
25 changes: 15 additions & 10 deletions packages/babel-parser/src/parser/statement.js
Expand Up @@ -35,6 +35,7 @@ import {
newExpressionScope,
newParameterDeclarationScope,
} from "../util/expression-scope";
import type { SourceType } from "../options";

const loopLabel = { kind: "loop" },
switchLabel = { kind: "switch" };
Expand All @@ -55,12 +56,22 @@ export default class StatementParser extends ExpressionParser {
// to its body instead of creating a new node.

parseTopLevel(file: N.File, program: N.Program): N.File {
program.sourceType = this.options.sourceType;
file.program = this.parseProgram(program);
file.comments = this.state.comments;

program.interpreter = this.parseInterpreterDirective();
if (this.options.tokens) file.tokens = this.tokens;

this.parseBlockBody(program, true, true, tt.eof);
return this.finishNode(file, "File");
}

parseProgram(
program: N.Program,
end: TokenType = tt.eof,
sourceType: SourceType = this.options.sourceType,
): N.Program {
program.sourceType = sourceType;
program.interpreter = this.parseInterpreterDirective();
this.parseBlockBody(program, true, true, end);
if (
this.inModule &&
!this.options.allowUndeclaredExports &&
Expand All @@ -72,13 +83,7 @@ export default class StatementParser extends ExpressionParser {
this.raise(pos, Errors.ModuleExportUndefined, name);
}
}

file.program = this.finishNode(program, "Program");
file.comments = this.state.comments;

if (this.options.tokens) file.tokens = this.tokens;

return this.finishNode(file, "File");
return this.finishNode<N.Program>(program, "Program");
}

// TODO
Expand Down
65 changes: 65 additions & 0 deletions packages/babel-parser/src/parser/util.js
Expand Up @@ -6,7 +6,17 @@ import State from "../tokenizer/state";
import type { Node } from "../types";
import { lineBreak } from "../util/whitespace";
import { isIdentifierChar } from "../util/identifier";
import ClassScopeHandler from "../util/class-scope";
import ExpressionScopeHandler from "../util/expression-scope";
import { SCOPE_PROGRAM } from "../util/scopeflags";
import ProductionParameterHandler, {
PARAM_AWAIT,
PARAM,
} from "../util/production-parameter";
import { Errors } from "./error";
/*::
import type ScopeHandler from "../util/scope";
*/

type TryParse<Node, Error, Thrown, Aborted, FailState> = {
node: Node,
Expand All @@ -19,6 +29,11 @@ type TryParse<Node, Error, Thrown, Aborted, FailState> = {
// ## Parser utilities

export default class UtilParser extends Tokenizer {
// Forward-declaration: defined in parser/index.js
/*::
+getScopeHandler: () => Class<ScopeHandler<*>>;
*/

// TODO

addExtra(node: Node, key: string, val: any): void {
Expand Down Expand Up @@ -304,6 +319,56 @@ export default class UtilParser extends Tokenizer {
isObjectMethod(node: Node): boolean {
return node.type === "ObjectMethod";
}

initializeScopes(
inModule: boolean = this.options.sourceType === "module",
): () => void {
// Initialize state
const oldLabels = this.state.labels;
this.state.labels = [];

const oldExportedIdentifiers = this.state.exportedIdentifiers;
this.state.exportedIdentifiers = [];

// initialize scopes
const oldInModule = this.inModule;
this.inModule = inModule;

const oldScope = this.scope;
const ScopeHandler = this.getScopeHandler();
this.scope = new ScopeHandler(this.raise.bind(this), this.inModule);

const oldProdParam = this.prodParam;
this.prodParam = new ProductionParameterHandler();

const oldClassScope = this.classScope;
this.classScope = new ClassScopeHandler(this.raise.bind(this));

const oldExpressionScope = this.expressionScope;
this.expressionScope = new ExpressionScopeHandler(this.raise.bind(this));

return () => {
// Revert state
this.state.labels = oldLabels;
this.state.exportedIdentifiers = oldExportedIdentifiers;

// Revert scopes
this.inModule = oldInModule;
this.scope = oldScope;
this.prodParam = oldProdParam;
this.classScope = oldClassScope;
this.expressionScope = oldExpressionScope;
};
}

enterInitialScopes() {
let paramFlags = PARAM;
if (this.hasPlugin("topLevelAwait") && this.inModule) {
paramFlags |= PARAM_AWAIT;
}
this.scope.enter(SCOPE_PROGRAM);
this.prodParam.enter(paramFlags);
}
}

/**
Expand Down
5 changes: 5 additions & 0 deletions packages/babel-parser/src/types.js
Expand Up @@ -655,6 +655,11 @@ export type TemplateElement = NodeBase & {
},
};

export type ModuleExpression = NodeBase & {
type: "ModuleExpression",
body: Program,
};

// Patterns

// TypeScript access modifiers
Expand Down
@@ -0,0 +1 @@
module { await: 3 };
@@ -0,0 +1,4 @@
{
"sourceType": "script",
"plugins": ["moduleBlocks"]
}
@@ -0,0 +1,55 @@
{
"type": "File",
"start":0,"end":20,"loc":{"start":{"line":1,"column":0},"end":{"line":1,"column":20}},
"errors": [
"SyntaxError: Unexpected reserved word 'await' (1:9)"
],
"program": {
"type": "Program",
"start":0,"end":20,"loc":{"start":{"line":1,"column":0},"end":{"line":1,"column":20}},
"sourceType": "script",
"interpreter": null,
"body": [
{
"type": "ExpressionStatement",
"start":0,"end":20,"loc":{"start":{"line":1,"column":0},"end":{"line":1,"column":20}},
"expression": {
"type": "ModuleExpression",
"start":0,"end":19,"loc":{"start":{"line":1,"column":0},"end":{"line":1,"column":19}},
"body": {
"type": "Program",
"start":9,"end":19,"loc":{"start":{"line":1,"column":9},"end":{"line":1,"column":19}},
"sourceType": "module",
"interpreter": null,
"body": [
{
"type": "LabeledStatement",
"start":9,"end":17,"loc":{"start":{"line":1,"column":9},"end":{"line":1,"column":17}},
"body": {
"type": "ExpressionStatement",
"start":16,"end":17,"loc":{"start":{"line":1,"column":16},"end":{"line":1,"column":17}},
"expression": {
"type": "NumericLiteral",
"start":16,"end":17,"loc":{"start":{"line":1,"column":16},"end":{"line":1,"column":17}},
"extra": {
"rawValue": 3,
"raw": "3"
},
"value": 3
}
},
"label": {
"type": "Identifier",
"start":9,"end":14,"loc":{"start":{"line":1,"column":9},"end":{"line":1,"column":14},"identifierName":"await"},
"name": "await"
}
}
],
"directives": []
}
}
}
],
"directives": []
}
}
@@ -0,0 +1,7 @@
class B {
#p() {
module {
class C { [this.#p]; }
};
}
}