diff --git a/src/configs/all.ts b/src/configs/all.ts index fb98cc7f242..53a9ad9f040 100644 --- a/src/configs/all.ts +++ b/src/configs/all.ts @@ -107,6 +107,7 @@ export const rules = { // "import-blacklist": no sensible default "label-position": true, "no-arg": true, + "no-async-without-await": true, "no-bitwise": true, "no-conditional-assignment": true, "no-console": true, diff --git a/src/rules/code-examples/noAsyncWithoutAwait.examples.ts b/src/rules/code-examples/noAsyncWithoutAwait.examples.ts new file mode 100644 index 00000000000..bf2d6c8b289 --- /dev/null +++ b/src/rules/code-examples/noAsyncWithoutAwait.examples.ts @@ -0,0 +1,51 @@ +/** + * @license + * Copyright 2018 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 Lint from "../../index"; + +export const codeExamples = [ + { + config: Lint.Utils.dedent` + "rules": { "no-async-without-await": true } + `, + description: "Do not use the async keyword if it is not needed", + fail: Lint.Utils.dedent` + async function f() { + fetch(); + } + + async function f() { + async function g() { + await h(); + } + } + `, + pass: Lint.Utils.dedent` + async function f() { + await fetch(); + } + + const f = async () => { + await fetch(); + }; + + const f = async () => { + return 'value'; + }; + `, + }, +]; diff --git a/src/rules/noAsyncWithoutAwaitRule.ts b/src/rules/noAsyncWithoutAwaitRule.ts new file mode 100644 index 00000000000..220c4888aed --- /dev/null +++ b/src/rules/noAsyncWithoutAwaitRule.ts @@ -0,0 +1,140 @@ +/** + * @license + * Copyright 2018 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 tsutils from "tsutils"; +import * as ts from "typescript"; + +import * as Lint from "../index"; + +import { codeExamples } from "./code-examples/noAsyncWithoutAwait.examples"; + +type FunctionNodeType = + | ts.ArrowFunction + | ts.FunctionDeclaration + | ts.MethodDeclaration + | ts.FunctionExpression; + +export class Rule extends Lint.Rules.AbstractRule { + public static FAILURE_STRING = + "Functions marked async must contain an await or return statement."; + + public static metadata: Lint.IRuleMetadata = { + codeExamples, + description: Rule.FAILURE_STRING, + hasFix: false, + optionExamples: [true], + options: null, + optionsDescription: "Not configurable.", + /* tslint:disable:max-line-length */ + rationale: Lint.Utils.dedent` + Marking a function as \`async\` without using \`await\` or returning a value inside it can lead to an unintended promise return and a larger transpiled output. + Often the function can be synchronous and the \`async\` keyword is there by mistake. + Return statements are allowed as sometimes it is desirable to wrap the returned value in a Promise.`, + /* tslint:enable:max-line-length */ + ruleName: "no-async-without-await", + type: "functionality", + typescriptOnly: false, + }; + + public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { + return this.applyWithFunction(sourceFile, walk); + } +} + +function walk(context: Lint.WalkContext) { + const reportFailureIfAsyncFunction = (node: FunctionNodeType) => { + const asyncModifier = getAsyncModifier(node); + if (asyncModifier !== undefined) { + context.addFailureAt( + asyncModifier.getStart(), + asyncModifier.getEnd() - asyncModifier.getStart(), + Rule.FAILURE_STRING, + ); + } + }; + + const addFailureIfAsyncFunctionHasNoAwait = (node: FunctionNodeType) => { + if (node.body === undefined) { + reportFailureIfAsyncFunction(node); + return; + } + + if ( + !isShortArrowReturn(node) && + !functionBlockHasAwait(node.body) && + !functionBlockHasReturn(node.body) + ) { + reportFailureIfAsyncFunction(node); + } + }; + + return ts.forEachChild(context.sourceFile, function visitNode(node): void { + if ( + tsutils.isArrowFunction(node) || + tsutils.isFunctionDeclaration(node) || + tsutils.isFunctionExpression(node) || + tsutils.isMethodDeclaration(node) + ) { + addFailureIfAsyncFunctionHasNoAwait(node); + } + + return ts.forEachChild(node, visitNode); + }); +} + +const getAsyncModifier = (node: ts.Node) => { + if (node.modifiers !== undefined) { + return node.modifiers.find(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword); + } + + return undefined; +}; + +const isReturn = (node: ts.Node): boolean => node.kind === ts.SyntaxKind.ReturnKeyword; + +const functionBlockHasAwait = (node: ts.Node): boolean => { + if (tsutils.isAwaitExpression(node)) { + return true; + } + + if ( + node.kind === ts.SyntaxKind.ArrowFunction || + node.kind === ts.SyntaxKind.FunctionDeclaration + ) { + return false; + } + + return node.getChildren().some(functionBlockHasAwait); +}; + +const functionBlockHasReturn = (node: ts.Node): boolean => { + if (isReturn(node)) { + return true; + } + + if ( + node.kind === ts.SyntaxKind.ArrowFunction || + node.kind === ts.SyntaxKind.FunctionDeclaration + ) { + return false; + } + + return node.getChildren().some(functionBlockHasReturn); +}; + +const isShortArrowReturn = (node: FunctionNodeType) => + node.kind === ts.SyntaxKind.ArrowFunction && node.body.kind !== ts.SyntaxKind.Block; diff --git a/test/rules/no-async-without-await/test.ts.lint b/test/rules/no-async-without-await/test.ts.lint new file mode 100644 index 00000000000..452278893ac --- /dev/null +++ b/test/rules/no-async-without-await/test.ts.lint @@ -0,0 +1,122 @@ +async function a(){ +~~~~~ [0] + let b = 1; + console.log(b); +} + +async function a(){ + let b = 1; + await console.log(b); +} + +async function a(){ + let b = 1; + console.log(await b()); +} + +async function a(){ +~~~~~ [0] + let b = 1; + let c = async () => { + await fetch(); + }; +} + +async function a(){ +~~~~~ [Functions marked async must contain an await or return statement.] + let b = 1; + async function f() { + await fetch(); + }; +} + +function a(){ + let b = 1; + async function f() { + ~~~~~ [Functions marked async must contain an await or return statement.] + fetch(); + }; +} + +const a = async () => { + ~~~~~ [Functions marked async must contain an await or return statement.] + let b = 1; + console.log(b); +} + +class A { + async b() { + ~~~~~ [Functions marked async must contain an await or return statement.] + console.log(1); + } +} + +class A { + async b() { + await b(); + } +} + +class A { + public a = async function b() { + await b(); + } + } + +class A { + public a = async function b() { + ~~~~~ [Functions marked async must contain an await or return statement.] + b(); + } + } + +class A { + public a = async () => { + await b(); + } + } + +class A { + public a = async () => { + ~~~~~ [Functions marked async must contain an await or return statement.] + b(); + } + } + +class A { + public a = async () => 1; + } + +async () => { + await a(); + class A { + async b() { + ~~~~~ [Functions marked async must contain an await or return statement.] + console.log(1); + } + } +}; + +async function a() { + let b = 1; + return b; +} + +let a = async () => 1; + +async function a() { +~~~~~ [Functions marked async must contain an await or return statement.] + let b = 1; + let a = () => { + return 1; + } +} + +async function foo; +~~~~~ [Functions marked async must contain an await or return statement.] + +function * foo() { + return 1; +} + +[0]: Functions marked async must contain an await or return statement. diff --git a/test/rules/no-async-without-await/tslint.json b/test/rules/no-async-without-await/tslint.json new file mode 100644 index 00000000000..140ce628454 --- /dev/null +++ b/test/rules/no-async-without-await/tslint.json @@ -0,0 +1,5 @@ +{ + "rules": { + "no-async-without-await": true + } +}