Skip to content

Commit 8e4cd8f

Browse files
committed
refactor(linter/func-names): use run_once over looping over all nodes (#13798)
- Remove run_once method in favor of run for per-node processing - Eliminate ctx.nodes() iteration loop for better performance - Simplify recursive function detection using scope IDs - Maintain all existing functionality and test compatibility
1 parent 42eb61c commit 8e4cd8f

File tree

3 files changed

+101
-62
lines changed

3 files changed

+101
-62
lines changed

crates/oxc_linter/src/generated/rule_runner_impls.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ impl RuleRunner for crate::rules::eslint::for_direction::ForDirection {
5555
}
5656

5757
impl RuleRunner for crate::rules::eslint::func_names::FuncNames {
58-
const NODE_TYPES: Option<&AstTypesBitset> = None;
58+
const NODE_TYPES: Option<&AstTypesBitset> =
59+
Some(&AstTypesBitset::from_types(&[AstType::Function]));
5960
}
6061

6162
impl RuleRunner for crate::rules::eslint::func_style::FuncStyle {

crates/oxc_linter/src/rules/eslint/func_names.rs

Lines changed: 64 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -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-
260227
impl 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

313287
const 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 },

crates/oxc_linter/src/snapshots/eslint_func_names.snap

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,41 @@ source: crates/oxc_linter/src/tester.rs
120120
╰────
121121
help: Remove the name on this function expression.
122122

123+
⚠ eslint(func-names): Unexpected named function `foo`.
124+
╭─[func_names.tsx:1:9]
125+
1 │ var x = function foo() { return foo.length; };
126+
· ────────────
127+
╰────
128+
help: Remove the name on this function expression.
129+
130+
⚠ eslint(func-names): Unexpected named function `foo`.
131+
╭─[func_names.tsx:1:22]
132+
1 │ var foo = 1; var x = function foo() { return foo + 1; };
133+
· ────────────
134+
╰────
135+
help: Remove the name on this function expression.
136+
137+
⚠ eslint(func-names): Unexpected named function `foo`.
138+
╭─[func_names.tsx:1:9]
139+
1 │ var x = function foo() { console.log('hello'); };
140+
· ────────────
141+
╰────
142+
help: Remove the name on this function expression.
143+
144+
⚠ eslint(func-names): Unexpected named function `inner`.
145+
╭─[func_names.tsx:1:13]
146+
1 │ var outer = function inner() { function nested() { nested(); } };
147+
· ──────────────
148+
╰────
149+
help: Remove the name on this function expression.
150+
151+
⚠ eslint(func-names): Unexpected named function `ticker`.
152+
╭─[func_names.tsx:1:12]
153+
1 │ setTimeout(function ticker() { setTimeout(ticker, 1000); }, 1000);
154+
· ───────────────
155+
╰────
156+
help: Remove the name on this function expression.
157+
123158
⚠ eslint(func-names): Unexpected named function `foo`.
124159
╭─[func_names.tsx:1:21]
125160
1 │ Foo.prototype.bar = function foo() {};

0 commit comments

Comments
 (0)