Remove length limit on spelling suggestions; use levenshteinWithMax for performance (#19937)

* Remove length limit on spelling suggestions; use levenshteinWithMax for performance

* Remove suggestion exceptions

* Move to checker.ts

* Reintroduce candidateName max length
This commit is contained in:
Andy 2017-11-28 12:37:30 -05:00 committed by GitHub
parent 5b30bef2d1
commit 6df0575acd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 77 additions and 66 deletions

View file

@ -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 &&

View file

@ -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;
}