Skip to content

fix(directive): compile inputs:/outputs: declared in decorator metadata - #493

Merged
Brooooooklyn merged 7 commits into
mainfrom
fix/decorator-metadata-inputs-outputs
Oct 1, 2026
Merged

Brooooooklyn merged 7 commits into
mainfrom
fix/decorator-metadata-inputs-outputs

Conversation

@ashley-hunter

@ashley-hunter ashley-hunter commented Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator

Stack (1/5): #493 inputs/outputs → #494 transform validation → #495 queries: → #496 .d.ts transform types → #504 signal API import identity. This is #493.

Fixes inputs: and outputs: declared in @Directive / @Component metadata being silently dropped from the compiled definition.

The bug

@Directive({
  selector: 'a',
  inputs: ['x', { name: 'y', alias: 'why', transform: bool }],
})
class A { x?: string; y?: boolean; }

compiled to ɵɵdefineDirective({ type: A, selectors: [["a"]] }): no inputs at all. The decorator parsers never read the inputs/outputs keys, so there was no error either, and a binding like [x]="..." went to the element as a property instead of reaching the directive. The same happened for @Component, for outputs:, and on abstract @Directive() bases (whose subclasses then inherited nothing).

The fix

Both decorator parsers now read inputs: ('name', 'name: alias', { name, alias?, required?, transform? }) and outputs: ('name', 'name: alias') and merge them with @Input/@Output/signal members the way ngtsc does ({...inputsFromMeta, ...inputsFromFields}): keyed by class property, a member replacing a metadata entry in place, a repeated metadata entry keeping its first position, and anything after a second : ignored. Keys follow JS object rules, as in ngtsc's object: a __proto__ entry is dropped and integer-like keys come first.

Values are read through directive/evaluator.rs, a single-file model of ngtsc's partial evaluator. It resolves what ngtsc resolves within one file:

  • literals, arrays and objects with spreads, template literals (also with numbers and enum members)
  • same-file const/let/var (the first var wins), destructuring (const {a} = ..., const [a] = ...), enums, and declare const X: ['a']
  • binary, unary, conditional and &&/|| operators
  • calls to same-file functions and static methods whose body is a single return. A transform returned from such a helper (inputs: make('x', booleanAttribute)) is emitted as the expression passed at the call site, so it stays in scope
  • array.concat/slice/length and string.concat

