Skip to content

Commit

Permalink
feat: check array patterns
Browse files Browse the repository at this point in the history
  • Loading branch information
bradzacher committed Apr 1, 2020
1 parent 41b7b15 commit 69ec840
Show file tree
Hide file tree
Showing 4 changed files with 267 additions and 34 deletions.
3 changes: 3 additions & 0 deletions packages/eslint-plugin/docs/rules/no-unsafe-assignment.md
Expand Up @@ -15,6 +15,8 @@ const x = 1 as any,
y = 1 as any;
const [x] = 1 as any;
const [x] = [] as any[];
const [x] = [1 as any];
[x] = [1] as [any];

function foo(a = 1 as any) {}
class Foo {
Expand All @@ -37,6 +39,7 @@ Examples of **correct** code for this rule:
const x = 1,
y = 1;
const [x] = [1];
[x] = [1] as [number];

function foo(a = 1) {}
class Foo {
Expand Down
147 changes: 128 additions & 19 deletions packages/eslint-plugin/src/rules/no-unsafe-assignment.ts
Expand Up @@ -2,6 +2,7 @@ import {
TSESTree,
AST_NODE_TYPES,
} from '@typescript-eslint/experimental-utils';
import * as ts from 'typescript';
import * as util from '../util';

const enum ComparisonType {
Expand All @@ -26,6 +27,8 @@ export default util.createRule({
messages: {
anyAssignment: 'Unsafe assignment of an any value',
unsafeArrayPattern: 'Unsafe array destructuring of an any array value',
unsafeArrayPatternFromTuple:
'Unsafe array destructuring of a tuple element with an any value',
unsafeAssignment:
'Unsafe asignment of type {{sender}} to a variable of type {{receiver}}',
},
Expand All @@ -36,12 +39,101 @@ export default util.createRule({
const { program, esTreeNodeToTSNodeMap } = util.getParserServices(context);
const checker = program.getTypeChecker();

function checkArrayDestructureHelper(
receiverNode: TSESTree.Node,
senderNode: TSESTree.Node,
): boolean {
if (receiverNode.type !== AST_NODE_TYPES.ArrayPattern) {
return true;
}

const senderType = checker.getTypeAtLocation(
esTreeNodeToTSNodeMap.get(senderNode),
);

return checkArrayDestructure(receiverNode, senderType);
}

// returns true if the assignment is safe
function checkArrayDestructure(
receiverNode: TSESTree.ArrayPattern,
senderType: ts.Type,
): boolean {
// any array
// const [x] = ([] as any[]);
if (util.isTypeAnyArrayType(senderType, checker)) {
context.report({
node: receiverNode,
messageId: 'unsafeArrayPattern',
});
return false;
}

if (!checker.isTupleType(senderType)) {
return true;
}

const tupleElements = util.getTypeArguments(senderType, checker);

// tuple with any
// const [x] = [1 as any];
let didReport = false;
for (
let receiverIndex = 0;
receiverIndex < receiverNode.elements.length;
receiverIndex += 1
) {
const receiverElement = receiverNode.elements[receiverIndex];
if (!receiverElement) {
continue;
}

if (receiverElement.type === AST_NODE_TYPES.RestElement) {
// const [...x] = [1, 2, 3 as any];
// check the remaining elements to see if one of them is typed as any
for (
let senderIndex = receiverIndex;
senderIndex < tupleElements.length;
senderIndex += 1
) {
const senderType = tupleElements[senderIndex];
if (senderType && util.isTypeAnyType(senderType)) {
context.report({
node: receiverElement,
messageId: 'unsafeArrayPatternFromTuple',
});
return false;
}
}
// rest element must be the last one in a destructure
return true;
}

const senderType = tupleElements[receiverIndex];
if (receiverElement.type === AST_NODE_TYPES.ArrayPattern) {
didReport = checkArrayDestructure(receiverElement, senderType);
} else {
if (senderType && util.isTypeAnyType(senderType)) {
context.report({
node: receiverElement,
messageId: 'unsafeArrayPatternFromTuple',
});
// we want to report on every invalid element in the tuple
didReport = true;
}
}
}

return didReport;
}

// returns true if the assignment is safe
function checkAssignment(
receiverNode: TSESTree.Node,
senderNode: TSESTree.Node,
reportingNode: TSESTree.Node,
comparisonType: ComparisonType,
): void {
): boolean {
const receiverType = checker.getTypeAtLocation(
esTreeNodeToTSNodeMap.get(receiverNode),
);
Expand All @@ -50,40 +142,32 @@ export default util.createRule({
);

if (util.isTypeAnyType(senderType)) {
return context.report({
context.report({
node: reportingNode,
messageId: 'anyAssignment',
});
}

if (
receiverNode.type === AST_NODE_TYPES.ArrayPattern &&
util.isTypeAnyArrayType(senderType, checker)
) {
return context.report({
node: reportingNode,
messageId: 'unsafeArrayPattern',
});
return false;
}

if (comparisonType === ComparisonType.None) {
return;
return true;
}

const result = util.isUnsafeAssignment(senderType, receiverType, checker);
if (!result) {
return;
return true;
}

const { sender, receiver } = result;
return context.report({
context.report({
node: reportingNode,
messageId: 'unsafeAssignment',
data: {
sender: checker.typeToString(sender),
receiver: checker.typeToString(receiver),
},
});
return false;
}

function getComparisonType(
Expand All @@ -100,12 +184,16 @@ export default util.createRule({
'VariableDeclarator[init != null]'(
node: TSESTree.VariableDeclarator,
): void {
checkAssignment(
const isSafe = checkAssignment(
node.id,
node.init!,
node,
getComparisonType(node.id.typeAnnotation),
);

if (isSafe) {
checkArrayDestructureHelper(node.id, node.init!);
}
},
'ClassProperty[value != null]'(node: TSESTree.ClassProperty): void {
checkAssignment(
Expand All @@ -118,16 +206,37 @@ export default util.createRule({
'AssignmentExpression[operator = "="], AssignmentPattern'(
node: TSESTree.AssignmentExpression | TSESTree.AssignmentPattern,
): void {
checkAssignment(
const isSafe = checkAssignment(
node.left,
node.right,
node,
// the variable already has some form of a type to compare against
ComparisonType.Basic,
);
},

// TODO - { x: 1 }
if (isSafe) {
checkArrayDestructureHelper(node.left, node.right);
}
},
// Property(node): void {
// checkAssignment(
// node.key,
// node.value,
// node,
// ComparisonType.Contextual, // TODO - is this required???
// );
// },
// 'JSXAttribute[value != null]'(node: TSESTree.JSXAttribute): void {
// if (!node.value) {
// return;
// }
// checkAssignment(
// node.name,
// node.value,
// node,
// ComparisonType.Basic, // TODO
// );
// },
};
},
});
16 changes: 13 additions & 3 deletions packages/eslint-plugin/src/util/types.ts
Expand Up @@ -297,6 +297,18 @@ export function getEqualsKind(operator: string): EqualsKind | undefined {
}
}

export function getTypeArguments(
type: ts.TypeReference,
checker: ts.TypeChecker,
): readonly ts.Type[] {
// getTypeArguments was only added in TS3.7
if (checker.getTypeArguments) {
return checker.getTypeArguments(type);
}

return type.typeArguments ?? [];
}

/**
* @returns true if the type is `any`
*/
Expand All @@ -315,9 +327,7 @@ export function isTypeAnyArrayType(
checker.isArrayType(type) &&
isTypeAnyType(
// getTypeArguments was only added in TS3.7
checker.getTypeArguments
? checker.getTypeArguments(type)[0]
: (type.typeArguments ?? [])[0],
getTypeArguments(type, checker)[0],
)
);
}
Expand Down

0 comments on commit 69ec840

Please sign in to comment.