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

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 +21183 -19410
  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 +174 -113
  10. package/fesm2022/testing.mjs.map +1 -1
  11. package/index.d.ts +524 -92
  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-9ca42e51.js} +2303 -1006
  17. package/schematics/bundles/combine_units-a16385aa.js +1634 -0
  18. package/schematics/bundles/{compiler_host-b4ba5a28.js → compiler_host-31afa4ed.js} +8 -5
  19. package/schematics/bundles/control-flow-migration.js +3 -3
  20. package/schematics/bundles/explicit-standalone-flag.js +29 -9
  21. package/schematics/bundles/imports-4ac08251.js +1 -1
  22. package/schematics/bundles/inject-migration.js +222 -54
  23. package/schematics/bundles/leading_space-d190b83b.js +1 -1
  24. package/schematics/bundles/migrate_ts_type_references-b2a28742.js +1463 -0
  25. package/schematics/bundles/nodes-0e7d45ca.js +1 -1
  26. package/schematics/bundles/output-migration.js +575 -0
  27. package/schematics/bundles/pending-tasks.js +3 -3
  28. package/schematics/bundles/{program-6534a30a.js → program-71beec0b.js} +1385 -460
  29. package/schematics/bundles/project_tsconfig_paths-e9ccccbf.js +1 -1
  30. package/schematics/bundles/provide-initializer.js +179 -0
  31. package/schematics/bundles/route-lazy-loading.js +9 -4
  32. package/schematics/bundles/signal-input-migration.js +183 -311
  33. package/schematics/bundles/signal-queries-migration.js +404 -146
  34. package/schematics/bundles/signals.js +54 -0
  35. package/schematics/bundles/standalone-migration.js +29 -12
  36. package/schematics/collection.json +11 -0
  37. package/schematics/migrations.json +7 -1
  38. package/schematics/ng-generate/output-migration/schema.json +19 -0
  39. package/schematics/ng-generate/signal-queries-migration/schema.json +11 -0
  40. package/schematics/ng-generate/signals/schema.json +65 -0
  41. package/testing/index.d.ts +1 -1
  42. package/schematics/bundles/group_replacements-e1b5cbf8.js +0 -31571
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
  /**
3
- * @license Angular v19.0.0-next.9
3
+ * @license Angular v19.0.0-rc.1
4
4
  * (c) 2010-2024 Google LLC. https://angular.io/
5
5
  * License: MIT
6
6
  */
@@ -10,18 +10,20 @@ Object.defineProperty(exports, '__esModule', { value: true });
10
10
 
11
11
  var schematics = require('@angular-devkit/schematics');
12
12
  var project_tsconfig_paths = require('./project_tsconfig_paths-e9ccccbf.js');
13
- var group_replacements = require('./group_replacements-e1b5cbf8.js');
13
+ var combine_units = require('./combine_units-a16385aa.js');
14
14
  require('os');
15
15
  var ts = require('typescript');
16
- var checker = require('./checker-3b2ea20f.js');
17
- var program = require('./program-6534a30a.js');
18
- require('path');
16
+ var checker = require('./checker-9ca42e51.js');
17
+ var program = require('./program-71beec0b.js');
19
18
  var assert = require('assert');
19
+ require('path');
20
+ var migrate_ts_type_references = require('./migrate_ts_type_references-b2a28742.js');
20
21
  require('@angular-devkit/core');
21
22
  require('node:path/posix');
22
23
  require('fs');
23
24
  require('module');
24
25
  require('url');
26
+ require('./leading_space-d190b83b.js');
25
27
 
26
28
  function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
27
29
 
