signal-queries-migration.js 53 KB

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