diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index eaa28767166c2..87d5f9e97110d 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -14301,11 +14301,11 @@ namespace ts { candidateName === "set") { continue; } - const distance = levenshtein(name, candidateName); + const distance = dameraulevenshtein(name, candidateName); if (distance > worstDistance) { continue; } - if (distance < 3) { + if (distance <= MIN_LEV_DIST) { return candidate; } else if (distance < bestDistance) { diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index a859fde9cc1ec..a1a3dfe412414 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -4642,26 +4642,48 @@ namespace ts { return identifier.length >= 3 && identifier.charCodeAt(0) === CharacterCodes._ && identifier.charCodeAt(1) === CharacterCodes._ && identifier.charCodeAt(2) === CharacterCodes._ ? identifier.substr(1) : identifier; } - 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++) { + const CHANGE_COST = 2; // Make substitutions costly, since when they appear in short strings they're normally obvious errors we don't want to promote + const INSERT_COST = 1; + const DELETE_COST = 1; + const TRANSPOSE_COST = 0.95; // Transpositions slightly cheaper than insertion/deletion/substitution to promote transposed errors + export const MIN_LEV_DIST = TRANSPOSE_COST; + export function dameraulevenshtein(s1: string, s2: string): number { + if (s1.length > s2.length) [s1, s2] = [s2, s1]; // Swap arguments so s2 is always the short one to use minimal memory + const rowLength = s2.length + 1; + let prevprev: number[] = new Array(rowLength); + let previous: number[] = new Array(rowLength); + let current: number[] = new Array(rowLength); + for (let i = 0; i < rowLength; i++) { + prevprev[i] = Infinity; previous[i] = i; - current[i] = -1; } + + // For each row in the matrix beyond the first (which is in previous) for (let i = 1; i < s1.length + 1; i++) { + // Set the first element to the row number (representing the distance required to remove the prefix to that point) 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)); + // Then for each element in the row, minimize it's cost based on the cost of editing the string at that point to match the character in the second string + for (let j = 1; j < rowLength; j++) { + // Check if the characters match, which makes a 'substitution' or 'transposition' free (since they match, and so aren't really an edit) + const eq = s1.charCodeAt(i - 1) === s2.charCodeAt(j - 1); + // First, assume delete is cheapest + let min = previous[j] + DELETE_COST; + // Then calculate an insertion's cost + let potentialDist = current[j - 1] + INSERT_COST; + // And see if it's cheaper + if (potentialDist < min) min = potentialDist; + // Then check against the cost of a substitution + if ((potentialDist = previous[j - 1] + (eq ? 0 : CHANGE_COST)) < min) min = potentialDist; + // And finally check if a transposition is cheapest if one is present + if ((potentialDist = (i > 1 && j > 1 && s1.charCodeAt(i - 1) === s2.charCodeAt(j - 2) && s1.charCodeAt(i - 2) === s2.charCodeAt(j - 1)) ? (prevprev[j - 2] + (eq ? 0 : TRANSPOSE_COST)) : Infinity) < min) min = potentialDist; + // Then actually set the element to the minimal cost for that edit + current[j] = min; } + // shift current back to previous, and then reuse previous' array - const tmp = previous; - previous = current; - current = tmp; + [prevprev, previous, current] = [previous, current, prevprev]; } - return previous[previous.length - 1]; + + return previous[rowLength - 1]; } } diff --git a/tests/baselines/reference/suggestionRankingPreferences.errors.txt b/tests/baselines/reference/suggestionRankingPreferences.errors.txt new file mode 100644 index 0000000000000..0102d0e8c8656 --- /dev/null +++ b/tests/baselines/reference/suggestionRankingPreferences.errors.txt @@ -0,0 +1,9 @@ +tests/cases/compiler/suggestionRankingPreferences.ts(3,13): error TS2552: Cannot find name 'tewnty'. Did you mean 'twenty'? + + +==== tests/cases/compiler/suggestionRankingPreferences.ts (1 errors) ==== + var twnty; + var twenty; + var which = tewnty; + ~~~~~~ +!!! error TS2552: Cannot find name 'tewnty'. Did you mean 'twenty'? \ No newline at end of file diff --git a/tests/baselines/reference/suggestionRankingPreferences.js b/tests/baselines/reference/suggestionRankingPreferences.js new file mode 100644 index 0000000000000..f5e5f80c92807 --- /dev/null +++ b/tests/baselines/reference/suggestionRankingPreferences.js @@ -0,0 +1,9 @@ +//// [suggestionRankingPreferences.ts] +var twnty; +var twenty; +var which = tewnty; + +//// [suggestionRankingPreferences.js] +var twnty; +var twenty; +var which = tewnty; diff --git a/tests/cases/compiler/suggestionRankingPreferences.ts b/tests/cases/compiler/suggestionRankingPreferences.ts new file mode 100644 index 0000000000000..cc31399bff22f --- /dev/null +++ b/tests/cases/compiler/suggestionRankingPreferences.ts @@ -0,0 +1,3 @@ +var twnty; +var twenty; +var which = tewnty; \ No newline at end of file