/* @internal */ namespace ts.codefix { const fixId = "inferFromUsage"; const errorCodes = [ // Variable declarations Diagnostics.Variable_0_implicitly_has_type_1_in_some_locations_where_its_type_cannot_be_determined.code, // Variable uses Diagnostics.Variable_0_implicitly_has_an_1_type.code, // Parameter declarations Diagnostics.Parameter_0_implicitly_has_an_1_type.code, Diagnostics.Rest_parameter_0_implicitly_has_an_any_type.code, // Get Accessor declarations Diagnostics.Property_0_implicitly_has_type_any_because_its_get_accessor_lacks_a_return_type_annotation.code, Diagnostics._0_which_lacks_return_type_annotation_implicitly_has_an_1_return_type.code, // Set Accessor declarations Diagnostics.Property_0_implicitly_has_type_any_because_its_set_accessor_lacks_a_parameter_type_annotation.code, // Property declarations Diagnostics.Member_0_implicitly_has_an_1_type.code, //// Suggestions // Variable declarations Diagnostics.Variable_0_implicitly_has_type_1_in_some_locations_but_a_better_type_may_be_inferred_from_usage.code, // Variable uses Diagnostics.Variable_0_implicitly_has_an_1_type_but_a_better_type_may_be_inferred_from_usage.code, // Parameter declarations Diagnostics.Parameter_0_implicitly_has_an_1_type_but_a_better_type_may_be_inferred_from_usage.code, Diagnostics.Rest_parameter_0_implicitly_has_an_any_type_but_a_better_type_may_be_inferred_from_usage.code, // Get Accessor declarations Diagnostics.Property_0_implicitly_has_type_any_but_a_better_type_for_its_get_accessor_may_be_inferred_from_usage.code, Diagnostics._0_implicitly_has_an_1_return_type_but_a_better_type_may_be_inferred_from_usage.code, // Set Accessor declarations Diagnostics.Property_0_implicitly_has_type_any_but_a_better_type_for_its_set_accessor_may_be_inferred_from_usage.code, // Property declarations Diagnostics.Member_0_implicitly_has_an_1_type_but_a_better_type_may_be_inferred_from_usage.code, // Function expressions and declarations Diagnostics.this_implicitly_has_type_any_because_it_does_not_have_a_type_annotation.code, ]; registerCodeFix({ errorCodes, getCodeActions(context) { const { sourceFile, program, span: { start }, errorCode, cancellationToken, host, preferences } = context; const token = getTokenAtPosition(sourceFile, start); let declaration: Declaration | undefined; const changes = textChanges.ChangeTracker.with(context, changes => { declaration = doChange(changes, sourceFile, token, errorCode, program, cancellationToken, /*markSeen*/ returnTrue, host, preferences); }); const name = declaration && getNameOfDeclaration(declaration); return !name || changes.length === 0 ? undefined : [createCodeFixAction(fixId, changes, [getDiagnostic(errorCode, token), name.getText(sourceFile)], fixId, Diagnostics.Infer_all_types_from_usage)]; }, fixIds: [fixId], getAllCodeActions(context) { const { sourceFile, program, cancellationToken, host, preferences } = context; const markSeen = nodeSeenTracker(); return codeFixAll(context, errorCodes, (changes, err) => { doChange(changes, sourceFile, getTokenAtPosition(err.file, err.start), err.code, program, cancellationToken, markSeen, host, preferences); }); }, }); function getDiagnostic(errorCode: number, token: Node): DiagnosticMessage { switch (errorCode) { case Diagnostics.Parameter_0_implicitly_has_an_1_type.code: case Diagnostics.Parameter_0_implicitly_has_an_1_type_but_a_better_type_may_be_inferred_from_usage.code: return isSetAccessorDeclaration(getContainingFunction(token)!) ? Diagnostics.Infer_type_of_0_from_usage : Diagnostics.Infer_parameter_types_from_usage; // TODO: GH#18217 case Diagnostics.Rest_parameter_0_implicitly_has_an_any_type.code: case Diagnostics.Rest_parameter_0_implicitly_has_an_any_type_but_a_better_type_may_be_inferred_from_usage.code: return Diagnostics.Infer_parameter_types_from_usage; case Diagnostics.this_implicitly_has_type_any_because_it_does_not_have_a_type_annotation.code: return Diagnostics.Infer_this_type_of_0_from_usage; default: return Diagnostics.Infer_type_of_0_from_usage; } } /** Map suggestion code to error code */ function mapSuggestionDiagnostic(errorCode: number) { switch (errorCode) { case Diagnostics.Variable_0_implicitly_has_type_1_in_some_locations_but_a_better_type_may_be_inferred_from_usage.code: return Diagnostics.Variable_0_implicitly_has_type_1_in_some_locations_where_its_type_cannot_be_determined.code; case Diagnostics.Variable_0_implicitly_has_an_1_type_but_a_better_type_may_be_inferred_from_usage.code: return Diagnostics.Variable_0_implicitly_has_an_1_type.code; case Diagnostics.Parameter_0_implicitly_has_an_1_type_but_a_better_type_may_be_inferred_from_usage.code: return Diagnostics.Parameter_0_implicitly_has_an_1_type.code; case Diagnostics.Rest_parameter_0_implicitly_has_an_any_type_but_a_better_type_may_be_inferred_from_usage.code: return Diagnostics.Rest_parameter_0_implicitly_has_an_any_type.code; case Diagnostics.Property_0_implicitly_has_type_any_but_a_better_type_for_its_get_accessor_may_be_inferred_from_usage.code: return Diagnostics.Property_0_implicitly_has_type_any_because_its_get_accessor_lacks_a_return_type_annotation.code; case Diagnostics._0_implicitly_has_an_1_return_type_but_a_better_type_may_be_inferred_from_usage.code: return Diagnostics._0_which_lacks_return_type_annotation_implicitly_has_an_1_return_type.code; case Diagnostics.Property_0_implicitly_has_type_any_but_a_better_type_for_its_set_accessor_may_be_inferred_from_usage.code: return Diagnostics.Property_0_implicitly_has_type_any_because_its_set_accessor_lacks_a_parameter_type_annotation.code; case Diagnostics.Member_0_implicitly_has_an_1_type_but_a_better_type_may_be_inferred_from_usage.code: return Diagnostics.Member_0_implicitly_has_an_1_type.code; } return errorCode; } function doChange(changes: textChanges.ChangeTracker, sourceFile: SourceFile, token: Node, errorCode: number, program: Program, cancellationToken: CancellationToken, markSeen: NodeSeenTracker, host: LanguageServiceHost, preferences: UserPreferences): Declaration | undefined { if (!isParameterPropertyModifier(token.kind) && token.kind !== SyntaxKind.Identifier && token.kind !== SyntaxKind.DotDotDotToken && token.kind !== SyntaxKind.ThisKeyword) { return undefined; } const { parent } = token; const importAdder = createImportAdder(sourceFile, program, preferences, host); errorCode = mapSuggestionDiagnostic(errorCode); switch (errorCode) { // Variable and Property declarations case Diagnostics.Member_0_implicitly_has_an_1_type.code: case Diagnostics.Variable_0_implicitly_has_type_1_in_some_locations_where_its_type_cannot_be_determined.code: if ((isVariableDeclaration(parent) && markSeen(parent)) || isPropertyDeclaration(parent) || isPropertySignature(parent)) { // handle bad location annotateVariableDeclaration(changes, importAdder, sourceFile, parent, program, host, cancellationToken); importAdder.writeFixes(changes); return parent; } if (isPropertyAccessExpression(parent)) { const type = inferTypeForVariableFromUsage(parent.name, program, cancellationToken); const typeNode = getTypeNodeIfAccessible(type, parent, program, host); if (typeNode) { // Note that the codefix will never fire with an existing `@type` tag, so there is no need to merge tags const typeTag = factory.createJSDocTypeTag(/*tagName*/ undefined, factory.createJSDocTypeExpression(typeNode), /*comment*/ undefined); addJSDocTags(changes, sourceFile, cast(parent.parent.parent, isExpressionStatement), [typeTag]); } importAdder.writeFixes(changes); return parent; } return undefined; case Diagnostics.Variable_0_implicitly_has_an_1_type.code: { const symbol = program.getTypeChecker().getSymbolAtLocation(token); if (symbol && symbol.valueDeclaration && isVariableDeclaration(symbol.valueDeclaration) && markSeen(symbol.valueDeclaration)) { annotateVariableDeclaration(changes, importAdder, sourceFile, symbol.valueDeclaration, program, host, cancellationToken); importAdder.writeFixes(changes); return symbol.valueDeclaration; } return undefined; } } const containingFunction = getContainingFunction(token); if (containingFunction === undefined) { return undefined; } let declaration: Declaration | undefined; switch (errorCode) { // Parameter declarations case Diagnostics.Parameter_0_implicitly_has_an_1_type.code: if (isSetAccessorDeclaration(containingFunction)) { annotateSetAccessor(changes, importAdder, sourceFile, containingFunction, program, host, cancellationToken); declaration = containingFunction; break; } // falls through case Diagnostics.Rest_parameter_0_implicitly_has_an_any_type.code: if (markSeen(containingFunction)) { const param = cast(parent, isParameter); annotateParameters(changes, importAdder, sourceFile, param, containingFunction, program, host, cancellationToken); declaration = param; } break; // Get Accessor declarations case Diagnostics.Property_0_implicitly_has_type_any_because_its_get_accessor_lacks_a_return_type_annotation.code: case Diagnostics._0_which_lacks_return_type_annotation_implicitly_has_an_1_return_type.code: if (isGetAccessorDeclaration(containingFunction) && isIdentifier(containingFunction.name)) { annotate(changes, importAdder, sourceFile, containingFunction, inferTypeForVariableFromUsage(containingFunction.name, program, cancellationToken), program, host); declaration = containingFunction; } break; // Set Accessor declarations case Diagnostics.Property_0_implicitly_has_type_any_because_its_set_accessor_lacks_a_parameter_type_annotation.code: if (isSetAccessorDeclaration(containingFunction)) { annotateSetAccessor(changes, importAdder, sourceFile, containingFunction, program, host, cancellationToken); declaration = containingFunction; } break; // Function 'this' case Diagnostics.this_implicitly_has_type_any_because_it_does_not_have_a_type_annotation.code: if (textChanges.isThisTypeAnnotatable(containingFunction) && markSeen(containingFunction)) { annotateThis(changes, sourceFile, containingFunction, program, host, cancellationToken); declaration = containingFunction; } break; default: return Debug.fail(String(errorCode)); } importAdder.writeFixes(changes); return declaration; } function annotateVariableDeclaration( changes: textChanges.ChangeTracker, importAdder: ImportAdder, sourceFile: SourceFile, declaration: VariableDeclaration | PropertyDeclaration | PropertySignature, program: Program, host: LanguageServiceHost, cancellationToken: CancellationToken, ): void { if (isIdentifier(declaration.name)) { annotate(changes, importAdder, sourceFile, declaration, inferTypeForVariableFromUsage(declaration.name, program, cancellationToken), program, host); } } function annotateParameters( changes: textChanges.ChangeTracker, importAdder: ImportAdder, sourceFile: SourceFile, parameterDeclaration: ParameterDeclaration, containingFunction: SignatureDeclaration, program: Program, host: LanguageServiceHost, cancellationToken: CancellationToken, ): void { if (!isIdentifier(parameterDeclaration.name)) { return; } const parameterInferences = inferTypeForParametersFromUsage(containingFunction, sourceFile, program, cancellationToken); Debug.assert(containingFunction.parameters.length === parameterInferences.length, "Parameter count and inference count should match"); if (isInJSFile(containingFunction)) { annotateJSDocParameters(changes, sourceFile, parameterInferences, program, host); } else { const needParens = isArrowFunction(containingFunction) && !findChildOfKind(containingFunction, SyntaxKind.OpenParenToken, sourceFile); if (needParens) changes.insertNodeBefore(sourceFile, first(containingFunction.parameters), factory.createToken(SyntaxKind.OpenParenToken)); for (const { declaration, type } of parameterInferences) { if (declaration && !declaration.type && !declaration.initializer) { annotate(changes, importAdder, sourceFile, declaration, type, program, host); } } if (needParens) changes.insertNodeAfter(sourceFile, last(containingFunction.parameters), factory.createToken(SyntaxKind.CloseParenToken)); } } function annotateThis(changes: textChanges.ChangeTracker, sourceFile: SourceFile, containingFunction: textChanges.ThisTypeAnnotatable, program: Program, host: LanguageServiceHost, cancellationToken: CancellationToken) { const references = getFunctionReferences(containingFunction, sourceFile, program, cancellationToken); if (!references || !references.length) { return; } const thisInference = inferTypeFromReferences(program, references, cancellationToken).thisParameter(); const typeNode = getTypeNodeIfAccessible(thisInference, containingFunction, program, host); if (!typeNode) { return; } if (isInJSFile(containingFunction)) { annotateJSDocThis(changes, sourceFile, containingFunction, typeNode); } else { changes.tryInsertThisTypeAnnotation(sourceFile, containingFunction, typeNode); } } function annotateJSDocThis(changes: textChanges.ChangeTracker, sourceFile: SourceFile, containingFunction: SignatureDeclaration, typeNode: TypeNode) { addJSDocTags(changes, sourceFile, containingFunction, [ factory.createJSDocThisTag(/*tagName*/ undefined, factory.createJSDocTypeExpression(typeNode)), ]); } function annotateSetAccessor( changes: textChanges.ChangeTracker, importAdder: ImportAdder, sourceFile: SourceFile, setAccessorDeclaration: SetAccessorDeclaration, program: Program, host: LanguageServiceHost, cancellationToken: CancellationToken, ): void { const param = firstOrUndefined(setAccessorDeclaration.parameters); if (param && isIdentifier(setAccessorDeclaration.name) && isIdentifier(param.name)) { let type = inferTypeForVariableFromUsage(setAccessorDeclaration.name, program, cancellationToken); if (type === program.getTypeChecker().getAnyType()) { type = inferTypeForVariableFromUsage(param.name, program, cancellationToken); } if (isInJSFile(setAccessorDeclaration)) { annotateJSDocParameters(changes, sourceFile, [{ declaration: param, type }], program, host); } else { annotate(changes, importAdder, sourceFile, param, type, program, host); } } } function annotate(changes: textChanges.ChangeTracker, importAdder: ImportAdder, sourceFile: SourceFile, declaration: textChanges.TypeAnnotatable, type: Type, program: Program, host: LanguageServiceHost): void { const typeNode = getTypeNodeIfAccessible(type, declaration, program, host); if (typeNode) { if (isInJSFile(sourceFile) && declaration.kind !== SyntaxKind.PropertySignature) { const parent = isVariableDeclaration(declaration) ? tryCast(declaration.parent.parent, isVariableStatement) : declaration; if (!parent) { return; } const typeExpression = factory.createJSDocTypeExpression(typeNode); const typeTag = isGetAccessorDeclaration(declaration) ? factory.createJSDocReturnTag(/*tagName*/ undefined, typeExpression, /*comment*/ undefined) : factory.createJSDocTypeTag(/*tagName*/ undefined, typeExpression, /*comment*/ undefined); addJSDocTags(changes, sourceFile, parent, [typeTag]); } else if (!tryReplaceImportTypeNodeWithAutoImport(typeNode, declaration, sourceFile, changes, importAdder, getEmitScriptTarget(program.getCompilerOptions()))) { changes.tryInsertTypeAnnotation(sourceFile, declaration, typeNode); } } } function tryReplaceImportTypeNodeWithAutoImport( typeNode: TypeNode, declaration: textChanges.TypeAnnotatable, sourceFile: SourceFile, changes: textChanges.ChangeTracker, importAdder: ImportAdder, scriptTarget: ScriptTarget ): boolean { const importableReference = tryGetAutoImportableReferenceFromTypeNode(typeNode, scriptTarget); if (importableReference && changes.tryInsertTypeAnnotation(sourceFile, declaration, importableReference.typeNode)) { forEach(importableReference.symbols, s => importAdder.addImportFromExportedSymbol(s, /*usageIsTypeOnly*/ true)); return true; } return false; } function annotateJSDocParameters(changes: textChanges.ChangeTracker, sourceFile: SourceFile, parameterInferences: readonly ParameterInference[], program: Program, host: LanguageServiceHost): void { const signature = parameterInferences.length && parameterInferences[0].declaration.parent; if (!signature) { return; } const inferences = mapDefined(parameterInferences, inference => { const param = inference.declaration; // only infer parameters that have (1) no type and (2) an accessible inferred type if (param.initializer || getJSDocType(param) || !isIdentifier(param.name)) { return; } const typeNode = inference.type && getTypeNodeIfAccessible(inference.type, param, program, host); if (typeNode) { const name = factory.cloneNode(param.name); setEmitFlags(name, EmitFlags.NoComments | EmitFlags.NoNestedComments); return { name: factory.cloneNode(param.name), param, isOptional: !!inference.isOptional, typeNode }; } }); if (!inferences.length) { return; } if (isArrowFunction(signature) || isFunctionExpression(signature)) { const needParens = isArrowFunction(signature) && !findChildOfKind(signature, SyntaxKind.OpenParenToken, sourceFile); if (needParens) { changes.insertNodeBefore(sourceFile, first(signature.parameters), factory.createToken(SyntaxKind.OpenParenToken)); } forEach(inferences, ({ typeNode, param }) => { const typeTag = factory.createJSDocTypeTag(/*tagName*/ undefined, factory.createJSDocTypeExpression(typeNode)); const jsDoc = factory.createJSDocComment(/*comment*/ undefined, [typeTag]); changes.insertNodeAt(sourceFile, param.getStart(sourceFile), jsDoc, { suffix: " " }); }); if (needParens) { changes.insertNodeAfter(sourceFile, last(signature.parameters), factory.createToken(SyntaxKind.CloseParenToken)); } } else { const paramTags = map(inferences, ({ name, typeNode, isOptional }) => factory.createJSDocParameterTag(/*tagName*/ undefined, name, /*isBracketed*/ !!isOptional, factory.createJSDocTypeExpression(typeNode), /* isNameFirst */ false, /*comment*/ undefined)); addJSDocTags(changes, sourceFile, signature, paramTags); } } export function addJSDocTags(changes: textChanges.ChangeTracker, sourceFile: SourceFile, parent: HasJSDoc, newTags: readonly JSDocTag[]): void { const comments = flatMap(parent.jsDoc, j => typeof j.comment === "string" ? factory.createJSDocText(j.comment) : j.comment) as JSDocComment[]; const oldTags = flatMapToMutable(parent.jsDoc, j => j.tags); const unmergedNewTags = newTags.filter(newTag => !oldTags || !oldTags.some((tag, i) => { const merged = tryMergeJsdocTags(tag, newTag); if (merged) oldTags[i] = merged; return !!merged; })); const tag = factory.createJSDocComment(factory.createNodeArray(intersperse(comments, factory.createJSDocText("\n"))), factory.createNodeArray([...(oldTags || emptyArray), ...unmergedNewTags])); const jsDocNode = parent.kind === SyntaxKind.ArrowFunction ? getJsDocNodeForArrowFunction(parent) : parent; jsDocNode.jsDoc = parent.jsDoc; jsDocNode.jsDocCache = parent.jsDocCache; changes.insertJsdocCommentBefore(sourceFile, jsDocNode, tag); } function getJsDocNodeForArrowFunction(signature: ArrowFunction): HasJSDoc { if (signature.parent.kind === SyntaxKind.PropertyDeclaration) { return signature.parent as HasJSDoc; } return signature.parent.parent as HasJSDoc; } function tryMergeJsdocTags(oldTag: JSDocTag, newTag: JSDocTag): JSDocTag | undefined { if (oldTag.kind !== newTag.kind) { return undefined; } switch (oldTag.kind) { case SyntaxKind.JSDocParameterTag: { const oldParam = oldTag as JSDocParameterTag; const newParam = newTag as JSDocParameterTag; return isIdentifier(oldParam.name) && isIdentifier(newParam.name) && oldParam.name.escapedText === newParam.name.escapedText ? factory.createJSDocParameterTag(/*tagName*/ undefined, newParam.name, /*isBracketed*/ false, newParam.typeExpression, newParam.isNameFirst, oldParam.comment) : undefined; } case SyntaxKind.JSDocReturnTag: return factory.createJSDocReturnTag(/*tagName*/ undefined, (newTag as JSDocReturnTag).typeExpression, oldTag.comment); } } function getReferences(token: PropertyName | Token, program: Program, cancellationToken: CancellationToken): readonly Identifier[] { // Position shouldn't matter since token is not a SourceFile. return mapDefined(FindAllReferences.getReferenceEntriesForNode(-1, token, program, program.getSourceFiles(), cancellationToken), entry => entry.kind !== FindAllReferences.EntryKind.Span ? tryCast(entry.node, isIdentifier) : undefined); } function inferTypeForVariableFromUsage(token: Identifier | PrivateIdentifier, program: Program, cancellationToken: CancellationToken): Type { const references = getReferences(token, program, cancellationToken); return inferTypeFromReferences(program, references, cancellationToken).single(); } function inferTypeForParametersFromUsage(func: SignatureDeclaration, sourceFile: SourceFile, program: Program, cancellationToken: CancellationToken) { const references = getFunctionReferences(func, sourceFile, program, cancellationToken); return references && inferTypeFromReferences(program, references, cancellationToken).parameters(func) || func.parameters.map(p => ({ declaration: p, type: isIdentifier(p.name) ? inferTypeForVariableFromUsage(p.name, program, cancellationToken) : program.getTypeChecker().getAnyType() })); } function getFunctionReferences(containingFunction: SignatureDeclaration, sourceFile: SourceFile, program: Program, cancellationToken: CancellationToken): readonly Identifier[] | undefined { let searchToken; switch (containingFunction.kind) { case SyntaxKind.Constructor: searchToken = findChildOfKind>(containingFunction, SyntaxKind.ConstructorKeyword, sourceFile); break; case SyntaxKind.ArrowFunction: case SyntaxKind.FunctionExpression: const parent = containingFunction.parent; searchToken = (isVariableDeclaration(parent) || isPropertyDeclaration(parent)) && isIdentifier(parent.name) ? parent.name : containingFunction.name; break; case SyntaxKind.FunctionDeclaration: case SyntaxKind.MethodDeclaration: case SyntaxKind.MethodSignature: searchToken = containingFunction.name; break; } if (!searchToken) { return undefined; } return getReferences(searchToken, program, cancellationToken); } interface ParameterInference { readonly declaration: ParameterDeclaration; readonly type: Type; readonly isOptional?: boolean; } function inferTypeFromReferences(program: Program, references: readonly Identifier[], cancellationToken: CancellationToken) { const checker = program.getTypeChecker(); const builtinConstructors: { [s: string]: (t: Type) => Type } = { string: () => checker.getStringType(), number: () => checker.getNumberType(), Array: t => checker.createArrayType(t), Promise: t => checker.createPromiseType(t), }; const builtins = [ checker.getStringType(), checker.getNumberType(), checker.createArrayType(checker.getAnyType()), checker.createPromiseType(checker.getAnyType()), ]; return { single, parameters, thisParameter, }; interface CallUsage { argumentTypes: Type[]; return_: Usage; } interface Usage { isNumber: boolean | undefined; isString: boolean | undefined; /** Used ambiguously, eg x + ___ or object[___]; results in string | number if no other evidence exists */ isNumberOrString: boolean | undefined; candidateTypes: Type[] | undefined; properties: UnderscoreEscapedMap | undefined; calls: CallUsage[] | undefined; constructs: CallUsage[] | undefined; numberIndex: Usage | undefined; stringIndex: Usage | undefined; candidateThisTypes: Type[] | undefined; inferredTypes: Type[] | undefined; } function createEmptyUsage(): Usage { return { isNumber: undefined, isString: undefined, isNumberOrString: undefined, candidateTypes: undefined, properties: undefined, calls: undefined, constructs: undefined, numberIndex: undefined, stringIndex: undefined, candidateThisTypes: undefined, inferredTypes: undefined, }; } function combineUsages(usages: Usage[]): Usage { const combinedProperties = new Map<__String, Usage[]>(); for (const u of usages) { if (u.properties) { u.properties.forEach((p, name) => { if (!combinedProperties.has(name)) { combinedProperties.set(name, []); } combinedProperties.get(name)!.push(p); }); } } const properties = new Map<__String, Usage>(); combinedProperties.forEach((ps, name) => { properties.set(name, combineUsages(ps)); }); return { isNumber: usages.some(u => u.isNumber), isString: usages.some(u => u.isString), isNumberOrString: usages.some(u => u.isNumberOrString), candidateTypes: flatMap(usages, u => u.candidateTypes) as Type[], properties, calls: flatMap(usages, u => u.calls) as CallUsage[], constructs: flatMap(usages, u => u.constructs) as CallUsage[], numberIndex: forEach(usages, u => u.numberIndex), stringIndex: forEach(usages, u => u.stringIndex), candidateThisTypes: flatMap(usages, u => u.candidateThisTypes) as Type[], inferredTypes: undefined, // clear type cache }; } function single(): Type { return combineTypes(inferTypesFromReferencesSingle(references)); } function parameters(declaration: SignatureDeclaration): ParameterInference[] | undefined { if (references.length === 0 || !declaration.parameters) { return undefined; } const usage = createEmptyUsage(); for (const reference of references) { cancellationToken.throwIfCancellationRequested(); calculateUsageOfNode(reference, usage); } const calls = [...usage.constructs || [], ...usage.calls || []]; return declaration.parameters.map((parameter, parameterIndex): ParameterInference => { const types = []; const isRest = isRestParameter(parameter); let isOptional = false; for (const call of calls) { if (call.argumentTypes.length <= parameterIndex) { isOptional = isInJSFile(declaration); types.push(checker.getUndefinedType()); } else if (isRest) { for (let i = parameterIndex; i < call.argumentTypes.length; i++) { types.push(checker.getBaseTypeOfLiteralType(call.argumentTypes[i])); } } else { types.push(checker.getBaseTypeOfLiteralType(call.argumentTypes[parameterIndex])); } } if (isIdentifier(parameter.name)) { const inferred = inferTypesFromReferencesSingle(getReferences(parameter.name, program, cancellationToken)); types.push(...(isRest ? mapDefined(inferred, checker.getElementTypeOfArrayType) : inferred)); } const type = combineTypes(types); return { type: isRest ? checker.createArrayType(type) : type, isOptional: isOptional && !isRest, declaration: parameter }; }); } function thisParameter() { const usage = createEmptyUsage(); for (const reference of references) { cancellationToken.throwIfCancellationRequested(); calculateUsageOfNode(reference, usage); } return combineTypes(usage.candidateThisTypes || emptyArray); } function inferTypesFromReferencesSingle(references: readonly Identifier[]): Type[] { const usage: Usage = createEmptyUsage(); for (const reference of references) { cancellationToken.throwIfCancellationRequested(); calculateUsageOfNode(reference, usage); } return inferTypes(usage); } function calculateUsageOfNode(node: Expression, usage: Usage): void { while (isRightSideOfQualifiedNameOrPropertyAccess(node)) { node = node.parent as Expression; } switch (node.parent.kind) { case SyntaxKind.ExpressionStatement: inferTypeFromExpressionStatement(node, usage); break; case SyntaxKind.PostfixUnaryExpression: usage.isNumber = true; break; case SyntaxKind.PrefixUnaryExpression: inferTypeFromPrefixUnaryExpression(node.parent as PrefixUnaryExpression, usage); break; case SyntaxKind.BinaryExpression: inferTypeFromBinaryExpression(node, node.parent as BinaryExpression, usage); break; case SyntaxKind.CaseClause: case SyntaxKind.DefaultClause: inferTypeFromSwitchStatementLabel(node.parent as CaseOrDefaultClause, usage); break; case SyntaxKind.CallExpression: case SyntaxKind.NewExpression: if ((node.parent as CallExpression | NewExpression).expression === node) { inferTypeFromCallExpression(node.parent as CallExpression | NewExpression, usage); } else { inferTypeFromContextualType(node, usage); } break; case SyntaxKind.PropertyAccessExpression: inferTypeFromPropertyAccessExpression(node.parent as PropertyAccessExpression, usage); break; case SyntaxKind.ElementAccessExpression: inferTypeFromPropertyElementExpression(node.parent as ElementAccessExpression, node, usage); break; case SyntaxKind.PropertyAssignment: case SyntaxKind.ShorthandPropertyAssignment: inferTypeFromPropertyAssignment(node.parent as PropertyAssignment | ShorthandPropertyAssignment, usage); break; case SyntaxKind.PropertyDeclaration: inferTypeFromPropertyDeclaration(node.parent as PropertyDeclaration, usage); break; case SyntaxKind.VariableDeclaration: { const { name, initializer } = node.parent as VariableDeclaration; if (node === name) { if (initializer) { // This can happen for `let x = null;` which still has an implicit-any error. addCandidateType(usage, checker.getTypeAtLocation(initializer)); } break; } } // falls through default: return inferTypeFromContextualType(node, usage); } } function inferTypeFromContextualType(node: Expression, usage: Usage): void { if (isExpressionNode(node)) { addCandidateType(usage, checker.getContextualType(node)); } } function inferTypeFromExpressionStatement(node: Expression, usage: Usage): void { addCandidateType(usage, isCallExpression(node) ? checker.getVoidType() : checker.getAnyType()); } function inferTypeFromPrefixUnaryExpression(node: PrefixUnaryExpression, usage: Usage): void { switch (node.operator) { case SyntaxKind.PlusPlusToken: case SyntaxKind.MinusMinusToken: case SyntaxKind.MinusToken: case SyntaxKind.TildeToken: usage.isNumber = true; break; case SyntaxKind.PlusToken: usage.isNumberOrString = true; break; // case SyntaxKind.ExclamationToken: // no inferences here; } } function inferTypeFromBinaryExpression(node: Expression, parent: BinaryExpression, usage: Usage): void { switch (parent.operatorToken.kind) { // ExponentiationOperator case SyntaxKind.AsteriskAsteriskToken: // MultiplicativeOperator // falls through case SyntaxKind.AsteriskToken: case SyntaxKind.SlashToken: case SyntaxKind.PercentToken: // ShiftOperator // falls through case SyntaxKind.LessThanLessThanToken: case SyntaxKind.GreaterThanGreaterThanToken: case SyntaxKind.GreaterThanGreaterThanGreaterThanToken: // BitwiseOperator // falls through case SyntaxKind.AmpersandToken: case SyntaxKind.BarToken: case SyntaxKind.CaretToken: // CompoundAssignmentOperator // falls through case SyntaxKind.MinusEqualsToken: case SyntaxKind.AsteriskAsteriskEqualsToken: case SyntaxKind.AsteriskEqualsToken: case SyntaxKind.SlashEqualsToken: case SyntaxKind.PercentEqualsToken: case SyntaxKind.AmpersandEqualsToken: case SyntaxKind.BarEqualsToken: case SyntaxKind.CaretEqualsToken: case SyntaxKind.LessThanLessThanEqualsToken: case SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken: case SyntaxKind.GreaterThanGreaterThanEqualsToken: // AdditiveOperator // falls through case SyntaxKind.MinusToken: // RelationalOperator // falls through case SyntaxKind.LessThanToken: case SyntaxKind.LessThanEqualsToken: case SyntaxKind.GreaterThanToken: case SyntaxKind.GreaterThanEqualsToken: const operandType = checker.getTypeAtLocation(parent.left === node ? parent.right : parent.left); if (operandType.flags & TypeFlags.EnumLike) { addCandidateType(usage, operandType); } else { usage.isNumber = true; } break; case SyntaxKind.PlusEqualsToken: case SyntaxKind.PlusToken: const otherOperandType = checker.getTypeAtLocation(parent.left === node ? parent.right : parent.left); if (otherOperandType.flags & TypeFlags.EnumLike) { addCandidateType(usage, otherOperandType); } else if (otherOperandType.flags & TypeFlags.NumberLike) { usage.isNumber = true; } else if (otherOperandType.flags & TypeFlags.StringLike) { usage.isString = true; } else if (otherOperandType.flags & TypeFlags.Any) { // do nothing, maybe we'll learn something elsewhere } else { usage.isNumberOrString = true; } break; // AssignmentOperators case SyntaxKind.EqualsToken: case SyntaxKind.EqualsEqualsToken: case SyntaxKind.EqualsEqualsEqualsToken: case SyntaxKind.ExclamationEqualsEqualsToken: case SyntaxKind.ExclamationEqualsToken: addCandidateType(usage, checker.getTypeAtLocation(parent.left === node ? parent.right : parent.left)); break; case SyntaxKind.InKeyword: if (node === parent.left) { usage.isString = true; } break; // LogicalOperator Or NullishCoalescing case SyntaxKind.BarBarToken: case SyntaxKind.QuestionQuestionToken: if (node === parent.left && (node.parent.parent.kind === SyntaxKind.VariableDeclaration || isAssignmentExpression(node.parent.parent, /*excludeCompoundAssignment*/ true))) { // var x = x || {}; // TODO: use getFalsyflagsOfType addCandidateType(usage, checker.getTypeAtLocation(parent.right)); } break; case SyntaxKind.AmpersandAmpersandToken: case SyntaxKind.CommaToken: case SyntaxKind.InstanceOfKeyword: // nothing to infer here break; } } function inferTypeFromSwitchStatementLabel(parent: CaseOrDefaultClause, usage: Usage): void { addCandidateType(usage, checker.getTypeAtLocation(parent.parent.parent.expression)); } function inferTypeFromCallExpression(parent: CallExpression | NewExpression, usage: Usage): void { const call: CallUsage = { argumentTypes: [], return_: createEmptyUsage() }; if (parent.arguments) { for (const argument of parent.arguments) { call.argumentTypes.push(checker.getTypeAtLocation(argument)); } } calculateUsageOfNode(parent, call.return_); if (parent.kind === SyntaxKind.CallExpression) { (usage.calls || (usage.calls = [])).push(call); } else { (usage.constructs || (usage.constructs = [])).push(call); } } function inferTypeFromPropertyAccessExpression(parent: PropertyAccessExpression, usage: Usage): void { const name = escapeLeadingUnderscores(parent.name.text); if (!usage.properties) { usage.properties = new Map(); } const propertyUsage = usage.properties.get(name) || createEmptyUsage(); calculateUsageOfNode(parent, propertyUsage); usage.properties.set(name, propertyUsage); } function inferTypeFromPropertyElementExpression(parent: ElementAccessExpression, node: Expression, usage: Usage): void { if (node === parent.argumentExpression) { usage.isNumberOrString = true; return; } else { const indexType = checker.getTypeAtLocation(parent.argumentExpression); const indexUsage = createEmptyUsage(); calculateUsageOfNode(parent, indexUsage); if (indexType.flags & TypeFlags.NumberLike) { usage.numberIndex = indexUsage; } else { usage.stringIndex = indexUsage; } } } function inferTypeFromPropertyAssignment(assignment: PropertyAssignment | ShorthandPropertyAssignment, usage: Usage) { const nodeWithRealType = isVariableDeclaration(assignment.parent.parent) ? assignment.parent.parent : assignment.parent; addCandidateThisType(usage, checker.getTypeAtLocation(nodeWithRealType)); } function inferTypeFromPropertyDeclaration(declaration: PropertyDeclaration, usage: Usage) { addCandidateThisType(usage, checker.getTypeAtLocation(declaration.parent)); } interface Priority { high: (t: Type) => boolean; low: (t: Type) => boolean; } function removeLowPriorityInferences(inferences: readonly Type[], priorities: Priority[]): Type[] { const toRemove: ((t: Type) => boolean)[] = []; for (const i of inferences) { for (const { high, low } of priorities) { if (high(i)) { Debug.assert(!low(i), "Priority can't have both low and high"); toRemove.push(low); } } } return inferences.filter(i => toRemove.every(f => !f(i))); } function combineFromUsage(usage: Usage) { return combineTypes(inferTypes(usage)); } function combineTypes(inferences: readonly Type[]): Type { if (!inferences.length) return checker.getAnyType(); // 1. string or number individually override string | number // 2. non-any, non-void overrides any or void // 3. non-nullable, non-any, non-void, non-anonymous overrides anonymous types const stringNumber = checker.getUnionType([checker.getStringType(), checker.getNumberType()]); const priorities: Priority[] = [ { high: t => t === checker.getStringType() || t === checker.getNumberType(), low: t => t === stringNumber }, { high: t => !(t.flags & (TypeFlags.Any | TypeFlags.Void)), low: t => !!(t.flags & (TypeFlags.Any | TypeFlags.Void)) }, { high: t => !(t.flags & (TypeFlags.Nullable | TypeFlags.Any | TypeFlags.Void)) && !(getObjectFlags(t) & ObjectFlags.Anonymous), low: t => !!(getObjectFlags(t) & ObjectFlags.Anonymous) }]; let good = removeLowPriorityInferences(inferences, priorities); const anons = good.filter(i => getObjectFlags(i) & ObjectFlags.Anonymous) as AnonymousType[]; if (anons.length) { good = good.filter(i => !(getObjectFlags(i) & ObjectFlags.Anonymous)); good.push(combineAnonymousTypes(anons)); } return checker.getWidenedType(checker.getUnionType(good.map(checker.getBaseTypeOfLiteralType), UnionReduction.Subtype)); } function combineAnonymousTypes(anons: AnonymousType[]) { if (anons.length === 1) { return anons[0]; } const calls = []; const constructs = []; const stringIndices = []; const numberIndices = []; let stringIndexReadonly = false; let numberIndexReadonly = false; const props = createMultiMap(); for (const anon of anons) { for (const p of checker.getPropertiesOfType(anon)) { props.add(p.name, p.valueDeclaration ? checker.getTypeOfSymbolAtLocation(p, p.valueDeclaration) : checker.getAnyType()); } calls.push(...checker.getSignaturesOfType(anon, SignatureKind.Call)); constructs.push(...checker.getSignaturesOfType(anon, SignatureKind.Construct)); const stringIndexInfo = checker.getIndexInfoOfType(anon, IndexKind.String); if (stringIndexInfo) { stringIndices.push(stringIndexInfo.type); stringIndexReadonly = stringIndexReadonly || stringIndexInfo.isReadonly; } const numberIndexInfo = checker.getIndexInfoOfType(anon, IndexKind.Number); if (numberIndexInfo) { numberIndices.push(numberIndexInfo.type); numberIndexReadonly = numberIndexReadonly || numberIndexInfo.isReadonly; } } const members = mapEntries(props, (name, types) => { const isOptional = types.length < anons.length ? SymbolFlags.Optional : 0; const s = checker.createSymbol(SymbolFlags.Property | isOptional, name as __String); s.type = checker.getUnionType(types); return [name, s]; }); const indexInfos = []; if (stringIndices.length) indexInfos.push(checker.createIndexInfo(checker.getStringType(), checker.getUnionType(stringIndices), stringIndexReadonly)); if (numberIndices.length) indexInfos.push(checker.createIndexInfo(checker.getNumberType(), checker.getUnionType(numberIndices), numberIndexReadonly)); return checker.createAnonymousType( anons[0].symbol, members as UnderscoreEscapedMap, calls, constructs, indexInfos); } function inferTypes(usage: Usage): Type[] { const types = []; if (usage.isNumber) { types.push(checker.getNumberType()); } if (usage.isString) { types.push(checker.getStringType()); } if (usage.isNumberOrString) { types.push(checker.getUnionType([checker.getStringType(), checker.getNumberType()])); } if (usage.numberIndex) { types.push(checker.createArrayType(combineFromUsage(usage.numberIndex))); } if (usage.properties?.size || usage.calls?.length || usage.constructs?.length || usage.stringIndex) { types.push(inferStructuralType(usage)); } types.push(...(usage.candidateTypes || []).map(t => checker.getBaseTypeOfLiteralType(t))); types.push(...inferNamedTypesFromProperties(usage)); return types; } function inferStructuralType(usage: Usage) { const members = new Map<__String, Symbol>(); if (usage.properties) { usage.properties.forEach((u, name) => { const symbol = checker.createSymbol(SymbolFlags.Property, name); symbol.type = combineFromUsage(u); members.set(name, symbol); }); } const callSignatures: Signature[] = usage.calls ? [getSignatureFromCalls(usage.calls)] : []; const constructSignatures: Signature[] = usage.constructs ? [getSignatureFromCalls(usage.constructs)] : []; const indexInfos = usage.stringIndex ? [checker.createIndexInfo(checker.getStringType(), combineFromUsage(usage.stringIndex), /*isReadonly*/ false)] : []; return checker.createAnonymousType(/*symbol*/ undefined, members, callSignatures, constructSignatures, indexInfos); } function inferNamedTypesFromProperties(usage: Usage): Type[] { if (!usage.properties || !usage.properties.size) return []; const types = builtins.filter(t => allPropertiesAreAssignableToUsage(t, usage)); if (0 < types.length && types.length < 3) { return types.map(t => inferInstantiationFromUsage(t, usage)); } return []; } function allPropertiesAreAssignableToUsage(type: Type, usage: Usage) { if (!usage.properties) return false; return !forEachEntry(usage.properties, (propUsage, name) => { const source = checker.getTypeOfPropertyOfType(type, name as string); if (!source) { return true; } if (propUsage.calls) { const sigs = checker.getSignaturesOfType(source, SignatureKind.Call); return !sigs.length || !checker.isTypeAssignableTo(source, getFunctionFromCalls(propUsage.calls)); } else { return !checker.isTypeAssignableTo(source, combineFromUsage(propUsage)); } }); } /** * inference is limited to * 1. generic types with a single parameter * 2. inference to/from calls with a single signature */ function inferInstantiationFromUsage(type: Type, usage: Usage) { if (!(getObjectFlags(type) & ObjectFlags.Reference) || !usage.properties) { return type; } const generic = (type as TypeReference).target; const singleTypeParameter = singleOrUndefined(generic.typeParameters); if (!singleTypeParameter) return type; const types: Type[] = []; usage.properties.forEach((propUsage, name) => { const genericPropertyType = checker.getTypeOfPropertyOfType(generic, name as string); Debug.assert(!!genericPropertyType, "generic should have all the properties of its reference."); types.push(...inferTypeParameters(genericPropertyType, combineFromUsage(propUsage), singleTypeParameter)); }); return builtinConstructors[type.symbol.escapedName as string](combineTypes(types)); } function inferTypeParameters(genericType: Type, usageType: Type, typeParameter: Type): readonly Type[] { if (genericType === typeParameter) { return [usageType]; } else if (genericType.flags & TypeFlags.UnionOrIntersection) { return flatMap((genericType as UnionOrIntersectionType).types, t => inferTypeParameters(t, usageType, typeParameter)); } else if (getObjectFlags(genericType) & ObjectFlags.Reference && getObjectFlags(usageType) & ObjectFlags.Reference) { // this is wrong because we need a reference to the targetType to, so we can check that it's also a reference const genericArgs = checker.getTypeArguments(genericType as TypeReference); const usageArgs = checker.getTypeArguments(usageType as TypeReference); const types = []; if (genericArgs && usageArgs) { for (let i = 0; i < genericArgs.length; i++) { if (usageArgs[i]) { types.push(...inferTypeParameters(genericArgs[i], usageArgs[i], typeParameter)); } } } return types; } const genericSigs = checker.getSignaturesOfType(genericType, SignatureKind.Call); const usageSigs = checker.getSignaturesOfType(usageType, SignatureKind.Call); if (genericSigs.length === 1 && usageSigs.length === 1) { return inferFromSignatures(genericSigs[0], usageSigs[0], typeParameter); } return []; } function inferFromSignatures(genericSig: Signature, usageSig: Signature, typeParameter: Type) { const types = []; for (let i = 0; i < genericSig.parameters.length; i++) { const genericParam = genericSig.parameters[i]; const usageParam = usageSig.parameters[i]; const isRest = genericSig.declaration && isRestParameter(genericSig.declaration.parameters[i]); if (!usageParam) { break; } let genericParamType = genericParam.valueDeclaration ? checker.getTypeOfSymbolAtLocation(genericParam, genericParam.valueDeclaration) : checker.getAnyType(); const elementType = isRest && checker.getElementTypeOfArrayType(genericParamType); if (elementType) { genericParamType = elementType; } const targetType = (usageParam as SymbolLinks).type || (usageParam.valueDeclaration ? checker.getTypeOfSymbolAtLocation(usageParam, usageParam.valueDeclaration) : checker.getAnyType()); types.push(...inferTypeParameters(genericParamType, targetType, typeParameter)); } const genericReturn = checker.getReturnTypeOfSignature(genericSig); const usageReturn = checker.getReturnTypeOfSignature(usageSig); types.push(...inferTypeParameters(genericReturn, usageReturn, typeParameter)); return types; } function getFunctionFromCalls(calls: CallUsage[]) { return checker.createAnonymousType(/*symbol*/ undefined, createSymbolTable(), [getSignatureFromCalls(calls)], emptyArray, emptyArray); } function getSignatureFromCalls(calls: CallUsage[]): Signature { const parameters: Symbol[] = []; const length = Math.max(...calls.map(c => c.argumentTypes.length)); for (let i = 0; i < length; i++) { const symbol = checker.createSymbol(SymbolFlags.FunctionScopedVariable, escapeLeadingUnderscores(`arg${i}`)); symbol.type = combineTypes(calls.map(call => call.argumentTypes[i] || checker.getUndefinedType())); if (calls.some(call => call.argumentTypes[i] === undefined)) { symbol.flags |= SymbolFlags.Optional; } parameters.push(symbol); } const returnType = combineFromUsage(combineUsages(calls.map(call => call.return_))); return checker.createSignature(/*declaration*/ undefined, /*typeParameters*/ undefined, /*thisParameter*/ undefined, parameters, returnType, /*typePredicate*/ undefined, length, SignatureFlags.None); } function addCandidateType(usage: Usage, type: Type | undefined) { if (type && !(type.flags & TypeFlags.Any) && !(type.flags & TypeFlags.Never)) { (usage.candidateTypes || (usage.candidateTypes = [])).push(type); } } function addCandidateThisType(usage: Usage, type: Type | undefined) { if (type && !(type.flags & TypeFlags.Any) && !(type.flags & TypeFlags.Never)) { (usage.candidateThisTypes || (usage.candidateThisTypes = [])).push(type); } } } }