Skip to content

Commit e1c7d2e

Browse files
fiskersindresorhus
andauthored
Add no-immediate-mutation rule (#2787)
Co-authored-by: Sindre Sorhus <[email protected]>
1 parent 8b6f598 commit e1c7d2e

18 files changed

+4959
-80
lines changed
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# Disallow immediate mutation after variable assignment
2+
3+
💼🚫 This rule is enabled in the ✅ `recommended` [config](https://github.com/sindresorhus/eslint-plugin-unicorn#recommended-config). This rule is _disabled_ in the ☑️ `unopinionated` [config](https://github.com/sindresorhus/eslint-plugin-unicorn#recommended-config).
4+
5+
🔧💡 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix) and manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions).
6+
7+
<!-- end auto-generated rule header -->
8+
<!-- Do not manually modify this header. Run: `npm run fix:eslint-docs` -->
9+
10+
When you create a variable and immediately mutate it, you should instead include those changes in the initial value.
11+
12+
- Assign variable to an array literal and immediately mutate with `Array#{push,unshift}(…)`.
13+
- Assign variable to an object literal and immediately assign another property.
14+
- Assign variable to an object literal and immediately mutate with `Object.assign(…)`.
15+
- Assign variable to a `Set` or `WeakSet` from an array literal and immediately adding an new element with `{Set,WeakSet}.add(…)`.
16+
- Assign variable to a `Map` or `WeakMap` from an array literal and immediately set another key with `{Map,WeakMap}.set(…, …)`.
17+
18+
## Examples
19+
20+
```js
21+
//
22+
const array = [1, 2];
23+
array.push(3, 4);
24+
25+
//
26+
const array = [1, 2, 3, 4];
27+
```
28+
29+
```js
30+
//
31+
const array = [3, 4];
32+
array.unshift(1, 2);
33+
34+
//
35+
const array = [1, 2, 3, 4];
36+
```
37+
38+
```js
39+
//
40+
const object = {foo: 1};
41+
object.bar = 2;
42+
43+
//
44+
const object = {foo: 1, bar: 2};
45+
```
46+
47+
```js
48+
//
49+
const object = {foo: 1};
50+
Object.assign(object, {bar: 2});
51+
52+
//
53+
const object = {foo: 1, bar: 2};
54+
```
55+
56+
```js
57+
//
58+
const object = {foo: 1};
59+
Object.assign(object, bar);
60+
61+
//
62+
const object = {foo: 1, ...bar};
63+
```
64+
65+
```js
66+
//
67+
const set = new Set([1, 2]);
68+
set.add(3);
69+
70+
//
71+
const set = new Set([1, 2, 3]);
72+
```
73+
74+
```js
75+
//
76+
const weakSet = new WeakSet([foo, bar]);
77+
weakSet.add(baz);
78+
79+
//
80+
const weakSet = new WeakSet([foo, bar, baz]);
81+
```
82+
83+
```js
84+
//
85+
const map = new Map([
86+
['foo', 1],
87+
]);
88+
map.set('bar', 2);
89+
90+
//
91+
const map = new Map([
92+
['foo', 1],
93+
['bar', 2],
94+
]);
95+
```
96+
97+
```js
98+
//
99+
const weakMap = new WeakMap([
100+
[foo, 1],
101+
]);
102+
weakMap.set(bar, 2);
103+
104+
//
105+
const weakMap = new WeakMap([
106+
[foo, 1],
107+
[bar, 2],
108+
]);
109+
```

