diff --git a/packages/eslint-plugin/src/rules/no-misused-promises.ts b/packages/eslint-plugin/src/rules/no-misused-promises.ts index 6e42e5474361..7f08402334cc 100644 --- a/packages/eslint-plugin/src/rules/no-misused-promises.ts +++ b/packages/eslint-plugin/src/rules/no-misused-promises.ts @@ -11,7 +11,15 @@ type Options = [ }, ]; -export default util.createRule({ +type MessageId = + | 'conditional' + | 'voidReturnArgument' + | 'voidReturnVariable' + | 'voidReturnProperty' + | 'voidReturnReturnValue' + | 'voidReturnAttribute'; + +export default util.createRule({ name: 'no-misused-promises', meta: { docs: { @@ -20,8 +28,16 @@ export default util.createRule({ requiresTypeChecking: true, }, messages: { - voidReturn: + 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.', + 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.', conditional: 'Expected non-Promise value in a boolean conditional.', }, schema: [ @@ -67,6 +83,11 @@ export default util.createRule({ const voidReturnChecks: TSESLint.RuleListener = { CallExpression: checkArguments, NewExpression: checkArguments, + AssignmentExpression: checkAssignment, + VariableDeclarator: checkVariableDeclaration, + Property: checkProperty, + ReturnStatement: checkReturnStatement, + JSXAttribute: checkJSXAttribute, }; function checkTestConditional(node: { @@ -130,13 +151,168 @@ export default util.createRule({ const tsNode = parserServices.esTreeNodeToTSNodeMap.get(argument); if (returnsThenable(checker, tsNode as ts.Expression)) { context.report({ - messageId: 'voidReturn', + messageId: 'voidReturnArgument', node: argument, }); } } } + function checkAssignment(node: TSESTree.AssignmentExpression): void { + const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node); + const varType = checker.getTypeAtLocation(tsNode.left); + if (!isVoidReturningFunctionType(checker, tsNode.left, varType)) { + return; + } + + if (returnsThenable(checker, tsNode.right)) { + context.report({ + messageId: 'voidReturnVariable', + node: node.right, + }); + } + } + + function checkVariableDeclaration(node: TSESTree.VariableDeclarator): void { + const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node); + if (tsNode.initializer === undefined || node.init === null) { + return; + } + const varType = checker.getTypeAtLocation(tsNode.name); + if (!isVoidReturningFunctionType(checker, tsNode.initializer, varType)) { + return; + } + + if (returnsThenable(checker, tsNode.initializer)) { + context.report({ + messageId: 'voidReturnVariable', + node: node.init, + }); + } + } + + function checkProperty(node: TSESTree.Property): void { + const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node); + if (ts.isPropertyAssignment(tsNode)) { + const contextualType = checker.getContextualType(tsNode.initializer); + if ( + contextualType !== undefined && + isVoidReturningFunctionType( + checker, + tsNode.initializer, + contextualType, + ) && + returnsThenable(checker, tsNode.initializer) + ) { + context.report({ + messageId: 'voidReturnProperty', + node: node.value, + }); + } + } else if (ts.isShorthandPropertyAssignment(tsNode)) { + const contextualType = checker.getContextualType(tsNode.name); + if ( + contextualType !== undefined && + isVoidReturningFunctionType(checker, tsNode.name, contextualType) && + returnsThenable(checker, tsNode.name) + ) { + context.report({ + messageId: 'voidReturnProperty', + node: node.value, + }); + } + } else if (ts.isMethodDeclaration(tsNode)) { + if (ts.isComputedPropertyName(tsNode.name)) { + return; + } + const obj = tsNode.parent; + + // Below condition isn't satisfied unless something goes wrong, + // but is needed for type checking. + // 'node' does not include class method declaration so 'obj' is + // always an object literal expression, but after converting 'node' + // to TypeScript AST, its type includes MethodDeclaration which + // does include the case of class method declaration. + if (!ts.isObjectLiteralExpression(obj)) { + return; + } + + const objType = checker.getContextualType(obj); + if (objType === undefined) { + return; + } + const propertySymbol = checker.getPropertyOfType( + objType, + tsNode.name.text, + ); + if (propertySymbol === undefined) { + return; + } + + const contextualType = checker.getTypeOfSymbolAtLocation( + propertySymbol, + tsNode.name, + ); + + if ( + isVoidReturningFunctionType(checker, tsNode.name, contextualType) && + returnsThenable(checker, tsNode) + ) { + context.report({ + messageId: 'voidReturnProperty', + node: node.value, + }); + } + return; + } + } + + function checkReturnStatement(node: TSESTree.ReturnStatement): void { + const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node); + if (tsNode.expression === undefined || node.argument === null) { + return; + } + const contextualType = checker.getContextualType(tsNode.expression); + if ( + contextualType !== undefined && + isVoidReturningFunctionType( + checker, + tsNode.expression, + contextualType, + ) && + returnsThenable(checker, tsNode.expression) + ) { + context.report({ + messageId: 'voidReturnReturnValue', + node: node.argument, + }); + } + } + + function checkJSXAttribute(node: TSESTree.JSXAttribute): void { + const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node); + const value = tsNode.initializer; + if ( + node.value === null || + value === undefined || + !ts.isJsxExpression(value) || + value.expression === undefined + ) { + return; + } + const contextualType = checker.getContextualType(value); + if ( + contextualType !== undefined && + isVoidReturningFunctionType(checker, value, contextualType) && + returnsThenable(checker, value.expression) + ) { + context.report({ + messageId: 'voidReturnAttribute', + node: node.value, + }); + } + } + return { ...(checksConditionals ? conditionalChecks : {}), ...(checksVoidReturn ? voidReturnChecks : {}), @@ -219,7 +395,6 @@ function voidFunctionParams( node: ts.CallExpression | ts.NewExpression, ): Set { const voidReturnIndices = new Set(); - const thenableReturnIndices = new Set(); const type = checker.getTypeAtLocation(node.expression); for (const subType of tsutils.unionTypeParts(type)) { @@ -233,36 +408,41 @@ function voidFunctionParams( parameter, node.expression, ); - for (const subType of tsutils.unionTypeParts(type)) { - for (const signature of subType.getCallSignatures()) { - const returnType = signature.getReturnType(); - if (tsutils.isTypeFlagSet(returnType, ts.TypeFlags.Void)) { - voidReturnIndices.add(index); - } else if ( - tsutils.isThenableType(checker, node.expression, returnType) - ) { - thenableReturnIndices.add(index); - } - } + if (isVoidReturningFunctionType(checker, node.expression, type)) { + voidReturnIndices.add(index); } } } } - // If a certain positional argument accepts both thenable and void returns, - // a promise-returning function is valid - for (const thenable of thenableReturnIndices) { - voidReturnIndices.delete(thenable); - } - return voidReturnIndices; } -// Returns true if the expression is a function that returns a thenable -function returnsThenable( +// Returns true if given type is a void-returning function. +function isVoidReturningFunctionType( checker: ts.TypeChecker, - node: ts.Expression, + node: ts.Node, + type: ts.Type, ): boolean { + let hasVoidReturningFunction = false; + let hasThenableReturningFunction = false; + for (const subType of tsutils.unionTypeParts(type)) { + for (const signature of subType.getCallSignatures()) { + const returnType = signature.getReturnType(); + if (tsutils.isTypeFlagSet(returnType, ts.TypeFlags.Void)) { + hasVoidReturningFunction = true; + } else if (tsutils.isThenableType(checker, node, returnType)) { + hasThenableReturningFunction = true; + } + } + } + // If a certain positional argument accepts both thenable and void returns, + // a promise-returning function is valid + return hasVoidReturningFunction && !hasThenableReturningFunction; +} + +// Returns true if the expression is a function that returns a thenable +function returnsThenable(checker: ts.TypeChecker, node: ts.Node): boolean { const type = checker.getApparentType(checker.getTypeAtLocation(node)); for (const subType of tsutils.unionTypeParts(type)) { 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 63e429000c03..961a21d4c892 100644 --- a/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts @@ -166,6 +166,92 @@ async function test(p: Promise | undefined) { } } `, + ` +let f; +f = async () => 10; + `, + ` +let f: () => Promise; +f = async () => 10; +const g = async () => 0; +const h: () => Promise = async () => 10; + `, + ` +const obj = { + f: async () => 10, +}; + `, + ` +const f = async () => 123; +const obj = { + f, +}; + `, + ` +const obj = { + async f() { + return 0; + }, +}; + `, + ` +type O = { f: () => Promise; g: () => Promise }; +const g = async () => 0; +const obj: O = { + f: async () => 10, + g, +}; + `, + ` +type O = { f: () => Promise }; +const name = 'f'; +const obj: O = { + async [name]() { + return 10; + }, +}; + `, + ` +const obj: number = { + g() { + return 10; + }, +}; + `, + ` +const obj = { + f: async () => 'foo', + async g() { + return 0; + }, +}; + `, + ` +function f() { + return async () => 0; +} +function g() { + return; +} + `, + { + code: ` +type O = { + bool: boolean; + func: () => Promise; +}; +const Component = (obj: O) => null; + 10} />; + `, + filename: 'react.tsx', + }, + { + code: ` +const Component: any = () => null; + 10} />; + `, + filename: 'react.tsx', + }, ], invalid: [ @@ -265,7 +351,7 @@ if (!Promise.resolve()) { errors: [ { line: 2, - messageId: 'voidReturn', + messageId: 'voidReturnArgument', }, ], }, @@ -279,7 +365,7 @@ new Promise(async (resolve, reject) => { errors: [ { line: 2, - messageId: 'voidReturn', + messageId: 'voidReturnArgument', }, ], }, @@ -296,7 +382,7 @@ fnWithCallback('val', async (err, res) => { errors: [ { line: 6, - messageId: 'voidReturn', + messageId: 'voidReturnArgument', }, ], }, @@ -311,7 +397,7 @@ fnWithCallback('val', (err, res) => Promise.resolve(res)); errors: [ { line: 6, - messageId: 'voidReturn', + messageId: 'voidReturnArgument', }, ], }, @@ -332,7 +418,7 @@ fnWithCallback('val', (err, res) => { errors: [ { line: 6, - messageId: 'voidReturn', + messageId: 'voidReturnArgument', }, ], }, @@ -349,7 +435,7 @@ fnWithCallback?.('val', (err, res) => Promise.resolve(res)); errors: [ { line: 8, - messageId: 'voidReturn', + messageId: 'voidReturnArgument', }, ], }, @@ -372,7 +458,7 @@ fnWithCallback('val', (err, res) => { errors: [ { line: 8, - messageId: 'voidReturn', + messageId: 'voidReturnArgument', }, ], }, @@ -432,5 +518,174 @@ function test(p: Promise | undefined) { }, ], }, + { + code: ` +let f: () => void; +f = async () => { + return 3; +}; + `, + errors: [ + { + line: 3, + messageId: 'voidReturnVariable', + }, + ], + }, + { + 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 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: ` +type O = { + func: () => void; +}; +const Component = (obj: O) => null; + 0} />; + `, + filename: 'react.tsx', + errors: [ + { + line: 6, + messageId: 'voidReturnAttribute', + }, + ], + }, + { + code: ` +type O = { + func: () => void; +}; +const g = async () => 'foo'; +const Component = (obj: O) => null; +; + `, + filename: 'react.tsx', + errors: [ + { + line: 7, + messageId: 'voidReturnAttribute', + }, + ], + }, ], }); diff --git a/packages/website/src/components/OptionsSelector.tsx b/packages/website/src/components/OptionsSelector.tsx index 83715fc3ae5c..86c842965073 100644 --- a/packages/website/src/components/OptionsSelector.tsx +++ b/packages/website/src/components/OptionsSelector.tsx @@ -74,17 +74,21 @@ function OptionsSelector({ [setState], ); - const copyLinkToClipboard = useCallback(async () => { - await navigator.clipboard.writeText(document.location.toString()); - setCopyLink(true); + const copyLinkToClipboard = useCallback(() => { + void navigator.clipboard + .writeText(document.location.toString()) + .then(() => { + setCopyLink(true); + }); }, []); - const copyMarkdownToClipboard = useCallback(async () => { + const copyMarkdownToClipboard = useCallback(() => { if (isLoading) { return; } - await navigator.clipboard.writeText(createMarkdown(state)); - setCopyMarkdown(true); + void navigator.clipboard.writeText(createMarkdown(state)).then(() => { + setCopyMarkdown(true); + }); }, [state, isLoading]); const openIssue = useCallback(() => { 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