feat: "Did you mean <option>?"

Print a "Did you mean" line under the unknown option error
with any option that look similar to the typed one.

close #2603
PR #2623
This commit is contained in:
Zoltan Kochan
2020-06-11 01:12:45 +03:00
committed by GitHub
parent 740219129c
commit 561f389554
10 changed files with 109 additions and 22 deletions

View File

@@ -0,0 +1,7 @@
---
"@pnpm/parse-cli-args": major
---
`unknownOptions` in the result object is a `Map` instead of an `Array`.
`unknownOptions` is a map of unknown options to options that are similar to the unknown options.

View File

@@ -0,0 +1,5 @@
---
"pnpm": patch
---
Print a "Did you mean" line under the unknown option error with any option that look similar to the typed one.

View File

@@ -34,6 +34,7 @@
},
"dependencies": {
"@pnpm/find-workspace-dir": "workspace:1.0.0",
"didyoumean2": "^4.0.0",
"nopt": "4.0.3"
},
"funding": "https://opencollective.com/pnpm"

View File

@@ -1,4 +1,5 @@
import findWorkspaceDir from '@pnpm/find-workspace-dir'
import didYouMean, { ReturnTypeEnums } from 'didyoumean2'
import nopt = require('nopt')
const RECURSIVE_CMDS = new Set(['recursive', 'multi', 'm'])
@@ -13,7 +14,7 @@ export interface ParsedCliArgs {
// tslint:disable-next-line: no-any
options: Record<string, any>
cmd: string | null
unknownOptions: string[]
unknownOptions: Map<string, string[]>
workspaceDir?: string
}
@@ -50,7 +51,7 @@ export default async function parseCliArgs (
cmd: 'help',
options: {},
params: noptExploratoryResults.argv.remain,
unknownOptions: [] as string[],
unknownOptions: new Map(),
}
}
@@ -126,13 +127,8 @@ export default async function parseCliArgs (
cmd = 'recursive'
}
const allowedOptions = new Set(Object.keys(types))
const unknownOptions = [] as string[]
for (const cliOption of Object.keys(options)) {
if (!allowedOptions.has(cliOption) && !cliOption.startsWith('//')) {
unknownOptions.push(cliOption)
}
}
const knownOptions = new Set(Object.keys(types))
const unknownOptions = getUnknownOptions(Object.keys(options), knownOptions)
return {
argv,
cmd,
@@ -142,3 +138,21 @@ export default async function parseCliArgs (
workspaceDir,
}
}
function getUnknownOptions (usedOptions: string[], knownOptions: Set<string>) {
const unknownOptions = new Map<string, string[]>()
const closestMatches = getClosestOptionMatches.bind(null, Array.from(knownOptions))
for (const usedOption of usedOptions) {
if (knownOptions.has(usedOption) || usedOption.startsWith('//')) continue
unknownOptions.set(usedOption, closestMatches(usedOption))
}
return unknownOptions
}
function getClosestOptionMatches (knownOptions: string[], option: string) {
return didYouMean(option, knownOptions, {
returnType: ReturnTypeEnums.ALL_CLOSEST_MATCHES,
}) as (string[] | null) ?? []
}

View File

