inject-migration.js 52 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173
  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 p = require('path');
  10. var compiler_host = require('./compiler_host-DzM2hemp.js');
  11. var ts = require('typescript');
  12. var ng_decorators = require('./ng_decorators-DznZ5jMl.js');
  13. var imports = require('./imports-CIX-JgAN.js');
  14. var nodes = require('./nodes-B16H9JUd.js');
  15. var leading_space = require('./leading_space-D9nQ8UQC.js');
  16. require('./checker-DP-zos5Q.js');
  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. });
  237. }
  238. else {
  239. node.forEachChild(walk);
  240. }
  241. });
  242. return usedParams;
  243. }
  244. /**
  245. * Determines if a specific parameter has references to other parameters.
  246. * @param param Parameter to check.
  247. * @param allParameters All parameters of the containing function.
  248. * @param localTypeChecker Type checker scoped to the current file.
  249. */
  250. function parameterReferencesOtherParameters(param, allParameters, localTypeChecker) {
  251. // A parameter can only reference other parameters through its initializer.
  252. if (!param.initializer || allParameters.length < 2) {
  253. return false;
  254. }
  255. const paramNames = new Set();
  256. for (const current of allParameters) {
  257. if (current !== param && ts.isIdentifier(current.name)) {
  258. paramNames.add(current.name.text);
  259. }
  260. }
  261. let result = false;
  262. const analyze = (node) => {
  263. if (ts.isIdentifier(node) && paramNames.has(node.text) && !isAccessedViaThis(node)) {
  264. const symbol = localTypeChecker.getSymbolAtLocation(node);
  265. const referencesOtherParam = symbol?.declarations?.some((decl) => {
  266. return allParameters.includes(decl);
  267. });
  268. if (referencesOtherParam) {
  269. result = true;
  270. }
  271. }
  272. if (!result) {
  273. node.forEachChild(analyze);
  274. }
  275. };
  276. analyze(param.initializer);
  277. return result;
  278. }
  279. /** Checks whether a parameter node declares a property on its class. */
  280. function parameterDeclaresProperty(node) {
  281. return !!node.modifiers?.some(({ kind }) => kind === ts.SyntaxKind.PublicKeyword ||
  282. kind === ts.SyntaxKind.PrivateKeyword ||
  283. kind === ts.SyntaxKind.ProtectedKeyword ||
  284. kind === ts.SyntaxKind.ReadonlyKeyword);
  285. }
  286. /** Checks whether a type node is nullable. */
  287. function isNullableType(node) {
  288. // Apparently `foo: null` is `Parameter<TypeNode<NullKeyword>>`,
  289. // while `foo: undefined` is `Parameter<UndefinedKeyword>`...
  290. if (node.kind === ts.SyntaxKind.UndefinedKeyword || node.kind === ts.SyntaxKind.VoidKeyword) {
  291. return true;
  292. }
  293. if (ts.isLiteralTypeNode(node)) {
  294. return node.literal.kind === ts.SyntaxKind.NullKeyword;
  295. }
  296. if (ts.isUnionTypeNode(node)) {
  297. return node.types.some(isNullableType);
  298. }
  299. return false;
  300. }
  301. /** Checks whether a type node has generic arguments. */
  302. function hasGenerics(node) {
  303. if (ts.isTypeReferenceNode(node)) {
  304. return node.typeArguments != null && node.typeArguments.length > 0;
  305. }
  306. if (ts.isUnionTypeNode(node)) {
  307. return node.types.some(hasGenerics);
  308. }
  309. return false;
  310. }
  311. /** Checks whether an identifier is accessed through `this`, e.g. `this.<some identifier>`. */
  312. function isAccessedViaThis(node) {
  313. return (ts.isPropertyAccessExpression(node.parent) &&
  314. node.parent.expression.kind === ts.SyntaxKind.ThisKeyword &&
  315. node.parent.name === node);
  316. }
  317. /** Finds a `super` call inside of a specific node. */
  318. function findSuperCall(root) {
  319. let result = null;
  320. root.forEachChild(function find(node) {
  321. if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.SuperKeyword) {
  322. result = node;
  323. }
  324. else if (result === null) {
  325. node.forEachChild(find);
  326. }
  327. });
  328. return result;
  329. }
  330. /*!
  331. * @license
  332. * Copyright Google LLC All Rights Reserved.
  333. *
  334. * Use of this source code is governed by an MIT-style license that can be
  335. * found in the LICENSE file at https://angular.dev/license
  336. */
  337. /**
  338. * Finds class property declarations without initializers whose constructor-based initialization
  339. * can be inlined into the declaration spot after migrating to `inject`. For example:
  340. *
  341. * ```ts
  342. * private foo: number;
  343. *
  344. * constructor(private service: MyService) {
  345. * this.foo = this.service.getFoo();
  346. * }
  347. * ```
  348. *
  349. * The initializer of `foo` can be inlined, because `service` will be initialized
  350. * before it after the `inject` migration has finished running.
  351. *
  352. * @param node Class declaration that is being migrated.
  353. * @param constructor Constructor declaration of the class being migrated.
  354. * @param localTypeChecker Type checker scoped to the current file.
  355. */
  356. function findUninitializedPropertiesToCombine(node, constructor, localTypeChecker) {
  357. let toCombine = null;
  358. let toHoist = [];
  359. const membersToDeclarations = new Map();
  360. for (const member of node.members) {
  361. if (ts.isPropertyDeclaration(member) &&
  362. !member.initializer &&
  363. !ts.isComputedPropertyName(member.name)) {
  364. membersToDeclarations.set(member.name.text, member);
  365. }
  366. }
  367. if (membersToDeclarations.size === 0) {
  368. return null;
  369. }
  370. const memberInitializers = getMemberInitializers(constructor);
  371. if (memberInitializers === null) {
  372. return null;
  373. }
  374. for (const [name, decl] of membersToDeclarations.entries()) {
  375. if (memberInitializers.has(name)) {
  376. const initializer = memberInitializers.get(name);
  377. if (!hasLocalReferences(initializer, constructor, localTypeChecker)) {
  378. toCombine ??= [];
  379. toCombine.push({ declaration: membersToDeclarations.get(name), initializer });
  380. }
  381. }
  382. else {
  383. // Mark members that have no initializers and can't be combined to be hoisted above the
  384. // injected members. This is either a no-op or it allows us to avoid some patterns internally
  385. // like the following:
  386. // ```
  387. // class Foo {
  388. // publicFoo: Foo;
  389. // private privateFoo: Foo;
  390. //
  391. // constructor() {
  392. // this.initializePrivateFooSomehow();
  393. // this.publicFoo = this.privateFoo;
  394. // }
  395. // }
  396. // ```
  397. toHoist.push(decl);
  398. }
  399. }
  400. // If no members need to be combined, none need to be hoisted either.
  401. return toCombine === null ? null : { toCombine, toHoist };
  402. }
  403. /**
  404. * In some cases properties may be declared out of order, but initialized in the correct order.
  405. * The internal-specific migration will combine such properties which will result in a compilation
  406. * error, for example:
  407. *
  408. * ```ts
  409. * class MyClass {
  410. * foo: Foo;
  411. * bar: Bar;
  412. *
  413. * constructor(bar: Bar) {
  414. * this.bar = bar;
  415. * this.foo = this.bar.getFoo();
  416. * }
  417. * }
  418. * ```
  419. *
  420. * Will become:
  421. *
  422. * ```ts
  423. * class MyClass {
  424. * foo: Foo = this.bar.getFoo();
  425. * bar: Bar = inject(Bar);
  426. * }
  427. * ```
  428. *
  429. * This function determines if cases like this can be saved by reordering the properties so their
  430. * declaration order matches the order in which they're initialized.
  431. *
  432. * @param toCombine Properties that are candidates to be combined.
  433. * @param constructor
  434. */
  435. function shouldCombineInInitializationOrder(toCombine, constructor) {
  436. let combinedMemberReferenceCount = 0;
  437. let otherMemberReferenceCount = 0;
  438. const injectedMemberNames = new Set();
  439. const combinedMemberNames = new Set();
  440. // Collect the name of constructor parameters that declare new properties.
  441. // These can be ignored since they'll be hoisted above other properties.
  442. constructor.parameters.forEach((param) => {
  443. if (parameterDeclaresProperty(param) && ts.isIdentifier(param.name)) {
  444. injectedMemberNames.add(param.name.text);
  445. }
  446. });
  447. // Collect the names of the properties being combined. We should only reorder
  448. // the properties if at least one of them refers to another one.
  449. toCombine.forEach(({ declaration: { name } }) => {
  450. if (ts.isStringLiteralLike(name) || ts.isIdentifier(name)) {
  451. combinedMemberNames.add(name.text);
  452. }
  453. });
  454. // Visit all the initializers and check all the property reads in the form of `this.<name>`.
  455. // Skip over the ones referring to injected parameters since they're going to be hoisted.
  456. const walkInitializer = (node) => {
  457. if (ts.isPropertyAccessExpression(node) && node.expression.kind === ts.SyntaxKind.ThisKeyword) {
  458. if (combinedMemberNames.has(node.name.text)) {
  459. combinedMemberReferenceCount++;
  460. }
  461. else if (!injectedMemberNames.has(node.name.text)) {
  462. otherMemberReferenceCount++;
  463. }
  464. }
  465. node.forEachChild(walkInitializer);
  466. };
  467. toCombine.forEach((candidate) => walkInitializer(candidate.initializer));
  468. // If at the end there is at least one reference between a combined member and another,
  469. // and there are no references to any other class members, we can safely reorder the
  470. // properties based on how they were initialized.
  471. return combinedMemberReferenceCount > 0 && otherMemberReferenceCount === 0;
  472. }
  473. /**
  474. * Finds the expressions from the constructor that initialize class members, for example:
  475. *
  476. * ```ts
  477. * private foo: number;
  478. *
  479. * constructor() {
  480. * this.foo = 123;
  481. * }
  482. * ```
  483. *
  484. * @param constructor Constructor declaration being analyzed.
  485. */
  486. function getMemberInitializers(constructor) {
  487. let memberInitializers = null;
  488. if (!constructor.body) {
  489. return memberInitializers;
  490. }
  491. // Only look at top-level constructor statements.
  492. for (const node of constructor.body.statements) {
  493. // Only look for statements in the form of `this.<name> = <expr>;` or `this[<name>] = <expr>;`.
  494. if (!ts.isExpressionStatement(node) ||
  495. !ts.isBinaryExpression(node.expression) ||
  496. node.expression.operatorToken.kind !== ts.SyntaxKind.EqualsToken ||
  497. (!ts.isPropertyAccessExpression(node.expression.left) &&
  498. !ts.isElementAccessExpression(node.expression.left)) ||
  499. node.expression.left.expression.kind !== ts.SyntaxKind.ThisKeyword) {
  500. continue;
  501. }
  502. let name;
  503. if (ts.isPropertyAccessExpression(node.expression.left)) {
  504. name = node.expression.left.name.text;
  505. }
  506. else if (ts.isElementAccessExpression(node.expression.left)) {
  507. name = ts.isStringLiteralLike(node.expression.left.argumentExpression)
  508. ? node.expression.left.argumentExpression.text
  509. : undefined;
  510. }
  511. // If the member is initialized multiple times, take the first one.
  512. if (name && (!memberInitializers || !memberInitializers.has(name))) {
  513. memberInitializers = memberInitializers || new Map();
  514. memberInitializers.set(name, node.expression.right);
  515. }
  516. }
  517. return memberInitializers;
  518. }
  519. /**
  520. * Determines if a node has references to local symbols defined in the constructor.
  521. * @param root Expression to check for local references.
  522. * @param constructor Constructor within which the expression is used.
  523. * @param localTypeChecker Type checker scoped to the current file.
  524. */
  525. function hasLocalReferences(root, constructor, localTypeChecker) {
  526. const sourceFile = root.getSourceFile();
  527. let hasLocalRefs = false;
  528. const walk = (node) => {
  529. // Stop searching if we know that it has local references.
  530. if (hasLocalRefs) {
  531. return;
  532. }
  533. // Skip identifiers that are accessed via `this` since they're accessing class members
  534. // that aren't local to the constructor. This is here primarily to catch cases like this
  535. // where `foo` is defined inside the constructor, but is a class member:
  536. // ```
  537. // constructor(private foo: Foo) {
  538. // this.bar = this.foo.getFoo();
  539. // }
  540. // ```
  541. if (ts.isIdentifier(node) && !isAccessedViaThis(node)) {
  542. const declarations = localTypeChecker.getSymbolAtLocation(node)?.declarations;
  543. const isReferencingLocalSymbol = declarations?.some((decl) =>
  544. // The source file check is a bit redundant since the type checker
  545. // is local to the file, but it's inexpensive and it can prevent
  546. // bugs in the future if we decide to use a full type checker.
  547. decl.getSourceFile() === sourceFile &&
  548. decl.getStart() >= constructor.getStart() &&
  549. decl.getEnd() <= constructor.getEnd() &&
  550. !isInsideInlineFunction(decl, constructor));
  551. if (isReferencingLocalSymbol) {
  552. hasLocalRefs = true;
  553. }
  554. }
  555. if (!hasLocalRefs) {
  556. node.forEachChild(walk);
  557. }
  558. };
  559. walk(root);
  560. return hasLocalRefs;
  561. }
  562. /**
  563. * Determines if a node is defined inside of an inline function.
  564. * @param startNode Node from which to start checking for inline functions.
  565. * @param boundary Node at which to stop searching.
  566. */
  567. function isInsideInlineFunction(startNode, boundary) {
  568. let current = startNode;
  569. while (current) {
  570. if (current === boundary) {
  571. return false;
  572. }
  573. if (ts.isFunctionDeclaration(current) ||
  574. ts.isFunctionExpression(current) ||
  575. ts.isArrowFunction(current)) {
  576. return true;
  577. }
  578. current = current.parent;
  579. }
  580. return false;
  581. }
  582. /**
  583. * Placeholder used to represent expressions inside the AST.
  584. * Includes Unicode characters to reduce the chance of collisions.
  585. */
  586. const PLACEHOLDER = 'ɵɵngGeneratePlaceholderɵɵ';
  587. /**
  588. * Migrates all of the classes in a `SourceFile` away from constructor injection.
  589. * @param sourceFile File to be migrated.
  590. * @param options Options that configure the migration.
  591. */
  592. function migrateFile(sourceFile, options) {
  593. // Note: even though externally we have access to the full program with a proper type
  594. // checker, we create a new one that is local to the file for a couple of reasons:
  595. // 1. Not having to depend on a program makes running the migration internally faster and easier.
  596. // 2. All the necessary information for this migration is local so using a file-specific type
  597. // checker should speed up the lookups.
  598. const localTypeChecker = getLocalTypeChecker(sourceFile);
  599. const analysis = analyzeFile(sourceFile, localTypeChecker, options);
  600. if (analysis === null || analysis.classes.length === 0) {
  601. return [];
  602. }
  603. const printer = ts.createPrinter();
  604. const tracker = new compiler_host.ChangeTracker(printer);
  605. analysis.classes.forEach(({ node, constructor, superCall }) => {
  606. const memberIndentation = leading_space.getLeadingLineWhitespaceOfNode(node.members[0]);
  607. const prependToClass = [];
  608. const afterInjectCalls = [];
  609. const removedStatements = new Set();
  610. const removedMembers = new Set();
  611. if (options._internalCombineMemberInitializers) {
  612. applyInternalOnlyChanges(node, constructor, localTypeChecker, tracker, printer, removedStatements, removedMembers, prependToClass, afterInjectCalls, memberIndentation);
  613. }
  614. migrateClass(node, constructor, superCall, options, memberIndentation, prependToClass, afterInjectCalls, removedStatements, removedMembers, localTypeChecker, printer, tracker);
  615. });
  616. DI_PARAM_SYMBOLS.forEach((name) => {
  617. // Both zero and undefined are fine here.
  618. if (!analysis.nonDecoratorReferences[name]) {
  619. tracker.removeImport(sourceFile, name, '@angular/core');
  620. }
  621. });
  622. return tracker.recordChanges().get(sourceFile) || [];
  623. }
  624. /**
  625. * Migrates a class away from constructor injection.
  626. * @param node Class to be migrated.
  627. * @param constructor Reference to the class' constructor node.
  628. * @param superCall Reference to the constructor's `super()` call, if any.
  629. * @param options Options used to configure the migration.
  630. * @param memberIndentation Indentation string of the members of the class.
  631. * @param prependToClass Text that should be prepended to the class.
  632. * @param afterInjectCalls Text that will be inserted after the newly-added `inject` calls.
  633. * @param removedStatements Statements that have been removed from the constructor already.
  634. * @param removedMembers Class members that have been removed by the migration.
  635. * @param localTypeChecker Type checker set up for the specific file.
  636. * @param printer Printer used to output AST nodes as strings.
  637. * @param tracker Object keeping track of the changes made to the file.
  638. */
  639. function migrateClass(node, constructor, superCall, options, memberIndentation, prependToClass, afterInjectCalls, removedStatements, removedMembers, localTypeChecker, printer, tracker) {
  640. const sourceFile = node.getSourceFile();
  641. const unusedParameters = getConstructorUnusedParameters(constructor, localTypeChecker, removedStatements);
  642. const superParameters = superCall
  643. ? getSuperParameters(constructor, superCall, localTypeChecker)
  644. : null;
  645. const removedStatementCount = removedStatements.size;
  646. const firstConstructorStatement = constructor.body?.statements.find((statement) => !removedStatements.has(statement));
  647. const innerReference = superCall || firstConstructorStatement || constructor;
  648. const innerIndentation = leading_space.getLeadingLineWhitespaceOfNode(innerReference);
  649. const prependToConstructor = [];
  650. const afterSuper = [];
  651. for (const param of constructor.parameters) {
  652. const usedInSuper = superParameters !== null && superParameters.has(param);
  653. const usedInConstructor = !unusedParameters.has(param);
  654. const usesOtherParams = parameterReferencesOtherParameters(param, constructor.parameters, localTypeChecker);
  655. migrateParameter(param, options, localTypeChecker, printer, tracker, superCall, usedInSuper, usedInConstructor, usesOtherParams, memberIndentation, innerIndentation, prependToConstructor, prependToClass, afterSuper);
  656. }
  657. // Delete all of the constructor overloads since below we're either going to
  658. // remove the implementation, or we're going to delete all of the parameters.
  659. for (const member of node.members) {
  660. if (ts.isConstructorDeclaration(member) && member !== constructor) {
  661. removedMembers.add(member);
  662. tracker.removeNode(member, true);
  663. }
  664. }
  665. if (canRemoveConstructor(options, constructor, removedStatementCount, prependToConstructor, superCall)) {
  666. // Drop the constructor if it was empty.
  667. removedMembers.add(constructor);
  668. tracker.removeNode(constructor, true);
  669. }
  670. else {
  671. // If the constructor contains any statements, only remove the parameters.
  672. // We always do this no matter what is passed into `backwardsCompatibleConstructors`.
  673. stripConstructorParameters(constructor, tracker);
  674. if (prependToConstructor.length > 0) {
  675. if (firstConstructorStatement ||
  676. (innerReference !== constructor &&
  677. innerReference.getStart() >= constructor.getStart() &&
  678. innerReference.getEnd() <= constructor.getEnd())) {
  679. tracker.insertText(sourceFile, (firstConstructorStatement || innerReference).getFullStart(), `\n${prependToConstructor.join('\n')}\n`);
  680. }
  681. else {
  682. tracker.insertText(sourceFile, constructor.body.getStart() + 1, `\n${prependToConstructor.map((p) => innerIndentation + p).join('\n')}\n${innerIndentation}`);
  683. }
  684. }
  685. }
  686. if (afterSuper.length > 0 && superCall !== null) {
  687. // Note that if we can, we should insert before the next statement after the `super` call,
  688. // rather than after the end of it. Otherwise the string buffering implementation may drop
  689. // the text if the statement after the `super` call is being deleted. This appears to be because
  690. // the full start of the next statement appears to always be the end of the `super` call plus 1.
  691. const nextStatement = getNextPreservedStatement(superCall, removedStatements);
  692. tracker.insertText(sourceFile, nextStatement ? nextStatement.getFullStart() : constructor.getEnd() - 1, `\n${afterSuper.join('\n')}\n` + (nextStatement ? '' : memberIndentation));
  693. }
  694. // Need to resolve this once all constructor signatures have been removed.
  695. const memberReference = node.members.find((m) => !removedMembers.has(m)) || node.members[0];
  696. // If `backwardsCompatibleConstructors` is enabled, we maintain
  697. // backwards compatibility by adding a catch-all signature.
  698. if (options.backwardsCompatibleConstructors) {
  699. const extraSignature = `\n${memberIndentation}/** Inserted by Angular inject() migration for backwards compatibility */\n` +
  700. `${memberIndentation}constructor(...args: unknown[]);`;
  701. // The new signature always has to be right before the constructor implementation.
  702. if (memberReference === constructor) {
  703. prependToClass.push(extraSignature);
  704. }
  705. else {
  706. tracker.insertText(sourceFile, constructor.getFullStart(), '\n' + extraSignature);
  707. }
  708. }
  709. // Push the block of code that should appear after the `inject`
  710. // calls now once all the members have been generated.
  711. prependToClass.push(...afterInjectCalls);
  712. if (prependToClass.length > 0) {
  713. if (removedMembers.size === node.members.length) {
  714. tracker.insertText(sourceFile,
  715. // If all members were deleted, insert after the last one.
  716. // This allows us to preserve the indentation.
  717. node.members.length > 0
  718. ? node.members[node.members.length - 1].getEnd() + 1
  719. : node.getEnd() - 1, `${prependToClass.join('\n')}\n`);
  720. }
  721. else {
  722. // Insert the new properties after the first member that hasn't been deleted.
  723. tracker.insertText(sourceFile, memberReference.getFullStart(), `\n${prependToClass.join('\n')}\n`);
  724. }
  725. }
  726. }
  727. /**
  728. * Migrates a single parameter to `inject()` DI.
  729. * @param node Parameter to be migrated.
  730. * @param options Options used to configure the migration.
  731. * @param localTypeChecker Type checker set up for the specific file.
  732. * @param printer Printer used to output AST nodes as strings.
  733. * @param tracker Object keeping track of the changes made to the file.
  734. * @param superCall Call to `super()` from the class' constructor.
  735. * @param usedInSuper Whether the parameter is referenced inside of `super`.
  736. * @param usedInConstructor Whether the parameter is referenced inside the body of the constructor.
  737. * @param memberIndentation Indentation string to use when inserting new class members.
  738. * @param innerIndentation Indentation string to use when inserting new constructor statements.
  739. * @param prependToConstructor Statements to be prepended to the constructor.
  740. * @param propsToAdd Properties to be added to the class.
  741. * @param afterSuper Statements to be added after the `super` call.
  742. */
  743. function migrateParameter(node, options, localTypeChecker, printer, tracker, superCall, usedInSuper, usedInConstructor, usesOtherParams, memberIndentation, innerIndentation, prependToConstructor, propsToAdd, afterSuper) {
  744. if (!ts.isIdentifier(node.name)) {
  745. return;
  746. }
  747. const name = node.name.text;
  748. const replacementCall = createInjectReplacementCall(node, options, localTypeChecker, printer, tracker);
  749. const declaresProp = parameterDeclaresProperty(node);
  750. // If the parameter declares a property, we need to declare it (e.g. `private foo: Foo`).
  751. if (declaresProp) {
  752. // We can't initialize the property if it's referenced within a `super` call or it references
  753. // other parameters. See the logic further below for the initialization.
  754. const canInitialize = !usedInSuper && !usesOtherParams;
  755. const prop = ts.factory.createPropertyDeclaration(cloneModifiers(node.modifiers?.filter((modifier) => {
  756. // Strip out the DI decorators, as well as `public` which is redundant.
  757. return !ts.isDecorator(modifier) && modifier.kind !== ts.SyntaxKind.PublicKeyword;
  758. })), name,
  759. // Don't add the question token to private properties since it won't affect interface implementation.
  760. node.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.PrivateKeyword)
  761. ? undefined
  762. : node.questionToken, canInitialize ? undefined : node.type, canInitialize ? ts.factory.createIdentifier(PLACEHOLDER) : undefined);
  763. propsToAdd.push(memberIndentation +
  764. replaceNodePlaceholder(node.getSourceFile(), prop, replacementCall, printer));
  765. }
  766. // If the parameter is referenced within the constructor, we need to declare it as a variable.
  767. if (usedInConstructor) {
  768. if (usedInSuper) {
  769. // Usages of `this` aren't allowed before `super` calls so we need to
  770. // create a variable which calls `inject()` directly instead...
  771. prependToConstructor.push(`${innerIndentation}const ${name} = ${replacementCall};`);
  772. // ...then we can initialize the property after the `super` call.
  773. if (declaresProp) {
  774. afterSuper.push(`${innerIndentation}this.${name} = ${name};`);
  775. }
  776. }
  777. else if (declaresProp) {
  778. // If the parameter declares a property (`private foo: foo`) and is used inside the class
  779. // at the same time, we need to ensure that it's initialized to the value from the variable
  780. // and that we only reference `this` after the `super` call.
  781. const initializer = `${innerIndentation}const ${name} = this.${name};`;
  782. if (superCall === null) {
  783. prependToConstructor.push(initializer);
  784. }
  785. else {
  786. afterSuper.push(initializer);
  787. }
  788. }
  789. else {
  790. // If the parameter is only referenced in the constructor, we
  791. // don't need to declare any new properties.
  792. prependToConstructor.push(`${innerIndentation}const ${name} = ${replacementCall};`);
  793. }
  794. }
  795. else if (usesOtherParams && declaresProp) {
  796. const toAdd = `${innerIndentation}this.${name} = ${replacementCall};`;
  797. if (superCall === null) {
  798. prependToConstructor.push(toAdd);
  799. }
  800. else {
  801. afterSuper.push(toAdd);
  802. }
  803. }
  804. }
  805. /**
  806. * Creates a replacement `inject` call from a function parameter.
  807. * @param param Parameter for which to generate the `inject` call.
  808. * @param options Options used to configure the migration.
  809. * @param localTypeChecker Type checker set up for the specific file.
  810. * @param printer Printer used to output AST nodes as strings.
  811. * @param tracker Object keeping track of the changes made to the file.
  812. */
  813. function createInjectReplacementCall(param, options, localTypeChecker, printer, tracker) {
  814. const moduleName = '@angular/core';
  815. const sourceFile = param.getSourceFile();
  816. const decorators = ng_decorators.getAngularDecorators(localTypeChecker, ts.getDecorators(param) || []);
  817. const literalProps = [];
  818. const type = param.type;
  819. let injectedType = '';
  820. let typeArguments = type && hasGenerics(type) ? [type] : undefined;
  821. let hasOptionalDecorator = false;
  822. if (type) {
  823. // Remove the type arguments from generic type references, because
  824. // they'll be specified as type arguments to `inject()`.
  825. if (ts.isTypeReferenceNode(type) && type.typeArguments && type.typeArguments.length > 0) {
  826. injectedType = type.typeName.getText();
  827. }
  828. else if (ts.isUnionTypeNode(type)) {
  829. injectedType = (type.types.find((t) => !ts.isLiteralTypeNode(t)) || type.types[0]).getText();
  830. }
  831. else {
  832. injectedType = type.getText();
  833. }
  834. }
  835. for (const decorator of decorators) {
  836. if (decorator.moduleName !== moduleName) {
  837. continue;
  838. }
  839. const firstArg = decorator.node.expression.arguments[0];
  840. switch (decorator.name) {
  841. case 'Inject':
  842. if (firstArg) {
  843. const injectResult = migrateInjectDecorator(firstArg, type, localTypeChecker);
  844. injectedType = injectResult.injectedType;
  845. if (injectResult.typeArguments) {
  846. typeArguments = injectResult.typeArguments;
  847. }
  848. }
  849. break;
  850. case 'Attribute':
  851. if (firstArg) {
  852. const constructorRef = tracker.addImport(sourceFile, 'HostAttributeToken', moduleName);
  853. const expression = ts.factory.createNewExpression(constructorRef, undefined, [firstArg]);
  854. injectedType = printer.printNode(ts.EmitHint.Unspecified, expression, sourceFile);
  855. typeArguments = undefined;
  856. }
  857. break;
  858. case 'Optional':
  859. hasOptionalDecorator = true;
  860. literalProps.push(ts.factory.createPropertyAssignment('optional', ts.factory.createTrue()));
  861. break;
  862. case 'SkipSelf':
  863. literalProps.push(ts.factory.createPropertyAssignment('skipSelf', ts.factory.createTrue()));
  864. break;
  865. case 'Self':
  866. literalProps.push(ts.factory.createPropertyAssignment('self', ts.factory.createTrue()));
  867. break;
  868. case 'Host':
  869. literalProps.push(ts.factory.createPropertyAssignment('host', ts.factory.createTrue()));
  870. break;
  871. }
  872. }
  873. // The injected type might be a `TypeNode` which we can't easily convert into an `Expression`.
  874. // Since the value gets passed through directly anyway, we generate the call using a placeholder
  875. // which we then replace with the raw text of the `TypeNode`.
  876. const injectRef = tracker.addImport(param.getSourceFile(), 'inject', moduleName);
  877. const args = [ts.factory.createIdentifier(PLACEHOLDER)];
  878. if (literalProps.length > 0) {
  879. args.push(ts.factory.createObjectLiteralExpression(literalProps));
  880. }
  881. let expression = ts.factory.createCallExpression(injectRef, typeArguments, args);
  882. if (hasOptionalDecorator && options.nonNullableOptional) {
  883. const hasNullableType = param.questionToken != null || (param.type != null && isNullableType(param.type));
  884. // Only wrap the expression if the type wasn't already nullable.
  885. // If it was, the app was likely accounting for it already.
  886. if (!hasNullableType) {
  887. expression = ts.factory.createNonNullExpression(expression);
  888. }
  889. }
  890. // If the parameter is initialized, add the initializer as a fallback.
  891. if (param.initializer) {
  892. expression = ts.factory.createBinaryExpression(expression, ts.SyntaxKind.QuestionQuestionToken, param.initializer);
  893. }
  894. return replaceNodePlaceholder(param.getSourceFile(), expression, injectedType, printer);
  895. }
  896. /**
  897. * Migrates a parameter based on its `@Inject()` decorator.
  898. * @param firstArg First argument to `@Inject()`.
  899. * @param type Type of the parameter.
  900. * @param localTypeChecker Type checker set up for the specific file.
  901. */
  902. function migrateInjectDecorator(firstArg, type, localTypeChecker) {
  903. let injectedType = firstArg.getText();
  904. let typeArguments = null;
  905. // `inject` no longer officially supports string injection so we need
  906. // to cast to any. We maintain the type by passing it as a generic.
  907. if (ts.isStringLiteralLike(firstArg)) {
  908. typeArguments = [type || ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)];
  909. injectedType += ' as any';
  910. }
  911. else if (ts.isCallExpression(firstArg) &&
  912. ts.isIdentifier(firstArg.expression) &&
  913. firstArg.arguments.length === 1) {
  914. const callImport = imports.getImportOfIdentifier(localTypeChecker, firstArg.expression);
  915. const arrowFn = firstArg.arguments[0];
  916. // If the first parameter is a `forwardRef`, unwrap it for a more
  917. // accurate type and because it's no longer necessary.
  918. if (callImport !== null &&
  919. callImport.name === 'forwardRef' &&
  920. callImport.importModule === '@angular/core' &&
  921. ts.isArrowFunction(arrowFn)) {
  922. if (ts.isBlock(arrowFn.body)) {
  923. const returnStatement = arrowFn.body.statements.find((stmt) => ts.isReturnStatement(stmt));
  924. if (returnStatement && returnStatement.expression) {
  925. injectedType = returnStatement.expression.getText();
  926. }
  927. }
  928. else {
  929. injectedType = arrowFn.body.getText();
  930. }
  931. }
  932. }
  933. else if (type &&
  934. (ts.isTypeReferenceNode(type) ||
  935. ts.isTypeLiteralNode(type) ||
  936. ts.isTupleTypeNode(type) ||
  937. (ts.isUnionTypeNode(type) && type.types.some(ts.isTypeReferenceNode)))) {
  938. typeArguments = [type];
  939. }
  940. return { injectedType, typeArguments };
  941. }
  942. /**
  943. * Removes the parameters from a constructor. This is a bit more complex than just replacing an AST
  944. * node, because `NodeArray.pos` includes any leading whitespace, but `NodeArray.end` does **not**
  945. * include trailing whitespace. Since we want to produce somewhat formatted code, we need to find
  946. * the end of the arguments ourselves. We do it by finding the next parenthesis after the last
  947. * parameter.
  948. * @param node Constructor from which to remove the parameters.
  949. * @param tracker Object keeping track of the changes made to the file.
  950. */
  951. function stripConstructorParameters(node, tracker) {
  952. if (node.parameters.length === 0) {
  953. return;
  954. }
  955. const constructorText = node.getText();
  956. const lastParamText = node.parameters[node.parameters.length - 1].getText();
  957. const lastParamStart = constructorText.indexOf(lastParamText);
  958. // This shouldn't happen, but bail out just in case so we don't mangle the code.
  959. if (lastParamStart === -1) {
  960. return;
  961. }
  962. for (let i = lastParamStart + lastParamText.length; i < constructorText.length; i++) {
  963. const char = constructorText[i];
  964. if (char === ')') {
  965. tracker.replaceText(node.getSourceFile(), node.parameters.pos, node.getStart() + i - node.parameters.pos, '');
  966. break;
  967. }
  968. }
  969. }
  970. /**
  971. * Creates a type checker scoped to a specific file.
  972. * @param sourceFile File for which to create the type checker.
  973. */
  974. function getLocalTypeChecker(sourceFile) {
  975. const options = { noEmit: true, skipLibCheck: true };
  976. const host = ts.createCompilerHost(options);
  977. host.getSourceFile = (fileName) => (fileName === sourceFile.fileName ? sourceFile : undefined);
  978. const program = ts.createProgram({
  979. rootNames: [sourceFile.fileName],
  980. options,
  981. host,
  982. });
  983. return program.getTypeChecker();
  984. }
  985. /**
  986. * Prints out an AST node and replaces the placeholder inside of it.
  987. * @param sourceFile File in which the node will be inserted.
  988. * @param node Node to be printed out.
  989. * @param replacement Replacement for the placeholder.
  990. * @param printer Printer used to output AST nodes as strings.
  991. */
  992. function replaceNodePlaceholder(sourceFile, node, replacement, printer) {
  993. const result = printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);
  994. return result.replace(PLACEHOLDER, replacement);
  995. }
  996. /**
  997. * Clones an optional array of modifiers. Can be useful to
  998. * strip the comments from a node with modifiers.
  999. */
  1000. function cloneModifiers(modifiers) {
  1001. return modifiers?.map((modifier) => {
  1002. return ts.isDecorator(modifier)
  1003. ? ts.factory.createDecorator(modifier.expression)
  1004. : ts.factory.createModifier(modifier.kind);
  1005. });
  1006. }
  1007. /**
  1008. * Clones the name of a property. Can be useful to strip away
  1009. * the comments of a property without modifiers.
  1010. */
  1011. function cloneName(node) {
  1012. switch (node.kind) {
  1013. case ts.SyntaxKind.Identifier:
  1014. return ts.factory.createIdentifier(node.text);
  1015. case ts.SyntaxKind.StringLiteral:
  1016. return ts.factory.createStringLiteral(node.text, node.getText()[0] === `'`);
  1017. case ts.SyntaxKind.NoSubstitutionTemplateLiteral:
  1018. return ts.factory.createNoSubstitutionTemplateLiteral(node.text, node.rawText);
  1019. case ts.SyntaxKind.NumericLiteral:
  1020. return ts.factory.createNumericLiteral(node.text);
  1021. case ts.SyntaxKind.ComputedPropertyName:
  1022. return ts.factory.createComputedPropertyName(node.expression);
  1023. case ts.SyntaxKind.PrivateIdentifier:
  1024. return ts.factory.createPrivateIdentifier(node.text);
  1025. default:
  1026. return node;
  1027. }
  1028. }
  1029. /**
  1030. * Determines whether it's safe to delete a class constructor.
  1031. * @param options Options used to configure the migration.
  1032. * @param constructor Node representing the constructor.
  1033. * @param removedStatementCount Number of statements that were removed by the migration.
  1034. * @param prependToConstructor Statements that should be prepended to the constructor.
  1035. * @param superCall Node representing the `super()` call within the constructor.
  1036. */
  1037. function canRemoveConstructor(options, constructor, removedStatementCount, prependToConstructor, superCall) {
  1038. if (options.backwardsCompatibleConstructors || prependToConstructor.length > 0) {
  1039. return false;
  1040. }
  1041. const statementCount = constructor.body
  1042. ? constructor.body.statements.length - removedStatementCount
  1043. : 0;
  1044. return (statementCount === 0 ||
  1045. (statementCount === 1 && superCall !== null && superCall.arguments.length === 0));
  1046. }
  1047. /**
  1048. * Gets the next statement after a node that *won't* be deleted by the migration.
  1049. * @param startNode Node from which to start the search.
  1050. * @param removedStatements Statements that have been removed by the migration.
  1051. * @returns
  1052. */
  1053. function getNextPreservedStatement(startNode, removedStatements) {
  1054. const body = nodes.closestNode(startNode, ts.isBlock);
  1055. const closestStatement = nodes.closestNode(startNode, ts.isStatement);
  1056. if (body === null || closestStatement === null) {
  1057. return null;
  1058. }
  1059. const index = body.statements.indexOf(closestStatement);
  1060. if (index === -1) {
  1061. return null;
  1062. }
  1063. for (let i = index + 1; i < body.statements.length; i++) {
  1064. if (!removedStatements.has(body.statements[i])) {
  1065. return body.statements[i];
  1066. }
  1067. }
  1068. return null;
  1069. }
  1070. /**
  1071. * Applies the internal-specific migrations to a class.
  1072. * @param node Class being migrated.
  1073. * @param constructor The migrated class' constructor.
  1074. * @param localTypeChecker File-specific type checker.
  1075. * @param tracker Object keeping track of the changes.
  1076. * @param printer Printer used to output AST nodes as text.
  1077. * @param removedStatements Statements that have been removed by the migration.
  1078. * @param removedMembers Class members that have been removed by the migration.
  1079. * @param prependToClass Text that will be prepended to a class.
  1080. * @param afterInjectCalls Text that will be inserted after the newly-added `inject` calls.
  1081. * @param memberIndentation Indentation string of the class' members.
  1082. */
  1083. function applyInternalOnlyChanges(node, constructor, localTypeChecker, tracker, printer, removedStatements, removedMembers, prependToClass, afterInjectCalls, memberIndentation) {
  1084. const result = findUninitializedPropertiesToCombine(node, constructor, localTypeChecker);
  1085. if (result === null) {
  1086. return;
  1087. }
  1088. const preserveInitOrder = shouldCombineInInitializationOrder(result.toCombine, constructor);
  1089. // Sort the combined members based on the declaration order of their initializers, only if
  1090. // we've determined that would be safe. Note that `Array.prototype.sort` is in-place so we
  1091. // can just call it conditionally here.
  1092. if (preserveInitOrder) {
  1093. result.toCombine.sort((a, b) => a.initializer.getStart() - b.initializer.getStart());
  1094. }
  1095. result.toCombine.forEach(({ declaration, initializer }) => {
  1096. const initializerStatement = nodes.closestNode(initializer, ts.isStatement);
  1097. const newProperty = ts.factory.createPropertyDeclaration(cloneModifiers(declaration.modifiers), cloneName(declaration.name), declaration.questionToken, declaration.type, initializer);
  1098. // If the initialization order is being preserved, we have to remove the original
  1099. // declaration and re-declare it. Otherwise we can do the replacement in-place.
  1100. if (preserveInitOrder) {
  1101. tracker.removeNode(declaration, true);
  1102. removedMembers.add(declaration);
  1103. afterInjectCalls.push(memberIndentation +
  1104. printer.printNode(ts.EmitHint.Unspecified, newProperty, declaration.getSourceFile()));
  1105. }
  1106. else {
  1107. tracker.replaceNode(declaration, newProperty);
  1108. }
  1109. // This should always be defined, but null check it just in case.
  1110. if (initializerStatement) {
  1111. tracker.removeNode(initializerStatement, true);
  1112. removedStatements.add(initializerStatement);
  1113. }
  1114. });
  1115. result.toHoist.forEach((decl) => {
  1116. prependToClass.push(memberIndentation + printer.printNode(ts.EmitHint.Unspecified, decl, decl.getSourceFile()));
  1117. tracker.removeNode(decl, true);
  1118. removedMembers.add(decl);
  1119. });
  1120. // If we added any hoisted properties, separate them visually with a new line.
  1121. if (prependToClass.length > 0) {
  1122. prependToClass.push('');
  1123. }
  1124. }
  1125. function migrate(options) {
  1126. return async (tree) => {
  1127. const basePath = process.cwd();
  1128. const pathToMigrate = compiler_host.normalizePath(p.join(basePath, options.path));
  1129. let allPaths = [];
  1130. if (pathToMigrate.trim() !== '') {
  1131. allPaths.push(pathToMigrate);
  1132. }
  1133. if (!allPaths.length) {
  1134. throw new schematics.SchematicsException('Could not find any tsconfig file. Cannot run the inject migration.');
  1135. }
  1136. for (const tsconfigPath of allPaths) {
  1137. runInjectMigration(tree, tsconfigPath, basePath, pathToMigrate, options);
  1138. }
  1139. };
  1140. }
  1141. function runInjectMigration(tree, tsconfigPath, basePath, pathToMigrate, schematicOptions) {
  1142. if (schematicOptions.path.startsWith('..')) {
  1143. throw new schematics.SchematicsException('Cannot run inject migration outside of the current project.');
  1144. }
  1145. const program = compiler_host.createMigrationProgram(tree, tsconfigPath, basePath);
  1146. const sourceFiles = program
  1147. .getSourceFiles()
  1148. .filter((sourceFile) => sourceFile.fileName.startsWith(pathToMigrate) &&
  1149. compiler_host.canMigrateFile(basePath, sourceFile, program));
  1150. if (sourceFiles.length === 0) {
  1151. throw new schematics.SchematicsException(`Could not find any files to migrate under the path ${pathToMigrate}. Cannot run the inject migration.`);
  1152. }
  1153. for (const sourceFile of sourceFiles) {
  1154. const changes = migrateFile(sourceFile, schematicOptions);
  1155. if (changes.length > 0) {
  1156. const update = tree.beginUpdate(p.relative(basePath, sourceFile.fileName));
  1157. for (const change of changes) {
  1158. if (change.removeLength != null) {
  1159. update.remove(change.start, change.removeLength);
  1160. }
  1161. update.insertRight(change.start, change.text);
  1162. }
  1163. tree.commitUpdate(update);
  1164. }
  1165. }
  1166. }
  1167. exports.migrate = migrate;