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; } pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy