@@ -6,9 +6,21 @@ import type {
6
6
Node as SelectorNode ,
7
7
Tag as SelectorTag
8
8
} from 'postcss-selector-parser' ;
9
+ import type { SvelteHTMLElement } from 'svelte-eslint-parser/lib/ast' ;
9
10
import { findClassesInAttribute } from '../utils/ast-utils.js' ;
11
+ import {
12
+ extractExpressionPrefixLiteral ,
13
+ extractExpressionSuffixLiteral
14
+ } from '../utils/expression-affixes.js' ;
10
15
import { createRule } from '../utils/index.js' ;
11
16
17
+ interface Selections {
18
+ exact : Map < string , AST . SvelteHTMLElement [ ] > ;
19
+ // [prefix, suffix]
20
+ affixes : Map < [ string | null , string | null ] , AST . SvelteHTMLElement [ ] > ;
21
+ universalSelector : boolean ;
22
+ }
23
+
12
24
export default createRule ( 'consistent-selector-style' , {
13
25
meta : {
14
26
docs : {
@@ -62,9 +74,24 @@ export default createRule('consistent-selector-style', {
62
74
const style = context . options [ 0 ] ?. style ?? [ 'type' , 'id' , 'class' ] ;
63
75
64
76
const whitelistedClasses : string [ ] = [ ] ;
65
- const classSelections : Map < string , AST . SvelteHTMLElement [ ] > = new Map ( ) ;
66
- const idSelections : Map < string , AST . SvelteHTMLElement [ ] > = new Map ( ) ;
67
- const typeSelections : Map < string , AST . SvelteHTMLElement [ ] > = new Map ( ) ;
77
+
78
+ const selections : {
79
+ class : Selections ;
80
+ id : Selections ;
81
+ type : Map < string , AST . SvelteHTMLElement [ ] > ;
82
+ } = {
83
+ class : {
84
+ exact : new Map ( ) ,
85
+ affixes : new Map ( ) ,
86
+ universalSelector : false
87
+ } ,
88
+ id : {
89
+ exact : new Map ( ) ,
90
+ affixes : new Map ( ) ,
91
+ universalSelector : false
92
+ } ,
93
+ type : new Map ( )
94
+ } ;
68
95
69
96
/**
70
97
* Checks selectors in a given PostCSS node
@@ -109,10 +136,10 @@ export default createRule('consistent-selector-style', {
109
136
* Checks a class selector
110
137
*/
111
138
function checkClassSelector ( node : SelectorClass ) : void {
112
- if ( whitelistedClasses . includes ( node . value ) ) {
139
+ if ( selections . class . universalSelector || whitelistedClasses . includes ( node . value ) ) {
113
140
return ;
114
141
}
115
- const selection = classSelections . get ( node . value ) ?? [ ] ;
142
+ const selection = matchSelection ( selections . class , node . value ) ;
116
143
for ( const styleValue of style ) {
117
144
if ( styleValue === 'class' ) {
118
145
return ;
@@ -124,7 +151,7 @@ export default createRule('consistent-selector-style', {
124
151
} ) ;
125
152
return ;
126
153
}
127
- if ( styleValue === 'type' && canUseTypeSelector ( selection , typeSelections ) ) {
154
+ if ( styleValue === 'type' && canUseTypeSelector ( selection , selections . type ) ) {
128
155
context . report ( {
129
156
messageId : 'classShouldBeType' ,
130
157
loc : styleSelectorNodeLoc ( node ) as AST . SourceLocation
@@ -138,7 +165,10 @@ export default createRule('consistent-selector-style', {
138
165
* Checks an ID selector
139
166
*/
140
167
function checkIdSelector ( node : SelectorIdentifier ) : void {
141
- const selection = idSelections . get ( node . value ) ?? [ ] ;
168
+ if ( selections . id . universalSelector ) {
169
+ return ;
170
+ }
171
+ const selection = matchSelection ( selections . id , node . value ) ;
142
172
for ( const styleValue of style ) {
143
173
if ( styleValue === 'class' ) {
144
174
context . report ( {
@@ -150,7 +180,7 @@ export default createRule('consistent-selector-style', {
150
180
if ( styleValue === 'id' ) {
151
181
return ;
152
182
}
153
- if ( styleValue === 'type' && canUseTypeSelector ( selection , typeSelections ) ) {
183
+ if ( styleValue === 'type' && canUseTypeSelector ( selection , selections . type ) ) {
154
184
context . report ( {
155
185
messageId : 'idShouldBeType' ,
156
186
loc : styleSelectorNodeLoc ( node ) as AST . SourceLocation
@@ -164,7 +194,7 @@ export default createRule('consistent-selector-style', {
164
194
* Checks a type selector
165
195
*/
166
196
function checkTypeSelector ( node : SelectorTag ) : void {
167
- const selection = typeSelections . get ( node . value ) ?? [ ] ;
197
+ const selection = selections . type . get ( node . value ) ?? [ ] ;
168
198
for ( const styleValue of style ) {
169
199
if ( styleValue === 'class' ) {
170
200
context . report ( {
@@ -191,21 +221,39 @@ export default createRule('consistent-selector-style', {
191
221
if ( node . kind !== 'html' ) {
192
222
return ;
193
223
}
194
- addToArrayMap ( typeSelections , node . name . name , node ) ;
195
- const classes = node . startTag . attributes . flatMap ( findClassesInAttribute ) ;
196
- for ( const className of classes ) {
197
- addToArrayMap ( classSelections , className , node ) ;
198
- }
224
+ addToArrayMap ( selections . type , node . name . name , node ) ;
199
225
for ( const attribute of node . startTag . attributes ) {
200
226
if ( attribute . type === 'SvelteDirective' && attribute . kind === 'Class' ) {
201
227
whitelistedClasses . push ( attribute . key . name . name ) ;
202
228
}
203
- if ( attribute . type !== 'SvelteAttribute' || attribute . key . name !== 'id' ) {
229
+ for ( const className of findClassesInAttribute ( attribute ) ) {
230
+ addToArrayMap ( selections . class . exact , className , node ) ;
231
+ }
232
+ if ( attribute . type !== 'SvelteAttribute' ) {
204
233
continue ;
205
234
}
206
235
for ( const value of attribute . value ) {
207
- if ( value . type === 'SvelteLiteral' ) {
208
- addToArrayMap ( idSelections , value . value , node ) ;
236
+ if ( attribute . key . name === 'class' && value . type === 'SvelteMustacheTag' ) {
237
+ const prefix = extractExpressionPrefixLiteral ( context , value . expression ) ;
238
+ const suffix = extractExpressionSuffixLiteral ( context , value . expression ) ;
239
+ if ( prefix === null && suffix === null ) {
240
+ selections . class . universalSelector = true ;
241
+ } else {
242
+ addToArrayMap ( selections . class . affixes , [ prefix , suffix ] , node ) ;
243
+ }
244
+ }
245
+ if ( attribute . key . name === 'id' ) {
246
+ if ( value . type === 'SvelteLiteral' ) {
247
+ addToArrayMap ( selections . id . exact , value . value , node ) ;
248
+ } else if ( value . type === 'SvelteMustacheTag' ) {
249
+ const prefix = extractExpressionPrefixLiteral ( context , value . expression ) ;
250
+ const suffix = extractExpressionSuffixLiteral ( context , value . expression ) ;
251
+ if ( prefix === null && suffix === null ) {
252
+ selections . id . universalSelector = true ;
253
+ } else {
254
+ addToArrayMap ( selections . id . affixes , [ prefix , suffix ] , node ) ;
255
+ }
256
+ }
209
257
}
210
258
}
211
259
}
@@ -227,14 +275,27 @@ export default createRule('consistent-selector-style', {
227
275
/**
228
276
* Helper function to add a value to a Map of arrays
229
277
*/
230
- function addToArrayMap (
231
- map : Map < string , AST . SvelteHTMLElement [ ] > ,
232
- key : string ,
278
+ function addToArrayMap < T > (
279
+ map : Map < T , AST . SvelteHTMLElement [ ] > ,
280
+ key : T ,
233
281
value : AST . SvelteHTMLElement
234
282
) : void {
235
283
map . set ( key , ( map . get ( key ) ?? [ ] ) . concat ( value ) ) ;
236
284
}
237
285
286
+ /**
287
+ * Finds all nodes in selections that could be matched by key
288
+ */
289
+ function matchSelection ( selections : Selections , key : string ) : SvelteHTMLElement [ ] {
290
+ const selection = selections . exact . get ( key ) ?? [ ] ;
291
+ selections . affixes . forEach ( ( nodes , [ prefix , suffix ] ) => {
292
+ if ( ( prefix === null || key . startsWith ( prefix ) ) && ( suffix === null || key . endsWith ( suffix ) ) ) {
293
+ selection . push ( ...nodes ) ;
294
+ }
295
+ } ) ;
296
+ return selection ;
297
+ }
298
+
238
299
/**
239
300
* Checks whether a given selection could be obtained using an ID selector
240
301
*/
0 commit comments