Skip to content

Commit 9d0fe9d

Browse files
devversionAndrewKushnir
authored andcommitted
refactor(migrations): detect if an input is not narrowed and can be migrated (angular#57308)
By default, we don't migrate inputs if they are part of e.g. `@if` for now. That is because we don't have the template narrowing feature available yet. To improve impact of the migration until we have the narrowing, we add some additional checks that allow us to migrate instances of inputs that are part of e.g. `@if` but are actually not used inside (and hence are guaranteed to be _not_ narrowed). PR Close angular#57308
1 parent 2c321a0 commit 9d0fe9d

File tree

5 files changed

+119
-81
lines changed

5 files changed

+119
-81
lines changed

packages/core/schematics/migrations/signal-migration/src/input_detection/template_reference_visitor.ts

Lines changed: 95 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import {
3838
import {KnownInputs} from './known_inputs';
3939
import {attemptRetrieveInputFromSymbol} from './nodes_to_input';
4040
import {MigrationHost} from '../migration_host';
41-
import {InputDescriptor} from '../utils/input_id';
41+
import {InputDescriptor, InputUniqueKey} from '../utils/input_id';
4242
import {InputIncompatibilityReason} from './incompatibility';
4343
import {BoundAttribute, BoundEvent} from '../../../../../../compiler/src/render3/r3_ast';
4444

@@ -51,8 +51,8 @@ export interface TmplInputExpressionReference<ExprContext> {
5151
targetInput: InputDescriptor;
5252
read: PropertyRead;
5353
context: ExprContext;
54-
isInsideNarrowingExpression: boolean;
5554
isObjectShorthandExpression: boolean;
55+
isLikelyNarrowed: boolean;
5656
}
5757

5858
/**
@@ -67,12 +67,15 @@ export class TemplateReferenceVisitor extends TmplAstRecursiveVisitor {
6767

6868
/**
6969
* Whether we are currently descending into HTML AST nodes
70-
* where bound attributes should be considered "narrowed".
70+
* where all bound attributes are considered potentially narrowing.
7171
*
72-
* TODO: Remove with: https://github.com/angular/angular/pull/55456.
72+
* Keeps track of all referenced inputs in such attribute expressions.
7373
*/
74-
private isAttributeInsideNarrowingExpression = false;
74+
private templateAttributeReferencedInputs: TmplInputExpressionReference<TmplAstNode>[] | null =
75+
null;
76+
7577
private expressionVisitor: TemplateExpressionReferenceVisitor<TmplAstNode>;
78+
private seenInputsCount = new Map<InputUniqueKey, number>();
7679

7780
constructor(
7881
host: MigrationHost,
@@ -88,16 +91,56 @@ export class TemplateReferenceVisitor extends TmplAstRecursiveVisitor {
8891
templateTypeChecker,
8992
componentClass,
9093
knownInputs,
91-
this.result,
9294
);
9395
}
9496

97+
private checkExpressionForReferencedInputs(activeNode: TmplAstNode, expressionNode: AST) {
98+
const referencedInputs = this.expressionVisitor.checkTemplateExpression(
99+
activeNode,
100+
expressionNode,
101+
);
102+
// Add all references to the overall visitor result.
103+
this.result.push(...referencedInputs);
104+
105+
// Count usages of seen input references. We'll use this to make decisions
106+
// based on whether inputs are potentially narrowed or not.
107+
for (const input of referencedInputs) {
108+
this.seenInputsCount.set(
109+
input.targetInput.key,
110+
(this.seenInputsCount.get(input.targetInput.key) ?? 0) + 1,
111+
);
112+
}
113+
114+
return referencedInputs;
115+
}
116+
117+
private descendAndCheckForNarrowedSimilarReferences(
118+
potentiallyNarrowedInputs: TmplInputExpressionReference<TmplAstNode>[],
119+
descend: () => void,
120+
) {
121+
const inputs = potentiallyNarrowedInputs.map((i) => ({
122+
ref: i,
123+
key: i.targetInput.key,
124+
pastCount: this.seenInputsCount.get(i.targetInput.key) ?? 0,
125+
}));
126+
127+
descend();
128+
129+
for (const input of inputs) {
130+
// Input was referenced inside a narrowable spot, and is used in child nodes.
131+
// This is a sign for the input to be narrowed. Mark it as such.
132+
if ((this.seenInputsCount.get(input.key) ?? 0) > input.pastCount) {
133+
input.ref.isLikelyNarrowed = true;
134+
}
135+
}
136+
}
137+
95138
override visitTemplate(template: TmplAstTemplate): void {
96139
// Note: We assume all bound expressions for templates may be subject
97140
// to TCB narrowing. This is relevant for now until we support narrowing
98141
// of signal calls in templates.
99142
// TODO: Remove with: https://github.com/angular/angular/pull/55456.
100-
this.isAttributeInsideNarrowingExpression = true;
143+
this.templateAttributeReferencedInputs = [];
101144

102145
tmplAstVisitAll(this, template.attributes);
103146
tmplAstVisitAll(this, template.templateAttrs);
@@ -110,98 +153,77 @@ export class TemplateReferenceVisitor extends TmplAstRecursiveVisitor {
110153
tmplAstVisitAll(this, template.outputs);
111154
}
112155

113-
this.isAttributeInsideNarrowingExpression = false;
156+
const referencedInputs = this.templateAttributeReferencedInputs;
157+
this.templateAttributeReferencedInputs = null;
114158

115-
tmplAstVisitAll(this, template.children);
116-
tmplAstVisitAll(this, template.references);
117-
tmplAstVisitAll(this, template.variables);
159+
this.descendAndCheckForNarrowedSimilarReferences(referencedInputs, () => {
160+
tmplAstVisitAll(this, template.children);
161+
tmplAstVisitAll(this, template.references);
162+
tmplAstVisitAll(this, template.variables);
163+
});
118164
}
119165

120166
override visitIfBlockBranch(block: TmplAstIfBlockBranch): void {
121167
if (block.expression) {
122-
this.expressionVisitor.checkTemplateExpression(
123-
block,
124-
/* isInsideNarrowingExpression */ true,
125-
block.expression,
126-
);
168+
const referencedInputs = this.checkExpressionForReferencedInputs(block, block.expression);
169+
this.descendAndCheckForNarrowedSimilarReferences(referencedInputs, () => {
170+
super.visitIfBlockBranch(block);
171+
});
172+
} else {
173+
super.visitIfBlockBranch(block);
127174
}
128-
super.visitIfBlockBranch(block);
129175
}
130176

131177
override visitForLoopBlock(block: TmplAstForLoopBlock): void {
132-
this.expressionVisitor.checkTemplateExpression(
133-
block,
134-
/* isInsideNarrowingExpression */ false,
135-
block.expression,
136-
);
137-
this.expressionVisitor.checkTemplateExpression(
138-
block,
139-
/* isInsideNarrowingExpression */ false,
140-
block.trackBy,
141-
);
178+
this.checkExpressionForReferencedInputs(block, block.expression);
179+
this.checkExpressionForReferencedInputs(block, block.trackBy);
142180
super.visitForLoopBlock(block);
143181
}
144182

145183
override visitSwitchBlock(block: TmplAstSwitchBlock): void {
146-
this.expressionVisitor.checkTemplateExpression(
147-
block,
148-
/* isInsideNarrowingExpression */ true,
149-
block.expression,
150-
);
151-
super.visitSwitchBlock(block);
184+
const referencedInputs = this.checkExpressionForReferencedInputs(block, block.expression);
185+
this.descendAndCheckForNarrowedSimilarReferences(referencedInputs, () => {
186+
super.visitSwitchBlock(block);
187+
});
152188
}
153189

154190
override visitSwitchBlockCase(block: TmplAstSwitchBlockCase): void {
155191
if (block.expression) {
156-
this.expressionVisitor.checkTemplateExpression(
157-
block,
158-
/* isInsideNarrowingExpression */ true,
159-
block.expression,
160-
);
192+
const referencedInputs = this.checkExpressionForReferencedInputs(block, block.expression);
193+
this.descendAndCheckForNarrowedSimilarReferences(referencedInputs, () => {
194+
super.visitSwitchBlockCase(block);
195+
});
196+
} else {
197+
super.visitSwitchBlockCase(block);
161198
}
162-
super.visitSwitchBlockCase(block);
163199
}
164200

165201
override visitDeferredBlock(deferred: TmplAstDeferredBlock): void {
166202
if (deferred.triggers.when) {
167-
this.expressionVisitor.checkTemplateExpression(
168-
deferred,
169-
/* isInsideNarrowingExpression */ false,
170-
deferred.triggers.when.value,
171-
);
203+
this.checkExpressionForReferencedInputs(deferred, deferred.triggers.when.value);
172204
}
173205
if (deferred.prefetchTriggers.when) {
174-
this.expressionVisitor.checkTemplateExpression(
175-
deferred,
176-
/* isInsideNarrowingExpression */ false,
177-
deferred.prefetchTriggers.when.value,
178-
);
206+
this.checkExpressionForReferencedInputs(deferred, deferred.prefetchTriggers.when.value);
179207
}
180208
super.visitDeferredBlock(deferred);
181209
}
182210

183211
override visitBoundText(text: TmplAstBoundText): void {
184-
this.expressionVisitor.checkTemplateExpression(
185-
text,
186-
/* isInsideNarrowingExpression */ false,
187-
text.value,
188-
);
212+
this.checkExpressionForReferencedInputs(text, text.value);
189213
}
190214

191215
override visitBoundEvent(attribute: TmplAstBoundEvent): void {
192-
this.expressionVisitor.checkTemplateExpression(
193-
attribute,
194-
/* isInsideNarrowingExpression */ false,
195-
attribute.handler,
196-
);
216+
this.checkExpressionForReferencedInputs(attribute, attribute.handler);
197217
}
198218

199219
override visitBoundAttribute(attribute: TmplAstBoundAttribute): void {
200-
this.expressionVisitor.checkTemplateExpression(
201-
attribute,
202-
/* isInsideNarrowingExpression */ this.isAttributeInsideNarrowingExpression,
203-
attribute.value,
204-
);
220+
const referencedInputs = this.checkExpressionForReferencedInputs(attribute, attribute.value);
221+
222+
// Attributes inside templates are potentially "narrowed" and hence we
223+
// keep track of all referenced inputs to see if they actually are.
224+
if (this.templateAttributeReferencedInputs !== null) {
225+
this.templateAttributeReferencedInputs.push(...referencedInputs);
226+
}
205227
}
206228
}
207229

@@ -214,7 +236,8 @@ export class TemplateReferenceVisitor extends TmplAstRecursiveVisitor {
214236
*/
215237
export class TemplateExpressionReferenceVisitor<ExprContext> extends RecursiveAstVisitor {
216238
private activeTmplAstNode: ExprContext | null = null;
217-
private isInsideNarrowingExpression = false;
239+
private detectedInputReferences: TmplInputExpressionReference<ExprContext>[] = [];
240+
218241
private isInsideObjectShorthandExpression = false;
219242

220243
constructor(
@@ -223,21 +246,20 @@ export class TemplateExpressionReferenceVisitor<ExprContext> extends RecursiveAs
223246
private templateTypeChecker: TemplateTypeChecker | null,
224247
private componentClass: ts.ClassDeclaration,
225248
private knownInputs: KnownInputs,
226-
private result: TmplInputExpressionReference<ExprContext>[],
227249
) {
228250
super();
229251
}
230252

231253
/** Checks the given AST expression. */
232254
checkTemplateExpression(
233255
activeNode: ExprContext,
234-
isInsideNarrowingExpression: boolean,
235256
expressionNode: AST,
236-
) {
257+
): TmplInputExpressionReference<ExprContext>[] {
258+
this.detectedInputReferences = [];
237259
this.activeTmplAstNode = activeNode;
238-
this.isInsideNarrowingExpression = isInsideNarrowingExpression;
239260

240261
expressionNode.visit(this);
262+
return this.detectedInputReferences;
241263
}
242264

243265
// Keep track when we are inside an object shorthand expression. This is
@@ -316,12 +338,12 @@ export class TemplateExpressionReferenceVisitor<ExprContext> extends RecursiveAs
316338
return null;
317339
}
318340

319-
this.result.push({
341+
this.detectedInputReferences.push({
320342
target: targetInput.descriptor.node,
321343
targetInput: targetInput.descriptor,
322344
read: ast,
323345
context: this.activeTmplAstNode!,
324-
isInsideNarrowingExpression: this.isInsideNarrowingExpression,
346+
isLikelyNarrowed: false,
325347
isObjectShorthandExpression: this.isInsideObjectShorthandExpression,
326348
});
327349

@@ -365,12 +387,12 @@ export class TemplateExpressionReferenceVisitor<ExprContext> extends RecursiveAs
365387
return null;
366388
}
367389

368-
this.result.push({
390+
this.detectedInputReferences.push({
369391
target: matchingTarget.descriptor.node,
370392
targetInput: matchingTarget.descriptor,
371393
read: ast,
372394
context: this.activeTmplAstNode!,
373-
isInsideNarrowingExpression: this.isInsideNarrowingExpression,
395+
isLikelyNarrowed: false,
374396
isObjectShorthandExpression: this.isInsideObjectShorthandExpression,
375397
});
376398
return matchingTarget.descriptor;

packages/core/schematics/migrations/signal-migration/src/passes/references/identify_host_references.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,13 +95,12 @@ export function identifyHostBindingReferences(
9595
}
9696
const hostMap = reflectObjectLiteral(hostField);
9797
const expressionResult: TmplInputExpressionReference<ts.Node>[] = [];
98-
const expressionVisitor = new TemplateExpressionReferenceVisitor(
98+
const expressionVisitor = new TemplateExpressionReferenceVisitor<ts.Node>(
9999
host,
100100
checker,
101101
null,
102102
node,
103103
knownDecoratorInputs,
104-
expressionResult,
105104
);
106105

107106
for (const [rawName, expression] of hostMap.entries()) {
@@ -158,7 +157,7 @@ export function identifyHostBindingReferences(
158157
}
159158

160159
if (parsed != null) {
161-
expressionVisitor.checkTemplateExpression(expression, false, parsed);
160+
expressionResult.push(...expressionVisitor.checkTemplateExpression(expression, parsed));
162161
}
163162
}
164163

packages/core/schematics/migrations/signal-migration/src/passes/references/identify_template_references.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,7 @@ export function identifyTemplateReferences(
5555

5656
// TODO: Remove this when we support signal narrowing in templates.
5757
// https://github.com/angular/angular/pull/55456.
58-
if (
59-
process.env['MIGRATE_NARROWED_NARROWED_IN_TEMPLATES'] !== '1' &&
60-
res.isInsideNarrowingExpression
61-
) {
58+
if (process.env['MIGRATE_NARROWED_NARROWED_IN_TEMPLATES'] !== '1' && res.isLikelyNarrowed) {
6259
knownInputs.markInputAsIncompatible(res.targetInput, {
6360
reason: InputIncompatibilityReason.NarrowedInTemplateButNotSupportedYetTODO,
6461
context: null,

packages/core/schematics/migrations/signal-migration/test/golden-test/template_ng_if.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,20 @@ import {Component, Input} from '@angular/core';
1515
<div *ngIf="third">
1616
{{third}}
1717
</div>
18+
19+
<div *ngIf="fourth">
20+
{{notTheInput}}
21+
</div>
22+
23+
@if (fifth) {
24+
{{notTheInput}}
25+
}
1826
`,
1927
})
2028
export class MyComp {
2129
@Input() first = true;
2230
@Input() second = false;
2331
@Input() third = true;
32+
@Input() fourth = true;
33+
@Input() fifth = true;
2434
}

packages/core/schematics/migrations/signal-migration/test/golden.txt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -848,7 +848,7 @@ spyOn<MyComp>(new MyComp(), 'myInput').and.returnValue();
848848

849849
// tslint:disable
850850

851-
import {Component, Input} from '@angular/core';
851+
import {Component, Input, input} from '@angular/core';
852852

853853
@Component({
854854
template: `
@@ -863,12 +863,22 @@ import {Component, Input} from '@angular/core';
863863
<div *ngIf="third">
864864
{{third}}
865865
</div>
866+
867+
<div *ngIf="fourth()">
868+
{{notTheInput}}
869+
</div>
870+
871+
@if (fifth()) {
872+
{{notTheInput}}
873+
}
866874
`,
867875
})
868876
export class MyComp {
869877
@Input() first = true;
870878
@Input() second = false;
871879
@Input() third = true;
880+
fourth = input(true);
881+
fifth = input(true);
872882
}
873883
@@@@@@ template_object_shorthand.ts @@@@@@
874884

0 commit comments

Comments
 (0)