123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165 |
- 'use strict';
- const {getStaticValue} = require('@eslint-community/eslint-utils');
- const {newExpressionSelector} = require('./selectors/index.js');
- const {replaceStringLiteral} = require('./fix/index.js');
- const MESSAGE_ID_NEVER = 'never';
- const MESSAGE_ID_ALWAYS = 'always';
- const MESSAGE_ID_REMOVE = 'remove';
- const messages = {
- [MESSAGE_ID_NEVER]: 'Remove the `./` prefix from the relative URL.',
- [MESSAGE_ID_ALWAYS]: 'Add a `./` prefix to the relative URL.',
- [MESSAGE_ID_REMOVE]: 'Remove leading `./`.',
- };
- const templateLiteralSelector = [
- newExpressionSelector({name: 'URL', argumentsLength: 2}),
- ' > TemplateLiteral.arguments:first-child',
- ].join('');
- const literalSelector = [
- newExpressionSelector({name: 'URL', argumentsLength: 2}),
- ' > Literal.arguments:first-child',
- ].join('');
- const DOT_SLASH = './';
- const TEST_URL_BASES = [
- 'https://example.com/a/b/',
- 'https://example.com/a/b.html',
- ];
- const isSafeToAddDotSlashToUrl = (url, base) => {
- try {
- return new URL(url, base).href === new URL(DOT_SLASH + url, base).href;
- } catch {}
- return false;
- };
- const isSafeToAddDotSlash = (url, bases = TEST_URL_BASES) => bases.every(base => isSafeToAddDotSlashToUrl(url, base));
- const isSafeToRemoveDotSlash = (url, bases = TEST_URL_BASES) => bases.every(base => isSafeToAddDotSlashToUrl(url.slice(DOT_SLASH.length), base));
- function canAddDotSlash(node, context) {
- const url = node.value;
- if (url.startsWith(DOT_SLASH) || url.startsWith('.') || url.startsWith('/')) {
- return false;
- }
- const baseNode = node.parent.arguments[1];
- const staticValueResult = getStaticValue(baseNode, context.getScope());
- if (
- typeof staticValueResult?.value === 'string'
- && isSafeToAddDotSlash(url, [staticValueResult.value])
- ) {
- return true;
- }
- return isSafeToAddDotSlash(url);
- }
- function canRemoveDotSlash(node, context) {
- const rawValue = node.raw.slice(1, -1);
- if (!rawValue.startsWith(DOT_SLASH)) {
- return false;
- }
- const baseNode = node.parent.arguments[1];
- const staticValueResult = getStaticValue(baseNode, context.getScope());
- if (
- typeof staticValueResult?.value === 'string'
- && isSafeToRemoveDotSlash(node.value, [staticValueResult.value])
- ) {
- return true;
- }
- return isSafeToRemoveDotSlash(node.value);
- }
- function addDotSlash(node, context) {
- if (!canAddDotSlash(node, context)) {
- return;
- }
- return fixer => replaceStringLiteral(fixer, node, DOT_SLASH, 0, 0);
- }
- function removeDotSlash(node, context) {
- if (!canRemoveDotSlash(node, context)) {
- return;
- }
- return fixer => replaceStringLiteral(fixer, node, '', 0, 2);
- }
- /** @param {import('eslint').Rule.RuleContext} context */
- const create = context => {
- const style = context.options[0] || 'never';
- const listeners = {};
- // TemplateLiteral are not always safe to remove `./`, but if it's starts with `./` we'll report
- if (style === 'never') {
- listeners[templateLiteralSelector] = function (node) {
- const firstPart = node.quasis[0];
- if (!firstPart.value.raw.startsWith(DOT_SLASH)) {
- return;
- }
- return {
- node,
- messageId: style,
- suggest: [
- {
- messageId: MESSAGE_ID_REMOVE,
- fix(fixer) {
- const start = firstPart.range[0] + 1;
- return fixer.removeRange([start, start + 2]);
- },
- },
- ],
- };
- };
- }
- listeners[literalSelector] = function (node) {
- if (typeof node.value !== 'string') {
- return;
- }
- const fix = (style === 'never' ? removeDotSlash : addDotSlash)(node, context);
- if (!fix) {
- return;
- }
- return {
- node,
- messageId: style,
- fix,
- };
- };
- return listeners;
- };
- const schema = [
- {
- enum: ['never', 'always'],
- default: 'never',
- },
- ];
- /** @type {import('eslint').Rule.RuleModule} */
- module.exports = {
- create,
- meta: {
- type: 'suggestion',
- docs: {
- description: 'Enforce consistent relative URL style.',
- },
- fixable: 'code',
- hasSuggestions: true,
- schema,
- messages,
- },
- };
|