readme.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ export default [
9090
| [no-empty-file](docs/rules/no-empty-file.md) | Disallow empty files. | ✅ ☑️ | | |
9191
| [no-for-loop](docs/rules/no-for-loop.md) | Do not use a `for` loop that can be replaced with a `for-of` loop. || 🔧 | 💡 |
9292
| [no-hex-escape](docs/rules/no-hex-escape.md) | Enforce the use of Unicode escapes instead of hexadecimal escapes. | ✅ ☑️ | 🔧 | |
93+
| [no-immediate-mutation](docs/rules/no-immediate-mutation.md) | Disallow immediate mutation after variable assignment. || 🔧 | 💡 |
9394
| [no-instanceof-builtins](docs/rules/no-instanceof-builtins.md) | Disallow `instanceof` with built-in objects | ✅ ☑️ | 🔧 | 💡 |
9495
| [no-invalid-fetch-options](docs/rules/no-invalid-fetch-options.md) | Disallow invalid options in `fetch()` and `new Request()`. | ✅ ☑️ | | |
9596
| [no-invalid-remove-event-listener](docs/rules/no-invalid-remove-event-listener.md) | Prevent calling `EventTarget#removeEventListener()` with the result of an expression. | ✅ ☑️ | | |

rules/fix/index.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,14 @@ export {
1010
removeMemberExpressionProperty,
1111
} from './replace-member-expression-property.js';
1212
export {default as removeMethodCall} from './remove-method-call.js';
13-
export {default as replaceTemplateElement} from './replace-template-element.js';
14-
export {default as replaceReferenceIdentifier} from './replace-reference-identifier.js';
15-
export {default as renameVariable} from './rename-variable.js';
16-
export {default as replaceNodeOrTokenAndSpacesBefore} from './replace-node-or-token-and-spaces-before.js';
13+
export {default as removeExpressionStatement} from './remove-expression-statement.js';
1714
export {default as removeSpacesAfter} from './remove-spaces-after.js';
1815
export {default as removeSpecifier} from './remove-specifier.js';
1916
export {default as removeObjectProperty} from './remove-object-property.js';
17+
export {default as renameVariable} from './rename-variable.js';
18+
export {default as replaceTemplateElement} from './replace-template-element.js';
19+
export {default as replaceReferenceIdentifier} from './replace-reference-identifier.js';
20+
export {default as replaceNodeOrTokenAndSpacesBefore} from './replace-node-or-token-and-spaces-before.js';
2021
export {default as fixSpaceAroundKeyword} from './fix-space-around-keywords.js';
2122
export {default as replaceStringRaw} from './replace-string-raw.js';
2223
export {default as addParenthesizesToReturnOrThrowExpression} from './add-parenthesizes-to-return-or-throw-expression.js';
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import {isSemicolonToken} from '@eslint-community/eslint-utils';
2+
const isWhitespaceOnly = text => /^\s*$/.test(text);
3+
4+
function removeExpressionStatement(expressionStatement, context, fixer, preserveSemiColon = false) {
5+
const {sourceCode} = context;
6+
const {lines} = sourceCode;
7+
let endToken = expressionStatement;
8+
9+
if (preserveSemiColon) {
10+
const [penultimateToken, lastToken] = sourceCode.getLastTokens(expressionStatement, 2);
11+
12+
if (isSemicolonToken(lastToken)) {
13+
endToken = penultimateToken;
14+
}
15+
}
16+
17+
const startLocation = sourceCode.getLoc(expressionStatement).start;
18+
const endLocation = sourceCode.getLoc(endToken).end;
19+
20+
const textBefore = lines[startLocation.line - 1].slice(0, startLocation.column);
21+
const textAfter = lines[endLocation.line - 1].slice(endLocation.column);
22+
23+
let [start] = sourceCode.getRange(expressionStatement);
24+
let [, end] = sourceCode.getRange(endToken);
25+
26+
if (isWhitespaceOnly(textBefore) && isWhitespaceOnly(textAfter)) {
27+
start = Math.max(0, start - textBefore.length - 1);
28+
end += textAfter.length;
29+
}
30+
31+
return fixer.removeRange([start, end]);
32+
}
33+
34+
export default removeExpressionStatement;

rules/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export {default as 'no-document-cookie'} from './no-document-cookie.js';
3333
export {default as 'no-empty-file'} from './no-empty-file.js';
3434
export {default as 'no-for-loop'} from './no-for-loop.js';
3535
export {default as 'no-hex-escape'} from './no-hex-escape.js';
36+
export {default as 'no-immediate-mutation'} from './no-immediate-mutation.js';
3637
export {default as 'no-instanceof-builtins'} from './no-instanceof-builtins.js';
3738
export {default as 'no-invalid-fetch-options'} from './no-invalid-fetch-options.js';
3839
export {default as 'no-invalid-remove-event-listener'} from './no-invalid-remove-event-listener.js';

0 commit comments

Comments
 (0)