Skip to content

Commit 11e0440

Browse files
authored
fix(linter/jsx-handler-name): improve handler name position in error messages (#14174)
With this change, when an event handler name is detected, the underline in the error message will point only to the event handler name.
1 parent 3374b8e commit 11e0440

File tree

2 files changed

+170
-66
lines changed

2 files changed

+170
-66
lines changed

crates/oxc_linter/src/rules/react/jsx_handler_names.rs

Lines changed: 113 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use oxc_ast::{
55
ast::{
66
ArrowFunctionExpression, Expression, JSXAttributeName, JSXAttributeValue, JSXElementName,
77
JSXExpression, JSXMemberExpression, JSXMemberExpressionObject, Statement,
8+
StaticMemberExpression,
89
},
910
};
1011
use oxc_diagnostics::OxcDiagnostic;
@@ -141,12 +142,9 @@ fn build_event_handler_regex(handler_prefix: &str, handler_prop_prefix: &str) ->
141142
if prefix_pattern.is_empty() || prop_prefix_pattern.is_empty() {
142143
return None;
143144
}
144-
let regex = RegexBuilder::new(
145-
format!(r"^((props\.({prop_prefix_pattern}))|((.*\.)?({prefix_pattern})))[0-9]*[A-Z].*$")
146-
.as_str(),
147-
)
148-
.build()
149-
.expect("Failed to compile regex for event handler prefixes");
145+
let regex = RegexBuilder::new(format!(r"^((.*\.)?({prefix_pattern}))[0-9]*[A-Z].*$").as_str())
146+
.build()
147+
.expect("Failed to compile regex for event handler prefixes");
150148
Some(regex)
151149
}
152150

