@@ -98,6 +98,47 @@ impl<'a> ScopeMaps<'a> {
9898 }
9999}
100100
101+ /// Build scope maps from listener handler_ops.
102+ ///
103+ /// Per Angular's resolve_names.ts (line 86), handler ops are processed as a
104+ /// completely separate lexical scope via recursive `processLexicalScope(unit, op.handlerOps, savedView)`.
105+ /// The generateVariables phase already prepends all necessary variables to handler_ops,
106+ /// so no merging from the parent view's scope is needed. This matches Angular's
107+ /// behavior where each `processLexicalScope` call starts with fresh scope/localDefinitions maps.
108+ fn build_scope_from_handler_ops < ' a , ' b > (
109+ ops : impl Iterator < Item = & ' b UpdateOp < ' a > > ,
110+ ) -> ScopeMaps < ' a >
111+ where
112+ ' a : ' b ,
113+ {
114+ let mut maps = ScopeMaps :: default ( ) ;
115+ for op in ops {
116+ if let UpdateOp :: Variable ( var_op) = op {
117+ match var_op. kind {
118+ SemanticVariableKind :: Identifier => {
119+ if var_op. local {
120+ if !maps. local_definitions . contains_key ( & var_op. name ) {
121+ maps. local_definitions . insert ( var_op. name . clone ( ) , var_op. xref ) ;
122+ }
123+ } else if !maps. scope . contains_key ( & var_op. name ) {
124+ maps. scope . insert ( var_op. name . clone ( ) , var_op. xref ) ;
125+ }
126+ if !maps. scope . contains_key ( & var_op. name ) {
127+ maps. scope . insert ( var_op. name . clone ( ) , var_op. xref ) ;
128+ }
129+ }
130+ SemanticVariableKind :: Alias => {
131+ if !maps. scope . contains_key ( & var_op. name ) {
132+ maps. scope . insert ( var_op. name . clone ( ) , var_op. xref ) ;
133+ }
134+ }
135+ _ => { }
136+ }
137+ }
138+ }
139+ maps
140+ }
141+
101142/// Build scope maps from update operations (for variables like context reads).
102143fn build_scope_from_update_ops < ' a > ( ops : & crate :: ir:: list:: UpdateOpList < ' a > ) -> ScopeMaps < ' a > {
103144 let mut maps = ScopeMaps :: default ( ) ;
@@ -193,58 +234,13 @@ fn process_lexical_scope_create<'a>(
193234 for op in ops. iter_mut ( ) {
194235 match op {
195236 CreateOp :: Listener ( listener) => {
196- // Build handler-specific scope that includes Variables from handler_ops.
197- // This is critical for @for loops where listeners need access to loop variables
198- // that were prepended to handler_ops by generate_variables phase.
199- //
200- // We use "first wins" semantics: the first Variable with a given name takes
201- // precedence. This matches Angular TypeScript compiler behavior in
202- // resolve_names.ts (lines 55-58).
203- //
204- // handler_ops Variables override update_scope Variables, but among handler_ops
205- // Variables themselves, the first one wins. This is achieved by:
206- // 1. First building a scope from handler_ops only (first wins)
207- // 2. Then merging in update_scope for names not in handler_ops
208- let mut handler_scope = ScopeMaps :: default ( ) ;
209- // First pass: add handler_ops Variables (first wins)
210- for handler_op in listener. handler_ops . iter ( ) {
211- if let UpdateOp :: Variable ( var_op) = handler_op {
212- match var_op. kind {
213- SemanticVariableKind :: Identifier => {
214- // Handler-local variables take precedence
215- if var_op. local
216- && !handler_scope. local_definitions . contains_key ( & var_op. name )
217- {
218- handler_scope
219- . local_definitions
220- . insert ( var_op. name . clone ( ) , var_op. xref ) ;
221- }
222- // Only add if not already present (first wins)
223- if !handler_scope. scope . contains_key ( & var_op. name ) {
224- handler_scope. scope . insert ( var_op. name . clone ( ) , var_op. xref ) ;
225- }
226- }
227- SemanticVariableKind :: Alias => {
228- // Only add if not already present (first wins)
229- if !handler_scope. scope . contains_key ( & var_op. name ) {
230- handler_scope. scope . insert ( var_op. name . clone ( ) , var_op. xref ) ;
231- }
232- }
233- _ => { }
234- }
235- }
236- }
237- // Second pass: merge in update_scope for names not in handler_ops
238- for ( name, xref) in & scope. scope {
239- if !handler_scope. scope . contains_key ( name) {
240- handler_scope. scope . insert ( name. clone ( ) , * xref) ;
241- }
242- }
243- for ( name, xref) in & scope. local_definitions {
244- if !handler_scope. local_definitions . contains_key ( name) {
245- handler_scope. local_definitions . insert ( name. clone ( ) , * xref) ;
246- }
247- }
237+ // Per Angular's resolve_names.ts (line 86):
238+ // processLexicalScope(unit, op.handlerOps, savedView);
239+ // Handler ops are processed as a SEPARATE lexical scope — a fresh
240+ // scope/localDefinitions is built from handler_ops variables only,
241+ // with NO merging from the parent view's scope. The generateVariables
242+ // phase already prepends all necessary variables to handler_ops.
243+ let handler_scope = build_scope_from_handler_ops ( listener. handler_ops . iter ( ) ) ;
248244
249245 // Process listener handler_ops with the handler scope
250246 for handler_op in listener. handler_ops . iter_mut ( ) {
@@ -276,43 +272,8 @@ fn process_lexical_scope_create<'a>(
276272 }
277273 }
278274 CreateOp :: TwoWayListener ( listener) => {
279- // Build handler-specific scope for TwoWayListener
280- // Uses same "first wins" semantics as Listener (see above)
281- let mut handler_scope = ScopeMaps :: default ( ) ;
282- for handler_op in listener. handler_ops . iter ( ) {
283- if let UpdateOp :: Variable ( var_op) = handler_op {
284- match var_op. kind {
285- SemanticVariableKind :: Identifier => {
286- if var_op. local
287- && !handler_scope. local_definitions . contains_key ( & var_op. name )
288- {
289- handler_scope
290- . local_definitions
291- . insert ( var_op. name . clone ( ) , var_op. xref ) ;
292- }
293- if !handler_scope. scope . contains_key ( & var_op. name ) {
294- handler_scope. scope . insert ( var_op. name . clone ( ) , var_op. xref ) ;
295- }
296- }
297- SemanticVariableKind :: Alias => {
298- if !handler_scope. scope . contains_key ( & var_op. name ) {
299- handler_scope. scope . insert ( var_op. name . clone ( ) , var_op. xref ) ;
300- }
301- }
302- _ => { }
303- }
304- }
305- }
306- for ( name, xref) in & scope. scope {
307- if !handler_scope. scope . contains_key ( name) {
308- handler_scope. scope . insert ( name. clone ( ) , * xref) ;
309- }
310- }
311- for ( name, xref) in & scope. local_definitions {
312- if !handler_scope. local_definitions . contains_key ( name) {
313- handler_scope. local_definitions . insert ( name. clone ( ) , * xref) ;
314- }
315- }
275+ // Per Angular's resolve_names.ts: handler ops get their own scope
276+ let handler_scope = build_scope_from_handler_ops ( listener. handler_ops . iter ( ) ) ;
316277
317278 for handler_op in listener. handler_ops . iter_mut ( ) {
318279 transform_expressions_in_update_op (
@@ -330,46 +291,10 @@ fn process_lexical_scope_create<'a>(
330291 VisitorContextFlag :: NONE ,
331292 ) ;
332293 }
333- // Note: TwoWayListenerOp has no handler_expression field
334294 }
335295 CreateOp :: AnimationListener ( listener) => {
336- // Build handler-specific scope for AnimationListener
337- // Uses same "first wins" semantics as Listener (see above)
338- let mut handler_scope = ScopeMaps :: default ( ) ;
339- for handler_op in listener. handler_ops . iter ( ) {
340- if let UpdateOp :: Variable ( var_op) = handler_op {
341- match var_op. kind {
342- SemanticVariableKind :: Identifier => {
343- if var_op. local
344- && !handler_scope. local_definitions . contains_key ( & var_op. name )
345- {
346- handler_scope
347- . local_definitions
348- . insert ( var_op. name . clone ( ) , var_op. xref ) ;
349- }
350- if !handler_scope. scope . contains_key ( & var_op. name ) {
351- handler_scope. scope . insert ( var_op. name . clone ( ) , var_op. xref ) ;
352- }
353- }
354- SemanticVariableKind :: Alias => {
355- if !handler_scope. scope . contains_key ( & var_op. name ) {
356- handler_scope. scope . insert ( var_op. name . clone ( ) , var_op. xref ) ;
357- }
358- }
359- _ => { }
360- }
361- }
362- }
363- for ( name, xref) in & scope. scope {
364- if !handler_scope. scope . contains_key ( name) {
365- handler_scope. scope . insert ( name. clone ( ) , * xref) ;
366- }
367- }
368- for ( name, xref) in & scope. local_definitions {
369- if !handler_scope. local_definitions . contains_key ( name) {
370- handler_scope. local_definitions . insert ( name. clone ( ) , * xref) ;
371- }
372- }
296+ // Per Angular's resolve_names.ts: handler ops get their own scope
297+ let handler_scope = build_scope_from_handler_ops ( listener. handler_ops . iter ( ) ) ;
373298
374299 for handler_op in listener. handler_ops . iter_mut ( ) {
375300 transform_expressions_in_update_op (
@@ -387,46 +312,10 @@ fn process_lexical_scope_create<'a>(
387312 VisitorContextFlag :: NONE ,
388313 ) ;
389314 }
390- // Note: AnimationListenerOp has no handler_expression field
391315 }
392316 CreateOp :: Animation ( animation) => {
393- // Build handler-specific scope for Animation
394- // Uses same "first wins" semantics as Listener (see above)
395- let mut handler_scope = ScopeMaps :: default ( ) ;
396- for handler_op in animation. handler_ops . iter ( ) {
397- if let UpdateOp :: Variable ( var_op) = handler_op {
398- match var_op. kind {
399- SemanticVariableKind :: Identifier => {
400- if var_op. local
401- && !handler_scope. local_definitions . contains_key ( & var_op. name )
402- {
403- handler_scope
404- . local_definitions
405- . insert ( var_op. name . clone ( ) , var_op. xref ) ;
406- }
407- if !handler_scope. scope . contains_key ( & var_op. name ) {
408- handler_scope. scope . insert ( var_op. name . clone ( ) , var_op. xref ) ;
409- }
410- }
411- SemanticVariableKind :: Alias => {
412- if !handler_scope. scope . contains_key ( & var_op. name ) {
413- handler_scope. scope . insert ( var_op. name . clone ( ) , var_op. xref ) ;
414- }
415- }
416- _ => { }
417- }
418- }
419- }
420- for ( name, xref) in & scope. scope {
421- if !handler_scope. scope . contains_key ( name) {
422- handler_scope. scope . insert ( name. clone ( ) , * xref) ;
423- }
424- }
425- for ( name, xref) in & scope. local_definitions {
426- if !handler_scope. local_definitions . contains_key ( name) {
427- handler_scope. local_definitions . insert ( name. clone ( ) , * xref) ;
428- }
429- }
317+ // Per Angular's resolve_names.ts: handler ops get their own scope
318+ let handler_scope = build_scope_from_handler_ops ( animation. handler_ops . iter ( ) ) ;
430319
431320 for handler_op in animation. handler_ops . iter_mut ( ) {
432321 transform_expressions_in_update_op (
@@ -444,7 +333,6 @@ fn process_lexical_scope_create<'a>(
444333 VisitorContextFlag :: NONE ,
445334 ) ;
446335 }
447- // Note: AnimationOp has no handler_expression field
448336 }
449337 _ => {
450338 transform_expressions_in_create_op (
0 commit comments