diff --git a/.eslintrc b/.eslintrc index 8972db1c2..14feb3d4d 100644 --- a/.eslintrc +++ b/.eslintrc @@ -31,9 +31,8 @@ "no-var": "error", // Our rules. + "ts-immutable/immutable-data": "error", "ts-immutable/no-let": "error", - "ts-immutable/no-array-mutation": "error", - "ts-immutable/no-object-mutation": "error", "ts-immutable/no-delete": "error", "ts-immutable/readonly-array": ["error", { "ignoreReturnType": true }], "ts-immutable/readonly-keyword": "error", diff --git a/README.md b/README.md index 198faa426..8456fe8a0 100644 --- a/README.md +++ b/README.md @@ -115,8 +115,7 @@ In addition to immutable rules this project also contains a few rules for enforc | [`ts-immutable/readonly-keyword`](./docs/rules/readonly-keyword.md) | Enforce readonly modifiers are used where possible | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :wrench: | :thought_balloon: | | [`ts-immutable/readonly-array`](./docs/rules/readonly-array.md) | Enforce readonly array over mutable arrays | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :wrench: | :thought_balloon: | | [`ts-immutable/no-let`](./docs/rules/no-let.md) | Disallow mutable variables | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :wrench: | | -| [`ts-immutable/no-array-mutation`](./docs/rules/no-array-mutation.md) | Disallow mutating arrays | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | :thought_balloon: | -| [`ts-immutable/no-object-mutation`](./docs/rules/no-object-mutation.md) | Disallow mutating objects | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | :blue_heart: | +| [`ts-immutable/immutable-data`](./docs/rules/immutable-data.md) | Disallow mutating objects and arrays | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | :blue_heart: | | [`ts-immutable/no-method-signature`](./docs/rules/no-method-signature.md) | Enforce property signatures with readonly modifiers over method signatures | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | :thought_balloon: | | [`ts-immutable/no-delete`](./docs/rules/no-delete.md) | Disallow delete expressions | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | | diff --git a/docs/rules/immutable-data.md b/docs/rules/immutable-data.md new file mode 100644 index 000000000..ff0797757 --- /dev/null +++ b/docs/rules/immutable-data.md @@ -0,0 +1,91 @@ +# Disallow mutating objects and arrays (immutable-data) + +This rule prohibits syntax that mutates existing objects and arrays via assignment to or deletion of their properties/elements. + +## Rule Details + +While requiring the `readonly` modifier forces declared types to be immutable, it won't stop assignment into or modification of untyped objects or external types declared under different rules. + +```typescript +const x = { a: 1 }; +const y = [0, 1, 2]; + +x.foo = "bar"; // <- Modifying an existing object/array is not allowed. +x.a += 1; // <- Modifying an existing object/array is not allowed. +delete x.a; // <- Modifying an existing object/array is not allowed. +Object.assign(x, { b: 2 }); // <- Modifying properties of existing object not allowed. + +y[0] = 4; // <- Modifying an array is not allowed. +y.length = 1; // <- Modifying an array is not allowed. +y.push(3); // <- Modifying an array is not allowed. +``` + +## Options + +The rule accepts an options object with the following properties: + +```typescript +type Options = { + readonly ignorePattern?: string | Array; + readonly ignoreAccessorPattern?: string | Array; + readonly assumeTypes: + | boolean + | { + readonly forArrays?: boolean; + readonly forObjects?: boolean; + } +}; + +const defaults = { + assumeTypes: true +}; +``` + +### `assumeTypes` + +The rule take advantage of TypeScript's typing engine to check if mutation is taking place. +If you are not using TypeScript, type checking cannot be performed; hence this option exists. + +This option will make the rule assume the type of the nodes it is checking are of type Array/Object. +This may however result in some false positives being picked up. + +```typescript +const x = [0, 1, 2]; +x.push(3); // This will not be flagged as an issue if this option is disabled. + // This is due to the fact that without a typing engine, we cannot tell that x is an array. +``` + +Note: This option will have no effect if the TypeScript typing engine is avaliable (i.e. you are using TypeScript and have configured eslint correctly). + +### `ignorePattern` + +See the [ignorePattern](./options/ignore-pattern.md) docs. + +### `ignoreAccessorPattern` + +This option takes a match string or an array of match strings (not a RegExp pattern). + +The match string allows you to specify dot seperated `.` object paths and has support for "glob" `*` and "globstar" `**` matching. + +For example: + +```js +{ + // Ignore all mutations directly on top-level objects that are prefixed with "mutable_". + "ignorePattern": "mutable_*" +} +``` + +```js +{ + // Ignore all mutations directly on all objects, and any of their deeply nested properties, where that object is prefixed with "mutable_". + "ignorePattern": "**.mutable_*.**" +} +``` + +#### Wildcards + +The following wildcards can be used when specifing a pattern: + +`**` - Match any depth (including zero). Can only be used as a full accessor. +`*` - When used as a full accessor, match the next accessor. When used as part of an accessor, match any characters. diff --git a/docs/rules/no-array-mutation.md b/docs/rules/no-array-mutation.md deleted file mode 100644 index c7075a493..000000000 --- a/docs/rules/no-array-mutation.md +++ /dev/null @@ -1,48 +0,0 @@ -# Disallow mutating arrays (no-array-mutation) - -This rule prohibits mutating an array via assignment to or deletion of their elements/properties. This rule enforces array immutability without the use of `ReadonlyArray` (as apposed to [readonly-array](./readonly-array.md)). - -## Rule Details - -```typescript -const x = [0, 1, 2]; - -x[0] = 4; // <- Mutating an array is not allowed. -x.length = 1; // <- Mutating an array is not allowed. -x.push(3); // <- Mutating an array is not allowed. -``` - -## Options - -The rule accepts an options object with the following properties: - -```typescript -type Options = { - readonly ignoreNewArray?: boolean; - readonly ignorePattern?: string | Array; - readonly ignoreAccessorPattern?: string | Array; -}; - -const defaults = { - ignoreNewArray: true -}; -``` - -### `ignore-new-array` - -This option allows for the use of array mutator methods to be chained to newly created arrays. - -For example, an array can be immutably sorted like so: - -```typescript -const original = ["foo", "bar", "baz"]; -const sorted = original.slice().sort((a, b) => a.localeCompare(b)); // This is OK with ignore-new-array - note the use of the `slice` method which returns a copy of the original array. -``` - -### `ignorePattern` - -See the [ignorePattern](./options/ignore-pattern.md) docs. - -### `ignoreAccessorPattern` - -See the [ignoreAccessorPattern](./options/ignore-accessor-pattern.md) docs. diff --git a/docs/rules/no-object-mutation.md b/docs/rules/no-object-mutation.md deleted file mode 100644 index 3e0e1c317..000000000 --- a/docs/rules/no-object-mutation.md +++ /dev/null @@ -1,37 +0,0 @@ -# Disallow mutating objects (no-object-mutation) - -This rule prohibits syntax that mutates existing objects via assignment to or deletion of their properties. - -## Rule Details - -While requiring the `readonly` modifier forces declared types to be immutable, it won't stop assignment into or modification of untyped objects or external types declared under different rules. Forbidding forms like `a.b = 'c'` is one way to plug this hole. Inspired by the no-mutation rule of [eslint-plugin-immutable](https://github.com/jhusain/eslint-plugin-immutable). - -```typescript -const x = { a: 1 }; - -x.foo = "bar"; // <- Modifying properties of existing object not allowed. -x.a += 1; // <- Modifying properties of existing object not allowed. -delete x.a; // <- Modifying properties of existing object not allowed. -Object.assign(x, { b: 2 }); // <- Modifying properties of existing object not allowed. -``` - -## Options - -The rule accepts an options object with the following properties: - -```typescript -type Options = { - readonly ignorePattern?: string | Array; - readonly ignoreAccessorPattern?: string | Array; -}; - -const defaults = {}; -``` - -### `ignorePattern` - -See the [ignorePattern](./options/ignore-pattern.md) docs. - -### `ignoreAccessorPattern` - -See the [ignoreAccessorPattern](./options/ignore-accessor-pattern.md) docs. diff --git a/docs/rules/options/ignore-accessor-pattern.md b/docs/rules/options/ignore-accessor-pattern.md deleted file mode 100644 index 39ea51e6a..000000000 --- a/docs/rules/options/ignore-accessor-pattern.md +++ /dev/null @@ -1,28 +0,0 @@ -# Using the `ignoreAccessorPattern` option - -This option takes a match string or an array of match strings (not a RegExp pattern). - -The match string allows you to specify dot seperated `.` object paths and has support for "glob" `*` and "globstar" `**` matching. - -For example: - -```js -{ - // Ignore all mutations directly on top-level objects that are prefixed with "mutable_". - "ignorePattern": "mutable_*" -} -``` - -```js -{ - // Ignore all mutations directly on all objects, and any of their deeply nested properties, where that object is prefixed with "mutable_". - "ignorePattern": "**.mutable_*.**" -} -``` - -## Wildcards - -The following wildcards can be used when specifing a pattern: - -`**` - Match any depth (including zero). Can only be used as a full accessor. -`*` - When used as a full accessor, match the next accessor. When used as part of an accessor, match any characters. diff --git a/src/common/types-options.ts b/src/common/types-options.ts new file mode 100644 index 000000000..67bb6cbc9 --- /dev/null +++ b/src/common/types-options.ts @@ -0,0 +1,36 @@ +import { JSONSchema4 } from "json-schema"; + +export type AssumeTypesOption = { + readonly assumeTypes: + | boolean + | { + readonly forArrays?: boolean; + readonly forObjects?: boolean; + }; +}; + +export const assumeTypesOptionSchema: JSONSchema4 = { + type: "object", + properties: { + assumeTypes: { + oneOf: [ + { + type: "boolean" + }, + { + type: "object", + properties: { + forArrays: { + type: "boolean" + }, + forObjects: { + type: "boolean" + } + }, + additionalProperties: false + } + ] + } + }, + additionalProperties: false +}; diff --git a/src/configs/all.ts b/src/configs/all.ts index bacc8c20e..865719411 100644 --- a/src/configs/all.ts +++ b/src/configs/all.ts @@ -1,13 +1,12 @@ const config = { rules: { - "ts-immutable/no-array-mutation": "error", + "ts-immutable/immutable-data": "error", "ts-immutable/no-class": "error", "ts-immutable/no-delete": "error", "ts-immutable/no-expression-statement": "error", "ts-immutable/no-if-statement": "error", "ts-immutable/no-let": "error", "ts-immutable/no-loop-statement": "error", - "ts-immutable/no-object-mutation": "error", "ts-immutable/no-reject": "error", "ts-immutable/no-this": "error", "ts-immutable/no-throw": "error", diff --git a/src/configs/immutable.ts b/src/configs/immutable.ts index 5bfb4f21c..9637bff70 100644 --- a/src/configs/immutable.ts +++ b/src/configs/immutable.ts @@ -7,14 +7,13 @@ const config = deepMerge([ { rules: { "ts-immutable/no-let": "error", - "ts-immutable/no-object-mutation": "error", + "ts-immutable/immutable-data": "error", "ts-immutable/no-delete": "error" }, overrides: [ { files: ["*.ts", "*.tsx"], rules: { - "ts-immutable/no-array-mutation": "error", "ts-immutable/no-method-signature": "warn", "ts-immutable/readonly-array": "error", "ts-immutable/readonly-keyword": "error" diff --git a/src/rules/no-array-mutation.ts b/src/rules/immutable-data.ts similarity index 60% rename from src/rules/no-array-mutation.ts rename to src/rules/immutable-data.ts index 32e79f3fc..1226f520c 100644 --- a/src/rules/no-array-mutation.ts +++ b/src/rules/immutable-data.ts @@ -3,6 +3,11 @@ import { all as deepMerge } from "deepmerge"; import { JSONSchema4 } from "json-schema"; import * as ignore from "../common/ignore-options"; +import { + AssumeTypesOption, + assumeTypesOptionSchema +} from "../common/types-options"; +import { isExpected } from "../util/misc"; import { checkNode, createRule, @@ -11,6 +16,7 @@ import { RuleMetaData, RuleResult } from "../util/rule"; +import { inConstructor } from "../util/tree"; import { isArrayConstructorType, isArrayExpression, @@ -18,17 +24,18 @@ import { isCallExpression, isIdentifier, isMemberExpression, - isNewExpression + isNewExpression, + isObjectConstructorType } from "../util/typeguard"; // The name of this rule. -export const name = "no-array-mutation" as const; +export const name = "immutable-data" as const; // The options this rule can take. type Options = readonly [ ignore.IgnorePatternOption & ignore.IgnoreAccessorPatternOption & - ignore.IgnoreNewArrayOption + AssumeTypesOption ]; // The schema for the rule options. @@ -36,27 +43,29 @@ const schema: JSONSchema4 = [ deepMerge([ ignore.ignorePatternOptionSchema, ignore.ignoreAccessorPatternOptionSchema, - ignore.ignoreNewArrayOptionSchema + assumeTypesOptionSchema ]) ]; // The default options for the rule. const defaultOptions: Options = [ { - ignoreNewArray: true + assumeTypes: true } ]; // The possible error messages. const errorMessages = { - generic: "Mutating an array is not allowed." + generic: "Modifying an existing object/array is not allowed.", + object: "Modifying properties of existing object not allowed.", + array: "Modifying an array is not allowed." } as const; // The meta data for this rule. const meta: RuleMetaData = { type: "suggestion", docs: { - description: "Disallow mutating arrays.", + description: "Enforce treating data as immutable.", category: "Best Practices", recommended: "error" }, @@ -65,11 +74,11 @@ const meta: RuleMetaData = { }; /** - * Methods that mutate an array. + * Array 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 = [ +const arrayMutatorMethods = [ "copyWithin", "fill", "pop", @@ -82,12 +91,12 @@ const mutatorMethods = [ ] as const; /** - * Methods that return a new array without mutating the original. + * Array methods that return a new object (or 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 = [ +const arrayNewObjectReturningMethods = [ "concat", "slice", "filter", @@ -97,14 +106,14 @@ const newArrayReturningMethods = [ ] as const; /** - * Functions that create a new array. + * Array constructor 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"] as const; +const arrayConstructorFunctions = ["from", "of"] as const; /** - * Check if the given node violates this rule. + * Check if the given assignment expression violates this rule. */ function checkAssignmentExpression( node: TSESTree.AssignmentExpression, @@ -114,7 +123,8 @@ function checkAssignmentExpression( context, descriptors: isMemberExpression(node.left) && - isArrayType(getTypeOfNode(node.left.object, context)) + // Ignore if in a constructor - allow for field initialization. + !inConstructor(node) ? [{ node, messageId: "generic" }] : [] }; @@ -130,9 +140,7 @@ function checkUnaryExpression( return { context, descriptors: - node.operator === "delete" && - isMemberExpression(node.argument) && - isArrayType(getTypeOfNode(node.argument.object, context)) + node.operator === "delete" && isMemberExpression(node.argument) ? [{ node, messageId: "generic" }] : [] }; @@ -147,11 +155,9 @@ function checkUpdateExpression( ): RuleResult { return { context, - descriptors: - isMemberExpression(node.argument) && - isArrayType(getTypeOfNode(node.argument.object, context)) - ? [{ node, messageId: "generic" }] - : [] + descriptors: isMemberExpression(node.argument) + ? [{ node, messageId: "generic" }] + : [] }; } @@ -163,32 +169,52 @@ function checkCallExpression( context: RuleContext, [options]: Options ): RuleResult { + const assumeTypesForArrays = + options.assumeTypes === true || + (options.assumeTypes !== false && Boolean(options.assumeTypes.forArrays)); + const assumeTypesForObjects = + options.assumeTypes === true || + (options.assumeTypes !== false && Boolean(options.assumeTypes.forObjects)); + return { context, descriptors: - isMemberExpression(node.callee) && - isIdentifier(node.callee.property) && - mutatorMethods.some( - m => - m === - ((node.callee as TSESTree.MemberExpression) - .property as TSESTree.Identifier).name - ) && - (!options.ignoreNewArray || - !isInChainCallAndFollowsNew(node.callee, context)) && - isArrayType(getTypeOfNode(node.callee.object, context)) - ? [{ node, messageId: "generic" }] + // Potential object mutation? + isMemberExpression(node.callee) && isIdentifier(node.callee.property) + ? // Potential array mutation? + arrayMutatorMethods.some( + m => + m === + ((node.callee as TSESTree.MemberExpression) + .property as TSESTree.Identifier).name + ) && + !isInChainCallAndFollowsNew( + node.callee, + context, + assumeTypesForArrays + ) && + isArrayType( + getTypeOfNode(node.callee.object, context), + assumeTypesForArrays, + node.callee.object + ) + ? [{ node, messageId: "array" }] + : // Potential non-array object mutation (Object.assign on identifier)? + node.callee.property.name === "assign" && + node.arguments.length >= 2 && + (isIdentifier(node.arguments[0]) || + isMemberExpression(node.arguments[0])) && + isObjectConstructorType( + getTypeOfNode(node.callee.object, context), + assumeTypesForObjects, + node.callee.object + ) + ? [{ node, messageId: "object" }] + : [] : [] }; } -/** - * 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. @@ -198,26 +224,33 @@ function isExpected(expected: T): (actual: T) => boolean { */ function isInChainCallAndFollowsNew( node: TSESTree.MemberExpression, - context: RuleContext + context: RuleContext, + assumeArrayTypes: boolean ): boolean { return ( // Check for: [0, 1, 2] isArrayExpression(node.object) || // Check for: new Array() ((isNewExpression(node.object) && - isArrayConstructorType(getTypeOfNode(node.object.callee, context))) || + isArrayConstructorType( + getTypeOfNode(node.object.callee, context), + assumeArrayTypes, + node.object.callee + )) || (isCallExpression(node.object) && isMemberExpression(node.object.callee) && isIdentifier(node.object.callee.property) && // Check for: Object.from(iterable) - ((constructorFunctions.some( + ((arrayConstructorFunctions.some( isExpected(node.object.callee.property.name) ) && isArrayConstructorType( - getTypeOfNode(node.object.callee.object, context) + getTypeOfNode(node.object.callee.object, context), + assumeArrayTypes, + node.object.callee.object )) || // Check for: array.slice(0) - newArrayReturningMethods.some( + arrayNewObjectReturningMethods.some( isExpected(node.object.callee.property.name) )))) ); @@ -247,6 +280,8 @@ export const rule = createRule({ ignoreOptions, otherOptions ); + // This functionality is only avaliable if the parser services are + // avaliable. const _checkCallExpression = checkNode( checkCallExpression, context, diff --git a/src/rules/index.ts b/src/rules/index.ts index fd18425e5..1ed4754d6 100644 --- a/src/rules/index.ts +++ b/src/rules/index.ts @@ -1,7 +1,7 @@ import { - name as noArrayMutationRuleName, - rule as noArrayMutationRule -} from "./no-array-mutation"; + name as immutableDataRuleName, + rule as immutableDataRule +} from "./immutable-data"; import { name as noClassRuleName, rule as noClassRule } from "./no-class"; import { name as noDeleteRuleName, rule as noDeleteRule } from "./no-delete"; import { @@ -17,14 +17,6 @@ import { name as noLoopRuleName, rule as noLoopRule } from "./no-loop-statement"; -import { - name as noObjectMutationRuleName, - rule as noObjectMutationRule -} from "./no-object-mutation"; -import { name as noRejectRuleName, rule as noRejectRule } from "./no-reject"; -import { name as noThisRuleName, rule as noThisRule } from "./no-this"; -import { name as noThrowRuleName, rule as noThrowRule } from "./no-throw"; -import { name as noTryRuleName, rule as noTryRule } from "./no-try"; import { name as noMethodSignatureRuleName, rule as noMethodSignatureRule @@ -33,6 +25,10 @@ import { name as noMixedInterfaceRuleName, rule as noMixedInterfaceRule } from "./no-mixed-interface"; +import { name as noRejectRuleName, rule as noRejectRule } from "./no-reject"; +import { name as noThisRuleName, rule as noThisRule } from "./no-this"; +import { name as noThrowRuleName, rule as noThrowRule } from "./no-throw"; +import { name as noTryRuleName, rule as noTryRule } from "./no-try"; import { name as readonlyArrayRuleName, rule as readonlyArrayRule @@ -46,7 +42,7 @@ import { * All of the custom rules. */ export const rules = { - [noArrayMutationRuleName]: noArrayMutationRule, + [immutableDataRuleName]: immutableDataRule, [noClassRuleName]: noClassRule, [noDeleteRuleName]: noDeleteRule, [noExpressionStatementRuleName]: noExpressionStatementRule, @@ -55,7 +51,6 @@ export const rules = { [noLoopRuleName]: noLoopRule, [noMethodSignatureRuleName]: noMethodSignatureRule, [noMixedInterfaceRuleName]: noMixedInterfaceRule, - [noObjectMutationRuleName]: noObjectMutationRule, [noRejectRuleName]: noRejectRule, [noThisRuleName]: noThisRule, [noThrowRuleName]: noThrowRule, diff --git a/src/rules/no-object-mutation.ts b/src/rules/no-object-mutation.ts deleted file mode 100644 index 0fa59a87f..000000000 --- a/src/rules/no-object-mutation.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { TSESTree } from "@typescript-eslint/typescript-estree"; -import { all as deepMerge } from "deepmerge"; -import { JSONSchema4 } from "json-schema"; - -import * as ignore from "../common/ignore-options"; -import { - checkNode, - createRule, - getTypeOfNode, - parserServicesAvaliable, - RuleContext, - RuleMetaData, - RuleResult -} from "../util/rule"; -import { inConstructor } from "../util/tree"; -import { - isIdentifier, - isMemberExpression, - isObjectConstructorType -} from "../util/typeguard"; - -// The name of this rule. -export const name = "no-object-mutation" as const; - -// The options this rule can take. -type Options = readonly [ - ignore.IgnorePatternOption & ignore.IgnoreAccessorPatternOption -]; - -// The schema for the rule options. -const schema: JSONSchema4 = [ - deepMerge([ - ignore.ignorePatternOptionSchema, - ignore.ignoreAccessorPatternOptionSchema - ]) -]; - -// The default options for the rule. -const defaultOptions: Options = [{}]; - -// The possible error messages. -const errorMessages = { - generic: "Modifying properties of existing object not allowed." -} as const; - -// The meta data for this rule. -const meta: RuleMetaData = { - type: "suggestion", - docs: { - description: "Disallow mutating objects.", - category: "Best Practices", - recommended: "error" - }, - messages: errorMessages, - schema -}; - -/** - * Check if the given assignment expression violates this rule. - */ -function checkAssignmentExpression( - node: TSESTree.AssignmentExpression, - context: RuleContext -): RuleResult { - return { - context, - descriptors: - isMemberExpression(node.left) && - // Ignore if in a constructor - allow for field initialization. - !inConstructor(node) - ? [{ node, messageId: "generic" }] - : [] - }; -} - -/** - * Check if the given node violates this rule. - */ -function checkUnaryExpression( - node: TSESTree.UnaryExpression, - context: RuleContext -): RuleResult { - return { - context, - descriptors: - node.operator === "delete" && isMemberExpression(node.argument) - ? [{ node, messageId: "generic" }] - : [] - }; -} - -/** - * Check if the given node violates this rule. - */ -function checkUpdateExpression( - node: TSESTree.UpdateExpression, - context: RuleContext -): RuleResult { - return { - context, - descriptors: isMemberExpression(node.argument) - ? [{ node, messageId: "generic" }] - : [] - }; -} - -/** - * Check if the given node violates this rule. - */ -function checkCallExpression( - node: TSESTree.CallExpression, - context: RuleContext -): RuleResult { - // No Object.assign on identifiers. - return { - context, - descriptors: - isMemberExpression(node.callee) && - isIdentifier(node.callee.property) && - node.callee.property.name === "assign" && - node.arguments.length >= 2 && - (isIdentifier(node.arguments[0]) || - isMemberExpression(node.arguments[0])) && - // Type checking if avaliable? - ((parserServicesAvaliable(context) && - isObjectConstructorType(getTypeOfNode(node.callee.object, context))) || - // Type checking not avaliable? - (isIdentifier(node.callee.object) && - node.callee.object.name === "Object")) - ? [{ node, messageId: "generic" }] - : [] - }; -} - -// Create the rule. -export const rule = createRule({ - name, - meta, - defaultOptions, - 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 - ); - // This functionality is only avaliable if the parser services are - // avaliable. - const _checkCallExpression = checkNode( - checkCallExpression, - context, - ignoreOptions, - otherOptions - ); - - return { - AssignmentExpression: _checkAssignmentExpression, - UnaryExpression: _checkUnaryExpression, - UpdateExpression: _checkUpdateExpression, - CallExpression: _checkCallExpression - }; - } -}); diff --git a/src/util/misc.ts b/src/util/misc.ts new file mode 100644 index 000000000..b1c048a82 --- /dev/null +++ b/src/util/misc.ts @@ -0,0 +1,6 @@ +/** + * Returns a function that checks if the given value is the same as the expected value. + */ +export function isExpected(expected: T): (actual: T) => boolean { + return actual => actual === expected; +} diff --git a/src/util/rule.ts b/src/util/rule.ts index 1c298331f..b890e4834 100644 --- a/src/util/rule.ts +++ b/src/util/rule.ts @@ -113,44 +113,14 @@ export function checkNode< export function getTypeOfNode>( node: TSESTree.Node, context: Context -): Type { - const parserServices = getParserServices(context); - - return parserServices.program - .getTypeChecker() - .getTypeAtLocation(parserServices.esTreeNodeToTSNodeMap.get(node)); -} - -/** - * Ensure the type info is avaliable. - */ -export function parserServicesAvaliable< - Context extends RuleContext ->(context: Context): boolean { - return ( - context.parserServices !== undefined && - context.parserServices.program !== undefined && - context.parserServices.esTreeNodeToTSNodeMap !== undefined - ); -} - -/** - * Ensure the type info is avaliable. - */ -export function getParserServices< - Context extends RuleContext ->(context: Context): ParserServices { - /* eslint-disable-next-line ts-immutable/no-if-statement */ - if (parserServicesAvaliable(context)) { - return context.parserServices as ParserServices; - } - - /** - * The user needs to have configured "project" in their parserOptions - * for @typescript-eslint/parser - */ - /* eslint-disable-next-line ts-immutable/no-throw */ - throw new Error( - 'You have used a rule which is only avaliable for TypeScript files and requires parserServices to be generated. You must therefore provide a value for the "parserOptions.project" property for @typescript-eslint/parser.' - ); +): Type | null { + const parserServices = context.parserServices; + + return parserServices === undefined || + parserServices.program === undefined || + parserServices.esTreeNodeToTSNodeMap === undefined + ? null + : parserServices.program + .getTypeChecker() + .getTypeAtLocation(parserServices.esTreeNodeToTSNodeMap.get(node)); } diff --git a/src/util/typeguard.ts b/src/util/typeguard.ts index 9247b8623..b0594b5d0 100644 --- a/src/util/typeguard.ts +++ b/src/util/typeguard.ts @@ -155,27 +155,93 @@ export function isUnionType(type: Type): type is UnionType { return ts !== undefined && type.flags === ts.TypeFlags.Union; } -export function isArrayType(type: Type): type is ArrayType { - return ( - (type.symbol && type.symbol.name === "Array") || - (isUnionType(type) && type.types.some(isArrayType)) - ); +export function isArrayType(type: Type | null): type is ArrayType; +export function isArrayType( + type: Type, + assumeType: false, + node: null +): type is ArrayType; +export function isArrayType( + type: Type | null, + assumeType: boolean, + node: TSESTree.Node | null +): type is ArrayType; +export function isArrayType( + type: null, + assumeType: true, + node: TSESTree.Node +): boolean; +export function isArrayType( + type: Type | null, + assumeType: boolean = false, + node: TSESTree.Node | null = null +): boolean { + return assumeType === true + ? node !== null + : type !== null && + ((type.symbol && type.symbol.name === "Array") || + (isUnionType(type) && + type.types.some(t => isArrayType(t, false, null)))); } export function isArrayConstructorType( - type: Type -): type is ArrayConstructorType { - return ( - (type.symbol && type.symbol.name === "ArrayConstructor") || - (isUnionType(type) && type.types.some(isArrayConstructorType)) - ); + type: Type | null +): type is ArrayConstructorType; +export function isArrayConstructorType( + type: Type, + assumeType: false, + node: null +): type is ArrayConstructorType; +export function isArrayConstructorType( + type: Type | null, + assumeType: boolean, + node: TSESTree.Node | null +): type is ArrayConstructorType; +export function isArrayConstructorType( + type: null, + assumeType: true, + node: TSESTree.Node +): boolean; +export function isArrayConstructorType( + type: Type | null, + assumeType: boolean = false, + node: TSESTree.Node | null = null +): boolean { + return assumeType === true + ? node !== null && isIdentifier(node) && node.name === "Array" + : type !== null && + ((type.symbol && type.symbol.name === "ArrayConstructor") || + (isUnionType(type) && + type.types.some(t => isArrayConstructorType(t, false, null)))); } export function isObjectConstructorType( - type: Type -): type is ObjectConstructorType { - return ( - (type.symbol && type.symbol.name === "ObjectConstructor") || - (isUnionType(type) && type.types.some(isObjectConstructorType)) - ); + type: Type | null +): type is ObjectConstructorType; +export function isObjectConstructorType( + type: Type, + assumeType: false, + node: null +): type is ObjectConstructorType; +export function isObjectConstructorType( + type: Type | null, + assumeType: boolean, + node: TSESTree.Node | null +): type is ObjectConstructorType; +export function isObjectConstructorType( + type: null, + assumeType: true, + node: TSESTree.Node +): boolean; +export function isObjectConstructorType( + type: Type | null, + assumeType: boolean = false, + node: TSESTree.Node | null = null +): boolean { + return assumeType === true + ? node !== null && isIdentifier(node) && node.name === "Object" + : type !== null && + ((type.symbol && type.symbol.name === "ObjectConstructor") || + (isUnionType(type) && + type.types.some(t => isObjectConstructorType(t, false, null)))); } diff --git a/tests/rules/no-array-mutation.test.ts b/tests/rules/immutable-data.test.ts similarity index 57% rename from tests/rules/no-array-mutation.test.ts rename to tests/rules/immutable-data.test.ts index 55590a5a0..5b076b25a 100644 --- a/tests/rules/no-array-mutation.test.ts +++ b/tests/rules/immutable-data.test.ts @@ -1,13 +1,13 @@ /** - * @fileoverview Tests for no-array-mutation + * @fileoverview Tests for immutable-data */ import dedent from "dedent"; import { Rule, RuleTester } from "eslint"; -import { name, rule } from "../../src/rules/no-array-mutation"; +import { name, rule } from "../../src/rules/immutable-data"; -import { typescript } from "../configs"; +import { es3, es6, typescript } from "../configs"; import { InvalidTestCase, processInvalidTestCase, @@ -16,47 +16,310 @@ import { } from "../util"; // Valid test cases. -const valid: ReadonlyArray = [ +const objectES3Valid: ReadonlyArray = [ + // Allowed non-object mutation patterns. + { + code: dedent` + var y = x.a; + var z = x["a"]; + if (x.a && y.a) {} + var w = ~x.a; + if (!x.a) {}`, + optionsSet: [[]] + }, + // Allow Object.assign() on non identifiers. + { + code: dedent` + var x = { msg1: "hello", obj: { a: 1, b: 2}, func: function() {} }; + var bar = function(a, b, c) { return { a: a, b: b, c: c }; }; + + var a = Object.assign({}, { msg: "hello world" }); + var b = Object.assign(bar(1, 2, 3), { d: 4 }); + var c = Object.assign(x.func(), { d: 4 });`, + optionsSet: [[]] + } +]; + +// Invalid test cases. +const objectES3Invalid: ReadonlyArray = [ + // Disallowed object mutation patterns. + { + code: dedent` + var x = {a: 1}; + x.foo = "bar"; + x["foo"] = "bar"; + x.a += 1; + x.a -= 1; + x.a *= 1; + x.a /= 1; + x.a %= 1; + x.a <<= 1; + x.a >>= 1; + x.a >>>= 1; + x.a &= 1; + x.a |= 1; + x.a ^= 1; + delete x.a; + delete x["a"]; + x.a++; + x.a--; + ++x.a; + --x.a; + if (x.a = 2) {} + if (x.a++) {}`, + optionsSet: [[]], + errors: [ + { + messageId: "generic", + type: "AssignmentExpression", + line: 2, + column: 1 + }, + { + messageId: "generic", + type: "AssignmentExpression", + line: 3, + column: 1 + }, + { + messageId: "generic", + type: "AssignmentExpression", + line: 4, + column: 1 + }, + { + messageId: "generic", + type: "AssignmentExpression", + line: 5, + column: 1 + }, + { + messageId: "generic", + type: "AssignmentExpression", + line: 6, + column: 1 + }, + { + messageId: "generic", + type: "AssignmentExpression", + line: 7, + column: 1 + }, + { + messageId: "generic", + type: "AssignmentExpression", + line: 8, + column: 1 + }, + { + messageId: "generic", + type: "AssignmentExpression", + line: 9, + column: 1 + }, + { + messageId: "generic", + type: "AssignmentExpression", + line: 10, + column: 1 + }, + { + messageId: "generic", + type: "AssignmentExpression", + line: 11, + column: 1 + }, + { + messageId: "generic", + type: "AssignmentExpression", + line: 12, + column: 1 + }, + { + messageId: "generic", + type: "AssignmentExpression", + line: 13, + column: 1 + }, + { + messageId: "generic", + type: "AssignmentExpression", + line: 14, + column: 1 + }, + { + messageId: "generic", + type: "UnaryExpression", + line: 15, + column: 1 + }, + { + messageId: "generic", + type: "UnaryExpression", + line: 16, + column: 1 + }, + { + messageId: "generic", + type: "UpdateExpression", + line: 17, + column: 1 + }, + { + messageId: "generic", + type: "UpdateExpression", + line: 18, + column: 1 + }, + { + messageId: "generic", + type: "UpdateExpression", + line: 19, + column: 1 + }, + { + messageId: "generic", + type: "UpdateExpression", + line: 20, + column: 1 + }, + { + messageId: "generic", + type: "AssignmentExpression", + line: 21, + column: 5 + }, + { + messageId: "generic", + type: "UpdateExpression", + line: 22, + column: 5 + } + ] + }, + // Disallow Object.assign on identifers. + { + code: dedent` + var x = { msg1: "hello", obj: { a: 1, b: 2} }; + + var a = Object.assign(x, { msg2: "world" }); + var b = Object.assign(x.obj, { msg2: "world" });`, + optionsSet: [[]], + errors: [ + { + messageId: "object", + type: "CallExpression", + line: 3, + column: 9 + }, + { + messageId: "object", + type: "CallExpression", + line: 4, + column: 9 + } + ] + } +]; + +/** + * Valid tests that only apply to es6 and above. + */ +const objectES6Valid: ReadonlyArray = [ + ...objectES3Valid, + // Allow initialization of class members in constructor + { + code: dedent` + class Klass { + bar = 1; + baz: string; + constructor() { + this.baz = "hello"; + } + }`, + optionsSet: [[]] + } +]; + +/** + * Invalid tests that only apply to es6 and above. + */ +const objectES6Invalid: ReadonlyArray = [ + ...objectES3Invalid, + { + code: dedent` + const x = {a: 1}; + x.a **= 1;`, + optionsSet: [[]], + errors: [ + { + messageId: "generic", + type: "AssignmentExpression", + line: 2, + column: 1 + } + ] + }, + // No mutation in class methods. + { + code: dedent` + class Klass { + bar = 1; + baz: string; + + constructor() { + this.baz = "hello"; + } + + zoo() { + this.bar = 2; + this.baz = 3; + } + }`, + optionsSet: [[]], + errors: [ + { + messageId: "generic", + type: "AssignmentExpression", + line: 10, + column: 5 + }, + { + messageId: "generic", + type: "AssignmentExpression", + line: 11, + column: 5 + } + ] + } +]; + +const arrayES3Valid: ReadonlyArray = [ // Allowed non-array mutation patterns. { code: dedent` - const foo = () => {}; - const bar = { + var foo = function () {}; + var bar = { x: 1, y: foo }; - let x = 0; + var 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`, + if (x++) {}`, optionsSet: [[]] }, // Allow array non-mutation methods { code: dedent` - const x = [5, 6]; - const y = [{ z: [3, 7] }]; + var x = [5, 6]; + var y = [{ z: [3, 7] }]; x.concat([3, 4]); x.includes(2); @@ -148,64 +411,62 @@ const valid: ReadonlyArray = [ 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.filter(function (v) { return v > 1; }).copyWithin(0, 1, 2); + x.filter(function (v) { return v > 1; }).fill(3); + x.filter(function (v) { return v > 1; }).pop(); + x.filter(function (v) { return v > 1; }).push(3); + x.filter(function (v) { return v > 1; }).reverse(); + x.filter(function (v) { return v > 1; }).shift(); + x.filter(function (v) { return v > 1; }).sort(); + x.filter(function (v) { return v > 1; }).splice(0, 1, 9); + x.filter(function (v) { return 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.map(function (v) { return v * 2; }).copyWithin(0, 1, 2); + x.map(function (v) { return v * 2; }).fill(3); + x.map(function (v) { return v * 2; }).pop(); + x.map(function (v) { return v * 2; }).push(3); + x.map(function (v) { return v * 2; }).reverse(); + x.map(function (v) { return v * 2; }).shift(); + x.map(function (v) { return v * 2; }).sort(); + x.map(function (v) { return v * 2; }).splice(0, 1, 9); + x.map(function (v) { return 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.reduce(function (r, v) { return r.concat([v + 1]); }, []).copyWithin(0, 1, 2); + x.reduce(function (r, v) { return r.concat([v + 1]); }, []).fill(3); + x.reduce(function (r, v) { return r.concat([v + 1]); }, []).pop(); + x.reduce(function (r, v) { return r.concat([v + 1]); }, []).push(3); + x.reduce(function (r, v) { return r.concat([v + 1]); }, []).reverse(); + x.reduce(function (r, v) { return r.concat([v + 1]); }, []).shift(); + x.reduce(function (r, v) { return r.concat([v + 1]); }, []).sort(); + x.reduce(function (r, v) { return r.concat([v + 1]); }, []).splice(0, 1, 9); + x.reduce(function (r, v) { return r.concat([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);`, + x.reduceRight(function (r, v) { return r.concat([v + 1]); }, []).copyWithin(0, 1, 2); + x.reduceRight(function (r, v) { return r.concat([v + 1]); }, []).fill(3); + x.reduceRight(function (r, v) { return r.concat([v + 1]); }, []).pop(); + x.reduceRight(function (r, v) { return r.concat([v + 1]); }, []).push(3); + x.reduceRight(function (r, v) { return r.concat([v + 1]); }, []).reverse(); + x.reduceRight(function (r, v) { return r.concat([v + 1]); }, []).shift(); + x.reduceRight(function (r, v) { return r.concat([v + 1]); }, []).sort(); + x.reduceRight(function (r, v) { return r.concat([v + 1]); }, []).splice(0, 1, 9); + x.reduceRight(function (r, v) { return r.concat([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: () => {} + var z = { + copyWithin: function () {}, + fill: function () {}, + pop: function () {}, + push: function () {}, + reverse: function () {}, + shift: function () {}, + sort: function () {}, + splice: function () {}, + unshift: function () {} }; - z.length = 7; z.copyWithin(); z.fill(); z.pop(); @@ -215,16 +476,15 @@ const valid: ReadonlyArray = [ z.sort(); z.splice(); z.unshift();`, - optionsSet: [[{ ignoreNewArray: false }]] + optionsSet: [[{ assumeTypes: false }]] } ]; -// Invalid test cases. -const invalid: ReadonlyArray = [ +const arrayES3Invalid: ReadonlyArray = [ { code: dedent` - const x = [5, 6]; - const y = [{ z: [3, 7] }]; + var x = [5, 6]; + var y = [{ z: [3, 7] }]; x[0] = 4; y[0].z[0] = 4; `, @@ -246,8 +506,8 @@ const invalid: ReadonlyArray = [ }, { code: dedent` - const x = [5, 6]; - const y = [{ z: [3, 7] }]; + var x = [5, 6]; + var y = [{ z: [3, 7] }]; x[0] += 1; y[0].z[0] += 1; `, @@ -269,8 +529,8 @@ const invalid: ReadonlyArray = [ }, { code: dedent` - const x = [5, 6]; - const y = [{ z: [3, 7] }]; + var x = [5, 6]; + var y = [{ z: [3, 7] }]; x[0] -= 1; y[0].z[0] -= 1; `, @@ -292,22 +552,22 @@ const invalid: ReadonlyArray = [ }, { code: dedent` - const x = [5, 6]; - const y = [{ z: [3, 7] }]; - x[0] *= 2; - y[0].z[0] *= 2; + var x = [5, 6]; + var y = [{ z: [3, 7] }]; + delete x[0]; + delete y[0].z[0]; `, optionsSet: [[]], errors: [ { messageId: "generic", - type: "AssignmentExpression", + type: "UnaryExpression", line: 3, column: 1 }, { messageId: "generic", - type: "AssignmentExpression", + type: "UnaryExpression", line: 4, column: 1 } @@ -315,22 +575,22 @@ const invalid: ReadonlyArray = [ }, { code: dedent` - const x = [5, 6]; - const y = [{ z: [3, 7] }]; - x[0] **= 2; - y[0].z[0] **= 2; + var x = [5, 6]; + var y = [{ z: [3, 7] }]; + x[0]++; + y[0].z[0]++; `, optionsSet: [[]], errors: [ { messageId: "generic", - type: "AssignmentExpression", + type: "UpdateExpression", line: 3, column: 1 }, { messageId: "generic", - type: "AssignmentExpression", + type: "UpdateExpression", line: 4, column: 1 } @@ -338,22 +598,22 @@ const invalid: ReadonlyArray = [ }, { code: dedent` - const x = [5, 6]; - const y = [{ z: [3, 7] }]; - x[0] /= 1; - y[0].z[0] /= 1; + var x = [5, 6]; + var y = [{ z: [3, 7] }]; + x[0]--; + y[0].z[0]--; `, optionsSet: [[]], errors: [ { messageId: "generic", - type: "AssignmentExpression", + type: "UpdateExpression", line: 3, column: 1 }, { messageId: "generic", - type: "AssignmentExpression", + type: "UpdateExpression", line: 4, column: 1 } @@ -361,22 +621,22 @@ const invalid: ReadonlyArray = [ }, { code: dedent` - const x = [5, 6]; - const y = [{ z: [3, 7] }]; - x[0] %= 1; - y[0].z[0] %= 1; + var x = [5, 6]; + var y = [{ z: [3, 7] }]; + ++x[0]; + ++y[0].z[0]; `, optionsSet: [[]], errors: [ { messageId: "generic", - type: "AssignmentExpression", + type: "UpdateExpression", line: 3, column: 1 }, { messageId: "generic", - type: "AssignmentExpression", + type: "UpdateExpression", line: 4, column: 1 } @@ -384,22 +644,22 @@ const invalid: ReadonlyArray = [ }, { code: dedent` - const x = [5, 6]; - const y = [{ z: [3, 7] }]; - x[0] <<= 1; - y[0].z[0] <<= 1; + var x = [5, 6]; + var y = [{ z: [3, 7] }]; + --x[0]; + --y[0].z[0]; `, optionsSet: [[]], errors: [ { messageId: "generic", - type: "AssignmentExpression", + type: "UpdateExpression", line: 3, column: 1 }, { messageId: "generic", - type: "AssignmentExpression", + type: "UpdateExpression", line: 4, column: 1 } @@ -407,10 +667,10 @@ const invalid: ReadonlyArray = [ }, { code: dedent` - const x = [5, 6]; - const y = [{ z: [3, 7] }]; - x[0] >>= 1; - y[0].z[0] >>= 1; + var x = [5, 6]; + var y = [{ z: [3, 7] }]; + if (x[0] = 2) {} + if (y[0].z[0] = 2) {} `, optionsSet: [[]], errors: [ @@ -418,45 +678,45 @@ const invalid: ReadonlyArray = [ messageId: "generic", type: "AssignmentExpression", line: 3, - column: 1 + column: 5 }, { messageId: "generic", type: "AssignmentExpression", line: 4, - column: 1 + column: 5 } ] }, { code: dedent` - const x = [5, 6]; - const y = [{ z: [3, 7] }]; - x[0] >>>= 1; - y[0].z[0] >>>= 1; + var x = [5, 6]; + var y = [{ z: [3, 7] }]; + if (x[0]++) {} + if (y[0].z[0]++) {} `, optionsSet: [[]], errors: [ { messageId: "generic", - type: "AssignmentExpression", + type: "UpdateExpression", line: 3, - column: 1 + column: 5 }, { messageId: "generic", - type: "AssignmentExpression", + type: "UpdateExpression", line: 4, - column: 1 + column: 5 } ] }, { code: dedent` - const x = [5, 6]; - const y = [{ z: [3, 7] }]; - x[0] &= 1; - y[0].z[0] &= 1; + var x = [5, 6]; + var y = [{ z: [3, 7] }]; + x.length = 5; + y[0].z.length = 1; `, optionsSet: [[]], errors: [ @@ -474,35 +734,173 @@ const invalid: ReadonlyArray = [ } ] }, + // Disallowed array mutation methods. { code: dedent` - const x = [5, 6]; - const y = [{ z: [3, 7] }]; - x[0] |= 1; - y[0].z[0] |= 1; - `, - optionsSet: [[]], + var 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); + var 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: [[{ assumeTypes: true }]], errors: [ { - messageId: "generic", - type: "AssignmentExpression", + messageId: "array", + type: "CallExpression", + line: 2, + column: 1 + }, + { + messageId: "array", + type: "CallExpression", line: 3, column: 1 }, { - messageId: "generic", - type: "AssignmentExpression", + messageId: "array", + type: "CallExpression", line: 4, column: 1 + }, + { + messageId: "array", + type: "CallExpression", + line: 5, + column: 1 + }, + { + messageId: "array", + type: "CallExpression", + line: 6, + column: 1 + }, + { + messageId: "array", + type: "CallExpression", + line: 7, + column: 1 + }, + { + messageId: "array", + type: "CallExpression", + line: 8, + column: 1 + }, + { + messageId: "array", + type: "CallExpression", + line: 9, + column: 1 + }, + { + messageId: "array", + type: "CallExpression", + line: 10, + column: 1 + }, + { + messageId: "array", + type: "CallExpression", + line: 12, + column: 1 + }, + { + messageId: "array", + type: "CallExpression", + line: 13, + column: 1 + }, + { + messageId: "array", + type: "CallExpression", + line: 14, + column: 1 + }, + { + messageId: "array", + type: "CallExpression", + line: 15, + column: 1 + }, + { + messageId: "array", + type: "CallExpression", + line: 16, + column: 1 + }, + { + messageId: "array", + type: "CallExpression", + line: 17, + column: 1 + }, + { + messageId: "array", + type: "CallExpression", + line: 18, + column: 1 + }, + { + messageId: "array", + type: "CallExpression", + line: 19, + column: 1 + }, + { + messageId: "array", + type: "CallExpression", + line: 20, + column: 1 } ] - }, + } +]; + +// Valid test cases. +const arrayES6Valid: ReadonlyArray = [ + ...arrayES3Valid, // Allowed non-array mutation patterns. + { + code: dedent` + const x = 0; + x *= 1; + x **= 1; + x /= 1; + x %= 1; + x <<= 1; + x >>= 1; + x >>>= 1; + x &= 1; + x |= 1; + x ^= 1;`, + optionsSet: [[]] + } +]; + +// Invalid test cases. +const arrayES6Invalid: ReadonlyArray = [ + ...arrayES3Invalid, + { code: dedent` const x = [5, 6]; const y = [{ z: [3, 7] }]; - x[0] ^= 1; - y[0].z[0] ^= 1; + x[0] *= 2; + y[0].z[0] *= 2; `, optionsSet: [[]], errors: [ @@ -524,20 +922,20 @@ const invalid: ReadonlyArray = [ code: dedent` const x = [5, 6]; const y = [{ z: [3, 7] }]; - delete x[0]; - delete y[0].z[0]; + x[0] **= 2; + y[0].z[0] **= 2; `, optionsSet: [[]], errors: [ { messageId: "generic", - type: "UnaryExpression", + type: "AssignmentExpression", line: 3, column: 1 }, { messageId: "generic", - type: "UnaryExpression", + type: "AssignmentExpression", line: 4, column: 1 } @@ -547,20 +945,20 @@ const invalid: ReadonlyArray = [ code: dedent` const x = [5, 6]; const y = [{ z: [3, 7] }]; - x[0]++; - y[0].z[0]++; + x[0] /= 1; + y[0].z[0] /= 1; `, optionsSet: [[]], errors: [ { messageId: "generic", - type: "UpdateExpression", + type: "AssignmentExpression", line: 3, column: 1 }, { messageId: "generic", - type: "UpdateExpression", + type: "AssignmentExpression", line: 4, column: 1 } @@ -570,20 +968,20 @@ const invalid: ReadonlyArray = [ code: dedent` const x = [5, 6]; const y = [{ z: [3, 7] }]; - x[0]--; - y[0].z[0]--; + x[0] %= 1; + y[0].z[0] %= 1; `, optionsSet: [[]], errors: [ { messageId: "generic", - type: "UpdateExpression", + type: "AssignmentExpression", line: 3, column: 1 }, { messageId: "generic", - type: "UpdateExpression", + type: "AssignmentExpression", line: 4, column: 1 } @@ -593,20 +991,20 @@ const invalid: ReadonlyArray = [ code: dedent` const x = [5, 6]; const y = [{ z: [3, 7] }]; - ++x[0]; - ++y[0].z[0]; + x[0] <<= 1; + y[0].z[0] <<= 1; `, optionsSet: [[]], errors: [ { messageId: "generic", - type: "UpdateExpression", + type: "AssignmentExpression", line: 3, column: 1 }, { messageId: "generic", - type: "UpdateExpression", + type: "AssignmentExpression", line: 4, column: 1 } @@ -616,20 +1014,20 @@ const invalid: ReadonlyArray = [ code: dedent` const x = [5, 6]; const y = [{ z: [3, 7] }]; - --x[0]; - --y[0].z[0]; + x[0] >>= 1; + y[0].z[0] >>= 1; `, optionsSet: [[]], errors: [ { messageId: "generic", - type: "UpdateExpression", + type: "AssignmentExpression", line: 3, column: 1 }, { messageId: "generic", - type: "UpdateExpression", + type: "AssignmentExpression", line: 4, column: 1 } @@ -639,8 +1037,8 @@ const invalid: ReadonlyArray = [ code: dedent` const x = [5, 6]; const y = [{ z: [3, 7] }]; - if (x[0] = 2) {} - if (y[0].z[0] = 2) {} + x[0] >>>= 1; + y[0].z[0] >>>= 1; `, optionsSet: [[]], errors: [ @@ -648,13 +1046,13 @@ const invalid: ReadonlyArray = [ messageId: "generic", type: "AssignmentExpression", line: 3, - column: 5 + column: 1 }, { messageId: "generic", type: "AssignmentExpression", line: 4, - column: 5 + column: 1 } ] }, @@ -662,22 +1060,22 @@ const invalid: ReadonlyArray = [ code: dedent` const x = [5, 6]; const y = [{ z: [3, 7] }]; - if (x[0]++) {} - if (y[0].z[0]++) {} + x[0] &= 1; + y[0].z[0] &= 1; `, optionsSet: [[]], errors: [ { messageId: "generic", - type: "UpdateExpression", + type: "AssignmentExpression", line: 3, - column: 5 + column: 1 }, { messageId: "generic", - type: "UpdateExpression", + type: "AssignmentExpression", line: 4, - column: 5 + column: 1 } ] }, @@ -685,8 +1083,8 @@ const invalid: ReadonlyArray = [ code: dedent` const x = [5, 6]; const y = [{ z: [3, 7] }]; - x.length = 5; - y[0].z.length = 1; + x[0] |= 1; + y[0].z[0] |= 1; `, optionsSet: [[]], errors: [ @@ -704,147 +1102,69 @@ const invalid: ReadonlyArray = [ } ] }, - // 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 }]], + x[0] ^= 1; + y[0].z[0] ^= 1; + `, + optionsSet: [[]], errors: [ { messageId: "generic", - type: "CallExpression", - line: 2, - column: 1 - }, - { - messageId: "generic", - type: "CallExpression", + type: "AssignmentExpression", line: 3, column: 1 }, { messageId: "generic", - type: "CallExpression", + type: "AssignmentExpression", 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 } ] } ]; +const es3Valid: ReadonlyArray = [ + ...objectES3Valid, + ...arrayES3Valid +]; +const es3Invalid: ReadonlyArray = [ + ...objectES3Invalid, + ...arrayES3Invalid +]; + +const es6Valid: ReadonlyArray = [ + ...objectES6Valid, + ...arrayES6Valid +]; +const es6Invalid: ReadonlyArray = [ + ...objectES6Invalid, + ...arrayES6Invalid +]; + describe("TypeScript", () => { const ruleTester = new RuleTester(typescript); ruleTester.run(name, (rule as unknown) as Rule.RuleModule, { - valid: processValidTestCase(valid), - invalid: processInvalidTestCase(invalid) + valid: processValidTestCase(es6Valid), + invalid: processInvalidTestCase(es6Invalid) + }); +}); + +describe("JavaScript (es6)", () => { + const ruleTester = new RuleTester(es6); + ruleTester.run(name, (rule as unknown) as Rule.RuleModule, { + valid: processValidTestCase(es6Valid), + invalid: processInvalidTestCase(es6Invalid) + }); +}); + +describe("JavaScript (es3)", () => { + const ruleTester = new RuleTester(es3); + ruleTester.run(name, (rule as unknown) as Rule.RuleModule, { + valid: processValidTestCase(es3Valid), + invalid: processInvalidTestCase(es3Invalid) }); }); diff --git a/tests/rules/no-object-mutation.test.ts b/tests/rules/no-object-mutation.test.ts deleted file mode 100644 index 8e8a30e75..000000000 --- a/tests/rules/no-object-mutation.test.ts +++ /dev/null @@ -1,319 +0,0 @@ -/** - * @fileoverview Tests for no-object-mutation - */ - -import dedent from "dedent"; -import { Rule, RuleTester } from "eslint"; - -import { name, rule } from "../../src/rules/no-object-mutation"; - -import { es3, es6, typescript } from "../configs"; -import { - InvalidTestCase, - processInvalidTestCase, - processValidTestCase, - ValidTestCase -} from "../util"; - -// Valid test cases. -const es3Valid: ReadonlyArray = [ - // Allowed non-object mutation patterns. - { - code: dedent` - var y = x.a; - var z = x["a"]; - if (x.a && y.a) {} - var w = ~x.a; - if (!x.a) {}`, - optionsSet: [[]] - }, - // Allow Object.assign() on non identifiers. - { - code: dedent` - var x = { msg1: "hello", obj: { a: 1, b: 2}, func: function() {} }; - var bar = function(a, b, c) { return { a: a, b: b, c: c }; }; - - var a = Object.assign({}, { msg: "hello world" }); - var b = Object.assign(bar(1, 2, 3), { d: 4 }); - var c = Object.assign(x.func(), { d: 4 });`, - optionsSet: [[]] - } -]; - -// Invalid test cases. -const es3Invalid: ReadonlyArray = [ - // Disallowed object mutation patterns. - { - code: dedent` - var x = {a: 1}; - x.foo = "bar"; - x["foo"] = "bar"; - x.a += 1; - x.a -= 1; - x.a *= 1; - x.a /= 1; - x.a %= 1; - x.a <<= 1; - x.a >>= 1; - x.a >>>= 1; - x.a &= 1; - x.a |= 1; - x.a ^= 1; - delete x.a; - delete x["a"]; - x.a++; - x.a--; - ++x.a; - --x.a; - if (x.a = 2) {} - if (x.a++) {}`, - optionsSet: [[]], - errors: [ - { - messageId: "generic", - type: "AssignmentExpression", - line: 2, - column: 1 - }, - { - messageId: "generic", - type: "AssignmentExpression", - line: 3, - column: 1 - }, - { - messageId: "generic", - type: "AssignmentExpression", - line: 4, - column: 1 - }, - { - messageId: "generic", - type: "AssignmentExpression", - line: 5, - column: 1 - }, - { - messageId: "generic", - type: "AssignmentExpression", - line: 6, - column: 1 - }, - { - messageId: "generic", - type: "AssignmentExpression", - line: 7, - column: 1 - }, - { - messageId: "generic", - type: "AssignmentExpression", - line: 8, - column: 1 - }, - { - messageId: "generic", - type: "AssignmentExpression", - line: 9, - column: 1 - }, - { - messageId: "generic", - type: "AssignmentExpression", - line: 10, - column: 1 - }, - { - messageId: "generic", - type: "AssignmentExpression", - line: 11, - column: 1 - }, - { - messageId: "generic", - type: "AssignmentExpression", - line: 12, - column: 1 - }, - { - messageId: "generic", - type: "AssignmentExpression", - line: 13, - column: 1 - }, - { - messageId: "generic", - type: "AssignmentExpression", - line: 14, - column: 1 - }, - { - messageId: "generic", - type: "UnaryExpression", - line: 15, - column: 1 - }, - { - messageId: "generic", - type: "UnaryExpression", - line: 16, - column: 1 - }, - { - messageId: "generic", - type: "UpdateExpression", - line: 17, - column: 1 - }, - { - messageId: "generic", - type: "UpdateExpression", - line: 18, - column: 1 - }, - { - messageId: "generic", - type: "UpdateExpression", - line: 19, - column: 1 - }, - { - messageId: "generic", - type: "UpdateExpression", - line: 20, - column: 1 - }, - { - messageId: "generic", - type: "AssignmentExpression", - line: 21, - column: 5 - }, - { - messageId: "generic", - type: "UpdateExpression", - line: 22, - column: 5 - } - ] - }, - // Disallow Object.assign on identifers. - { - code: dedent` - var x = { msg1: "hello", obj: { a: 1, b: 2} }; - - var a = Object.assign(x, { msg2: "world" }); - var b = Object.assign(x.obj, { msg2: "world" });`, - optionsSet: [[]], - errors: [ - { - messageId: "generic", - type: "CallExpression", - line: 3, - column: 9 - }, - { - messageId: "generic", - type: "CallExpression", - line: 4, - column: 9 - } - ] - } -]; - -/** - * Valid tests that only apply to es6 and above. - */ -const es6Valid: ReadonlyArray = [ - ...es3Valid, - // Allow initialization of class members in constructor - { - code: dedent` - class Klass { - bar = 1; - baz: string; - constructor() { - this.baz = "hello"; - } - }`, - optionsSet: [[]] - } -]; - -/** - * Invalid tests that only apply to es6 and above. - */ -const es6Invalid: ReadonlyArray = [ - ...es3Invalid, - { - code: dedent` - const x = {a: 1}; - x.a **= 1;`, - optionsSet: [[]], - errors: [ - { - messageId: "generic", - type: "AssignmentExpression", - line: 2, - column: 1 - } - ] - }, - // No mutation in class methods. - { - code: dedent` - class Klass { - bar = 1; - baz: string; - - constructor() { - this.baz = "hello"; - } - - zoo() { - this.bar = 2; - this.baz = 3; - } - }`, - optionsSet: [[]], - errors: [ - { - messageId: "generic", - type: "AssignmentExpression", - line: 10, - column: 5 - }, - { - messageId: "generic", - type: "AssignmentExpression", - line: 11, - column: 5 - } - ] - } -]; - -describe("TypeScript", () => { - const ruleTester = new RuleTester(typescript); - ruleTester.run(name, (rule as unknown) as Rule.RuleModule, { - valid: processValidTestCase(es6Valid), - invalid: processInvalidTestCase(es6Invalid) - }); -}); - -describe("JavaScript (es6)", () => { - const ruleTester = new RuleTester(es6); - ruleTester.run(name, (rule as unknown) as Rule.RuleModule, { - valid: processValidTestCase(es6Valid), - invalid: processInvalidTestCase(es6Invalid) - }); -}); - -describe("JavaScript (es3)", () => { - const ruleTester = new RuleTester(es3); - ruleTester.run(name, (rule as unknown) as Rule.RuleModule, { - valid: processValidTestCase(es3Valid), - invalid: processInvalidTestCase(es3Invalid) - }); -});