fix(compiler): recognise Angular decorators and signal APIs by their @angular/core import, like ngtsc - #504
Brooooooklyn wants to merge 12 commits into
Conversation
@angular/core import, like ngtsc
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
8f170d7 to
b84d218
Compare
@angular/core import, like ngtsc@angular/core import, like ngtsc
df18eb1 to
940c32d
Compare
940c32d to
dbf5681
Compare
dbf5681 to
0d8951f
Compare
0d8951f to
8606c3e
Compare
…ore import, like ngtsc
`@Component`, `@Directive`, `@Pipe`, `@Injectable` and `@NgModule` were
matched by name. Like ngtsc's `findAngularDecorator`, a class decorator
is now Angular's only when it's imported from `@angular/core`: by name
under any alias (`import {Component as Cmp}`), or through a namespace
import (`@ng.Component()`). This is the rule #504 uses for member
decorators, and the same check (`angular_core_decorator`).
Checked against @angular/compiler-cli 22.1.7 (44 probes in
tests/fixtures/class_decorators_ngtsc.json, 40 compared):
- Another library's, a local, an undeclared or a default-imported
`@Component` (and the other four) is left on the class, which isn't
compiled, and its `inputs:` aren't checked.
- One re-exported through another module (`import {Component} from
'./shared'`) is left alone too: ngtsc follows the re-export, oxc can't.
- An aliased one (`@Cmp`, `@D`, `@P`, `@M`, `@Inj`) and a namespaced
`@ng.Injectable` are now compiled; `setClassMetadata` lists them as
written (`type: Cmp`). An aliased `@Component` keeps `templateUrl` in
`setClassMetadata`, as ngtsc does.
- `import {Injectable as Component}` compiles as an `@Injectable`.
This holds for AOT compilation and decorator removal, setClassMetadata,
the `@Service` collision check, JIT, the hoisting of declarations a
decorator references, NAPI `extractComponentMetadataSync` /
`extractComponentUrls` / the pipe and class-metadata APIs, and the Vite
plugin: its quick decorator check lets `@Cmp(...)` / `@ng.Component(...)`
files through, and the HMR template/styles diff only blanks the
components the compiler compiled. The public `extract_pipe_metadata`,
`extract_injectable_metadata`, `extract_ng_module_metadata` and
`find_*_decorator_span`, which don't get the file's imports, still match
by name (`extract_pipe_metadata_in` takes them).
Not matched to ngtsc, on purpose: an aliased `@Injectable` gets a
factory (ngtsc's `needsFactory` compares the written name with
'Injectable', so its `ɵprov` points at a missing `ɵfac`).
117 unit-test sources that used `@Component` / `@Directive` without
importing it now import it from `@angular/core`.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@angular/core import, like ngtsc@angular/core import, like ngtsc
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fa0db68028
ℹ️ 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".
…like ngtsc
Checked against @angular/compiler-cli 22.1.7 (probes are in the fixture:
`signal-*`, `shorthandRef-*`):
- Signal members were compiled by the name of the function they call. So
`value = input(0)` with a local `input`, one imported from './other', an
undeclared one, or `local.input(0)` became a signal input
(`value: [1, "value"]`), a foreign `viewChild()` a signal query, and so
on, where ngtsc sees a plain property; an aliased
`import {input as inp}` was missed. Like ngtsc's
`tryParseInitializerApi`, `input` / `model` / `output` / the query
functions (`outputFromObservable` from `@angular/core/rxjs-interop`)
now count only when imported from their module by name, under any
alias, or through a namespace import (`ng.input()`), plus the
`.required` forms. This holds for the `ɵdir`/`ɵcmp` definition, the
`.d.ts`, the synthetic `setClassMetadata` / JIT `propDecorators`, and
NAPI's `extractComponentMetadataSync` / class metadata. The public
`extract_*` / `build_prop_decorators_metadata` functions, which aren't
given the file's imports, still match by name. `ng.input(0, {alias})`
and `ng.model(0, {alias})` now read their alias too.
- A transform that resolves to an imported function or a global is
emitted by the name ngtsc gives it (the reference's identity in the
file): `const transform = booleanAttribute` with `{transform}` or
`transform: t` now emits `booleanAttribute`, not `transform` / `t`;
`import {booleanAttribute as ba}` emits `ba`; and `o.t` in a helper
called with `{t: booleanAttribute}` compiles to `booleanAttribute`
instead of being reported. A value computed from an import (a call, a
member, a condition on one) is still kept as written.
- A shorthand property naming an import (`{transform}` with
`import {toNum as transform}`, `{name: 'x', alias}` with an imported
`alias`) can't be evaluated by ngtsc (TypeScript returns the alias
symbol, which has no value declaration). oxc now treats it the same
way: "Input transform must be a function" for a transform, and an
imported alias or `required` is ignored, as ngtsc does.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…port, and keep JIT's i0 free
Member decorators (`@Input`, `@Output`, `@HostBinding`, `@HostListener`,
`@ViewChild(ren)`, `@ContentChild(ren)`) were matched by name. Like ngtsc,
the compiler now only treats one imported from `@angular/core` as Angular's:
by name under any alias (`import { Input as In }`), or through a namespace
import (`@core.Input()`). A same-named decorator from another module, a local
function or an undeclared name is left alone:
- not compiled into the definition, and its options aren't checked
(`@Input({ transform: 5 })` from `./foreign` no longer reports "Input
transform must be a function")
- not removed from the class, and not listed in `setClassMetadata`
- in JIT, applied with `__decorate` instead of moved to `propDecorators`
An aliased or namespaced one is compiled, removed from the class (a
namespaced `@core.Input()` was compiled but left on the class) and, in JIT,
listed in `propDecorators` as written (`type: In`, `type: core.Input`).
The public `extract_*` functions, which don't get the file's imports, still
match by name.
JIT synthesis of signal-member decorators referenced `@angular/core` as a
new `import * as i0`, which redeclared any `i0` the file already had (a
SyntaxError; e.g. `import { input as i0 }`). Like ngtsc's `ImportManager`, it
now reuses the file's own `import * as x from '@angular/core'`, or names the
new import `i0_1`, `i0_2`, ... when `i0` is taken.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…uator, like ngtsc
A same-file value passed to a member decorator lost its alias with no
error: `const NAME = 'y'` with `@Input(NAME)` compiled to `x: "x"` (ngtsc:
`x: [0, "y", "x"]`), `@Input(OPTS)` with `const OPTS = {alias: 'y',
required: true}` dropped the alias and `required`, and `@Output(NAME)`
compiled to `e: "e"` (ngtsc: `e: "y"`). Only literals were read.
Like ngtsc's `tryParseInputFieldMapping` / `tryParseDecoratorOutput`, the
argument is now evaluated (checked against @angular/compiler-cli 22.1.7):
- `@Input`: a string (a const, `let`, template literal, `'a' + 'b'`,
`as const`, a ternary, a same-file function call, ...) is the alias; an
object (a const, a spread, `{alias: NAME}`) gives `alias`, `required`
and `transform`. This holds for the `ɵdir`/`ɵcmp` inputs, the `.d.ts`
and NAPI's `extractComponentMetadataSync`, which now also gets the
transform ngtsc emits.
- `@Output`: a string is the alias.
- ngtsc's errors: "@input can have at most one argument, got N
argument(s)" (named as written: `@In` for `import {Input as In}`),
"@input decorator argument must resolve to a string or an object
literal" (numbers, booleans, `undefined`, arrays, enum members,
`declare`d values; `null` is allowed), and the same two for `@Output`
("must resolve to a string", which also rejects `null` and objects).
- An imported `@Output(NAME)` (`ns.NAME`, `` `${NAME}` ``, a local copy)
gets the "imported from another module" error, like `@Input(OPTS)`.
The public `extract_*` functions, which don't get the file's imports,
still read literals only.
Left as before: ngtsc's "Input/Output 'y' is bound to both ..." error,
and `@Output()` on a setter (ngtsc compiles it; oxc doesn't read outputs
from methods).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…its name A global like the DOM's `atob` is now assumed to be a transform function (see the change below in the stack). Reached through a variable, a helper, an object or a condition (`const t = atob` and `transform: t`), it was emitted as written (`t`), where ngtsc emits `atob`. It now gets the same reference identity as an ES lib global like `parseInt`. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…and outputs like ngtsc `@Input() value = input(0)` compiled as a plain input with no error; ngtsc reports "Using @input with a signal input is not allowed." oxc only looked at the initializer of a member also listed in `inputs:` (for a different error), and had no such check. The same went for `@Input` on `model()`, and `@Output` on `output()`, `outputFromObservable()` or `model()`. Each is now reported on the decorator with ngtsc's message, in its order: an input's check comes before its `@Input` arguments are read, an output's after its `@Output` arguments. The call counts only when it's Angular's API by its import (any alias, a namespace, `.required`), so a local or foreign `input()` is left alone. The compiled output is unchanged, as for other errors. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…and helper-returned transforms
`@Input(LOCAL || NAME)`, `@Input({alias: LOCAL || NAME, required: true || FLAG})`,
`@Output(LOCAL || NAME)` and `transform: 0 || booleanAttribute` now compile like
ngtsc (the evaluator fix at the bottom of the stack). Also pins
`@Input(opts({t: booleanAttribute}))`, which emits `booleanAttribute` through
the reference identity this PR adds.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rom namespaces with ngtsc Arguments of member `@Input(...)` / `@Output(...)` read through `typeof NS.X` or `import S = NS.X` (a name, or options with `alias` and `required`) now evaluate like ngtsc, `NS.X` in a value position stays dynamic, and a namespace is a reference to it. Add those cases to the ngtsc snapshot. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ore import, like ngtsc
`@Component`, `@Directive`, `@Pipe`, `@Injectable` and `@NgModule` were
matched by name. Like ngtsc's `findAngularDecorator`, a class decorator
is now Angular's only when it's imported from `@angular/core`: by name
under any alias (`import {Component as Cmp}`), or through a namespace
import (`@ng.Component()`). This is the rule #504 uses for member
decorators, and the same check (`angular_core_decorator`).
Checked against @angular/compiler-cli 22.1.7 (44 probes in
tests/fixtures/class_decorators_ngtsc.json, 40 compared):
- Another library's, a local, an undeclared or a default-imported
`@Component` (and the other four) is left on the class, which isn't
compiled, and its `inputs:` aren't checked.
- One re-exported through another module (`import {Component} from
'./shared'`) is left alone too: ngtsc follows the re-export, oxc can't.
- An aliased one (`@Cmp`, `@D`, `@P`, `@M`, `@Inj`) and a namespaced
`@ng.Injectable` are now compiled; `setClassMetadata` lists them as
written (`type: Cmp`). An aliased `@Component` keeps `templateUrl` in
`setClassMetadata`, as ngtsc does.
- `import {Injectable as Component}` compiles as an `@Injectable`.
This holds for AOT compilation and decorator removal, setClassMetadata,
the `@Service` collision check, JIT, the hoisting of declarations a
decorator references, NAPI `extractComponentMetadataSync` /
`extractComponentUrls` / the pipe and class-metadata APIs, and the Vite
plugin: its quick decorator check lets `@Cmp(...)` / `@ng.Component(...)`
files through, and the HMR template/styles diff only blanks the
components the compiler compiled. The public `extract_pipe_metadata`,
`extract_injectable_metadata`, `extract_ng_module_metadata` and
`find_*_decorator_span`, which don't get the file's imports, still match
by name (`extract_pipe_metadata_in` takes them).
Not matched to ngtsc, on purpose: an aliased `@Injectable` gets a
factory (ngtsc's `needsFactory` compares the written name with
'Injectable', so its `ɵprov` points at a missing `ɵfac`).
117 unit-test sources that used `@Component` / `@Directive` without
importing it now import it from `@angular/core`.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… assuming it's a function Only an imported function named directly (`transform: fn`, or one an expression passes through unchanged, like `0 || fn`) is assumed to be a function. A value computed from an import (`!FLAG`, `make()`, `Helpers.fn`, `FLAG ? a : b`, `fn || a`, `FLAG && a`) now gets the "imported from another module" error in both `inputs:` and `@Input`, where it used to be emitted as written with no diagnostic (`!FLAG` is a boolean: a TypeError at runtime). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
fa0db68 to
3e0b07f
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3e0b07ff2c
ℹ️ 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".
…e the compiler reports
The HMR check empties the inline `template:` / `styles:` of each compiled
component and compares the rest of the file. It found the decorator by
looking for the literal `@Component(`, so a component compiled through
`import { Component as Cmp }` (`@Cmp(...)`) or `import * as ng`
(`@ng.Component(...)`) was never emptied, and an inline template or
styles edit to it reloaded the page instead of hot-replacing it.
The compiler now reports, with HMR on, each compiled component's
decorator as written (`componentDecorators`: component ID → `Component`,
`Cmp`, `ng.Component`, ...). The plugin looks for exactly that
spelling on that class, so the text scan never decides on its own which
decorators are Angular's. Only the decorator the compiler took is
emptied: another library's `@Component`, even on the same class, is left
whole and an edit to it still reloads. A spelling the scan can't match
(`@Cmp<T>(`, `@ Cmp(`, a comment before `(`) is never emptied, so edits
to it reload, as before.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ngular/core import, like ngtsc
`@Inject`, `@Optional`, `@Self`, `@SkipSelf`, `@Host` and `@Attribute`
on constructor parameters were matched by their written name. So with
`import { Inject as Inj } from '@angular/core'`, `@Inj(TOKEN)` was
ignored and then removed: the factory injected the parameter's type
instead of `TOKEN`, and `setClassMetadata` / JIT `ctorParameters`
dropped it. `@ng.Inject(TOKEN)` was ignored by the factory the same way.
And another module's `@Inject` / `@Optional` was taken as Angular's.
Like ngtsc (`isAngularCore` in `getConstructorDependencies`, and the JIT
`downlevel_decorators_transform`), a parameter decorator is now Angular's
only when it's imported from `@angular/core`: by name under any alias,
or through a namespace import. It's the same check as the class and
member decorators (`angular_core_decorator`), used for:
- the factory (`ɵfac`) of components, directives, pipes, injectables and
NgModules
- `setClassMetadata`'s `ctorParameters`, which lists Angular's
decorators as written (`type: Inj`), and gives a parameter whose
decorators are all another module's `decorators: []`, like ngtsc
- JIT `ctorParameters`
- removing the decorators, and dropping the imports only they use
Another module's or a local parameter decorator is left on the parameter
(AOT), or lowered as `__param(index, decorator)` in the class's
`__decorate` (JIT), as TypeScript does for ngtsc. Its import stays.
Checked against @angular/compiler-cli 22.1.7 and its JIT transform (33
probes in tests/fixtures/ctor_param_decorators_ngtsc.json, 29 compared):
named, aliased, namespaced, another module's, local, another module's
namespace, mixed and swapped (`import {Optional as Self}`) imports, on
`@Component`, `@Directive`, `@Injectable`, `@Pipe` and `@NgModule`.
Skipped: an aliased `@Injectable` (ngtsc gives it no factory), and
`@NgModule` with `@Inject(TOKEN)`, whose factory ignores `@Inject` on
main too.
The public `extract_*_metadata` and `build_ctor_params_metadata`, which
don't get the file's imports, still match by name
(`build_ctor_params_metadata_in` takes them).
Nine unit-test sources that used `@Inject` / `@Optional` / ... without
importing them now import them from `@angular/core`.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c2d3731798
ℹ️ 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".
…le's `@Inject` on JIT members
The hoist that moves a declaration an Angular decorator references above
the class still required `@Service` to be written as `Service`, so
`import { Service as NgService }` + `@NgService({ factory: FACTORY })` with
a later `const FACTORY` was compiled but not hoisted, and the class's
`setClassMetadata` read `FACTORY` in its TDZ. It now takes the import's
exported name, like the compile path.
JIT member decorators named like Angular's (`Inject`, `Component`,
`Optional`, ...) were dropped whatever module they came from. Like ngtsc's
JIT transform, only `@angular/core`'s are now; another module's stays a
`__decorate()` call, and `@angular/core`'s `Inject` under an alias is
treated like `@Inject`.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Stack (5/5): #493 inputs/outputs → #494 transform validation → #495
queries:→ #496.d.tstransform types → #504 signal API import identity. This is #504.Recognises signal members by the
@angular/coreimport of the function they call, like ngtsc, and emits a transform aliased through a variable the way ngtsc does.The bug
Signal members were compiled by the name of the function they call:
compiled to
inputs: { value: [1, "value"] }(a signal input). ngtsc sees a plain property here, so it emitsinputs: { value: "value" }. The same happened forlocal.input(), a foreignoutput()/model(), and a foreignviewChild()(which added a signal query). An aliasedimport { input as inp }was missed. #493 and #495 already checked the import for their new collision errors, but compilation didn't.Separately,
const transform = booleanAttributewithinputs: [{ name: 'x', transform }]emittedtransform, where ngtsc emitsbooleanAttribute.The fix
Signal APIs. One check (
initializer_api, reusing the fix(directive): compileinputs:/outputs:declared in decorator metadata #493/fix(queries): compile decoratorqueries:and match ngtsc's query options #495 import check) matches ngtsc'stryParseInitializerApi. It acceptsf()/f.required()withfimported by name from the API's module (any alias), andns.f()/ns.f.required()through a namespace import. The module is@angular/core, or@angular/core/rxjs-interopforoutputFromObservable. Every place that reads signal members uses it:.d.tssetClassMetadataand JITpropDecoratorsextractComponentMetadataSyncand class metadataThe public
extract_*andbuild_prop_decorators_metadatafunctions don't get the file's imports, so they still match by name.ng.input(0, { alias })andng.model(0, { alias })now read their alias.Transforms. A transform that resolves to an imported function or a global is emitted by its reference identity, like ngtsc's
getIdentityIn. That is the name it was first reached by in the file:booleanAttributeforconst t = booleanAttribute+transform: tor{ transform }baforimport { booleanAttribute as ba }booleanAttributeforo.tin a helper called with{ t: booleanAttribute }, which was an error beforeOnly an imported function named directly (or passed through unchanged, like
0 || fn) is assumed to be a function; a transform computed from an import (!FLAG,make(),Helpers.fn,FLAG && a,fn || a) gets the "imported from another module" error ininputs:and@Input. A shorthand naming an import ({ transform }withimport { f as transform }) can't be evaluated, as in ngtsc, so it now gets "Input transform must be a function", and an imported shorthandalias/requiredis ignored, as ngtsc does.Member decorators (
@Input…@ContentChildren) also count as Angular's only when imported from@angular/core(any alias, or a namespace import), in compilation, decorator stripping,setClassMetadataand JITpropDecorators. A foreign or undeclared one is left on the class, like ngtsc. JIT synthesis reuses the file's own@angular/corenamespace import, or names the new onei0_1,i0_2, … wheni0is already taken.Member
@Input(...)/@Output(...)arguments are evaluated like ngtsc: a same-file const, template literal or concatenation gives the alias, and a const or spread object givesalias/required/transform. Before, only literals were read and the alias was dropped silently. Bad arguments get ngtsc's "must resolve to a string (or an object literal)" and "can have at most one argument" errors, and an imported@Output(NAME)reports the "imported from another module" error, like@Input(OPTS).@Inputoninput()/model()and@Outputonoutput()/outputFromObservable()/model()report ngtsc's "Using @input with a signal input is not allowed." (and its other variants) on the decorator, in ngtsc's order. Only Angular's API counts, matched by its import. A global transform declared outside the file (const t = atob) is emitted by its name, likeparseInt.Angular's class decorators (
@Component,@Directive,@Pipe,@Injectable,@NgModule) count only when imported from@angular/core, by name under any alias or through a namespace import, like ngtsc'sisAngularCore. This is the same check member decorators use, and it applies to AOT, JIT, setClassMetadata, hoisting, NAPI metadata and the Vite plugin. A same-named decorator from another library, a local one or an undeclared one is left on the class, which isn't compiled. Like ngtsc, a decorator imported through a module that re-exports Angular's (import {Component} from './shared') isn't recognised either, so import Angular's decorators from@angular/coredirectly.Constructor parameter decorators (
@Inject,@Optional,@Self,@SkipSelf,@Host,@Attribute) also count only when imported from@angular/core, under any alias or through a namespace import. This applies to the factory,setClassMetadata, JITctorParametersand decorator removal; another module's is left on the parameter (__paramin JIT), like ngtsc. The Vite plugin's HMR template/styles diff finds each component's decorator by the spelling the compiler reports (componentDecorators), so@Cmp(...)/@ng.Component(...)components hot-reload.In JIT mode, a member decorator named like an Angular one (
@Inject,@Component,@Optional, …) counts only when imported from@angular/core; another module's stays a__decorate()call instead of being dropped. An aliased@Servicehoists the declarations it references, like@Service.Tests
signal-*: named, aliased, namespace, rxjs-interop, local, undeclared, other-module and default importsshorthandRef-*: const, let, typed, destructured, chained,as, ternary, spread and@Input(OPTS)forms, aliased imports, helpers, shorthand imports, and queriescpropDecoratorssynthesis, and the NAPIextractComponentMetadataSync. All of them failed before this fix.No test source needed new imports.
🤖 Generated with Claude Code