@@ -224,39 +224,6 @@ declare_oxc_lint!(
224224 conditional_fix_suggestion
225225) ;
226226
227- impl FuncNames {
228- fn get_invalid_functions < ' a > (
229- & self ,
230- ctx : & ' a LintContext < ' _ > ,
231- ) -> Vec < ( & ' a Function < ' a > , & ' a AstNode < ' a > , & ' a AstNode < ' a > ) > {
232- let mut invalid_functions: Vec < ( & Function , & AstNode , & AstNode ) > = Vec :: new ( ) ;
233-
234- for node in ctx. nodes ( ) {
235- match node. kind ( ) {
236- // check function if it invalid, do not report it because maybe later the function is calling itself
237- AstKind :: Function ( func) => {
238- let parent_node = ctx. nodes ( ) . parent_node ( node. id ( ) ) ;
239- let config =
240- if func. generator { self . config . generators } else { self . config . functions } ;
241-
242- if is_invalid_function ( config, func, parent_node) {
243- invalid_functions. push ( ( func, node, parent_node) ) ;
244- }
245- }
246-
247- // check if the calling function is inside its own body
248- // then, remove it from invalid_functions because recursion are always named
249- AstKind :: CallExpression ( expression) => {
250- remove_recursive_functions ( & mut invalid_functions, expression, node, ctx) ;
251- }
252- _ => { }
253- }
254- }
255-
256- invalid_functions
257- }
258- }
259-
260227impl Rule for FuncNames {
261228 fn from_configuration ( value : serde_json:: Value ) -> Self {
262229 let Some ( functions_config) = value. get ( 0 ) else {
@@ -273,41 +240,48 @@ impl Rule for FuncNames {
273240 }
274241 }
275242
276- fn run_once ( & self , ctx : & LintContext < ' _ > ) {
277- for ( func, node, parent_node) in self . get_invalid_functions ( ctx) {
278- diagnostic_invalid_function ( func, node, parent_node, ctx) ;
243+ fn run < ' a > ( & self , node : & AstNode < ' a > , ctx : & LintContext < ' a > ) {
244+ if let AstKind :: Function ( func) = node. kind ( ) {
245+ let parent_node = ctx. nodes ( ) . parent_node ( node. id ( ) ) ;
246+ let config =
247+ if func. generator { self . config . generators } else { self . config . functions } ;
248+
249+ if is_invalid_function ( config, func, parent_node) {
250+ // For named functions, check if they're recursive (need their name for recursion)
251+ if let Some ( func_name) = func. name ( ) {
252+ if is_recursive_function ( func, func_name. as_str ( ) , ctx) {
253+ return ;
254+ }
255+ }
256+ diagnostic_invalid_function ( func, node, parent_node, ctx) ;
257+ }
279258 }
280259 }
281260}
282261
283- fn remove_recursive_functions (
284- invalid_functions : & mut Vec < ( & Function , & AstNode , & AstNode ) > ,
285- expression : & oxc_ast:: ast:: CallExpression ,
286- node : & AstNode ,
287- ctx : & LintContext ,
288- ) {
289- let Expression :: Identifier ( identifier) = & expression. callee else {
290- return ;
262+ fn is_recursive_function ( func : & Function , func_name : & str , ctx : & LintContext ) -> bool {
263+ let Some ( func_scope_id) = func. scope_id . get ( ) else {
264+ return false ;
291265 } ;
292- // check at first if the callee calls an invalid function
293- if !invalid_functions
294- . iter ( )
295- . filter_map ( |( func, _, _) | func. name ( ) )
296- . any ( |func_name| func_name == identifier. name )
297- {
298- return ;
299- }
300266
301- // a function which is calling itself inside is always valid
302- if let Some ( span) = ctx. nodes ( ) . ancestors ( node. id ( ) ) . find_map ( |p| {
303- if let AstKind :: Function ( func) = p. kind ( ) {
304- func. name ( ) . filter ( |n| * n == identifier. name ) . map ( |_| func. span )
305- } else {
306- None
307- }
308- } ) {
309- invalid_functions. retain ( |( func, _, _) | func. span != span) ;
267+ if let Some ( binding) = ctx. scoping ( ) . find_binding ( func_scope_id, func_name) {
268+ return ctx. semantic ( ) . symbol_references ( binding) . any ( |reference| {
269+ let parent = ctx. nodes ( ) . parent_node ( reference. node_id ( ) ) ;
270+ if matches ! ( parent. kind( ) , AstKind :: CallExpression ( _) ) {
271+ ctx. nodes ( ) . ancestors ( reference. node_id ( ) ) . any ( |ancestor| {
272+ if let AstKind :: Function ( f) = ancestor. kind ( ) {
273+ f. scope_id . get ( ) == Some ( func_scope_id)
274+ } else {
275+ false
276+ }
277+ } )
278+ } else {
279+ false
280+ }
281+ } ) ;
310282 }
283+
284+ false
311285}
312286
313287const INVALID_IDENTIFIER_NAMES : [ & str ; 9 ] =
@@ -526,6 +500,30 @@ fn test() {
526500 ( "function foo() {}" , never. clone( ) ) ,
527501 ( "var a = function() {};" , never. clone( ) ) ,
528502 ( "var a = function foo() { foo(); };" , never. clone( ) ) ,
503+ (
504+ "var factorial = function fact(n) { return n <= 1 ? 1 : n * fact(n - 1); };" ,
505+ never. clone( ) ,
506+ ) ,
507+ (
508+ "const fibonacci = function fib(n) { if (n <= 1) return n; return fib(n - 1) + fib(n - 2); };" ,
509+ never. clone( ) ,
510+ ) ,
511+ // Multiple references, but only one is a call - still recursive
512+ ( "var a = function foo() { var x = foo; foo(); };" , never. clone( ) ) ,
513+ // Direct recursive call in setTimeout - this is actually recursive
514+ ( "setTimeout(function ticker() { ticker(); }, 1000);" , never. clone( ) ) ,
515+ // Mutual recursion doesn't count as self-recursion (would need different handling)
516+ ( "var a = function foo() { function bar() { foo(); } bar(); };" , never. clone( ) ) ,
517+ // Critical test: function with multiple references where the recursive call is not the first reference
518+ // This tests the fix for the control flow bug where early returns could miss later recursive calls
519+ (
520+ "var x = function foo() { var ref1 = foo.name; var ref2 = foo.length; foo(); };" ,
521+ never. clone( ) ,
522+ ) ,
523+ (
524+ "var y = function bar() { if (false) { bar.toString(); } if (true) { bar(); } };" ,
525+ never. clone( ) ,
526+ ) ,
529527 ( "var foo = {bar: function() {}};" , never. clone( ) ) ,
530528 ( "$('#foo').click(function() {});" , never. clone( ) ) ,
531529 ( "Foo.prototype.bar = function() {};" , never. clone( ) ) ,
@@ -597,6 +595,11 @@ fn test() {
597595 ( "var { a: [b] = function(){} } = foo;" , as_needed. clone( ) ) , // { "ecmaVersion": 6 },
598596 ( "function foo({ a } = function(){}) {};" , as_needed. clone( ) ) , // { "ecmaVersion": 6 },
599597 ( "var x = function foo() {};" , never. clone( ) ) ,
598+ ( "var x = function foo() { return foo.length; };" , never. clone( ) ) ,
599+ ( "var foo = 1; var x = function foo() { return foo + 1; };" , never. clone( ) ) ,
600+ ( "var x = function foo() { console.log('hello'); };" , never. clone( ) ) ,
601+ ( "var outer = function inner() { function nested() { nested(); } };" , never. clone( ) ) ,
602+ ( "setTimeout(function ticker() { setTimeout(ticker, 1000); }, 1000);" , never. clone( ) ) ,
600603 ( "Foo.prototype.bar = function foo() {};" , never. clone( ) ) ,
601604 ( "({foo: function foo() {}})" , never. clone( ) ) ,
602605 ( "export default function() {}" , always. clone( ) ) , // { "sourceType": "module", "ecmaVersion": 6 },
0 commit comments