Skip to content

Commit 152f153

Browse files
authored
prefer-dom-node-dataset: Check .hasAttribute() and .getAttribute() (#1673)
1 parent 021aa9b commit 152f153

File tree

6 files changed

+548
-10
lines changed

6 files changed

+548
-10
lines changed
Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
1-
# Prefer using `.dataset` on DOM elements over `.setAttribute(…)` and `.removeAttribute(…)`
1+
# Prefer using `.dataset` on DOM elements over calling attribute methods
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(…)` and `.removeAttribute(…)`.
7+
Use [`.dataset`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset) on DOM elements over `getAttribute(…)`, `.setAttribute(…)`, `.removeAttribute(…)` and `.hasAttribute(…)`.
88

99
## Fail
1010

11+
```js
12+
const unicorn = element.getAttribute('data-unicorn');
13+
```
14+
1115
```js
1216
element.setAttribute('data-unicorn', '🦄');
1317
```
@@ -16,8 +20,16 @@ element.setAttribute('data-unicorn', '🦄');
1620
element.removeAttribute('data-unicorn');
1721
```
1822

23+
```js
24+
const hasUnicorn = element.hasAttribute('data-unicorn');
25+
```
26+
1927
## Pass
2028

29+
```js
30+
const {unicorn} = element.dataset;
31+
```
32+
2133
```js
2234
element.dataset.unicorn = '🦄';
2335
```
@@ -26,10 +38,22 @@ element.dataset.unicorn = '🦄';
2638
delete element.dataset.unicorn;
2739
```
2840

41+
```js
42+
const hasUnicorn = Object.hasOwn(element.dataset, 'unicorn');
43+
```
44+
45+
```js
46+
const foo = element.getAttribute('foo');
47+
```
48+
2949
```js
3050
element.setAttribute('not-dataset', '🦄');
3151
```
3252

3353
```js
3454
element.removeAttribute('not-dataset');
3555
```
56+
57+
```js
58+
const hasFoo = element.hasAttribute('foo');
59+
```

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(…)` and `.removeAttribute(…)`. || 🔧 | |
216+
| [prefer-dom-node-dataset](docs/rules/prefer-dom-node-dataset.md) | Prefer using `.dataset` on DOM elements over calling attribute methods. || 🔧 | |
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: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const messages = {
1111
const selector = [
1212
matches([
1313
methodCallSelector({method: 'setAttribute', argumentsLength: 2}),
14-
methodCallSelector({method: 'removeAttribute', argumentsLength: 1}),
14+
methodCallSelector({methods: ['getAttribute', 'removeAttribute', 'hasAttribute'], argumentsLength: 1}),
1515
]),
1616
'[arguments.0.type="Literal"]',
1717
].join('');
@@ -30,14 +30,34 @@ const create = context => ({
3030

3131
const method = node.callee.property.name;
3232
const name = dashToCamelCase(attributeName.slice(5));
33-
let text = isValidVariableName(name) ? `.${name}` : `[${quoteString(name, nameNode.raw.charAt(0))}]`;
3433

3534
const sourceCode = context.getSourceCode();
36-
text = `${sourceCode.getText(node.callee.object)}.dataset${text}`;
35+
let text = '';
36+
const datasetText = `${sourceCode.getText(node.callee.object)}.dataset`;
37+
switch (method) {
38+
case 'setAttribute':
39+
case 'getAttribute':
40+
case 'removeAttribute': {
41+
text = isValidVariableName(name) ? `.${name}` : `[${quoteString(name, nameNode.raw.charAt(0))}]`;
42+
text = `${datasetText}${text}`;
43+
if (method === 'setAttribute') {
44+
text += ` = ${sourceCode.getText(node.arguments[1])}`;
45+
} else if (method === 'removeAttribute') {
46+
text = `delete ${text}`;
47+
}
3748

38-
text = method === 'setAttribute'
39-
? `${text} = ${sourceCode.getText(node.arguments[1])}`
40-
: `delete ${text}`;
49+
/*
50+
For non-exists attribute, `element.getAttribute('data-foo')` returns `null`,
51+
but `element.dataset.foo` returns `undefined`, switch to suggestions if necessary
52+
*/
53+
break;
54+
}
55+
56+
case 'hasAttribute':
57+
text = `Object.hasOwn(${datasetText}, ${quoteString(name, nameNode.raw.charAt(0))})`;
58+
break;
59+
// No default
60+
}
4161

4262
return {
4363
node,
@@ -54,7 +74,7 @@ module.exports = {
5474
meta: {
5575
type: 'suggestion',
5676
docs: {
57-
description: 'Prefer using `.dataset` on DOM elements over `.setAttribute(…)` and `.removeAttribute(…)`.',
77+
description: 'Prefer using `.dataset` on DOM elements over calling attribute methods.',
5878
},
5979
fixable: 'code',
6080
messages,

test/prefer-dom-node-dataset.mjs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,101 @@ test.snapshot({
9797
'element.querySelector("#selector").removeAttribute("data-AllowAccess");',
9898
],
9999
});
100+
101+
// `hasAttribute``
102+
test.snapshot({
103+
valid: [
104+
'"unicorn" in element.dataset',
105+
'element.dataset.hasOwnProperty("unicorn")',
106+
'Object.prototype.hasOwnProperty.call(element.dataset, "unicorn")',
107+
'Object.hasOwn(element.dataset, "unicorn")',
108+
'Reflect.has(element.dataset, "unicorn")',
109+
// Not `CallExpression`
110+
'new element.hasAttribute("data-unicorn");',
111+
// Not `MemberExpression`
112+
'hasAttribute("data-unicorn");',
113+
// `callee.property` is not a `Identifier`
114+
'element["hasAttribute"]("data-unicorn");',
115+
// Computed
116+
'element[hasAttribute]("data-unicorn");',
117+
// Not `removeAttribute`
118+
'element.foo("data-unicorn");',
119+
// More or less argument(s)
120+
'element.hasAttribute("data-unicorn", "extra");',
121+
'element.hasAttribute();',
122+
'element.hasAttribute(...argumentsArray, ...argumentsArray2)',
123+
// First Argument is not `Literal`
124+
'element.hasAttribute(`data-unicorn`);',
125+
// First Argument is not `string`
126+
'element.hasAttribute(0);',
127+
// First Argument is not startsWith `data-`
128+
'element.hasAttribute("foo-unicorn");',
129+
// First Argument is `data-`
130+
'element.hasAttribute("data-");',
131+
],
132+
invalid: [
133+
outdent`
134+
element.hasAttribute(
135+
"data-foo", // comment
136+
);
137+
`,
138+
'element.hasAttribute(\'data-unicorn\');',
139+
'element.hasAttribute("data-unicorn");',
140+
'element.hasAttribute("data-unicorn",);',
141+
'element.hasAttribute("data-🦄");',
142+
'element.hasAttribute("data-foo2");',
143+
'element.hasAttribute("data-foo:bar");',
144+
'element.hasAttribute("data-foo:bar");',
145+
'element.hasAttribute("data-foo.bar");',
146+
'element.hasAttribute("data-foo-bar");',
147+
'element.hasAttribute("data-foo");',
148+
'element.querySelector("#selector").hasAttribute("data-AllowAccess");',
149+
],
150+
});
151+
152+
// `getAttribute``
153+
test.snapshot({
154+
valid: [
155+
'element.dataset.unicorn',
156+
// Not `CallExpression`
157+
'new element.getAttribute("data-unicorn");',
158+
// Not `MemberExpression`
159+
'getAttribute("data-unicorn");',
160+
// `callee.property` is not a `Identifier`
161+
'element["getAttribute"]("data-unicorn");',
162+
// Computed
163+
'element[getAttribute]("data-unicorn");',
164+
// Not `getAttribute`
165+
'element.foo("data-unicorn");',
166+
// More or less argument(s)
167+
'element.getAttribute("data-unicorn", "extra");',
168+
'element.getAttribute();',
169+
'element.getAttribute(...argumentsArray, ...argumentsArray2)',
170+
// First Argument is not `Literal`
171+
'element.getAttribute(`data-unicorn`);',
172+
// First Argument is not `string`
173+
'element.getAttribute(0);',
174+
// First Argument is not startsWith `data-`
175+
'element.getAttribute("foo-unicorn");',
176+
// First Argument is `data-`
177+
'element.getAttribute("data-");',
178+
],
179+
invalid: [
180+
outdent`
181+
element.getAttribute(
182+
"data-foo", // comment
183+
);
184+
`,
185+
'element.getAttribute(\'data-unicorn\');',
186+
'element.getAttribute("data-unicorn");',
187+
'element.getAttribute("data-unicorn",);',
188+
'element.getAttribute("data-🦄");',
189+
'element.getAttribute("data-foo2");',
190+
'element.getAttribute("data-foo:bar");',
191+
'element.getAttribute("data-foo:bar");',
192+
'element.getAttribute("data-foo.bar");',
193+
'element.getAttribute("data-foo-bar");',
194+
'element.getAttribute("data-foo");',
195+
'element.querySelector("#selector").getAttribute("data-AllowAccess");',
196+
],
197+
});

0 commit comments

Comments
 (0)