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

Findings

✓ No findings reported for this file.

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.