perf(cli): skip impossible typo distance comparisons (#18977)

This commit is contained in:
Neil
2026-09-05 20:04:26 -07:00
committed by GitHub
parent 37427bfd1a
commit 64374d5dff
2 changed files with 58 additions and 5 deletions
+44
View File
@@ -0,0 +1,44 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import * as distance from '../shared/edit-distance'
import { suggestCommands, unknownFlagData } from './command-suggestion'
import type { CommandSpec } from './command-spec'
const specs: CommandSpec[] = [
{ path: ['list'], summary: '', usage: '', allowedFlags: [] },
{ path: ['remove'], summary: '', usage: '', allowedFlags: [], destructive: true }
]
afterEach(() => vi.restoreAllMocks())
describe('suggestion distance work', () => {
it('does no distance calculations for a long command, including destructive intent', () => {
const spy = vi.spyOn(distance, 'levenshtein')
expect(suggestCommands(specs, ['x'.repeat(32_768)])).toEqual([])
expect(spy).not.toHaveBeenCalled()
})
it('does no distance calculations for a long flag but still lists valid flags', () => {
const spy = vi.spyOn(distance, 'levenshtein')
expect(unknownFlagData('x'.repeat(32_768), ['worktree', 'json'])).toEqual({
validFlags: ['json', 'worktree'],
suggestions: [],
nextSteps: ['Valid flags: --json, --worktree']
})
expect(spy).not.toHaveBeenCalled()
})
it('keeps the inclusive three-edit suggestion boundary', () => {
expect(suggestCommands(specs, ['listxxx'])).toEqual(['list'])
expect(unknownFlagData('jsonxxx', ['json']).suggestions).toEqual(['json'])
})
it('keeps the inclusive one-edit destructive intent boundary', () => {
expect(suggestCommands(specs, ['remov'])).toEqual(['remove'])
expect(suggestCommands(specs, ['remo'])).toEqual([])
})
it('retains UTF-16 distance semantics at the length boundary', () => {
expect(unknownFlagData('json😀x', ['json']).suggestions).toEqual(['json'])
expect(unknownFlagData('json😀😀', ['json']).suggestions).toEqual([])
})
})
+14 -5
View File
@@ -37,7 +37,10 @@ function destructiveVerbs(specs: CommandSpec[]): Set<string> {
// input token is itself a near-miss of a destructive verb. #6303
function intendsDestruction(inputToken: string, verbs: Set<string>): boolean {
for (const verb of verbs) {
if (levenshtein(inputToken, verb) <= DESTRUCTIVE_INTENT_THRESHOLD) {
if (
Math.abs(inputToken.length - verb.length) <= DESTRUCTIVE_INTENT_THRESHOLD &&
levenshtein(inputToken, verb) <= DESTRUCTIVE_INTENT_THRESHOLD
) {
return true
}
}
@@ -85,7 +88,9 @@ export function suggestCommands(specs: CommandSpec[], commandPath: string[]): st
continue
}
seen.add(joined)
scored.push({ label: joined, distance: levenshtein(input, joined) })
if (Math.abs(input.length - joined.length) <= SUGGESTION_THRESHOLD) {
scored.push({ label: joined, distance: levenshtein(input, joined) })
}
}
}
return rankByDistance(scored)
@@ -106,9 +111,13 @@ export type FlagErrorData = {
}
function suggestFlags(flag: string, validFlags: string[]): string[] {
return rankByDistance(
validFlags.map((candidate) => ({ label: candidate, distance: levenshtein(flag, candidate) }))
)
const scored: { label: string; distance: number }[] = []
for (const candidate of validFlags) {
if (Math.abs(flag.length - candidate.length) <= SUGGESTION_THRESHOLD) {
scored.push({ label: candidate, distance: levenshtein(flag, candidate) })
}
}
return rankByDistance(scored)
}
// Why: include the accepted set so agents can recover without another help call.