Syntax ngtsc doesn't evaluate is rejected too: satisfies, <T>x, ??, numeric computed keys, array['0'], string.length, computed decorator keys ([K]: [...]). Each file's top-level declarations are only collected when a decorator needs one, each const is evaluated once and dependencies-first (so long chains don't nest), and a depth and work limit stop cycles, recursion and exponential growth.

Forms ngtsc rejects now report its diagnostic word for word, at the node ngtsc points to (the inputs:/outputs: value, or the colliding member), instead of being dropped: a non-array inputs:/outputs:, entries that aren't strings or object literals, objects without a string name, and a signal input()/output()/model() also listed in the metadata. That check, like ngtsc's, only counts Angular's own functions: a named import (aliases too) or a namespace import from @angular/core, and outputFromObservable from @angular/core/rxjs-interop. A same-named local or foreign function doesn't collide. The value descriptions match too (a bad spread marks only its element, enum members are of type 'E', let x; is undefined, unknown names could not be determined statically). Like ngtsc, only the first error per class is reported.

One deliberate difference: ngtsc follows imports, but OXC compiles one file at a time. A value imported from another module (including an imported alias or required, which would otherwise change the binding) reports:

@Directive.inputs depends on 'SHARED', which is imported from another module. OXC compiles one file at a time and cannot evaluate values from other files.

Two more differences, both where ngtsc emits code that doesn't work:

  • A transform that uses its helper's parameters in any other way ((v) => v + name, with name a helper parameter) is an error. ngtsc copies it, and name then refers to a global.
  • An import passed inside an object argument (make({t: booleanAttribute}), returning o.t) is an error. ngtsc emits booleanAttribute.

(A small output-only difference is recorded in the snapshot: oxc quotes integer-like keys in the JS inputs map, where ngtsc leaves them bare. The order is the same.)

A transform returned by a helper is checked with JavaScript scoping: names the transform declares itself (parameters, const, functions, classes, loop and catch bindings, hoisted vars) hide the helper's parameters, and a nested normal function has its own this / arguments. Arguments passed through spreads (make(...args)) or read through rest parameters (a[1]) keep where they were written, so they emit like ngtsc.

&& / || need an imported operand only when it is the left one or the one picked (LOCAL || NAME is LOCAL), and c ? a : b evaluates only the branch it picks, like ngtsc. When the import decides the result, the "imported from another module" error is kept.

The scope check for a helper's transform ignores types (annotations, return types, type arguments and parameters, the type part of as / satisfies / <T>, local type aliases and interfaces), since they're erased; ngtsc accepts these. A helper parameter used at runtime next to a type (name!, name as T, a default value) is still reported.

Values exported from a same-file namespace are evaluated through typeof NS.X and aliases like import I = NS.X, scoped and merged like TypeScript. As in ngtsc, NS.X in a value position stays dynamic, and a transform written in a namespace that uses the namespace's own declarations is reported.

Tests

  • decorator_inputs_outputs_test.rs: the original report, which failed before the fix.
  • decorator_metadata_ngtsc_test.rs compares diagnostics, diagnostic spans, the inputs/outputs maps and the .d.ts declarations with a snapshot of @angular/compiler-cli 22.1.7's output (tests/fixtures/decorator_metadata_ngtsc.json, 229 cases). 14 are Angular's own specs for decorator inputs/outputs (ngtsc_spec, authoring_{inputs,outputs,models}_spec, the order_bindings compliance case), the rest probe the evaluator and its diagnostics. Maps are compared ignoring whitespace and parentheses (which the two emitters place differently) outside string literals, so a stray space inside an emitted name still fails.
  • Separate tests cover the imported-value message and that long chains, cycles and exponential inputs don't overflow the stack or hang.
  • The Angular specs, plus the directive_spec / inherit_definition_feature_spec acceptance cases, are also added to the compare harness (decorator-metadata-angular-spec.fixture.ts), which diffs the full file against a live ngtsc.

…tadata

`@Directive({ inputs: [...] })` and `@Component({ inputs: [...], outputs: [...] })`
were ignored by the decorator parsers, so metadata-declared inputs and outputs
were silently dropped from the compiled definition and template bindings went
to the element instead of the directive.

Both parsers now read `inputs:` (`'name'`, `'name: alias'`,
`{ name, alias?, required?, transform? }`) and `outputs:` (`'name'`,
`'name: alias'`) and merge them with `@Input`/`@Output`/signal members the way
ngtsc does: keyed by class property, a member declaration replacing a metadata
entry in place, a repeated metadata entry keeping its first position.

Values are read through a single-file model of ngtsc's partial evaluator
(`directive/evaluator.rs`): literals, template strings, arrays and objects with
spreads, and same-file `const`/`let`/`var`, so `inputs: [...SHARED, 'x']`
resolves like it does in ngtsc. Forms ngtsc rejects now report its diagnostics
word for word instead of being dropped (non-array values, invalid entries,
entries without a `name`, signal members also listed in metadata). Values
imported from another file can't be evaluated from one file and are reported
as unresolvable.

`decorator_metadata_ngtsc_test.rs` compares diagnostics, inputs/outputs maps
and `.d.ts` declarations against a snapshot of `@angular/compiler-cli` 22.1.7's
output for Angular's own specs covering decorator `inputs`/`outputs`, plus
probes of the evaluator.
Brooooooklyn and others added 2 commits September 30, 2026 13:42
…iagnostics

Checked against @angular/compiler-cli 22.1.7 (probe results are in the
fixture):

- Evaluate what ngtsc's partial evaluator resolves in one file: binary,
  logical (`&&`, `||`), unary and conditional operators, template literals
  with non-string parts, enums, `const {a} = ...` / `const [a] = ...`,
  calls to same-file functions and static methods whose body is one
  `return`, `array.concat/slice/length`, `string.concat`, and
  `declare const X: ['a']`. Top-level consts are evaluated
  dependencies-first, so long chains don't nest; depth and a work budget
  bound cycles, recursion and exponential growth.
- Reject what ngtsc rejects: `satisfies`, `<T>x`, `??`, computed numeric
  keys, `array['0']`, `string.length`, computed decorator keys
  (`[K]: [...]`). The first `var X` declaration wins.
- Messages word for word: a bad spread marks only its element, enum
  members are `of type 'E'`, `let x;` is `undefined`, unknown names are
  `could not be determined statically`.
- A value imported from another module is reported as something OXC can't
  read (it compiles one file), including an imported `alias`/`required`,
  which was silently ignored.
- Diagnostics point where ngtsc's do: the `inputs:`/`outputs:` value, or
  the colliding member.
- Maps follow JS object key rules: `__proto__` entries are dropped and
  integer-like keys come first.
- The ngtsc snapshot test only ignores whitespace outside string
  literals, and compares diagnostic spans.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…g Angular's signal APIs

Checked against @angular/compiler-cli 22.1.7 (probes are in the fixture):

- A transform returned by a same-file function the metadata calls was
  emitted as written inside that function, so
  `make('x', booleanAttribute)` with `return [{name, transform}]`
  compiled to `transform`, a name that only exists there (ReferenceError
  at class init). A parameter now becomes the argument it was passed
  (`booleanAttribute`), through nested calls and default values, as
  ngtsc emits it. A transform that uses the function's parameters, `this`
  or `arguments` in any other way is reported instead: ngtsc copies it
  as written, where it means something else or nothing.
- The "also declared as non-signal" / "unexpectedly declared" errors
  fired for any call named `input()`, `model()`, `output()` or
  `outputFromObservable()`. Like ngtsc, they now fire only for Angular's
  own: imported by name from `@angular/core` (or
  `@angular/core/rxjs-interop`), under any alias, or through a namespace
  import. A local `output()` or `local.input()` no longer errors, and an
  aliased `input as inp` now does. Signal members are still compiled by
  name, as before.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Brooooooklyn and others added 4 commits September 30, 2026 23:35
…read and rest arguments

A transform returned by a helper was rejected when it merely looked like it
used the helper's parameters:

- A name the transform declares itself (its own parameter, a `const`, a
  nested function or class, a loop or `catch` binding, a hoisted `var`)
  hides the helper's parameter of the same name, and a nested non-arrow
  function has its own `this` and `arguments`. Only a name that really
  resolves to the helper's parameter (or the helper's `this` /
  `arguments`) is still an error.
