@angular/core 19.0.0-next.9 → 19.0.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/fesm2022/core.mjs +21506 -19585
  2. package/fesm2022/core.mjs.map +1 -1
  3. package/fesm2022/primitives/event-dispatch.mjs +71 -47
  4. package/fesm2022/primitives/event-dispatch.mjs.map +1 -1
  5. package/fesm2022/primitives/signals.mjs +8 -6
  6. package/fesm2022/primitives/signals.mjs.map +1 -1
  7. package/fesm2022/rxjs-interop.mjs +90 -10
  8. package/fesm2022/rxjs-interop.mjs.map +1 -1
  9. package/fesm2022/testing.mjs +175 -114
  10. package/fesm2022/testing.mjs.map +1 -1
  11. package/index.d.ts +556 -89
  12. package/package.json +1 -1
  13. package/primitives/event-dispatch/index.d.ts +7 -4
  14. package/primitives/signals/index.d.ts +3 -1
  15. package/rxjs-interop/index.d.ts +35 -4
  16. package/schematics/bundles/{checker-3b2ea20f.js → checker-2451e7c5.js} +2347 -1063
  17. package/schematics/bundles/{group_replacements-e1b5cbf8.js → combine_units-c52492ab.js} +1767 -2136
  18. package/schematics/bundles/{compiler_host-b4ba5a28.js → compiler_host-f54f8309.js} +2 -2
  19. package/schematics/bundles/control-flow-migration.js +3 -3
  20. package/schematics/bundles/explicit-standalone-flag.js +31 -11
  21. package/schematics/bundles/{imports-4ac08251.js → imports-44987700.js} +1 -1
  22. package/schematics/bundles/inject-migration.js +122 -48
  23. package/schematics/bundles/{leading_space-d190b83b.js → leading_space-6e7a8ec6.js} +1 -1
  24. package/schematics/bundles/migrate_ts_type_references-ab18a7c3.js +1463 -0
  25. package/schematics/bundles/{nodes-0e7d45ca.js → ng_decorators-3ad437d2.js} +2 -15
  26. package/schematics/bundles/nodes-ffdce442.js +27 -0
  27. package/schematics/bundles/output-migration.js +7450 -0
  28. package/schematics/bundles/pending-tasks.js +5 -5
  29. package/schematics/bundles/{program-6534a30a.js → program-58424797.js} +1305 -447
  30. package/schematics/bundles/{project_tsconfig_paths-e9ccccbf.js → project_tsconfig_paths-6c9cde78.js} +1 -1
  31. package/schematics/bundles/provide-initializer.js +190 -0
  32. package/schematics/bundles/route-lazy-loading.js +4 -4
  33. package/schematics/bundles/signal-input-migration.js +184 -312
  34. package/schematics/bundles/signal-queries-migration.js +401 -143
  35. package/schematics/bundles/signals.js +54 -0
  36. package/schematics/bundles/standalone-migration.js +38 -20
  37. package/schematics/collection.json +11 -0
  38. package/schematics/migrations.json +7 -1
  39. package/schematics/ng-generate/output-migration/schema.json +19 -0
  40. package/schematics/ng-generate/signal-queries-migration/schema.json +11 -0
  41. package/schematics/ng-generate/signals/schema.json +65 -0
  42. package/testing/index.d.ts +1 -1
