11use rustc_hash:: FxHashSet ;
22
3- use oxc_ast:: {
4- AstKind ,
5- ast:: { Function , IdentifierReference , JSXElementName , ThisExpression } ,
6- } ;
3+ use oxc_ast:: { AstKind , ast:: * } ;
74use oxc_ast_visit:: { Visit , walk} ;
85use oxc_diagnostics:: OxcDiagnostic ;
96use oxc_macros:: declare_oxc_lint;
10- use oxc_semantic:: { ReferenceId , ScopeFlags } ;
11- use oxc_span:: { GetSpan , Span } ;
7+ use oxc_semantic:: { ReferenceId , ScopeFlags , ScopeId , SymbolId } ;
8+ use oxc_span:: { Atom , GetSpan , Span } ;
129
1310use crate :: {
1411 AstNode ,
@@ -18,10 +15,41 @@ use crate::{
1815 utils:: is_react_hook,
1916} ;
2017
21- fn consistent_function_scoping ( span : Span ) -> OxcDiagnostic {
22- OxcDiagnostic :: warn ( "Function does not capture any variables from the outer scope." )
23- . with_help ( "Move this function to the outer scope." )
24- . with_label ( span)
18+ fn consistent_function_scoping (
19+ fn_span : Span ,
20+ parent_scope_span : Option < Span > ,
21+ parent_scope_kind : Option < & ' static str > ,
22+ function_name : Option < Atom < ' _ > > ,
23+ ) -> OxcDiagnostic {
24+ debug_assert ! ( Some ( fn_span) != parent_scope_span) ;
25+ let function_label = if let Some ( name) = function_name {
26+ format ! ( "Function `{name}` does not capture any variables from its parent scope" )
27+ } else {
28+ "This function does not use any variables from its parent scope" . into ( )
29+ } ;
30+
31+ let d = OxcDiagnostic :: warn ( function_label) . with_help ( match function_name {
32+ Some ( name) => {
33+ format ! ( "Move `{name}` to the outer scope to avoid recreating it on every call." )
34+ }
35+ None => {
36+ "Move this function to the outer scope to avoid recreating it on every call." . into ( )
37+ }
38+ } ) ;
39+
40+ match parent_scope_span {
41+ Some ( parent) => d. with_labels ( [
42+ parent. label ( "Outer scope where this function is defined" ) ,
43+ fn_span. primary_label ( if let Some ( parent_scope_kind) = parent_scope_kind {
44+ format ! (
45+ "This function does not use any variables from the parent {parent_scope_kind}"
46+ )
47+ } else {
48+ "This function does not use any variables from here" . into ( )
49+ } ) ,
50+ ] ) ,
51+ None => d. with_label ( fn_span) ,
52+ }
2553}
2654
2755#[ derive( Debug , Default , Clone ) ]
@@ -148,7 +176,7 @@ impl Rule for ConsistentFunctionScoping {
148176 }
149177
150178 fn run < ' a > ( & self , node : & AstNode < ' a > , ctx : & LintContext < ' a > ) {
151- let ( function_declaration_symbol_id, function_body, reporter_span) =
179+ let ( function_declaration_symbol_id, function_name , function_body, reporter_span) =
152180 match node. kind ( ) {
153181 AstKind :: Function ( function) => {
154182 if function. is_typescript_syntax ( ) {
@@ -176,14 +204,20 @@ impl Rule for ConsistentFunctionScoping {
176204 if let Some ( binding_ident) = get_function_like_declaration ( node, ctx) {
177205 (
178206 binding_ident. symbol_id ( ) ,
207+ Some ( binding_ident. name ) ,
179208 function_body,
180209 function. id . as_ref ( ) . map_or (
181210 Span :: sized ( function. span . start , 8 ) ,
182211 |func_binding_ident| func_binding_ident. span ,
183212 ) ,
184213 )
185214 } else if let Some ( function_id) = & function. id {
186- ( function_id. symbol_id ( ) , function_body, function_id. span ( ) )
215+ (
216+ function_id. symbol_id ( ) ,
217+ Some ( function_id. name ) ,
218+ function_body,
219+ function_id. span ( ) ,
220+ )
187221 } else {
188222 return ;
189223 }
@@ -193,7 +227,12 @@ impl Rule for ConsistentFunctionScoping {
193227 return ;
194228 } ;
195229
196- ( binding_ident. symbol_id ( ) , & arrow_function. body , binding_ident. span ( ) )
230+ (
231+ binding_ident. symbol_id ( ) ,
232+ Some ( binding_ident. name ) ,
233+ & arrow_function. body ,
234+ binding_ident. span ( ) ,
235+ )
197236 }
198237 _ => return ,
199238 } ;
@@ -249,7 +288,16 @@ impl Rule for ConsistentFunctionScoping {
249288 }
250289 }
251290
252- ctx. diagnostic ( consistent_function_scoping ( reporter_span) ) ;
291+ let ( maybe_parent_scope_span, maybe_parent_scope_type) =
292+ get_short_span_for_fn_scope ( ctx, function_declaration_symbol_id, scope)
293+ . map_or ( ( None , None ) , |( span, rtype) | ( Some ( span) , Some ( rtype) ) ) ;
294+
295+ ctx. diagnostic ( consistent_function_scoping (
296+ reporter_span,
297+ maybe_parent_scope_span,
298+ maybe_parent_scope_type,
299+ function_name,
300+ ) ) ;
253301 }
254302}
255303
@@ -311,6 +359,64 @@ fn is_in_react_hook<'a>(node: &AstNode<'a>, ctx: &LintContext<'a>) -> bool {
311359 false
312360}
313361
362+ fn get_short_span_for_fn_scope (
363+ ctx : & LintContext < ' _ > ,
364+ function_symbol_id : SymbolId ,
365+ scope_id : ScopeId ,
366+ ) -> Option < ( Span , & ' static str ) > {
367+ let scoping = ctx. scoping ( ) ;
368+
369+ debug_assert ! ( !scoping. scope_flags( scope_id) . contains( ScopeFlags :: Top ) ) ;
370+
371+ let scope_id =
372+ match ctx. nodes ( ) . parent_kind ( ctx. scoping ( ) . symbol_declaration ( function_symbol_id) ) {
373+ Some ( AstKind :: AssignmentExpression ( _) ) => {
374+ ctx. scoping ( ) . scope_parent_id ( scope_id) . unwrap_or ( scope_id)
375+ }
376+ _ => scope_id,
377+ } ;
378+
379+ let node_creating_parent_scope = ctx. nodes ( ) . get_node ( scoping. get_node_id ( scope_id) ) ;
380+
381+ match node_creating_parent_scope. kind ( ) {
382+ AstKind :: Function ( f) => f. id . as_ref ( ) . map ( |id| ( id. span ( ) , "function" ) ) ,
383+ AstKind :: ArrowFunctionExpression ( _) => {
384+ let parent = ctx. nodes ( ) . parent_kind ( node_creating_parent_scope. id ( ) ) ?;
385+ match parent {
386+ AstKind :: VariableDeclarator ( v) => Some ( ( v. id . span ( ) , "arrow function" ) ) ,
387+ AstKind :: AssignmentExpression ( a) => Some ( ( a. left . span ( ) , "arrow function" ) ) ,
388+ _ => None ,
389+ }
390+ }
391+ AstKind :: Class ( c) => c. id . as_ref ( ) . map ( |id| ( id. span ( ) , "class" ) ) ,
392+ // only cover keywords of control flow statements
393+ AstKind :: ForInStatement ( ForInStatement { span, .. } )
394+ | AstKind :: ForOfStatement ( ForOfStatement { span, .. } )
395+ | AstKind :: ForStatement ( ForStatement { span, .. } ) => {
396+ Some ( ( Span :: sized ( span. start , 3 ) , "for loop" ) )
397+ }
398+ AstKind :: TryStatement ( TryStatement { span, .. } ) => {
399+ Some ( ( Span :: sized ( span. start , 3 ) , "try statement" ) )
400+ }
401+ AstKind :: IfStatement ( IfStatement { span, .. } ) => {
402+ Some ( ( Span :: sized ( span. start , 2 ) , "if statement" ) )
403+ }
404+ AstKind :: DoWhileStatement ( DoWhileStatement { span, .. } ) => {
405+ Some ( ( Span :: sized ( span. start , 2 ) , "do while statement" ) )
406+ }
407+ AstKind :: SwitchStatement ( SwitchStatement { span, .. } ) => {
408+ Some ( ( Span :: sized ( span. start , 6 ) , "switch statement" ) )
409+ }
410+ AstKind :: WhileStatement ( WhileStatement { span, .. } ) => {
411+ Some ( ( Span :: sized ( span. start , 5 ) , "while statement" ) )
412+ }
413+ AstKind :: CatchClause ( CatchClause { span, .. } ) => {
414+ Some ( ( Span :: sized ( span. start , 5 ) , "catch block" ) )
415+ }
416+ _ => None ,
417+ }
418+ }
419+
314420#[ test]
315421fn test ( ) {
316422 use crate :: tester:: Tester ;
0 commit comments