Skip to content

Commit b5540a5

Browse files
committed
Move getSpellingSuggestion to core
1 parent 618da24 commit b5540a5

File tree

2 files changed

+204
-80
lines changed

2 files changed

+204
-80
lines changed

src/compiler/checker.ts

+3-80
Original file line numberDiff line numberDiff line change
@@ -16979,88 +16979,11 @@ namespace ts {
1697916979
* and 1 insertion/deletion at 3 characters)
1698016980
*/
1698116981
function getSpellingSuggestionForName(name: string, symbols: Symbol[], meaning: SymbolFlags): Symbol | undefined {
16982-
const maximumLengthDifference = Math.min(2, Math.floor(name.length * 0.34));
16983-
let bestDistance = Math.floor(name.length * 0.4) + 1; // If the best result isn't better than this, don't bother.
16984-
let bestCandidate: Symbol | undefined;
16985-
let justCheckExactMatches = false;
16986-
const nameLowerCase = name.toLowerCase();
16987-
for (const candidate of symbols) {
16982+
return getSpellingSuggestion(name, symbols, getCandidateName);
16983+
function getCandidateName(candidate: Symbol) {
1698816984
const candidateName = symbolName(candidate);
16989-
if (candidateName.charCodeAt(0) === CharacterCodes.doubleQuote
16990-
|| !(candidate.flags & meaning && Math.abs(candidateName.length - nameLowerCase.length) <= maximumLengthDifference)) {
16991-
continue;
16992-
}
16993-
const candidateNameLowerCase = candidateName.toLowerCase();
16994-
if (candidateNameLowerCase === nameLowerCase) {
16995-
return candidate;
16996-
}
16997-
if (justCheckExactMatches) {
16998-
continue;
16999-
}
17000-
if (candidateName.length < 3) {
17001-
// Don't bother, user would have noticed a 2-character name having an extra character
17002-
continue;
17003-
}
17004-
// Only care about a result better than the best so far.
17005-
const distance = levenshteinWithMax(nameLowerCase, candidateNameLowerCase, bestDistance - 1);
17006-
if (distance === undefined) {
17007-
continue;
17008-
}
17009-
if (distance < 3) {
17010-
justCheckExactMatches = true;
17011-
bestCandidate = candidate;
17012-
}
17013-
else {
17014-
Debug.assert(distance < bestDistance); // Else `levenshteinWithMax` should return undefined
17015-
bestDistance = distance;
17016-
bestCandidate = candidate;
17017-
}
16985+
return !startsWith(candidateName, "\"") && candidate.flags & meaning ? candidateName : undefined;
1701816986
}
17019-
return bestCandidate;
17020-
}
17021-
17022-
function levenshteinWithMax(s1: string, s2: string, max: number): number | undefined {
17023-
let previous = new Array(s2.length + 1);
17024-
let current = new Array(s2.length + 1);
17025-
/** Represents any value > max. We don't care about the particular value. */
17026-
const big = max + 1;
17027-
17028-
for (let i = 0; i <= s2.length; i++) {
17029-
previous[i] = i;
17030-
}
17031-
17032-
for (let i = 1; i <= s1.length; i++) {
17033-
const c1 = s1.charCodeAt(i - 1);
17034-
const minJ = i > max ? i - max : 1;
17035-
const maxJ = s2.length > max + i ? max + i : s2.length;
17036-
current[0] = i;
17037-
/** Smallest value of the matrix in the ith column. */
17038-
let colMin = i;
17039-
for (let j = 1; j < minJ; j++) {
17040-
current[j] = big;
17041-
}
17042-
for (let j = minJ; j <= maxJ; j++) {
17043-
const dist = c1 === s2.charCodeAt(j - 1)
17044-
? previous[j - 1]
17045-
: Math.min(/*delete*/ previous[j] + 1, /*insert*/ current[j - 1] + 1, /*substitute*/ previous[j - 1] + 2);
17046-
current[j] = dist;
17047-
colMin = Math.min(colMin, dist);
17048-
}
17049-
for (let j = maxJ + 1; j <= s2.length; j++) {
17050-
current[j] = big;
17051-
}
17052-
if (colMin > max) {
17053-
// Give up -- everything in this column is > max and it can't get better in future columns.
17054-
return undefined;
17055-
}
17056-
17057-
const temp = previous;
17058-
previous = current;
17059-
current = temp;
17060-
}
17061-
17062-
const res = previous[s2.length];
17063-
return res > max ? undefined : res;
1706416987
}
1706516988

1706616989
function markPropertyAsReferenced(prop: Symbol, nodeForCheckWriteOnly: Node | undefined, isThisAccess: boolean) {

src/compiler/core.ts

+201
Original file line numberDiff line numberDiff line change
@@ -1932,6 +1932,207 @@ namespace ts {
19321932
return text1 ? Comparison.GreaterThan : Comparison.LessThan;
19331933
}
19341934

1935+
/**
1936+
* Given a name and a list of names that are *not* equal to the name, return a spelling suggestion if there is one that is close enough.
1937+
* Names less than length 3 only check for case-insensitive equality, not Levenshtein distance.
1938+
*
1939+
* If there is a candidate that's the same except for case, return that.
1940+
* If there is a candidate that's within one edit of the name, return that.
1941+
* Otherwise, return the candidate with the smallest Levenshtein distance,
1942+
* except for candidates:
1943+
* * With no name
1944+
* * Whose length differs from the target name by more than 0.34 of the length of the name.
1945+
* * Whose levenshtein distance is more than 0.4 of the length of the name
1946+
* (0.4 allows 1 substitution/transposition for every 5 characters,
1947+
* and 1 insertion/deletion at 3 characters)
1948+
*/
1949+
export function getSpellingSuggestion<T>(name: string, candidates: T[], getName: (candidate: T) => string | undefined): T | undefined {
1950+
const maximumLengthDifference = Math.min(2, Math.floor(name.length * 0.34));
1951+
let bestDistance = Math.floor(name.length * 0.4) + 1; // If the best result isn't better than this, don't bother.
1952+
let bestCandidate: T | undefined;
1953+
let justCheckExactMatches = false;
1954+
const nameLowerCase = name.toLowerCase();
1955+
for (const candidate of candidates) {
1956+
const candidateName = getName(candidate);
1957+
if (candidateName !== undefined && Math.abs(candidateName.length - nameLowerCase.length) <= maximumLengthDifference) {
1958+
const candidateNameLowerCase = candidateName.toLowerCase();
1959+
if (candidateNameLowerCase === nameLowerCase) {
1960+
return candidate;
1961+
}
1962+
if (justCheckExactMatches) {
1963+
continue;
1964+
}
1965+
if (candidateName.length < 3) {
1966+
// Don't bother, user would have noticed a 2-character name having an extra character
1967+
continue;
1968+
}
1969+
// Only care about a result better than the best so far.
1970+
const distance = levenshteinWithMax(nameLowerCase, candidateNameLowerCase, bestDistance - 1);
1971+
if (distance === undefined) {
1972+
continue;
1973+
}
1974+
if (distance < 3) {
1975+
justCheckExactMatches = true;
1976+
bestCandidate = candidate;
1977+
}
1978+
else {
1979+
Debug.assert(distance < bestDistance); // Else `levenshteinWithMax` should return undefined
1980+
bestDistance = distance;
1981+
bestCandidate = candidate;
1982+
}
1983+
}
1984+
}
1985+
return bestCandidate;
1986+
}
1987+
1988+
function levenshteinWithMax(s1: string, s2: string, max: number): number | undefined {
1989+
let previous = new Array(s2.length + 1);
1990+
let current = new Array(s2.length + 1);
1991+
/** Represents any value > max. We don't care about the particular value. */
1992+
const big = max + 1;
1993+
1994+
for (let i = 0; i <= s2.length; i++) {
1995+
previous[i] = i;
1996+
}
1997+
1998+
for (let i = 1; i <= s1.length; i++) {
1999+
const c1 = s1.charCodeAt(i - 1);
2000+
const minJ = i > max ? i - max : 1;
2001+
const maxJ = s2.length > max + i ? max + i : s2.length;
2002+
current[0] = i;
2003+
/** Smallest value of the matrix in the ith column. */
2004+
let colMin = i;
2005+
for (let j = 1; j < minJ; j++) {
2006+
current[j] = big;
2007+
}
2008+
for (let j = minJ; j <= maxJ; j++) {
2009+
const dist = c1 === s2.charCodeAt(j - 1)
2010+
? previous[j - 1]
2011+
: Math.min(/*delete*/ previous[j] + 1, /*insert*/ current[j - 1] + 1, /*substitute*/ previous[j - 1] + 2);
2012+
current[j] = dist;
2013+
colMin = Math.min(colMin, dist);
2014+
}
2015+
for (let j = maxJ + 1; j <= s2.length; j++) {
2016+
current[j] = big;
2017+
}
2018+
if (colMin > max) {
2019+
// Give up -- everything in this column is > max and it can't get better in future columns.
2020+
return undefined;
2021+
}
2022+
2023+
const temp = previous;
2024+
previous = current;
2025+
current = temp;
2026+
}
2027+
2028+
const res = previous[s2.length];
2029+
return res > max ? undefined : res;
2030+
}
2031+
2032+
export function normalizeSlashes(path: string): string {
2033+
return path.replace(/\\/g, "/");
2034+
}
2035+
2036+
/**
2037+
* Returns length of path root (i.e. length of "/", "x:/", "//server/share/, file:///user/files")
2038+
*/
2039+
export function getRootLength(path: string): number {
2040+
if (path.charCodeAt(0) === CharacterCodes.slash) {
2041+
if (path.charCodeAt(1) !== CharacterCodes.slash) return 1;
2042+
const p1 = path.indexOf("/", 2);
2043+
if (p1 < 0) return 2;
2044+
const p2 = path.indexOf("/", p1 + 1);
2045+
if (p2 < 0) return p1 + 1;
2046+
return p2 + 1;
2047+
}
2048+
if (path.charCodeAt(1) === CharacterCodes.colon) {
2049+
if (path.charCodeAt(2) === CharacterCodes.slash || path.charCodeAt(2) === CharacterCodes.backslash) return 3;
2050+
}
2051+
// Per RFC 1738 'file' URI schema has the shape file://<host>/<path>
2052+
// if <host> is omitted then it is assumed that host value is 'localhost',
2053+
// however slash after the omitted <host> is not removed.
2054+
// file:///folder1/file1 - this is a correct URI
2055+
// file://folder2/file2 - this is an incorrect URI
2056+
if (path.lastIndexOf("file:///", 0) === 0) {
2057+
return "file:///".length;
2058+
}
2059+
const idx = path.indexOf("://");
2060+
if (idx !== -1) {
2061+
return idx + "://".length;
2062+
}
2063+
return 0;
2064+
}
2065+
2066+
/**
2067+
* Internally, we represent paths as strings with '/' as the directory separator.
2068+
* When we make system calls (eg: LanguageServiceHost.getDirectory()),
2069+
* we expect the host to correctly handle paths in our specified format.
2070+
*/
2071+
export const directorySeparator = "/";
2072+
const directorySeparatorCharCode = CharacterCodes.slash;
2073+
function getNormalizedParts(normalizedSlashedPath: string, rootLength: number): string[] {
2074+
const parts = normalizedSlashedPath.substr(rootLength).split(directorySeparator);
2075+
const normalized: string[] = [];
2076+
for (const part of parts) {
2077+
if (part !== ".") {
2078+
if (part === ".." && normalized.length > 0 && lastOrUndefined(normalized) !== "..") {
2079+
normalized.pop();
2080+
}
2081+
else {
2082+
// A part may be an empty string (which is 'falsy') if the path had consecutive slashes,
2083+
// e.g. "path//file.ts". Drop these before re-joining the parts.
2084+
if (part) {
2085+
normalized.push(part);
2086+
}
2087+
}
2088+
}
2089+
}
2090+
2091+
return normalized;
2092+
}
2093+
2094+
export function normalizePath(path: string): string {
2095+
return normalizePathAndParts(path).path;
2096+
}
2097+
2098+
export function normalizePathAndParts(path: string): { path: string, parts: string[] } {
2099+
path = normalizeSlashes(path);
2100+
const rootLength = getRootLength(path);
2101+
const root = path.substr(0, rootLength);
2102+
const parts = getNormalizedParts(path, rootLength);
2103+
if (parts.length) {
2104+
const joinedParts = root + parts.join(directorySeparator);
2105+
return { path: pathEndsWithDirectorySeparator(path) ? joinedParts + directorySeparator : joinedParts, parts };
2106+
}
2107+
else {
2108+
return { path: root, parts };
2109+
}
2110+
}
2111+
2112+
/** A path ending with '/' refers to a directory only, never a file. */
2113+
export function pathEndsWithDirectorySeparator(path: string): boolean {
2114+
return path.charCodeAt(path.length - 1) === directorySeparatorCharCode;
2115+
}
2116+
2117+
/**
2118+
* Returns the path except for its basename. Eg:
2119+
*
2120+
* /path/to/file.ext -> /path/to
2121+
*/
2122+
export function getDirectoryPath(path: Path): Path;
2123+
export function getDirectoryPath(path: string): string;
2124+
export function getDirectoryPath(path: string): string {
2125+
return path.substr(0, Math.max(getRootLength(path), path.lastIndexOf(directorySeparator)));
2126+
}
2127+
2128+
export function isUrl(path: string) {
2129+
return path && !isRootedDiskPath(path) && stringContains(path, "://");
2130+
}
2131+
2132+
export function pathIsRelative(path: string): boolean {
2133+
return /^\.\.?($|[\\/])/.test(path);
2134+
}
2135+
19352136
export function getEmitScriptTarget(compilerOptions: CompilerOptions) {
19362137
return compilerOptions.target || ScriptTarget.ES3;
19372138
}

0 commit comments

Comments
 (0)