From 79b7438defe33ea42e143b8f8532e18d336bae96 Mon Sep 17 00:00:00 2001 From: Damian Kaczmarek Date: Wed, 21 Oct 2026 12:55:54 -0500 Subject: [PATCH] fix(lexer): a `<` doesn't end a @let value `@let small = count < 10;` was cut at the `<`, which the lexer took for a tag, and reported as an incomplete @let declaration, so the component wasn't compiled. Like Angular's `_consumeLetDeclarationValue`, only a `;` outside quotes (or EOF) ends the value now. --- .../src/parser/html/lexer.rs | 5 +-- .../tests/html_lexer_test.rs | 30 +++++++++++++++++ .../tests/let_value_less_than_test.rs | 32 +++++++++++++++++++ 3 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 crates/oxc_angular_compiler/tests/let_value_less_than_test.rs diff --git a/crates/oxc_angular_compiler/src/parser/html/lexer.rs b/crates/oxc_angular_compiler/src/parser/html/lexer.rs index 2b03220dd..059d82b6f 100644 --- a/crates/oxc_angular_compiler/src/parser/html/lexer.rs +++ b/crates/oxc_angular_compiler/src/parser/html/lexer.rs @@ -1360,8 +1360,9 @@ impl<'a> HtmlLexer<'a> { break; } - // EOF or tag start ends the value - if ch == chars::EOF || ch == '<' { + // EOF ends the value. A `<` does not (`@let a = b < c;`): like Angular's + // `_consumeLetDeclarationValue`, only a semicolon outside quotes ends it. + if ch == chars::EOF { break; } diff --git a/crates/oxc_angular_compiler/tests/html_lexer_test.rs b/crates/oxc_angular_compiler/tests/html_lexer_test.rs index 8a1202c16..b950f3ce6 100644 --- a/crates/oxc_angular_compiler/tests/html_lexer_test.rs +++ b/crates/oxc_angular_compiler/tests/html_lexer_test.rs @@ -1675,6 +1675,36 @@ mod let_declarations { ); } + #[test] + fn should_parse_let_declaration_with_less_than_in_its_value() { + // Angular's `_consumeLetDeclarationValue` ends the value only at a `;` + // outside quotes (or EOF): a `<` is a comparison, not a tag + let result = tokenize_and_humanize_parts("@let small = count < 10;"); + assert_eq!( + result, + vec![ + (HtmlTokenType::LetStart, vec!["small".to_string()]), + (HtmlTokenType::LetValue, vec!["count < 10".to_string()]), + (HtmlTokenType::LetEnd, vec![]), + (HtmlTokenType::Eof, vec![]), + ] + ); + } + + #[test] + fn should_parse_let_declaration_with_comparisons_followed_by_an_element() { + let result = tokenize_and_humanize_parts("@let both = a < b && b < c;"); + assert_eq!( + result[..3], + [ + (HtmlTokenType::LetStart, vec!["both".to_string()]), + (HtmlTokenType::LetValue, vec!["a < b && b < c".to_string()]), + (HtmlTokenType::LetEnd, vec![]), + ] + ); + assert_eq!(result[3].0, HtmlTokenType::TagOpenStart); + } + #[test] fn should_report_incomplete_let_without_equals_after_name() { // No upstream spec case; Angular's _consumeLetDeclaration marks the diff --git a/crates/oxc_angular_compiler/tests/let_value_less_than_test.rs b/crates/oxc_angular_compiler/tests/let_value_less_than_test.rs new file mode 100644 index 000000000..ca1b8f49a --- /dev/null +++ b/crates/oxc_angular_compiler/tests/let_value_less_than_test.rs @@ -0,0 +1,32 @@ +//! A `@let` value can contain `<` (`@let small = count < 10;`): as in Angular's +//! lexer, only a `;` outside quotes ends it. `@angular/compiler-cli` 21.0.1 +//! compiles the template below to `const small_r1 = ctx.count < 10;` and +//! `const both_r2 = ctx.a < ctx.b && ctx.b < ctx.c;`. + +use oxc_allocator::Allocator; +use oxc_angular_compiler::{TransformOptions, transform_angular_file}; + +#[test] +fn let_value_with_less_than_compiles_as_a_comparison() { + let source = r" +import { Component } from '@angular/core'; +@Component({ + selector: 'b-cmp', + template: `@let small = count < 10; @let both = a < b && b < c; +{{ small }} {{ both }}`, +}) +export class BCmp { count = 1; a = 1; b = 2; c = 3; } +"; + let allocator = Allocator::default(); + let result = transform_angular_file( + &allocator, + "test.ts", + source, + Some(&TransformOptions::default()), + None, + ); + assert!(!result.has_errors(), "Should not have errors: {:?}", result.diagnostics); + let code: String = result.code.chars().filter(|c| !c.is_whitespace()).collect(); + assert!(code.contains("constsmall_r1=(ctx.count<10);"), "Got:\n{}", result.code); + assert!(code.contains("constboth_r2=((ctx.a