From 5c7cf4a5df7c1016c5c7e6f6594960bd2547e9c2 Mon Sep 17 00:00:00 2001 From: Ryan Lester Date: Sun, 27 Nov 2016 00:19:50 -0500 Subject: [PATCH] Rule: `Promise`-returning methods must be `async` (#1779) --- src/rules/promiseFunctionAsyncRule.ts | 89 +++++++++++++++++++ .../rules/promise-function-async/test.ts.lint | 67 ++++++++++++++ test/rules/promise-function-async/tslint.json | 8 ++ 3 files changed, 164 insertions(+) create mode 100644 src/rules/promiseFunctionAsyncRule.ts create mode 100644 test/rules/promise-function-async/test.ts.lint create mode 100644 test/rules/promise-function-async/tslint.json diff --git a/src/rules/promiseFunctionAsyncRule.ts b/src/rules/promiseFunctionAsyncRule.ts new file mode 100644 index 00000000000..a058b686a44 --- /dev/null +++ b/src/rules/promiseFunctionAsyncRule.ts @@ -0,0 +1,89 @@ +/** + * @license + * Copyright 2016 Palantir Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as ts from "typescript"; +import * as Lint from "../index"; + +export class Rule extends Lint.Rules.TypedRule { + /* tslint:disable:object-literal-sort-keys */ + public static metadata: Lint.IRuleMetadata = { + ruleName: "promise-function-async", + description: "Requires any function or method that returns a promise to be marked async.", + rationale: Lint.Utils.dedent` + Ensures that each function is only capable of 1) returning a rejected promise, or 2) + throwing an Error object. In contrast, non-\`async\` \`Promise\`-returning functions + are technically capable of either. This practice removes a requirement for consuming + code to handle both cases. + `, + optionsDescription: "Not configurable.", + options: null, + optionExamples: ["true"], + type: "typescript", + typescriptOnly: false, + requiresTypeInfo: true, + }; + /* tslint:enable:object-literal-sort-keys */ + + public static FAILURE_STRING = "functions that return promises must be async"; + + public applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] { + return this.applyWithWalker(new PromiseAsyncWalker(sourceFile, this.getOptions(), program)); + } +} + +class PromiseAsyncWalker extends Lint.ProgramAwareRuleWalker { + public visitArrowFunction(node: ts.ArrowFunction) { + this.test(node); + super.visitArrowFunction(node); + } + + public visitFunctionDeclaration(node: ts.FunctionDeclaration) { + this.test(node); + super.visitFunctionDeclaration(node); + } + + public visitFunctionExpression(node: ts.FunctionExpression) { + this.test(node); + super.visitFunctionExpression(node); + } + + public visitMethodDeclaration(node: ts.MethodDeclaration) { + this.test(node); + super.visitMethodDeclaration(node); + } + + private test(node: ts.SignatureDeclaration & { body?: ts.Node}) { + const tc = this.getTypeChecker(); + + const signature = tc.getTypeAtLocation(node).getCallSignatures()[0]; + const returnType = tc.typeToString(tc.getReturnTypeOfSignature(signature)); + + const isAsync = Lint.hasModifier(node.modifiers, ts.SyntaxKind.AsyncKeyword); + const isPromise = returnType.indexOf("Promise<") === 0; + + const signatureEnd = node.body ? + node.body.getStart() - node.getStart() - 1 : + node.getWidth() + ; + + if (isAsync || !isPromise) { + return; + } + + this.addFailure(this.createFailure(node.getStart(), signatureEnd, Rule.FAILURE_STRING)); + } +} diff --git a/test/rules/promise-function-async/test.ts.lint b/test/rules/promise-function-async/test.ts.lint new file mode 100644 index 00000000000..692f314831e --- /dev/null +++ b/test/rules/promise-function-async/test.ts.lint @@ -0,0 +1,67 @@ +declare class Promise{} + +const nonAsyncPromiseFunctionExpressionA = function(p: Promise) { return p; }; + ~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + +const nonAsyncPromiseFunctionExpressionB = function() { return new Promise(); }; + ~~~~~~~~~~ [0] + +// 'async' 'Promise'-returning function expressions are allowed +const asyncPromiseFunctionExpressionA = async function(p: Promise) { return p; }; +const asyncPromiseFunctionExpressionB = async function() { return new Promise(); }; + +// non-'async' non-'Promise'-returning function expressions are allowed +const nonAsyncNonPromiseFunctionExpression = function(n: number) { return n; }; + +function nonAsyncPromiseFunctionDeclarationA(p: Promise) { return p; } +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + +function nonAsyncPromiseFunctionDeclarationB() { return new Promise(); } +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + +// 'async' 'Promise'-returning function declarations are allowed +async function asyncPromiseFunctionDeclarationA(p: Promise) { return p; } +async function asyncPromiseFunctionDeclarationB() { return new Promise(); } + +// non-'async' non-'Promise'-returning function declarations are allowed +function nonAsyncNonPromiseFunctionDeclaration(n: number) { return n; } + +const nonAsyncPromiseArrowFunctionA = (p: Promise) => p; + ~~~~~~~~~~~~~~~~~~~~~ [0] + +const nonAsyncPromiseArrowFunctionB = () => new Promise(); + ~~~~~ [0] + +// 'async' 'Promise'-returning arrow functions are allowed +const asyncPromiseArrowFunctionA = async (p: Promise) => p; +const asyncPromiseArrowFunctionB = async () => new Promise(); + +// non-'async' non-'Promise'-returning arrow functions are allowed +const nonAsyncNonPromiseArrowFunction = (n: number) => n; + +class Test { + public nonAsyncPromiseMethodA(p: Promise) { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + return p; + } + + public nonAsyncPromiseMethodB() { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + return new Promise(); + } + + // 'async' 'Promise'-returning methods are allowed + public async asyncPromiseMethodA(p: Promise) { + return p; + } + public async asyncPromiseMethodB() { + return new Promise(); + } + + // non-'async' non-'Promise'-returning methods are allowed + public nonAsyncNonPromiseMethod(n: number) { + return n; + } +} + +[0]: functions that return promises must be async diff --git a/test/rules/promise-function-async/tslint.json b/test/rules/promise-function-async/tslint.json new file mode 100644 index 00000000000..a5c4617c770 --- /dev/null +++ b/test/rules/promise-function-async/tslint.json @@ -0,0 +1,8 @@ +{ + "linterOptions": { + "typeCheck": true + }, + "rules": { + "promise-function-async": true + } +}