diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index d8cbbe5f75..ffb2b67321 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -15748,61 +15748,95 @@ namespace ts { * except for candidates: * * With no name * * Whose meaning doesn't match the `meaning` parameter. - * * Whose length differs from the target name by more than 0.3 of the length of the name. + * * Whose length differs from the target name by more than 0.34 of the length of the name. * * Whose levenshtein distance is more than 0.4 of the length of the name * (0.4 allows 1 substitution/transposition for every 5 characters, * and 1 insertion/deletion at 3 characters) - * Names longer than 30 characters don't get suggestions because Levenshtein distance is an n**2 algorithm. */ function getSpellingSuggestionForName(name: string, symbols: Symbol[], meaning: SymbolFlags): Symbol | undefined { - const worstDistance = name.length * 0.4; - const maximumLengthDifference = Math.min(3, name.length * 0.34); - let bestDistance = Number.MAX_VALUE; - let bestCandidate = undefined; + const maximumLengthDifference = Math.min(2, Math.floor(name.length * 0.34)); + let bestDistance = Math.floor(name.length * 0.4) + 1; // If the best result isn't better than this, don't bother. + let bestCandidate: Symbol | undefined; let justCheckExactMatches = false; - if (name.length > 30) { - return undefined; - } - name = name.toLowerCase(); + const nameLowerCase = name.toLowerCase(); for (const candidate of symbols) { - let candidateName = symbolName(candidate); - if (candidate.flags & meaning && - candidateName && - Math.abs(candidateName.length - name.length) < maximumLengthDifference) { - candidateName = candidateName.toLowerCase(); - if (candidateName === name) { - return candidate; - } - if (justCheckExactMatches) { - continue; - } - if (candidateName.length < 3 || - name.length < 3 || - candidateName === "eval" || - candidateName === "intl" || - candidateName === "undefined" || - candidateName === "map" || - candidateName === "nan" || - candidateName === "set") { - continue; - } - const distance = levenshtein(name, candidateName); - if (distance > worstDistance) { - continue; - } - if (distance < 3) { - justCheckExactMatches = true; - bestCandidate = candidate; - } - else if (distance < bestDistance) { - bestDistance = distance; - bestCandidate = candidate; - } + const candidateName = symbolName(candidate); + if (!(candidate.flags & meaning && Math.abs(candidateName.length - nameLowerCase.length) <= maximumLengthDifference)) { + continue; + } + const candidateNameLowerCase = candidateName.toLowerCase(); + if (candidateNameLowerCase === nameLowerCase) { + return candidate; + } + if (justCheckExactMatches) { + continue; + } + if (candidateName.length < 3) { + // Don't bother, user would have noticed a 2-character name having an extra character + continue; + } + // Only care about a result better than the best so far. + const distance = levenshteinWithMax(nameLowerCase, candidateNameLowerCase, bestDistance - 1); + if (distance === undefined) { + continue; + } + if (distance < 3) { + justCheckExactMatches = true; + bestCandidate = candidate; + } + else { + Debug.assert(distance < bestDistance); // Else `levenshteinWithMax` should return undefined + bestDistance = distance; + bestCandidate = candidate; } } return bestCandidate; } + function levenshteinWithMax(s1: string, s2: string, max: number): number | undefined { + let previous = new Array(s2.length + 1); + let current = new Array(s2.length + 1); + /** Represents any value > max. We don't care about the particular value. */ + const big = max + 1; + + for (let i = 0; i <= s2.length; i++) { + previous[i] = i; + } + + for (let i = 1; i <= s1.length; i++) { + const c1 = s1.charCodeAt(i - 1); + const minJ = i > max ? i - max : 1; + const maxJ = s2.length > max + i ? max + i : s2.length; + current[0] = i; + /** Smallest value of the matrix in the ith column. */ + let colMin = i; + for (let j = 1; j < minJ; j++) { + current[j] = big; + } + for (let j = minJ; j <= maxJ; j++) { + const dist = c1 === s2.charCodeAt(j - 1) + ? previous[j - 1] + : Math.min(/*delete*/ previous[j] + 1, /*insert*/ current[j - 1] + 1, /*substitute*/ previous[j - 1] + 2); + current[j] = dist; + colMin = Math.min(colMin, dist); + } + for (let j = maxJ + 1; j <= s2.length; j++) { + current[j] = big; + } + if (colMin > max) { + // Give up -- everything in this column is > max and it can't get better in future columns. + return undefined; + } + + const temp = previous; + previous = current; + current = temp; + } + + const res = previous[s2.length]; + return res > max ? undefined : res; + } + function markPropertyAsReferenced(prop: Symbol, nodeForCheckWriteOnly: Node | undefined, isThisAccess: boolean) { if (prop && noUnusedIdentifiers && diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 28520919e1..366c4bc229 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -3518,29 +3518,6 @@ namespace ts { return 0; } - export function levenshtein(s1: string, s2: string): number { - let previous: number[] = new Array(s2.length + 1); - let current: number[] = new Array(s2.length + 1); - for (let i = 0; i < s2.length + 1; i++) { - previous[i] = i; - current[i] = -1; - } - for (let i = 1; i < s1.length + 1; i++) { - current[0] = i; - for (let j = 1; j < s2.length + 1; j++) { - current[j] = Math.min( - previous[j] + 1, - current[j - 1] + 1, - previous[j - 1] + (s1[i - 1] === s2[j - 1] ? 0 : 2)); - } - // shift current back to previous, and then reuse previous' array - const tmp = previous; - previous = current; - current = tmp; - } - return previous[previous.length - 1]; - } - export function skipAlias(symbol: Symbol, checker: TypeChecker) { return symbol.flags & SymbolFlags.Alias ? checker.getAliasedSymbol(symbol) : symbol; }