diff --git a/packages/eslint-plugin/docs/rules/no-unsafe-assignment.md b/packages/eslint-plugin/docs/rules/no-unsafe-assignment.md index 1a452e431eb..64207e5365e 100644 --- a/packages/eslint-plugin/docs/rules/no-unsafe-assignment.md +++ b/packages/eslint-plugin/docs/rules/no-unsafe-assignment.md @@ -56,6 +56,16 @@ const x: Set = new Set(); const x: Set>> = new Set>>(); ``` +There are cases where the rule allows assignment of `any` to `unknown`. + +Example of `any` to `unknown` assignment that are allowed. + +```ts +const x: unknown = y as any; +const x: unknown[] = y as any[]; +const x: Set = y as Set; +``` + ## Related to - [`no-explicit-any`](./no-explicit-any.md) diff --git a/packages/eslint-plugin/docs/rules/no-unsafe-return.md b/packages/eslint-plugin/docs/rules/no-unsafe-return.md index 507abc3dfa2..9810be3cf16 100644 --- a/packages/eslint-plugin/docs/rules/no-unsafe-return.md +++ b/packages/eslint-plugin/docs/rules/no-unsafe-return.md @@ -69,6 +69,20 @@ type TAssign = () => Set; const assignability2: TAssign = () => new Set(['foo']); ``` +There are cases where the rule allows to return `any` to `unknown`. + +Examples of `any` to `unknown` return that are allowed. + +```ts +function foo1(): unknown { + return JSON.parse(singleObjString); // Return type for JSON.parse is any. +} + +function foo2(): unknown[] { + return [] as any[]; +} +``` + ## Related to - [`no-explicit-any`](./no-explicit-any.md) diff --git a/packages/eslint-plugin/src/rules/no-unsafe-assignment.ts b/packages/eslint-plugin/src/rules/no-unsafe-assignment.ts index 21e646483bc..4a8f2a9ea23 100644 --- a/packages/eslint-plugin/src/rules/no-unsafe-assignment.ts +++ b/packages/eslint-plugin/src/rules/no-unsafe-assignment.ts @@ -238,6 +238,11 @@ export default util.createRule({ ); if (util.isTypeAnyType(senderType)) { + // handle cases when we assign any ==> unknown. + if (util.isTypeUnknownType(receiverType)) { + return false; + } + context.report({ node: reportingNode, messageId: 'anyAssignment', diff --git a/packages/eslint-plugin/src/rules/no-unsafe-return.ts b/packages/eslint-plugin/src/rules/no-unsafe-return.ts index b0168dc6f50..c2366bc96f2 100644 --- a/packages/eslint-plugin/src/rules/no-unsafe-return.ts +++ b/packages/eslint-plugin/src/rules/no-unsafe-return.ts @@ -58,16 +58,6 @@ export default util.createRule({ ): void { const tsNode = esTreeNodeToTSNodeMap.get(returnNode); const anyType = util.isAnyOrAnyArrayTypeDiscriminated(tsNode, checker); - if (anyType !== util.AnyType.Safe) { - return context.report({ - node: reportingNode, - messageId: 'unsafeReturn', - data: { - type: anyType === util.AnyType.Any ? 'any' : 'any[]', - }, - }); - } - const functionNode = getParentFunctionNode(returnNode); /* istanbul ignore if */ if (!functionNode) { return; @@ -91,6 +81,35 @@ export default util.createRule({ functionType = checker.getTypeAtLocation(functionTSNode); } + if (anyType !== util.AnyType.Safe) { + // Allow cases when the declared return type of the function is either unknown or unknown[] + // and the function is returning any or any[]. + for (const signature of functionType.getCallSignatures()) { + const functionReturnType = signature.getReturnType(); + if ( + anyType === util.AnyType.Any && + util.isTypeUnknownType(functionReturnType) + ) { + return; + } + if ( + anyType === util.AnyType.AnyArray && + util.isTypeUnknownArrayType(functionReturnType, checker) + ) { + return; + } + } + + // If the function return type was not unknown/unknown[], mark usage as unsafeReturn. + return context.report({ + node: reportingNode, + messageId: 'unsafeReturn', + data: { + type: anyType === util.AnyType.Any ? 'any' : 'any[]', + }, + }); + } + for (const signature of functionType.getCallSignatures()) { const functionReturnType = signature.getReturnType(); if (returnNodeType === functionReturnType) { diff --git a/packages/eslint-plugin/src/util/types.ts b/packages/eslint-plugin/src/util/types.ts index d030b829cd4..d29349f812f 100644 --- a/packages/eslint-plugin/src/util/types.ts +++ b/packages/eslint-plugin/src/util/types.ts @@ -365,6 +365,22 @@ export function isTypeAnyArrayType( ); } +/** + * @returns true if the type is `unknown[]` + */ +export function isTypeUnknownArrayType( + type: ts.Type, + checker: ts.TypeChecker, +): boolean { + return ( + checker.isArrayType(type) && + isTypeUnknownType( + // getTypeArguments was only added in TS3.7 + getTypeArguments(type, checker)[0], + ) + ); +} + export const enum AnyType { Any, AnyArray, @@ -403,8 +419,15 @@ export function isUnsafeAssignment( receiver: ts.Type, checker: ts.TypeChecker, ): false | { sender: ts.Type; receiver: ts.Type } { - if (isTypeAnyType(type) && !isTypeAnyType(receiver)) { - return { sender: type, receiver }; + if (isTypeAnyType(type)) { + // Allow assignment of any ==> unknown. + if (isTypeUnknownType(receiver)) { + return false; + } + + if (!isTypeAnyType(receiver)) { + return { sender: type, receiver }; + } } if (isTypeReference(type) && isTypeReference(receiver)) { diff --git a/packages/eslint-plugin/tests/rules/no-unsafe-assignment.test.ts b/packages/eslint-plugin/tests/rules/no-unsafe-assignment.test.ts index 708969310ed..edfa8abadd2 100644 --- a/packages/eslint-plugin/tests/rules/no-unsafe-assignment.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unsafe-assignment.test.ts @@ -119,6 +119,15 @@ declare function Foo(props: Props): never; `, filename: 'react.tsx', }, + ` + const x: unknown = y as any; + `, + ` + const x: unknown[] = y as any[]; + `, + ` + const x: Set = y as Set; + `, ], invalid: [ ...batchedSingleLineTests({ diff --git a/packages/eslint-plugin/tests/rules/no-unsafe-return.test.ts b/packages/eslint-plugin/tests/rules/no-unsafe-return.test.ts index 91627f91b02..7d7777e4663 100644 --- a/packages/eslint-plugin/tests/rules/no-unsafe-return.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unsafe-return.test.ts @@ -77,6 +77,21 @@ function foo(): Set { return x; } `, + ` + function fn(x: T): unknown { + return x as any; + } + `, + ` + function fn(x: T): unknown[] { + return x as any[]; + } + `, + ` + function fn(x: T): Set { + return x as Set; + } + `, ], invalid: [ ...batchedSingleLineTests({ diff --git a/packages/eslint-plugin/tests/util/isUnsafeAssignment.test.ts b/packages/eslint-plugin/tests/util/isUnsafeAssignment.test.ts index 3862b38902c..efe1fe9d1ba 100644 --- a/packages/eslint-plugin/tests/util/isUnsafeAssignment.test.ts +++ b/packages/eslint-plugin/tests/util/isUnsafeAssignment.test.ts @@ -166,5 +166,29 @@ describe('isUnsafeAssignment', () => { expect(isUnsafeAssignment(sender, receiver, checker)).toBeFalsy(); }); + + it('any to a unknown', () => { + const { sender, receiver, checker } = getTypes( + 'const test: unknown = [] as any;', + ); + + expect(isUnsafeAssignment(sender, receiver, checker)).toBeFalsy(); + }); + + it('any[] in a generic position to a unknown[]', () => { + const { sender, receiver, checker } = getTypes( + 'const test: unknown[] = [] as any[]', + ); + + expect(isUnsafeAssignment(sender, receiver, checker)).toBeFalsy(); + }); + + it('any in a generic position to a unknown (nested)', () => { + const { sender, receiver, checker } = getTypes( + 'const test: Set>> = new Set>>();', + ); + + expect(isUnsafeAssignment(sender, receiver, checker)).toBeFalsy(); + }); }); });