Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions docs/rules/no-duplicate-imports.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,23 @@ Examples of **incorrect** code:
/* eslint css/no-duplicate-imports: "error" */

@import url(a.css);
@import "b.css";
@import url("c.css");
@import "b.css" print;
@import url("c.css") print, screen;

/* duplicates */
@import "a.css";
@import url(b.css);
@import "c.css";
@import "c.css" print;
```

Examples of **correct** code:

```css
/* eslint css/no-duplicate-imports: "error" */

@import url(a.css);
@import "b.css";
@import url("c.css") print;
```

## When Not to Use It
Expand Down
184 changes: 165 additions & 19 deletions src/rules/no-duplicate-imports.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,73 @@

/**
* @import { CSSRuleDefinition } from "../types.js"
* @typedef {"duplicateImport"} NoDuplicateKeysMessageIds
* @typedef {"duplicateImport" | "removeDuplicateImportWithConditions" | "removeDuplicateImportWithoutConditions"} 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 conditions of an import statement.
* @param {Object} importNode The import node to get conditions from.
* @param {Object} sourceCode The source code object.
* @returns {string[]} An array of conditions for the import statement.
*/
function getImportConditions(importNode, sourceCode) {
const importConditions = [];

const importHasConditions = importNode.prelude.children.length > 1;

if (importHasConditions) {
importNode.prelude.children.slice(1).forEach(condition => {
const conditionText = sourceCode.getText(condition).trim();
importConditions.push(conditionText);
});
}

return importConditions;
}

/**
* 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} condition A boolean indicating whether the import statement has conditions 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, condition) {
const removeEnd = getImportEnd(text, end);

if (condition) {
return fixer.removeRange([start, removeEnd]);
}

return null;
}

//-----------------------------------------------------------------------------
// Rule
//-----------------------------------------------------------------------------
Expand All @@ -25,6 +88,7 @@ export default {
type: "problem",

fixable: "code",
hasSuggestions: true,

docs: {
description: "Disallow duplicate @import rules",
Expand All @@ -34,42 +98,124 @@ export default {

messages: {
duplicateImport: "Unexpected duplicate @import rule for '{{url}}'.",
removeDuplicateImportWithConditions:
"Remove duplicate @import rule with condition(s) - {{conditions}}.",
removeDuplicateImportWithoutConditions:
"Remove duplicate @import rule without conditions.",
},
},

create(context) {
const { sourceCode } = context;
const imports = new Set();
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 firstImporthHasConditions =
firstImportNode.prelude.children.length > 1;
const nodeHasConditions = node.prelude.children.length > 1;

const [start, end] = sourceCode.getRange(node);
const text = sourceCode.text;

const firstImportConditions = getImportConditions(
firstImportNode,
sourceCode,
);
const duplicateImportConditions = getImportConditions(
node,
sourceCode,
);

const hasSameConditions =
firstImportConditions.length ===
duplicateImportConditions.length &&
firstImportConditions.every(
(condition, index) =>
condition === duplicateImportConditions[index],
);

if (imports.has(url)) {
context.report({
loc: node.loc,
messageId: "duplicateImport",
data: { url },
fix(fixer) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As the fixer is the same for the autofix and suggestion, I think we should extract this logic to a function.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

const [start, end] = sourceCode.getRange(node);
const text = sourceCode.text;
// Remove the node, and also remove a following newline if present
let removeEnd = end;
if (text[removeEnd] === "\r") {
removeEnd +=
text[removeEnd + 1] === "\n" ? 2 : 1;
} else if (
text[removeEnd] === "\n" ||
text[removeEnd] === "\f"
) {
removeEnd += 1;
}

return fixer.removeRange([start, removeEnd]);
const condition =
(!firstImporthHasConditions &&
!nodeHasConditions) ||
hasSameConditions;

return getFixForImport(
fixer,
text,
start,
end,
condition,
);
},
suggest: [
{
messageId: firstImporthHasConditions
? "removeDuplicateImportWithConditions"
: "removeDuplicateImportWithoutConditions",
data: {
conditions: firstImportConditions.join(" "),
},
fix(fixer) {
const condition =
(firstImporthHasConditions ||
nodeHasConditions) &&
!hasSameConditions;

return getFixForImport(
fixer,
text,
firstImportStart,
firstImportEnd,
condition,
);
},
},
{
messageId: nodeHasConditions
? "removeDuplicateImportWithConditions"
: "removeDuplicateImportWithoutConditions",
data: {
conditions:
duplicateImportConditions.join(" "),
},
fix(fixer) {
const condition =
(firstImporthHasConditions ||
nodeHasConditions) &&
!hasSameConditions;

return getFixForImport(
fixer,
text,
start,
end,
condition,
);
},
},
],
});
} else {
imports.add(url);
imports.push(node);
}
},
};
Expand Down
Loading
Loading