Permalink
Browse files

Suggest spelling for unknown symbols + properties

  • Loading branch information...
sandersn committed May 1, 2017
1 parent 20bba9c commit 2345ecdcb84ff91dc5a9b2221a9fccd1d43c670a
Showing with 187 additions and 50 deletions.
  1. +150 −49 src/compiler/checker.ts
  2. +12 −1 src/compiler/diagnosticMessages.json
  3. +2 −0 src/compiler/types.ts
  4. +23 −0 src/compiler/utilities.ts
View
@@ -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 !== (<FunctionLikeDeclaration>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(<ClassLikeDeclaration>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
? (<PropertyAccessExpression>node).expression
@@ -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",
View
@@ -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;
View
@@ -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];
}
}

0 comments on commit 2345ecd

Please sign in to comment.