/** * @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); } }, }; }, });