Skip to content

Commit 2345ecd

Browse files
committed
Suggest spelling for unknown symbols + properties
1 parent 20bba9c commit 2345ecd

File tree

4 files changed

+187
-50
lines changed

4 files changed

+187
-50
lines changed

src/compiler/checker.ts

Lines changed: 150 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,9 @@ namespace ts {
204204
// since we are only interested in declarations of the module itself
205205
return tryFindAmbientModule(moduleName, /*withAugmentations*/ false);
206206
},
207-
getApparentType
207+
getApparentType,
208+
getSuggestionForNonexistentProperty,
209+
getSuggestionForNonexistentSymbol,
208210
};
209211

210212
const tupleTypes: GenericType[] = [];
@@ -840,7 +842,25 @@ namespace ts {
840842
// Resolve a given name for a given meaning at a given location. An error is reported if the name was not found and
841843
// the nameNotFoundMessage argument is not undefined. Returns the resolved symbol, or undefined if no symbol with
842844
// the given name can be found.
843-
function resolveName(location: Node | undefined, name: string, meaning: SymbolFlags, nameNotFoundMessage: DiagnosticMessage, nameArg: string | Identifier): Symbol {
845+
function resolveName(
846+
location: Node | undefined,
847+
name: string,
848+
meaning: SymbolFlags,
849+
nameNotFoundMessage: DiagnosticMessage,
850+
nameArg: string | Identifier,
851+
suggestedNameNotFoundMessage?: DiagnosticMessage): Symbol {
852+
return resolveNameHelper(location, name, meaning, nameNotFoundMessage, nameArg, getSymbol, suggestedNameNotFoundMessage);
853+
}
854+
855+
function resolveNameHelper(
856+
location: Node | undefined,
857+
name: string,
858+
meaning: SymbolFlags,
859+
nameNotFoundMessage: DiagnosticMessage,
860+
nameArg: string | Identifier,
861+
lookup: (symbols: SymbolTable, name: string, meaning: SymbolFlags) => Symbol,
862+
suggestedNameNotFoundMessage?: DiagnosticMessage): Symbol {
863+
const originalLocation = location; // needed for did-you-mean error reporting, which gathers candidates starting from the original location
844864
let result: Symbol;
845865
let lastLocation: Node;
846866
let propertyWithInvalidInitializer: Node;
@@ -851,7 +871,7 @@ namespace ts {
851871
loop: while (location) {
852872
// Locals of a source file are not in scope (because they get merged into the global symbol table)
853873
if (location.locals && !isGlobalSourceFile(location)) {
854-
if (result = getSymbol(location.locals, name, meaning)) {
874+
if (result = lookup(location.locals, name, meaning)) {
855875
let useResult = true;
856876
if (isFunctionLike(location) && lastLocation && lastLocation !== (<FunctionLikeDeclaration>location).body) {
857877
// symbol lookup restrictions for function-like declarations
@@ -929,12 +949,12 @@ namespace ts {
929949
}
930950
}
931951

932-
if (result = getSymbol(moduleExports, name, meaning & SymbolFlags.ModuleMember)) {
952+
if (result = lookup(moduleExports, name, meaning & SymbolFlags.ModuleMember)) {
933953
break loop;
934954
}
935955
break;
936956
case SyntaxKind.EnumDeclaration:
937-
if (result = getSymbol(getSymbolOfNode(location).exports, name, meaning & SymbolFlags.EnumMember)) {
957+
if (result = lookup(getSymbolOfNode(location).exports, name, meaning & SymbolFlags.EnumMember)) {
938958
break loop;
939959
}
940960
break;
@@ -949,7 +969,7 @@ namespace ts {
949969
if (isClassLike(location.parent) && !(getModifierFlags(location) & ModifierFlags.Static)) {
950970
const ctor = findConstructorDeclaration(<ClassLikeDeclaration>location.parent);
951971
if (ctor && ctor.locals) {
952-
if (getSymbol(ctor.locals, name, meaning & SymbolFlags.Value)) {
972+
if (lookup(ctor.locals, name, meaning & SymbolFlags.Value)) {
953973
// Remember the property node, it will be used later to report appropriate error
954974
propertyWithInvalidInitializer = location;
955975
}
@@ -959,7 +979,7 @@ namespace ts {
959979
case SyntaxKind.ClassDeclaration:
960980
case SyntaxKind.ClassExpression:
961981
case SyntaxKind.InterfaceDeclaration:
962-
if (result = getSymbol(getSymbolOfNode(location).members, name, meaning & SymbolFlags.Type)) {
982+
if (result = lookup(getSymbolOfNode(location).members, name, meaning & SymbolFlags.Type)) {
963983
if (!isTypeParameterSymbolDeclaredInContainer(result, location)) {
964984
// ignore type parameters not declared in this container
965985
result = undefined;
@@ -995,7 +1015,7 @@ namespace ts {
9951015
grandparent = location.parent.parent;
9961016
if (isClassLike(grandparent) || grandparent.kind === SyntaxKind.InterfaceDeclaration) {
9971017
// A reference to this grandparent's type parameters would be an error
998-
if (result = getSymbol(getSymbolOfNode(grandparent).members, name, meaning & SymbolFlags.Type)) {
1018+
if (result = lookup(getSymbolOfNode(grandparent).members, name, meaning & SymbolFlags.Type)) {
9991019
error(errorLocation, Diagnostics.A_computed_property_name_cannot_reference_a_type_parameter_from_its_containing_type);
10001020
return undefined;
10011021
}
@@ -1059,7 +1079,7 @@ namespace ts {
10591079
}
10601080

10611081
if (!result) {
1062-
result = getSymbol(globals, name, meaning);
1082+
result = lookup(globals, name, meaning);
10631083
}
10641084

10651085
if (!result) {
@@ -1070,7 +1090,16 @@ namespace ts {
10701090
!checkAndReportErrorForUsingTypeAsNamespace(errorLocation, name, meaning) &&
10711091
!checkAndReportErrorForUsingTypeAsValue(errorLocation, name, meaning) &&
10721092
!checkAndReportErrorForUsingNamespaceModuleAsValue(errorLocation, name, meaning)) {
1073-
error(errorLocation, nameNotFoundMessage, typeof nameArg === "string" ? nameArg : declarationNameToString(nameArg));
1093+
let suggestion: string | undefined;
1094+
if (suggestedNameNotFoundMessage) {
1095+
suggestion = getSuggestionForNonexistentSymbol(originalLocation, name, meaning);
1096+
if (suggestion) {
1097+
error(errorLocation, suggestedNameNotFoundMessage, typeof nameArg === "string" ? nameArg : declarationNameToString(nameArg), suggestion);
1098+
}
1099+
}
1100+
if (!suggestion) {
1101+
error(errorLocation, nameNotFoundMessage, typeof nameArg === "string" ? nameArg : declarationNameToString(nameArg));
1102+
}
10741103
}
10751104
}
10761105
return undefined;
@@ -10411,7 +10440,7 @@ namespace ts {
1041110440
function getResolvedSymbol(node: Identifier): Symbol {
1041210441
const links = getNodeLinks(node);
1041310442
if (!links.resolvedSymbol) {
10414-
links.resolvedSymbol = !nodeIsMissing(node) && resolveName(node, node.text, SymbolFlags.Value | SymbolFlags.ExportValue, Diagnostics.Cannot_find_name_0, node) || unknownSymbol;
10443+
links.resolvedSymbol = !nodeIsMissing(node) && resolveName(node, node.text, SymbolFlags.Value | SymbolFlags.ExportValue, Diagnostics.Cannot_find_name_0, node, Diagnostics.Cannot_find_name_0_Did_you_mean_1) || unknownSymbol;
1041510444
}
1041610445
return links.resolvedSymbol;
1041710446
}
@@ -14051,44 +14080,6 @@ namespace ts {
1405114080
return checkPropertyAccessExpressionOrQualifiedName(node, node.left, node.right);
1405214081
}
1405314082

14054-
function reportNonexistentProperty(propNode: Identifier, containingType: Type) {
14055-
let errorInfo: DiagnosticMessageChain;
14056-
if (containingType.flags & TypeFlags.Union && !(containingType.flags & TypeFlags.Primitive)) {
14057-
for (const subtype of (containingType as UnionType).types) {
14058-
if (!getPropertyOfType(subtype, propNode.text)) {
14059-
errorInfo = chainDiagnosticMessages(errorInfo, Diagnostics.Property_0_does_not_exist_on_type_1, declarationNameToString(propNode), typeToString(subtype));
14060-
break;
14061-
}
14062-
}
14063-
}
14064-
errorInfo = chainDiagnosticMessages(errorInfo, Diagnostics.Property_0_does_not_exist_on_type_1, declarationNameToString(propNode), typeToString(containingType));
14065-
diagnostics.add(createDiagnosticForNodeFromMessageChain(propNode, errorInfo));
14066-
}
14067-
14068-
function markPropertyAsReferenced(prop: Symbol) {
14069-
if (prop &&
14070-
noUnusedIdentifiers &&
14071-
(prop.flags & SymbolFlags.ClassMember) &&
14072-
prop.valueDeclaration && (getModifierFlags(prop.valueDeclaration) & ModifierFlags.Private)) {
14073-
if (getCheckFlags(prop) & CheckFlags.Instantiated) {
14074-
getSymbolLinks(prop).target.isReferenced = true;
14075-
}
14076-
else {
14077-
prop.isReferenced = true;
14078-
}
14079-
}
14080-
}
14081-
14082-
function isInPropertyInitializer(node: Node): boolean {
14083-
while (node) {
14084-
if (node.parent && node.parent.kind === SyntaxKind.PropertyDeclaration && (node.parent as PropertyDeclaration).initializer === node) {
14085-
return true;
14086-
}
14087-
node = node.parent;
14088-
}
14089-
return false;
14090-
}
14091-
1409214083
function checkPropertyAccessExpressionOrQualifiedName(node: PropertyAccessExpression | QualifiedName, left: Expression | QualifiedName, right: Identifier) {
1409314084
const type = checkNonNullExpression(left);
1409414085
if (isTypeAny(type) || type === silentNeverType) {
@@ -14152,6 +14143,116 @@ namespace ts {
1415214143
return assignmentKind ? getBaseTypeOfLiteralType(flowType) : flowType;
1415314144
}
1415414145

14146+
function reportNonexistentProperty(propNode: Identifier, containingType: Type) {
14147+
let errorInfo: DiagnosticMessageChain;
14148+
if (containingType.flags & TypeFlags.Union && !(containingType.flags & TypeFlags.Primitive)) {
14149+
for (const subtype of (containingType as UnionType).types) {
14150+
if (!getPropertyOfType(subtype, propNode.text)) {
14151+
errorInfo = chainDiagnosticMessages(errorInfo, Diagnostics.Property_0_does_not_exist_on_type_1, declarationNameToString(propNode), typeToString(subtype));
14152+
break;
14153+
}
14154+
}
14155+
}
14156+
const suggestion = getSuggestionForNonexistentProperty(propNode, containingType);
14157+
if (suggestion) {
14158+
errorInfo = chainDiagnosticMessages(errorInfo, Diagnostics.Property_0_does_not_exist_on_type_1_Did_you_mean_2, declarationNameToString(propNode), typeToString(containingType), suggestion);
14159+
}
14160+
else {
14161+
errorInfo = chainDiagnosticMessages(errorInfo, Diagnostics.Property_0_does_not_exist_on_type_1, declarationNameToString(propNode), typeToString(containingType));
14162+
}
14163+
diagnostics.add(createDiagnosticForNodeFromMessageChain(propNode, errorInfo));
14164+
}
14165+
14166+
function getSuggestionForNonexistentProperty(node: Identifier, containingType: Type): string | undefined {
14167+
const suggestion = getSpellingSuggestionForName(node.text, getPropertiesOfObjectType(containingType), SymbolFlags.Value);
14168+
return suggestion && suggestion.name;
14169+
}
14170+
14171+
function getSuggestionForNonexistentSymbol(location: Node, name: string, meaning: SymbolFlags): string {
14172+
const result = resolveNameHelper(location, name, meaning, /*nameNotFoundMessage*/ undefined, name, (symbols, name, meaning) => {
14173+
const symbol = getSymbol(symbols, name, meaning);
14174+
if (symbol) {
14175+
// Sometimes the symbol is found when location is a return type of a function: `typeof x` and `x` is declared in the body of the function
14176+
// So the table *contains* `x` but `x` isn't actually in scope.
14177+
// However, resolveNameHelper will continue and call this callback again, so we'll eventually get a correct suggestion.
14178+
return symbol;
14179+
}
14180+
return getSpellingSuggestionForName(name, arrayFrom(symbols.values()), meaning);
14181+
});
14182+
if (result) {
14183+
return result.name;
14184+
}
14185+
}
14186+
14187+
/**
14188+
* Given a name and a list of symbols whose names are *not* equal to the name, return a spelling suggestion if there is one that is close enough.
14189+
* Names less than length 3 only check for case-insensitive equality, not levenshtein distance.
14190+
*
14191+
* If there is a candidate that's the same except for case, return that.
14192+
* If there is a candidate that's within one edit of the name, return that.
14193+
* Otherwise, return the candidate with the smallest Levenshtein distance,
14194+
* except for candidates:
14195+
* * With no name
14196+
* * Whose meaning doesn't match the `meaning` parameter.
14197+
* * Whose length differs from the target name by more than 3.
14198+
* * Whose levenshtein distance is more than 0.7 of the length of the name
14199+
* (0.7 allows identifiers of length 3 to have a distance of 2 to allow for one substitution)
14200+
* Names longer than 30 characters don't get suggestions because Levenshtein distance is an n**2 algorithm.
14201+
*/
14202+
function getSpellingSuggestionForName(name: string, symbols: Symbol[], meaning: SymbolFlags): Symbol | undefined {
14203+
const worstDistance = name.length * 0.7;
14204+
let bestDistance = Number.MAX_VALUE;
14205+
let bestCandidate = undefined;
14206+
if (name.length > 30) {
14207+
return undefined;
14208+
}
14209+
name = name.toLowerCase();
14210+
for (const candidate of symbols) {
14211+
if (candidate.flags & meaning && candidate.name && Math.abs(candidate.name.length - name.length) < 4) {
14212+
const candidateName = candidate.name.toLowerCase();
14213+
if (candidateName === name) {
14214+
return candidate;
14215+
}
14216+
if (candidateName.length < 3) {
14217+
continue;
14218+
}
14219+
const distance = levenshtein(candidateName, name);
14220+
if (distance < 2) {
14221+
return candidate;
14222+
}
14223+
else if (distance < bestDistance && distance < worstDistance) {
14224+
bestDistance = distance;
14225+
bestCandidate = candidate;
14226+
}
14227+
}
14228+
}
14229+
return bestCandidate;
14230+
}
14231+
14232+
function markPropertyAsReferenced(prop: Symbol) {
14233+
if (prop &&
14234+
noUnusedIdentifiers &&
14235+
(prop.flags & SymbolFlags.ClassMember) &&
14236+
prop.valueDeclaration && (getModifierFlags(prop.valueDeclaration) & ModifierFlags.Private)) {
14237+
if (getCheckFlags(prop) & CheckFlags.Instantiated) {
14238+
getSymbolLinks(prop).target.isReferenced = true;
14239+
}
14240+
else {
14241+
prop.isReferenced = true;
14242+
}
14243+
}
14244+
}
14245+
14246+
function isInPropertyInitializer(node: Node): boolean {
14247+
while (node) {
14248+
if (node.parent && node.parent.kind === SyntaxKind.PropertyDeclaration && (node.parent as PropertyDeclaration).initializer === node) {
14249+
return true;
14250+
}
14251+
node = node.parent;
14252+
}
14253+
return false;
14254+
}
14255+
1415514256
function isValidPropertyAccess(node: PropertyAccessExpression | QualifiedName, propertyName: string): boolean {
1415614257
const left = node.kind === SyntaxKind.PropertyAccessExpression
1415714258
? (<PropertyAccessExpression>node).expression

src/compiler/diagnosticMessages.json

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1843,6 +1843,14 @@
18431843
"category": "Error",
18441844
"code": 2550
18451845
},
1846+
"Property '{0}' does not exist on type '{1}'. Did you mean '{2}'?": {
1847+
"category": "Error",
1848+
"code": 2551
1849+
},
1850+
"Cannot find name '{0}'. Did you mean '{1}'?": {
1851+
"category": "Error",
1852+
"code": 2552
1853+
},
18461854
"JSX element attributes type '{0}' may not be a union type.": {
18471855
"category": "Error",
18481856
"code": 2600
@@ -3532,7 +3540,10 @@
35323540
"category": "Message",
35333541
"code": 90021
35343542
},
3535-
3543+
"Change spelling to '{0}'.": {
3544+
"category": "Message",
3545+
"code": 90022
3546+
},
35363547

35373548
"Octal literal types must use ES2015 syntax. Use the syntax '{0}'.": {
35383549
"category": "Error",

src/compiler/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2545,6 +2545,8 @@ namespace ts {
25452545

25462546
tryGetMemberInModuleExports(memberName: string, moduleSymbol: Symbol): Symbol | undefined;
25472547
getApparentType(type: Type): Type;
2548+
getSuggestionForNonexistentProperty(node: Identifier, containingType: Type): string | undefined;
2549+
getSuggestionForNonexistentSymbol(location: Node, name: string, meaning: SymbolFlags): string;
25482550

25492551
/* @internal */ tryFindAmbientModuleWithoutAugmentations(moduleName: string): Symbol;
25502552

src/compiler/utilities.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4629,4 +4629,27 @@ namespace ts {
46294629
export function unescapeIdentifier(identifier: string): string {
46304630
return identifier.length >= 3 && identifier.charCodeAt(0) === CharacterCodes._ && identifier.charCodeAt(1) === CharacterCodes._ && identifier.charCodeAt(2) === CharacterCodes._ ? identifier.substr(1) : identifier;
46314631
}
4632+
4633+
export function levenshtein(s1: string, s2: string): number {
4634+
let previous: number[] = new Array(s2.length + 1);
4635+
let current: number[] = new Array(s2.length + 1);
4636+
for (let i = 0; i < s2.length + 1; i++) {
4637+
previous[i] = i;
4638+
current[i] = -1;
4639+
}
4640+
for (let i = 1; i < s1.length + 1; i++) {
4641+
current[0] = i;
4642+
for (let j = 1; j < s2.length + 1; j++) {
4643+
current[j] = Math.min(
4644+
previous[j] + 1,
4645+
current[j - 1] + 1,
4646+
previous[j - 1] + (s1[i - 1] === s2[j - 1] ? 0 : 2))
4647+
}
4648+
// shift current back to previous, and then reuse previous' array
4649+
const tmp = previous;
4650+
previous = current;
4651+
current = tmp;
4652+
}
4653+
return previous[previous.length - 1];
4654+
}
46324655
}

0 commit comments

Comments
 (0)