main
rs 446 lines 16.4 KB
Raw
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
7 use rustc_hash::{FxHashMap, FxHashSet};
8
9 use react_compiler_ast::expressions::*;
10 use react_compiler_ast::patterns::*;
11 use react_compiler_ast::scope::*;
12 use react_compiler_ast::statements::FunctionDeclaration;
13 use react_compiler_ast::visitor::AstWalker;
14 use react_compiler_ast::visitor::Visitor;
15 use react_compiler_diagnostics::CompilerError;
16 use react_compiler_diagnostics::CompilerErrorDetail;
17 use react_compiler_diagnostics::ErrorCategory;
18 use react_compiler_diagnostics::Position;
19 use react_compiler_diagnostics::SourceLocation;
20 use react_compiler_hir::environment::Environment;
21
22 use crate::FunctionNode;
23
24 #[derive(Default)]
25 struct BindingInfo {
26 reassigned: bool,
27 reassigned_by_inner_fn: bool,
28 referenced_by_inner_fn: bool,
29 }
30
31 struct 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 }
40
41 impl<'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 }
48
49 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 }
55
56 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 }
71
72 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 }
85
86 impl<'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 }
119
120 fn enter_identifier(&mut self, node: &'ast Identifier, _scope_stack: &[ScopeId]) {
121 self.check_captured_reference(node.base.start, node.base.node_id);
122 }
123
124 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 }
131
132 fn enter_assignment_expression(
133 &mut self,
134 node: &'ast AssignmentExpression,
135 scope_stack: &[ScopeId],
136 ) {
137 let current_scope = scope_stack
138 .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 }
147
148 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_stack
151 .last()
152 .copied()
153 .unwrap_or(self.scope_info.program_scope);
154 self.handle_reassignment_identifier(&ident.name, current_scope);
155 }
156 }
157 }
158
159 /// Recursively walk an LVal pattern to find all reassignment target identifiers.
160 fn 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 reassignment
196 }
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 }
235
236 fn 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 }
250
251 fn convert_opt_loc(
252 loc: &Option<react_compiler_ast::common::SourceLocation>,
253 ) -> Option<SourceLocation> {
254 loc.as_ref().map(convert_loc)
255 }
256
257 /// Record the TS-faithful Todo for an unsupported assignment-target wrapper
258 /// node, mirroring the TypeScript `FindContextIdentifiers` pass. TS throws
259 /// immediately (CompilerError.throwTodo in handleAssignment's default case),
260 /// aborting before BuildHIR ever runs or logs, so this must return Err rather
261 /// than record-and-continue: otherwise Rust emits HIR debug entries for a
262 /// function TS never lowered.
263 fn 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 }
281
282 /// 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).
284 fn 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 ancestor
297 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 false
305 }
306
307 /// Build a set of `(BindingId, position)` pairs that are declaration sites
308 /// in `reference_to_binding`, not true references. Uses node-ID comparison
309 /// 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 in
312 /// ref_node_id_to_binding. These are entries where the reference's node_id
313 /// matches the binding's declaration_node_id — i.e., the "reference" is
314 /// actually the declaration itself.
315 fn 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 result
324 }
325
326 /// Find context identifiers for a function: variables that are captured across
327 /// 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`), OR
331 /// - It is reassigned AND referenced from inside a nested function
332 /// (`reassigned && referencedByInnerFn`)
333 ///
334 /// This is the Rust equivalent of the TypeScript `FindContextIdentifiers` pass.
335 pub 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_info
342 .resolve_scope_for_node(func.node_id())
343 .unwrap_or(scope_info.program_scope);
344
345 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);
353
354 // 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 }
382
383 if let Some(error) = visitor.error {
384 return Err(error);
385 }
386
387 // 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 scopes
392 // 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 synthetic
397 // 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 checks
409 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 scope
415 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_end
419 && matches!(
420 scope_info.scopes[scope_id.0 as usize].kind,
421 ScopeKind::Function
422 )
423 && is_captured_by_function(scope_info, binding.scope, scope_id)
424 {
425 visitor
426 .binding_info
427 .get_mut(&binding_id)
428 .unwrap()
429 .referenced_by_inner_fn = true;
430 break;
431 }
432 }
433 }
434 }
435 }
436
437 // Collect results
438 Ok(visitor
439 .binding_info
440 .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 }