prefer-dom-node-dataset.js 2.4 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
  1. 'use strict';
  2. const isValidVariableName = require('./utils/is-valid-variable-name.js');
  3. const quoteString = require('./utils/quote-string.js');
  4. const {methodCallSelector, matches} = require('./selectors/index.js');
  5. const MESSAGE_ID = 'prefer-dom-node-dataset';
  6. const messages = {
  7. [MESSAGE_ID]: 'Prefer `.dataset` over `{{method}}(…)`.',
  8. };
  9. const selector = [
  10. matches([
  11. methodCallSelector({method: 'setAttribute', argumentsLength: 2}),
  12. methodCallSelector({methods: ['getAttribute', 'removeAttribute', 'hasAttribute'], argumentsLength: 1}),
  13. ]),
  14. '[arguments.0.type="Literal"]',
  15. ].join('');
  16. const dashToCamelCase = string => string.replace(/-[a-z]/g, s => s[1].toUpperCase());
  17. /** @param {import('eslint').Rule.RuleContext} context */
  18. const create = context => ({
  19. [selector](node) {
  20. const [nameNode] = node.arguments;
  21. let attributeName = nameNode.value;
  22. if (typeof attributeName !== 'string') {
  23. return;
  24. }
  25. attributeName = attributeName.toLowerCase();
  26. if (!attributeName.startsWith('data-')) {
  27. return;
  28. }
  29. const method = node.callee.property.name;
  30. const name = dashToCamelCase(attributeName.slice(5));
  31. const sourceCode = context.getSourceCode();
  32. let text = '';
  33. const datasetText = `${sourceCode.getText(node.callee.object)}.dataset`;
  34. switch (method) {
  35. case 'setAttribute':
  36. case 'getAttribute':
  37. case 'removeAttribute': {
  38. text = isValidVariableName(name) ? `.${name}` : `[${quoteString(name, nameNode.raw.charAt(0))}]`;
  39. text = `${datasetText}${text}`;
  40. if (method === 'setAttribute') {
  41. text += ` = ${sourceCode.getText(node.arguments[1])}`;
  42. } else if (method === 'removeAttribute') {
  43. text = `delete ${text}`;
  44. }
  45. /*
  46. For non-exists attribute, `element.getAttribute('data-foo')` returns `null`,
  47. but `element.dataset.foo` returns `undefined`, switch to suggestions if necessary
  48. */
  49. break;
  50. }
  51. case 'hasAttribute':
  52. text = `Object.hasOwn(${datasetText}, ${quoteString(name, nameNode.raw.charAt(0))})`;
  53. break;
  54. // No default
  55. }
  56. return {
  57. node,
  58. messageId: MESSAGE_ID,
  59. data: {method},
  60. fix: fixer => fixer.replaceText(node, text),
  61. };
  62. },
  63. });
  64. /** @type {import('eslint').Rule.RuleModule} */
  65. module.exports = {
  66. create,
  67. meta: {
  68. type: 'suggestion',
  69. docs: {
  70. description: 'Prefer using `.dataset` on DOM elements over calling attribute methods.',
  71. },
  72. fixable: 'code',
  73. messages,
  74. },
  75. };