- Arguments passed through a spread (`make(...args)`, `make('x', ...[f])`)
  and read through a rest parameter (`function make(...a)` with `a[1]`)
  keep where each one was written, so the transform is emitted as that
  expression, like ngtsc.

Adds 20 ngtsc 22.1.7 probes (`scope-shadowed*`, `scope-spread*`,
`scope-rest*`) to the snapshot, more cases to the scoped-transform test, and
bounds for cyclic and exponential spread arguments.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e result

`inputs: [LOCAL || NAME]` with `NAME` imported reported the "imported from
another module" error, although `LOCAL` is truthy and picks the result. ngtsc
emits `LOCAL`'s value there. An import now only matters as the left operand
(it picks the result) or as the operand picked; a dynamic operand still makes
the whole expression dynamic, as in ngtsc. `c ? a : b` already evaluated only
the branch it picks.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A transform returned from a helper was rejected when it mentioned one of
the helper's parameters only in a type: `(v: string): typeof name => v`,
`v as typeof name`, `id<typeof name>(v)`, a parameter's or a nested
declaration's type, or a local type alias or interface. Types are erased
from the emitted code, so ngtsc 22.1.7 accepts these. The scope check now
skips type annotations, type arguments and parameters, and type-only
declarations, and still checks the runtime parts next to them (a
parameter's default, the expression inside `x!`, `as`, `satisfies` and
`<T>x`).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ike ngtsc

ngtsc resolves the last part of `typeof NS.X` (in a `declare const`) to its
declaration and evaluates it, so a value exported from a namespace in the same
file compiles. oxc reported it as a reference to `X`.

The evaluator now collects namespace bodies (merged like TypeScript: by name at
the top level, by parent for exported nested ones, `namespace A.B`, and every
member exported in `declare namespace`) and resolves `typeof A.B.X` through
them. Code written in a namespace sees its declarations first, then its
merged bodies' exports, then the enclosing ones; functions and static members
declared there are evaluated in that scope. Import-equals aliases
(`import I = NS.X`, `import A = NS`, also in namespaces) resolve to what they
name, as TypeScript resolves them.

