@@ -5,6 +5,7 @@ use oxc_ast::{
55 ast:: {
66 ArrowFunctionExpression , Expression , JSXAttributeName , JSXAttributeValue , JSXElementName ,
77 JSXExpression , JSXMemberExpression , JSXMemberExpressionObject , Statement ,
8+ StaticMemberExpression ,
89 } ,
910} ;
1011use 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+
353403fn 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