diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index a14b3ced2fd..7e956af5742 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -133,6 +133,7 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int | [`@typescript-eslint/no-unnecessary-type-arguments`](./docs/rules/no-unnecessary-type-arguments.md) | Enforces that type arguments will not be used if not required | | :wrench: | :thought_balloon: | | [`@typescript-eslint/no-unnecessary-type-assertion`](./docs/rules/no-unnecessary-type-assertion.md) | Warns if a type assertion does not change the type of an expression | :heavy_check_mark: | :wrench: | :thought_balloon: | | [`@typescript-eslint/no-unsafe-member-access`](./docs/rules/no-unsafe-member-access.md) | Disallows member access on any typed variables | | | :thought_balloon: | +| [`@typescript-eslint/no-unsafe-return`](./docs/rules/no-unsafe-return.md) | Disallows returning any from a function | | | :thought_balloon: | | [`@typescript-eslint/no-unused-vars-experimental`](./docs/rules/no-unused-vars-experimental.md) | Disallow unused variables and arguments | | | :thought_balloon: | | [`@typescript-eslint/no-var-requires`](./docs/rules/no-var-requires.md) | Disallows the use of require statements except in import statements | :heavy_check_mark: | | | | [`@typescript-eslint/prefer-as-const`](./docs/rules/prefer-as-const.md) | Prefer usage of `as const` over literal type | | :wrench: | | diff --git a/packages/eslint-plugin/docs/rules/no-unsafe-return.md b/packages/eslint-plugin/docs/rules/no-unsafe-return.md new file mode 100644 index 00000000000..507abc3dfa2 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/no-unsafe-return.md @@ -0,0 +1,75 @@ +# Disallows returning any from a function (`no-unsafe-return`) + +Despite your best intentions, the `any` type can sometimes leak into your codebase. +Returned `any` typed values not checked at all by TypeScript, so it creates a potential safety hole, and source of bugs in your codebase. + +## Rule Details + +This rule disallows returning `any` or `any[]` from a function. +This rule also compares the return type to the function's declared/inferred return type to ensure you don't return an unsafe `any` in a generic position to a receiver that's expecting a specific type. For example, it will error if you return `Set` from a function declared as returning `Set`. + +Examples of **incorrect** code for this rule: + +```ts +function foo1() { + return 1 as any; +} +function foo2() { + return Object.create(null); +} +const foo3 = () => { + return 1 as any; +}; +const foo4 = () => Object.create(null); + +function foo5() { + return [] as any[]; +} +function foo6() { + return [] as Array; +} +function foo7() { + return [] as readonly any[]; +} +function foo8() { + return [] as Readonly; +} +const foo9 = () => { + return [] as any[]; +}; +const foo10 = () => [] as any[]; + +const foo11 = (): string[] => [1, 2, 3] as any[]; + +// generic position examples +function assignability1(): Set { + return new Set([1]); +} +type TAssign = () => Set; +const assignability2: TAssign = () => new Set([true]); +``` + +Examples of **correct** code for this rule: + +```ts +function foo1() { + return 1; +} +function foo2() { + return Object.create(null) as Record; +} + +const foo3 = () => []; +const foo4 = () => ['a']; + +function assignability1(): Set { + return new Set(['foo']); +} +type TAssign = () => Set; +const assignability2: TAssign = () => new Set(['foo']); +``` + +## Related to + +- [`no-explicit-any`](./no-explicit-any.md) +- TSLint: [`no-unsafe-any`](https://palantir.github.io/tslint/rules/no-unsafe-any/) diff --git a/packages/eslint-plugin/src/configs/all.json b/packages/eslint-plugin/src/configs/all.json index 47f4e509068..f4a7bd43562 100644 --- a/packages/eslint-plugin/src/configs/all.json +++ b/packages/eslint-plugin/src/configs/all.json @@ -62,6 +62,7 @@ "@typescript-eslint/no-unnecessary-type-arguments": "error", "@typescript-eslint/no-unnecessary-type-assertion": "error", "@typescript-eslint/no-unsafe-member-access": "error", + "@typescript-eslint/no-unsafe-return": "error", "no-unused-expressions": "off", "@typescript-eslint/no-unused-expressions": "error", "no-unused-vars": "off", diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index f09033aa2d7..0befe184fbe 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -54,6 +54,7 @@ import noUnnecessaryQualifier from './no-unnecessary-qualifier'; import noUnnecessaryTypeArguments from './no-unnecessary-type-arguments'; import noUnnecessaryTypeAssertion from './no-unnecessary-type-assertion'; import noUnsafeMemberAccess from './no-unsafe-member-access'; +import noUnsafeReturn from './no-unsafe-return'; import noUntypedPublicSignature from './no-untyped-public-signature'; import noUnusedExpressions from './no-unused-expressions'; import noUnusedVars from './no-unused-vars'; @@ -146,6 +147,7 @@ export default { 'no-unnecessary-type-arguments': noUnnecessaryTypeArguments, 'no-unnecessary-type-assertion': noUnnecessaryTypeAssertion, 'no-unsafe-member-access': noUnsafeMemberAccess, + 'no-unsafe-return': noUnsafeReturn, 'no-untyped-public-signature': noUntypedPublicSignature, 'no-unused-expressions': noUnusedExpressions, 'no-unused-vars-experimental': noUnusedVarsExperimental, diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-type-assertion.ts b/packages/eslint-plugin/src/rules/no-unnecessary-type-assertion.ts index 943beb459d7..dd4395b290b 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-type-assertion.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-type-assertion.ts @@ -1,12 +1,7 @@ import { TSESTree } from '@typescript-eslint/experimental-utils'; import { - isCallExpression, - isJsxExpression, - isNewExpression, isObjectType, isObjectFlagSet, - isParameterDeclaration, - isPropertyDeclaration, isStrictCompilerOptionEnabled, isTypeFlagSet, isVariableDeclaration, @@ -91,48 +86,6 @@ export default util.createRule({ return true; } - /** - * Returns the contextual type of a given node. - * Contextual type is the type of the target the node is going into. - * i.e. the type of a called function's parameter, or the defined type of a variable declaration - */ - function getContextualType( - checker: ts.TypeChecker, - node: ts.Expression, - ): ts.Type | undefined { - const parent = node.parent; - if (!parent) { - return; - } - - if (isCallExpression(parent) || isNewExpression(parent)) { - if (node === parent.expression) { - // is the callee, so has no contextual type - return; - } - } else if ( - isVariableDeclaration(parent) || - isPropertyDeclaration(parent) || - isParameterDeclaration(parent) - ) { - return parent.type - ? checker.getTypeFromTypeNode(parent.type) - : undefined; - } else if (isJsxExpression(parent)) { - return checker.getContextualType(parent); - } else if ( - ![ts.SyntaxKind.TemplateSpan, ts.SyntaxKind.JsxExpression].includes( - parent.kind, - ) - ) { - // parent is not something we know we can get the contextual type of - return; - } - // TODO - support return statement checking - - return checker.getContextualType(node); - } - /** * Returns true if there's a chance the variable has been used before a value has been assigned to it */ @@ -196,7 +149,7 @@ export default util.createRule({ // we know it's a nullable type // so figure out if the variable is used in a place that accepts nullable types - const contextualType = getContextualType(checker, originalNode); + const contextualType = util.getContextualType(checker, originalNode); if (contextualType) { // in strict mode you can't assign null to undefined, so we have to make sure that // the two types share a nullable type diff --git a/packages/eslint-plugin/src/rules/no-unsafe-return.ts b/packages/eslint-plugin/src/rules/no-unsafe-return.ts new file mode 100644 index 00000000000..fef04cd6365 --- /dev/null +++ b/packages/eslint-plugin/src/rules/no-unsafe-return.ts @@ -0,0 +1,135 @@ +import { + TSESTree, + AST_NODE_TYPES, +} from '@typescript-eslint/experimental-utils'; +import { isExpression } from 'tsutils'; +import * as util from '../util'; + +export default util.createRule({ + name: 'no-unsafe-return', + meta: { + type: 'problem', + docs: { + description: 'Disallows returning any from a function', + category: 'Possible Errors', + recommended: false, + requiresTypeChecking: true, + }, + messages: { + unsafeReturn: 'Unsafe return of an {{type}} typed value', + unsafeReturnAssignment: + 'Unsafe return of type {{sender}} from function with return type {{receiver}}', + }, + schema: [], + }, + defaultOptions: [], + create(context) { + const { program, esTreeNodeToTSNodeMap } = util.getParserServices(context); + const checker = program.getTypeChecker(); + + function getParentFunctionNode( + node: TSESTree.Node, + ): + | TSESTree.ArrowFunctionExpression + | TSESTree.FunctionDeclaration + | TSESTree.FunctionExpression + | null { + let current = node.parent; + while (current) { + if ( + current.type === AST_NODE_TYPES.ArrowFunctionExpression || + current.type === AST_NODE_TYPES.FunctionDeclaration || + current.type === AST_NODE_TYPES.FunctionExpression + ) { + return current; + } + + current = current.parent; + } + + // this shouldn't happen in correct code, but someone may attempt to parse bad code + // the parser won't error, so we shouldn't throw here + /* istanbul ignore next */ return null; + } + + function checkReturn( + returnNode: TSESTree.Node, + reportingNode: TSESTree.Node = returnNode, + ): 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; + } + + // function has an explicit return type, so ensure it's a safe return + const returnNodeType = checker.getTypeAtLocation( + esTreeNodeToTSNodeMap.get(returnNode), + ); + const functionTSNode = esTreeNodeToTSNodeMap.get(functionNode); + + // function expressions will not have their return type modified based on receiver typing + // so we have to use the contextual typing in these cases, i.e. + // const foo1: () => Set = () => new Set(); + // the return type of the arrow function is Set even though the variable is typed as Set + let functionType = isExpression(functionTSNode) + ? util.getContextualType(checker, functionTSNode) + : checker.getTypeAtLocation(functionTSNode); + if (!functionType) { + functionType = checker.getTypeAtLocation(functionTSNode); + } + + for (const signature of functionType.getCallSignatures()) { + const functionReturnType = signature.getReturnType(); + if (returnNodeType === functionReturnType) { + // don't bother checking if they're the same + // either the function is explicitly declared to return the same type + // or there was no declaration, so the return type is implicit + return; + } + + const result = util.isUnsafeAssignment( + returnNodeType, + functionReturnType, + checker, + ); + if (!result) { + return; + } + + const { sender, receiver } = result; + return context.report({ + node: reportingNode, + messageId: 'unsafeReturnAssignment', + data: { + sender: checker.typeToString(sender), + receiver: checker.typeToString(receiver), + }, + }); + } + } + + return { + ReturnStatement(node): void { + const argument = node.argument; + if (!argument) { + return; + } + + checkReturn(argument, node); + }, + 'ArrowFunctionExpression > :not(BlockStatement).body': checkReturn, + }; + }, +}); diff --git a/packages/eslint-plugin/src/util/types.ts b/packages/eslint-plugin/src/util/types.ts index f59f3d7c53e..924c68e151d 100644 --- a/packages/eslint-plugin/src/util/types.ts +++ b/packages/eslint-plugin/src/util/types.ts @@ -1,6 +1,12 @@ import { + isCallExpression, + isJsxExpression, + isNewExpression, + isParameterDeclaration, + isPropertyDeclaration, isTypeReference, isUnionOrIntersectionType, + isVariableDeclaration, unionTypeParts, } from 'tsutils'; import * as ts from 'typescript'; @@ -297,3 +303,126 @@ export function getEqualsKind(operator: string): EqualsKind | undefined { export function isTypeAnyType(type: ts.Type): boolean { return isTypeFlagSet(type, ts.TypeFlags.Any); } + +export const enum AnyType { + Any, + AnyArray, + Safe, +} +/** + * @returns `AnyType.Any` if the type is `any`, `AnyType.AnyArray` if the type is `any[]` or `readonly any[]`, + * otherwise it returns `AnyType.Safe`. + */ +export function isAnyOrAnyArrayTypeDiscriminated( + node: ts.Node, + checker: ts.TypeChecker, +): AnyType { + const type = checker.getTypeAtLocation(node); + if (isTypeAnyType(type)) { + return AnyType.Any; + } + if ( + checker.isArrayType(type) && + isTypeAnyType(checker.getTypeArguments(type)[0]) + ) { + return AnyType.AnyArray; + } + return AnyType.Safe; +} + +/** + * Does a simple check to see if there is an any being assigned to a non-any type. + * + * This also checks generic positions to ensure there's no unsafe sub-assignments. + * Note: in the case of generic positions, it makes the assumption that the two types are the same. + * + * @example See tests for examples + * + * @returns false if it's safe, or an object with the two types if it's unsafe + */ +export function isUnsafeAssignment( + type: ts.Type, + receiver: ts.Type, + checker: ts.TypeChecker, +): false | { sender: ts.Type; receiver: ts.Type } { + if (isTypeReference(type) && isTypeReference(receiver)) { + // TODO - figure out how to handle cases like this, + // where the types are assignable, but not the same type + /* + function foo(): ReadonlySet { return new Set(); } + + // and + + type Test = { prop: T } + type Test2 = { prop: string } + declare const a: Test; + const b: Test2 = a; + */ + + if (type.target !== receiver.target) { + // if the type references are different, assume safe, as we won't know how to compare the two types + // the generic positions might not be equivalent for both types + return false; + } + + const typeArguments = type.typeArguments ?? []; + const receiverTypeArguments = receiver.typeArguments ?? []; + + for (let i = 0; i < typeArguments.length; i += 1) { + const arg = typeArguments[i]; + const receiverArg = receiverTypeArguments[i]; + + const unsafe = isUnsafeAssignment(arg, receiverArg, checker); + if (unsafe) { + return { sender: type, receiver }; + } + } + + return false; + } + + if (isTypeAnyType(type) && !isTypeAnyType(receiver)) { + return { sender: type, receiver }; + } + return false; +} + +/** + * Returns the contextual type of a given node. + * Contextual type is the type of the target the node is going into. + * i.e. the type of a called function's parameter, or the defined type of a variable declaration + */ +export function getContextualType( + checker: ts.TypeChecker, + node: ts.Expression, +): ts.Type | undefined { + const parent = node.parent; + if (!parent) { + return; + } + + if (isCallExpression(parent) || isNewExpression(parent)) { + if (node === parent.expression) { + // is the callee, so has no contextual type + return; + } + } else if ( + isVariableDeclaration(parent) || + isPropertyDeclaration(parent) || + isParameterDeclaration(parent) + ) { + return parent.type ? checker.getTypeFromTypeNode(parent.type) : undefined; + } else if (isJsxExpression(parent)) { + return checker.getContextualType(parent); + } else if ( + ![ts.SyntaxKind.TemplateSpan, ts.SyntaxKind.JsxExpression].includes( + parent.kind, + ) + ) { + // parent is not something we know we can get the contextual type of + return; + } + // TODO - support return statement checking + + return checker.getContextualType(node); +} diff --git a/packages/eslint-plugin/tests/rules/no-unsafe-return.test.ts b/packages/eslint-plugin/tests/rules/no-unsafe-return.test.ts new file mode 100644 index 00000000000..0fdcaddf36b --- /dev/null +++ b/packages/eslint-plugin/tests/rules/no-unsafe-return.test.ts @@ -0,0 +1,235 @@ +import rule from '../../src/rules/no-unsafe-return'; +import { + RuleTester, + batchedSingleLineTests, + getFixturesRootDir, +} from '../RuleTester'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', + parserOptions: { + project: './tsconfig.json', + tsconfigRootDir: getFixturesRootDir(), + }, +}); + +ruleTester.run('no-unsafe-return', rule, { + valid: [ + 'function foo() { return; }', + 'function foo() { return 1; }', + 'function foo() { return ""; }', + 'function foo() { return true; }', + // this actually types as `never[]` + 'function foo() { return []; }', + // explicit any generic return type is allowed, if you want to be unsafe like that + 'function foo(): Set { return new Set(); }', + // TODO - this should error, but it's hard to detect, as the type references are different + 'function foo(): ReadonlySet { return new Set(); }', + 'function foo(): Set { return new Set([1]); }', + ` + type Foo = { prop: T }; + function foo(): Foo { return ({ prop: 1 } as Foo)} + `, + ` + type Foo = { prop: any }; + function foo(): Foo { return { prop: '' } as Foo; } + `, + ], + invalid: [ + ...batchedSingleLineTests({ + code: ` +function foo() { return (1 as any); } +function foo() { return Object.create(null); } +const foo = () => { return (1 as any) }; +const foo = () => Object.create(null); + `, + errors: [ + { + messageId: 'unsafeReturn', + data: { + type: 'any', + }, + line: 2, + column: 18, + }, + { + messageId: 'unsafeReturn', + data: { + type: 'any', + }, + line: 3, + column: 18, + }, + { + messageId: 'unsafeReturn', + data: { + type: 'any', + }, + line: 4, + column: 21, + }, + { + messageId: 'unsafeReturn', + data: { + type: 'any', + }, + line: 5, + column: 19, + }, + ], + }), + ...batchedSingleLineTests({ + code: ` +function foo() { return ([] as any[]); } +function foo() { return ([] as Array); } +function foo() { return ([] as readonly any[]); } +function foo() { return ([] as Readonly); } +const foo = () => { return ([] as any[]) }; +const foo = () => ([] as any[]); + `, + errors: [ + { + messageId: 'unsafeReturn', + data: { + type: 'any[]', + }, + line: 2, + column: 18, + }, + { + messageId: 'unsafeReturn', + data: { + type: 'any[]', + }, + line: 3, + column: 18, + }, + { + messageId: 'unsafeReturn', + data: { + type: 'any[]', + }, + line: 4, + column: 18, + }, + { + messageId: 'unsafeReturn', + data: { + type: 'any[]', + }, + line: 5, + column: 18, + }, + { + messageId: 'unsafeReturn', + data: { + type: 'any[]', + }, + line: 6, + column: 21, + }, + { + messageId: 'unsafeReturn', + data: { + type: 'any[]', + }, + line: 7, + column: 20, + }, + ], + }), + ...batchedSingleLineTests({ + code: ` +function foo(): Set { return new Set(); } +function foo(): Map { return new Map(); } +function foo(): Set { return new Set(); } +function foo(): Set>> { return new Set>>(); } + `, + errors: [ + { + messageId: 'unsafeReturnAssignment', + data: { + sender: 'Set', + receiver: 'Set', + }, + line: 2, + }, + { + messageId: 'unsafeReturnAssignment', + data: { + sender: 'Map', + receiver: 'Map', + }, + line: 3, + }, + { + messageId: 'unsafeReturnAssignment', + data: { + sender: 'Set', + receiver: 'Set', + }, + line: 4, + }, + { + messageId: 'unsafeReturnAssignment', + data: { + sender: 'Set>>', + receiver: 'Set>>', + }, + line: 5, + }, + ], + }), + { + code: ` + type Fn = () => Set; + const foo1: Fn = () => new Set(); + const foo2: Fn = function test() { return new Set() }; + `, + errors: [ + { + messageId: 'unsafeReturnAssignment', + line: 3, + data: { + sender: 'Set', + receiver: 'Set', + }, + }, + { + messageId: 'unsafeReturnAssignment', + line: 4, + data: { + sender: 'Set', + receiver: 'Set', + }, + }, + ], + }, + { + code: ` + type Fn = () => Set; + function receiver(arg: Fn) {} + receiver(() => new Set()); + receiver(function test() { return new Set() }); + `, + errors: [ + { + messageId: 'unsafeReturnAssignment', + line: 4, + data: { + sender: 'Set', + receiver: 'Set', + }, + }, + { + messageId: 'unsafeReturnAssignment', + line: 5, + data: { + sender: 'Set', + receiver: 'Set', + }, + }, + ], + }, + ], +}); diff --git a/packages/eslint-plugin/tests/util/isUnsafeAssignment.test.ts b/packages/eslint-plugin/tests/util/isUnsafeAssignment.test.ts new file mode 100644 index 00000000000..bd644832b30 --- /dev/null +++ b/packages/eslint-plugin/tests/util/isUnsafeAssignment.test.ts @@ -0,0 +1,170 @@ +import * as ts from 'typescript'; +import { TSESTree } from '@typescript-eslint/experimental-utils'; +import { parseForESLint } from '@typescript-eslint/parser'; +import path from 'path'; +import { getFixturesRootDir } from '../RuleTester'; +import { isUnsafeAssignment } from '../../src/util/types'; + +describe('isUnsafeAssignment', () => { + const rootDir = getFixturesRootDir(); + + function getTypes( + code: string, + ): { sender: ts.Type; receiver: ts.Type; checker: ts.TypeChecker } { + const { ast, services } = parseForESLint(code, { + project: './tsconfig.json', + filePath: path.join(rootDir, 'file.ts'), + tsconfigRootDir: rootDir, + }); + const checker = services.program!.getTypeChecker(); + const esTreeNodeToTSNodeMap = services.esTreeNodeToTSNodeMap!; + + const declaration = ast.body[0] as TSESTree.VariableDeclaration; + const declarator = declaration.declarations[0]; + return { + receiver: checker.getTypeAtLocation( + esTreeNodeToTSNodeMap.get(declarator.id), + ), + sender: checker.getTypeAtLocation( + esTreeNodeToTSNodeMap.get(declarator.init!), + ), + checker, + }; + } + + describe('unsafe', () => { + function expectTypesAre( + result: ReturnType, + checker: ts.TypeChecker, + senderStr: string, + receiverStr: string, + ): void { + expect(result).toBeTruthy(); + const { sender, receiver } = result as Exclude; + + expect(checker.typeToString(sender)).toBe(senderStr); + expect(checker.typeToString(receiver)).toBe(receiverStr); + } + + it('any to a non-any', () => { + const { sender, receiver, checker } = getTypes( + 'const test: string = (1 as any);', + ); + + expectTypesAre( + isUnsafeAssignment(sender, receiver, checker), + checker, + 'any', + 'string', + ); + }); + + it('any in a generic position to a non-any', () => { + const { sender, receiver, checker } = getTypes( + 'const test: Set = new Set();', + ); + + expectTypesAre( + isUnsafeAssignment(sender, receiver, checker), + checker, + 'Set', + 'Set', + ); + }); + + it('any in a generic position to a non-any (multiple generics)', () => { + const { sender, receiver, checker } = getTypes( + 'const test: Map = new Map();', + ); + + expectTypesAre( + isUnsafeAssignment(sender, receiver, checker), + checker, + 'Map', + 'Map', + ); + }); + + it('any[] in a generic position to a non-any[]', () => { + const { sender, receiver, checker } = getTypes( + 'const test: Set = new Set();', + ); + + expectTypesAre( + isUnsafeAssignment(sender, receiver, checker), + checker, + 'Set', + 'Set', + ); + }); + + it('any in a generic position to a non-any (nested)', () => { + const { sender, receiver, checker } = getTypes( + 'const test: Set>> = new Set>>();', + ); + + expectTypesAre( + isUnsafeAssignment(sender, receiver, checker), + checker, + 'Set>>', + 'Set>>', + ); + }); + }); + + describe('safe', () => { + it('non-any to a non-any', () => { + const { sender, receiver, checker } = getTypes( + 'const test: string = "";', + ); + + expect(isUnsafeAssignment(sender, receiver, checker)).toBeFalsy(); + }); + + it('non-any to a any', () => { + const { sender, receiver, checker } = getTypes('const test: any = "";'); + + expect(isUnsafeAssignment(sender, receiver, checker)).toBeFalsy(); + }); + + it('non-any in a generic position to a non-any', () => { + const { sender, receiver, checker } = getTypes( + 'const test: Set = new Set();', + ); + + expect(isUnsafeAssignment(sender, receiver, checker)).toBeFalsy(); + }); + + it('non-any in a generic position to a non-any (multiple generics)', () => { + const { sender, receiver, checker } = getTypes( + 'const test: Map = new Map();', + ); + + expect(isUnsafeAssignment(sender, receiver, checker)).toBeFalsy(); + }); + + it('non-any[] in a generic position to a non-any[]', () => { + const { sender, receiver, checker } = getTypes( + 'const test: Set = new Set();', + ); + + expect(isUnsafeAssignment(sender, receiver, checker)).toBeFalsy(); + }); + + it('non-any in a generic position to a non-any (nested)', () => { + const { sender, receiver, checker } = getTypes( + 'const test: Set>> = new Set>>();', + ); + + expect(isUnsafeAssignment(sender, receiver, checker)).toBeFalsy(); + }); + + it('non-any in a generic position to a any (nested)', () => { + const { sender, receiver, checker } = getTypes( + 'const test: Set>> = new Set>>();', + ); + + expect(isUnsafeAssignment(sender, receiver, checker)).toBeFalsy(); + }); + }); +});