Skip to content

Commit fda5517

Browse files
medusalixsindresorhus
authored andcommitted
Add prefer-node-remove rule (#222)
Fixes #213
1 parent 2026ab2 commit fda5517

File tree

5 files changed

+199
-2
lines changed

5 files changed

+199
-2
lines changed

docs/rules/prefer-node-remove.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Prefer `remove` over `parentNode.removeChild` and `parentElement.removeChild`
2+
3+
Enforces the use of, for example, `child.remove();` over `child.parentNode.removeChild(child);` and `child.parentElement.removeChild(child);` for DOM nodes. The DOM function [`.remove()`](https://developer.mozilla.org/en-US/docs/Web/API/ChildNode/remove) is preferred over the indirect removal of an object with [`.removeChild()`](https://developer.mozilla.org/en-US/docs/Web/API/Node/removeChild).
4+
5+
This rule is fixable.
6+
7+
8+
## Fail
9+
10+
```js
11+
foo.parentNode.removeChild(foo);
12+
foo.parentElement.removeChild(foo);
13+
this.parentNode.removeChild(this);
14+
this.parentElement.removeChild(this);
15+
foo.parentNode.removeChild(bar);
16+
foo.parentElement.removeChild(bar);
17+
this.parentNode.removeChild(foo);
18+
this.parentElement.removeChild(foo);
19+
```
20+
21+
## Pass
22+
23+
```js
24+
foo.remove();
25+
this.remove();
26+
```

index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ module.exports = {
5454
'unicorn/no-unreadable-array-destructuring': 'error',
5555
'unicorn/no-unused-properties': 'off',
5656
'unicorn/prefer-node-append': 'error',
57-
'unicorn/prefer-query-selector': 'error'
57+
'unicorn/prefer-query-selector': 'error',
58+
'unicorn/prefer-node-remove': 'error'
5859
}
5960
}
6061
}

readme.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ Configure it in `package.json`.
7171
"unicorn/no-unreadable-array-destructuring": "error",
7272
"unicorn/no-unused-properties": "off",
7373
"unicorn/prefer-node-append": "error",
74-
"unicorn/prefer-query-selector": "error"
74+
"unicorn/prefer-query-selector": "error",
75+
"unicorn/prefer-node-remove': 'error"
7576
}
7677
}
7778
}
@@ -108,6 +109,7 @@ Configure it in `package.json`.
108109
- [no-unused-properties](docs/rules/no-unused-properties.md) - Disallow unused object properties.
109110
- [prefer-node-append](docs/rules/prefer-node-append.md) - Prefer `append` over `appendChild`. *(fixable)*
110111
- [prefer-query-selector](docs/rules/prefer-query-selector.md) - Prefer `querySelector` over `getElementById`, `querySelectorAll` over `getElementsByClassName` and `getElementsByTagName`. *(partly fixable)*
112+
- [prefer-node-remove](docs/rules/prefer-node-remove.md) - Prefer `remove` over `parentNode.removeChild` and `parentElement.removeChild`. *(fixable)*
111113

112114

113115
## Recommended config

rules/prefer-node-remove.js

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
'use strict';
2+
const getDocsUrl = require('./utils/get-docs-url');
3+
4+
const getMethodName = callee => {
5+
const {property} = callee;
6+
7+
if (property.type === 'Identifier') {
8+
return property.name;
9+
}
10+
11+
return null;
12+
};
13+
14+
const getCallerName = callee => {
15+
const {object} = callee;
16+
17+
if (object.type === 'Identifier') {
18+
return object.name;
19+
}
20+
21+
if (object.type === 'MemberExpression') {
22+
const {property} = object;
23+
24+
if (property.type === 'Identifier') {
25+
return property.name;
26+
}
27+
}
28+
29+
return null;
30+
};
31+
32+
const getArgumentName = args => {
33+
const [identifier] = args;
34+
35+
if (identifier.type === 'ThisExpression') {
36+
return 'this';
37+
}
38+
39+
if (identifier.type === 'Identifier') {
40+
return identifier.name;
41+
}
42+
43+
return null;
44+
};
45+
46+
const create = context => {
47+
return {
48+
CallExpression(node) {
49+
const {callee} = node;
50+
51+
if (node.arguments.length === 0 ||
52+
callee.type !== 'MemberExpression' ||
53+
callee.computed
54+
) {
55+
return;
56+
}
57+
58+
const methodName = getMethodName(callee);
59+
const callerName = getCallerName(callee);
60+
61+
if (methodName === 'removeChild' && (
62+
callerName === 'parentNode' ||
63+
callerName === 'parentElement'
64+
)) {
65+
const argumentName = getArgumentName(node.arguments);
66+
67+
if (argumentName) {
68+
context.report({
69+
node,
70+
message: `Prefer \`remove\` over \`${callerName}.removeChild\``,
71+
fix: fixer => fixer.replaceText(node, `${argumentName}.remove()`)
72+
});
73+
}
74+
}
75+
}
76+
};
77+
};
78+
79+
module.exports = {
80+
create,
81+
meta: {
82+
docs: {
83+
url: getDocsUrl(__filename)
84+
},
85+
fixable: 'code'
86+
}
87+
};

test/prefer-node-remove.js

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import test from 'ava';
2+
import avaRuleTester from 'eslint-ava-rule-tester';
3+
import rule from '../rules/prefer-node-remove';
4+
5+
const ruleTester = avaRuleTester(test, {
6+
env: {
7+
es6: true
8+
}
9+
});
10+
11+
const invalidTestCase = (code, output, parentType) => {
12+
return {
13+
code,
14+
output,
15+
errors: [{message: `Prefer \`remove\` over \`${parentType}.removeChild\``}]
16+
};
17+
};
18+
19+
ruleTester.run('prefer-node-remove', rule, {
20+
valid: [
21+
'foo.remove()',
22+
'this.remove()',
23+
'remove()',
24+
'foo.parentNode.removeChild(\'bar\')',
25+
'foo.parentNode[\'bar\'](foo)',
26+
'foo.parentNode[removeChild](foo)',
27+
'foo.parentNode.removeChild()'
28+
],
29+
invalid: [
30+
invalidTestCase(
31+
'foo.parentNode.removeChild(foo)',
32+
'foo.remove()',
33+
'parentNode'
34+
),
35+
invalidTestCase(
36+
'this.parentNode.removeChild(this)',
37+
'this.remove()',
38+
'parentNode'
39+
),
40+
invalidTestCase(
41+
'parentNode.removeChild(this)',
42+
'this.remove()',
43+
'parentNode'
44+
),
45+
invalidTestCase(
46+
'foo.parentNode.removeChild(bar)',
47+
'bar.remove()',
48+
'parentNode'
49+
),
50+
invalidTestCase(
51+
'this.parentNode.removeChild(foo)',
52+
'foo.remove()',
53+
'parentNode'
54+
),
55+
invalidTestCase(
56+
'foo.parentElement.removeChild(foo)',
57+
'foo.remove()',
58+
'parentElement'
59+
),
60+
invalidTestCase(
61+
'this.parentElement.removeChild(this)',
62+
'this.remove()',
63+
'parentElement'
64+
),
65+
invalidTestCase(
66+
'parentElement.removeChild(this)',
67+
'this.remove()',
68+
'parentElement'
69+
),
70+
invalidTestCase(
71+
'foo.parentElement.removeChild(bar)',
72+
'bar.remove()',
73+
'parentElement'
74+
),
75+
invalidTestCase(
76+
'this.parentElement.removeChild(foo)',
77+
'foo.remove()',
78+
'parentElement'
79+
)
80+
]
81+
});

0 commit comments

Comments
 (0)