diff --git a/src/rules/noFloatingPromisesRule.ts b/src/rules/noFloatingPromisesRule.ts new file mode 100644 index 00000000000..b5612ace3f2 --- /dev/null +++ b/src/rules/noFloatingPromisesRule.ts @@ -0,0 +1,104 @@ +/** + * @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: "no-floating-promises", + description: "Promises returned by functions must be handled appropriately.", + optionsDescription: Lint.Utils.dedent` + A list of \'string\' names of any additional classes that should also be handled as Promises. + `, + options: { + type: "list", + listType: { + type: "array", + items: {type: "string"}, + }, + }, + optionExamples: ["true", `[true, "JQueryPromise"]`], + rationale: "Unhandled Promises can cause unexpected behavior, such as resolving at unexpected times.", + type: "functionality", + typescriptOnly: false, + }; + /* tslint:enable:object-literal-sort-keys */ + + public static FAILURE_STRING = "Promises must be handled appropriately"; + + public applyWithProgram(sourceFile: ts.SourceFile, langSvc: ts.LanguageService): Lint.RuleFailure[] { + const walker = new NoFloatingPromisesWalker(sourceFile, this.getOptions(), langSvc.getProgram()); + + for (const className of this.getOptions().ruleArguments) { + walker.addPromiseClass(className); + } + + return this.applyWithWalker(walker); + } +} + +class NoFloatingPromisesWalker extends Lint.ProgramAwareRuleWalker { + private static barredParentKinds: { [x: number]: boolean } = { + [ts.SyntaxKind.Block]: true, + [ts.SyntaxKind.ExpressionStatement]: true, + [ts.SyntaxKind.SourceFile]: true, + }; + + private promiseClasses = ["Promise"]; + + public addPromiseClass(className: string) { + this.promiseClasses.push(className); + } + + public visitCallExpression(node: ts.CallExpression): void { + this.checkNode(node); + super.visitCallExpression(node); + } + + public visitExpressionStatement(node: ts.ExpressionStatement): void { + this.checkNode(node); + super.visitExpressionStatement(node); + } + + private checkNode(node: ts.CallExpression | ts.ExpressionStatement) { + if (node.parent && this.kindCanContainPromise(node.parent.kind)) { + return; + } + + const typeChecker = this.getTypeChecker(); + const type = typeChecker.getTypeAtLocation(node); + + if (this.symbolIsPromise(type.symbol)) { + this.addFailure(this.createFailure(node.getStart(), node.getWidth(), Rule.FAILURE_STRING)); + } + } + + private symbolIsPromise(symbol?: ts.Symbol) { + if (!symbol) { + return false; + } + + return this.promiseClasses.indexOf(symbol.name) !== -1; + } + + private kindCanContainPromise(kind: ts.SyntaxKind) { + return !NoFloatingPromisesWalker.barredParentKinds[kind]; + } +} diff --git a/test/rules/no-floating-promises/jquerypromise/test.ts.lint b/test/rules/no-floating-promises/jquerypromise/test.ts.lint new file mode 100644 index 00000000000..4d6096e18c1 --- /dev/null +++ b/test/rules/no-floating-promises/jquerypromise/test.ts.lint @@ -0,0 +1,17 @@ +class Promise { } +class JQueryPromise { } +class NotAPromise { } + +const returnsPromise = () => new Promise(); +const returnsJQueryPromise = () => new JQueryPromise(); +const returnsNotAPromise = () => new NotAPromise(); + +returnsPromise(); +~~~~~~~~~~~~~~~~ [0] + +returnsJQueryPromise(); +~~~~~~~~~~~~~~~~~~~~~~ [0] + +returnsNotAPromise(); + +[0]: Promises must be handled appropriately diff --git a/test/rules/no-floating-promises/jquerypromise/tslint.json b/test/rules/no-floating-promises/jquerypromise/tslint.json new file mode 100644 index 00000000000..f207ba50df6 --- /dev/null +++ b/test/rules/no-floating-promises/jquerypromise/tslint.json @@ -0,0 +1,8 @@ +{ + "linterOptions": { + "typeCheck": true + }, + "rules": { + "no-floating-promises": [true, "JQueryPromise"] + } +} diff --git a/test/rules/no-floating-promises/promises/test.ts.lint b/test/rules/no-floating-promises/promises/test.ts.lint new file mode 100644 index 00000000000..baeb2b5c3dd --- /dev/null +++ b/test/rules/no-floating-promises/promises/test.ts.lint @@ -0,0 +1,141 @@ +class Promise { + then(): Promise; +} + +function returnsPromiseFunction() { + return new Promise(); +} + +const returnsPromiseVariable = () => new Promise(); + +class ReturnsPromiseClass { + returnsPromiseMemberFunction() { + return new Promise(); + } + + returnsPromiseMemberVariable = () => new Promise(); + + static returnsPromiseStaticFunction = () => new Promise(); +} + +let a = returnsPromiseFunction(); +let b = returnsPromiseVariable(); +let c = new ReturnsPromiseClass().returnsPromiseMemberFunction(); +let d = new ReturnsPromiseClass().returnsPromiseMemberVariable(); +let e = ReturnsPromiseClass.returnsPromiseStaticFunction(); + +returnsPromiseFunction(); +~~~~~~~~~~~~~~~~~~~~~~~~ [0] + +returnsPromiseFunction().then(); +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + +returnsPromiseVariable(); +~~~~~~~~~~~~~~~~~~~~~~~~ [0] + +returnsPromiseVariable().then(); +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + +new ReturnsPromiseClass().returnsPromiseMemberFunction(); +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + +new ReturnsPromiseClass().returnsPromiseMemberFunction().then(); +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + +new ReturnsPromiseClass().returnsPromiseMemberVariable(); +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + +new ReturnsPromiseClass().returnsPromiseMemberVariable().then(); +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + +ReturnsPromiseClass.returnsPromiseStaticFunction(); +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + +ReturnsPromiseClass.returnsPromiseStaticFunction().then(); +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + +(function () { + let a = returnsPromiseFunction(); + + returnsPromiseFunction(); + ~~~~~~~~~~~~~~~~~~~~~~~~ [0] + + returnsPromiseFunction().then(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + + new ReturnsPromiseClass().returnsPromiseMemberFunction(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + + new ReturnsPromiseClass().returnsPromiseMemberFunction().then(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + + new ReturnsPromiseClass().returnsPromiseMemberVariable(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + + new ReturnsPromiseClass().returnsPromiseMemberVariable().then(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + + ReturnsPromiseClass.returnsPromiseStaticFunction(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + + ReturnsPromiseClass.returnsPromiseStaticFunction().then(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] +})(); + +(() => { + let a = returnsPromiseFunction(); + + returnsPromiseFunction(); + ~~~~~~~~~~~~~~~~~~~~~~~~ [0] + + returnsPromiseFunction().then(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + + new ReturnsPromiseClass().returnsPromiseMemberFunction(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + + new ReturnsPromiseClass().returnsPromiseMemberFunction().then(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + + new ReturnsPromiseClass().returnsPromiseMemberVariable(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + + new ReturnsPromiseClass().returnsPromiseMemberVariable().then(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + + ReturnsPromiseClass.returnsPromiseStaticFunction(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + + ReturnsPromiseClass.returnsPromiseStaticFunction().then(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] +})(); + +[].push(returnsPromiseFunction()); + +[].push(returnsPromiseFunction().then()); + +[].push(ReturnsPromiseClass.returnsPromiseStaticFunction()); + +[].push(ReturnsPromiseClass.returnsPromiseStaticFunction().then()); + +while (returnsPromiseFunction()); + +while (returnsPromiseFunction().then()); + +for (let i = 0; i < returnsPromiseFunction(); i += 1); + +for (let i = 0; i < returnsPromiseFunction().then(); i += 1); + +for (let i in returnsPromiseFunction()); + +for (let i in returnsPromiseFunction().then()); + +for (const promise of returnsPromiseFunction()); + +for (const promise of returnsPromiseFunction().then()); + +let promise = Math.random() > .5 + ? returnsPromiseFunction() + : returnsPromiseFunction().then(); + +[0]: Promises must be handled appropriately \ No newline at end of file diff --git a/test/rules/no-floating-promises/promises/tslint.json b/test/rules/no-floating-promises/promises/tslint.json new file mode 100644 index 00000000000..2f3715da4c5 --- /dev/null +++ b/test/rules/no-floating-promises/promises/tslint.json @@ -0,0 +1,8 @@ +{ + "linterOptions": { + "typeCheck": true + }, + "rules": { + "no-floating-promises": true + } +} \ No newline at end of file