diff --git a/package.json b/package.json index ef82aacb..664af0d9 100644 --- a/package.json +++ b/package.json @@ -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" }, diff --git a/src/cli/constant.js b/src/cli/constant.js index 5bb1db0a..de815c6c 100644 --- a/src/cli/constant.js +++ b/src/cli/constant.js @@ -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", diff --git a/src/cli/util.js b/src/cli/util.js index be6e5c3e..23c58bbe 100644 --- a/src/cli/util.js +++ b/src/cli/util.js @@ -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), - {} ) }; } diff --git a/src/main/core-options.js b/src/main/core-options.js index 688f45c7..39627106 100644 --- a/src/main/core-options.js +++ b/src/main/core-options.js @@ -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" }, diff --git a/src/main/options-descriptor.js b/src/main/options-descriptor.js deleted file mode 100644 index 8dc53ef7..00000000 --- a/src/main/options-descriptor.js +++ /dev/null @@ -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 -}; diff --git a/src/main/options-normalizer.js b/src/main/options-normalizer.js index 686d7353..ddec7663 100644 --- a/src/main/options-normalizer.js +++ b/src/main/options-normalizer.js @@ -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 = { diff --git a/src/main/options-validator.js b/src/main/options-validator.js deleted file mode 100644 index 7608c4b7..00000000 --- a/src/main/options-validator.js +++ /dev/null @@ -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 }; diff --git a/src/main/options.js b/src/main/options.js index 302f0c8b..b1f678ef 100644 --- a/src/main/options.js +++ b/src/main/options.js @@ -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) ); diff --git a/tests_integration/__tests__/__snapshots__/arg-parsing.js.snap b/tests_integration/__tests__/__snapshots__/arg-parsing.js.snap index b1486adf..2ecb96e9 100644 --- a/tests_integration/__tests__/__snapshots__/arg-parsing.js.snap +++ b/tests_integration/__tests__/__snapshots__/arg-parsing.js.snap @@ -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. " `; diff --git a/tests_integration/__tests__/__snapshots__/config-invalid.js.snap b/tests_integration/__tests__/__snapshots__/config-invalid.js.snap index e3dbb858..90e9857c 100644 --- a/tests_integration/__tests__/__snapshots__/config-invalid.js.snap +++ b/tests_integration/__tests__/__snapshots__/config-invalid.js.snap @@ -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\\". " `; diff --git a/tests_integration/__tests__/__snapshots__/deprecated-parser.js.snap b/tests_integration/__tests__/__snapshots__/deprecated-parser.js.snap index beb62c7a..5eda05a7 100644 --- a/tests_integration/__tests__/__snapshots__/deprecated-parser.js.snap +++ b/tests_integration/__tests__/__snapshots__/deprecated-parser.js.snap @@ -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\\" }. " `; diff --git a/tests_integration/__tests__/__snapshots__/with-config-precedence.js.snap b/tests_integration/__tests__/__snapshots__/with-config-precedence.js.snap index ce5002c6..c1a87149 100644 --- a/tests_integration/__tests__/__snapshots__/with-config-precedence.js.snap +++ b/tests_integration/__tests__/__snapshots__/with-config-precedence.js.snap @@ -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. " `; diff --git a/yarn.lock b/yarn.lock index 0edd7aee..feef2fe6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"