prefer-dom-node-dataset.js 2.4 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889
  1. 'use strict';
  2. const {isIdentifierName} = require('@babel/helper-validator-identifier');
  3. const escapeString = require('./utils/escape-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 = isIdentifierName(name) ? `.${name}` : `[${escapeString(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}, ${escapeString(name, nameNode.raw.charAt(0))})`;
  53. break;
  54. }
  55. // No default
  56. }
  57. return {
  58. node,
  59. messageId: MESSAGE_ID,
  60. data: {method},
  61. fix: fixer => fixer.replaceText(node, text),
  62. };
  63. },
  64. });
  65. /** @type {import('eslint').Rule.RuleModule} */
  66. module.exports = {
  67. create,
  68. meta: {
  69. type: 'suggestion',
  70. docs: {
  71. description: 'Prefer using `.dataset` on DOM elements over calling attribute methods.',
  72. },
  73. fixable: 'code',
  74. messages,
  75. },
  76. };