From 5f821cf4855a65cef537d7b2edca055d261dc60f Mon Sep 17 00:00:00 2001 From: Michael Giambalvo Date: Thu, 25 Jan 2018 14:37:37 -0800 Subject: [PATCH] Add isPromiseType. Returns true if a type is a union type that includes Promise, or a type that extends Promise. --- test/rules/promise-use/promise.ts.lint | 42 ++++++++++++++++++++ test/rules/promise-use/testPromiseUseRule.ts | 37 +++++++++++++++++ test/rules/promise-use/tsconfig.json | 26 ++++++++++++ test/rules/promise-use/tslint.json | 8 ++++ util/type.ts | 19 +++++++++ 5 files changed, 132 insertions(+) create mode 100644 test/rules/promise-use/promise.ts.lint create mode 100644 test/rules/promise-use/testPromiseUseRule.ts create mode 100644 test/rules/promise-use/tsconfig.json create mode 100644 test/rules/promise-use/tslint.json diff --git a/test/rules/promise-use/promise.ts.lint b/test/rules/promise-use/promise.ts.lint new file mode 100644 index 0000000..ded5521 --- /dev/null +++ b/test/rules/promise-use/promise.ts.lint @@ -0,0 +1,42 @@ +function returnsPromise() { + return Promise.resolve(true); +} + +type Future = Promise; +function aliasedPromise(): Future { + return Promise.resolve(1); +} + +class Extended extends Promise {} +function extendedPromise(): Extended { + return Promise.resolve(1); +} + +class DoubleExtended extends Extended {} +function doubleExtendedPromise(): Extended { + return Promise.resolve(1); +} + +function maybePromise(): Promise|number { + return 3; +} + +async function returnLaterPromise() { + return () => Promise.resolve(1); +} + +function unusedPromises() { + returnsPromise(); + ~~~~~~~~~~~~~~~~ [Promise] + maybePromise(); + ~~~~~~~~~~~~~~ [Promise] + aliasedPromise(); + ~~~~~~~~~~~~~~~~ [Promise] + extendedPromise(); + ~~~~~~~~~~~~~~~~~ [Promise] + doubleExtendedPromise(); + ~~~~~~~~~~~~~~~~~~~~~~~ [Promise] + const later = await returnLaterPromise(); + later(); + ~~~~~~~ [Promise] +} diff --git a/test/rules/promise-use/testPromiseUseRule.ts b/test/rules/promise-use/testPromiseUseRule.ts new file mode 100644 index 0000000..3a0101c --- /dev/null +++ b/test/rules/promise-use/testPromiseUseRule.ts @@ -0,0 +1,37 @@ +import * as ts from 'typescript'; +import * as Lint from 'tslint'; +import { isCallExpression } from "../../../typeguard/node"; +import { isExpressionValueUsed } from "../../../util/util"; +import { isPromiseType } from '../../../util/type'; + +export class Rule extends Lint.Rules.TypedRule { + applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] { + return this.applyWithFunction(sourceFile, (ctx) => walk(ctx, program)); + } +} + +function walk(ctx: Lint.WalkContext, program: ts.Program) { + const cb = (node: ts.Node): void => { + if (isCallExpression(node)) { + if (!isExpressionValueUsed(node) && isPromiseExpression(node, program)) { + ctx.addFailureAtNode(node, 'Promise'); + } + } + return ts.forEachChild(node, cb); + }; + return ts.forEachChild(ctx.sourceFile, cb); +} + +function isPromiseExpression(node: ts.CallExpression, program: ts.Program) { + const checker = program.getTypeChecker(); + const signature = checker.getResolvedSignature(node); + if (signature === undefined) { + return false; + } + const returnType = checker.getReturnTypeOfSignature(signature); + if (!!(returnType.flags & ts.TypeFlags.Void)) { + return false; + } + + return isPromiseType(returnType); +} diff --git a/test/rules/promise-use/tsconfig.json b/test/rules/promise-use/tsconfig.json new file mode 100644 index 0000000..5ce8453 --- /dev/null +++ b/test/rules/promise-use/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "es6", + "module": "commonjs", + "removeComments": true, + "inlineSourceMap": true, + "preserveConstEnums": true, + "noImplicitAny": true, + "noImplicitThis": true, + "suppressImplicitAnyIndexErrors": true, + "strictFunctionTypes": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "strictNullChecks": true, + "stripInternal": true, + "lib": ["es2016"], + "skipLibCheck": true, + "declaration": true, + "importHelpers": true + }, + "exclude": [ + ], + "files": [ + "../../type/promise.ts" + ] +} diff --git a/test/rules/promise-use/tslint.json b/test/rules/promise-use/tslint.json new file mode 100644 index 0000000..253f80e --- /dev/null +++ b/test/rules/promise-use/tslint.json @@ -0,0 +1,8 @@ +{ + "rulesDirectory": [ + "." + ], + "rules":{ + "test-promise-use": true + } +} diff --git a/util/type.ts b/util/type.ts index 47bb5d6..997dce5 100644 --- a/util/type.ts +++ b/util/type.ts @@ -67,6 +67,25 @@ function isTypeAssignableTo(checker: ts.TypeChecker, type: ts.Type, flags: ts.Ty })(type); } +/** + * Returns true if the given type is a union type that includes Promise or a type that extends + * Promise. + */ +export function isPromiseType(type: ts.Type): boolean { + const isPromise = (t: ts.Type) => { + const sym = t.getSymbol(); + if (sym !== undefined) return sym.name === 'Promise'; + return false; + }; + + const baseTypes = type.getBaseTypes(); + if (baseTypes && baseTypes.some(isPromise)) return true; + + if (isUnionType(type) || isIntersectionType(type)) return type.types.some(isPromise); + + return isPromise(type); +} + export function getCallSignaturesOfType(type: ts.Type): ts.Signature[] { if (isUnionType(type)) { const signatures = [];