153 lines
7.3 KiB
JavaScript
153 lines
7.3 KiB
JavaScript
/**
|
|
* @fileoverview Rule to prevent duplicate imports in CSS.
|
|
* @author Nicholas C. Zakas
|
|
*/
|
|
//-----------------------------------------------------------------------------
|
|
// Type Definitions
|
|
//-----------------------------------------------------------------------------
|
|
/**
|
|
* @import { CSSRuleDefinition } from "../types.js"
|
|
* @typedef {"duplicateImport" | "removeDuplicateImportWithModifiers" | "removeDuplicateImportWithoutModifiers"} NoDuplicateKeysMessageIds
|
|
* @typedef {CSSRuleDefinition<{ RuleOptions: [], MessageIds: NoDuplicateKeysMessageIds }>} NoDuplicateImportsRuleDefinition
|
|
*/
|
|
//-----------------------------------------------------------------------------
|
|
// Helpers
|
|
//-----------------------------------------------------------------------------
|
|
/**
|
|
* Get the end index of import statement including a following newline if present.
|
|
* @param {string} text The full text of the source code.
|
|
* @param {number} end The end index of the import statement.
|
|
* @returns {number} The end index of the import statement including a following newline.
|
|
*/
|
|
function getImportEnd(text, end) {
|
|
let removeEnd = end;
|
|
// Remove the node, and also remove a following newline if present
|
|
if (text[removeEnd] === "\r") {
|
|
removeEnd += text[removeEnd + 1] === "\n" ? 2 : 1;
|
|
}
|
|
else if (text[removeEnd] === "\n" || text[removeEnd] === "\f") {
|
|
removeEnd += 1;
|
|
}
|
|
return removeEnd;
|
|
}
|
|
/**
|
|
* Get the modifiers of an import statement.
|
|
* @param {Object} importNode The import node to get modifiers from.
|
|
* @param {Object} sourceCode The source code object.
|
|
* @returns {string[]} An array of modifiers for the import statement.
|
|
*/
|
|
function getImportModifiers(importNode, sourceCode) {
|
|
const importModifiers = [];
|
|
const importHasModifiers = importNode.prelude?.children.length > 1;
|
|
if (importHasModifiers) {
|
|
importNode.prelude?.children.slice(1).forEach(modifier => {
|
|
const modifierText = sourceCode.getText(modifier).trim();
|
|
importModifiers.push(modifierText);
|
|
});
|
|
}
|
|
return importModifiers;
|
|
}
|
|
/**
|
|
* Get the fix for a duplicate import statement.
|
|
* @param {Object} fixer The fixer object.
|
|
* @param {string} text The full text of the source code.
|
|
* @param {number} start The start index of the import statement to fix.
|
|
* @param {number} end The end index of the import statement to fix.
|
|
* @param {boolean} hasModifiers A boolean indicating whether the import statement has modifiers that differ from the original import.
|
|
* @returns {Object|null} A fix object if a fix is applicable, or null if no fix should be applied.
|
|
*/
|
|
function getFixForImport(fixer, text, start, end, hasModifiers) {
|
|
const removeEnd = getImportEnd(text, end);
|
|
if (hasModifiers) {
|
|
return fixer.removeRange([start, removeEnd]);
|
|
}
|
|
return null;
|
|
}
|
|
//-----------------------------------------------------------------------------
|
|
// Rule
|
|
//-----------------------------------------------------------------------------
|
|
export default /** @satisfies {NoDuplicateImportsRuleDefinition} */ ({
|
|
meta: {
|
|
type: "problem",
|
|
fixable: "code",
|
|
hasSuggestions: true,
|
|
docs: {
|
|
description: "Disallow duplicate @import rules",
|
|
recommended: true,
|
|
url: "https://github.com/eslint/css/blob/main/docs/rules/no-duplicate-imports.md",
|
|
},
|
|
messages: {
|
|
duplicateImport: "Unexpected duplicate @import rule for '{{url}}'.",
|
|
removeDuplicateImportWithModifiers: "Remove duplicate @import rule with modifier(s) - {{modifiers}}.",
|
|
removeDuplicateImportWithoutModifiers: "Remove duplicate @import rule without modifiers.",
|
|
},
|
|
},
|
|
create(context) {
|
|
const { sourceCode } = context;
|
|
const imports = [];
|
|
return {
|
|
"Atrule[name=/^import$/i]"(node) {
|
|
const url = node.prelude?.children[0].value;
|
|
const hasImport = imports.some(importNode => importNode.prelude?.children[0].value === url);
|
|
if (hasImport) {
|
|
const firstImportNode = imports.find(importNode => importNode.prelude?.children[0].value === url);
|
|
const [firstImportStart, firstImportEnd] = sourceCode.getRange(firstImportNode);
|
|
const firstImportHasModifiers = firstImportNode.prelude?.children.length > 1;
|
|
const nodeHasModifiers = node.prelude?.children.length > 1;
|
|
const [start, end] = sourceCode.getRange(node);
|
|
const text = sourceCode.text;
|
|
const firstImportModifiers = getImportModifiers(firstImportNode, sourceCode);
|
|
const duplicateImportModifiers = getImportModifiers(node, sourceCode);
|
|
const hasSameModifiers = firstImportModifiers.length ===
|
|
duplicateImportModifiers.length &&
|
|
firstImportModifiers.every((modifier, index) => modifier === duplicateImportModifiers[index]);
|
|
context.report({
|
|
loc: node.loc,
|
|
messageId: "duplicateImport",
|
|
data: { url },
|
|
fix(fixer) {
|
|
const hasModifiers = (!firstImportHasModifiers &&
|
|
!nodeHasModifiers) ||
|
|
hasSameModifiers;
|
|
return getFixForImport(fixer, text, start, end, hasModifiers);
|
|
},
|
|
suggest: [
|
|
{
|
|
messageId: firstImportHasModifiers
|
|
? "removeDuplicateImportWithModifiers"
|
|
: "removeDuplicateImportWithoutModifiers",
|
|
data: {
|
|
modifiers: firstImportModifiers.join(" "),
|
|
},
|
|
fix(fixer) {
|
|
const hasModifiers = (firstImportHasModifiers ||
|
|
nodeHasModifiers) &&
|
|
!hasSameModifiers;
|
|
return getFixForImport(fixer, text, firstImportStart, firstImportEnd, hasModifiers);
|
|
},
|
|
},
|
|
{
|
|
messageId: nodeHasModifiers
|
|
? "removeDuplicateImportWithModifiers"
|
|
: "removeDuplicateImportWithoutModifiers",
|
|
data: {
|
|
modifiers: duplicateImportModifiers.join(" "),
|
|
},
|
|
fix(fixer) {
|
|
const hasModifiers = (firstImportHasModifiers ||
|
|
nodeHasModifiers) &&
|
|
!hasSameModifiers;
|
|
return getFixForImport(fixer, text, start, end, hasModifiers);
|
|
},
|
|
},
|
|
],
|
|
});
|
|
}
|
|
else {
|
|
imports.push(node);
|
|
}
|
|
},
|
|
};
|
|
},
|
|
});
|