TypeScript/src/services/codefixes/inferFromUsage.ts
Ryan Cavanaugh e00b5ecd40
Enable max-statements-per-line lint rule (#45475)
* Enable the rule

* Fix all the violations
2021-08-16 13:53:51 -07:00

1161 lines
59 KiB
TypeScript

/* @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<SyntaxKind.ConstructorKeyword>, 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<ParameterInference>(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<Token<SyntaxKind.ConstructorKeyword>>(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<Usage> | 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<Type>();
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<TransientSymbol>,
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);
}
}
}
}