Extend hasPlugin to accept plugin-configuration array pairs (#13982)

Co-authored-by: Nicolò Ribaudo <nicolo.ribaudo@gmail.com>
This commit is contained in:
J. S. Choi
2021-12-02 17:17:51 -05:00
committed by GitHub
parent ecae53a98e
commit f4236f43a1
42 changed files with 165 additions and 94 deletions

View File

@@ -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
// plugins 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 }];

View File

@@ -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(
@@ -1422,11 +1422,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;
@@ -2740,7 +2741,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.
@@ -2841,8 +2842,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 parsers topic context only if the smart-mix pipe proposal is active.
const outerContextTopicState = this.state.topicContext;
this.state.topicContext = {

View File

@@ -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";
*/
@@ -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(", ")}.`,
);
}
}

View File

@@ -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 functions 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;
}
});
}
@@ -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")) {