Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
feat(eslint-plugin): [no-unsafe-assignment/return] allow assigning an…
…y => unknown (#2371)
  • Loading branch information
yasarsid committed Aug 10, 2020
1 parent 30993cd commit e7528e6
Show file tree
Hide file tree
Showing 8 changed files with 131 additions and 12 deletions.
10 changes: 10 additions & 0 deletions packages/eslint-plugin/docs/rules/no-unsafe-assignment.md
Expand Up @@ -56,6 +56,16 @@ const x: Set<string[]> = new Set<string[]>();
const x: Set<Set<Set<string>>> = new Set<Set<Set<string>>>();
```

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<unknown> = y as Set<any>;
```

## Related to

- [`no-explicit-any`](./no-explicit-any.md)
Expand Down
14 changes: 14 additions & 0 deletions packages/eslint-plugin/docs/rules/no-unsafe-return.md
Expand Up @@ -69,6 +69,20 @@ type TAssign = () => Set<string>;
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)
Expand Down
5 changes: 5 additions & 0 deletions packages/eslint-plugin/src/rules/no-unsafe-assignment.ts
Expand Up @@ -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',
Expand Down
39 changes: 29 additions & 10 deletions packages/eslint-plugin/src/rules/no-unsafe-return.ts
Expand Up @@ -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;
Expand All @@ -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) {
Expand Down
27 changes: 25 additions & 2 deletions packages/eslint-plugin/src/util/types.ts
Expand Up @@ -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,
Expand Down Expand Up @@ -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)) {
Expand Down
Expand Up @@ -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<unknown> = y as Set<any>;
`,
],
invalid: [
...batchedSingleLineTests({
Expand Down
15 changes: 15 additions & 0 deletions packages/eslint-plugin/tests/rules/no-unsafe-return.test.ts
Expand Up @@ -77,6 +77,21 @@ function foo(): Set<number> {
return x;
}
`,
`
function fn<T extends any>(x: T): unknown {
return x as any;
}
`,
`
function fn<T extends any>(x: T): unknown[] {
return x as any[];
}
`,
`
function fn<T extends any>(x: T): Set<unknown> {
return x as Set<any>;
}
`,
],
invalid: [
...batchedSingleLineTests({
Expand Down
24 changes: 24 additions & 0 deletions packages/eslint-plugin/tests/util/isUnsafeAssignment.test.ts
Expand Up @@ -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<Set<Set<unknown>>> = new Set<Set<Set<any>>>();',
);

expect(isUnsafeAssignment(sender, receiver, checker)).toBeFalsy();
});
});
});

0 comments on commit e7528e6

Please sign in to comment.