signal-input-migration.cjs 67 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496
  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 migrate_ts_type_references = require('./migrate_ts_type_references-Czrg1gcB.cjs');
  9. var ts = require('typescript');
  10. require('os');
  11. var checker = require('./checker-5pyJrZ9G.cjs');
  12. var index$1 = require('./index-BIvVb6in.cjs');
  13. require('path');
  14. var project_paths = require('./project_paths-CyWVEsbT.cjs');
  15. var index = require('./index-BPhQoCcF.cjs');
  16. var assert = require('assert');
  17. var apply_import_manager = require('./apply_import_manager-QQDfWa1Z.cjs');
  18. require('@angular-devkit/core');
  19. require('node:path/posix');
  20. require('./leading_space-D9nQ8UQC.cjs');
  21. require('fs');
  22. require('module');
  23. require('url');
  24. require('@angular-devkit/schematics');
  25. require('./project_tsconfig_paths-CDVxT6Ov.cjs');
  26. /**
  27. * Class that holds information about a given directive and its input fields.
  28. */
  29. class DirectiveInfo {
  30. clazz;
  31. /**
  32. * Map of inputs detected in the given class.
  33. * Maps string-based input ids to the detailed input metadata.
  34. */
  35. inputFields = new Map();
  36. /** Map of input IDs and their incompatibilities. */
  37. memberIncompatibility = new Map();
  38. /**
  39. * Whether the whole class is incompatible.
  40. *
  41. * Class incompatibility precedes individual member incompatibility.
  42. * All members in the class are considered incompatible.
  43. */
  44. incompatible = null;
  45. constructor(clazz) {
  46. this.clazz = clazz;
  47. }
  48. /**
  49. * Checks whether there are any migrated inputs for the
  50. * given class.
  51. *
  52. * Returns `false` if all inputs are incompatible.
  53. */
  54. hasMigratedFields() {
  55. return Array.from(this.inputFields.values()).some(({ descriptor }) => !this.isInputMemberIncompatible(descriptor));
  56. }
  57. /**
  58. * Whether the given input member is incompatible. If the class is incompatible,
  59. * then the member is as well.
  60. */
  61. isInputMemberIncompatible(input) {
  62. return this.getInputMemberIncompatibility(input) !== null;
  63. }
  64. /** Get incompatibility of the given member, if it's incompatible for migration. */
  65. getInputMemberIncompatibility(input) {
  66. return this.memberIncompatibility.get(input.key) ?? this.incompatible ?? null;
  67. }
  68. }
  69. /**
  70. * A migration host is in practice a container object that
  71. * exposes commonly accessed contextual helpers throughout
  72. * the whole migration.
  73. */
  74. class MigrationHost {
  75. isMigratingCore;
  76. programInfo;
  77. config;
  78. _sourceFiles;
  79. compilerOptions;
  80. constructor(isMigratingCore, programInfo, config, sourceFiles) {
  81. this.isMigratingCore = isMigratingCore;
  82. this.programInfo = programInfo;
  83. this.config = config;
  84. this._sourceFiles = new WeakSet(sourceFiles);
  85. this.compilerOptions = programInfo.userOptions;
  86. }
  87. /** Whether the given file is a source file to be migrated. */
  88. isSourceFileForCurrentMigration(file) {
  89. return this._sourceFiles.has(file);
  90. }
  91. }
  92. function getInputDescriptor(hostOrInfo, node) {
  93. let className;
  94. if (ts.isAccessor(node)) {
  95. className = node.parent.name?.text || '<anonymous>';
  96. }
  97. else {
  98. className = node.parent.name?.text ?? '<anonymous>';
  99. }
  100. const info = hostOrInfo instanceof MigrationHost ? hostOrInfo.programInfo : hostOrInfo;
  101. const file = project_paths.projectFile(node.getSourceFile(), info);
  102. // Inputs may be detected in `.d.ts` files. Ensure that if the file IDs
  103. // match regardless of extension. E.g. `/google3/blaze-out/bin/my_file.ts` should
  104. // have the same ID as `/google3/my_file.ts`.
  105. const id = file.id.replace(/\.d\.ts$/, '.ts');
  106. return {
  107. key: `${id}@@${className}@@${node.name.text}`,
  108. node,
  109. };
  110. }
  111. /**
  112. * Attempts to resolve the known `@Input` metadata for the given
  113. * type checking symbol. Returns `null` if it's not for an input.
  114. */
  115. function attemptRetrieveInputFromSymbol(programInfo, memberSymbol, knownInputs) {
  116. // Even for declared classes from `.d.ts`, the value declaration
  117. // should exist and point to the property declaration.
  118. if (memberSymbol.valueDeclaration !== undefined &&
  119. index.isInputContainerNode(memberSymbol.valueDeclaration)) {
  120. const member = memberSymbol.valueDeclaration;
  121. // If the member itself is an input that is being migrated, we
  122. // do not need to check, as overriding would be fine then— like before.
  123. const memberInputDescr = index.isInputContainerNode(member)
  124. ? getInputDescriptor(programInfo, member)
  125. : null;
  126. return memberInputDescr !== null ? (knownInputs.get(memberInputDescr) ?? null) : null;
  127. }
  128. return null;
  129. }
  130. /**
  131. * Registry keeping track of all known `@Input()`s in the compilation.
  132. *
  133. * A known `@Input()` may be defined in sources, or inside some `d.ts` files
  134. * loaded into the program.
  135. */
  136. class KnownInputs {
  137. programInfo;
  138. config;
  139. /**
  140. * Known inputs from the whole program.
  141. */
  142. knownInputIds = new Map();
  143. /** Known container classes of inputs. */
  144. _allClasses = new Set();
  145. /** Maps classes to their directive info. */
  146. _classToDirectiveInfo = new Map();
  147. constructor(programInfo, config) {
  148. this.programInfo = programInfo;
  149. this.config = config;
  150. }
  151. /** Whether the given input exists. */
  152. has(descr) {
  153. return this.knownInputIds.has(descr.key);
  154. }
  155. /** Whether the given class contains `@Input`s. */
  156. isInputContainingClass(clazz) {
  157. return this._classToDirectiveInfo.has(clazz);
  158. }
  159. /** Gets precise `@Input()` information for the given class. */
  160. getDirectiveInfoForClass(clazz) {
  161. return this._classToDirectiveInfo.get(clazz);
  162. }
  163. /** Gets known input information for the given `@Input()`. */
  164. get(descr) {
  165. return this.knownInputIds.get(descr.key);
  166. }
  167. /** Gets all classes containing `@Input`s in the compilation. */
  168. getAllInputContainingClasses() {
  169. return Array.from(this._allClasses.values());
  170. }
  171. /** Registers an `@Input()` in the registry. */
  172. register(data) {
  173. if (!this._classToDirectiveInfo.has(data.node.parent)) {
  174. this._classToDirectiveInfo.set(data.node.parent, new DirectiveInfo(data.node.parent));
  175. }
  176. const directiveInfo = this._classToDirectiveInfo.get(data.node.parent);
  177. const inputInfo = {
  178. file: project_paths.projectFile(data.node.getSourceFile(), this.programInfo),
  179. metadata: data.metadata,
  180. descriptor: data.descriptor,
  181. container: directiveInfo,
  182. extendsFrom: null,
  183. isIncompatible: () => directiveInfo.isInputMemberIncompatible(data.descriptor),
  184. };
  185. directiveInfo.inputFields.set(data.descriptor.key, {
  186. descriptor: data.descriptor,
  187. metadata: data.metadata,
  188. });
  189. this.knownInputIds.set(data.descriptor.key, inputInfo);
  190. this._allClasses.add(data.node.parent);
  191. }
  192. /** Whether the given input is incompatible for migration. */
  193. isFieldIncompatible(descriptor) {
  194. return !!this.get(descriptor)?.isIncompatible();
  195. }
  196. /** Marks the given input as incompatible for migration. */
  197. markFieldIncompatible(input, incompatibility) {
  198. if (!this.knownInputIds.has(input.key)) {
  199. throw new Error(`Input cannot be marked as incompatible because it's not registered.`);
  200. }
  201. const inputInfo = this.knownInputIds.get(input.key);
  202. const existingIncompatibility = inputInfo.container.getInputMemberIncompatibility(input);
  203. // Ensure an existing more significant incompatibility is not overridden.
  204. if (existingIncompatibility !== null && migrate_ts_type_references.isFieldIncompatibility(existingIncompatibility)) {
  205. incompatibility = migrate_ts_type_references.pickFieldIncompatibility(existingIncompatibility, incompatibility);
  206. }
  207. this.knownInputIds
  208. .get(input.key)
  209. .container.memberIncompatibility.set(input.key, incompatibility);
  210. }
  211. /** Marks the given class as incompatible for migration. */
  212. markClassIncompatible(clazz, incompatibility) {
  213. if (!this._classToDirectiveInfo.has(clazz)) {
  214. throw new Error(`Class cannot be marked as incompatible because it's not known.`);
  215. }
  216. this._classToDirectiveInfo.get(clazz).incompatible = incompatibility;
  217. }
  218. attemptRetrieveDescriptorFromSymbol(symbol) {
  219. return attemptRetrieveInputFromSymbol(this.programInfo, symbol, this)?.descriptor ?? null;
  220. }
  221. shouldTrackClassReference(clazz) {
  222. return this.isInputContainingClass(clazz);
  223. }
  224. captureKnownFieldInheritanceRelationship(derived, parent) {
  225. if (!this.has(derived)) {
  226. throw new Error(`Expected input to exist in registry: ${derived.key}`);
  227. }
  228. this.get(derived).extendsFrom = parent;
  229. }
  230. captureUnknownDerivedField(field) {
  231. this.markFieldIncompatible(field, {
  232. context: null,
  233. reason: migrate_ts_type_references.FieldIncompatibilityReason.OverriddenByDerivedClass,
  234. });
  235. }
  236. captureUnknownParentField(field) {
  237. this.markFieldIncompatible(field, {
  238. context: null,
  239. reason: migrate_ts_type_references.FieldIncompatibilityReason.TypeConflictWithBaseClass,
  240. });
  241. }
  242. }
  243. /**
  244. * Prepares migration analysis for the given program.
  245. *
  246. * Unlike {@link createAndPrepareAnalysisProgram} this does not create the program,
  247. * and can be used for integrations with e.g. the language service.
  248. */
  249. function prepareAnalysisInfo(userProgram, compiler, programAbsoluteRootPaths) {
  250. let refEmitter = null;
  251. let metaReader = null;
  252. let templateTypeChecker = null;
  253. let resourceLoader = null;
  254. if (compiler !== null) {
  255. // Analyze sync and retrieve necessary dependencies.
  256. // Note: `getTemplateTypeChecker` requires the `enableTemplateTypeChecker` flag, but
  257. // this has negative effects as it causes optional TCB operations to execute, which may
  258. // error with unsuccessful reference emits that previously were ignored outside of the migration.
  259. // The migration is resilient to TCB information missing, so this is fine, and all the information
  260. // we need is part of required TCB operations anyway.
  261. const state = compiler['ensureAnalyzed']();
  262. resourceLoader = compiler['resourceManager'];
  263. refEmitter = state.refEmitter;
  264. metaReader = state.metaReader;
  265. templateTypeChecker = state.templateTypeChecker;
  266. // Generate all type check blocks.
  267. state.templateTypeChecker.generateAllTypeCheckBlocks();
  268. }
  269. const typeChecker = userProgram.getTypeChecker();
  270. const reflector = new checker.TypeScriptReflectionHost(typeChecker);
  271. const evaluator = new index$1.PartialEvaluator(reflector, typeChecker, null);
  272. const dtsMetadataReader = new index$1.DtsMetadataReader(typeChecker, reflector);
  273. return {
  274. metaRegistry: metaReader,
  275. dtsMetadataReader,
  276. evaluator,
  277. reflector,
  278. typeChecker,
  279. refEmitter,
  280. templateTypeChecker,
  281. resourceLoader,
  282. };
  283. }
  284. /**
  285. * State of the migration that is passed between
  286. * the individual phases.
  287. *
  288. * The state/phase captures information like:
  289. * - list of inputs that are defined in `.ts` and need migration.
  290. * - list of references.
  291. * - keeps track of computed replacements.
  292. * - imports that may need to be updated.
  293. */
  294. class MigrationResult {
  295. printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
  296. // May be `null` if the input cannot be converted. This is also
  297. // signified by an incompatibility- but the input is tracked here as it
  298. // still is a "source input".
  299. sourceInputs = new Map();
  300. references = [];
  301. // Execution data
  302. replacements = [];
  303. inputDecoratorSpecifiers = new Map();
  304. }
  305. /** Attempts to extract metadata of a potential TypeScript `@Input()` declaration. */
  306. function extractDecoratorInput(node, host, reflector, metadataReader, evaluator) {
  307. return (extractSourceCodeInput(node, host, reflector, evaluator) ??
  308. extractDtsInput(node, metadataReader));
  309. }
  310. /**
  311. * Attempts to extract `@Input()` information for the given node, assuming it's
  312. * part of a `.d.ts` file.
  313. */
  314. function extractDtsInput(node, metadataReader) {
  315. if (!index.isInputContainerNode(node) ||
  316. !ts.isIdentifier(node.name) ||
  317. !node.getSourceFile().isDeclarationFile) {
  318. return null;
  319. }
  320. // If the potential node is not part of a valid input class, skip.
  321. if (!ts.isClassDeclaration(node.parent) ||
  322. node.parent.name === undefined ||
  323. !ts.isIdentifier(node.parent.name)) {
  324. return null;
  325. }
  326. let directiveMetadata = null;
  327. // Getting directive metadata can throw errors when e.g. types referenced
  328. // in the `.d.ts` aren't resolvable. This seems to be unexpected and shouldn't
  329. // result in the entire migration to be failing.
  330. try {
  331. directiveMetadata = metadataReader.getDirectiveMetadata(new checker.Reference(node.parent));
  332. }
  333. catch (e) {
  334. console.error('Unexpected error. Gracefully ignoring.');
  335. console.error('Could not parse directive metadata:', e);
  336. return null;
  337. }
  338. const inputMapping = directiveMetadata?.inputs.getByClassPropertyName(node.name.text);
  339. // Signal inputs are never tracked and migrated.
  340. if (inputMapping?.isSignal) {
  341. return null;
  342. }
  343. return inputMapping == null
  344. ? null
  345. : {
  346. ...inputMapping,
  347. inputDecorator: null,
  348. inSourceFile: false,
  349. // Inputs from `.d.ts` cannot have any field decorators applied.
  350. fieldDecorators: [],
  351. };
  352. }
  353. /**
  354. * Attempts to extract `@Input()` information for the given node, assuming it's
  355. * directly defined inside a source file (`.ts`).
  356. */
  357. function extractSourceCodeInput(node, host, reflector, evaluator) {
  358. if (!index.isInputContainerNode(node) ||
  359. !ts.isIdentifier(node.name) ||
  360. node.getSourceFile().isDeclarationFile) {
  361. return null;
  362. }
  363. const decorators = reflector.getDecoratorsOfDeclaration(node);
  364. if (decorators === null) {
  365. return null;
  366. }
  367. const ngDecorators = checker.getAngularDecorators(decorators, ['Input'], host.isMigratingCore);
  368. if (ngDecorators.length === 0) {
  369. return null;
  370. }
  371. const inputDecorator = ngDecorators[0];
  372. let publicName = node.name.text;
  373. let isRequired = false;
  374. let transformResult = null;
  375. // Check options object from `@Input()`.
  376. if (inputDecorator.args?.length === 1) {
  377. const evaluatedInputOpts = evaluator.evaluate(inputDecorator.args[0]);
  378. if (typeof evaluatedInputOpts === 'string') {
  379. publicName = evaluatedInputOpts;
  380. }
  381. else if (evaluatedInputOpts instanceof Map) {
  382. if (evaluatedInputOpts.has('alias') && typeof evaluatedInputOpts.get('alias') === 'string') {
  383. publicName = evaluatedInputOpts.get('alias');
  384. }
  385. if (evaluatedInputOpts.has('required') &&
  386. typeof evaluatedInputOpts.get('required') === 'boolean') {
  387. isRequired = !!evaluatedInputOpts.get('required');
  388. }
  389. if (evaluatedInputOpts.has('transform') && evaluatedInputOpts.get('transform') != null) {
  390. transformResult = parseTransformOfInput(evaluatedInputOpts, node, reflector);
  391. }
  392. }
  393. }
  394. return {
  395. bindingPropertyName: publicName,
  396. classPropertyName: node.name.text,
  397. required: isRequired,
  398. isSignal: false,
  399. inSourceFile: true,
  400. transform: transformResult,
  401. inputDecorator,
  402. fieldDecorators: decorators,
  403. };
  404. }
  405. /**
  406. * Gracefully attempts to parse the `transform` option of an `@Input()`
  407. * and extracts its metadata.
  408. */
  409. function parseTransformOfInput(evaluatedInputOpts, node, reflector) {
  410. const transformValue = evaluatedInputOpts.get('transform');
  411. if (!(transformValue instanceof checker.DynamicValue) && !(transformValue instanceof checker.Reference)) {
  412. return null;
  413. }
  414. // For parsing the transform, we don't need a real reference emitter, as
  415. // the emitter is only used for verifying that the transform type could be
  416. // copied into e.g. an `ngInputAccept` class member.
  417. const noopRefEmitter = new checker.ReferenceEmitter([
  418. {
  419. emit: () => ({
  420. kind: checker.ReferenceEmitKind.Success,
  421. expression: migrate_ts_type_references.NULL_EXPR,
  422. importedFile: null,
  423. }),
  424. },
  425. ]);
  426. try {
  427. return checker.parseDecoratorInputTransformFunction(node.parent, node.name.text, transformValue, reflector, noopRefEmitter, checker.CompilationMode.FULL);
  428. }
  429. catch (e) {
  430. if (!(e instanceof checker.FatalDiagnosticError)) {
  431. throw e;
  432. }
  433. // TODO: implement error handling.
  434. // See failing case: e.g. inherit_definition_feature_spec.ts
  435. console.error(`${e.node.getSourceFile().fileName}: ${e.toString()}`);
  436. return null;
  437. }
  438. }
  439. /**
  440. * Prepares a potential migration of the given node by performing
  441. * initial analysis and checking whether it an be migrated.
  442. *
  443. * For example, required inputs that don't have an explicit type may not
  444. * be migrated as we don't have a good type for `input.required<T>`.
  445. * (Note: `typeof Bla` may be usable— but isn't necessarily a good practice
  446. * for complex expressions)
  447. */
  448. function prepareAndCheckForConversion(node, metadata, checker, options) {
  449. // Accessor inputs cannot be migrated right now.
  450. if (ts.isAccessor(node)) {
  451. return {
  452. context: node,
  453. reason: migrate_ts_type_references.FieldIncompatibilityReason.Accessor,
  454. };
  455. }
  456. assert(metadata.inputDecorator !== null, 'Expected an input decorator for inputs that are being migrated.');
  457. let initialValue = node.initializer;
  458. let isUndefinedInitialValue = node.initializer === undefined ||
  459. (ts.isIdentifier(node.initializer) && node.initializer.text === 'undefined');
  460. const strictNullChecksEnabled = options.strict === true || options.strictNullChecks === true;
  461. const strictPropertyInitialization = options.strict === true || options.strictPropertyInitialization === true;
  462. // Shorthand should never be used, as would expand the type of `T` to be `T|undefined`.
  463. // This wouldn't matter with strict null checks disabled, but it can break if this is
  464. // a library that is later consumed with strict null checks enabled.
  465. const avoidTypeExpansion = !strictNullChecksEnabled;
  466. // If an input can be required, due to the non-null assertion on the property,
  467. // make it required if there is no initializer.
  468. if (node.exclamationToken !== undefined && initialValue === undefined) {
  469. metadata.required = true;
  470. }
  471. let typeToAdd = node.type;
  472. let preferShorthandIfPossible = null;
  473. // If there is no initial value, or it's `undefined`, we can prefer the `input()`
  474. // shorthand which automatically uses `undefined` as initial value, and includes it
  475. // in the input type.
  476. if (!metadata.required &&
  477. node.type !== undefined &&
  478. isUndefinedInitialValue &&
  479. !avoidTypeExpansion) {
  480. preferShorthandIfPossible = { originalType: node.type };
  481. }
  482. // If the input is using `@Input() bla?: string;` with the "optional question mark",
  483. // then we try to explicitly add `undefined` as type, if it's not part of the type already.
  484. // This is ensuring correctness, as `bla?` automatically includes `undefined` currently.
  485. if (node.questionToken !== undefined) {
  486. // If there is no type, but we have an initial value, try inferring
  487. // it from the initializer.
  488. if (typeToAdd === undefined && initialValue !== undefined) {
  489. const inferredType = inferImportableTypeForInput(checker, node, initialValue);
  490. if (inferredType !== null) {
  491. typeToAdd = inferredType;
  492. }
  493. }
  494. if (typeToAdd === undefined) {
  495. return {
  496. context: node,
  497. reason: migrate_ts_type_references.FieldIncompatibilityReason.SignalInput__QuestionMarkButNoGoodExplicitTypeExtractable,
  498. };
  499. }
  500. if (!checker.isTypeAssignableTo(checker.getUndefinedType(), checker.getTypeFromTypeNode(typeToAdd))) {
  501. typeToAdd = ts.factory.createUnionTypeNode([
  502. typeToAdd,
  503. ts.factory.createKeywordTypeNode(ts.SyntaxKind.UndefinedKeyword),
  504. ]);
  505. }
  506. }
  507. let leadingTodoText = null;
  508. // If the input does not have an initial value, and strict property initialization
  509. // is disabled, while strict null checks are enabled; then we know that `undefined`
  510. // cannot be used as initial value, nor do we want to expand the input's type magically.
  511. // Instead, we detect this case and migrate to `undefined!` which leaves the behavior unchanged.
  512. if (strictNullChecksEnabled &&
  513. !strictPropertyInitialization &&
  514. node.initializer === undefined &&
  515. node.type !== undefined &&
  516. node.questionToken === undefined &&
  517. node.exclamationToken === undefined &&
  518. metadata.required === false &&
  519. !checker.isTypeAssignableTo(checker.getUndefinedType(), checker.getTypeFromTypeNode(node.type))) {
  520. leadingTodoText =
  521. 'Input is initialized to `undefined` but type does not allow this value. ' +
  522. 'This worked with `@Input` because your project uses `--strictPropertyInitialization=false`.';
  523. isUndefinedInitialValue = false;
  524. initialValue = ts.factory.createNonNullExpression(ts.factory.createIdentifier('undefined'));
  525. }
  526. // Attempt to extract type from input initial value. No explicit type, but input is required.
  527. // Hence we need an explicit type, or fall back to `typeof`.
  528. if (typeToAdd === undefined && initialValue !== undefined && metadata.required) {
  529. const inferredType = inferImportableTypeForInput(checker, node, initialValue);
  530. if (inferredType !== null) {
  531. typeToAdd = inferredType;
  532. }
  533. else {
  534. // Note that we could use `typeToTypeNode` here but it's likely breaking because
  535. // the generated type might depend on imports that we cannot add here (nor want).
  536. return {
  537. context: node,
  538. reason: migrate_ts_type_references.FieldIncompatibilityReason.SignalInput__RequiredButNoGoodExplicitTypeExtractable,
  539. };
  540. }
  541. }
  542. return {
  543. requiredButIncludedUndefinedPreviously: metadata.required && node.questionToken !== undefined,
  544. resolvedMetadata: metadata,
  545. resolvedType: typeToAdd,
  546. preferShorthandIfPossible,
  547. originalInputDecorator: metadata.inputDecorator,
  548. initialValue: isUndefinedInitialValue ? undefined : initialValue,
  549. leadingTodoText,
  550. };
  551. }
  552. function inferImportableTypeForInput(checker, node, initialValue) {
  553. const propertyType = checker.getTypeAtLocation(node);
  554. // If the resolved type is a primitive, or union of primitive types,
  555. // return a type node fully derived from the resolved type.
  556. if (isPrimitiveImportableTypeNode(propertyType) ||
  557. (propertyType.isUnion() && propertyType.types.every(isPrimitiveImportableTypeNode))) {
  558. return checker.typeToTypeNode(propertyType, node, ts.NodeBuilderFlags.NoTypeReduction) ?? null;
  559. }
  560. // Alternatively, try to infer a simple importable type from\
  561. // the initializer.
  562. if (ts.isIdentifier(initialValue)) {
  563. // @Input({required: true}) bla = SOME_DEFAULT;
  564. return ts.factory.createTypeQueryNode(initialValue);
  565. }
  566. else if (ts.isPropertyAccessExpression(initialValue) &&
  567. ts.isIdentifier(initialValue.name) &&
  568. ts.isIdentifier(initialValue.expression)) {
  569. // @Input({required: true}) bla = prop.SOME_DEFAULT;
  570. return ts.factory.createTypeQueryNode(ts.factory.createQualifiedName(initialValue.name, initialValue.expression));
  571. }
  572. return null;
  573. }
  574. function isPrimitiveImportableTypeNode(type) {
  575. return !!(type.flags & ts.TypeFlags.BooleanLike ||
  576. type.flags & ts.TypeFlags.StringLike ||
  577. type.flags & ts.TypeFlags.NumberLike ||
  578. type.flags & ts.TypeFlags.Undefined ||
  579. type.flags & ts.TypeFlags.Null);
  580. }
  581. /**
  582. * Phase where we iterate through all source files of the program (including `.d.ts`)
  583. * and keep track of all `@Input`'s we discover.
  584. */
  585. function pass1__IdentifySourceFileAndDeclarationInputs(sf, host, checker, reflector, dtsMetadataReader, evaluator, knownDecoratorInputs, result) {
  586. const visitor = (node) => {
  587. const decoratorInput = extractDecoratorInput(node, host, reflector, dtsMetadataReader, evaluator);
  588. if (decoratorInput !== null) {
  589. assert(index.isInputContainerNode(node), 'Expected input to be declared on accessor or property.');
  590. const inputDescr = getInputDescriptor(host, node);
  591. // track all inputs, even from declarations for reference resolution.
  592. knownDecoratorInputs.register({ descriptor: inputDescr, metadata: decoratorInput, node });
  593. // track source file inputs in the result of this target.
  594. // these are then later migrated in the migration phase.
  595. if (decoratorInput.inSourceFile && host.isSourceFileForCurrentMigration(sf)) {
  596. const conversionPreparation = prepareAndCheckForConversion(node, decoratorInput, checker, host.compilerOptions);
  597. if (migrate_ts_type_references.isFieldIncompatibility(conversionPreparation)) {
  598. knownDecoratorInputs.markFieldIncompatible(inputDescr, conversionPreparation);
  599. result.sourceInputs.set(inputDescr, null);
  600. }
  601. else {
  602. result.sourceInputs.set(inputDescr, conversionPreparation);
  603. }
  604. }
  605. }
  606. // track all imports to `Input` or `input`.
  607. let importName = null;
  608. if (ts.isImportSpecifier(node) &&
  609. ((importName = (node.propertyName ?? node.name).text) === 'Input' ||
  610. importName === 'input') &&
  611. ts.isStringLiteral(node.parent.parent.parent.moduleSpecifier) &&
  612. (host.isMigratingCore || node.parent.parent.parent.moduleSpecifier.text === '@angular/core')) {
  613. if (!result.inputDecoratorSpecifiers.has(sf)) {
  614. result.inputDecoratorSpecifiers.set(sf, []);
  615. }
  616. result.inputDecoratorSpecifiers.get(sf).push({
  617. kind: importName === 'input' ? 'signal-input-import' : 'decorator-input-import',
  618. node,
  619. });
  620. }
  621. ts.forEachChild(node, visitor);
  622. };
  623. ts.forEachChild(sf, visitor);
  624. }
  625. /**
  626. * Phase where problematic patterns are detected and advise
  627. * the migration to skip certain inputs.
  628. *
  629. * For example, detects classes that are instantiated manually. Those
  630. * cannot be migrated as `input()` requires an injection context.
  631. *
  632. * In addition, spying onto an input may be problematic- so we skip migrating
  633. * such.
  634. */
  635. function pass3__checkIncompatiblePatterns(host, inheritanceGraph, checker$1, groupedTsAstVisitor, knownInputs) {
  636. migrate_ts_type_references.checkIncompatiblePatterns(inheritanceGraph, checker$1, groupedTsAstVisitor, knownInputs, () => knownInputs.getAllInputContainingClasses());
  637. for (const input of knownInputs.knownInputIds.values()) {
  638. const hostBindingDecorators = checker.getAngularDecorators(input.metadata.fieldDecorators, ['HostBinding'], host.isMigratingCore);
  639. if (hostBindingDecorators.length > 0) {
  640. knownInputs.markFieldIncompatible(input.descriptor, {
  641. context: hostBindingDecorators[0].node,
  642. reason: migrate_ts_type_references.FieldIncompatibilityReason.SignalIncompatibleWithHostBinding,
  643. });
  644. }
  645. }
  646. }
  647. /**
  648. * Phase where problematic patterns are detected and advise
  649. * the migration to skip certain inputs.
  650. *
  651. * For example, detects classes that are instantiated manually. Those
  652. * cannot be migrated as `input()` requires an injection context.
  653. *
  654. * In addition, spying onto an input may be problematic- so we skip migrating
  655. * such.
  656. */
  657. function pass2_IdentifySourceFileReferences(programInfo, checker, reflector, resourceLoader, evaluator, templateTypeChecker, groupedTsAstVisitor, knownInputs, result, fieldNamesToConsiderForReferenceLookup) {
  658. groupedTsAstVisitor.register(index.createFindAllSourceFileReferencesVisitor(programInfo, checker, reflector, resourceLoader, evaluator, templateTypeChecker, knownInputs, fieldNamesToConsiderForReferenceLookup, result).visitor);
  659. }
  660. /**
  661. * Executes the analysis phase of the migration.
  662. *
  663. * This includes:
  664. * - finding all inputs
  665. * - finding all references
  666. * - determining incompatible inputs
  667. * - checking inheritance
  668. */
  669. function executeAnalysisPhase(host, knownInputs, result, { sourceFiles, fullProgramSourceFiles, reflector, dtsMetadataReader, typeChecker, templateTypeChecker, resourceLoader, evaluator, }) {
  670. // Pass 1
  671. fullProgramSourceFiles.forEach((sf) =>
  672. // Shim shim files. Those are unnecessary and might cause unexpected slowness.
  673. // e.g. `ngtypecheck` files.
  674. !checker.isShim(sf) &&
  675. pass1__IdentifySourceFileAndDeclarationInputs(sf, host, typeChecker, reflector, dtsMetadataReader, evaluator, knownInputs, result));
  676. const fieldNamesToConsiderForReferenceLookup = new Set();
  677. for (const input of knownInputs.knownInputIds.values()) {
  678. if (host.config.shouldMigrateInput?.(input) === false) {
  679. continue;
  680. }
  681. fieldNamesToConsiderForReferenceLookup.add(input.descriptor.node.name.text);
  682. }
  683. // A graph starting with source files is sufficient. We will resolve into
  684. // declaration files if a source file depends on such.
  685. const inheritanceGraph = new migrate_ts_type_references.InheritanceGraph(typeChecker).expensivePopulate(sourceFiles);
  686. const pass2And3SourceFileVisitor = new migrate_ts_type_references.GroupedTsAstVisitor(sourceFiles);
  687. // Register pass 2. Find all source file references.
  688. pass2_IdentifySourceFileReferences(host.programInfo, typeChecker, reflector, resourceLoader, evaluator, templateTypeChecker, pass2And3SourceFileVisitor, knownInputs, result, fieldNamesToConsiderForReferenceLookup);
  689. // Register pass 3. Check incompatible patterns pass.
  690. pass3__checkIncompatiblePatterns(host, inheritanceGraph, typeChecker, pass2And3SourceFileVisitor, knownInputs);
  691. // Perform Pass 2 and Pass 3, efficiently in one pass.
  692. pass2And3SourceFileVisitor.execute();
  693. // Determine incompatible inputs based on resolved references.
  694. for (const reference of result.references) {
  695. if (index.isTsReference(reference) && reference.from.isWrite) {
  696. knownInputs.markFieldIncompatible(reference.target, {
  697. reason: migrate_ts_type_references.FieldIncompatibilityReason.WriteAssignment,
  698. context: reference.from.node,
  699. });
  700. }
  701. if (index.isTemplateReference(reference) || index.isHostBindingReference(reference)) {
  702. if (reference.from.isWrite) {
  703. knownInputs.markFieldIncompatible(reference.target, {
  704. reason: migrate_ts_type_references.FieldIncompatibilityReason.WriteAssignment,
  705. // No TS node context available for template or host bindings.
  706. context: null,
  707. });
  708. }
  709. }
  710. // TODO: Remove this when we support signal narrowing in templates.
  711. // https://github.com/angular/angular/pull/55456.
  712. if (index.isTemplateReference(reference)) {
  713. if (reference.from.isLikelyPartOfNarrowing) {
  714. knownInputs.markFieldIncompatible(reference.target, {
  715. reason: migrate_ts_type_references.FieldIncompatibilityReason.PotentiallyNarrowedInTemplateButNoSupportYet,
  716. context: null,
  717. });
  718. }
  719. }
  720. }
  721. return { inheritanceGraph };
  722. }
  723. /**
  724. * Phase that propagates incompatibilities to derived classes or
  725. * base classes. For example, consider:
  726. *
  727. * ```ts
  728. * class Base {
  729. * bla = true;
  730. * }
  731. *
  732. * class Derived extends Base {
  733. * @Input() bla = false;
  734. * }
  735. * ```
  736. *
  737. * Whenever we migrate `Derived`, the inheritance would fail
  738. * and result in a build breakage because `Base#bla` is not an Angular input.
  739. *
  740. * The logic here detects such cases and marks `bla` as incompatible. If `Derived`
  741. * would then have other derived classes as well, it would propagate the status.
  742. */
  743. function pass4__checkInheritanceOfInputs(inheritanceGraph, metaRegistry, knownInputs) {
  744. migrate_ts_type_references.checkInheritanceOfKnownFields(inheritanceGraph, metaRegistry, knownInputs, {
  745. isClassWithKnownFields: (clazz) => knownInputs.isInputContainingClass(clazz),
  746. getFieldsForClass: (clazz) => {
  747. const directiveInfo = knownInputs.getDirectiveInfoForClass(clazz);
  748. assert(directiveInfo !== undefined, 'Expected directive info to exist for input.');
  749. return Array.from(directiveInfo.inputFields.values()).map((i) => i.descriptor);
  750. },
  751. });
  752. }
  753. function getCompilationUnitMetadata(knownInputs) {
  754. const struct = {
  755. knownInputs: Array.from(knownInputs.knownInputIds.entries()).reduce((res, [inputClassFieldIdStr, info]) => {
  756. const classIncompatibility = info.container.incompatible !== null ? info.container.incompatible : null;
  757. const memberIncompatibility = info.container.memberIncompatibility.has(inputClassFieldIdStr)
  758. ? info.container.memberIncompatibility.get(inputClassFieldIdStr).reason
  759. : null;
  760. // Note: Trim off the `context` as it cannot be serialized with e.g. TS nodes.
  761. return {
  762. ...res,
  763. [inputClassFieldIdStr]: {
  764. owningClassIncompatibility: classIncompatibility,
  765. memberIncompatibility,
  766. seenAsSourceInput: info.metadata.inSourceFile,
  767. extendsFrom: info.extendsFrom?.key ?? null,
  768. },
  769. };
  770. }, {}),
  771. };
  772. return struct;
  773. }
  774. /**
  775. * Sorts the inheritance graph topologically, so that
  776. * nodes without incoming edges are returned first.
  777. *
  778. * I.e. The returned list is sorted, so that dependencies
  779. * of a given class are guaranteed to be included at
  780. * an earlier position than the inspected class.
  781. *
  782. * This sort is helpful for detecting inheritance problems
  783. * for the migration in simpler ways, without having to
  784. * check in both directions (base classes, and derived classes).
  785. */
  786. function topologicalSort(graph) {
  787. // All nodes without incoming edges.
  788. const S = graph.filter((n) => n.incoming.size === 0);
  789. const result = [];
  790. const invalidatedEdges = new WeakMap();
  791. const invalidateEdge = (from, to) => {
  792. if (!invalidatedEdges.has(from)) {
  793. invalidatedEdges.set(from, new Set());
  794. }
  795. invalidatedEdges.get(from).add(to);
  796. };
  797. const filterEdges = (from, edges) => {
  798. return Array.from(edges).filter((e) => !invalidatedEdges.has(from) || !invalidatedEdges.get(from).has(e));
  799. };
  800. while (S.length) {
  801. const node = S.pop();
  802. result.push(node);
  803. for (const next of filterEdges(node, node.outgoing)) {
  804. // Remove edge from "node -> next".
  805. invalidateEdge(node, next);
  806. // Remove edge from "next -> node".
  807. invalidateEdge(next, node);
  808. // if there are no incoming edges for `next`. add it to `S`.
  809. if (filterEdges(next, next.incoming).length === 0) {
  810. S.push(next);
  811. }
  812. }
  813. }
  814. return result;
  815. }
  816. /** Merges a list of compilation units into a combined unit. */
  817. function combineCompilationUnitData(unitA, unitB) {
  818. const result = {
  819. knownInputs: {},
  820. };
  821. for (const file of [unitA, unitB]) {
  822. for (const [key, info] of Object.entries(file.knownInputs)) {
  823. const existing = result.knownInputs[key];
  824. if (existing === undefined) {
  825. result.knownInputs[key] = info;
  826. continue;
  827. }
  828. // Merge metadata.
  829. if (existing.extendsFrom === null && info.extendsFrom !== null) {
  830. existing.extendsFrom = info.extendsFrom;
  831. }
  832. if (!existing.seenAsSourceInput && info.seenAsSourceInput) {
  833. existing.seenAsSourceInput = true;
  834. }
  835. // Merge member incompatibility.
  836. if (info.memberIncompatibility !== null) {
  837. if (existing.memberIncompatibility === null) {
  838. existing.memberIncompatibility = info.memberIncompatibility;
  839. }
  840. else {
  841. // Input might not be incompatible in one target, but others might invalidate it.
  842. // merge the incompatibility state.
  843. existing.memberIncompatibility = migrate_ts_type_references.pickFieldIncompatibility({ reason: info.memberIncompatibility, context: null }, { reason: existing.memberIncompatibility, context: null }).reason;
  844. }
  845. }
  846. // Merge incompatibility of the class owning the input.
  847. // Note: This metadata is stored per field for simplicity currently,
  848. // but in practice it could be a separate field in the compilation data.
  849. if (info.owningClassIncompatibility !== null &&
  850. existing.owningClassIncompatibility === null) {
  851. existing.owningClassIncompatibility = info.owningClassIncompatibility;
  852. }
  853. }
  854. }
  855. return result;
  856. }
  857. function convertToGlobalMeta(combinedData) {
  858. const globalMeta = {
  859. knownInputs: {},
  860. };
  861. const idToGraphNode = new Map();
  862. const inheritanceGraph = [];
  863. const isNodeIncompatible = (node) => node.info.memberIncompatibility !== null || node.info.owningClassIncompatibility !== null;
  864. for (const [key, info] of Object.entries(combinedData.knownInputs)) {
  865. const existing = globalMeta.knownInputs[key];
  866. if (existing !== undefined) {
  867. continue;
  868. }
  869. const node = {
  870. incoming: new Set(),
  871. outgoing: new Set(),
  872. data: { info, key },
  873. };
  874. inheritanceGraph.push(node);
  875. idToGraphNode.set(key, node);
  876. globalMeta.knownInputs[key] = info;
  877. }
  878. for (const [key, info] of Object.entries(globalMeta.knownInputs)) {
  879. if (info.extendsFrom !== null) {
  880. const from = idToGraphNode.get(key);
  881. const target = idToGraphNode.get(info.extendsFrom);
  882. from.outgoing.add(target);
  883. target.incoming.add(from);
  884. }
  885. }
  886. // Sort topologically and iterate super classes first, so that we can trivially
  887. // propagate incompatibility statuses (and other checks) without having to check
  888. // in both directions (derived classes, or base classes). This simplifies the
  889. // propagation.
  890. for (const node of topologicalSort(inheritanceGraph).reverse()) {
  891. const existingMemberIncompatibility = node.data.info.memberIncompatibility !== null
  892. ? { reason: node.data.info.memberIncompatibility, context: null }
  893. : null;
  894. for (const parent of node.outgoing) {
  895. // If parent is incompatible and not migrated, then this input
  896. // cannot be migrated either. Try propagating parent incompatibility then.
  897. if (isNodeIncompatible(parent.data)) {
  898. node.data.info.memberIncompatibility = migrate_ts_type_references.pickFieldIncompatibility({ reason: migrate_ts_type_references.FieldIncompatibilityReason.ParentIsIncompatible, context: null }, existingMemberIncompatibility).reason;
  899. break;
  900. }
  901. }
  902. }
  903. for (const info of Object.values(combinedData.knownInputs)) {
  904. // We never saw a source file for this input, globally. Try marking it as incompatible,
  905. // so that all references and inheritance checks can propagate accordingly.
  906. if (!info.seenAsSourceInput) {
  907. const existingMemberIncompatibility = info.memberIncompatibility !== null
  908. ? { reason: info.memberIncompatibility, context: null }
  909. : null;
  910. info.memberIncompatibility = migrate_ts_type_references.pickFieldIncompatibility({ reason: migrate_ts_type_references.FieldIncompatibilityReason.OutsideOfMigrationScope, context: null }, existingMemberIncompatibility).reason;
  911. }
  912. }
  913. return globalMeta;
  914. }
  915. function populateKnownInputsFromGlobalData(knownInputs, globalData) {
  916. // Populate from batch metadata.
  917. for (const [_key, info] of Object.entries(globalData.knownInputs)) {
  918. const key = _key;
  919. // irrelevant for this compilation unit.
  920. if (!knownInputs.has({ key })) {
  921. continue;
  922. }
  923. const inputMetadata = knownInputs.get({ key });
  924. if (info.memberIncompatibility !== null) {
  925. knownInputs.markFieldIncompatible(inputMetadata.descriptor, {
  926. context: null, // No context serializable.
  927. reason: info.memberIncompatibility,
  928. });
  929. }
  930. if (info.owningClassIncompatibility !== null) {
  931. knownInputs.markClassIncompatible(inputMetadata.container.clazz, info.owningClassIncompatibility);
  932. }
  933. }
  934. }
  935. // TODO: Consider initializations inside the constructor. Those are not migrated right now
  936. // though, as they are writes.
  937. /**
  938. * Converts an `@Input()` property declaration to a signal input.
  939. *
  940. * @returns Replacements for converting the input.
  941. */
  942. function convertToSignalInput(node, { resolvedMetadata: metadata, resolvedType, preferShorthandIfPossible, originalInputDecorator, initialValue, leadingTodoText, }, info, checker, importManager, result) {
  943. let optionsLiteral = null;
  944. // We need an options array for the input because:
  945. // - the input is either aliased,
  946. // - or we have a transform.
  947. if (metadata.bindingPropertyName !== metadata.classPropertyName || metadata.transform !== null) {
  948. const properties = [];
  949. if (metadata.bindingPropertyName !== metadata.classPropertyName) {
  950. properties.push(ts.factory.createPropertyAssignment('alias', ts.factory.createStringLiteral(metadata.bindingPropertyName)));
  951. }
  952. if (metadata.transform !== null) {
  953. const transformRes = extractTransformOfInput(metadata.transform, resolvedType, checker);
  954. properties.push(transformRes.node);
  955. // Propagate TODO if one was requested from the transform extraction/validation.
  956. if (transformRes.leadingTodoText !== null) {
  957. leadingTodoText =
  958. (leadingTodoText ? `${leadingTodoText} ` : '') + transformRes.leadingTodoText;
  959. }
  960. }
  961. optionsLiteral = ts.factory.createObjectLiteralExpression(properties);
  962. }
  963. // The initial value is `undefined` or none is present:
  964. // - We may be able to use the `input()` shorthand
  965. // - or we use an explicit `undefined` initial value.
  966. if (initialValue === undefined) {
  967. // Shorthand not possible, so explicitly add `undefined`.
  968. if (preferShorthandIfPossible === null) {
  969. initialValue = ts.factory.createIdentifier('undefined');
  970. }
  971. else {
  972. resolvedType = preferShorthandIfPossible.originalType;
  973. // When using the `input()` shorthand, try cutting of `undefined` from potential
  974. // union types. `undefined` will be automatically included in the type.
  975. if (ts.isUnionTypeNode(resolvedType)) {
  976. resolvedType = migrate_ts_type_references.removeFromUnionIfPossible(resolvedType, (t) => t.kind !== ts.SyntaxKind.UndefinedKeyword);
  977. }
  978. }
  979. }
  980. const inputArgs = [];
  981. const typeArguments = [];
  982. if (resolvedType !== undefined) {
  983. typeArguments.push(resolvedType);
  984. if (metadata.transform !== null) {
  985. // Note: The TCB code generation may use the same type node and attach
  986. // synthetic comments for error reporting. We remove those explicitly here.
  987. typeArguments.push(ts.setSyntheticTrailingComments(metadata.transform.type.node, undefined));
  988. }
  989. }
  990. // Always add an initial value when the input is optional, and we have one, or we need one
  991. // to be able to pass options as the second argument.
  992. if (!metadata.required && (initialValue !== undefined || optionsLiteral !== null)) {
  993. inputArgs.push(initialValue ?? ts.factory.createIdentifier('undefined'));
  994. }
  995. if (optionsLiteral !== null) {
  996. inputArgs.push(optionsLiteral);
  997. }
  998. const inputFnRef = importManager.addImport({
  999. exportModuleSpecifier: '@angular/core',
  1000. exportSymbolName: 'input',
  1001. requestedFile: node.getSourceFile(),
  1002. });
  1003. const inputInitializerFn = metadata.required
  1004. ? ts.factory.createPropertyAccessExpression(inputFnRef, 'required')
  1005. : inputFnRef;
  1006. const inputInitializer = ts.factory.createCallExpression(inputInitializerFn, typeArguments, inputArgs);
  1007. let modifiersWithoutInputDecorator = node.modifiers?.filter((m) => m !== originalInputDecorator.node) ?? [];
  1008. // Add `readonly` to all new signal input declarations.
  1009. if (!modifiersWithoutInputDecorator?.some((s) => s.kind === ts.SyntaxKind.ReadonlyKeyword)) {
  1010. modifiersWithoutInputDecorator.push(ts.factory.createModifier(ts.SyntaxKind.ReadonlyKeyword));
  1011. }
  1012. const newNode = ts.factory.createPropertyDeclaration(modifiersWithoutInputDecorator, node.name, undefined, undefined, inputInitializer);
  1013. const newPropertyText = result.printer.printNode(ts.EmitHint.Unspecified, newNode, node.getSourceFile());
  1014. const replacements = [];
  1015. if (leadingTodoText !== null) {
  1016. replacements.push(migrate_ts_type_references.insertPrecedingLine(node, info, '// TODO: Notes from signal input migration:'), ...migrate_ts_type_references.cutStringToLineLimit(leadingTodoText, 70).map((line) => migrate_ts_type_references.insertPrecedingLine(node, info, `// ${line}`)));
  1017. }
  1018. replacements.push(new project_paths.Replacement(project_paths.projectFile(node.getSourceFile(), info), new project_paths.TextUpdate({
  1019. position: node.getStart(),
  1020. end: node.getEnd(),
  1021. toInsert: newPropertyText,
  1022. })));
  1023. return replacements;
  1024. }
  1025. /**
  1026. * Extracts the transform for the given input and returns a property assignment
  1027. * that works for the new signal `input()` API.
  1028. */
  1029. function extractTransformOfInput(transform, resolvedType, checker) {
  1030. assert(ts.isExpression(transform.node), `Expected transform to be an expression.`);
  1031. let transformFn = transform.node;
  1032. let leadingTodoText = null;
  1033. // If there is an explicit type, check if the transform return type actually works.
  1034. // In some cases, the transform function is not compatible because with decorator inputs,
  1035. // those were not checked. We cast the transform to `any` and add a TODO.
  1036. // TODO: Capture this in the design doc.
  1037. if (resolvedType !== undefined && !ts.isSyntheticExpression(resolvedType)) {
  1038. // Note: If the type is synthetic, we cannot check, and we accept that in the worst case
  1039. // we will create code that is not necessarily compiling. This is unlikely, but notably
  1040. // the errors would be correct and valuable.
  1041. const transformType = checker.getTypeAtLocation(transform.node);
  1042. const transformSignature = transformType.getCallSignatures()[0];
  1043. assert(transformSignature !== undefined, 'Expected transform to be an invoke-able.');
  1044. if (!checker.isTypeAssignableTo(checker.getReturnTypeOfSignature(transformSignature), checker.getTypeFromTypeNode(resolvedType))) {
  1045. leadingTodoText =
  1046. 'Input type is incompatible with transform. The migration added an `any` cast. ' +
  1047. 'This worked previously because Angular was unable to check transforms.';
  1048. transformFn = ts.factory.createAsExpression(ts.factory.createParenthesizedExpression(transformFn), ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword));
  1049. }
  1050. }
  1051. return {
  1052. node: ts.factory.createPropertyAssignment('transform', transformFn),
  1053. leadingTodoText,
  1054. };
  1055. }
  1056. /**
  1057. * Phase that migrates `@Input()` declarations to signal inputs and
  1058. * manages imports within the given file.
  1059. */
  1060. function pass6__migrateInputDeclarations(host, checker, result, knownInputs, importManager, info) {
  1061. let filesWithMigratedInputs = new Set();
  1062. let filesWithIncompatibleInputs = new WeakSet();
  1063. for (const [input, metadata] of result.sourceInputs) {
  1064. const sf = input.node.getSourceFile();
  1065. const inputInfo = knownInputs.get(input);
  1066. // Do not migrate incompatible inputs.
  1067. if (inputInfo.isIncompatible()) {
  1068. const incompatibilityReason = inputInfo.container.getInputMemberIncompatibility(input);
  1069. // Add a TODO for the incompatible input, if desired.
  1070. if (incompatibilityReason !== null && host.config.insertTodosForSkippedFields) {
  1071. result.replacements.push(...migrate_ts_type_references.insertTodoForIncompatibility(input.node, info, incompatibilityReason, {
  1072. single: 'input',
  1073. plural: 'inputs',
  1074. }));
  1075. }
  1076. filesWithIncompatibleInputs.add(sf);
  1077. continue;
  1078. }
  1079. assert(metadata !== null, `Expected metadata to exist for input isn't marked incompatible.`);
  1080. assert(!ts.isAccessor(input.node), 'Accessor inputs are incompatible.');
  1081. filesWithMigratedInputs.add(sf);
  1082. result.replacements.push(...convertToSignalInput(input.node, metadata, info, checker, importManager, result));
  1083. }
  1084. for (const file of filesWithMigratedInputs) {
  1085. // All inputs were migrated, so we can safely remove the `Input` symbol.
  1086. if (!filesWithIncompatibleInputs.has(file)) {
  1087. importManager.removeImport(file, 'Input', '@angular/core');
  1088. }
  1089. }
  1090. }
  1091. /**
  1092. * Phase that applies all changes recorded by the import manager in
  1093. * previous migrate phases.
  1094. */
  1095. function pass10_applyImportManager(importManager, result, sourceFiles, info) {
  1096. apply_import_manager.applyImportManagerChanges(importManager, result.replacements, sourceFiles, info);
  1097. }
  1098. /**
  1099. * Phase that migrates TypeScript input references to be signal compatible.
  1100. *
  1101. * The phase takes care of control flow analysis and generates temporary variables
  1102. * where needed to ensure narrowing continues to work. E.g.
  1103. */
  1104. function pass5__migrateTypeScriptReferences(host, references, checker, info) {
  1105. migrate_ts_type_references.migrateTypeScriptReferences(host, references, checker, info);
  1106. }
  1107. /**
  1108. * Phase that migrates Angular template references to
  1109. * unwrap signals.
  1110. */
  1111. function pass7__migrateTemplateReferences(host, references) {
  1112. const seenFileReferences = new Set();
  1113. for (const reference of references) {
  1114. // This pass only deals with HTML template references.
  1115. if (!index.isTemplateReference(reference)) {
  1116. continue;
  1117. }
  1118. // Skip references to incompatible inputs.
  1119. if (!host.shouldMigrateReferencesToField(reference.target)) {
  1120. continue;
  1121. }
  1122. // Skip duplicate references. E.g. if a template is shared.
  1123. const fileReferenceId = `${reference.from.templateFile.id}:${reference.from.read.sourceSpan.end}`;
  1124. if (seenFileReferences.has(fileReferenceId)) {
  1125. continue;
  1126. }
  1127. seenFileReferences.add(fileReferenceId);
  1128. // Expand shorthands like `{bla}` to `{bla: bla()}`.
  1129. const appendText = reference.from.isObjectShorthandExpression
  1130. ? `: ${reference.from.read.name}()`
  1131. : `()`;
  1132. host.replacements.push(new project_paths.Replacement(reference.from.templateFile, new project_paths.TextUpdate({
  1133. position: reference.from.read.sourceSpan.end,
  1134. end: reference.from.read.sourceSpan.end,
  1135. toInsert: appendText,
  1136. })));
  1137. }
  1138. }
  1139. /**
  1140. * Phase that migrates Angular host binding references to
  1141. * unwrap signals.
  1142. */
  1143. function pass8__migrateHostBindings(host, references, info) {
  1144. const seenReferences = new WeakMap();
  1145. for (const reference of references) {
  1146. // This pass only deals with host binding references.
  1147. if (!index.isHostBindingReference(reference)) {
  1148. continue;
  1149. }
  1150. // Skip references to incompatible inputs.
  1151. if (!host.shouldMigrateReferencesToField(reference.target)) {
  1152. continue;
  1153. }
  1154. const bindingField = reference.from.hostPropertyNode;
  1155. const expressionOffset = bindingField.getStart() + 1; // account for quotes.
  1156. const readEndPos = expressionOffset + reference.from.read.sourceSpan.end;
  1157. // Skip duplicate references. Can happen if the host object is shared.
  1158. if (seenReferences.get(bindingField)?.has(readEndPos)) {
  1159. continue;
  1160. }
  1161. if (seenReferences.has(bindingField)) {
  1162. seenReferences.get(bindingField).add(readEndPos);
  1163. }
  1164. else {
  1165. seenReferences.set(bindingField, new Set([readEndPos]));
  1166. }
  1167. // Expand shorthands like `{bla}` to `{bla: bla()}`.
  1168. const appendText = reference.from.isObjectShorthandExpression
  1169. ? `: ${reference.from.read.name}()`
  1170. : `()`;
  1171. host.replacements.push(new project_paths.Replacement(project_paths.projectFile(bindingField.getSourceFile(), info), new project_paths.TextUpdate({ position: readEndPos, end: readEndPos, toInsert: appendText })));
  1172. }
  1173. }
  1174. /**
  1175. * Migrates TypeScript "ts.Type" references. E.g.
  1176. * - `Partial<MyComp>` will be converted to `UnwrapSignalInputs<Partial<MyComp>>`.
  1177. in Catalyst test files.
  1178. */
  1179. function pass9__migrateTypeScriptTypeReferences(host, references, importManager, info) {
  1180. migrate_ts_type_references.migrateTypeScriptTypeReferences(host, references, importManager, info);
  1181. }
  1182. /**
  1183. * Executes the migration phase.
  1184. *
  1185. * This involves:
  1186. * - migrating TS references.
  1187. * - migrating `@Input()` declarations.
  1188. * - migrating template references.
  1189. * - migrating host binding references.
  1190. */
  1191. function executeMigrationPhase(host, knownInputs, result, info) {
  1192. const { typeChecker, sourceFiles } = info;
  1193. const importManager = new checker.ImportManager({
  1194. // For the purpose of this migration, we always use `input` and don't alias
  1195. // it to e.g. `input_1`.
  1196. generateUniqueIdentifier: () => null,
  1197. });
  1198. const referenceMigrationHost = {
  1199. printer: result.printer,
  1200. replacements: result.replacements,
  1201. shouldMigrateReferencesToField: (inputDescr) => knownInputs.has(inputDescr) && knownInputs.get(inputDescr).isIncompatible() === false,
  1202. shouldMigrateReferencesToClass: (clazz) => knownInputs.getDirectiveInfoForClass(clazz) !== undefined &&
  1203. knownInputs.getDirectiveInfoForClass(clazz).hasMigratedFields(),
  1204. };
  1205. // Migrate passes.
  1206. pass5__migrateTypeScriptReferences(referenceMigrationHost, result.references, typeChecker, info);
  1207. pass6__migrateInputDeclarations(host, typeChecker, result, knownInputs, importManager, info);
  1208. pass7__migrateTemplateReferences(referenceMigrationHost, result.references);
  1209. pass8__migrateHostBindings(referenceMigrationHost, result.references, info);
  1210. pass9__migrateTypeScriptTypeReferences(referenceMigrationHost, result.references, importManager, info);
  1211. pass10_applyImportManager(importManager, result, sourceFiles, info);
  1212. }
  1213. /** Filters ignorable input incompatibilities when best effort mode is enabled. */
  1214. function filterIncompatibilitiesForBestEffortMode(knownInputs) {
  1215. knownInputs.knownInputIds.forEach(({ container: c }) => {
  1216. // All class incompatibilities are "filterable" right now.
  1217. c.incompatible = null;
  1218. for (const [key, i] of c.memberIncompatibility.entries()) {
  1219. if (!migrate_ts_type_references.nonIgnorableFieldIncompatibilities.includes(i.reason)) {
  1220. c.memberIncompatibility.delete(key);
  1221. }
  1222. }
  1223. });
  1224. }
  1225. /**
  1226. * Tsurge migration for migrating Angular `@Input()` declarations to
  1227. * signal inputs, with support for batch execution.
  1228. */
  1229. class SignalInputMigration extends project_paths.TsurgeComplexMigration {
  1230. config;
  1231. upgradedAnalysisPhaseResults = null;
  1232. constructor(config = {}) {
  1233. super();
  1234. this.config = config;
  1235. }
  1236. createProgram(tsconfigAbsPath, fs) {
  1237. return super.createProgram(tsconfigAbsPath, fs, {
  1238. _compilePoisonedComponents: true,
  1239. // We want to migrate non-exported classes too.
  1240. compileNonExportedClasses: true,
  1241. // Always generate as much TCB code as possible.
  1242. // This allows us to check references in templates as much as possible.
  1243. // Note that this may yield more diagnostics, but we are not collecting these anyway.
  1244. strictTemplates: true,
  1245. });
  1246. }
  1247. /**
  1248. * Prepares the program for this migration with additional custom
  1249. * fields to allow for batch-mode testing.
  1250. */
  1251. _prepareProgram(info) {
  1252. // Optional filter for testing. Allows for simulation of parallel execution
  1253. // even if some tsconfig's have overlap due to sharing of TS sources.
  1254. // (this is commonly not the case in g3 where deps are `.d.ts` files).
  1255. const limitToRootNamesOnly = process.env['LIMIT_TO_ROOT_NAMES_ONLY'] === '1';
  1256. const filteredSourceFiles = info.sourceFiles.filter((f) =>
  1257. // Optional replacement filter. Allows parallel execution in case
  1258. // some tsconfig's have overlap due to sharing of TS sources.
  1259. // (this is commonly not the case in g3 where deps are `.d.ts` files).
  1260. !limitToRootNamesOnly || info.__programAbsoluteRootFileNames.includes(f.fileName));
  1261. return {
  1262. ...info,
  1263. sourceFiles: filteredSourceFiles,
  1264. };
  1265. }
  1266. // Extend the program info with the analysis information we need in every phase.
  1267. prepareAnalysisDeps(info) {
  1268. const analysisInfo = {
  1269. ...info,
  1270. ...prepareAnalysisInfo(info.program, info.ngCompiler, info.__programAbsoluteRootFileNames),
  1271. };
  1272. return analysisInfo;
  1273. }
  1274. async analyze(info) {
  1275. info = this._prepareProgram(info);
  1276. const analysisDeps = this.prepareAnalysisDeps(info);
  1277. const knownInputs = new KnownInputs(info, this.config);
  1278. const result = new MigrationResult();
  1279. const host = createMigrationHost(info, this.config);
  1280. this.config.reportProgressFn?.(10, 'Analyzing project (input usages)..');
  1281. const { inheritanceGraph } = executeAnalysisPhase(host, knownInputs, result, analysisDeps);
  1282. // Mark filtered inputs before checking inheritance. This ensures filtered
  1283. // inputs properly influence e.g. inherited or derived inputs that now wouldn't
  1284. // be safe either (BUT can still be skipped via best effort mode later).
  1285. filterInputsViaConfig(result, knownInputs, this.config);
  1286. // Analyze inheritance, track edges etc. and later propagate incompatibilities in
  1287. // the merge stage.
  1288. this.config.reportProgressFn?.(40, 'Checking inheritance..');
  1289. pass4__checkInheritanceOfInputs(inheritanceGraph, analysisDeps.metaRegistry, knownInputs);
  1290. // Filter best effort incompatibilities, so that the new filtered ones can
  1291. // be accordingly respected in the merge phase.
  1292. if (this.config.bestEffortMode) {
  1293. filterIncompatibilitiesForBestEffortMode(knownInputs);
  1294. }
  1295. const unitData = getCompilationUnitMetadata(knownInputs);
  1296. // Non-batch mode!
  1297. if (this.config.upgradeAnalysisPhaseToAvoidBatch) {
  1298. const globalMeta = await this.globalMeta(unitData);
  1299. const { replacements } = await this.migrate(globalMeta, info, {
  1300. knownInputs,
  1301. result,
  1302. host,
  1303. analysisDeps,
  1304. });
  1305. this.config.reportProgressFn?.(100, 'Completed migration.');
  1306. // Expose the upgraded analysis stage results.
  1307. this.upgradedAnalysisPhaseResults = {
  1308. replacements,
  1309. projectRoot: info.projectRoot,
  1310. knownInputs,
  1311. };
  1312. }
  1313. return project_paths.confirmAsSerializable(unitData);
  1314. }
  1315. async combine(unitA, unitB) {
  1316. return project_paths.confirmAsSerializable(combineCompilationUnitData(unitA, unitB));
  1317. }
  1318. async globalMeta(combinedData) {
  1319. return project_paths.confirmAsSerializable(convertToGlobalMeta(combinedData));
  1320. }
  1321. async migrate(globalMetadata, info, nonBatchData) {
  1322. info = this._prepareProgram(info);
  1323. const knownInputs = nonBatchData?.knownInputs ?? new KnownInputs(info, this.config);
  1324. const result = nonBatchData?.result ?? new MigrationResult();
  1325. const host = nonBatchData?.host ?? createMigrationHost(info, this.config);
  1326. const analysisDeps = nonBatchData?.analysisDeps ?? this.prepareAnalysisDeps(info);
  1327. // Can't re-use analysis structures, so re-build them.
  1328. if (nonBatchData === undefined) {
  1329. executeAnalysisPhase(host, knownInputs, result, analysisDeps);
  1330. }
  1331. // Incorporate global metadata into known inputs.
  1332. populateKnownInputsFromGlobalData(knownInputs, globalMetadata);
  1333. if (this.config.bestEffortMode) {
  1334. filterIncompatibilitiesForBestEffortMode(knownInputs);
  1335. }
  1336. this.config.reportProgressFn?.(60, 'Collecting migration changes..');
  1337. executeMigrationPhase(host, knownInputs, result, analysisDeps);
  1338. return { replacements: result.replacements };
  1339. }
  1340. async stats(globalMetadata) {
  1341. let fullCompilationInputs = 0;
  1342. let sourceInputs = 0;
  1343. let incompatibleInputs = 0;
  1344. const fieldIncompatibleCounts = {};
  1345. const classIncompatibleCounts = {};
  1346. for (const [id, input] of Object.entries(globalMetadata.knownInputs)) {
  1347. fullCompilationInputs++;
  1348. const isConsideredSourceInput = input.seenAsSourceInput &&
  1349. input.memberIncompatibility !== migrate_ts_type_references.FieldIncompatibilityReason.OutsideOfMigrationScope &&
  1350. input.memberIncompatibility !== migrate_ts_type_references.FieldIncompatibilityReason.SkippedViaConfigFilter;
  1351. // We won't track incompatibilities to inputs that aren't considered source inputs.
  1352. // Tracking their statistics wouldn't provide any value.
  1353. if (!isConsideredSourceInput) {
  1354. continue;
  1355. }
  1356. sourceInputs++;
  1357. if (input.memberIncompatibility !== null || input.owningClassIncompatibility !== null) {
  1358. incompatibleInputs++;
  1359. }
  1360. if (input.memberIncompatibility !== null) {
  1361. const reasonName = migrate_ts_type_references.FieldIncompatibilityReason[input.memberIncompatibility];
  1362. const key = `input-field-incompatibility-${reasonName}`;
  1363. fieldIncompatibleCounts[key] ??= 0;
  1364. fieldIncompatibleCounts[key]++;
  1365. }
  1366. if (input.owningClassIncompatibility !== null) {
  1367. const reasonName = migrate_ts_type_references.ClassIncompatibilityReason[input.owningClassIncompatibility];
  1368. const key = `input-owning-class-incompatibility-${reasonName}`;
  1369. classIncompatibleCounts[key] ??= 0;
  1370. classIncompatibleCounts[key]++;
  1371. }
  1372. }
  1373. return {
  1374. counters: {
  1375. fullCompilationInputs,
  1376. sourceInputs,
  1377. incompatibleInputs,
  1378. ...fieldIncompatibleCounts,
  1379. ...classIncompatibleCounts,
  1380. },
  1381. };
  1382. }
  1383. }
  1384. /**
  1385. * Updates the migration state to filter inputs based on a filter
  1386. * method defined in the migration config.
  1387. */
  1388. function filterInputsViaConfig(result, knownInputs, config) {
  1389. if (config.shouldMigrateInput === undefined) {
  1390. return;
  1391. }
  1392. const skippedInputs = new Set();
  1393. // Mark all skipped inputs as incompatible for migration.
  1394. for (const input of knownInputs.knownInputIds.values()) {
  1395. if (!config.shouldMigrateInput(input)) {
  1396. skippedInputs.add(input.descriptor.key);
  1397. knownInputs.markFieldIncompatible(input.descriptor, {
  1398. context: null,
  1399. reason: migrate_ts_type_references.FieldIncompatibilityReason.SkippedViaConfigFilter,
  1400. });
  1401. }
  1402. }
  1403. }
  1404. function createMigrationHost(info, config) {
  1405. return new MigrationHost(/* isMigratingCore */ false, info, config, info.sourceFiles);
  1406. }
  1407. function migrate(options) {
  1408. return async (tree, context) => {
  1409. await project_paths.runMigrationInDevkit({
  1410. tree,
  1411. getMigration: (fs) => new SignalInputMigration({
  1412. bestEffortMode: options.bestEffortMode,
  1413. insertTodosForSkippedFields: options.insertTodos,
  1414. shouldMigrateInput: (input) => {
  1415. return (input.file.rootRelativePath.startsWith(fs.normalize(options.path)) &&
  1416. !/(^|\/)node_modules\//.test(input.file.rootRelativePath));
  1417. },
  1418. }),
  1419. beforeProgramCreation: (tsconfigPath, stage) => {
  1420. if (stage === project_paths.MigrationStage.Analysis) {
  1421. context.logger.info(`Preparing analysis for: ${tsconfigPath}...`);
  1422. }
  1423. else {
  1424. context.logger.info(`Running migration for: ${tsconfigPath}...`);
  1425. }
  1426. },
  1427. afterProgramCreation: (info, fs) => {
  1428. const analysisPath = fs.resolve(options.analysisDir);
  1429. // Support restricting the analysis to subfolders for larger projects.
  1430. if (analysisPath !== '/') {
  1431. info.sourceFiles = info.sourceFiles.filter((sf) => sf.fileName.startsWith(analysisPath));
  1432. info.fullProgramSourceFiles = info.fullProgramSourceFiles.filter((sf) => sf.fileName.startsWith(analysisPath));
  1433. }
  1434. },
  1435. beforeUnitAnalysis: (tsconfigPath) => {
  1436. context.logger.info(`Scanning for inputs: ${tsconfigPath}...`);
  1437. },
  1438. afterAllAnalyzed: () => {
  1439. context.logger.info(``);
  1440. context.logger.info(`Processing analysis data between targets...`);
  1441. context.logger.info(``);
  1442. },
  1443. afterAnalysisFailure: () => {
  1444. context.logger.error('Migration failed unexpectedly with no analysis data');
  1445. },
  1446. whenDone: ({ counters }) => {
  1447. const { sourceInputs, incompatibleInputs } = counters;
  1448. const migratedInputs = sourceInputs - incompatibleInputs;
  1449. context.logger.info('');
  1450. context.logger.info(`Successfully migrated to signal inputs 🎉`);
  1451. context.logger.info(` -> Migrated ${migratedInputs}/${sourceInputs} inputs.`);
  1452. if (incompatibleInputs > 0 && !options.insertTodos) {
  1453. context.logger.warn(`To see why ${incompatibleInputs} inputs couldn't be migrated`);
  1454. context.logger.warn(`consider re-running with "--insert-todos" or "--best-effort-mode".`);
  1455. }
  1456. if (options.bestEffortMode) {
  1457. context.logger.warn(`You ran with best effort mode. Manually verify all code ` +
  1458. `works as intended, and fix where necessary.`);
  1459. }
  1460. },
  1461. });
  1462. };
  1463. }
  1464. exports.migrate = migrate;