Skip to content

Commit

Permalink
prefer-dom-node-dataset: Check .removeAttribute() (#1668)
Browse files Browse the repository at this point in the history
  • Loading branch information
fisker committed Dec 29, 2021
1 parent 9179afe commit 22d8d03
Show file tree
Hide file tree
Showing 6 changed files with 442 additions and 98 deletions.
20 changes: 18 additions & 2 deletions docs/rules/prefer-dom-node-dataset.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,35 @@
# Prefer using `.dataset` on DOM elements over `.setAttribute(…)`
# Prefer using `.dataset` on DOM elements over `.setAttribute(…)` and `.removeAttribute(…)`

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

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

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

## Fail

```js
element.setAttribute('data-unicorn', '🦄');
```

```js
element.removeAttribute('data-unicorn');
```

## Pass

```js
element.dataset.unicorn = '🦄';
```

```js
delete element.dataset.unicorn;
```

```js
element.setAttribute('not-dataset', '🦄');
```

```js
element.removeAttribute('not-dataset');
```
2 changes: 1 addition & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ Each rule has emojis denoting:
| [prefer-date-now](docs/rules/prefer-date-now.md) | Prefer `Date.now()` to get the number of milliseconds since the Unix Epoch. || 🔧 | |
| [prefer-default-parameters](docs/rules/prefer-default-parameters.md) | Prefer default parameters over reassignment. || 🔧 | 💡 |
| [prefer-dom-node-append](docs/rules/prefer-dom-node-append.md) | Prefer `Node#append()` over `Node#appendChild()`. || 🔧 | |
| [prefer-dom-node-dataset](docs/rules/prefer-dom-node-dataset.md) | Prefer using `.dataset` on DOM elements over `.setAttribute(…)`. || 🔧 | |
| [prefer-dom-node-dataset](docs/rules/prefer-dom-node-dataset.md) | Prefer using `.dataset` on DOM elements over `.setAttribute(…)` and `.removeAttribute(…)`. || 🔧 | |
| [prefer-dom-node-remove](docs/rules/prefer-dom-node-remove.md) | Prefer `childNode.remove()` over `parentNode.removeChild(childNode)`. || 🔧 | 💡 |
| [prefer-dom-node-text-content](docs/rules/prefer-dom-node-text-content.md) | Prefer `.textContent` over `.innerText`. || | 💡 |
| [prefer-export-from](docs/rules/prefer-export-from.md) | Prefer `export…from` when re-exporting. || 🔧 | 💡 |
Expand Down
51 changes: 23 additions & 28 deletions rules/prefer-dom-node-dataset.js
Original file line number Diff line number Diff line change
@@ -1,54 +1,49 @@
'use strict';
const isValidVariableName = require('./utils/is-valid-variable-name.js');
const quoteString = require('./utils/quote-string.js');
const {methodCallSelector} = require('./selectors/index.js');
const {methodCallSelector, matches} = require('./selectors/index.js');

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

const selector = [
methodCallSelector({
method: 'setAttribute',
argumentsLength: 2,
}),
matches([
methodCallSelector({method: 'setAttribute', argumentsLength: 2}),
methodCallSelector({method: 'removeAttribute', argumentsLength: 1}),
]),
'[arguments.0.type="Literal"]',
].join('');

const parseNodeText = (context, argument) => context.getSourceCode().getText(argument);

const dashToCamelCase = string => string.replace(/-[a-z]/g, s => s[1].toUpperCase());

const fix = (context, node, fixer) => {
const [nameNode, valueNode] = node.arguments;
const calleeObject = parseNodeText(context, node.callee.object);

const name = dashToCamelCase(nameNode.value.slice(5));
const value = parseNodeText(context, valueNode);

const replacement = `${calleeObject}.dataset${
isValidVariableName(name)
? `.${name}`
: `[${quoteString(name, nameNode.raw.charAt(0))}]`
} = ${value}`;

return fixer.replaceText(node, replacement);
};

/** @param {import('eslint').Rule.RuleContext} context */
const create = context => ({
[selector](node) {
const name = node.arguments[0].value;
const [nameNode] = node.arguments;
const attributeName = nameNode.value;

if (typeof name !== 'string' || !name.startsWith('data-') || name === 'data-') {
if (typeof attributeName !== 'string' || !attributeName.startsWith('data-') || attributeName === 'data-') {
return;
}

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

const sourceCode = context.getSourceCode();
text = `${sourceCode.getText(node.callee.object)}.dataset${text}`;

text = method === 'setAttribute'
? `${text} = ${sourceCode.getText(node.arguments[1])}`
: `delete ${text}`;

return {
node,
messageId: MESSAGE_ID,
fix: fixer => fix(context, node, fixer),
data: {method},
fix: fixer => fixer.replaceText(node, text),
};
},
});
Expand All @@ -59,7 +54,7 @@ module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'Prefer using `.dataset` on DOM elements over `.setAttribute(…)`.',
description: 'Prefer using `.dataset` on DOM elements over `.setAttribute(…)` and `.removeAttribute(…)`.',
},
fixable: 'code',
messages,
Expand Down
125 changes: 58 additions & 67 deletions test/prefer-dom-node-dataset.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,8 @@ import {getTester} from './utils/test.mjs';

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

const errors = [
{
messageId: 'prefer-dom-node-dataset',
},
];

test({
// `setAttribute`
test.snapshot({
valid: [
'element.dataset.unicorn = \'🦄\';',
'element.dataset[\'unicorn\'] = \'🦄\';',
Expand All @@ -21,7 +16,7 @@ test({
'element[\'setAttribute\'](\'data-unicorn\', \'🦄\');',
// Computed
'element[setAttribute](\'data-unicorn\', \'🦄\');',
// Not `appendChild`
// Not `setAttribute`
'element.foo(\'data-unicorn\', \'🦄\');',
// More or less argument(s)
'element.setAttribute(\'data-unicorn\', \'🦄\', \'extra\');',
Expand All @@ -37,72 +32,68 @@ test({
'element.setAttribute(\'data-\', \'🦄\');',
],
invalid: [
{
code: 'element.setAttribute(\'data-unicorn\', \'🦄\');',
errors,
output: 'element.dataset.unicorn = \'🦄\';',
},
{
code: 'element.setAttribute(\'data-🦄\', \'🦄\');',
errors,
output: 'element.dataset[\'🦄\'] = \'🦄\';',
},
{
code: 'element.setAttribute(\'data-foo2\', \'🦄\');',
errors,
output: 'element.dataset.foo2 = \'🦄\';',
},
{
code: 'element.setAttribute(\'data-foo:bar\', \'zaz\');',
errors,
output: 'element.dataset[\'foo:bar\'] = \'zaz\';',
},
{
code: 'element.setAttribute("data-foo:bar", "zaz");',
errors,
output: 'element.dataset["foo:bar"] = "zaz";',
},
{
code: 'element.setAttribute(\'data-foo.bar\', \'zaz\');',
errors,
output: 'element.dataset[\'foo.bar\'] = \'zaz\';',
},
{
code: 'element.setAttribute(\'data-foo-bar\', \'zaz\');',
errors,
output: 'element.dataset.fooBar = \'zaz\';',
},
{
code: 'element.setAttribute(\'data-foo\', /* comment */ \'bar\');',
errors,
output: 'element.dataset.foo = \'bar\';',
},
{
code: outdent`
element.setAttribute(
\'data-foo\', // comment
\'bar\' // comment
);
`,
errors,
output: 'element.dataset.foo = \'bar\';',
},
{
code: 'element.querySelector(\'#selector\').setAttribute(\'data-AllowAccess\', true);',
errors,
output: 'element.querySelector(\'#selector\').dataset.AllowAccess = true;',
},
outdent`
element.setAttribute(
\'data-foo\', // comment
\'bar\' // comment
);
`,
'element.setAttribute(\'data-unicorn\', \'🦄\');',
'element.setAttribute(\'data-🦄\', \'🦄\');',
'element.setAttribute(\'data-foo2\', \'🦄\');',
'element.setAttribute(\'data-foo:bar\', \'zaz\');',
'element.setAttribute("data-foo:bar", "zaz");',
'element.setAttribute(\'data-foo.bar\', \'zaz\');',
'element.setAttribute(\'data-foo-bar\', \'zaz\');',
'element.setAttribute(\'data-foo\', /* comment */ \'bar\');',
'element.querySelector(\'#selector\').setAttribute(\'data-AllowAccess\', true);',
],
});

// `removeAttribute``
test.snapshot({
valid: [],
valid: [
'delete element.dataset.unicorn;',
'delete element.dataset["unicorn"];',
// Not `CallExpression`
'new element.removeAttribute("data-unicorn");',
// Not `MemberExpression`
'removeAttribute("data-unicorn");',
// `callee.property` is not a `Identifier`
'element["removeAttribute"]("data-unicorn");',
// Computed
'element[removeAttribute]("data-unicorn");',
// Not `removeAttribute`
'element.foo("data-unicorn");',
// More or less argument(s)
'element.removeAttribute("data-unicorn", "extra");',
'element.removeAttribute();',
'element.removeAttribute(...argumentsArray, ...argumentsArray2)',
// First Argument is not `Literal`
'element.removeAttribute(`data-unicorn`);',
// First Argument is not `string`
'element.removeAttribute(0);',
// First Argument is not startsWith `data-`
'element.removeAttribute("foo-unicorn");',
// First Argument is `data-`
'element.removeAttribute("data-");',
],
invalid: [
outdent`
element.setAttribute(
\'data-foo\', // comment
\'bar\' // comment
element.removeAttribute(
"data-foo", // comment
);
`,
'element.removeAttribute(\'data-unicorn\');',
'element.removeAttribute("data-unicorn");',
'element.removeAttribute("data-unicorn",);',
'element.removeAttribute("data-🦄");',
'element.removeAttribute("data-foo2");',
'element.removeAttribute("data-foo:bar");',
'element.removeAttribute("data-foo:bar");',
'element.removeAttribute("data-foo.bar");',
'element.removeAttribute("data-foo-bar");',
'element.removeAttribute("data-foo");',
'element.querySelector("#selector").removeAttribute("data-AllowAccess");',
],
});

0 comments on commit 22d8d03

Please sign in to comment.