A namespace that declares a value is a reference to it, like ngtsc
(`inputs: NS` is "a reference to 'NS'"); one with only types has no value.
`NS.X` in a value position stays dynamic, as in ngtsc.

A transform written in a namespace that uses the namespace's declarations is
reported: ngtsc emits it as written, where it isn't in scope.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Brooooooklyn
Brooooooklyn removed this pull request from stack #503 October 1, 2026 08:45
@Brooooooklyn
Brooooooklyn merged commit b7cb3a6 into main Oct 1, 2026
10 checks passed
@Brooooooklyn
Brooooooklyn deleted the fix/decorator-metadata-inputs-outputs branch October 1, 2026 08:45
Brooooooklyn added a commit that referenced this pull request Oct 1, 2026
…type names

- An enum member reached through a namespace (`NS.E.A`, `C.E.A` for a
  class merged with a namespace, `NS.M.E.A`, `namespace A.B`) is kept as
  written, like `E.A`. It was shortened to `A`, which doesn't resolve;
  ngtsc 22.1.7 throws on these. `NS.E` and `A.B.T` are still shortened
  to `E` / `T`, as ngtsc writes them.
- A computed property name in a transform type (`{ [token]: string }`,
  `{ [ns.token]: string }`) follows the type-name rules: an
  `@angular/core` value becomes `i0.token`, another module's makes the
  type `unknown`. ngtsc copies the expression, which names an import the
  `.d.ts` doesn't have. Local and global names are unchanged.

Also compares the `ngAcceptInputType_*` types of the `scope-*` probes added
under #493, and adds a `dts-qualifiedNamespaceEnum` probe.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Brooooooklyn added a commit that referenced this pull request Oct 1, 2026
…type names

- An enum member reached through a namespace (`NS.E.A`, `C.E.A` for a
  class merged with a namespace, `NS.M.E.A`, `namespace A.B`) is kept as
  written, like `E.A`. It was shortened to `A`, which doesn't resolve;
  ngtsc 22.1.7 throws on these. `NS.E` and `A.B.T` are still shortened
  to `E` / `T`, as ngtsc writes them.
- A computed property name in a transform type (`{ [token]: string }`,
  `{ [ns.token]: string }`) follows the type-name rules: an
  `@angular/core` value becomes `i0.token`, another module's makes the
  type `unknown`. ngtsc copies the expression, which names an import the
  `.d.ts` doesn't have. Local and global names are unchanged.

Also compares the `ngAcceptInputType_*` types of the `scope-*` probes added
under #493, and adds a `dts-qualifiedNamespaceEnum` probe.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Brooooooklyn added a commit that referenced this pull request Oct 1, 2026
…type names

- An enum member reached through a namespace (`NS.E.A`, `C.E.A` for a
  class merged with a namespace, `NS.M.E.A`, `namespace A.B`) is kept as
  written, like `E.A`. It was shortened to `A`, which doesn't resolve;
  ngtsc 22.1.7 throws on these. `NS.E` and `A.B.T` are still shortened
  to `E` / `T`, as ngtsc writes them.
- A computed property name in a transform type (`{ [token]: string }`,
  `{ [ns.token]: string }`) follows the type-name rules: an
  `@angular/core` value becomes `i0.token`, another module's makes the
  type `unknown`. ngtsc copies the expression, which names an import the
  `.d.ts` doesn't have. Local and global names are unchanged.

