Skip to content

Commit 5a3325b

Browse files
feat: replace fix with suggestions in no-duplicate-imports (#445)
1 parent 752efae commit 5a3325b

3 files changed

Lines changed: 549 additions & 22 deletions

File tree

docs/rules/no-duplicate-imports.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,23 @@ Examples of **incorrect** code:
2525
/* eslint css/no-duplicate-imports: "error" */
2626

2727
@import url(a.css);
28-
@import "b.css";
29-
@import url("c.css");
28+
@import "b.css" print;
29+
@import url("c.css") print, screen;
3030

3131
/* duplicates */
3232
@import "a.css";
3333
@import url(b.css);
34-
@import "c.css";
34+
@import "c.css" print;
35+
```
36+
37+
Examples of **correct** code:
38+
39+
```css
40+
/* eslint css/no-duplicate-imports: "error" */
41+
42+
@import url(a.css);
43+
@import "b.css";
44+
@import url("c.css") print;
3545
```
3646

3747
## When Not to Use It

src/rules/no-duplicate-imports.js

Lines changed: 165 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,73 @@
99

1010
/**
1111
* @import { CSSRuleDefinition } from "../types.js"
12-
* @typedef {"duplicateImport"} NoDuplicateKeysMessageIds
12+
* @typedef {"duplicateImport" | "removeDuplicateImportWithModifiers" | "removeDuplicateImportWithoutModifiers"} NoDuplicateKeysMessageIds
1313
* @typedef {CSSRuleDefinition<{ RuleOptions: [], MessageIds: NoDuplicateKeysMessageIds }>} NoDuplicateImportsRuleDefinition
1414
*/
1515

16+
//-----------------------------------------------------------------------------
17+
// Helpers
18+
//-----------------------------------------------------------------------------
19+
20+
/**
21+
* Get the end index of import statement including a following newline if present.
22+
* @param {string} text The full text of the source code.
23+
* @param {number} end The end index of the import statement.
24+
* @returns {number} The end index of the import statement including a following newline.
25+
*/
26+
function getImportEnd(text, end) {
27+
let removeEnd = end;
28+
29+
// Remove the node, and also remove a following newline if present
30+
if (text[removeEnd] === "\r") {
31+
removeEnd += text[removeEnd + 1] === "\n" ? 2 : 1;
32+
} else if (text[removeEnd] === "\n" || text[removeEnd] === "\f") {
33+
removeEnd += 1;
34+
}
35+
36+
return removeEnd;
37+
}
38+
39+
/**
40+
* Get the modifiers of an import statement.
41+
* @param {Object} importNode The import node to get modifiers from.
42+
* @param {Object} sourceCode The source code object.
43+
* @returns {string[]} An array of modifiers for the import statement.
44+
*/
45+
function getImportModifiers(importNode, sourceCode) {
46+
const importModifiers = [];
47+
48+
const importHasModifiers = importNode.prelude.children.length > 1;
49+
50+
if (importHasModifiers) {
51+
importNode.prelude.children.slice(1).forEach(modifier => {
52+
const modifierText = sourceCode.getText(modifier).trim();
53+
importModifiers.push(modifierText);
54+
});
55+
}
56+
57+
return importModifiers;
58+
}
59+
60+
/**
61+
* Get the fix for a duplicate import statement.
62+
* @param {Object} fixer The fixer object.
63+
* @param {string} text The full text of the source code.
64+
* @param {number} start The start index of the import statement to fix.
65+
* @param {number} end The end index of the import statement to fix.
66+
* @param {boolean} hasModifiers A boolean indicating whether the import statement has modifiers that differ from the original import.
67+
* @returns {Object|null} A fix object if a fix is applicable, or null if no fix should be applied.
68+
*/
69+
function getFixForImport(fixer, text, start, end, hasModifiers) {
70+
const removeEnd = getImportEnd(text, end);
71+
72+
if (hasModifiers) {
73+
return fixer.removeRange([start, removeEnd]);
74+
}
75+
76+
return null;
77+
}
78+
1679
//-----------------------------------------------------------------------------
1780
// Rule
1881
//-----------------------------------------------------------------------------
@@ -25,6 +88,7 @@ export default {
2588
type: "problem",
2689

2790
fixable: "code",
91+
hasSuggestions: true,
2892

2993
docs: {
3094
description: "Disallow duplicate @import rules",
@@ -34,42 +98,124 @@ export default {
3498

3599
messages: {
36100
duplicateImport: "Unexpected duplicate @import rule for '{{url}}'.",
101+
removeDuplicateImportWithModifiers:
102+
"Remove duplicate @import rule with modifier(s) - {{modifiers}}.",
103+
removeDuplicateImportWithoutModifiers:
104+
"Remove duplicate @import rule without modifiers.",
37105
},
38106
},
39107

40108
create(context) {
41109
const { sourceCode } = context;
42-
const imports = new Set();
110+
const imports = [];
43111

44112
return {
45113
"Atrule[name=/^import$/i]"(node) {
46114
const url = node.prelude.children[0].value;
115+
const hasImport = imports.some(
116+
importNode => importNode.prelude.children[0].value === url,
117+
);
118+
119+
if (hasImport) {
120+
const firstImportNode = imports.find(
121+
importNode =>
122+
importNode.prelude.children[0].value === url,
123+
);
124+
const [firstImportStart, firstImportEnd] =
125+
sourceCode.getRange(firstImportNode);
126+
127+
const firstImportHasModifiers =
128+
firstImportNode.prelude.children.length > 1;
129+
const nodeHasModifiers = node.prelude.children.length > 1;
130+
131+
const [start, end] = sourceCode.getRange(node);
132+
const text = sourceCode.text;
133+
134+
const firstImportModifiers = getImportModifiers(
135+
firstImportNode,
136+
sourceCode,
137+
);
138+
const duplicateImportModifiers = getImportModifiers(
139+
node,
140+
sourceCode,
141+
);
142+
143+
const hasSameModifiers =
144+
firstImportModifiers.length ===
145+
duplicateImportModifiers.length &&
146+
firstImportModifiers.every(
147+
(modifier, index) =>
148+
modifier === duplicateImportModifiers[index],
149+
);
47150

48-
if (imports.has(url)) {
49151
context.report({
50152
loc: node.loc,
51153
messageId: "duplicateImport",
52154
data: { url },
53155
fix(fixer) {
54-
const [start, end] = sourceCode.getRange(node);
55-
const text = sourceCode.text;
56-
// Remove the node, and also remove a following newline if present
57-
let removeEnd = end;
58-
if (text[removeEnd] === "\r") {
59-
removeEnd +=
60-
text[removeEnd + 1] === "\n" ? 2 : 1;
61-
} else if (
62-
text[removeEnd] === "\n" ||
63-
text[removeEnd] === "\f"
64-
) {
65-
removeEnd += 1;
66-
}
67-
68-
return fixer.removeRange([start, removeEnd]);
156+
const hasModifiers =
157+
(!firstImportHasModifiers &&
158+
!nodeHasModifiers) ||
159+
hasSameModifiers;
160+
161+
return getFixForImport(
162+
fixer,
163+
text,
164+
start,
165+
end,
166+
hasModifiers,
167+
);
69168
},
169+
suggest: [
170+
{
171+
messageId: firstImportHasModifiers
172+
? "removeDuplicateImportWithModifiers"
173+
: "removeDuplicateImportWithoutModifiers",
174+
data: {
175+
modifiers: firstImportModifiers.join(" "),
176+
},
177+
fix(fixer) {
178+
const hasModifiers =
179+
(firstImportHasModifiers ||
180+
nodeHasModifiers) &&
181+
!hasSameModifiers;
182+
183+
return getFixForImport(
184+
fixer,
185+
text,
186+
firstImportStart,
187+
firstImportEnd,
188+
hasModifiers,
189+
);
190+
},
191+
},
192+
{
193+
messageId: nodeHasModifiers
194+
? "removeDuplicateImportWithModifiers"
195+
: "removeDuplicateImportWithoutModifiers",
196+
data: {
197+
modifiers:
198+
duplicateImportModifiers.join(" "),
199+
},
200+
fix(fixer) {
201+
const hasModifiers =
202+
(firstImportHasModifiers ||
203+
nodeHasModifiers) &&
204+
!hasSameModifiers;
205+
206+
return getFixForImport(
207+
fixer,
208+
text,
209+
start,
210+
end,
211+
hasModifiers,
212+
);
213+
},
214+
},
215+
],
70216
});
71217
} else {
72-
imports.add(url);
218+
imports.push(node);
73219
}
74220
},
75221
};

0 commit comments

Comments
 (0)