Skip to content

Commit

Permalink
feat: more checks
Browse files Browse the repository at this point in the history
  • Loading branch information
bradzacher committed Dec 29, 2019
1 parent 7cac42f commit b528874
Show file tree
Hide file tree
Showing 4 changed files with 391 additions and 64 deletions.
194 changes: 163 additions & 31 deletions packages/eslint-plugin/src/rules/no-unsafe-any.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { TSESTree } from '@typescript-eslint/experimental-utils';
import { isTypeReference } from 'tsutils';
import * as ts from 'typescript';
import * as util from '../util';

type Options = [
Expand All @@ -8,10 +10,15 @@ type Options = [
];
type MessageIds =
| 'typeReferenceResolvesToAny'
| 'variableDeclarationInitToAnyWithoutAnnotation'
| 'variableDeclarationInitToAnyWithAnnotation'
| 'variableLetInitialisedToNullishAndNoAnnotation'
| 'variableLetWithNoInitialAndNoAnnotation';
| 'variableDeclarationInitialisedToAnyWithoutAnnotation'
| 'variableDeclarationInitialisedToAnyWithAnnotation'
| 'letVariableInitialisedToNullishAndNoAnnotation'
| 'letVariableWithNoInitialAndNoAnnotation'
| 'variableDeclarationInitialisedToAnyArrayWithoutAnnotation'
| 'loopVariableInitialisedToAny'
| 'returnAny'
| 'passedArgumentIsAny'
| 'assignmentValueIsAny';

export default util.createRule<Options, MessageIds>({
name: 'no-unsafe-any',
Expand All @@ -26,14 +33,20 @@ export default util.createRule<Options, MessageIds>({
messages: {
typeReferenceResolvesToAny:
'Referenced type {{typeName}} resolves to `any`.',
variableDeclarationInitToAnyWithAnnotation:
'Variable declaration is initialised to `any` with an explicit type annotation, which is unsafe. Prefer explicit type narrowing via type guards.',
variableDeclarationInitToAnyWithoutAnnotation:
variableDeclarationInitialisedToAnyWithAnnotation:
'Variable declaration is initialised to `any` with an explicit type annotation, which is potentially unsafe. Prefer explicit type narrowing via type guards.',
variableDeclarationInitialisedToAnyWithoutAnnotation:
'Variable declaration is initialised to `any` without an assertion or a type annotation.',
variableLetInitialisedToNullishAndNoAnnotation:
letVariableInitialisedToNullishAndNoAnnotation:
'Variable declared with {{kind}} and initialised to `null` or `undefined` is implicitly typed as `any`. Add an explicit type annotation.',
variableLetWithNoInitialAndNoAnnotation:
letVariableWithNoInitialAndNoAnnotation:
'Variable declared with {{kind}} with no initial value is implicitly typed as `any`.',
variableDeclarationInitialisedToAnyArrayWithoutAnnotation:
'Variable declaration is initialised to `any[]` without an assertion or a type annotation.',
loopVariableInitialisedToAny: 'Loop variable is typed as `any`.',
returnAny: 'The type of the return is `any`.',
passedArgumentIsAny: 'The passed argument is `any`.',
assignmentValueIsAny: 'The value being assigned is `any`.',
},
schema: [
{
Expand All @@ -57,20 +70,33 @@ export default util.createRule<Options, MessageIds>({
const checker = program.getTypeChecker();
const sourceCode = context.getSourceCode();

function typeReferenceResolvesToAny(node: TSESTree.TypeNode): boolean {
const tsNode = esTreeNodeToTSNodeMap.get(node);
const type = checker.getTypeAtLocation(tsNode);
if (util.isAnyType(type)) {
return true;
}

return false;
/**
* @returns true if the type is `any`
*/
function isAnyType(node: ts.Node): boolean {
const type = checker.getTypeAtLocation(node);
return util.isTypeFlagSet(type, ts.TypeFlags.Any);
}
/**
* @returns true if the type is `any[]` or `readonly any[]`
*/
function isAnyArrayType(node: ts.Node): boolean {
const type = checker.getTypeAtLocation(node);
return (
checker.isArrayType(type) &&
isTypeReference(type) &&
util.isTypeFlagSet(checker.getTypeArguments(type)[0], ts.TypeFlags.Any)
);
}

return {
// Handled by the no-explicit-any rule (with a fixer)
//TSAnyKeyword(node): void {},

// typeReferenceResolvesToAny
TSTypeReference(node): void {
if (!typeReferenceResolvesToAny(node)) {
const tsNode = esTreeNodeToTSNodeMap.get(node);
if (!isAnyType(tsNode)) {
return;
}

Expand All @@ -83,10 +109,8 @@ export default util.createRule<Options, MessageIds>({
},
});
},
// Handled by the no-explicit-any rule (with a fixer)
//TSAnyKeyword(node): void {},

// variableLetWithNoInitialAndNoAnnotation
// letVariableWithNoInitialAndNoAnnotation
'VariableDeclaration:matches([kind = "let"], [kind = "var"]) > VariableDeclarator:not([init])'(
node: TSESTree.VariableDeclarator,
): void {
Expand All @@ -97,14 +121,14 @@ export default util.createRule<Options, MessageIds>({
const parent = node.parent as TSESTree.VariableDeclaration;
context.report({
node,
messageId: 'variableLetWithNoInitialAndNoAnnotation',
messageId: 'letVariableWithNoInitialAndNoAnnotation',
data: {
kind: parent.kind,
},
});
},

// variableLetInitialisedToNullishAndNoAnnotation
// letVariableInitialisedToNullishAndNoAnnotation
'VariableDeclaration:matches([kind = "let"], [kind = "var"]) > VariableDeclarator[init]'(
node: TSESTree.VariableDeclarator,
): void {
Expand All @@ -119,16 +143,16 @@ export default util.createRule<Options, MessageIds>({
) {
context.report({
node,
messageId: 'variableLetInitialisedToNullishAndNoAnnotation',
messageId: 'letVariableInitialisedToNullishAndNoAnnotation',
data: {
kind: parent.kind,
},
});
}
},

// variableDeclarationInitToAnyWithAnnotation
// variableDeclarationInitToAnyWithoutAnnotation
// variableDeclarationInitialisedToAnyWithAnnotation
// variableDeclarationInitialisedToAnyWithoutAnnotation
'VariableDeclaration > VariableDeclarator[init]'(
node: TSESTree.VariableDeclarator,
): void {
Expand All @@ -138,9 +162,7 @@ export default util.createRule<Options, MessageIds>({
}

const tsNode = esTreeNodeToTSNodeMap.get(node.init);
const type = checker.getTypeAtLocation(tsNode);

if (!util.isAnyType(type)) {
if (!isAnyType(tsNode)) {
return;
}

Expand All @@ -149,7 +171,7 @@ export default util.createRule<Options, MessageIds>({
if (!node.id.typeAnnotation) {
return context.report({
node,
messageId: 'variableDeclarationInitToAnyWithoutAnnotation',
messageId: 'variableDeclarationInitialisedToAnyWithoutAnnotation',
});
}

Expand All @@ -162,9 +184,119 @@ export default util.createRule<Options, MessageIds>({

return context.report({
node,
messageId: 'variableDeclarationInitToAnyWithAnnotation',
messageId: 'variableDeclarationInitialisedToAnyWithAnnotation',
});
},

// #region variableDeclarationInitialisedToAnyArrayWithoutAnnotation

// const x = []
'VariableDeclaration > VariableDeclarator > ArrayExpression[elements.length = 0].init'(
node: TSESTree.ArrayExpression,
): void {
const parent = node.parent as TSESTree.VariableDeclarator;
if (parent.id.typeAnnotation) {
return;
}

context.report({
node: parent,
messageId:
'variableDeclarationInitialisedToAnyArrayWithoutAnnotation',
});
},
[[
// const x = Array(...)
'VariableDeclaration > VariableDeclarator > CallExpression[callee.name = "Array"].init',
// const x = new Array(...)
'VariableDeclaration > VariableDeclarator > NewExpression[callee.name = "Array"].init',
].join(', ')](
node: TSESTree.CallExpression | TSESTree.NewExpression,
): void {
const parent = node.parent as TSESTree.VariableDeclarator;
if (parent.id.typeAnnotation) {
return;
}

if (node.arguments.length > 1) {
// Array(1, 2) === [1, 2]
return;
}

if (node.arguments.length === 1) {
// check if the 1 argument is a number, as Array(1) === [empty] === any[]
const tsNode = esTreeNodeToTSNodeMap.get(node.arguments[0]);
const type = checker.getTypeAtLocation(tsNode);
if (!util.isTypeFlagSetNonUnion(type, ts.TypeFlags.NumberLike)) {
return;
}
}

context.report({
node: parent,
messageId:
'variableDeclarationInitialisedToAnyArrayWithoutAnnotation',
});
},

// #endregion variableDeclarationInitialisedToAnyArrayWithoutAnnotation

// loopVariableInitialisedToAny
'ForOfStatement > VariableDeclaration.left > VariableDeclarator'(
node: TSESTree.VariableDeclarator,
): void {
const tsNode = esTreeNodeToTSNodeMap.get(node);
if (isAnyType(tsNode) || isAnyArrayType(tsNode)) {
return context.report({
node,
messageId: 'loopVariableInitialisedToAny',
});
}
},

// returnAny
'ReturnStatement[argument]'(node: TSESTree.ReturnStatement): void {
const argument = util.nullThrows(
node.argument,
util.NullThrowsReasons.MissingToken('argument', 'ReturnStatement'),
);
const tsNode = esTreeNodeToTSNodeMap.get(argument);

if (isAnyType(tsNode) || isAnyArrayType(tsNode)) {
context.report({
node,
messageId: 'returnAny',
});
}
},

// passedArgumentIsAny
'CallExpression[arguments.length > 0]'(
node: TSESTree.CallExpression,
): void {
for (const argument of node.arguments) {
const tsNode = esTreeNodeToTSNodeMap.get(argument);

if (isAnyType(tsNode) || isAnyArrayType(tsNode)) {
context.report({
node,
messageId: 'passedArgumentIsAny',
});
}
}
},

// assignmentValueIsAny
AssignmentExpression(node): void {
const tsNode = esTreeNodeToTSNodeMap.get(node.right);

if (isAnyType(tsNode) || isAnyArrayType(tsNode)) {
context.report({
node,
messageId: 'assignmentValueIsAny',
});
}
},
};
},
});
14 changes: 9 additions & 5 deletions packages/eslint-plugin/src/util/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,8 @@ export function getTypeFlags(type: ts.Type): ts.TypeFlags {
}

/**
* Checks if the given type is (or accepts) the given flags
* Checks if the given type is (or accepts) the given flags.
* This collects all types across a union.
* @param isReceiver true if the type is a receiving type (i.e. the type of a called function's parameter)
*/
export function isTypeFlagSet(
Expand All @@ -186,6 +187,13 @@ export function isTypeFlagSet(
return (flags & flagsToCheck) !== 0;
}

export function isTypeFlagSetNonUnion(
type: ts.Type,
flagsToCheck: ts.TypeFlags,
): boolean {
return (type.flags & flagsToCheck) !== 0;
}

/**
* @returns Whether a type is an instance of the parent type, including for the parent's base types.
*/
Expand Down Expand Up @@ -254,7 +262,3 @@ export function getTokenAtPosition(
}
return current!;
}

export function isAnyType(type: ts.Type): boolean {
return isTypeFlagSet(type, ts.TypeFlags.Any);
}

0 comments on commit b528874

Please sign in to comment.