diff --git a/crates/oxc_ast/src/ast/js.rs b/crates/oxc_ast/src/ast/js.rs index 5811da2d9c72b1..1477f65edd182e 100644 --- a/crates/oxc_ast/src/ast/js.rs +++ b/crates/oxc_ast/src/ast/js.rs @@ -1826,6 +1826,8 @@ pub struct ArrowFunctionExpression<'a> { #[serde(flatten)] pub span: Span, /// Is the function body an arrow expression? i.e. `() => expr` instead of `() => {}` + /// + /// When `true`, this function implicitly returns the expression. pub expression: bool, pub r#async: bool, pub type_parameters: Option>>, diff --git a/crates/oxc_linter/src/rules/eslint/no_unused_vars/allowed.rs b/crates/oxc_linter/src/rules/eslint/no_unused_vars/allowed.rs index 92d84b767871d7..5e4845b17e979a 100644 --- a/crates/oxc_linter/src/rules/eslint/no_unused_vars/allowed.rs +++ b/crates/oxc_linter/src/rules/eslint/no_unused_vars/allowed.rs @@ -57,7 +57,7 @@ impl<'s, 'a> Symbol<'s, 'a> { let Some(AstKind::FunctionBody(body)) = self.nodes().parent_kind(parent.id()) else { return false; }; - return body.span.contains_inclusive(self.span()) && body.statements.len() == 1 && !self.get_snippet(body.span).starts_with('{') + return self.is_implicit_return(body); } _ => { parent.kind().debug_name(); @@ -103,6 +103,10 @@ impl<'s, 'a> Symbol<'s, 'a> { .map(|node_id| nodes.get_node(node_id)) .any(|node| matches!(node.kind(), AstKind::TSModuleDeclaration(namespace) if is_ambient_namespace(namespace))) } + + pub fn is_implicit_return(&self, body: &FunctionBody) -> bool { + body.statements.len() == 1 && !self.get_snippet(body.span).starts_with('{') + } } #[inline] diff --git a/crates/oxc_linter/src/rules/eslint/no_unused_vars/tests/oxc.rs b/crates/oxc_linter/src/rules/eslint/no_unused_vars/tests/oxc.rs index b15a3b2a715134..a1bb181d335903 100644 --- a/crates/oxc_linter/src/rules/eslint/no_unused_vars/tests/oxc.rs +++ b/crates/oxc_linter/src/rules/eslint/no_unused_vars/tests/oxc.rs @@ -227,6 +227,7 @@ fn test_vars_reassignment() { ", "let i = 0; i > 0 ? 'positive' : 'negative';", "let i = 0; i > 0 && console.log('positive');", + "export function counter() { let i = 0; return () => ++i }", ]; let fail = vec![ diff --git a/crates/oxc_semantic/src/builder.rs b/crates/oxc_semantic/src/builder.rs index 5c68400be0e198..ae87242b47a0d7 100644 --- a/crates/oxc_semantic/src/builder.rs +++ b/crates/oxc_semantic/src/builder.rs @@ -82,6 +82,7 @@ pub struct SemanticBuilder<'a> { // and when we reach a value declaration we set it // to value like pub(crate) namespace_stack: Vec, + pub(crate) is_in_return: bool, current_reference_flags: ReferenceFlags, pub(crate) hoisting_variables: FxHashMap, SymbolId>>, @@ -134,6 +135,7 @@ impl<'a> SemanticBuilder<'a> { current_scope_id, function_stack: vec![], namespace_stack: vec![], + is_in_return: false, nodes: AstNodes::default(), hoisting_variables: FxHashMap::default(), scope, @@ -1229,7 +1231,10 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { /* cfg */ let ret_kind = if let Some(arg) = &stmt.argument { + let in_return = self.is_in_return; + self.is_in_return = true; self.visit_expression(arg); + self.is_in_return = in_return; ReturnInstructionKind::NotImplicitUndefined } else { ReturnInstructionKind::ImplicitUndefined @@ -1624,7 +1629,10 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { self.visit_ts_type_annotation(return_type); } if let Some(body) = &func.body { + let in_return = self.is_in_return; + self.is_in_return = false; self.visit_function_body(body); + self.is_in_return = in_return; } /* cfg */ @@ -1677,7 +1685,10 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { self.visit_ts_type_annotation(return_type); } + let in_return = self.is_in_return; + self.is_in_return = expr.expression; self.visit_function_body(&expr.body); + self.is_in_return = in_return; /* cfg */ control_flow!(self, |cfg| { @@ -1990,6 +2001,8 @@ impl<'a> SemanticBuilder<'a> { fn resolve_reference_usages(&self) -> ReferenceFlags { if self.current_reference_flags.is_empty() { ReferenceFlags::Read + } else if self.is_in_return { + self.current_reference_flags | ReferenceFlags::Read } else { self.current_reference_flags } diff --git a/crates/oxc_semantic/tests/integration/symbols.rs b/crates/oxc_semantic/tests/integration/symbols.rs index 64c6382238f1fd..2a498b5f0b354c 100644 --- a/crates/oxc_semantic/tests/integration/symbols.rs +++ b/crates/oxc_semantic/tests/integration/symbols.rs @@ -355,3 +355,51 @@ fn test_ts_interface_heritage() { .has_number_of_references(1) .test(); } + +#[test] +fn test_arrow_implicit_return() { + SemanticTester::js("let i = 0; const x = () => i") + .has_root_symbol("i") + .has_number_of_reads(1) + .has_number_of_writes(0) + .test(); + + SemanticTester::js("let i = 0; const x = () => ++i") + .has_root_symbol("i") + .has_number_of_reads(1) + .has_number_of_writes(1) + .test(); + + SemanticTester::js("let i = 0; const x = () => { ++i }") + .has_root_symbol("i") + .has_number_of_reads(0) + .has_number_of_writes(1) + .test(); + + SemanticTester::js("let i = 0; const x = () => (0, ++i)") + .has_root_symbol("i") + .has_number_of_reads(1) + .has_number_of_writes(1) + .test(); + + SemanticTester::js("let i = 0; const x = () => (++i, 0)") + .has_root_symbol("i") + .has_number_of_reads(1) + .has_number_of_writes(1) + .test(); +} + +#[test] +fn test_arrow_explicit_return() { + SemanticTester::js("let i = 0; const x = () => { return i }") + .has_root_symbol("i") + .has_number_of_reads(1) + .has_number_of_writes(0) + .test(); + + SemanticTester::js("let i = 0; const x = () => { return ++i }") + .has_root_symbol("i") + .has_number_of_reads(1) + .has_number_of_writes(1) + .test(); +} diff --git a/tasks/coverage/semantic_babel.snap b/tasks/coverage/semantic_babel.snap index 9677c740da0764..c964d4db5f9b90 100644 --- a/tasks/coverage/semantic_babel.snap +++ b/tasks/coverage/semantic_babel.snap @@ -154,14 +154,14 @@ after transform: ["t"] rebuilt : [] tasks/coverage/babel/packages/babel-parser/test/fixtures/typescript/arrow-function/arrow-like-in-conditional-3/input.ts -semantic error: Unresolved references mismatch: -after transform: ["v"] -rebuilt : [] +semantic error: Symbol reference IDs mismatch: +after transform: SymbolId(0): [ReferenceId(0), ReferenceId(1)] +rebuilt : SymbolId(0): [ReferenceId(0)] tasks/coverage/babel/packages/babel-parser/test/fixtures/typescript/arrow-function/arrow-like-in-conditional-4/input.ts -semantic error: Unresolved references mismatch: -after transform: ["v"] -rebuilt : [] +semantic error: Symbol reference IDs mismatch: +after transform: SymbolId(0): [ReferenceId(0)] +rebuilt : SymbolId(0): [] tasks/coverage/babel/packages/babel-parser/test/fixtures/typescript/arrow-function/async-await-null/input.ts semantic error: Bindings mismatch: diff --git a/tasks/coverage/semantic_typescript.snap b/tasks/coverage/semantic_typescript.snap index 1fe5f7b95ac195..b3d200a939d532 100644 --- a/tasks/coverage/semantic_typescript.snap +++ b/tasks/coverage/semantic_typescript.snap @@ -10884,9 +10884,9 @@ after transform: ScopeId(1): ["T", "i", "o"] rebuilt : ScopeId(1): ["i", "o"] tasks/coverage/typescript/tests/cases/compiler/inferenceLimit.ts -semantic error: Bindings mismatch: -after transform: ScopeId(0): ["BrokenClass", "MyModule"] -rebuilt : ScopeId(0): ["BrokenClass"] +semantic error: Symbol reference IDs mismatch: +after transform: SymbolId(0): [ReferenceId(2), ReferenceId(13), ReferenceId(17)] +rebuilt : SymbolId(0): [] Unresolved references mismatch: after transform: ["Array", "Promise"] rebuilt : ["Promise"] diff --git a/tasks/coverage/transformer_typescript.snap b/tasks/coverage/transformer_typescript.snap index 0a58c5eb56dd0a..094e0f6b4cf0a3 100644 --- a/tasks/coverage/transformer_typescript.snap +++ b/tasks/coverage/transformer_typescript.snap @@ -2,5 +2,6 @@ commit: d8086f14 transformer_typescript Summary: AST Parsed : 6456/6456 (100.00%) -Positive Passed: 6455/6456 (99.98%) +Positive Passed: 6454/6456 (99.97%) +Mismatch: tasks/coverage/typescript/tests/cases/compiler/inferenceLimit.ts Mismatch: tasks/coverage/typescript/tests/cases/conformance/jsx/inline/inlineJsxAndJsxFragPragmaOverridesCompilerOptions.tsx