relative-url-style.js 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. 'use strict';
  2. const {getStaticValue} = require('@eslint-community/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. typeof staticValueResult?.value === 'string'
  43. && isSafeToAddDotSlash(url, [staticValueResult.value])
  44. ) {
  45. return true;
  46. }
  47. return isSafeToAddDotSlash(url);
  48. }
  49. function canRemoveDotSlash(node, context) {
  50. const rawValue = node.raw.slice(1, -1);
  51. if (!rawValue.startsWith(DOT_SLASH)) {
  52. return false;
  53. }
  54. const baseNode = node.parent.arguments[1];
  55. const staticValueResult = getStaticValue(baseNode, context.getScope());
  56. if (
  57. typeof staticValueResult?.value === 'string'
  58. && isSafeToRemoveDotSlash(node.value, [staticValueResult.value])
  59. ) {
  60. return true;
  61. }
  62. return isSafeToRemoveDotSlash(node.value);
  63. }
  64. function addDotSlash(node, context) {
  65. if (!canAddDotSlash(node, context)) {
  66. return;
  67. }
  68. return fixer => replaceStringLiteral(fixer, node, DOT_SLASH, 0, 0);
  69. }
  70. function removeDotSlash(node, context) {
  71. if (!canRemoveDotSlash(node, context)) {
  72. return;
  73. }
  74. return fixer => replaceStringLiteral(fixer, node, '', 0, 2);
  75. }
  76. /** @param {import('eslint').Rule.RuleContext} context */
  77. const create = context => {
  78. const style = context.options[0] || 'never';
  79. const listeners = {};
  80. // TemplateLiteral are not always safe to remove `./`, but if it's starts with `./` we'll report
  81. if (style === 'never') {
  82. listeners[templateLiteralSelector] = function (node) {
  83. const firstPart = node.quasis[0];
  84. if (!firstPart.value.raw.startsWith(DOT_SLASH)) {
  85. return;
  86. }
  87. return {
  88. node,
  89. messageId: style,
  90. suggest: [
  91. {
  92. messageId: MESSAGE_ID_REMOVE,
  93. fix(fixer) {
  94. const start = firstPart.range[0] + 1;
  95. return fixer.removeRange([start, start + 2]);
  96. },
  97. },
  98. ],
  99. };
  100. };
  101. }
  102. listeners[literalSelector] = function (node) {
  103. if (typeof node.value !== 'string') {
  104. return;
  105. }
  106. const fix = (style === 'never' ? removeDotSlash : addDotSlash)(node, context);
  107. if (!fix) {
  108. return;
  109. }
  110. return {
  111. node,
  112. messageId: style,
  113. fix,
  114. };
  115. };
  116. return listeners;
  117. };
  118. const schema = [
  119. {
  120. enum: ['never', 'always'],
  121. default: 'never',
  122. },
  123. ];
  124. /** @type {import('eslint').Rule.RuleModule} */
  125. module.exports = {
  126. create,
  127. meta: {
  128. type: 'suggestion',
  129. docs: {
  130. description: 'Enforce consistent relative URL style.',
  131. },
  132. fixable: 'code',
  133. hasSuggestions: true,
  134. schema,
  135. messages,
  136. },
  137. };