prefer-query-selector.js 3.6 KB

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