refactor: extract options-normalizer/validator (#5020)

- Uses [`vnopts`](https://github.com/ikatyang/vnopts#readme)
- This way it should be easier to support language-specific options (https://github.com/prettier/prettier/pull/4798#issuecomment-407258477) and map the common options to language-specific options using [`forward`](https://github.com/ikatyang/vnopts#forward), e.g. `singleQuote: true` -> `"javascript/singleQuote": "js"`, `singleQuote: false` -> `"javascript/singleQuote": "none"`.
master
Ika 2018-08-31 11:26:07 +08:00 committed by GitHub
parent 32390cd6a3
commit 49e2f77bff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 160 additions and 257 deletions

View File

@ -61,6 +61,7 @@
"typescript-eslint-parser": "18.0.0",
"unicode-regex": "1.0.1",
"unified": "6.1.6",
"vnopts": "1.0.2",
"yaml": "ikatyang/yaml#a765c1ee16d6b8a5e715564645f2b85f7e04828b",
"yaml-unist-parser": "ikatyang/yaml-unist-parser#cd4f73325b3fc02a6d17842d0d9cee0dfc729c9b"
},

View File

@ -85,7 +85,8 @@ const options = {
category: coreOptions.CATEGORY_CONFIG,
description:
"Path to a Prettier configuration file (.prettierrc, package.json, prettier.config.js).",
oppositeDescription: "Do not look for a configuration file."
oppositeDescription: "Do not look for a configuration file.",
exception: value => value === false
},
"config-precedence": {
type: "choice",

View File

@ -848,12 +848,16 @@ function normalizeDetailedOptionMap(detailedOptionMap) {
function createMinimistOptions(detailedOptions) {
return {
// we use vnopts' AliasSchema to handle aliases for better error messages
alias: {},
boolean: detailedOptions
.filter(option => option.type === "boolean")
.map(option => option.name),
.map(option => [option.name].concat(option.alias || []))
.reduce((a, b) => a.concat(b)),
string: detailedOptions
.filter(option => option.type !== "boolean")
.map(option => option.name),
.map(option => [option.name].concat(option.alias || []))
.reduce((a, b) => a.concat(b)),
default: detailedOptions
.filter(option => !option.deprecated)
.filter(
@ -867,13 +871,6 @@ function createMinimistOptions(detailedOptions) {
(current, option) =>
Object.assign({ [option.name]: option.default }, current),
{}
),
alias: detailedOptions
.filter(option => option.alias !== undefined)
.reduce(
(current, option) =>
Object.assign({ [option.name]: option.alias }, current),
{}
)
};
}

View File

@ -66,7 +66,6 @@ const options = {
since: "1.4.0",
category: CATEGORY_SPECIAL,
type: "path",
default: undefined,
description:
"Specify the input filepath. This will be used to do parser inference.",
cliName: "stdin-filepath",
@ -208,7 +207,10 @@ const options = {
since: "0.0.0",
category: CATEGORY_GLOBAL,
type: "boolean",
default: false,
default: [
{ since: "0.0.0", value: false },
{ since: "1.15.0", value: undefined }
],
deprecated: "0.0.10",
description: "Use flow parser.",
redirect: { option: "parser", value: "flow" },

View File

@ -1,22 +0,0 @@
"use strict";
function apiDescriptor(name, value) {
return arguments.length === 1
? JSON.stringify(name)
: `\`{ ${apiDescriptor(name)}: ${JSON.stringify(value)} }\``;
}
function cliDescriptor(name, value) {
return value === false
? `\`--no-${name}\``
: value === true || arguments.length === 1
? `\`--${name}\``
: value === ""
? `\`--${name}\` without an argument`
: `\`--${name}=${value}\``;
}
module.exports = {
apiDescriptor,
cliDescriptor
};

View File

@ -1,152 +1,143 @@
"use strict";
const leven = require("leven");
const validator = require("./options-validator");
const descriptors = require("./options-descriptor");
const vnopts = require("vnopts");
function normalizeOptions(options, optionInfos, opts) {
opts = opts || {};
const logger =
opts.logger === false
? { warn() {} }
: opts.logger !== undefined
? opts.logger
: console;
const descriptor = opts.descriptor || descriptors.apiDescriptor;
const passThrough = opts.passThrough || [];
const cliDescriptor = {
key: key => (key.length === 1 ? `-${key}` : `--${key}`),
value: value => vnopts.apiDescriptor.value(value),
pair: ({ key, value }) =>
value === false
? `--no-${key}`
: value === true
? cliDescriptor.key(key)
: value === ""
? `${cliDescriptor.key(key)} without an argument`
: `${cliDescriptor.key(key)}=${value}`
};
const optionInfoMap = optionInfos.reduce(
(reduced, optionInfo) =>
Object.assign(reduced, { [optionInfo.name]: optionInfo }),
{}
);
const normalizedOptions = Object.keys(options).reduce((newOptions, key) => {
const optionInfo = optionInfoMap[key];
function normalizeOptions(
options,
optionInfos,
{ logger, isCLI = false, passThrough = false } = {}
) {
const unknown = !passThrough
? vnopts.levenUnknownHandler
: Array.isArray(passThrough)
? (key, value) =>
passThrough.indexOf(key) === -1 ? undefined : { [key]: value }
: (key, value) => ({ [key]: value });
let optionName = key;
let optionValue = options[key];
if (!optionInfo) {
if (passThrough === true || passThrough.indexOf(optionName) !== -1) {
newOptions[optionName] = optionValue;
} else {
logger.warn(
createUnknownOptionMessage(
optionName,
optionValue,
optionInfos,
descriptor
)
);
}
return newOptions;
}
if (!optionInfo.deprecated) {
optionValue = normalizeOption(optionValue, optionInfo);
} else if (typeof optionInfo.redirect === "string") {
logger.warn(createRedirectOptionMessage(optionInfo, descriptor));
optionName = optionInfo.redirect;
} else if (optionValue) {
logger.warn(createRedirectOptionMessage(optionInfo, descriptor));
optionValue = optionInfo.redirect.value;
optionName = optionInfo.redirect.option;
}
if (optionInfo.choices) {
const choiceInfo = optionInfo.choices.find(
choice => choice.value === optionValue
);
if (choiceInfo && choiceInfo.deprecated) {
logger.warn(
createRedirectChoiceMessage(optionInfo, choiceInfo, descriptor)
);
optionValue = choiceInfo.redirect;
}
}
if (optionInfo.array && !Array.isArray(optionValue)) {
optionValue = [optionValue];
}
if (optionValue !== optionInfo.default) {
validator.validateOption(optionValue, optionInfoMap[optionName], {
descriptor
});
}
newOptions[optionName] = optionValue;
return newOptions;
}, {});
return normalizedOptions;
const descriptor = isCLI ? cliDescriptor : vnopts.apiDescriptor;
const schemas = optionInfosToSchemas(optionInfos, { isCLI });
return vnopts.normalize(options, schemas, { logger, unknown, descriptor });
}
function normalizeOption(option, optionInfo) {
return optionInfo.type === "int" ? Number(option) : option;
}
function optionInfosToSchemas(optionInfos, { isCLI }) {
const schemas = [];
function createUnknownOptionMessage(key, value, optionInfos, descriptor) {
const messages = [`Ignored unknown option ${descriptor(key, value)}.`];
const suggestedOptionInfo = optionInfos.find(
optionInfo => leven(optionInfo.name, key) < 3
);
if (suggestedOptionInfo) {
messages.push(`Did you mean ${JSON.stringify(suggestedOptionInfo.name)}?`);
if (isCLI) {
schemas.push(vnopts.AnySchema.create({ name: "_" }));
}
return messages.join(" ");
for (const optionInfo of optionInfos) {
schemas.push(optionInfoToSchema(optionInfo, { isCLI }));
if (optionInfo.alias && isCLI) {
schemas.push(
vnopts.AliasSchema.create({
name: optionInfo.alias,
sourceName: optionInfo.name
})
);
}
}
return schemas;
}
function createRedirectOptionMessage(optionInfo, descriptor) {
return `${descriptor(
optionInfo.name
)} is deprecated. Prettier now treats it as ${
typeof optionInfo.redirect === "string"
? descriptor(optionInfo.redirect)
: descriptor(optionInfo.redirect.option, optionInfo.redirect.value)
}.`;
}
function optionInfoToSchema(optionInfo, { isCLI }) {
let SchemaConstructor;
const parameters = { name: optionInfo.name };
const handlers = {};
function createRedirectChoiceMessage(optionInfo, choiceInfo, descriptor) {
return `${descriptor(
optionInfo.name,
choiceInfo.value
)} is deprecated. Prettier now treats it as ${descriptor(
optionInfo.name,
choiceInfo.redirect
)}.`;
switch (optionInfo.type) {
case "int":
SchemaConstructor = vnopts.IntegerSchema;
if (isCLI) {
parameters.preprocess = value => Number(value);
}
break;
case "choice":
SchemaConstructor = vnopts.ChoiceSchema;
parameters.choices = optionInfo.choices.map(
choiceInfo =>
typeof choiceInfo === "object" && choiceInfo.redirect
? Object.assign({}, choiceInfo, {
redirect: {
to: { key: optionInfo.name, value: choiceInfo.redirect }
}
})
: choiceInfo
);
break;
case "boolean":
SchemaConstructor = vnopts.BooleanSchema;
break;
case "flag":
case "path":
SchemaConstructor = vnopts.StringSchema;
break;
default:
throw new Error(`Unexpected type ${optionInfo.type}`);
}
if (optionInfo.exception) {
parameters.validate = (value, schema, utils) => {
return optionInfo.exception(value) || schema.validate(value, utils);
};
} else {
parameters.validate = (value, schema, utils) => {
return value === undefined || schema.validate(value, utils);
};
}
if (optionInfo.redirect) {
handlers.redirect = value =>
!value
? undefined
: {
to: {
key: optionInfo.redirect.option,
value: optionInfo.redirect.value
}
};
}
if (optionInfo.deprecated) {
handlers.deprecated = true;
}
return optionInfo.array
? vnopts.ArraySchema.create(
Object.assign(
isCLI ? { preprocess: v => [].concat(v) } : {},
handlers,
{ valueSchema: SchemaConstructor.create(parameters) }
)
)
: SchemaConstructor.create(Object.assign({}, parameters, handlers));
}
function normalizeApiOptions(options, optionInfos, opts) {
return normalizeOptions(
options,
optionInfos,
Object.assign({ descriptor: descriptors.apiDescriptor }, opts)
);
return normalizeOptions(options, optionInfos, opts);
}
function normalizeCliOptions(options, optionInfos, opts) {
const args = options["_"] || [];
const newOptions = normalizeOptions(
Object.keys(options).reduce(
(reduced, key) =>
Object.assign(
reduced,
key.length === 1 // omit alias
? null
: { [key]: options[key] }
),
{}
),
return normalizeOptions(
options,
optionInfos,
Object.assign({ descriptor: descriptors.cliDescriptor }, opts)
Object.assign({ isCLI: true }, opts)
);
newOptions["_"] = args;
return newOptions;
}
module.exports = {

View File

@ -1,81 +0,0 @@
"use strict";
const descriptors = require("./options-descriptor");
function validateOption(value, optionInfo, opts) {
opts = opts || {};
const descriptor = opts.descriptor || descriptors.apiDescriptor;
if (
typeof optionInfo.exception === "function" &&
optionInfo.exception(value)
) {
return;
}
try {
validateOptionType(value, optionInfo);
} catch (error) {
throw new Error(
`Invalid \`${descriptor(optionInfo.name)}\` value. ${
error.message
}, but received \`${JSON.stringify(value)}\`.`
);
}
}
function validateOptionType(value, optionInfo) {
if (optionInfo.array) {
if (!Array.isArray(value)) {
throw new Error(`Expected an array`);
}
value.forEach(v =>
validateOptionType(v, Object.assign({}, optionInfo, { array: false }))
);
} else {
switch (optionInfo.type) {
case "int":
validateIntOption(value);
break;
case "boolean":
validateBooleanOption(value);
break;
case "choice":
validateChoiceOption(value, optionInfo.choices);
break;
}
}
}
function validateBooleanOption(value) {
if (typeof value !== "boolean") {
throw new Error(`Expected a boolean`);
}
}
function validateIntOption(value) {
if (
!(
typeof value === "number" &&
Math.floor(value) === value &&
value >= 0 &&
value !== Infinity
)
) {
throw new Error(`Expected an integer`);
}
}
function validateChoiceOption(value, choiceInfos) {
if (!choiceInfos.some(choiceInfo => choiceInfo.value === value)) {
const choices = choiceInfos
.filter(choiceInfo => !choiceInfo.deprecated)
.map(choiceInfo => JSON.stringify(choiceInfo.value))
.sort();
const head = choices.slice(0, -2);
const tail = choices.slice(-2);
throw new Error(`Expected ${head.concat(tail.join(" or ")).join(", ")}`);
}
}
module.exports = { validateOption };

View File

@ -27,7 +27,9 @@ function normalize(options, opts) {
}).options;
const defaults = supportOptions.reduce(
(reduced, optionInfo) =>
Object.assign(reduced, { [optionInfo.name]: optionInfo.default }),
optionInfo.default !== undefined
? Object.assign(reduced, { [optionInfo.name]: optionInfo.default })
: reduced,
Object.assign({}, hiddenDefaults)
);

View File

@ -10,7 +10,7 @@ exports[`boolean flags do not swallow the next argument (stdout) 1`] = `
exports[`boolean flags do not swallow the next argument (write) 1`] = `Array []`;
exports[`deprecated option values are warned (stderr) 1`] = `
"[warn] \`--trailing-comma\` without an argument is deprecated. Prettier now treats it as \`--trailing-comma=es5\`.
"[warn] --trailing-comma without an argument is deprecated; we now treat it as --trailing-comma=es5.
"
`;
@ -22,7 +22,7 @@ exports[`deprecated option values are warned (stdout) 1`] = `
exports[`deprecated option values are warned (write) 1`] = `Array []`;
exports[`deprecated options are warned (stderr) 1`] = `
"[warn] \`--flow-parser\` is deprecated. Prettier now treats it as \`--parser=flow\`.
"[warn] --flow-parser is deprecated; we now treat it as --parser=flow.
"
`;
@ -43,7 +43,7 @@ exports[`negated options work (stdout) 1`] = `
exports[`negated options work (write) 1`] = `Array []`;
exports[`unknown negated options are warned (stderr) 1`] = `
"[warn] Ignored unknown option \`--no-unknown\`.
"[warn] Ignored unknown option --no-unknown.
"
`;
@ -55,7 +55,7 @@ exports[`unknown negated options are warned (stdout) 1`] = `
exports[`unknown negated options are warned (write) 1`] = `Array []`;
exports[`unknown options are warned (stderr) 1`] = `
"[warn] Ignored unknown option \`--unknown\`.
"[warn] Ignored unknown option --unknown.
"
`;

View File

@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`show warning with kebab-case option key (stderr) 1`] = `
"[warn] Ignored unknown option \`{ \\"print-width\\": 3 }\`. Did you mean \\"printWidth\\"?
"[warn] Ignored unknown option { \\"print-width\\": 3 }. Did you mean printWidth?
"
`;
@ -10,7 +10,7 @@ exports[`show warning with kebab-case option key (stdout) 1`] = `""`;
exports[`show warning with kebab-case option key (write) 1`] = `Array []`;
exports[`show warning with unknown option (stderr) 1`] = `
"[warn] Ignored unknown option \`{ \\"hello\\": \\"world\\" }\`.
"[warn] Ignored unknown option { hello: \\"world\\" }.
"
`;
@ -37,7 +37,7 @@ exports[`throw error with invalid config format (stdout) 1`] = `""`;
exports[`throw error with invalid config format (write) 1`] = `Array []`;
exports[`throw error with invalid config option (int) (stderr) 1`] = `
"[error] Invalid \`\\"tabWidth\\"\` value. Expected an integer, but received \`0.5\`.
"[error] Invalid tabWidth value. Expected an integer, but received 0.5.
"
`;
@ -46,7 +46,7 @@ exports[`throw error with invalid config option (int) (stdout) 1`] = `""`;
exports[`throw error with invalid config option (int) (write) 1`] = `Array []`;
exports[`throw error with invalid config option (trailingComma) (stderr) 1`] = `
"[error] Invalid \`\\"trailingComma\\"\` value. Expected \\"all\\", \\"es5\\" or \\"none\\", but received \`\\"wow\\"\`.
"[error] Invalid trailingComma value. Expected \\"all\\", \\"es5\\" or \\"none\\", but received \\"wow\\".
"
`;
@ -55,7 +55,7 @@ exports[`throw error with invalid config option (trailingComma) (stdout) 1`] = `
exports[`throw error with invalid config option (trailingComma) (write) 1`] = `Array []`;
exports[`throw error with invalid config precedence option (configPrecedence) (stderr) 1`] = `
"[error] Invalid \`\`--config-precedence\`\` value. Expected \\"cli-override\\", \\"file-override\\" or \\"prefer-file\\", but received \`\\"option/configPrecedence\\"\`.
"[error] Invalid --config-precedence value. Expected \\"cli-override\\", \\"file-override\\" or \\"prefer-file\\", but received \\"option/configPrecedence\\".
"
`;

View File

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`API format with deprecated parser should work 1`] = `
"\`{ \\"parser\\": \\"postcss\\" }\` is deprecated. Prettier now treats it as \`{ \\"parser\\": \\"css\\" }\`.
"{ parser: \\"postcss\\" } is deprecated; we now treat it as { parser: \\"css\\" }.
"
`;

View File

@ -252,7 +252,7 @@ function rcYaml() {
exports[`CLI overrides take precedence without --config-precedence (write) 1`] = `Array []`;
exports[`CLI validate options with --config-precedence cli-override (stderr) 1`] = `
"[error] Invalid \`\\"printWidth\\"\` value. Expected an integer, but received \`0.5\`.
"[error] Invalid printWidth value. Expected an integer, but received 0.5.
"
`;
@ -261,7 +261,7 @@ exports[`CLI validate options with --config-precedence cli-override (stdout) 1`]
exports[`CLI validate options with --config-precedence cli-override (write) 1`] = `Array []`;
exports[`CLI validate options with --config-precedence file-override (stderr) 1`] = `
"[error] Invalid \`\\"printWidth\\"\` value. Expected an integer, but received \`0.5\`.
"[error] Invalid printWidth value. Expected an integer, but received 0.5.
"
`;
@ -270,7 +270,7 @@ exports[`CLI validate options with --config-precedence file-override (stdout) 1`
exports[`CLI validate options with --config-precedence file-override (write) 1`] = `Array []`;
exports[`CLI validate options with --config-precedence prefer-file (stderr) 1`] = `
"[error] Invalid \`\\"printWidth\\"\` value. Expected an integer, but received \`0.5\`.
"[error] Invalid printWidth value. Expected an integer, but received 0.5.
"
`;

View File

@ -5658,6 +5658,10 @@ tslib@^1.8.0, tslib@^1.9.1:
version "1.9.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.1.tgz#a5d1f0532a49221c87755cfcc89ca37197242ba7"
tslib@^1.9.3:
version "1.9.3"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286"
tty-browserify@0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"
@ -5917,6 +5921,14 @@ vm-browserify@0.0.4:
dependencies:
indexof "0.0.1"
vnopts@1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/vnopts/-/vnopts-1.0.2.tgz#f6a331473de0179d1679112cc090572b695202f7"
dependencies:
chalk "^2.4.1"
leven "^2.1.0"
tslib "^1.9.3"
w3c-hr-time@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz#82ac2bff63d950ea9e3189a58a65625fedf19045"