Skip to content

Enhancement: [await-thenable] should prohibit using a sync disposable with await using #10208

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

Closed
4 tasks done
kirkwaiblinger opened this issue Oct 25, 2024 · 6 comments
Closed
4 tasks done
Labels
accepting prs Go ahead, send a pull request that resolves this issue enhancement: plugin rule option New rule option for an existing eslint-plugin rule locked due to age Please open a new issue if you'd like to say more. See https://typescript-eslint.io/contributing. package: eslint-plugin Issues related to @typescript-eslint/eslint-plugin

Comments

@kirkwaiblinger
Copy link
Member

kirkwaiblinger commented Oct 25, 2024

Before You File a Proposal Please Confirm You Have Done The Following...

My proposal is suitable for this project

  • I believe my proposal would be useful to the broader TypeScript community (meaning it is not a niche proposal).

Link to the rule's documentation

https://typescript-eslint.io/rules/await-thenable/, https://typescript-eslint.io/rules/no-floating-promises, https://typescript-eslint.io/rules/no-misused-promises

Description

I would like to find a place to lint for providing a sync disposable to an await using statement. This relates closely to #8858, so maybe await-thenable is the right place for this check? But it also has concerns very adjacent to no-floating-promises and no-misused-promises, so I'm not sure.

Fail

async function unhandledRejection() {             
    await using _ = {
        async [Symbol.dispose]() {
            throw new Error('dispose error');
        }
    }
}

Pass

async function properlyHandledRejection() {             
    await using _ = {
        async [Symbol.asyncDispose]() {
            throw new Error('dispose error');
        }
    }
}

Additional Info

The promise rejection in unhandledRejection cannot be caught, since the [Symbol.dispose]() method is called, but is not awaited, by an await using statement. Only the [Symbol.asyncDispose]() method is awaited.

There's a lot of subtlety here so I've provided a repo to be able to play with variations on this at https://github.com/kirkwaiblinger/typescript-eslint-repro-await-using

@kirkwaiblinger kirkwaiblinger added package: eslint-plugin Issues related to @typescript-eslint/eslint-plugin triage Waiting for team members to take a look enhancement: plugin rule option New rule option for an existing eslint-plugin rule labels Oct 25, 2024
@kirkwaiblinger
Copy link
Member Author

Note that the fundamental concern here is that a Promise-returning [Symbol.dispose]() method is a clear vector for unhandled rejections, whether invoked by an await using or an ordinary using statement.

@bradzacher
Copy link
Member

probably makes most sense for this to be in await-thenable, I think.

@bradzacher bradzacher added accepting prs Go ahead, send a pull request that resolves this issue and removed triage Waiting for team members to take a look labels Oct 25, 2024
@bradzacher bradzacher changed the title Enhancement: [no-misused-promises or no-floating-promises or await-thenable?] should prohibit using a sync disposable with await using Enhancement: [await-thenable?] should prohibit using a sync disposable with await using Oct 25, 2024
@bradzacher bradzacher changed the title Enhancement: [await-thenable?] should prohibit using a sync disposable with await using Enhancement: [await-thenable] should prohibit using a sync disposable with await using Oct 25, 2024
@kirkwaiblinger
Copy link
Member Author

@bradzacher
So my thing is, if we do that, how do we also ban

function unhandledRejection() {             
    using _ = {
        async [Symbol.dispose]() {
            throw new Error('dispose error');
        }
    }
}

It doesn't go in await-thenable, since that's all about unnecessary awaiting of non-thenables. Maybe no-misused-promises then? Note that there's no () => Promise<void> to () => void assignment happening here, since the object literal is provided literally to the using declaration, therefore, we'd need to add a new check for using declarations specifically, rather than being able to rely on the existing checks of no-misused-promises.

@bradzacher
Copy link
Member

Does the disposable method have a default type definition? If it does then iirc there's another rule that should already catch it.

If it doesn't then no-misused-promises would probably be a good fit - iirc it already handles array iteration callbacks.

@kirkwaiblinger
Copy link
Member Author

kirkwaiblinger commented Oct 26, 2024

Does the disposable method have a default type definition? If it does then iirc there's another rule that should already catch it.

If it doesn't then no-misused-promises would probably be a good fit - iirc it already handles array iteration callbacks.

After a bit of investigation...

await using sync disposable ✅

await using x = {
    [Symbol.dispose]() { }
}

can be easily added to await-thenable, #10209 in progress.

EDIT: RESOLVED in #10209

Promise returning function assigned to Disposable variable. ✅

const x: Disposable = {
    async [Symbol.dispose]() {}
} 

already flagged by no-misused-promises. This is a garden variety () => Promise<void> to () => void assignment.

Promise returning function in variable assigned to Disposable variable ❌

const x = {
    async [Symbol.dispose]() {}
} 
const y: Disposable = x;

not flagged by current tooling at all. Could/should be added to no-misused-promises (or upcoming strict-void-return, #9707). Arguably this is just an existing hole in the rule for checksVoidReturn that it doesn't inspect the properties within a variable being assigned.

EDIT: Filed #10357

using x with variable ❌

const x = {
    async [Symbol.dispose]() {}
} 
using y = x;

not flagged by current tooling at all (unless an explicit : Disposable type annotation is provided). Could/should be added to no-misused-promises (or upcoming strict-void-return, #9707).

EDIT: Filed #10357

using x with literal ❌

using y = {
    async [Symbol.dispose]() {}
};

not flagged by current tooling at all. Could/should be added to no-misused-promises (or upcoming strict-void-return, #9707).

EDIT: Filed #10357

async disposable class ⏳

class d implements Disposable {
    async [Symbol.dispose]() {}
}

Not flagged by current tooling at all. But it should be, by no-misused-promises's inheritedMethods checks. This is a bug due to the key being a (static) "computed" symbol rather than an identifier. Filed #10211

@kirkwaiblinger
Copy link
Member Author

Closing this issue since #10209 is resolved.

Remaining work is tracked in #10211 and #10357

@github-actions github-actions bot added the locked due to age Please open a new issue if you'd like to say more. See https://typescript-eslint.io/contributing. label Nov 26, 2024
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Nov 26, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
accepting prs Go ahead, send a pull request that resolves this issue enhancement: plugin rule option New rule option for an existing eslint-plugin rule locked due to age Please open a new issue if you'd like to say more. See https://typescript-eslint.io/contributing. package: eslint-plugin Issues related to @typescript-eslint/eslint-plugin
Projects
None yet
Development

No branches or pull requests

2 participants