diff --git a/packages/eslint-plugin/docs/rules/no-unsafe-call.mdx b/packages/eslint-plugin/docs/rules/no-unsafe-call.mdx index 3e56c1e0f092..6fc46d6cbf18 100644 --- a/packages/eslint-plugin/docs/rules/no-unsafe-call.mdx +++ b/packages/eslint-plugin/docs/rules/no-unsafe-call.mdx @@ -59,6 +59,34 @@ String.raw`foo`; +## 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`. + + + + +```ts +const f: Function = () => {}; +f(); +``` + + + + +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 +} +``` + ## 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. diff --git a/packages/eslint-plugin/docs/rules/no-unsafe-function-type.mdx b/packages/eslint-plugin/docs/rules/no-unsafe-function-type.mdx index ea7b60794e4e..ee1a84395d8d 100644 --- a/packages/eslint-plugin/docs/rules/no-unsafe-function-type.mdx +++ b/packages/eslint-plugin/docs/rules/no-unsafe-function-type.mdx @@ -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) diff --git a/packages/eslint-plugin/src/rules/no-unsafe-call.ts b/packages/eslint-plugin/src/rules/no-unsafe-call.ts index 6bdec17a4427..8149f2736a15 100644 --- a/packages/eslint-plugin/src/rules/no-unsafe-call.ts +++ b/packages/eslint-plugin/src/rules/no-unsafe-call.ts @@ -6,6 +6,7 @@ import { getConstrainedTypeAtLocation, getParserServices, getThisExpression, + isBuiltinSymbolLike, isTypeAnyType, } from '../util'; @@ -25,13 +26,13 @@ export default createRule<[], MessageIds>({ 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: [], }, @@ -74,6 +75,49 @@ export default createRule<[], MessageIds>({ 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; + } + } else if (callSignatures.length > 0) { + return; + } + + context.report({ + node: reportingNode, + messageId, + data: { + type: '`Function`', + }, + }); + return; } } diff --git a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-unsafe-call.shot b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-unsafe-call.shot index bfc1388d6cf9..7b3ee29e7a93 100644 --- a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-unsafe-call.shot +++ b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-unsafe-call.shot @@ -7,24 +7,24 @@ declare const anyVar: any; declare const nestedAny: { prop: any }; anyVar(); -~~~~~~ Unsafe call of an \`any\` typed value. +~~~~~~ Unsafe call of a(n) \`any\` typed value. anyVar.a.b(); -~~~~~~~~~~ Unsafe call of an \`any\` typed value. +~~~~~~~~~~ Unsafe call of a(n) \`any\` typed value. nestedAny.prop(); -~~~~~~~~~~~~~~ Unsafe call of an \`any\` typed value. +~~~~~~~~~~~~~~ Unsafe call of a(n) \`any\` typed value. nestedAny.prop['a'](); -~~~~~~~~~~~~~~~~~~~ Unsafe call of an \`any\` typed value. +~~~~~~~~~~~~~~~~~~~ Unsafe call of a(n) \`any\` typed value. new anyVar(); -~~~~~~~~~~~~ Unsafe construction of an any type value. +~~~~~~~~~~~~ Unsafe construction of a(n) \`any\` typed value. new nestedAny.prop(); -~~~~~~~~~~~~~~~~~~~~ Unsafe construction of an any type value. +~~~~~~~~~~~~~~~~~~~~ Unsafe construction of a(n) \`any\` typed value. anyVar\`foo\`; -~~~~~~ Unsafe any typed template tag. +~~~~~~ Unsafe use of a(n) \`any\` typed template tag. nestedAny.prop\`foo\`; -~~~~~~~~~~~~~~ Unsafe any typed template tag. +~~~~~~~~~~~~~~ Unsafe use of a(n) \`any\` typed template tag. " `; @@ -44,3 +44,12 @@ new Map(); String.raw\`foo\`; " `; + +exports[`Validating rule docs no-unsafe-call.mdx code examples ESLint output 3`] = ` +"Incorrect + +const f: Function = () => {}; +f(); +~ Unsafe call of a(n) \`Function\` typed value. +" +`; diff --git a/packages/eslint-plugin/tests/rules/no-unsafe-call.test.ts b/packages/eslint-plugin/tests/rules/no-unsafe-call.test.ts index 1a26a4ef3d33..8baf831f96e6 100644 --- a/packages/eslint-plugin/tests/rules/no-unsafe-call.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unsafe-call.test.ts @@ -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: [ { @@ -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`', + }, + }, + ], + }, ], }); diff --git a/packages/eslint-plugin/tests/rules/no-unsafe-function-type.test.ts b/packages/eslint-plugin/tests/rules/no-unsafe-function-type.test.ts index 75c116a66aa5..ae5b96d1ad51 100644 --- a/packages/eslint-plugin/tests/rules/no-unsafe-function-type.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unsafe-function-type.test.ts @@ -9,8 +9,12 @@ ruleTester.run('no-unsafe-function-type', rule, { 'let value: () => void;', 'let value: (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: [ diff --git a/packages/website/src/components/lib/parseConfig.ts b/packages/website/src/components/lib/parseConfig.ts index 2915e9f350db..29cd8d6f4a91 100644 --- a/packages/website/src/components/lib/parseConfig.ts +++ b/packages/website/src/components/lib/parseConfig.ts @@ -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 return new Function(` "use strict"; var module = { exports: {} };