Skip to content

Commit f7b7dc2

Browse files
committed
Move getSpellingSuggestion to core
1 parent b172311 commit f7b7dc2

File tree

2 files changed

+204
-80
lines changed

2 files changed

+204
-80
lines changed

src/compiler/checker.ts

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

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

src/compiler/core.ts

Lines changed: 201 additions & 0 deletions
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)