166 lines
3.8 KiB
JavaScript
166 lines
3.8 KiB
JavaScript
|
'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,
|
||
|
},
|
||
|
};
|