Skip to content

feat(eslint-plugin): [no-unsafe-call] check calls of Function #10010

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

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
28 changes: 28 additions & 0 deletions packages/eslint-plugin/docs/rules/no-unsafe-call.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,34 @@ String.raw`foo`;
</TabItem>
</Tabs>

## The Unsafe `Function` Type

The `Function` type is behaves almost identically to `any` when called, so this rule also disallows calling values of type `Function`.

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

```ts
const f: Function = () => {};
f();
```

</TabItem>
</Tabs>

Note that whereas [no-unsafe-function-type](./no-unsafe-function-type.mdx) helps prevent the _creation_ of `Function` types, this rule helps prevent the unsafe _use_ of `Function` types, which may creep into your codebase without explicitly referencing the `Function` type at all.
See, for example, the following code:

```ts
function unsafe(maybeFunction: unknown): string {
if (typeof maybeFunction === 'function') {
// TypeScript allows this, but it's completely unsound.
return maybeFunction('call', 'with', 'any', 'args');
}
// etc
}
```
Copy link
Member

Choose a reason for hiding this comment

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

[Praise] Great example, really clear and concise.


## When Not To Use It

If your codebase has many existing `any`s or areas of unsafe code, it may be difficult to enable this rule.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,5 @@ You might consider using [ESLint disable comments](https://eslint.org/docs/lates

- [`no-empty-object-type`](./no-empty-object-type.mdx)
- [`no-restricted-types`](./no-restricted-types.mdx)
- [`no-unsafe-call`](./no-unsafe-call.mdx)
- [`no-wrapper-object-types`](./no-wrapper-object-types.mdx)
52 changes: 48 additions & 4 deletions packages/eslint-plugin/src/rules/no-unsafe-call.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
getConstrainedTypeAtLocation,
getParserServices,
getThisExpression,
isBuiltinSymbolLike,
isTypeAnyType,
} from '../util';

Expand All @@ -25,13 +26,13 @@
requiresTypeChecking: true,
},
messages: {
unsafeCall: 'Unsafe call of an {{type}} typed value.',
unsafeCall: 'Unsafe call of a(n) {{type}} typed value.',
unsafeCallThis: [
'Unsafe call of an `any` typed value. `this` is typed as `any`.',
'Unsafe call of a(n) {{type}} typed value. `this` is typed as {{type}}.',
'You can try to fix this by turning on the `noImplicitThis` compiler option, or adding a `this` parameter to the function.',
].join('\n'),
unsafeNew: 'Unsafe construction of an any type value.',
unsafeTemplateTag: 'Unsafe any typed template tag.',
unsafeNew: 'Unsafe construction of a(n) {{type}} typed value.',
unsafeTemplateTag: 'Unsafe use of a(n) {{type}} typed template tag.',
},
schema: [],
},
Expand Down Expand Up @@ -74,6 +75,49 @@
type: isErrorType ? '`error` type' : '`any`',
},
});
return;
}

if (isBuiltinSymbolLike(services.program, type, 'Function')) {
// this also matches subtypes of `Function`, like `interface Foo extends Function {}`.
//
// For weird TS reasons that I don't understand, these are
//
// safe to construct if:
// - they have at least one call signature _that is not void-returning_,
// - OR they have at least one construct signature.
//
// safe to call (including as template) if:
// - they have at least one call signature
// - OR they have at least one construct signature.

const constructSignatures = type.getConstructSignatures();
if (constructSignatures.length > 0) {
return;
}

const callSignatures = type.getCallSignatures();
if (messageId === 'unsafeNew') {
if (
callSignatures.some(
signature =>
!tsutils.isIntrinsicVoidType(signature.getReturnType()),
)
) {
return;

Check warning on line 107 in packages/eslint-plugin/src/rules/no-unsafe-call.ts

View check run for this annotation

Codecov / codecov/patch

packages/eslint-plugin/src/rules/no-unsafe-call.ts#L107

Added line #L107 was not covered by tests
}
} else if (callSignatures.length > 0) {
return;
}

context.report({
node: reportingNode,
messageId,
data: {
type: '`Function`',
},
});
return;
}
}

Expand Down

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

