Skip to content

Commit 44d14b9

Browse files
Markus Hatvansindresorhus
andcommitted
Add prefer-modern-dom-apis rule (#362)
Co-authored-by: Sindre Sorhus <[email protected]>
1 parent 44a67f1 commit 44d14b9

File tree

6 files changed

+892
-3
lines changed

6 files changed

+892
-3
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Prefer modern DOM APIs
2+
3+
Enforces the use of:
4+
5+
- [childNode.replaceWith(newNode)](https://developer.mozilla.org/en-US/docs/Web/API/ChildNode/replaceWith) over [parentNode.replaceChild(newNode, oldNode)](https://developer.mozilla.org/en-US/docs/Web/API/Node/replaceChild)
6+
- [referenceNode.before(newNode)](https://developer.mozilla.org/en-US/docs/Web/API/ChildNode/before) over [parentNode.insertBefore(newNode, referenceNode)](https://developer.mozilla.org/en-US/docs/Web/API/Node/insertBefore)
7+
- [referenceNode.before('text')](https://developer.mozilla.org/en-US/docs/Web/API/ChildNode/before) over [referenceNode.insertAdjacentText('beforebegin', 'text')](https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentText)
8+
- [referenceNode.before(newNode)](https://developer.mozilla.org/en-US/docs/Web/API/ChildNode/before) over [referenceNode.insertAdjacentElement('beforebegin', newNode)](https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentElement)
9+
10+
There are some advantages of using the newer DOM APIs, like:
11+
12+
- Traversing to the parent node is not necessary.
13+
- Appending multiple nodes at once.
14+
- Both [`DOMString`](https://developer.mozilla.org/en-US/docs/Web/API/DOMString) and [DOM node objects](https://developer.mozilla.org/en-US/docs/Web/API/Element) can be manipulated.
15+
16+
This rule is fixable.
17+
18+
## Fail
19+
20+
```js
21+
foo.replaceChild(baz, bar);
22+
23+
foo.insertBefore(baz, bar);
24+
25+
foo.insertAdjacentText('position', bar);
26+
27+
foo.insertAdjacentElement('position', bar);
28+
```
29+
30+
## Pass
31+
32+
```js
33+
foo.replaceWith(bar);
34+
foo.replaceWith('bar');
35+
foo.replaceWith(bar, 'baz'));
36+
37+
foo.before(bar)
38+
foo.before('bar')
39+
foo.before(bar, 'baz')
40+
41+
foo.prepend(bar)
42+
foo.prepend('bar')
43+
foo.prepend(bar, 'baz')
44+
45+
foo.append(bar)
46+
foo.append('bar')
47+
foo.append(bar, 'baz')
48+
49+
foo.after(bar)
50+
foo.after('bar')
51+
foo.after(bar, 'baz')
52+
```

index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ module.exports = {
4949
'unicorn/prefer-exponentiation-operator': 'error',
5050
'unicorn/prefer-flat-map': 'error',
5151
'unicorn/prefer-includes': 'error',
52+
'unicorn/prefer-modern-dom-apis': 'error',
5253
'unicorn/prefer-negative-index': 'error',
5354
'unicorn/prefer-node-append': 'error',
5455
'unicorn/prefer-node-remove': 'error',

readme.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ You might want to check out [XO](https://github.com/xojs/xo), which includes thi
1111

1212
## Install
1313

14-
```
14+
```console
1515
$ npm install --save-dev eslint eslint-plugin-unicorn
1616
```
1717

@@ -67,6 +67,7 @@ Configure it in `package.json`.
6767
"unicorn/prefer-exponentiation-operator": "error",
6868
"unicorn/prefer-flat-map": "error",
6969
"unicorn/prefer-includes": "error",
70+
"unicorn/prefer-modern-dom-apis": "error",
7071
"unicorn/prefer-negative-index": "error",
7172
"unicorn/prefer-node-append": "error",
7273
"unicorn/prefer-node-remove": "error",
@@ -120,6 +121,7 @@ Configure it in `package.json`.
120121
- [prefer-exponentiation-operator](docs/rules/prefer-exponentiation-operator.md) - Prefer the exponentiation operator over `Math.pow()` *(fixable)*
121122
- [prefer-flat-map](docs/rules/prefer-flat-map.md) - Prefer `.flatMap(…)` over `.map(…).flat()`. *(fixable)*
122123
- [prefer-includes](docs/rules/prefer-includes.md) - Prefer `.includes()` over `.indexOf()` when checking for existence or non-existence. *(fixable)*
124+
- [prefer-modern-dom-apis](docs/rules/prefer-modern-dom-apis.md) - Prefer `.before()` over `.insertBefore()`, `.replaceWith()` over `.replaceChild()`, prefer one of `.before()`, `.after()`, `.append()` or `.prepend()` over `insertAdjacentText()` and `insertAdjacentElement()`. *(fixable)*
123125
- [prefer-negative-index](docs/rules/prefer-negative-index.md) - Prefer negative index over `.length - index` for `{String,Array,TypedArray}#slice()` and `Array#splice()`. *(fixable)*
124126
- [prefer-node-append](docs/rules/prefer-node-append.md) - Prefer `Node#append()` over `Node#appendChild()`. *(fixable)*
125127
- [prefer-node-remove](docs/rules/prefer-node-remove.md) - Prefer `node.remove()` over `parentNode.removeChild(node)` and `parentElement.removeChild(node)`. *(fixable)*

rules/prefer-modern-dom-apis.js

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
'use strict';
2+
const getDocumentationUrl = require('./utils/get-documentation-url');
3+
4+
const getArgumentNameForReplaceChildOrInsertBefore = nodeArguments => {
5+
if (nodeArguments.type === 'Identifier') {
6+
return nodeArguments.name;
7+
}
8+
};
9+
10+
const forbiddenIdentifierNames = new Map([
11+
['replaceChild', 'replaceWith'],
12+
['insertBefore', 'before']
13+
]);
14+
15+
const isPartOfVariableAssignment = nodeParentType => {
16+
if (nodeParentType === 'VariableDeclarator' || nodeParentType === 'AssignmentExpression') {
17+
return true;
18+
}
19+
20+
return false;
21+
};
22+
23+
const checkForReplaceChildOrInsertBefore = (context, node) => {
24+
const identifierName = node.callee.property.name;
25+
26+
// Return early when specified methods don't exist in forbiddenIdentifierNames
27+
if (!forbiddenIdentifierNames.has(identifierName)) {
28+
return;
29+
}
30+
31+
const nodeArguments = node.arguments;
32+
const newChildNodeArgument = getArgumentNameForReplaceChildOrInsertBefore(
33+
nodeArguments[0]
34+
);
35+
const oldChildNodeArgument = getArgumentNameForReplaceChildOrInsertBefore(
36+
nodeArguments[1]
37+
);
38+
39+
// Return early in case that one of the provided arguments is not a node
40+
if (!newChildNodeArgument || !oldChildNodeArgument) {
41+
return;
42+
}
43+
44+
const parentNode = node.callee.object.name;
45+
// This check makes sure that only the first method of chained methods with same identifier name e.g: parentNode.insertBefore(alfa, beta).insertBefore(charlie, delta); gets transformed
46+
if (!parentNode) {
47+
return;
48+
}
49+
50+
const preferredSelector = forbiddenIdentifierNames.get(identifierName);
51+
52+
let fix = fixer => fixer.replaceText(
53+
node,
54+
`${oldChildNodeArgument}.${preferredSelector}(${newChildNodeArgument})`
55+
);
56+
57+
// Report error when the method is part of a variable assignment
58+
// but don't offer to autofix `.replaceWith()` and `.before()`
59+
// which don't have a return value.
60+
if (isPartOfVariableAssignment(node.parent.type)) {
61+
fix = undefined;
62+
}
63+
64+
return context.report({
65+
node,
66+
message: `Prefer \`${oldChildNodeArgument}.${preferredSelector}(${newChildNodeArgument})\` over \`${parentNode}.${identifierName}(${newChildNodeArgument}, ${oldChildNodeArgument})\`.`,
67+
fix
68+
});
69+
};
70+
71+
// Handle both `Identifier` and `Literal` because the preferred selectors support nodes and DOMString.
72+
const getArgumentNameForInsertAdjacentMethods = nodeArguments => {
73+
if (nodeArguments.type === 'Identifier') {
74+
return nodeArguments.name;
75+
}
76+
77+
if (nodeArguments.type === 'Literal') {
78+
return nodeArguments.raw;
79+
}
80+
};
81+
82+
const positionReplacers = new Map([
83+
['beforebegin', 'before'],
84+
['afterbegin', 'prepend'],
85+
['beforeend', 'append'],
86+
['afterend', 'after']
87+
]);
88+
89+
const checkForInsertAdjacentTextOrInsertAdjacentElement = (context, node) => {
90+
const identifierName = node.callee.property.name;
91+
92+
// Return early when method name is not one of the targeted ones.
93+
if (
94+
identifierName !== 'insertAdjacentText' &&
95+
identifierName !== 'insertAdjacentElement'
96+
) {
97+
return;
98+
}
99+
100+
const nodeArguments = node.arguments;
101+
const positionArgument = getArgumentNameForInsertAdjacentMethods(nodeArguments[0]);
102+
const positionAsValue = nodeArguments[0].value;
103+
104+
// Return early when specified position value of first argument is not a recognized value.
105+
if (!positionReplacers.has(positionAsValue)) {
106+
return;
107+
}
108+
109+
const referenceNode = node.callee.object.name;
110+
const preferredSelector = positionReplacers.get(positionAsValue);
111+
const insertedTextArgument = getArgumentNameForInsertAdjacentMethods(
112+
nodeArguments[1]
113+
);
114+
115+
let fix = fixer =>
116+
fixer.replaceText(
117+
node,
118+
`${referenceNode}.${preferredSelector}(${insertedTextArgument})`
119+
);
120+
121+
// Report error when the method is part of a variable assignment
122+
// but don't offer to autofix `.insertAdjacentElement()`
123+
// which don't have a return value.
124+
if (identifierName === 'insertAdjacentElement' && isPartOfVariableAssignment(node.parent.type)) {
125+
fix = undefined;
126+
}
127+
128+
return context.report({
129+
node,
130+
message: `Prefer \`${referenceNode}.${preferredSelector}(${insertedTextArgument})\` over \`${referenceNode}.${identifierName}(${positionArgument}, ${insertedTextArgument})\`.`,
131+
fix
132+
});
133+
};
134+
135+
const create = context => {
136+
return {
137+
CallExpression(node) {
138+
if (
139+
node.callee.type === 'MemberExpression' &&
140+
node.arguments.length === 2
141+
) {
142+
checkForReplaceChildOrInsertBefore(context, node);
143+
checkForInsertAdjacentTextOrInsertAdjacentElement(context, node);
144+
}
145+
}
146+
};
147+
};
148+
149+
module.exports = {
150+
create,
151+
meta: {
152+
type: 'suggestion',
153+
docs: {
154+
url: getDocumentationUrl(__filename)
155+
},
156+
fixable: 'code'
157+
}
158+
};

0 commit comments

Comments
 (0)