Skip to content

no-unsafe-call: ban calling Function #9108

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
6 tasks done
kirkwaiblinger opened this issue May 16, 2024 · 17 comments · Fixed by #10010
Closed
6 tasks done

no-unsafe-call: ban calling Function #9108

kirkwaiblinger opened this issue May 16, 2024 · 17 comments · Fixed by #10010
Labels
accepting prs Go ahead, send a pull request that resolves this issue enhancement New feature or request 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

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

My proposal is suitable for this project

  • My proposal specifically checks TypeScript syntax, or it proposes a check that requires type information to be accurate.
  • My proposal is not a "formatting rule"; meaning it does not just enforce how code is formatted (whitespace, brace placement, etc).
  • I believe my proposal would be useful to the broader TypeScript community (meaning it is not a niche proposal).

Description

Unintuitively, TS narrows typeof x === 'function' to the unsafe Function type if x is not already constrained. However, it is safe to use on a constrained type. We should report on unconstrained callables.

Fail Cases

declare const maybeFunction: unknown;
if (typeof maybeFunction === 'function') {
    // TS allows this.
    maybeFunction('call', 'with', 'any', 'args');
}

Pass Cases

declare const maybeFunction: string | (() => string);
if (typeof maybeFunction === 'function') {
    // TS errors if more args are provided than the declared call signature, so this is safe.
    maybeFunction();
}

Additional Info

it's probably fine/good to allow typeof x === 'function as long as x isn't called. I'm not sure exactly where the line should be drawn between reporting and not reporting. It seems like it should be reported if we pass x to a function that might expect a specific call signature. Maybe a heuristic could be typeof x === 'function is ok as long as x isn't referenced in the true branch of the conditional?

@kirkwaiblinger kirkwaiblinger added package: eslint-plugin Issues related to @typescript-eslint/eslint-plugin triage Waiting for team members to take a look enhancement: new plugin rule New rule request for eslint-plugin labels May 16, 2024
@bradzacher
Copy link
Member

bradzacher commented May 16, 2024

This would probably be better as a part of no-unsafe-call. Eg ban all calls of a Function typed value, just like it bans all calls of an any typed values

@kirkwaiblinger
Copy link
Member Author

I think I'm ok with that, too. After a little experimentation i think that's safe as far as passing the narrowed function, too, since

declare const looselyTypedFunction: Function;
// TS error, not assignable.
const strictlyTypedFunction: () => void = looselyTypedFunction;

// ditto for passing as a parameter to functions
declare function callsWithAConcreteCallSignature(f: () => void): void;
// TS error
callsWithAConcreteCallSignature(looselyTypedFunction)

So I think Function is only unsafe when directly called, which would get caught with no-unsafe-call

@kirkwaiblinger
Copy link
Member Author

I guess the downside is just that I could see wanting no-unsafe-call disabled but still wanting the sneaky Function call safety

@JoshuaKGoldberg
Copy link
Member

JoshuaKGoldberg commented May 24, 2024

💡🧠 completely different direction: what if we instead made a rule like no-unsafe-function-type as a part of #9102? It could be aimed at catching any case where the unsafe Function type creeps in:

  • : Function type annotations
  • typeof type narrowing on an unknown

Function is already different from the other types banned in #9102's current new no-uppercase-wrapper-types in that it's not a class wrapper around a primitive. And thanks to this issue we can see there's this second difference around typeof narrowing too. That makes me suspect we should have a dedicated rule for the Function type rather than piecemeal it across multiple other somewhat-related rules (no-uppercase-wrapper-types, no-unsafe-call).

@bradzacher
Copy link
Member

bradzacher commented May 24, 2024

The problem with doing that is that it requires type info to handle the typeof usage.

no-uppercase-wrapper-types will be pure syntactic.
no-unsafe-call is already type-aware.
So it makes sense (IMO) to split the checks as we don't want to gate a Function ban behind type-info.

@Josh-Cena
Copy link
Member

So is our conclusion to build this as a part of no-unsafe-call instead? Should this be an option or just enabled by default?

@kirkwaiblinger
Copy link
Member Author

My hot take would be to just put both the existing behavior of no-unsafe-call and the Function safety under separate options in order to accomodate #9108 (comment). I don't feel strongly though

