11import * as AST from "@eslint-react/ast" ;
22import { isCloneElementCall , isCreateElementCall , isInitializedFromReact } from "@eslint-react/core" ;
3- import { isNullable , O , or } from "@eslint-react/eff" ;
3+ import { isNullable , O } from "@eslint-react/eff" ;
44import { unsafeDecodeSettings } from "@eslint-react/shared" ;
55import type { RuleContext , RuleFeature } from "@eslint-react/types" ;
66import { AST_NODE_TYPES as T } from "@typescript-eslint/types" ;
@@ -46,17 +46,6 @@ const iteratorFunctionIndexParamPosition = new Map<string, number>([
4646
4747// #region Helpers
4848
49- const isToStringCall = isMatching ( {
50- type : T . CallExpression ,
51- callee : {
52- type : T . MemberExpression ,
53- property : {
54- type : T . Identifier ,
55- name : "toString" ,
56- } ,
57- } ,
58- } ) ;
59-
6049function isReactChildrenMethod ( name : string ) : name is typeof reactChildrenMethod [ number ] {
6150 return reactChildrenMethod . some ( ( method ) => method === name ) ;
6251}
@@ -132,53 +121,70 @@ export default createRule<[], MessageID>({
132121 create ( context ) {
133122 const indexParamNames : O . Option < string > [ ] = [ ] ;
134123
135- const isCreateOrCloneElementCall = or ( isCreateElementCall ( context ) , isCloneElementCall ( context ) ) ;
136-
137124 function isArrayIndex ( node : TSESTree . Node ) : node is TSESTree . Identifier {
138- return node . type === T . Identifier && indexParamNames . some ( O . exists ( ( name ) => name === node . name ) ) ;
125+ return node . type === T . Identifier
126+ && indexParamNames . some ( O . exists ( ( name ) => name === node . name ) ) ;
139127 }
140128
141- function getReportDescriptor ( node : TSESTree . Node ) : ReportDescriptor < MessageID > [ ] {
142- // key={bar}
143- if ( isArrayIndex ( node ) ) {
144- return [ { messageId : "noArrayIndexKey" , node } ] ;
145- }
146- // key={`foo-${bar}`} or key={'foo' + bar}
147- if ( AST . isOneOf ( [ T . TemplateLiteral , T . BinaryExpression ] ) ( node ) ) {
148- const exps = T . TemplateLiteral === node . type
149- ? node . expressions
150- : AST . getIdentifiersFromBinaryExpression ( node ) ;
151- return exps . reduce < ReportDescriptor < MessageID > [ ] > ( ( acc , exp ) => {
152- if ( isArrayIndex ( exp ) ) {
153- return [ ...acc , { messageId : "noArrayIndexKey" , node : exp } ] ;
154- }
155- return acc ;
156- } , [ ] ) ;
157- }
129+ function isCreateOrCloneElementCall ( node : TSESTree . Node ) : node is TSESTree . CallExpression {
130+ return isCreateElementCall ( node , context ) || isCloneElementCall ( node , context ) ;
131+ }
158132
159- // key={bar.toString()}
160- if ( isToStringCall ( node ) ) {
161- if ( ! ( "object" in node . callee && isArrayIndex ( node . callee . object ) ) ) {
133+ function getReportDescriptors ( node : TSESTree . Node ) : ReportDescriptor < MessageID > [ ] {
134+ switch ( node . type ) {
135+ // key={bar}
136+ case T . Identifier : {
137+ if ( indexParamNames . some ( O . exists ( ( name ) => name === node . name ) ) ) {
138+ return [ {
139+ messageId : "noArrayIndexKey" ,
140+ node,
141+ } ] ;
142+ }
162143 return [ ] ;
163144 }
164-
165- return [ { messageId : "noArrayIndexKey" , node : node . callee . object } ] ;
166- }
167- // key={String(bar)}
168- const isStringCall = isMatching ( {
169- type : T . CallExpression ,
170- callee : {
171- type : T . Identifier ,
172- name : "String" ,
173- } ,
174- } , node ) ;
175- if ( isStringCall ) {
176- const [ arg ] = node . arguments ;
177- if ( arg && isArrayIndex ( arg ) ) {
178- return [ { messageId : "noArrayIndexKey" , node : arg } ] ;
145+ // key={`foo-${bar}`} or key={'foo' + bar}
146+ case T . TemplateLiteral :
147+ case T . BinaryExpression : {
148+ const descriptors : ReportDescriptor < MessageID > [ ] = [ ] ;
149+ const expressions = node . type === T . TemplateLiteral
150+ ? node . expressions
151+ : AST . getIdentifiersFromBinaryExpression ( node ) ;
152+ for ( const expression of expressions ) {
153+ if ( isArrayIndex ( expression ) ) {
154+ descriptors . push ( {
155+ messageId : "noArrayIndexKey" ,
156+ node : expression ,
157+ } ) ;
158+ }
159+ }
160+ return descriptors ;
161+ }
162+ // key={bar.toString()} or key={String(bar)}
163+ case T . CallExpression : {
164+ switch ( true ) {
165+ // key={bar.toString()}
166+ case node . callee . type === T . MemberExpression
167+ && node . callee . property . type === T . Identifier
168+ && node . callee . property . name === "toString"
169+ && isArrayIndex ( node . callee . object ) : {
170+ return [ {
171+ messageId : "noArrayIndexKey" ,
172+ node : node . callee . object ,
173+ } ] ;
174+ }
175+ // key={String(bar)}
176+ case node . callee . type === T . Identifier
177+ && node . callee . name === "String"
178+ && node . arguments [ 0 ]
179+ && isArrayIndex ( node . arguments [ 0 ] ) : {
180+ return [ {
181+ messageId : "noArrayIndexKey" ,
182+ node : node . arguments [ 0 ] ,
183+ } ] ;
184+ }
185+ }
179186 }
180187 }
181-
182188 return [ ] ;
183189 }
184190
@@ -202,7 +208,7 @@ export default createRule<[], MessageID>({
202208 if ( ! ( "value" in prop ) ) {
203209 continue ;
204210 }
205- const descriptors = getReportDescriptor ( prop . value ) ;
211+ const descriptors = getReportDescriptors ( prop . value ) ;
206212 for ( const descriptor of descriptors ) {
207213 context . report ( descriptor ) ;
208214 }
@@ -218,11 +224,10 @@ export default createRule<[], MessageID>({
218224 if ( indexParamNames . length === 0 ) {
219225 return ;
220226 }
221- const { value } = node ;
222- if ( value ?. type !== T . JSXExpressionContainer ) {
227+ if ( node . value ?. type !== T . JSXExpressionContainer ) {
223228 return ;
224229 }
225- const descriptors = getReportDescriptor ( value . expression ) ;
230+ const descriptors = getReportDescriptors ( node . value . expression ) ;
226231 for ( const descriptor of descriptors ) {
227232 context . report ( descriptor ) ;
228233 }
0 commit comments