Skip to content

Commit

Permalink
feat: check assignability safety
Browse files Browse the repository at this point in the history
  • Loading branch information
bradzacher committed Mar 3, 2020
1 parent 0ac6922 commit 940ec37
Show file tree
Hide file tree
Showing 4 changed files with 352 additions and 6 deletions.
80 changes: 75 additions & 5 deletions packages/eslint-plugin/src/rules/no-unsafe-return.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import * as util from '../util';
import { TSESTree } from '@typescript-eslint/experimental-utils';
import {
TSESTree,
AST_NODE_TYPES,
} from '@typescript-eslint/experimental-utils';

export default util.createRule({
name: 'no-unsafe-return',
Expand All @@ -13,6 +16,8 @@ export default util.createRule({
},
messages: {
unsafeReturn: 'Unsafe return of an {{type}} typed value',
unsafeReturnAssignment:
'Unsafe return of type {{sender}} from function with return type {{receiver}}',
},
schema: [],
},
Expand All @@ -21,21 +26,86 @@ export default util.createRule({
const { program, esTreeNodeToTSNodeMap } = util.getParserServices(context);
const checker = program.getTypeChecker();

function checkReturn(
function getParentFunctionNode(
node: TSESTree.Node,
reportingNode: TSESTree.Node = 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;
}

return null;
}

function checkReturn(
returnNode: TSESTree.Node,
reportingNode: TSESTree.Node = returnNode,
): void {
const tsNode = esTreeNodeToTSNodeMap.get(node);
const tsNode = esTreeNodeToTSNodeMap.get(returnNode);
const anyType = util.isAnyOrAnyArrayTypeDiscriminated(tsNode, checker);
if (anyType !== util.AnyType.Safe) {
context.report({
return context.report({
node: reportingNode,
messageId: 'unsafeReturn',
data: {
type: anyType === util.AnyType.Any ? 'any' : 'any[]',
},
});
}

const functionNode = getParentFunctionNode(returnNode);
if (!functionNode?.returnType) {
return;
}

// function has an explicit return type, so ensure it's a safe return
const returnNodeType = checker.getTypeAtLocation(
esTreeNodeToTSNodeMap.get(returnNode),
);
const functionType = checker.getTypeAtLocation(
esTreeNodeToTSNodeMap.get(functionNode),
);

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 {
Expand Down
66 changes: 65 additions & 1 deletion packages/eslint-plugin/src/util/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,18 @@ export function isAnyOrAnyArrayType(
node: ts.Node,
checker: ts.TypeChecker,
): boolean {
return isAnyType(node, checker) || isAnyArrayType(node, checker);
const type = checker.getTypeAtLocation(node);
return isTypeAnyType(type) || isTypeAnyArrayType(type, checker);
}

/**
* @returns true if the type is `any`, `any[]` or `readonly any[]`
*/
export function isTypeAnyOrAnyArrayType(
type: ts.Type,
checker: ts.TypeChecker,
): boolean {
return isTypeAnyType(type) || isTypeAnyArrayType(type, checker);
}

export const enum AnyType {
Expand All @@ -363,3 +374,56 @@ export function isAnyOrAnyArrayTypeDiscriminated(
}
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
/*
type Test<T> = { prop: T }
type Test2 = { prop: string }
declare const a: Test<any>;
const b: Test2 = a;
*/
if (type.target !== receiver.target) {
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];

if (!arg || !receiverArg) {
return false;
}

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;
}
42 changes: 42 additions & 0 deletions packages/eslint-plugin/tests/rules/no-unsafe-return.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,5 +125,47 @@ const foo = () => ([] as any[]);
},
],
}),
...batchedSingleLineTests({
code: `
function foo(): Set<string> { return new Set<any>(); }
function foo(): Map<string, string> { return new Map<string, any>(); }
function foo(): Set<string[]> { return new Set<any[]>(); }
function foo(): Set<Set<Set<string>>> { return new Set<Set<Set<any>>>(); }
`,
errors: [
{
messageId: 'unsafeReturnAssignment',
data: {
sender: 'Set<any>',
receiver: 'Set<string>',
},
line: 2,
},
{
messageId: 'unsafeReturnAssignment',
data: {
sender: 'Map<string, any>',
receiver: 'Map<string, string>',
},
line: 3,
},
{
messageId: 'unsafeReturnAssignment',
data: {
sender: 'Set<any[]>',
receiver: 'Set<string[]>',
},
line: 4,
},
{
messageId: 'unsafeReturnAssignment',
data: {
sender: 'Set<Set<Set<any>>>',
receiver: 'Set<Set<Set<string>>>',
},
line: 5,
},
],
}),
],
});

0 comments on commit 940ec37

Please sign in to comment.