diff --git a/docs/rules/prefer-dom-node-dataset.md b/docs/rules/prefer-dom-node-dataset.md index 2c66ee0629..e0c704cb5d 100644 --- a/docs/rules/prefer-dom-node-dataset.md +++ b/docs/rules/prefer-dom-node-dataset.md @@ -1,13 +1,17 @@ -# Prefer using `.dataset` on DOM elements over `.setAttribute(…)` and `.removeAttribute(…)` +# Prefer using `.dataset` on DOM elements over calling attribute methods βœ… *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(…)` and `.removeAttribute(…)`. +Use [`.dataset`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset) on DOM elements over `getAttribute(…)`, `.setAttribute(…)`, `.removeAttribute(…)` and `.hasAttribute(…)`. ## Fail +```js +const unicorn = element.getAttribute('data-unicorn'); +``` + ```js element.setAttribute('data-unicorn', 'πŸ¦„'); ``` @@ -16,8 +20,16 @@ element.setAttribute('data-unicorn', 'πŸ¦„'); element.removeAttribute('data-unicorn'); ``` +```js +const hasUnicorn = element.hasAttribute('data-unicorn'); +``` + ## Pass +```js +const {unicorn} = element.dataset; +``` + ```js element.dataset.unicorn = 'πŸ¦„'; ``` @@ -26,6 +38,14 @@ element.dataset.unicorn = 'πŸ¦„'; delete element.dataset.unicorn; ``` +```js +const hasUnicorn = Object.hasOwn(element.dataset, 'unicorn'); +``` + +```js +const foo = element.getAttribute('foo'); +``` + ```js element.setAttribute('not-dataset', 'πŸ¦„'); ``` @@ -33,3 +53,7 @@ element.setAttribute('not-dataset', 'πŸ¦„'); ```js element.removeAttribute('not-dataset'); ``` + +```js +const hasFoo = element.hasAttribute('foo'); +``` diff --git a/readme.md b/readme.md index 10eae5dcb1..2fab9fdad5 100644 --- a/readme.md +++ b/readme.md @@ -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(…)` and `.removeAttribute(…)`. | βœ… | πŸ”§ | | +| [prefer-dom-node-dataset](docs/rules/prefer-dom-node-dataset.md) | Prefer using `.dataset` on DOM elements over calling attribute methods. | βœ… | πŸ”§ | | | [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. | βœ… | πŸ”§ | πŸ’‘ | diff --git a/rules/prefer-dom-node-dataset.js b/rules/prefer-dom-node-dataset.js index 5959c85470..520cc9f392 100644 --- a/rules/prefer-dom-node-dataset.js +++ b/rules/prefer-dom-node-dataset.js @@ -11,7 +11,7 @@ const messages = { const selector = [ matches([ methodCallSelector({method: 'setAttribute', argumentsLength: 2}), - methodCallSelector({method: 'removeAttribute', argumentsLength: 1}), + methodCallSelector({methods: ['getAttribute', 'removeAttribute', 'hasAttribute'], argumentsLength: 1}), ]), '[arguments.0.type="Literal"]', ].join(''); @@ -30,14 +30,34 @@ const create = context => ({ 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}`; + let text = ''; + const datasetText = `${sourceCode.getText(node.callee.object)}.dataset`; + switch (method) { + case 'setAttribute': + case 'getAttribute': + case 'removeAttribute': { + text = isValidVariableName(name) ? `.${name}` : `[${quoteString(name, nameNode.raw.charAt(0))}]`; + text = `${datasetText}${text}`; + if (method === 'setAttribute') { + text += ` = ${sourceCode.getText(node.arguments[1])}`; + } else if (method === 'removeAttribute') { + text = `delete ${text}`; + } - text = method === 'setAttribute' - ? `${text} = ${sourceCode.getText(node.arguments[1])}` - : `delete ${text}`; + /* + For non-exists attribute, `element.getAttribute('data-foo')` returns `null`, + but `element.dataset.foo` returns `undefined`, switch to suggestions if necessary + */ + break; + } + + case 'hasAttribute': + text = `Object.hasOwn(${datasetText}, ${quoteString(name, nameNode.raw.charAt(0))})`; + break; + // No default + } return { node, @@ -54,7 +74,7 @@ module.exports = { meta: { type: 'suggestion', docs: { - description: 'Prefer using `.dataset` on DOM elements over `.setAttribute(…)` and `.removeAttribute(…)`.', + description: 'Prefer using `.dataset` on DOM elements over calling attribute methods.', }, fixable: 'code', messages, diff --git a/test/prefer-dom-node-dataset.mjs b/test/prefer-dom-node-dataset.mjs index 46c57e9ec0..a8a3662cec 100644 --- a/test/prefer-dom-node-dataset.mjs +++ b/test/prefer-dom-node-dataset.mjs @@ -97,3 +97,101 @@ test.snapshot({ 'element.querySelector("#selector").removeAttribute("data-AllowAccess");', ], }); + +// `hasAttribute`` +test.snapshot({ + valid: [ + '"unicorn" in element.dataset', + 'element.dataset.hasOwnProperty("unicorn")', + 'Object.prototype.hasOwnProperty.call(element.dataset, "unicorn")', + 'Object.hasOwn(element.dataset, "unicorn")', + 'Reflect.has(element.dataset, "unicorn")', + // Not `CallExpression` + 'new element.hasAttribute("data-unicorn");', + // Not `MemberExpression` + 'hasAttribute("data-unicorn");', + // `callee.property` is not a `Identifier` + 'element["hasAttribute"]("data-unicorn");', + // Computed + 'element[hasAttribute]("data-unicorn");', + // Not `removeAttribute` + 'element.foo("data-unicorn");', + // More or less argument(s) + 'element.hasAttribute("data-unicorn", "extra");', + 'element.hasAttribute();', + 'element.hasAttribute(...argumentsArray, ...argumentsArray2)', + // First Argument is not `Literal` + 'element.hasAttribute(`data-unicorn`);', + // First Argument is not `string` + 'element.hasAttribute(0);', + // First Argument is not startsWith `data-` + 'element.hasAttribute("foo-unicorn");', + // First Argument is `data-` + 'element.hasAttribute("data-");', + ], + invalid: [ + outdent` + element.hasAttribute( + "data-foo", // comment + ); + `, + 'element.hasAttribute(\'data-unicorn\');', + 'element.hasAttribute("data-unicorn");', + 'element.hasAttribute("data-unicorn",);', + 'element.hasAttribute("data-πŸ¦„");', + 'element.hasAttribute("data-foo2");', + 'element.hasAttribute("data-foo:bar");', + 'element.hasAttribute("data-foo:bar");', + 'element.hasAttribute("data-foo.bar");', + 'element.hasAttribute("data-foo-bar");', + 'element.hasAttribute("data-foo");', + 'element.querySelector("#selector").hasAttribute("data-AllowAccess");', + ], +}); + +// `getAttribute`` +test.snapshot({ + valid: [ + 'element.dataset.unicorn', + // Not `CallExpression` + 'new element.getAttribute("data-unicorn");', + // Not `MemberExpression` + 'getAttribute("data-unicorn");', + // `callee.property` is not a `Identifier` + 'element["getAttribute"]("data-unicorn");', + // Computed + 'element[getAttribute]("data-unicorn");', + // Not `getAttribute` + 'element.foo("data-unicorn");', + // More or less argument(s) + 'element.getAttribute("data-unicorn", "extra");', + 'element.getAttribute();', + 'element.getAttribute(...argumentsArray, ...argumentsArray2)', + // First Argument is not `Literal` + 'element.getAttribute(`data-unicorn`);', + // First Argument is not `string` + 'element.getAttribute(0);', + // First Argument is not startsWith `data-` + 'element.getAttribute("foo-unicorn");', + // First Argument is `data-` + 'element.getAttribute("data-");', + ], + invalid: [ + outdent` + element.getAttribute( + "data-foo", // comment + ); + `, + 'element.getAttribute(\'data-unicorn\');', + 'element.getAttribute("data-unicorn");', + 'element.getAttribute("data-unicorn",);', + 'element.getAttribute("data-πŸ¦„");', + 'element.getAttribute("data-foo2");', + 'element.getAttribute("data-foo:bar");', + 'element.getAttribute("data-foo:bar");', + 'element.getAttribute("data-foo.bar");', + 'element.getAttribute("data-foo-bar");', + 'element.getAttribute("data-foo");', + 'element.querySelector("#selector").getAttribute("data-AllowAccess");', + ], +}); diff --git a/test/snapshots/prefer-dom-node-dataset.mjs.md b/test/snapshots/prefer-dom-node-dataset.mjs.md index 79558ef739..f95faaf01e 100644 --- a/test/snapshots/prefer-dom-node-dataset.mjs.md +++ b/test/snapshots/prefer-dom-node-dataset.mjs.md @@ -370,3 +370,399 @@ Generated by [AVA](https://avajs.dev). > 1 | element.querySelector("#selector").removeAttribute("data-AllowAccess");␊ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer \`.dataset\` over \`removeAttribute(…)\`.␊ ` + +## Invalid #1 + 1 | element.hasAttribute( + 2 | "data-foo", // comment + 3 | ); + +> Output + + `␊ + 1 | Object.hasOwn(element.dataset, "foo");␊ + ` + +> Error 1/1 + + `␊ + > 1 | element.hasAttribute(␊ + | ^^^^^^^^^^^^^^^^^^^^^␊ + > 2 | "data-foo", // comment␊ + | ^^^^^^^^^^^^^^^^^^^^^^^␊ + > 3 | );␊ + | ^^ Prefer \`.dataset\` over \`hasAttribute(…)\`.␊ + ` + +## Invalid #2 + 1 | element.hasAttribute('data-unicorn'); + +> Output + + `␊ + 1 | Object.hasOwn(element.dataset, 'unicorn');␊ + ` + +> Error 1/1 + + `␊ + > 1 | element.hasAttribute('data-unicorn');␊ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer \`.dataset\` over \`hasAttribute(…)\`.␊ + ` + +## Invalid #3 + 1 | element.hasAttribute("data-unicorn"); + +> Output + + `␊ + 1 | Object.hasOwn(element.dataset, "unicorn");␊ + ` + +> Error 1/1 + + `␊ + > 1 | element.hasAttribute("data-unicorn");␊ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer \`.dataset\` over \`hasAttribute(…)\`.␊ + ` + +## Invalid #4 + 1 | element.hasAttribute("data-unicorn",); + +> Output + + `␊ + 1 | Object.hasOwn(element.dataset, "unicorn");␊ + ` + +> Error 1/1 + + `␊ + > 1 | element.hasAttribute("data-unicorn",);␊ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer \`.dataset\` over \`hasAttribute(…)\`.␊ + ` + +## Invalid #5 + 1 | element.hasAttribute("data-πŸ¦„"); + +> Output + + `␊ + 1 | Object.hasOwn(element.dataset, "πŸ¦„");␊ + ` + +> Error 1/1 + + `␊ + > 1 | element.hasAttribute("data-πŸ¦„");␊ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer \`.dataset\` over \`hasAttribute(…)\`.␊ + ` + +## Invalid #6 + 1 | element.hasAttribute("data-foo2"); + +> Output + + `␊ + 1 | Object.hasOwn(element.dataset, "foo2");␊ + ` + +> Error 1/1 + + `␊ + > 1 | element.hasAttribute("data-foo2");␊ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer \`.dataset\` over \`hasAttribute(…)\`.␊ + ` + +## Invalid #7 + 1 | element.hasAttribute("data-foo:bar"); + +> Output + + `␊ + 1 | Object.hasOwn(element.dataset, "foo:bar");␊ + ` + +> Error 1/1 + + `␊ + > 1 | element.hasAttribute("data-foo:bar");␊ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer \`.dataset\` over \`hasAttribute(…)\`.␊ + ` + +## Invalid #8 + 1 | element.hasAttribute("data-foo:bar"); + +> Output + + `␊ + 1 | Object.hasOwn(element.dataset, "foo:bar");␊ + ` + +> Error 1/1 + + `␊ + > 1 | element.hasAttribute("data-foo:bar");␊ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer \`.dataset\` over \`hasAttribute(…)\`.␊ + ` + +## Invalid #9 + 1 | element.hasAttribute("data-foo.bar"); + +> Output + + `␊ + 1 | Object.hasOwn(element.dataset, "foo.bar");␊ + ` + +> Error 1/1 + + `␊ + > 1 | element.hasAttribute("data-foo.bar");␊ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer \`.dataset\` over \`hasAttribute(…)\`.␊ + ` + +## Invalid #10 + 1 | element.hasAttribute("data-foo-bar"); + +> Output + + `␊ + 1 | Object.hasOwn(element.dataset, "fooBar");␊ + ` + +> Error 1/1 + + `␊ + > 1 | element.hasAttribute("data-foo-bar");␊ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer \`.dataset\` over \`hasAttribute(…)\`.␊ + ` + +## Invalid #11 + 1 | element.hasAttribute("data-foo"); + +> Output + + `␊ + 1 | Object.hasOwn(element.dataset, "foo");␊ + ` + +> Error 1/1 + + `␊ + > 1 | element.hasAttribute("data-foo");␊ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer \`.dataset\` over \`hasAttribute(…)\`.␊ + ` + +## Invalid #12 + 1 | element.querySelector("#selector").hasAttribute("data-AllowAccess"); + +> Output + + `␊ + 1 | Object.hasOwn(element.querySelector("#selector").dataset, "AllowAccess");␊ + ` + +> Error 1/1 + + `␊ + > 1 | element.querySelector("#selector").hasAttribute("data-AllowAccess");␊ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer \`.dataset\` over \`hasAttribute(…)\`.␊ + ` + +## Invalid #1 + 1 | element.getAttribute( + 2 | "data-foo", // comment + 3 | ); + +> Output + + `␊ + 1 | element.dataset.foo;␊ + ` + +> Error 1/1 + + `␊ + > 1 | element.getAttribute(␊ + | ^^^^^^^^^^^^^^^^^^^^^␊ + > 2 | "data-foo", // comment␊ + | ^^^^^^^^^^^^^^^^^^^^^^^␊ + > 3 | );␊ + | ^^ Prefer \`.dataset\` over \`getAttribute(…)\`.␊ + ` + +## Invalid #2 + 1 | element.getAttribute('data-unicorn'); + +> Output + + `␊ + 1 | element.dataset.unicorn;␊ + ` + +> Error 1/1 + + `␊ + > 1 | element.getAttribute('data-unicorn');␊ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer \`.dataset\` over \`getAttribute(…)\`.␊ + ` + +## Invalid #3 + 1 | element.getAttribute("data-unicorn"); + +> Output + + `␊ + 1 | element.dataset.unicorn;␊ + ` + +> Error 1/1 + + `␊ + > 1 | element.getAttribute("data-unicorn");␊ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer \`.dataset\` over \`getAttribute(…)\`.␊ + ` + +## Invalid #4 + 1 | element.getAttribute("data-unicorn",); + +> Output + + `␊ + 1 | element.dataset.unicorn;␊ + ` + +> Error 1/1 + + `␊ + > 1 | element.getAttribute("data-unicorn",);␊ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer \`.dataset\` over \`getAttribute(…)\`.␊ + ` + +## Invalid #5 + 1 | element.getAttribute("data-πŸ¦„"); + +> Output + + `␊ + 1 | element.dataset["πŸ¦„"];␊ + ` + +> Error 1/1 + + `␊ + > 1 | element.getAttribute("data-πŸ¦„");␊ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer \`.dataset\` over \`getAttribute(…)\`.␊ + ` + +## Invalid #6 + 1 | element.getAttribute("data-foo2"); + +> Output + + `␊ + 1 | element.dataset.foo2;␊ + ` + +> Error 1/1 + + `␊ + > 1 | element.getAttribute("data-foo2");␊ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer \`.dataset\` over \`getAttribute(…)\`.␊ + ` + +## Invalid #7 + 1 | element.getAttribute("data-foo:bar"); + +> Output + + `␊ + 1 | element.dataset["foo:bar"];␊ + ` + +> Error 1/1 + + `␊ + > 1 | element.getAttribute("data-foo:bar");␊ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer \`.dataset\` over \`getAttribute(…)\`.␊ + ` + +## Invalid #8 + 1 | element.getAttribute("data-foo:bar"); + +> Output + + `␊ + 1 | element.dataset["foo:bar"];␊ + ` + +> Error 1/1 + + `␊ + > 1 | element.getAttribute("data-foo:bar");␊ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer \`.dataset\` over \`getAttribute(…)\`.␊ + ` + +## Invalid #9 + 1 | element.getAttribute("data-foo.bar"); + +> Output + + `␊ + 1 | element.dataset["foo.bar"];␊ + ` + +> Error 1/1 + + `␊ + > 1 | element.getAttribute("data-foo.bar");␊ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer \`.dataset\` over \`getAttribute(…)\`.␊ + ` + +## Invalid #10 + 1 | element.getAttribute("data-foo-bar"); + +> Output + + `␊ + 1 | element.dataset.fooBar;␊ + ` + +> Error 1/1 + + `␊ + > 1 | element.getAttribute("data-foo-bar");␊ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer \`.dataset\` over \`getAttribute(…)\`.␊ + ` + +## Invalid #11 + 1 | element.getAttribute("data-foo"); + +> Output + + `␊ + 1 | element.dataset.foo;␊ + ` + +> Error 1/1 + + `␊ + > 1 | element.getAttribute("data-foo");␊ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer \`.dataset\` over \`getAttribute(…)\`.␊ + ` + +## Invalid #12 + 1 | element.querySelector("#selector").getAttribute("data-AllowAccess"); + +> Output + + `␊ + 1 | element.querySelector("#selector").dataset.AllowAccess;␊ + ` + +> Error 1/1 + + `␊ + > 1 | element.querySelector("#selector").getAttribute("data-AllowAccess");␊ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer \`.dataset\` over \`getAttribute(…)\`.␊ + ` diff --git a/test/snapshots/prefer-dom-node-dataset.mjs.snap b/test/snapshots/prefer-dom-node-dataset.mjs.snap index aa7a149283..10995f8619 100644 Binary files a/test/snapshots/prefer-dom-node-dataset.mjs.snap and b/test/snapshots/prefer-dom-node-dataset.mjs.snap differ