diff --git a/packages/eslint-plugin/docs/rules/no-misused-promises.mdx b/packages/eslint-plugin/docs/rules/no-misused-promises.mdx index 3621829cb977..4ac2c491cde0 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`. + +#### `inheritedMethods` + +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 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` + +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,45 +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` -- `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`: @@ -140,6 +180,15 @@ document.addEventListener('click', async () => { await fetch('/'); console.log('synchronous call'); }); + +interface MySyncInterface { + setThing(): void; +} +class MyClass implements MySyncInterface { + async setThing(): Promise { + this.thing = await fetchThing(); + } +} ``` @@ -182,6 +231,15 @@ document.addEventListener('click', () => { handler().catch(handleError); }); + +interface MyAsyncInterface { + setThing(): Promise; +} +class MyClass implements MyAsyncInterface { + async setThing(): Promise { + this.thing = await fetchThing(); + } +} ``` @@ -189,19 +247,6 @@ document.addEventListener('click', () => { ### `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`: diff --git a/packages/eslint-plugin/src/rules/no-misused-promises.ts b/packages/eslint-plugin/src/rules/no-misused-promises.ts index 46d3d9759b1c..a5433edf7d10 100644 --- a/packages/eslint-plugin/src/rules/no-misused-promises.ts +++ b/packages/eslint-plugin/src/rules/no-misused-promises.ts @@ -23,6 +23,7 @@ type Options = [ interface ChecksVoidReturnOptions { arguments?: boolean; attributes?: boolean; + inheritedMethods?: boolean; properties?: boolean; returns?: boolean; variables?: boolean; @@ -33,6 +34,7 @@ type MessageId = | 'spread' | 'voidReturnArgument' | 'voidReturnAttribute' + | 'voidReturnInheritedMethod' | 'voidReturnProperty' | 'voidReturnReturnValue' | 'voidReturnVariable'; @@ -49,6 +51,7 @@ function parseChecksVoidReturn( return { arguments: true, attributes: true, + inheritedMethods: true, properties: true, returns: true, variables: true, @@ -58,6 +61,7 @@ function parseChecksVoidReturn( return { arguments: checksVoidReturn.arguments ?? true, attributes: checksVoidReturn.attributes ?? true, + inheritedMethods: checksVoidReturn.inheritedMethods ?? true, properties: checksVoidReturn.properties ?? true, returns: checksVoidReturn.returns ?? true, variables: checksVoidReturn.variables ?? true, @@ -76,14 +80,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.', + 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: '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.', + 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.', }, @@ -103,6 +109,7 @@ export default createRule({ properties: { arguments: { type: 'boolean' }, attributes: { type: 'boolean' }, + inheritedMethods: { type: 'boolean' }, properties: { type: 'boolean' }, returns: { type: 'boolean' }, variables: { type: 'boolean' }, @@ -156,6 +163,11 @@ export default createRule({ ...(checksVoidReturn.attributes && { JSXAttribute: checkJSXAttribute, }), + ...(checksVoidReturn.inheritedMethods && { + ClassDeclaration: checkClassLikeOrInterfaceNode, + ClassExpression: checkClassLikeOrInterfaceNode, + TSInterfaceDeclaration: checkClassLikeOrInterfaceNode, + }), ...(checksVoidReturn.properties && { Property: checkProperty, }), @@ -466,6 +478,71 @@ export default createRule({ } } + function checkClassLikeOrInterfaceNode( + node: + | TSESTree.ClassDeclaration + | TSESTree.ClassExpression + | TSESTree.TSInterfaceDeclaration, + ): void { + const tsNode = services.esTreeNodeToTSNodeMap.get(node); + + const heritageTypes = getHeritageTypes(checker, tsNode); + if (!heritageTypes?.length) { + return; + } + + 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)) { + continue; + } + for (const heritageType of heritageTypes) { + checkHeritageTypeForMemberReturningVoid( + nodeMember, + heritageType, + memberName, + ); + } + } + } + + /** + * Checks `heritageType` for a member named `memberName` that returns void; reports the + * '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 + */ + 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: 'voidReturnInheritedMethod', + data: { heritageTypeName: checker.typeToString(heritageType) }, + }); + } + function checkJSXAttribute(node: TSESTree.JSXAttribute): void { if ( node.value == null || @@ -777,3 +854,26 @@ function returnsThenable(checker: ts.TypeChecker, node: ts.Node): boolean { .unionTypeParts(type) .some(t => anySignatureIsThenableType(checker, node, t)); } + +function getHeritageTypes( + checker: ts.TypeChecker, + tsNode: ts.ClassDeclaration | ts.ClassExpression | ts.InterfaceDeclaration, +): ts.Type[] | undefined { + return tsNode.heritageClauses + ?.flatMap(clause => clause.types) + .map(typeExpression => checker.getTypeAtLocation(typeExpression)); +} + +/** + * @returns The member with the given name in `type`, if it exists. + */ +function getMemberIfExists( + type: ts.Type, + memberName: string, +): ts.Symbol | undefined { + const escapedMemberName = ts.escapeLeadingUnderscores(memberName); + const symbolMemberMatch = type.getSymbol()?.members?.get(escapedMemberName); + return ( + symbolMemberMatch ?? tsutils.getPropertyOfType(type, escapedMemberName) + ); +} 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 e42445e017bb..17d39d27c875 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 @@ -65,6 +65,18 @@ document.addEventListener('click', async () => { await fetch('/'); console.log('synchronous call'); }); + +interface MySyncInterface { + setThing(): void; +} +class MyClass implements MySyncInterface { + async setThing(): Promise { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Promise-returning method provided where a void return was expected by extended/implemented type 'MySyncInterface'. + this.thing = await fetchThing(); +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + } +~~~ +} " `; @@ -108,6 +120,15 @@ document.addEventListener('click', () => { handler().catch(handleError); }); + +interface MyAsyncInterface { + setThing(): Promise; +} +class MyClass implements MyAsyncInterface { + async setThing(): Promise { + 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 ce7f5c0eef90..fc2229b31cf4 100644 --- a/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts @@ -508,794 +508,1739 @@ foo(bar); }, options: [{ checksVoidReturn: { attributes: true } }], }, - ` - const notAFn1: string = ''; - const notAFn2: number = 1; - const notAFn3: boolean = true; - const notAFn4: { prop: 1 } = { prop: 1 }; - const notAFn5: {} = {}; - const notAFn5: {} = {}; - `, - ], - - invalid: [ { code: ` -if (Promise.resolve()) { +class MyClass { + setThing(): void { + return; + } +} + +class MySubclassExtendsMyClass extends MyClass { + setThing(): void { + return; + } } `, - errors: [ - { - line: 2, - messageId: 'conditional', - }, - ], + options: [{ checksVoidReturn: { inheritedMethods: true } }], }, { code: ` -if (Promise.resolve()) { -} else if (Promise.resolve()) { -} else { +class MyClass { + async setThing(): Promise { + await Promise.resolve(); + } +} + +class MySubclassExtendsMyClass extends MyClass { + async setThing(): Promise { + await Promise.resolve(); + } } `, - errors: [ - { - line: 2, - messageId: 'conditional', - }, - { - line: 3, - messageId: 'conditional', - }, - ], + options: [{ checksVoidReturn: { inheritedMethods: true } }], }, { - code: 'for (let i; Promise.resolve(); i++) {}', - errors: [ - { - line: 1, - messageId: 'conditional', - }, - ], + code: ` +class MyClass { + setThing(): void { + return; + } +} + +class MySubclassExtendsMyClass extends MyClass { + async setThing(): Promise { + await Promise.resolve(); + } +} + `, + options: [{ checksVoidReturn: { inheritedMethods: false } }], }, { - code: 'do {} while (Promise.resolve());', - errors: [ - { - line: 1, - messageId: 'conditional', - }, - ], + code: ` +class MyClass { + setThing(): void { + return; + } +} + +abstract class MyAbstractClassExtendsMyClass extends MyClass { + abstract setThing(): void; +} + `, + options: [{ checksVoidReturn: { inheritedMethods: true } }], }, { - code: 'while (Promise.resolve()) {}', - errors: [ - { - line: 1, - messageId: 'conditional', - }, - ], + code: ` +class MyClass { + setThing(): void { + return; + } +} + +abstract class MyAbstractClassExtendsMyClass extends MyClass { + abstract setThing(): Promise; +} + `, + options: [{ checksVoidReturn: { inheritedMethods: false } }], }, { - code: 'Promise.resolve() ? 123 : 456;', - errors: [ - { - line: 1, - messageId: 'conditional', - }, - ], + code: ` +class MyClass { + setThing(): void { + return; + } +} + +interface MyInterfaceExtendsMyClass extends MyClass { + setThing(): void; +} + `, + options: [{ checksVoidReturn: { inheritedMethods: true } }], }, { code: ` -if (!Promise.resolve()) { +class MyClass { + setThing(): void { + return; + } +} + +interface MyInterfaceExtendsMyClass extends MyClass { + setThing(): Promise; } `, - errors: [ - { - line: 2, - messageId: 'conditional', - }, - ], + options: [{ checksVoidReturn: { inheritedMethods: false } }], }, { - code: 'Promise.resolve() || false;', - errors: [ - { - line: 1, - messageId: 'conditional', - }, - ], + code: ` +abstract class MyAbstractClass { + abstract setThing(): void; +} + +class MySubclassExtendsMyAbstractClass extends MyAbstractClass { + setThing(): void { + return; + } +} + `, + options: [{ checksVoidReturn: { inheritedMethods: true } }], }, { code: ` -[Promise.resolve(), Promise.reject()].forEach(async val => { - await val; -}); +abstract class MyAbstractClass { + abstract setThing(): void; +} + +class MySubclassExtendsMyAbstractClass extends MyAbstractClass { + async setThing(): Promise { + await Promise.resolve(); + } +} `, - errors: [ - { - line: 2, - messageId: 'voidReturnArgument', - }, - ], + options: [{ checksVoidReturn: { inheritedMethods: false } }], }, { code: ` -new Promise(async (resolve, reject) => { - await Promise.resolve(); - resolve(); -}); +abstract class MyAbstractClass { + abstract setThing(): void; +} + +abstract class MyAbstractSubclassExtendsMyAbstractClass extends MyAbstractClass { + abstract setThing(): void; +} `, - errors: [ - { - line: 2, - messageId: 'voidReturnArgument', - }, - ], + options: [{ checksVoidReturn: { inheritedMethods: true } }], }, { code: ` -const fnWithCallback = (arg: string, cb: (err: any, res: string) => void) => { - cb(null, arg); -}; +abstract class MyAbstractClass { + abstract setThing(): void; +} -fnWithCallback('val', async (err, res) => { - await res; -}); +abstract class MyAbstractSubclassExtendsMyAbstractClass extends MyAbstractClass { + abstract setThing(): Promise; +} `, - errors: [ - { - line: 6, - messageId: 'voidReturnArgument', - }, - ], + options: [{ checksVoidReturn: { inheritedMethods: false } }], }, { code: ` -const fnWithCallback = (arg: string, cb: (err: any, res: string) => void) => { - cb(null, arg); -}; +abstract class MyAbstractClass { + abstract setThing(): void; +} -fnWithCallback('val', (err, res) => Promise.resolve(res)); +interface MyInterfaceExtendsMyAbstractClass extends MyAbstractClass { + setThing(): void; +} `, - errors: [ - { - line: 6, - messageId: 'voidReturnArgument', - }, - ], + options: [{ checksVoidReturn: { inheritedMethods: true } }], }, { code: ` -const fnWithCallback = (arg: string, cb: (err: any, res: string) => void) => { - cb(null, arg); -}; +abstract class MyAbstractClass { + abstract setThing(): void; +} -fnWithCallback('val', (err, res) => { - if (err) { - return 'abc'; - } else { - return Promise.resolve(res); - } -}); +interface MyInterfaceExtendsMyAbstractClass extends MyAbstractClass { + setThing(): Promise; +} `, - errors: [ - { - line: 6, - messageId: 'voidReturnArgument', - }, - ], + options: [{ checksVoidReturn: { inheritedMethods: false } }], }, { code: ` -const fnWithCallback: - | ((arg: string, cb: (err: any, res: string) => void) => void) - | null = (arg, cb) => { - cb(null, arg); -}; +interface MyInterface { + setThing(): void; +} -fnWithCallback?.('val', (err, res) => Promise.resolve(res)); +interface MySubInterfaceExtendsMyInterface extends MyInterface { + setThing(): void; +} `, - errors: [ - { - line: 8, - messageId: 'voidReturnArgument', - }, - ], + options: [{ checksVoidReturn: { inheritedMethods: true } }], }, { code: ` -const fnWithCallback: - | ((arg: string, cb: (err: any, res: string) => void) => void) - | null = (arg, cb) => { - cb(null, arg); -}; +interface MyInterface { + setThing(): void; +} -fnWithCallback('val', (err, res) => { - if (err) { - return 'abc'; - } else { - return Promise.resolve(res); - } -}); +interface MySubInterfaceExtendsMyInterface extends MyInterface { + setThing(): Promise; +} `, - errors: [ - { - line: 8, - messageId: 'voidReturnArgument', - }, - ], + options: [{ checksVoidReturn: { inheritedMethods: false } }], }, { code: ` -function test(bool: boolean, p: Promise) { - if (bool || p) { +interface MyInterface { + setThing(): void; +} + +class MyClassImplementsMyInterface implements MyInterface { + setThing(): void { + return; } } `, - errors: [ - { - line: 3, - messageId: 'conditional', - }, - ], + options: [{ checksVoidReturn: { inheritedMethods: true } }], }, { code: ` -function test(bool: boolean, p: Promise) { - if (bool && p) { +interface MyInterface { + setThing(): void; +} + +class MyClassImplementsMyInterface implements MyInterface { + async setThing(): Promise { + await Promise.resolve(); } } `, - errors: [ - { - line: 3, - messageId: 'conditional', - }, - ], + options: [{ checksVoidReturn: { inheritedMethods: false } }], }, { code: ` -function test(a: any, p: Promise) { - if (a ?? p) { - } +interface MyInterface { + setThing(): void; +} + +abstract class MyAbstractClassImplementsMyInterface implements MyInterface { + abstract setThing(): void; } `, - errors: [ - { - line: 3, - messageId: 'conditional', - }, - ], + options: [{ checksVoidReturn: { inheritedMethods: true } }], }, { code: ` -function test(p: Promise | undefined) { - if (p ?? Promise.reject()) { - } +interface MyInterface { + setThing(): void; +} + +abstract class MyAbstractClassImplementsMyInterface implements MyInterface { + abstract setThing(): Promise; } `, - errors: [ - { - line: 3, - messageId: 'conditional', - }, - ], + options: [{ checksVoidReturn: { inheritedMethods: false } }], }, { code: ` -let f: () => void; -f = async () => { - return 3; +type MyTypeLiteralsIntersection = { setThing(): void } & { thing: number }; + +class MyClass implements MyTypeLiteralsIntersection { + thing = 1; + setThing(): void { + return; + } +} + `, + options: [{ checksVoidReturn: { inheritedMethods: true } }], + }, + { + code: ` +type MyTypeLiteralsIntersection = { setThing(): void } & { thing: number }; + +class MyClass implements MyTypeLiteralsIntersection { + thing = 1; + async setThing(): Promise { + await Promise.resolve(); + } +} + `, + options: [{ checksVoidReturn: { inheritedMethods: false } }], + }, + { + code: ` +type MyGenericType = IsAsync extends true + ? { setThing(): Promise } + : { setThing(): void }; + +interface MyAsyncInterface extends MyGenericType { + setThing(): Promise; +} + `, + options: [{ checksVoidReturn: { inheritedMethods: true } }], + }, + { + code: ` +type MyGenericType = IsAsync extends true + ? { setThing(): Promise } + : { setThing(): void }; + +interface MyAsyncInterface extends MyGenericType { + setThing(): Promise; +} + `, + options: [{ checksVoidReturn: { inheritedMethods: false } }], + }, + { + code: ` +interface MyInterface { + setThing(): void; +} + +interface MyOtherInterface { + setThing(): void; +} + +interface MyThirdInterface extends MyInterface, MyOtherInterface { + setThing(): void; +} + `, + options: [{ checksVoidReturn: { inheritedMethods: true } }], + }, + { + code: ` +class MyClass { + setThing(): void { + return; + } +} + +class MyOtherClass { + setThing(): void { + return; + } +} + +interface MyInterface extends MyClass, MyOtherClass { + setThing(): void; +} + `, + options: [{ checksVoidReturn: { inheritedMethods: true } }], + }, + { + 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: { inheritedMethods: true } }], + }, + { + code: ` +class MyClass { + setThing(): void { + return; + } +} + +const MyClassExpressionExtendsMyClass = class extends MyClass { + setThing(): void { + return; + } }; `, - errors: [ - { - line: 3, - messageId: 'voidReturnVariable', - }, - ], + options: [{ checksVoidReturn: { inheritedMethods: true } }], }, { code: ` -let f: () => void; -f = async () => { - return 3; +const MyClassExpression = class { + setThing(): void { + return; + } }; + +class MyClassExtendsMyClassExpression extends MyClassExpression { + setThing(): void { + return; + } +} `, - errors: [ - { - line: 3, - messageId: 'voidReturnVariable', - }, - ], - options: [{ checksVoidReturn: { variables: true } }], + options: [{ checksVoidReturn: { inheritedMethods: true } }], }, { code: ` -const f: () => void = async () => { - return 0; +const MyClassExpression = class { + setThing(): void { + return; + } }; -const g = async () => 1, - h: () => void = async () => {}; +type MyClassExpressionType = typeof MyClassExpression; + +interface MyInterfaceExtendsMyClassExpression extends MyClassExpressionType { + setThing(): void; +} + `, + options: [{ checksVoidReturn: { inheritedMethods: true } }], + }, + { + code: ` +interface MySyncCallSignatures { + (): void; + (arg: string): void; +} +interface MyAsyncInterface extends MySyncCallSignatures { + (): Promise; + (arg: string): Promise; +} + `, + options: [{ checksVoidReturn: { inheritedMethods: true } }], + }, + { + code: ` +interface MySyncConstructSignatures { + new (): void; + new (arg: string): void; +} +interface ThisIsADifferentIssue extends MySyncConstructSignatures { + new (): Promise; + new (arg: string): Promise; +} + `, + options: [{ checksVoidReturn: { inheritedMethods: true } }], + }, + { + code: ` +interface MySyncIndexSignatures { + [key: string]: void; + [key: number]: void; +} +interface ThisIsADifferentIssue extends MySyncIndexSignatures { + [key: string]: Promise; + [key: number]: Promise; +} + `, + options: [{ checksVoidReturn: { inheritedMethods: true } }], + }, + { + 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: { inheritedMethods: true } }], + }, + { + 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: { inheritedMethods: true } }], + }, + "const notAFn1: string = '';", + 'const notAFn2: number = 1;', + 'const notAFn3: boolean = true;', + 'const notAFn4: { prop: 1 } = { prop: 1 };', + 'const notAFn5: {} = {};', + ], + + invalid: [ + { + code: ` +if (Promise.resolve()) { +} `, errors: [ { line: 2, - messageId: 'voidReturnVariable', - }, - { - line: 6, - messageId: 'voidReturnVariable', + messageId: 'conditional', }, ], }, { code: ` -const obj: { - f?: () => void; -} = {}; -obj.f = async () => { - return 0; -}; +if (Promise.resolve()) { +} else if (Promise.resolve()) { +} else { +} `, errors: [ { - line: 5, - messageId: 'voidReturnVariable', + line: 2, + messageId: 'conditional', + }, + { + line: 3, + messageId: 'conditional', }, ], }, { - code: ` -type O = { f: () => void }; -const obj: O = { - f: async () => 'foo', -}; - `, + code: 'for (let i; Promise.resolve(); i++) {}', errors: [ { - line: 4, - messageId: 'voidReturnProperty', + line: 1, + messageId: 'conditional', }, ], }, { - code: ` -type O = { f: () => void }; -const obj: O = { - f: async () => 'foo', -}; - `, + code: 'do {} while (Promise.resolve());', errors: [ { - line: 4, - messageId: 'voidReturnProperty', + line: 1, + messageId: 'conditional', }, ], - options: [{ checksVoidReturn: { properties: true } }], }, { - code: ` -type O = { f: () => void }; -const f = async () => 0; -const obj: O = { - f, -}; - `, + code: 'while (Promise.resolve()) {}', errors: [ { - line: 5, - messageId: 'voidReturnProperty', + line: 1, + messageId: 'conditional', }, ], }, { - code: ` -type O = { f: () => void }; -const obj: O = { - async f() { - return 0; - }, -}; - `, + code: 'Promise.resolve() ? 123 : 456;', errors: [ { - line: 4, - messageId: 'voidReturnProperty', + line: 1, + messageId: 'conditional', }, ], }, { code: ` -type O = { f: () => void; g: () => void; h: () => void }; -function f(): O { - const h = async () => 0; - return { - async f() { - return 123; - }, - g: async () => 0, - h, - }; +if (!Promise.resolve()) { } `, errors: [ { - line: 6, - messageId: 'voidReturnProperty', - }, - { - line: 9, - messageId: 'voidReturnProperty', - }, - { - line: 10, - messageId: 'voidReturnProperty', + line: 2, + messageId: 'conditional', }, ], }, { - code: ` -function f(): () => void { - return async () => 0; -} - `, + code: 'Promise.resolve() || false;', errors: [ { - line: 3, - messageId: 'voidReturnReturnValue', + line: 1, + messageId: 'conditional', }, ], }, { code: ` -function f(): () => void { - return async () => 0; -} +[Promise.resolve(), Promise.reject()].forEach(async val => { + await val; +}); `, errors: [ { - line: 3, - messageId: 'voidReturnReturnValue', + line: 2, + messageId: 'voidReturnArgument', }, ], - options: [{ checksVoidReturn: { returns: true } }], }, { code: ` -type O = { - func: () => void; -}; -const Component = (obj: O) => null; - 0} />; +new Promise(async (resolve, reject) => { + await Promise.resolve(); + resolve(); +}); `, - languageOptions: { - parserOptions: { - ecmaFeatures: { - jsx: true, - }, + errors: [ + { + line: 2, + messageId: 'voidReturnArgument', }, - }, + ], + }, + { + code: ` +const fnWithCallback = (arg: string, cb: (err: any, res: string) => void) => { + cb(null, arg); +}; + +fnWithCallback('val', async (err, res) => { + await res; +}); + `, errors: [ { line: 6, - messageId: 'voidReturnAttribute', + messageId: 'voidReturnArgument', }, ], }, { code: ` -type O = { - func: () => void; +const fnWithCallback = (arg: string, cb: (err: any, res: string) => void) => { + cb(null, arg); }; -const Component = (obj: O) => null; - 0} />; + +fnWithCallback('val', (err, res) => Promise.resolve(res)); `, - languageOptions: { - parserOptions: { - ecmaFeatures: { - jsx: true, - }, - }, - }, errors: [ { line: 6, - messageId: 'voidReturnAttribute', + messageId: 'voidReturnArgument', }, ], - options: [{ checksVoidReturn: { attributes: true } }], }, { code: ` -type O = { - func: () => void; +const fnWithCallback = (arg: string, cb: (err: any, res: string) => void) => { + cb(null, arg); }; -const g = async () => 'foo'; -const Component = (obj: O) => null; -; + +fnWithCallback('val', (err, res) => { + if (err) { + return 'abc'; + } else { + return Promise.resolve(res); + } +}); `, - languageOptions: { - parserOptions: { - ecmaFeatures: { - jsx: true, - }, - }, - }, errors: [ { - line: 7, - messageId: 'voidReturnAttribute', + line: 6, + messageId: 'voidReturnArgument', }, ], }, { code: ` -interface ItLike { - (name: string, callback: () => number): void; - (name: string, callback: () => void): void; -} +const fnWithCallback: + | ((arg: string, cb: (err: any, res: string) => void) => void) + | null = (arg, cb) => { + cb(null, arg); +}; -declare const it: ItLike; +fnWithCallback?.('val', (err, res) => Promise.resolve(res)); + `, + errors: [ + { + line: 8, + messageId: 'voidReturnArgument', + }, + ], + }, + { + code: ` +const fnWithCallback: + | ((arg: string, cb: (err: any, res: string) => void) => void) + | null = (arg, cb) => { + cb(null, arg); +}; -it('', async () => {}); +fnWithCallback('val', (err, res) => { + if (err) { + return 'abc'; + } else { + return Promise.resolve(res); + } +}); `, errors: [ { - line: 9, + line: 8, messageId: 'voidReturnArgument', }, ], }, { code: ` -interface ItLike { - (name: string, callback: () => number): void; +function test(bool: boolean, p: Promise) { + if (bool || p) { + } } -interface ItLike { - (name: string, callback: () => void): void; + `, + errors: [ + { + line: 3, + messageId: 'conditional', + }, + ], + }, + { + code: ` +function test(bool: boolean, p: Promise) { + if (bool && p) { + } } - -declare const it: ItLike; - -it('', async () => {}); `, errors: [ { - line: 11, - messageId: 'voidReturnArgument', + line: 3, + messageId: 'conditional', + }, + ], + }, + { + code: ` +function test(a: any, p: Promise) { + if (a ?? p) { + } +} + `, + errors: [ + { + line: 3, + messageId: 'conditional', + }, + ], + }, + { + code: ` +function test(p: Promise | undefined) { + if (p ?? Promise.reject()) { + } +} + `, + errors: [ + { + line: 3, + messageId: 'conditional', + }, + ], + }, + { + code: ` +let f: () => void; +f = async () => { + return 3; +}; + `, + errors: [ + { + line: 3, + messageId: 'voidReturnVariable', + }, + ], + }, + { + code: ` +let f: () => void; +f = async () => { + return 3; +}; + `, + errors: [ + { + line: 3, + messageId: 'voidReturnVariable', + }, + ], + options: [{ checksVoidReturn: { variables: true } }], + }, + { + code: ` +const f: () => void = async () => { + return 0; +}; +const g = async () => 1, + h: () => void = async () => {}; + `, + errors: [ + { + line: 2, + messageId: 'voidReturnVariable', + }, + { + line: 6, + messageId: 'voidReturnVariable', + }, + ], + }, + { + code: ` +const obj: { + f?: () => void; +} = {}; +obj.f = async () => { + return 0; +}; + `, + errors: [ + { + line: 5, + messageId: 'voidReturnVariable', + }, + ], + }, + { + code: ` +type O = { f: () => void }; +const obj: O = { + f: async () => 'foo', +}; + `, + errors: [ + { + line: 4, + messageId: 'voidReturnProperty', + }, + ], + }, + { + code: ` +type O = { f: () => void }; +const obj: O = { + f: async () => 'foo', +}; + `, + errors: [ + { + line: 4, + messageId: 'voidReturnProperty', + }, + ], + options: [{ checksVoidReturn: { properties: true } }], + }, + { + code: ` +type O = { f: () => void }; +const f = async () => 0; +const obj: O = { + f, +}; + `, + errors: [ + { + line: 5, + messageId: 'voidReturnProperty', + }, + ], + }, + { + code: ` +type O = { f: () => void }; +const obj: O = { + async f() { + return 0; + }, +}; + `, + errors: [ + { + line: 4, + messageId: 'voidReturnProperty', + }, + ], + }, + { + code: ` +type O = { f: () => void; g: () => void; h: () => void }; +function f(): O { + const h = async () => 0; + return { + async f() { + return 123; + }, + g: async () => 0, + h, + }; +} + `, + errors: [ + { + line: 6, + messageId: 'voidReturnProperty', + }, + { + line: 9, + messageId: 'voidReturnProperty', + }, + { + line: 10, + messageId: 'voidReturnProperty', + }, + ], + }, + { + code: ` +function f(): () => void { + return async () => 0; +} + `, + errors: [ + { + line: 3, + messageId: 'voidReturnReturnValue', + }, + ], + }, + { + code: ` +function f(): () => void { + return async () => 0; +} + `, + errors: [ + { + line: 3, + messageId: 'voidReturnReturnValue', + }, + ], + options: [{ checksVoidReturn: { returns: true } }], + }, + { + code: ` +type O = { + func: () => void; +}; +const Component = (obj: O) => null; + 0} />; + `, + languageOptions: { + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + }, + errors: [ + { + line: 6, + messageId: 'voidReturnAttribute', + }, + ], + }, + { + code: ` +type O = { + func: () => void; +}; +const Component = (obj: O) => null; + 0} />; + `, + languageOptions: { + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + }, + errors: [ + { + line: 6, + messageId: 'voidReturnAttribute', + }, + ], + options: [{ checksVoidReturn: { attributes: true } }], + }, + { + code: ` +type O = { + func: () => void; +}; +const g = async () => 'foo'; +const Component = (obj: O) => null; +; + `, + languageOptions: { + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + }, + errors: [ + { + line: 7, + messageId: 'voidReturnAttribute', + }, + ], + }, + { + code: ` +interface ItLike { + (name: string, callback: () => number): void; + (name: string, callback: () => void): void; +} + +declare const it: ItLike; + +it('', async () => {}); + `, + errors: [ + { + line: 9, + messageId: 'voidReturnArgument', + }, + ], + }, + { + code: ` +interface ItLike { + (name: string, callback: () => number): void; +} +interface ItLike { + (name: string, callback: () => void): void; +} + +declare const it: ItLike; + +it('', async () => {}); + `, + errors: [ + { + line: 11, + messageId: 'voidReturnArgument', + }, + ], + }, + { + code: ` +interface ItLike { + (name: string, callback: () => void): void; +} +interface ItLike { + (name: string, callback: () => number): void; +} + +declare const it: ItLike; + +it('', async () => {}); + `, + errors: [ + { + line: 11, + messageId: 'voidReturnArgument', + }, + ], + }, + { + code: ` +console.log({ ...Promise.resolve({ key: 42 }) }); + `, + errors: [ + { + line: 2, + messageId: 'spread', + }, + ], + }, + { + code: ` +const getData = () => Promise.resolve({ key: 42 }); + +console.log({ + someData: 42, + ...getData(), +}); + `, + errors: [ + { + line: 6, + messageId: 'spread', + }, + ], + }, + { + code: ` +declare const condition: boolean; + +console.log({ ...(condition && Promise.resolve({ key: 42 })) }); +console.log({ ...(condition || Promise.resolve({ key: 42 })) }); +console.log({ ...(condition ? {} : Promise.resolve({ key: 42 })) }); +console.log({ ...(condition ? Promise.resolve({ key: 42 }) : {}) }); + `, + errors: [ + { line: 4, messageId: 'spread' }, + { line: 5, messageId: 'spread' }, + { line: 6, messageId: 'spread' }, + { line: 7, messageId: 'spread' }, + ], + }, + { + code: ` +function restPromises(first: Boolean, ...callbacks: Array<() => void>): void {} + +restPromises( + true, + () => Promise.resolve(true), + () => Promise.resolve(null), + () => true, + () => Promise.resolve('Hello'), +); + `, + errors: [ + { line: 6, messageId: 'voidReturnArgument' }, + { line: 7, messageId: 'voidReturnArgument' }, + { line: 9, messageId: 'voidReturnArgument' }, + ], + }, + { + code: ` +type MyUnion = (() => void) | boolean; + +function restUnion(first: string, ...callbacks: Array): void {} +restUnion('Testing', false, () => Promise.resolve(true)); + `, + errors: [{ line: 5, messageId: 'voidReturnArgument' }], + }, + { + code: ` +function restTupleOne(first: string, ...callbacks: [() => void]): void {} +restTupleOne('My string', () => Promise.resolve(1)); + `, + errors: [{ line: 3, messageId: 'voidReturnArgument' }], + }, + { + code: ` +function restTupleTwo( + first: boolean, + ...callbacks: [undefined, () => void, undefined] +): void {} + +restTupleTwo(true, undefined, () => Promise.resolve(true), undefined); + `, + errors: [{ line: 7, messageId: 'voidReturnArgument' }], + }, + { + code: ` +function restTupleFour( + first: number, + ...callbacks: [() => void, boolean, () => void, () => void] +): void; + +restTupleFour( + 1, + () => Promise.resolve(true), + false, + () => {}, + () => Promise.resolve(1), +); + `, + errors: [ + { line: 9, messageId: 'voidReturnArgument' }, + { line: 12, messageId: 'voidReturnArgument' }, + ], + }, + { + // Prettier adds a () but this tests arguments being undefined, not [] + // eslint-disable-next-line @typescript-eslint/internal/plugin-test-formatting + code: ` +class TakesVoidCb { + constructor(first: string, ...args: Array<() => void>); +} + +new TakesVoidCb; +new TakesVoidCb(); +new TakesVoidCb( + 'Testing', + () => {}, + () => Promise.resolve(true), +); + `, + errors: [{ line: 11, messageId: 'voidReturnArgument' }], + }, + { + code: ` +function restTuple(...args: []): void; +function restTuple(...args: [boolean, () => void]): void; +function restTuple(..._args: any[]): void {} + +restTuple(); +restTuple(true, () => Promise.resolve(1)); + `, + errors: [{ line: 7, messageId: 'voidReturnArgument' }], + }, + { + code: ` +type ReturnsRecord = () => Record void>; + +const test: ReturnsRecord = () => { + return { asynchronous: async () => {} }; +}; + `, + errors: [{ line: 5, messageId: 'voidReturnProperty' }], + }, + { + code: ` +let value: Record void>; +value.asynchronous = async () => {}; + `, + errors: [{ line: 3, messageId: 'voidReturnVariable' }], + }, + { + code: ` +type ReturnsRecord = () => Record void>; + +async function asynchronous() {} + +const test: ReturnsRecord = () => { + return { asynchronous }; +}; + `, + errors: [{ line: 7, messageId: 'voidReturnProperty' }], + }, + { + code: ` +declare function foo(cb: undefined | (() => void)); +declare const bar: undefined | (() => Promise); +foo(bar); + `, + errors: [{ line: 4, messageId: 'voidReturnArgument' }], + }, + { + code: ` +declare function foo(cb: string & (() => void)); +declare const bar: string & (() => Promise); +foo(bar); + `, + errors: [{ line: 4, messageId: 'voidReturnArgument' }], + }, + { + code: ` +function consume(..._callbacks: Array<() => void>): void {} +let cbs: Array<() => Promise> = [ + () => Promise.resolve(true), + () => Promise.resolve(true), +]; +consume(...cbs); + `, + errors: [{ line: 7, messageId: 'voidReturnArgument' }], + }, + { + code: ` +function consume(..._callbacks: Array<() => void>): void {} +let cbs = [() => Promise.resolve(true), () => Promise.resolve(true)] as const; +consume(...cbs); + `, + errors: [{ line: 4, messageId: 'voidReturnArgument' }], + }, + { + code: ` +function consume(..._callbacks: Array<() => void>): void {} +let cbs = [() => Promise.resolve(true), () => Promise.resolve(true)]; +consume(...cbs); + `, + errors: [{ line: 4, messageId: 'voidReturnArgument' }], + }, + { + code: ` +class MyClass { + setThing(): void { + return; + } +} + +class MySubclassExtendsMyClass extends MyClass { + async setThing(): Promise { + await Promise.resolve(); + } +} + `, + errors: [ + { + line: 9, + messageId: 'voidReturnInheritedMethod', + data: { heritageTypeName: 'MyClass' }, + }, + ], + }, + { + code: ` +class MyClass { + setThing(): void { + return; + } +} + +abstract class MyAbstractClassExtendsMyClass extends MyClass { + abstract setThing(): Promise; +} + `, + errors: [ + { + line: 9, + messageId: 'voidReturnInheritedMethod', + data: { heritageTypeName: 'MyClass' }, + }, + ], + }, + { + code: ` +class MyClass { + setThing(): void { + return; + } +} + +interface MyInterfaceExtendsMyClass extends MyClass { + setThing(): Promise; +} + `, + errors: [ + { + line: 9, + messageId: 'voidReturnInheritedMethod', + data: { heritageTypeName: 'MyClass' }, }, ], }, { code: ` -interface ItLike { - (name: string, callback: () => void): void; -} -interface ItLike { - (name: string, callback: () => number): void; +abstract class MyAbstractClass { + abstract setThing(): void; } -declare const it: ItLike; - -it('', async () => {}); +class MySubclassExtendsMyAbstractClass extends MyAbstractClass { + async setThing(): Promise { + await Promise.resolve(); + } +} `, errors: [ { - line: 11, - messageId: 'voidReturnArgument', + line: 7, + messageId: 'voidReturnInheritedMethod', + data: { heritageTypeName: 'MyAbstractClass' }, }, ], }, { code: ` -console.log({ ...Promise.resolve({ key: 42 }) }); +abstract class MyAbstractClass { + abstract setThing(): void; +} + +abstract class MyAbstractSubclassExtendsMyAbstractClass extends MyAbstractClass { + abstract setThing(): Promise; +} `, errors: [ { - line: 2, - messageId: 'spread', + line: 7, + messageId: 'voidReturnInheritedMethod', + data: { heritageTypeName: 'MyAbstractClass' }, }, ], }, { code: ` -const getData = () => Promise.resolve({ key: 42 }); +abstract class MyAbstractClass { + abstract setThing(): void; +} -console.log({ - someData: 42, - ...getData(), -}); +interface MyInterfaceExtendsMyAbstractClass extends MyAbstractClass { + setThing(): Promise; +} `, errors: [ { - line: 6, - messageId: 'spread', + line: 7, + messageId: 'voidReturnInheritedMethod', + data: { heritageTypeName: 'MyAbstractClass' }, }, ], }, { code: ` -declare const condition: boolean; +interface MyInterface { + setThing(): void; +} -console.log({ ...(condition && Promise.resolve({ key: 42 })) }); -console.log({ ...(condition || Promise.resolve({ key: 42 })) }); -console.log({ ...(condition ? {} : Promise.resolve({ key: 42 })) }); -console.log({ ...(condition ? Promise.resolve({ key: 42 }) : {}) }); +class MyInterfaceSubclass implements MyInterface { + async setThing(): Promise { + await Promise.resolve(); + } +} `, errors: [ - { line: 4, messageId: 'spread' }, - { line: 5, messageId: 'spread' }, - { line: 6, messageId: 'spread' }, - { line: 7, messageId: 'spread' }, + { + line: 7, + messageId: 'voidReturnInheritedMethod', + data: { heritageTypeName: 'MyInterface' }, + }, ], }, { code: ` -function restPromises(first: Boolean, ...callbacks: Array<() => void>): void {} +interface MyInterface { + setThing(): void; +} -restPromises( - true, - () => Promise.resolve(true), - () => Promise.resolve(null), - () => true, - () => Promise.resolve('Hello'), -); +abstract class MyAbstractClassImplementsMyInterface implements MyInterface { + abstract setThing(): Promise; +} `, errors: [ - { line: 6, messageId: 'voidReturnArgument' }, - { line: 7, messageId: 'voidReturnArgument' }, - { line: 9, messageId: 'voidReturnArgument' }, + { + line: 7, + messageId: 'voidReturnInheritedMethod', + data: { heritageTypeName: 'MyInterface' }, + }, ], }, { code: ` -type MyUnion = (() => void) | boolean; +interface MyInterface { + setThing(): void; +} -function restUnion(first: string, ...callbacks: Array): void {} -restUnion('Testing', false, () => Promise.resolve(true)); - `, - errors: [{ line: 5, messageId: 'voidReturnArgument' }], - }, - { - code: ` -function restTupleOne(first: string, ...callbacks: [() => void]): void {} -restTupleOne('My string', () => Promise.resolve(1)); +interface MySubInterface extends MyInterface { + setThing(): Promise; +} `, - errors: [{ line: 3, messageId: 'voidReturnArgument' }], + errors: [ + { + line: 7, + messageId: 'voidReturnInheritedMethod', + data: { heritageTypeName: 'MyInterface' }, + }, + ], }, { code: ` -function restTupleTwo( - first: boolean, - ...callbacks: [undefined, () => void, undefined] -): void {} +type MyTypeIntersection = { setThing(): void } & { thing: number }; -restTupleTwo(true, undefined, () => Promise.resolve(true), undefined); +class MyClassImplementsMyTypeIntersection implements MyTypeIntersection { + thing = 1; + async setThing(): Promise { + await Promise.resolve(); + } +} `, - errors: [{ line: 7, messageId: 'voidReturnArgument' }], + errors: [ + { + line: 6, + messageId: 'voidReturnInheritedMethod', + data: { heritageTypeName: 'MyTypeIntersection' }, + }, + ], }, { code: ` -function restTupleFour( - first: number, - ...callbacks: [() => void, boolean, () => void, () => void] -): void; +type MyGenericType = IsAsync extends true + ? { setThing(): Promise } + : { setThing(): void }; -restTupleFour( - 1, - () => Promise.resolve(true), - false, - () => {}, - () => Promise.resolve(1), -); +interface MyAsyncInterface extends MyGenericType { + setThing(): Promise; +} `, errors: [ - { line: 9, messageId: 'voidReturnArgument' }, - { line: 12, messageId: 'voidReturnArgument' }, + { + line: 7, + messageId: 'voidReturnInheritedMethod', + data: { heritageTypeName: '{ setThing(): void; }' }, + }, ], }, { - // Prettier adds a () but this tests arguments being undefined, not [] - // eslint-disable-next-line @typescript-eslint/internal/plugin-test-formatting code: ` -class TakesVoidCb { - constructor(first: string, ...args: Array<() => void>); +interface MyInterface { + setThing(): void; } -new TakesVoidCb; -new TakesVoidCb(); -new TakesVoidCb( - 'Testing', - () => {}, - () => Promise.resolve(true), -); - `, - errors: [{ line: 11, messageId: 'voidReturnArgument' }], - }, - { - code: ` -function restTuple(...args: []): void; -function restTuple(...args: [boolean, () => void]): void; -function restTuple(..._args: any[]): void {} +interface MyOtherInterface { + setThing(): void; +} -restTuple(); -restTuple(true, () => Promise.resolve(1)); +interface MyThirdInterface extends MyInterface, MyOtherInterface { + setThing(): Promise; +} `, - errors: [{ line: 7, messageId: 'voidReturnArgument' }], + errors: [ + { + line: 11, + messageId: 'voidReturnInheritedMethod', + data: { heritageTypeName: 'MyInterface' }, + }, + { + line: 11, + messageId: 'voidReturnInheritedMethod', + data: { heritageTypeName: 'MyOtherInterface' }, + }, + ], }, { code: ` -type ReturnsRecord = () => Record void>; +class MyClass { + setThing(): void { + return; + } +} -const test: ReturnsRecord = () => { - return { asynchronous: async () => {} }; -}; - `, - errors: [{ line: 5, messageId: 'voidReturnProperty' }], - }, - { - code: ` -let value: Record void>; -value.asynchronous = async () => {}; +class MyOtherClass { + setThing(): void { + return; + } +} + +interface MyInterface extends MyClass, MyOtherClass { + setThing(): Promise; +} `, - errors: [{ line: 3, messageId: 'voidReturnVariable' }], + errors: [ + { + line: 15, + messageId: 'voidReturnInheritedMethod', + data: { heritageTypeName: 'MyClass' }, + }, + { + line: 15, + messageId: 'voidReturnInheritedMethod', + data: { heritageTypeName: 'MyOtherClass' }, + }, + ], }, { code: ` -type ReturnsRecord = () => Record void>; +interface MyAsyncInterface { + setThing(): Promise; +} -async function asynchronous() {} +interface MySyncInterface { + setThing(): void; +} -const test: ReturnsRecord = () => { - return { asynchronous }; -}; +class MyClass { + setThing(): void { + return; + } +} + +class MySubclass extends MyClass implements MyAsyncInterface, MySyncInterface { + async setThing(): Promise { + await Promise.resolve(); + } +} `, - errors: [{ line: 7, messageId: 'voidReturnProperty' }], + errors: [ + { + line: 17, + messageId: 'voidReturnInheritedMethod', + data: { heritageTypeName: 'MyClass' }, + }, + { + line: 17, + messageId: 'voidReturnInheritedMethod', + data: { heritageTypeName: 'MySyncInterface' }, + }, + ], }, { code: ` -declare function foo(cb: undefined | (() => void)); -declare const bar: undefined | (() => Promise); -foo(bar); +interface MyInterface { + setThing(): void; +} + +const MyClassExpressionExtendsMyClass = class implements MyInterface { + setThing(): Promise { + await Promise.resolve(); + } +}; `, - errors: [{ line: 4, messageId: 'voidReturnArgument' }], + errors: [ + { + line: 7, + messageId: 'voidReturnInheritedMethod', + data: { heritageTypeName: 'MyInterface' }, + }, + ], }, { code: ` -declare function foo(cb: string & (() => void)); -declare const bar: string & (() => Promise); -foo(bar); +const MyClassExpression = class { + setThing(): void { + return; + } +}; + +class MyClassExtendsMyClassExpression extends MyClassExpression { + async setThing(): Promise { + await Promise.resolve(); + } +} `, - errors: [{ line: 4, messageId: 'voidReturnArgument' }], + errors: [ + { + line: 9, + messageId: 'voidReturnInheritedMethod', + data: { heritageTypeName: 'MyClassExpression' }, + }, + ], }, { code: ` -function consume(..._callbacks: Array<() => void>): void {} -let cbs: Array<() => Promise> = [ - () => Promise.resolve(true), - () => Promise.resolve(true), -]; -consume(...cbs); +const MyClassExpression = class { + setThing(): void { + return; + } +}; +type MyClassExpressionType = typeof MyClassExpression; + +interface MyInterfaceExtendsMyClassExpression extends MyClassExpressionType { + setThing(): Promise; +} `, - errors: [{ line: 7, messageId: 'voidReturnArgument' }], + errors: [ + { + line: 10, + messageId: 'voidReturnInheritedMethod', + data: { heritageTypeName: 'typeof MyClassExpression' }, + }, + ], }, { code: ` -function consume(..._callbacks: Array<() => void>): void {} -let cbs = [() => Promise.resolve(true), () => Promise.resolve(true)] as const; -consume(...cbs); +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: 4, messageId: 'voidReturnArgument' }], + errors: [ + { + line: 16, + messageId: 'voidReturnInheritedMethod', + data: { heritageTypeName: 'MySyncInterface' }, + }, + ], }, { code: ` -function consume(..._callbacks: Array<() => void>): void {} -let cbs = [() => Promise.resolve(true), () => Promise.resolve(true)]; -consume(...cbs); +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: 4, messageId: 'voidReturnArgument' }], + errors: [ + { + line: 29, + messageId: 'voidReturnInheritedMethod', + data: { heritageTypeName: 'MyMethods' }, + }, + { + line: 31, + 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 2ba6191f4645..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,6 +28,9 @@ exports[`Rule schemas should be convertible to TS types for documentation purpos "attributes": { "type": "boolean" }, + "inheritedMethods": { + "type": "boolean" + }, "properties": { "type": "boolean" }, @@ -58,6 +61,7 @@ type Options = [ | { arguments?: boolean; attributes?: boolean; + inheritedMethods?: boolean; properties?: boolean; returns?: boolean; variables?: boolean;