@@ -0,0 +1,1463 @@
1
+ 'use strict';
2
+ /**
3
+ * @license Angular v19.0.0-rc.0
4
+ * (c) 2010-2024 Google LLC. https://angular.io/
5
+ * License: MIT
6
+ */
7
+ 'use strict';
8
+
9
+ var checker = require('./checker-2451e7c5.js');
10
+ var ts = require('typescript');
11
+ require('os');
12
+ var assert = require('assert');
13
+ var combine_units = require('./combine_units-c52492ab.js');
14
+ var leading_space = require('./leading_space-6e7a8ec6.js');
15
+ require('./program-58424797.js');
16
+ require('path');
17
+
18
+ function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
19
+
20
+ var ts__default = /*#__PURE__*/_interopDefaultLegacy(ts);
21
+ var assert__default = /*#__PURE__*/_interopDefaultLegacy(assert);
22
+
23
+ /**
24
+ * Reasons why a field cannot be migrated.
25
+ *
26
+ * Higher values of incompatibility reasons indicate a more significant
27
+ * incompatibility reason. Lower ones may be overridden by higher ones.
28
+ * */
29
+ exports.FieldIncompatibilityReason = void 0;
30
+ (function (FieldIncompatibilityReason) {
31
+ FieldIncompatibilityReason[FieldIncompatibilityReason["OverriddenByDerivedClass"] = 1] = "OverriddenByDerivedClass";
32
+ FieldIncompatibilityReason[FieldIncompatibilityReason["RedeclaredViaDerivedClassInputsArray"] = 2] = "RedeclaredViaDerivedClassInputsArray";
33
+ FieldIncompatibilityReason[FieldIncompatibilityReason["TypeConflictWithBaseClass"] = 3] = "TypeConflictWithBaseClass";
34
+ FieldIncompatibilityReason[FieldIncompatibilityReason["ParentIsIncompatible"] = 4] = "ParentIsIncompatible";
35
+ FieldIncompatibilityReason[FieldIncompatibilityReason["DerivedIsIncompatible"] = 5] = "DerivedIsIncompatible";
36
+ FieldIncompatibilityReason[FieldIncompatibilityReason["SpyOnThatOverwritesField"] = 6] = "SpyOnThatOverwritesField";
37
+ FieldIncompatibilityReason[FieldIncompatibilityReason["PotentiallyNarrowedInTemplateButNoSupportYet"] = 7] = "PotentiallyNarrowedInTemplateButNoSupportYet";
38
+ FieldIncompatibilityReason[FieldIncompatibilityReason["SignalIncompatibleWithHostBinding"] = 8] = "SignalIncompatibleWithHostBinding";
39
+ FieldIncompatibilityReason[FieldIncompatibilityReason["SignalInput__RequiredButNoGoodExplicitTypeExtractable"] = 9] = "SignalInput__RequiredButNoGoodExplicitTypeExtractable";
40
+ FieldIncompatibilityReason[FieldIncompatibilityReason["SignalInput__QuestionMarkButNoGoodExplicitTypeExtractable"] = 10] = "SignalInput__QuestionMarkButNoGoodExplicitTypeExtractable";
41
+ FieldIncompatibilityReason[FieldIncompatibilityReason["SignalQueries__QueryListProblematicFieldAccessed"] = 11] = "SignalQueries__QueryListProblematicFieldAccessed";
42
+ FieldIncompatibilityReason[FieldIncompatibilityReason["SignalQueries__IncompatibleMultiUnionType"] = 12] = "SignalQueries__IncompatibleMultiUnionType";
43
+ FieldIncompatibilityReason[FieldIncompatibilityReason["WriteAssignment"] = 13] = "WriteAssignment";
44
+ FieldIncompatibilityReason[FieldIncompatibilityReason["Accessor"] = 14] = "Accessor";
45
+ FieldIncompatibilityReason[FieldIncompatibilityReason["OutsideOfMigrationScope"] = 15] = "OutsideOfMigrationScope";
46
+ FieldIncompatibilityReason[FieldIncompatibilityReason["SkippedViaConfigFilter"] = 16] = "SkippedViaConfigFilter";
47
+ })(exports.FieldIncompatibilityReason || (exports.FieldIncompatibilityReason = {}));
48
+ /** Field reasons that cannot be ignored. */
49
+ const nonIgnorableFieldIncompatibilities = [
50
+ // Outside of scope fields should not be migrated. E.g. references to inputs in `node_modules/`.
51
+ exports.FieldIncompatibilityReason.OutsideOfMigrationScope,
52
+ // Explicitly filtered fields cannot be skipped via best effort mode.
53
+ exports.FieldIncompatibilityReason.SkippedViaConfigFilter,
54
+ // There is no good output for accessor fields.
55
+ exports.FieldIncompatibilityReason.Accessor,
56
+ // There is no good output for such inputs. We can't perform "conversion".
57
+ exports.FieldIncompatibilityReason.SignalInput__RequiredButNoGoodExplicitTypeExtractable,
58
+ exports.FieldIncompatibilityReason.SignalInput__QuestionMarkButNoGoodExplicitTypeExtractable,
59
+ ];
60
+ /** Reasons why a whole class and its fields cannot be migrated. */
61
+ exports.ClassIncompatibilityReason = void 0;
62
+ (function (ClassIncompatibilityReason) {
63
+ ClassIncompatibilityReason[ClassIncompatibilityReason["ClassManuallyInstantiated"] = 0] = "ClassManuallyInstantiated";
64
+ ClassIncompatibilityReason[ClassIncompatibilityReason["OwningClassReferencedInClassProperty"] = 1] = "OwningClassReferencedInClassProperty";
65
+ })(exports.ClassIncompatibilityReason || (exports.ClassIncompatibilityReason = {}));
66
+ /** Whether the given value refers to an field incompatibility. */
67
+ function isFieldIncompatibility(value) {
68
+ return (value.reason !== undefined &&
69
+ value.context !== undefined &&
70
+ exports.FieldIncompatibilityReason.hasOwnProperty(value.reason));
71
+ }
72
+ /** Picks the more significant field compatibility. */
73
+ function pickFieldIncompatibility(a, b) {
74
+ if (b === null) {
75
+ return a;
76
+ }
77
+ if (a.reason < b.reason) {
78
+ return b;
79
+ }
80
+ return a;
81
+ }
82
+
83
+ /**
84
+ * Detects `spyOn(dirInstance, 'myInput')` calls that likely modify
85
+ * the input signal. There is no way to change the value inside the input signal,
86
+ * and hence observing is not possible.
87
+ */
88
+ class SpyOnFieldPattern {
89
+ checker;
90
+ fields;
91
+ constructor(checker, fields) {
92
+ this.checker = checker;
93
+ this.fields = fields;
94
+ }
95
+ detect(node) {
96
+ if (ts__default["default"].isCallExpression(node) &&
97
+ ts__default["default"].isIdentifier(node.expression) &&
98
+ node.expression.text === 'spyOn' &&
99
+ node.arguments.length === 2 &&
100
+ ts__default["default"].isStringLiteralLike(node.arguments[1])) {
101
+ const spyTargetType = this.checker.getTypeAtLocation(node.arguments[0]);
102
+ const spyProperty = spyTargetType.getProperty(node.arguments[1].text);
103
+ if (spyProperty === undefined) {
104
+ return;
105
+ }
106
+ const fieldTarget = this.fields.attemptRetrieveDescriptorFromSymbol(spyProperty);
107
+ if (fieldTarget === null) {
108
+ return;
109
+ }
110
+ this.fields.markFieldIncompatible(fieldTarget, {
111
+ reason: exports.FieldIncompatibilityReason.SpyOnThatOverwritesField,
112
+ context: node,
113
+ });
114
+ }
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Phase where problematic patterns are detected and advise
120
+ * the migration to skip certain inputs.
121
+ *
122
+ * For example, detects classes that are instantiated manually. Those
123
+ * cannot be migrated as `input()` requires an injection context.
124
+ *
125
+ * In addition, spying onto an input may be problematic- so we skip migrating
126
+ * such.
127
+ */
128
+ function checkIncompatiblePatterns(inheritanceGraph, checker$1, groupedTsAstVisitor, fields, getAllClassesWithKnownFields) {
129
+ const inputClassSymbolsToClass = new Map();
130
+ for (const knownFieldClass of getAllClassesWithKnownFields()) {
131
+ const classSymbol = checker$1.getTypeAtLocation(knownFieldClass).symbol;
132
+ assert__default["default"](classSymbol != null, 'Expected a symbol to exist for the container of known field class.');
133
+ assert__default["default"](classSymbol.valueDeclaration !== undefined, 'Expected declaration to exist for known field class.');
134
+ assert__default["default"](ts__default["default"].isClassDeclaration(classSymbol.valueDeclaration), 'Expected declaration to be a class.');
135
+ // track class symbol for derived class checks.
136
+ inputClassSymbolsToClass.set(classSymbol, classSymbol.valueDeclaration);
137
+ }
138
+ const spyOnPattern = new SpyOnFieldPattern(checker$1, fields);
139
+ const visitor = (node) => {
140
+ // Check for manual class instantiations.
141
+ if (ts__default["default"].isNewExpression(node) && ts__default["default"].isIdentifier(checker.unwrapExpression(node.expression))) {
142
+ let newTarget = checker$1.getSymbolAtLocation(checker.unwrapExpression(node.expression));
143
+ // Plain identifier references can point to alias symbols (e.g. imports).
144
+ if (newTarget !== undefined && newTarget.flags & ts__default["default"].SymbolFlags.Alias) {
145
+ newTarget = checker$1.getAliasedSymbol(newTarget);
146
+ }
147
+ if (newTarget && inputClassSymbolsToClass.has(newTarget)) {
148
+ fields.markClassIncompatible(inputClassSymbolsToClass.get(newTarget), exports.ClassIncompatibilityReason.ClassManuallyInstantiated);
149
+ }
150
+ }
151
+ // Detect `spyOn` problematic usages and record them.
152
+ spyOnPattern.detect(node);
153
+ const insidePropertyDeclaration = groupedTsAstVisitor.state.insidePropertyDeclaration;
154
+ // Check for problematic class references inside property declarations.
155
+ // These are likely problematic, causing type conflicts, if the containing
156
+ // class inherits a non-input member with the same name.
157
+ // Suddenly the derived class changes its signature, but the base class may not.
158
+ problematicReferencesCheck: if (insidePropertyDeclaration !== null &&
159
+ ts__default["default"].isIdentifier(node) &&
160
+ insidePropertyDeclaration.parent.heritageClauses !== undefined) {
161
+ let newTarget = checker$1.getSymbolAtLocation(checker.unwrapExpression(node));
162
+ // Plain identifier references can point to alias symbols (e.g. imports).
163
+ if (newTarget !== undefined && newTarget.flags & ts__default["default"].SymbolFlags.Alias) {
164
+ newTarget = checker$1.getAliasedSymbol(newTarget);
165
+ }
166
+ if (newTarget && inputClassSymbolsToClass.has(newTarget)) {
167
+ const memberName = combine_units.getMemberName(insidePropertyDeclaration);
168
+ if (memberName === null) {
169
+ break problematicReferencesCheck;
170
+ }
171
+ const { derivedMembers, inherited } = inheritanceGraph.checkOverlappingMembers(insidePropertyDeclaration.parent, insidePropertyDeclaration, memberName);
172
+ // Member is not inherited, or derived.
173
+ // Hence the reference is unproblematic and is expected to not
174
+ // cause any type conflicts.
175
+ if (derivedMembers.length === 0 && inherited === undefined) {
176
+ break problematicReferencesCheck;
177
+ }
178
+ fields.markClassIncompatible(inputClassSymbolsToClass.get(newTarget), exports.ClassIncompatibilityReason.OwningClassReferencedInClassProperty);
179
+ }
180
+ }
181
+ };
182
+ groupedTsAstVisitor.register(visitor);
183
+ }
184
+
185
+ /** Gets all types that are inherited (implemented or extended). */
186
+ function getInheritedTypes(node, checker) {
187
+ if (node.heritageClauses === undefined) {
188
+ return [];
189
+ }
190
+ const heritageTypes = [];
191
+ for (const heritageClause of node.heritageClauses) {
192
+ for (const typeNode of heritageClause.types) {
193
+ heritageTypes.push(checker.getTypeFromTypeNode(typeNode));
194
+ }
195
+ }
196
+ return heritageTypes;
197
+ }
198
+
199
+ /**
200
+ * Inheritance graph tracks edges between classes that describe
201
+ * heritage.
202
+ *
203
+ * This graph is helpful for efficient lookups whether e.g. an input
204
+ * is overridden, or inherited etc. This is helpful when detecting
205
+ * and propagating input incompatibility statuses.
206
+ */
207
+ class InheritanceGraph {
208
+ checker;
209
+ /** Maps nodes to their parent nodes. */
210
+ classToParents = new Map();
211
+ /** Maps nodes to their derived nodes. */
212
+ parentToChildren = new Map();
213
+ /** All classes seen participating in inheritance chains. */
214
+ allClassesInInheritance = new Set();
215
+ constructor(checker) {
216
+ this.checker = checker;
217
+ }
218
+ /** Registers a given class in the graph. */
219
+ registerClass(clazz, parents) {
220
+ this.classToParents.set(clazz, parents);
221
+ this.allClassesInInheritance.add(clazz);
222
+ for (const parent of parents) {
223
+ this.allClassesInInheritance.add(parent);
224
+ if (!this.parentToChildren.has(parent)) {
225
+ this.parentToChildren.set(parent, []);
226
+ }
227
+ this.parentToChildren.get(parent).push(clazz);
228
+ }
229
+ }
230
+ /**
231
+ * Checks if the given class has overlapping members, either
232
+ * inherited or derived.
233
+ *
234
+ * @returns Symbols of the inherited or derived members, if they exist.
235
+ */
236
+ checkOverlappingMembers(clazz, member, memberName) {
237
+ const inheritedTypes = (this.classToParents.get(clazz) ?? []).map((c) => this.checker.getTypeAtLocation(c));
238
+ const derivedLeafs = this._traceDerivedChainToLeafs(clazz).map((c) => this.checker.getTypeAtLocation(c));
239
+ const inheritedMember = inheritedTypes
240
+ .map((t) => t.getProperty(memberName))
241
+ .find((m) => m !== undefined);
242
+ const derivedMembers = derivedLeafs
243
+ .map((t) => t.getProperty(memberName))
244
+ // Skip members that point back to the current class element. The derived type
245
+ // might look up back to our starting point— which we ignore.
246
+ .filter((m) => m !== undefined && m.valueDeclaration !== member);
247
+ return { inherited: inheritedMember, derivedMembers };
248
+ }
249
+ /** Gets all leaf derived classes that extend from the given class. */
250
+ _traceDerivedChainToLeafs(clazz) {
251
+ const queue = [clazz];
252
+ const leafs = [];
253
+ while (queue.length) {
254
+ const node = queue.shift();
255
+ if (!this.parentToChildren.has(node)) {
256
+ if (node !== clazz) {
257
+ leafs.push(node);
258
+ }
259
+ continue;
260
+ }
261
+ queue.push(...this.parentToChildren.get(node));
262
+ }
263
+ return leafs;
264
+ }
265
+ /** Gets all derived classes of the given node. */
266
+ traceDerivedClasses(clazz) {
267
+ const queue = [clazz];
268
+ const derived = [];
269
+ while (queue.length) {
270
+ const node = queue.shift();
271
+ if (node !== clazz) {
272
+ derived.push(node);
273
+ }
274
+ if (!this.parentToChildren.has(node)) {
275
+ continue;
276
+ }
277
+ queue.push(...this.parentToChildren.get(node));
278
+ }
279
+ return derived;
280
+ }
281
+ /**
282
+ * Populates the graph.
283
+ *
284
+ * NOTE: This is expensive and should be called with caution.
285
+ */
286
+ expensivePopulate(files) {
287
+ for (const file of files) {
288
+ const visitor = (node) => {
289
+ if ((ts__default["default"].isClassLike(node) || ts__default["default"].isInterfaceDeclaration(node)) &&
290
+ node.heritageClauses !== undefined) {
291
+ const heritageTypes = getInheritedTypes(node, this.checker);
292
+ const parents = heritageTypes
293
+ // Interfaces participate in the graph and are not "value declarations".
294
+ // Also, symbol may be undefined for unresolvable nodes.
295
+ .map((t) => (t.symbol ? t.symbol.declarations?.[0] : undefined))
296
+ .filter((d) => d !== undefined && (ts__default["default"].isClassLike(d) || ts__default["default"].isInterfaceDeclaration(d)));
297
+ this.registerClass(node, parents);
298
+ }
299
+ ts__default["default"].forEachChild(node, visitor);
300
+ };
301
+ ts__default["default"].forEachChild(file, visitor);
302
+ }
303
+ return this;
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Class that allows for efficient grouping of TypeScript node AST
309
+ * traversal.
310
+ *
311
+ * Allows visitors to execute in a single pass when visiting all
312
+ * children of source files.
313
+ */
314
+ class GroupedTsAstVisitor {
315
+ files;
316
+ visitors = [];
317
+ doneFns = [];
318
+ constructor(files) {
319
+ this.files = files;
320
+ }
321
+ state = {
322
+ insidePropertyDeclaration: null,
323
+ };
324
+ register(visitor, done) {
325
+ this.visitors.push(visitor);
326
+ if (done !== undefined) {
327
+ this.doneFns.push(done);
328
+ }
329
+ }
330
+ execute() {
331
+ const visitor = (node) => {
332
+ for (const v of this.visitors) {
333
+ v(node);
334
+ }
335
+ if (ts__default["default"].isPropertyDeclaration(node)) {
336
+ this.state.insidePropertyDeclaration = node;
337
+ ts__default["default"].forEachChild(node, visitor);
338
+ this.state.insidePropertyDeclaration = null;
339
+ }
340
+ else {
341
+ ts__default["default"].forEachChild(node, visitor);
342
+ }
343
+ };
344
+ for (const file of this.files) {
345
+ ts__default["default"].forEachChild(file, visitor);
346
+ }
347
+ for (const doneFn of this.doneFns) {
348
+ doneFn();
349
+ }
350
+ this.visitors = [];
351
+ }
352
+ }
353
+
354
+ /**
355
+ * Phase that propagates incompatibilities to derived classes or
356
+ * base classes. For example, consider:
357
+ *
358
+ * ```
359
+ * class Base {
360
+ * bla = true;
361
+ * }
362
+ *
363
+ * class Derived extends Base {
364
+ * @Input() bla = false;
365
+ * }
366
+ * ```
367
+ *
368
+ * Whenever we migrate `Derived`, the inheritance would fail
369
+ * and result in a build breakage because `Base#bla` is not an Angular input.
370
+ *
371
+ * The logic here detects such cases and marks `bla` as incompatible. If `Derived`
372
+ * would then have other derived classes as well, it would propagate the status.
373
+ */
374
+ function checkInheritanceOfKnownFields(inheritanceGraph, metaRegistry, fields, opts) {
375
+ const allInputClasses = Array.from(inheritanceGraph.allClassesInInheritance).filter((t) => ts__default["default"].isClassDeclaration(t) && opts.isClassWithKnownFields(t));
376
+ for (const inputClass of allInputClasses) {
377
+ // Note: Class parents of `inputClass` were already checked by
378
+ // the previous iterations (given the reverse topological sort)—
379
+ // hence it's safe to assume that incompatibility of parent classes will
380
+ // not change again, at a later time.
381
+ assert__default["default"](ts__default["default"].isClassDeclaration(inputClass), 'Expected input graph node to be always a class.');
382
+ const classFields = opts.getFieldsForClass(inputClass);
383
+ // Iterate through derived class chains and determine all inputs that are overridden
384
+ // via class metadata fields. e.g `@Component#inputs`. This is later used to mark a
385
+ // potential similar class input as incompatible— because those cannot be migrated.
386
+ const inputFieldNamesFromMetadataArray = new Set();
387
+ for (const derivedClasses of inheritanceGraph.traceDerivedClasses(inputClass)) {
388
+ const derivedMeta = ts__default["default"].isClassDeclaration(derivedClasses) && derivedClasses.name !== undefined
389
+ ? metaRegistry.getDirectiveMetadata(new checker.Reference(derivedClasses))
390
+ : null;
391
+ if (derivedMeta !== null && derivedMeta.inputFieldNamesFromMetadataArray !== null) {
392
+ derivedMeta.inputFieldNamesFromMetadataArray.forEach((b) => inputFieldNamesFromMetadataArray.add(b));
393
+ }
394
+ }
395
+ // Check inheritance of every input in the given "directive class".
396
+ inputCheck: for (const fieldDescr of classFields) {
397
+ const inputNode = fieldDescr.node;
398
+ const { derivedMembers, inherited } = inheritanceGraph.checkOverlappingMembers(inputClass, inputNode, combine_units.getMemberName(inputNode));
399
+ // If we discover a derived, input re-declared via class metadata, then it
400
+ // will cause conflicts as we cannot migrate it/ nor mark it as signal-based.
401
+ if (fieldDescr.node.name !== undefined &&
402
+ (ts__default["default"].isIdentifier(fieldDescr.node.name) || ts__default["default"].isStringLiteralLike(fieldDescr.node.name)) &&
403
+ inputFieldNamesFromMetadataArray.has(fieldDescr.node.name.text)) {
404
+ fields.captureUnknownDerivedField(fieldDescr);
405
+ }
406
+ for (const derived of derivedMembers) {
407
+ const derivedInput = fields.attemptRetrieveDescriptorFromSymbol(derived);
408
+ if (derivedInput !== null) {
409
+ // Note: We always track dependencies from the child to the parent,
410
+ // so skip here for now.
411
+ continue;
412
+ }
413
+ // If we discover a derived, non-input member, then it will cause
414
+ // conflicts, and we mark the current input as incompatible.
415
+ fields.captureUnknownDerivedField(fieldDescr);
416
+ continue inputCheck;
417
+ }
418
+ // If there is no parent, we are done. Otherwise, check the parent
419
+ // to either inherit or check the incompatibility with the inheritance.
420
+ if (inherited === undefined) {
421
+ continue;
422
+ }
423
+ const inheritedMemberInput = fields.attemptRetrieveDescriptorFromSymbol(inherited);
424
+ // Parent is not an input, and hence will conflict..
425
+ if (inheritedMemberInput === null) {
426
+ fields.captureUnknownParentField(fieldDescr);
427
+ continue;
428
+ }
429
+ fields.captureKnownFieldInheritanceRelationship(fieldDescr, inheritedMemberInput);
430
+ }
431
+ }
432
+ }
433
+
434
+ function removeFromUnionIfPossible(union, filter) {
435
+ const filtered = union.types.filter(filter);
436
+ if (filtered.length === union.types.length) {
437
+ return union;
438
+ }
439
+ // If there is only item at this point, avoid the union structure.
440
+ if (filtered.length === 1) {
441
+ return filtered[0];
442
+ }
443
+ return ts__default["default"].factory.updateUnionTypeNode(union, ts__default["default"].factory.createNodeArray(filtered));
444
+ }
445
+
446
+ /**
447
+ * Inserts a leading string for the given node, respecting
448
+ * indentation of the given anchor node.
449
+ *
450
+ * Useful for inserting TODOs.
451
+ */
452
+ function insertPrecedingLine(node, info, text) {
453
+ const leadingSpace = leading_space.getLeadingLineWhitespaceOfNode(node);
454
+ return new combine_units.Replacement(combine_units.projectFile(node.getSourceFile(), info), new combine_units.TextUpdate({
455
+ position: node.getStart(),
456
+ end: node.getStart(),
457
+ toInsert: `${text}\n${leadingSpace}`,
458
+ }));
459
+ }
460
+
461
+ /**
462
+ * Cuts the given string into lines basing around the specified
463
+ * line length limit. This function breaks the string on a per-word basis.
464
+ */
465
+ function cutStringToLineLimit(str, limit) {
466
+ const words = str.split(' ');
467
+ const chunks = [];
468
+ let chunkIdx = 0;
469
+ while (words.length) {
470
+ // New line if we exceed limit.
471
+ if (chunks[chunkIdx] !== undefined && chunks[chunkIdx].length > limit) {
472
+ chunkIdx++;
473
+ }
474
+ // Ensure line is initialized for the given index.
475
+ if (chunks[chunkIdx] === undefined) {
476
+ chunks[chunkIdx] = '';
477
+ }
478
+ const word = words.shift();
479
+ const needsSpace = chunks[chunkIdx].length > 0;
480
+ // Insert word. Add space before, if the line already contains text.
481
+ chunks[chunkIdx] += `${needsSpace ? ' ' : ''}${word}`;
482
+ }
483
+ return chunks;
484
+ }
485
+
486
+ /**
487
+ * Gets human-readable message information for the given field incompatibility.
488
+ * This text will be used by the language service, or CLI-based migration.
489
+ */
490
+ function getMessageForFieldIncompatibility(reason, fieldName) {
491
+ switch (reason) {
492
+ case exports.FieldIncompatibilityReason.Accessor:
493
+ return {
494
+ short: `Accessor ${fieldName.plural} cannot be migrated as they are too complex.`,
495
+ extra: 'The migration potentially requires usage of `effect` or `computed`, but ' +
496
+ 'the intent is unclear. The migration cannot safely migrate.',
497
+ };
498
+ case exports.FieldIncompatibilityReason.OverriddenByDerivedClass:
499
+ return {
500
+ short: `The ${fieldName.single} cannot be migrated because the field is overridden by a subclass.`,
501
+ extra: 'The field in the subclass is not a signal, so migrating would break your build.',
502
+ };
503
+ case exports.FieldIncompatibilityReason.ParentIsIncompatible:
504
+ return {
505
+ short: `This ${fieldName.single} is inherited from a superclass, but the parent cannot be migrated.`,
506
+ extra: 'Migrating this field would cause your build to fail.',
507
+ };
508
+ case exports.FieldIncompatibilityReason.DerivedIsIncompatible:
509
+ return {
510
+ short: `This ${fieldName.single} cannot be migrated because the field is overridden by a subclass.`,
511
+ extra: 'The field in the subclass is incompatible for migration, so migrating this field would ' +
512
+ 'break your build.',
513
+ };
514
+ case exports.FieldIncompatibilityReason.SignalIncompatibleWithHostBinding:
515
+ return {
516
+ short: `This ${fieldName.single} is used in combination with \`@HostBinding\` and ` +
517
+ `migrating would break.`,
518
+ extra: `\`@HostBinding\` does not invoke the signal automatically and your code would. ` +
519
+ `break after migration. Use \`host\` of \`@Directive\`/\`@Component\`for host bindings.`,
520
+ };
521
+ case exports.FieldIncompatibilityReason.PotentiallyNarrowedInTemplateButNoSupportYet:
522
+ return {
523
+ short: `This ${fieldName.single} is used in a control flow expression (e.g. \`@if\` or \`*ngIf\`) and ` +
524
+ 'migrating would break narrowing currently.',
525
+ extra: `In the future, Angular intends to support narrowing of signals.`,
526
+ };
527
+ case exports.FieldIncompatibilityReason.RedeclaredViaDerivedClassInputsArray:
528
+ return {
529
+ short: `The ${fieldName.single} is overridden by a subclass that cannot be migrated.`,
530
+ extra: `The subclass overrides this ${fieldName.single} via the \`inputs\` array in @Directive/@Component. ` +
531
+ 'Migrating the field would break your build because the subclass field cannot be a signal.',
532
+ };
533
+ case exports.FieldIncompatibilityReason.SignalInput__RequiredButNoGoodExplicitTypeExtractable:
534
+ return {
535
+ short: `Input is required, but the migration cannot determine a good type for the input.`,
536
+ extra: 'Consider adding an explicit type to make the migration possible.',
537
+ };
538
+ case exports.FieldIncompatibilityReason.SignalInput__QuestionMarkButNoGoodExplicitTypeExtractable:
539
+ return {
540
+ short: `Input is marked with a question mark. Migration could not determine a good type for the input.`,
541
+ extra: 'The migration needs to be able to resolve a type, so that it can include `undefined` in your type. ' +
542
+ 'Consider adding an explicit type to make the migration possible.',
543
+ };
544
+ case exports.FieldIncompatibilityReason.SignalQueries__QueryListProblematicFieldAccessed:
545
+ return {
546
+ short: `There are references to this query that cannot be migrated automatically.`,
547
+ extra: "For example, it's not possible to migrate `.changes` or `.dirty` trivially.",
548
+ };
549
+ case exports.FieldIncompatibilityReason.SignalQueries__IncompatibleMultiUnionType:
550
+ return {
551
+ short: `Query type is too complex to automatically migrate.`,
552
+ extra: "The new query API doesn't allow us to migrate safely without breaking your app.",
553
+ };
554
+ case exports.FieldIncompatibilityReason.SkippedViaConfigFilter:
555
+ return {
556
+ short: `This ${fieldName.single} is not part of the current migration scope.`,
557
+ extra: 'Skipped via migration config.',
558
+ };
559
+ case exports.FieldIncompatibilityReason.SpyOnThatOverwritesField:
560
+ return {
561
+ short: 'A jasmine `spyOn` call spies on this field. This breaks with signals.',
562
+ extra: `Migration cannot safely migrate as "spyOn" writes to the ${fieldName.single}. ` +
563
+ `Signal ${fieldName.plural} are readonly.`,
564
+ };
565
+ case exports.FieldIncompatibilityReason.TypeConflictWithBaseClass:
566
+ return {
567
+ short: `This ${fieldName.single} overrides a field from a superclass, while the superclass ` +
568
+ `field is not migrated.`,
569
+ extra: 'Migrating the field would break your build because of a type conflict.',
570
+ };
571
+ case exports.FieldIncompatibilityReason.WriteAssignment:
572
+ return {
573
+ short: `Your application code writes to the ${fieldName.single}. This prevents migration.`,
574
+ extra: `Signal ${fieldName.plural} are readonly, so migrating would break your build.`,
575
+ };
576
+ case exports.FieldIncompatibilityReason.OutsideOfMigrationScope:
577
+ return {
578
+ short: `This ${fieldName.single} is not part of any source files in your project.`,
579
+ extra: `The migration excludes ${fieldName.plural} if no source file declaring them was seen.`,
580
+ };
581
+ }
582
+ }
583
+ /**
584
+ * Gets human-readable message information for the given class incompatibility.
585
+ * This text will be used by the language service, or CLI-based migration.
586
+ */
587
+ function getMessageForClassIncompatibility(reason, fieldName) {
588
+ switch (reason) {
589
+ case exports.ClassIncompatibilityReason.OwningClassReferencedInClassProperty:
590
+ return {
591
+ short: `Class of this ${fieldName.single} is referenced in the signature of another class.`,
592
+ extra: 'The other class is likely typed to expect a non-migrated field, so ' +
593
+ 'migration is skipped to not break your build.',
594
+ };
595
+ case exports.ClassIncompatibilityReason.ClassManuallyInstantiated:
596
+ return {
597
+ short: `Class of this ${fieldName.single} is manually instantiated. ` +
598
+ 'This is discouraged and prevents migration',
599
+ extra: `Signal ${fieldName.plural} require a DI injection context. Manually instantiating ` +
600
+ 'breaks this requirement in some cases, so the migration is skipped.',
601
+ };
602
+ }
603
+ }
604
+
605
+ /**
606
+ * Inserts a TODO for the incompatibility blocking the given node
607
+ * from being migrated.
608
+ */
609
+ function insertTodoForIncompatibility(node, programInfo, incompatibility, fieldName) {
610
+ // If a field is skipped via config filter or outside migration scope, do not
611
+ // insert TODOs, as this could results in lots of unnecessary comments.
612
+ if (isFieldIncompatibility(incompatibility) &&
613
+ (incompatibility.reason === exports.FieldIncompatibilityReason.SkippedViaConfigFilter ||
614
+ incompatibility.reason === exports.FieldIncompatibilityReason.OutsideOfMigrationScope)) {
615
+ return [];
616
+ }
617
+ const message = isFieldIncompatibility(incompatibility)
618
+ ? getMessageForFieldIncompatibility(incompatibility.reason, fieldName).short
619
+ : getMessageForClassIncompatibility(incompatibility, fieldName).short;
620
+ const lines = cutStringToLineLimit(message, 70);
621
+ return [
622
+ insertPrecedingLine(node, programInfo, `// TODO: Skipped for migration because:`),
623
+ ...lines.map((line) => insertPrecedingLine(node, programInfo, `// ${line}`)),
624
+ ];
625
+ }
626
+
627
+ /** Whether the given node is a descendant of the given ancestor. */
628
+ function isNodeDescendantOf(node, ancestor) {
629
+ while (node) {
630
+ if (node === ancestor)
631
+ return true;
632
+ node = node.parent;
633
+ }
634
+ return false;
635
+ }
636
+
637
+ /** Symbol that can be used to mark a variable as reserved, synthetically. */
638
+ const ReservedMarker = Symbol();
639
+ /**
640
+ * Gets whether the given identifier name is free for use in the
641
+ * given location, avoiding shadowed variable names.
642
+ *
643
+ */
644
+ function isIdentifierFreeInScope(name, location) {
645
+ const startContainer = findClosestParentLocalsContainer(location);
646
+ assert__default["default"](startContainer !== undefined, 'Expecting a locals container.');
647
+ // Traverse up and check for potential collisions.
648
+ let container = startContainer;
649
+ let firstNextContainer = undefined;
650
+ while (container !== undefined) {
651
+ if (!isIdentifierFreeInContainer(name, container)) {
652
+ return null;
653
+ }
654
+ if (firstNextContainer === undefined && container.nextContainer !== undefined) {
655
+ firstNextContainer = container.nextContainer;
656
+ }
657
+ container = findClosestParentLocalsContainer(container.parent);
658
+ }
659
+ // Check descendent local containers to avoid shadowing variables.
660
+ // Note that this is not strictly needed, but it's helping avoid
661
+ // some lint errors, like TSLint's no shadowed variables.
662
+ container = firstNextContainer;
663
+ while (container && isNodeDescendantOf(container, startContainer)) {
664
+ if (!isIdentifierFreeInContainer(name, container)) {
665
+ return null;
666
+ }
667
+ container = container.nextContainer;
668
+ }
669
+ return { container: startContainer };
670
+ }
671
+ /** Finds the closest parent locals container. */
672
+ function findClosestParentLocalsContainer(node) {
673
+ return ts__default["default"].findAncestor(node, isLocalsContainer);
674
+ }
675
+ /** Whether the given identifier is free in the given locals container. */
676
+ function isIdentifierFreeInContainer(name, container) {
677
+ if (container.locals === undefined || !container.locals.has(name)) {
678
+ return true;
679
+ }
680
+ // We consider alias symbols as locals conservatively.
681
+ // Note: This check is similar to the check by the TypeScript emitter.
682
+ // typescript/stable/src/compiler/emitter.ts;l=5436;rcl=651008033
683
+ const local = container.locals.get(name);
684
+ return (local !== ReservedMarker &&
685
+ !(local.flags & (ts__default["default"].SymbolFlags.Value | ts__default["default"].SymbolFlags.ExportValue | ts__default["default"].SymbolFlags.Alias)));
686
+ }
687
+ /**
688
+ * Whether the given node can contain local variables.
689
+ *
690
+ * Note: This is similar to TypeScript's `canHaveLocals` internal helper.
691
+ * typescript/stable/src/compiler/utilitiesPublic.ts;l=2265;rcl=651008033
692
+ */
693
+ function isLocalsContainer(node) {
694
+ switch (node.kind) {
695
+ case ts__default["default"].SyntaxKind.ArrowFunction:
696
+ case ts__default["default"].SyntaxKind.Block:
697
+ case ts__default["default"].SyntaxKind.CallSignature:
698
+ case ts__default["default"].SyntaxKind.CaseBlock:
699
+ case ts__default["default"].SyntaxKind.CatchClause:
700
+ case ts__default["default"].SyntaxKind.ClassStaticBlockDeclaration:
701
+ case ts__default["default"].SyntaxKind.ConditionalType:
702
+ case ts__default["default"].SyntaxKind.Constructor:
703
+ case ts__default["default"].SyntaxKind.ConstructorType:
704
+ case ts__default["default"].SyntaxKind.ConstructSignature:
705
+ case ts__default["default"].SyntaxKind.ForStatement:
706
+ case ts__default["default"].SyntaxKind.ForInStatement:
707
+ case ts__default["default"].SyntaxKind.ForOfStatement:
708
+ case ts__default["default"].SyntaxKind.FunctionDeclaration:
709
+ case ts__default["default"].SyntaxKind.FunctionExpression:
710
+ case ts__default["default"].SyntaxKind.FunctionType:
711
+ case ts__default["default"].SyntaxKind.GetAccessor:
712
+ case ts__default["default"].SyntaxKind.IndexSignature:
713
+ case ts__default["default"].SyntaxKind.JSDocCallbackTag:
714
+ case ts__default["default"].SyntaxKind.JSDocEnumTag:
715
+ case ts__default["default"].SyntaxKind.JSDocFunctionType:
716
+ case ts__default["default"].SyntaxKind.JSDocSignature:
717
+ case ts__default["default"].SyntaxKind.JSDocTypedefTag:
718
+ case ts__default["default"].SyntaxKind.MappedType:
719
+ case ts__default["default"].SyntaxKind.MethodDeclaration:
720
+ case ts__default["default"].SyntaxKind.MethodSignature:
721
+ case ts__default["default"].SyntaxKind.ModuleDeclaration:
722
+ case ts__default["default"].SyntaxKind.SetAccessor:
723
+ case ts__default["default"].SyntaxKind.SourceFile:
724
+ case ts__default["default"].SyntaxKind.TypeAliasDeclaration:
725
+ return true;
726
+ default:
727
+ return false;
728
+ }
729
+ }
730
+
731
+ /**
732
+ * Helper that can generate unique identifier names at a
733
+ * given location.
734
+ *
735
+ * Used for generating unique names to extract input reads
736
+ * to support narrowing.
737
+ */
738
+ class UniqueNamesGenerator {
739
+ fallbackSuffixes;
740
+ constructor(fallbackSuffixes) {
741
+ this.fallbackSuffixes = fallbackSuffixes;
742
+ }
743
+ generate(base, location) {
744
+ const checkNameAndClaimIfAvailable = (name) => {
745
+ const freeInfo = isIdentifierFreeInScope(name, location);
746
+ if (freeInfo === null) {
747
+ return false;
748
+ }
749
+ // Claim the locals to avoid conflicts with future generations.
750
+ freeInfo.container.locals ??= new Map();
751
+ freeInfo.container.locals.set(name, ReservedMarker);
752
+ return true;
753
+ };
754
+ // Check the base name. Ideally, we'd use this one.
755
+ if (checkNameAndClaimIfAvailable(base)) {
756
+ return base;
757
+ }
758
+ // Try any of the possible suffixes.
759
+ for (const suffix of this.fallbackSuffixes) {
760
+ const name = `${base}${suffix}`;
761
+ if (checkNameAndClaimIfAvailable(name)) {
762
+ return name;
763
+ }
764
+ }
765
+ // Worst case, suffix the base name with a unique number until
766
+ // we find an available name.
767
+ let name = null;
768
+ let counter = 1;
769
+ do {
770
+ name = `${base}_${counter++}`;
771
+ } while (!checkNameAndClaimIfAvailable(name));
772
+ return name;
773
+ }
774
+ }
775
+
776
+ /**
777
+ * Creates replacements to insert the given statement as
778
+ * first statement into the arrow function.
779
+ *
780
+ * The arrow function is converted to a block-based arrow function
781
+ * that can hold multiple statements. The original expression is
782
+ * simply returned like before.
783
+ */
784
+ function createNewBlockToInsertVariable(node, file, toInsert) {
785
+ const sf = node.getSourceFile();
786
+ // For indentation, we traverse up and find the earliest statement.
787
+ // This node is most of the time a good candidate for acceptable
788
+ // indentation of a new block.
789
+ const spacingNode = ts__default["default"].findAncestor(node, ts__default["default"].isStatement) ?? node.parent;
790
+ const { character } = ts__default["default"].getLineAndCharacterOfPosition(sf, spacingNode.getStart());
791
+ const blockSpace = ' '.repeat(character);
792
+ const contentSpace = ' '.repeat(character + 2);
793
+ return [
794
+ // Delete leading whitespace of the concise body.
795
+ new combine_units.Replacement(file, new combine_units.TextUpdate({
796
+ position: node.body.getFullStart(),
797
+ end: node.body.getStart(),
798
+ toInsert: '',
799
+ })),
800
+ // Insert leading block braces, and `toInsert` content.
801
+ // Wrap the previous expression in a return now.
802
+ new combine_units.Replacement(file, new combine_units.TextUpdate({
803
+ position: node.body.getStart(),
804
+ end: node.body.getStart(),
805
+ toInsert: ` {\n${contentSpace}${toInsert}\n${contentSpace}return `,
806
+ })),
807
+ // Add trailing brace.
808
+ new combine_units.Replacement(file, new combine_units.TextUpdate({
809
+ position: node.body.getEnd(),
810
+ end: node.body.getEnd(),
811
+ toInsert: `;\n${blockSpace}}`,
812
+ })),
813
+ ];
814
+ }
815
+
816
+ /**
817
+ * Migrates a binding element that refers to an Angular input.
818
+ *
819
+ * E.g. `const {myInput} = this`.
820
+ *
821
+ * For references in binding elements, we extract the element into a variable
822
+ * where we unwrap the input. This ensures narrowing naturally works in subsequent
823
+ * places, and we also don't need to detect potential aliases.
824
+ *
825
+ * ```ts
826
+ * const {myInput} = this;
827
+ * // turns into
828
+ * const {myInput: myInputValue} = this;
829
+ * const myInput = myInputValue();
830
+ * ```
831
+ */
832
+ function migrateBindingElementInputReference(tsReferencesInBindingElements, info, replacements, printer) {
833
+ const nameGenerator = new UniqueNamesGenerator(['Input', 'Signal', 'Ref']);
834
+ for (const reference of tsReferencesInBindingElements) {
835
+ const bindingElement = reference.parent;
836
+ const bindingDecl = combine_units.getBindingElementDeclaration(bindingElement);
837
+ const sourceFile = bindingElement.getSourceFile();
838
+ const file = combine_units.projectFile(sourceFile, info);
839
+ const inputFieldName = bindingElement.propertyName ?? bindingElement.name;
840
+ assert__default["default"](!ts__default["default"].isObjectBindingPattern(inputFieldName) && !ts__default["default"].isArrayBindingPattern(inputFieldName), 'Property of binding element cannot be another pattern.');
841
+ const tmpName = nameGenerator.generate(reference.text, bindingElement);
842
+ // Only use the temporary name, if really needed. A temporary name is needed if
843
+ // the input field simply aliased via the binding element, or if the exposed identifier
844
+ // is a string-literal like.
845
+ const useTmpNameForInputField = !ts__default["default"].isObjectBindingPattern(bindingElement.name) || !ts__default["default"].isIdentifier(inputFieldName);
846
+ const propertyName = useTmpNameForInputField ? inputFieldName : undefined;
847
+ const exposedName = useTmpNameForInputField
848
+ ? ts__default["default"].factory.createIdentifier(tmpName)
849
+ : inputFieldName;
850
+ const newBindingToAccessInputField = ts__default["default"].factory.updateBindingElement(bindingElement, bindingElement.dotDotDotToken, propertyName, exposedName, bindingElement.initializer);
851
+ const temporaryVariableReplacements = insertTemporaryVariableForBindingElement(bindingDecl, file, `const ${bindingElement.name.getText()} = ${exposedName.text}();`);
852
+ if (temporaryVariableReplacements === null) {
853
+ console.error(`Could not migrate reference ${reference.text} in ${file.rootRelativePath}`);
854
+ continue;
855
+ }
856
+ replacements.push(new combine_units.Replacement(file, new combine_units.TextUpdate({
857
+ position: bindingElement.getStart(),
858
+ end: bindingElement.getEnd(),
859
+ toInsert: printer.printNode(ts__default["default"].EmitHint.Unspecified, newBindingToAccessInputField, sourceFile),
860
+ })), ...temporaryVariableReplacements);
861
+ }
862
+ }
863
+ /**
864
+ * Inserts the given code snippet after the given variable or
865
+ * parameter declaration.
866
+ *
867
+ * If this is a parameter of an arrow function, a block may be
868
+ * added automatically.
869
+ */
870
+ function insertTemporaryVariableForBindingElement(expansionDecl, file, toInsert) {
871
+ const sf = expansionDecl.getSourceFile();
872
+ const parent = expansionDecl.parent;
873
+ // The snippet is simply inserted after the variable declaration.
874
+ // The other case of a variable declaration inside a catch clause is handled
875
+ // below.
876
+ if (ts__default["default"].isVariableDeclaration(expansionDecl) && ts__default["default"].isVariableDeclarationList(parent)) {
877
+ const leadingSpaceCount = ts__default["default"].getLineAndCharacterOfPosition(sf, parent.getStart()).character;
878
+ const leadingSpace = ' '.repeat(leadingSpaceCount);
879
+ const statement = parent.parent;
880
+ return [
881
+ new combine_units.Replacement(file, new combine_units.TextUpdate({
882
+ position: statement.getEnd(),
883
+ end: statement.getEnd(),
884
+ toInsert: `\n${leadingSpace}${toInsert}`,
885
+ })),
886
+ ];
887
+ }
888
+ // If we are dealing with a object expansion inside a parameter of
889
+ // a function-like declaration w/ block, add the variable as the first
890
+ // node inside the block.
891
+ const bodyBlock = getBodyBlockOfNode(parent);
892
+ if (bodyBlock !== null) {
893
+ const firstElementInBlock = bodyBlock.statements[0];
894
+ const spaceReferenceNode = firstElementInBlock ?? bodyBlock;
895
+ const spaceOffset = firstElementInBlock !== undefined ? 0 : 2;
896
+ const leadingSpaceCount = ts__default["default"].getLineAndCharacterOfPosition(sf, spaceReferenceNode.getStart()).character + spaceOffset;
897
+ const leadingSpace = ' '.repeat(leadingSpaceCount);
898
+ return [
899
+ new combine_units.Replacement(file, new combine_units.TextUpdate({
900
+ position: bodyBlock.getStart() + 1,
901
+ end: bodyBlock.getStart() + 1,
902
+ toInsert: `\n${leadingSpace}${toInsert}`,
903
+ })),
904
+ ];
905
+ }
906
+ // Other cases where we see an arrow function without a block.
907
+ // We need to create one now.
908
+ if (ts__default["default"].isArrowFunction(parent) && !ts__default["default"].isBlock(parent.body)) {
909
+ return createNewBlockToInsertVariable(parent, file, toInsert);
910
+ }
911
+ return null;
912
+ }
913
+ /** Gets the body block of a given node, if available. */
914
+ function getBodyBlockOfNode(node) {
915
+ if ((ts__default["default"].isMethodDeclaration(node) ||
916
+ ts__default["default"].isFunctionDeclaration(node) ||
917
+ ts__default["default"].isGetAccessorDeclaration(node) ||
918
+ ts__default["default"].isConstructorDeclaration(node) ||
919
+ ts__default["default"].isArrowFunction(node)) &&
920
+ node.body !== undefined &&
921
+ ts__default["default"].isBlock(node.body)) {
922
+ return node.body;
923
+ }
924
+ if (ts__default["default"].isCatchClause(node.parent)) {
925
+ return node.parent.block;
926
+ }
927
+ return null;
928
+ }
929
+
930
+ /**
931
+ * Whether the given node represents a control flow container boundary.
932
+ * E.g. variables cannot be narrowed when descending into children of `node`.
933
+ */
934
+ function isControlFlowBoundary(node) {
935
+ return ((ts__default["default"].isFunctionLike(node) && !getImmediatelyInvokedFunctionExpression(node)) ||
936
+ node.kind === ts__default["default"].SyntaxKind.ModuleBlock ||
937
+ node.kind === ts__default["default"].SyntaxKind.SourceFile ||
938
+ node.kind === ts__default["default"].SyntaxKind.PropertyDeclaration);
939
+ }
940
+ /** Determines the current flow container of a given node. */
941
+ function getControlFlowContainer(node) {
942
+ return ts__default["default"].findAncestor(node.parent, (node) => isControlFlowBoundary(node));
943
+ }
944
+ /** Checks whether the given node refers to an IIFE declaration. */
945
+ function getImmediatelyInvokedFunctionExpression(func) {
946
+ if (func.kind === ts__default["default"].SyntaxKind.FunctionExpression || func.kind === ts__default["default"].SyntaxKind.ArrowFunction) {
947
+ let prev = func;
948
+ let parent = func.parent;
949
+ while (parent.kind === ts__default["default"].SyntaxKind.ParenthesizedExpression) {
950
+ prev = parent;
951
+ parent = parent.parent;
952
+ }
953
+ if (parent.kind === ts__default["default"].SyntaxKind.CallExpression &&
954
+ parent.expression === prev) {
955
+ return parent;
956
+ }
957
+ }
958
+ return undefined;
959
+ }
960
+
961
+ /** @internal */
962
+ var FlowFlags;
963
+ (function (FlowFlags) {
964
+ FlowFlags[FlowFlags["Unreachable"] = 1] = "Unreachable";
965
+ FlowFlags[FlowFlags["Start"] = 2] = "Start";
966
+ FlowFlags[FlowFlags["BranchLabel"] = 4] = "BranchLabel";
967
+ FlowFlags[FlowFlags["LoopLabel"] = 8] = "LoopLabel";
968
+ FlowFlags[FlowFlags["Assignment"] = 16] = "Assignment";
969
+ FlowFlags[FlowFlags["TrueCondition"] = 32] = "TrueCondition";
970
+ FlowFlags[FlowFlags["FalseCondition"] = 64] = "FalseCondition";
971
+ FlowFlags[FlowFlags["SwitchClause"] = 128] = "SwitchClause";
972
+ FlowFlags[FlowFlags["ArrayMutation"] = 256] = "ArrayMutation";
973
+ FlowFlags[FlowFlags["Call"] = 512] = "Call";
974
+ FlowFlags[FlowFlags["ReduceLabel"] = 1024] = "ReduceLabel";
975
+ FlowFlags[FlowFlags["Referenced"] = 2048] = "Referenced";
976
+ FlowFlags[FlowFlags["Shared"] = 4096] = "Shared";
977
+ FlowFlags[FlowFlags["Label"] = 12] = "Label";
978
+ FlowFlags[FlowFlags["Condition"] = 96] = "Condition";
979
+ })(FlowFlags || (FlowFlags = {}));
980
+
981
+ /**
982
+ * Traverses the graph of the TypeScript flow nodes, exploring all possible branches
983
+ * and keeps track of interesting nodes that may contribute to "narrowing".
984
+ *
985
+ * This allows us to figure out which nodes may be narrowed or not, and need
986
+ * temporary variables in the migration to allowing narrowing to continue working.
987
+ *
988
+ * Some resources on flow nodes by TypeScript:
989
+ * https://effectivetypescript.com/2024/03/24/flownodes/.
990
+ */
991
+ function traverseFlowForInterestingNodes(flow) {
992
+ let flowDepth = 0;
993
+ let interestingNodes = [];
994
+ const queue = new Set([flow]);
995
+ // Queue is evolved during iteration, and new items will be added
996
+ // to the end of the iteration. Effectively implementing a queue
997
+ // with deduping out of the box.
998
+ for (const flow of queue) {
999
+ if (++flowDepth === 2000) {
1000
+ // We have made 2000 recursive invocations. To avoid overflowing the call stack we report an
1001
+ // error and disable further control flow analysis in the containing function or module body.
1002
+ return interestingNodes;
1003
+ }
1004
+ const flags = flow.flags;
1005
+ if (flags & FlowFlags.Assignment) {
1006
+ const assignment = flow;
1007
+ queue.add(assignment.antecedent);
1008
+ if (ts__default["default"].isVariableDeclaration(assignment.node)) {
1009
+ interestingNodes.push(assignment.node.name);
1010
+ }
1011
+ else if (ts__default["default"].isBindingElement(assignment.node)) {
1012
+ interestingNodes.push(assignment.node.name);
1013
+ }
1014
+ else {
1015
+ interestingNodes.push(assignment.node);
1016
+ }
1017
+ }
1018
+ else if (flags & FlowFlags.Call) {
1019
+ queue.add(flow.antecedent);
1020
+ // Arguments can be narrowed using `FlowCall`s.
1021
+ // See: node_modules/typescript/stable/src/compiler/checker.ts;l=28786-28810
1022
+ interestingNodes.push(...flow.node.arguments);
1023
+ }
1024
+ else if (flags & FlowFlags.Condition) {
1025
+ queue.add(flow.antecedent);
1026
+ interestingNodes.push(flow.node);
1027
+ }
1028
+ else if (flags & FlowFlags.SwitchClause) {
1029
+ queue.add(flow.antecedent);
1030
+ // The switch expression can be narrowed, so it's an interesting node.
1031
+ interestingNodes.push(flow.node.switchStatement.expression);
1032
+ }
1033
+ else if (flags & FlowFlags.Label) {
1034
+ // simple label, a single ancestor.
1035
+ if (flow.antecedent?.length === 1) {
1036
+ queue.add(flow.antecedent[0]);
1037
+ continue;
1038
+ }
1039
+ if (flags & FlowFlags.BranchLabel) {
1040
+ // Normal branches. e.g. switch.
1041
+ for (const f of flow.antecedent ?? []) {
1042
+ queue.add(f);
1043
+ }
1044
+ }
1045
+ else {
1046
+ // Branch for loops.
1047
+ // The first antecedent always points to the flow node before the loop
1048
+ // was entered. All other narrowing expressions, if present, are direct
1049
+ // antecedents of the starting flow node, so we only need to look at the first.
1050
+ // See: node_modules/typescript/stable/src/compiler/checker.ts;l=28108-28109
1051
+ queue.add(flow.antecedent[0]);
1052
+ }
1053
+ }
1054
+ else if (flags & FlowFlags.ArrayMutation) {
1055
+ queue.add(flow.antecedent);
1056
+ // Array mutations are never interesting for inputs, as we cannot migrate
1057
+ // assignments to inputs.
1058
+ }
1059
+ else if (flags & FlowFlags.ReduceLabel) {
1060
+ // reduce label is a try/catch re-routing.
1061
+ // visit all possible branches.
1062
+ // TODO: explore this more.
1063
+ // See: node_modules/typescript/stable/src/compiler/binder.ts;l=1636-1649.
1064
+ queue.add(flow.antecedent);
1065
+ for (const f of flow.node.antecedents) {
1066
+ queue.add(f);
1067
+ }
1068
+ }
1069
+ else if (flags & FlowFlags.Start) {
1070
+ // Note: TS itself only ever continues with parent control flows, if the pre-determined `flowContainer`
1071
+ // of the referenced is different. E.g. narrowing might decide to choose a higher flow container if we
1072
+ // reference a constant. In which case, TS allows escaping the flow container for narrowing. See:
1073
+ // http://google3/third_party/javascript/node_modules/typescript/stable/src/compiler/checker.ts;l=29399-29414;rcl=623599846.
1074
+ // and TypeScript's `narrowedConstInMethod` baseline test.
1075
+ // --> We don't need this as an input cannot be a constant!
1076
+ return interestingNodes;
1077
+ }
1078
+ else {
1079
+ break;
1080
+ }
1081
+ }
1082
+ return null;
1083
+ }
1084
+ /** Gets the flow node for the given node. */
1085
+ function getFlowNode(node) {
1086
+ return node.flowNode ?? null;
1087
+ }
1088
+
1089
+ /**
1090
+ * Analyzes the control flow of a list of references and returns
1091
+ * information about which nodes can be shared via a temporary variable
1092
+ * to enable narrowing.
1093
+ *
1094
+ * E.g. consider the following snippet:
1095
+ *
1096
+ * ```
1097
+ * someMethod() {
1098
+ * if (this.bla) {
1099
+ * this.bla.charAt(0);
1100
+ * }
1101
+ * }
1102
+ * ```
1103
+ *
1104
+ * The analysis would inform the caller that `this.bla.charAt` can
1105
+ * be shared with the `this.bla` of the `if` condition.
1106
+ *
1107
+ * This is useful for the signal migration as it allows us to efficiently,
1108
+ * and minimally transform references into shared variables where needed.
1109
+ * Needed because signals are not narrowable by default, as they are functions.
1110
+ */
1111
+ function analyzeControlFlow(entries, checker) {
1112
+ const result = [];
1113
+ const referenceToMetadata = new Map();
1114
+ // Prepare easy lookups for reference nodes to flow info.
1115
+ for (const [idx, entry] of entries.entries()) {
1116
+ referenceToMetadata.set(entry, {
1117
+ flowContainer: getControlFlowContainer(entry),
1118
+ resultIndex: idx,
1119
+ });
1120
+ }
1121
+ for (const entry of entries) {
1122
+ const { flowContainer, resultIndex } = referenceToMetadata.get(entry);
1123
+ const flowPathInterestingNodes = traverseFlowForInterestingNodes(getFlowNode(entry));
1124
+ assert__default["default"](flowContainer !== null && flowPathInterestingNodes !== null, 'Expected a flow container to exist.');
1125
+ const narrowPartners = getAllMatchingReferencesInFlowPath(flowPathInterestingNodes, entry, referenceToMetadata, flowContainer, checker);
1126
+ result.push({
1127
+ id: resultIndex,
1128
+ originalNode: entry,
1129
+ flowContainer,
1130
+ recommendedNode: 'preserve',
1131
+ });
1132
+ if (narrowPartners.length !== 0) {
1133
+ connectSharedReferences(result, narrowPartners, resultIndex);
1134
+ }
1135
+ }
1136
+ return result;
1137
+ }
1138
+ /**
1139
+ * Iterates through all partner flow nodes and connects them so that
1140
+ * the first node will act as the share partner, while all subsequent
1141
+ * nodes will point to the share node.
1142
+ */
1143
+ function connectSharedReferences(result, flowPartners, refId) {
1144
+ const refFlowContainer = result[refId].flowContainer;
1145
+ // Inside the list of flow partners (i.e. references to the same target),
1146
+ // find the node that is the first one in the flow container (via its start pos).
1147
+ let earliestPartner = null;
1148
+ let earliestPartnerId = null;
1149
+ for (const partnerId of flowPartners) {
1150
+ if (earliestPartner === null ||
1151
+ result[partnerId].originalNode.getStart() < earliestPartner.getStart()) {
1152
+ earliestPartner = result[partnerId].originalNode;
1153
+ earliestPartnerId = partnerId;
1154
+ }
1155
+ }
1156
+ assert__default["default"](earliestPartner !== null, 'Expected an earliest partner to be found.');
1157
+ assert__default["default"](earliestPartnerId !== null, 'Expected an earliest partner to be found.');
1158
+ // Then, incorporate all similar references (or flow nodes) in between
1159
+ // the reference and the earliest partner. References in between can also
1160
+ // use the shared flow node and not preserve their original reference— as
1161
+ // this would be rather unreadable and inefficient.
1162
+ const seenBlocks = new Set();
1163
+ let highestBlock = null;
1164
+ for (let i = earliestPartnerId; i <= refId; i++) {
1165
+ // Different flow container captured sequentially in result. Ignore.
1166
+ if (result[i].flowContainer !== refFlowContainer) {
1167
+ continue;
1168
+ }
1169
+ // Iterate up the block, find the highest block within the flow container.
1170
+ let current = result[i].originalNode.parent;
1171
+ while (current !== undefined && !ts__default["default"].isSourceFile(current)) {
1172
+ if (isBlockLikeAncestor(current)) {
1173
+ // If we saw this block already, it is a common ancestor from another
1174
+ // partner. Check if it would be higher than the current highest block;
1175
+ // and choose it accordingly.
1176
+ if (seenBlocks.has(current)) {
1177
+ if (highestBlock === null || current.getStart() < highestBlock.getStart()) {
1178
+ highestBlock = current;
1179
+ }
1180
+ break;
1181
+ }
1182
+ seenBlocks.add(current);
1183
+ }
1184
+ current = current.parent;
1185
+ }
1186
+ if (i !== earliestPartnerId) {
1187
+ result[i].recommendedNode = earliestPartnerId;
1188
+ }
1189
+ }
1190
+ assert__default["default"](highestBlock, 'Expected a block anchor to be found');
1191
+ result[earliestPartnerId].recommendedNode = highestBlock;
1192
+ }
1193
+ function isBlockLikeAncestor(node) {
1194
+ // Note: Arrow functions may not have a block, but instead use an expression
1195
+ // directly. This still signifies a "block" as we can convert the concise body
1196
+ // to a block.
1197
+ return ts__default["default"].isSourceFile(node) || ts__default["default"].isBlock(node) || ts__default["default"].isArrowFunction(node);
1198
+ }
1199
+ /**
1200
+ * Looks through the flow path and interesting nodes to determine which
1201
+ * of the potential "interesting" nodes point to the same reference.
1202
+ *
1203
+ * These nodes are then considered "partners" and will be returned via
1204
+ * their IDs (or practically their result indices).
1205
+ */
1206
+ function getAllMatchingReferencesInFlowPath(flowPathInterestingNodes, reference, referenceToMetadata, restrainingFlowContainer, checker) {
1207
+ const partners = [];
1208
+ for (const flowNode of flowPathInterestingNodes) {
1209
+ // quick naive perf-optimized check to see if the flow node has a potential
1210
+ // similar reference.
1211
+ if (!flowNode.getText().includes(reference.getText())) {
1212
+ continue;
1213
+ }
1214
+ const similarRefNodeId = findSimilarReferenceNode(flowNode, reference, referenceToMetadata, restrainingFlowContainer, checker);
1215
+ if (similarRefNodeId !== null) {
1216
+ partners.push(similarRefNodeId);
1217
+ }
1218
+ }
1219
+ return partners;
1220
+ }
1221
+ /**
1222
+ * Checks if the given node contains an identifier that
1223
+ * matches the given reference. If so, returns its flow ID.
1224
+ */
1225
+ function findSimilarReferenceNode(start, reference, referenceToMetadata, restrainingFlowContainer, checker) {
1226
+ return (ts__default["default"].forEachChild(start, function visitChild(node) {
1227
+ // do not descend into control flow boundaries.
1228
+ // only references sharing the same container are relevant.
1229
+ // This is a performance optimization.
1230
+ if (isControlFlowBoundary(node)) {
1231
+ return;
1232
+ }
1233
+ // If this is not a potential matching identifier, check its children.
1234
+ if (!ts__default["default"].isIdentifier(node) ||
1235
+ referenceToMetadata.get(node)?.flowContainer !== restrainingFlowContainer) {
1236
+ return ts__default["default"].forEachChild(node, visitChild);
1237
+ }
1238
+ // If this refers to a different instantiation of the input reference,
1239
+ // continue looking.
1240
+ if (!isLexicalSameReference(checker, node, reference)) {
1241
+ return;
1242
+ }
1243
+ return { idx: referenceToMetadata.get(node).resultIndex };
1244
+ })?.idx ?? null);
1245
+ }
1246
+ /**
1247
+ * Checks whether a given identifier is lexically equivalent.
1248
+ * e.g. checks that they have similar property receiver accesses.
1249
+ */
1250
+ function isLexicalSameReference(checker, sharePartner, reference) {
1251
+ const aParent = combine_units.unwrapParent(reference.parent);
1252
+ // If the reference is not part a property access, return true. The references
1253
+ // are guaranteed symbol matches.
1254
+ if (!ts__default["default"].isPropertyAccessExpression(aParent) && !ts__default["default"].isElementAccessExpression(aParent)) {
1255
+ return sharePartner.text === reference.text;
1256
+ }
1257
+ // If reference parent is part of a property expression, but the share
1258
+ // partner not, then this cannot be shared.
1259
+ const bParent = combine_units.unwrapParent(sharePartner.parent);
1260
+ if (aParent.kind !== bParent.kind) {
1261
+ return false;
1262
+ }
1263
+ const aParentExprSymbol = checker.getSymbolAtLocation(aParent.expression);
1264
+ const bParentExprSymbol = checker.getSymbolAtLocation(bParent.expression);
1265
+ return aParentExprSymbol === bParentExprSymbol;
1266
+ }
1267
+
1268
+ function migrateStandardTsReference(tsReferencesWithNarrowing, checker, info, replacements) {
1269
+ const nameGenerator = new UniqueNamesGenerator(['Value', 'Val', 'Input']);
1270
+ // TODO: Consider checking/properly handling optional chaining and narrowing.
1271
+ for (const reference of tsReferencesWithNarrowing.values()) {
1272
+ const controlFlowResult = analyzeControlFlow(reference.accesses, checker);
1273
+ const idToSharedField = new Map();
1274
+ for (const { id, originalNode, recommendedNode } of controlFlowResult) {
1275
+ const sf = originalNode.getSourceFile();
1276
+ // Original node is preserved. No narrowing, and hence not shared.
1277
+ // Unwrap the signal directly.
1278
+ if (recommendedNode === 'preserve') {
1279
+ // Append `()` to unwrap the signal.
1280
+ replacements.push(new combine_units.Replacement(combine_units.projectFile(sf, info), new combine_units.TextUpdate({
1281
+ position: originalNode.getEnd(),
1282
+ end: originalNode.getEnd(),
1283
+ toInsert: '()',
1284
+ })));
1285
+ continue;
1286
+ }
1287
+ // This reference is shared with a previous reference. Replace the access
1288
+ // with the temporary variable.
1289
+ if (typeof recommendedNode === 'number') {
1290
+ const replaceNode = combine_units.traverseAccess(originalNode);
1291
+ replacements.push(new combine_units.Replacement(combine_units.projectFile(sf, info), new combine_units.TextUpdate({
1292
+ position: replaceNode.getStart(),
1293
+ end: replaceNode.getEnd(),
1294
+ // Extract the shared field name.
1295
+ toInsert: idToSharedField.get(recommendedNode),
1296
+ })));
1297
+ continue;
1298
+ }
1299
+ // Otherwise, we are creating a "shared reference" at the given node and
1300
+ // block.
1301
+ // Iterate up the original node, until we hit the "recommended block" level.
1302
+ // We then use the previous child as anchor for inserting. This allows us
1303
+ // to insert right before the first reference in the container, at the proper
1304
+ // block level— instead of always inserting at the beginning of the container.
1305
+ let parent = originalNode.parent;
1306
+ let referenceNodeInBlock = originalNode;
1307
+ while (parent !== recommendedNode) {
1308
+ referenceNodeInBlock = parent;
1309
+ parent = parent.parent;
1310
+ }
1311
+ const replaceNode = combine_units.traverseAccess(originalNode);
1312
+ const fieldName = nameGenerator.generate(originalNode.text, referenceNodeInBlock);
1313
+ const filePath = combine_units.projectFile(sf, info);
1314
+ const temporaryVariableStr = `const ${fieldName} = ${replaceNode.getText()}();`;
1315
+ idToSharedField.set(id, fieldName);
1316
+ // If the common ancestor block of all shared references is an arrow function
1317
+ // without a block, convert the arrow function to a block and insert the temporary
1318
+ // variable at the beginning.
1319
+ if (ts__default["default"].isArrowFunction(parent) && !ts__default["default"].isBlock(parent.body)) {
1320
+ replacements.push(...createNewBlockToInsertVariable(parent, filePath, temporaryVariableStr));
1321
+ }
1322
+ else {
1323
+ const leadingSpace = ts__default["default"].getLineAndCharacterOfPosition(sf, referenceNodeInBlock.getStart());
1324
+ replacements.push(new combine_units.Replacement(filePath, new combine_units.TextUpdate({
1325
+ position: referenceNodeInBlock.getStart(),
1326
+ end: referenceNodeInBlock.getStart(),
1327
+ toInsert: `${temporaryVariableStr}\n${' '.repeat(leadingSpace.character)}`,
1328
+ })));
1329
+ }
1330
+ replacements.push(new combine_units.Replacement(combine_units.projectFile(sf, info), new combine_units.TextUpdate({
1331
+ position: replaceNode.getStart(),
1332
+ end: replaceNode.getEnd(),
1333
+ toInsert: fieldName,
1334
+ })));
1335
+ }
1336
+ }
1337
+ }
1338
+
1339
+ /**
1340
+ * Migrates TypeScript input references to be signal compatible.
1341
+ *
1342
+ * The phase takes care of control flow analysis and generates temporary variables
1343
+ * where needed to ensure narrowing continues to work. E.g.
1344
+ *
1345
+ * ```
1346
+ * someMethod() {
1347
+ * if (this.input) {
1348
+ * this.input.charAt(0);
1349
+ * }
1350
+ * }
1351
+ * ```
1352
+ *
1353
+ * will be transformed into:
1354
+ *
1355
+ * ```
1356
+ * someMethod() {
1357
+ * const input_1 = this.input();
1358
+ * if (input_1) {
1359
+ * input_1.charAt(0);
1360
+ * }
1361
+ * }
1362
+ * ```
1363
+ */
1364
+ function migrateTypeScriptReferences(host, references, checker, info) {
1365
+ const tsReferencesWithNarrowing = new Map();
1366
+ const tsReferencesInBindingElements = new Set();
1367
+ const seenIdentifiers = new WeakSet();
1368
+ for (const reference of references) {
1369
+ // This pass only deals with TS references.
1370
+ if (!combine_units.isTsReference(reference)) {
1371
+ continue;
1372
+ }
1373
+ // Skip references to incompatible inputs.
1374
+ if (!host.shouldMigrateReferencesToField(reference.target)) {
1375
+ continue;
1376
+ }
1377
+ // Never attempt to migrate write references.
1378
+ // Those usually invalidate the target input most of the time, but in
1379
+ // best-effort mode they are not.
1380
+ if (reference.from.isWrite) {
1381
+ continue;
1382
+ }
1383
+ // Skip duplicate references. E.g. in batching.
1384
+ if (seenIdentifiers.has(reference.from.node)) {
1385
+ continue;
1386
+ }
1387
+ seenIdentifiers.add(reference.from.node);
1388
+ const targetKey = reference.target.key;
1389
+ if (reference.from.isPartOfElementBinding) {
1390
+ tsReferencesInBindingElements.add(reference.from.node);
1391
+ }
1392
+ else {
1393
+ if (!tsReferencesWithNarrowing.has(targetKey)) {
1394
+ tsReferencesWithNarrowing.set(targetKey, { accesses: [] });
1395
+ }
1396
+ tsReferencesWithNarrowing.get(targetKey).accesses.push(reference.from.node);
1397
+ }
1398
+ }
1399
+ migrateBindingElementInputReference(tsReferencesInBindingElements, info, host.replacements, host.printer);
1400
+ migrateStandardTsReference(tsReferencesWithNarrowing, checker, info, host.replacements);
1401
+ }
1402
+
1403
+ /**
1404
+ * Migrates TypeScript "ts.Type" references. E.g.
1405
+
1406
+ * - `Partial<MyComp>` will be converted to `UnwrapSignalInputs<Partial<MyComp>>`.
1407
+ in Catalyst test files.
1408
+ */
1409
+ function migrateTypeScriptTypeReferences(host, references, importManager, info) {
1410
+ const seenTypeNodes = new WeakSet();
1411
+ for (const reference of references) {
1412
+ // This pass only deals with TS input class type references.
1413
+ if (!combine_units.isTsClassTypeReference(reference)) {
1414
+ continue;
1415
+ }
1416
+ // Skip references to classes that are not fully migrated.
1417
+ if (!host.shouldMigrateReferencesToClass(reference.target)) {
1418
+ continue;
1419
+ }
1420
+ // Skip duplicate references. E.g. in batching.
1421
+ if (seenTypeNodes.has(reference.from.node)) {
1422
+ continue;
1423
+ }
1424
+ seenTypeNodes.add(reference.from.node);
1425
+ if (reference.isPartialReference && reference.isPartOfCatalystFile) {
1426
+ assert__default["default"](reference.from.node.typeArguments, 'Expected type arguments for partial reference.');
1427
+ assert__default["default"](reference.from.node.typeArguments.length === 1, 'Expected an argument for reference.');
1428
+ const firstArg = reference.from.node.typeArguments[0];
1429
+ const sf = firstArg.getSourceFile();
1430
+ // Naive detection of the import. Sufficient for this test file migration.
1431
+ const catalystImport = sf.text.includes('google3/javascript/angular2/testing/catalyst/fake_async')
1432
+ ? 'google3/javascript/angular2/testing/catalyst/fake_async'
1433
+ : 'google3/javascript/angular2/testing/catalyst/async';
1434
+ const unwrapImportExpr = importManager.addImport({
1435
+ exportModuleSpecifier: catalystImport,
1436
+ exportSymbolName: 'UnwrapSignalInputs',
1437
+ requestedFile: sf,
1438
+ });
1439
+ host.replacements.push(new combine_units.Replacement(combine_units.projectFile(sf, info), new combine_units.TextUpdate({
1440
+ position: firstArg.getStart(),
1441
+ end: firstArg.getStart(),
1442
+ toInsert: `${host.printer.printNode(ts__default["default"].EmitHint.Unspecified, unwrapImportExpr, sf)}<`,
1443
+ })));
1444
+ host.replacements.push(new combine_units.Replacement(combine_units.projectFile(sf, info), new combine_units.TextUpdate({ position: firstArg.getEnd(), end: firstArg.getEnd(), toInsert: '>' })));
1445
+ }
1446
+ }
1447
+ }
1448
+
1449
+ exports.GroupedTsAstVisitor = GroupedTsAstVisitor;
1450
+ exports.InheritanceGraph = InheritanceGraph;
1451
+ exports.checkIncompatiblePatterns = checkIncompatiblePatterns;
1452
+ exports.checkInheritanceOfKnownFields = checkInheritanceOfKnownFields;
1453
+ exports.cutStringToLineLimit = cutStringToLineLimit;
1454
+ exports.getMessageForClassIncompatibility = getMessageForClassIncompatibility;
1455
+ exports.getMessageForFieldIncompatibility = getMessageForFieldIncompatibility;
1456
+ exports.insertPrecedingLine = insertPrecedingLine;
1457
+ exports.insertTodoForIncompatibility = insertTodoForIncompatibility;
1458
+ exports.isFieldIncompatibility = isFieldIncompatibility;
1459
+ exports.migrateTypeScriptReferences = migrateTypeScriptReferences;
1460
+ exports.migrateTypeScriptTypeReferences = migrateTypeScriptTypeReferences;
1461
+ exports.nonIgnorableFieldIncompatibilities = nonIgnorableFieldIncompatibilities;
1462
+ exports.pickFieldIncompatibility = pickFieldIncompatibility;
1463
+ exports.removeFromUnionIfPossible = removeFromUnionIfPossible;