@@ -120,6 +120,7 @@ test('detect unknown options', async (t) => {
getTypesByCommandName: (commandName: string) => {
if (commandName === 'install') {
return {
bar: Boolean,
recursive: Boolean,
registry: String,
}
@@ -128,7 +129,10 @@ test('detect unknown options', async (t) => {
},
universalOptionsTypes: { filter: [String, Array] },
}, ['install', '--save-dev', '--registry=https://example.com', '--qar', '--filter=packages'])
t.deepEqual(unknownOptions, ['save-dev', 'qar'])
t.deepEqual(
Array.from(unknownOptions.entries()),
[['save-dev', []], ['qar', ['bar']]]
)
t.end()
})

View File

@@ -0,0 +1,16 @@
import chalk = require('chalk')
export function formatUnknownOptionsError (unknownOptions: Map<string, string[]>) {
let output = chalk.bgRed.black('\u2009ERROR\u2009')
const unknownOptionsArray = Array.from(unknownOptions.keys())
if (unknownOptionsArray.length > 1) {
return `${output} ${chalk.red(`Unknown options: ${unknownOptionsArray.map(unknownOption => `'${unknownOption}'`).join(', ')}`)}`
}
const unknownOption = unknownOptionsArray[0]
output += ` ${chalk.red(`Unknown option: '${unknownOption}'`)}`
const didYouMeanOptions = unknownOptions.get(unknownOption)
if (!didYouMeanOptions?.length) {
return output
}
return `${output}\nDid you mean '${didYouMeanOptions.join("', or '")}'?`
}

View File

@@ -28,6 +28,7 @@ import R = require('ramda')
import which = require('which')
import checkForUpdates from './checkForUpdates'
import pnpmCmds, { getRCOptionsTypes } from './cmd'
import { formatUnknownOptionsError } from './formatError'
import './logging/fileLogger'
import parseCliArgs from './parseCliArgs'
import initReporter, { ReporterType } from './reporter'
@@ -53,23 +54,18 @@ export default async function run (inputArgv: string[]) {
process.exit(1)
}
if (unknownOptions.length > 0) {
if (unknownOptions.every((option) => DEPRECATED_OPTIONS.has(option))) {
if (unknownOptions.size > 0) {
const unknownOptionsArray = Array.from(unknownOptions.keys())
if (unknownOptionsArray.every((option) => DEPRECATED_OPTIONS.has(option))) {
let deprecationMsg = `${chalk.bgYellow.black('\u2009WARN\u2009')}`
if (unknownOptions.length === 1) {
deprecationMsg += ` ${chalk.yellow(`Deprecated option: '${unknownOptions[0]}'`)}`
if (unknownOptionsArray.length === 1) {
deprecationMsg += ` ${chalk.yellow(`Deprecated option: '${unknownOptionsArray[0]}'`)}`
} else {
deprecationMsg += ` ${chalk.yellow(`Deprecated options: ${unknownOptions.map(unknownOption => `'${unknownOption}'`).join(', ')}`)}`
deprecationMsg += ` ${chalk.yellow(`Deprecated options: ${unknownOptionsArray.map(unknownOption => `'${unknownOption}'`).join(', ')}`)}`
}
console.log(deprecationMsg)
} else {
let errorMsg = `${chalk.bgRed.black('\u2009ERROR\u2009')}`
if (unknownOptions.length === 1) {
errorMsg += ` ${chalk.red(`Unknown option: '${unknownOptions[0]}'`)}`
} else {
errorMsg += ` ${chalk.red(`Unknown options: ${unknownOptions.map(unknownOption => `'${unknownOption}'`).join(', ')}`)}`
}
console.error(errorMsg)
console.error(formatUnknownOptionsError(unknownOptions))
console.log(`For help, run: pnpm help${cmd ? ` ${cmd}` : ''}`)
process.exit(1)
}

View File

@@ -0,0 +1,22 @@
import chalk = require('chalk')
import test = require('tape')
import { formatUnknownOptionsError } from '../src/formatError'
let ERROR = chalk.bgRed.black('\u2009ERROR\u2009')
test('formatUnknownOptionsError()', async (t) => {
t.equal(
formatUnknownOptionsError(new Map([['foo', []]])),
`${ERROR} ${chalk.red("Unknown option: 'foo'")}`
)
t.equal(
formatUnknownOptionsError(new Map([['foo', ['foa', 'fob']]])),
`${ERROR} ${chalk.red("Unknown option: 'foo'")}
Did you mean 'foa', or 'fob'?`
)
t.equal(
formatUnknownOptionsError(new Map([['foo', []], ['bar', []]])),
`${ERROR} ${chalk.red("Unknown options: 'foo', 'bar'")}`
)
t.end()
})

View File

@@ -1,6 +1,7 @@
///<reference path="../../../typings/index.d.ts" />
import './cli'
import './complete.test'
import './formatError.test'
import './getOptionType.test'
import './help.spec'
import './install'

21
pnpm-lock.yaml generated
View File

@@ -1371,6 +1371,7 @@ importers:
packages/parse-cli-args:
dependencies:
'@pnpm/find-workspace-dir': 'link:../find-workspace-dir'
didyoumean2: 4.0.0
nopt: 4.0.3
devDependencies:
'@pnpm/parse-cli-args': 'link:'
@@ -1379,6 +1380,7 @@ importers:
'@pnpm/find-workspace-dir': 'workspace:1.0.0'
'@pnpm/parse-cli-args': 'link:'
'@types/nopt': 3.0.29
didyoumean2: ^4.0.0
nopt: 4.0.3
packages/parse-wanted-dependency:
dependencies:
@@ -5934,6 +5936,15 @@ packages:
hasBin: true
resolution:
integrity: sha512-bOICKN2/UvsTjEnJpUld3/2SDahgHH6KvG7umRv9WNlYTXOlwplM5ZkJBQh72swRjI5D3iu2Aqde1STulCWUWg==
/didyoumean2/4.0.0:
dependencies:
leven: 3.1.0
lodash.deburr: 4.1.0
dev: false
engines:
node: '>=10.13'
resolution:
integrity: sha512-7+OMIHqPDJ4uxeExQx8cSk26oD3KUloAQzi2R+3rmTU4IHvSDDmWZTQ6bmC4+MTw61DkYoh5ARxwS9MRoz0t2A==
/diff-dates/1.0.12:
dependencies:
date-unit-ms: 1.1.13
@@ -8218,6 +8229,12 @@ packages:
node: '>=6'
resolution:
integrity: sha512-94++VFO3qN95cM/d6eBXvd894oJE0w3cInq9USsyQzzoJxmiYzPAocNcuGCPGGjoXqDVJcr3C1jzt1TSjyaiLQ==
/leven/3.1.0:
dev: false
engines:
node: '>=6'
resolution:
integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==
/levn/0.3.0:
dependencies:
prelude-ls: 1.1.2
@@ -8340,6 +8357,10 @@ packages:
dev: true
resolution:
integrity: sha1-GVhwRQ9aExkkeN9Lw9I9LeoZB7Y=
/lodash.deburr/4.1.0:
dev: false
resolution:
integrity: sha1-3bG7s+8HRYwBd7oH3hRCLLAz/5s=
/lodash.get/4.4.2:
dev: true
resolution: