diff --git a/src/rules/promiseFunctionAsyncRule.ts b/src/rules/promiseFunctionAsyncRule.ts index 60f1fa9a607..00129f28fee 100644 --- a/src/rules/promiseFunctionAsyncRule.ts +++ b/src/rules/promiseFunctionAsyncRule.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { hasModifier, isCallExpression } from "tsutils"; +import * as tsutils from "tsutils"; import * as ts from "typescript"; import * as Lint from "../index"; @@ -102,34 +102,41 @@ export class Rule extends Lint.Rules.TypedRule { function walk(ctx: Lint.WalkContext, tc: ts.TypeChecker) { const { sourceFile, options } = ctx; return ts.forEachChild(sourceFile, function cb(node): void { - if (options.has(node.kind)) { - const declaration = node as ts.FunctionLikeDeclaration; - switch (node.kind) { - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.FunctionDeclaration: - if (declaration.body === undefined) { - break; - } - // falls through - case ts.SyntaxKind.FunctionExpression: - case ts.SyntaxKind.ArrowFunction: - if ( - !hasModifier(node.modifiers, ts.SyntaxKind.AsyncKeyword) && - returnsPromise(declaration, tc) && - !isCallExpression(declaration.body as ts.Expression) - ) { - ctx.addFailure( - node.getStart(sourceFile), - (node as ts.FunctionLikeDeclaration).body!.pos, - Rule.FAILURE_STRING, - ); - } + if (options.has(node.kind) && isFunctionLikeWithBody(node)) { + if ( + !tsutils.hasModifier(node.modifiers, ts.SyntaxKind.AsyncKeyword) && + !isCallExpressionBody(node.body) && + returnsPromise(node, tc) + ) { + ctx.addFailure(node.getStart(sourceFile), node.body.pos, Rule.FAILURE_STRING); } } return ts.forEachChild(node, cb); }); } +function isFunctionLikeWithBody( + node: ts.Node, +): node is ts.FunctionLikeDeclaration & { body: ts.Node } { + switch (node.kind) { + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.FunctionExpression: + case ts.SyntaxKind.ArrowFunction: + return (node as ts.FunctionLikeDeclaration).body !== undefined; + } + + return false; +} + +function isCallExpressionBody(body: ts.Node) { + while (tsutils.isParenthesizedExpression(body)) { + body = body.expression; + } + + return tsutils.isCallExpression(body); +} + function returnsPromise(node: ts.FunctionLikeDeclaration, tc: ts.TypeChecker): boolean { const type = tc.getReturnTypeOfSignature(tc.getTypeAtLocation(node).getCallSignatures()[0]); return type.symbol !== undefined && type.symbol.name === "Promise"; diff --git a/test/rules/promise-function-async/test.ts.lint b/test/rules/promise-function-async/test.ts.lint index f3bc9e6bec6..c85dc8c7139 100644 --- a/test/rules/promise-function-async/test.ts.lint +++ b/test/rules/promise-function-async/test.ts.lint @@ -38,6 +38,8 @@ const asyncPromiseArrowFunctionB = async () => new Promise(); // non-'async' non-'Promise'-returning arrow functions are allowed const nonAsyncNonPromiseArrowFunction = (n: number) => n; +const nonAsyncNonPromiseArrowFunctionParenthesisOne = (n: number) => (n); +const nonAsyncNonPromiseArrowFunctionParenthesisTwo = (n: number) => ((n)); class Test { public nonAsyncPromiseMethodA(p: Promise) { @@ -65,6 +67,8 @@ class Test { // single statement lamda functions that delegate to another promise-returning function are allowed public delegatingMethod = () => this.asyncPromiseMethodB(1); + public delegatingMethodParenthesisOne = () => (this.asyncPromiseMethodB(1)); + public delegatingMethodParenthesisTwo = () => ((this.asyncPromiseMethodB(1))); } [0]: functions that return promises must be async