Skip to content

Commit 17e7dc9

Browse files
authored
pref: improve 'no-array-index-key' (#913)
1 parent 38d7c0d commit 17e7dc9

File tree

1 file changed

+60
-55
lines changed

1 file changed

+60
-55
lines changed

packages/plugins/eslint-plugin-react-x/src/rules/no-array-index-key.ts

Lines changed: 60 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as AST from "@eslint-react/ast";
22
import { isCloneElementCall, isCreateElementCall, isInitializedFromReact } from "@eslint-react/core";
3-
import { isNullable, O, or } from "@eslint-react/eff";
3+
import { isNullable, O } from "@eslint-react/eff";
44
import { unsafeDecodeSettings } from "@eslint-react/shared";
55
import type { RuleContext, RuleFeature } from "@eslint-react/types";
66
import { 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-
6049
function 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

Comments
 (0)