mirror of
https://github.com/microsoft/TypeScript.git
synced 2025-11-18 17:21:48 +00:00
Suggest spelling for unknown symbols + properties
This commit is contained in:
+150
-49
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user