import * as ts from "typescript" ; import * as Lint from "tslint" ; import * as utils from "tsutils/typeguard/2.8" ; import { isAssignmentKind } from "tsutils/util" ; import { createInvalidNode , CheckNodeResult , createCheckNodeTypedRule } from "./shared/check-node" ; import * as Ignore from "./shared/ignore" ; import { isAccessExpression , AccessExpression } from "./shared/typeguard" ; type Options = Ignore . IgnoreNewArrayOption & Ignore . IgnoreMutationFollowingAccessorOption & Ignore . IgnoreOption ; type ArrayType = ts . Type & { symbol : { name : "Array" ; } ; } ; type ArrayConstructorType = ts . Type & { symbol : { name : "ArrayConstructor" ; } ; } ; export const Rule = createCheckNodeTypedRule ( checkTypedNode , "Mutating an array is not allowed." ) ; export function isArrayType ( type ) { return Boolean ( type . symbol && type . symbol . name === "Array" ) ; } export function isArrayConstructorType ( type ) { return Boolean ( type . symbol && type . symbol . name === "ArrayConstructor" ) ; } const forbidUnaryOps = [ ts . SyntaxKind . PlusPlusToken , ts . SyntaxKind . MinusMinusToken ] ; see https const mutatorMethods = [ "copyWithin" , "fill" , "pop" , "push" , "reverse" , "shift" , "sort" , "splice" , "unshift" ] ; see https see https const newArrayReturningMethods = [ "concat" , "slice" , "filter" , "map" , "reduce" , "reduceRight" ] ; see https const constructorFunctions = [ "from" , "of" ] ; function checkTypedNode ( node , ctx , checker ) { if ( utils . isBinaryExpression ( node ) ) { return checkBinaryExpression ( node , ctx , checker ) ; } if ( utils . isDeleteExpression ( node ) ) { return checkDeleteExpression ( node , ctx , checker ) ; } if ( utils . isPrefixUnaryExpression ( node ) ) { return checkPrefixUnaryExpression ( node , ctx , checker ) ; } if ( utils . isPostfixUnaryExpression ( node ) ) { return checkPostfixUnaryExpression ( node , ctx , checker ) ; } if ( utils . isCallExpression ( node ) ) { return checkCallExpression ( node , ctx , checker ) ; } return { invalidNodes : [ ] } ; } /**
 * No assignment with array[index] on the left.
 * No assignment with array.property on the left (e.g. array.length).
 */ function checkBinaryExpression ( node , ctx , checker ) { if ( ! Ignore . isIgnored ( node . left , ctx . options . ignorePattern , ctx . options . ignorePrefix , ctx . options . ignoreSuffix ) && isAssignmentKind ( node . operatorToken . kind ) && isAccessExpression ( node . left ) ) { const leftExpressionType = checker . getTypeAtLocation ( getRootAccessExpression ( node . left ) . expression ) ; if ( isArrayType ( leftExpressionType ) ) { return { invalidNodes : [ createInvalidNode ( node , [ ] ) ] } ; } } return { invalidNodes : [ ] } ; } /**
 * No deleting array properties/values.
 */ function checkDeleteExpression ( node , ctx , checker ) { if ( ! Ignore . isIgnored ( node . expression , ctx . options . ignorePattern , ctx . options . ignorePrefix , ctx . options . ignoreSuffix ) && isAccessExpression ( node . expression ) ) { const expressionType = checker . getTypeAtLocation ( getRootAccessExpression ( node . expression ) . expression ) ; if ( isArrayType ( expressionType ) ) { return { invalidNodes : [ createInvalidNode ( node , [ ] ) ] } ; } } return { invalidNodes : [ ] } ; } /**
 * No prefix inc/dec.
 */ function checkPrefixUnaryExpression ( node , ctx , checker ) { if ( ! Ignore . isIgnored ( node . operand , ctx . options . ignorePattern , ctx . options . ignorePrefix , ctx . options . ignoreSuffix ) && isAccessExpression ( node . operand ) && forbidUnaryOps . some ( o => o === node . operator ) ) { const operandExpressionType = checker . getTypeAtLocation ( getRootAccessExpression ( node . operand ) . expression ) ; if ( isArrayType ( operandExpressionType ) ) { return { invalidNodes : [ createInvalidNode ( node , [ ] ) ] } ; } } return { invalidNodes : [ ] } ; } /**
 * No postfix inc/dec.
 */ function checkPostfixUnaryExpression ( node , ctx , checker ) { if ( ! Ignore . isIgnored ( node . operand , ctx . options . ignorePattern , ctx . options . ignorePrefix , ctx . options . ignoreSuffix ) && isAccessExpression ( node . operand ) && forbidUnaryOps . some ( o => o === node . operator ) ) { const operandExpressionType = checker . getTypeAtLocation ( getRootAccessExpression ( node . operand ) . expression ) ; if ( isArrayType ( operandExpressionType ) ) { return { invalidNodes : [ createInvalidNode ( node , [ ] ) ] } ; } } return { invalidNodes : [ ] } ; } /**
 * No calls to array mutating methods.
 */ function checkCallExpression ( node , ctx , checker ) { if ( ! Ignore . isIgnored ( node . expression , ctx . options . ignorePattern , ctx . options . ignorePrefix , ctx . options . ignoreSuffix ) && utils . isPropertyAccessExpression ( node . expression ) && ( ! ( ctx . options . ignoreNewArray || ctx . options . ignoreMutationFollowingAccessor ) || ! isInChainCallAndFollowsNew ( node . expression , checker ) ) && mutatorMethods . some ( m => m === ( node . expression as ts . PropertyAccessExpression ) . name . text ) ) { const expressionType = checker . getTypeAtLocation ( getRootAccessExpression ( node . expression ) . expression ) ; if ( isArrayType ( expressionType ) ) { return { invalidNodes : [ createInvalidNode ( node , [ ] ) ] } ; } } return { invalidNodes : [ ] } ; } /**
 * Check if the given the given PropertyAccessExpression is part of a chain and
 * immediately follows a method/function call that returns a new array.
 *
 * If this is the case, then the given PropertyAccessExpression is allowed to be a mutator method call.
 */ function isInChainCallAndFollowsNew ( node , checker ) { return ( utils . isArrayLiteralExpression ( node . expression ) || ( utils . isNewExpression ( node . expression ) && isArrayConstructorType ( checker . getTypeAtLocation ( node . expression . expression ) ) ) || ( utils . isCallExpression ( node . expression ) && utils . isPropertyAccessExpression ( node . expression . expression ) && constructorFunctions . some ( isExpected ( node . expression . expression . name . text ) ) && isArrayConstructorType ( checker . getTypeAtLocation ( node . expression . expression . expression ) ) ) || ( utils . isCallExpression ( node . expression ) && utils . isPropertyAccessExpression ( node . expression . expression ) && newArrayReturningMethods . some ( isExpected ( node . expression . expression . name . text ) ) ) ) ; } /**
 * Returns a function that checks if the given value is the same as the expected value.
 */ function isExpected < T > ( expected ) { return actual => actual === expected ; } function getRootAccessExpression ( n ) { if ( isAccessExpression ( n . expression ) ) { return getRootAccessExpression ( n . expression ) ; } return n ; }