177 changes: 177 additions & 0 deletions packages/eslint-plugin/tests/rules/no-unsafe-call.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,52 @@ function foo(x: { a?: () => void }) {
x();
}
`,
`
// create a scope since it's illegal to declare a duplicate identifier
// 'Function' in the global script scope.
{
type Function = () => void;
const notGlobalFunctionType: Function = (() => {}) as Function;
notGlobalFunctionType();
}
`,
`
interface SurprisinglySafe extends Function {
(): string;
}
declare const safe: SurprisinglySafe;
safe();
`,
`
interface CallGoodConstructBad extends Function {
(): void;
}
declare const safe: CallGoodConstructBad;
safe();
`,
`
interface ConstructSignatureMakesSafe extends Function {
new (): ConstructSignatureMakesSafe;
}
declare const safe: ConstructSignatureMakesSafe;
new safe();
`,
`
interface SafeWithNonVoidCallSignature extends Function {
(): void;
(x: string): string;
}
declare const safe: SafeWithNonVoidCallSignature;
safe();
`,
// Function has type FunctionConstructor, so it's not within this rule's purview
`
new Function('lol');
`,
// Function has type FunctionConstructor, so it's not within this rule's purview
`
Function('lol');
`,
],
invalid: [
{
Expand Down Expand Up @@ -251,5 +297,136 @@ value();
},
],
},
{
code: `
const t: Function = () => {};
t();
`,
errors: [
{
messageId: 'unsafeCall',
line: 3,
data: {
type: '`Function`',
},
},
],
},
{
code: `
const f: Function = () => {};
f\`oo\`;
`,
errors: [
{
messageId: 'unsafeTemplateTag',
line: 3,
data: {
type: '`Function`',
},
},
],
},
{
code: `
declare const maybeFunction: unknown;
if (typeof maybeFunction === 'function') {
maybeFunction('call', 'with', 'any', 'args');
}
`,
errors: [
{
messageId: 'unsafeCall',
line: 4,
data: {
type: '`Function`',
},
},
],
},
{
code: `
interface Unsafe extends Function {}
declare const unsafe: Unsafe;
unsafe();
`,
errors: [
{
messageId: 'unsafeCall',
line: 4,
data: {
type: '`Function`',
},
},
],
},
{
code: `
interface Unsafe extends Function {}
declare const unsafe: Unsafe;
unsafe\`bad\`;
`,
errors: [
{
messageId: 'unsafeTemplateTag',
line: 4,
data: {
type: '`Function`',
},
},
],
},
{
code: `
interface Unsafe extends Function {}
declare const unsafe: Unsafe;
new unsafe();
`,
errors: [
{
messageId: 'unsafeNew',
line: 4,
data: {
type: '`Function`',
},
},
],
},
{
code: `
interface UnsafeToConstruct extends Function {
(): void;
}
declare const unsafe: UnsafeToConstruct;
new unsafe();
`,
errors: [
{
messageId: 'unsafeNew',
line: 6,
data: {
type: '`Function`',
},
},
],
},
{
code: `
interface StillUnsafe extends Function {
property: string;
}
declare const unsafe: StillUnsafe;
unsafe();
`,
errors: [
{
messageId: 'unsafeCall',
line: 6,
data: {
type: '`Function`',
},
},
],
},
],
});
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,12 @@ ruleTester.run('no-unsafe-function-type', rule, {
'let value: () => void;',
'let value: <T>(t: T) => T;',
`
type Function = () => void;
let value: Function;
// create a scope since it's illegal to declare a duplicate identifier
// 'Function' in the global script scope.
{
type Function = () => void;
let value: Function;
}
`,
],
invalid: [
Expand Down
2 changes: 1 addition & 1 deletion packages/website/src/components/lib/parseConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export function parseTSConfig(code?: string): TSConfig {
const moduleRegexp = /(module\.exports\s*=)/g;

function constrainedScopeEval(obj: string): unknown {
// eslint-disable-next-line @typescript-eslint/no-implied-eval
// eslint-disable-next-line @typescript-eslint/no-implied-eval, @typescript-eslint/no-unsafe-call
Copy link
Member Author

Choose a reason for hiding this comment

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

To be clear,

// this is the no-implied-eval violation
const yoloFunction = Function('inline function');
// this is the no-unsafe-call violation
yoloFunction();

They just happen to be on the same line here. So, this isn't a case of these rules stepping on each others' toes IMO.

return new Function(`
"use strict";
var module = { exports: {} };
Expand Down
Loading