@@ -6,9 +6,21 @@ import type {
66 Node as SelectorNode ,
77 Tag as SelectorTag
88} from 'postcss-selector-parser' ;
9+ import type { SvelteHTMLElement } from 'svelte-eslint-parser/lib/ast' ;
910import { findClassesInAttribute } from '../utils/ast-utils.js' ;
11+ import {
12+ extractExpressionPrefixLiteral ,
13+ extractExpressionSuffixLiteral
14+ } from '../utils/expression-affixes.js' ;
1015import { createRule } from '../utils/index.js' ;
1116
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+
1224export default createRule ( 'consistent-selector-style' , {
1325 meta : {
1426 docs : {
@@ -62,9 +74,24 @@ export default createRule('consistent-selector-style', {
6274 const style = context . options [ 0 ] ?. style ?? [ 'type' , 'id' , 'class' ] ;
6375
6476 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+ } ;
6895
6996 /**
7097 * Checks selectors in a given PostCSS node
@@ -109,10 +136,10 @@ export default createRule('consistent-selector-style', {
109136 * Checks a class selector
110137 */
111138 function checkClassSelector ( node : SelectorClass ) : void {
112- if ( whitelistedClasses . includes ( node . value ) ) {
139+ if ( selections . class . universalSelector || whitelistedClasses . includes ( node . value ) ) {
113140 return ;
114141 }
115- const selection = classSelections . get ( node . value ) ?? [ ] ;
142+ const selection = matchSelection ( selections . class , node . value ) ;
116143 for ( const styleValue of style ) {
117144 if ( styleValue === 'class' ) {
118145 return ;
@@ -124,7 +151,7 @@ export default createRule('consistent-selector-style', {
124151 } ) ;
125152 return ;
126153 }
127- if ( styleValue === 'type' && canUseTypeSelector ( selection , typeSelections ) ) {
154+ if ( styleValue === 'type' && canUseTypeSelector ( selection , selections . type ) ) {
128155 context . report ( {
129156 messageId : 'classShouldBeType' ,
130157 loc : styleSelectorNodeLoc ( node ) as AST . SourceLocation
@@ -138,7 +165,10 @@ export default createRule('consistent-selector-style', {
138165 * Checks an ID selector
139166 */
140167 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 ) ;
142172 for ( const styleValue of style ) {
143173 if ( styleValue === 'class' ) {
144174 context . report ( {
@@ -150,7 +180,7 @@ export default createRule('consistent-selector-style', {
150180 if ( styleValue === 'id' ) {
151181 return ;
152182 }
153- if ( styleValue === 'type' && canUseTypeSelector ( selection , typeSelections ) ) {
183+ if ( styleValue === 'type' && canUseTypeSelector ( selection , selections . type ) ) {
154184 context . report ( {
155185 messageId : 'idShouldBeType' ,
156186 loc : styleSelectorNodeLoc ( node ) as AST . SourceLocation
@@ -164,7 +194,7 @@ export default createRule('consistent-selector-style', {
164194 * Checks a type selector
165195 */
166196 function checkTypeSelector ( node : SelectorTag ) : void {
167- const selection = typeSelections . get ( node . value ) ?? [ ] ;
197+ const selection = selections . type . get ( node . value ) ?? [ ] ;
168198 for ( const styleValue of style ) {
169199 if ( styleValue === 'class' ) {
170200 context . report ( {
@@ -191,21 +221,39 @@ export default createRule('consistent-selector-style', {
191221 if ( node . kind !== 'html' ) {
192222 return ;
193223 }
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 ) ;
199225 for ( const attribute of node . startTag . attributes ) {
200226 if ( attribute . type === 'SvelteDirective' && attribute . kind === 'Class' ) {
201227 whitelistedClasses . push ( attribute . key . name . name ) ;
202228 }
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' ) {
204233 continue ;
205234 }
206235 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+ }
209257 }
210258 }
211259 }
@@ -227,14 +275,27 @@ export default createRule('consistent-selector-style', {
227275/**
228276 * Helper function to add a value to a Map of arrays
229277 */
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 ,
233281 value : AST . SvelteHTMLElement
234282) : void {
235283 map . set ( key , ( map . get ( key ) ?? [ ] ) . concat ( value ) ) ;
236284}
237285
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+
238299/**
239300 * Checks whether a given selection could be obtained using an ID selector
240301 */
0 commit comments