Skip to content

Commit 909bda4

Browse files
committed
pass all tests
1 parent 0b92835 commit 909bda4

36 files changed

+16257
-249
lines changed

Cargo.lock

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/oxc_angular_compiler/src/ast/html.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ pub struct HtmlElement<'a> {
4747
pub name: Atom<'a>,
4848
/// The element attributes.
4949
pub attrs: Vec<'a, HtmlAttribute<'a>>,
50+
/// Selectorless directives (e.g., @Dir, @Dir(attr="value")).
51+
pub directives: Vec<'a, HtmlDirective<'a>>,
5052
/// The child nodes.
5153
pub children: Vec<'a, HtmlNode<'a>>,
5254
/// The source span.
@@ -59,6 +61,23 @@ pub struct HtmlElement<'a> {
5961
pub is_self_closing: bool,
6062
}
6163

64+
/// A selectorless directive in the HTML AST (e.g., @Dir or @Dir(attr="value")).
65+
#[derive(Debug)]
66+
pub struct HtmlDirective<'a> {
67+
/// The directive name (without the @ prefix).
68+
pub name: Atom<'a>,
69+
/// The directive attributes (inside parentheses, if any).
70+
pub attrs: Vec<'a, HtmlAttribute<'a>>,
71+
/// The source span for the entire directive.
72+
pub span: Span,
73+
/// The span for @DirectiveName.
74+
pub name_span: Span,
75+
/// The span for the opening paren (if present).
76+
pub start_paren_span: Option<Span>,
77+
/// The span for the closing paren (if present).
78+
pub end_paren_span: Option<Span>,
79+
}
80+
6281
/// An attribute node in the HTML AST.
6382
#[derive(Debug)]
6483
pub struct HtmlAttribute<'a> {
@@ -372,6 +391,7 @@ mod tests {
372391
let child1 = HtmlElement {
373392
name: Atom::from("span"),
374393
attrs: Vec::new_in(&allocator),
394+
directives: Vec::new_in(&allocator),
375395
children: Vec::new_in(&allocator),
376396
span: Span::default(),
377397
start_span: Span::default(),
@@ -382,6 +402,7 @@ mod tests {
382402
let child2 = HtmlElement {
383403
name: Atom::from("p"),
384404
attrs: Vec::new_in(&allocator),
405+
directives: Vec::new_in(&allocator),
385406
children: Vec::new_in(&allocator),
386407
span: Span::default(),
387408
start_span: Span::default(),
@@ -396,6 +417,7 @@ mod tests {
396417
let root = HtmlElement {
397418
name: Atom::from("div"),
398419
attrs: Vec::new_in(&allocator),
420+
directives: Vec::new_in(&allocator),
399421
children,
400422
span: Span::default(),
401423
start_span: Span::default(),

crates/oxc_angular_compiler/src/parser/expression/lexer.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1464,7 +1464,7 @@ impl<'a> Lexer<'a> {
14641464
};
14651465

14661466
if seen[idx] {
1467-
self.error(start, &format!("Duplicate regular expression flag '{ch}'"));
1467+
self.error(start, &format!("Duplicate regular expression flag \"{ch}\""));
14681468
return false;
14691469
}
14701470
seen[idx] = true;

crates/oxc_angular_compiler/src/parser/expression/parser.rs

Lines changed: 82 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)