relative-url-style.js 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. 'use strict';
  2. const {getStaticValue} = require('eslint-utils');
  3. const {newExpressionSelector} = require('./selectors/index.js');
  4. const {replaceStringLiteral} = require('./fix/index.js');
  5. const MESSAGE_ID_NEVER = 'never';
  6. const MESSAGE_ID_ALWAYS = 'always';
  7. const MESSAGE_ID_REMOVE = 'remove';
  8. const messages = {
  9. [MESSAGE_ID_NEVER]: 'Remove the `./` prefix from the relative URL.',
  10. [MESSAGE_ID_ALWAYS]: 'Add a `./` prefix to the relative URL.',
  11. [MESSAGE_ID_REMOVE]: 'Remove leading `./`.',
  12. };
  13. const templateLiteralSelector = [
  14. newExpressionSelector({name: 'URL', argumentsLength: 2}),
  15. ' > TemplateLiteral.arguments:first-child',
  16. ].join('');
  17. const literalSelector = [
  18. newExpressionSelector({name: 'URL', argumentsLength: 2}),
  19. ' > Literal.arguments:first-child',
  20. ].join('');
  21. const DOT_SLASH = './';
  22. const TEST_URL_BASES = [
  23. 'https://example.com/a/b/',
  24. 'https://example.com/a/b.html',
  25. ];
  26. const isSafeToAddDotSlashToUrl = (url, base) => {
  27. try {
  28. return new URL(url, base).href === new URL(DOT_SLASH + url, base).href;
  29. } catch {}
  30. return false;
  31. };
  32. const isSafeToAddDotSlash = (url, bases = TEST_URL_BASES) => bases.every(base => isSafeToAddDotSlashToUrl(url, base));
  33. const isSafeToRemoveDotSlash = (url, bases = TEST_URL_BASES) => bases.every(base => isSafeToAddDotSlashToUrl(url.slice(DOT_SLASH.length), base));
  34. function canAddDotSlash(node, context) {
  35. const url = node.value;
  36. if (url.startsWith(DOT_SLASH) || url.startsWith('.') || url.startsWith('/')) {
  37. return false;
  38. }
  39. const baseNode = node.parent.arguments[1];
  40. const staticValueResult = getStaticValue(baseNode, context.getScope());
  41. if (
  42. staticValueResult
  43. && typeof staticValueResult.value === 'string'
  44. && isSafeToAddDotSlash(url, [staticValueResult.value])
  45. ) {
  46. return true;
  47. }
  48. return isSafeToAddDotSlash(url);
  49. }
  50. function canRemoveDotSlash(node, context) {
  51. const rawValue = node.raw.slice(1, -1);
  52. if (!rawValue.startsWith(DOT_SLASH)) {
  53. return false;
  54. }
  55. const baseNode = node.parent.arguments[1];
  56. const staticValueResult = getStaticValue(baseNode, context.getScope());
  57. if (
  58. staticValueResult
  59. && typeof staticValueResult.value === 'string'
  60. && isSafeToRemoveDotSlash(node.value, [staticValueResult.value])
  61. ) {
  62. return true;
  63. }
  64. return isSafeToRemoveDotSlash(node.value);
  65. }
  66. function addDotSlash(node, context) {
  67. if (!canAddDotSlash(node, context)) {
  68. return;
  69. }
  70. return fixer => replaceStringLiteral(fixer, node, DOT_SLASH, 0, 0);
  71. }
  72. function removeDotSlash(node, context) {
  73. if (!canRemoveDotSlash(node, context)) {
  74. return;
  75. }
  76. return fixer => replaceStringLiteral(fixer, node, '', 0, 2);
  77. }
  78. /** @param {import('eslint').Rule.RuleContext} context */
  79. const create = context => {
  80. const style = context.options[0] || 'never';
  81. const listeners = {};
  82. // TemplateLiteral are not always safe to remove `./`, but if it's starts with `./` we'll report
  83. if (style === 'never') {
  84. listeners[templateLiteralSelector] = function (node) {
  85. const firstPart = node.quasis[0];
  86. if (!firstPart.value.raw.startsWith(DOT_SLASH)) {
  87. return;
  88. }
  89. return {
  90. node,
  91. messageId: style,
  92. suggest: [
  93. {
  94. messageId: MESSAGE_ID_REMOVE,
  95. fix(fixer) {
  96. const start = firstPart.range[0] + 1;
  97. return fixer.removeRange([start, start + 2]);
  98. },
  99. },
  100. ],
  101. };
  102. };
  103. }
  104. listeners[literalSelector] = function (node) {
  105. if (typeof node.value !== 'string') {
  106. return;
  107. }
  108. const fix = (style === 'never' ? removeDotSlash : addDotSlash)(node, context);
  109. if (!fix) {
  110. return;
  111. }
  112. return {
  113. node,
  114. messageId: style,
  115. fix,
  116. };
  117. };
  118. return listeners;
  119. };
  120. const schema = [
  121. {
  122. enum: ['never', 'always'],
  123. default: 'never',
  124. },
  125. ];
  126. /** @type {import('eslint').Rule.RuleModule} */
  127. module.exports = {
  128. create,
  129. meta: {
  130. type: 'suggestion',
  131. docs: {
  132. description: 'Enforce consistent relative URL style.',
  133. },
  134. fixable: 'code',
  135. hasSuggestions: true,
  136. schema,
  137. messages,
  138. },
  139. };