signal-queries-migration.cjs 52 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179
  1. 'use strict';
  2. /**
  3. * @license Angular v19.2.13
  4. * (c) 2010-2025 Google LLC. https://angular.io/
  5. * License: MIT
  6. */
  7. 'use strict';
  8. var checker = require('./checker-5pyJrZ9G.cjs');
  9. var ts = require('typescript');
  10. require('os');
  11. var index$1 = require('./index-BIvVb6in.cjs');
  12. require('path');
  13. var project_paths = require('./project_paths-CyWVEsbT.cjs');
  14. var apply_import_manager = require('./apply_import_manager-QQDfWa1Z.cjs');
  15. var migrate_ts_type_references = require('./migrate_ts_type_references-Czrg1gcB.cjs');
  16. var assert = require('assert');
  17. var index = require('./index-BPhQoCcF.cjs');
  18. require('@angular-devkit/core');
  19. require('node:path/posix');
  20. require('fs');
  21. require('module');
  22. require('url');
  23. require('@angular-devkit/schematics');
  24. require('./project_tsconfig_paths-CDVxT6Ov.cjs');
  25. require('./leading_space-D9nQ8UQC.cjs');
  26. /**
  27. * Phase that migrates Angular host binding references to
  28. * unwrap signals.
  29. */
  30. function migrateHostBindings(host, references, info) {
  31. const seenReferences = new WeakMap();
  32. for (const reference of references) {
  33. // This pass only deals with host binding references.
  34. if (!index.isHostBindingReference(reference)) {
  35. continue;
  36. }
  37. // Skip references to incompatible inputs.
  38. if (!host.shouldMigrateReferencesToField(reference.target)) {
  39. continue;
  40. }
  41. const bindingField = reference.from.hostPropertyNode;
  42. const expressionOffset = bindingField.getStart() + 1; // account for quotes.
  43. const readEndPos = expressionOffset + reference.from.read.sourceSpan.end;
  44. // Skip duplicate references. Can happen if the host object is shared.
  45. if (seenReferences.get(bindingField)?.has(readEndPos)) {
  46. continue;
  47. }
  48. if (seenReferences.has(bindingField)) {
  49. seenReferences.get(bindingField).add(readEndPos);
  50. }
  51. else {
  52. seenReferences.set(bindingField, new Set([readEndPos]));
  53. }
  54. // Expand shorthands like `{bla}` to `{bla: bla()}`.
  55. const appendText = reference.from.isObjectShorthandExpression
  56. ? `: ${reference.from.read.name}()`
  57. : `()`;
  58. host.replacements.push(new project_paths.Replacement(project_paths.projectFile(bindingField.getSourceFile(), info), new project_paths.TextUpdate({ position: readEndPos, end: readEndPos, toInsert: appendText })));
  59. }
  60. }
  61. /**
  62. * Phase that migrates Angular template references to
  63. * unwrap signals.
  64. */
  65. function migrateTemplateReferences(host, references) {
  66. const seenFileReferences = new Set();
  67. for (const reference of references) {
  68. // This pass only deals with HTML template references.
  69. if (!index.isTemplateReference(reference)) {
  70. continue;
  71. }
  72. // Skip references to incompatible inputs.
  73. if (!host.shouldMigrateReferencesToField(reference.target)) {
  74. continue;
  75. }
  76. // Skip duplicate references. E.g. if a template is shared.
  77. const fileReferenceId = `${reference.from.templateFile.id}:${reference.from.read.sourceSpan.end}`;
  78. if (seenFileReferences.has(fileReferenceId)) {
  79. continue;
  80. }
  81. seenFileReferences.add(fileReferenceId);
  82. // Expand shorthands like `{bla}` to `{bla: bla()}`.
  83. const appendText = reference.from.isObjectShorthandExpression
  84. ? `: ${reference.from.read.name}()`
  85. : `()`;
  86. host.replacements.push(new project_paths.Replacement(reference.from.templateFile, new project_paths.TextUpdate({
  87. position: reference.from.read.sourceSpan.end,
  88. end: reference.from.read.sourceSpan.end,
  89. toInsert: appendText,
  90. })));
  91. }
  92. }
  93. /**
  94. * Extracts the type `T` of expressions referencing `QueryList<T>`.
  95. */
  96. function extractQueryListType(node) {
  97. // Initializer variant of `new QueryList<T>()`.
  98. if (ts.isNewExpression(node) &&
  99. ts.isIdentifier(node.expression) &&
  100. node.expression.text === 'QueryList') {
  101. return node.typeArguments?.[0];
  102. }
  103. // Type variant of `: QueryList<T>`.
  104. if (ts.isTypeReferenceNode(node) &&
  105. ts.isIdentifier(node.typeName) &&
  106. node.typeName.text === 'QueryList') {
  107. return node.typeArguments?.[0];
  108. }
  109. return undefined;
  110. }
  111. /**
  112. * A few notes on changes:
  113. *
  114. * @ViewChild()
  115. * --> static is gone!
  116. * --> read stays
  117. *
  118. * @ViewChildren()
  119. * --> emitDistinctChangesOnly is gone!
  120. * --> read stays
  121. *
  122. * @ContentChild()
  123. * --> descendants stays
  124. * --> read stays
  125. * --> static is gone!
  126. *
  127. * @ContentChildren()
  128. * --> descendants stays
  129. * --> read stays
  130. * --> emitDistinctChangesOnly is gone!
  131. */
  132. function computeReplacementsToMigrateQuery(node, metadata, importManager, info, printer, options, checker$1) {
  133. const sf = node.getSourceFile();
  134. let newQueryFn = importManager.addImport({
  135. requestedFile: sf,
  136. exportModuleSpecifier: '@angular/core',
  137. exportSymbolName: metadata.kind,
  138. });
  139. // The default value for descendants is `true`, except for `ContentChildren`.
  140. const defaultDescendants = metadata.kind !== 'contentChildren';
  141. const optionProperties = [];
  142. const args = [
  143. metadata.args[0], // Locator.
  144. ];
  145. let type = node.type;
  146. // For multi queries, attempt to unwrap `QueryList` types, or infer the
  147. // type from the initializer, if possible.
  148. if (!metadata.queryInfo.first) {
  149. if (type === undefined && node.initializer !== undefined) {
  150. type = extractQueryListType(node.initializer);
  151. }
  152. else if (type !== undefined) {
  153. type = extractQueryListType(type);
  154. }
  155. }
  156. if (metadata.queryInfo.read !== null) {
  157. assert(metadata.queryInfo.read instanceof checker.WrappedNodeExpr);
  158. optionProperties.push(ts.factory.createPropertyAssignment('read', metadata.queryInfo.read.node));
  159. }
  160. if (metadata.queryInfo.descendants !== defaultDescendants) {
  161. optionProperties.push(ts.factory.createPropertyAssignment('descendants', metadata.queryInfo.descendants ? ts.factory.createTrue() : ts.factory.createFalse()));
  162. }
  163. if (optionProperties.length > 0) {
  164. args.push(ts.factory.createObjectLiteralExpression(optionProperties));
  165. }
  166. const strictNullChecksEnabled = options.strict === true || options.strictNullChecks === true;
  167. const strictPropertyInitialization = options.strict === true || options.strictPropertyInitialization === true;
  168. let isRequired = node.exclamationToken !== undefined;
  169. // If we come across an application with strict null checks enabled, but strict
  170. // property initialization is disabled, there are two options:
  171. // - Either the query is already typed to include `undefined` explicitly,
  172. // in which case an option query makes sense.
  173. // - OR, the query is not typed to include `undefined`. In which case, the query
  174. // should be marked as required to not break the app. The user-code throughout
  175. // the application (given strict null checks) already assumes non-nullable!
  176. if (strictNullChecksEnabled &&
  177. !strictPropertyInitialization &&
  178. node.initializer === undefined &&
  179. node.questionToken === undefined &&
  180. type !== undefined &&
  181. !checker$1.isTypeAssignableTo(checker$1.getUndefinedType(), checker$1.getTypeFromTypeNode(type))) {
  182. isRequired = true;
  183. }
  184. if (isRequired && metadata.queryInfo.first) {
  185. // If the query is required already via some indicators, and this is a "single"
  186. // query, use the available `.required` method.
  187. newQueryFn = ts.factory.createPropertyAccessExpression(newQueryFn, 'required');
  188. }
  189. // If this query is still nullable (i.e. not required), attempt to remove
  190. // explicit `undefined` types if possible.
  191. if (!isRequired && type !== undefined && ts.isUnionTypeNode(type)) {
  192. type = migrate_ts_type_references.removeFromUnionIfPossible(type, (v) => v.kind !== ts.SyntaxKind.UndefinedKeyword);
  193. }
  194. let locatorType = Array.isArray(metadata.queryInfo.predicate)
  195. ? null
  196. : metadata.queryInfo.predicate.expression;
  197. let resolvedReadType = metadata.queryInfo.read ?? locatorType;
  198. // If the original property type and the read type are matching, we can rely
  199. // on the TS inference, instead of repeating types, like in `viewChild<Button>(Button)`.
  200. if (type !== undefined &&
  201. resolvedReadType instanceof checker.WrappedNodeExpr &&
  202. ts.isIdentifier(resolvedReadType.node) &&
  203. ts.isTypeReferenceNode(type) &&
  204. ts.isIdentifier(type.typeName) &&
  205. type.typeName.text === resolvedReadType.node.text) {
  206. locatorType = null;
  207. }
  208. const call = ts.factory.createCallExpression(newQueryFn,
  209. // If there is no resolved `ReadT` (e.g. string predicate), we use the
  210. // original type explicitly as generic. Otherwise, query API is smart
  211. // enough to always infer.
  212. resolvedReadType === null && type !== undefined ? [type] : undefined, args);
  213. const accessibilityModifier = getAccessibilityModifier(node);
  214. let modifiers = [
  215. ts.factory.createModifier(ts.SyntaxKind.ReadonlyKeyword),
  216. ];
  217. if (accessibilityModifier) {
  218. modifiers = [accessibilityModifier, ...modifiers];
  219. }
  220. const updated = ts.factory.createPropertyDeclaration(modifiers, node.name, undefined, undefined, call);
  221. return [
  222. new project_paths.Replacement(project_paths.projectFile(node.getSourceFile(), info), new project_paths.TextUpdate({
  223. position: node.getStart(),
  224. end: node.getEnd(),
  225. toInsert: printer.printNode(ts.EmitHint.Unspecified, updated, sf),
  226. })),
  227. ];
  228. }
  229. function getAccessibilityModifier(node) {
  230. return node.modifiers?.find((mod) => mod.kind === ts.SyntaxKind.PublicKeyword ||
  231. mod.kind === ts.SyntaxKind.PrivateKeyword ||
  232. mod.kind === ts.SyntaxKind.ProtectedKeyword);
  233. }
  234. /**
  235. * Attempts to get a class field descriptor if the given symbol
  236. * points to a class field.
  237. */
  238. function getClassFieldDescriptorForSymbol(symbol, info) {
  239. if (symbol?.valueDeclaration === undefined ||
  240. !ts.isPropertyDeclaration(symbol.valueDeclaration)) {
  241. return null;
  242. }
  243. const key = getUniqueIDForClassProperty(symbol.valueDeclaration, info);
  244. if (key === null) {
  245. return null;
  246. }
  247. return {
  248. key,
  249. node: symbol.valueDeclaration,
  250. };
  251. }
  252. /**
  253. * Gets a unique ID for the given class property.
  254. *
  255. * This is useful for matching class fields across compilation units.
  256. * E.g. a reference may point to the field via `.d.ts`, while the other
  257. * may reference it via actual `.ts` sources. IDs for the same fields
  258. * would then match identity.
  259. */
  260. function getUniqueIDForClassProperty(property, info) {
  261. if (!ts.isClassDeclaration(property.parent) || property.parent.name === undefined) {
  262. return null;
  263. }
  264. if (property.name === undefined) {
  265. return null;
  266. }
  267. const id = project_paths.projectFile(property.getSourceFile(), info).id.replace(/\.d\.ts$/, '.ts');
  268. // Note: If a class is nested, there could be an ID clash.
  269. // This is highly unlikely though, and this is not a problem because
  270. // in such cases, there is even less chance there are any references to
  271. // a non-exported classes; in which case, cross-compilation unit references
  272. // likely can't exist anyway.
  273. return `${id}-${property.parent.name.text}-${property.name.getText()}`;
  274. }
  275. /**
  276. * Determines if the given node refers to a decorator-based query, and
  277. * returns its resolved metadata if possible.
  278. */
  279. function extractSourceQueryDefinition(node, reflector, evaluator, info) {
  280. if ((!ts.isPropertyDeclaration(node) && !ts.isAccessor(node)) ||
  281. !ts.isClassDeclaration(node.parent) ||
  282. node.parent.name === undefined ||
  283. !ts.isIdentifier(node.name)) {
  284. return null;
  285. }
  286. const decorators = reflector.getDecoratorsOfDeclaration(node) ?? [];
  287. const ngDecorators = checker.getAngularDecorators(decorators, checker.queryDecoratorNames, /* isCore */ false);
  288. if (ngDecorators.length === 0) {
  289. return null;
  290. }
  291. const decorator = ngDecorators[0];
  292. const id = getUniqueIDForClassProperty(node, info);
  293. if (id === null) {
  294. return null;
  295. }
  296. let kind;
  297. if (decorator.name === 'ViewChild') {
  298. kind = 'viewChild';
  299. }
  300. else if (decorator.name === 'ViewChildren') {
  301. kind = 'viewChildren';
  302. }
  303. else if (decorator.name === 'ContentChild') {
  304. kind = 'contentChild';
  305. }
  306. else if (decorator.name === 'ContentChildren') {
  307. kind = 'contentChildren';
  308. }
  309. else {
  310. throw new Error('Unexpected query decorator detected.');
  311. }
  312. let queryInfo = null;
  313. try {
  314. queryInfo = checker.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. }
  323. return {
  324. id,
  325. kind,
  326. args: decorator.args ?? [],
  327. queryInfo,
  328. node: node,
  329. fieldDecorators: decorators,
  330. };
  331. }
  332. function markFieldIncompatibleInMetadata(data, id, reason) {
  333. const existing = data[id];
  334. if (existing === undefined) {
  335. data[id] = {
  336. fieldReason: reason,
  337. classReason: null,
  338. };
  339. }
  340. else if (existing.fieldReason === null) {
  341. existing.fieldReason = reason;
  342. }
  343. else {
  344. existing.fieldReason = migrate_ts_type_references.pickFieldIncompatibility({ reason, context: null }, { reason: existing.fieldReason, context: null }).reason;
  345. }
  346. }
  347. function filterBestEffortIncompatibilities(knownQueries) {
  348. for (const query of Object.values(knownQueries.globalMetadata.problematicQueries)) {
  349. if (query.fieldReason !== null &&
  350. !migrate_ts_type_references.nonIgnorableFieldIncompatibilities.includes(query.fieldReason)) {
  351. query.fieldReason = null;
  352. }
  353. }
  354. }
  355. class KnownQueries {
  356. info;
  357. config;
  358. globalMetadata;
  359. classToQueryFields = new Map();
  360. knownQueryIDs = new Map();
  361. constructor(info, config, globalMetadata) {
  362. this.info = info;
  363. this.config = config;
  364. this.globalMetadata = globalMetadata;
  365. }
  366. isFieldIncompatible(descriptor) {
  367. return this.getIncompatibilityForField(descriptor) !== null;
  368. }
  369. markFieldIncompatible(field, incompatibility) {
  370. markFieldIncompatibleInMetadata(this.globalMetadata.problematicQueries, field.key, incompatibility.reason);
  371. }
  372. markClassIncompatible(node, reason) {
  373. this.classToQueryFields.get(node)?.forEach((f) => {
  374. this.globalMetadata.problematicQueries[f.key] ??= { classReason: null, fieldReason: null };
  375. this.globalMetadata.problematicQueries[f.key].classReason = reason;
  376. });
  377. }
  378. registerQueryField(queryField, id) {
  379. if (!this.classToQueryFields.has(queryField.parent)) {
  380. this.classToQueryFields.set(queryField.parent, []);
  381. }
  382. this.classToQueryFields.get(queryField.parent).push({
  383. key: id,
  384. node: queryField,
  385. });
  386. this.knownQueryIDs.set(id, { key: id, node: queryField });
  387. const descriptor = { key: id, node: queryField };
  388. const file = project_paths.projectFile(queryField.getSourceFile(), this.info);
  389. if (this.config.shouldMigrateQuery !== undefined &&
  390. !this.config.shouldMigrateQuery(descriptor, file)) {
  391. this.markFieldIncompatible(descriptor, {
  392. context: null,
  393. reason: migrate_ts_type_references.FieldIncompatibilityReason.SkippedViaConfigFilter,
  394. });
  395. }
  396. }
  397. attemptRetrieveDescriptorFromSymbol(symbol) {
  398. const descriptor = getClassFieldDescriptorForSymbol(symbol, this.info);
  399. if (descriptor !== null && this.knownQueryIDs.has(descriptor.key)) {
  400. return descriptor;
  401. }
  402. return null;
  403. }
  404. shouldTrackClassReference(clazz) {
  405. return this.classToQueryFields.has(clazz);
  406. }
  407. getQueryFieldsOfClass(clazz) {
  408. return this.classToQueryFields.get(clazz);
  409. }
  410. getAllClassesWithQueries() {
  411. return Array.from(this.classToQueryFields.keys()).filter((c) => ts.isClassDeclaration(c));
  412. }
  413. captureKnownFieldInheritanceRelationship(derived, parent) {
  414. // Note: The edge problematic pattern recognition is not as good as the one
  415. // we have in the signal input migration. That is because we couldn't trivially
  416. // build up an inheritance graph during analyze phase where we DON'T know what
  417. // fields refer to queries. Usually we'd use the graph to smartly propagate
  418. // incompatibilities using topological sort. This doesn't work here and is
  419. // unnecessarily complex, so we try our best at detecting direct edge
  420. // incompatibilities (which are quite order dependent).
  421. if (this.isFieldIncompatible(parent) && !this.isFieldIncompatible(derived)) {
  422. this.markFieldIncompatible(derived, {
  423. context: null,
  424. reason: migrate_ts_type_references.FieldIncompatibilityReason.ParentIsIncompatible,
  425. });
  426. return;
  427. }
  428. if (this.isFieldIncompatible(derived) && !this.isFieldIncompatible(parent)) {
  429. this.markFieldIncompatible(parent, {
  430. context: null,
  431. reason: migrate_ts_type_references.FieldIncompatibilityReason.DerivedIsIncompatible,
  432. });
  433. }
  434. }
  435. captureUnknownDerivedField(field) {
  436. this.markFieldIncompatible(field, {
  437. context: null,
  438. reason: migrate_ts_type_references.FieldIncompatibilityReason.OverriddenByDerivedClass,
  439. });
  440. }
  441. captureUnknownParentField(field) {
  442. this.markFieldIncompatible(field, {
  443. context: null,
  444. reason: migrate_ts_type_references.FieldIncompatibilityReason.TypeConflictWithBaseClass,
  445. });
  446. }
  447. getIncompatibilityForField(descriptor) {
  448. const problematicInfo = this.globalMetadata.problematicQueries[descriptor.key];
  449. if (problematicInfo === undefined) {
  450. return null;
  451. }
  452. if (problematicInfo.fieldReason !== null) {
  453. return { context: null, reason: problematicInfo.fieldReason };
  454. }
  455. if (problematicInfo.classReason !== null) {
  456. return problematicInfo.classReason;
  457. }
  458. return null;
  459. }
  460. getIncompatibilityTextForField(field) {
  461. const incompatibilityInfo = this.globalMetadata.problematicQueries[field.key];
  462. if (incompatibilityInfo.fieldReason !== null) {
  463. return migrate_ts_type_references.getMessageForFieldIncompatibility(incompatibilityInfo.fieldReason, {
  464. single: 'query',
  465. plural: 'queries',
  466. });
  467. }
  468. if (incompatibilityInfo.classReason !== null) {
  469. return migrate_ts_type_references.getMessageForClassIncompatibility(incompatibilityInfo.classReason, {
  470. single: 'query',
  471. plural: 'queries',
  472. });
  473. }
  474. return null;
  475. }
  476. }
  477. /** Converts an initializer query API name to its decorator-equivalent. */
  478. function queryFunctionNameToDecorator(name) {
  479. if (name === 'viewChild') {
  480. return 'ViewChild';
  481. }
  482. else if (name === 'viewChildren') {
  483. return 'ViewChildren';
  484. }
  485. else if (name === 'contentChild') {
  486. return 'ContentChild';
  487. }
  488. else if (name === 'contentChildren') {
  489. return 'ContentChildren';
  490. }
  491. throw new Error(`Unexpected query function name: ${name}`);
  492. }
  493. /**
  494. * Gets whether the given field is accessed via the
  495. * given reference.
  496. *
  497. * E.g. whether `<my-read>.toArray` is detected.
  498. */
  499. function checkTsReferenceAccessesField(ref, fieldName) {
  500. const accessNode = index.traverseAccess(ref.from.node);
  501. // Check if the reference is part of a property access.
  502. if (!ts.isPropertyAccessExpression(accessNode.parent) ||
  503. !ts.isIdentifier(accessNode.parent.name)) {
  504. return null;
  505. }
  506. // Check if the reference is refers to the given field name.
  507. if (accessNode.parent.name.text !== fieldName) {
  508. return null;
  509. }
  510. return accessNode.parent;
  511. }
  512. /**
  513. * Gets whether the given read is used to access
  514. * the specified field.
  515. *
  516. * E.g. whether `<my-read>.toArray` is detected.
  517. */
  518. function checkNonTsReferenceAccessesField(ref, fieldName) {
  519. const readFromPath = ref.from.readAstPath.at(-1);
  520. const parentRead = ref.from.readAstPath.at(-2);
  521. if (ref.from.read !== readFromPath) {
  522. return null;
  523. }
  524. if (!(parentRead instanceof checker.PropertyRead) || parentRead.name !== fieldName) {
  525. return null;
  526. }
  527. return parentRead;
  528. }
  529. /**
  530. * Gets whether the given reference is accessed to call the
  531. * specified function on it.
  532. *
  533. * E.g. whether `<my-read>.toArray()` is detected.
  534. */
  535. function checkTsReferenceCallsField(ref, fieldName) {
  536. const propertyAccess = checkTsReferenceAccessesField(ref, fieldName);
  537. if (propertyAccess === null) {
  538. return null;
  539. }
  540. if (ts.isCallExpression(propertyAccess.parent) &&
  541. propertyAccess.parent.expression === propertyAccess) {
  542. return propertyAccess.parent;
  543. }
  544. return null;
  545. }
  546. /**
  547. * Gets whether the given reference is accessed to call the
  548. * specified function on it.
  549. *
  550. * E.g. whether `<my-read>.toArray()` is detected.
  551. */
  552. function checkNonTsReferenceCallsField(ref, fieldName) {
  553. const propertyAccess = checkNonTsReferenceAccessesField(ref, fieldName);
  554. if (propertyAccess === null) {
  555. return null;
  556. }
  557. const accessIdx = ref.from.readAstPath.indexOf(propertyAccess);
  558. if (accessIdx === -1) {
  559. return null;
  560. }
  561. const potentialCall = ref.from.readAstPath[accessIdx - 1];
  562. if (potentialCall === undefined || !(potentialCall instanceof checker.Call)) {
  563. return null;
  564. }
  565. return potentialCall;
  566. }
  567. function removeQueryListToArrayCall(ref, info, globalMetadata, knownQueries, replacements) {
  568. if (!index.isHostBindingReference(ref) && !index.isTemplateReference(ref) && !index.isTsReference(ref)) {
  569. return;
  570. }
  571. if (knownQueries.isFieldIncompatible(ref.target)) {
  572. return;
  573. }
  574. if (!globalMetadata.knownQueryFields[ref.target.key]?.isMulti) {
  575. return;
  576. }
  577. // TS references.
  578. if (index.isTsReference(ref)) {
  579. const toArrayCallExpr = checkTsReferenceCallsField(ref, 'toArray');
  580. if (toArrayCallExpr === null) {
  581. return;
  582. }
  583. const toArrayExpr = toArrayCallExpr.expression;
  584. replacements.push(new project_paths.Replacement(project_paths.projectFile(toArrayExpr.getSourceFile(), info), new project_paths.TextUpdate({
  585. // Delete from expression end to call end. E.g. `.toArray(<..>)`.
  586. position: toArrayExpr.expression.getEnd(),
  587. end: toArrayCallExpr.getEnd(),
  588. toInsert: '',
  589. })));
  590. return;
  591. }
  592. // Template and host binding references.
  593. const callExpr = checkNonTsReferenceCallsField(ref, 'toArray');
  594. if (callExpr === null) {
  595. return;
  596. }
  597. const file = index.isHostBindingReference(ref) ? ref.from.file : ref.from.templateFile;
  598. const offset = index.isHostBindingReference(ref) ? ref.from.hostPropertyNode.getStart() + 1 : 0;
  599. replacements.push(new project_paths.Replacement(file, new project_paths.TextUpdate({
  600. // Delete from expression end to call end. E.g. `.toArray(<..>)`.
  601. position: offset + callExpr.receiver.receiver.sourceSpan.end,
  602. end: offset + callExpr.sourceSpan.end,
  603. toInsert: '',
  604. })));
  605. }
  606. function replaceQueryListGetCall(ref, info, globalMetadata, knownQueries, replacements) {
  607. if (!index.isHostBindingReference(ref) && !index.isTemplateReference(ref) && !index.isTsReference(ref)) {
  608. return;
  609. }
  610. if (knownQueries.isFieldIncompatible(ref.target)) {
  611. return;
  612. }
  613. if (!globalMetadata.knownQueryFields[ref.target.key]?.isMulti) {
  614. return;
  615. }
  616. if (index.isTsReference(ref)) {
  617. const getCallExpr = checkTsReferenceCallsField(ref, 'get');
  618. if (getCallExpr === null) {
  619. return;
  620. }
  621. const getExpr = getCallExpr.expression;
  622. replacements.push(new project_paths.Replacement(project_paths.projectFile(getExpr.getSourceFile(), info), new project_paths.TextUpdate({
  623. position: getExpr.name.getStart(),
  624. end: getExpr.name.getEnd(),
  625. toInsert: 'at',
  626. })));
  627. return;
  628. }
  629. // Template and host binding references.
  630. const callExpr = checkNonTsReferenceCallsField(ref, 'get');
  631. if (callExpr === null) {
  632. return;
  633. }
  634. const file = index.isHostBindingReference(ref) ? ref.from.file : ref.from.templateFile;
  635. const offset = index.isHostBindingReference(ref) ? ref.from.hostPropertyNode.getStart() + 1 : 0;
  636. replacements.push(new project_paths.Replacement(file, new project_paths.TextUpdate({
  637. position: offset + callExpr.receiver.nameSpan.start,
  638. end: offset + callExpr.receiver.nameSpan.end,
  639. toInsert: 'at',
  640. })));
  641. }
  642. const problematicQueryListMethods = [
  643. 'dirty',
  644. 'changes',
  645. 'setDirty',
  646. 'reset',
  647. 'notifyOnChanges',
  648. 'destroy',
  649. ];
  650. function checkForIncompatibleQueryListAccesses(ref, result) {
  651. if (index.isTsReference(ref)) {
  652. for (const problematicFn of problematicQueryListMethods) {
  653. const access = checkTsReferenceAccessesField(ref, problematicFn);
  654. if (access !== null) {
  655. result.potentialProblematicReferenceForMultiQueries[ref.target.key] = true;
  656. return;
  657. }
  658. }
  659. }
  660. if (index.isHostBindingReference(ref) || index.isTemplateReference(ref)) {
  661. for (const problematicFn of problematicQueryListMethods) {
  662. const access = checkNonTsReferenceAccessesField(ref, problematicFn);
  663. if (access !== null) {
  664. result.potentialProblematicReferenceForMultiQueries[ref.target.key] = true;
  665. return;
  666. }
  667. }
  668. }
  669. }
  670. const mapping = new Map([
  671. ['first', 'at(0)!'],
  672. ['last', 'at(-1)!'],
  673. ]);
  674. function replaceQueryListFirstAndLastReferences(ref, info, globalMetadata, knownQueries, replacements) {
  675. if (!index.isHostBindingReference(ref) && !index.isTemplateReference(ref) && !index.isTsReference(ref)) {
  676. return;
  677. }
  678. if (knownQueries.isFieldIncompatible(ref.target)) {
  679. return;
  680. }
  681. if (!globalMetadata.knownQueryFields[ref.target.key]?.isMulti) {
  682. return;
  683. }
  684. if (index.isTsReference(ref)) {
  685. const expr = checkTsReferenceAccessesField(ref, 'first') ?? checkTsReferenceAccessesField(ref, 'last');
  686. if (expr === null) {
  687. return;
  688. }
  689. replacements.push(new project_paths.Replacement(project_paths.projectFile(expr.getSourceFile(), info), new project_paths.TextUpdate({
  690. position: expr.name.getStart(),
  691. end: expr.name.getEnd(),
  692. toInsert: mapping.get(expr.name.text),
  693. })));
  694. return;
  695. }
  696. // Template and host binding references.
  697. const expr = checkNonTsReferenceAccessesField(ref, 'first') ?? checkNonTsReferenceAccessesField(ref, 'last');
  698. if (expr === null) {
  699. return;
  700. }
  701. const file = index.isHostBindingReference(ref) ? ref.from.file : ref.from.templateFile;
  702. const offset = index.isHostBindingReference(ref) ? ref.from.hostPropertyNode.getStart() + 1 : 0;
  703. replacements.push(new project_paths.Replacement(file, new project_paths.TextUpdate({
  704. position: offset + expr.nameSpan.start,
  705. end: offset + expr.nameSpan.end,
  706. toInsert: mapping.get(expr.name),
  707. })));
  708. }
  709. class SignalQueriesMigration extends project_paths.TsurgeComplexMigration {
  710. config;
  711. constructor(config = {}) {
  712. super();
  713. this.config = config;
  714. }
  715. async analyze(info) {
  716. // Pre-Analyze the program and get access to the template type checker.
  717. const { templateTypeChecker } = info.ngCompiler?.['ensureAnalyzed']() ?? {
  718. templateTypeChecker: null,
  719. };
  720. const resourceLoader = info.ngCompiler?.['resourceManager'] ?? null;
  721. // Generate all type check blocks, if we have Angular template information.
  722. if (templateTypeChecker !== null) {
  723. templateTypeChecker.generateAllTypeCheckBlocks();
  724. }
  725. const { sourceFiles, program } = info;
  726. const checker$1 = program.getTypeChecker();
  727. const reflector = new checker.TypeScriptReflectionHost(checker$1);
  728. const evaluator = new index$1.PartialEvaluator(reflector, checker$1, null);
  729. const res = {
  730. knownQueryFields: {},
  731. potentialProblematicQueries: {},
  732. potentialProblematicReferenceForMultiQueries: {},
  733. reusableAnalysisReferences: null,
  734. };
  735. const groupedAstVisitor = new migrate_ts_type_references.GroupedTsAstVisitor(sourceFiles);
  736. const referenceResult = { references: [] };
  737. const classesWithFilteredQueries = new Set();
  738. const filteredQueriesForCompilationUnit = new Map();
  739. const findQueryDefinitionsVisitor = (node) => {
  740. const extractedQuery = extractSourceQueryDefinition(node, reflector, evaluator, info);
  741. if (extractedQuery !== null) {
  742. const queryNode = extractedQuery.node;
  743. const descriptor = {
  744. key: extractedQuery.id,
  745. node: queryNode,
  746. };
  747. const containingFile = project_paths.projectFile(queryNode.getSourceFile(), info);
  748. // If we have a config filter function, use it here for later
  749. // perf-boosted reference lookups. Useful in non-batch mode.
  750. if (this.config.shouldMigrateQuery === undefined ||
  751. this.config.shouldMigrateQuery(descriptor, containingFile)) {
  752. classesWithFilteredQueries.add(queryNode.parent);
  753. filteredQueriesForCompilationUnit.set(extractedQuery.id, {
  754. fieldName: extractedQuery.queryInfo.propertyName,
  755. });
  756. }
  757. res.knownQueryFields[extractedQuery.id] = {
  758. fieldName: extractedQuery.queryInfo.propertyName,
  759. isMulti: extractedQuery.queryInfo.first === false,
  760. };
  761. if (ts.isAccessor(queryNode)) {
  762. markFieldIncompatibleInMetadata(res.potentialProblematicQueries, extractedQuery.id, migrate_ts_type_references.FieldIncompatibilityReason.Accessor);
  763. }
  764. // Detect queries with union types that are uncommon to be
  765. // automatically migrate-able. E.g. `refs: ElementRef|null`,
  766. // or `ElementRef|SomeOtherType`.
  767. if (queryNode.type !== undefined &&
  768. ts.isUnionTypeNode(queryNode.type) &&
  769. // Either too large union, or doesn't match `T|undefined`.
  770. (queryNode.type.types.length > 2 ||
  771. !queryNode.type.types.some((t) => t.kind === ts.SyntaxKind.UndefinedKeyword))) {
  772. markFieldIncompatibleInMetadata(res.potentialProblematicQueries, extractedQuery.id, migrate_ts_type_references.FieldIncompatibilityReason.SignalQueries__IncompatibleMultiUnionType);
  773. }
  774. // Migrating fields with `@HostBinding` is incompatible as
  775. // the host binding decorator does not invoke the signal.
  776. const hostBindingDecorators = checker.getAngularDecorators(extractedQuery.fieldDecorators, ['HostBinding'],
  777. /* isCore */ false);
  778. if (hostBindingDecorators.length > 0) {
  779. markFieldIncompatibleInMetadata(res.potentialProblematicQueries, extractedQuery.id, migrate_ts_type_references.FieldIncompatibilityReason.SignalIncompatibleWithHostBinding);
  780. }
  781. }
  782. };
  783. this.config.reportProgressFn?.(20, 'Scanning for queries..');
  784. groupedAstVisitor.register(findQueryDefinitionsVisitor);
  785. groupedAstVisitor.execute();
  786. const allFieldsOrKnownQueries = {
  787. // Note: We don't support cross-target migration of `Partial<T>` usages.
  788. // This is an acceptable limitation for performance reasons.
  789. shouldTrackClassReference: (node) => classesWithFilteredQueries.has(node),
  790. attemptRetrieveDescriptorFromSymbol: (s) => {
  791. const descriptor = getClassFieldDescriptorForSymbol(s, info);
  792. // If we are executing in upgraded analysis phase mode, we know all
  793. // of the queries since there aren't any other compilation units.
  794. // Ignore references to non-query class fields.
  795. if (this.config.assumeNonBatch &&
  796. (descriptor === null || !filteredQueriesForCompilationUnit.has(descriptor.key))) {
  797. return null;
  798. }
  799. // In batch mode, we eagerly, rather expensively, track all references.
  800. // We don't know yet if something refers to a different query or not, so we
  801. // eagerly detect such and later filter those problematic references that
  802. // turned out to refer to queries (once we have the global metadata).
  803. return descriptor;
  804. },
  805. };
  806. groupedAstVisitor.register(index.createFindAllSourceFileReferencesVisitor(info, checker$1, reflector, resourceLoader, evaluator, templateTypeChecker, allFieldsOrKnownQueries,
  807. // In non-batch mode, we know what inputs exist and can optimize the reference
  808. // resolution significantly (for e.g. VSCode integration)— as we know what
  809. // field names may be used to reference potential queries.
  810. this.config.assumeNonBatch
  811. ? new Set(Array.from(filteredQueriesForCompilationUnit.values()).map((f) => f.fieldName))
  812. : null, referenceResult).visitor);
  813. const inheritanceGraph = new migrate_ts_type_references.InheritanceGraph(checker$1).expensivePopulate(info.sourceFiles);
  814. migrate_ts_type_references.checkIncompatiblePatterns(inheritanceGraph, checker$1, groupedAstVisitor, {
  815. ...allFieldsOrKnownQueries,
  816. isFieldIncompatible: (f) => res.potentialProblematicQueries[f.key]?.fieldReason !== null ||
  817. res.potentialProblematicQueries[f.key]?.classReason !== null,
  818. markClassIncompatible: (clazz, reason) => {
  819. for (const field of clazz.members) {
  820. const key = getUniqueIDForClassProperty(field, info);
  821. if (key !== null) {
  822. res.potentialProblematicQueries[key] ??= { classReason: null, fieldReason: null };
  823. res.potentialProblematicQueries[key].classReason = reason;
  824. }
  825. }
  826. },
  827. markFieldIncompatible: (f, incompatibility) => markFieldIncompatibleInMetadata(res.potentialProblematicQueries, f.key, incompatibility.reason),
  828. }, () => Array.from(classesWithFilteredQueries));
  829. this.config.reportProgressFn?.(60, 'Scanning for references and problematic patterns..');
  830. groupedAstVisitor.execute();
  831. // Determine incompatible queries based on problematic references
  832. // we saw in TS code, templates or host bindings.
  833. for (const ref of referenceResult.references) {
  834. if (index.isTsReference(ref) && ref.from.isWrite) {
  835. markFieldIncompatibleInMetadata(res.potentialProblematicQueries, ref.target.key, migrate_ts_type_references.FieldIncompatibilityReason.WriteAssignment);
  836. }
  837. if ((index.isTemplateReference(ref) || index.isHostBindingReference(ref)) && ref.from.isWrite) {
  838. markFieldIncompatibleInMetadata(res.potentialProblematicQueries, ref.target.key, migrate_ts_type_references.FieldIncompatibilityReason.WriteAssignment);
  839. }
  840. // TODO: Remove this when we support signal narrowing in templates.
  841. // https://github.com/angular/angular/pull/55456.
  842. if (index.isTemplateReference(ref) && ref.from.isLikelyPartOfNarrowing) {
  843. markFieldIncompatibleInMetadata(res.potentialProblematicQueries, ref.target.key, migrate_ts_type_references.FieldIncompatibilityReason.PotentiallyNarrowedInTemplateButNoSupportYet);
  844. }
  845. // Check for other incompatible query list accesses.
  846. checkForIncompatibleQueryListAccesses(ref, res);
  847. }
  848. if (this.config.assumeNonBatch) {
  849. res.reusableAnalysisReferences = referenceResult.references;
  850. }
  851. return project_paths.confirmAsSerializable(res);
  852. }
  853. async combine(unitA, unitB) {
  854. const combined = {
  855. knownQueryFields: {},
  856. potentialProblematicQueries: {},
  857. potentialProblematicReferenceForMultiQueries: {},
  858. reusableAnalysisReferences: null,
  859. };
  860. for (const unit of [unitA, unitB]) {
  861. for (const [id, value] of Object.entries(unit.knownQueryFields)) {
  862. combined.knownQueryFields[id] = value;
  863. }
  864. for (const [id, info] of Object.entries(unit.potentialProblematicQueries)) {
  865. if (info.fieldReason !== null) {
  866. markFieldIncompatibleInMetadata(combined.potentialProblematicQueries, id, info.fieldReason);
  867. }
  868. if (info.classReason !== null) {
  869. combined.potentialProblematicQueries[id] ??= {
  870. classReason: null,
  871. fieldReason: null,
  872. };
  873. combined.potentialProblematicQueries[id].classReason =
  874. info.classReason;
  875. }
  876. }
  877. for (const id of Object.keys(unit.potentialProblematicReferenceForMultiQueries)) {
  878. combined.potentialProblematicReferenceForMultiQueries[id] = true;
  879. }
  880. if (unit.reusableAnalysisReferences !== null) {
  881. combined.reusableAnalysisReferences = unit.reusableAnalysisReferences;
  882. }
  883. }
  884. for (const unit of [unitA, unitB]) {
  885. for (const id of Object.keys(unit.potentialProblematicReferenceForMultiQueries)) {
  886. if (combined.knownQueryFields[id]?.isMulti) {
  887. markFieldIncompatibleInMetadata(combined.potentialProblematicQueries, id, migrate_ts_type_references.FieldIncompatibilityReason.SignalQueries__QueryListProblematicFieldAccessed);
  888. }
  889. }
  890. }
  891. return project_paths.confirmAsSerializable(combined);
  892. }
  893. async globalMeta(combinedData) {
  894. const globalUnitData = {
  895. knownQueryFields: combinedData.knownQueryFields,
  896. problematicQueries: combinedData.potentialProblematicQueries,
  897. reusableAnalysisReferences: combinedData.reusableAnalysisReferences,
  898. };
  899. for (const id of Object.keys(combinedData.potentialProblematicReferenceForMultiQueries)) {
  900. if (combinedData.knownQueryFields[id]?.isMulti) {
  901. markFieldIncompatibleInMetadata(globalUnitData.problematicQueries, id, migrate_ts_type_references.FieldIncompatibilityReason.SignalQueries__QueryListProblematicFieldAccessed);
  902. }
  903. }
  904. return project_paths.confirmAsSerializable(globalUnitData);
  905. }
  906. async migrate(globalMetadata, info) {
  907. // Pre-Analyze the program and get access to the template type checker.
  908. const { templateTypeChecker, metaReader } = info.ngCompiler?.['ensureAnalyzed']() ?? {
  909. templateTypeChecker: null,
  910. metaReader: null,
  911. };
  912. const resourceLoader = info.ngCompiler?.['resourceManager'] ?? null;
  913. const { program, sourceFiles } = info;
  914. const checker$1 = program.getTypeChecker();
  915. const reflector = new checker.TypeScriptReflectionHost(checker$1);
  916. const evaluator = new index$1.PartialEvaluator(reflector, checker$1, null);
  917. const replacements = [];
  918. const importManager = new checker.ImportManager();
  919. const printer = ts.createPrinter();
  920. const filesWithSourceQueries = new Map();
  921. const filesWithIncompleteMigration = new Map();
  922. const filesWithQueryListOutsideOfDeclarations = new WeakSet();
  923. const knownQueries = new KnownQueries(info, this.config, globalMetadata);
  924. const referenceResult = { references: [] };
  925. const sourceQueries = [];
  926. // Detect all queries in this unit.
  927. const queryWholeProgramVisitor = (node) => {
  928. // Detect all SOURCE queries and migrate them, if possible.
  929. const extractedQuery = extractSourceQueryDefinition(node, reflector, evaluator, info);
  930. if (extractedQuery !== null) {
  931. knownQueries.registerQueryField(extractedQuery.node, extractedQuery.id);
  932. sourceQueries.push(extractedQuery);
  933. return;
  934. }
  935. // Detect OTHER queries, inside `.d.ts`. Needed for reference resolution below.
  936. if (ts.isPropertyDeclaration(node) ||
  937. (ts.isAccessor(node) && ts.isClassDeclaration(node.parent))) {
  938. const classFieldID = getUniqueIDForClassProperty(node, info);
  939. if (classFieldID !== null && globalMetadata.knownQueryFields[classFieldID] !== undefined) {
  940. knownQueries.registerQueryField(node, classFieldID);
  941. return;
  942. }
  943. }
  944. // Detect potential usages of `QueryList` outside of queries or imports.
  945. // Those prevent us from removing the import later.
  946. if (ts.isIdentifier(node) &&
  947. node.text === 'QueryList' &&
  948. ts.findAncestor(node, ts.isImportDeclaration) === undefined) {
  949. filesWithQueryListOutsideOfDeclarations.add(node.getSourceFile());
  950. }
  951. ts.forEachChild(node, queryWholeProgramVisitor);
  952. };
  953. for (const sf of info.fullProgramSourceFiles) {
  954. ts.forEachChild(sf, queryWholeProgramVisitor);
  955. }
  956. // Set of all queries in the program. Useful for speeding up reference
  957. // lookups below.
  958. const fieldNamesToConsiderForReferenceLookup = new Set(Object.values(globalMetadata.knownQueryFields).map((f) => f.fieldName));
  959. // Find all references.
  960. const groupedAstVisitor = new migrate_ts_type_references.GroupedTsAstVisitor(sourceFiles);
  961. // Re-use previous reference result if available, instead of
  962. // looking for references which is quite expensive.
  963. if (globalMetadata.reusableAnalysisReferences !== null) {
  964. referenceResult.references = globalMetadata.reusableAnalysisReferences;
  965. }
  966. else {
  967. groupedAstVisitor.register(index.createFindAllSourceFileReferencesVisitor(info, checker$1, reflector, resourceLoader, evaluator, templateTypeChecker, knownQueries, fieldNamesToConsiderForReferenceLookup, referenceResult).visitor);
  968. }
  969. // Check inheritance.
  970. // NOTE: Inheritance is only checked in the migrate stage as we cannot reliably
  971. // check during analyze— where we don't know what fields from foreign `.d.ts`
  972. // files refer to queries or not.
  973. const inheritanceGraph = new migrate_ts_type_references.InheritanceGraph(checker$1).expensivePopulate(info.sourceFiles);
  974. migrate_ts_type_references.checkInheritanceOfKnownFields(inheritanceGraph, metaReader, knownQueries, {
  975. getFieldsForClass: (n) => knownQueries.getQueryFieldsOfClass(n) ?? [],
  976. isClassWithKnownFields: (clazz) => knownQueries.getQueryFieldsOfClass(clazz) !== undefined,
  977. });
  978. this.config.reportProgressFn?.(80, 'Checking inheritance..');
  979. groupedAstVisitor.execute();
  980. if (this.config.bestEffortMode) {
  981. filterBestEffortIncompatibilities(knownQueries);
  982. }
  983. this.config.reportProgressFn?.(90, 'Migrating queries..');
  984. // Migrate declarations.
  985. for (const extractedQuery of sourceQueries) {
  986. const node = extractedQuery.node;
  987. const sf = node.getSourceFile();
  988. const descriptor = { key: extractedQuery.id, node: extractedQuery.node };
  989. const incompatibility = knownQueries.getIncompatibilityForField(descriptor);
  990. updateFileState(filesWithSourceQueries, sf, extractedQuery.kind);
  991. if (incompatibility !== null) {
  992. // Add a TODO for the incompatible query, if desired.
  993. if (this.config.insertTodosForSkippedFields) {
  994. replacements.push(...migrate_ts_type_references.insertTodoForIncompatibility(node, info, incompatibility, {
  995. single: 'query',
  996. plural: 'queries',
  997. }));
  998. }
  999. updateFileState(filesWithIncompleteMigration, sf, extractedQuery.kind);
  1000. continue;
  1001. }
  1002. replacements.push(...computeReplacementsToMigrateQuery(node, extractedQuery, importManager, info, printer, info.userOptions, checker$1));
  1003. }
  1004. // Migrate references.
  1005. const referenceMigrationHost = {
  1006. printer,
  1007. replacements,
  1008. shouldMigrateReferencesToField: (field) => !knownQueries.isFieldIncompatible(field),
  1009. shouldMigrateReferencesToClass: (clazz) => !!knownQueries
  1010. .getQueryFieldsOfClass(clazz)
  1011. ?.some((q) => !knownQueries.isFieldIncompatible(q)),
  1012. };
  1013. migrate_ts_type_references.migrateTypeScriptReferences(referenceMigrationHost, referenceResult.references, checker$1, info);
  1014. migrateTemplateReferences(referenceMigrationHost, referenceResult.references);
  1015. migrateHostBindings(referenceMigrationHost, referenceResult.references, info);
  1016. migrate_ts_type_references.migrateTypeScriptTypeReferences(referenceMigrationHost, referenceResult.references, importManager, info);
  1017. // Fix problematic calls, like `QueryList#toArray`, or `QueryList#get`.
  1018. for (const ref of referenceResult.references) {
  1019. removeQueryListToArrayCall(ref, info, globalMetadata, knownQueries, replacements);
  1020. replaceQueryListGetCall(ref, info, globalMetadata, knownQueries, replacements);
  1021. replaceQueryListFirstAndLastReferences(ref, info, globalMetadata, knownQueries, replacements);
  1022. }
  1023. // Remove imports if possible.
  1024. for (const [file, types] of filesWithSourceQueries) {
  1025. let seenIncompatibleMultiQuery = false;
  1026. for (const type of types) {
  1027. const incompatibleQueryTypesForFile = filesWithIncompleteMigration.get(file);
  1028. // Query type is fully migrated. No incompatible queries in file.
  1029. if (!incompatibleQueryTypesForFile?.has(type)) {
  1030. importManager.removeImport(file, queryFunctionNameToDecorator(type), '@angular/core');
  1031. }
  1032. else if (type === 'viewChildren' || type === 'contentChildren') {
  1033. seenIncompatibleMultiQuery = true;
  1034. }
  1035. }
  1036. if (!seenIncompatibleMultiQuery && !filesWithQueryListOutsideOfDeclarations.has(file)) {
  1037. importManager.removeImport(file, 'QueryList', '@angular/core');
  1038. }
  1039. }
  1040. apply_import_manager.applyImportManagerChanges(importManager, replacements, sourceFiles, info);
  1041. return { replacements, knownQueries };
  1042. }
  1043. async stats(globalMetadata) {
  1044. let queriesCount = 0;
  1045. let multiQueries = 0;
  1046. let incompatibleQueries = 0;
  1047. const fieldIncompatibleCounts = {};
  1048. const classIncompatibleCounts = {};
  1049. for (const query of Object.values(globalMetadata.knownQueryFields)) {
  1050. queriesCount++;
  1051. if (query.isMulti) {
  1052. multiQueries++;
  1053. }
  1054. }
  1055. for (const [id, info] of Object.entries(globalMetadata.problematicQueries)) {
  1056. if (globalMetadata.knownQueryFields[id] === undefined) {
  1057. continue;
  1058. }
  1059. // Do not count queries that were forcibly ignored via best effort mode.
  1060. if (this.config.bestEffortMode &&
  1061. (info.fieldReason === null ||
  1062. !migrate_ts_type_references.nonIgnorableFieldIncompatibilities.includes(info.fieldReason))) {
  1063. continue;
  1064. }
  1065. incompatibleQueries++;
  1066. if (info.classReason !== null) {
  1067. const reasonName = migrate_ts_type_references.ClassIncompatibilityReason[info.classReason];
  1068. const key = `incompat-class-${reasonName}`;
  1069. classIncompatibleCounts[key] ??= 0;
  1070. classIncompatibleCounts[key]++;
  1071. }
  1072. if (info.fieldReason !== null) {
  1073. const reasonName = migrate_ts_type_references.FieldIncompatibilityReason[info.fieldReason];
  1074. const key = `incompat-field-${reasonName}`;
  1075. fieldIncompatibleCounts[key] ??= 0;
  1076. fieldIncompatibleCounts[key]++;
  1077. }
  1078. }
  1079. return {
  1080. counters: {
  1081. queriesCount,
  1082. multiQueries,
  1083. incompatibleQueries,
  1084. ...fieldIncompatibleCounts,
  1085. ...classIncompatibleCounts,
  1086. },
  1087. };
  1088. }
  1089. }
  1090. /**
  1091. * Updates the given map to capture the given query type.
  1092. * The map may track migrated queries in a file, or query types
  1093. * that couldn't be migrated.
  1094. */
  1095. function updateFileState(stateMap, node, queryType) {
  1096. const file = node.getSourceFile();
  1097. if (!stateMap.has(file)) {
  1098. stateMap.set(file, new Set());
  1099. }
  1100. stateMap.get(file).add(queryType);
  1101. }
  1102. function migrate(options) {
  1103. return async (tree, context) => {
  1104. await project_paths.runMigrationInDevkit({
  1105. tree,
  1106. getMigration: (fs) => new SignalQueriesMigration({
  1107. bestEffortMode: options.bestEffortMode,
  1108. insertTodosForSkippedFields: options.insertTodos,
  1109. shouldMigrateQuery: (_query, file) => {
  1110. return (file.rootRelativePath.startsWith(fs.normalize(options.path)) &&
  1111. !/(^|\/)node_modules\//.test(file.rootRelativePath));
  1112. },
  1113. }),
  1114. beforeProgramCreation: (tsconfigPath, stage) => {
  1115. if (stage === project_paths.MigrationStage.Analysis) {
  1116. context.logger.info(`Preparing analysis for: ${tsconfigPath}...`);
  1117. }
  1118. else {
  1119. context.logger.info(`Running migration for: ${tsconfigPath}...`);
  1120. }
  1121. },
  1122. afterProgramCreation: (info, fs) => {
  1123. const analysisPath = fs.resolve(options.analysisDir);
  1124. // Support restricting the analysis to subfolders for larger projects.
  1125. if (analysisPath !== '/') {
  1126. info.sourceFiles = info.sourceFiles.filter((sf) => sf.fileName.startsWith(analysisPath));
  1127. info.fullProgramSourceFiles = info.fullProgramSourceFiles.filter((sf) => sf.fileName.startsWith(analysisPath));
  1128. }
  1129. },
  1130. beforeUnitAnalysis: (tsconfigPath) => {
  1131. context.logger.info(`Scanning for queries: ${tsconfigPath}...`);
  1132. },
  1133. afterAnalysisFailure: () => {
  1134. context.logger.error('Migration failed unexpectedly with no analysis data');
  1135. },
  1136. afterAllAnalyzed: () => {
  1137. context.logger.info(``);
  1138. context.logger.info(`Processing analysis data between targets...`);
  1139. context.logger.info(``);
  1140. },
  1141. whenDone: ({ counters }) => {
  1142. context.logger.info('');
  1143. context.logger.info(`Successfully migrated to signal queries 🎉`);
  1144. const { queriesCount, incompatibleQueries } = counters;
  1145. const migratedQueries = queriesCount - incompatibleQueries;
  1146. context.logger.info('');
  1147. context.logger.info(`Successfully migrated to signal queries 🎉`);
  1148. context.logger.info(` -> Migrated ${migratedQueries}/${queriesCount} queries.`);
  1149. if (incompatibleQueries > 0 && !options.insertTodos) {
  1150. context.logger.warn(`To see why ${incompatibleQueries} queries couldn't be migrated`);
  1151. context.logger.warn(`consider re-running with "--insert-todos" or "--best-effort-mode".`);
  1152. }
  1153. if (options.bestEffortMode) {
  1154. context.logger.warn(`You ran with best effort mode. Manually verify all code ` +
  1155. `works as intended, and fix where necessary.`);
  1156. }
  1157. },
  1158. });
  1159. };
  1160. }
  1161. exports.migrate = migrate;