Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
feat(eslint-plugin): add rule no-unsafe-return (#1644)
- Loading branch information
1 parent
ba22ea7
commit cfc3ef1
Showing
9 changed files
with
749 additions
and
48 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<any>` from a function declared as returning `Set<string>`. | ||
|
||
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<any>; | ||
} | ||
function foo7() { | ||
return [] as readonly any[]; | ||
} | ||
function foo8() { | ||
return [] as Readonly<any[]>; | ||
} | ||
const foo9 = () => { | ||
return [] as any[]; | ||
}; | ||
const foo10 = () => [] as any[]; | ||
|
||
const foo11 = (): string[] => [1, 2, 3] as any[]; | ||
|
||
// generic position examples | ||
function assignability1(): Set<string> { | ||
return new Set<any>([1]); | ||
} | ||
type TAssign = () => Set<string>; | ||
const assignability2: TAssign = () => new Set<any>([true]); | ||
``` | ||
|
||
Examples of **correct** code for this rule: | ||
|
||
```ts | ||
function foo1() { | ||
return 1; | ||
} | ||
function foo2() { | ||
return Object.create(null) as Record<string, unknown>; | ||
} | ||
|
||
const foo3 = () => []; | ||
const foo4 = () => ['a']; | ||
|
||
function assignability1(): Set<string> { | ||
return new Set<string>(['foo']); | ||
} | ||
type TAssign = () => Set<string>; | ||
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/) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<string> = () => new Set<any>(); | ||
// the return type of the arrow function is Set<any> even though the variable is typed as Set<string> | ||
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, | ||
}; | ||
}, | ||
}); |
Oops, something went wrong.