Skip to content

Commit 7f8747d

Browse files
authored
feat(linter): implement react/no-array-index-key (#6960)
Implement not recommended rule `no-array-index-key` (#1022 )
1 parent 8cfea3c commit 7f8747d

File tree

3 files changed

+408
-0
lines changed

3 files changed

+408
-0
lines changed

crates/oxc_linter/src/rules.rs

+2
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,7 @@ mod react {
253253
pub mod jsx_no_undef;
254254
pub mod jsx_no_useless_fragment;
255255
pub mod jsx_props_no_spread_multi;
256+
pub mod no_array_index_key;
256257
pub mod no_children_prop;
257258
pub mod no_danger;
258259
pub mod no_danger_with_children;
@@ -804,6 +805,7 @@ oxc_macros::declare_all_lint_rules! {
804805
react::jsx_no_undef,
805806
react::jsx_no_useless_fragment,
806807
react::jsx_props_no_spread_multi,
808+
react::no_array_index_key,
807809
react::no_children_prop,
808810
react::no_danger_with_children,
809811
react::no_danger,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
1+
use oxc_ast::{
2+
ast::{
3+
Argument, CallExpression, Expression, JSXAttributeItem, JSXAttributeName,
4+
JSXAttributeValue, JSXElement, JSXExpression, ObjectPropertyKind, PropertyKey,
5+
},
6+
AstKind,
7+
};
8+
use oxc_diagnostics::OxcDiagnostic;
9+
use oxc_macros::declare_oxc_lint;
10+
use oxc_span::Span;
11+
12+
use crate::{ast_util::is_method_call, context::LintContext, rule::Rule, AstNode};
13+
14+
fn no_array_index_key_diagnostic(span: Span) -> OxcDiagnostic {
15+
OxcDiagnostic::warn("Usage of Array index in keys is not allowed")
16+
.with_help("Use a unique data-dependent key to avoid unnecessary rerenders")
17+
.with_label(span)
18+
}
19+
20+
#[derive(Debug, Default, Clone)]
21+
pub struct NoArrayIndexKey;
22+
23+
declare_oxc_lint!(
24+
/// ### What it does
25+
/// Warn if an element uses an Array index in its key.
26+
///
27+
/// ### Why is this bad?
28+
/// It's a bad idea to use the array index since it doesn't uniquely identify your elements.
29+
/// In cases where the array is sorted or an element is added to the beginning of the array,
30+
/// the index will be changed even though the element representing that index may be the same.
31+
/// This results in unnecessary renders.
32+
///
33+
/// ### Examples
34+
///
35+
/// Examples of **incorrect** code for this rule:
36+
/// ```jsx
37+
/// things.map((thing, index) => (
38+
/// <Hello key={index} />
39+
/// ));
40+
/// ```
41+
///
42+
/// Examples of **correct** code for this rule:
43+
/// ```jsx
44+
/// things.map((thing, index) => (
45+
/// <Hello key={thing.id} />
46+
/// ));
47+
/// ```
48+
NoArrayIndexKey,
49+
perf,
50+
);
51+
52+
fn check_jsx_element<'a>(
53+
jsx: &'a JSXElement,
54+
node: &'a AstNode,
55+
ctx: &'a LintContext,
56+
prop_name: &'static str,
57+
) {
58+
let Some(index_param_name) = find_index_param_name(node, ctx) else {
59+
return;
60+
};
61+
62+
for attr in &jsx.opening_element.attributes {
63+
let JSXAttributeItem::Attribute(attr) = attr else {
64+
return;
65+
};
66+
67+
let JSXAttributeName::Identifier(ident) = &attr.name else {
68+
return;
69+
};
70+
71+
if ident.name.as_str() != prop_name {
72+
return;
73+
}
74+
75+
let Some(JSXAttributeValue::ExpressionContainer(container)) = &attr.value else {
76+
return;
77+
};
78+
79+
let JSXExpression::Identifier(expr) = &container.expression else {
80+
return;
81+
};
82+
83+
if expr.name.as_str() == index_param_name {
84+
ctx.diagnostic(no_array_index_key_diagnostic(attr.span));
85+
}
86+
}
87+
}
88+
89+
fn check_react_clone_element<'a>(
90+
call_expr: &'a CallExpression,
91+
node: &'a AstNode,
92+
ctx: &'a LintContext,
93+
) {
94+
let Some(index_param_name) = find_index_param_name(node, ctx) else {
95+
return;
96+
};
97+
98+
if is_method_call(call_expr, Some(&["React"]), Some(&["cloneElement"]), Some(2), Some(3)) {
99+
let Some(Argument::ObjectExpression(obj_expr)) = call_expr.arguments.get(1) else {
100+
return;
101+
};
102+
103+
for prop_kind in &obj_expr.properties {
104+
let ObjectPropertyKind::ObjectProperty(prop) = prop_kind else {
105+
continue;
106+
};
107+
108+
let PropertyKey::StaticIdentifier(key_ident) = &prop.key else {
109+
continue;
110+
};
111+
112+
let Expression::Identifier(value_ident) = &prop.value else {
113+
continue;
114+
};
115+
116+
if key_ident.name.as_str() == "key" && value_ident.name.as_str() == index_param_name {
117+
ctx.diagnostic(no_array_index_key_diagnostic(obj_expr.span));
118+
}
119+
}
120+
}
121+
}
122+
123+
fn find_index_param_name<'a>(node: &'a AstNode, ctx: &'a LintContext) -> Option<&'a str> {
124+
for ancestor in ctx.nodes().iter_parents(node.id()).skip(1) {
125+
if let AstKind::CallExpression(call_expr) = ancestor.kind() {
126+
let Expression::StaticMemberExpression(expr) = &call_expr.callee else {
127+
return None;
128+
};
129+
130+
if SECOND_INDEX_METHODS.contains(expr.property.name.as_str()) {
131+
return find_index_param_name_by_position(call_expr, 1);
132+
}
133+
134+
if THIRD_INDEX_METHODS.contains(expr.property.name.as_str()) {
135+
return find_index_param_name_by_position(call_expr, 2);
136+
}
137+
}
138+
}
139+
140+
None
141+
}
142+
143+
fn find_index_param_name_by_position<'a>(
144+
call_expr: &'a CallExpression,
145+
position: usize,
146+
) -> Option<&'a str> {
147+
match &call_expr.arguments[0] {
148+
Argument::ArrowFunctionExpression(arrow_fn_expr) => {
149+
return Some(
150+
arrow_fn_expr.params.items.get(position)?.pattern.get_identifier()?.as_str(),
151+
);
152+
}
153+
154+
Argument::FunctionExpression(regular_fn_expr) => {
155+
return Some(
156+
regular_fn_expr.params.items.get(position)?.pattern.get_identifier()?.as_str(),
157+
);
158+
}
159+
160+
_ => (),
161+
}
162+
163+
None
164+
}
165+
166+
const SECOND_INDEX_METHODS: phf::Set<&'static str> = phf::phf_set! {
167+
// things.map((thing, index) => (<Hello key={index} />));
168+
"map",
169+
// things.forEach((thing, index) => {otherThings.push(<Hello key={index} />);});
170+
"forEach",
171+
// things.filter((thing, index) => {otherThings.push(<Hello key={index} />);});
172+
"filter",
173+
// things.some((thing, index) => {otherThings.push(<Hello key={index} />);});
174+
"some",
175+
// things.every((thing, index) => {otherThings.push(<Hello key={index} />);});
176+
"every",
177+
// things.find((thing, index) => {otherThings.push(<Hello key={index} />);});
178+
"find",
179+
// things.findIndex((thing, index) => {otherThings.push(<Hello key={index} />);});
180+
"findIndex",
181+
// things.flatMap((thing, index) => (<Hello key={index} />));
182+
"flatMap",
183+
};
184+
185+
const THIRD_INDEX_METHODS: phf::Set<&'static str> = phf::phf_set! {
186+
// things.reduce((collection, thing, index) => (collection.concat(<Hello key={index} />)), []);
187+
"reduce",
188+
// things.reduceRight((collection, thing, index) => (collection.concat(<Hello key={index} />)), []);
189+
"reduceRight",
190+
};
191+
192+
impl Rule for NoArrayIndexKey {
193+
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
194+
match node.kind() {
195+
AstKind::JSXElement(jsx) => {
196+
check_jsx_element(jsx, node, ctx, "key");
197+
}
198+
AstKind::CallExpression(call_expr) => {
199+
check_react_clone_element(call_expr, node, ctx);
200+
}
201+
_ => (),
202+
}
203+
}
204+
}
205+
206+
#[test]
207+
fn test() {
208+
use crate::tester::Tester;
209+
210+
let pass = vec![
211+
r"things.map((thing) => (
212+
<Hello key={thing.id} />
213+
));
214+
",
215+
r"things.map((thing, index) => (
216+
React.cloneElement(thing, { key: thing.id })
217+
));
218+
",
219+
r"things.forEach((thing, index) => {
220+
otherThings.push(<Hello key={thing.id} />);
221+
});
222+
",
223+
r"things.filter((thing, index) => {
224+
otherThings.push(<Hello key={thing.id} />);
225+
});
226+
",
227+
r"things.some((thing, index) => {
228+
otherThings.push(<Hello key={thing.id} />);
229+
});
230+
",
231+
r"things.every((thing, index) => {
232+
otherThings.push(<Hello key={thing.id} />);
233+
});
234+
",
235+
r"things.find((thing, index) => {
236+
otherThings.push(<Hello key={thing.id} />);
237+
});
238+
",
239+
r"things.findIndex((thing, index) => {
240+
otherThings.push(<Hello key={thing.id} />);
241+
});
242+
",
243+
r"things.flatMap((thing, index) => (
244+
<Hello key={thing.id} />
245+
));
246+
",
247+
r"things.reduce((collection, thing, index) => (
248+
collection.concat(<Hello key={thing.id} />)
249+
), []);
250+
",
251+
r"things.reduceRight((collection, thing, index) => (
252+
collection.concat(<Hello key={thing.id} />)
253+
), []);
254+
",
255+
];
256+
257+
let fail = vec![
258+
r"things.map((thing, index) => (
259+
<Hello key={index} />
260+
));
261+
",
262+
r"things.map((thing, index) => (
263+
React.cloneElement(thing, { key: index })
264+
));
265+
",
266+
r"things.forEach((thing, index) => {
267+
otherThings.push(<Hello key={index} />);
268+
});
269+
",
270+
r"things.filter((thing, index) => {
271+
otherThings.push(<Hello key={index} />);
272+
});
273+
",
274+
r"things.some((thing, index) => {
275+
otherThings.push(<Hello key={index} />);
276+
});
277+
",
278+
r"things.every((thing, index) => {
279+
otherThings.push(<Hello key={index} />);
280+
});
281+
",
282+
r"things.find((thing, index) => {
283+
otherThings.push(<Hello key={index} />);
284+
});
285+
",
286+
r"things.findIndex((thing, index) => {
287+
otherThings.push(<Hello key={index} />);
288+
});
289+
",
290+
r"things.flatMap((thing, index) => (
291+
<Hello key={index} />
292+
));
293+
",
294+
r"things.reduce((collection, thing, index) => (
295+
collection.concat(<Hello key={index} />)
296+
), []);
297+
",
298+
r"things.reduceRight((collection, thing, index) => (
299+
collection.concat(<Hello key={index} />)
300+
), []);
301+
",
302+
];
303+
304+
Tester::new(NoArrayIndexKey::NAME, pass, fail).test_and_snapshot();
305+
}

0 commit comments

Comments
 (0)