From 48d6c718efd3275b90164571de7a265498c034d9 Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Tue, 2 Jul 2019 16:26:25 +1200 Subject: [PATCH] chore(no-array-mutation): Port of rule. --- src/rules/noArrayMutation.ts | 274 ++++++++- src/util/rule.ts | 33 +- src/util/typeguard.ts | 63 ++ tests/configs.ts | 3 +- tests/rules/no-array-mutation.test.ts | 850 ++++++++++++++++++++++++++ 5 files changed, 1210 insertions(+), 13 deletions(-) create mode 100644 tests/rules/no-array-mutation.test.ts diff --git a/src/rules/noArrayMutation.ts b/src/rules/noArrayMutation.ts index 38d76594b..46384f2b4 100644 --- a/src/rules/noArrayMutation.ts +++ b/src/rules/noArrayMutation.ts @@ -1,15 +1,46 @@ import { TSESTree } from "@typescript-eslint/typescript-estree"; +import { all as deepMerge } from "deepmerge"; +import { JSONSchema4 } from "json-schema"; -import { checkNode, createRule, RuleContext, RuleMetaData } from "../util/rule"; +import * as ignore from "../common/ignoreOptions"; +import { + checkNode, + createRule, + getParserServices, + ParserServices, + RuleContext, + RuleMetaData +} from "../util/rule"; +import { + isArrayConstructorType, + isArrayExpression, + isArrayType, + isCallExpression, + isIdentifier, + isMemberExpression, + isNewExpression +} from "../util/typeguard"; // The name of this rule. export const name = "no-array-mutation" as const; // The options this rule can take. -type Options = []; +type Options = [ignore.IgnorePatternOptions & ignore.IgnoreNewArrayOption]; + +// The schema for the rule options. +const schema: JSONSchema4 = [ + deepMerge([ + ignore.ignorePatternOptionsSchema, + ignore.ignoreNewArrayOptionSchema + ]) +]; // The default options for the rule. -const defaultOptions: Options = []; +const defaultOptions: Options = [ + { + ignoreNewArray: true + } +]; // The possible error messages. const errorMessages = { @@ -25,18 +56,212 @@ const meta: RuleMetaData = { recommended: "error" }, messages: errorMessages, - schema: [] + schema }; +/** + * Methods that mutate an array. + * + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/prototype#Methods#Mutator_methods + */ +const mutatorMethods: ReadonlyArray = [ + "copyWithin", + "fill", + "pop", + "push", + "reverse", + "shift", + "sort", + "splice", + "unshift" +]; + +/** + * Methods that return a new array without mutating the original. + * + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/prototype#Methods#Accessor_methods + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/prototype#Iteration_methods + */ +const newArrayReturningMethods: ReadonlyArray = [ + "concat", + "slice", + "filter", + "map", + "reduce", + "reduceRight" +]; + +/** + * Functions that create a new array. + * + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array#Methods + */ +const constructorFunctions = ["from", "of"]; + /** * Check if the given node violates this rule. */ -function check( - node: TSESTree.Node, +function checkAssignmentExpression( + node: TSESTree.AssignmentExpression, context: RuleContext ) { - // TODO: port rule. - context.report({ node, messageId: "generic" }); + if (isMemberExpression(node.left)) { + const parserServices = getParserServices(context); + + if ( + isArrayType( + parserServices.program + .getTypeChecker() + .getTypeAtLocation( + parserServices.esTreeNodeToTSNodeMap.get(node.left.object) + ) + ) + ) { + context.report({ node, messageId: "generic" }); + } + } +} + +/** + * Check if the given node violates this rule. + */ +function checkUnaryExpression( + node: TSESTree.UnaryExpression, + context: RuleContext +) { + if (node.operator === "delete" && isMemberExpression(node.argument)) { + const parserServices = getParserServices(context); + const type = parserServices.program + .getTypeChecker() + .getTypeAtLocation( + parserServices.esTreeNodeToTSNodeMap.get(node.argument.object) + ); + + if (isArrayType(type)) { + context.report({ node, messageId: "generic" }); + } + } +} + +/** + * Check if the given node violates this rule. + */ +function checkUpdateExpression( + node: TSESTree.UpdateExpression, + context: RuleContext +) { + if ( + (node.operator === "++" || node.operator === "--") && + isMemberExpression(node.argument) + ) { + const parserServices = getParserServices(context); + + if ( + isArrayType( + parserServices.program + .getTypeChecker() + .getTypeAtLocation( + parserServices.esTreeNodeToTSNodeMap.get(node.argument.object) + ) + ) + ) { + context.report({ node, messageId: "generic" }); + } + } +} + +/** + * Check if the given node violates this rule. + */ +function checkCallExpression( + node: TSESTree.CallExpression, + context: RuleContext, + [options]: Options +) { + if ( + isMemberExpression(node.callee) && + isIdentifier(node.callee.property) && + mutatorMethods.some( + m => + m === + ((node.callee as TSESTree.MemberExpression) + .property as TSESTree.Identifier).name + ) + ) { + const parserServices = getParserServices(context); + + if ( + options.ignoreNewArray && + isInChainCallAndFollowsNew(node.callee, parserServices) + ) { + return; + } + + if ( + isArrayType( + parserServices.program + .getTypeChecker() + .getTypeAtLocation( + parserServices.esTreeNodeToTSNodeMap.get(node.callee.object) + ) + ) + ) { + context.report({ node, messageId: "generic" }); + } + } +} + +/** + * Returns a function that checks if the given value is the same as the expected value. + */ +function isExpected(expected: T): (actual: T) => boolean { + return actual => actual === expected; +} + +/** + * Check if the given the given MemberExpression 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 MemberExpression is allowed to be + * a mutator method call. + */ +function isInChainCallAndFollowsNew( + node: TSESTree.MemberExpression, + parserServices: ParserServices +): boolean { + return ( + // Check for: [0, 1, 2] + isArrayExpression(node.object) || + // Check for: new Array() + ((isNewExpression(node.object) && + isArrayConstructorType( + parserServices.program + .getTypeChecker() + .getTypeAtLocation( + parserServices.esTreeNodeToTSNodeMap.get(node.object.callee) + ) + )) || + (isCallExpression(node.object) && + isMemberExpression(node.object.callee) && + isIdentifier(node.object.callee.property) && + // Check for: Object.from(iterable) + ((constructorFunctions.some( + isExpected(node.object.callee.property.name) + ) && + isArrayConstructorType( + parserServices.program + .getTypeChecker() + .getTypeAtLocation( + parserServices.esTreeNodeToTSNodeMap.get( + node.object.callee.object + ) + ) + )) || + // Check for: array.slice(0) + newArrayReturningMethods.some( + isExpected(node.object.callee.property.name) + )))) + ); } // Create the rule. @@ -44,10 +269,37 @@ export const rule = createRule({ name, meta, defaultOptions, - create(context, options) { - const _checkNode = checkNode(check, context, undefined, options); + create(context, [ignoreOptions, ...otherOptions]) { + const _checkAssignmentExpression = checkNode( + checkAssignmentExpression, + context, + ignoreOptions, + otherOptions + ); + const _checkUnaryExpression = checkNode( + checkUnaryExpression, + context, + ignoreOptions, + otherOptions + ); + const _checkUpdateExpression = checkNode( + checkUpdateExpression, + context, + ignoreOptions, + otherOptions + ); + const _checkCallExpression = checkNode( + checkCallExpression, + context, + ignoreOptions, + otherOptions + ); + return { - ExpressionStatement: _checkNode + AssignmentExpression: _checkAssignmentExpression, + UnaryExpression: _checkUnaryExpression, + UpdateExpression: _checkUpdateExpression, + CallExpression: _checkCallExpression }; } }); diff --git a/src/util/rule.ts b/src/util/rule.ts index d22ab786c..3a9385dea 100644 --- a/src/util/rule.ts +++ b/src/util/rule.ts @@ -1,4 +1,8 @@ -import { ESLintUtils, TSESTree } from "@typescript-eslint/experimental-utils"; +import { + ESLintUtils, + ParserServices as UtilParserServices, + TSESTree +} from "@typescript-eslint/experimental-utils"; import * as Rule from "@typescript-eslint/experimental-utils/dist/ts-eslint/Rule"; import { version } from "../../package.json"; @@ -20,6 +24,10 @@ export type RuleContext< Options extends BaseOptions > = Rule.RuleContext; +export type ParserServices = { + [k in keyof UtilParserServices]: Exclude; +}; + /** * Create a rule. */ @@ -66,3 +74,26 @@ export function checkNode< return check(node, context, options); }; } + +/** + * Ensure the type info is avaliable. + */ +export function getParserServices< + Context extends RuleContext +>(context: Context) { + if ( + !context.parserServices || + !context.parserServices.program || + !context.parserServices.esTreeNodeToTSNodeMap + ) { + /** + * The user needs to have configured "project" in their parserOptions + * for @typescript-eslint/parser + */ + throw new Error( + 'You have used a rule which requires parserServices to be generated. You must therefore provide a value for the "parserOptions.project" property for @typescript-eslint/parser.' + ); + } + + return context.parserServices as ParserServices; +} diff --git a/src/util/typeguard.ts b/src/util/typeguard.ts index 629d18122..a49eb1586 100644 --- a/src/util/typeguard.ts +++ b/src/util/typeguard.ts @@ -3,10 +3,37 @@ */ import { TSESTree } from "@typescript-eslint/typescript-estree"; +import ts from "typescript"; import { getForXStatement } from "./tree"; import { ForXStatement } from "./types"; +/* + * TS Types. + */ + +export type ArrayType = ts.Type & { + symbol: { + name: "Array"; + }; +}; + +export type ArrayConstructorType = ts.Type & { + symbol: { + name: "ArrayConstructor"; + }; +}; + +/* + * Node type guards. + */ + +export function isArrayExpression( + node: TSESTree.Node +): node is TSESTree.ArrayExpression { + return node.type === "ArrayExpression"; +} + export function isCallExpression( node: TSESTree.Node ): node is TSESTree.CallExpression { @@ -59,6 +86,12 @@ export function isMemberExpression( return node.type === "MemberExpression"; } +export function isNewExpression( + node: TSESTree.Node +): node is TSESTree.NewExpression { + return node.type === "NewExpression"; +} + export function isTSIndexSignature( node: TSESTree.Node ): node is TSESTree.TSIndexSignature { @@ -106,3 +139,33 @@ export function isVariableDeclarator( ): node is TSESTree.VariableDeclarator { return node.type === "VariableDeclarator"; } + +/* + * TS types type guards. + */ + +export function isUnionType(type: ts.Type): type is ts.UnionType { + return type.flags === ts.TypeFlags.Union; +} + +export function isArrayType(type: ts.Type): type is ArrayType { + if (type.symbol && type.symbol.name === "Array") { + return true; + } + if (isUnionType(type)) { + return type.types.some(isArrayType); + } + return false; +} + +export function isArrayConstructorType( + type: ts.Type +): type is ArrayConstructorType { + if (type.symbol && type.symbol.name === "ArrayConstructor") { + return true; + } + if (isUnionType(type)) { + return type.types.some(isArrayConstructorType); + } + return false; +} diff --git a/tests/configs.ts b/tests/configs.ts index 7ab6b4968..759b86373 100644 --- a/tests/configs.ts +++ b/tests/configs.ts @@ -3,7 +3,8 @@ import { Linter } from "eslint"; export const typescript: Linter.Config = { parser: require.resolve("@typescript-eslint/parser"), parserOptions: { - sourceType: "module" + sourceType: "module", + project: "./tsconfig.json" } }; diff --git a/tests/rules/no-array-mutation.test.ts b/tests/rules/no-array-mutation.test.ts new file mode 100644 index 000000000..1e569a0f9 --- /dev/null +++ b/tests/rules/no-array-mutation.test.ts @@ -0,0 +1,850 @@ +/** + * @fileoverview Tests for no-array-mutation + */ + +import dedent from "dedent"; +import { Rule, RuleTester } from "eslint"; + +import { name, rule } from "../../src/rules/noArrayMutation"; + +import { typescript } from "../configs"; +import { + InvalidTestCase, + processInvalidTestCase, + processValidTestCase, + ValidTestCase +} from "../util"; + +// Valid test cases. +const valid: ReadonlyArray = [ + // Allowed non-array mutation patterns. + { + code: dedent` + const foo = () => {}; + const bar = { + x: 1, + y: foo + }; + let x = 0; + x = 4; + x += 1; + x -= 1; + x *= 1; + x **= 1; + x /= 1; + x %= 1; + x <<= 1; + x >>= 1; + x >>>= 1; + x &= 1; + x |= 1; + x ^= 1; + delete x; + x++; + x--; + ++x; + --x; + if (x = 2) {} + if (x++) {} + bar.x = 2; + bar.x++; + --bar.x; + delete bar.x`, + optionsSet: [[]] + }, + // Allow array non-mutation methods + { + code: dedent` + const x = [5, 6]; + const y = [{ z: [3, 7] }]; + + x.concat([3, 4]); + x.includes(2); + x.indexOf(1); + x.join(', '); + x.lastIndexOf(0); + x.slice(1, 2); + x.toString(); + x.toLocaleString("en", {timeZone: "UTC"}); + + y[0].z.concat([3, 4]); + y[0].z.includes(2); + y[0].z.indexOf(1); + y[0].z.join(', '); + y[0].z.lastIndexOf(0); + y[0].z.slice(1, 2); + y[0].z.toString(); + y[0].z.toLocaleString("en", {timeZone: "UTC"});`, + optionsSet: [[]] + }, + // Allowed array mutation methods to be chained to the creation of an array. + { + code: dedent` + [0, 1, 2].copyWithin(0, 1, 2); + [0, 1, 2].fill(3); + [0, 1, 2].pop(); + [0, 1, 2].push(3); + [0, 1, 2].reverse(); + [0, 1, 2].shift(); + [0, 1, 2].sort(); + [0, 1, 2].splice(0, 1, 9); + [0, 1, 2].unshift(6); + + new Array(5).copyWithin(0, 1, 2); + new Array(5).fill(3); + new Array(5).pop(); + new Array(5).push(3); + new Array(5).reverse(); + new Array(5).shift(); + new Array(5).sort(); + new Array(5).splice(0, 1, 9); + new Array(5).unshift(6);`, + optionsSet: [[]] + }, + // Allowed array mutation methods to be chained to array constructor functions. + { + code: dedent` + Array.of(0, 1, 2).copyWithin(0, 1, 2); + Array.of(0, 1, 2).fill(3); + Array.of(0, 1, 2).pop(); + Array.of(0, 1, 2).push(3); + Array.of(0, 1, 2).reverse(); + Array.of(0, 1, 2).shift(); + Array.of(0, 1, 2).sort(); + Array.of(0, 1, 2).splice(0, 1, 9); + Array.of(0, 1, 2).unshift(6); + + Array.from({ length: 10 }).copyWithin(0, 1, 2); + Array.from({ length: 10 }).fill(3); + Array.from({ length: 10 }).pop(); + Array.from({ length: 10 }).push(3); + Array.from({ length: 10 }).reverse(); + Array.from({ length: 10 }).shift(); + Array.from({ length: 10 }).sort(); + Array.from({ length: 10 }).splice(0, 1, 9); + Array.from({ length: 10 }).unshift(6);`, + optionsSet: [[]] + }, + // Allowed array mutation methods to be chained to array accessor/iteration methods. + { + code: dedent` + x.slice().copyWithin(0, 1, 2); + x.slice().fill(3); + x.slice().pop(); + x.slice().push(3); + x.slice().reverse(); + x.slice().shift(); + x.slice().sort(); + x.slice().splice(0, 1, 9); + x.slice().unshift(6); + + x.concat([1, 2, 3]).copyWithin(0, 1, 2); + x.concat([1, 2, 3]).fill(3); + x.concat([1, 2, 3]).pop(); + x.concat([1, 2, 3]).push(3); + x.concat([1, 2, 3]).reverse(); + x.concat([1, 2, 3]).shift(); + x.concat([1, 2, 3]).sort(); + x.concat([1, 2, 3]).splice(0, 1, 9); + x.concat([1, 2, 3]).unshift(6); + + x.filter(v => v > 1).copyWithin(0, 1, 2); + x.filter(v => v > 1).fill(3); + x.filter(v => v > 1).pop(); + x.filter(v => v > 1).push(3); + x.filter(v => v > 1).reverse(); + x.filter(v => v > 1).shift(); + x.filter(v => v > 1).sort(); + x.filter(v => v > 1).splice(0, 1, 9); + x.filter(v => v > 1).unshift(6); + + x.map(v => v * 2).copyWithin(0, 1, 2); + x.map(v => v * 2).fill(3); + x.map(v => v * 2).pop(); + x.map(v => v * 2).push(3); + x.map(v => v * 2).reverse(); + x.map(v => v * 2).shift(); + x.map(v => v * 2).sort(); + x.map(v => v * 2).splice(0, 1, 9); + x.map(v => v * 2).unshift(6); + + x.reduce((r, v) => [...r, v + 1], []).copyWithin(0, 1, 2); + x.reduce((r, v) => [...r, v + 1], []).fill(3); + x.reduce((r, v) => [...r, v + 1], []).pop(); + x.reduce((r, v) => [...r, v + 1], []).push(3); + x.reduce((r, v) => [...r, v + 1], []).reverse(); + x.reduce((r, v) => [...r, v + 1], []).shift(); + x.reduce((r, v) => [...r, v + 1], []).sort(); + x.reduce((r, v) => [...r, v + 1], []).splice(0, 1, 9); + x.reduce((r, v) => [...r, v + 1], []).unshift(6); + + x.reduceRight((r, v) => [...r, v + 1], []).copyWithin(0, 1, 2); + x.reduceRight((r, v) => [...r, v + 1], []).fill(3); + x.reduceRight((r, v) => [...r, v + 1], []).pop(); + x.reduceRight((r, v) => [...r, v + 1], []).push(3); + x.reduceRight((r, v) => [...r, v + 1], []).reverse(); + x.reduceRight((r, v) => [...r, v + 1], []).shift(); + x.reduceRight((r, v) => [...r, v + 1], []).sort(); + x.reduceRight((r, v) => [...r, v + 1], []).splice(0, 1, 9); + x.reduceRight((r, v) => [...r, v + 1], []).unshift(6);`, + optionsSet: [[]] + }, + // Don't catch calls of array mutation methods on non-array objects. + { + code: dedent` + const z = { + length: 5, + copyWithin: () => {}, + fill: () => {}, + pop: () => {}, + push: () => {}, + reverse: () => {}, + shift: () => {}, + sort: () => {}, + splice: () => {}, + unshift: () => {} + }; + + z.length = 7; + z.copyWithin(); + z.fill(); + z.pop(); + z.push(); + z.reverse(); + z.shift(); + z.sort(); + z.splice(); + z.unshift();`, + optionsSet: [[{ ignoreNewArray: false }]] + } +]; + +// Invalid test cases. +const invalid: ReadonlyArray = [ + { + code: dedent` + const x = [5, 6]; + const y = [{ z: [3, 7] }]; + x[0] = 4; + y[0].z[0] = 4; + `, + optionsSet: [[]], + errors: [ + { + messageId: "generic", + type: "AssignmentExpression", + line: 3, + column: 1 + }, + { + messageId: "generic", + type: "AssignmentExpression", + line: 4, + column: 1 + } + ] + }, + { + code: dedent` + const x = [5, 6]; + const y = [{ z: [3, 7] }]; + x[0] += 1; + y[0].z[0] += 1; + `, + optionsSet: [[]], + errors: [ + { + messageId: "generic", + type: "AssignmentExpression", + line: 3, + column: 1 + }, + { + messageId: "generic", + type: "AssignmentExpression", + line: 4, + column: 1 + } + ] + }, + { + code: dedent` + const x = [5, 6]; + const y = [{ z: [3, 7] }]; + x[0] -= 1; + y[0].z[0] -= 1; + `, + optionsSet: [[]], + errors: [ + { + messageId: "generic", + type: "AssignmentExpression", + line: 3, + column: 1 + }, + { + messageId: "generic", + type: "AssignmentExpression", + line: 4, + column: 1 + } + ] + }, + { + code: dedent` + const x = [5, 6]; + const y = [{ z: [3, 7] }]; + x[0] *= 2; + y[0].z[0] *= 2; + `, + optionsSet: [[]], + errors: [ + { + messageId: "generic", + type: "AssignmentExpression", + line: 3, + column: 1 + }, + { + messageId: "generic", + type: "AssignmentExpression", + line: 4, + column: 1 + } + ] + }, + { + code: dedent` + const x = [5, 6]; + const y = [{ z: [3, 7] }]; + x[0] **= 2; + y[0].z[0] **= 2; + `, + optionsSet: [[]], + errors: [ + { + messageId: "generic", + type: "AssignmentExpression", + line: 3, + column: 1 + }, + { + messageId: "generic", + type: "AssignmentExpression", + line: 4, + column: 1 + } + ] + }, + { + code: dedent` + const x = [5, 6]; + const y = [{ z: [3, 7] }]; + x[0] /= 1; + y[0].z[0] /= 1; + `, + optionsSet: [[]], + errors: [ + { + messageId: "generic", + type: "AssignmentExpression", + line: 3, + column: 1 + }, + { + messageId: "generic", + type: "AssignmentExpression", + line: 4, + column: 1 + } + ] + }, + { + code: dedent` + const x = [5, 6]; + const y = [{ z: [3, 7] }]; + x[0] %= 1; + y[0].z[0] %= 1; + `, + optionsSet: [[]], + errors: [ + { + messageId: "generic", + type: "AssignmentExpression", + line: 3, + column: 1 + }, + { + messageId: "generic", + type: "AssignmentExpression", + line: 4, + column: 1 + } + ] + }, + { + code: dedent` + const x = [5, 6]; + const y = [{ z: [3, 7] }]; + x[0] <<= 1; + y[0].z[0] <<= 1; + `, + optionsSet: [[]], + errors: [ + { + messageId: "generic", + type: "AssignmentExpression", + line: 3, + column: 1 + }, + { + messageId: "generic", + type: "AssignmentExpression", + line: 4, + column: 1 + } + ] + }, + { + code: dedent` + const x = [5, 6]; + const y = [{ z: [3, 7] }]; + x[0] >>= 1; + y[0].z[0] >>= 1; + `, + optionsSet: [[]], + errors: [ + { + messageId: "generic", + type: "AssignmentExpression", + line: 3, + column: 1 + }, + { + messageId: "generic", + type: "AssignmentExpression", + line: 4, + column: 1 + } + ] + }, + { + code: dedent` + const x = [5, 6]; + const y = [{ z: [3, 7] }]; + x[0] >>>= 1; + y[0].z[0] >>>= 1; + `, + optionsSet: [[]], + errors: [ + { + messageId: "generic", + type: "AssignmentExpression", + line: 3, + column: 1 + }, + { + messageId: "generic", + type: "AssignmentExpression", + line: 4, + column: 1 + } + ] + }, + { + code: dedent` + const x = [5, 6]; + const y = [{ z: [3, 7] }]; + x[0] &= 1; + y[0].z[0] &= 1; + `, + optionsSet: [[]], + errors: [ + { + messageId: "generic", + type: "AssignmentExpression", + line: 3, + column: 1 + }, + { + messageId: "generic", + type: "AssignmentExpression", + line: 4, + column: 1 + } + ] + }, + { + code: dedent` + const x = [5, 6]; + const y = [{ z: [3, 7] }]; + x[0] |= 1; + y[0].z[0] |= 1; + `, + optionsSet: [[]], + errors: [ + { + messageId: "generic", + type: "AssignmentExpression", + line: 3, + column: 1 + }, + { + messageId: "generic", + type: "AssignmentExpression", + line: 4, + column: 1 + } + ] + }, + { + code: dedent` + const x = [5, 6]; + const y = [{ z: [3, 7] }]; + x[0] ^= 1; + y[0].z[0] ^= 1; + `, + optionsSet: [[]], + errors: [ + { + messageId: "generic", + type: "AssignmentExpression", + line: 3, + column: 1 + }, + { + messageId: "generic", + type: "AssignmentExpression", + line: 4, + column: 1 + } + ] + }, + { + code: dedent` + const x = [5, 6]; + const y = [{ z: [3, 7] }]; + delete x[0]; + delete y[0].z[0]; + `, + optionsSet: [[]], + errors: [ + { + messageId: "generic", + type: "UnaryExpression", + line: 3, + column: 1 + }, + { + messageId: "generic", + type: "UnaryExpression", + line: 4, + column: 1 + } + ] + }, + { + code: dedent` + const x = [5, 6]; + const y = [{ z: [3, 7] }]; + x[0]++; + y[0].z[0]++; + `, + optionsSet: [[]], + errors: [ + { + messageId: "generic", + type: "UpdateExpression", + line: 3, + column: 1 + }, + { + messageId: "generic", + type: "UpdateExpression", + line: 4, + column: 1 + } + ] + }, + { + code: dedent` + const x = [5, 6]; + const y = [{ z: [3, 7] }]; + x[0]--; + y[0].z[0]--; + `, + optionsSet: [[]], + errors: [ + { + messageId: "generic", + type: "UpdateExpression", + line: 3, + column: 1 + }, + { + messageId: "generic", + type: "UpdateExpression", + line: 4, + column: 1 + } + ] + }, + { + code: dedent` + const x = [5, 6]; + const y = [{ z: [3, 7] }]; + ++x[0]; + ++y[0].z[0]; + `, + optionsSet: [[]], + errors: [ + { + messageId: "generic", + type: "UpdateExpression", + line: 3, + column: 1 + }, + { + messageId: "generic", + type: "UpdateExpression", + line: 4, + column: 1 + } + ] + }, + { + code: dedent` + const x = [5, 6]; + const y = [{ z: [3, 7] }]; + --x[0]; + --y[0].z[0]; + `, + optionsSet: [[]], + errors: [ + { + messageId: "generic", + type: "UpdateExpression", + line: 3, + column: 1 + }, + { + messageId: "generic", + type: "UpdateExpression", + line: 4, + column: 1 + } + ] + }, + { + code: dedent` + const x = [5, 6]; + const y = [{ z: [3, 7] }]; + if (x[0] = 2) {} + if (y[0].z[0] = 2) {} + `, + optionsSet: [[]], + errors: [ + { + messageId: "generic", + type: "AssignmentExpression", + line: 3, + column: 5 + }, + { + messageId: "generic", + type: "AssignmentExpression", + line: 4, + column: 5 + } + ] + }, + { + code: dedent` + const x = [5, 6]; + const y = [{ z: [3, 7] }]; + if (x[0]++) {} + if (y[0].z[0]++) {} + `, + optionsSet: [[]], + errors: [ + { + messageId: "generic", + type: "UpdateExpression", + line: 3, + column: 5 + }, + { + messageId: "generic", + type: "UpdateExpression", + line: 4, + column: 5 + } + ] + }, + { + code: dedent` + const x = [5, 6]; + const y = [{ z: [3, 7] }]; + x.length = 5; + y[0].z.length = 1; + `, + optionsSet: [[]], + errors: [ + { + messageId: "generic", + type: "AssignmentExpression", + line: 3, + column: 1 + }, + { + messageId: "generic", + type: "AssignmentExpression", + line: 4, + column: 1 + } + ] + }, + // Disallowed array mutation methods. + { + code: dedent` + const x = [5, 6]; + x.copyWithin(0, 1, 2); + x.fill(3); + x.pop(); + x.push(3); + x.reverse(); + x.shift(); + x.sort(); + x.splice(0, 1, 9); + x.unshift(6); + const y = [{ z: [3, 7] }]; + y[0].z.copyWithin(0, 1, 2); + y[0].z.fill(3); + y[0].z.pop(); + y[0].z.push(3); + y[0].z.reverse(); + y[0].z.shift(); + y[0].z.sort(); + y[0].z.splice(0, 1, 9); + y[0].z.unshift(6);`, + optionsSet: [[{ ignoreNewArray: false }]], + errors: [ + { + messageId: "generic", + type: "CallExpression", + line: 2, + column: 1 + }, + { + messageId: "generic", + type: "CallExpression", + line: 3, + column: 1 + }, + { + messageId: "generic", + type: "CallExpression", + line: 4, + column: 1 + }, + { + messageId: "generic", + type: "CallExpression", + line: 5, + column: 1 + }, + { + messageId: "generic", + type: "CallExpression", + line: 6, + column: 1 + }, + { + messageId: "generic", + type: "CallExpression", + line: 7, + column: 1 + }, + { + messageId: "generic", + type: "CallExpression", + line: 8, + column: 1 + }, + { + messageId: "generic", + type: "CallExpression", + line: 9, + column: 1 + }, + { + messageId: "generic", + type: "CallExpression", + line: 10, + column: 1 + }, + { + messageId: "generic", + type: "CallExpression", + line: 12, + column: 1 + }, + { + messageId: "generic", + type: "CallExpression", + line: 13, + column: 1 + }, + { + messageId: "generic", + type: "CallExpression", + line: 14, + column: 1 + }, + { + messageId: "generic", + type: "CallExpression", + line: 15, + column: 1 + }, + { + messageId: "generic", + type: "CallExpression", + line: 16, + column: 1 + }, + { + messageId: "generic", + type: "CallExpression", + line: 17, + column: 1 + }, + { + messageId: "generic", + type: "CallExpression", + line: 18, + column: 1 + }, + { + messageId: "generic", + type: "CallExpression", + line: 19, + column: 1 + }, + { + messageId: "generic", + type: "CallExpression", + line: 20, + column: 1 + } + ] + } +]; + +describe("TypeScript", () => { + const ruleTester = new RuleTester(typescript); + ruleTester.run(name, rule as Rule.RuleModule, { + valid: processValidTestCase(valid), + invalid: processInvalidTestCase(invalid) + }); +});