@@ -160,7 +158,7 @@ fn build_event_handler_prop_regex(handler_prop_prefix: &str) -> Option<Regex> {
160158
if prop_prefix_pattern.is_empty() {
161159
return None;
162160
}
163-
let regex = RegexBuilder::new(format!(r"^(({prop_prefix_pattern})[A-Z].*|ref)$").as_str())
161+
let regex = RegexBuilder::new(format!(r"^({prop_prefix_pattern})[A-Z].*$").as_str())
164162
.build()
165163
.expect("Failed to compile regex for event handler prop prefixes");
166164
Some(regex)
@@ -274,13 +272,44 @@ impl Rule for JsxHandlerNames {
274272
return;
275273
};
276274
let value_expr = &expression_container.expression;
277-
let is_inline_handler = matches!(value_expr, JSXExpression::ArrowFunctionExpression(_));
278-
if !self.check_inline_functions && is_inline_handler {
279-
return;
280-
}
281-
if !self.check_local_variables && !is_member_expression_event_handler(value_expr) {
282-
return;
283-
}
275+
276+
let (handler_name, handler_span, is_props_handler) = match value_expr {
277+
JSXExpression::ArrowFunctionExpression(arrow_function) => {
278+
if !self.check_inline_functions {
279+
return;
280+
}
281+
if !self.check_local_variables && !is_member_expression_callee(arrow_function) {
282+
return;
283+
}
284+
if let Some((name, span, is_props_handler)) =
285+
get_event_handler_name_from_arrow_function(arrow_function)
286+
{
287+
(Some(name), span, is_props_handler)
288+
} else {
289+
(None, arrow_function.body.span, false)
290+
}
291+
}
292+
JSXExpression::Identifier(ident) => {
293+
if !self.check_local_variables {
294+
return;
295+
}
296+
(Some(ident.name.as_str().into()), ident.span, false)
297+
}
298+
JSXExpression::StaticMemberExpression(member_expr) => {
299+
let (name, span, is_props_handler) =
300+
get_event_handler_name_from_static_member_expression(member_expr);
301+
(Some(name), span, is_props_handler)
302+
}
303+
_ => {
304+
if !self.check_local_variables && !value_expr.is_member_expression() {
305+
return;
306+
}
307+
// For other expressions types, use the whole content inside the braces as the handler name,
308+
// which will be marked as a bad handler name if the prop key is an event handler prop.
309+
let span = expression_container.span.shrink(1);
310+
(Some(normalize_handler_name(ctx.source_range(span))), span, false)
311+
}
312+
};
284313

285314
let (prop_key, prop_span) = match &jsx_attribute.name {
286315
JSXAttributeName::Identifier(ident) => (ident.name.as_str(), ident.span),
@@ -289,32 +318,24 @@ impl Rule for JsxHandlerNames {
289318
}
290319
};
291320

292-
let prop_value = if self.check_inline_functions && is_inline_handler {
293-
match &expression_container.expression {
294-
JSXExpression::ArrowFunctionExpression(arrow_function) => {
295-
extract_callee_name_from_arrow_function(arrow_function)
296-
.map(normalize_handler_name)
297-
}
298-
_ => None,
299-
}
300-
} else {
301-
Some(normalize_handler_name(ctx.source_range(expression_container.span.shrink(1))))
302-
};
303-
321+
// "ref" prop is allowed to be assigned to a function with any name.
304322
if prop_key == "ref" {
305323
return;
306324
}
307325

308-
let prop_is_event_handler =
309-
self.event_handler_prop_regex.as_ref().map(|r| r.is_match(prop_key));
310-
let is_handler_name_correct = prop_value
311-
.as_ref()
312-
.map_or(Some(false), |v| self.event_handler_regex.as_ref().map(|r| r.is_match(v)));
326+
let prop_is_event_handler = self.match_event_handler_props_name(prop_key);
327+
let is_handler_name_correct = handler_name.as_ref().map_or(Some(false), |name| {
328+
// if the event handler is "this.props.*" or "props.*", the handler name can be the pattern of event handler props.
329+
if is_props_handler && self.match_event_handler_props_name(name).unwrap_or(false) {
330+
return Some(true);
331+
}
332+
self.match_event_handler_name(name)
333+
});
313334

314-
match (prop_value, prop_is_event_handler, is_handler_name_correct) {
335+
match (handler_name, prop_is_event_handler, is_handler_name_correct) {
315336
(value, Some(true), Some(false)) => {
316337
ctx.diagnostic(bad_handler_name_diagnostic(
317-
expression_container.span,
338+
handler_span,
318339
prop_key,
319340
value,
320341
&self.event_handler_prefixes,
@@ -335,12 +356,19 @@ impl Rule for JsxHandlerNames {
335356
}
336357
}
337358

359+
impl JsxHandlerNames {
360+
fn match_event_handler_props_name(&self, name: &str) -> Option<bool> {
361+
self.event_handler_prop_regex.as_ref().map(|r| r.is_match(name))
362+
}
363+
364+
fn match_event_handler_name(&self, name: &str) -> Option<bool> {
365+
self.event_handler_regex.as_ref().map(|r| r.is_match(name))
366+
}
367+
}
368+
338369
/// true if the expression is in the form of "foo.bar" or "() => foo.bar()"
339370
/// like event handler methods in class components.
340-
fn is_member_expression_event_handler(value_expr: &JSXExpression<'_>) -> bool {
341-
let JSXExpression::ArrowFunctionExpression(arrow_function) = value_expr else {
342-
return value_expr.is_member_expression();
343-
};
371+
fn is_member_expression_callee(arrow_function: &ArrowFunctionExpression<'_>) -> bool {
344372
let Some(Statement::ExpressionStatement(stmt)) = arrow_function.body.statements.first() else {
345373
return false;
346374
};
@@ -350,11 +378,35 @@ fn is_member_expression_event_handler(value_expr: &JSXExpression<'_>) -> bool {
350378
callee_expr.callee.is_member_expression()
351379
}
352380

381+
fn get_event_handler_name_from_static_member_expression(
382+
member_expr: &StaticMemberExpression,
383+
) -> (CompactStr, Span, bool) {
384+
let name = member_expr.property.name.as_str();
385+
let span = member_expr.property.span;
386+
match &member_expr.object {
387+
Expression::Identifier(ident) => {
388+
let obj_name = ident.name.as_str();
389+
(name.into(), span, obj_name == "props") // props.handleChange or obj.handleChange
390+
}
391+
Expression::StaticMemberExpression(expr) => {
392+
if let Expression::ThisExpression(_) = &expr.object {
393+
let obj_name = expr.property.name.as_str();
394+
(name.into(), span, obj_name == "props") // this.props.handleChange or this.obj.handleChange
395+
} else {
396+
(name.into(), span, false) // foo.props.handleChange, props.foo.handleChange, foo.bar.handleChange, etc.
397+
}
398+
}
399+
_ => (name.into(), span, false), // this.handleChange
400+
}
401+
}
402+
353403
fn get_element_name(name: &JSXElementName<'_>) -> CompactStr {
354404
match name {
355405
JSXElementName::Identifier(ident) => ident.name.as_str().into(),
356406
JSXElementName::IdentifierReference(ident) => ident.name.as_str().into(),
357-
JSXElementName::MemberExpression(member_expr) => get_member_expression_name(member_expr),
407+
JSXElementName::MemberExpression(member_expr) => {
408+
get_element_name_of_member_expression(member_expr)
409+
}
358410
JSXElementName::NamespacedName(namespaced_name) => format!(
359411
"{}:{}",
360412
namespaced_name.namespace.name.as_str(),
@@ -365,13 +417,13 @@ fn get_element_name(name: &JSXElementName<'_>) -> CompactStr {
365417
}
366418
}
367419

368-
fn get_member_expression_name(member_expr: &JSXMemberExpression) -> CompactStr {
420+
fn get_element_name_of_member_expression(member_expr: &JSXMemberExpression) -> CompactStr {
369421
match &member_expr.object {
370422
JSXMemberExpressionObject::IdentifierReference(ident) => ident.name.as_str().into(),
371423
JSXMemberExpressionObject::ThisExpression(_) => "this".into(),
372424
JSXMemberExpressionObject::MemberExpression(next_expr) => format!(
373425
"{}.{}",
374-
get_member_expression_name(next_expr),
426+
get_element_name_of_member_expression(next_expr),
375427
member_expr.property.name.as_str()
376428
)
377429
.into(),
@@ -403,9 +455,9 @@ fn test_normalize_handler_name() {
403455
assert_eq!(normalize_handler_name("props::handleChange"), "handleChange");
404456
}
405457

406-
fn extract_callee_name_from_arrow_function<'a>(
458+
fn get_event_handler_name_from_arrow_function<'a>(
407459
arrow_function: &'a ArrowFunctionExpression<'a>,
408-
) -> Option<&'a str> {
460+
) -> Option<(CompactStr, Span, bool)> {
409461
if !arrow_function.expression {
410462
// Ignore arrow functions with block bodies like `() => { this.handleChange() }`.
411463
// The event handler name can only be extracted from arrow functions
@@ -418,9 +470,12 @@ fn extract_callee_name_from_arrow_function<'a>(
418470
let Expression::CallExpression(call_expr) = &stmt.expression else {
419471
return None;
420472
};
473+
421474
match &call_expr.callee {
422-
Expression::Identifier(ident) => Some(ident.name.as_str()),
423-
Expression::StaticMemberExpression(member_expr) => Some(member_expr.property.name.as_str()),
475+
Expression::Identifier(ident) => Some((ident.name.as_str().into(), ident.span, false)),
476+
Expression::StaticMemberExpression(member_expr) => {
477+
Some(get_event_handler_name_from_static_member_expression(member_expr))
478+
}
424479
_ => None,
425480
}
426481
}
@@ -432,6 +487,7 @@ fn test() {
432487
let pass = vec![
433488
("<TestComponent onChange={this.handleChange} />", None),
434489
("<TestComponent onChange={this.handle123Change} />", None),
490+
("<TestComponent onChange={this.props.handleChange} />", None),
435491
("<TestComponent onChange={this.props.onChange} />", None),
436492
(
437493
"
@@ -559,6 +615,8 @@ fn test() {
559615
serde_json::json!([{ "checkLocalVariables": true, "ignoreComponentNames": ["MyLib*"] }]),
560616
),
561617
),
618+
("<TestComponent onChange={true} />", None), // ok if not checking local variables (the same behavior as eslint version)
619+
("<TestComponent onChange={'value'} />", None), // ok if not checking local variables (the same behavior as eslint version)
562620
];
563621

564622
let fail = vec![
@@ -568,6 +626,9 @@ fn test() {
568626
("<TestComponent onChange={this.handle2} />", None),
569627
("<TestComponent onChange={this.handl3Change} />", None),
570628
("<TestComponent onChange={this.handle4change} />", None),
629+
("<TestComponent onChange={this.props.doSomethingOnChange} />", None),
630+
("<TestComponent onChange={this.props.obj.onChange} />", None),
631+
("<TestComponent onChange={props.obj.onChange} />", None),
571632
(
572633
"<TestComponent onChange={takeCareOfChange} />",
573634
Some(serde_json::json!([{ "checkLocalVariables": true }])),
@@ -632,6 +693,14 @@ fn test() {
632693
serde_json::json!([{ "checkLocalVariables": true, "ignoreComponentNames": ["MyLibrary*"] }]),
633694
),
634695
),
696+
(
697+
"<TestComponent onChange={true} />",
698+
Some(serde_json::json!([{ "checkLocalVariables": true }])),
699+
),
700+
(
701+
"<TestComponent onChange={'value'} />",
702+
Some(serde_json::json!([{ "checkLocalVariables": true }])),
703+
),
635704
];
636705

637706
Tester::new(JsxHandlerNames::NAME, JsxHandlerNames::PLUGIN, pass, fail).test_and_snapshot();

0 commit comments

Comments
 (0)