inject-migration.cjs 57 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279
  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 schematics = require('@angular-devkit/schematics');
  9. var p = require('path');
  10. var compiler_host = require('./compiler_host-B1Gyeytz.cjs');
  11. var ts = require('typescript');
  12. var ng_decorators = require('./ng_decorators-B5HCqr20.cjs');
  13. var imports = require('./imports-CIX-JgAN.cjs');
  14. var nodes = require('./nodes-B16H9JUd.cjs');
  15. var leading_space = require('./leading_space-D9nQ8UQC.cjs');
  16. require('./checker-5pyJrZ9G.cjs');
  17. require('os');
  18. require('fs');
  19. require('module');
  20. require('url');
  21. /*!
  22. * @license
  23. * Copyright Google LLC All Rights Reserved.
  24. *
  25. * Use of this source code is governed by an MIT-style license that can be
  26. * found in the LICENSE file at https://angular.dev/license
  27. */
  28. /** Names of decorators that enable DI on a class declaration. */
  29. const DECORATORS_SUPPORTING_DI = new Set([
  30. 'Component',
  31. 'Directive',
  32. 'Pipe',
  33. 'NgModule',
  34. 'Injectable',
  35. ]);
  36. /** Names of symbols used for DI on parameters. */
  37. const DI_PARAM_SYMBOLS = new Set([
  38. 'Inject',
  39. 'Attribute',
  40. 'Optional',
  41. 'SkipSelf',
  42. 'Self',
  43. 'Host',
  44. 'forwardRef',
  45. ]);
  46. /** Kinds of nodes which aren't injectable when set as a type of a parameter. */
  47. const UNINJECTABLE_TYPE_KINDS = new Set([
  48. ts.SyntaxKind.TrueKeyword,
  49. ts.SyntaxKind.FalseKeyword,
  50. ts.SyntaxKind.NumberKeyword,
  51. ts.SyntaxKind.StringKeyword,
  52. ts.SyntaxKind.NullKeyword,
  53. ts.SyntaxKind.VoidKeyword,
  54. ]);
  55. /**
  56. * Finds the necessary information for the `inject` migration in a file.
  57. * @param sourceFile File which to analyze.
  58. * @param localTypeChecker Type checker scoped to the specific file.
  59. */
  60. function analyzeFile(sourceFile, localTypeChecker, options) {
  61. const coreSpecifiers = imports.getNamedImports(sourceFile, '@angular/core');
  62. // Exit early if there are no Angular imports.
  63. if (coreSpecifiers === null || coreSpecifiers.elements.length === 0) {
  64. return null;
  65. }
  66. const classes = [];
  67. const nonDecoratorReferences = {};
  68. const importsToSpecifiers = coreSpecifiers.elements.reduce((map, specifier) => {
  69. const symbolName = (specifier.propertyName || specifier.name).text;
  70. if (DI_PARAM_SYMBOLS.has(symbolName)) {
  71. map.set(symbolName, specifier);
  72. }
  73. return map;
  74. }, new Map());
  75. sourceFile.forEachChild(function walk(node) {
  76. // Skip import declarations since they can throw off the identifier
  77. // could below and we don't care about them in this migration.
  78. if (ts.isImportDeclaration(node)) {
  79. return;
  80. }
  81. if (ts.isParameter(node)) {
  82. const closestConstructor = nodes.closestNode(node, ts.isConstructorDeclaration);
  83. // Visiting the same parameters that we're about to remove can throw off the reference
  84. // counting logic below. If we run into an initializer, we always visit its initializer
  85. // and optionally visit the modifiers/decorators if it's not due to be deleted. Note that
  86. // here we technically aren't dealing with the the full list of classes, but the parent class
  87. // will have been visited by the time we reach the parameters.
  88. if (node.initializer) {
  89. walk(node.initializer);
  90. }
  91. if (closestConstructor === null ||
  92. // This is meant to avoid the case where this is a
  93. // parameter inside a function placed in a constructor.
  94. !closestConstructor.parameters.includes(node) ||
  95. !classes.some((c) => c.constructor === closestConstructor)) {
  96. node.modifiers?.forEach(walk);
  97. }
  98. return;
  99. }
  100. if (ts.isIdentifier(node) && importsToSpecifiers.size > 0) {
  101. let symbol;
  102. for (const [name, specifier] of importsToSpecifiers) {
  103. const localName = (specifier.propertyName || specifier.name).text;
  104. // Quick exit if the two symbols don't match up.
  105. if (localName === node.text) {
  106. if (!symbol) {
  107. symbol = localTypeChecker.getSymbolAtLocation(node);
  108. // If the symbol couldn't be resolved the first time, it won't be resolved the next
  109. // time either. Stop the loop since we won't be able to get an accurate result.
  110. if (!symbol || !symbol.declarations) {
  111. break;
  112. }
  113. else if (symbol.declarations.some((decl) => decl === specifier)) {
  114. nonDecoratorReferences[name] = (nonDecoratorReferences[name] || 0) + 1;
  115. }
  116. }
  117. }
  118. }
  119. }
  120. else if (ts.isClassDeclaration(node)) {
  121. const decorators = ng_decorators.getAngularDecorators(localTypeChecker, ts.getDecorators(node) || []);
  122. const isAbstract = !!node.modifiers?.some((m) => m.kind === ts.SyntaxKind.AbstractKeyword);
  123. const supportsDI = decorators.some((dec) => DECORATORS_SUPPORTING_DI.has(dec.name));
  124. const constructorNode = node.members.find((member) => ts.isConstructorDeclaration(member) &&
  125. member.body != null &&
  126. member.parameters.length > 0);
  127. // Basic check to determine if all parameters are injectable. This isn't exhaustive, but it
  128. // should catch the majority of cases. An exhaustive check would require a full type checker
  129. // which we don't have in this migration.
  130. const allParamsInjectable = !!constructorNode?.parameters.every((param) => {
  131. if (!param.type || !UNINJECTABLE_TYPE_KINDS.has(param.type.kind)) {
  132. return true;
  133. }
  134. return ng_decorators.getAngularDecorators(localTypeChecker, ts.getDecorators(param) || []).some((dec) => dec.name === 'Inject' || dec.name === 'Attribute');
  135. });
  136. // Don't migrate abstract classes by default, because
  137. // their parameters aren't guaranteed to be injectable.
  138. if (supportsDI &&
  139. constructorNode &&
  140. allParamsInjectable &&
  141. (!isAbstract || options.migrateAbstractClasses)) {
  142. classes.push({
  143. node,
  144. constructor: constructorNode,
  145. superCall: node.heritageClauses ? findSuperCall(constructorNode) : null,
  146. });
  147. }
  148. }
  149. node.forEachChild(walk);
  150. });
  151. return { classes, nonDecoratorReferences };
  152. }
  153. /**
  154. * Returns the parameters of a function that aren't used within its body.
  155. * @param declaration Function in which to search for unused parameters.
  156. * @param localTypeChecker Type checker scoped to the file in which the function was declared.
  157. * @param removedStatements Statements that were already removed from the constructor.
  158. */
  159. function getConstructorUnusedParameters(declaration, localTypeChecker, removedStatements) {
  160. const accessedTopLevelParameters = new Set();
  161. const topLevelParameters = new Set();
  162. const topLevelParameterNames = new Set();
  163. const unusedParams = new Set();
  164. // Prepare the parameters for quicker checks further down.
  165. for (const param of declaration.parameters) {
  166. if (ts.isIdentifier(param.name)) {
  167. topLevelParameters.add(param);
  168. topLevelParameterNames.add(param.name.text);
  169. }
  170. }
  171. if (!declaration.body) {
  172. return topLevelParameters;
  173. }
  174. const analyze = (node) => {
  175. // Don't descend into statements that were removed already.
  176. if (ts.isStatement(node) && removedStatements.has(node)) {
  177. return;
  178. }
  179. if (!ts.isIdentifier(node) || !topLevelParameterNames.has(node.text)) {
  180. node.forEachChild(analyze);
  181. return;
  182. }
  183. // Don't consider `this.<name>` accesses as being references to
  184. // parameters since they'll be moved to property declarations.
  185. if (isAccessedViaThis(node)) {
  186. return;
  187. }
  188. localTypeChecker.getSymbolAtLocation(node)?.declarations?.forEach((decl) => {
  189. if (ts.isParameter(decl) && topLevelParameters.has(decl)) {
  190. accessedTopLevelParameters.add(decl);
  191. }
  192. if (ts.isShorthandPropertyAssignment(decl)) {
  193. const symbol = localTypeChecker.getShorthandAssignmentValueSymbol(decl);
  194. if (symbol && symbol.valueDeclaration && ts.isParameter(symbol.valueDeclaration)) {
  195. accessedTopLevelParameters.add(symbol.valueDeclaration);
  196. }
  197. }
  198. });
  199. };
  200. declaration.parameters.forEach((param) => {
  201. if (param.initializer) {
  202. analyze(param.initializer);
  203. }
  204. });
  205. declaration.body.forEachChild(analyze);
  206. for (const param of topLevelParameters) {
  207. if (!accessedTopLevelParameters.has(param)) {
  208. unusedParams.add(param);
  209. }
  210. }
  211. return unusedParams;
  212. }
  213. /**
  214. * Determines which parameters of a function declaration are used within its `super` call.
  215. * @param declaration Function whose parameters to search for.
  216. * @param superCall `super()` call within the function.
  217. * @param localTypeChecker Type checker scoped to the file in which the function is declared.
  218. */
  219. function getSuperParameters(declaration, superCall, localTypeChecker) {
  220. const usedParams = new Set();
  221. const topLevelParameters = new Set();
  222. const topLevelParameterNames = new Set();
  223. // Prepare the parameters for quicker checks further down.
  224. for (const param of declaration.parameters) {
  225. if (ts.isIdentifier(param.name)) {
  226. topLevelParameters.add(param);
  227. topLevelParameterNames.add(param.name.text);
  228. }
  229. }
  230. superCall.forEachChild(function walk(node) {
  231. if (ts.isIdentifier(node) && topLevelParameterNames.has(node.text)) {
  232. localTypeChecker.getSymbolAtLocation(node)?.declarations?.forEach((decl) => {
  233. if (ts.isParameter(decl) && topLevelParameters.has(decl)) {
  234. usedParams.add(decl);
  235. }
  236. else if (ts.isShorthandPropertyAssignment(decl) &&
  237. topLevelParameterNames.has(decl.name.text)) {
  238. for (const param of topLevelParameters) {
  239. if (ts.isIdentifier(param.name) && decl.name.text === param.name.text) {
  240. usedParams.add(param);
  241. break;
  242. }
  243. }
  244. }
  245. });
  246. // Parameters referenced inside callbacks can be used directly
  247. // within `super` so don't descend into inline functions.
  248. }
  249. else if (!isInlineFunction(node)) {
  250. node.forEachChild(walk);
  251. }
  252. });
  253. return usedParams;
  254. }
  255. /**
  256. * Determines if a specific parameter has references to other parameters.
  257. * @param param Parameter to check.
  258. * @param allParameters All parameters of the containing function.
  259. * @param localTypeChecker Type checker scoped to the current file.
  260. */
  261. function parameterReferencesOtherParameters(param, allParameters, localTypeChecker) {
  262. // A parameter can only reference other parameters through its initializer.
  263. if (!param.initializer || allParameters.length < 2) {
  264. return false;
  265. }
  266. const paramNames = new Set();
  267. for (const current of allParameters) {
  268. if (current !== param && ts.isIdentifier(current.name)) {
  269. paramNames.add(current.name.text);
  270. }
  271. }
  272. let result = false;
  273. const analyze = (node) => {
  274. if (ts.isIdentifier(node) && paramNames.has(node.text) && !isAccessedViaThis(node)) {
  275. const symbol = localTypeChecker.getSymbolAtLocation(node);
  276. const referencesOtherParam = symbol?.declarations?.some((decl) => {
  277. return allParameters.includes(decl);
  278. });
  279. if (referencesOtherParam) {
  280. result = true;
  281. }
  282. }
  283. if (!result) {
  284. node.forEachChild(analyze);
  285. }
  286. };
  287. analyze(param.initializer);
  288. return result;
  289. }
  290. /** Checks whether a parameter node declares a property on its class. */
  291. function parameterDeclaresProperty(node) {
  292. return !!node.modifiers?.some(({ kind }) => kind === ts.SyntaxKind.PublicKeyword ||
  293. kind === ts.SyntaxKind.PrivateKeyword ||
  294. kind === ts.SyntaxKind.ProtectedKeyword ||
  295. kind === ts.SyntaxKind.ReadonlyKeyword);
  296. }
  297. /** Checks whether a type node is nullable. */
  298. function isNullableType(node) {
  299. // Apparently `foo: null` is `Parameter<TypeNode<NullKeyword>>`,
  300. // while `foo: undefined` is `Parameter<UndefinedKeyword>`...
  301. if (node.kind === ts.SyntaxKind.UndefinedKeyword || node.kind === ts.SyntaxKind.VoidKeyword) {
  302. return true;
  303. }
  304. if (ts.isLiteralTypeNode(node)) {
  305. return node.literal.kind === ts.SyntaxKind.NullKeyword;
  306. }
  307. if (ts.isUnionTypeNode(node)) {
  308. return node.types.some(isNullableType);
  309. }
  310. return false;
  311. }
  312. /** Checks whether a type node has generic arguments. */
  313. function hasGenerics(node) {
  314. if (ts.isTypeReferenceNode(node)) {
  315. return node.typeArguments != null && node.typeArguments.length > 0;
  316. }
  317. if (ts.isUnionTypeNode(node)) {
  318. return node.types.some(hasGenerics);
  319. }
  320. return false;
  321. }
  322. /** Checks whether an identifier is accessed through `this`, e.g. `this.<some identifier>`. */
  323. function isAccessedViaThis(node) {
  324. return (ts.isPropertyAccessExpression(node.parent) &&
  325. node.parent.expression.kind === ts.SyntaxKind.ThisKeyword &&
  326. node.parent.name === node);
  327. }
  328. /** Finds a `super` call inside of a specific node. */
  329. function findSuperCall(root) {
  330. let result = null;
  331. root.forEachChild(function find(node) {
  332. if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.SuperKeyword) {
  333. result = node;
  334. }
  335. else if (result === null) {
  336. node.forEachChild(find);
  337. }
  338. });
  339. return result;
  340. }
  341. /** Checks whether a node is an inline function. */
  342. function isInlineFunction(node) {
  343. return (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node));
  344. }
  345. /*!
  346. * @license
  347. * Copyright Google LLC All Rights Reserved.
  348. *
  349. * Use of this source code is governed by an MIT-style license that can be
  350. * found in the LICENSE file at https://angular.dev/license
  351. */
  352. /**
  353. * Finds class property declarations without initializers whose constructor-based initialization
  354. * can be inlined into the declaration spot after migrating to `inject`. For example:
  355. *
  356. * ```ts
  357. * private foo: number;
  358. *
  359. * constructor(private service: MyService) {
  360. * this.foo = this.service.getFoo();
  361. * }
  362. * ```
  363. *
  364. * The initializer of `foo` can be inlined, because `service` will be initialized
  365. * before it after the `inject` migration has finished running.
  366. *
  367. * @param node Class declaration that is being migrated.
  368. * @param constructor Constructor declaration of the class being migrated.
  369. * @param localTypeChecker Type checker scoped to the current file.
  370. */
  371. function findUninitializedPropertiesToCombine(node, constructor, localTypeChecker, options) {
  372. let toCombine = null;
  373. let toHoist = [];
  374. const membersToDeclarations = new Map();
  375. for (const member of node.members) {
  376. if (ts.isPropertyDeclaration(member) &&
  377. !member.initializer &&
  378. !ts.isComputedPropertyName(member.name)) {
  379. membersToDeclarations.set(member.name.text, member);
  380. }
  381. }
  382. if (membersToDeclarations.size === 0) {
  383. return null;
  384. }
  385. const memberInitializers = getMemberInitializers(constructor);
  386. if (memberInitializers === null) {
  387. return null;
  388. }
  389. const inlinableParameters = options._internalReplaceParameterReferencesInInitializers
  390. ? findInlinableParameterReferences(constructor, localTypeChecker)
  391. : new Set();
  392. for (const [name, decl] of membersToDeclarations.entries()) {
  393. if (memberInitializers.has(name)) {
  394. const initializer = memberInitializers.get(name);
  395. if (!hasLocalReferences(initializer, constructor, inlinableParameters, localTypeChecker)) {
  396. toCombine ??= [];
  397. toCombine.push({ declaration: membersToDeclarations.get(name), initializer });
  398. }
  399. }
  400. else {
  401. // Mark members that have no initializers and can't be combined to be hoisted above the
  402. // injected members. This is either a no-op or it allows us to avoid some patterns internally
  403. // like the following:
  404. // ```
  405. // class Foo {
  406. // publicFoo: Foo;
  407. // private privateFoo: Foo;
  408. //
  409. // constructor() {
  410. // this.initializePrivateFooSomehow();
  411. // this.publicFoo = this.privateFoo;
  412. // }
  413. // }
  414. // ```
  415. toHoist.push(decl);
  416. }
  417. }
  418. // If no members need to be combined, none need to be hoisted either.
  419. return toCombine === null ? null : { toCombine, toHoist };
  420. }
  421. /**
  422. * In some cases properties may be declared out of order, but initialized in the correct order.
  423. * The internal-specific migration will combine such properties which will result in a compilation
  424. * error, for example:
  425. *
  426. * ```ts
  427. * class MyClass {
  428. * foo: Foo;
  429. * bar: Bar;
  430. *
  431. * constructor(bar: Bar) {
  432. * this.bar = bar;
  433. * this.foo = this.bar.getFoo();
  434. * }
  435. * }
  436. * ```
  437. *
  438. * Will become:
  439. *
  440. * ```ts
  441. * class MyClass {
  442. * foo: Foo = this.bar.getFoo();
  443. * bar: Bar = inject(Bar);
  444. * }
  445. * ```
  446. *
  447. * This function determines if cases like this can be saved by reordering the properties so their
  448. * declaration order matches the order in which they're initialized.
  449. *
  450. * @param toCombine Properties that are candidates to be combined.
  451. * @param constructor
  452. */
  453. function shouldCombineInInitializationOrder(toCombine, constructor) {
  454. let combinedMemberReferenceCount = 0;
  455. let otherMemberReferenceCount = 0;
  456. const injectedMemberNames = new Set();
  457. const combinedMemberNames = new Set();
  458. // Collect the name of constructor parameters that declare new properties.
  459. // These can be ignored since they'll be hoisted above other properties.
  460. constructor.parameters.forEach((param) => {
  461. if (parameterDeclaresProperty(param) && ts.isIdentifier(param.name)) {
  462. injectedMemberNames.add(param.name.text);
  463. }
  464. });
  465. // Collect the names of the properties being combined. We should only reorder
  466. // the properties if at least one of them refers to another one.
  467. toCombine.forEach(({ declaration: { name } }) => {
  468. if (ts.isStringLiteralLike(name) || ts.isIdentifier(name)) {
  469. combinedMemberNames.add(name.text);
  470. }
  471. });
  472. // Visit all the initializers and check all the property reads in the form of `this.<name>`.
  473. // Skip over the ones referring to injected parameters since they're going to be hoisted.
  474. const walkInitializer = (node) => {
  475. if (ts.isPropertyAccessExpression(node) && node.expression.kind === ts.SyntaxKind.ThisKeyword) {
  476. if (combinedMemberNames.has(node.name.text)) {
  477. combinedMemberReferenceCount++;
  478. }
  479. else if (!injectedMemberNames.has(node.name.text)) {
  480. otherMemberReferenceCount++;
  481. }
  482. }
  483. node.forEachChild(walkInitializer);
  484. };
  485. toCombine.forEach((candidate) => walkInitializer(candidate.initializer));
  486. // If at the end there is at least one reference between a combined member and another,
  487. // and there are no references to any other class members, we can safely reorder the
  488. // properties based on how they were initialized.
  489. return combinedMemberReferenceCount > 0 && otherMemberReferenceCount === 0;
  490. }
  491. /**
  492. * Finds the expressions from the constructor that initialize class members, for example:
  493. *
  494. * ```ts
  495. * private foo: number;
  496. *
  497. * constructor() {
  498. * this.foo = 123;
  499. * }
  500. * ```
  501. *
  502. * @param constructor Constructor declaration being analyzed.
  503. */
  504. function getMemberInitializers(constructor) {
  505. let memberInitializers = null;
  506. if (!constructor.body) {
  507. return memberInitializers;
  508. }
  509. // Only look at top-level constructor statements.
  510. for (const node of constructor.body.statements) {
  511. // Only look for statements in the form of `this.<name> = <expr>;` or `this[<name>] = <expr>;`.
  512. if (!ts.isExpressionStatement(node) ||
  513. !ts.isBinaryExpression(node.expression) ||
  514. node.expression.operatorToken.kind !== ts.SyntaxKind.EqualsToken ||
  515. (!ts.isPropertyAccessExpression(node.expression.left) &&
  516. !ts.isElementAccessExpression(node.expression.left)) ||
  517. node.expression.left.expression.kind !== ts.SyntaxKind.ThisKeyword) {
  518. continue;
  519. }
  520. let name;
  521. if (ts.isPropertyAccessExpression(node.expression.left)) {
  522. name = node.expression.left.name.text;
  523. }
  524. else if (ts.isElementAccessExpression(node.expression.left)) {
  525. name = ts.isStringLiteralLike(node.expression.left.argumentExpression)
  526. ? node.expression.left.argumentExpression.text
  527. : undefined;
  528. }
  529. // If the member is initialized multiple times, take the first one.
  530. if (name && (!memberInitializers || !memberInitializers.has(name))) {
  531. memberInitializers = memberInitializers || new Map();
  532. memberInitializers.set(name, node.expression.right);
  533. }
  534. }
  535. return memberInitializers;
  536. }
  537. /**
  538. * Checks if the node is an identifier that references a property from the given
  539. * list. Returns the property if it is.
  540. */
  541. function getIdentifierReferencingProperty(node, localTypeChecker, propertyNames, properties) {
  542. if (!ts.isIdentifier(node) || !propertyNames.has(node.text)) {
  543. return undefined;
  544. }
  545. const declarations = localTypeChecker.getSymbolAtLocation(node)?.declarations;
  546. if (!declarations) {
  547. return undefined;
  548. }
  549. for (const decl of declarations) {
  550. if (properties.has(decl)) {
  551. return decl;
  552. }
  553. }
  554. return undefined;
  555. }
  556. /**
  557. * Returns true if the node introduces a new `this` scope (so we can't
  558. * reference the outer this).
  559. */
  560. function introducesNewThisScope(node) {
  561. return (ts.isFunctionDeclaration(node) ||
  562. ts.isFunctionExpression(node) ||
  563. ts.isMethodDeclaration(node) ||
  564. ts.isClassDeclaration(node) ||
  565. ts.isClassExpression(node));
  566. }
  567. /**
  568. * Finds constructor parameter references which can be inlined as `this.prop`.
  569. * - prop must be a readonly property
  570. * - the reference can't be in a nested function where `this` might refer
  571. * to something else
  572. */
  573. function findInlinableParameterReferences(constructorDeclaration, localTypeChecker) {
  574. const eligibleProperties = constructorDeclaration.parameters.filter((p) => ts.isIdentifier(p.name) && p.modifiers?.some((s) => s.kind === ts.SyntaxKind.ReadonlyKeyword));
  575. const eligibleNames = new Set(eligibleProperties.map((p) => p.name.text));
  576. const eligiblePropertiesSet = new Set(eligibleProperties);
  577. function walk(node, canReferenceThis) {
  578. const property = getIdentifierReferencingProperty(node, localTypeChecker, eligibleNames, eligiblePropertiesSet);
  579. if (property && !canReferenceThis) {
  580. // The property is referenced in a nested context where
  581. // we can't use `this`, so we can't inline it.
  582. eligiblePropertiesSet.delete(property);
  583. }
  584. else if (introducesNewThisScope(node)) {
  585. canReferenceThis = false;
  586. }
  587. ts.forEachChild(node, (child) => {
  588. walk(child, canReferenceThis);
  589. });
  590. }
  591. walk(constructorDeclaration, true);
  592. return eligiblePropertiesSet;
  593. }
  594. /**
  595. * Determines if a node has references to local symbols defined in the constructor.
  596. * @param root Expression to check for local references.
  597. * @param constructor Constructor within which the expression is used.
  598. * @param localTypeChecker Type checker scoped to the current file.
  599. */
  600. function hasLocalReferences(root, constructor, allowedParameters, localTypeChecker) {
  601. const sourceFile = root.getSourceFile();
  602. let hasLocalRefs = false;
  603. const walk = (node) => {
  604. // Stop searching if we know that it has local references.
  605. if (hasLocalRefs) {
  606. return;
  607. }
  608. // Skip identifiers that are accessed via `this` since they're accessing class members
  609. // that aren't local to the constructor. This is here primarily to catch cases like this
  610. // where `foo` is defined inside the constructor, but is a class member:
  611. // ```
  612. // constructor(private foo: Foo) {
  613. // this.bar = this.foo.getFoo();
  614. // }
  615. // ```
  616. if (ts.isIdentifier(node) && !isAccessedViaThis(node)) {
  617. const declarations = localTypeChecker.getSymbolAtLocation(node)?.declarations;
  618. const isReferencingLocalSymbol = declarations?.some((decl) =>
  619. // The source file check is a bit redundant since the type checker
  620. // is local to the file, but it's inexpensive and it can prevent
  621. // bugs in the future if we decide to use a full type checker.
  622. !allowedParameters.has(decl) &&
  623. decl.getSourceFile() === sourceFile &&
  624. decl.getStart() >= constructor.getStart() &&
  625. decl.getEnd() <= constructor.getEnd() &&
  626. !isInsideInlineFunction(decl, constructor));
  627. if (isReferencingLocalSymbol) {
  628. hasLocalRefs = true;
  629. }
  630. }
  631. if (!hasLocalRefs) {
  632. node.forEachChild(walk);
  633. }
  634. };
  635. walk(root);
  636. return hasLocalRefs;
  637. }
  638. /**
  639. * Determines if a node is defined inside of an inline function.
  640. * @param startNode Node from which to start checking for inline functions.
  641. * @param boundary Node at which to stop searching.
  642. */
  643. function isInsideInlineFunction(startNode, boundary) {
  644. let current = startNode;
  645. while (current) {
  646. if (current === boundary) {
  647. return false;
  648. }
  649. if (isInlineFunction(current)) {
  650. return true;
  651. }
  652. current = current.parent;
  653. }
  654. return false;
  655. }
  656. /**
  657. * Placeholder used to represent expressions inside the AST.
  658. * Includes Unicode characters to reduce the chance of collisions.
  659. */
  660. const PLACEHOLDER = 'ɵɵngGeneratePlaceholderɵɵ';
  661. /**
  662. * Migrates all of the classes in a `SourceFile` away from constructor injection.
  663. * @param sourceFile File to be migrated.
  664. * @param options Options that configure the migration.
  665. */
  666. function migrateFile(sourceFile, options) {
  667. // Note: even though externally we have access to the full program with a proper type
  668. // checker, we create a new one that is local to the file for a couple of reasons:
  669. // 1. Not having to depend on a program makes running the migration internally faster and easier.
  670. // 2. All the necessary information for this migration is local so using a file-specific type
  671. // checker should speed up the lookups.
  672. const localTypeChecker = getLocalTypeChecker(sourceFile);
  673. const analysis = analyzeFile(sourceFile, localTypeChecker, options);
  674. if (analysis === null || analysis.classes.length === 0) {
  675. return [];
  676. }
  677. const printer = ts.createPrinter();
  678. const tracker = new compiler_host.ChangeTracker(printer);
  679. analysis.classes.forEach(({ node, constructor, superCall }) => {
  680. const memberIndentation = leading_space.getLeadingLineWhitespaceOfNode(node.members[0]);
  681. const prependToClass = [];
  682. const afterInjectCalls = [];
  683. const removedStatements = new Set();
  684. const removedMembers = new Set();
  685. if (options._internalCombineMemberInitializers) {
  686. applyInternalOnlyChanges(node, constructor, localTypeChecker, tracker, printer, removedStatements, removedMembers, prependToClass, afterInjectCalls, memberIndentation, options);
  687. }
  688. migrateClass(node, constructor, superCall, options, memberIndentation, prependToClass, afterInjectCalls, removedStatements, removedMembers, localTypeChecker, printer, tracker);
  689. });
  690. DI_PARAM_SYMBOLS.forEach((name) => {
  691. // Both zero and undefined are fine here.
  692. if (!analysis.nonDecoratorReferences[name]) {
  693. tracker.removeImport(sourceFile, name, '@angular/core');
  694. }
  695. });
  696. return tracker.recordChanges().get(sourceFile) || [];
  697. }
  698. /**
  699. * Migrates a class away from constructor injection.
  700. * @param node Class to be migrated.
  701. * @param constructor Reference to the class' constructor node.
  702. * @param superCall Reference to the constructor's `super()` call, if any.
  703. * @param options Options used to configure the migration.
  704. * @param memberIndentation Indentation string of the members of the class.
  705. * @param prependToClass Text that should be prepended to the class.
  706. * @param afterInjectCalls Text that will be inserted after the newly-added `inject` calls.
  707. * @param removedStatements Statements that have been removed from the constructor already.
  708. * @param removedMembers Class members that have been removed by the migration.
  709. * @param localTypeChecker Type checker set up for the specific file.
  710. * @param printer Printer used to output AST nodes as strings.
  711. * @param tracker Object keeping track of the changes made to the file.
  712. */
  713. function migrateClass(node, constructor, superCall, options, memberIndentation, prependToClass, afterInjectCalls, removedStatements, removedMembers, localTypeChecker, printer, tracker) {
  714. const sourceFile = node.getSourceFile();
  715. const unusedParameters = getConstructorUnusedParameters(constructor, localTypeChecker, removedStatements);
  716. const superParameters = superCall
  717. ? getSuperParameters(constructor, superCall, localTypeChecker)
  718. : null;
  719. const removedStatementCount = removedStatements.size;
  720. const firstConstructorStatement = constructor.body?.statements.find((statement) => !removedStatements.has(statement));
  721. const innerReference = superCall || firstConstructorStatement || constructor;
  722. const innerIndentation = leading_space.getLeadingLineWhitespaceOfNode(innerReference);
  723. const prependToConstructor = [];
  724. const afterSuper = [];
  725. for (const param of constructor.parameters) {
  726. const usedInSuper = superParameters !== null && superParameters.has(param);
  727. const usedInConstructor = !unusedParameters.has(param);
  728. const usesOtherParams = parameterReferencesOtherParameters(param, constructor.parameters, localTypeChecker);
  729. migrateParameter(param, options, localTypeChecker, printer, tracker, superCall, usedInSuper, usedInConstructor, usesOtherParams, memberIndentation, innerIndentation, prependToConstructor, prependToClass, afterSuper);
  730. }
  731. // Delete all of the constructor overloads since below we're either going to
  732. // remove the implementation, or we're going to delete all of the parameters.
  733. for (const member of node.members) {
  734. if (ts.isConstructorDeclaration(member) && member !== constructor) {
  735. removedMembers.add(member);
  736. tracker.removeNode(member, true);
  737. }
  738. }
  739. if (canRemoveConstructor(options, constructor, removedStatementCount, prependToConstructor, superCall)) {
  740. // Drop the constructor if it was empty.
  741. removedMembers.add(constructor);
  742. tracker.removeNode(constructor, true);
  743. }
  744. else {
  745. // If the constructor contains any statements, only remove the parameters.
  746. // We always do this no matter what is passed into `backwardsCompatibleConstructors`.
  747. stripConstructorParameters(constructor, tracker);
  748. if (prependToConstructor.length > 0) {
  749. if (firstConstructorStatement ||
  750. (innerReference !== constructor &&
  751. innerReference.getStart() >= constructor.getStart() &&
  752. innerReference.getEnd() <= constructor.getEnd())) {
  753. tracker.insertText(sourceFile, (firstConstructorStatement || innerReference).getFullStart(), `\n${prependToConstructor.join('\n')}\n`);
  754. }
  755. else {
  756. tracker.insertText(sourceFile, constructor.body.getStart() + 1, `\n${prependToConstructor.map((p) => innerIndentation + p).join('\n')}\n${innerIndentation}`);
  757. }
  758. }
  759. }
  760. if (afterSuper.length > 0 && superCall !== null) {
  761. // Note that if we can, we should insert before the next statement after the `super` call,
  762. // rather than after the end of it. Otherwise the string buffering implementation may drop
  763. // the text if the statement after the `super` call is being deleted. This appears to be because
  764. // the full start of the next statement appears to always be the end of the `super` call plus 1.
  765. const nextStatement = getNextPreservedStatement(superCall, removedStatements);
  766. tracker.insertText(sourceFile, nextStatement ? nextStatement.getFullStart() : constructor.getEnd() - 1, `\n${afterSuper.join('\n')}\n` + (nextStatement ? '' : memberIndentation));
  767. }
  768. // Need to resolve this once all constructor signatures have been removed.
  769. const memberReference = node.members.find((m) => !removedMembers.has(m)) || node.members[0];
  770. // If `backwardsCompatibleConstructors` is enabled, we maintain
  771. // backwards compatibility by adding a catch-all signature.
  772. if (options.backwardsCompatibleConstructors) {
  773. const extraSignature = `\n${memberIndentation}/** Inserted by Angular inject() migration for backwards compatibility */\n` +
  774. `${memberIndentation}constructor(...args: unknown[]);`;
  775. // The new signature always has to be right before the constructor implementation.
  776. if (memberReference === constructor) {
  777. prependToClass.push(extraSignature);
  778. }
  779. else {
  780. tracker.insertText(sourceFile, constructor.getFullStart(), '\n' + extraSignature);
  781. }
  782. }
  783. // Push the block of code that should appear after the `inject`
  784. // calls now once all the members have been generated.
  785. prependToClass.push(...afterInjectCalls);
  786. if (prependToClass.length > 0) {
  787. if (removedMembers.size === node.members.length) {
  788. tracker.insertText(sourceFile,
  789. // If all members were deleted, insert after the last one.
  790. // This allows us to preserve the indentation.
  791. node.members.length > 0
  792. ? node.members[node.members.length - 1].getEnd() + 1
  793. : node.getEnd() - 1, `${prependToClass.join('\n')}\n`);
  794. }
  795. else {
  796. // Insert the new properties after the first member that hasn't been deleted.
  797. tracker.insertText(sourceFile, memberReference.getFullStart(), `\n${prependToClass.join('\n')}\n`);
  798. }
  799. }
  800. }
  801. /**
  802. * Migrates a single parameter to `inject()` DI.
  803. * @param node Parameter to be migrated.
  804. * @param options Options used to configure the migration.
  805. * @param localTypeChecker Type checker set up for the specific file.
  806. * @param printer Printer used to output AST nodes as strings.
  807. * @param tracker Object keeping track of the changes made to the file.
  808. * @param superCall Call to `super()` from the class' constructor.
  809. * @param usedInSuper Whether the parameter is referenced inside of `super`.
  810. * @param usedInConstructor Whether the parameter is referenced inside the body of the constructor.
  811. * @param memberIndentation Indentation string to use when inserting new class members.
  812. * @param innerIndentation Indentation string to use when inserting new constructor statements.
  813. * @param prependToConstructor Statements to be prepended to the constructor.
  814. * @param propsToAdd Properties to be added to the class.
  815. * @param afterSuper Statements to be added after the `super` call.
  816. */
  817. function migrateParameter(node, options, localTypeChecker, printer, tracker, superCall, usedInSuper, usedInConstructor, usesOtherParams, memberIndentation, innerIndentation, prependToConstructor, propsToAdd, afterSuper) {
  818. if (!ts.isIdentifier(node.name)) {
  819. return;
  820. }
  821. const name = node.name.text;
  822. const replacementCall = createInjectReplacementCall(node, options, localTypeChecker, printer, tracker);
  823. const declaresProp = parameterDeclaresProperty(node);
  824. // If the parameter declares a property, we need to declare it (e.g. `private foo: Foo`).
  825. if (declaresProp) {
  826. // We can't initialize the property if it's referenced within a `super` call or it references
  827. // other parameters. See the logic further below for the initialization.
  828. const canInitialize = !usedInSuper && !usesOtherParams;
  829. const prop = ts.factory.createPropertyDeclaration(cloneModifiers(node.modifiers?.filter((modifier) => {
  830. // Strip out the DI decorators, as well as `public` which is redundant.
  831. return !ts.isDecorator(modifier) && modifier.kind !== ts.SyntaxKind.PublicKeyword;
  832. })), name,
  833. // Don't add the question token to private properties since it won't affect interface implementation.
  834. node.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.PrivateKeyword)
  835. ? undefined
  836. : node.questionToken, canInitialize ? undefined : node.type, canInitialize ? ts.factory.createIdentifier(PLACEHOLDER) : undefined);
  837. propsToAdd.push(memberIndentation +
  838. replaceNodePlaceholder(node.getSourceFile(), prop, replacementCall, printer));
  839. }
  840. // If the parameter is referenced within the constructor, we need to declare it as a variable.
  841. if (usedInConstructor) {
  842. if (usedInSuper) {
  843. // Usages of `this` aren't allowed before `super` calls so we need to
  844. // create a variable which calls `inject()` directly instead...
  845. prependToConstructor.push(`${innerIndentation}const ${name} = ${replacementCall};`);
  846. // ...then we can initialize the property after the `super` call.
  847. if (declaresProp) {
  848. afterSuper.push(`${innerIndentation}this.${name} = ${name};`);
  849. }
  850. }
  851. else if (declaresProp) {
  852. // If the parameter declares a property (`private foo: foo`) and is used inside the class
  853. // at the same time, we need to ensure that it's initialized to the value from the variable
  854. // and that we only reference `this` after the `super` call.
  855. const initializer = `${innerIndentation}const ${name} = this.${name};`;
  856. if (superCall === null) {
  857. prependToConstructor.push(initializer);
  858. }
  859. else {
  860. afterSuper.push(initializer);
  861. }
  862. }
  863. else {
  864. // If the parameter is only referenced in the constructor, we
  865. // don't need to declare any new properties.
  866. prependToConstructor.push(`${innerIndentation}const ${name} = ${replacementCall};`);
  867. }
  868. }
  869. else if (usesOtherParams && declaresProp) {
  870. const toAdd = `${innerIndentation}this.${name} = ${replacementCall};`;
  871. if (superCall === null) {
  872. prependToConstructor.push(toAdd);
  873. }
  874. else {
  875. afterSuper.push(toAdd);
  876. }
  877. }
  878. }
  879. /**
  880. * Creates a replacement `inject` call from a function parameter.
  881. * @param param Parameter for which to generate the `inject` call.
  882. * @param options Options used to configure the migration.
  883. * @param localTypeChecker Type checker set up for the specific file.
  884. * @param printer Printer used to output AST nodes as strings.
  885. * @param tracker Object keeping track of the changes made to the file.
  886. */
  887. function createInjectReplacementCall(param, options, localTypeChecker, printer, tracker) {
  888. const moduleName = '@angular/core';
  889. const sourceFile = param.getSourceFile();
  890. const decorators = ng_decorators.getAngularDecorators(localTypeChecker, ts.getDecorators(param) || []);
  891. const literalProps = [];
  892. const type = param.type;
  893. let injectedType = '';
  894. let typeArguments = type && hasGenerics(type) ? [type] : undefined;
  895. let hasOptionalDecorator = false;
  896. if (type) {
  897. // Remove the type arguments from generic type references, because
  898. // they'll be specified as type arguments to `inject()`.
  899. if (ts.isTypeReferenceNode(type) && type.typeArguments && type.typeArguments.length > 0) {
  900. injectedType = type.typeName.getText();
  901. }
  902. else if (ts.isUnionTypeNode(type)) {
  903. injectedType = (type.types.find((t) => !ts.isLiteralTypeNode(t)) || type.types[0]).getText();
  904. }
  905. else {
  906. injectedType = type.getText();
  907. }
  908. }
  909. for (const decorator of decorators) {
  910. if (decorator.moduleName !== moduleName) {
  911. continue;
  912. }
  913. const firstArg = decorator.node.expression.arguments[0];
  914. switch (decorator.name) {
  915. case 'Inject':
  916. if (firstArg) {
  917. const injectResult = migrateInjectDecorator(firstArg, type, localTypeChecker);
  918. injectedType = injectResult.injectedType;
  919. if (injectResult.typeArguments) {
  920. typeArguments = injectResult.typeArguments;
  921. }
  922. }
  923. break;
  924. case 'Attribute':
  925. if (firstArg) {
  926. const constructorRef = tracker.addImport(sourceFile, 'HostAttributeToken', moduleName);
  927. const expression = ts.factory.createNewExpression(constructorRef, undefined, [firstArg]);
  928. injectedType = printer.printNode(ts.EmitHint.Unspecified, expression, sourceFile);
  929. typeArguments = undefined;
  930. }
  931. break;
  932. case 'Optional':
  933. hasOptionalDecorator = true;
  934. literalProps.push(ts.factory.createPropertyAssignment('optional', ts.factory.createTrue()));
  935. break;
  936. case 'SkipSelf':
  937. literalProps.push(ts.factory.createPropertyAssignment('skipSelf', ts.factory.createTrue()));
  938. break;
  939. case 'Self':
  940. literalProps.push(ts.factory.createPropertyAssignment('self', ts.factory.createTrue()));
  941. break;
  942. case 'Host':
  943. literalProps.push(ts.factory.createPropertyAssignment('host', ts.factory.createTrue()));
  944. break;
  945. }
  946. }
  947. // The injected type might be a `TypeNode` which we can't easily convert into an `Expression`.
  948. // Since the value gets passed through directly anyway, we generate the call using a placeholder
  949. // which we then replace with the raw text of the `TypeNode`.
  950. const injectRef = tracker.addImport(param.getSourceFile(), 'inject', moduleName);
  951. const args = [ts.factory.createIdentifier(PLACEHOLDER)];
  952. if (literalProps.length > 0) {
  953. args.push(ts.factory.createObjectLiteralExpression(literalProps));
  954. }
  955. let expression = ts.factory.createCallExpression(injectRef, typeArguments, args);
  956. if (hasOptionalDecorator && options.nonNullableOptional) {
  957. const hasNullableType = param.questionToken != null || (param.type != null && isNullableType(param.type));
  958. // Only wrap the expression if the type wasn't already nullable.
  959. // If it was, the app was likely accounting for it already.
  960. if (!hasNullableType) {
  961. expression = ts.factory.createNonNullExpression(expression);
  962. }
  963. }
  964. // If the parameter is initialized, add the initializer as a fallback.
  965. if (param.initializer) {
  966. expression = ts.factory.createBinaryExpression(expression, ts.SyntaxKind.QuestionQuestionToken, param.initializer);
  967. }
  968. return replaceNodePlaceholder(param.getSourceFile(), expression, injectedType, printer);
  969. }
  970. /**
  971. * Migrates a parameter based on its `@Inject()` decorator.
  972. * @param firstArg First argument to `@Inject()`.
  973. * @param type Type of the parameter.
  974. * @param localTypeChecker Type checker set up for the specific file.
  975. */
  976. function migrateInjectDecorator(firstArg, type, localTypeChecker) {
  977. let injectedType = firstArg.getText();
  978. let typeArguments = null;
  979. // `inject` no longer officially supports string injection so we need
  980. // to cast to any. We maintain the type by passing it as a generic.
  981. if (ts.isStringLiteralLike(firstArg)) {
  982. typeArguments = [type || ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)];
  983. injectedType += ' as any';
  984. }
  985. else if (ts.isCallExpression(firstArg) &&
  986. ts.isIdentifier(firstArg.expression) &&
  987. firstArg.arguments.length === 1) {
  988. const callImport = imports.getImportOfIdentifier(localTypeChecker, firstArg.expression);
  989. const arrowFn = firstArg.arguments[0];
  990. // If the first parameter is a `forwardRef`, unwrap it for a more
  991. // accurate type and because it's no longer necessary.
  992. if (callImport !== null &&
  993. callImport.name === 'forwardRef' &&
  994. callImport.importModule === '@angular/core' &&
  995. ts.isArrowFunction(arrowFn)) {
  996. if (ts.isBlock(arrowFn.body)) {
  997. const returnStatement = arrowFn.body.statements.find((stmt) => ts.isReturnStatement(stmt));
  998. if (returnStatement && returnStatement.expression) {
  999. injectedType = returnStatement.expression.getText();
  1000. }
  1001. }
  1002. else {
  1003. injectedType = arrowFn.body.getText();
  1004. }
  1005. }
  1006. }
  1007. else if (type &&
  1008. (ts.isTypeReferenceNode(type) ||
  1009. ts.isTypeLiteralNode(type) ||
  1010. ts.isTupleTypeNode(type) ||
  1011. (ts.isUnionTypeNode(type) && type.types.some(ts.isTypeReferenceNode)))) {
  1012. typeArguments = [type];
  1013. }
  1014. return { injectedType, typeArguments };
  1015. }
  1016. /**
  1017. * Removes the parameters from a constructor. This is a bit more complex than just replacing an AST
  1018. * node, because `NodeArray.pos` includes any leading whitespace, but `NodeArray.end` does **not**
  1019. * include trailing whitespace. Since we want to produce somewhat formatted code, we need to find
  1020. * the end of the arguments ourselves. We do it by finding the next parenthesis after the last
  1021. * parameter.
  1022. * @param node Constructor from which to remove the parameters.
  1023. * @param tracker Object keeping track of the changes made to the file.
  1024. */
  1025. function stripConstructorParameters(node, tracker) {
  1026. if (node.parameters.length === 0) {
  1027. return;
  1028. }
  1029. const constructorText = node.getText();
  1030. const lastParamText = node.parameters[node.parameters.length - 1].getText();
  1031. const lastParamStart = constructorText.indexOf(lastParamText);
  1032. // This shouldn't happen, but bail out just in case so we don't mangle the code.
  1033. if (lastParamStart === -1) {
  1034. return;
  1035. }
  1036. for (let i = lastParamStart + lastParamText.length; i < constructorText.length; i++) {
  1037. const char = constructorText[i];
  1038. if (char === ')') {
  1039. tracker.replaceText(node.getSourceFile(), node.parameters.pos, node.getStart() + i - node.parameters.pos, '');
  1040. break;
  1041. }
  1042. }
  1043. }
  1044. /**
  1045. * Creates a type checker scoped to a specific file.
  1046. * @param sourceFile File for which to create the type checker.
  1047. */
  1048. function getLocalTypeChecker(sourceFile) {
  1049. const options = { noEmit: true, skipLibCheck: true };
  1050. const host = ts.createCompilerHost(options);
  1051. host.getSourceFile = (fileName) => (fileName === sourceFile.fileName ? sourceFile : undefined);
  1052. const program = ts.createProgram({
  1053. rootNames: [sourceFile.fileName],
  1054. options,
  1055. host,
  1056. });
  1057. return program.getTypeChecker();
  1058. }
  1059. /**
  1060. * Prints out an AST node and replaces the placeholder inside of it.
  1061. * @param sourceFile File in which the node will be inserted.
  1062. * @param node Node to be printed out.
  1063. * @param replacement Replacement for the placeholder.
  1064. * @param printer Printer used to output AST nodes as strings.
  1065. */
  1066. function replaceNodePlaceholder(sourceFile, node, replacement, printer) {
  1067. const result = printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);
  1068. return result.replace(PLACEHOLDER, replacement);
  1069. }
  1070. /**
  1071. * Clones an optional array of modifiers. Can be useful to
  1072. * strip the comments from a node with modifiers.
  1073. */
  1074. function cloneModifiers(modifiers) {
  1075. return modifiers?.map((modifier) => {
  1076. return ts.isDecorator(modifier)
  1077. ? ts.factory.createDecorator(modifier.expression)
  1078. : ts.factory.createModifier(modifier.kind);
  1079. });
  1080. }
  1081. /**
  1082. * Clones the name of a property. Can be useful to strip away
  1083. * the comments of a property without modifiers.
  1084. */
  1085. function cloneName(node) {
  1086. switch (node.kind) {
  1087. case ts.SyntaxKind.Identifier:
  1088. return ts.factory.createIdentifier(node.text);
  1089. case ts.SyntaxKind.StringLiteral:
  1090. return ts.factory.createStringLiteral(node.text, node.getText()[0] === `'`);
  1091. case ts.SyntaxKind.NoSubstitutionTemplateLiteral:
  1092. return ts.factory.createNoSubstitutionTemplateLiteral(node.text, node.rawText);
  1093. case ts.SyntaxKind.NumericLiteral:
  1094. return ts.factory.createNumericLiteral(node.text);
  1095. case ts.SyntaxKind.ComputedPropertyName:
  1096. return ts.factory.createComputedPropertyName(node.expression);
  1097. case ts.SyntaxKind.PrivateIdentifier:
  1098. return ts.factory.createPrivateIdentifier(node.text);
  1099. default:
  1100. return node;
  1101. }
  1102. }
  1103. /**
  1104. * Determines whether it's safe to delete a class constructor.
  1105. * @param options Options used to configure the migration.
  1106. * @param constructor Node representing the constructor.
  1107. * @param removedStatementCount Number of statements that were removed by the migration.
  1108. * @param prependToConstructor Statements that should be prepended to the constructor.
  1109. * @param superCall Node representing the `super()` call within the constructor.
  1110. */
  1111. function canRemoveConstructor(options, constructor, removedStatementCount, prependToConstructor, superCall) {
  1112. if (options.backwardsCompatibleConstructors || prependToConstructor.length > 0) {
  1113. return false;
  1114. }
  1115. const statementCount = constructor.body
  1116. ? constructor.body.statements.length - removedStatementCount
  1117. : 0;
  1118. return (statementCount === 0 ||
  1119. (statementCount === 1 && superCall !== null && superCall.arguments.length === 0));
  1120. }
  1121. /**
  1122. * Gets the next statement after a node that *won't* be deleted by the migration.
  1123. * @param startNode Node from which to start the search.
  1124. * @param removedStatements Statements that have been removed by the migration.
  1125. * @returns
  1126. */
  1127. function getNextPreservedStatement(startNode, removedStatements) {
  1128. const body = nodes.closestNode(startNode, ts.isBlock);
  1129. const closestStatement = nodes.closestNode(startNode, ts.isStatement);
  1130. if (body === null || closestStatement === null) {
  1131. return null;
  1132. }
  1133. const index = body.statements.indexOf(closestStatement);
  1134. if (index === -1) {
  1135. return null;
  1136. }
  1137. for (let i = index + 1; i < body.statements.length; i++) {
  1138. if (!removedStatements.has(body.statements[i])) {
  1139. return body.statements[i];
  1140. }
  1141. }
  1142. return null;
  1143. }
  1144. /**
  1145. * Applies the internal-specific migrations to a class.
  1146. * @param node Class being migrated.
  1147. * @param constructor The migrated class' constructor.
  1148. * @param localTypeChecker File-specific type checker.
  1149. * @param tracker Object keeping track of the changes.
  1150. * @param printer Printer used to output AST nodes as text.
  1151. * @param removedStatements Statements that have been removed by the migration.
  1152. * @param removedMembers Class members that have been removed by the migration.
  1153. * @param prependToClass Text that will be prepended to a class.
  1154. * @param afterInjectCalls Text that will be inserted after the newly-added `inject` calls.
  1155. * @param memberIndentation Indentation string of the class' members.
  1156. */
  1157. function applyInternalOnlyChanges(node, constructor, localTypeChecker, tracker, printer, removedStatements, removedMembers, prependToClass, afterInjectCalls, memberIndentation, options) {
  1158. const result = findUninitializedPropertiesToCombine(node, constructor, localTypeChecker, options);
  1159. if (result === null) {
  1160. return;
  1161. }
  1162. const preserveInitOrder = shouldCombineInInitializationOrder(result.toCombine, constructor);
  1163. // Sort the combined members based on the declaration order of their initializers, only if
  1164. // we've determined that would be safe. Note that `Array.prototype.sort` is in-place so we
  1165. // can just call it conditionally here.
  1166. if (preserveInitOrder) {
  1167. result.toCombine.sort((a, b) => a.initializer.getStart() - b.initializer.getStart());
  1168. }
  1169. result.toCombine.forEach(({ declaration, initializer }) => {
  1170. const initializerStatement = nodes.closestNode(initializer, ts.isStatement);
  1171. // Strip comments if we are just going modify the node in-place.
  1172. const modifiers = preserveInitOrder
  1173. ? declaration.modifiers
  1174. : cloneModifiers(declaration.modifiers);
  1175. const name = preserveInitOrder ? declaration.name : cloneName(declaration.name);
  1176. const newProperty = ts.factory.createPropertyDeclaration(modifiers, name, declaration.questionToken, declaration.type, undefined);
  1177. const propText = printer.printNode(ts.EmitHint.Unspecified, newProperty, declaration.getSourceFile());
  1178. const initializerText = replaceParameterReferencesInInitializer(initializer, constructor, localTypeChecker);
  1179. const withInitializer = `${propText.slice(0, -1)} = ${initializerText};`;
  1180. // If the initialization order is being preserved, we have to remove the original
  1181. // declaration and re-declare it. Otherwise we can do the replacement in-place.
  1182. if (preserveInitOrder) {
  1183. tracker.removeNode(declaration, true);
  1184. removedMembers.add(declaration);
  1185. afterInjectCalls.push(memberIndentation + withInitializer);
  1186. }
  1187. else {
  1188. const sourceFile = declaration.getSourceFile();
  1189. tracker.replaceText(sourceFile, declaration.getStart(), declaration.getWidth(), withInitializer);
  1190. }
  1191. // This should always be defined, but null check it just in case.
  1192. if (initializerStatement) {
  1193. tracker.removeNode(initializerStatement, true);
  1194. removedStatements.add(initializerStatement);
  1195. }
  1196. });
  1197. result.toHoist.forEach((decl) => {
  1198. prependToClass.push(memberIndentation + printer.printNode(ts.EmitHint.Unspecified, decl, decl.getSourceFile()));
  1199. tracker.removeNode(decl, true);
  1200. removedMembers.add(decl);
  1201. });
  1202. // If we added any hoisted properties, separate them visually with a new line.
  1203. if (prependToClass.length > 0) {
  1204. prependToClass.push('');
  1205. }
  1206. }
  1207. function replaceParameterReferencesInInitializer(initializer, constructor, localTypeChecker) {
  1208. // 1. Collect the locations of identifier nodes that reference constructor parameters.
  1209. // 2. Add `this.` to those locations.
  1210. const insertLocations = [0];
  1211. function walk(node) {
  1212. if (ts.isIdentifier(node) &&
  1213. !(ts.isPropertyAccessExpression(node.parent) && node === node.parent.name) &&
  1214. localTypeChecker
  1215. .getSymbolAtLocation(node)
  1216. ?.declarations?.some((decl) => constructor.parameters.includes(decl))) {
  1217. insertLocations.push(node.getStart() - initializer.getStart());
  1218. }
  1219. ts.forEachChild(node, walk);
  1220. }
  1221. walk(initializer);
  1222. const initializerText = initializer.getText();
  1223. insertLocations.push(initializerText.length);
  1224. insertLocations.sort((a, b) => a - b);
  1225. const result = [];
  1226. for (let i = 0; i < insertLocations.length - 1; i++) {
  1227. result.push(initializerText.slice(insertLocations[i], insertLocations[i + 1]));
  1228. }
  1229. return result.join('this.');
  1230. }
  1231. function migrate(options) {
  1232. return async (tree) => {
  1233. const basePath = process.cwd();
  1234. const pathToMigrate = compiler_host.normalizePath(p.join(basePath, options.path));
  1235. let allPaths = [];
  1236. if (pathToMigrate.trim() !== '') {
  1237. allPaths.push(pathToMigrate);
  1238. }
  1239. if (!allPaths.length) {
  1240. throw new schematics.SchematicsException('Could not find any tsconfig file. Cannot run the inject migration.');
  1241. }
  1242. for (const tsconfigPath of allPaths) {
  1243. runInjectMigration(tree, tsconfigPath, basePath, pathToMigrate, options);
  1244. }
  1245. };
  1246. }
  1247. function runInjectMigration(tree, tsconfigPath, basePath, pathToMigrate, schematicOptions) {
  1248. if (schematicOptions.path.startsWith('..')) {
  1249. throw new schematics.SchematicsException('Cannot run inject migration outside of the current project.');
  1250. }
  1251. const program = compiler_host.createMigrationProgram(tree, tsconfigPath, basePath);
  1252. const sourceFiles = program
  1253. .getSourceFiles()
  1254. .filter((sourceFile) => sourceFile.fileName.startsWith(pathToMigrate) &&
  1255. compiler_host.canMigrateFile(basePath, sourceFile, program));
  1256. if (sourceFiles.length === 0) {
  1257. throw new schematics.SchematicsException(`Could not find any files to migrate under the path ${pathToMigrate}. Cannot run the inject migration.`);
  1258. }
  1259. for (const sourceFile of sourceFiles) {
  1260. const changes = migrateFile(sourceFile, schematicOptions);
  1261. if (changes.length > 0) {
  1262. const update = tree.beginUpdate(p.relative(basePath, sourceFile.fileName));
  1263. for (const change of changes) {
  1264. if (change.removeLength != null) {
  1265. update.remove(change.start, change.removeLength);
  1266. }
  1267. update.insertRight(change.start, change.text);
  1268. }
  1269. tree.commitUpdate(update);
  1270. }
  1271. }
  1272. }
  1273. exports.migrate = migrate;