From 5bc1c5f3355a69de4ff3bc03955a52ca71fd7fda Mon Sep 17 00:00:00 2001 From: Aly Thobani Date: Fri, 22 Mar 2024 09:42:15 -0700 Subject: [PATCH 01/26] Implement checking of subtypes for no-misused-promises (currently works for subtypes implementing interfaces, and interfaces extending classes, but not classes extending classes) --- .../src/rules/no-misused-promises.ts | 87 ++++++++++++++++++- 1 file changed, 83 insertions(+), 4 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-misused-promises.ts b/packages/eslint-plugin/src/rules/no-misused-promises.ts index 1a58d884dc7c..909277e6b6e3 100644 --- a/packages/eslint-plugin/src/rules/no-misused-promises.ts +++ b/packages/eslint-plugin/src/rules/no-misused-promises.ts @@ -22,6 +22,7 @@ interface ChecksVoidReturnOptions { attributes?: boolean; properties?: boolean; returns?: boolean; + subtypes?: boolean; variables?: boolean; } @@ -32,6 +33,7 @@ type MessageId = | 'voidReturnAttribute' | 'voidReturnProperty' | 'voidReturnReturnValue' + | 'voidReturnSubtype' | 'voidReturnVariable'; function parseChecksVoidReturn( @@ -48,6 +50,7 @@ function parseChecksVoidReturn( attributes: true, properties: true, returns: true, + subtypes: true, variables: true, }; @@ -57,6 +60,7 @@ function parseChecksVoidReturn( attributes: checksVoidReturn.attributes ?? true, properties: checksVoidReturn.properties ?? true, returns: checksVoidReturn.returns ?? true, + subtypes: checksVoidReturn.subtypes ?? true, variables: checksVoidReturn.variables ?? true, }; } @@ -73,14 +77,16 @@ export default createRule({ messages: { voidReturnArgument: 'Promise returned in function argument where a void return was expected.', - voidReturnVariable: - 'Promise-returning function provided to variable where a void return was expected.', + voidReturnAttribute: + 'Promise-returning function provided to attribute where a void return was expected.', voidReturnProperty: 'Promise-returning function provided to property where a void return was expected.', voidReturnReturnValue: 'Promise-returning function provided to return value where a void return was expected.', - voidReturnAttribute: - 'Promise-returning function provided to attribute where a void return was expected.', + voidReturnSubtype: + "Promise-returning method provided where a void return was expected by base type '{{ baseTypeName }}'.", + voidReturnVariable: + 'Promise-returning function provided to variable where a void return was expected.', conditional: 'Expected non-Promise value in a boolean conditional.', spread: 'Expected a non-Promise value to be spreaded in an object.', }, @@ -102,6 +108,7 @@ export default createRule({ attributes: { type: 'boolean' }, properties: { type: 'boolean' }, returns: { type: 'boolean' }, + methods: { type: 'boolean' }, variables: { type: 'boolean' }, }, type: 'object', @@ -159,6 +166,11 @@ export default createRule({ ...(checksVoidReturn.returns && { ReturnStatement: checkReturnStatement, }), + ...(checksVoidReturn.subtypes && { + ClassDeclaration: checkClassLikeOrInterfaceNode, + ClassExpression: checkClassLikeOrInterfaceNode, + TSInterfaceDeclaration: checkClassLikeOrInterfaceNode, + }), ...(checksVoidReturn.variables && { AssignmentExpression: checkAssignment, VariableDeclarator: checkVariableDeclaration, @@ -369,6 +381,73 @@ export default createRule({ } } + function checkClassLikeOrInterfaceNode( + node: + | TSESTree.ClassDeclaration + | TSESTree.ClassExpression + | TSESTree.TSInterfaceDeclaration, + ): void { + const tsNode = services.esTreeNodeToTSNodeMap.get(node); + if ( + tsNode.heritageClauses === undefined || + tsNode.heritageClauses.length === 0 + ) { + return; + } + + const heritageTypes = tsNode.heritageClauses + .map(heritageClause => heritageClause.types) + .flat() + .map( + expressionWithTypeArguments => expressionWithTypeArguments.expression, + ) + .map(expression => checker.getTypeAtLocation(expression)); + + tsNode.members.forEach(member => { + const memberName = member.name?.getText(); + if (!memberName) { + return; + } + const doesReturnPromise = returnsThenable(checker, member); + if (!doesReturnPromise) { + return; + } + heritageTypes.forEach(heritageType => { + const heritageTypeName = heritageType.getSymbol()?.name; + if (!heritageTypeName) { + return; + } + for (const subType of tsutils.unionTypeParts( + checker.getApparentType(heritageType), + )) { + const subtypeMember = checker.getPropertyOfType( + subType, + memberName, + ); + if (!subtypeMember) { + return; + } + } + const heritageMember = checker.getPropertyOfType( + heritageType, + memberName, + ); + if (!heritageMember) { + return; + } + const baseMemberType = checker.getTypeOfSymbol(heritageMember); + + if (isVoidReturningFunctionType(checker, member, baseMemberType)) { + context.report({ + node: services.tsNodeToESTreeNodeMap.get(member), + messageId: 'voidReturnSubtype', // Ensure you have a corresponding message for this ID + data: { baseTypeName: heritageTypeName }, + }); + } + }); + }); + } + function checkJSXAttribute(node: TSESTree.JSXAttribute): void { if ( node.value == null || From 8a75efa1832425346d50a697cbb46739a2d2e2f0 Mon Sep 17 00:00:00 2001 From: Aly Thobani Date: Sat, 23 Mar 2024 10:39:57 -0700 Subject: [PATCH 02/26] Finished working logic for no-misused-promises checksVoidReturn.subtypes --- .../src/rules/no-misused-promises.ts | 93 ++-- .../tests/rules/no-misused-promises.test.ts | 403 ++++++++++++++++++ 2 files changed, 458 insertions(+), 38 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-misused-promises.ts b/packages/eslint-plugin/src/rules/no-misused-promises.ts index 909277e6b6e3..eaf6036fd454 100644 --- a/packages/eslint-plugin/src/rules/no-misused-promises.ts +++ b/packages/eslint-plugin/src/rules/no-misused-promises.ts @@ -108,7 +108,7 @@ export default createRule({ attributes: { type: 'boolean' }, properties: { type: 'boolean' }, returns: { type: 'boolean' }, - methods: { type: 'boolean' }, + subtypes: { type: 'boolean' }, variables: { type: 'boolean' }, }, type: 'object', @@ -387,21 +387,20 @@ export default createRule({ | TSESTree.ClassExpression | TSESTree.TSInterfaceDeclaration, ): void { + const nodeName = node.id?.name; const tsNode = services.esTreeNodeToTSNodeMap.get(node); - if ( - tsNode.heritageClauses === undefined || - tsNode.heritageClauses.length === 0 - ) { - return; - } - const heritageTypes = tsNode.heritageClauses - .map(heritageClause => heritageClause.types) + const heritageBaseTypes = tsNode.heritageClauses + ?.map(clause => clause.types) .flat() - .map( - expressionWithTypeArguments => expressionWithTypeArguments.expression, - ) - .map(expression => checker.getTypeAtLocation(expression)); + .map(type => checker.getTypeAtLocation(type.expression)) + .map(type => checker.getApparentType(type)) + .map(type => tsutils.unionTypeParts(type)) + .flat(); + + if (heritageBaseTypes === undefined || heritageBaseTypes.length === 0) { + return; + } tsNode.members.forEach(member => { const memberName = member.name?.getText(); @@ -412,37 +411,55 @@ export default createRule({ if (!doesReturnPromise) { return; } - heritageTypes.forEach(heritageType => { - const heritageTypeName = heritageType.getSymbol()?.name; - if (!heritageTypeName) { + heritageBaseTypes.forEach(heritageBaseType => { + const heritageTypeSymbol = heritageBaseType.getSymbol(); + if (heritageTypeSymbol === undefined) { return; } - for (const subType of tsutils.unionTypeParts( - checker.getApparentType(heritageType), - )) { - const subtypeMember = checker.getPropertyOfType( - subType, + + const heritageTypeName = heritageTypeSymbol.name; + const { valueDeclaration } = heritageTypeSymbol; + if ( + valueDeclaration !== undefined && + ts.isClassLike(valueDeclaration) + ) { + const heritageMemberMatch = valueDeclaration.members.find( + member => member.name?.getText() === memberName, + ); + if (heritageMemberMatch === undefined) { + return; + } + const heritageMemberType = + checker.getTypeAtLocation(heritageMemberMatch); + if ( + isVoidReturningFunctionType(checker, member, heritageMemberType) + ) { + nodeName && memberName; + context.report({ + node: services.tsNodeToESTreeNodeMap.get(member), + messageId: 'voidReturnSubtype', + data: { baseTypeName: heritageTypeName }, + }); + } + } else { + const heritageMemberMatch = checker.getPropertyOfType( + heritageBaseType, memberName, ); - if (!subtypeMember) { + if (heritageMemberMatch === undefined) { return; } - } - const heritageMember = checker.getPropertyOfType( - heritageType, - memberName, - ); - if (!heritageMember) { - return; - } - const baseMemberType = checker.getTypeOfSymbol(heritageMember); - - if (isVoidReturningFunctionType(checker, member, baseMemberType)) { - context.report({ - node: services.tsNodeToESTreeNodeMap.get(member), - messageId: 'voidReturnSubtype', // Ensure you have a corresponding message for this ID - data: { baseTypeName: heritageTypeName }, - }); + const heritageMemberType = + checker.getTypeOfSymbol(heritageMemberMatch); + if ( + isVoidReturningFunctionType(checker, member, heritageMemberType) + ) { + context.report({ + node: services.tsNodeToESTreeNodeMap.get(member), + messageId: 'voidReturnSubtype', + data: { baseTypeName: heritageTypeName }, + }); + } } }); }); diff --git a/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts b/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts index 0db6c0831b04..1b1a51e56c8b 100644 --- a/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts @@ -500,6 +500,272 @@ foo(bar); }, options: [{ checksVoidReturn: { attributes: true } }], }, + // #region checksVoidReturn.subtypes + // #region checksVoidReturn.subtypes: Extending a class + { + // Valid class + code: ` +class MyClass { + setThing(): void { + return; + } +} + +class MySubclassExtendsMyClass extends MyClass { + setThing(): void { + return; + } +} + `, + options: [{ checksVoidReturn: { subtypes: true } }], + }, + { + // Invalid class with rule off + code: ` +class MyClass { + setThing(): void { + return; + } +} + +class MySubclassExtendsMyClass extends MyClass { + async setThing(): Promise { + await Promise.resolve(); + } +} + `, + options: [{ checksVoidReturn: { subtypes: false } }], + }, + { + // Valid abstract class + code: ` +class MyClass { + setThing(): void { + return; + } +} + +abstract class MyAbstractClassExtendsMyClass extends MyClass { + abstract setThing(): void; +} + `, + options: [{ checksVoidReturn: { subtypes: true } }], + }, + { + // Invalid abstract class with rule off + code: ` +class MyClass { + setThing(): void { + return; + } +} + +abstract class MyAbstractClassExtendsMyClass extends MyClass { + abstract setThing(): Promise; +} + `, + options: [{ checksVoidReturn: { subtypes: false } }], + }, + { + // Valid interface + code: ` +class MyClass { + setThing(): void { + return; + } +} + +interface MyInterfaceExtendsMyClass extends MyClass { + setThing(): void; +} + `, + options: [{ checksVoidReturn: { subtypes: true } }], + }, + { + // Invalid interface with rule off + code: ` +class MyClass { + setThing(): void { + return; + } +} + +interface MyInterfaceExtendsMyClass extends MyClass { + setThing(): Promise; +} + `, + options: [{ checksVoidReturn: { subtypes: false } }], + }, + // #endregion + // #region checksVoidReturn.subtypes: Extending an abstract class + { + // Valid class + code: ` +abstract class MyAbstractClass { + abstract setThing(): void; +} + +class MySubclassExtendsMyAbstractClass extends MyAbstractClass { + setThing(): void { + return; + } +} + `, + options: [{ checksVoidReturn: { subtypes: true } }], + }, + { + // Invalid class with rule off + code: ` +abstract class MyAbstractClass { + abstract setThing(): void; +} + +class MySubclassExtendsMyAbstractClass extends MyAbstractClass { + setThing(): Promise { + return; + } +} + `, + options: [{ checksVoidReturn: { subtypes: false } }], + }, + { + // Valid abstract class + code: ` +abstract class MyAbstractClass { + abstract setThing(): void; +} + +abstract class MyAbstractSubclassExtendsMyAbstractClass extends MyAbstractClass { + abstract setThing(): void; +} + `, + options: [{ checksVoidReturn: { subtypes: true } }], + }, + { + // Invalid abstract class with rule off + code: ` +abstract class MyAbstractClass { + abstract setThing(): void; +} + +abstract class MyAbstractSubclassExtendsMyAbstractClass extends MyAbstractClass { + abstract setThing(): Promise; +} + `, + options: [{ checksVoidReturn: { subtypes: false } }], + }, + { + // Valid interface extending abstract class + code: ` +abstract class MyAbstractClass { + abstract setThing(): void; +} + +interface MyInterfaceExtendsMyAbstractClass extends MyAbstractClass { + setThing(): void; +} + `, + options: [{ checksVoidReturn: { subtypes: true } }], + }, + { + // Invalid interface with rule off + code: ` +abstract class MyAbstractClass { + abstract setThing(): void; +} + +interface MyInterfaceExtendsMyAbstractClass extends MyAbstractClass { + setThing(): Promise; +} + `, + options: [{ checksVoidReturn: { subtypes: false } }], + }, + // #endregion + // #region checksVoidReturn.subtypes: Extending an interface + { + // Valid interface + code: ` +interface MyInterface { + setThing(): void; +} + +interface MySubInterfaceExtendsMyInterface extends MyInterface { + setThing(): void; +} + `, + options: [{ checksVoidReturn: { subtypes: true } }], + }, + { + // Invalid interface with rule off + code: ` +interface MyInterface { + setThing(): void; +} + +interface MySubInterfaceExtendsMyInterface extends MyInterface { + setThing(): Promise; +} + `, + options: [{ checksVoidReturn: { subtypes: false } }], + }, + { + // Valid class + code: ` +interface MyInterface { + setThing(): void; +} + +class MyClassImplementsMyInterface implements MyInterface { + setThing(): void { + return; + } +} + `, + options: [{ checksVoidReturn: { subtypes: true } }], + }, + { + // Invalid class with rule off + code: ` +interface MyInterface { + setThing(): void; +} + +class MyClassImplementsMyInterface implements MyInterface { + setThing(): Promise { + return; + } +} + `, + options: [{ checksVoidReturn: { subtypes: false } }], + }, + { + // Valid abstract class + code: ` +interface MyInterface { + setThing(): void; +} + +abstract class MyAbstractClassImplementsMyInterface implements MyInterface { + abstract setThing(): void; +} + `, + options: [{ checksVoidReturn: { subtypes: true } }], + }, + { + // Invalid abstract class with rule off + code: ` +interface MyInterface { + setThing(): void; +} + +abstract class MyAbstractClassImplementsMyInterface implements MyInterface { + abstract setThing(): Promise; +} + `, + options: [{ checksVoidReturn: { subtypes: false } }], + }, + // #endregion + // #endregion ], invalid: [ @@ -1275,5 +1541,142 @@ consume(...cbs); `, errors: [{ line: 4, messageId: 'voidReturnArgument' }], }, + // #region checksVoidReturn.subtypes + // #region checksVoidReturn.subtypes: Extending a class + { + // Invalid class + code: ` +class MyClass { + setThing(): void { + return; + } +} + +class MySubclassExtendsMyClass extends MyClass { + setThing(): Promise { + await Promise.resolve(); + } +} + `, + errors: [{ line: 9, messageId: 'voidReturnSubtype' }], + }, + { + // Invalid abstract class + code: ` +class MyClass { + setThing(): void { + return; + } +} + +abstract class MyAbstractClassExtendsMyClass extends MyClass { + abstract setThing(): Promise; +} + `, + errors: [{ line: 9, messageId: 'voidReturnSubtype' }], + }, + { + // Invalid interface + code: ` +class MyClass { + setThing(): void { + return; + } +} + +interface MyInterfaceExtendsMyClass extends MyClass { + setThing(): Promise; +} + `, + errors: [{ line: 9, messageId: 'voidReturnSubtype' }], + }, + // #endregion + // #region checksVoidReturn.subtypes: Extending an abstract class + { + // Invalid class + code: ` +abstract class MyAbstractClass { + abstract setThing(): void; +} + +class MySubclassExtendsMyAbstractClass extends MyAbstractClass { + setThing(): Promise { + await Promise.resolve(); + } +} + `, + errors: [{ line: 7, messageId: 'voidReturnSubtype' }], + }, + { + // Invalid abstract class + code: ` +abstract class MyAbstractClass { + abstract setThing(): void; +} + +abstract class MyAbstractSubclassExtendsMyAbstractClass extends MyAbstractClass { + abstract setThing(): Promise; +} + `, + errors: [{ line: 7, messageId: 'voidReturnSubtype' }], + }, + { + // Invalid interface + code: ` +abstract class MyAbstractClass { + abstract setThing(): void; +} + +interface MyInterfaceExtendsMyAbstractClass extends MyAbstractClass { + setThing(): Promise; +} + `, + errors: [{ line: 7, messageId: 'voidReturnSubtype' }], + }, + // #endregion + // #region checksVoidReturn.subtypes: Extending an interface + { + // Invalid class + code: ` +interface MyInterface { + setThing(): void; +} + +class MyInterfaceSubclass implements MyInterface { + async setThing(): Promise { + await Promise.resolve(); + } +} + `, + errors: [{ line: 7, messageId: 'voidReturnSubtype' }], + }, + { + // Invalid abstract class + code: ` +interface MyInterface { + setThing(): void; +} + +abstract class MyAbstractClassImplementsMyInterface implements MyInterface { + abstract setThing(): Promise; +} + `, + errors: [{ line: 7, messageId: 'voidReturnSubtype' }], + }, + { + // Invalid interface + code: ` +interface MyInterface { + setThing(): void; +} + +interface MySubInterface extends MyInterface { + setThing(): Promise; +} + `, + errors: [{ line: 7, messageId: 'voidReturnSubtype' }], + }, + // #endregion + // #endregion ], }); From 24283e0e795da9333c8dbc4bbf0a5e0456f8a5ee Mon Sep 17 00:00:00 2001 From: Aly Thobani Date: Sat, 23 Mar 2024 11:06:59 -0700 Subject: [PATCH 03/26] Cleanup --- .../src/rules/no-misused-promises.ts | 43 +++++++++++-------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-misused-promises.ts b/packages/eslint-plugin/src/rules/no-misused-promises.ts index eaf6036fd454..832c3909cbbd 100644 --- a/packages/eslint-plugin/src/rules/no-misused-promises.ts +++ b/packages/eslint-plugin/src/rules/no-misused-promises.ts @@ -387,7 +387,6 @@ export default createRule({ | TSESTree.ClassExpression | TSESTree.TSInterfaceDeclaration, ): void { - const nodeName = node.id?.name; const tsNode = services.esTreeNodeToTSNodeMap.get(node); const heritageBaseTypes = tsNode.heritageClauses @@ -402,23 +401,22 @@ export default createRule({ return; } - tsNode.members.forEach(member => { - const memberName = member.name?.getText(); + tsNode.members.forEach(nodeMember => { + const memberName = nodeMember.name?.getText(); if (!memberName) { return; } - const doesReturnPromise = returnsThenable(checker, member); - if (!doesReturnPromise) { + const nodeMemberReturnsPromise = returnsThenable(checker, nodeMember); + if (!nodeMemberReturnsPromise) { return; } heritageBaseTypes.forEach(heritageBaseType => { - const heritageTypeSymbol = heritageBaseType.getSymbol(); - if (heritageTypeSymbol === undefined) { + const heritageBaseTypeSymbol = heritageBaseType.getSymbol(); + if (heritageBaseTypeSymbol === undefined) { return; } - - const heritageTypeName = heritageTypeSymbol.name; - const { valueDeclaration } = heritageTypeSymbol; + const heritageBaseTypeName = heritageBaseTypeSymbol.name; + const { valueDeclaration } = heritageBaseTypeSymbol; if ( valueDeclaration !== undefined && ts.isClassLike(valueDeclaration) @@ -429,16 +427,19 @@ export default createRule({ if (heritageMemberMatch === undefined) { return; } - const heritageMemberType = + const heritageMemberMatchType = checker.getTypeAtLocation(heritageMemberMatch); if ( - isVoidReturningFunctionType(checker, member, heritageMemberType) + isVoidReturningFunctionType( + checker, + nodeMember, + heritageMemberMatchType, + ) ) { - nodeName && memberName; context.report({ - node: services.tsNodeToESTreeNodeMap.get(member), + node: services.tsNodeToESTreeNodeMap.get(nodeMember), messageId: 'voidReturnSubtype', - data: { baseTypeName: heritageTypeName }, + data: { baseTypeName: heritageBaseTypeName }, }); } } else { @@ -449,15 +450,19 @@ export default createRule({ if (heritageMemberMatch === undefined) { return; } - const heritageMemberType = + const heritageMemberMatchType = checker.getTypeOfSymbol(heritageMemberMatch); if ( - isVoidReturningFunctionType(checker, member, heritageMemberType) + isVoidReturningFunctionType( + checker, + nodeMember, + heritageMemberMatchType, + ) ) { context.report({ - node: services.tsNodeToESTreeNodeMap.get(member), + node: services.tsNodeToESTreeNodeMap.get(nodeMember), messageId: 'voidReturnSubtype', - data: { baseTypeName: heritageTypeName }, + data: { baseTypeName: heritageBaseTypeName }, }); } } From 0936f3b41e80b4233c0c12791880c3da614e87fd Mon Sep 17 00:00:00 2001 From: Aly Thobani Date: Sat, 23 Mar 2024 14:25:22 -0700 Subject: [PATCH 04/26] Refactor and improve (better more concise approach), and add more test cases --- .../src/rules/no-misused-promises.ts | 78 +++---- .../tests/rules/no-misused-promises.test.ts | 200 ++++++++++++++++-- 2 files changed, 213 insertions(+), 65 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-misused-promises.ts b/packages/eslint-plugin/src/rules/no-misused-promises.ts index 832c3909cbbd..41929aaafc7f 100644 --- a/packages/eslint-plugin/src/rules/no-misused-promises.ts +++ b/packages/eslint-plugin/src/rules/no-misused-promises.ts @@ -403,7 +403,7 @@ export default createRule({ tsNode.members.forEach(nodeMember => { const memberName = nodeMember.name?.getText(); - if (!memberName) { + if (memberName === undefined) { return; } const nodeMemberReturnsPromise = returnsThenable(checker, nodeMember); @@ -415,56 +415,22 @@ export default createRule({ if (heritageBaseTypeSymbol === undefined) { return; } - const heritageBaseTypeName = heritageBaseTypeSymbol.name; - const { valueDeclaration } = heritageBaseTypeSymbol; + const heritageMemberType = getTypeOfMatchingHeritageMemberIfExists( + checker, + heritageBaseTypeSymbol, + memberName, + ); + if (heritageMemberType === undefined) { + return; + } if ( - valueDeclaration !== undefined && - ts.isClassLike(valueDeclaration) + isVoidReturningFunctionType(checker, nodeMember, heritageMemberType) ) { - const heritageMemberMatch = valueDeclaration.members.find( - member => member.name?.getText() === memberName, - ); - if (heritageMemberMatch === undefined) { - return; - } - const heritageMemberMatchType = - checker.getTypeAtLocation(heritageMemberMatch); - if ( - isVoidReturningFunctionType( - checker, - nodeMember, - heritageMemberMatchType, - ) - ) { - context.report({ - node: services.tsNodeToESTreeNodeMap.get(nodeMember), - messageId: 'voidReturnSubtype', - data: { baseTypeName: heritageBaseTypeName }, - }); - } - } else { - const heritageMemberMatch = checker.getPropertyOfType( - heritageBaseType, - memberName, - ); - if (heritageMemberMatch === undefined) { - return; - } - const heritageMemberMatchType = - checker.getTypeOfSymbol(heritageMemberMatch); - if ( - isVoidReturningFunctionType( - checker, - nodeMember, - heritageMemberMatchType, - ) - ) { - context.report({ - node: services.tsNodeToESTreeNodeMap.get(nodeMember), - messageId: 'voidReturnSubtype', - data: { baseTypeName: heritageBaseTypeName }, - }); - } + context.report({ + node: services.tsNodeToESTreeNodeMap.get(nodeMember), + messageId: 'voidReturnSubtype', + data: { baseTypeName: heritageBaseTypeSymbol.name }, + }); } }); }); @@ -781,3 +747,17 @@ function returnsThenable(checker: ts.TypeChecker, node: ts.Node): boolean { .unionTypeParts(type) .some(t => anySignatureIsThenableType(checker, node, t)); } + +function getTypeOfMatchingHeritageMemberIfExists( + checker: ts.TypeChecker, + heritageBaseTypeSymbol: ts.Symbol, + memberName: string, +): ts.Type | undefined { + const memberSymbol = heritageBaseTypeSymbol.members?.get( + ts.escapeLeadingUnderscores(memberName), + ); + if (memberSymbol === undefined) { + return undefined; + } + return checker.getTypeOfSymbol(memberSymbol); +} diff --git a/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts b/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts index 1b1a51e56c8b..d34b133dec6b 100644 --- a/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts @@ -503,7 +503,7 @@ foo(bar); // #region checksVoidReturn.subtypes // #region checksVoidReturn.subtypes: Extending a class { - // Valid class + // Valid void-returning class code: ` class MyClass { setThing(): void { @@ -515,6 +515,23 @@ class MySubclassExtendsMyClass extends MyClass { setThing(): void { return; } +} + `, + options: [{ checksVoidReturn: { subtypes: true } }], + }, + { + // Valid promise-returning class + code: ` +class MyClass { + async setThing(): Promise { + await Promise.resolve(); + } +} + +class MySubclassExtendsMyClass extends MyClass { + async setThing(): Promise { + await Promise.resolve(); + } } `, options: [{ checksVoidReturn: { subtypes: true } }], @@ -621,8 +638,8 @@ abstract class MyAbstractClass { } class MySubclassExtendsMyAbstractClass extends MyAbstractClass { - setThing(): Promise { - return; + async setThing(): Promise { + await Promise.resolve(); } } `, @@ -731,8 +748,8 @@ interface MyInterface { } class MyClassImplementsMyInterface implements MyInterface { - setThing(): Promise { - return; + async setThing(): Promise { + await Promise.resolve(); } } `, @@ -765,6 +782,71 @@ abstract class MyAbstractClassImplementsMyInterface implements MyInterface { options: [{ checksVoidReturn: { subtypes: false } }], }, // #endregion + // #region checksVoidReturn.subtypes: Multiple heritage types + { + // Valid interface extending two interfaces + code: ` +interface MyInterface { + setThing(): void; +} + +interface MyOtherInterface { + setThing(): void; +} + +interface MyThirdInterface extends MyInterface, MyOtherInterface { + setThing(): void; +} + `, + options: [{ checksVoidReturn: { subtypes: true } }], + }, + { + // Valid interface extending two classes + code: ` +class MyClass { + setThing(): void { + return; + } +} + +class MyOtherClass { + setThing(): void { + return; + } +} + +interface MyInterface extends MyClass, MyOtherClass { + setThing(): void; +} + `, + options: [{ checksVoidReturn: { subtypes: true } }], + }, + { + // Valid class extending a class and implementing two interfaces + code: ` +interface MyInterface { + setThing(): void; +} + +interface MyOtherInterface { + setThing(): void; +} + +class MyClass { + setThing(): void { + return; + } +} + +class MySubclass extends MyClass implements MyInterface, MyOtherInterface { + setThing(): void { + return; + } +} + `, + options: [{ checksVoidReturn: { subtypes: true } }], + }, + // #endregion // #endregion ], @@ -1544,7 +1626,6 @@ consume(...cbs); // #region checksVoidReturn.subtypes // #region checksVoidReturn.subtypes: Extending a class { - // Invalid class code: ` class MyClass { setThing(): void { @@ -1553,7 +1634,7 @@ class MyClass { } class MySubclassExtendsMyClass extends MyClass { - setThing(): Promise { + async setThing(): Promise { await Promise.resolve(); } } @@ -1561,7 +1642,6 @@ class MySubclassExtendsMyClass extends MyClass { errors: [{ line: 9, messageId: 'voidReturnSubtype' }], }, { - // Invalid abstract class code: ` class MyClass { setThing(): void { @@ -1576,7 +1656,6 @@ abstract class MyAbstractClassExtendsMyClass extends MyClass { errors: [{ line: 9, messageId: 'voidReturnSubtype' }], }, { - // Invalid interface code: ` class MyClass { setThing(): void { @@ -1593,14 +1672,13 @@ interface MyInterfaceExtendsMyClass extends MyClass { // #endregion // #region checksVoidReturn.subtypes: Extending an abstract class { - // Invalid class code: ` abstract class MyAbstractClass { abstract setThing(): void; } class MySubclassExtendsMyAbstractClass extends MyAbstractClass { - setThing(): Promise { + async setThing(): Promise { await Promise.resolve(); } } @@ -1608,7 +1686,6 @@ class MySubclassExtendsMyAbstractClass extends MyAbstractClass { errors: [{ line: 7, messageId: 'voidReturnSubtype' }], }, { - // Invalid abstract class code: ` abstract class MyAbstractClass { abstract setThing(): void; @@ -1621,7 +1698,6 @@ abstract class MyAbstractSubclassExtendsMyAbstractClass extends MyAbstractClass errors: [{ line: 7, messageId: 'voidReturnSubtype' }], }, { - // Invalid interface code: ` abstract class MyAbstractClass { abstract setThing(): void; @@ -1636,7 +1712,6 @@ interface MyInterfaceExtendsMyAbstractClass extends MyAbstractClass { // #endregion // #region checksVoidReturn.subtypes: Extending an interface { - // Invalid class code: ` interface MyInterface { setThing(): void; @@ -1651,7 +1726,6 @@ class MyInterfaceSubclass implements MyInterface { errors: [{ line: 7, messageId: 'voidReturnSubtype' }], }, { - // Invalid abstract class code: ` interface MyInterface { setThing(): void; @@ -1664,7 +1738,6 @@ abstract class MyAbstractClassImplementsMyInterface implements MyInterface { errors: [{ line: 7, messageId: 'voidReturnSubtype' }], }, { - // Invalid interface code: ` interface MyInterface { setThing(): void; @@ -1677,6 +1750,101 @@ interface MySubInterface extends MyInterface { errors: [{ line: 7, messageId: 'voidReturnSubtype' }], }, // #endregion + // #region checksVoidReturn.subtypes: Multiple heritage types + { + code: ` +interface MyInterface { + setThing(): void; +} + +interface MyOtherInterface { + setThing(): void; +} + +interface MyThirdInterface extends MyInterface, MyOtherInterface { + setThing(): Promise; +} + `, + errors: [ + { + line: 11, + messageId: 'voidReturnSubtype', + data: { baseTypeName: 'MyInterface' }, + }, + { + line: 11, + messageId: 'voidReturnSubtype', + data: { baseTypeName: 'MyOtherInterface' }, + }, + ], + }, + { + code: ` +class MyClass { + setThing(): void { + return; + } +} + +class MyOtherClass { + setThing(): void { + return; + } +} + +interface MyInterface extends MyClass, MyOtherClass { + setThing(): Promise; +} + `, + errors: [ + { + line: 15, + messageId: 'voidReturnSubtype', + data: { baseTypeName: 'MyClass' }, + }, + { + line: 15, + messageId: 'voidReturnSubtype', + data: { baseTypeName: 'MyOtherClass' }, + }, + ], + }, + { + code: ` +interface MyAsyncInterface { + setThing(): Promise; +} + +interface MySyncInterface { + setThing(): void; +} + +class MyClass { + setThing(): void { + return; + } +} + +class MySubclass extends MyClass implements MyAsyncInterface, MySyncInterface { + async setThing(): Promise { + await Promise.resolve(); + } +} + `, + errors: [ + { + line: 17, + messageId: 'voidReturnSubtype', + data: { baseTypeName: 'MyClass' }, + }, + { + line: 17, + messageId: 'voidReturnSubtype', + data: { baseTypeName: 'MySyncInterface' }, + }, + ], + }, + // #endregion // #endregion ], }); From 3cc1c0d8582fdf03d132ce7ff32214d67e283f10 Mon Sep 17 00:00:00 2001 From: Aly Thobani Date: Sat, 23 Mar 2024 19:39:57 -0700 Subject: [PATCH 05/26] Handle type aliases and add type alias test cases --- .../src/rules/no-misused-promises.ts | 32 +++---- .../tests/rules/no-misused-promises.test.ts | 83 +++++++++++++++++++ 2 files changed, 100 insertions(+), 15 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-misused-promises.ts b/packages/eslint-plugin/src/rules/no-misused-promises.ts index 41929aaafc7f..5d84e2d8c9b0 100644 --- a/packages/eslint-plugin/src/rules/no-misused-promises.ts +++ b/packages/eslint-plugin/src/rules/no-misused-promises.ts @@ -388,14 +388,10 @@ export default createRule({ | TSESTree.TSInterfaceDeclaration, ): void { const tsNode = services.esTreeNodeToTSNodeMap.get(node); - const heritageBaseTypes = tsNode.heritageClauses ?.map(clause => clause.types) .flat() - .map(type => checker.getTypeAtLocation(type.expression)) - .map(type => checker.getApparentType(type)) - .map(type => tsutils.unionTypeParts(type)) - .flat(); + .map(type => checker.getTypeFromTypeNode(type)); if (heritageBaseTypes === undefined || heritageBaseTypes.length === 0) { return; @@ -411,13 +407,9 @@ export default createRule({ return; } heritageBaseTypes.forEach(heritageBaseType => { - const heritageBaseTypeSymbol = heritageBaseType.getSymbol(); - if (heritageBaseTypeSymbol === undefined) { - return; - } const heritageMemberType = getTypeOfMatchingHeritageMemberIfExists( checker, - heritageBaseTypeSymbol, + heritageBaseType, memberName, ); if (heritageMemberType === undefined) { @@ -429,7 +421,7 @@ export default createRule({ context.report({ node: services.tsNodeToESTreeNodeMap.get(nodeMember), messageId: 'voidReturnSubtype', - data: { baseTypeName: heritageBaseTypeSymbol.name }, + data: { baseTypeName: checker.typeToString(heritageBaseType) }, }); } }); @@ -748,14 +740,24 @@ function returnsThenable(checker: ts.TypeChecker, node: ts.Node): boolean { .some(t => anySignatureIsThenableType(checker, node, t)); } +/** + * @returns The type of the member or property with the given name in the heritage base type, if it exists. + */ function getTypeOfMatchingHeritageMemberIfExists( checker: ts.TypeChecker, - heritageBaseTypeSymbol: ts.Symbol, + heritageBaseType: ts.Type, memberName: string, ): ts.Type | undefined { - const memberSymbol = heritageBaseTypeSymbol.members?.get( - ts.escapeLeadingUnderscores(memberName), - ); + const escapedMemberName = ts.escapeLeadingUnderscores(memberName); + const heritageBaseTypeSymbol = heritageBaseType.getSymbol(); + if (heritageBaseTypeSymbol === undefined) { + const matchingProperty = tsutils.getPropertyOfType( + heritageBaseType, + escapedMemberName, + ); + return matchingProperty && checker.getTypeOfSymbol(matchingProperty); + } + const memberSymbol = heritageBaseTypeSymbol.members?.get(escapedMemberName); if (memberSymbol === undefined) { return undefined; } diff --git a/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts b/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts index d34b133dec6b..03384cc30df4 100644 --- a/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts @@ -777,6 +777,62 @@ interface MyInterface { abstract class MyAbstractClassImplementsMyInterface implements MyInterface { abstract setThing(): Promise; +} + `, + options: [{ checksVoidReturn: { subtypes: false } }], + }, + // #endregion + // #region checksVoidReturn.subtypes: Extending type aliases + { + // Valid class extending type literals intersection + code: ` +type MyTypeLiteralsIntersection = { setThing(): void } & { thing: number }; + +class MyClass implements MyTypeLiteralsIntersection { + thing = 1; + setThing(): void { + return; + } +} + `, + options: [{ checksVoidReturn: { subtypes: true } }], + }, + { + // Invalid class with rule off + code: ` +type MyTypeLiteralsIntersection = { setThing(): void } & { thing: number }; + +class MyClass implements MyTypeLiteralsIntersection { + thing = 1; + async setThing(): Promise { + await Promise.resolve(); + } +} + `, + options: [{ checksVoidReturn: { subtypes: false } }], + }, + { + // Valid interface extending generic type + code: ` +type MyGenericType = IsAsync extends true + ? { setThing(): Promise } + : { setThing(): void }; + +interface MyAsyncInterface extends MyGenericType { + setThing(): Promise; +} + `, + options: [{ checksVoidReturn: { subtypes: true } }], + }, + { + // Invalid interface with rule off + code: ` +type MyGenericType = IsAsync extends true + ? { setThing(): Promise } + : { setThing(): void }; + +interface MyAsyncInterface extends MyGenericType { + setThing(): Promise; } `, options: [{ checksVoidReturn: { subtypes: false } }], @@ -1745,6 +1801,33 @@ interface MyInterface { interface MySubInterface extends MyInterface { setThing(): Promise; +} + `, + errors: [{ line: 7, messageId: 'voidReturnSubtype' }], + }, + // #endregion + // #region checksVoidReturn.subtypes: Extending type aliases + { + code: ` +type MyTypeIntersection = { setThing(): void } & { thing: number }; + +class MyClassImplementsMyTypeIntersection implements MyTypeIntersection { + thing = 1; + async setThing(): Promise { + await Promise.resolve(); + } +} + `, + errors: [{ line: 6, messageId: 'voidReturnSubtype' }], + }, + { + code: ` +type MyGenericType = IsAsync extends true + ? { setThing(): Promise } + : { setThing(): void }; + +interface MyAsyncInterface extends MyGenericType { + setThing(): Promise; } `, errors: [{ line: 7, messageId: 'voidReturnSubtype' }], From 7146bcebf2dd27bd81b29de8a335dd8897e6d00f Mon Sep 17 00:00:00 2001 From: Aly Thobani Date: Sat, 23 Mar 2024 23:59:32 -0700 Subject: [PATCH 06/26] Added expected {{ baseTypeName }} values to test cases --- .../tests/rules/no-misused-promises.test.ts | 88 ++++++++++++++++--- 1 file changed, 77 insertions(+), 11 deletions(-) diff --git a/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts b/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts index 03384cc30df4..edec74aaf2cd 100644 --- a/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts @@ -1695,7 +1695,13 @@ class MySubclassExtendsMyClass extends MyClass { } } `, - errors: [{ line: 9, messageId: 'voidReturnSubtype' }], + errors: [ + { + line: 9, + messageId: 'voidReturnSubtype', + data: { baseTypeName: 'MyClass' }, + }, + ], }, { code: ` @@ -1709,7 +1715,13 @@ abstract class MyAbstractClassExtendsMyClass extends MyClass { abstract setThing(): Promise; } `, - errors: [{ line: 9, messageId: 'voidReturnSubtype' }], + errors: [ + { + line: 9, + messageId: 'voidReturnSubtype', + data: { baseTypeName: 'MyClass' }, + }, + ], }, { code: ` @@ -1723,7 +1735,13 @@ interface MyInterfaceExtendsMyClass extends MyClass { setThing(): Promise; } `, - errors: [{ line: 9, messageId: 'voidReturnSubtype' }], + errors: [ + { + line: 9, + messageId: 'voidReturnSubtype', + data: { baseTypeName: 'MyClass' }, + }, + ], }, // #endregion // #region checksVoidReturn.subtypes: Extending an abstract class @@ -1739,7 +1757,13 @@ class MySubclassExtendsMyAbstractClass extends MyAbstractClass { } } `, - errors: [{ line: 7, messageId: 'voidReturnSubtype' }], + errors: [ + { + line: 7, + messageId: 'voidReturnSubtype', + data: { baseTypeName: 'MyAbstractClass' }, + }, + ], }, { code: ` @@ -1751,7 +1775,13 @@ abstract class MyAbstractSubclassExtendsMyAbstractClass extends MyAbstractClass abstract setThing(): Promise; } `, - errors: [{ line: 7, messageId: 'voidReturnSubtype' }], + errors: [ + { + line: 7, + messageId: 'voidReturnSubtype', + data: { baseTypeName: 'MyAbstractClass' }, + }, + ], }, { code: ` @@ -1763,7 +1793,13 @@ interface MyInterfaceExtendsMyAbstractClass extends MyAbstractClass { setThing(): Promise; } `, - errors: [{ line: 7, messageId: 'voidReturnSubtype' }], + errors: [ + { + line: 7, + messageId: 'voidReturnSubtype', + data: { baseTypeName: 'MyAbstractClass' }, + }, + ], }, // #endregion // #region checksVoidReturn.subtypes: Extending an interface @@ -1779,7 +1815,13 @@ class MyInterfaceSubclass implements MyInterface { } } `, - errors: [{ line: 7, messageId: 'voidReturnSubtype' }], + errors: [ + { + line: 7, + messageId: 'voidReturnSubtype', + data: { baseTypeName: 'MyInterface' }, + }, + ], }, { code: ` @@ -1791,7 +1833,13 @@ abstract class MyAbstractClassImplementsMyInterface implements MyInterface { abstract setThing(): Promise; } `, - errors: [{ line: 7, messageId: 'voidReturnSubtype' }], + errors: [ + { + line: 7, + messageId: 'voidReturnSubtype', + data: { baseTypeName: 'MyInterface' }, + }, + ], }, { code: ` @@ -1803,7 +1851,13 @@ interface MySubInterface extends MyInterface { setThing(): Promise; } `, - errors: [{ line: 7, messageId: 'voidReturnSubtype' }], + errors: [ + { + line: 7, + messageId: 'voidReturnSubtype', + data: { baseTypeName: 'MyInterface' }, + }, + ], }, // #endregion // #region checksVoidReturn.subtypes: Extending type aliases @@ -1818,7 +1872,13 @@ class MyClassImplementsMyTypeIntersection implements MyTypeIntersection { } } `, - errors: [{ line: 6, messageId: 'voidReturnSubtype' }], + errors: [ + { + line: 6, + messageId: 'voidReturnSubtype', + data: { baseTypeName: 'MyTypeIntersection' }, + }, + ], }, { code: ` @@ -1830,7 +1890,13 @@ interface MyAsyncInterface extends MyGenericType { setThing(): Promise; } `, - errors: [{ line: 7, messageId: 'voidReturnSubtype' }], + errors: [ + { + line: 7, + messageId: 'voidReturnSubtype', + data: { baseTypeName: '{ setThing(): void; }' }, + }, + ], }, // #endregion // #region checksVoidReturn.subtypes: Multiple heritage types From 87c69471beb8b8612390a99e95203bac40463fe0 Mon Sep 17 00:00:00 2001 From: Aly Thobani Date: Sun, 24 Mar 2024 23:15:56 -0700 Subject: [PATCH 07/26] Added test cases for handling class expressions, and fixed code to handle them --- .../src/rules/no-misused-promises.ts | 44 +++---- .../tests/rules/no-misused-promises.test.ts | 120 ++++++++++++++++++ 2 files changed, 136 insertions(+), 28 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-misused-promises.ts b/packages/eslint-plugin/src/rules/no-misused-promises.ts index 5d84e2d8c9b0..0670a96874c6 100644 --- a/packages/eslint-plugin/src/rules/no-misused-promises.ts +++ b/packages/eslint-plugin/src/rules/no-misused-promises.ts @@ -388,12 +388,12 @@ export default createRule({ | TSESTree.TSInterfaceDeclaration, ): void { const tsNode = services.esTreeNodeToTSNodeMap.get(node); - const heritageBaseTypes = tsNode.heritageClauses + const heritageTypes = tsNode.heritageClauses ?.map(clause => clause.types) .flat() - .map(type => checker.getTypeFromTypeNode(type)); + .map(typeExpression => checker.getTypeAtLocation(typeExpression)); - if (heritageBaseTypes === undefined || heritageBaseTypes.length === 0) { + if (heritageTypes === undefined || heritageTypes.length === 0) { return; } @@ -402,26 +402,23 @@ export default createRule({ if (memberName === undefined) { return; } - const nodeMemberReturnsPromise = returnsThenable(checker, nodeMember); - if (!nodeMemberReturnsPromise) { + if (!returnsThenable(checker, nodeMember)) { return; } - heritageBaseTypes.forEach(heritageBaseType => { - const heritageMemberType = getTypeOfMatchingHeritageMemberIfExists( + heritageTypes.forEach(heritageType => { + const heritageMemberType = getTypeOfMemberOrPropertyIfExists( checker, - heritageBaseType, + heritageType, memberName, ); - if (heritageMemberType === undefined) { - return; - } if ( + heritageMemberType !== undefined && isVoidReturningFunctionType(checker, nodeMember, heritageMemberType) ) { context.report({ node: services.tsNodeToESTreeNodeMap.get(nodeMember), messageId: 'voidReturnSubtype', - data: { baseTypeName: checker.typeToString(heritageBaseType) }, + data: { baseTypeName: checker.typeToString(heritageType) }, }); } }); @@ -741,25 +738,16 @@ function returnsThenable(checker: ts.TypeChecker, node: ts.Node): boolean { } /** - * @returns The type of the member or property with the given name in the heritage base type, if it exists. + * @returns The type of the member or property with the given name in the given type, if it exists. */ -function getTypeOfMatchingHeritageMemberIfExists( +function getTypeOfMemberOrPropertyIfExists( checker: ts.TypeChecker, - heritageBaseType: ts.Type, + type: ts.Type, memberName: string, ): ts.Type | undefined { const escapedMemberName = ts.escapeLeadingUnderscores(memberName); - const heritageBaseTypeSymbol = heritageBaseType.getSymbol(); - if (heritageBaseTypeSymbol === undefined) { - const matchingProperty = tsutils.getPropertyOfType( - heritageBaseType, - escapedMemberName, - ); - return matchingProperty && checker.getTypeOfSymbol(matchingProperty); - } - const memberSymbol = heritageBaseTypeSymbol.members?.get(escapedMemberName); - if (memberSymbol === undefined) { - return undefined; - } - return checker.getTypeOfSymbol(memberSymbol); + const symbolMemberMatch = type.getSymbol()?.members?.get(escapedMemberName); + const memberMatch = + symbolMemberMatch ?? tsutils.getPropertyOfType(type, escapedMemberName); + return memberMatch && checker.getTypeOfSymbol(memberMatch); } diff --git a/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts b/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts index edec74aaf2cd..0bd3190e043e 100644 --- a/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts @@ -898,6 +898,58 @@ class MySubclass extends MyClass implements MyInterface, MyOtherInterface { setThing(): void { return; } +} + `, + options: [{ checksVoidReturn: { subtypes: true } }], + }, + // #endregion + // #region checksVoidReturn.subtypes: Class expressions + { + // Valid class expression extending a class + code: ` +class MyClass { + setThing(): void { + return; + } +} + +const MyClassExpressionExtendsMyClass = class extends MyClass { + setThing(): void { + return; + } +}; + `, + options: [{ checksVoidReturn: { subtypes: true } }], + }, + { + // Valid class extending a class expression + code: ` +const MyClassExpression = class { + setThing(): void { + return; + } +}; + +class MyClassExtendsMyClassExpression extends MyClassExpression { + setThing(): void { + return; + } +} + `, + options: [{ checksVoidReturn: { subtypes: true } }], + }, + { + // Valid interface implementing a class expression + code: ` +const MyClassExpression = class { + setThing(): void { + return; + } +}; +type MyClassExpressionType = typeof MyClassExpression; + +interface MyInterfaceExtendsMyClassExpression extends MyClassExpressionType { + setThing(): void; } `, options: [{ checksVoidReturn: { subtypes: true } }], @@ -1994,6 +2046,74 @@ class MySubclass extends MyClass implements MyAsyncInterface, MySyncInterface { ], }, // #endregion + // #region checksVoidReturn.subtypes: Class expressions + { + // Invalid class expression implementing an interface + code: ` +interface MyInterface { + setThing(): void; +} + +const MyClassExpressionExtendsMyClass = class implements MyInterface { + setThing(): Promise { + await Promise.resolve(); + } +}; + `, + errors: [ + { + line: 7, + messageId: 'voidReturnSubtype', + data: { baseTypeName: 'MyInterface' }, + }, + ], + }, + { + // Invalid class extending a class expression + code: ` +const MyClassExpression = class { + setThing(): void { + return; + } +}; + +class MyClassExtendsMyClassExpression extends MyClassExpression { + async setThing(): Promise { + await Promise.resolve(); + } +} + `, + errors: [ + { + line: 9, + messageId: 'voidReturnSubtype', + data: { baseTypeName: 'MyClassExpression' }, + }, + ], + }, + { + // Invalid interface implementing a class expression + code: ` +const MyClassExpression = class { + setThing(): void { + return; + } +}; +type MyClassExpressionType = typeof MyClassExpression; + +interface MyInterfaceExtendsMyClassExpression extends MyClassExpressionType { + setThing(): Promise; +} + `, + errors: [ + { + line: 10, + messageId: 'voidReturnSubtype', + data: { baseTypeName: 'typeof MyClassExpression' }, + }, + ], + }, + // #endregion // #endregion ], }); From b3c477ef198812f80f8618f2bc9c6b7c28555109 Mon Sep 17 00:00:00 2001 From: Aly Thobani Date: Sun, 24 Mar 2024 23:29:50 -0700 Subject: [PATCH 08/26] Fix no-misused-promises schema snapshot to account for new subtypes option --- .../tests/schema-snapshots/no-misused-promises.shot | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/eslint-plugin/tests/schema-snapshots/no-misused-promises.shot b/packages/eslint-plugin/tests/schema-snapshots/no-misused-promises.shot index 2ba6191f4645..b6bc5b453a33 100644 --- a/packages/eslint-plugin/tests/schema-snapshots/no-misused-promises.shot +++ b/packages/eslint-plugin/tests/schema-snapshots/no-misused-promises.shot @@ -34,6 +34,9 @@ exports[`Rule schemas should be convertible to TS types for documentation purpos "returns": { "type": "boolean" }, + "subtypes": { + "type": "boolean" + }, "variables": { "type": "boolean" } @@ -60,6 +63,7 @@ type Options = [ attributes?: boolean; properties?: boolean; returns?: boolean; + subtypes?: boolean; variables?: boolean; } | boolean; From 714fffed4d3474e66815effb088cf0efdb7a5182 Mon Sep 17 00:00:00 2001 From: Aly Thobani Date: Mon, 25 Mar 2024 00:08:14 -0700 Subject: [PATCH 09/26] Updated copy (base type => heritage type) and added documentation for new subtypes option --- .../docs/rules/no-misused-promises.mdx | 19 +++++++++++++++++++ .../src/rules/no-misused-promises.ts | 4 ++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/no-misused-promises.mdx b/packages/eslint-plugin/docs/rules/no-misused-promises.mdx index ff044b712d12..c65655e86cb0 100644 --- a/packages/eslint-plugin/docs/rules/no-misused-promises.mdx +++ b/packages/eslint-plugin/docs/rules/no-misused-promises.mdx @@ -102,6 +102,7 @@ The following options are supported: - `attributes`: Disables checking an asynchronous function passed as a JSX attribute expected to be a function that returns `void` - `properties`: Disables checking an asynchronous function passed as an object property expected to be a function that returns `void` - `returns`: Disables checking an asynchronous function returned in a function whose return type is a function that returns `void` +- `subtypes`: Disables checking an asynchronous method in a type whose extended/implemented heritage type expects that method to return `void` - `variables`: Disables checking an asynchronous function used as a variable whose return type is a function that returns `void` For example, if you don't mind that passing a `() => Promise` to a `() => void` parameter or JSX attribute can lead to a floating unhandled Promise: @@ -219,6 +220,15 @@ const getData2 = async () => { }; return { foo: 42, ...getData2() }; + +interface MyInterface { + setThing(): void; +} +class MyClass implements MyInterface { + async setThing(): Promise { + this.thing = await fetchThing(); + } +} ``` @@ -234,6 +244,15 @@ const getData2 = async () => { }; return { foo: 42, ...(await getData2()) }; + +interface MyInterface { + setThing(): Promise; +} +class MyClass implements MyInterface { + async setThing(): Promise { + this.thing = await fetchThing(); + } +} ``` diff --git a/packages/eslint-plugin/src/rules/no-misused-promises.ts b/packages/eslint-plugin/src/rules/no-misused-promises.ts index 0670a96874c6..49245496e030 100644 --- a/packages/eslint-plugin/src/rules/no-misused-promises.ts +++ b/packages/eslint-plugin/src/rules/no-misused-promises.ts @@ -84,7 +84,7 @@ export default createRule({ voidReturnReturnValue: 'Promise-returning function provided to return value where a void return was expected.', voidReturnSubtype: - "Promise-returning method provided where a void return was expected by base type '{{ baseTypeName }}'.", + "Promise-returning method provided where a void return was expected by heritage type '{{ heritageTypeName }}'.", voidReturnVariable: 'Promise-returning function provided to variable where a void return was expected.', conditional: 'Expected non-Promise value in a boolean conditional.', @@ -418,7 +418,7 @@ export default createRule({ context.report({ node: services.tsNodeToESTreeNodeMap.get(nodeMember), messageId: 'voidReturnSubtype', - data: { baseTypeName: checker.typeToString(heritageType) }, + data: { heritageTypeName: checker.typeToString(heritageType) }, }); } }); From 1f09c9b33012a0ef533f9559723d26cf34bff13b Mon Sep 17 00:00:00 2001 From: Aly Thobani Date: Mon, 25 Mar 2024 00:50:25 -0700 Subject: [PATCH 10/26] Update copy in test cases (baseTypeName => heritageTypeName) --- .../tests/rules/no-misused-promises.test.ts | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts b/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts index 0bd3190e043e..6d3ec6947081 100644 --- a/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts @@ -1751,7 +1751,7 @@ class MySubclassExtendsMyClass extends MyClass { { line: 9, messageId: 'voidReturnSubtype', - data: { baseTypeName: 'MyClass' }, + data: { heritageTypeName: 'MyClass' }, }, ], }, @@ -1771,7 +1771,7 @@ abstract class MyAbstractClassExtendsMyClass extends MyClass { { line: 9, messageId: 'voidReturnSubtype', - data: { baseTypeName: 'MyClass' }, + data: { heritageTypeName: 'MyClass' }, }, ], }, @@ -1791,7 +1791,7 @@ interface MyInterfaceExtendsMyClass extends MyClass { { line: 9, messageId: 'voidReturnSubtype', - data: { baseTypeName: 'MyClass' }, + data: { heritageTypeName: 'MyClass' }, }, ], }, @@ -1813,7 +1813,7 @@ class MySubclassExtendsMyAbstractClass extends MyAbstractClass { { line: 7, messageId: 'voidReturnSubtype', - data: { baseTypeName: 'MyAbstractClass' }, + data: { heritageTypeName: 'MyAbstractClass' }, }, ], }, @@ -1831,7 +1831,7 @@ abstract class MyAbstractSubclassExtendsMyAbstractClass extends MyAbstractClass { line: 7, messageId: 'voidReturnSubtype', - data: { baseTypeName: 'MyAbstractClass' }, + data: { heritageTypeName: 'MyAbstractClass' }, }, ], }, @@ -1849,7 +1849,7 @@ interface MyInterfaceExtendsMyAbstractClass extends MyAbstractClass { { line: 7, messageId: 'voidReturnSubtype', - data: { baseTypeName: 'MyAbstractClass' }, + data: { heritageTypeName: 'MyAbstractClass' }, }, ], }, @@ -1871,7 +1871,7 @@ class MyInterfaceSubclass implements MyInterface { { line: 7, messageId: 'voidReturnSubtype', - data: { baseTypeName: 'MyInterface' }, + data: { heritageTypeName: 'MyInterface' }, }, ], }, @@ -1889,7 +1889,7 @@ abstract class MyAbstractClassImplementsMyInterface implements MyInterface { { line: 7, messageId: 'voidReturnSubtype', - data: { baseTypeName: 'MyInterface' }, + data: { heritageTypeName: 'MyInterface' }, }, ], }, @@ -1907,7 +1907,7 @@ interface MySubInterface extends MyInterface { { line: 7, messageId: 'voidReturnSubtype', - data: { baseTypeName: 'MyInterface' }, + data: { heritageTypeName: 'MyInterface' }, }, ], }, @@ -1928,7 +1928,7 @@ class MyClassImplementsMyTypeIntersection implements MyTypeIntersection { { line: 6, messageId: 'voidReturnSubtype', - data: { baseTypeName: 'MyTypeIntersection' }, + data: { heritageTypeName: 'MyTypeIntersection' }, }, ], }, @@ -1946,7 +1946,7 @@ interface MyAsyncInterface extends MyGenericType { { line: 7, messageId: 'voidReturnSubtype', - data: { baseTypeName: '{ setThing(): void; }' }, + data: { heritageTypeName: '{ setThing(): void; }' }, }, ], }, @@ -1970,12 +1970,12 @@ interface MyThirdInterface extends MyInterface, MyOtherInterface { { line: 11, messageId: 'voidReturnSubtype', - data: { baseTypeName: 'MyInterface' }, + data: { heritageTypeName: 'MyInterface' }, }, { line: 11, messageId: 'voidReturnSubtype', - data: { baseTypeName: 'MyOtherInterface' }, + data: { heritageTypeName: 'MyOtherInterface' }, }, ], }, @@ -2001,12 +2001,12 @@ interface MyInterface extends MyClass, MyOtherClass { { line: 15, messageId: 'voidReturnSubtype', - data: { baseTypeName: 'MyClass' }, + data: { heritageTypeName: 'MyClass' }, }, { line: 15, messageId: 'voidReturnSubtype', - data: { baseTypeName: 'MyOtherClass' }, + data: { heritageTypeName: 'MyOtherClass' }, }, ], }, @@ -2036,12 +2036,12 @@ class MySubclass extends MyClass implements MyAsyncInterface, MySyncInterface { { line: 17, messageId: 'voidReturnSubtype', - data: { baseTypeName: 'MyClass' }, + data: { heritageTypeName: 'MyClass' }, }, { line: 17, messageId: 'voidReturnSubtype', - data: { baseTypeName: 'MySyncInterface' }, + data: { heritageTypeName: 'MySyncInterface' }, }, ], }, @@ -2064,7 +2064,7 @@ const MyClassExpressionExtendsMyClass = class implements MyInterface { { line: 7, messageId: 'voidReturnSubtype', - data: { baseTypeName: 'MyInterface' }, + data: { heritageTypeName: 'MyInterface' }, }, ], }, @@ -2087,7 +2087,7 @@ class MyClassExtendsMyClassExpression extends MyClassExpression { { line: 9, messageId: 'voidReturnSubtype', - data: { baseTypeName: 'MyClassExpression' }, + data: { heritageTypeName: 'MyClassExpression' }, }, ], }, @@ -2109,7 +2109,7 @@ interface MyInterfaceExtendsMyClassExpression extends MyClassExpressionType { { line: 10, messageId: 'voidReturnSubtype', - data: { baseTypeName: 'typeof MyClassExpression' }, + data: { heritageTypeName: 'typeof MyClassExpression' }, }, ], }, From 32dd7f74ae3ffe645d61d374047362cceef0d51f Mon Sep 17 00:00:00 2001 From: Aly Thobani Date: Mon, 25 Mar 2024 09:38:20 -0700 Subject: [PATCH 11/26] Refactoring --- .../src/rules/no-misused-promises.ts | 66 ++++++++++++------- 1 file changed, 42 insertions(+), 24 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-misused-promises.ts b/packages/eslint-plugin/src/rules/no-misused-promises.ts index 49245496e030..85ee1f30a61b 100644 --- a/packages/eslint-plugin/src/rules/no-misused-promises.ts +++ b/packages/eslint-plugin/src/rules/no-misused-promises.ts @@ -397,31 +397,50 @@ export default createRule({ return; } - tsNode.members.forEach(nodeMember => { + for (const nodeMember of tsNode.members) { const memberName = nodeMember.name?.getText(); if (memberName === undefined) { - return; + continue; } if (!returnsThenable(checker, nodeMember)) { - return; + continue; } - heritageTypes.forEach(heritageType => { - const heritageMemberType = getTypeOfMemberOrPropertyIfExists( - checker, + for (const heritageType of heritageTypes) { + checkHeritageTypeForMemberReturningVoid( + nodeMember, heritageType, memberName, ); - if ( - heritageMemberType !== undefined && - isVoidReturningFunctionType(checker, nodeMember, heritageMemberType) - ) { - context.report({ - node: services.tsNodeToESTreeNodeMap.get(nodeMember), - messageId: 'voidReturnSubtype', - data: { heritageTypeName: checker.typeToString(heritageType) }, - }); - } - }); + } + } + } + + /** + * Checks `heritageType` for a member named `memberName` that returns void; reports the + * 'voidReturnSubtype' message if found. + * @param nodeMember Subtype member that returns a Promise + * @param heritageType Heritage type to check against + */ + function checkHeritageTypeForMemberReturningVoid( + nodeMember: ts.Node, + heritageType: ts.Type, + memberName: string, + ): void { + const heritageMember = getMemberIfExists(heritageType, memberName); + if (heritageMember === undefined) { + return; + } + const memberType = checker.getTypeOfSymbolAtLocation( + heritageMember, + nodeMember, + ); + if (!isVoidReturningFunctionType(checker, nodeMember, memberType)) { + return; + } + context.report({ + node: services.tsNodeToESTreeNodeMap.get(nodeMember), + messageId: 'voidReturnSubtype', + data: { heritageTypeName: checker.typeToString(heritageType) }, }); } @@ -738,16 +757,15 @@ function returnsThenable(checker: ts.TypeChecker, node: ts.Node): boolean { } /** - * @returns The type of the member or property with the given name in the given type, if it exists. + * @returns The member with the given name in `type`, if it exists. */ -function getTypeOfMemberOrPropertyIfExists( - checker: ts.TypeChecker, +function getMemberIfExists( type: ts.Type, memberName: string, -): ts.Type | undefined { +): ts.Symbol | undefined { const escapedMemberName = ts.escapeLeadingUnderscores(memberName); const symbolMemberMatch = type.getSymbol()?.members?.get(escapedMemberName); - const memberMatch = - symbolMemberMatch ?? tsutils.getPropertyOfType(type, escapedMemberName); - return memberMatch && checker.getTypeOfSymbol(memberMatch); + return ( + symbolMemberMatch ?? tsutils.getPropertyOfType(type, escapedMemberName) + ); } From 36b460cf03a3fe6ea9e0b36438b6792dc3d977eb Mon Sep 17 00:00:00 2001 From: Aly Thobani Date: Mon, 25 Mar 2024 10:10:45 -0700 Subject: [PATCH 12/26] Copy change again: subtypes => heritageTypes --- .../docs/rules/no-misused-promises.mdx | 2 +- .../src/rules/no-misused-promises.ts | 30 +++--- .../tests/rules/no-misused-promises.test.ts | 98 +++++++++---------- .../schema-snapshots/no-misused-promises.shot | 8 +- 4 files changed, 69 insertions(+), 69 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/no-misused-promises.mdx b/packages/eslint-plugin/docs/rules/no-misused-promises.mdx index c65655e86cb0..bd7e4ad37336 100644 --- a/packages/eslint-plugin/docs/rules/no-misused-promises.mdx +++ b/packages/eslint-plugin/docs/rules/no-misused-promises.mdx @@ -100,9 +100,9 @@ The following options are supported: - `arguments`: Disables checking an asynchronous function passed as argument where the parameter type expects a function that returns `void` - `attributes`: Disables checking an asynchronous function passed as a JSX attribute expected to be a function that returns `void` +- `heritageTypes`: Disables checking an asynchronous method in a type whose extended/implemented heritage type expects that method to return `void` - `properties`: Disables checking an asynchronous function passed as an object property expected to be a function that returns `void` - `returns`: Disables checking an asynchronous function returned in a function whose return type is a function that returns `void` -- `subtypes`: Disables checking an asynchronous method in a type whose extended/implemented heritage type expects that method to return `void` - `variables`: Disables checking an asynchronous function used as a variable whose return type is a function that returns `void` For example, if you don't mind that passing a `() => Promise` to a `() => void` parameter or JSX attribute can lead to a floating unhandled Promise: diff --git a/packages/eslint-plugin/src/rules/no-misused-promises.ts b/packages/eslint-plugin/src/rules/no-misused-promises.ts index 85ee1f30a61b..2d94525c66ad 100644 --- a/packages/eslint-plugin/src/rules/no-misused-promises.ts +++ b/packages/eslint-plugin/src/rules/no-misused-promises.ts @@ -20,9 +20,9 @@ type Options = [ interface ChecksVoidReturnOptions { arguments?: boolean; attributes?: boolean; + heritageTypes?: boolean; properties?: boolean; returns?: boolean; - subtypes?: boolean; variables?: boolean; } @@ -31,9 +31,9 @@ type MessageId = | 'spread' | 'voidReturnArgument' | 'voidReturnAttribute' + | 'voidReturnHeritageType' | 'voidReturnProperty' | 'voidReturnReturnValue' - | 'voidReturnSubtype' | 'voidReturnVariable'; function parseChecksVoidReturn( @@ -48,9 +48,9 @@ function parseChecksVoidReturn( return { arguments: true, attributes: true, + heritageTypes: true, properties: true, returns: true, - subtypes: true, variables: true, }; @@ -58,9 +58,9 @@ function parseChecksVoidReturn( return { arguments: checksVoidReturn.arguments ?? true, attributes: checksVoidReturn.attributes ?? true, + heritageTypes: checksVoidReturn.heritageTypes ?? true, properties: checksVoidReturn.properties ?? true, returns: checksVoidReturn.returns ?? true, - subtypes: checksVoidReturn.subtypes ?? true, variables: checksVoidReturn.variables ?? true, }; } @@ -79,12 +79,12 @@ export default createRule({ 'Promise returned in function argument where a void return was expected.', voidReturnAttribute: 'Promise-returning function provided to attribute where a void return was expected.', + voidReturnHeritageType: + "Promise-returning method provided where a void return was expected by heritage type '{{ heritageTypeName }}'.", voidReturnProperty: 'Promise-returning function provided to property where a void return was expected.', voidReturnReturnValue: 'Promise-returning function provided to return value where a void return was expected.', - voidReturnSubtype: - "Promise-returning method provided where a void return was expected by heritage type '{{ heritageTypeName }}'.", voidReturnVariable: 'Promise-returning function provided to variable where a void return was expected.', conditional: 'Expected non-Promise value in a boolean conditional.', @@ -106,9 +106,9 @@ export default createRule({ properties: { arguments: { type: 'boolean' }, attributes: { type: 'boolean' }, + heritageTypes: { type: 'boolean' }, properties: { type: 'boolean' }, returns: { type: 'boolean' }, - subtypes: { type: 'boolean' }, variables: { type: 'boolean' }, }, type: 'object', @@ -160,17 +160,17 @@ export default createRule({ ...(checksVoidReturn.attributes && { JSXAttribute: checkJSXAttribute, }), + ...(checksVoidReturn.heritageTypes && { + ClassDeclaration: checkClassLikeOrInterfaceNode, + ClassExpression: checkClassLikeOrInterfaceNode, + TSInterfaceDeclaration: checkClassLikeOrInterfaceNode, + }), ...(checksVoidReturn.properties && { Property: checkProperty, }), ...(checksVoidReturn.returns && { ReturnStatement: checkReturnStatement, }), - ...(checksVoidReturn.subtypes && { - ClassDeclaration: checkClassLikeOrInterfaceNode, - ClassExpression: checkClassLikeOrInterfaceNode, - TSInterfaceDeclaration: checkClassLikeOrInterfaceNode, - }), ...(checksVoidReturn.variables && { AssignmentExpression: checkAssignment, VariableDeclarator: checkVariableDeclaration, @@ -417,8 +417,8 @@ export default createRule({ /** * Checks `heritageType` for a member named `memberName` that returns void; reports the - * 'voidReturnSubtype' message if found. - * @param nodeMember Subtype member that returns a Promise + * 'voidReturnHeritageType' message if found. + * @param nodeMember Node member that returns a Promise * @param heritageType Heritage type to check against */ function checkHeritageTypeForMemberReturningVoid( @@ -439,7 +439,7 @@ export default createRule({ } context.report({ node: services.tsNodeToESTreeNodeMap.get(nodeMember), - messageId: 'voidReturnSubtype', + messageId: 'voidReturnHeritageType', data: { heritageTypeName: checker.typeToString(heritageType) }, }); } diff --git a/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts b/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts index 6d3ec6947081..48086099568a 100644 --- a/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts @@ -517,7 +517,7 @@ class MySubclassExtendsMyClass extends MyClass { } } `, - options: [{ checksVoidReturn: { subtypes: true } }], + options: [{ checksVoidReturn: { heritageTypes: true } }], }, { // Valid promise-returning class @@ -534,7 +534,7 @@ class MySubclassExtendsMyClass extends MyClass { } } `, - options: [{ checksVoidReturn: { subtypes: true } }], + options: [{ checksVoidReturn: { heritageTypes: true } }], }, { // Invalid class with rule off @@ -551,7 +551,7 @@ class MySubclassExtendsMyClass extends MyClass { } } `, - options: [{ checksVoidReturn: { subtypes: false } }], + options: [{ checksVoidReturn: { heritageTypes: false } }], }, { // Valid abstract class @@ -566,7 +566,7 @@ abstract class MyAbstractClassExtendsMyClass extends MyClass { abstract setThing(): void; } `, - options: [{ checksVoidReturn: { subtypes: true } }], + options: [{ checksVoidReturn: { heritageTypes: true } }], }, { // Invalid abstract class with rule off @@ -581,7 +581,7 @@ abstract class MyAbstractClassExtendsMyClass extends MyClass { abstract setThing(): Promise; } `, - options: [{ checksVoidReturn: { subtypes: false } }], + options: [{ checksVoidReturn: { heritageTypes: false } }], }, { // Valid interface @@ -596,7 +596,7 @@ interface MyInterfaceExtendsMyClass extends MyClass { setThing(): void; } `, - options: [{ checksVoidReturn: { subtypes: true } }], + options: [{ checksVoidReturn: { heritageTypes: true } }], }, { // Invalid interface with rule off @@ -611,7 +611,7 @@ interface MyInterfaceExtendsMyClass extends MyClass { setThing(): Promise; } `, - options: [{ checksVoidReturn: { subtypes: false } }], + options: [{ checksVoidReturn: { heritageTypes: false } }], }, // #endregion // #region checksVoidReturn.subtypes: Extending an abstract class @@ -628,7 +628,7 @@ class MySubclassExtendsMyAbstractClass extends MyAbstractClass { } } `, - options: [{ checksVoidReturn: { subtypes: true } }], + options: [{ checksVoidReturn: { heritageTypes: true } }], }, { // Invalid class with rule off @@ -643,7 +643,7 @@ class MySubclassExtendsMyAbstractClass extends MyAbstractClass { } } `, - options: [{ checksVoidReturn: { subtypes: false } }], + options: [{ checksVoidReturn: { heritageTypes: false } }], }, { // Valid abstract class @@ -656,7 +656,7 @@ abstract class MyAbstractSubclassExtendsMyAbstractClass extends MyAbstractClass abstract setThing(): void; } `, - options: [{ checksVoidReturn: { subtypes: true } }], + options: [{ checksVoidReturn: { heritageTypes: true } }], }, { // Invalid abstract class with rule off @@ -669,7 +669,7 @@ abstract class MyAbstractSubclassExtendsMyAbstractClass extends MyAbstractClass abstract setThing(): Promise; } `, - options: [{ checksVoidReturn: { subtypes: false } }], + options: [{ checksVoidReturn: { heritageTypes: false } }], }, { // Valid interface extending abstract class @@ -682,7 +682,7 @@ interface MyInterfaceExtendsMyAbstractClass extends MyAbstractClass { setThing(): void; } `, - options: [{ checksVoidReturn: { subtypes: true } }], + options: [{ checksVoidReturn: { heritageTypes: true } }], }, { // Invalid interface with rule off @@ -695,7 +695,7 @@ interface MyInterfaceExtendsMyAbstractClass extends MyAbstractClass { setThing(): Promise; } `, - options: [{ checksVoidReturn: { subtypes: false } }], + options: [{ checksVoidReturn: { heritageTypes: false } }], }, // #endregion // #region checksVoidReturn.subtypes: Extending an interface @@ -710,7 +710,7 @@ interface MySubInterfaceExtendsMyInterface extends MyInterface { setThing(): void; } `, - options: [{ checksVoidReturn: { subtypes: true } }], + options: [{ checksVoidReturn: { heritageTypes: true } }], }, { // Invalid interface with rule off @@ -723,7 +723,7 @@ interface MySubInterfaceExtendsMyInterface extends MyInterface { setThing(): Promise; } `, - options: [{ checksVoidReturn: { subtypes: false } }], + options: [{ checksVoidReturn: { heritageTypes: false } }], }, { // Valid class @@ -738,7 +738,7 @@ class MyClassImplementsMyInterface implements MyInterface { } } `, - options: [{ checksVoidReturn: { subtypes: true } }], + options: [{ checksVoidReturn: { heritageTypes: true } }], }, { // Invalid class with rule off @@ -753,7 +753,7 @@ class MyClassImplementsMyInterface implements MyInterface { } } `, - options: [{ checksVoidReturn: { subtypes: false } }], + options: [{ checksVoidReturn: { heritageTypes: false } }], }, { // Valid abstract class @@ -766,7 +766,7 @@ abstract class MyAbstractClassImplementsMyInterface implements MyInterface { abstract setThing(): void; } `, - options: [{ checksVoidReturn: { subtypes: true } }], + options: [{ checksVoidReturn: { heritageTypes: true } }], }, { // Invalid abstract class with rule off @@ -779,7 +779,7 @@ abstract class MyAbstractClassImplementsMyInterface implements MyInterface { abstract setThing(): Promise; } `, - options: [{ checksVoidReturn: { subtypes: false } }], + options: [{ checksVoidReturn: { heritageTypes: false } }], }, // #endregion // #region checksVoidReturn.subtypes: Extending type aliases @@ -795,7 +795,7 @@ class MyClass implements MyTypeLiteralsIntersection { } } `, - options: [{ checksVoidReturn: { subtypes: true } }], + options: [{ checksVoidReturn: { heritageTypes: true } }], }, { // Invalid class with rule off @@ -809,7 +809,7 @@ class MyClass implements MyTypeLiteralsIntersection { } } `, - options: [{ checksVoidReturn: { subtypes: false } }], + options: [{ checksVoidReturn: { heritageTypes: false } }], }, { // Valid interface extending generic type @@ -822,7 +822,7 @@ interface MyAsyncInterface extends MyGenericType { setThing(): Promise; } `, - options: [{ checksVoidReturn: { subtypes: true } }], + options: [{ checksVoidReturn: { heritageTypes: true } }], }, { // Invalid interface with rule off @@ -835,7 +835,7 @@ interface MyAsyncInterface extends MyGenericType { setThing(): Promise; } `, - options: [{ checksVoidReturn: { subtypes: false } }], + options: [{ checksVoidReturn: { heritageTypes: false } }], }, // #endregion // #region checksVoidReturn.subtypes: Multiple heritage types @@ -854,7 +854,7 @@ interface MyThirdInterface extends MyInterface, MyOtherInterface { setThing(): void; } `, - options: [{ checksVoidReturn: { subtypes: true } }], + options: [{ checksVoidReturn: { heritageTypes: true } }], }, { // Valid interface extending two classes @@ -875,7 +875,7 @@ interface MyInterface extends MyClass, MyOtherClass { setThing(): void; } `, - options: [{ checksVoidReturn: { subtypes: true } }], + options: [{ checksVoidReturn: { heritageTypes: true } }], }, { // Valid class extending a class and implementing two interfaces @@ -900,7 +900,7 @@ class MySubclass extends MyClass implements MyInterface, MyOtherInterface { } } `, - options: [{ checksVoidReturn: { subtypes: true } }], + options: [{ checksVoidReturn: { heritageTypes: true } }], }, // #endregion // #region checksVoidReturn.subtypes: Class expressions @@ -919,7 +919,7 @@ const MyClassExpressionExtendsMyClass = class extends MyClass { } }; `, - options: [{ checksVoidReturn: { subtypes: true } }], + options: [{ checksVoidReturn: { heritageTypes: true } }], }, { // Valid class extending a class expression @@ -936,7 +936,7 @@ class MyClassExtendsMyClassExpression extends MyClassExpression { } } `, - options: [{ checksVoidReturn: { subtypes: true } }], + options: [{ checksVoidReturn: { heritageTypes: true } }], }, { // Valid interface implementing a class expression @@ -952,7 +952,7 @@ interface MyInterfaceExtendsMyClassExpression extends MyClassExpressionType { setThing(): void; } `, - options: [{ checksVoidReturn: { subtypes: true } }], + options: [{ checksVoidReturn: { heritageTypes: true } }], }, // #endregion // #endregion @@ -1750,7 +1750,7 @@ class MySubclassExtendsMyClass extends MyClass { errors: [ { line: 9, - messageId: 'voidReturnSubtype', + messageId: 'voidReturnHeritageType', data: { heritageTypeName: 'MyClass' }, }, ], @@ -1770,7 +1770,7 @@ abstract class MyAbstractClassExtendsMyClass extends MyClass { errors: [ { line: 9, - messageId: 'voidReturnSubtype', + messageId: 'voidReturnHeritageType', data: { heritageTypeName: 'MyClass' }, }, ], @@ -1790,7 +1790,7 @@ interface MyInterfaceExtendsMyClass extends MyClass { errors: [ { line: 9, - messageId: 'voidReturnSubtype', + messageId: 'voidReturnHeritageType', data: { heritageTypeName: 'MyClass' }, }, ], @@ -1812,7 +1812,7 @@ class MySubclassExtendsMyAbstractClass extends MyAbstractClass { errors: [ { line: 7, - messageId: 'voidReturnSubtype', + messageId: 'voidReturnHeritageType', data: { heritageTypeName: 'MyAbstractClass' }, }, ], @@ -1830,7 +1830,7 @@ abstract class MyAbstractSubclassExtendsMyAbstractClass extends MyAbstractClass errors: [ { line: 7, - messageId: 'voidReturnSubtype', + messageId: 'voidReturnHeritageType', data: { heritageTypeName: 'MyAbstractClass' }, }, ], @@ -1848,7 +1848,7 @@ interface MyInterfaceExtendsMyAbstractClass extends MyAbstractClass { errors: [ { line: 7, - messageId: 'voidReturnSubtype', + messageId: 'voidReturnHeritageType', data: { heritageTypeName: 'MyAbstractClass' }, }, ], @@ -1870,7 +1870,7 @@ class MyInterfaceSubclass implements MyInterface { errors: [ { line: 7, - messageId: 'voidReturnSubtype', + messageId: 'voidReturnHeritageType', data: { heritageTypeName: 'MyInterface' }, }, ], @@ -1888,7 +1888,7 @@ abstract class MyAbstractClassImplementsMyInterface implements MyInterface { errors: [ { line: 7, - messageId: 'voidReturnSubtype', + messageId: 'voidReturnHeritageType', data: { heritageTypeName: 'MyInterface' }, }, ], @@ -1906,7 +1906,7 @@ interface MySubInterface extends MyInterface { errors: [ { line: 7, - messageId: 'voidReturnSubtype', + messageId: 'voidReturnHeritageType', data: { heritageTypeName: 'MyInterface' }, }, ], @@ -1927,7 +1927,7 @@ class MyClassImplementsMyTypeIntersection implements MyTypeIntersection { errors: [ { line: 6, - messageId: 'voidReturnSubtype', + messageId: 'voidReturnHeritageType', data: { heritageTypeName: 'MyTypeIntersection' }, }, ], @@ -1945,7 +1945,7 @@ interface MyAsyncInterface extends MyGenericType { errors: [ { line: 7, - messageId: 'voidReturnSubtype', + messageId: 'voidReturnHeritageType', data: { heritageTypeName: '{ setThing(): void; }' }, }, ], @@ -1969,12 +1969,12 @@ interface MyThirdInterface extends MyInterface, MyOtherInterface { errors: [ { line: 11, - messageId: 'voidReturnSubtype', + messageId: 'voidReturnHeritageType', data: { heritageTypeName: 'MyInterface' }, }, { line: 11, - messageId: 'voidReturnSubtype', + messageId: 'voidReturnHeritageType', data: { heritageTypeName: 'MyOtherInterface' }, }, ], @@ -2000,12 +2000,12 @@ interface MyInterface extends MyClass, MyOtherClass { errors: [ { line: 15, - messageId: 'voidReturnSubtype', + messageId: 'voidReturnHeritageType', data: { heritageTypeName: 'MyClass' }, }, { line: 15, - messageId: 'voidReturnSubtype', + messageId: 'voidReturnHeritageType', data: { heritageTypeName: 'MyOtherClass' }, }, ], @@ -2035,12 +2035,12 @@ class MySubclass extends MyClass implements MyAsyncInterface, MySyncInterface { errors: [ { line: 17, - messageId: 'voidReturnSubtype', + messageId: 'voidReturnHeritageType', data: { heritageTypeName: 'MyClass' }, }, { line: 17, - messageId: 'voidReturnSubtype', + messageId: 'voidReturnHeritageType', data: { heritageTypeName: 'MySyncInterface' }, }, ], @@ -2063,7 +2063,7 @@ const MyClassExpressionExtendsMyClass = class implements MyInterface { errors: [ { line: 7, - messageId: 'voidReturnSubtype', + messageId: 'voidReturnHeritageType', data: { heritageTypeName: 'MyInterface' }, }, ], @@ -2086,7 +2086,7 @@ class MyClassExtendsMyClassExpression extends MyClassExpression { errors: [ { line: 9, - messageId: 'voidReturnSubtype', + messageId: 'voidReturnHeritageType', data: { heritageTypeName: 'MyClassExpression' }, }, ], @@ -2108,7 +2108,7 @@ interface MyInterfaceExtendsMyClassExpression extends MyClassExpressionType { errors: [ { line: 10, - messageId: 'voidReturnSubtype', + messageId: 'voidReturnHeritageType', data: { heritageTypeName: 'typeof MyClassExpression' }, }, ], diff --git a/packages/eslint-plugin/tests/schema-snapshots/no-misused-promises.shot b/packages/eslint-plugin/tests/schema-snapshots/no-misused-promises.shot index b6bc5b453a33..cf2c8e723994 100644 --- a/packages/eslint-plugin/tests/schema-snapshots/no-misused-promises.shot +++ b/packages/eslint-plugin/tests/schema-snapshots/no-misused-promises.shot @@ -28,13 +28,13 @@ exports[`Rule schemas should be convertible to TS types for documentation purpos "attributes": { "type": "boolean" }, - "properties": { + "heritageTypes": { "type": "boolean" }, - "returns": { + "properties": { "type": "boolean" }, - "subtypes": { + "returns": { "type": "boolean" }, "variables": { @@ -61,9 +61,9 @@ type Options = [ | { arguments?: boolean; attributes?: boolean; + heritageTypes?: boolean; properties?: boolean; returns?: boolean; - subtypes?: boolean; variables?: boolean; } | boolean; From a7b4e1ffad820effd272caf15327b1304e439469 Mon Sep 17 00:00:00 2001 From: Aly Thobani Date: Mon, 25 Mar 2024 10:14:47 -0700 Subject: [PATCH 13/26] Fix location of heritageTypes examples in no-misused-promises mdx doc --- .../docs/rules/no-misused-promises.mdx | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/no-misused-promises.mdx b/packages/eslint-plugin/docs/rules/no-misused-promises.mdx index bd7e4ad37336..3b679123235a 100644 --- a/packages/eslint-plugin/docs/rules/no-misused-promises.mdx +++ b/packages/eslint-plugin/docs/rules/no-misused-promises.mdx @@ -142,6 +142,15 @@ eventEmitter.on('some-event', async () => { await doSomething(); otherSynchronousCall(); }); + +interface MyInterface { + setThing(): void; +} +class MyClass implements MyInterface { + async setThing(): Promise { + this.thing = await fetchThing(); + } +} ``` @@ -185,6 +194,15 @@ eventEmitter.on('some-event', () => { handler().catch(handleError); }); + +interface MyInterface { + setThing(): Promise; +} +class MyClass implements MyInterface { + async setThing(): Promise { + this.thing = await fetchThing(); + } +} ``` @@ -220,15 +238,6 @@ const getData2 = async () => { }; return { foo: 42, ...getData2() }; - -interface MyInterface { - setThing(): void; -} -class MyClass implements MyInterface { - async setThing(): Promise { - this.thing = await fetchThing(); - } -} ``` @@ -244,15 +253,6 @@ const getData2 = async () => { }; return { foo: 42, ...(await getData2()) }; - -interface MyInterface { - setThing(): Promise; -} -class MyClass implements MyInterface { - async setThing(): Promise { - this.thing = await fetchThing(); - } -} ``` From 3c9c9455e59dd64b630d089ecb063549ecfa2f6f Mon Sep 17 00:00:00 2001 From: Aly Thobani Date: Mon, 25 Mar 2024 10:50:39 -0700 Subject: [PATCH 14/26] Refactor out getHeritageTypes function --- .../src/rules/no-misused-promises.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-misused-promises.ts b/packages/eslint-plugin/src/rules/no-misused-promises.ts index 2d94525c66ad..1d5e6026646f 100644 --- a/packages/eslint-plugin/src/rules/no-misused-promises.ts +++ b/packages/eslint-plugin/src/rules/no-misused-promises.ts @@ -388,11 +388,8 @@ export default createRule({ | TSESTree.TSInterfaceDeclaration, ): void { const tsNode = services.esTreeNodeToTSNodeMap.get(node); - const heritageTypes = tsNode.heritageClauses - ?.map(clause => clause.types) - .flat() - .map(typeExpression => checker.getTypeAtLocation(typeExpression)); + const heritageTypes = getHeritageTypes(checker, tsNode); if (heritageTypes === undefined || heritageTypes.length === 0) { return; } @@ -756,6 +753,16 @@ function returnsThenable(checker: ts.TypeChecker, node: ts.Node): boolean { .some(t => anySignatureIsThenableType(checker, node, t)); } +function getHeritageTypes( + checker: ts.TypeChecker, + tsNode: ts.ClassDeclaration | ts.ClassExpression | ts.InterfaceDeclaration, +): ts.Type[] | undefined { + return tsNode.heritageClauses + ?.map(clause => clause.types) + .flat() + .map(typeExpression => checker.getTypeAtLocation(typeExpression)); +} + /** * @returns The member with the given name in `type`, if it exists. */ From c71dfcab460a43341e726784d35479bfbd5357ba Mon Sep 17 00:00:00 2001 From: Aly Thobani Date: Fri, 29 Mar 2024 11:38:00 -0700 Subject: [PATCH 15/26] Update no-misused-promises doc to specify that it checks named methods --- .../eslint-plugin/docs/rules/no-misused-promises.mdx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/no-misused-promises.mdx b/packages/eslint-plugin/docs/rules/no-misused-promises.mdx index 3b679123235a..b2c79b769ffc 100644 --- a/packages/eslint-plugin/docs/rules/no-misused-promises.mdx +++ b/packages/eslint-plugin/docs/rules/no-misused-promises.mdx @@ -100,7 +100,7 @@ The following options are supported: - `arguments`: Disables checking an asynchronous function passed as argument where the parameter type expects a function that returns `void` - `attributes`: Disables checking an asynchronous function passed as a JSX attribute expected to be a function that returns `void` -- `heritageTypes`: Disables checking an asynchronous method in a type whose extended/implemented heritage type expects that method to return `void` +- `heritageTypes`: Disables checking an asynchronous named method in a type whose extended/implemented heritage type expects that method to return `void` - `properties`: Disables checking an asynchronous function passed as an object property expected to be a function that returns `void` - `returns`: Disables checking an asynchronous function returned in a function whose return type is a function that returns `void` - `variables`: Disables checking an asynchronous function used as a variable whose return type is a function that returns `void` @@ -143,10 +143,10 @@ eventEmitter.on('some-event', async () => { otherSynchronousCall(); }); -interface MyInterface { +interface MySyncInterface { setThing(): void; } -class MyClass implements MyInterface { +class MyClass implements MySyncInterface { async setThing(): Promise { this.thing = await fetchThing(); } @@ -195,10 +195,10 @@ eventEmitter.on('some-event', () => { handler().catch(handleError); }); -interface MyInterface { +interface MyAsyncInterface { setThing(): Promise; } -class MyClass implements MyInterface { +class MyClass implements MyAsyncInterface { async setThing(): Promise { this.thing = await fetchThing(); } From 62cc0083358dff459a2953d9781d2040c97736c7 Mon Sep 17 00:00:00 2001 From: Aly Thobani Date: Fri, 29 Mar 2024 12:36:53 -0700 Subject: [PATCH 16/26] Add test cases for ignoring unnamed methods (signatures) and add explanatory comments to the code that ignores unnamed methods --- .../src/rules/no-misused-promises.ts | 5 + .../tests/rules/no-misused-promises.test.ts | 96 +++++++++++++++++++ 2 files changed, 101 insertions(+) diff --git a/packages/eslint-plugin/src/rules/no-misused-promises.ts b/packages/eslint-plugin/src/rules/no-misused-promises.ts index 1d5e6026646f..0b584f92e753 100644 --- a/packages/eslint-plugin/src/rules/no-misused-promises.ts +++ b/packages/eslint-plugin/src/rules/no-misused-promises.ts @@ -397,6 +397,10 @@ export default createRule({ for (const nodeMember of tsNode.members) { const memberName = nodeMember.name?.getText(); if (memberName === undefined) { + // Call/construct/index signatures don't have names. TS allows call signatures to mismatch, + // and construct signatures can't be async. + // TODO - Once we're able to use `checker.isTypeAssignableTo` (v8), we can check an index + // signature here against its compatible index signatures in `heritageTypes` continue; } if (!returnsThenable(checker, nodeMember)) { @@ -417,6 +421,7 @@ export default createRule({ * 'voidReturnHeritageType' message if found. * @param nodeMember Node member that returns a Promise * @param heritageType Heritage type to check against + * @param memberName Name of the member to check for */ function checkHeritageTypeForMemberReturningVoid( nodeMember: ts.Node, diff --git a/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts b/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts index 48086099568a..73002dc7f42b 100644 --- a/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts @@ -950,6 +950,72 @@ type MyClassExpressionType = typeof MyClassExpression; interface MyInterfaceExtendsMyClassExpression extends MyClassExpressionType { setThing(): void; +} + `, + options: [{ checksVoidReturn: { heritageTypes: true } }], + }, + // #endregion + // #region checksVoidReturn.subtypes: Unnamed methods (call/construct/index signatures) + { + // Call signatures: TS allows overloads with different return types, so we ignore these + code: ` +interface MySyncCallSignatures { + (): void; + (arg: string): void; +} +interface MyAsyncInterface extends MySyncCallSignatures { + (): Promise; + (arg: string): Promise; +} + `, + options: [{ checksVoidReturn: { heritageTypes: true } }], + }, + { + // Construct signatures: These can't be async in the first place + code: ` +interface MySyncConstructSignatures { + new (): void; + new (arg: string): void; +} +interface ThisIsADifferentIssue extends MySyncConstructSignatures { + new (): Promise; + new (arg: string): Promise; +} + `, + options: [{ checksVoidReturn: { heritageTypes: true } }], + }, + { + // Index signatures: For now not handling until we can use checker.isTypeAssignableTo (v8) + // https://github.com/typescript-eslint/typescript-eslint/pull/8765 + // https://github.com/typescript-eslint/typescript-eslint/discussions/7936 + code: ` +interface MySyncIndexSignatures { + [key: string]: void; + [key: number]: void; +} +interface ThisIsADifferentIssue extends MySyncIndexSignatures { + [key: string]: Promise; + [key: number]: Promise; +} + `, + options: [{ checksVoidReturn: { heritageTypes: true } }], + }, + { + // Mixed signatures: ignoring call/construct, not handling index (yet) + code: ` +interface MySyncInterfaceSignatures { + (): void; + (arg: string): void; + new (): void; + [key: string]: () => void; + [key: number]: () => void; +} +interface MyAsyncInterface extends MySyncInterfaceSignatures { + (): Promise; + (arg: string): Promise; + new (): Promise; + [key: string]: () => Promise; + [key: number]: () => Promise; } `, options: [{ checksVoidReturn: { heritageTypes: true } }], @@ -2114,6 +2180,36 @@ interface MyInterfaceExtendsMyClassExpression extends MyClassExpressionType { ], }, // #endregion + // #region checksVoidReturn.subtypes: Unnamed methods (call/construct/index signatures) + { + // Mixed signatures: not handling index signatures (yet); only the named method should error + code: ` +interface MySyncInterface { + (): void; + (arg: string): void; + new (): void; + [key: string]: () => void; + [key: number]: () => void; + myMethod(): void; +} +interface MyAsyncInterface extends MySyncInterface { + (): Promise; + (arg: string): Promise; + new (): Promise; + [key: string]: () => Promise; + [key: number]: () => Promise; + myMethod(): Promise; +} + `, + errors: [ + { + line: 16, + messageId: 'voidReturnHeritageType', + data: { heritageTypeName: 'MySyncInterface' }, + }, + ], + }, + // #endregion // #endregion ], }); From 330b8d4cbeb825a489960327a512eea3773edc3d Mon Sep 17 00:00:00 2001 From: Aly Thobani Date: Fri, 29 Mar 2024 14:08:35 -0700 Subject: [PATCH 17/26] Add combination test cases which add coverage for when the member is not found in the heritage type --- .../tests/rules/no-misused-promises.test.ts | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts b/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts index 73002dc7f42b..4a46916ac8c5 100644 --- a/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts @@ -1016,6 +1016,45 @@ interface MyAsyncInterface extends MySyncInterfaceSignatures { new (): Promise; [key: string]: () => Promise; [key: number]: () => Promise; +} + `, + options: [{ checksVoidReturn: { heritageTypes: true } }], + }, + // #endregion + // #region checksVoidReturn.subtypes: Combinations + { + // Valid combination + code: ` +interface MyCall { + (): void; + (arg: string): void; +} + +interface MyIndex { + [key: string]: () => void; + [key: number]: () => void; +} + +interface MyConstruct { + new (): void; + new (arg: string): void; +} + +interface MyMethods { + doSyncThing(): void; + doOtherSyncThing(): void; + syncMethodProperty: () => void; +} +interface MyInterface extends MyCall, MyIndex, MyConstruct, MyMethods { + (): void; + (arg: string): void; + new (): void; + new (arg: string): void; + [key: string]: () => void; + [key: number]: () => void; + doSyncThing(): void; + doAsyncThing(): Promise; + syncMethodProperty: () => void; } `, options: [{ checksVoidReturn: { heritageTypes: true } }], @@ -2210,6 +2249,55 @@ interface MyAsyncInterface extends MySyncInterface { ], }, // #endregion + // #region checksVoidReturn.subtypes: Combinations + { + code: ` +interface MyCall { + (): void; + (arg: string): void; +} + +interface MyIndex { + [key: string]: () => void; + [key: number]: () => void; +} + +interface MyConstruct { + new (): void; + new (arg: string): void; +} + +interface MyMethods { + doSyncThing(): void; + doOtherSyncThing(): void; + syncMethodProperty: () => void; +} +interface MyInterface extends MyCall, MyIndex, MyConstruct, MyMethods { + (): void; + (arg: string): Promise; + new (): void; + new (arg: string): void; + [key: string]: () => Promise; + [key: number]: () => void; + doSyncThing(): Promise; + doAsyncThing(): Promise; + syncMethodProperty: () => Promise; +} + `, + errors: [ + { + line: 29, + messageId: 'voidReturnHeritageType', + data: { heritageTypeName: 'MyMethods' }, + }, + { + line: 31, + messageId: 'voidReturnHeritageType', + data: { heritageTypeName: 'MyMethods' }, + }, + ], + }, + // #endregion // #endregion ], }); From ce240c79faf902d40d4190e4d7283c31cf6e69d6 Mon Sep 17 00:00:00 2001 From: Aly Thobani Date: Fri, 29 Mar 2024 14:14:15 -0700 Subject: [PATCH 18/26] Rename subtypes => heritageTypes in region comments --- .../tests/rules/no-misused-promises.test.ts | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts b/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts index 4a46916ac8c5..98c56f4eb5ce 100644 --- a/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts @@ -500,8 +500,8 @@ foo(bar); }, options: [{ checksVoidReturn: { attributes: true } }], }, - // #region checksVoidReturn.subtypes - // #region checksVoidReturn.subtypes: Extending a class + // #region checksVoidReturn.heritageTypes + // #region checksVoidReturn.heritageTypes: Extending a class { // Valid void-returning class code: ` @@ -614,7 +614,7 @@ interface MyInterfaceExtendsMyClass extends MyClass { options: [{ checksVoidReturn: { heritageTypes: false } }], }, // #endregion - // #region checksVoidReturn.subtypes: Extending an abstract class + // #region checksVoidReturn.heritageTypes: Extending an abstract class { // Valid class code: ` @@ -698,7 +698,7 @@ interface MyInterfaceExtendsMyAbstractClass extends MyAbstractClass { options: [{ checksVoidReturn: { heritageTypes: false } }], }, // #endregion - // #region checksVoidReturn.subtypes: Extending an interface + // #region checksVoidReturn.heritageTypes: Extending an interface { // Valid interface code: ` @@ -782,7 +782,7 @@ abstract class MyAbstractClassImplementsMyInterface implements MyInterface { options: [{ checksVoidReturn: { heritageTypes: false } }], }, // #endregion - // #region checksVoidReturn.subtypes: Extending type aliases + // #region checksVoidReturn.heritageTypes: Extending type aliases { // Valid class extending type literals intersection code: ` @@ -838,7 +838,7 @@ interface MyAsyncInterface extends MyGenericType { options: [{ checksVoidReturn: { heritageTypes: false } }], }, // #endregion - // #region checksVoidReturn.subtypes: Multiple heritage types + // #region checksVoidReturn.heritageTypes: Multiple heritage types { // Valid interface extending two interfaces code: ` @@ -903,7 +903,7 @@ class MySubclass extends MyClass implements MyInterface, MyOtherInterface { options: [{ checksVoidReturn: { heritageTypes: true } }], }, // #endregion - // #region checksVoidReturn.subtypes: Class expressions + // #region checksVoidReturn.heritageTypes: Class expressions { // Valid class expression extending a class code: ` @@ -955,7 +955,7 @@ interface MyInterfaceExtendsMyClassExpression extends MyClassExpressionType { options: [{ checksVoidReturn: { heritageTypes: true } }], }, // #endregion - // #region checksVoidReturn.subtypes: Unnamed methods (call/construct/index signatures) + // #region checksVoidReturn.heritageTypes: Unnamed methods (call/construct/index signatures) { // Call signatures: TS allows overloads with different return types, so we ignore these code: ` @@ -1021,7 +1021,7 @@ interface MyAsyncInterface extends MySyncInterfaceSignatures { options: [{ checksVoidReturn: { heritageTypes: true } }], }, // #endregion - // #region checksVoidReturn.subtypes: Combinations + // #region checksVoidReturn.heritageTypes: Combinations { // Valid combination code: ` @@ -1836,8 +1836,8 @@ consume(...cbs); `, errors: [{ line: 4, messageId: 'voidReturnArgument' }], }, - // #region checksVoidReturn.subtypes - // #region checksVoidReturn.subtypes: Extending a class + // #region checksVoidReturn.heritageTypes + // #region checksVoidReturn.heritageTypes: Extending a class { code: ` class MyClass { @@ -1901,7 +1901,7 @@ interface MyInterfaceExtendsMyClass extends MyClass { ], }, // #endregion - // #region checksVoidReturn.subtypes: Extending an abstract class + // #region checksVoidReturn.heritageTypes: Extending an abstract class { code: ` abstract class MyAbstractClass { @@ -1959,7 +1959,7 @@ interface MyInterfaceExtendsMyAbstractClass extends MyAbstractClass { ], }, // #endregion - // #region checksVoidReturn.subtypes: Extending an interface + // #region checksVoidReturn.heritageTypes: Extending an interface { code: ` interface MyInterface { @@ -2017,7 +2017,7 @@ interface MySubInterface extends MyInterface { ], }, // #endregion - // #region checksVoidReturn.subtypes: Extending type aliases + // #region checksVoidReturn.heritageTypes: Extending type aliases { code: ` type MyTypeIntersection = { setThing(): void } & { thing: number }; @@ -2056,7 +2056,7 @@ interface MyAsyncInterface extends MyGenericType { ], }, // #endregion - // #region checksVoidReturn.subtypes: Multiple heritage types + // #region checksVoidReturn.heritageTypes: Multiple heritage types { code: ` interface MyInterface { @@ -2151,7 +2151,7 @@ class MySubclass extends MyClass implements MyAsyncInterface, MySyncInterface { ], }, // #endregion - // #region checksVoidReturn.subtypes: Class expressions + // #region checksVoidReturn.heritageTypes: Class expressions { // Invalid class expression implementing an interface code: ` @@ -2219,7 +2219,7 @@ interface MyInterfaceExtendsMyClassExpression extends MyClassExpressionType { ], }, // #endregion - // #region checksVoidReturn.subtypes: Unnamed methods (call/construct/index signatures) + // #region checksVoidReturn.heritageTypes: Unnamed methods (call/construct/index signatures) { // Mixed signatures: not handling index signatures (yet); only the named method should error code: ` @@ -2249,7 +2249,7 @@ interface MyAsyncInterface extends MySyncInterface { ], }, // #endregion - // #region checksVoidReturn.subtypes: Combinations + // #region checksVoidReturn.heritageTypes: Combinations { code: ` interface MyCall { From ab6174ce2ad453c6830011c8e4930727f76c0069 Mon Sep 17 00:00:00 2001 From: Aly Thobani Date: Tue, 23 Apr 2024 15:22:09 -0700 Subject: [PATCH 19/26] Adjust no-misused-promises doc to be more explicit about heritageTypes ignoring signatures / only checking named methods --- packages/eslint-plugin/docs/rules/no-misused-promises.mdx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/eslint-plugin/docs/rules/no-misused-promises.mdx b/packages/eslint-plugin/docs/rules/no-misused-promises.mdx index b2c79b769ffc..a22abc53b078 100644 --- a/packages/eslint-plugin/docs/rules/no-misused-promises.mdx +++ b/packages/eslint-plugin/docs/rules/no-misused-promises.mdx @@ -100,7 +100,8 @@ The following options are supported: - `arguments`: Disables checking an asynchronous function passed as argument where the parameter type expects a function that returns `void` - `attributes`: Disables checking an asynchronous function passed as a JSX attribute expected to be a function that returns `void` -- `heritageTypes`: Disables checking an asynchronous named method in a type whose extended/implemented heritage type expects that method to return `void` +- `heritageTypes`: Disables checking an asynchronous method in a type whose extended/implemented heritage type expects that method to return `void` + - **Limitation:** For now, this option only checks named methods: that is, call/construct/index signatures are ignored. Call signatures are not required in TypeScript to be consistent with one another, and construct signatures cannot be `async` in the first place. Index signature checking may be implemented in the future. - `properties`: Disables checking an asynchronous function passed as an object property expected to be a function that returns `void` - `returns`: Disables checking an asynchronous function returned in a function whose return type is a function that returns `void` - `variables`: Disables checking an asynchronous function used as a variable whose return type is a function that returns `void` From 93ce98334f7d4c85ba4d8b56899af227db3e958b Mon Sep 17 00:00:00 2001 From: Aly Thobani Date: Tue, 23 Apr 2024 15:27:12 -0700 Subject: [PATCH 20/26] Remove `#region`s from no-misused-promises test file --- .../tests/rules/no-misused-promises.test.ts | 36 ------------------- 1 file changed, 36 deletions(-) diff --git a/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts b/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts index 98c56f4eb5ce..c810f111b733 100644 --- a/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts @@ -500,8 +500,6 @@ foo(bar); }, options: [{ checksVoidReturn: { attributes: true } }], }, - // #region checksVoidReturn.heritageTypes - // #region checksVoidReturn.heritageTypes: Extending a class { // Valid void-returning class code: ` @@ -613,8 +611,6 @@ interface MyInterfaceExtendsMyClass extends MyClass { `, options: [{ checksVoidReturn: { heritageTypes: false } }], }, - // #endregion - // #region checksVoidReturn.heritageTypes: Extending an abstract class { // Valid class code: ` @@ -697,8 +693,6 @@ interface MyInterfaceExtendsMyAbstractClass extends MyAbstractClass { `, options: [{ checksVoidReturn: { heritageTypes: false } }], }, - // #endregion - // #region checksVoidReturn.heritageTypes: Extending an interface { // Valid interface code: ` @@ -781,8 +775,6 @@ abstract class MyAbstractClassImplementsMyInterface implements MyInterface { `, options: [{ checksVoidReturn: { heritageTypes: false } }], }, - // #endregion - // #region checksVoidReturn.heritageTypes: Extending type aliases { // Valid class extending type literals intersection code: ` @@ -837,8 +829,6 @@ interface MyAsyncInterface extends MyGenericType { `, options: [{ checksVoidReturn: { heritageTypes: false } }], }, - // #endregion - // #region checksVoidReturn.heritageTypes: Multiple heritage types { // Valid interface extending two interfaces code: ` @@ -902,8 +892,6 @@ class MySubclass extends MyClass implements MyInterface, MyOtherInterface { `, options: [{ checksVoidReturn: { heritageTypes: true } }], }, - // #endregion - // #region checksVoidReturn.heritageTypes: Class expressions { // Valid class expression extending a class code: ` @@ -954,8 +942,6 @@ interface MyInterfaceExtendsMyClassExpression extends MyClassExpressionType { `, options: [{ checksVoidReturn: { heritageTypes: true } }], }, - // #endregion - // #region checksVoidReturn.heritageTypes: Unnamed methods (call/construct/index signatures) { // Call signatures: TS allows overloads with different return types, so we ignore these code: ` @@ -1020,8 +1006,6 @@ interface MyAsyncInterface extends MySyncInterfaceSignatures { `, options: [{ checksVoidReturn: { heritageTypes: true } }], }, - // #endregion - // #region checksVoidReturn.heritageTypes: Combinations { // Valid combination code: ` @@ -1059,8 +1043,6 @@ interface MyInterface extends MyCall, MyIndex, MyConstruct, MyMethods { `, options: [{ checksVoidReturn: { heritageTypes: true } }], }, - // #endregion - // #endregion ], invalid: [ @@ -1836,8 +1818,6 @@ consume(...cbs); `, errors: [{ line: 4, messageId: 'voidReturnArgument' }], }, - // #region checksVoidReturn.heritageTypes - // #region checksVoidReturn.heritageTypes: Extending a class { code: ` class MyClass { @@ -1900,8 +1880,6 @@ interface MyInterfaceExtendsMyClass extends MyClass { }, ], }, - // #endregion - // #region checksVoidReturn.heritageTypes: Extending an abstract class { code: ` abstract class MyAbstractClass { @@ -1958,8 +1936,6 @@ interface MyInterfaceExtendsMyAbstractClass extends MyAbstractClass { }, ], }, - // #endregion - // #region checksVoidReturn.heritageTypes: Extending an interface { code: ` interface MyInterface { @@ -2016,8 +1992,6 @@ interface MySubInterface extends MyInterface { }, ], }, - // #endregion - // #region checksVoidReturn.heritageTypes: Extending type aliases { code: ` type MyTypeIntersection = { setThing(): void } & { thing: number }; @@ -2055,8 +2029,6 @@ interface MyAsyncInterface extends MyGenericType { }, ], }, - // #endregion - // #region checksVoidReturn.heritageTypes: Multiple heritage types { code: ` interface MyInterface { @@ -2150,8 +2122,6 @@ class MySubclass extends MyClass implements MyAsyncInterface, MySyncInterface { }, ], }, - // #endregion - // #region checksVoidReturn.heritageTypes: Class expressions { // Invalid class expression implementing an interface code: ` @@ -2218,8 +2188,6 @@ interface MyInterfaceExtendsMyClassExpression extends MyClassExpressionType { }, ], }, - // #endregion - // #region checksVoidReturn.heritageTypes: Unnamed methods (call/construct/index signatures) { // Mixed signatures: not handling index signatures (yet); only the named method should error code: ` @@ -2248,8 +2216,6 @@ interface MyAsyncInterface extends MySyncInterface { }, ], }, - // #endregion - // #region checksVoidReturn.heritageTypes: Combinations { code: ` interface MyCall { @@ -2297,7 +2263,5 @@ interface MyInterface extends MyCall, MyIndex, MyConstruct, MyMethods { }, ], }, - // #endregion - // #endregion ], }); From 74d421d40874c98001e935517b83711f5404106e Mon Sep 17 00:00:00 2001 From: Aly Thobani Date: Tue, 23 Apr 2024 17:46:46 -0700 Subject: [PATCH 21/26] Update (use jest to generate) no-misused-promises rules snapshot --- .../no-misused-promises.shot | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-misused-promises.shot b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-misused-promises.shot index 3c1170e888c0..ff2540794e7f 100644 --- a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-misused-promises.shot +++ b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-misused-promises.shot @@ -66,6 +66,18 @@ eventEmitter.on('some-event', async () => { await doSomething(); otherSynchronousCall(); }); + +interface MySyncInterface { + setThing(): void; +} +class MyClass implements MySyncInterface { + async setThing(): Promise { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Promise-returning method provided where a void return was expected by heritage type 'MySyncInterface'. + this.thing = await fetchThing(); +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + } +~~~ +} " `; @@ -110,6 +122,15 @@ eventEmitter.on('some-event', () => { handler().catch(handleError); }); + +interface MyAsyncInterface { + setThing(): Promise; +} +class MyClass implements MyAsyncInterface { + async setThing(): Promise { + this.thing = await fetchThing(); + } +} " `; From d6cfd24ff4a3498fb1fd8bb2199ab8bf3758494b Mon Sep 17 00:00:00 2001 From: Aly Thobani Date: Thu, 6 Jun 2024 16:06:02 -0700 Subject: [PATCH 22/26] refactor: map(...).flat() => flatMap(...) --- packages/eslint-plugin/src/rules/no-misused-promises.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-misused-promises.ts b/packages/eslint-plugin/src/rules/no-misused-promises.ts index 0b584f92e753..209e1be7b5e7 100644 --- a/packages/eslint-plugin/src/rules/no-misused-promises.ts +++ b/packages/eslint-plugin/src/rules/no-misused-promises.ts @@ -763,8 +763,7 @@ function getHeritageTypes( tsNode: ts.ClassDeclaration | ts.ClassExpression | ts.InterfaceDeclaration, ): ts.Type[] | undefined { return tsNode.heritageClauses - ?.map(clause => clause.types) - .flat() + ?.flatMap(clause => clause.types) .map(typeExpression => checker.getTypeAtLocation(typeExpression)); } From eca836f59a8cd983425248bcba41124d45591d80 Mon Sep 17 00:00:00 2001 From: Aly Thobani Date: Thu, 6 Jun 2024 17:25:13 -0700 Subject: [PATCH 23/26] docs: restructure no-misused-promises doc - checksVoidReturn sub-options each get their own subsection - option descriptions go under Options subsection instead of Examples --- .../docs/rules/no-misused-promises.mdx | 135 +++++++++++------- 1 file changed, 80 insertions(+), 55 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/no-misused-promises.mdx b/packages/eslint-plugin/docs/rules/no-misused-promises.mdx index a22abc53b078..3e47dded11b9 100644 --- a/packages/eslint-plugin/docs/rules/no-misused-promises.mdx +++ b/packages/eslint-plugin/docs/rules/no-misused-promises.mdx @@ -37,10 +37,89 @@ If you don't want to check conditionals, you can configure the rule with `"check Doing so prevents the rule from looking at code like `if (somePromise)`. -Examples of code for this rule with `checksConditionals: true`: +### `checksVoidReturn` + +Likewise, if you don't want to check functions that return promises where a void return is +expected, your configuration will look like this: + +```json +{ + "@typescript-eslint/no-misused-promises": [ + "error", + { + "checksVoidReturn": false + } + ] +} +``` + +You can disable selective parts of the `checksVoidReturn` option by providing an object that disables specific checks. For example, if you don't mind that passing a `() => Promise` to a `() => void` parameter or JSX attribute can lead to a floating unhandled Promise: + +```json +{ + "@typescript-eslint/no-misused-promises": [ + "error", + { + "checksVoidReturn": { + "arguments": false, + "attributes": false + } + } + ] +} +``` + +The following sub-options are supported: + +#### `arguments` + +Disables checking an asynchronous function passed as argument where the parameter type expects a function that returns `void`. + +#### `attributes` + +Disables checking an asynchronous function passed as a JSX attribute expected to be a function that returns `void`. + +#### `heritageTypes` + +Disables checking an asynchronous method in a type whose extended/implemented heritage type expects that method to return `void`. + +:::note +For now, `no-misused-promises` only checks named methods against heritage types: that is, call/construct/index signatures are ignored. Call signatures are not required in TypeScript to be consistent with one another, and construct signatures cannot be `async` in the first place. Index signature checking may be implemented in the future. +::: + +#### `properties` + +Disables checking an asynchronous function passed as an object property expected to be a function that returns `void`. + +#### `returns` + +Disables checking an asynchronous function returned in a function whose return type is a function that returns `void`. + +#### `variables` + +Disables checking an asynchronous function used as a variable whose return type is a function that returns `void`. + +### `checksSpreads` + +If you don't want to check object spreads, you can add this configuration: + +```json +{ + "@typescript-eslint/no-misused-promises": [ + "error", + { + "checksSpreads": false + } + ] +} +``` ## Examples +### `checksConditionals` + +Examples of code for this rule with `checksConditionals: true`: + @@ -81,47 +160,6 @@ while (await promise) { ### `checksVoidReturn` -Likewise, if you don't want to check functions that return promises where a void return is -expected, your configuration will look like this: - -```json -{ - "@typescript-eslint/no-misused-promises": [ - "error", - { - "checksVoidReturn": false - } - ] -} -``` - -You can disable selective parts of the `checksVoidReturn` option by providing an object that disables specific checks. -The following options are supported: - -- `arguments`: Disables checking an asynchronous function passed as argument where the parameter type expects a function that returns `void` -- `attributes`: Disables checking an asynchronous function passed as a JSX attribute expected to be a function that returns `void` -- `heritageTypes`: Disables checking an asynchronous method in a type whose extended/implemented heritage type expects that method to return `void` - - **Limitation:** For now, this option only checks named methods: that is, call/construct/index signatures are ignored. Call signatures are not required in TypeScript to be consistent with one another, and construct signatures cannot be `async` in the first place. Index signature checking may be implemented in the future. -- `properties`: Disables checking an asynchronous function passed as an object property expected to be a function that returns `void` -- `returns`: Disables checking an asynchronous function returned in a function whose return type is a function that returns `void` -- `variables`: Disables checking an asynchronous function used as a variable whose return type is a function that returns `void` - -For example, if you don't mind that passing a `() => Promise` to a `() => void` parameter or JSX attribute can lead to a floating unhandled Promise: - -```json -{ - "@typescript-eslint/no-misused-promises": [ - "error", - { - "checksVoidReturn": { - "arguments": false, - "attributes": false - } - } - ] -} -``` - Examples of code for this rule with `checksVoidReturn: true`: @@ -211,19 +249,6 @@ class MyClass implements MyAsyncInterface { ### `checksSpreads` -If you don't want to check object spreads, you can add this configuration: - -```json -{ - "@typescript-eslint/no-misused-promises": [ - "error", - { - "checksSpreads": false - } - ] -} -``` - Examples of code for this rule with `checksSpreads: true`: From fbcc229817f9ea2529c07b60ebdac37e96a8dcc8 Mon Sep 17 00:00:00 2001 From: Aly Thobani Date: Thu, 6 Jun 2024 17:32:11 -0700 Subject: [PATCH 24/26] chore: remove my unit-test-labeling comments --- .../tests/rules/no-misused-promises.test.ts | 40 ------------------- 1 file changed, 40 deletions(-) diff --git a/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts b/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts index c810f111b733..556d1ee38114 100644 --- a/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts @@ -501,7 +501,6 @@ foo(bar); options: [{ checksVoidReturn: { attributes: true } }], }, { - // Valid void-returning class code: ` class MyClass { setThing(): void { @@ -518,7 +517,6 @@ class MySubclassExtendsMyClass extends MyClass { options: [{ checksVoidReturn: { heritageTypes: true } }], }, { - // Valid promise-returning class code: ` class MyClass { async setThing(): Promise { @@ -535,7 +533,6 @@ class MySubclassExtendsMyClass extends MyClass { options: [{ checksVoidReturn: { heritageTypes: true } }], }, { - // Invalid class with rule off code: ` class MyClass { setThing(): void { @@ -552,7 +549,6 @@ class MySubclassExtendsMyClass extends MyClass { options: [{ checksVoidReturn: { heritageTypes: false } }], }, { - // Valid abstract class code: ` class MyClass { setThing(): void { @@ -567,7 +563,6 @@ abstract class MyAbstractClassExtendsMyClass extends MyClass { options: [{ checksVoidReturn: { heritageTypes: true } }], }, { - // Invalid abstract class with rule off code: ` class MyClass { setThing(): void { @@ -582,7 +577,6 @@ abstract class MyAbstractClassExtendsMyClass extends MyClass { options: [{ checksVoidReturn: { heritageTypes: false } }], }, { - // Valid interface code: ` class MyClass { setThing(): void { @@ -597,7 +591,6 @@ interface MyInterfaceExtendsMyClass extends MyClass { options: [{ checksVoidReturn: { heritageTypes: true } }], }, { - // Invalid interface with rule off code: ` class MyClass { setThing(): void { @@ -612,7 +605,6 @@ interface MyInterfaceExtendsMyClass extends MyClass { options: [{ checksVoidReturn: { heritageTypes: false } }], }, { - // Valid class code: ` abstract class MyAbstractClass { abstract setThing(): void; @@ -627,7 +619,6 @@ class MySubclassExtendsMyAbstractClass extends MyAbstractClass { options: [{ checksVoidReturn: { heritageTypes: true } }], }, { - // Invalid class with rule off code: ` abstract class MyAbstractClass { abstract setThing(): void; @@ -642,7 +633,6 @@ class MySubclassExtendsMyAbstractClass extends MyAbstractClass { options: [{ checksVoidReturn: { heritageTypes: false } }], }, { - // Valid abstract class code: ` abstract class MyAbstractClass { abstract setThing(): void; @@ -655,7 +645,6 @@ abstract class MyAbstractSubclassExtendsMyAbstractClass extends MyAbstractClass options: [{ checksVoidReturn: { heritageTypes: true } }], }, { - // Invalid abstract class with rule off code: ` abstract class MyAbstractClass { abstract setThing(): void; @@ -668,7 +657,6 @@ abstract class MyAbstractSubclassExtendsMyAbstractClass extends MyAbstractClass options: [{ checksVoidReturn: { heritageTypes: false } }], }, { - // Valid interface extending abstract class code: ` abstract class MyAbstractClass { abstract setThing(): void; @@ -681,7 +669,6 @@ interface MyInterfaceExtendsMyAbstractClass extends MyAbstractClass { options: [{ checksVoidReturn: { heritageTypes: true } }], }, { - // Invalid interface with rule off code: ` abstract class MyAbstractClass { abstract setThing(): void; @@ -694,7 +681,6 @@ interface MyInterfaceExtendsMyAbstractClass extends MyAbstractClass { options: [{ checksVoidReturn: { heritageTypes: false } }], }, { - // Valid interface code: ` interface MyInterface { setThing(): void; @@ -707,7 +693,6 @@ interface MySubInterfaceExtendsMyInterface extends MyInterface { options: [{ checksVoidReturn: { heritageTypes: true } }], }, { - // Invalid interface with rule off code: ` interface MyInterface { setThing(): void; @@ -720,7 +705,6 @@ interface MySubInterfaceExtendsMyInterface extends MyInterface { options: [{ checksVoidReturn: { heritageTypes: false } }], }, { - // Valid class code: ` interface MyInterface { setThing(): void; @@ -735,7 +719,6 @@ class MyClassImplementsMyInterface implements MyInterface { options: [{ checksVoidReturn: { heritageTypes: true } }], }, { - // Invalid class with rule off code: ` interface MyInterface { setThing(): void; @@ -750,7 +733,6 @@ class MyClassImplementsMyInterface implements MyInterface { options: [{ checksVoidReturn: { heritageTypes: false } }], }, { - // Valid abstract class code: ` interface MyInterface { setThing(): void; @@ -763,7 +745,6 @@ abstract class MyAbstractClassImplementsMyInterface implements MyInterface { options: [{ checksVoidReturn: { heritageTypes: true } }], }, { - // Invalid abstract class with rule off code: ` interface MyInterface { setThing(): void; @@ -776,7 +757,6 @@ abstract class MyAbstractClassImplementsMyInterface implements MyInterface { options: [{ checksVoidReturn: { heritageTypes: false } }], }, { - // Valid class extending type literals intersection code: ` type MyTypeLiteralsIntersection = { setThing(): void } & { thing: number }; @@ -790,7 +770,6 @@ class MyClass implements MyTypeLiteralsIntersection { options: [{ checksVoidReturn: { heritageTypes: true } }], }, { - // Invalid class with rule off code: ` type MyTypeLiteralsIntersection = { setThing(): void } & { thing: number }; @@ -804,7 +783,6 @@ class MyClass implements MyTypeLiteralsIntersection { options: [{ checksVoidReturn: { heritageTypes: false } }], }, { - // Valid interface extending generic type code: ` type MyGenericType = IsAsync extends true ? { setThing(): Promise } @@ -817,7 +795,6 @@ interface MyAsyncInterface extends MyGenericType { options: [{ checksVoidReturn: { heritageTypes: true } }], }, { - // Invalid interface with rule off code: ` type MyGenericType = IsAsync extends true ? { setThing(): Promise } @@ -830,7 +807,6 @@ interface MyAsyncInterface extends MyGenericType { options: [{ checksVoidReturn: { heritageTypes: false } }], }, { - // Valid interface extending two interfaces code: ` interface MyInterface { setThing(): void; @@ -847,7 +823,6 @@ interface MyThirdInterface extends MyInterface, MyOtherInterface { options: [{ checksVoidReturn: { heritageTypes: true } }], }, { - // Valid interface extending two classes code: ` class MyClass { setThing(): void { @@ -868,7 +843,6 @@ interface MyInterface extends MyClass, MyOtherClass { options: [{ checksVoidReturn: { heritageTypes: true } }], }, { - // Valid class extending a class and implementing two interfaces code: ` interface MyInterface { setThing(): void; @@ -893,7 +867,6 @@ class MySubclass extends MyClass implements MyInterface, MyOtherInterface { options: [{ checksVoidReturn: { heritageTypes: true } }], }, { - // Valid class expression extending a class code: ` class MyClass { setThing(): void { @@ -910,7 +883,6 @@ const MyClassExpressionExtendsMyClass = class extends MyClass { options: [{ checksVoidReturn: { heritageTypes: true } }], }, { - // Valid class extending a class expression code: ` const MyClassExpression = class { setThing(): void { @@ -927,7 +899,6 @@ class MyClassExtendsMyClassExpression extends MyClassExpression { options: [{ checksVoidReturn: { heritageTypes: true } }], }, { - // Valid interface implementing a class expression code: ` const MyClassExpression = class { setThing(): void { @@ -943,7 +914,6 @@ interface MyInterfaceExtendsMyClassExpression extends MyClassExpressionType { options: [{ checksVoidReturn: { heritageTypes: true } }], }, { - // Call signatures: TS allows overloads with different return types, so we ignore these code: ` interface MySyncCallSignatures { (): void; @@ -957,7 +927,6 @@ interface MyAsyncInterface extends MySyncCallSignatures { options: [{ checksVoidReturn: { heritageTypes: true } }], }, { - // Construct signatures: These can't be async in the first place code: ` interface MySyncConstructSignatures { new (): void; @@ -971,9 +940,6 @@ interface ThisIsADifferentIssue extends MySyncConstructSignatures { options: [{ checksVoidReturn: { heritageTypes: true } }], }, { - // Index signatures: For now not handling until we can use checker.isTypeAssignableTo (v8) - // https://github.com/typescript-eslint/typescript-eslint/pull/8765 - // https://github.com/typescript-eslint/typescript-eslint/discussions/7936 code: ` interface MySyncIndexSignatures { [key: string]: void; @@ -987,7 +953,6 @@ interface ThisIsADifferentIssue extends MySyncIndexSignatures { options: [{ checksVoidReturn: { heritageTypes: true } }], }, { - // Mixed signatures: ignoring call/construct, not handling index (yet) code: ` interface MySyncInterfaceSignatures { (): void; @@ -1007,7 +972,6 @@ interface MyAsyncInterface extends MySyncInterfaceSignatures { options: [{ checksVoidReturn: { heritageTypes: true } }], }, { - // Valid combination code: ` interface MyCall { (): void; @@ -2123,7 +2087,6 @@ class MySubclass extends MyClass implements MyAsyncInterface, MySyncInterface { ], }, { - // Invalid class expression implementing an interface code: ` interface MyInterface { setThing(): void; @@ -2144,7 +2107,6 @@ const MyClassExpressionExtendsMyClass = class implements MyInterface { ], }, { - // Invalid class extending a class expression code: ` const MyClassExpression = class { setThing(): void { @@ -2167,7 +2129,6 @@ class MyClassExtendsMyClassExpression extends MyClassExpression { ], }, { - // Invalid interface implementing a class expression code: ` const MyClassExpression = class { setThing(): void { @@ -2189,7 +2150,6 @@ interface MyInterfaceExtendsMyClassExpression extends MyClassExpressionType { ], }, { - // Mixed signatures: not handling index signatures (yet); only the named method should error code: ` interface MySyncInterface { (): void; From f10b62d2e87ef1ebef018b7a3c006527ad1a411a Mon Sep 17 00:00:00 2001 From: Aly Thobani Date: Wed, 17 Jul 2024 11:06:26 -0700 Subject: [PATCH 25/26] rename `heritageTypes` suboption to `inheritedMethods` --- .../docs/rules/no-misused-promises.mdx | 6 +- .../src/rules/no-misused-promises.ts | 20 +-- .../no-misused-promises.shot | 2 +- .../tests/rules/no-misused-promises.test.ts | 114 +++++++++--------- .../schema-snapshots/no-misused-promises.shot | 4 +- 5 files changed, 73 insertions(+), 73 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/no-misused-promises.mdx b/packages/eslint-plugin/docs/rules/no-misused-promises.mdx index 3e47dded11b9..deda2e1bfabb 100644 --- a/packages/eslint-plugin/docs/rules/no-misused-promises.mdx +++ b/packages/eslint-plugin/docs/rules/no-misused-promises.mdx @@ -79,12 +79,12 @@ Disables checking an asynchronous function passed as argument where the paramete Disables checking an asynchronous function passed as a JSX attribute expected to be a function that returns `void`. -#### `heritageTypes` +#### `inheritedMethods` -Disables checking an asynchronous method in a type whose extended/implemented heritage type expects that method to return `void`. +Disables checking an asynchronous method in a type that extends or implements another type expecting that method to return `void`. :::note -For now, `no-misused-promises` only checks named methods against heritage types: that is, call/construct/index signatures are ignored. Call signatures are not required in TypeScript to be consistent with one another, and construct signatures cannot be `async` in the first place. Index signature checking may be implemented in the future. +For now, `no-misused-promises` only checks _named_ methods against extended/implemented types: that is, call/construct/index signatures are ignored. Call signatures are not required in TypeScript to be consistent with one another, and construct signatures cannot be `async` in the first place. Index signature checking may be implemented in the future. ::: #### `properties` diff --git a/packages/eslint-plugin/src/rules/no-misused-promises.ts b/packages/eslint-plugin/src/rules/no-misused-promises.ts index 209e1be7b5e7..cf3e8649ff80 100644 --- a/packages/eslint-plugin/src/rules/no-misused-promises.ts +++ b/packages/eslint-plugin/src/rules/no-misused-promises.ts @@ -20,7 +20,7 @@ type Options = [ interface ChecksVoidReturnOptions { arguments?: boolean; attributes?: boolean; - heritageTypes?: boolean; + inheritedMethods?: boolean; properties?: boolean; returns?: boolean; variables?: boolean; @@ -31,7 +31,7 @@ type MessageId = | 'spread' | 'voidReturnArgument' | 'voidReturnAttribute' - | 'voidReturnHeritageType' + | 'voidReturnInheritedMethod' | 'voidReturnProperty' | 'voidReturnReturnValue' | 'voidReturnVariable'; @@ -48,7 +48,7 @@ function parseChecksVoidReturn( return { arguments: true, attributes: true, - heritageTypes: true, + inheritedMethods: true, properties: true, returns: true, variables: true, @@ -58,7 +58,7 @@ function parseChecksVoidReturn( return { arguments: checksVoidReturn.arguments ?? true, attributes: checksVoidReturn.attributes ?? true, - heritageTypes: checksVoidReturn.heritageTypes ?? true, + inheritedMethods: checksVoidReturn.inheritedMethods ?? true, properties: checksVoidReturn.properties ?? true, returns: checksVoidReturn.returns ?? true, variables: checksVoidReturn.variables ?? true, @@ -79,8 +79,8 @@ export default createRule({ 'Promise returned in function argument where a void return was expected.', voidReturnAttribute: 'Promise-returning function provided to attribute where a void return was expected.', - voidReturnHeritageType: - "Promise-returning method provided where a void return was expected by heritage type '{{ heritageTypeName }}'.", + voidReturnInheritedMethod: + "Promise-returning method provided where a void return was expected by extended/implemented type '{{ heritageTypeName }}'.", voidReturnProperty: 'Promise-returning function provided to property where a void return was expected.', voidReturnReturnValue: @@ -106,7 +106,7 @@ export default createRule({ properties: { arguments: { type: 'boolean' }, attributes: { type: 'boolean' }, - heritageTypes: { type: 'boolean' }, + inheritedMethods: { type: 'boolean' }, properties: { type: 'boolean' }, returns: { type: 'boolean' }, variables: { type: 'boolean' }, @@ -160,7 +160,7 @@ export default createRule({ ...(checksVoidReturn.attributes && { JSXAttribute: checkJSXAttribute, }), - ...(checksVoidReturn.heritageTypes && { + ...(checksVoidReturn.inheritedMethods && { ClassDeclaration: checkClassLikeOrInterfaceNode, ClassExpression: checkClassLikeOrInterfaceNode, TSInterfaceDeclaration: checkClassLikeOrInterfaceNode, @@ -418,7 +418,7 @@ export default createRule({ /** * Checks `heritageType` for a member named `memberName` that returns void; reports the - * 'voidReturnHeritageType' message if found. + * 'voidReturnInheritedMethod' message if found. * @param nodeMember Node member that returns a Promise * @param heritageType Heritage type to check against * @param memberName Name of the member to check for @@ -441,7 +441,7 @@ export default createRule({ } context.report({ node: services.tsNodeToESTreeNodeMap.get(nodeMember), - messageId: 'voidReturnHeritageType', + messageId: 'voidReturnInheritedMethod', data: { heritageTypeName: checker.typeToString(heritageType) }, }); } diff --git a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-misused-promises.shot b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-misused-promises.shot index ff2540794e7f..0fe620529ffc 100644 --- a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-misused-promises.shot +++ b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-misused-promises.shot @@ -72,7 +72,7 @@ interface MySyncInterface { } class MyClass implements MySyncInterface { async setThing(): Promise { - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Promise-returning method provided where a void return was expected by heritage type 'MySyncInterface'. + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Promise-returning method provided where a void return was expected by extended/implemented type 'MySyncInterface'. this.thing = await fetchThing(); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ } diff --git a/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts b/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts index 556d1ee38114..22909bd13f3d 100644 --- a/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts @@ -514,7 +514,7 @@ class MySubclassExtendsMyClass extends MyClass { } } `, - options: [{ checksVoidReturn: { heritageTypes: true } }], + options: [{ checksVoidReturn: { inheritedMethods: true } }], }, { code: ` @@ -530,7 +530,7 @@ class MySubclassExtendsMyClass extends MyClass { } } `, - options: [{ checksVoidReturn: { heritageTypes: true } }], + options: [{ checksVoidReturn: { inheritedMethods: true } }], }, { code: ` @@ -546,7 +546,7 @@ class MySubclassExtendsMyClass extends MyClass { } } `, - options: [{ checksVoidReturn: { heritageTypes: false } }], + options: [{ checksVoidReturn: { inheritedMethods: false } }], }, { code: ` @@ -560,7 +560,7 @@ abstract class MyAbstractClassExtendsMyClass extends MyClass { abstract setThing(): void; } `, - options: [{ checksVoidReturn: { heritageTypes: true } }], + options: [{ checksVoidReturn: { inheritedMethods: true } }], }, { code: ` @@ -574,7 +574,7 @@ abstract class MyAbstractClassExtendsMyClass extends MyClass { abstract setThing(): Promise; } `, - options: [{ checksVoidReturn: { heritageTypes: false } }], + options: [{ checksVoidReturn: { inheritedMethods: false } }], }, { code: ` @@ -588,7 +588,7 @@ interface MyInterfaceExtendsMyClass extends MyClass { setThing(): void; } `, - options: [{ checksVoidReturn: { heritageTypes: true } }], + options: [{ checksVoidReturn: { inheritedMethods: true } }], }, { code: ` @@ -602,7 +602,7 @@ interface MyInterfaceExtendsMyClass extends MyClass { setThing(): Promise; } `, - options: [{ checksVoidReturn: { heritageTypes: false } }], + options: [{ checksVoidReturn: { inheritedMethods: false } }], }, { code: ` @@ -616,7 +616,7 @@ class MySubclassExtendsMyAbstractClass extends MyAbstractClass { } } `, - options: [{ checksVoidReturn: { heritageTypes: true } }], + options: [{ checksVoidReturn: { inheritedMethods: true } }], }, { code: ` @@ -630,7 +630,7 @@ class MySubclassExtendsMyAbstractClass extends MyAbstractClass { } } `, - options: [{ checksVoidReturn: { heritageTypes: false } }], + options: [{ checksVoidReturn: { inheritedMethods: false } }], }, { code: ` @@ -642,7 +642,7 @@ abstract class MyAbstractSubclassExtendsMyAbstractClass extends MyAbstractClass abstract setThing(): void; } `, - options: [{ checksVoidReturn: { heritageTypes: true } }], + options: [{ checksVoidReturn: { inheritedMethods: true } }], }, { code: ` @@ -654,7 +654,7 @@ abstract class MyAbstractSubclassExtendsMyAbstractClass extends MyAbstractClass abstract setThing(): Promise; } `, - options: [{ checksVoidReturn: { heritageTypes: false } }], + options: [{ checksVoidReturn: { inheritedMethods: false } }], }, { code: ` @@ -666,7 +666,7 @@ interface MyInterfaceExtendsMyAbstractClass extends MyAbstractClass { setThing(): void; } `, - options: [{ checksVoidReturn: { heritageTypes: true } }], + options: [{ checksVoidReturn: { inheritedMethods: true } }], }, { code: ` @@ -678,7 +678,7 @@ interface MyInterfaceExtendsMyAbstractClass extends MyAbstractClass { setThing(): Promise; } `, - options: [{ checksVoidReturn: { heritageTypes: false } }], + options: [{ checksVoidReturn: { inheritedMethods: false } }], }, { code: ` @@ -690,7 +690,7 @@ interface MySubInterfaceExtendsMyInterface extends MyInterface { setThing(): void; } `, - options: [{ checksVoidReturn: { heritageTypes: true } }], + options: [{ checksVoidReturn: { inheritedMethods: true } }], }, { code: ` @@ -702,7 +702,7 @@ interface MySubInterfaceExtendsMyInterface extends MyInterface { setThing(): Promise; } `, - options: [{ checksVoidReturn: { heritageTypes: false } }], + options: [{ checksVoidReturn: { inheritedMethods: false } }], }, { code: ` @@ -716,7 +716,7 @@ class MyClassImplementsMyInterface implements MyInterface { } } `, - options: [{ checksVoidReturn: { heritageTypes: true } }], + options: [{ checksVoidReturn: { inheritedMethods: true } }], }, { code: ` @@ -730,7 +730,7 @@ class MyClassImplementsMyInterface implements MyInterface { } } `, - options: [{ checksVoidReturn: { heritageTypes: false } }], + options: [{ checksVoidReturn: { inheritedMethods: false } }], }, { code: ` @@ -742,7 +742,7 @@ abstract class MyAbstractClassImplementsMyInterface implements MyInterface { abstract setThing(): void; } `, - options: [{ checksVoidReturn: { heritageTypes: true } }], + options: [{ checksVoidReturn: { inheritedMethods: true } }], }, { code: ` @@ -754,7 +754,7 @@ abstract class MyAbstractClassImplementsMyInterface implements MyInterface { abstract setThing(): Promise; } `, - options: [{ checksVoidReturn: { heritageTypes: false } }], + options: [{ checksVoidReturn: { inheritedMethods: false } }], }, { code: ` @@ -767,7 +767,7 @@ class MyClass implements MyTypeLiteralsIntersection { } } `, - options: [{ checksVoidReturn: { heritageTypes: true } }], + options: [{ checksVoidReturn: { inheritedMethods: true } }], }, { code: ` @@ -780,7 +780,7 @@ class MyClass implements MyTypeLiteralsIntersection { } } `, - options: [{ checksVoidReturn: { heritageTypes: false } }], + options: [{ checksVoidReturn: { inheritedMethods: false } }], }, { code: ` @@ -792,7 +792,7 @@ interface MyAsyncInterface extends MyGenericType { setThing(): Promise; } `, - options: [{ checksVoidReturn: { heritageTypes: true } }], + options: [{ checksVoidReturn: { inheritedMethods: true } }], }, { code: ` @@ -804,7 +804,7 @@ interface MyAsyncInterface extends MyGenericType { setThing(): Promise; } `, - options: [{ checksVoidReturn: { heritageTypes: false } }], + options: [{ checksVoidReturn: { inheritedMethods: false } }], }, { code: ` @@ -820,7 +820,7 @@ interface MyThirdInterface extends MyInterface, MyOtherInterface { setThing(): void; } `, - options: [{ checksVoidReturn: { heritageTypes: true } }], + options: [{ checksVoidReturn: { inheritedMethods: true } }], }, { code: ` @@ -840,7 +840,7 @@ interface MyInterface extends MyClass, MyOtherClass { setThing(): void; } `, - options: [{ checksVoidReturn: { heritageTypes: true } }], + options: [{ checksVoidReturn: { inheritedMethods: true } }], }, { code: ` @@ -864,7 +864,7 @@ class MySubclass extends MyClass implements MyInterface, MyOtherInterface { } } `, - options: [{ checksVoidReturn: { heritageTypes: true } }], + options: [{ checksVoidReturn: { inheritedMethods: true } }], }, { code: ` @@ -880,7 +880,7 @@ const MyClassExpressionExtendsMyClass = class extends MyClass { } }; `, - options: [{ checksVoidReturn: { heritageTypes: true } }], + options: [{ checksVoidReturn: { inheritedMethods: true } }], }, { code: ` @@ -896,7 +896,7 @@ class MyClassExtendsMyClassExpression extends MyClassExpression { } } `, - options: [{ checksVoidReturn: { heritageTypes: true } }], + options: [{ checksVoidReturn: { inheritedMethods: true } }], }, { code: ` @@ -911,7 +911,7 @@ interface MyInterfaceExtendsMyClassExpression extends MyClassExpressionType { setThing(): void; } `, - options: [{ checksVoidReturn: { heritageTypes: true } }], + options: [{ checksVoidReturn: { inheritedMethods: true } }], }, { code: ` @@ -924,7 +924,7 @@ interface MyAsyncInterface extends MySyncCallSignatures { (arg: string): Promise; } `, - options: [{ checksVoidReturn: { heritageTypes: true } }], + options: [{ checksVoidReturn: { inheritedMethods: true } }], }, { code: ` @@ -937,7 +937,7 @@ interface ThisIsADifferentIssue extends MySyncConstructSignatures { new (arg: string): Promise; } `, - options: [{ checksVoidReturn: { heritageTypes: true } }], + options: [{ checksVoidReturn: { inheritedMethods: true } }], }, { code: ` @@ -950,7 +950,7 @@ interface ThisIsADifferentIssue extends MySyncIndexSignatures { [key: number]: Promise; } `, - options: [{ checksVoidReturn: { heritageTypes: true } }], + options: [{ checksVoidReturn: { inheritedMethods: true } }], }, { code: ` @@ -969,7 +969,7 @@ interface MyAsyncInterface extends MySyncInterfaceSignatures { [key: number]: () => Promise; } `, - options: [{ checksVoidReturn: { heritageTypes: true } }], + options: [{ checksVoidReturn: { inheritedMethods: true } }], }, { code: ` @@ -1005,7 +1005,7 @@ interface MyInterface extends MyCall, MyIndex, MyConstruct, MyMethods { syncMethodProperty: () => void; } `, - options: [{ checksVoidReturn: { heritageTypes: true } }], + options: [{ checksVoidReturn: { inheritedMethods: true } }], }, ], @@ -1799,7 +1799,7 @@ class MySubclassExtendsMyClass extends MyClass { errors: [ { line: 9, - messageId: 'voidReturnHeritageType', + messageId: 'voidReturnInheritedMethod', data: { heritageTypeName: 'MyClass' }, }, ], @@ -1819,7 +1819,7 @@ abstract class MyAbstractClassExtendsMyClass extends MyClass { errors: [ { line: 9, - messageId: 'voidReturnHeritageType', + messageId: 'voidReturnInheritedMethod', data: { heritageTypeName: 'MyClass' }, }, ], @@ -1839,7 +1839,7 @@ interface MyInterfaceExtendsMyClass extends MyClass { errors: [ { line: 9, - messageId: 'voidReturnHeritageType', + messageId: 'voidReturnInheritedMethod', data: { heritageTypeName: 'MyClass' }, }, ], @@ -1859,7 +1859,7 @@ class MySubclassExtendsMyAbstractClass extends MyAbstractClass { errors: [ { line: 7, - messageId: 'voidReturnHeritageType', + messageId: 'voidReturnInheritedMethod', data: { heritageTypeName: 'MyAbstractClass' }, }, ], @@ -1877,7 +1877,7 @@ abstract class MyAbstractSubclassExtendsMyAbstractClass extends MyAbstractClass errors: [ { line: 7, - messageId: 'voidReturnHeritageType', + messageId: 'voidReturnInheritedMethod', data: { heritageTypeName: 'MyAbstractClass' }, }, ], @@ -1895,7 +1895,7 @@ interface MyInterfaceExtendsMyAbstractClass extends MyAbstractClass { errors: [ { line: 7, - messageId: 'voidReturnHeritageType', + messageId: 'voidReturnInheritedMethod', data: { heritageTypeName: 'MyAbstractClass' }, }, ], @@ -1915,7 +1915,7 @@ class MyInterfaceSubclass implements MyInterface { errors: [ { line: 7, - messageId: 'voidReturnHeritageType', + messageId: 'voidReturnInheritedMethod', data: { heritageTypeName: 'MyInterface' }, }, ], @@ -1933,7 +1933,7 @@ abstract class MyAbstractClassImplementsMyInterface implements MyInterface { errors: [ { line: 7, - messageId: 'voidReturnHeritageType', + messageId: 'voidReturnInheritedMethod', data: { heritageTypeName: 'MyInterface' }, }, ], @@ -1951,7 +1951,7 @@ interface MySubInterface extends MyInterface { errors: [ { line: 7, - messageId: 'voidReturnHeritageType', + messageId: 'voidReturnInheritedMethod', data: { heritageTypeName: 'MyInterface' }, }, ], @@ -1970,7 +1970,7 @@ class MyClassImplementsMyTypeIntersection implements MyTypeIntersection { errors: [ { line: 6, - messageId: 'voidReturnHeritageType', + messageId: 'voidReturnInheritedMethod', data: { heritageTypeName: 'MyTypeIntersection' }, }, ], @@ -1988,7 +1988,7 @@ interface MyAsyncInterface extends MyGenericType { errors: [ { line: 7, - messageId: 'voidReturnHeritageType', + messageId: 'voidReturnInheritedMethod', data: { heritageTypeName: '{ setThing(): void; }' }, }, ], @@ -2010,12 +2010,12 @@ interface MyThirdInterface extends MyInterface, MyOtherInterface { errors: [ { line: 11, - messageId: 'voidReturnHeritageType', + messageId: 'voidReturnInheritedMethod', data: { heritageTypeName: 'MyInterface' }, }, { line: 11, - messageId: 'voidReturnHeritageType', + messageId: 'voidReturnInheritedMethod', data: { heritageTypeName: 'MyOtherInterface' }, }, ], @@ -2041,12 +2041,12 @@ interface MyInterface extends MyClass, MyOtherClass { errors: [ { line: 15, - messageId: 'voidReturnHeritageType', + messageId: 'voidReturnInheritedMethod', data: { heritageTypeName: 'MyClass' }, }, { line: 15, - messageId: 'voidReturnHeritageType', + messageId: 'voidReturnInheritedMethod', data: { heritageTypeName: 'MyOtherClass' }, }, ], @@ -2076,12 +2076,12 @@ class MySubclass extends MyClass implements MyAsyncInterface, MySyncInterface { errors: [ { line: 17, - messageId: 'voidReturnHeritageType', + messageId: 'voidReturnInheritedMethod', data: { heritageTypeName: 'MyClass' }, }, { line: 17, - messageId: 'voidReturnHeritageType', + messageId: 'voidReturnInheritedMethod', data: { heritageTypeName: 'MySyncInterface' }, }, ], @@ -2101,7 +2101,7 @@ const MyClassExpressionExtendsMyClass = class implements MyInterface { errors: [ { line: 7, - messageId: 'voidReturnHeritageType', + messageId: 'voidReturnInheritedMethod', data: { heritageTypeName: 'MyInterface' }, }, ], @@ -2123,7 +2123,7 @@ class MyClassExtendsMyClassExpression extends MyClassExpression { errors: [ { line: 9, - messageId: 'voidReturnHeritageType', + messageId: 'voidReturnInheritedMethod', data: { heritageTypeName: 'MyClassExpression' }, }, ], @@ -2144,7 +2144,7 @@ interface MyInterfaceExtendsMyClassExpression extends MyClassExpressionType { errors: [ { line: 10, - messageId: 'voidReturnHeritageType', + messageId: 'voidReturnInheritedMethod', data: { heritageTypeName: 'typeof MyClassExpression' }, }, ], @@ -2171,7 +2171,7 @@ interface MyAsyncInterface extends MySyncInterface { errors: [ { line: 16, - messageId: 'voidReturnHeritageType', + messageId: 'voidReturnInheritedMethod', data: { heritageTypeName: 'MySyncInterface' }, }, ], @@ -2213,12 +2213,12 @@ interface MyInterface extends MyCall, MyIndex, MyConstruct, MyMethods { errors: [ { line: 29, - messageId: 'voidReturnHeritageType', + messageId: 'voidReturnInheritedMethod', data: { heritageTypeName: 'MyMethods' }, }, { line: 31, - messageId: 'voidReturnHeritageType', + messageId: 'voidReturnInheritedMethod', data: { heritageTypeName: 'MyMethods' }, }, ], diff --git a/packages/eslint-plugin/tests/schema-snapshots/no-misused-promises.shot b/packages/eslint-plugin/tests/schema-snapshots/no-misused-promises.shot index cf2c8e723994..4cd7d094b174 100644 --- a/packages/eslint-plugin/tests/schema-snapshots/no-misused-promises.shot +++ b/packages/eslint-plugin/tests/schema-snapshots/no-misused-promises.shot @@ -28,7 +28,7 @@ exports[`Rule schemas should be convertible to TS types for documentation purpos "attributes": { "type": "boolean" }, - "heritageTypes": { + "inheritedMethods": { "type": "boolean" }, "properties": { @@ -61,7 +61,7 @@ type Options = [ | { arguments?: boolean; attributes?: boolean; - heritageTypes?: boolean; + inheritedMethods?: boolean; properties?: boolean; returns?: boolean; variables?: boolean; From 6944e66b1475dcd4a0cde57e00cf2b562b87c0b2 Mon Sep 17 00:00:00 2001 From: Aly Thobani Date: Sun, 21 Jul 2024 18:29:55 -0700 Subject: [PATCH 26/26] Style nitty nit - not worrying about strict-boolean-expressions, condensing undefined and 0 check into one --- packages/eslint-plugin/src/rules/no-misused-promises.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/no-misused-promises.ts b/packages/eslint-plugin/src/rules/no-misused-promises.ts index cf3e8649ff80..5db6e12a2c3f 100644 --- a/packages/eslint-plugin/src/rules/no-misused-promises.ts +++ b/packages/eslint-plugin/src/rules/no-misused-promises.ts @@ -390,7 +390,7 @@ export default createRule({ const tsNode = services.esTreeNodeToTSNodeMap.get(node); const heritageTypes = getHeritageTypes(checker, tsNode); - if (heritageTypes === undefined || heritageTypes.length === 0) { + if (!heritageTypes?.length) { return; }