Skip to content

Commit

Permalink
parser: Extend hasPlugin to accept plugin-configuration array pairs
Browse files Browse the repository at this point in the history
This also allows expectPlugin and expectOnePlugin to give better error messages. For example:
> This experimental syntax requires enabling the parser plugin "pipelineOperator".
> This experimental syntax requires enabling the parser plugin ["pipelineOperator", {proposal: "hack", topicToken: "^^"}].

See #13973 (comment).
  • Loading branch information
js-choi committed Nov 20, 2021
1 parent 87fc2e7 commit e7c71dc
Show file tree
Hide file tree
Showing 42 changed files with 156 additions and 94 deletions.
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Expand Up @@ -378,7 +378,7 @@ Note that the code shown in Chrome DevTools is compiled code and therefore diffe
- After the ESTree PR is accepted, update [ast/spec.md](https://github.com/babel/babel/blob/master/packages/babel-parser/ast/spec.md). Note that there are differences between Babel AST and ESTree. In these cases, consistency with current Babel AST outweighs alignment to ESTree. Otherwise it should follow ESTree.

- [ ] Implement parser plugins based on the new AST. The parser plugin name should be the unprefixed slug of the TC39 proposal URL in _camelcase_, i.e. `exportDefaultFrom` from `https://github.com/tc39/proposal-export-default-from`.
- [ ] Use the `this.expectPlugin("newSyntax")` check within `@babel/parser` to ensure your new plugin code only runs when that flag is turned on (not default behavior), and a friendly error is thrown if users forget to enable a plugin.
- [ ] Use the `this.expectPlugin("pluginName")` check within `@babel/parser` to ensure your new plugin code only runs when that flag is turned on (not default behavior), and a friendly error is thrown if users forget to enable a plugin. You can also supply an array pair to require certain configuration options, e.g., `this.expectPlugin(["pluginName", { configOption: value }])`.
- [ ] Add failing/passing tests according to spec behavior
- [ ] Add `@babel/syntax-new-syntax` package. You can copy `packages/babel-plugin-syntax-decimal` and replace `decimal` to `new-syntax`.
- [ ] Add `@babel/syntax-new-syntax` to `@babel/standalone`.
Expand Down
24 changes: 22 additions & 2 deletions packages/babel-parser/src/parser/base.js
Expand Up @@ -31,12 +31,32 @@ export default class BaseParser {
declare input: string;
declare length: number;

hasPlugin(name: string): boolean {
return this.plugins.has(name);
// This method accepts either a string (plugin name) or an array pair
// (plugin name and options object). If an options object is given,
// then each value is non-recursively checked for identity with that
// plugin鈥檚 actual option value.
hasPlugin(pluginConfig: PluginConfig): boolean {
if (typeof pluginConfig === "string") {
return this.plugins.has(pluginConfig);
} else {
const [pluginName, pluginOptions] = pluginConfig;
if (!this.hasPlugin(pluginName)) {
return false;
}
const actualOptions = this.plugins.get(pluginName);
for (const [key, value] of Object.entries(pluginOptions)) {
if (value !== actualOptions?.[key]) {
return false;
}
}
return true;
}
}

getPluginOption(plugin: string, name: string) {
// $FlowIssue
if (this.hasPlugin(plugin)) return this.plugins.get(plugin)[name];
}
}

export type PluginConfig = string | [string, { [string]: any }];
5 changes: 3 additions & 2 deletions packages/babel-parser/src/parser/error.js
Expand Up @@ -3,6 +3,7 @@
import { getLineInfo, type Position } from "../util/location";
import CommentsParser from "./comments";
import { type ErrorCode, ErrorCodes } from "./error-codes";
import type { PluginConfig } from "./base";

// This function is used to raise exceptions on parse errors. It
// takes an offset integer (into the current `input`) to indicate
Expand All @@ -13,7 +14,7 @@ import { type ErrorCode, ErrorCodes } from "./error-codes";
type ErrorContext = {
pos: number,
loc: Position,
missingPlugin?: Array<string>,
missingPlugin?: Array<PluginConfig>,
code?: string,
reasonCode?: String,
};
Expand Down Expand Up @@ -129,7 +130,7 @@ export default class ParserError extends CommentsParser {
raiseWithData(
pos: number,
data?: {
missingPlugin?: Array<string>,
missingPlugin?: Array<PluginConfig>,
code?: string,
},
errorTemplate: string,
Expand Down
16 changes: 8 additions & 8 deletions packages/babel-parser/src/parser/expression.js
Expand Up @@ -448,7 +448,7 @@ export default class ExpressionParser extends LValParser {

if (
op === tt.pipeline &&
this.getPluginOption("pipelineOperator", "proposal") === "minimal"
this.hasPlugin(["pipelineOperator", { proposal: "minimal" }])
) {
if (this.state.type === tt._await && this.prodParam.hasAwait) {
throw this.raise(
Expand Down Expand Up @@ -1429,11 +1429,12 @@ export default class ExpressionParser extends LValParser {
): boolean {
switch (pipeProposal) {
case "hack": {
const pluginTopicToken = this.getPluginOption(
return this.hasPlugin([
"pipelineOperator",
"topicToken",
);
return tokenLabelName(tokenType) === pluginTopicToken;
{
topicToken: tokenLabelName(tokenType),
},
]);
}
case "smart":
return tokenType === tt.hash;
Expand Down Expand Up @@ -2742,7 +2743,7 @@ export default class ExpressionParser extends LValParser {
// of the infix operator `|>`.

checkPipelineAtInfixOperator(left: N.Expression, leftStartPos: number) {
if (this.getPluginOption("pipelineOperator", "proposal") === "smart") {
if (this.hasPlugin(["pipelineOperator", { proposal: "smart" }])) {
if (left.type === "SequenceExpression") {
// Ensure that the pipeline head is not a comma-delimited
// sequence expression.
Expand Down Expand Up @@ -2843,8 +2844,7 @@ export default class ExpressionParser extends LValParser {
// had before the function was called.

withSmartMixTopicForbiddingContext<T>(callback: () => T): T {
const proposal = this.getPluginOption("pipelineOperator", "proposal");
if (proposal === "smart") {
if (this.hasPlugin(["pipelineOperator", { proposal: "smart" }])) {
// Reset the parser鈥檚 topic context only if the smart-mix pipe proposal is active.
const outerContextTopicState = this.state.topicContext;
this.state.topicContext = {
Expand Down
23 changes: 13 additions & 10 deletions packages/babel-parser/src/parser/util.js
Expand Up @@ -21,6 +21,7 @@ import ProductionParameterHandler, {
} from "../util/production-parameter";
import { Errors, type ErrorTemplate, ErrorCodes } from "./error";
import type { ParsingError } from "./error";
import type { PluginConfig } from "./base";
/*::
import type ScopeHandler from "../util/scope";
*/
Expand Down Expand Up @@ -175,26 +176,28 @@ export default class UtilParser extends Tokenizer {
/* eslint-enable @babel/development-internal/dry-error-messages */
}

expectPlugin(name: string, pos?: ?number): true {
if (!this.hasPlugin(name)) {
expectPlugin(pluginConfig: PluginConfig, pos?: ?number): true {
if (!this.hasPlugin(pluginConfig)) {
throw this.raiseWithData(
pos != null ? pos : this.state.start,
{ missingPlugin: [name] },
`This experimental syntax requires enabling the parser plugin: '${name}'`,
{ missingPlugin: [pluginConfig] },
`This experimental syntax requires enabling the parser plugin: ${JSON.stringify(
pluginConfig,
)}.`,
);
}

return true;
}

expectOnePlugin(names: Array<string>, pos?: ?number): void {
if (!names.some(n => this.hasPlugin(n))) {
expectOnePlugin(pluginConfigs: Array<PluginConfig>, pos?: ?number): void {
if (!pluginConfigs.some(c => this.hasPlugin(c))) {
throw this.raiseWithData(
pos != null ? pos : this.state.start,
{ missingPlugin: names },
`This experimental syntax requires enabling one of the following parser plugin(s): '${names.join(
", ",
)}'`,
{ missingPlugin: pluginConfigs },
`This experimental syntax requires enabling one of the following parser plugin(s): ${pluginConfigs
.map(c => JSON.stringify(c))
.join(", ")}.`,
);
}
}
Expand Down
48 changes: 38 additions & 10 deletions packages/babel-parser/src/plugin-utils.js
@@ -1,19 +1,46 @@
// @flow

import type Parser from "./parser";
import type { PluginConfig } from "./parser/base";

export type Plugin = string | [string, Object];
export type Plugin = PluginConfig;

export type PluginList = $ReadOnlyArray<Plugin>;
export type PluginList = $ReadOnlyArray<PluginConfig>;

export type MixinPlugin = (superClass: Class<Parser>) => Class<Parser>;

export function hasPlugin(plugins: PluginList, name: string): boolean {
return plugins.some(plugin => {
if (Array.isArray(plugin)) {
return plugin[0] === name;
// This function鈥檚 second parameter accepts either a string (plugin name) or an
// array pair (plugin name and options object). If an options object is given,
// then each value is non-recursively checked for identity with the actual
// option value of each plugin in the first argument (which is an array of
// plugin names or array pairs).
export function hasPlugin(
plugins: PluginList,
expectedConfig: PluginConfig,
): boolean {
// The expectedOptions object is by default an empty object if the given
// expectedConfig argument does not give an options object (i.e., if it is a
// string).
const [expectedName, expectedOptions] =
typeof expectedConfig === "string" ? [expectedConfig, {}] : expectedConfig;

const expectedOptionsIsEmpty =
[...Object.entries(expectedOptions)].length === 0;

return plugins.some(p => {
if (typeof p === "string") {
return expectedOptionsIsEmpty && p === expectedName;
} else {
return plugin === name;
const [pluginName, pluginOptions] = p;
if (pluginName !== expectedName) {
return false;
}
for (const [key, expectedValue] of Object.entries(expectedOptions)) {
if (expectedValue !== pluginOptions[key]) {
return false;
}
}
return true;
}
});
}
Expand Down Expand Up @@ -85,9 +112,10 @@ export function validatePlugins(plugins: PluginList) {
);
}

const tupleSyntaxIsHash =
hasPlugin(plugins, "recordAndTuple") &&
getPluginOption(plugins, "recordAndTuple", "syntaxType") === "hash";
const tupleSyntaxIsHash = hasPlugin(plugins, [
"recordAndTuple",
{ syntaxType: "hash" },
]);

if (proposal === "hack") {
if (hasPlugin(plugins, "placeholders")) {
Expand Down
@@ -1,4 +1,3 @@
{
"sourceType": "module",
"throws": "Unexpected token (1:18)"
}
@@ -1,3 +1,4 @@
{
"throws": "This experimental syntax requires enabling the parser plugin: 'exportDefaultFrom' (1:7)"
}
"sourceType": "module",
"throws": "This experimental syntax requires enabling the parser plugin: \"exportDefaultFrom\". (1:7)"
}
@@ -1,6 +1,6 @@
{
"throws": "This experimental syntax requires enabling the parser plugin: 'asyncDoExpressions' (1:7)",
"throws": "This experimental syntax requires enabling the parser plugin: \"asyncDoExpressions\". (1:7)",
"plugins": [
"doExpressions"
]
}
}
@@ -1,4 +1,4 @@
{
"throws": "This experimental syntax requires enabling the parser plugin: 'doExpressions' (1:7)",
"throws": "This experimental syntax requires enabling the parser plugin: \"doExpressions\". (1:7)",
"plugins": []
}
}
@@ -1,3 +1,3 @@
{
"throws": "This experimental syntax requires enabling the parser plugin: 'decimal' (1:2)"
}
"throws": "This experimental syntax requires enabling the parser plugin: \"decimal\". (1:2)"
}
@@ -1,4 +1,4 @@
{
"throws": "This experimental syntax requires enabling one of the following parser plugin(s): 'decorators-legacy, decorators' (1:0)",
"throws": "This experimental syntax requires enabling one of the following parser plugin(s): \"decorators-legacy\", \"decorators\". (1:0)",
"plugins": []
}
@@ -1,4 +1,4 @@
{
"throws": "This experimental syntax requires enabling the parser plugin: 'doExpressions' (1:1)",
"throws": "This experimental syntax requires enabling the parser plugin: \"doExpressions\". (1:1)",
"plugins": []
}
@@ -1,4 +1,4 @@
{
"throws": "This experimental syntax requires enabling the parser plugin: 'exportDefaultFrom' (1:7)",
"throws": "This experimental syntax requires enabling the parser plugin: \"exportDefaultFrom\". (1:7)",
"plugins": []
}
@@ -1,5 +1,5 @@
{
"throws": "This experimental syntax requires enabling the parser plugin: 'importAssertions' (1:24)",
"throws": "This experimental syntax requires enabling the parser plugin: \"importAssertions\". (1:24)",
"sourceType": "module",
"plugins": []
}
}
@@ -1,5 +1,5 @@
{
"throws": "This experimental syntax requires enabling the parser plugin: 'importAssertions' (1:27)",
"throws": "This experimental syntax requires enabling the parser plugin: \"importAssertions\". (1:27)",
"sourceType": "module",
"plugins": []
}
}
@@ -1,4 +1,4 @@
{
"throws": "This experimental syntax requires enabling the parser plugin: 'moduleAttributes' (1:27)",
"throws": "This experimental syntax requires enabling the parser plugin: \"moduleAttributes\". (1:27)",
"BABEL_8_BREAKING": false
}
@@ -1,3 +1,3 @@
{
"throws": "This experimental syntax requires enabling the parser plugin: 'moduleBlocks' (1:18)"
"throws": "This experimental syntax requires enabling the parser plugin: \"moduleBlocks\". (1:18)"
}
@@ -1,5 +1,5 @@
{
"plugins": null,
"sourceType": "module",
"throws": "This experimental syntax requires enabling one of the following parser plugin(s): 'decorators, decorators-legacy' (1:7)",
"plugins": null
}
"throws": "This experimental syntax requires enabling one of the following parser plugin(s): \"decorators\", \"decorators-legacy\". (1:7)"
}
@@ -1,3 +1,3 @@
{
"throws": "This experimental syntax requires enabling the parser plugin: 'functionSent' (2:11)"
}
"throws": "This experimental syntax requires enabling the parser plugin: \"functionSent\". (2:11)"
}
@@ -1,3 +1,3 @@
{
"throws": "This experimental syntax requires enabling the parser plugin: 'functionSent' (2:12)"
}
"throws": "This experimental syntax requires enabling the parser plugin: \"functionSent\". (2:12)"
}
@@ -1,3 +1,3 @@
{
"throws": "This experimental syntax requires enabling the parser plugin: 'functionSent' (2:18)"
}
"throws": "This experimental syntax requires enabling the parser plugin: \"functionSent\". (2:18)"
}
@@ -1,4 +1,5 @@
{
"throws": "This experimental syntax requires enabling the parser plugin: 'importAssertions' (1:27)",
"plugins": []
"plugins": [],
"sourceType": "module",
"throws": "This experimental syntax requires enabling the parser plugin: \"importAssertions\". (1:27)"
}
@@ -1,4 +1,4 @@
{
"throws": "This experimental syntax requires enabling the parser plugin: 'moduleAttributes' (1:27)",
"throws": "This experimental syntax requires enabling the parser plugin: \"moduleAttributes\". (1:27)",
"plugins": []
}
}
@@ -1,3 +1,3 @@
{
"throws": "This experimental syntax requires enabling the parser plugin: 'pipelineOperator' (1:2)"
}
"throws": "This experimental syntax requires enabling the parser plugin: \"pipelineOperator\". (1:2)"
}
@@ -1,3 +1,3 @@
{
"throws": "This experimental syntax requires enabling the parser plugin: 'recordAndTuple' (1:0)"
"throws": "This experimental syntax requires enabling the parser plugin: \"recordAndTuple\". (1:0)"
}
@@ -1,3 +1,3 @@
{
"throws": "This experimental syntax requires enabling the parser plugin: 'throwExpressions' (2:3)"
}
"throws": "This experimental syntax requires enabling the parser plugin: \"throwExpressions\". (2:3)"
}
@@ -1,7 +1,8 @@
{
"sourceType": "module",
"plugins": [
"jsx"
"jsx",
"estree"
],
"throws": "This experimental syntax requires enabling one of the following parser plugin(s): 'flow, typescript' (1:7)"
"throws": "This experimental syntax requires enabling one of the following parser plugin(s): \"flow\", \"typescript\". (1:7)"
}

0 comments on commit e7c71dc

Please sign in to comment.