prefer-modern-dom-apis.js 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. 'use strict';
  2. const isValueNotUsable = require('./utils/is-value-not-usable.js');
  3. const {methodCallSelector} = require('./selectors/index.js');
  4. const messages = {
  5. replaceChildOrInsertBefore:
  6. 'Prefer `{{oldChildNode}}.{{preferredMethod}}({{newChildNode}})` over `{{parentNode}}.{{method}}({{newChildNode}}, {{oldChildNode}})`.',
  7. insertAdjacentTextOrInsertAdjacentElement:
  8. 'Prefer `{{reference}}.{{preferredMethod}}({{content}})` over `{{reference}}.{{method}}({{position}}, {{content}})`.',
  9. };
  10. const replaceChildOrInsertBeforeSelector = [
  11. methodCallSelector({
  12. methods: ['replaceChild', 'insertBefore'],
  13. argumentsLength: 2,
  14. }),
  15. // We only allow Identifier for now
  16. '[arguments.0.type="Identifier"]',
  17. '[arguments.0.name!="undefined"]',
  18. '[arguments.1.type="Identifier"]',
  19. '[arguments.1.name!="undefined"]',
  20. // This check makes sure that only the first method of chained methods with same identifier name e.g: parentNode.insertBefore(alfa, beta).insertBefore(charlie, delta); gets reported
  21. '[callee.object.type="Identifier"]',
  22. ].join('');
  23. const forbiddenMethods = new Map([
  24. ['replaceChild', 'replaceWith'],
  25. ['insertBefore', 'before'],
  26. ]);
  27. const checkForReplaceChildOrInsertBefore = (context, node) => {
  28. const method = node.callee.property.name;
  29. const parentNode = node.callee.object.name;
  30. const [newChildNode, oldChildNode] = node.arguments.map(({name}) => name);
  31. const preferredMethod = forbiddenMethods.get(method);
  32. const fix = isValueNotUsable(node)
  33. ? fixer => fixer.replaceText(
  34. node,
  35. `${oldChildNode}.${preferredMethod}(${newChildNode})`,
  36. )
  37. : undefined;
  38. return {
  39. node,
  40. messageId: 'replaceChildOrInsertBefore',
  41. data: {
  42. parentNode,
  43. method,
  44. preferredMethod,
  45. newChildNode,
  46. oldChildNode,
  47. },
  48. fix,
  49. };
  50. };
  51. const insertAdjacentTextOrInsertAdjacentElementSelector = [
  52. methodCallSelector({
  53. methods: ['insertAdjacentText', 'insertAdjacentElement'],
  54. argumentsLength: 2,
  55. }),
  56. // Position argument should be `string`
  57. '[arguments.0.type="Literal"]',
  58. // TODO: remove this limits on second argument
  59. ':matches([arguments.1.type="Literal"], [arguments.1.type="Identifier"])',
  60. // TODO: remove this limits on callee
  61. '[callee.object.type="Identifier"]',
  62. ].join('');
  63. const positionReplacers = new Map([
  64. ['beforebegin', 'before'],
  65. ['afterbegin', 'prepend'],
  66. ['beforeend', 'append'],
  67. ['afterend', 'after'],
  68. ]);
  69. const checkForInsertAdjacentTextOrInsertAdjacentElement = (context, node) => {
  70. const method = node.callee.property.name;
  71. const [positionNode, contentNode] = node.arguments;
  72. const position = positionNode.value;
  73. // Return early when specified position value of first argument is not a recognized value.
  74. if (!positionReplacers.has(position)) {
  75. return;
  76. }
  77. const preferredMethod = positionReplacers.get(position);
  78. const content = context.getSource(contentNode);
  79. const reference = context.getSource(node.callee.object);
  80. const fix = method === 'insertAdjacentElement' && !isValueNotUsable(node)
  81. ? undefined
  82. // TODO: make a better fix, don't touch reference
  83. : fixer => fixer.replaceText(
  84. node,
  85. `${reference}.${preferredMethod}(${content})`,
  86. );
  87. return {
  88. node,
  89. messageId: 'insertAdjacentTextOrInsertAdjacentElement',
  90. data: {
  91. reference,
  92. method,
  93. preferredMethod,
  94. position: context.getSource(positionNode),
  95. content,
  96. },
  97. fix,
  98. };
  99. };
  100. /** @param {import('eslint').Rule.RuleContext} context */
  101. const create = context => ({
  102. [replaceChildOrInsertBeforeSelector](node) {
  103. return checkForReplaceChildOrInsertBefore(context, node);
  104. },
  105. [insertAdjacentTextOrInsertAdjacentElementSelector](node) {
  106. return checkForInsertAdjacentTextOrInsertAdjacentElement(context, node);
  107. },
  108. });
  109. /** @type {import('eslint').Rule.RuleModule} */
  110. module.exports = {
  111. create,
  112. meta: {
  113. type: 'suggestion',
  114. docs: {
  115. description: 'Prefer `.before()` over `.insertBefore()`, `.replaceWith()` over `.replaceChild()`, prefer one of `.before()`, `.after()`, `.append()` or `.prepend()` over `insertAdjacentText()` and `insertAdjacentElement()`.',
  116. },
  117. fixable: 'code',
  118. messages,
  119. },
  120. };