Skip to main content

react_compiler_lowering/
find_context_identifiers.rs

1//! Rust equivalent of the TypeScript `FindContextIdentifiers` pass.
2//!
3//! Determines which bindings need StoreContext/LoadContext semantics by
4//! walking the AST with scope tracking to find variables that cross
5//! function boundaries.
6
7use std::collections::HashMap;
8use std::collections::HashSet;
9
10use react_compiler_ast::expressions::*;
11use react_compiler_ast::patterns::*;
12use react_compiler_ast::scope::*;
13use react_compiler_ast::statements::FunctionDeclaration;
14use react_compiler_ast::visitor::AstWalker;
15use react_compiler_ast::visitor::Visitor;
16use react_compiler_diagnostics::CompilerError;
17use react_compiler_diagnostics::CompilerErrorDetail;
18use react_compiler_diagnostics::ErrorCategory;
19use react_compiler_diagnostics::Position;
20use react_compiler_diagnostics::SourceLocation;
21use react_compiler_hir::environment::Environment;
22
23use crate::FunctionNode;
24
25#[derive(Default)]
26struct BindingInfo {
27    reassigned: bool,
28    reassigned_by_inner_fn: bool,
29    referenced_by_inner_fn: bool,
30}
31
32struct ContextIdentifierVisitor<'a> {
33    scope_info: &'a ScopeInfo,
34    env: &'a mut Environment,
35    /// Stack of inner function scopes encountered during traversal.
36    /// Empty when at the top level of the function being compiled.
37    function_stack: Vec<ScopeId>,
38    binding_info: HashMap<BindingId, BindingInfo>,
39    error: Option<CompilerError>,
40}
41
42impl<'a> ContextIdentifierVisitor<'a> {
43    fn push_function_scope(&mut self, _start: Option<u32>, node_id: Option<u32>) {
44        let scope = self.scope_info.resolve_scope_for_node(node_id);
45        if let Some(scope) = scope {
46            self.function_stack.push(scope);
47        }
48    }
49
50    fn pop_function_scope(&mut self, _start: Option<u32>, node_id: Option<u32>) {
51        let has_scope = self.scope_info.resolve_scope_for_node(node_id);
52        if has_scope.is_some() {
53            self.function_stack.pop();
54        }
55    }
56
57    fn check_captured_reference(&mut self, _start: Option<u32>, node_id: Option<u32>) {
58        let binding_id = match self.scope_info.resolve_reference_id_for_node(node_id) {
59            Some(id) => id,
60            None => return,
61        };
62        let &fn_scope = match self.function_stack.last() {
63            Some(s) => s,
64            None => return,
65        };
66        let binding = &self.scope_info.bindings[binding_id.0 as usize];
67        if is_captured_by_function(self.scope_info, binding.scope, fn_scope) {
68            let info = self.binding_info.entry(binding_id).or_default();
69            info.referenced_by_inner_fn = true;
70        }
71    }
72
73    fn handle_reassignment_identifier(&mut self, name: &str, current_scope: ScopeId) {
74        if let Some(binding_id) = self.scope_info.get_binding(current_scope, name) {
75            let info = self.binding_info.entry(binding_id).or_default();
76            info.reassigned = true;
77            if let Some(&fn_scope) = self.function_stack.last() {
78                let binding = &self.scope_info.bindings[binding_id.0 as usize];
79                if is_captured_by_function(self.scope_info, binding.scope, fn_scope) {
80                    info.reassigned_by_inner_fn = true;
81                }
82            }
83        }
84    }
85}
86
87impl<'ast> Visitor<'ast> for ContextIdentifierVisitor<'_> {
88    fn enter_function_declaration(&mut self, node: &'ast FunctionDeclaration, _: &[ScopeId]) {
89        self.push_function_scope(node.base.start, node.base.node_id);
90    }
91    fn leave_function_declaration(&mut self, node: &'ast FunctionDeclaration, _: &[ScopeId]) {
92        self.pop_function_scope(node.base.start, node.base.node_id);
93    }
94    fn enter_function_expression(&mut self, node: &'ast FunctionExpression, _: &[ScopeId]) {
95        self.push_function_scope(node.base.start, node.base.node_id);
96    }
97    fn leave_function_expression(&mut self, node: &'ast FunctionExpression, _: &[ScopeId]) {
98        self.pop_function_scope(node.base.start, node.base.node_id);
99    }
100    fn enter_arrow_function_expression(
101        &mut self,
102        node: &'ast ArrowFunctionExpression,
103        _: &[ScopeId],
104    ) {
105        self.push_function_scope(node.base.start, node.base.node_id);
106    }
107    fn leave_arrow_function_expression(
108        &mut self,
109        node: &'ast ArrowFunctionExpression,
110        _: &[ScopeId],
111    ) {
112        self.pop_function_scope(node.base.start, node.base.node_id);
113    }
114    fn enter_object_method(&mut self, node: &'ast ObjectMethod, _: &[ScopeId]) {
115        self.push_function_scope(node.base.start, node.base.node_id);
116    }
117    fn leave_object_method(&mut self, node: &'ast ObjectMethod, _: &[ScopeId]) {
118        self.pop_function_scope(node.base.start, node.base.node_id);
119    }
120
121    fn enter_identifier(&mut self, node: &'ast Identifier, _scope_stack: &[ScopeId]) {
122        self.check_captured_reference(node.base.start, node.base.node_id);
123    }
124
125    fn enter_jsx_identifier(
126        &mut self,
127        node: &'ast react_compiler_ast::jsx::JSXIdentifier,
128        _scope_stack: &[ScopeId],
129    ) {
130        self.check_captured_reference(node.base.start, node.base.node_id);
131    }
132
133    fn enter_assignment_expression(
134        &mut self,
135        node: &'ast AssignmentExpression,
136        scope_stack: &[ScopeId],
137    ) {
138        let current_scope = scope_stack
139            .last()
140            .copied()
141            .unwrap_or(self.scope_info.program_scope);
142        if self.error.is_none() {
143            if let Err(error) = walk_lval_for_reassignment(self, &node.left, current_scope) {
144                self.error = Some(error);
145            }
146        }
147    }
148
149    fn enter_update_expression(&mut self, node: &'ast UpdateExpression, scope_stack: &[ScopeId]) {
150        if let Expression::Identifier(ident) = node.argument.as_ref() {
151            let current_scope = scope_stack
152                .last()
153                .copied()
154                .unwrap_or(self.scope_info.program_scope);
155            self.handle_reassignment_identifier(&ident.name, current_scope);
156        }
157    }
158}
159
160/// Recursively walk an LVal pattern to find all reassignment target identifiers.
161fn walk_lval_for_reassignment(
162    visitor: &mut ContextIdentifierVisitor<'_>,
163    pattern: &PatternLike,
164    current_scope: ScopeId,
165) -> Result<(), CompilerError> {
166    match pattern {
167        PatternLike::Identifier(ident) => {
168            visitor.handle_reassignment_identifier(&ident.name, current_scope);
169        }
170        PatternLike::ArrayPattern(pat) => {
171            for element in &pat.elements {
172                if let Some(el) = element {
173                    walk_lval_for_reassignment(visitor, el, current_scope)?;
174                }
175            }
176        }
177        PatternLike::ObjectPattern(pat) => {
178            for prop in &pat.properties {
179                match prop {
180                    ObjectPatternProperty::ObjectProperty(p) => {
181                        walk_lval_for_reassignment(visitor, &p.value, current_scope)?;
182                    }
183                    ObjectPatternProperty::RestElement(p) => {
184                        walk_lval_for_reassignment(visitor, &p.argument, current_scope)?;
185                    }
186                }
187            }
188        }
189        PatternLike::AssignmentPattern(pat) => {
190            walk_lval_for_reassignment(visitor, &pat.left, current_scope)?;
191        }
192        PatternLike::RestElement(pat) => {
193            walk_lval_for_reassignment(visitor, &pat.argument, current_scope)?;
194        }
195        PatternLike::MemberExpression(_) => {
196            // Interior mutability - not a variable reassignment
197        }
198        PatternLike::TSAsExpression(node) => {
199            record_unsupported_lval(visitor.env, "TSAsExpression", convert_opt_loc(&node.base.loc))?;
200        }
201        PatternLike::TSSatisfiesExpression(node) => {
202            record_unsupported_lval(
203                visitor.env,
204                "TSSatisfiesExpression",
205                convert_opt_loc(&node.base.loc),
206            )?;
207        }
208        PatternLike::TSNonNullExpression(node) => {
209            record_unsupported_lval(
210                visitor.env,
211                "TSNonNullExpression",
212                convert_opt_loc(&node.base.loc),
213            )?;
214        }
215        PatternLike::TSTypeAssertion(node) => {
216            record_unsupported_lval(
217                visitor.env,
218                "TSTypeAssertion",
219                convert_opt_loc(&node.base.loc),
220            )?;
221        }
222        PatternLike::TypeCastExpression(node) => {
223            record_unsupported_lval(
224                visitor.env,
225                "TypeCastExpression",
226                convert_opt_loc(&node.base.loc),
227            )?;
228        }
229    }
230    Ok(())
231}
232
233fn convert_loc(loc: &react_compiler_ast::common::SourceLocation) -> SourceLocation {
234    SourceLocation {
235        start: Position {
236            line: loc.start.line,
237            column: loc.start.column,
238            index: loc.start.index,
239        },
240        end: Position {
241            line: loc.end.line,
242            column: loc.end.column,
243            index: loc.end.index,
244        },
245    }
246}
247
248fn convert_opt_loc(
249    loc: &Option<react_compiler_ast::common::SourceLocation>,
250) -> Option<SourceLocation> {
251    loc.as_ref().map(convert_loc)
252}
253
254/// Record the TS-faithful Todo for an unsupported assignment-target wrapper
255/// node, mirroring the TypeScript `FindContextIdentifiers` pass. TS throws
256/// immediately (CompilerError.throwTodo in handleAssignment's default case),
257/// aborting before BuildHIR ever runs or logs, so this must return Err rather
258/// than record-and-continue: otherwise Rust emits HIR debug entries for a
259/// function TS never lowered.
260fn record_unsupported_lval(
261    env: &mut Environment,
262    type_name: &str,
263    loc: Option<SourceLocation>,
264) -> Result<(), CompilerError> {
265    let _ = env;
266    let mut err = CompilerError::new();
267    err.push_error_detail(CompilerErrorDetail {
268        category: ErrorCategory::Todo,
269        reason: format!(
270            "[FindContextIdentifiers] Cannot handle Object destructuring assignment target {type_name}"
271        ),
272        description: None,
273        loc,
274        suggestions: None,
275    });
276    Err(err)
277}
278
279/// Check if a binding declared at `binding_scope` is captured by a function at `function_scope`.
280/// Returns true if the binding is declared above the function (in the parent scope or higher).
281fn is_captured_by_function(
282    scope_info: &ScopeInfo,
283    binding_scope: ScopeId,
284    function_scope: ScopeId,
285) -> bool {
286    let fn_parent = match scope_info.scopes[function_scope.0 as usize].parent {
287        Some(p) => p,
288        None => return false,
289    };
290    if binding_scope == fn_parent {
291        return true;
292    }
293    // Walk up from fn_parent to see if binding_scope is an ancestor
294    let mut current = scope_info.scopes[fn_parent.0 as usize].parent;
295    while let Some(scope_id) = current {
296        if scope_id == binding_scope {
297            return true;
298        }
299        current = scope_info.scopes[scope_id.0 as usize].parent;
300    }
301    false
302}
303
304/// Build a set of `(BindingId, position)` pairs that are declaration sites
305/// in `reference_to_binding`, not true references. Uses node-ID comparison
306/// when available (from `ref_node_id_to_binding` + `declaration_node_id`),
307/// falling back to position comparison otherwise.
308/// Build a set of (BindingId, node_id) pairs for declaration sites in
309/// ref_node_id_to_binding. These are entries where the reference's node_id
310/// matches the binding's declaration_node_id — i.e., the "reference" is
311/// actually the declaration itself.
312fn build_declaration_node_ids(scope_info: &ScopeInfo) -> HashSet<(BindingId, u32)> {
313    let mut result = HashSet::new();
314    for (&ref_nid, &binding_id) in &scope_info.ref_node_id_to_binding {
315        let binding = &scope_info.bindings[binding_id.0 as usize];
316        if binding.declaration_node_id == Some(ref_nid) {
317            result.insert((binding_id, ref_nid));
318        }
319    }
320    result
321}
322
323/// Find context identifiers for a function: variables that are captured across
324/// function boundaries and need StoreContext/LoadContext semantics.
325///
326/// A binding is a context identifier if:
327/// - It is reassigned from inside a nested function (`reassignedByInnerFn`), OR
328/// - It is reassigned AND referenced from inside a nested function
329///   (`reassigned && referencedByInnerFn`)
330///
331/// This is the Rust equivalent of the TypeScript `FindContextIdentifiers` pass.
332pub fn find_context_identifiers(
333    func: &FunctionNode<'_>,
334    scope_info: &ScopeInfo,
335    env: &mut Environment,
336    identifier_locs: &crate::identifier_loc_index::IdentifierLocIndex,
337) -> Result<HashSet<BindingId>, CompilerError> {
338    let func_scope = scope_info
339        .resolve_scope_for_node(func.node_id())
340        .unwrap_or(scope_info.program_scope);
341
342    let mut visitor = ContextIdentifierVisitor {
343        scope_info,
344        env,
345        function_stack: Vec::new(),
346        binding_info: HashMap::new(),
347        error: None,
348    };
349    let mut walker = AstWalker::with_initial_scope(scope_info, func_scope);
350
351    // Walk params and body (like Babel's func.traverse())
352    match func {
353        FunctionNode::FunctionDeclaration(d) => {
354            for param in &d.params {
355                walker.walk_pattern(&mut visitor, param);
356            }
357            walker.walk_block_statement(&mut visitor, &d.body);
358        }
359        FunctionNode::FunctionExpression(e) => {
360            for param in &e.params {
361                walker.walk_pattern(&mut visitor, param);
362            }
363            walker.walk_block_statement(&mut visitor, &e.body);
364        }
365        FunctionNode::ArrowFunctionExpression(a) => {
366            for param in &a.params {
367                walker.walk_pattern(&mut visitor, param);
368            }
369            match a.body.as_ref() {
370                ArrowFunctionBody::BlockStatement(block) => {
371                    walker.walk_block_statement(&mut visitor, block);
372                }
373                ArrowFunctionBody::Expression(expr) => {
374                    walker.walk_expression(&mut visitor, expr);
375                }
376            }
377        }
378    }
379
380    if let Some(error) = visitor.error {
381        return Err(error);
382    }
383
384    // Supplement the walker-based analysis with referenceToBinding data.
385    // The AST walker doesn't visit identifiers inside type annotations,
386    // but Babel's traverse (used by TS findContextIdentifiers) does.
387    // After scope extraction includes type annotation references,
388    // we check if any reassigned binding has references in nested function scopes
389    // via referenceToBinding — matching the TS behavior.
390    //
391    // We must skip declaration sites (e.g., the `x` in `function x() {}`),
392    // which are included in reference_to_binding but are not true references.
393    // Prefer node-ID comparison (immune to position-0 collisions from synthetic
394    // nodes), falling back to position when node-IDs are unavailable.
395    let declaration_node_ids = build_declaration_node_ids(scope_info);
396    for (&ref_nid, &binding_id) in &scope_info.ref_node_id_to_binding {
397        let info = match visitor.binding_info.get(&binding_id) {
398            Some(info) if info.reassigned && !info.referenced_by_inner_fn => info,
399            _ => continue,
400        };
401        let _ = info;
402        if declaration_node_ids.contains(&(binding_id, ref_nid)) {
403            continue;
404        }
405        // Get the reference's position from identifier_locs for range checks
406        let ref_pos = match identifier_locs.get(&ref_nid) {
407            Some(entry) => entry.start,
408            None => continue,
409        };
410        let binding = &scope_info.bindings[binding_id.0 as usize];
411        // Check if ref_pos is inside a nested function scope
412        for (&scope_start, &scope_id) in &scope_info.node_to_scope {
413            if scope_start <= ref_pos {
414                if let Some(&scope_end) = scope_info.node_to_scope_end.get(&scope_start) {
415                    if ref_pos < scope_end
416                        && matches!(
417                            scope_info.scopes[scope_id.0 as usize].kind,
418                            ScopeKind::Function
419                        )
420                        && is_captured_by_function(scope_info, binding.scope, scope_id)
421                    {
422                        visitor
423                            .binding_info
424                            .get_mut(&binding_id)
425                            .unwrap()
426                            .referenced_by_inner_fn = true;
427                        break;
428                    }
429                }
430            }
431        }
432    }
433
434    // Collect results
435    Ok(visitor
436        .binding_info
437        .into_iter()
438        .filter(|(_, info)| {
439            info.reassigned_by_inner_fn || (info.reassigned && info.referenced_by_inner_fn)
440        })
441        .map(|(id, _)| id)
442        .collect())
443}