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:
parent
5b30bef2d1
commit
6df0575acd
2 changed files with 77 additions and 66 deletions
|
@ -15748,61 +15748,95 @@ namespace ts {
|
||||||
* except for candidates:
|
* except for candidates:
|
||||||
* * With no name
|
* * With no name
|
||||||
* * Whose meaning doesn't match the `meaning` parameter.
|
* * 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
|
* * Whose levenshtein distance is more than 0.4 of the length of the name
|
||||||
* (0.4 allows 1 substitution/transposition for every 5 characters,
|
* (0.4 allows 1 substitution/transposition for every 5 characters,
|
||||||
* and 1 insertion/deletion at 3 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 {
|
function getSpellingSuggestionForName(name: string, symbols: Symbol[], meaning: SymbolFlags): Symbol | undefined {
|
||||||
const worstDistance = name.length * 0.4;
|
const maximumLengthDifference = Math.min(2, Math.floor(name.length * 0.34));
|
||||||
const maximumLengthDifference = Math.min(3, 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 bestDistance = Number.MAX_VALUE;
|
let bestCandidate: Symbol | undefined;
|
||||||
let bestCandidate = undefined;
|
|
||||||
let justCheckExactMatches = false;
|
let justCheckExactMatches = false;
|
||||||
if (name.length > 30) {
|
const nameLowerCase = name.toLowerCase();
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
name = name.toLowerCase();
|
|
||||||
for (const candidate of symbols) {
|
for (const candidate of symbols) {
|
||||||
let candidateName = symbolName(candidate);
|
const candidateName = symbolName(candidate);
|
||||||
if (candidate.flags & meaning &&
|
if (!(candidate.flags & meaning && Math.abs(candidateName.length - nameLowerCase.length) <= maximumLengthDifference)) {
|
||||||
candidateName &&
|
continue;
|
||||||
Math.abs(candidateName.length - name.length) < maximumLengthDifference) {
|
}
|
||||||
candidateName = candidateName.toLowerCase();
|
const candidateNameLowerCase = candidateName.toLowerCase();
|
||||||
if (candidateName === name) {
|
if (candidateNameLowerCase === nameLowerCase) {
|
||||||
return candidate;
|
return candidate;
|
||||||
}
|
}
|
||||||
if (justCheckExactMatches) {
|
if (justCheckExactMatches) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (candidateName.length < 3 ||
|
if (candidateName.length < 3) {
|
||||||
name.length < 3 ||
|
// Don't bother, user would have noticed a 2-character name having an extra character
|
||||||
candidateName === "eval" ||
|
continue;
|
||||||
candidateName === "intl" ||
|
}
|
||||||
candidateName === "undefined" ||
|
// Only care about a result better than the best so far.
|
||||||
candidateName === "map" ||
|
const distance = levenshteinWithMax(nameLowerCase, candidateNameLowerCase, bestDistance - 1);
|
||||||
candidateName === "nan" ||
|
if (distance === undefined) {
|
||||||
candidateName === "set") {
|
continue;
|
||||||
continue;
|
}
|
||||||
}
|
if (distance < 3) {
|
||||||
const distance = levenshtein(name, candidateName);
|
justCheckExactMatches = true;
|
||||||
if (distance > worstDistance) {
|
bestCandidate = candidate;
|
||||||
continue;
|
}
|
||||||
}
|
else {
|
||||||
if (distance < 3) {
|
Debug.assert(distance < bestDistance); // Else `levenshteinWithMax` should return undefined
|
||||||
justCheckExactMatches = true;
|
bestDistance = distance;
|
||||||
bestCandidate = candidate;
|
bestCandidate = candidate;
|
||||||
}
|
|
||||||
else if (distance < bestDistance) {
|
|
||||||
bestDistance = distance;
|
|
||||||
bestCandidate = candidate;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return bestCandidate;
|
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) {
|
function markPropertyAsReferenced(prop: Symbol, nodeForCheckWriteOnly: Node | undefined, isThisAccess: boolean) {
|
||||||
if (prop &&
|
if (prop &&
|
||||||
noUnusedIdentifiers &&
|
noUnusedIdentifiers &&
|
||||||
|
|
|
@ -3518,29 +3518,6 @@ namespace ts {
|
||||||
return 0;
|
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) {
|
export function skipAlias(symbol: Symbol, checker: TypeChecker) {
|
||||||
return symbol.flags & SymbolFlags.Alias ? checker.getAliasedSymbol(symbol) : symbol;
|
return symbol.flags & SymbolFlags.Alias ? checker.getAliasedSymbol(symbol) : symbol;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue