Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(eslint-plugin): added new rule await-promise #192

Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
951a4fc
feat(eslint-plugin) Added await-promise rule
Feb 3, 2019
c821cc1
Update packages/eslint-plugin/docs/rules/await-promise.md
j-f1 Feb 3, 2019
5a3def8
Merge branch 'master'; moved type util to utils/types.js
Feb 3, 2019
62a6a71
Fixed formatting complaint, I think?
Feb 3, 2019
d40d23c
Merge branch 'master'
Feb 3, 2019
7c386cc
Whoops, docs typo
Feb 3, 2019
c899d72
Merge branch 'master'
Feb 3, 2019
5316313
30 done, not 29
Feb 3, 2019
b93f223
Merge branch 'master' into typescript-eslint-await-promise
Feb 5, 2019
db672a9
Merge branch 'master' into typescript-eslint-await-promise
bradzacher Feb 7, 2019
562fe93
Merge branch 'master'
Feb 9, 2019
b8bd73e
Fixed ROADMAP.md links for await-promise
Feb 9, 2019
3646331
Removed async iterable checking
Feb 9, 2019
bcce472
Merge branch 'master' into typescript-eslint-await-promise
Feb 24, 2019
d5726e6
Converted to TypeScript; used tsutils.isThenableType
Feb 24, 2019
61cfe81
Update packages/eslint-plugin/README.md
mysticatea Apr 3, 2019
30585b3
Update packages/eslint-plugin/docs/rules/await-thenable.md
mysticatea Apr 3, 2019
4731cd3
Update packages/eslint-plugin/ROADMAP.md
mysticatea Apr 3, 2019
89ab9ba
Update packages/eslint-plugin/ROADMAP.md
mysticatea Apr 3, 2019
9e66704
Merge branch 'master'
Apr 3, 2019
addce1d
Docs touchups as requested
Apr 3, 2019
fc74dda
Merge branch 'master' into typescript-eslint-await-promise
JamesHenry Apr 3, 2019
85fdeb0
Merge branch 'master' into typescript-eslint-await-promise
bradzacher Apr 11, 2019
d59c57e
Merge branch 'master' into typescript-eslint-await-promise
bradzacher Apr 11, 2019
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
1 change: 1 addition & 0 deletions packages/eslint-plugin/README.md
Expand Up @@ -96,6 +96,7 @@ Install [`eslint-config-prettier`](https://github.com/prettier/eslint-config-pre
| --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | -------- |
| [`@typescript-eslint/adjacent-overload-signatures`](./docs/rules/adjacent-overload-signatures.md) | Require that member overloads be consecutive (`adjacent-overload-signatures` from TSLint) | :heavy_check_mark: | |
| [`@typescript-eslint/array-type`](./docs/rules/array-type.md) | Requires using either `T[]` or `Array<T>` for arrays (`array-type` from TSLint) | :heavy_check_mark: | :wrench: |
| [`@typescript-eslint/await-promise`](./docs/rules/await-promise.md) | Disallow awaiting a value that is not a Promise (`await-promise` from TSLint) | :heavy_check_mark: | :wrench: |
| [`@typescript-eslint/ban-types`](./docs/rules/ban-types.md) | Enforces that types will not to be used (`ban-types` from TSLint) | :heavy_check_mark: | :wrench: |
| [`@typescript-eslint/camelcase`](./docs/rules/camelcase.md) | Enforce camelCase naming convention | :heavy_check_mark: | |
| [`@typescript-eslint/class-name-casing`](./docs/rules/class-name-casing.md) | Require PascalCased class and interface names (`class-name` from TSLint) | :heavy_check_mark: | |
Expand Down
29 changes: 15 additions & 14 deletions packages/eslint-plugin/ROADMAP.md
@@ -1,10 +1,10 @@
# Roadmap

✅ (28) = done
armano2 marked this conversation as resolved.
Show resolved Hide resolved
🌟 (79) = in ESLint core
🔌 (33) = in another plugin
🌓 (16) = implementations differ or ESLint version is missing functionality
🛑 (70) = unimplemented
✅ (28) = done
🌟 (79) = in ESLint core
🔌 (33) = in another plugin
🌓 (17) = implementations differ or ESLint version is missing functionality
🛑 (69) = unimplemented

## TSLint rules

Expand Down Expand Up @@ -39,7 +39,7 @@

| TSLint rule | | ESLint rule |
| ------------------------------------ | :-: | --------------------------------------------------------------------- |
| [`await-promise`] | 🛑 | N/A |
| [`await-promise`] | | [`@typescript-eslint/no-misused-new`] |
JoshuaKGoldberg marked this conversation as resolved.
Show resolved Hide resolved
| [`ban-comma-operator`] | 🌟 | [`no-sequences`][no-sequences] |
| [`ban`] | 🌟 | [`no-restricted-properties`][no-restricted-properties] |
| [`curly`] | 🌟 | [`curly`][curly] |
Expand Down Expand Up @@ -96,7 +96,7 @@
| [`use-default-type-parameter`] | 🛑 | N/A |
| [`use-isnan`] | 🌟 | [`use-isnan`][use-isnan] |

<sup>[1]</sup> The ESLint rule also supports silencing with an extra set of parens (`if ((foo = bar)) {}`)
<sup>[1]</sup> The ESLint rule also supports silencing with an extra set of parens (`if ((foo = bar)) {}`)
<sup>[2]</sup> Missing private class member support. [`@typescript-eslint/no-unused-vars`] adds support for some TS-specific features.

### Maintainability
Expand All @@ -120,7 +120,7 @@
| [`prefer-readonly`] | 🛑 | N/A |
| [`trailing-comma`] | 🌓 | [`comma-dangle`][comma-dangle] or [Prettier] |

<sup>[1]</sup> Only warns when importing deprecated symbols
<sup>[1]</sup> Only warns when importing deprecated symbols
<sup>[2]</sup> Missing support for blank-line-delimited sections

### Style
Expand Down Expand Up @@ -179,7 +179,7 @@
| [`variable-name`] | 🌟 | <sup>[2]</sup> |
| [`whitespace`] | 🔌 | Use [Prettier] |

<sup>[1]</sup> Recommended config: `["error", { blankLine: "always", prev: "*", next: "return" }]`
<sup>[1]</sup> Recommended config: `["error", { blankLine: "always", prev: "*", next: "return" }]`
<sup>[2]</sup> [`camelcase`][camelcase], [`no-underscore-dangle`][no-underscore-dangle], [`id-blacklist`][id-blacklist], and/or [`id-match`][id-match]

## tslint-microsoft-contrib rules
Expand Down Expand Up @@ -245,10 +245,10 @@ Relevant plugins: [`chai-expect-keywords`](https://github.com/gavinaiken/eslint-
| `use-named-parameter` | 🛑 | N/A |
| `use-simple-attributes` | 🛑 | N/A |

<sup>[1]</sup> Enforces blank lines both at the beginning and end of a block
<sup>[2]</sup> Recommended config: `["error", "ForInStatement"]`
<sup>[3]</sup> Recommended config: `["error", "declaration", { "allowArrowFunctions": true }]`
<sup>[4]</sup> Recommended config: `["error", { "terms": ["BUG", "HACK", "FIXME", "LATER", "LATER2", "TODO"], "location": "anywhere" }]`
<sup>[1]</sup> Enforces blank lines both at the beginning and end of a block
<sup>[2]</sup> Recommended config: `["error", "ForInStatement"]`
<sup>[3]</sup> Recommended config: `["error", "declaration", { "allowArrowFunctions": true }]`
<sup>[4]</sup> Recommended config: `["error", { "terms": ["BUG", "HACK", "FIXME", "LATER", "LATER2", "TODO"], "location": "anywhere" }]`
<sup>[5]</sup> Does not check class fields.

[insecure-random]: https://github.com/desktop/desktop/blob/master/eslint-rules/insecure-random.js
Expand Down Expand Up @@ -310,7 +310,7 @@ Relevant plugins: [`chai-expect-keywords`](https://github.com/gavinaiken/eslint-
| `react-a11y-titles` | 🛑 | N/A |
| `react-anchor-blank-noopener` | 🛑 | N/A |

<sup>[1]</sup> TSLint rule is more strict
<sup>[1]</sup> TSLint rule is more strict
<sup>[2]</sup> ESLint rule only reports for click handlers

[prettier]: https://prettier.io
Expand Down Expand Up @@ -558,6 +558,7 @@ Relevant plugins: [`chai-expect-keywords`](https://github.com/gavinaiken/eslint-
<!-- @typescript-eslint/eslint-plugin -->

[`@typescript-eslint/adjacent-overload-signatures`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/adjacent-overload-signatures.md
[`@typescript-eslint/await-promise`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/await-promise.md
JoshuaKGoldberg marked this conversation as resolved.
Show resolved Hide resolved
[`@typescript-eslint/ban-types`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/ban-types.md
[`@typescript-eslint/explicit-member-accessibility`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/explicit-member-accessibility.md
[`@typescript-eslint/member-ordering`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/member-ordering.md
Expand Down
84 changes: 84 additions & 0 deletions packages/eslint-plugin/docs/rules/await-promise.md
@@ -0,0 +1,84 @@
# Disallows awaiting a value that is not a Promise (await-promise)

This rule disallows awaiting a value that is not a Promise.
While it is valid JavaScript to await a non-`Promise`-like value (it will resolve immediately), this pattern is often a programmer error, such as forgetting to add parenthesis to call a function that returns a Promise.

## Rule Details

Examples of **incorrect** code for this rule:

```ts
await 'value';

const createValue = () => 'value';
await createValue();
```

```ts
// An array of Promises is not the same as an AsyncIterable
async function incorrect(arrayOfPromises: Array<Promise<string>>) {
for await (const element of arrayOfPromises) {
}
}
```

Examples of **correct** code for this rule:

```ts
await Promise.resolve('value');

const createValue = (async() = 'value');
await createValue();
```

```ts
async function overIterable(iterable: AsyncIterable<string>) {
for await (const element of iterable) {
}
}

async function overIterableIterator(iterable: AsyncIterableIterator<string>) {
for await (const element of iterable) {
}
}
```

## Options

The rule accepts an options object with the following property:

- `allowedPromiseNames` any extra names of classes or interfaces to be considered "awaitable" in `await` statements.
JoshuaKGoldberg marked this conversation as resolved.
Show resolved Hide resolved

Classes named `Promise` may always be awaited.
`allowedPromiseNames` does not affect `for-await-of` statements.

### allowedPromiseNames

Examples of **incorrect** code for this rule with `{ allowedPromiseNames: ["Thenable"] }`:

```ts
class Thenable {
/* ... */
}

await new Thenable();
```

Examples of **incorrect** code for this rule with `{ allowedPromiseNames: ["Thenable"] }`:

```ts
class OtherClass {
/* ... */
}

await new OtherClass();
```

## When Not To Use It

If you want to allow code to `await` non-Promise values.
This is generally not preferred, but can sometimes be useful for visual consistency.

## Related to

- TSLint: ['await-promise'](https://palantir.github.io/tslint/rules/await-promise)
92 changes: 92 additions & 0 deletions packages/eslint-plugin/lib/rules/await-promise.js
@@ -0,0 +1,92 @@
/**
* @fileoverview Disallows awaiting a value that is not a Promise
* @author Josh Goldberg
*/
'use strict';
const util = require('../util');
const types = require('../utils/types');

//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------

const defaultOptions = [
{
allowedPromiseNames: []
}
];

/**
* @type {import("eslint").Rule.RuleModule}
*/
module.exports = {
meta: {
docs: {
description: 'Disallows awaiting a value that is not a Promise',
category: 'Functionality',
recommended: 'error',
extraDescription: [util.tslintRule('await-promise')],
url: util.metaDocsUrl('await-promise')
},
fixable: null,
messages: {
await: 'Invalid `await` of a non-Promise value.',
forOf: 'Invalid `for-await-of` of a non-AsyncIterable value.'
},
schema: [
{
type: 'object',
properties: {
allowedPromiseNames: {
type: 'array',
items: {
type: 'string'
}
}
},
additionalProperties: false
}
],
type: 'problem'
},

create(context) {
const options = util.applyDefault(defaultOptions, context.options)[0];

const allowedAsyncIterableNames = new Set([
'AsyncIterable',
'AsyncIterableIterator'
JoshuaKGoldberg marked this conversation as resolved.
Show resolved Hide resolved
]);

const allowedPromiseNames = new Set([
'Promise',
...options.allowedPromiseNames
]);

const parserServices = util.getParserServices(context);
const checker = parserServices.program.getTypeChecker();

function validateNode(node, allowedSymbolNames, messageId) {
const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node);
const type = checker.getTypeAtLocation(originalNode.expression);

if (!types.containsTypeByName(type, allowedSymbolNames)) {
context.report({
messageId,
node
});
}
}

return {
AwaitExpression(node) {
validateNode(node, allowedPromiseNames, 'await');
},
ForOfStatement(node) {
if (node.await) {
validateNode(node, allowedAsyncIterableNames, 'forOf');
}
}
};
}
};
40 changes: 40 additions & 0 deletions packages/eslint-plugin/lib/utils/types.js
@@ -0,0 +1,40 @@
'use strict';

const tsutils = require('tsutils');
const ts = require('typescript');

/**
* @param {string} type Type being checked by name.
* @param {Set<string>} allowedNames Symbol names checking on the type.
* @returns {boolean} Whether the type is, extends, or contains any of the allowed names.
*/
function containsTypeByName(type, allowedNames) {
if (tsutils.isTypeFlagSet(type, ts.TypeFlags.Any | ts.TypeFlags.Unknown)) {
return true;
}

if (tsutils.isTypeReference(type)) {
type = type.target;
}

if (
typeof type.symbol !== 'undefined' &&
allowedNames.has(type.symbol.name)
) {
return true;
}

if (tsutils.isUnionOrIntersectionType(type)) {
return type.types.some(innerType =>
containsTypeByName(innerType, allowedNames)
);
}

const baseTypes = type.getBaseTypes();
return (
typeof baseTypes !== 'undefined' &&
baseTypes.some(baseType => containsTypeByName(baseType, allowedNames))
);
}

exports.containsTypeByName = containsTypeByName;