@@ -31,64 +31,182 @@ export default createRule<Options, MessageIds>({
3131 } ,
3232 defaultOptions : [ ] ,
3333 create : ( context ) => {
34- return {
35- 'VariableDeclarator[id.name!=/^select[^a-z].+$/]:not(:has(Identifier[name="createFeature"])):matches([id.typeAnnotation.typeAnnotation.typeName.name=/^MemoizedSelector(WithProps)?$/], :has(CallExpression[callee.name=/^(create(Feature)?Selector|createSelectorFactory)$/]))' ( {
36- id,
37- } : TSESTree . VariableDeclarator & { id : TSESTree . Identifier } ) {
38- const suggestedName = getSuggestedName ( id . name ) ;
34+ function reportIfInvalid ( name : string , node : TSESTree . Identifier ) {
35+ // Name starts with select and
36+ // the first character after select is an uppercase ASCII letter, _, or $
37+ const isValid =
38+ name . startsWith ( 'select' ) &&
39+ name . length > 'select' . length &&
40+ / ^ [ A - Z _ $ ] / . test ( name . slice ( 'select' . length ) ) ;
41+
42+ if ( ! isValid ) {
43+ const suggestedName = getSuggestedName ( name ) ;
3944 context . report ( {
45+ node,
4046 loc : {
41- ... id . loc ,
47+ start : node . loc . start ,
4248 end : {
43- ...id . loc . end ,
44- column : id . typeAnnotation ?. range [ 0 ]
45- ? id . typeAnnotation . range [ 0 ] - 1
46- : id . loc . end . column ,
49+ line : node . loc . start . line ,
50+ column : node . loc . start . column + name . length ,
4751 } ,
4852 } ,
4953 messageId : prefixSelectorsWithSelect ,
5054 suggest : [
5155 {
5256 messageId : prefixSelectorsWithSelectSuggest ,
53- data : {
54- name : suggestedName ,
57+ data : { name : suggestedName } ,
58+ fix : ( fixer ) => {
59+ const parent = node . parent ;
60+ const sourceCode = context . getSourceCode ( ) ;
61+
62+ // Handle destructuring: { selectAll: allItems }
63+ if (
64+ parent &&
65+ parent . type === 'Property' &&
66+ parent . value === node &&
67+ parent . parent &&
68+ parent . parent . type === 'ObjectPattern'
69+ ) {
70+ return fixer . replaceText ( node , suggestedName ) ;
71+ }
72+
73+ // Handle simple variable declarator: const allItems = ...
74+ if (
75+ parent &&
76+ parent . type === 'VariableDeclarator' &&
77+ parent . id . type === 'Identifier'
78+ ) {
79+ const typeAnnotation = parent . id . typeAnnotation
80+ ? sourceCode . getText ( parent . id . typeAnnotation )
81+ : '' ;
82+ return fixer . replaceText (
83+ parent . id ,
84+ `${ suggestedName } ${ typeAnnotation } `
85+ ) ;
86+ }
87+
88+ // Fallback: just replace the identifier
89+ return fixer . replaceText ( node , suggestedName ) ;
5590 } ,
56- fix : ( fixer ) =>
57- fixer . replaceTextRange (
58- [ id . range [ 0 ] , id . typeAnnotation ?. range [ 0 ] ?? id . range [ 1 ] ] ,
59- suggestedName
60- ) ,
6191 } ,
6292 ] ,
6393 } ) ;
94+ }
95+ }
96+
97+ function isSelectorFactoryCall ( node : TSESTree . CallExpression ) : boolean {
98+ const callee = node . callee ;
99+ return (
100+ callee . type === 'Identifier' &&
101+ [
102+ 'createSelector' ,
103+ 'createFeatureSelector' ,
104+ 'createSelectorFactory' ,
105+ ] . includes ( callee . name )
106+ ) ;
107+ }
108+
109+ function checkFunctionBody (
110+ name : string ,
111+ node : TSESTree . Identifier ,
112+ body : TSESTree . BlockStatement | TSESTree . Expression
113+ ) {
114+ if ( body . type === 'CallExpression' && isSelectorFactoryCall ( body ) ) {
115+ reportIfInvalid ( name , node ) ;
116+ }
117+
118+ if ( body . type === 'BlockStatement' ) {
119+ for ( const stmt of body . body ) {
120+ if (
121+ stmt . type === 'ReturnStatement' &&
122+ stmt . argument &&
123+ stmt . argument . type === 'CallExpression' &&
124+ isSelectorFactoryCall ( stmt . argument )
125+ ) {
126+ reportIfInvalid ( name , node ) ;
127+ }
128+ }
129+ }
130+ }
131+
132+ return {
133+ VariableDeclarator ( node : TSESTree . VariableDeclarator ) {
134+ const { id, init } = node ;
135+
136+ const isSelectorSource =
137+ init ?. type === 'CallExpression' &&
138+ ( ( init . callee . type === 'Identifier' &&
139+ init . callee . name === 'getSelectors' ) ||
140+ ( init . callee . type === 'MemberExpression' &&
141+ init . callee . property . type === 'Identifier' &&
142+ init . callee . property . name === 'getSelectors' ) ) ;
143+
144+ if ( id . type === 'ObjectPattern' && isSelectorSource ) {
145+ for ( const prop of id . properties ) {
146+ if ( prop . type === 'Property' && prop . value . type === 'Identifier' ) {
147+ reportIfInvalid ( prop . value . name , prop . value ) ;
148+ }
149+ }
150+ return ;
151+ }
152+
153+ if ( id . type === 'Identifier' ) {
154+ const typeName =
155+ node . id . typeAnnotation ?. typeAnnotation . type === 'TSTypeReference' &&
156+ node . id . typeAnnotation . typeAnnotation . typeName . type === 'Identifier'
157+ ? node . id . typeAnnotation . typeAnnotation . typeName . name
158+ : null ;
159+
160+ const hasSelectorType =
161+ typeName !== null && / S e l e c t o r $ / . test ( typeName ) ;
162+
163+ const isSelectorCall =
164+ init ?. type === 'CallExpression' && isSelectorFactoryCall ( init ) ;
165+
166+ const isArrowFunction =
167+ init ?. type === 'ArrowFunctionExpression' &&
168+ init . body &&
169+ ( init . body . type === 'CallExpression' ||
170+ init . body . type === 'BlockStatement' ) ;
171+
172+ const isFunctionExpression =
173+ init ?. type === 'FunctionExpression' &&
174+ init . body &&
175+ init . body . type === 'BlockStatement' ;
176+
177+ if ( hasSelectorType || isSelectorCall ) {
178+ reportIfInvalid ( id . name , id ) ;
179+ } else if ( isArrowFunction || isFunctionExpression ) {
180+ checkFunctionBody ( id . name , id , init . body ) ;
181+ }
182+ }
64183 } ,
65184 } ;
66185 } ,
67186} ) ;
68187
69- function getSuggestedName ( name : string ) {
188+ function getSuggestedName ( name : string ) : string {
70189 const selectWord = 'select' ;
71- // Ex: 'selectfeature' => 'selectFeature'
72- let possibleReplacedName = name . replace (
73- new RegExp ( `^${ selectWord } (.+)` ) ,
74- ( _ , word : string ) => {
75- return `${ selectWord } ${ capitalize ( word ) } ` ;
76- }
77- ) ;
78190
79- if ( name !== possibleReplacedName ) {
80- return possibleReplacedName ;
191+ if ( name . startsWith ( selectWord ) ) {
192+ const rest = name . slice ( selectWord . length ) ;
193+ if ( rest . length === 0 ) {
194+ return 'selectSelect' ;
195+ }
196+ if ( / ^ [ A - Z _ ] + $ / . test ( rest ) ) {
197+ return `${ selectWord } ${ rest } ` ;
198+ }
199+ return `${ selectWord } ${ capitalize ( rest ) } ` ;
81200 }
82201
83- // Ex: 'getCount' => 'selectCount'
84- possibleReplacedName = name . replace ( / ^ g e t ( [ ^ a - z ] . + ) / , ( _ , word : string ) => {
85- return `${ selectWord } ${ capitalize ( word ) } ` ;
86- } ) ;
202+ if ( / ^ g e t ( [ ^ a - z ] . + ) / . test ( name ) ) {
203+ const rest = name . slice ( 3 ) ;
204+ return `${ selectWord } ${ capitalize ( rest ) } ` ;
205+ }
87206
88- if ( name !== possibleReplacedName ) {
89- return possibleReplacedName ;
207+ if ( / ^ [ A - Z _ ] + $ / . test ( name ) ) {
208+ return ` ${ selectWord } ${ name } ` ;
90209 }
91210
92- // Ex: 'item' => 'selectItem'
93211 return `${ selectWord } ${ capitalize ( name ) } ` ;
94212}
0 commit comments