diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 18e57303c12..7b7aaf9937c 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -204,7 +204,9 @@ namespace ts { // since we are only interested in declarations of the module itself return tryFindAmbientModule(moduleName, /*withAugmentations*/ false); }, - getApparentType + getApparentType, + getSuggestionForNonexistentProperty, + getSuggestionForNonexistentSymbol, }; const tupleTypes: GenericType[] = []; @@ -840,7 +842,25 @@ namespace ts { // Resolve a given name for a given meaning at a given location. An error is reported if the name was not found and // the nameNotFoundMessage argument is not undefined. Returns the resolved symbol, or undefined if no symbol with // the given name can be found. - function resolveName(location: Node | undefined, name: string, meaning: SymbolFlags, nameNotFoundMessage: DiagnosticMessage, nameArg: string | Identifier): Symbol { + function resolveName( + location: Node | undefined, + name: string, + meaning: SymbolFlags, + nameNotFoundMessage: DiagnosticMessage, + nameArg: string | Identifier, + suggestedNameNotFoundMessage?: DiagnosticMessage): Symbol { + return resolveNameHelper(location, name, meaning, nameNotFoundMessage, nameArg, getSymbol, suggestedNameNotFoundMessage); + } + + function resolveNameHelper( + location: Node | undefined, + name: string, + meaning: SymbolFlags, + nameNotFoundMessage: DiagnosticMessage, + nameArg: string | Identifier, + lookup: (symbols: SymbolTable, name: string, meaning: SymbolFlags) => Symbol, + suggestedNameNotFoundMessage?: DiagnosticMessage): Symbol { + const originalLocation = location; // needed for did-you-mean error reporting, which gathers candidates starting from the original location let result: Symbol; let lastLocation: Node; let propertyWithInvalidInitializer: Node; @@ -851,7 +871,7 @@ namespace ts { loop: while (location) { // Locals of a source file are not in scope (because they get merged into the global symbol table) if (location.locals && !isGlobalSourceFile(location)) { - if (result = getSymbol(location.locals, name, meaning)) { + if (result = lookup(location.locals, name, meaning)) { let useResult = true; if (isFunctionLike(location) && lastLocation && lastLocation !== (location).body) { // symbol lookup restrictions for function-like declarations @@ -929,12 +949,12 @@ namespace ts { } } - if (result = getSymbol(moduleExports, name, meaning & SymbolFlags.ModuleMember)) { + if (result = lookup(moduleExports, name, meaning & SymbolFlags.ModuleMember)) { break loop; } break; case SyntaxKind.EnumDeclaration: - if (result = getSymbol(getSymbolOfNode(location).exports, name, meaning & SymbolFlags.EnumMember)) { + if (result = lookup(getSymbolOfNode(location).exports, name, meaning & SymbolFlags.EnumMember)) { break loop; } break; @@ -949,7 +969,7 @@ namespace ts { if (isClassLike(location.parent) && !(getModifierFlags(location) & ModifierFlags.Static)) { const ctor = findConstructorDeclaration(location.parent); if (ctor && ctor.locals) { - if (getSymbol(ctor.locals, name, meaning & SymbolFlags.Value)) { + if (lookup(ctor.locals, name, meaning & SymbolFlags.Value)) { // Remember the property node, it will be used later to report appropriate error propertyWithInvalidInitializer = location; } @@ -959,7 +979,7 @@ namespace ts { case SyntaxKind.ClassDeclaration: case SyntaxKind.ClassExpression: case SyntaxKind.InterfaceDeclaration: - if (result = getSymbol(getSymbolOfNode(location).members, name, meaning & SymbolFlags.Type)) { + if (result = lookup(getSymbolOfNode(location).members, name, meaning & SymbolFlags.Type)) { if (!isTypeParameterSymbolDeclaredInContainer(result, location)) { // ignore type parameters not declared in this container result = undefined; @@ -995,7 +1015,7 @@ namespace ts { grandparent = location.parent.parent; if (isClassLike(grandparent) || grandparent.kind === SyntaxKind.InterfaceDeclaration) { // A reference to this grandparent's type parameters would be an error - if (result = getSymbol(getSymbolOfNode(grandparent).members, name, meaning & SymbolFlags.Type)) { + if (result = lookup(getSymbolOfNode(grandparent).members, name, meaning & SymbolFlags.Type)) { error(errorLocation, Diagnostics.A_computed_property_name_cannot_reference_a_type_parameter_from_its_containing_type); return undefined; } @@ -1059,7 +1079,7 @@ namespace ts { } if (!result) { - result = getSymbol(globals, name, meaning); + result = lookup(globals, name, meaning); } if (!result) { @@ -1070,7 +1090,16 @@ namespace ts { !checkAndReportErrorForUsingTypeAsNamespace(errorLocation, name, meaning) && !checkAndReportErrorForUsingTypeAsValue(errorLocation, name, meaning) && !checkAndReportErrorForUsingNamespaceModuleAsValue(errorLocation, name, meaning)) { - error(errorLocation, nameNotFoundMessage, typeof nameArg === "string" ? nameArg : declarationNameToString(nameArg)); + let suggestion: string | undefined; + if (suggestedNameNotFoundMessage) { + suggestion = getSuggestionForNonexistentSymbol(originalLocation, name, meaning); + if (suggestion) { + error(errorLocation, suggestedNameNotFoundMessage, typeof nameArg === "string" ? nameArg : declarationNameToString(nameArg), suggestion); + } + } + if (!suggestion) { + error(errorLocation, nameNotFoundMessage, typeof nameArg === "string" ? nameArg : declarationNameToString(nameArg)); + } } } return undefined; @@ -10411,7 +10440,7 @@ namespace ts { function getResolvedSymbol(node: Identifier): Symbol { const links = getNodeLinks(node); if (!links.resolvedSymbol) { - links.resolvedSymbol = !nodeIsMissing(node) && resolveName(node, node.text, SymbolFlags.Value | SymbolFlags.ExportValue, Diagnostics.Cannot_find_name_0, node) || unknownSymbol; + 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; } return links.resolvedSymbol; } @@ -14051,44 +14080,6 @@ namespace ts { return checkPropertyAccessExpressionOrQualifiedName(node, node.left, node.right); } - function reportNonexistentProperty(propNode: Identifier, containingType: Type) { - let errorInfo: DiagnosticMessageChain; - if (containingType.flags & TypeFlags.Union && !(containingType.flags & TypeFlags.Primitive)) { - for (const subtype of (containingType as UnionType).types) { - if (!getPropertyOfType(subtype, propNode.text)) { - errorInfo = chainDiagnosticMessages(errorInfo, Diagnostics.Property_0_does_not_exist_on_type_1, declarationNameToString(propNode), typeToString(subtype)); - break; - } - } - } - errorInfo = chainDiagnosticMessages(errorInfo, Diagnostics.Property_0_does_not_exist_on_type_1, declarationNameToString(propNode), typeToString(containingType)); - diagnostics.add(createDiagnosticForNodeFromMessageChain(propNode, errorInfo)); - } - - function markPropertyAsReferenced(prop: Symbol) { - if (prop && - noUnusedIdentifiers && - (prop.flags & SymbolFlags.ClassMember) && - prop.valueDeclaration && (getModifierFlags(prop.valueDeclaration) & ModifierFlags.Private)) { - if (getCheckFlags(prop) & CheckFlags.Instantiated) { - getSymbolLinks(prop).target.isReferenced = true; - } - else { - prop.isReferenced = true; - } - } - } - - function isInPropertyInitializer(node: Node): boolean { - while (node) { - if (node.parent && node.parent.kind === SyntaxKind.PropertyDeclaration && (node.parent as PropertyDeclaration).initializer === node) { - return true; - } - node = node.parent; - } - return false; - } - function checkPropertyAccessExpressionOrQualifiedName(node: PropertyAccessExpression | QualifiedName, left: Expression | QualifiedName, right: Identifier) { const type = checkNonNullExpression(left); if (isTypeAny(type) || type === silentNeverType) { @@ -14152,6 +14143,116 @@ namespace ts { return assignmentKind ? getBaseTypeOfLiteralType(flowType) : flowType; } + function reportNonexistentProperty(propNode: Identifier, containingType: Type) { + let errorInfo: DiagnosticMessageChain; + if (containingType.flags & TypeFlags.Union && !(containingType.flags & TypeFlags.Primitive)) { + for (const subtype of (containingType as UnionType).types) { + if (!getPropertyOfType(subtype, propNode.text)) { + errorInfo = chainDiagnosticMessages(errorInfo, Diagnostics.Property_0_does_not_exist_on_type_1, declarationNameToString(propNode), typeToString(subtype)); + break; + } + } + } + const suggestion = getSuggestionForNonexistentProperty(propNode, containingType); + if (suggestion) { + errorInfo = chainDiagnosticMessages(errorInfo, Diagnostics.Property_0_does_not_exist_on_type_1_Did_you_mean_2, declarationNameToString(propNode), typeToString(containingType), suggestion); + } + else { + errorInfo = chainDiagnosticMessages(errorInfo, Diagnostics.Property_0_does_not_exist_on_type_1, declarationNameToString(propNode), typeToString(containingType)); + } + diagnostics.add(createDiagnosticForNodeFromMessageChain(propNode, errorInfo)); + } + + function getSuggestionForNonexistentProperty(node: Identifier, containingType: Type): string | undefined { + const suggestion = getSpellingSuggestionForName(node.text, getPropertiesOfObjectType(containingType), SymbolFlags.Value); + return suggestion && suggestion.name; + } + + function getSuggestionForNonexistentSymbol(location: Node, name: string, meaning: SymbolFlags): string { + const result = resolveNameHelper(location, name, meaning, /*nameNotFoundMessage*/ undefined, name, (symbols, name, meaning) => { + const symbol = getSymbol(symbols, name, meaning); + if (symbol) { + // 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 + // So the table *contains* `x` but `x` isn't actually in scope. + // However, resolveNameHelper will continue and call this callback again, so we'll eventually get a correct suggestion. + return symbol; + } + return getSpellingSuggestionForName(name, arrayFrom(symbols.values()), meaning); + }); + if (result) { + return result.name; + } + } + + /** + * 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. + * Names less than length 3 only check for case-insensitive equality, not levenshtein distance. + * + * If there is a candidate that's the same except for case, return that. + * If there is a candidate that's within one edit of the name, return that. + * Otherwise, return the candidate with the smallest Levenshtein distance, + * except for candidates: + * * With no name + * * Whose meaning doesn't match the `meaning` parameter. + * * Whose length differs from the target name by more than 3. + * * Whose levenshtein distance is more than 0.7 of the length of the name + * (0.7 allows identifiers of length 3 to have a distance of 2 to allow for one substitution) + * 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 { + const worstDistance = name.length * 0.7; + let bestDistance = Number.MAX_VALUE; + let bestCandidate = undefined; + if (name.length > 30) { + return undefined; + } + name = name.toLowerCase(); + for (const candidate of symbols) { + if (candidate.flags & meaning && candidate.name && Math.abs(candidate.name.length - name.length) < 4) { + const candidateName = candidate.name.toLowerCase(); + if (candidateName === name) { + return candidate; + } + if (candidateName.length < 3) { + continue; + } + const distance = levenshtein(candidateName, name); + if (distance < 2) { + return candidate; + } + else if (distance < bestDistance && distance < worstDistance) { + bestDistance = distance; + bestCandidate = candidate; + } + } + } + return bestCandidate; + } + + function markPropertyAsReferenced(prop: Symbol) { + if (prop && + noUnusedIdentifiers && + (prop.flags & SymbolFlags.ClassMember) && + prop.valueDeclaration && (getModifierFlags(prop.valueDeclaration) & ModifierFlags.Private)) { + if (getCheckFlags(prop) & CheckFlags.Instantiated) { + getSymbolLinks(prop).target.isReferenced = true; + } + else { + prop.isReferenced = true; + } + } + } + + function isInPropertyInitializer(node: Node): boolean { + while (node) { + if (node.parent && node.parent.kind === SyntaxKind.PropertyDeclaration && (node.parent as PropertyDeclaration).initializer === node) { + return true; + } + node = node.parent; + } + return false; + } + function isValidPropertyAccess(node: PropertyAccessExpression | QualifiedName, propertyName: string): boolean { const left = node.kind === SyntaxKind.PropertyAccessExpression ? (node).expression diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index b69adf708f2..b205cb85cb6 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -1843,6 +1843,14 @@ "category": "Error", "code": 2550 }, + "Property '{0}' does not exist on type '{1}'. Did you mean '{2}'?": { + "category": "Error", + "code": 2551 + }, + "Cannot find name '{0}'. Did you mean '{1}'?": { + "category": "Error", + "code": 2552 + }, "JSX element attributes type '{0}' may not be a union type.": { "category": "Error", "code": 2600 @@ -3532,7 +3540,10 @@ "category": "Message", "code": 90021 }, - + "Change spelling to '{0}'.": { + "category": "Message", + "code": 90022 + }, "Octal literal types must use ES2015 syntax. Use the syntax '{0}'.": { "category": "Error", diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 57cf17f7100..a923a8fff73 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2545,6 +2545,8 @@ namespace ts { tryGetMemberInModuleExports(memberName: string, moduleSymbol: Symbol): Symbol | undefined; getApparentType(type: Type): Type; + getSuggestionForNonexistentProperty(node: Identifier, containingType: Type): string | undefined; + getSuggestionForNonexistentSymbol(location: Node, name: string, meaning: SymbolFlags): string; /* @internal */ tryFindAmbientModuleWithoutAugmentations(moduleName: string): Symbol; diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 646a7b22e3f..1ce2cf2c991 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -4629,4 +4629,27 @@ namespace ts { export function unescapeIdentifier(identifier: string): string { 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++) { + 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]; + } }