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

Extend hasPlugin to accept plugin-configuration array pairs #13982

Merged
merged 2 commits into from Dec 2, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
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
27 changes: 23 additions & 4 deletions packages/babel-parser/src/parser/base.js
Expand Up @@ -31,12 +31,31 @@ 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’s 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 of Object.keys(pluginOptions)) {
if (actualOptions?.[key] !== pluginOptions[key]) {
return false;
}
}
return true;
}
}

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

export type PluginConfig = string | [string, { [string]: any }];
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’s topic context only if the smart-mix pipe proposal is active.
const outerContextTopicState = this.state.topicContext;
this.state.topicContext = {
Expand Down
33 changes: 23 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,38 @@ export default class UtilParser extends Tokenizer {
/* eslint-enable @babel/development-internal/dry-error-messages */
}

expectPlugin(name: string, pos?: ?number): true {
if (!this.hasPlugin(name)) {
getPluginNamesFromConfigs(pluginConfigs: Array<PluginConfig>): Array<string> {
return pluginConfigs.map(c => {
if (typeof c === "string") {
return c;
} else {
return c[0];
}
});
}

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: this.getPluginNamesFromConfigs([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: this.getPluginNamesFromConfigs(pluginConfigs) },
`This experimental syntax requires enabling one of the following parser plugin(s): ${pluginConfigs
.map(c => JSON.stringify(c))
.join(", ")}.`,
);
}
}
Expand Down
49 changes: 39 additions & 10 deletions packages/babel-parser/src/plugin-utils.js
@@ -1,19 +1,47 @@
// @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’s 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 expectedKeys = Object.keys(expectedOptions);

const expectedOptionsIsEmpty = expectedKeys.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 of expectedKeys) {
if (pluginOptions[key] !== expectedOptions[key]) {
return false;
}
}
return true;
}
});
}
Expand Down Expand Up @@ -85,9 +113,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",
js-choi marked this conversation as resolved.
Show resolved Hide resolved
"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)"
}
Expand Up @@ -3,5 +3,5 @@
"plugins": [
"jsx"
],
"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)"
}
Expand Up @@ -3,5 +3,5 @@
"plugins": [
"jsx"
],
"throws": "This experimental syntax requires enabling one of the following parser plugin(s): 'flow, typescript' (2:7)"
}
"throws": "This experimental syntax requires enabling one of the following parser plugin(s): \"flow\", \"typescript\". (2:7)"
}
Expand Up @@ -3,5 +3,5 @@
"plugins": [
"jsx"
],
"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)"
}