index-BPhQoCcF.cjs 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997
  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 ts = require('typescript');
  9. require('os');
  10. var checker = require('./checker-5pyJrZ9G.cjs');
  11. var index = require('./index-BIvVb6in.cjs');
  12. require('path');
  13. var project_paths = require('./project_paths-CyWVEsbT.cjs');
  14. function getMemberName(member) {
  15. if (member.name === undefined) {
  16. return null;
  17. }
  18. if (ts.isIdentifier(member.name) || ts.isStringLiteralLike(member.name)) {
  19. return member.name.text;
  20. }
  21. if (ts.isPrivateIdentifier(member.name)) {
  22. return `#${member.name.text}`;
  23. }
  24. return null;
  25. }
  26. /** Checks whether the given node can be an `@Input()` declaration node. */
  27. function isInputContainerNode(node) {
  28. return (((ts.isAccessor(node) && ts.isClassDeclaration(node.parent)) ||
  29. ts.isPropertyDeclaration(node)) &&
  30. getMemberName(node) !== null);
  31. }
  32. /**
  33. * Detects `query(By.directive(T)).componentInstance` patterns and enhances
  34. * them with information of `T`. This is important because `.componentInstance`
  35. * is currently typed as `any` and may cause runtime test failures after input
  36. * migrations then.
  37. *
  38. * The reference resolution pass leverages information from this pattern
  39. * recognizer.
  40. */
  41. class DebugElementComponentInstance {
  42. checker;
  43. cache = new WeakMap();
  44. constructor(checker) {
  45. this.checker = checker;
  46. }
  47. detect(node) {
  48. if (this.cache.has(node)) {
  49. return this.cache.get(node);
  50. }
  51. if (!ts.isPropertyAccessExpression(node)) {
  52. return null;
  53. }
  54. // Check for `<>.componentInstance`.
  55. if (!ts.isIdentifier(node.name) || node.name.text !== 'componentInstance') {
  56. return null;
  57. }
  58. // Check for `<>.query(..).<>`.
  59. if (!ts.isCallExpression(node.expression) ||
  60. !ts.isPropertyAccessExpression(node.expression.expression) ||
  61. !ts.isIdentifier(node.expression.expression.name) ||
  62. node.expression.expression.name.text !== 'query') {
  63. return null;
  64. }
  65. const queryCall = node.expression;
  66. if (queryCall.arguments.length !== 1) {
  67. return null;
  68. }
  69. const queryArg = queryCall.arguments[0];
  70. let typeExpr;
  71. if (ts.isCallExpression(queryArg) &&
  72. queryArg.arguments.length === 1 &&
  73. ts.isIdentifier(queryArg.arguments[0])) {
  74. // Detect references, like: `query(By.directive(T))`.
  75. typeExpr = queryArg.arguments[0];
  76. }
  77. else if (ts.isIdentifier(queryArg)) {
  78. // Detect references, like: `harness.query(T)`.
  79. typeExpr = queryArg;
  80. }
  81. else {
  82. return null;
  83. }
  84. const symbol = this.checker.getSymbolAtLocation(typeExpr);
  85. if (symbol?.valueDeclaration === undefined ||
  86. !ts.isClassDeclaration(symbol?.valueDeclaration)) {
  87. // Cache this as we use the expensive type checker.
  88. this.cache.set(node, null);
  89. return null;
  90. }
  91. const type = this.checker.getTypeAtLocation(symbol.valueDeclaration);
  92. this.cache.set(node, type);
  93. return type;
  94. }
  95. }
  96. /**
  97. * Recognizes `Partial<T>` instances in Catalyst tests. Those type queries
  98. * are likely used for typing property initialization values for the given class `T`
  99. * and we have a few scenarios:
  100. *
  101. * 1. The API does not unwrap signal inputs. In which case, the values are likely no
  102. * longer assignable to an `InputSignal`.
  103. * 2. The API does unwrap signal inputs, in which case we need to unwrap the `Partial`
  104. * because the values are raw initial values, like they were before.
  105. *
  106. * We can enable this heuristic when we detect Catalyst as we know it supports unwrapping.
  107. */
  108. class PartialDirectiveTypeInCatalystTests {
  109. checker;
  110. knownFields;
  111. constructor(checker, knownFields) {
  112. this.checker = checker;
  113. this.knownFields = knownFields;
  114. }
  115. detect(node) {
  116. // Detect `Partial<...>`
  117. if (!ts.isTypeReferenceNode(node) ||
  118. !ts.isIdentifier(node.typeName) ||
  119. node.typeName.text !== 'Partial') {
  120. return null;
  121. }
  122. // Ignore if the source file doesn't reference Catalyst.
  123. if (!node.getSourceFile().text.includes('angular2/testing/catalyst')) {
  124. return null;
  125. }
  126. // Extract T of `Partial<T>`.
  127. const cmpTypeArg = node.typeArguments?.[0];
  128. if (!cmpTypeArg ||
  129. !ts.isTypeReferenceNode(cmpTypeArg) ||
  130. !ts.isIdentifier(cmpTypeArg.typeName)) {
  131. return null;
  132. }
  133. const cmpType = cmpTypeArg.typeName;
  134. const symbol = this.checker.getSymbolAtLocation(cmpType);
  135. // Note: Technically the class might be derived of an input-containing class,
  136. // but this is out of scope for now. We can expand if we see it's a common case.
  137. if (symbol?.valueDeclaration === undefined ||
  138. !ts.isClassDeclaration(symbol.valueDeclaration) ||
  139. !this.knownFields.shouldTrackClassReference(symbol.valueDeclaration)) {
  140. return null;
  141. }
  142. return { referenceNode: node, targetClass: symbol.valueDeclaration };
  143. }
  144. }
  145. /**
  146. * Attempts to look up the given property access chain using
  147. * the type checker.
  148. *
  149. * Notably this is not as safe as using the type checker directly to
  150. * retrieve symbols of a given identifier, but in some cases this is
  151. * a necessary approach to compensate e.g. for a lack of TCB information
  152. * when processing Angular templates.
  153. *
  154. * The path is a list of properties to be accessed sequentially on the
  155. * given type.
  156. */
  157. function lookupPropertyAccess(checker, type, path, options = {}) {
  158. let symbol = null;
  159. for (const propName of path) {
  160. // Note: We support assuming `NonNullable` for the pathl This is necessary
  161. // in some situations as otherwise the lookups would fail to resolve the target
  162. // symbol just because of e.g. a ternary. This is used in the signal input migration
  163. // for host bindings.
  164. type = options.ignoreNullability ? type.getNonNullableType() : type;
  165. const propSymbol = type.getProperty(propName);
  166. if (propSymbol === undefined) {
  167. return null;
  168. }
  169. symbol = propSymbol;
  170. type = checker.getTypeOfSymbol(propSymbol);
  171. }
  172. if (symbol === null) {
  173. return null;
  174. }
  175. return { symbol, type };
  176. }
  177. /**
  178. * AST visitor that iterates through a template and finds all
  179. * input references.
  180. *
  181. * This resolution is important to be able to migrate references to inputs
  182. * that will be migrated to signal inputs.
  183. */
  184. class TemplateReferenceVisitor extends checker.RecursiveVisitor$1 {
  185. result = [];
  186. /**
  187. * Whether we are currently descending into HTML AST nodes
  188. * where all bound attributes are considered potentially narrowing.
  189. *
  190. * Keeps track of all referenced inputs in such attribute expressions.
  191. */
  192. templateAttributeReferencedFields = null;
  193. expressionVisitor;
  194. seenKnownFieldsCount = new Map();
  195. constructor(typeChecker, templateTypeChecker, componentClass, knownFields, fieldNamesToConsiderForReferenceLookup) {
  196. super();
  197. this.expressionVisitor = new TemplateExpressionReferenceVisitor(typeChecker, templateTypeChecker, componentClass, knownFields, fieldNamesToConsiderForReferenceLookup);
  198. }
  199. checkExpressionForReferencedFields(activeNode, expressionNode) {
  200. const referencedFields = this.expressionVisitor.checkTemplateExpression(activeNode, expressionNode);
  201. // Add all references to the overall visitor result.
  202. this.result.push(...referencedFields);
  203. // Count usages of seen input references. We'll use this to make decisions
  204. // based on whether inputs are potentially narrowed or not.
  205. for (const input of referencedFields) {
  206. this.seenKnownFieldsCount.set(input.targetField.key, (this.seenKnownFieldsCount.get(input.targetField.key) ?? 0) + 1);
  207. }
  208. return referencedFields;
  209. }
  210. descendAndCheckForNarrowedSimilarReferences(potentiallyNarrowedInputs, descend) {
  211. const inputs = potentiallyNarrowedInputs.map((i) => ({
  212. ref: i,
  213. key: i.targetField.key,
  214. pastCount: this.seenKnownFieldsCount.get(i.targetField.key) ?? 0,
  215. }));
  216. descend();
  217. for (const input of inputs) {
  218. // Input was referenced inside a narrowable spot, and is used in child nodes.
  219. // This is a sign for the input to be narrowed. Mark it as such.
  220. if ((this.seenKnownFieldsCount.get(input.key) ?? 0) > input.pastCount) {
  221. input.ref.isLikelyNarrowed = true;
  222. }
  223. }
  224. }
  225. visitTemplate(template) {
  226. // Note: We assume all bound expressions for templates may be subject
  227. // to TCB narrowing. This is relevant for now until we support narrowing
  228. // of signal calls in templates.
  229. // TODO: Remove with: https://github.com/angular/angular/pull/55456.
  230. this.templateAttributeReferencedFields = [];
  231. checker.visitAll$1(this, template.attributes);
  232. checker.visitAll$1(this, template.templateAttrs);
  233. // If we are dealing with a microsyntax template, do not check
  234. // inputs and outputs as those are already passed to the children.
  235. // Template attributes may contain relevant expressions though.
  236. if (template.tagName === 'ng-template') {
  237. checker.visitAll$1(this, template.inputs);
  238. checker.visitAll$1(this, template.outputs);
  239. }
  240. const referencedInputs = this.templateAttributeReferencedFields;
  241. this.templateAttributeReferencedFields = null;
  242. this.descendAndCheckForNarrowedSimilarReferences(referencedInputs, () => {
  243. checker.visitAll$1(this, template.children);
  244. checker.visitAll$1(this, template.references);
  245. checker.visitAll$1(this, template.variables);
  246. });
  247. }
  248. visitIfBlockBranch(block) {
  249. if (block.expression) {
  250. const referencedFields = this.checkExpressionForReferencedFields(block, block.expression);
  251. this.descendAndCheckForNarrowedSimilarReferences(referencedFields, () => {
  252. super.visitIfBlockBranch(block);
  253. });
  254. }
  255. else {
  256. super.visitIfBlockBranch(block);
  257. }
  258. }
  259. visitForLoopBlock(block) {
  260. this.checkExpressionForReferencedFields(block, block.expression);
  261. this.checkExpressionForReferencedFields(block, block.trackBy);
  262. super.visitForLoopBlock(block);
  263. }
  264. visitSwitchBlock(block) {
  265. const referencedFields = this.checkExpressionForReferencedFields(block, block.expression);
  266. this.descendAndCheckForNarrowedSimilarReferences(referencedFields, () => {
  267. super.visitSwitchBlock(block);
  268. });
  269. }
  270. visitSwitchBlockCase(block) {
  271. if (block.expression) {
  272. const referencedFields = this.checkExpressionForReferencedFields(block, block.expression);
  273. this.descendAndCheckForNarrowedSimilarReferences(referencedFields, () => {
  274. super.visitSwitchBlockCase(block);
  275. });
  276. }
  277. else {
  278. super.visitSwitchBlockCase(block);
  279. }
  280. }
  281. visitDeferredBlock(deferred) {
  282. if (deferred.triggers.when) {
  283. this.checkExpressionForReferencedFields(deferred, deferred.triggers.when.value);
  284. }
  285. if (deferred.prefetchTriggers.when) {
  286. this.checkExpressionForReferencedFields(deferred, deferred.prefetchTriggers.when.value);
  287. }
  288. super.visitDeferredBlock(deferred);
  289. }
  290. visitBoundText(text) {
  291. this.checkExpressionForReferencedFields(text, text.value);
  292. }
  293. visitBoundEvent(attribute) {
  294. this.checkExpressionForReferencedFields(attribute, attribute.handler);
  295. }
  296. visitBoundAttribute(attribute) {
  297. const referencedFields = this.checkExpressionForReferencedFields(attribute, attribute.value);
  298. // Attributes inside templates are potentially "narrowed" and hence we
  299. // keep track of all referenced inputs to see if they actually are.
  300. if (this.templateAttributeReferencedFields !== null) {
  301. this.templateAttributeReferencedFields.push(...referencedFields);
  302. }
  303. }
  304. }
  305. /**
  306. * Expression AST visitor that checks whether a given expression references
  307. * a known `@Input()`.
  308. *
  309. * This resolution is important to be able to migrate references to inputs
  310. * that will be migrated to signal inputs.
  311. */
  312. class TemplateExpressionReferenceVisitor extends checker.RecursiveAstVisitor {
  313. typeChecker;
  314. templateTypeChecker;
  315. componentClass;
  316. knownFields;
  317. fieldNamesToConsiderForReferenceLookup;
  318. activeTmplAstNode = null;
  319. detectedInputReferences = [];
  320. isInsideObjectShorthandExpression = false;
  321. insideConditionalExpressionsWithReads = [];
  322. constructor(typeChecker, templateTypeChecker, componentClass, knownFields, fieldNamesToConsiderForReferenceLookup) {
  323. super();
  324. this.typeChecker = typeChecker;
  325. this.templateTypeChecker = templateTypeChecker;
  326. this.componentClass = componentClass;
  327. this.knownFields = knownFields;
  328. this.fieldNamesToConsiderForReferenceLookup = fieldNamesToConsiderForReferenceLookup;
  329. }
  330. /** Checks the given AST expression. */
  331. checkTemplateExpression(activeNode, expressionNode) {
  332. this.detectedInputReferences = [];
  333. this.activeTmplAstNode = activeNode;
  334. expressionNode.visit(this, []);
  335. return this.detectedInputReferences;
  336. }
  337. visit(ast, context) {
  338. super.visit(ast, [...context, ast]);
  339. }
  340. // Keep track when we are inside an object shorthand expression. This is
  341. // necessary as we need to expand the shorthand to invoke a potential new signal.
  342. // E.g. `{bla}` may be transformed to `{bla: bla()}`.
  343. visitLiteralMap(ast, context) {
  344. for (const [idx, key] of ast.keys.entries()) {
  345. this.isInsideObjectShorthandExpression = !!key.isShorthandInitialized;
  346. ast.values[idx].visit(this, context);
  347. this.isInsideObjectShorthandExpression = false;
  348. }
  349. }
  350. visitPropertyRead(ast, context) {
  351. this._inspectPropertyAccess(ast, context);
  352. super.visitPropertyRead(ast, context);
  353. }
  354. visitSafePropertyRead(ast, context) {
  355. this._inspectPropertyAccess(ast, context);
  356. super.visitPropertyRead(ast, context);
  357. }
  358. visitPropertyWrite(ast, context) {
  359. this._inspectPropertyAccess(ast, context);
  360. super.visitPropertyWrite(ast, context);
  361. }
  362. visitConditional(ast, context) {
  363. this.visit(ast.condition, context);
  364. this.insideConditionalExpressionsWithReads.push(ast.condition);
  365. this.visit(ast.trueExp, context);
  366. this.visit(ast.falseExp, context);
  367. this.insideConditionalExpressionsWithReads.pop();
  368. }
  369. /**
  370. * Inspects the property access and attempts to resolve whether they access
  371. * a known field. If so, the result is captured.
  372. */
  373. _inspectPropertyAccess(ast, astPath) {
  374. if (this.fieldNamesToConsiderForReferenceLookup !== null &&
  375. !this.fieldNamesToConsiderForReferenceLookup.has(ast.name)) {
  376. return;
  377. }
  378. const isWrite = !!(ast instanceof checker.PropertyWrite ||
  379. (this.activeTmplAstNode && isTwoWayBindingNode(this.activeTmplAstNode)));
  380. this._checkAccessViaTemplateTypeCheckBlock(ast, isWrite, astPath) ||
  381. this._checkAccessViaOwningComponentClassType(ast, isWrite, astPath);
  382. }
  383. /**
  384. * Checks whether the node refers to an input using the TCB information.
  385. * Type check block may not exist for e.g. test components, so this can return `null`.
  386. */
  387. _checkAccessViaTemplateTypeCheckBlock(ast, isWrite, astPath) {
  388. // There might be no template type checker. E.g. if we check host bindings.
  389. if (this.templateTypeChecker === null) {
  390. return false;
  391. }
  392. const symbol = this.templateTypeChecker.getSymbolOfNode(ast, this.componentClass);
  393. if (symbol?.kind !== checker.SymbolKind.Expression || symbol.tsSymbol === null) {
  394. return false;
  395. }
  396. // Dangerous: Type checking symbol retrieval is a totally different `ts.Program`,
  397. // than the one where we analyzed `knownInputs`.
  398. // --> Find the input via its input id.
  399. const targetInput = this.knownFields.attemptRetrieveDescriptorFromSymbol(symbol.tsSymbol);
  400. if (targetInput === null) {
  401. return false;
  402. }
  403. this.detectedInputReferences.push({
  404. targetNode: targetInput.node,
  405. targetField: targetInput,
  406. read: ast,
  407. readAstPath: astPath,
  408. context: this.activeTmplAstNode,
  409. isLikelyNarrowed: this._isPartOfNarrowingTernary(ast),
  410. isObjectShorthandExpression: this.isInsideObjectShorthandExpression,
  411. isWrite,
  412. });
  413. return true;
  414. }
  415. /**
  416. * Simple resolution checking whether the given AST refers to a known input.
  417. * This is a fallback for when there is no type checking information (e.g. in host bindings).
  418. *
  419. * It attempts to resolve references by traversing accesses of the "component class" type.
  420. * e.g. `this.bla` is resolved via `CompType#bla` and further.
  421. */
  422. _checkAccessViaOwningComponentClassType(ast, isWrite, astPath) {
  423. // We might check host bindings, which can never point to template variables or local refs.
  424. const expressionTemplateTarget = this.templateTypeChecker === null
  425. ? null
  426. : this.templateTypeChecker.getExpressionTarget(ast, this.componentClass);
  427. // Skip checking if:
  428. // - the reference resolves to a template variable or local ref. No way to resolve without TCB.
  429. // - the owning component does not have a name (should not happen technically).
  430. if (expressionTemplateTarget !== null || this.componentClass.name === undefined) {
  431. return;
  432. }
  433. const property = traverseReceiverAndLookupSymbol(ast, this.componentClass, this.typeChecker);
  434. if (property === null) {
  435. return;
  436. }
  437. const matchingTarget = this.knownFields.attemptRetrieveDescriptorFromSymbol(property);
  438. if (matchingTarget === null) {
  439. return;
  440. }
  441. this.detectedInputReferences.push({
  442. targetNode: matchingTarget.node,
  443. targetField: matchingTarget,
  444. read: ast,
  445. readAstPath: astPath,
  446. context: this.activeTmplAstNode,
  447. isLikelyNarrowed: this._isPartOfNarrowingTernary(ast),
  448. isObjectShorthandExpression: this.isInsideObjectShorthandExpression,
  449. isWrite,
  450. });
  451. }
  452. _isPartOfNarrowingTernary(read) {
  453. // Note: We do not safe check that the reads are fully matching 1:1. This is acceptable
  454. // as worst case we just skip an input from being migrated. This is very unlikely too.
  455. return this.insideConditionalExpressionsWithReads.some((r) => (r instanceof checker.PropertyRead ||
  456. r instanceof checker.PropertyWrite ||
  457. r instanceof checker.SafePropertyRead) &&
  458. r.name === read.name);
  459. }
  460. }
  461. /**
  462. * Emulates an access to a given field using the TypeScript `ts.Type`
  463. * of the given class. The resolved symbol of the access is returned.
  464. */
  465. function traverseReceiverAndLookupSymbol(readOrWrite, componentClass, checker$1) {
  466. const path = [readOrWrite.name];
  467. let node = readOrWrite;
  468. while (node.receiver instanceof checker.PropertyRead || node.receiver instanceof checker.PropertyWrite) {
  469. node = node.receiver;
  470. path.unshift(node.name);
  471. }
  472. if (!(node.receiver instanceof checker.ImplicitReceiver || node.receiver instanceof checker.ThisReceiver)) {
  473. return null;
  474. }
  475. const classType = checker$1.getTypeAtLocation(componentClass.name);
  476. return (lookupPropertyAccess(checker$1, classType, path, {
  477. // Necessary to avoid breaking the resolution if there is
  478. // some narrowing involved. E.g. `myClass ? myClass.input`.
  479. ignoreNullability: true,
  480. })?.symbol ?? null);
  481. }
  482. /** Whether the given node refers to a two-way binding AST node. */
  483. function isTwoWayBindingNode(node) {
  484. return ((node instanceof checker.BoundAttribute && node.type === checker.BindingType.TwoWay) ||
  485. (node instanceof checker.BoundEvent && node.type === checker.ParsedEventType.TwoWay));
  486. }
  487. /** Possible types of references to known fields detected. */
  488. exports.ReferenceKind = void 0;
  489. (function (ReferenceKind) {
  490. ReferenceKind[ReferenceKind["InTemplate"] = 0] = "InTemplate";
  491. ReferenceKind[ReferenceKind["InHostBinding"] = 1] = "InHostBinding";
  492. ReferenceKind[ReferenceKind["TsReference"] = 2] = "TsReference";
  493. ReferenceKind[ReferenceKind["TsClassTypeReference"] = 3] = "TsClassTypeReference";
  494. })(exports.ReferenceKind || (exports.ReferenceKind = {}));
  495. /** Whether the given reference is a TypeScript reference. */
  496. function isTsReference(ref) {
  497. return ref.kind === exports.ReferenceKind.TsReference;
  498. }
  499. /** Whether the given reference is a template reference. */
  500. function isTemplateReference(ref) {
  501. return ref.kind === exports.ReferenceKind.InTemplate;
  502. }
  503. /** Whether the given reference is a host binding reference. */
  504. function isHostBindingReference(ref) {
  505. return ref.kind === exports.ReferenceKind.InHostBinding;
  506. }
  507. /**
  508. * Whether the given reference is a TypeScript `ts.Type` reference
  509. * to a class containing known fields.
  510. */
  511. function isTsClassTypeReference(ref) {
  512. return ref.kind === exports.ReferenceKind.TsClassTypeReference;
  513. }
  514. /**
  515. * Checks host bindings of the given class and tracks all
  516. * references to inputs within bindings.
  517. */
  518. function identifyHostBindingReferences(node, programInfo, checker$1, reflector, result, knownFields, fieldNamesToConsiderForReferenceLookup) {
  519. if (node.name === undefined) {
  520. return;
  521. }
  522. const decorators = reflector.getDecoratorsOfDeclaration(node);
  523. if (decorators === null) {
  524. return;
  525. }
  526. const angularDecorators = checker.getAngularDecorators(decorators, ['Directive', 'Component'],
  527. /* isAngularCore */ false);
  528. if (angularDecorators.length === 0) {
  529. return;
  530. }
  531. // Assume only one Angular decorator per class.
  532. const ngDecorator = angularDecorators[0];
  533. if (ngDecorator.args?.length !== 1) {
  534. return;
  535. }
  536. const metadataNode = checker.unwrapExpression(ngDecorator.args[0]);
  537. if (!ts.isObjectLiteralExpression(metadataNode)) {
  538. return;
  539. }
  540. const metadata = checker.reflectObjectLiteral(metadataNode);
  541. if (!metadata.has('host')) {
  542. return;
  543. }
  544. let hostField = checker.unwrapExpression(metadata.get('host'));
  545. // Special-case in case host bindings are shared via a variable.
  546. // e.g. Material button shares host bindings as a constant in the same target.
  547. if (ts.isIdentifier(hostField)) {
  548. let symbol = checker$1.getSymbolAtLocation(hostField);
  549. // Plain identifier references can point to alias symbols (e.g. imports).
  550. if (symbol !== undefined && symbol.flags & ts.SymbolFlags.Alias) {
  551. symbol = checker$1.getAliasedSymbol(symbol);
  552. }
  553. if (symbol !== undefined &&
  554. symbol.valueDeclaration !== undefined &&
  555. ts.isVariableDeclaration(symbol.valueDeclaration)) {
  556. hostField = symbol?.valueDeclaration.initializer;
  557. }
  558. }
  559. if (hostField === undefined || !ts.isObjectLiteralExpression(hostField)) {
  560. return;
  561. }
  562. const hostMap = checker.reflectObjectLiteral(hostField);
  563. const expressionResult = [];
  564. const expressionVisitor = new TemplateExpressionReferenceVisitor(checker$1, null, node, knownFields, fieldNamesToConsiderForReferenceLookup);
  565. for (const [rawName, expression] of hostMap.entries()) {
  566. if (!ts.isStringLiteralLike(expression)) {
  567. continue;
  568. }
  569. const isEventBinding = rawName.startsWith('(');
  570. const isPropertyBinding = rawName.startsWith('[');
  571. // Only migrate property or event bindings.
  572. if (!isPropertyBinding && !isEventBinding) {
  573. continue;
  574. }
  575. const parser = checker.makeBindingParser();
  576. const sourceSpan = new checker.ParseSourceSpan(
  577. // Fake source span to keep parsing offsets zero-based.
  578. // We then later combine these with the expression TS node offsets.
  579. new checker.ParseLocation({ content: '', url: '' }, 0, 0, 0), new checker.ParseLocation({ content: '', url: '' }, 0, 0, 0));
  580. const name = rawName.substring(1, rawName.length - 1);
  581. let parsed = undefined;
  582. if (isEventBinding) {
  583. const result = [];
  584. parser.parseEvent(name.substring(1, name.length - 1), expression.text, false, sourceSpan, sourceSpan, [], result, sourceSpan);
  585. parsed = result[0].handler;
  586. }
  587. else {
  588. const result = [];
  589. parser.parsePropertyBinding(name, expression.text, true,
  590. /* isTwoWayBinding */ false, sourceSpan, 0, sourceSpan, [], result, sourceSpan);
  591. parsed = result[0].expression;
  592. }
  593. if (parsed != null) {
  594. expressionResult.push(...expressionVisitor.checkTemplateExpression(expression, parsed));
  595. }
  596. }
  597. for (const ref of expressionResult) {
  598. result.references.push({
  599. kind: exports.ReferenceKind.InHostBinding,
  600. from: {
  601. read: ref.read,
  602. readAstPath: ref.readAstPath,
  603. isObjectShorthandExpression: ref.isObjectShorthandExpression,
  604. isWrite: ref.isWrite,
  605. file: project_paths.projectFile(ref.context.getSourceFile(), programInfo),
  606. hostPropertyNode: ref.context,
  607. },
  608. target: ref.targetField,
  609. });
  610. }
  611. }
  612. /**
  613. * Attempts to extract the `TemplateDefinition` for the given
  614. * class, if possible.
  615. *
  616. * The definition can then be used with the Angular compiler to
  617. * load/parse the given template.
  618. */
  619. function attemptExtractTemplateDefinition(node, checker$1, reflector, resourceLoader) {
  620. const classDecorators = reflector.getDecoratorsOfDeclaration(node);
  621. const evaluator = new index.PartialEvaluator(reflector, checker$1, null);
  622. const ngDecorators = classDecorators !== null
  623. ? checker.getAngularDecorators(classDecorators, ['Component'], /* isAngularCore */ false)
  624. : [];
  625. if (ngDecorators.length === 0 ||
  626. ngDecorators[0].args === null ||
  627. ngDecorators[0].args.length === 0 ||
  628. !ts.isObjectLiteralExpression(ngDecorators[0].args[0])) {
  629. return null;
  630. }
  631. const properties = checker.reflectObjectLiteral(ngDecorators[0].args[0]);
  632. const templateProp = properties.get('template');
  633. const templateUrlProp = properties.get('templateUrl');
  634. const containingFile = node.getSourceFile().fileName;
  635. // inline template.
  636. if (templateProp !== undefined) {
  637. const templateStr = evaluator.evaluate(templateProp);
  638. if (typeof templateStr === 'string') {
  639. return {
  640. isInline: true,
  641. expression: templateProp,
  642. interpolationConfig: checker.DEFAULT_INTERPOLATION_CONFIG,
  643. preserveWhitespaces: false,
  644. resolvedTemplateUrl: containingFile,
  645. templateUrl: containingFile,
  646. };
  647. }
  648. }
  649. try {
  650. // external template.
  651. if (templateUrlProp !== undefined) {
  652. const templateUrl = evaluator.evaluate(templateUrlProp);
  653. if (typeof templateUrl === 'string') {
  654. return {
  655. isInline: false,
  656. interpolationConfig: checker.DEFAULT_INTERPOLATION_CONFIG,
  657. preserveWhitespaces: false,
  658. templateUrlExpression: templateUrlProp,
  659. templateUrl,
  660. resolvedTemplateUrl: resourceLoader.resolve(templateUrl, containingFile),
  661. };
  662. }
  663. }
  664. }
  665. catch (e) {
  666. console.error(`Could not parse external template: ${e}`);
  667. }
  668. return null;
  669. }
  670. /**
  671. * Checks whether the given class has an Angular template, and resolves
  672. * all of the references to inputs.
  673. */
  674. function identifyTemplateReferences(programInfo, node, reflector, checker$1, evaluator, templateTypeChecker, resourceLoader, options, result, knownFields, fieldNamesToConsiderForReferenceLookup) {
  675. const template = templateTypeChecker.getTemplate(node, checker.OptimizeFor.WholeProgram) ??
  676. // If there is no template registered in the TCB or compiler, the template may
  677. // be skipped due to an explicit `jit: true` setting. We try to detect this case
  678. // and parse the template manually.
  679. extractTemplateWithoutCompilerAnalysis(node, checker$1, reflector, resourceLoader, evaluator, options);
  680. if (template !== null) {
  681. const visitor = new TemplateReferenceVisitor(checker$1, templateTypeChecker, node, knownFields, fieldNamesToConsiderForReferenceLookup);
  682. template.forEach((node) => node.visit(visitor));
  683. for (const res of visitor.result) {
  684. const templateFilePath = res.context.sourceSpan.start.file.url;
  685. // Templates without an URL are non-mappable artifacts of e.g.
  686. // string concatenated templates. See the `indirect` template
  687. // source mapping concept in the compiler. We skip such references
  688. // as those cannot be migrated, but print an error for now.
  689. if (templateFilePath === '') {
  690. // TODO: Incorporate a TODO potentially.
  691. console.error(`Found reference to field ${res.targetField.key} that cannot be ` +
  692. `migrated because the template cannot be parsed with source map information ` +
  693. `(in file: ${node.getSourceFile().fileName}).`);
  694. continue;
  695. }
  696. result.references.push({
  697. kind: exports.ReferenceKind.InTemplate,
  698. from: {
  699. read: res.read,
  700. readAstPath: res.readAstPath,
  701. node: res.context,
  702. isObjectShorthandExpression: res.isObjectShorthandExpression,
  703. originatingTsFile: project_paths.projectFile(node.getSourceFile(), programInfo),
  704. templateFile: project_paths.projectFile(checker.absoluteFrom(templateFilePath), programInfo),
  705. isLikelyPartOfNarrowing: res.isLikelyNarrowed,
  706. isWrite: res.isWrite,
  707. },
  708. target: res.targetField,
  709. });
  710. }
  711. }
  712. }
  713. /**
  714. * Attempts to extract a `@Component` template from the given class,
  715. * without relying on the `NgCompiler` program analysis.
  716. *
  717. * This is useful for JIT components using `jit: true` which were not
  718. * processed by the Angular compiler, but may still have templates that
  719. * contain references to inputs that we can resolve via the fallback
  720. * reference resolutions (that does not use the type check block).
  721. */
  722. function extractTemplateWithoutCompilerAnalysis(node, checker$1, reflector, resourceLoader, evaluator, options) {
  723. if (node.name === undefined) {
  724. return null;
  725. }
  726. const tmplDef = attemptExtractTemplateDefinition(node, checker$1, reflector, resourceLoader);
  727. if (tmplDef === null) {
  728. return null;
  729. }
  730. return index.extractTemplate(node, tmplDef, evaluator, null, resourceLoader, {
  731. enableBlockSyntax: true,
  732. enableLetSyntax: true,
  733. usePoisonedData: true,
  734. enableI18nLegacyMessageIdFormat: options.enableI18nLegacyMessageIdFormat !== false,
  735. i18nNormalizeLineEndingsInICUs: options.i18nNormalizeLineEndingsInICUs === true,
  736. }, checker.CompilationMode.FULL).nodes;
  737. }
  738. /** Gets the pattern and property name for a given binding element. */
  739. function resolveBindingElement(node) {
  740. const name = node.propertyName ?? node.name;
  741. // If we are discovering a non-analyzable element in the path, abort.
  742. if (!ts.isStringLiteralLike(name) && !ts.isIdentifier(name)) {
  743. return null;
  744. }
  745. return {
  746. pattern: node.parent,
  747. propertyName: name.text,
  748. };
  749. }
  750. /** Gets the declaration node of the given binding element. */
  751. function getBindingElementDeclaration(node) {
  752. while (true) {
  753. if (ts.isBindingElement(node.parent.parent)) {
  754. node = node.parent.parent;
  755. }
  756. else {
  757. return node.parent.parent;
  758. }
  759. }
  760. }
  761. /**
  762. * Expands the given reference to its containing expression, capturing
  763. * the full context.
  764. *
  765. * E.g. `traverseAccess(ref<`bla`>)` may return `this.bla`
  766. * or `traverseAccess(ref<`bla`>)` may return `this.someObj.a.b.c.bla`.
  767. *
  768. * This helper is useful as we will replace the full access with a temporary
  769. * variable for narrowing. Replacing just the identifier is wrong.
  770. */
  771. function traverseAccess(access) {
  772. if (ts.isPropertyAccessExpression(access.parent) && access.parent.name === access) {
  773. return access.parent;
  774. }
  775. else if (ts.isElementAccessExpression(access.parent) &&
  776. access.parent.argumentExpression === access) {
  777. return access.parent;
  778. }
  779. return access;
  780. }
  781. /**
  782. * Unwraps the parent of the given node, if it's a
  783. * parenthesized expression or `as` expression.
  784. */
  785. function unwrapParent(node) {
  786. if (ts.isParenthesizedExpression(node.parent)) {
  787. return unwrapParent(node.parent);
  788. }
  789. else if (ts.isAsExpression(node.parent)) {
  790. return unwrapParent(node.parent);
  791. }
  792. return node;
  793. }
  794. /**
  795. * List of binary operators that indicate a write operation.
  796. *
  797. * Useful for figuring out whether an expression assigns to
  798. * something or not.
  799. */
  800. const writeBinaryOperators = [
  801. ts.SyntaxKind.EqualsToken,
  802. ts.SyntaxKind.BarBarEqualsToken,
  803. ts.SyntaxKind.BarEqualsToken,
  804. ts.SyntaxKind.AmpersandEqualsToken,
  805. ts.SyntaxKind.AmpersandAmpersandEqualsToken,
  806. ts.SyntaxKind.SlashEqualsToken,
  807. ts.SyntaxKind.MinusEqualsToken,
  808. ts.SyntaxKind.PlusEqualsToken,
  809. ts.SyntaxKind.CaretEqualsToken,
  810. ts.SyntaxKind.PercentEqualsToken,
  811. ts.SyntaxKind.AsteriskEqualsToken,
  812. ts.SyntaxKind.ExclamationEqualsToken,
  813. ];
  814. /**
  815. * Checks whether given TypeScript reference refers to an Angular input, and captures
  816. * the reference if possible.
  817. *
  818. * @param fieldNamesToConsiderForReferenceLookup List of field names that should be
  819. * respected when expensively looking up references to known fields.
  820. * May be null if all identifiers should be inspected.
  821. */
  822. function identifyPotentialTypeScriptReference(node, programInfo, checker, knownFields, result, fieldNamesToConsiderForReferenceLookup, advisors) {
  823. // Skip all identifiers that never can point to a migrated field.
  824. // TODO: Capture these assumptions and performance optimizations in the design doc.
  825. if (fieldNamesToConsiderForReferenceLookup !== null &&
  826. !fieldNamesToConsiderForReferenceLookup.has(node.text)) {
  827. return;
  828. }
  829. let target = undefined;
  830. try {
  831. // Resolve binding elements to their declaration symbol.
  832. // Commonly inputs are accessed via object expansion. e.g. `const {input} = this;`.
  833. if (ts.isBindingElement(node.parent)) {
  834. // Skip binding elements that are using spread.
  835. if (node.parent.dotDotDotToken !== undefined) {
  836. return;
  837. }
  838. const bindingInfo = resolveBindingElement(node.parent);
  839. if (bindingInfo === null) {
  840. // The declaration could not be resolved. Skip analyzing this.
  841. return;
  842. }
  843. const bindingType = checker.getTypeAtLocation(bindingInfo.pattern);
  844. const resolved = lookupPropertyAccess(checker, bindingType, [bindingInfo.propertyName]);
  845. target = resolved?.symbol;
  846. }
  847. else {
  848. target = checker.getSymbolAtLocation(node);
  849. }
  850. }
  851. catch (e) {
  852. console.error('Unexpected error while trying to resolve identifier reference:');
  853. console.error(e);
  854. // Gracefully skip analyzing. This can happen when e.g. a reference is named similar
  855. // to an input, but is dependant on `.d.ts` that is not necessarily available (clutz dts).
  856. return;
  857. }
  858. noTargetSymbolCheck: if (target === undefined) {
  859. if (ts.isPropertyAccessExpression(node.parent) && node.parent.name === node) {
  860. const propAccessSymbol = checker.getSymbolAtLocation(node.parent.expression);
  861. if (propAccessSymbol !== undefined &&
  862. propAccessSymbol.valueDeclaration !== undefined &&
  863. ts.isVariableDeclaration(propAccessSymbol.valueDeclaration) &&
  864. propAccessSymbol.valueDeclaration.initializer !== undefined) {
  865. target = advisors.debugElComponentInstanceTracker
  866. .detect(propAccessSymbol.valueDeclaration.initializer)
  867. ?.getProperty(node.text);
  868. // We found a target in the fallback path. Break out.
  869. if (target !== undefined) {
  870. break noTargetSymbolCheck;
  871. }
  872. }
  873. }
  874. return;
  875. }
  876. let targetInput = knownFields.attemptRetrieveDescriptorFromSymbol(target);
  877. if (targetInput === null) {
  878. return;
  879. }
  880. const access = unwrapParent(traverseAccess(node));
  881. const accessParent = access.parent;
  882. const isWriteReference = ts.isBinaryExpression(accessParent) &&
  883. accessParent.left === access &&
  884. writeBinaryOperators.includes(accessParent.operatorToken.kind);
  885. // track accesses from source files to known fields.
  886. result.references.push({
  887. kind: exports.ReferenceKind.TsReference,
  888. from: {
  889. node,
  890. file: project_paths.projectFile(node.getSourceFile(), programInfo),
  891. isWrite: isWriteReference,
  892. isPartOfElementBinding: ts.isBindingElement(node.parent),
  893. },
  894. target: targetInput,
  895. });
  896. }
  897. /**
  898. * Phase where we iterate through all source file references and
  899. * detect references to known fields (e.g. commonly inputs).
  900. *
  901. * This is useful, for example in the signal input migration whe
  902. * references need to be migrated to unwrap signals, given that
  903. * their target properties is no longer holding a raw value, but
  904. * instead an `InputSignal`.
  905. *
  906. * This phase detects references in all types of locations:
  907. * - TS source files
  908. * - Angular templates (inline or external)
  909. * - Host binding expressions.
  910. */
  911. function createFindAllSourceFileReferencesVisitor(programInfo, checker, reflector, resourceLoader, evaluator, templateTypeChecker, knownFields, fieldNamesToConsiderForReferenceLookup, result) {
  912. const debugElComponentInstanceTracker = new DebugElementComponentInstance(checker);
  913. const partialDirectiveCatalystTracker = new PartialDirectiveTypeInCatalystTests(checker, knownFields);
  914. const perfCounters = {
  915. template: 0,
  916. hostBindings: 0,
  917. tsReferences: 0,
  918. tsTypes: 0,
  919. };
  920. // Schematic NodeJS execution may not have `global.performance` defined.
  921. const currentTimeInMs = () => typeof global.performance !== 'undefined' ? global.performance.now() : Date.now();
  922. const visitor = (node) => {
  923. let lastTime = currentTimeInMs();
  924. // Note: If there is no template type checker and resource loader, we aren't processing
  925. // an Angular program, and can skip template detection.
  926. if (ts.isClassDeclaration(node) && templateTypeChecker !== null && resourceLoader !== null) {
  927. identifyTemplateReferences(programInfo, node, reflector, checker, evaluator, templateTypeChecker, resourceLoader, programInfo.userOptions, result, knownFields, fieldNamesToConsiderForReferenceLookup);
  928. perfCounters.template += (currentTimeInMs() - lastTime) / 1000;
  929. lastTime = currentTimeInMs();
  930. identifyHostBindingReferences(node, programInfo, checker, reflector, result, knownFields, fieldNamesToConsiderForReferenceLookup);
  931. perfCounters.hostBindings += (currentTimeInMs() - lastTime) / 1000;
  932. lastTime = currentTimeInMs();
  933. }
  934. lastTime = currentTimeInMs();
  935. // find references, but do not capture input declarations itself.
  936. if (ts.isIdentifier(node) &&
  937. !(isInputContainerNode(node.parent) && node.parent.name === node)) {
  938. identifyPotentialTypeScriptReference(node, programInfo, checker, knownFields, result, fieldNamesToConsiderForReferenceLookup, {
  939. debugElComponentInstanceTracker,
  940. });
  941. }
  942. perfCounters.tsReferences += (currentTimeInMs() - lastTime) / 1000;
  943. lastTime = currentTimeInMs();
  944. // Detect `Partial<T>` references.
  945. // Those are relevant to be tracked as they may be updated in Catalyst to
  946. // unwrap signal inputs. Commonly people use `Partial` in Catalyst to type
  947. // some "component initial values".
  948. const partialDirectiveInCatalyst = partialDirectiveCatalystTracker.detect(node);
  949. if (partialDirectiveInCatalyst !== null) {
  950. result.references.push({
  951. kind: exports.ReferenceKind.TsClassTypeReference,
  952. from: {
  953. file: project_paths.projectFile(partialDirectiveInCatalyst.referenceNode.getSourceFile(), programInfo),
  954. node: partialDirectiveInCatalyst.referenceNode,
  955. },
  956. isPartialReference: true,
  957. isPartOfCatalystFile: true,
  958. target: partialDirectiveInCatalyst.targetClass,
  959. });
  960. }
  961. perfCounters.tsTypes += (currentTimeInMs() - lastTime) / 1000;
  962. };
  963. return {
  964. visitor,
  965. debugPrintMetrics: () => {
  966. console.info('Source file analysis performance', perfCounters);
  967. },
  968. };
  969. }
  970. exports.createFindAllSourceFileReferencesVisitor = createFindAllSourceFileReferencesVisitor;
  971. exports.getBindingElementDeclaration = getBindingElementDeclaration;
  972. exports.getMemberName = getMemberName;
  973. exports.isHostBindingReference = isHostBindingReference;
  974. exports.isInputContainerNode = isInputContainerNode;
  975. exports.isTemplateReference = isTemplateReference;
  976. exports.isTsClassTypeReference = isTsClassTypeReference;
  977. exports.isTsReference = isTsReference;
  978. exports.traverseAccess = traverseAccess;
  979. exports.unwrapParent = unwrapParent;