Also compares the `ngAcceptInputType_*` types of the `scope-*` probes added
under #493, and adds a `dts-qualifiedNamespaceEnum` probe.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Brooooooklyn added a commit that referenced this pull request Oct 1, 2026
…type (#496)

* feat(dts): type `ngAcceptInputType_*` with the transform's parameter type

Library `.d.ts` declarations wrote `static ngAcceptInputType_<input>: unknown`
for every input with a transform, so consumers' template type-checking accepted
any value. ngtsc writes the type of the transform's first parameter, so e.g.
`[count]="true"` against a `(value: string | number) => number` transform is a
type error. That type is now printed the way ngtsc prints it: `@angular/core`
names become `i0.Name`, string literals are re-quoted, spacing is normalised,
local and global names are kept, `unknown` for a transform with no parameters.

Two deliberate differences from ngtsc, both falling back to the previous
`unknown`:

- a type referencing another module: ngtsc adds `import * as iN` for it, but
  aliases numbered per source file can't be merged safely into bundled
  declaration files
- an imported transform, whose signature can't be read from one file

A member `@Input` overriding an `inputs:` entry decides the type, like the
compiled inputs map. Also emits `"ng-component"` as the `.d.ts` selector of a
component without one, as ngtsc does (it was `never`).

* fix(dts): rewrite every type position of transform types like ngtsc

The `ngAcceptInputType_*` printer copied type parameters, mapped types,
predicates, `this` parameters and non-property members verbatim, so
`@angular/core` names there missed the `i0.` rewrite and other-module names
escaped the `unknown` fallback. Print every node from the AST instead, and
fall back to `unknown` for forms that can't be printed.

Also match TypeScript's printer: property keys and destructuring keys
double-quoted, strings escaped to ASCII (`\0`, `😀`), `{ w }`
patterns, bigint/number normalisation, and the same-line comments it keeps in
front of list elements. `/** */` comments after a type or on their own line are
still dropped.

The new test compares 239 forms against ngtsc 22.1.7's `.d.ts` output.

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

* fix(dts): type overloads from the first declaration and print qualified local names like ngtsc

Checked against @angular/compiler-cli 22.1.7:

- An overloaded function or static method transform is typed from its first
  declaration (the one ngtsc reads), not its implementation.
- A qualified type name whose head the file declares (`NS.T`, `C.T` for a
  class merged with a namespace, `A.B.T`, `declare namespace`) is written as
  its last part, `T`, as ngtsc writes it. An enum member (`E.A`), which
  ngtsc can't emit, stays as written; `ng.Signal` through an
  `@angular/core` namespace is still `i0.Signal`, and names from other
  modules are still `unknown`.
- A TypeScript library function used as a transform (`parseInt`, `isNaN`,
  ...) is typed with its first parameter's type from `lib.es5.d.ts`.
- The ngtsc snapshot gets back the typed `ngAcceptInputType_*` of the 26
  cases added while every one was `unknown`, regenerated from ngtsc, plus
  probes of qualified names and library functions. A static
  `ngAcceptInputType_*` the class declares itself is TypeScript's own
  declaration output, so the test doesn't expect oxc to write it.

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

* fix(dts): quote and escape `ngAcceptInputType_*` names like ngtsc

An input whose class property is a string key was written quoted but not
escaped whenever it wasn't an identifier, so `'a-"b'` gave
`static "ngAcceptInputType_a-"b": ...`, which doesn't parse, and `'c.\\d'`
named a different property.

ngtsc (22.1.7) quotes the name only when Angular's `isUnsafeObjectKey`
(`/[-.]/`) matches, and prints it as a TypeScript string literal
(`"ngAcceptInputType_a-\"b"`, `"ngAcceptInputType_k-é"`). Every
other name is written as is, like `ngAcceptInputType_a"b`. oxc now does
the same, reusing the string printer of the transform types.

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

* test(dts): compare the `.d.ts` types of transforms passed through helper functions

The snapshot's `scope-*` probes (added earlier in the stack, before
`.d.ts` transform types) left out ngtsc's `ngAcceptInputType_*` types.
With them back, a function expression passed to a helper, as a default
value, through `@Input(opts(...))` or written in a helper is typed from
its own parameter, like ngtsc 22.1.7 (`string`).

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

* fix(dts): keep nested enum member types and type computed names like type names

- An enum member reached through a namespace (`NS.E.A`, `C.E.A` for a
  class merged with a namespace, `NS.M.E.A`, `namespace A.B`) is kept as
  written, like `E.A`. It was shortened to `A`, which doesn't resolve;
  ngtsc 22.1.7 throws on these. `NS.E` and `A.B.T` are still shortened
  to `E` / `T`, as ngtsc writes them.
- A computed property name in a transform type (`{ [token]: string }`,
  `{ [ns.token]: string }`) follows the type-name rules: an
  `@angular/core` value becomes `i0.token`, another module's makes the
  type `unknown`. ngtsc copies the expression, which names an import the
  `.d.ts` doesn't have. Local and global names are unchanged.

Also compares the `ngAcceptInputType_*` types of the `scope-*` probes added
under #493, and adds a `dts-qualifiedNamespaceEnum` probe.

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

* fix(dts): type import-equals aliases, `typeof this`, enum-merged namespaces and lone surrogates like ngtsc

