Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

feat(eslint-plugin): [no-misused-promises] check subtype methods against heritage type methods#8765

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
Show file tree
Hide file tree
Changes fromall commits
Commits
Show all changes
28 commits
Select commitHold shift + click to select a range
5bc1c5f
Implement checking of subtypes for no-misused-promises (currently wor…
alythobaniMar 22, 2024
8a75efa
Finished working logic for no-misused-promises checksVoidReturn.subtypes
alythobaniMar 23, 2024
24283e0
Cleanup
alythobaniMar 23, 2024
0936f3b
Refactor and improve (better more concise approach), and add more tes…
alythobaniMar 23, 2024
3cc1c0d
Handle type aliases and add type alias test cases
alythobaniMar 24, 2024
7146bce
Added expected {{ baseTypeName }} values to test cases
alythobaniMar 24, 2024
87c6947
Added test cases for handling class expressions, and fixed code to ha…
alythobaniMar 25, 2024
b3c477e
Fix no-misused-promises schema snapshot to account for new subtypes o…
alythobaniMar 25, 2024
714fffe
Updated copy (base type => heritage type) and added documentation for…
alythobaniMar 25, 2024
1f09c9b
Update copy in test cases (baseTypeName => heritageTypeName)
alythobaniMar 25, 2024
32dd7f7
Refactoring
alythobaniMar 25, 2024
36b460c
Copy change again: subtypes => heritageTypes
alythobaniMar 25, 2024
a7b4e1f
Fix location of heritageTypes examples in no-misused-promises mdx doc
alythobaniMar 25, 2024
3c9c945
Refactor out getHeritageTypes function
alythobaniMar 25, 2024
c71dfca
Update no-misused-promises doc to specify that it checks named methods
alythobaniMar 29, 2024
62cc008
Add test cases for ignoring unnamed methods (signatures) and add expl…
alythobaniMar 29, 2024
330b8d4
Add combination test cases which add coverage for when the member is …
alythobaniMar 29, 2024
ce240c7
Rename subtypes => heritageTypes in region comments
alythobaniMar 29, 2024
ab6174c
Adjust no-misused-promises doc to be more explicit about heritageTypes
alythobaniApr 23, 2024
93ce983
Remove `#region`s from no-misused-promises test file
alythobaniApr 23, 2024
a175db7
Merge branch 'main' into 1/no-misused-promises-abstract-method-issue-…
alythobaniApr 24, 2024
74d421d
Update (use jest to generate) no-misused-promises rules snapshot
alythobaniApr 24, 2024
d6cfd24
refactor: map(...).flat() => flatMap(...)
alythobaniJun 6, 2024
eca836f
docs: restructure no-misused-promises doc
alythobaniJun 7, 2024
fbcc229
chore: remove my unit-test-labeling comments
alythobaniJun 7, 2024
f10b62d
rename `heritageTypes` suboption to `inheritedMethods`
alythobaniJul 17, 2024
6944e66
Style nitty nit - not worrying about strict-boolean-expressions,
alythobaniJul 22, 2024
8304cef
Merge branch 'main'
JoshuaKGoldbergAug 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 98 additions & 53 deletionspackages/eslint-plugin/docs/rules/no-misused-promises.mdx
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,10 +37,89 @@ If you don't want to check conditionals, you can configure the rule with `"check

Doing so prevents the rule from looking at code like `if (somePromise)`.

Examples of code for this rule with `checksConditionals: true`:
### `checksVoidReturn`

Likewise, if you don't want to check functions that return promises where a void return is
expected, your configuration will look like this:

```json
{
"@typescript-eslint/no-misused-promises": [
"error",
{
"checksVoidReturn": false
}
]
}
```

You can disable selective parts of the `checksVoidReturn` option by providing an object that disables specific checks. For example, if you don't mind that passing a `() => Promise<void>` to a `() => void` parameter or JSX attribute can lead to a floating unhandled Promise:

```json
{
"@typescript-eslint/no-misused-promises": [
"error",
{
"checksVoidReturn": {
"arguments": false,
"attributes": false
}
}
]
}
```

The following sub-options are supported:

#### `arguments`

Disables checking an asynchronous function passed as argument where the parameter type expects a function that returns `void`.

#### `attributes`

Disables checking an asynchronous function passed as a JSX attribute expected to be a function that returns `void`.

#### `inheritedMethods`

Disables checking an asynchronous method in a type that extends or implements another type expecting that method to return `void`.

:::note
For now, `no-misused-promises` only checks _named_ methods against extended/implemented types: that is, call/construct/index signatures are ignored. Call signatures are not required in TypeScript to be consistent with one another, and construct signatures cannot be `async` in the first place. Index signature checking may be implemented in the future.
:::

#### `properties`

Disables checking an asynchronous function passed as an object property expected to be a function that returns `void`.

#### `returns`

Disables checking an asynchronous function returned in a function whose return type is a function that returns `void`.

#### `variables`

Disables checking an asynchronous function used as a variable whose return type is a function that returns `void`.

### `checksSpreads`

If you don't want to check object spreads, you can add this configuration:

```json
{
"@typescript-eslint/no-misused-promises": [
"error",
{
"checksSpreads": false
}
]
}
```

## Examples

### `checksConditionals`

Examples of code for this rule with `checksConditionals: true`:

<Tabs>
<TabItem value="❌ Incorrect">

Expand DownExpand Up@@ -81,45 +160,6 @@ while (await promise) {

### `checksVoidReturn`

Likewise, if you don't want to check functions that return promises where a void return is
expected, your configuration will look like this:

```json
{
"@typescript-eslint/no-misused-promises": [
"error",
{
"checksVoidReturn": false
}
]
}
```

You can disable selective parts of the `checksVoidReturn` option by providing an object that disables specific checks.
The following options are supported:

- `arguments`: Disables checking an asynchronous function passed as argument where the parameter type expects a function that returns `void`
- `attributes`: Disables checking an asynchronous function passed as a JSX attribute expected to be a function that returns `void`
- `properties`: Disables checking an asynchronous function passed as an object property expected to be a function that returns `void`
- `returns`: Disables checking an asynchronous function returned in a function whose return type is a function that returns `void`
- `variables`: Disables checking an asynchronous function used as a variable whose return type is a function that returns `void`

For example, if you don't mind that passing a `() => Promise<void>` to a `() => void` parameter or JSX attribute can lead to a floating unhandled Promise:

```json
{
"@typescript-eslint/no-misused-promises": [
"error",
{
"checksVoidReturn": {
"arguments": false,
"attributes": false
}
}
]
}
```

Examples of code for this rule with `checksVoidReturn: true`:

<Tabs>
Expand All@@ -140,6 +180,15 @@ document.addEventListener('click', async () => {
await fetch('/');
console.log('synchronous call');
});

interface MySyncInterface {
setThing(): void;
}
class MyClass implements MySyncInterface {
async setThing(): Promise<void> {
this.thing = await fetchThing();
}
}
```

</TabItem>
Expand DownExpand Up@@ -182,26 +231,22 @@ document.addEventListener('click', () => {

handler().catch(handleError);
});

interface MyAsyncInterface {
setThing(): Promise<void>;
}
class MyClass implements MyAsyncInterface {
async setThing(): Promise<void> {
this.thing = await fetchThing();
}
}
```

</TabItem>
</Tabs>

### `checksSpreads`

If you don't want to check object spreads, you can add this configuration:

```json
{
"@typescript-eslint/no-misused-promises": [
"error",
{
"checksSpreads": false
}
]
}
```

Examples of code for this rule with `checksSpreads: true`:

<Tabs>
Expand Down
108 changes: 104 additions & 4 deletionspackages/eslint-plugin/src/rules/no-misused-promises.ts
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,6 +23,7 @@ type Options = [
interface ChecksVoidReturnOptions {
arguments?: boolean;
attributes?: boolean;
inheritedMethods?: boolean;
properties?: boolean;
returns?: boolean;
variables?: boolean;
Expand All@@ -33,6 +34,7 @@ type MessageId =
| 'spread'
| 'voidReturnArgument'
| 'voidReturnAttribute'
| 'voidReturnInheritedMethod'
| 'voidReturnProperty'
| 'voidReturnReturnValue'
| 'voidReturnVariable';
Expand All@@ -49,6 +51,7 @@ function parseChecksVoidReturn(
return {
arguments: true,
attributes: true,
inheritedMethods: true,
properties: true,
returns: true,
variables: true,
Expand All@@ -58,6 +61,7 @@ function parseChecksVoidReturn(
return {
arguments: checksVoidReturn.arguments ?? true,
attributes: checksVoidReturn.attributes ?? true,
inheritedMethods: checksVoidReturn.inheritedMethods ?? true,
properties: checksVoidReturn.properties ?? true,
returns: checksVoidReturn.returns ?? true,
variables: checksVoidReturn.variables ?? true,
Expand All@@ -76,14 +80,16 @@ export default createRule<Options, MessageId>({
messages: {
voidReturnArgument:
'Promise returned in function argument where a void return was expected.',
voidReturnVariable:
'Promise-returning function provided to variable where a void return was expected.',
voidReturnAttribute:
'Promise-returning function provided to attribute where a void return was expected.',
voidReturnInheritedMethod:
"Promise-returning method provided where a void return was expected by extended/implemented type '{{ heritageTypeName }}'.",
voidReturnProperty:
'Promise-returning function provided to property where a void return was expected.',
voidReturnReturnValue:
'Promise-returning function provided to return value where a void return was expected.',
voidReturnAttribute:
'Promise-returning function provided toattribute where a void return was expected.',
voidReturnVariable:
'Promise-returning function provided tovariable where a void return was expected.',
conditional: 'Expected non-Promise value in a boolean conditional.',
spread: 'Expected a non-Promise value to be spreaded in an object.',
},
Expand All@@ -103,6 +109,7 @@ export default createRule<Options, MessageId>({
properties: {
arguments: { type: 'boolean' },
attributes: { type: 'boolean' },
inheritedMethods: { type: 'boolean' },
properties: { type: 'boolean' },
returns: { type: 'boolean' },
variables: { type: 'boolean' },
Expand DownExpand Up@@ -156,6 +163,11 @@ export default createRule<Options, MessageId>({
...(checksVoidReturn.attributes && {
JSXAttribute: checkJSXAttribute,
}),
...(checksVoidReturn.inheritedMethods && {
ClassDeclaration: checkClassLikeOrInterfaceNode,
ClassExpression: checkClassLikeOrInterfaceNode,
TSInterfaceDeclaration: checkClassLikeOrInterfaceNode,
}),
...(checksVoidReturn.properties && {
Property: checkProperty,
}),
Expand DownExpand Up@@ -466,6 +478,71 @@ export default createRule<Options, MessageId>({
}
}

function checkClassLikeOrInterfaceNode(
node:
| TSESTree.ClassDeclaration
| TSESTree.ClassExpression
| TSESTree.TSInterfaceDeclaration,
): void {
const tsNode = services.esTreeNodeToTSNodeMap.get(node);

const heritageTypes = getHeritageTypes(checker, tsNode);
if (!heritageTypes?.length) {
return;
}

for (const nodeMember of tsNode.members) {
const memberName = nodeMember.name?.getText();
if (memberName === undefined) {
// Call/construct/index signatures don't have names. TS allows call signatures to mismatch,
// and construct signatures can't be async.
// TODO - Once we're able to use `checker.isTypeAssignableTo` (v8), we can check an index

Choose a reason for hiding this comment

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

v8 is upon us! Though of course this PR has been in the works since long before that.

@alythobani, let's merge as-is, but would you mind filing a followup (that we now know will not be blocked 🙂)

JoshuaKGoldberg reacted with thumbs up emoji
Copy link
ContributorAuthor

Choose a reason for hiding this comment

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

Sweet, yeah definitely! Do you mean filing an issue for this? And I can start looking into a proper implementation too if you'd like

// signature here against its compatible index signatures in `heritageTypes`
continue;
}
if (!returnsThenable(checker, nodeMember)) {
continue;
}
for (const heritageType of heritageTypes) {
checkHeritageTypeForMemberReturningVoid(
nodeMember,
heritageType,
memberName,
);
}
}
}

/**
* Checks `heritageType` for a member named `memberName` that returns void; reports the
* 'voidReturnInheritedMethod' message if found.
* @param nodeMember Node member that returns a Promise
* @param heritageType Heritage type to check against
* @param memberName Name of the member to check for
*/
function checkHeritageTypeForMemberReturningVoid(
nodeMember: ts.Node,
heritageType: ts.Type,
memberName: string,
): void {
const heritageMember = getMemberIfExists(heritageType, memberName);
if (heritageMember === undefined) {
return;
}
const memberType = checker.getTypeOfSymbolAtLocation(
heritageMember,
nodeMember,
);
if (!isVoidReturningFunctionType(checker, nodeMember, memberType)) {
return;
}
context.report({
node: services.tsNodeToESTreeNodeMap.get(nodeMember),
messageId: 'voidReturnInheritedMethod',
data: { heritageTypeName: checker.typeToString(heritageType) },
});
}

function checkJSXAttribute(node: TSESTree.JSXAttribute): void {
if (
node.value == null ||
Expand DownExpand Up@@ -777,3 +854,26 @@ function returnsThenable(checker: ts.TypeChecker, node: ts.Node): boolean {
.unionTypeParts(type)
.some(t => anySignatureIsThenableType(checker, node, t));
}

function getHeritageTypes(
checker: ts.TypeChecker,
tsNode: ts.ClassDeclaration | ts.ClassExpression | ts.InterfaceDeclaration,
): ts.Type[] | undefined {
return tsNode.heritageClauses
?.flatMap(clause => clause.types)
.map(typeExpression => checker.getTypeAtLocation(typeExpression));
}

/**
* @returns The member with the given name in `type`, if it exists.
*/
function getMemberIfExists(
type: ts.Type,
memberName: string,
): ts.Symbol | undefined {
const escapedMemberName = ts.escapeLeadingUnderscores(memberName);
const symbolMemberMatch = type.getSymbol()?.members?.get(escapedMemberName);
return (
symbolMemberMatch ?? tsutils.getPropertyOfType(type, escapedMemberName)
);
}
View file
Open in desktop

Some generated files are not rendered by default. Learn more abouthow customized files appear on GitHub.

Loading
Loading

[8]ページ先頭

©2009-2025 Movatter.jp