Skip to content
This repository has been archived by the owner on Mar 25, 2021. It is now read-only.

await-promise: check for-await-of #3297

Merged
merged 6 commits into from
Oct 20, 2017
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
39 changes: 23 additions & 16 deletions src/rules/awaitPromiseRule.ts
Expand Up @@ -15,7 +15,7 @@
* limitations under the License.
*/

import { isAwaitExpression, isUnionOrIntersectionType } from "tsutils";
import { isAwaitExpression, isForOfStatement, isTypeReference, isUnionOrIntersectionType } from "tsutils";
import * as ts from "typescript";
import * as Lint from "../index";

Expand All @@ -42,6 +42,7 @@ export class Rule extends Lint.Rules.TypedRule {
/* tslint:enable:object-literal-sort-keys */

public static FAILURE_STRING = "'await' of non-Promise.";
public static FAILURE_FOR_AWAIT_OF = "'for-await-of' of non-AsyncIterable.";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realized both of these error messages are a bit too terse -- how about

  • "Invalid 'await' of a non-Promise value"
  • "Invalid 'for-await-of' of a non-AsyncIterable value"


public applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] {
const promiseTypes = new Set(["Promise", ...this.ruleArguments as string[]]);
Expand All @@ -54,27 +55,33 @@ function walk(ctx: Lint.WalkContext<Set<string>>, tc: ts.TypeChecker) {
return ts.forEachChild(ctx.sourceFile, cb);

function cb(node: ts.Node): void {
if (isAwaitExpression(node) && !couldBePromise(tc.getTypeAtLocation(node.expression))) {
if (isAwaitExpression(node) && !containsType(tc.getTypeAtLocation(node.expression), isPromiseType)) {
ctx.addFailureAtNode(node, Rule.FAILURE_STRING);
} else if (isForOfStatement(node) && node.awaitModifier !== undefined &&
!containsType(tc.getTypeAtLocation(node.expression), isAsyncIterable)) {
ctx.addFailureAtNode(node.expression, Rule.FAILURE_FOR_AWAIT_OF);
}
return ts.forEachChild(node, cb);
}

function couldBePromise(type: ts.Type): boolean {
if (Lint.isTypeFlagSet(type, ts.TypeFlags.Any) || isPromiseType(type)) {
return true;
}

if (isUnionOrIntersectionType(type)) {
return type.types.some(couldBePromise);
}

const bases = type.getBaseTypes();
return bases !== undefined && bases.some(couldBePromise);
function isPromiseType(name: string) {
return promiseTypes.has(name);
}
}

function isPromiseType(type: ts.Type): boolean {
const { target } = type as ts.TypeReference;
return target !== undefined && target.symbol !== undefined && promiseTypes.has(target.symbol.name);
function containsType(type: ts.Type, predicate: (name: string) => boolean): boolean {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems like a utility we might want in language/utils.ts, what do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. We also use a similar function in return-undefined and probably in other rules, too.
We need to find a better abstraction than the one used here to make it work for other cases. Will do that in a follow-up PR.

if (Lint.isTypeFlagSet(type, ts.TypeFlags.Any) ||
isTypeReference(type) && type.target.symbol !== undefined && predicate(type.target.symbol.name)) {
return true;
}
if (isUnionOrIntersectionType(type)) {
return type.types.some((t) => containsType(t, predicate));
}

const bases = type.getBaseTypes();
return bases !== undefined && bases.some((t) => containsType(t, predicate));
}

function isAsyncIterable(name: string) {
return name === "AsyncIterable" || name === "AsyncIterableIterator";
}
12 changes: 6 additions & 6 deletions src/test.ts
Expand Up @@ -119,14 +119,14 @@ export function runTest(testDirectory: string, rulesDirectory?: string | string[
getDefaultLibFileName: () => ts.getDefaultLibFileName(compilerOptions),
getDirectories: (dir) => fs.readdirSync(dir),
getNewLine: () => "\n",
getSourceFile(filenameToGet) {
const target = compilerOptions.target === undefined ? ts.ScriptTarget.ES5 : compilerOptions.target;
if (filenameToGet === ts.getDefaultLibFileName(compilerOptions)) {
const fileContent = fs.readFileSync(ts.getDefaultLibFilePath(compilerOptions), "utf8");
return ts.createSourceFile(filenameToGet, fileContent, target);
} else if (denormalizeWinPath(filenameToGet) === fileCompileName) {
getSourceFile(filenameToGet, target) {
if (denormalizeWinPath(filenameToGet) === fileCompileName) {
return ts.createSourceFile(filenameToGet, fileTextWithoutMarkup, target, true);
}
if (path.basename(filenameToGet) === filenameToGet) {
// resolve path of lib.xxx.d.ts
filenameToGet = path.join(path.dirname(ts.getDefaultLibFilePath(compilerOptions)), filenameToGet);
}
const text = fs.readFileSync(filenameToGet, "utf8");
return ts.createSourceFile(filenameToGet, text, target, true);
},
Expand Down
2 changes: 0 additions & 2 deletions src/verify/lintError.ts
Expand Up @@ -14,8 +14,6 @@
* limitations under the License.
*/

import { Error } from "../error";

export interface PositionInFile {
line: number;
col: number;
Expand Down
51 changes: 51 additions & 0 deletions test/rules/await-promise/for-await-of/test.ts.lint
@@ -0,0 +1,51 @@
[typescript]: >= 2.3.0
async function correct(foo: AsyncIterableIterator<string>) {
for await (const element of foo) {}
}

async function correct2() {
for await (const element of asyncGenerator()) {}
}

async function correct(foo: AsyncIterable<string>) {
for await (const element of foo) {}
}

async function correct3(foo: AsyncIterableIterator<string> | AsyncIterableIterator<number>) {
for await (const element of foo) {}
}

async function incorrect(foo: <Array<Promise<string>>) {
for await (const element of foo) {}
~~~ [0]
}

async function incorrect2(foo: IterableIterator<Promise<string>>) {
for await (const element of foo) {}
~~~ [0]
}

async function incorrect3() {
for await (const element of asyncGenerator) {}
~~~~~~~~~~~~~~ [0]
}

async function incorrect4() {
for await (const element of generator()) {}
~~~~~~~~~~~ [0]
}

async function incorrect5(foo: Iterable<string>) {
for await (const element of foo) {}
~~~ [0]
}

async function* asyncGenerator() {
yield 1;
}

function* generator() {
yield 1;
}

[0]: 'for-await-of' of non-AsyncIterable.
5 changes: 5 additions & 0 deletions test/rules/await-promise/for-await-of/tsconfig.json
@@ -0,0 +1,5 @@
{
"compilerOptions": {
"target": "esnext"
}
}
5 changes: 5 additions & 0 deletions test/rules/await-promise/for-await-of/tslint.json
@@ -0,0 +1,5 @@
{
"rules": {
"await-promise": true
}
}