Added --noImplicitThis code fix for functions used as object properties (#31138)

* Added --noImplicitThis code fix for functions used as object properties

Before trying out all the various possibilities for where these functions could be used, I figured I'd start out with a relatively simple use case to verify this is the right approach. Is it? 😄

Starts on #28964.

* Fixed function expression names; included new baselines

* Got JSDocs to work, hooray!

* Added test for 'any' case of no function uses

* Refactored for inferFunctionReferencesFromUsage

* Fixed inference bug: undefined references cause parameters to default

* Removed dead code comments
This commit is contained in:
Josh Goldberg
2019-06-17 22:00:15 -04:00
committed by Nathan Shively-Sanders
parent 7f65be44c1
commit ca00b3248b
26 changed files with 531 additions and 84 deletions

View File

@@ -42,6 +42,9 @@ namespace ts.codefix {
// 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,
@@ -73,6 +76,8 @@ namespace ts.codefix {
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;
}
@@ -176,6 +181,14 @@ namespace ts.codefix {
}
return undefined;
// 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);
return containingFunction;
}
return undefined;
default:
return Debug.fail(String(errorCode));
}
@@ -191,7 +204,9 @@ namespace ts.codefix {
if (!isIdentifier(parameterDeclaration.name)) {
return;
}
const parameterInferences = inferTypeForParametersFromUsage(containingFunction, sourceFile, program, cancellationToken) ||
const references = inferFunctionReferencesFromUsage(containingFunction, sourceFile, program, cancellationToken);
const parameterInferences = InferFromReference.inferTypeForParametersFromReferences(references, containingFunction, program, cancellationToken) ||
containingFunction.parameters.map<ParameterInference>(p => ({
declaration: p,
type: isIdentifier(p.name) ? inferTypeForVariableFromUsage(p.name, program, cancellationToken) : program.getTypeChecker().getAnyType()
@@ -213,6 +228,36 @@ namespace ts.codefix {
}
}
function annotateThis(changes: textChanges.ChangeTracker, sourceFile: SourceFile, containingFunction: textChanges.ThisTypeAnnotatable, program: Program, host: LanguageServiceHost, cancellationToken: CancellationToken) {
const references = inferFunctionReferencesFromUsage(containingFunction, sourceFile, program, cancellationToken);
if (!references) {
return;
}
const thisInference = InferFromReference.inferTypeForThisFromReferences(references, program, cancellationToken);
if (!thisInference) {
return;
}
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: FunctionLike, typeNode: TypeNode) {
addJSDocTags(changes, sourceFile, containingFunction, [
createJSDocThisTag(createJSDocTypeExpression(typeNode)),
]);
}
function annotateSetAccessor(changes: textChanges.ChangeTracker, sourceFile: SourceFile, setAccessorDeclaration: SetAccessorDeclaration, program: Program, host: LanguageServiceHost, cancellationToken: CancellationToken): void {
const param = firstOrUndefined(setAccessorDeclaration.parameters);
if (param && isIdentifier(setAccessorDeclaration.name) && isIdentifier(param.name)) {
@@ -317,7 +362,7 @@ namespace ts.codefix {
return InferFromReference.unifyFromContext(types, checker);
}
function inferTypeForParametersFromUsage(containingFunction: FunctionLike, sourceFile: SourceFile, program: Program, cancellationToken: CancellationToken): ParameterInference[] | undefined {
function inferFunctionReferencesFromUsage(containingFunction: FunctionLike, sourceFile: SourceFile, program: Program, cancellationToken: CancellationToken): ReadonlyArray<Identifier> | undefined {
let searchToken;
switch (containingFunction.kind) {
case SyntaxKind.Constructor:
@@ -335,9 +380,12 @@ namespace ts.codefix {
searchToken = containingFunction.name;
break;
}
if (searchToken) {
return InferFromReference.inferTypeForParametersFromReferences(getReferences(searchToken, program, cancellationToken), containingFunction, program, cancellationToken);
if (!searchToken) {
return undefined;
}
return getReferences(searchToken, program, cancellationToken);
}
interface ParameterInference {
@@ -364,6 +412,7 @@ namespace ts.codefix {
constructContexts?: CallContext[];
numberIndexContext?: UsageContext;
stringIndexContext?: UsageContext;
candidateThisTypes?: Type[];
}
export function inferTypesFromReferences(references: ReadonlyArray<Identifier>, checker: TypeChecker, cancellationToken: CancellationToken): Type[] {
@@ -375,15 +424,12 @@ namespace ts.codefix {
return inferFromContext(usageContext, checker);
}
export function inferTypeForParametersFromReferences(references: ReadonlyArray<Identifier>, declaration: FunctionLike, program: Program, cancellationToken: CancellationToken): ParameterInference[] | undefined {
const checker = program.getTypeChecker();
if (references.length === 0) {
return undefined;
}
if (!declaration.parameters) {
export function inferTypeForParametersFromReferences(references: ReadonlyArray<Identifier> | undefined, declaration: FunctionLike, program: Program, cancellationToken: CancellationToken): ParameterInference[] | undefined {
if (references === undefined || references.length === 0 || !declaration.parameters) {
return undefined;
}
const checker = program.getTypeChecker();
const usageContext: UsageContext = {};
for (const reference of references) {
cancellationToken.throwIfCancellationRequested();
@@ -421,6 +467,22 @@ namespace ts.codefix {
});
}
export function inferTypeForThisFromReferences(references: ReadonlyArray<Identifier>, program: Program, cancellationToken: CancellationToken) {
if (references.length === 0) {
return undefined;
}
const checker = program.getTypeChecker();
const usageContext: UsageContext = {};
for (const reference of references) {
cancellationToken.throwIfCancellationRequested();
inferTypeFromContext(reference, checker, usageContext);
}
return unifyFromContext(usageContext.candidateThisTypes || emptyArray, checker);
}
function inferTypeFromContext(node: Expression, checker: TypeChecker, usageContext: UsageContext): void {
while (isRightSideOfQualifiedNameOrPropertyAccess(node)) {
node = <Expression>node.parent;
@@ -455,6 +517,13 @@ namespace ts.codefix {
case SyntaxKind.ElementAccessExpression:
inferTypeFromPropertyElementExpressionContext(<ElementAccessExpression>node.parent, node, checker, usageContext);
break;
case SyntaxKind.PropertyAssignment:
case SyntaxKind.ShorthandPropertyAssignment:
inferTypeFromPropertyAssignment(<PropertyAssignment | ShorthandPropertyAssignment>node.parent, checker, usageContext);
break;
case SyntaxKind.PropertyDeclaration:
inferTypeFromPropertyDeclaration(<PropertyDeclaration>node.parent, checker, usageContext);
break;
case SyntaxKind.VariableDeclaration: {
const { name, initializer } = node.parent as VariableDeclaration;
if (node === name) {
@@ -647,6 +716,21 @@ namespace ts.codefix {
}
}
function inferTypeFromPropertyAssignment(assignment: PropertyAssignment | ShorthandPropertyAssignment, checker: TypeChecker, usageContext: UsageContext) {
const objectLiteral = isShorthandPropertyAssignment(assignment) ?
assignment.parent :
assignment.parent.parent;
const nodeWithRealType = isVariableDeclaration(objectLiteral.parent) ?
objectLiteral.parent :
objectLiteral;
addCandidateThisType(usageContext, checker.getTypeAtLocation(nodeWithRealType));
}
function inferTypeFromPropertyDeclaration(declaration: PropertyDeclaration, checker: TypeChecker, usageContext: UsageContext) {
addCandidateThisType(usageContext, checker.getTypeAtLocation(declaration.parent));
}
interface Priority {
high: (t: Type) => boolean;
low: (t: Type) => boolean;
@@ -841,6 +925,12 @@ namespace ts.codefix {
}
}
function addCandidateThisType(context: UsageContext, type: Type | undefined) {
if (type && !(type.flags & TypeFlags.Any) && !(type.flags & TypeFlags.Never)) {
(context.candidateThisTypes || (context.candidateThisTypes = [])).push(type);
}
}
function hasCallContext(usageContext: UsageContext | undefined): boolean {
return !!usageContext && !!usageContext.callContexts;
}

View File

@@ -222,6 +222,12 @@ namespace ts.textChanges {
export type TypeAnnotatable = SignatureDeclaration | VariableDeclaration | ParameterDeclaration | PropertyDeclaration | PropertySignature;
export type ThisTypeAnnotatable = FunctionDeclaration | FunctionExpression;
export function isThisTypeAnnotatable(containingFunction: FunctionLike): containingFunction is ThisTypeAnnotatable {
return isFunctionExpression(containingFunction) || isFunctionDeclaration(containingFunction);
}
export class ChangeTracker {
private readonly changes: Change[] = [];
private readonly newFiles: { readonly oldFile: SourceFile | undefined, readonly fileName: string, readonly statements: ReadonlyArray<Statement> }[] = [];
@@ -393,6 +399,13 @@ namespace ts.textChanges {
this.insertNodeAt(sourceFile, endNode.end, type, { prefix: ": " });
}
public tryInsertThisTypeAnnotation(sourceFile: SourceFile, node: ThisTypeAnnotatable, type: TypeNode): void {
const start = findChildOfKind(node, SyntaxKind.OpenParenToken, sourceFile)!.getStart(sourceFile) + 1;
const suffix = node.parameters.length ? ", " : "";
this.insertNodeAt(sourceFile, start, type, { prefix: "this: ", suffix });
}
public insertTypeParameters(sourceFile: SourceFile, node: SignatureDeclaration, typeParameters: ReadonlyArray<TypeParameterDeclaration>): void {
// If no `(`, is an arrow function `x => x`, so use the pos of the first parameter
const start = (findChildOfKind(node, SyntaxKind.OpenParenToken, sourceFile) || first(node.parameters)).getStart(sourceFile);