@@ -463,10 +463,12 @@ impl<'a> Parser<'a> {
463463 // Parse the primary expression (e.g., `condition` in `*ngIf="condition"`)
464464 let start = self . peek ( ) . map ( |t| t. index ) . unwrap_or ( 0 ) ;
465465 let value = self . parse_pipe_for_template_binding ( ) ;
466- // For template bindings, source includes trailing content up to end of next token
467- let end = self . peek ( ) . map ( |t| t. end ) . unwrap_or ( self . source . len ( ) as u32 ) ;
466+ // Value source is just the expression (up to current_end_index)
467+ let value_end = self . current_end_index ( ) ;
468+ // Source span extends to start of next token (includes trailing whitespace)
469+ let source_end = self . peek ( ) . map ( |t| t. index ) . unwrap_or ( self . source . len ( ) as u32 ) ;
468470
469- let value_source = & self . source [ start as usize ..end as usize ] ;
471+ let value_source = & self . source [ start as usize ..value_end as usize ] ;
470472 let ast_with_source = ASTWithSource {
471473 ast : value,
472474 source : Some ( Atom :: from_in ( value_source, self . allocator ) ) ,
@@ -480,13 +482,17 @@ impl<'a> Parser<'a> {
480482 template_key. span . end ,
481483 ) ;
482484 let source_span =
483- AbsoluteSourceSpan :: new ( template_key. span . start , self . absolute_offset + end ) ;
485+ AbsoluteSourceSpan :: new ( template_key. span . start , self . absolute_offset + source_end ) ;
484486 let expr_binding = ExpressionBinding { source_span, key, value : Some ( ast_with_source) } ;
485487 bindings. push ( TemplateBinding :: Expression ( expr_binding) ) ;
486488
487489 // Check for `as` binding after the primary expression (e.g., `*ngIf="cond | async as result"`)
488490 if self . optional_keyword ( "as" ) {
489- let as_binding = self . parse_as_binding ( & template_key. source ) ;
491+ let as_binding = self . parse_as_binding (
492+ & template_key. source ,
493+ template_key. span . start ,
494+ template_key. span ,
495+ ) ;
490496 if let Some ( binding) = as_binding {
491497 bindings. push ( binding) ;
492498 }
@@ -567,7 +573,11 @@ impl<'a> Parser<'a> {
567573 }
568574
569575 /// Parses a `let` binding: `let item` or `let i = index`.
570- fn parse_let_binding ( & mut self , template_key : & str ) -> Option < TemplateBinding < ' a > > {
576+ fn parse_let_binding ( & mut self , _template_key : & str ) -> Option < TemplateBinding < ' a > > {
577+ // Record the position of 'let' keyword before consuming it
578+ let let_token = self . peek ( ) ?. clone ( ) ;
579+ let let_start = self . absolute_offset + let_token. index ;
580+
571581 // Consume `let` keyword
572582 if !self . optional_keyword ( "let" ) {
573583 return None ;
@@ -587,7 +597,7 @@ impl<'a> Parser<'a> {
587597 let key = self . make_template_binding_identifier ( key_source. as_str ( ) , key_start, key_end) ;
588598
589599 // Check for `=` (e.g., `let i = index`)
590- let value = if self . optional_operator ( "=" ) {
600+ let ( value, source_end ) = if self . optional_operator ( "=" ) {
591601 let value_token = self . peek ( ) ?. clone ( ) ;
592602 if !value_token. is_identifier ( ) && !value_token. is_keyword ( ) {
593603 self . error ( "Expected identifier after '='" ) ;
@@ -596,20 +606,25 @@ impl<'a> Parser<'a> {
596606 let value_start = self . absolute_offset + value_token. index ;
597607 let value_end = self . absolute_offset + value_token. end ;
598608
599- // For `let i = index`, the value becomes `ngForIndex` (templateKey + capitalized value)
600- let value_source =
601- format ! ( "{}{}" , template_key, capitalize_first( value_token. str_value. as_str( ) ) ) ;
609+ // Use the value as-is (e.g., `let i = index` -> value is "index")
610+ let value_source = value_token. str_value . as_str ( ) ;
602611 self . advance ( ) ;
603- Some ( self . make_template_binding_identifier ( & value_source, value_start, value_end) )
612+ // Source span extends to include the value
613+ (
614+ Some ( self . make_template_binding_identifier ( value_source, value_start, value_end) ) ,
615+ value_end,
616+ )
604617 } else {
605618 // For bare `let item`, value is `$implicit`
606- Some ( self . make_template_binding_identifier ( "$implicit" , key_start, key_start) )
619+ // Source span includes trailing space after the variable name
620+ (
621+ Some ( self . make_template_binding_identifier ( "$implicit" , key_start, key_start) ) ,
622+ key_end + 1 ,
623+ )
607624 } ;
608625
609- let source_span = AbsoluteSourceSpan :: new (
610- key_start,
611- value. as_ref ( ) . map ( |v| v. span . end ) . unwrap_or ( key_end) ,
612- ) ;
626+ // Source span starts from 'let' keyword
627+ let source_span = AbsoluteSourceSpan :: new ( let_start, source_end) ;
613628 Some ( TemplateBinding :: Variable ( VariableBinding { source_span, key, value } ) )
614629 }
615630
@@ -655,17 +670,31 @@ impl<'a> Parser<'a> {
655670 }
656671
657672 let value = self . parse_pipe_for_template_binding ( ) ;
658- let expr_end = self . peek ( ) . map ( |t| t. end ) . unwrap_or ( self . source . len ( ) as u32 ) ;
673+ // Value source is just the expression (up to current_end_index)
674+ let value_end = self . current_end_index ( ) ;
675+
676+ // Source span extends to start of next binding (includes trailing separator and whitespace)
677+ // If the next token is `;` or `,`, consume it and use the start of the following token
678+ let source_end =
679+ if self . peek ( ) . map ( |t| t. is_character ( ';' ) || t. is_character ( ',' ) ) . unwrap_or ( false ) {
680+ // Consume the separator
681+ self . advance ( ) ;
682+ // Source span extends to start of next token (after separator)
683+ self . peek ( ) . map ( |t| t. index ) . unwrap_or ( self . source . len ( ) as u32 )
684+ } else {
685+ // No separator, source span extends to start of next token
686+ self . peek ( ) . map ( |t| t. index ) . unwrap_or ( self . source . len ( ) as u32 )
687+ } ;
659688
660- let value_source = & self . source [ expr_start as usize ..expr_end as usize ] ;
689+ let value_source = & self . source [ expr_start as usize ..value_end as usize ] ;
661690 let ast_with_source = ASTWithSource {
662691 ast : value,
663692 source : Some ( Atom :: from_in ( value_source, self . allocator ) ) ,
664693 location : Atom :: from_in ( "" , self . allocator ) ,
665694 absolute_offset : self . absolute_offset + expr_start,
666695 } ;
667696
668- let source_span = AbsoluteSourceSpan :: new ( keyword_start, self . absolute_offset + expr_end ) ;
697+ let source_span = AbsoluteSourceSpan :: new ( keyword_start, self . absolute_offset + source_end ) ;
669698 result. push ( TemplateBinding :: Expression ( ExpressionBinding {
670699 source_span,
671700 key,
@@ -674,7 +703,9 @@ impl<'a> Parser<'a> {
674703
675704 // Check for `as` binding after the expression
676705 if self . optional_keyword ( "as" ) {
677- let as_binding = self . parse_as_binding ( & full_key) ;
706+ // The value_key_span points to the keyword (e.g., "of" in "of items")
707+ let value_key_span = AbsoluteSourceSpan :: new ( keyword_start, keyword_end) ;
708+ let as_binding = self . parse_as_binding ( & full_key, keyword_start, value_key_span) ;
678709 if let Some ( binding) = as_binding {
679710 result. push ( binding) ;
680711 }
@@ -684,7 +715,14 @@ impl<'a> Parser<'a> {
684715 }
685716
686717 /// Parses an `as` binding: `... as alias`.
687- fn parse_as_binding ( & mut self , value_key : & str ) -> Option < TemplateBinding < ' a > > {
718+ /// `source_span_start` is the start of the source span (directive name start).
719+ /// `value_key_span` is the span of the directive name for the value.
720+ fn parse_as_binding (
721+ & mut self ,
722+ value_key : & str ,
723+ source_span_start : u32 ,
724+ value_key_span : AbsoluteSourceSpan ,
725+ ) -> Option < TemplateBinding < ' a > > {
688726 // Get alias name
689727 let alias_token = self . peek ( ) ?. clone ( ) ;
690728 if !alias_token. is_identifier ( ) && !alias_token. is_keyword ( ) {
@@ -698,9 +736,15 @@ impl<'a> Parser<'a> {
698736
699737 let key =
700738 self . make_template_binding_identifier ( alias_source. as_str ( ) , alias_start, alias_end) ;
701- let value = self . make_template_binding_identifier ( value_key, alias_start, alias_end) ;
739+ // The value's span should point to the directive name, not the alias
740+ let value = self . make_template_binding_identifier (
741+ value_key,
742+ value_key_span. start ,
743+ value_key_span. end ,
744+ ) ;
702745
703- let source_span = AbsoluteSourceSpan :: new ( alias_start, alias_end) ;
746+ // Source span extends from directive name start to alias end
747+ let source_span = AbsoluteSourceSpan :: new ( source_span_start, alias_end) ;
704748 Some ( TemplateBinding :: Variable ( VariableBinding { source_span, key, value : Some ( value) } ) )
705749 }
706750
@@ -1855,11 +1899,23 @@ impl<'a> Parser<'a> {
18551899 }
18561900 }
18571901
1858- // No identifier found after dot - emit error but preserve the incomplete access
1902+ // No identifier found after dot - emit errors but preserve the incomplete access
18591903 // Set WRITABLE context so skip() will stop at assignment operators
18601904 // Report error at the receiver's span end (right after the dot)
18611905 let receiver_end = receiver. span ( ) . end ;
18621906 self . context |= ParseContextFlags :: WRITABLE ;
1907+
1908+ // First, emit error about what unexpected token was found (matches Angular's expectIdentifierOrKeyword)
1909+ if let Some ( token) = self . peek ( ) {
1910+ self . error ( & format ! (
1911+ "Unexpected token {}, expected identifier or keyword" ,
1912+ token. str_value
1913+ ) ) ;
1914+ } else {
1915+ self . error ( "Unexpected end of input, expected identifier or keyword" ) ;
1916+ }
1917+
1918+ // Then emit the "Expected identifier for property access" error
18631919 self . error_at ( "Expected identifier for property access" , receiver_end) ;
18641920 self . context ^= ParseContextFlags :: WRITABLE ;
18651921
@@ -3043,12 +3099,12 @@ mod tests {
30433099 _ => panic ! ( "Expected expression binding" ) ,
30443100 }
30453101
3046- // Fourth: let i = ngForIndex
3102+ // Fourth: let i = index (value as-is, not prefixed with directive name)
30473103 match & result. bindings [ 3 ] {
30483104 TemplateBinding :: Variable ( var) => {
30493105 assert_eq ! ( var. key. source. as_str( ) , "i" ) ;
30503106 assert ! ( var. value. is_some( ) ) ;
3051- assert_eq ! ( var. value. as_ref( ) . unwrap( ) . source. as_str( ) , "ngForIndex " ) ;
3107+ assert_eq ! ( var. value. as_ref( ) . unwrap( ) . source. as_str( ) , "index " ) ;
30523108 }
30533109 _ => panic ! ( "Expected variable binding" ) ,
30543110 }
0 commit comments