Skip to content

Commit 22d8d03

Browse files
authored
prefer-dom-node-dataset: Check .removeAttribute() (#1668)
1 parent 9179afe commit 22d8d03

File tree

6 files changed

+442
-98
lines changed

6 files changed

+442
-98
lines changed
Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,35 @@
1-
# Prefer using `.dataset` on DOM elements over `.setAttribute(…)`
1+
# Prefer using `.dataset` on DOM elements over `.setAttribute(…)` and `.removeAttribute(…)`
22

33
*This rule is part of the [recommended](https://github.com/sindresorhus/eslint-plugin-unicorn#recommended-config) config.*
44

55
🔧 *This rule is [auto-fixable](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems).*
66

7-
Use [`.dataset`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset) on DOM elements over `.setAttribute(…)`.
7+
Use [`.dataset`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset) on DOM elements over `.setAttribute(…)` and `.removeAttribute(…)`.
88

99
## Fail
1010

1111
```js
1212
element.setAttribute('data-unicorn', '🦄');
1313
```
1414

15+
```js
16+
element.removeAttribute('data-unicorn');
17+
```
18+
1519
## Pass
1620

1721
```js
1822
element.dataset.unicorn = '🦄';
1923
```
24+
25+
```js
26+
delete element.dataset.unicorn;
27+
```
28+
29+
```js
30+
element.setAttribute('not-dataset', '🦄');
31+
```
32+
33+
```js
34+
element.removeAttribute('not-dataset');
35+
```

readme.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ Each rule has emojis denoting:
213213
| [prefer-date-now](docs/rules/prefer-date-now.md) | Prefer `Date.now()` to get the number of milliseconds since the Unix Epoch. || 🔧 | |
214214
| [prefer-default-parameters](docs/rules/prefer-default-parameters.md) | Prefer default parameters over reassignment. || 🔧 | 💡 |
215215
| [prefer-dom-node-append](docs/rules/prefer-dom-node-append.md) | Prefer `Node#append()` over `Node#appendChild()`. || 🔧 | |
216-
| [prefer-dom-node-dataset](docs/rules/prefer-dom-node-dataset.md) | Prefer using `.dataset` on DOM elements over `.setAttribute(…)`. || 🔧 | |
216+
| [prefer-dom-node-dataset](docs/rules/prefer-dom-node-dataset.md) | Prefer using `.dataset` on DOM elements over `.setAttribute(…)` and `.removeAttribute(…)`. || 🔧 | |
217217
| [prefer-dom-node-remove](docs/rules/prefer-dom-node-remove.md) | Prefer `childNode.remove()` over `parentNode.removeChild(childNode)`. || 🔧 | 💡 |
218218
| [prefer-dom-node-text-content](docs/rules/prefer-dom-node-text-content.md) | Prefer `.textContent` over `.innerText`. || | 💡 |
219219
| [prefer-export-from](docs/rules/prefer-export-from.md) | Prefer `export…from` when re-exporting. || 🔧 | 💡 |

rules/prefer-dom-node-dataset.js

Lines changed: 23 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,49 @@
11
'use strict';
22
const isValidVariableName = require('./utils/is-valid-variable-name.js');
33
const quoteString = require('./utils/quote-string.js');
4-
const {methodCallSelector} = require('./selectors/index.js');
4+
const {methodCallSelector, matches} = require('./selectors/index.js');
55

66
const MESSAGE_ID = 'prefer-dom-node-dataset';
77
const messages = {
8-
[MESSAGE_ID]: 'Prefer `.dataset` over `setAttribute(…)`.',
8+
[MESSAGE_ID]: 'Prefer `.dataset` over `{{method}}(…)`.',
99
};
1010

1111
const selector = [
12-
methodCallSelector({
13-
method: 'setAttribute',
14-
argumentsLength: 2,
15-
}),
12+
matches([
13+
methodCallSelector({method: 'setAttribute', argumentsLength: 2}),
14+
methodCallSelector({method: 'removeAttribute', argumentsLength: 1}),
15+
]),
1616
'[arguments.0.type="Literal"]',
1717
].join('');
1818

19-
const parseNodeText = (context, argument) => context.getSourceCode().getText(argument);
20-
2119
const dashToCamelCase = string => string.replace(/-[a-z]/g, s => s[1].toUpperCase());
2220

23-
const fix = (context, node, fixer) => {
24-
const [nameNode, valueNode] = node.arguments;
25-
const calleeObject = parseNodeText(context, node.callee.object);
26-
27-
const name = dashToCamelCase(nameNode.value.slice(5));
28-
const value = parseNodeText(context, valueNode);
29-
30-
const replacement = `${calleeObject}.dataset${
31-
isValidVariableName(name)
32-
? `.${name}`
33-
: `[${quoteString(name, nameNode.raw.charAt(0))}]`
34-
} = ${value}`;
35-
36-
return fixer.replaceText(node, replacement);
37-
};
38-
3921
/** @param {import('eslint').Rule.RuleContext} context */
4022
const create = context => ({
4123
[selector](node) {
42-
const name = node.arguments[0].value;
24+
const [nameNode] = node.arguments;
25+
const attributeName = nameNode.value;
4326

44-
if (typeof name !== 'string' || !name.startsWith('data-') || name === 'data-') {
27+
if (typeof attributeName !== 'string' || !attributeName.startsWith('data-') || attributeName === 'data-') {
4528
return;
4629
}
4730

31+
const method = node.callee.property.name;
32+
const name = dashToCamelCase(attributeName.slice(5));
33+
let text = isValidVariableName(name) ? `.${name}` : `[${quoteString(name, nameNode.raw.charAt(0))}]`;
34+
35+
const sourceCode = context.getSourceCode();
36+
text = `${sourceCode.getText(node.callee.object)}.dataset${text}`;
37+
38+
text = method === 'setAttribute'
39+
? `${text} = ${sourceCode.getText(node.arguments[1])}`
40+
: `delete ${text}`;
41+
4842
return {
4943
node,
5044
messageId: MESSAGE_ID,
51-
fix: fixer => fix(context, node, fixer),
45+
data: {method},
46+
fix: fixer => fixer.replaceText(node, text),
5247
};
5348
},
5449
});
@@ -59,7 +54,7 @@ module.exports = {
5954
meta: {
6055
type: 'suggestion',
6156
docs: {
62-
description: 'Prefer using `.dataset` on DOM elements over `.setAttribute(…)`.',
57+
description: 'Prefer using `.dataset` on DOM elements over `.setAttribute(…)` and `.removeAttribute(…)`.',
6358
},
6459
fixable: 'code',
6560
messages,

test/prefer-dom-node-dataset.mjs

Lines changed: 58 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,8 @@ import {getTester} from './utils/test.mjs';
33

44
const {test} = getTester(import.meta);
55

6-
const errors = [
7-
{
8-
messageId: 'prefer-dom-node-dataset',
9-
},
10-
];
11-
12-
test({
6+
// `setAttribute`
7+
test.snapshot({
138
valid: [
149
'element.dataset.unicorn = \'🦄\';',
1510
'element.dataset[\'unicorn\'] = \'🦄\';',
@@ -21,7 +16,7 @@ test({
2116
'element[\'setAttribute\'](\'data-unicorn\', \'🦄\');',
2217
// Computed
2318
'element[setAttribute](\'data-unicorn\', \'🦄\');',
24-
// Not `appendChild`
19+
// Not `setAttribute`
2520
'element.foo(\'data-unicorn\', \'🦄\');',
2621
// More or less argument(s)
2722
'element.setAttribute(\'data-unicorn\', \'🦄\', \'extra\');',
@@ -37,72 +32,68 @@ test({
3732
'element.setAttribute(\'data-\', \'🦄\');',
3833
],
3934
invalid: [
40-
{
41-
code: 'element.setAttribute(\'data-unicorn\', \'🦄\');',
42-
errors,
43-
output: 'element.dataset.unicorn = \'🦄\';',
44-
},
45-
{
46-
code: 'element.setAttribute(\'data-🦄\', \'🦄\');',
47-
errors,
48-
output: 'element.dataset[\'🦄\'] = \'🦄\';',
49-
},
50-
{
51-
code: 'element.setAttribute(\'data-foo2\', \'🦄\');',
52-
errors,
53-
output: 'element.dataset.foo2 = \'🦄\';',
54-
},
55-
{
56-
code: 'element.setAttribute(\'data-foo:bar\', \'zaz\');',
57-
errors,
58-
output: 'element.dataset[\'foo:bar\'] = \'zaz\';',
59-
},
60-
{
61-
code: 'element.setAttribute("data-foo:bar", "zaz");',
62-
errors,
63-
output: 'element.dataset["foo:bar"] = "zaz";',
64-
},
65-
{
66-
code: 'element.setAttribute(\'data-foo.bar\', \'zaz\');',
67-
errors,
68-
output: 'element.dataset[\'foo.bar\'] = \'zaz\';',
69-
},
70-
{
71-
code: 'element.setAttribute(\'data-foo-bar\', \'zaz\');',
72-
errors,
73-
output: 'element.dataset.fooBar = \'zaz\';',
74-
},
75-
{
76-
code: 'element.setAttribute(\'data-foo\', /* comment */ \'bar\');',
77-
errors,
78-
output: 'element.dataset.foo = \'bar\';',
79-
},
80-
{
81-
code: outdent`
82-
element.setAttribute(
83-
\'data-foo\', // comment
84-
\'bar\' // comment
85-
);
86-
`,
87-
errors,
88-
output: 'element.dataset.foo = \'bar\';',
89-
},
90-
{
91-
code: 'element.querySelector(\'#selector\').setAttribute(\'data-AllowAccess\', true);',
92-
errors,
93-
output: 'element.querySelector(\'#selector\').dataset.AllowAccess = true;',
94-
},
35+
outdent`
36+
element.setAttribute(
37+
\'data-foo\', // comment
38+
\'bar\' // comment
39+
);
40+
`,
41+
'element.setAttribute(\'data-unicorn\', \'🦄\');',
42+
'element.setAttribute(\'data-🦄\', \'🦄\');',
43+
'element.setAttribute(\'data-foo2\', \'🦄\');',
44+
'element.setAttribute(\'data-foo:bar\', \'zaz\');',
45+
'element.setAttribute("data-foo:bar", "zaz");',
46+
'element.setAttribute(\'data-foo.bar\', \'zaz\');',
47+
'element.setAttribute(\'data-foo-bar\', \'zaz\');',
48+
'element.setAttribute(\'data-foo\', /* comment */ \'bar\');',
49+
'element.querySelector(\'#selector\').setAttribute(\'data-AllowAccess\', true);',
9550
],
9651
});
9752

53+
// `removeAttribute``
9854
test.snapshot({
99-
valid: [],
55+
valid: [
56+
'delete element.dataset.unicorn;',
57+
'delete element.dataset["unicorn"];',
58+
// Not `CallExpression`
59+
'new element.removeAttribute("data-unicorn");',
60+
// Not `MemberExpression`
61+
'removeAttribute("data-unicorn");',
62+
// `callee.property` is not a `Identifier`
63+
'element["removeAttribute"]("data-unicorn");',
64+
// Computed
65+
'element[removeAttribute]("data-unicorn");',
66+
// Not `removeAttribute`
67+
'element.foo("data-unicorn");',
68+
// More or less argument(s)
69+
'element.removeAttribute("data-unicorn", "extra");',
70+
'element.removeAttribute();',
71+
'element.removeAttribute(...argumentsArray, ...argumentsArray2)',
72+
// First Argument is not `Literal`
73+
'element.removeAttribute(`data-unicorn`);',
74+
// First Argument is not `string`
75+
'element.removeAttribute(0);',
76+
// First Argument is not startsWith `data-`
77+
'element.removeAttribute("foo-unicorn");',
78+
// First Argument is `data-`
79+
'element.removeAttribute("data-");',
80+
],
10081
invalid: [
10182
outdent`
102-
element.setAttribute(
103-
\'data-foo\', // comment
104-
\'bar\' // comment
83+
element.removeAttribute(
84+
"data-foo", // comment
10585
);
10686
`,
87+
'element.removeAttribute(\'data-unicorn\');',
88+
'element.removeAttribute("data-unicorn");',
89+
'element.removeAttribute("data-unicorn",);',
90+
'element.removeAttribute("data-🦄");',
91+
'element.removeAttribute("data-foo2");',
92+
'element.removeAttribute("data-foo:bar");',
93+
'element.removeAttribute("data-foo:bar");',
94+
'element.removeAttribute("data-foo.bar");',
95+
'element.removeAttribute("data-foo-bar");',
96+
'element.removeAttribute("data-foo");',
97+
'element.querySelector("#selector").removeAttribute("data-AllowAccess");',
10798
],
10899
});

0 commit comments

Comments
 (0)