@@ -36,7 +38,7 @@ function migrateHostBindings(host, references, info) {
36
38
  const seenReferences = new WeakMap();
37
39
  for (const reference of references) {
38
40
  // This pass only deals with host binding references.
39
- if (!group_replacements.isHostBindingReference(reference)) {
41
+ if (!combine_units.isHostBindingReference(reference)) {
40
42
  continue;
41
43
  }
42
44
  // Skip references to incompatible inputs.
@@ -60,7 +62,7 @@ function migrateHostBindings(host, references, info) {
60
62
  const appendText = reference.from.isObjectShorthandExpression
61
63
  ? `: ${reference.from.read.name}()`
62
64
  : `()`;
63
- host.replacements.push(new group_replacements.Replacement(group_replacements.projectFile(bindingField.getSourceFile(), info), new group_replacements.TextUpdate({ position: readEndPos, end: readEndPos, toInsert: appendText })));
65
+ host.replacements.push(new combine_units.Replacement(combine_units.projectFile(bindingField.getSourceFile(), info), new combine_units.TextUpdate({ position: readEndPos, end: readEndPos, toInsert: appendText })));
64
66
  }
65
67
  }
66
68
 
@@ -72,7 +74,7 @@ function migrateTemplateReferences(host, references) {
72
74
  const seenFileReferences = new Set();
73
75
  for (const reference of references) {
74
76
  // This pass only deals with HTML template references.
75
- if (!group_replacements.isTemplateReference(reference)) {
77
+ if (!combine_units.isTemplateReference(reference)) {
76
78
  continue;
77
79
  }
78
80
  // Skip references to incompatible inputs.
@@ -89,7 +91,7 @@ function migrateTemplateReferences(host, references) {
89
91
  const appendText = reference.from.isObjectShorthandExpression
90
92
  ? `: ${reference.from.read.name}()`
91
93
  : `()`;
92
- host.replacements.push(new group_replacements.Replacement(reference.from.templateFile, new group_replacements.TextUpdate({
94
+ host.replacements.push(new combine_units.Replacement(reference.from.templateFile, new combine_units.TextUpdate({
93
95
  position: reference.from.read.sourceSpan.end,
94
96
  end: reference.from.read.sourceSpan.end,
95
97
  toInsert: appendText,
@@ -137,7 +139,7 @@ function extractQueryListType(node) {
137
139
  * --> read stays
138
140
  * --> emitDistinctChangesOnly is gone!
139
141
  */
140
- function computeReplacementsToMigrateQuery(node, metadata, importManager, info, printer) {
142
+ function computeReplacementsToMigrateQuery(node, metadata, importManager, info, printer, options, checker$1) {
141
143
  const sf = node.getSourceFile();
142
144
  let newQueryFn = importManager.addImport({
143
145
  requestedFile: sf,
@@ -171,36 +173,56 @@ function computeReplacementsToMigrateQuery(node, metadata, importManager, info,
171
173
  if (optionProperties.length > 0) {
172
174
  args.push(ts__default["default"].factory.createObjectLiteralExpression(optionProperties));
173
175
  }
174
- // TODO: Can we consult, based on references and non-null assertions?
175
- const isIndicatedAsRequired = node.exclamationToken !== undefined;
176
- // If the query is required already via some indicators, and this is a "single"
177
- // query, use the available `.required` method.
178
- if (isIndicatedAsRequired && metadata.queryInfo.first) {
176
+ const strictNullChecksEnabled = options.strict === true || options.strictNullChecks === true;
177
+ const strictPropertyInitialization = options.strict === true || options.strictPropertyInitialization === true;
178
+ let isRequired = node.exclamationToken !== undefined;
179
+ // If we come across an application with strict null checks enabled, but strict
180
+ // property initialization is disabled, there are two options:
181
+ // - Either the query is already typed to include `undefined` explicitly,
182
+ // in which case an option query makes sense.
183
+ // - OR, the query is not typed to include `undefined`. In which case, the query
184
+ // should be marked as required to not break the app. The user-code throughout
185
+ // the application (given strict null checks) already assumes non-nullable!
186
+ if (strictNullChecksEnabled &&
187
+ !strictPropertyInitialization &&
188
+ node.initializer === undefined &&
189
+ node.questionToken === undefined &&
190
+ type !== undefined &&
191
+ !checker$1.isTypeAssignableTo(checker$1.getUndefinedType(), checker$1.getTypeFromTypeNode(type))) {
192
+ isRequired = true;
193
+ }
194
+ if (isRequired && metadata.queryInfo.first) {
195
+ // If the query is required already via some indicators, and this is a "single"
196
+ // query, use the available `.required` method.
179
197
  newQueryFn = ts__default["default"].factory.createPropertyAccessExpression(newQueryFn, 'required');
180
198
  }
181
199
  // If this query is still nullable (i.e. not required), attempt to remove
182
200
  // explicit `undefined` types if possible.
183
- if (!isIndicatedAsRequired && type !== undefined && ts__default["default"].isUnionTypeNode(type)) {
184
- type = group_replacements.removeFromUnionIfPossible(type, (v) => v.kind !== ts__default["default"].SyntaxKind.UndefinedKeyword);
201
+ if (!isRequired && type !== undefined && ts__default["default"].isUnionTypeNode(type)) {
202
+ type = migrate_ts_type_references.removeFromUnionIfPossible(type, (v) => v.kind !== ts__default["default"].SyntaxKind.UndefinedKeyword);
185
203
  }
186
- const locatorType = Array.isArray(metadata.queryInfo.predicate)
204
+ let locatorType = Array.isArray(metadata.queryInfo.predicate)
187
205
  ? null
188
206
  : metadata.queryInfo.predicate.expression;
189
- const readType = metadata.queryInfo.read ?? locatorType;
190
- // If the type and the read type are matching, we can rely on the TS generic
191
- // signature rather than repeating e.g. `viewChild<Button>(Button)`.
207
+ let resolvedReadType = metadata.queryInfo.read ?? locatorType;
208
+ // If the original property type and the read type are matching, we can rely
209
+ // on the TS inference, instead of repeating types, like in `viewChild<Button>(Button)`.
192
210
  if (type !== undefined &&
193
- readType instanceof checker.WrappedNodeExpr &&
194
- ts__default["default"].isIdentifier(readType.node) &&
211
+ resolvedReadType instanceof checker.WrappedNodeExpr &&
212
+ ts__default["default"].isIdentifier(resolvedReadType.node) &&
195
213
  ts__default["default"].isTypeReferenceNode(type) &&
196
214
  ts__default["default"].isIdentifier(type.typeName) &&
197
- type.typeName.text === readType.node.text) {
198
- type = undefined;
199
- }
200
- const call = ts__default["default"].factory.createCallExpression(newQueryFn, type ? [type] : undefined, args);
215
+ type.typeName.text === resolvedReadType.node.text) {
216
+ locatorType = null;
217
+ }
218
+ const call = ts__default["default"].factory.createCallExpression(newQueryFn,
219
+ // If there is no resolved `ReadT` (e.g. string predicate), we use the
220
+ // original type explicitly as generic. Otherwise, query API is smart
221
+ // enough to always infer.
222
+ resolvedReadType === null && type !== undefined ? [type] : undefined, args);
201
223
  const updated = ts__default["default"].factory.createPropertyDeclaration([ts__default["default"].factory.createModifier(ts__default["default"].SyntaxKind.ReadonlyKeyword)], node.name, undefined, undefined, call);
202
224
  return [
203
- new group_replacements.Replacement(group_replacements.projectFile(node.getSourceFile(), info), new group_replacements.TextUpdate({
225
+ new combine_units.Replacement(combine_units.projectFile(node.getSourceFile(), info), new combine_units.TextUpdate({
204
226
  position: node.getStart(),
205
227
  end: node.getEnd(),
206
228
  toInsert: printer.printNode(ts__default["default"].EmitHint.Unspecified, updated, sf),
@@ -238,7 +260,10 @@ function getUniqueIDForClassProperty(property, info) {
238
260
  if (!ts__default["default"].isClassDeclaration(property.parent) || property.parent.name === undefined) {
239
261
  return null;
240
262
  }
241
- const id = group_replacements.projectFile(property.getSourceFile(), info).id.replace(/\.d\.ts$/, '.ts');
263
+ if (property.name === undefined) {
264
+ return null;
265
+ }
266
+ const id = combine_units.projectFile(property.getSourceFile(), info).id.replace(/\.d\.ts$/, '.ts');
242
267
  // Note: If a class is nested, there could be an ID clash.
243
268
  // This is highly unlikely though, and this is not a problem because
244
269
  // in such cases, there is even less chance there are any references to
@@ -252,7 +277,7 @@ function getUniqueIDForClassProperty(property, info) {
252
277
  * returns its resolved metadata if possible.
253
278
  */
254
279
  function extractSourceQueryDefinition(node, reflector, evaluator, info) {
255
- if (!ts__default["default"].isPropertyDeclaration(node) ||
280
+ if ((!ts__default["default"].isPropertyDeclaration(node) && !ts__default["default"].isAccessor(node)) ||
256
281
  !ts__default["default"].isClassDeclaration(node.parent) ||
257
282
  node.parent.name === undefined ||
258
283
  !ts__default["default"].isIdentifier(node.name)) {
@@ -284,32 +309,72 @@ function extractSourceQueryDefinition(node, reflector, evaluator, info) {
284
309
  else {
285
310
  throw new Error('Unexpected query decorator detected.');
286
311
  }
287
- const queryInfo = program.extractDecoratorQueryMetadata(node, decorator.name, decorator.args ?? [], node.name.text, reflector, evaluator);
312
+ let queryInfo = null;
313
+ try {
314
+ queryInfo = program.extractDecoratorQueryMetadata(node, decorator.name, decorator.args ?? [], node.name.text, reflector, evaluator);
315
+ }
316
+ catch (e) {
317
+ if (!(e instanceof checker.FatalDiagnosticError)) {
318
+ throw e;
319
+ }
320
+ console.error(`Skipping query: ${e.node.getSourceFile().fileName}: ${e.toString()}`);
321
+ return null;
322
+ }
288
323
  return {
289
324
  id,
290
325
  kind,
291
326
  args: decorator.args ?? [],
292
327
  queryInfo,
293
- node,
328
+ node: node,
329
+ fieldDecorators: decorators,
294
330
  };
295
331
  }
296
332
 
333
+ function markFieldIncompatibleInMetadata(data, id, reason) {
334
+ const existing = data[id];
335
+ if (existing === undefined) {
336
+ data[id] = {
337
+ fieldReason: reason,
338
+ classReason: null,
339
+ };
340
+ }
341
+ else if (existing.fieldReason === null) {
342
+ existing.fieldReason = reason;
343
+ }
344
+ else {
345
+ existing.fieldReason = migrate_ts_type_references.pickFieldIncompatibility({ reason, context: null }, { reason: existing.fieldReason, context: null }).reason;
346
+ }
347
+ }
348
+ function filterBestEffortIncompatibilities(knownQueries) {
349
+ for (const query of Object.values(knownQueries.globalMetadata.problematicQueries)) {
350
+ if (query.fieldReason !== null &&
351
+ !migrate_ts_type_references.nonIgnorableFieldIncompatibilities.includes(query.fieldReason)) {
352
+ query.fieldReason = null;
353
+ }
354
+ }
355
+ }
356
+
297
357
  class KnownQueries {
298
- constructor(info, globalMetadata) {
358
+ info;
359
+ config;
360
+ globalMetadata;
361
+ classToQueryFields = new Map();
362
+ knownQueryIDs = new Map();
363
+ constructor(info, config, globalMetadata) {
299
364
  this.info = info;
365
+ this.config = config;
300
366
  this.globalMetadata = globalMetadata;
301
- this.classToQueryFields = new Map();
302
- this.knownQueryIDs = new Map();
303
367
  }
304
368
  isFieldIncompatible(descriptor) {
305
- return this.globalMetadata.problematicQueries[descriptor.key] !== undefined;
369
+ return this.getIncompatibilityForField(descriptor) !== null;
306
370
  }
307
- markFieldIncompatible(field) {
308
- this.globalMetadata.problematicQueries[field.key] = true;
371
+ markFieldIncompatible(field, incompatibility) {
372
+ markFieldIncompatibleInMetadata(this.globalMetadata.problematicQueries, field.key, incompatibility.reason);
309
373
  }
310
- markClassIncompatible(node) {
374
+ markClassIncompatible(node, reason) {
311
375
  this.classToQueryFields.get(node)?.forEach((f) => {
312
- this.globalMetadata.problematicQueries[f.key] = true;
376
+ this.globalMetadata.problematicQueries[f.key] ??= { classReason: null, fieldReason: null };
377
+ this.globalMetadata.problematicQueries[f.key].classReason = reason;
313
378
  });
314
379
  }
315
380
  registerQueryField(queryField, id) {
@@ -321,6 +386,15 @@ class KnownQueries {
321
386
  node: queryField,
322
387
  });
323
388
  this.knownQueryIDs.set(id, { key: id, node: queryField });
389
+ const descriptor = { key: id, node: queryField };
390
+ const file = combine_units.projectFile(queryField.getSourceFile(), this.info);
391
+ if (this.config.shouldMigrateQuery !== undefined &&
392
+ !this.config.shouldMigrateQuery(descriptor, file)) {
393
+ this.markFieldIncompatible(descriptor, {
394
+ context: null,
395
+ reason: migrate_ts_type_references.FieldIncompatibilityReason.SkippedViaConfigFilter,
396
+ });
397
+ }
324
398
  }
325
399
  attemptRetrieveDescriptorFromSymbol(symbol) {
326
400
  const descriptor = getClassFieldDescriptorForSymbol(symbol, this.info);
@@ -339,16 +413,67 @@ class KnownQueries {
339
413
  return Array.from(this.classToQueryFields.keys()).filter((c) => ts__default["default"].isClassDeclaration(c));
340
414
  }
341
415
  captureKnownFieldInheritanceRelationship(derived, parent) {
342
- if (this.isFieldIncompatible(parent) || this.isFieldIncompatible(derived)) {
343
- this.markFieldIncompatible(parent);
344
- this.markFieldIncompatible(derived);
416
+ // Note: The edge problematic pattern recognition is not as good as the one
417
+ // we have in the signal input migration. That is because we couldn't trivially
418
+ // build up an inheritance graph during analyze phase where we DON'T know what
419
+ // fields refer to queries. Usually we'd use the graph to smartly propagate
420
+ // incompatibilities using topological sort. This doesn't work here and is
421
+ // unnecessarily complex, so we try our best at detecting direct edge
422
+ // incompatibilities (which are quite order dependent).
423
+ if (this.isFieldIncompatible(parent) && !this.isFieldIncompatible(derived)) {
424
+ this.markFieldIncompatible(derived, {
425
+ context: null,
426
+ reason: migrate_ts_type_references.FieldIncompatibilityReason.ParentIsIncompatible,
427
+ });
428
+ return;
429
+ }
430
+ if (this.isFieldIncompatible(derived) && !this.isFieldIncompatible(parent)) {
431
+ this.markFieldIncompatible(parent, {
432
+ context: null,
433
+ reason: migrate_ts_type_references.FieldIncompatibilityReason.DerivedIsIncompatible,
434
+ });
345
435
  }
346
436
  }
347
437
  captureUnknownDerivedField(field) {
348
- this.markFieldIncompatible(field);
438
+ this.markFieldIncompatible(field, {
439
+ context: null,
440
+ reason: migrate_ts_type_references.FieldIncompatibilityReason.OverriddenByDerivedClass,
441
+ });
349
442
  }
350
443
  captureUnknownParentField(field) {
351
- this.markFieldIncompatible(field);
444
+ this.markFieldIncompatible(field, {
445
+ context: null,
446
+ reason: migrate_ts_type_references.FieldIncompatibilityReason.TypeConflictWithBaseClass,
447
+ });
448
+ }
449
+ getIncompatibilityForField(descriptor) {
450
+ const problematicInfo = this.globalMetadata.problematicQueries[descriptor.key];
451
+ if (problematicInfo === undefined) {
452
+ return null;
453
+ }
454
+ if (problematicInfo.fieldReason !== null) {
455
+ return { context: null, reason: problematicInfo.fieldReason };
456
+ }
457
+ if (problematicInfo.classReason !== null) {
458
+ return problematicInfo.classReason;
459
+ }
460
+ return null;
461
+ }
462
+ getIncompatibilityTextForField(field) {
463
+ const incompatibilityInfo = this.globalMetadata.problematicQueries[field.key];
464
+ if (incompatibilityInfo.fieldReason !== null) {
465
+ return migrate_ts_type_references.getMessageForFieldIncompatibility(incompatibilityInfo.fieldReason, {
466
+ single: 'query',
467
+ plural: 'queries',
468
+ });
469
+ }
470
+ if (incompatibilityInfo.classReason !== null) {
471
+ return migrate_ts_type_references.getMessageForClassIncompatibility(incompatibilityInfo.classReason, {
472
+ single: 'query',
473
+ plural: 'queries',
474
+ });
475
+ }
476
+ return null;
352
477
  }
353
478
  }
354
479
 
@@ -376,7 +501,7 @@ function queryFunctionNameToDecorator(name) {
376
501
  * E.g. whether `<my-read>.toArray` is detected.
377
502
  */
378
503
  function checkTsReferenceAccessesField(ref, fieldName) {
379
- const accessNode = group_replacements.traverseAccess(ref.from.node);
504
+ const accessNode = combine_units.traverseAccess(ref.from.node);
380
505
  // Check if the reference is part of a property access.
381
506
  if (!ts__default["default"].isPropertyAccessExpression(accessNode.parent) ||
382
507
  !ts__default["default"].isIdentifier(accessNode.parent.name)) {
@@ -444,21 +569,24 @@ function checkNonTsReferenceCallsField(ref, fieldName) {
444
569
  return potentialCall;
445
570
  }
446
571
 
447
- function removeQueryListToArrayCall(ref, info, globalMetadata, replacements) {
448
- if (!group_replacements.isHostBindingReference(ref) && !group_replacements.isTemplateReference(ref) && !group_replacements.isTsReference(ref)) {
572
+ function removeQueryListToArrayCall(ref, info, globalMetadata, knownQueries, replacements) {
573
+ if (!combine_units.isHostBindingReference(ref) && !combine_units.isTemplateReference(ref) && !combine_units.isTsReference(ref)) {
574
+ return;
575
+ }
576
+ if (knownQueries.isFieldIncompatible(ref.target)) {
449
577
  return;
450
578
  }
451
579
  if (!globalMetadata.knownQueryFields[ref.target.key]?.isMulti) {
452
580
  return;
453
581
  }
454
582
  // TS references.
455
- if (group_replacements.isTsReference(ref)) {
583
+ if (combine_units.isTsReference(ref)) {
456
584
  const toArrayCallExpr = checkTsReferenceCallsField(ref, 'toArray');
457
585
  if (toArrayCallExpr === null) {
458
586
  return;
459
587
  }
460
588
  const toArrayExpr = toArrayCallExpr.expression;
461
- replacements.push(new group_replacements.Replacement(group_replacements.projectFile(toArrayExpr.getSourceFile(), info), new group_replacements.TextUpdate({
589
+ replacements.push(new combine_units.Replacement(combine_units.projectFile(toArrayExpr.getSourceFile(), info), new combine_units.TextUpdate({
462
590
  // Delete from expression end to call end. E.g. `.toArray(<..>)`.
463
591
  position: toArrayExpr.expression.getEnd(),
464
592
  end: toArrayCallExpr.getEnd(),
@@ -471,9 +599,9 @@ function removeQueryListToArrayCall(ref, info, globalMetadata, replacements) {
471
599
  if (callExpr === null) {
472
600
  return;
473
601
  }
474
- const file = group_replacements.isHostBindingReference(ref) ? ref.from.file : ref.from.templateFile;
475
- const offset = group_replacements.isHostBindingReference(ref) ? ref.from.hostPropertyNode.getStart() + 1 : 0;
476
- replacements.push(new group_replacements.Replacement(file, new group_replacements.TextUpdate({
602
+ const file = combine_units.isHostBindingReference(ref) ? ref.from.file : ref.from.templateFile;
603
+ const offset = combine_units.isHostBindingReference(ref) ? ref.from.hostPropertyNode.getStart() + 1 : 0;
604
+ replacements.push(new combine_units.Replacement(file, new combine_units.TextUpdate({
477
605
  // Delete from expression end to call end. E.g. `.toArray(<..>)`.
478
606
  position: offset + callExpr.receiver.receiver.sourceSpan.end,
479
607
  end: offset + callExpr.sourceSpan.end,
@@ -481,20 +609,23 @@ function removeQueryListToArrayCall(ref, info, globalMetadata, replacements) {
481
609
  })));
482
610
  }
483
611
 
484
- function replaceQueryListGetCall(ref, info, globalMetadata, replacements) {
485
- if (!group_replacements.isHostBindingReference(ref) && !group_replacements.isTemplateReference(ref) && !group_replacements.isTsReference(ref)) {
612
+ function replaceQueryListGetCall(ref, info, globalMetadata, knownQueries, replacements) {
613
+ if (!combine_units.isHostBindingReference(ref) && !combine_units.isTemplateReference(ref) && !combine_units.isTsReference(ref)) {
614
+ return;
615
+ }
616
+ if (knownQueries.isFieldIncompatible(ref.target)) {
486
617
  return;
487
618
  }
488
619
  if (!globalMetadata.knownQueryFields[ref.target.key]?.isMulti) {
489
620
  return;
490
621
  }
491
- if (group_replacements.isTsReference(ref)) {
622
+ if (combine_units.isTsReference(ref)) {
492
623
  const getCallExpr = checkTsReferenceCallsField(ref, 'get');
493
624
  if (getCallExpr === null) {
494
625
  return;
495
626
  }
496
627
  const getExpr = getCallExpr.expression;
497
- replacements.push(new group_replacements.Replacement(group_replacements.projectFile(getExpr.getSourceFile(), info), new group_replacements.TextUpdate({
628
+ replacements.push(new combine_units.Replacement(combine_units.projectFile(getExpr.getSourceFile(), info), new combine_units.TextUpdate({
498
629
  position: getExpr.name.getStart(),
499
630
  end: getExpr.name.getEnd(),
500
631
  toInsert: 'at',
@@ -506,9 +637,9 @@ function replaceQueryListGetCall(ref, info, globalMetadata, replacements) {
506
637
  if (callExpr === null) {
507
638
  return;
508
639
  }
509
- const file = group_replacements.isHostBindingReference(ref) ? ref.from.file : ref.from.templateFile;
510
- const offset = group_replacements.isHostBindingReference(ref) ? ref.from.hostPropertyNode.getStart() + 1 : 0;
511
- replacements.push(new group_replacements.Replacement(file, new group_replacements.TextUpdate({
640
+ const file = combine_units.isHostBindingReference(ref) ? ref.from.file : ref.from.templateFile;
641
+ const offset = combine_units.isHostBindingReference(ref) ? ref.from.hostPropertyNode.getStart() + 1 : 0;
642
+ replacements.push(new combine_units.Replacement(file, new combine_units.TextUpdate({
512
643
  position: offset + callExpr.receiver.nameSpan.start,
513
644
  end: offset + callExpr.receiver.nameSpan.end,
514
645
  toInsert: 'at',
@@ -524,7 +655,7 @@ const problematicQueryListMethods = [
524
655
  'destroy',
525
656
  ];
526
657
  function checkForIncompatibleQueryListAccesses(ref, result) {
527
- if (group_replacements.isTsReference(ref)) {
658
+ if (combine_units.isTsReference(ref)) {
528
659
  for (const problematicFn of problematicQueryListMethods) {
529
660
  const access = checkTsReferenceAccessesField(ref, problematicFn);
530
661
  if (access !== null) {
@@ -533,7 +664,7 @@ function checkForIncompatibleQueryListAccesses(ref, result) {
533
664
  }
534
665
  }
535
666
  }
536
- if (group_replacements.isHostBindingReference(ref) || group_replacements.isTemplateReference(ref)) {
667
+ if (combine_units.isHostBindingReference(ref) || combine_units.isTemplateReference(ref)) {
537
668
  for (const problematicFn of problematicQueryListMethods) {
538
669
  const access = checkNonTsReferenceAccessesField(ref, problematicFn);
539
670
  if (access !== null) {
@@ -548,19 +679,22 @@ const mapping = new Map([
548
679
  ['first', 'at(0)!'],
549
680
  ['last', 'at(-1)!'],
550
681
  ]);
551
- function replaceQueryListFirstAndLastReferences(ref, info, globalMetadata, replacements) {
552
- if (!group_replacements.isHostBindingReference(ref) && !group_replacements.isTemplateReference(ref) && !group_replacements.isTsReference(ref)) {
682
+ function replaceQueryListFirstAndLastReferences(ref, info, globalMetadata, knownQueries, replacements) {
683
+ if (!combine_units.isHostBindingReference(ref) && !combine_units.isTemplateReference(ref) && !combine_units.isTsReference(ref)) {
684
+ return;
685
+ }
686
+ if (knownQueries.isFieldIncompatible(ref.target)) {
553
687
  return;
554
688
  }
555
689
  if (!globalMetadata.knownQueryFields[ref.target.key]?.isMulti) {
556
690
  return;
557
691
  }
558
- if (group_replacements.isTsReference(ref)) {
692
+ if (combine_units.isTsReference(ref)) {
559
693
  const expr = checkTsReferenceAccessesField(ref, 'first') ?? checkTsReferenceAccessesField(ref, 'last');
560
694
  if (expr === null) {
561
695
  return;
562
696
  }
563
- replacements.push(new group_replacements.Replacement(group_replacements.projectFile(expr.getSourceFile(), info), new group_replacements.TextUpdate({
697
+ replacements.push(new combine_units.Replacement(combine_units.projectFile(expr.getSourceFile(), info), new combine_units.TextUpdate({
564
698
  position: expr.name.getStart(),
565
699
  end: expr.name.getEnd(),
566
700
  toInsert: mapping.get(expr.name.text),
@@ -572,16 +706,17 @@ function replaceQueryListFirstAndLastReferences(ref, info, globalMetadata, repla
572
706
  if (expr === null) {
573
707
  return;
574
708
  }
575
- const file = group_replacements.isHostBindingReference(ref) ? ref.from.file : ref.from.templateFile;
576
- const offset = group_replacements.isHostBindingReference(ref) ? ref.from.hostPropertyNode.getStart() + 1 : 0;
577
- replacements.push(new group_replacements.Replacement(file, new group_replacements.TextUpdate({
709
+ const file = combine_units.isHostBindingReference(ref) ? ref.from.file : ref.from.templateFile;
710
+ const offset = combine_units.isHostBindingReference(ref) ? ref.from.hostPropertyNode.getStart() + 1 : 0;
711
+ replacements.push(new combine_units.Replacement(file, new combine_units.TextUpdate({
578
712
  position: offset + expr.nameSpan.start,
579
713
  end: offset + expr.nameSpan.end,
580
714
  toInsert: mapping.get(expr.name),
581
715
  })));
582
716
  }
583
717
 
584
- class SignalQueriesMigration extends group_replacements.TsurgeComplexMigration {
718
+ class SignalQueriesMigration extends combine_units.TsurgeComplexMigration {
719
+ config;
585
720
  constructor(config = {}) {
586
721
  super();
587
722
  this.config = config;
@@ -590,6 +725,8 @@ class SignalQueriesMigration extends group_replacements.TsurgeComplexMigration {
590
725
  assert__default["default"](info.ngCompiler !== null, 'Expected queries migration to have an Angular program.');
591
726
  // Pre-Analyze the program and get access to the template type checker.
592
727
  const { templateTypeChecker } = info.ngCompiler['ensureAnalyzed']();
728
+ // Generate all type check blocks.
729
+ templateTypeChecker.generateAllTypeCheckBlocks();
593
730
  const { sourceFiles, program: program$1 } = info;
594
731
  const checker$1 = program$1.getTypeChecker();
595
732
  const reflector = new checker.TypeScriptReflectionHost(checker$1);
@@ -600,21 +737,24 @@ class SignalQueriesMigration extends group_replacements.TsurgeComplexMigration {
600
737
  potentialProblematicReferenceForMultiQueries: {},
601
738
  reusableAnalysisReferences: null,
602
739
  };
603
- const groupedAstVisitor = new group_replacements.GroupedTsAstVisitor(sourceFiles);
740
+ const groupedAstVisitor = new migrate_ts_type_references.GroupedTsAstVisitor(sourceFiles);
604
741
  const referenceResult = { references: [] };
605
- const classesWithFilteredQueries = new WeakSet();
742
+ const classesWithFilteredQueries = new Set();
606
743
  const filteredQueriesForCompilationUnit = new Map();
607
744
  const findQueryDefinitionsVisitor = (node) => {
608
745
  const extractedQuery = extractSourceQueryDefinition(node, reflector, evaluator, info);
609
746
  if (extractedQuery !== null) {
747
+ const queryNode = extractedQuery.node;
610
748
  const descriptor = {
611
749
  key: extractedQuery.id,
612
- node: extractedQuery.node,
750
+ node: queryNode,
613
751
  };
614
- const containingFile = group_replacements.projectFile(descriptor.node.getSourceFile(), info);
752
+ const containingFile = combine_units.projectFile(queryNode.getSourceFile(), info);
753
+ // If we have a config filter function, use it here for later
754
+ // perf-boosted reference lookups. Useful in non-batch mode.
615
755
  if (this.config.shouldMigrateQuery === undefined ||
616
756
  this.config.shouldMigrateQuery(descriptor, containingFile)) {
617
- classesWithFilteredQueries.add(extractedQuery.node.parent);
757
+ classesWithFilteredQueries.add(queryNode.parent);
618
758
  filteredQueriesForCompilationUnit.set(extractedQuery.id, {
619
759
  fieldName: extractedQuery.queryInfo.propertyName,
620
760
  });
@@ -623,20 +763,32 @@ class SignalQueriesMigration extends group_replacements.TsurgeComplexMigration {
623
763
  fieldName: extractedQuery.queryInfo.propertyName,
624
764
  isMulti: extractedQuery.queryInfo.first === false,
625
765
  };
766
+ if (ts__default["default"].isAccessor(queryNode)) {
767
+ markFieldIncompatibleInMetadata(res.potentialProblematicQueries, extractedQuery.id, migrate_ts_type_references.FieldIncompatibilityReason.Accessor);
768
+ }
769
+ // Detect queries with union types that are uncommon to be
770
+ // automatically migrate-able. E.g. `refs: ElementRef|null`,
771
+ // or `ElementRef|SomeOtherType`.
772
+ if (queryNode.type !== undefined &&
773
+ ts__default["default"].isUnionTypeNode(queryNode.type) &&
774
+ // Either too large union, or doesn't match `T|undefined`.
775
+ (queryNode.type.types.length > 2 ||
776
+ !queryNode.type.types.some((t) => t.kind === ts__default["default"].SyntaxKind.UndefinedKeyword))) {
777
+ markFieldIncompatibleInMetadata(res.potentialProblematicQueries, extractedQuery.id, migrate_ts_type_references.FieldIncompatibilityReason.SignalQueries__IncompatibleMultiUnionType);
778
+ }
779
+ // Migrating fields with `@HostBinding` is incompatible as
780
+ // the host binding decorator does not invoke the signal.
781
+ const hostBindingDecorators = checker.getAngularDecorators(extractedQuery.fieldDecorators, ['HostBinding'],
782
+ /* isCore */ false);
783
+ if (hostBindingDecorators.length > 0) {
784
+ markFieldIncompatibleInMetadata(res.potentialProblematicQueries, extractedQuery.id, migrate_ts_type_references.FieldIncompatibilityReason.SignalIncompatibleWithHostBinding);
785
+ }
626
786
  }
627
787
  };
788
+ this.config.reportProgressFn?.(20, 'Scanning for queries..');
628
789
  groupedAstVisitor.register(findQueryDefinitionsVisitor);
629
- if (this.config.assumeNonBatch) {
630
- // In non-batch, we need to find queries before, so we can perform
631
- // improved reference resolution.
632
- this.config.reportProgressFn?.(20, 'Scanning for queries..');
633
- groupedAstVisitor.execute();
634
- this.config.reportProgressFn?.(30, 'Scanning for references..');
635
- }
636
- else {
637
- this.config.reportProgressFn?.(20, 'Scanning for queries and references..');
638
- }
639
- groupedAstVisitor.register(group_replacements.createFindAllSourceFileReferencesVisitor(info, checker$1, reflector, info.ngCompiler['resourceManager'], evaluator, templateTypeChecker, {
790
+ groupedAstVisitor.execute();
791
+ const allFieldsOrKnownQueries = {
640
792
  // Note: We don't support cross-target migration of `Partial<T>` usages.
641
793
  // This is an acceptable limitation for performance reasons.
642
794
  shouldTrackClassReference: (node) => classesWithFilteredQueries.has(node),
@@ -646,39 +798,54 @@ class SignalQueriesMigration extends group_replacements.TsurgeComplexMigration {
646
798
  // of the queries since there aren't any other compilation units.
647
799
  // Ignore references to non-query class fields.
648
800
  if (this.config.assumeNonBatch &&
649
- descriptor !== null &&
650
- !filteredQueriesForCompilationUnit.has(descriptor.key)) {
801
+ (descriptor === null || !filteredQueriesForCompilationUnit.has(descriptor.key))) {
651
802
  return null;
652
803
  }
653
- // TODO: Also consider skipping if we know this cannot be a query.
654
- // e.g. missing class decorators or some other checks.
655
804
  // In batch mode, we eagerly, rather expensively, track all references.
656
805
  // We don't know yet if something refers to a different query or not, so we
657
806
  // eagerly detect such and later filter those problematic references that
658
807
  // turned out to refer to queries (once we have the global metadata).
659
808
  return descriptor;
660
809
  },
661
- },
810
+ };
811
+ groupedAstVisitor.register(combine_units.createFindAllSourceFileReferencesVisitor(info, checker$1, reflector, info.ngCompiler['resourceManager'], evaluator, templateTypeChecker, allFieldsOrKnownQueries,
662
812
  // In non-batch mode, we know what inputs exist and can optimize the reference
663
813
  // resolution significantly (for e.g. VSCode integration)— as we know what
664
814
  // field names may be used to reference potential queries.
665
815
  this.config.assumeNonBatch
666
816
  ? new Set(Array.from(filteredQueriesForCompilationUnit.values()).map((f) => f.fieldName))
667
817
  : null, referenceResult).visitor);
818
+ const inheritanceGraph = new migrate_ts_type_references.InheritanceGraph(checker$1).expensivePopulate(info.sourceFiles);
819
+ migrate_ts_type_references.checkIncompatiblePatterns(inheritanceGraph, checker$1, groupedAstVisitor, {
820
+ ...allFieldsOrKnownQueries,
821
+ isFieldIncompatible: (f) => res.potentialProblematicQueries[f.key]?.fieldReason !== null ||
822
+ res.potentialProblematicQueries[f.key]?.classReason !== null,
823
+ markClassIncompatible: (clazz, reason) => {
824
+ for (const field of clazz.members) {
825
+ const key = getUniqueIDForClassProperty(field, info);
826
+ if (key !== null) {
827
+ res.potentialProblematicQueries[key] ??= { classReason: null, fieldReason: null };
828
+ res.potentialProblematicQueries[key].classReason = reason;
829
+ }
830
+ }
831
+ },
832
+ markFieldIncompatible: (f, incompatibility) => markFieldIncompatibleInMetadata(res.potentialProblematicQueries, f.key, incompatibility.reason),
833
+ }, () => Array.from(classesWithFilteredQueries));
834
+ this.config.reportProgressFn?.(60, 'Scanning for references and problematic patterns..');
668
835
  groupedAstVisitor.execute();
669
836
  // Determine incompatible queries based on problematic references
670
837
  // we saw in TS code, templates or host bindings.
671
838
  for (const ref of referenceResult.references) {
672
- if (group_replacements.isTsReference(ref) && ref.from.isWrite) {
673
- res.potentialProblematicQueries[ref.target.key] = true;
839
+ if (combine_units.isTsReference(ref) && ref.from.isWrite) {
840
+ markFieldIncompatibleInMetadata(res.potentialProblematicQueries, ref.target.key, migrate_ts_type_references.FieldIncompatibilityReason.WriteAssignment);
674
841
  }
675
- if ((group_replacements.isTemplateReference(ref) || group_replacements.isHostBindingReference(ref)) && ref.from.isWrite) {
676
- res.potentialProblematicQueries[ref.target.key] = true;
842
+ if ((combine_units.isTemplateReference(ref) || combine_units.isHostBindingReference(ref)) && ref.from.isWrite) {
843
+ markFieldIncompatibleInMetadata(res.potentialProblematicQueries, ref.target.key, migrate_ts_type_references.FieldIncompatibilityReason.WriteAssignment);
677
844
  }
678
845
  // TODO: Remove this when we support signal narrowing in templates.
679
846
  // https://github.com/angular/angular/pull/55456.
680
- if (group_replacements.isTemplateReference(ref) && ref.from.isLikelyPartOfNarrowing) {
681
- res.potentialProblematicQueries[ref.target.key] = true;
847
+ if (combine_units.isTemplateReference(ref) && ref.from.isLikelyPartOfNarrowing) {
848
+ markFieldIncompatibleInMetadata(res.potentialProblematicQueries, ref.target.key, migrate_ts_type_references.FieldIncompatibilityReason.PotentiallyNarrowedInTemplateButNoSupportYet);
682
849
  }
683
850
  // Check for other incompatible query list accesses.
684
851
  checkForIncompatibleQueryListAccesses(ref, res);
@@ -686,34 +853,60 @@ class SignalQueriesMigration extends group_replacements.TsurgeComplexMigration {
686
853
  if (this.config.assumeNonBatch) {
687
854
  res.reusableAnalysisReferences = referenceResult.references;
688
855
  }
689
- return group_replacements.confirmAsSerializable(res);
856
+ return combine_units.confirmAsSerializable(res);
690
857
  }
691
- async merge(units) {
692
- const merged = {
858
+ async combine(unitA, unitB) {
859
+ const combined = {
693
860
  knownQueryFields: {},
694
- problematicQueries: {},
861
+ potentialProblematicQueries: {},
862
+ potentialProblematicReferenceForMultiQueries: {},
695
863
  reusableAnalysisReferences: null,
696
864
  };
697
- for (const unit of units) {
865
+ for (const unit of [unitA, unitB]) {
698
866
  for (const [id, value] of Object.entries(unit.knownQueryFields)) {
699
- merged.knownQueryFields[id] = value;
867
+ combined.knownQueryFields[id] = value;
700
868
  }
701
- for (const id of Object.keys(unit.potentialProblematicQueries)) {
702
- merged.problematicQueries[id] = true;
869
+ for (const [id, info] of Object.entries(unit.potentialProblematicQueries)) {
870
+ if (info.fieldReason !== null) {
871
+ markFieldIncompatibleInMetadata(combined.potentialProblematicQueries, id, info.fieldReason);
872
+ }
873
+ if (info.classReason !== null) {
874
+ combined.potentialProblematicQueries[id] ??= {
875
+ classReason: null,
876
+ fieldReason: null,
877
+ };
878
+ combined.potentialProblematicQueries[id].classReason =
879
+ info.classReason;
880
+ }
881
+ }
882
+ for (const id of Object.keys(unit.potentialProblematicReferenceForMultiQueries)) {
883
+ combined.potentialProblematicReferenceForMultiQueries[id] = true;
703
884
  }
704
885
  if (unit.reusableAnalysisReferences !== null) {
705
- assert__default["default"](units.length === 1, 'Expected migration to not run in batch mode');
706
- merged.reusableAnalysisReferences = unit.reusableAnalysisReferences;
886
+ combined.reusableAnalysisReferences = unit.reusableAnalysisReferences;
707
887
  }
708
888
  }
709
- for (const unit of units) {
889
+ for (const unit of [unitA, unitB]) {
710
890
  for (const id of Object.keys(unit.potentialProblematicReferenceForMultiQueries)) {
711
- if (merged.knownQueryFields[id]?.isMulti) {
712
- merged.problematicQueries[id] = true;
891
+ if (combined.knownQueryFields[id]?.isMulti) {
892
+ markFieldIncompatibleInMetadata(combined.potentialProblematicQueries, id, migrate_ts_type_references.FieldIncompatibilityReason.SignalQueries__QueryListProblematicFieldAccessed);
713
893
  }
714
894
  }
715
895
  }
716
- return group_replacements.confirmAsSerializable(merged);
896
+ return combine_units.confirmAsSerializable(combined);
897
+ }
898
+ async globalMeta(combinedData) {
899
+ const globalUnitData = {
900
+ knownQueryFields: combinedData.knownQueryFields,
901
+ problematicQueries: combinedData.potentialProblematicQueries,
902
+ reusableAnalysisReferences: combinedData.reusableAnalysisReferences,
903
+ };
904
+ for (const id of Object.keys(combinedData.potentialProblematicReferenceForMultiQueries)) {
905
+ if (combinedData.knownQueryFields[id]?.isMulti) {
906
+ markFieldIncompatibleInMetadata(globalUnitData.problematicQueries, id, migrate_ts_type_references.FieldIncompatibilityReason.SignalQueries__QueryListProblematicFieldAccessed);
907
+ }
908
+ }
909
+ return combine_units.confirmAsSerializable(globalUnitData);
717
910
  }
718
911
  async migrate(globalMetadata, info) {
719
912
  assert__default["default"](info.ngCompiler !== null, 'Expected queries migration to have an Angular program.');
@@ -729,13 +922,9 @@ class SignalQueriesMigration extends group_replacements.TsurgeComplexMigration {
729
922
  const filesWithSourceQueries = new Map();
730
923
  const filesWithIncompleteMigration = new Map();
731
924
  const filesWithQueryListOutsideOfDeclarations = new WeakSet();
732
- const knownQueries = new KnownQueries(info, globalMetadata);
925
+ const knownQueries = new KnownQueries(info, this.config, globalMetadata);
733
926
  const referenceResult = { references: [] };
734
927
  const sourceQueries = [];
735
- const isMigratedQuery = (descriptor) => globalMetadata.knownQueryFields[descriptor.key] !== undefined &&
736
- globalMetadata.problematicQueries[descriptor.key] === undefined &&
737
- (this.config.shouldMigrateQuery === undefined ||
738
- this.config.shouldMigrateQuery(descriptor, group_replacements.projectFile(descriptor.node.getSourceFile(), info)));
739
928
  // Detect all queries in this unit.
740
929
  const queryWholeProgramVisitor = (node) => {
741
930
  // Detect all SOURCE queries and migrate them, if possible.
@@ -746,7 +935,8 @@ class SignalQueriesMigration extends group_replacements.TsurgeComplexMigration {
746
935
  return;
747
936
  }
748
937
  // Detect OTHER queries, inside `.d.ts`. Needed for reference resolution below.
749
- if (ts__default["default"].isPropertyDeclaration(node)) {
938
+ if (ts__default["default"].isPropertyDeclaration(node) ||
939
+ (ts__default["default"].isAccessor(node) && ts__default["default"].isClassDeclaration(node.parent))) {
750
940
  const classFieldID = getUniqueIDForClassProperty(node, info);
751
941
  if (classFieldID !== null && globalMetadata.knownQueryFields[classFieldID] !== undefined) {
752
942
  knownQueries.registerQueryField(node, classFieldID);
@@ -762,7 +952,6 @@ class SignalQueriesMigration extends group_replacements.TsurgeComplexMigration {
762
952
  }
763
953
  ts__default["default"].forEachChild(node, queryWholeProgramVisitor);
764
954
  };
765
- this.config.reportProgressFn?.(40, 'Tracking query declarations..');
766
955
  for (const sf of info.fullProgramSourceFiles) {
767
956
  ts__default["default"].forEachChild(sf, queryWholeProgramVisitor);
768
957
  }
@@ -770,55 +959,68 @@ class SignalQueriesMigration extends group_replacements.TsurgeComplexMigration {
770
959
  // lookups below.
771
960
  const fieldNamesToConsiderForReferenceLookup = new Set(Object.values(globalMetadata.knownQueryFields).map((f) => f.fieldName));
772
961
  // Find all references.
773
- const groupedAstVisitor = new group_replacements.GroupedTsAstVisitor(sourceFiles);
962
+ const groupedAstVisitor = new migrate_ts_type_references.GroupedTsAstVisitor(sourceFiles);
774
963
  // Re-use previous reference result if available, instead of
775
964
  // looking for references which is quite expensive.
776
965
  if (globalMetadata.reusableAnalysisReferences !== null) {
777
966
  referenceResult.references = globalMetadata.reusableAnalysisReferences;
778
967
  }
779
968
  else {
780
- groupedAstVisitor.register(group_replacements.createFindAllSourceFileReferencesVisitor(info, checker$1, reflector, info.ngCompiler['resourceManager'], evaluator, templateTypeChecker, knownQueries, fieldNamesToConsiderForReferenceLookup, referenceResult).visitor);
969
+ groupedAstVisitor.register(combine_units.createFindAllSourceFileReferencesVisitor(info, checker$1, reflector, info.ngCompiler['resourceManager'], evaluator, templateTypeChecker, knownQueries, fieldNamesToConsiderForReferenceLookup, referenceResult).visitor);
781
970
  }
782
- const inheritanceGraph = new group_replacements.InheritanceGraph(checker$1).expensivePopulate(info.sourceFiles);
783
- group_replacements.checkIncompatiblePatterns(inheritanceGraph, checker$1, groupedAstVisitor, knownQueries, () => knownQueries.getAllClassesWithQueries());
784
- this.config.reportProgressFn?.(60, 'Checking for problematic patterns..');
785
- groupedAstVisitor.execute();
786
971
  // Check inheritance.
787
- this.config.reportProgressFn?.(70, 'Checking for inheritance patterns..');
788
- group_replacements.checkInheritanceOfKnownFields(inheritanceGraph, metaReader, knownQueries, {
972
+ // NOTE: Inheritance is only checked in the migrate stage as we cannot reliably
973
+ // check during analyze— where we don't know what fields from foreign `.d.ts`
974
+ // files refer to queries or not.
975
+ const inheritanceGraph = new migrate_ts_type_references.InheritanceGraph(checker$1).expensivePopulate(info.sourceFiles);
976
+ migrate_ts_type_references.checkInheritanceOfKnownFields(inheritanceGraph, metaReader, knownQueries, {
789
977
  getFieldsForClass: (n) => knownQueries.getQueryFieldsOfClass(n) ?? [],
790
978
  isClassWithKnownFields: (clazz) => knownQueries.getQueryFieldsOfClass(clazz) !== undefined,
791
979
  });
792
- this.config.reportProgressFn?.(80, 'Migrating queries..');
980
+ this.config.reportProgressFn?.(80, 'Checking inheritance..');
981
+ groupedAstVisitor.execute();
982
+ if (this.config.bestEffortMode) {
983
+ filterBestEffortIncompatibilities(knownQueries);
984
+ }
985
+ this.config.reportProgressFn?.(90, 'Migrating queries..');
793
986
  // Migrate declarations.
794
987
  for (const extractedQuery of sourceQueries) {
795
988
  const node = extractedQuery.node;
796
989
  const sf = node.getSourceFile();
797
990
  const descriptor = { key: extractedQuery.id, node: extractedQuery.node };
798
- if (!isMigratedQuery(descriptor)) {
799
- updateFileState(filesWithSourceQueries, sf, extractedQuery.kind);
991
+ const incompatibility = knownQueries.getIncompatibilityForField(descriptor);
992
+ updateFileState(filesWithSourceQueries, sf, extractedQuery.kind);
993
+ if (incompatibility !== null) {
994
+ // Add a TODO for the incompatible query, if desired.
995
+ if (this.config.insertTodosForSkippedFields) {
996
+ replacements.push(...migrate_ts_type_references.insertTodoForIncompatibility(node, info, incompatibility, {
997
+ single: 'query',
998
+ plural: 'queries',
999
+ }));
1000
+ }
800
1001
  updateFileState(filesWithIncompleteMigration, sf, extractedQuery.kind);
801
1002
  continue;
802
1003
  }
803
- updateFileState(filesWithSourceQueries, sf, extractedQuery.kind);
804
- replacements.push(...computeReplacementsToMigrateQuery(node, extractedQuery, importManager, info, printer));
1004
+ replacements.push(...computeReplacementsToMigrateQuery(node, extractedQuery, importManager, info, printer, info.userOptions, checker$1));
805
1005
  }
806
1006
  // Migrate references.
807
1007
  const referenceMigrationHost = {
808
1008
  printer,
809
1009
  replacements,
810
- shouldMigrateReferencesToField: (field) => isMigratedQuery(field),
811
- shouldMigrateReferencesToClass: (clazz) => !!knownQueries.getQueryFieldsOfClass(clazz)?.some((q) => isMigratedQuery(q)),
1010
+ shouldMigrateReferencesToField: (field) => !knownQueries.isFieldIncompatible(field),
1011
+ shouldMigrateReferencesToClass: (clazz) => !!knownQueries
1012
+ .getQueryFieldsOfClass(clazz)
1013
+ ?.some((q) => !knownQueries.isFieldIncompatible(q)),
812
1014
  };
813
- group_replacements.migrateTypeScriptReferences(referenceMigrationHost, referenceResult.references, checker$1, info);
1015
+ migrate_ts_type_references.migrateTypeScriptReferences(referenceMigrationHost, referenceResult.references, checker$1, info);
814
1016
  migrateTemplateReferences(referenceMigrationHost, referenceResult.references);
815
1017
  migrateHostBindings(referenceMigrationHost, referenceResult.references, info);
816
- group_replacements.migrateTypeScriptTypeReferences(referenceMigrationHost, referenceResult.references, importManager, info);
1018
+ migrate_ts_type_references.migrateTypeScriptTypeReferences(referenceMigrationHost, referenceResult.references, importManager, info);
817
1019
  // Fix problematic calls, like `QueryList#toArray`, or `QueryList#get`.
818
1020
  for (const ref of referenceResult.references) {
819
- removeQueryListToArrayCall(ref, info, globalMetadata, replacements);
820
- replaceQueryListGetCall(ref, info, globalMetadata, replacements);
821
- replaceQueryListFirstAndLastReferences(ref, info, globalMetadata, replacements);
1021
+ removeQueryListToArrayCall(ref, info, globalMetadata, knownQueries, replacements);
1022
+ replaceQueryListGetCall(ref, info, globalMetadata, knownQueries, replacements);
1023
+ replaceQueryListFirstAndLastReferences(ref, info, globalMetadata, knownQueries, replacements);
822
1024
  }
823
1025
  // Remove imports if possible.
824
1026
  for (const [file, types] of filesWithSourceQueries) {
@@ -837,12 +1039,48 @@ class SignalQueriesMigration extends group_replacements.TsurgeComplexMigration {
837
1039
  importManager.removeImport(file, 'QueryList', '@angular/core');
838
1040
  }
839
1041
  }
840
- group_replacements.applyImportManagerChanges(importManager, replacements, sourceFiles, info);
1042
+ combine_units.applyImportManagerChanges(importManager, replacements, sourceFiles, info);
841
1043
  return { replacements, knownQueries };
842
1044
  }
843
1045
  async stats(globalMetadata) {
844
- // TODO: Add statistics.
845
- return { counters: {} };
1046
+ let queriesCount = 0;
1047
+ let multiQueries = 0;
1048
+ let incompatibleQueries = 0;
1049
+ const fieldIncompatibleCounts = {};
1050
+ const classIncompatibleCounts = {};
1051
+ for (const query of Object.values(globalMetadata.knownQueryFields)) {
1052
+ queriesCount++;
1053
+ if (query.isMulti) {
1054
+ multiQueries++;
1055
+ }
1056
+ }
1057
+ for (const [id, info] of Object.entries(globalMetadata.problematicQueries)) {
1058
+ if (globalMetadata.knownQueryFields[id] === undefined) {
1059
+ continue;
1060
+ }
1061
+ incompatibleQueries++;
1062
+ if (info.classReason !== null) {
1063
+ const reasonName = migrate_ts_type_references.ClassIncompatibilityReason[info.classReason];
1064
+ const key = `incompat-class-${reasonName}`;
1065
+ classIncompatibleCounts[key] ??= 0;
1066
+ classIncompatibleCounts[key]++;
1067
+ }
1068
+ if (info.fieldReason !== null) {
1069
+ const reasonName = migrate_ts_type_references.FieldIncompatibilityReason[info.fieldReason];
1070
+ const key = `incompat-field-${reasonName}`;
1071
+ fieldIncompatibleCounts[key] ??= 0;
1072
+ fieldIncompatibleCounts[key]++;
1073
+ }
1074
+ }
1075
+ return {
1076
+ counters: {
1077
+ queriesCount,
1078
+ multiQueries,
1079
+ incompatibleQueries,
1080
+ ...fieldIncompatibleCounts,
1081
+ ...classIncompatibleCounts,
1082
+ },
1083
+ };
846
1084
  }
847
1085
  }
848
1086
  /**
@@ -862,11 +1100,13 @@ function migrate(options) {
862
1100
  return async (tree, context) => {
863
1101
  const { buildPaths, testPaths } = await project_tsconfig_paths.getProjectTsConfigPaths(tree);
864
1102
  if (!buildPaths.length && !testPaths.length) {
865
- throw new schematics.SchematicsException('Could not find any tsconfig file. Cannot run signal input migration.');
1103
+ throw new schematics.SchematicsException('Could not find any tsconfig file. Cannot run signal queries migration.');
866
1104
  }
867
- const fs = new group_replacements.DevkitMigrationFilesystem(tree);
1105
+ const fs = new combine_units.DevkitMigrationFilesystem(tree);
868
1106
  checker.setFileSystem(fs);
869
1107
  const migration = new SignalQueriesMigration({
1108
+ bestEffortMode: options.bestEffortMode,
1109
+ insertTodosForSkippedFields: options.insertTodos,
870
1110
  shouldMigrateQuery: (_query, file) => {
871
1111
  return (file.rootRelativePath.startsWith(fs.normalize(options.path)) &&
872
1112
  !/(^|\/)node_modules\//.test(file.rootRelativePath));
@@ -894,12 +1134,17 @@ function migrate(options) {
894
1134
  context.logger.info(``);
895
1135
  context.logger.info(`Processing analysis data between targets..`);
896
1136
  context.logger.info(``);
897
- const merged = await migration.merge(unitResults);
1137
+ const combined = await combine_units.synchronouslyCombineUnitData(migration, unitResults);
1138
+ if (combined === null) {
1139
+ context.logger.error('Migration failed unexpectedly with no analysis data');
1140
+ return;
1141
+ }
1142
+ const globalMeta = await migration.globalMeta(combined);
898
1143
  const replacementsPerFile = new Map();
899
1144
  for (const { info, tsconfigPath } of programInfos) {
900
1145
  context.logger.info(`Migrating: ${tsconfigPath}..`);
901
- const { replacements } = await migration.migrate(merged, info);
902
- const changesPerFile = group_replacements.groupReplacementsByFile(replacements);
1146
+ const { replacements } = await migration.migrate(globalMeta, info);
1147
+ const changesPerFile = combine_units.groupReplacementsByFile(replacements);
903
1148
  for (const [file, changes] of changesPerFile) {
904
1149
  if (!replacementsPerFile.has(file)) {
905
1150
  replacementsPerFile.set(file, changes);
@@ -918,6 +1163,19 @@ function migrate(options) {
918
1163
  }
919
1164
  context.logger.info('');
920
1165
  context.logger.info(`Successfully migrated to signal queries 🎉`);
1166
+ const { counters: { queriesCount, incompatibleQueries, multiQueries }, } = await migration.stats(globalMeta);
1167
+ const migratedQueries = queriesCount - incompatibleQueries;
1168
+ context.logger.info('');
1169
+ context.logger.info(`Successfully migrated to signal queries 🎉`);
1170
+ context.logger.info(` -> Migrated ${migratedQueries}/${queriesCount} queries.`);
1171
+ if (incompatibleQueries > 0 && !options.insertTodos) {
1172
+ context.logger.warn(`To see why ${incompatibleQueries} queries couldn't be migrated`);
1173
+ context.logger.warn(`consider re-running with "--insert-todos" or "--best-effort-mode".`);
1174
+ }
1175
+ if (options.bestEffortMode) {
1176
+ context.logger.warn(`You ran with best effort mode. Manually verify all code ` +
1177
+ `works as intended, and fix where necessary.`);
1178
+ }
921
1179
  };
922
1180
  }
923
1181