- `import A = NS` / `import C = NS.T` (and `export import`) stand for their
  target: `A.T` and `C` are written `T`, as ngtsc writes them. An alias of
  another module (`import R = require('./other')`, or of an import) makes
  the type `unknown`, like any other module's type (ngtsc writes the bare
  name, which doesn't resolve). An alias of `@angular/core` becomes `i0.X`,
  and one of a global is written as its target (`Intl.NumberFormat`).
- `typeof this` is kept, like ngtsc (it was `unknown`).
- A qualified name through an enum merged with a namespace (`E.T`) is
  shortened to `T` like ngtsc; only an actual enum member (`E.A`, from any
  declaration of the enum) is kept as written.
- Lone surrogates in string literal types and quoted keys (`'\uD800'`) are
  printed as `"\uD800"`, like ngtsc, instead of OXC's internal encoding.

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

* fix(dts): type import-equals aliases declared in namespaces like their target

`namespace NS { export import Alias = Local; }` with a parameter typed
`NS.Alias` printed `Alias`, where ngtsc resolves the alias and writes `Local`.
The same went for nested namespaces (`NS.Inner.Alias2`), aliases of aliases,
aliases reached through a top-level alias (`import X = NS.Alias`), and targets
named from the namespace's own scope (`export import Rel = In2`).

Namespace member aliases now resolve to their target, whose head is looked up
in the alias's namespace first, then the enclosing ones, then the file. The
result follows the existing rules: local names are shortened, `@angular/core`
names become `i0.X`, other modules make the type `unknown`, and globals keep
their target (`Intl.NumberFormat`).

Also tests that a transform that's a global declared outside the file (`atob`,
now assumed to be a function) is typed `unknown`, like an imported one.

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

* fix(dts): type `import Core = require('@angular/core')` members as `i0.X`

Every `require` alias was treated as another module's, so `Core.Signal<string>`
(also through `import S = Core.Signal`, `import C = Core` or a namespace's
alias of it) typed the input as `unknown`. An alias of `@angular/core` now
gives `i0.Signal<string>`, like a namespace import of it. ngtsc 22.1.7 writes
the bare `Signal<string>`, which doesn't resolve in its `.d.ts`; other modules
stay `unknown`.

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

* fix(dts): resolve import-equals aliases in computed property names

A computed name in a transform's parameter type (`{ [Core.ɵSIGNAL]: string }`)
skipped the alias resolution type names get, so an alias of
`@angular/core` (`import Core = require('@angular/core')`, `import C = ng`,
`import S = Core.ɵSIGNAL`, or one declared in a namespace) was written as
is, another module's alias didn't make the type `unknown`, and a global's
alias wasn't written as its target. Computed names now resolve aliases
the way type names do: `i0.ɵSIGNAL`, `unknown`, and `G.k`. ngtsc 22.1.7
copies all of these as written, which doesn't resolve in its `.d.ts`. An
alias of a name the file declares stays as written, like ngtsc.

The snapshot's `scope-typeOnly-*` probes (added earlier in the stack,
before `.d.ts` transform types) now also compare ngtsc's
`ngAcceptInputType_*` types, including `typeof name` for a helper's
parameter used as a type.

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

* fix(dts): keep enum members named by a string or template literal as written

`enum E { [`A`] = 'a' }` names its member with a template literal, which the
enum member check ignored: `E.A` was taken for a qualified local name and
shortened to `A`, which doesn't resolve (`static ngAcceptInputType_x: A;`),
also for an enum in a namespace. Members named `'A'`, `['A']` or `` [`A`] ``
(escapes included) are now recognised like `A`, so the type is kept as
written. ngtsc 22.1.7 throws on all of these.

Transforms declared in a namespace (through `typeof NS.fn`, a static method
of a class there, or `import H = NS.fn`) are typed from their declaration,
like ngtsc; add them to the tests and snapshot.

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

* fix(dts): follow alias chains of any length and keep U+2028 / U+2029 line breaks whole

An import-equals alias chain is followed until it ends, stopping only on a
cycle, instead of giving up after 16 aliases (`A17.T` through 18 aliases is
`T`, like ngtsc). Looking back for a `//` before a union now steps over a
U+2028 / U+2029 line break by its full UTF-8 length, where it used to slice
inside the character and panic.

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

---------

Co-authored-by: LongYinan <lynweklm@gmail.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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.

2 participants