@Josh-Cena Josh-Cena changed the title Rule proposal: no-unsafe-typeof-function no-unsafe-call: ban calling Function Jun 2, 2024
@Josh-Cena Josh-Cena added enhancement: plugin rule option New rule option for an existing eslint-plugin rule and removed enhancement: new plugin rule New rule request for eslint-plugin labels Jun 2, 2024
@JoshuaKGoldberg JoshuaKGoldberg 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 Jun 3, 2024
@bradzacher
Copy link
Member

bradzacher commented Jun 3, 2024

I don't think there's a case where you want to be unsafe in every case. What kind of codebase would say "yes I always want to allow unsafely calling Function"? I think that kind of user wouldn't even use the rule to begin with.

I think no option is fine - people can use a disable comment to opt-out on a case-by-case basis.

@kirkwaiblinger
Copy link
Member Author

kirkwaiblinger commented Jun 3, 2024

@bradzacher

I don't think there's a case where you want to be unsafe in every case. What kind of codebase would say "yes I always want to allow unsafely calling Function"? I think that kind of user wouldn't even use the rule to begin with.

100% agree.

I think no option is fine - people can use a disable comment to opt-out on a case-by-case basis.

However, I don't think this conclusion follows. It does make sense to want to allow calling any (i.e. people who don't currently use the rule at all) and ban calling Function.

@Josh-Cena
Copy link
Member

I don't see how Function is any more or less safe than any. You either ban both or neither

@bradzacher
Copy link
Member

I'd agree. I don't see why you'd ban one without the other. Both are very clear and present vectors for unsafety and they are really the same thing.

@kirkwaiblinger
Copy link
Member Author

I agree that there is zero difference in safety. But, there is a difference in ergonomics and developer intention. Banning using any as any is very unwieldy for when you want to just do some intentional yolo coding, but the typeof function laxness is just sneaky unsafety that looks safe, not an intentional opt-out from type safety.

@bradzacher
Copy link
Member

My thinking would be this: most people are going to just enable the rule with the defaults and do no more - we have learned that the majority of users don't want to read docs and granularly configure things and instead want sensible defaults.

So adding an option would likely be unused as we'd default it to true. Most users wouldn't read the docs and decide they want the rule on but with just the Function check only.

I definitely understand your reasoning - but I think that it's just not going to be a usecase people want (and thus unnecessary branching for us).

I'm not saying we never add an option! If people ask for it then we should. But I think shipping without it is completely okay.

@kirkwaiblinger
Copy link
Member Author

I'm not saying we never add an option! If people ask for it then we should. But I think shipping without it is completely okay.

Ah, gotcha. I fully agree with this! 🙂

@Josh-Cena Josh-Cena added enhancement New feature or request and removed enhancement: plugin rule option New rule option for an existing eslint-plugin rule labels Jun 4, 2024
@controversial
Copy link
Contributor

Is there any way to narrow unknown into something callable after this change?

I have an unknown value schema which I expect to contain a Zod schema, and I want to call it safely.

I used to do:

if (typeof schema !== 'object' || schema === null || !('parse' in schema) || typeof schema.parse !== 'function') throw new Error(`Expected a Zod schema`);
try {
  schema.parse(myData);
}

This narrows schema.parse to Function, which is no longer allowed to be called. I understand now that calling Function is unsafe, but I’m not clear on what the “safe” way is to narrow unknown into something callable.

Maybe the docs for the no-unsafe-call rule could address this?

@kirkwaiblinger
Copy link
Member Author

@controversial Interesting question. JS doesn't really provide any way to know something is safely callable at runtime, so I'm not sure of a "safe" way to narrow unknown into something callable by runtime checks.

Consider

class Foo {};

typeof Foo; // returns 'function'
Foo.length; // returns 0

// so I should be safe to call this with 0 args, right?

Foo(); // Uncaught TypeError: Class constructor Foo cannot be invoked without 'new'

With that in mind, the following will pass your checks, but still cause a runtime exception

const schema = { parse: Foo }; 

I think your best bet is to use eslint-disable comments and/or type assertions and surround stuff in try/catch 🤷

@JoshuaKGoldberg
Copy link
Member

I think this'd be a good note for the docs, yeah. If anybody wants to file an issue and/or send a PR, that's a 👍 from me!

@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 Oct 3, 2024
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Oct 3, 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 New feature or request 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

Successfully merging a pull request may close this issue.

5 participants