diff --git a/crates/oxc_semantic/src/builder.rs b/crates/oxc_semantic/src/builder.rs index a0174ff1f7e42..e4f00a2c4d18f 100644 --- a/crates/oxc_semantic/src/builder.rs +++ b/crates/oxc_semantic/src/builder.rs @@ -425,6 +425,23 @@ impl<'a> SemanticBuilder<'a> { let symbol_id = self.scoping.get_binding(scope_id, name).or_else(|| { self.hoisting_variables.get(&scope_id).and_then(|symbols| symbols.get(name).copied()) })?; + + // `(function n(n) {})()` + // ^ is not a redeclaration + // Since we put the function expression binding 'n' Symbol into the function itself scope, + // then defining a variable with the same name as the function name will be considered + // a redeclaration, but it's actually not a redeclaration, so if the symbol declaration + // is a function expression, then return None to tell the caller that it's not a redeclaration. + if self.scoping.symbol_flags(symbol_id).is_function() + && self + .nodes + .kind(self.scoping.symbol_declaration(symbol_id)) + .as_function() + .is_some_and(Function::is_expression) + { + return None; + } + if report_error { self.check_and_report_redeclaration(name, span, symbol_id, excludes); } @@ -447,11 +464,7 @@ impl<'a> SemanticBuilder<'a> { None }; - if ( - flags.intersects(excludes) - // `function n() { let n; }` - // ^ is not a redeclaration - && !function_kind.is_some_and(Function::is_expression)) + if flags.intersects(excludes) // Needs to further check if the previous declaration is a function and the function // is not allowed to be redeclared. // For example: `async function goo(); var goo;` diff --git a/crates/oxc_semantic/tests/integration/symbols.rs b/crates/oxc_semantic/tests/integration/symbols.rs index 9a17c87b3cd3e..41c09cbeb5fbf 100644 --- a/crates/oxc_semantic/tests/integration/symbols.rs +++ b/crates/oxc_semantic/tests/integration/symbols.rs @@ -451,3 +451,12 @@ fn test_class_merging() { .contains_flags(SymbolFlags::Interface) .test(); } + +#[test] +fn test_redeclaration() { + SemanticTester::js("(function n(n) { console.log (n) }) ()") + .has_symbol("n") // the first `n` is the function expression + .equal_flags(SymbolFlags::Function) // param `n` is a SymbolFlags::FunctionScopedVariable + .has_number_of_references(0) // no references to the function + .test(); +} diff --git a/crates/oxc_semantic/tests/integration/util/symbol_tester.rs b/crates/oxc_semantic/tests/integration/util/symbol_tester.rs index efd88f92475ba..f3efe642b3748 100644 --- a/crates/oxc_semantic/tests/integration/util/symbol_tester.rs +++ b/crates/oxc_semantic/tests/integration/util/symbol_tester.rs @@ -100,8 +100,8 @@ impl<'a> SymbolTester<'a> { ) -> Self { let symbols_with_target_name: Option = semantic .scoping() - .iter_bindings() - .find_map(|(_, bindings)| bindings.get(target).copied()); + .symbol_ids() + .find(|&symbol_id| semantic.scoping().symbol_name(symbol_id) == target); let data = match symbols_with_target_name { Some(symbol_id) => Ok(symbol_id), @@ -153,6 +153,25 @@ impl<'a> SymbolTester<'a> { self } + /// Checks if the resolved symbol has exactly the flags in `flags`, using [`SymbolFlags::eq()`] + pub fn equal_flags(mut self, flags: SymbolFlags) -> Self { + self.test_result = match self.test_result { + Ok(symbol_id) => { + let found_flags = self.semantic.scoping().symbol_flags(symbol_id); + if found_flags == flags { + Ok(symbol_id) + } else { + Err(OxcDiagnostic::error(format!( + "Expected {} to equal flags {:?}, but it is {:?}", + self.target_symbol_name, flags, found_flags + ))) + } + } + err => err, + }; + self + } + /// Check that this symbol has a certain number of read [`Reference`]s /// /// References that are both read and write are counted.