prefer-query-selector.js 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. 'use strict';
  2. const {methodCallSelector, notDomNodeSelector} = require('./selectors/index.js');
  3. const MESSAGE_ID = 'prefer-query-selector';
  4. const messages = {
  5. [MESSAGE_ID]: 'Prefer `.{{replacement}}()` over `.{{method}}()`.',
  6. };
  7. const selector = [
  8. methodCallSelector({
  9. methods: ['getElementById', 'getElementsByClassName', 'getElementsByTagName'],
  10. argumentsLength: 1,
  11. }),
  12. notDomNodeSelector('callee.object'),
  13. ].join('');
  14. const forbiddenIdentifierNames = new Map([
  15. ['getElementById', 'querySelector'],
  16. ['getElementsByClassName', 'querySelectorAll'],
  17. ['getElementsByTagName', 'querySelectorAll'],
  18. ]);
  19. const getReplacementForId = value => `#${value}`;
  20. const getReplacementForClass = value => value.match(/\S+/g).map(className => `.${className}`).join('');
  21. const getQuotedReplacement = (node, value) => {
  22. const leftQuote = node.raw.charAt(0);
  23. const rightQuote = node.raw.charAt(node.raw.length - 1);
  24. return `${leftQuote}${value}${rightQuote}`;
  25. };
  26. function * getLiteralFix(fixer, node, identifierName) {
  27. let replacement = node.raw;
  28. if (identifierName === 'getElementById') {
  29. replacement = getQuotedReplacement(node, getReplacementForId(node.value));
  30. }
  31. if (identifierName === 'getElementsByClassName') {
  32. replacement = getQuotedReplacement(node, getReplacementForClass(node.value));
  33. }
  34. yield fixer.replaceText(node, replacement);
  35. }
  36. function * getTemplateLiteralFix(fixer, node, identifierName) {
  37. yield fixer.insertTextAfter(node, '`');
  38. yield fixer.insertTextBefore(node, '`');
  39. for (const templateElement of node.quasis) {
  40. if (identifierName === 'getElementById') {
  41. yield fixer.replaceText(
  42. templateElement,
  43. getReplacementForId(templateElement.value.cooked),
  44. );
  45. }
  46. if (identifierName === 'getElementsByClassName') {
  47. yield fixer.replaceText(
  48. templateElement,
  49. getReplacementForClass(templateElement.value.cooked),
  50. );
  51. }
  52. }
  53. }
  54. const canBeFixed = node => {
  55. if (node.type === 'Literal') {
  56. return node.raw === 'null' || (typeof node.value === 'string' && Boolean(node.value.trim()));
  57. }
  58. if (node.type === 'TemplateLiteral') {
  59. return (
  60. node.expressions.length === 0
  61. && node.quasis.some(templateElement => templateElement.value.cooked.trim())
  62. );
  63. }
  64. return false;
  65. };
  66. const hasValue = node => {
  67. if (node.type === 'Literal') {
  68. return node.value;
  69. }
  70. return true;
  71. };
  72. const fix = (node, identifierName, preferredSelector) => {
  73. const nodeToBeFixed = node.arguments[0];
  74. if (identifierName === 'getElementsByTagName' || !hasValue(nodeToBeFixed)) {
  75. return fixer => fixer.replaceText(node.callee.property, preferredSelector);
  76. }
  77. const getArgumentFix = nodeToBeFixed.type === 'Literal' ? getLiteralFix : getTemplateLiteralFix;
  78. return function * (fixer) {
  79. yield * getArgumentFix(fixer, nodeToBeFixed, identifierName);
  80. yield fixer.replaceText(node.callee.property, preferredSelector);
  81. };
  82. };
  83. /** @param {import('eslint').Rule.RuleContext} context */
  84. const create = () => ({
  85. [selector](node) {
  86. const method = node.callee.property.name;
  87. const preferredSelector = forbiddenIdentifierNames.get(method);
  88. const problem = {
  89. node: node.callee.property,
  90. messageId: MESSAGE_ID,
  91. data: {
  92. replacement: preferredSelector,
  93. method,
  94. },
  95. };
  96. if (canBeFixed(node.arguments[0])) {
  97. problem.fix = fix(node, method, preferredSelector);
  98. }
  99. return problem;
  100. },
  101. });
  102. /** @type {import('eslint').Rule.RuleModule} */
  103. module.exports = {
  104. create,
  105. meta: {
  106. type: 'suggestion',
  107. docs: {
  108. description: 'Prefer `.querySelector()` over `.getElementById()`, `.querySelectorAll()` over `.getElementsByClassName()` and `.getElementsByTagName()`.',
  109. },
  110. fixable: 'code',
  111. messages,
  112. },
  113. };