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