Skip to content

fix(directive): compile input and output names with lone surrogates - #593

Merged
Brooooooklyn merged 2 commits into
mainfrom
fix/lone-surrogate-names
Oct 21, 2026
Merged

Brooooooklyn merged 2 commits into
mainfrom
fix/lone-surrogate-names

Conversation

@Brooooooklyn

Copy link
Copy Markdown
Member

Summary

Closes #505.

Since oxc 0.155 (#589), string values are JSStr, which has no &str form for a lone surrogate. So on main:

@Directive({ selector: '[d]' })
export class D {
  @Input() 'x-\uD800': any;          // dropped from `inputs`, no error
  @Input('z-\uDC00') z: any;         // error: "must resolve to a string ..."
  b = input('', { alias: 'b-\uD800' }); // error: "Alias needs to be a string ..."
}

ngtsc compiles all of them. I checked this by running ngc 22.2.3 (full and partial) and the linker on these cases. It prints each lone code unit in a generated string as an uppercase \uXXXX:

inputs: { "x-\uD800": "x-\uD800", z: [0, "z-\uDC00", "z"], b: [1, "b-\uD800", "b"] }

This PR emits the same, in full, partial and JIT output, in setClassMetadata and in the .d.ts.

How

A Rust &str can't hold a lone surrogate, so util/lone_surrogate.rs stores each lone code unit 0xD800 + k as two characters: the noncharacter U+FDD0 (which Unicode reserves for a program's internal use), then U+10F800 + k. Only that pair is a lone surrogate. Either character on its own, as real text, is printed as itself.

read   'x-\uD800'  ->  "x-" U+FDD0 U+10F800          member names, aliases, signal API
                                                     aliases, evaluated metadata strings
emit   U+FDD0 U+10F800+k  ->  \uD800+k               emitter escape_string, dts
                                                     escape_dts_string, dts quote,
                                                     JIT escape_js_string
Piece Change
util/lone_surrogate.rs Stores and reads back lone surrogates.
directive/evaluator.rs A lone-surrogate string evaluates to its string (was Dynamic). Transform types and the ngAcceptInputType_ clash check find such a member.
directive/property_decorators.rs, class_metadata/builders.rs Member names, @Input/input()/model()/output() aliases and propDecorators keys keep it. A string query predicate takes the same path as any string, so lone_surrogate_selectors goes.
directive/decorator.rs Drops the alias errors that existed only because the value couldn't be read. A selector holding a lone surrogate keeps main's error, because the selector parser can't read it.
component/transform.rs JIT member names and aliases keep it. The error plumbing #589 added for this case goes. The __decorate member name is now escaped.
output/emitter.rs, dts.rs, directive/dts_type.rs Print it as \uXXXX.

Where this differs from ngtsc

ngtsc writes invalid output in two places. This PR writes valid output there instead:

  • @ViewChild('r') 'q-\uD800': ngtsc writes ctx.q-<U+FFFD> = .... This PR writes ctx["q-\uD800"] = ....
  • An ngAcceptInputType_ member with a lone surrogate: ngtsc writes it unquoted, as U+FFFD. This PR quotes it.

Side effects of reading these strings

  • styles and host attribute values holding a lone surrogate now compile like ngtsc. On main they were errors.
  • A decorator query predicate is pooled into a const like any other (const _c0 = ["\uD800"]), as ngtsc does.

Not in this PR (same on main)

  • @HostBinding/@HostListener arguments, host keys, hostDirectives mappings, exportAs, @Pipe({name}) and @Attribute(...) holding a lone surrogate.
  • An inline template holding a lone surrogate. The component is silently left uncompiled.
  • Separate crash: the CSS selector parser panics on any non-ASCII selector (selector: '[ké]' panics at pipeline/selector.rs:240).
  • Diagnostics that quote such a name print the stored characters rather than \uXXXX.

Tests

tests/lone_surrogate_names_test.rs has 12 cases, with expected fragments taken from ngc's output. All 12 fail on main, except the two that pin main behaviour (the selector error, and private-use characters printed as themselves). The cases:

  • decorator inputs and outputs with .d.ts;
  • partial output;
  • signal APIs;
  • the inputs:/outputs: arrays;
  • lowercase escapes, and a lone unit at the start or end of a name;
  • JIT;
  • host values;
  • the selector error;
  • real U+10F800 and U+FDD0 characters printed as themselves;
  • quoted ngAcceptInputType_;
  • keyed query writes;
  • the clash error.

Three existing tests asserted the old behaviour (an error, or an inline predicate). They now assert ngtsc's output.

cargo fmt --all -- --check, cargo test --all-features (3340 passed) and cargo run -p oxc_angular_conformance (1264/1264, no snapshot diff) pass.

🤖 Generated with Claude Code

oxc 0.155 stores string values as `JSStr`, which has no `&str` form for a
lone surrogate (`'x-\uD800'`). Such an input or output member was dropped
from the definition, and such an alias was an error. ngtsc reads them like
any string and prints each lone code unit as a `\uXXXX` escape.

Names, aliases and evaluated metadata strings now keep each lone code unit
as two characters: the noncharacter U+FDD0, then U+10F800 + offset. The JS,
`.d.ts` and JIT string printers turn that pair back into `\uXXXX`; either
character on its own is printed as itself. A property name holding one is
written as a key (`ctx["q-\uD800"]`), and an `ngAcceptInputType_` member
holding one is quoted, where ngtsc's output is not valid JS / TS.

A selector holding a lone surrogate is still an error: the selector parser
can't read it.

Closes #505

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-21T05:58:16.212859Z c6da2db New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e5869b8826

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/oxc_angular_compiler/src/util/lone_surrogate.rs
Comment thread crates/oxc_angular_compiler/src/directive/evaluator.rs
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Brooooooklyn
Brooooooklyn merged commit b899787 into main Oct 21, 2026
10 checks passed
@Brooooooklyn
Brooooooklyn deleted the fix/lone-surrogate-names branch October 21, 2026 06:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(directive): input names with lone surrogates are emitted in oxc's internal U+FFFD encoding

1 participant