Skip to content

Commit 4866fce

Browse files
devversionalxhub
authored andcommitted
refactor(compiler-cli): capture signal inputs in semantic graph for directives (angular#53521)
Whenever an input of a directive changes, the semantic symbol should reflect this change for the type check API. This is important because signal inputs require special output in the type checking blocks- hence we need to ensure that such type checking blocks are re-generated properly. Test verify that incremental type-checking builds work as expected now. PR Close angular#53521
1 parent e6db288 commit 4866fce

File tree

2 files changed

+161
-4
lines changed

2 files changed

+161
-4
lines changed

packages/compiler-cli/src/ngtsc/annotations/directive/src/symbol.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,8 @@ function isInputMappingEqual(current: InputMapping, previous: InputMapping): boo
9393

9494
function isInputOrOutputEqual(current: InputOrOutput, previous: InputOrOutput): boolean {
9595
return current.classPropertyName === previous.classPropertyName &&
96-
current.bindingPropertyName === previous.bindingPropertyName;
96+
current.bindingPropertyName === previous.bindingPropertyName &&
97+
current.isSignal === previous.isSignal;
9798
}
9899

99100
function isTypeCheckMetaEqual(
@@ -102,9 +103,10 @@ function isTypeCheckMetaEqual(
102103
return false;
103104
}
104105
if (current.isGeneric !== previous.isGeneric) {
105-
// Note: changes in the number of type parameters is also considered in `areTypeParametersEqual`
106-
// so this check is technically not needed; it is done anyway for completeness in terms of
107-
// whether the `DirectiveTypeCheckMeta` struct itself compares equal or not.
106+
// Note: changes in the number of type parameters is also considered in
107+
// `areTypeParametersEqual` so this check is technically not needed; it is done anyway for
108+
// completeness in terms of whether the `DirectiveTypeCheckMeta` struct itself compares
109+
// equal or not.
108110
return false;
109111
}
110112
if (!isArrayEqual(current.ngTemplateGuards, previous.ngTemplateGuards, isTemplateGuardEqual)) {

packages/compiler-cli/test/ngtsc/incremental_typecheck_spec.ts

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,161 @@ runInEachFileSystem(() => {
8080
env.driveMain();
8181
});
8282

83+
it('should type-check correctly when a backing signal input field is renamed', () => {
84+
// This test verifies that renaming the class field of an input is correctly reflected into
85+
// the TCB.
86+
env.write('dir.ts', `
87+
import {Directive, input} from '@angular/core';
88+
89+
@Directive({
90+
selector: '[dir]',
91+
})
92+
export class Dir {
93+
dir = input.required<string>();
94+
}
95+
`);
96+
env.write('cmp.ts', `
97+
import {Component} from '@angular/core';
98+
99+
@Component({
100+
selector: 'test-cmp',
101+
template: '<div [dir]="foo"></div>',
102+
})
103+
export class Cmp {
104+
foo = 'foo';
105+
}
106+
`);
107+
env.write('mod.ts', `
108+
import {NgModule} from '@angular/core';
109+
import {Cmp} from './cmp';
110+
import {Dir} from './dir';
111+
112+
@NgModule({
113+
declarations: [Cmp, Dir],
114+
})
115+
export class Mod {}
116+
`);
117+
env.driveMain();
118+
119+
// Now rename the backing field of the input; the TCB should be updated such that the `dir`
120+
// input binding is still valid.
121+
env.write('dir.ts', `
122+
import {Directive, input} from '@angular/core';
123+
124+
@Directive({
125+
selector: '[dir]',
126+
})
127+
export class Dir {
128+
dirRenamed = input.required<string>({alias: 'dir'});
129+
}
130+
`);
131+
env.driveMain();
132+
});
133+
134+
it('should type-check correctly when an decorator-input is changed to a signal input', () => {
135+
env.write('dir.ts', `
136+
import {Directive, Input} from '@angular/core';
137+
138+
@Directive({
139+
selector: '[dir]',
140+
})
141+
export class Dir {
142+
@Input() dir!: string;
143+
}
144+
`);
145+
env.write('cmp.ts', `
146+
import {Component} from '@angular/core';
147+
148+
@Component({
149+
selector: 'test-cmp',
150+
template: '<div [dir]="foo"></div>',
151+
})
152+
export class Cmp {
153+
foo = 'foo';
154+
}
155+
`);
156+
env.write('mod.ts', `
157+
import {NgModule} from '@angular/core';
158+
import {Cmp} from './cmp';
159+
import {Dir} from './dir';
160+
161+
@NgModule({
162+
declarations: [Cmp, Dir],
163+
})
164+
export class Mod {}
165+
`);
166+
env.driveMain();
167+
168+
// Now, the input is changed to a signal input. The template should still be valid.
169+
// If this `isSignal` change would not be detected, `string` would never be assignable
170+
// to `InputSignal` because the TCB would not unwrap it.
171+
env.write('dir.ts', `
172+
import {Directive, input} from '@angular/core';
173+
174+
@Directive({
175+
selector: '[dir]',
176+
})
177+
export class Dir {
178+
dir = input<string>();
179+
}
180+
`);
181+
env.driveMain();
182+
});
183+
184+
it('should type-check correctly when signal input transform is added', () => {
185+
env.write('dir.ts', `
186+
import {Directive, input} from '@angular/core';
187+
188+
@Directive({
189+
selector: '[dir]',
190+
})
191+
export class Dir {
192+
dir = input.required<string>();
193+
}
194+
`);
195+
env.write('cmp.ts', `
196+
import {Component} from '@angular/core';
197+
198+
@Component({
199+
selector: 'test-cmp',
200+
template: '<div [dir]="foo"></div>',
201+
})
202+
export class Cmp {
203+
foo = 'foo';
204+
}
205+
`);
206+
env.write('mod.ts', `
207+
import {NgModule} from '@angular/core';
208+
import {Cmp} from './cmp';
209+
import {Dir} from './dir';
210+
211+
@NgModule({
212+
declarations: [Cmp, Dir],
213+
})
214+
export class Mod {}
215+
`);
216+
env.driveMain();
217+
218+
// Transform is added and the input no longer accepts `string`, but only a boolean.
219+
// This should result in diagnostics now, assuming the TCB is checked again.
220+
env.write('dir.ts', `
221+
import {Directive, input} from '@angular/core';
222+
223+
@Directive({
224+
selector: '[dir]',
225+
})
226+
export class Dir {
227+
dir = input.required<string, boolean>({
228+
transform: v => v.toString(),
229+
});
230+
}
231+
`);
232+
const diagnostics = env.driveDiagnostics();
233+
expect(diagnostics.length).toBe(1);
234+
expect(diagnostics[0].messageText)
235+
.toBe(`Type 'string' is not assignable to type 'boolean'.`);
236+
});
237+
83238
it('should type-check correctly when a backing output field is renamed', () => {
84239
// This test verifies that renaming the class field of an output is correctly reflected into
85240
// the TCB.

0 commit comments

Comments
 (0)