|  | 
|  | 1 | +/*! | 
|  | 2 | + * @license | 
|  | 3 | + * Copyright Google LLC All Rights Reserved. | 
|  | 4 | + * | 
|  | 5 | + * Use of this source code is governed by an MIT-style license that can be | 
|  | 6 | + * found in the LICENSE file at https://angular.io/license | 
|  | 7 | + */ | 
|  | 8 | + | 
|  | 9 | +import ts from 'typescript'; | 
|  | 10 | +import {isAccessedViaThis} from './analysis'; | 
|  | 11 | + | 
|  | 12 | +/** | 
|  | 13 | + * Finds class property declarations without initializers whose constructor-based initialization | 
|  | 14 | + * can be inlined into the declaration spot after migrating to `inject`. For example: | 
|  | 15 | + * | 
|  | 16 | + * ``` | 
|  | 17 | + * private foo: number; | 
|  | 18 | + * | 
|  | 19 | + * constructor(private service: MyService) { | 
|  | 20 | + * this.foo = this.service.getFoo(); | 
|  | 21 | + * } | 
|  | 22 | + * ``` | 
|  | 23 | + * | 
|  | 24 | + * The initializer of `foo` can be inlined, because `service` will be initialized | 
|  | 25 | + * before it after the `inject` migration has finished running. | 
|  | 26 | + * | 
|  | 27 | + * @param node Class declaration that is being migrated. | 
|  | 28 | + * @param constructor Constructor declaration of the class being migrated. | 
|  | 29 | + * @param localTypeChecker Type checker scoped to the current file. | 
|  | 30 | + */ | 
|  | 31 | +export function findUninitializedPropertiesToCombine( | 
|  | 32 | + node: ts.ClassDeclaration, | 
|  | 33 | + constructor: ts.ConstructorDeclaration, | 
|  | 34 | + localTypeChecker: ts.TypeChecker, | 
|  | 35 | +): Map<ts.PropertyDeclaration, ts.Expression> | null { | 
|  | 36 | + let result: Map<ts.PropertyDeclaration, ts.Expression> | null = null; | 
|  | 37 | + | 
|  | 38 | + const membersToDeclarations = new Map<string, ts.PropertyDeclaration>(); | 
|  | 39 | + for (const member of node.members) { | 
|  | 40 | + if ( | 
|  | 41 | + ts.isPropertyDeclaration(member) && | 
|  | 42 | + !member.initializer && | 
|  | 43 | + !ts.isComputedPropertyName(member.name) | 
|  | 44 | + ) { | 
|  | 45 | + membersToDeclarations.set(member.name.text, member); | 
|  | 46 | + } | 
|  | 47 | + } | 
|  | 48 | + | 
|  | 49 | + if (membersToDeclarations.size === 0) { | 
|  | 50 | + return result; | 
|  | 51 | + } | 
|  | 52 | + | 
|  | 53 | + const memberInitializers = getMemberInitializers(constructor); | 
|  | 54 | + if (memberInitializers === null) { | 
|  | 55 | + return result; | 
|  | 56 | + } | 
|  | 57 | + | 
|  | 58 | + for (const [name, initializer] of memberInitializers.entries()) { | 
|  | 59 | + if ( | 
|  | 60 | + membersToDeclarations.has(name) && | 
|  | 61 | + !hasLocalReferences(initializer, constructor, localTypeChecker) | 
|  | 62 | + ) { | 
|  | 63 | + result = result || new Map(); | 
|  | 64 | + result.set(membersToDeclarations.get(name)!, initializer); | 
|  | 65 | + } | 
|  | 66 | + } | 
|  | 67 | + | 
|  | 68 | + return result; | 
|  | 69 | +} | 
|  | 70 | + | 
|  | 71 | +/** | 
|  | 72 | + * Finds the expressions from the constructor that initialize class members, for example: | 
|  | 73 | + * | 
|  | 74 | + * ``` | 
|  | 75 | + * private foo: number; | 
|  | 76 | + * | 
|  | 77 | + * constructor() { | 
|  | 78 | + * this.foo = 123; | 
|  | 79 | + * } | 
|  | 80 | + * ``` | 
|  | 81 | + * | 
|  | 82 | + * @param constructor Constructor declaration being analyzed. | 
|  | 83 | + */ | 
|  | 84 | +function getMemberInitializers(constructor: ts.ConstructorDeclaration) { | 
|  | 85 | + let memberInitializers: Map<string, ts.Expression> | null = null; | 
|  | 86 | + | 
|  | 87 | + if (!constructor.body) { | 
|  | 88 | + return memberInitializers; | 
|  | 89 | + } | 
|  | 90 | + | 
|  | 91 | + // Only look at top-level constructor statements. | 
|  | 92 | + for (const node of constructor.body.statements) { | 
|  | 93 | + // Only look for statements in the form of `this.<name> = <expr>;` or `this[<name>] = <expr>;`. | 
|  | 94 | + if ( | 
|  | 95 | + !ts.isExpressionStatement(node) || | 
|  | 96 | + !ts.isBinaryExpression(node.expression) || | 
|  | 97 | + node.expression.operatorToken.kind !== ts.SyntaxKind.EqualsToken || | 
|  | 98 | + (!ts.isPropertyAccessExpression(node.expression.left) && | 
|  | 99 | + !ts.isElementAccessExpression(node.expression.left)) || | 
|  | 100 | + node.expression.left.expression.kind !== ts.SyntaxKind.ThisKeyword | 
|  | 101 | + ) { | 
|  | 102 | + continue; | 
|  | 103 | + } | 
|  | 104 | + | 
|  | 105 | + let name: string | undefined; | 
|  | 106 | + | 
|  | 107 | + if (ts.isPropertyAccessExpression(node.expression.left)) { | 
|  | 108 | + name = node.expression.left.name.text; | 
|  | 109 | + } else if (ts.isElementAccessExpression(node.expression.left)) { | 
|  | 110 | + name = ts.isStringLiteralLike(node.expression.left.argumentExpression) | 
|  | 111 | + ? node.expression.left.argumentExpression.text | 
|  | 112 | + : undefined; | 
|  | 113 | + } | 
|  | 114 | + | 
|  | 115 | + // If the member is initialized multiple times, take the first one. | 
|  | 116 | + if (name && (!memberInitializers || !memberInitializers.has(name))) { | 
|  | 117 | + memberInitializers = memberInitializers || new Map(); | 
|  | 118 | + memberInitializers.set(name, node.expression.right); | 
|  | 119 | + } | 
|  | 120 | + } | 
|  | 121 | + | 
|  | 122 | + return memberInitializers; | 
|  | 123 | +} | 
|  | 124 | + | 
|  | 125 | +/** | 
|  | 126 | + * Determines if a node has references to local symbols defined in the constructor. | 
|  | 127 | + * @param root Expression to check for local references. | 
|  | 128 | + * @param constructor Constructor within which the expression is used. | 
|  | 129 | + * @param localTypeChecker Type checker scoped to the current file. | 
|  | 130 | + */ | 
|  | 131 | +function hasLocalReferences( | 
|  | 132 | + root: ts.Expression, | 
|  | 133 | + constructor: ts.ConstructorDeclaration, | 
|  | 134 | + localTypeChecker: ts.TypeChecker, | 
|  | 135 | +): boolean { | 
|  | 136 | + const sourceFile = root.getSourceFile(); | 
|  | 137 | + let hasLocalRefs = false; | 
|  | 138 | + | 
|  | 139 | + root.forEachChild(function walk(node) { | 
|  | 140 | + // Stop searching if we know that it has local references. | 
|  | 141 | + if (hasLocalRefs) { | 
|  | 142 | + return; | 
|  | 143 | + } | 
|  | 144 | + | 
|  | 145 | + // Skip identifiers that are accessed via `this` since they're accessing class members | 
|  | 146 | + // that aren't local to the constructor. This is here primarily to catch cases like this | 
|  | 147 | + // where `foo` is defined inside the constructor, but is a class member: | 
|  | 148 | + // ``` | 
|  | 149 | + // constructor(private foo: Foo) { | 
|  | 150 | + // this.bar = this.foo.getFoo(); | 
|  | 151 | + // } | 
|  | 152 | + // ``` | 
|  | 153 | + if (ts.isIdentifier(node) && !isAccessedViaThis(node)) { | 
|  | 154 | + const declarations = localTypeChecker.getSymbolAtLocation(node)?.declarations; | 
|  | 155 | + const isReferencingLocalSymbol = declarations?.some( | 
|  | 156 | + (decl) => | 
|  | 157 | + // The source file check is a bit redundant since the type checker | 
|  | 158 | + // is local to the file, but it's inexpensive and it can prevent | 
|  | 159 | + // bugs in the future if we decide to use a full type checker. | 
|  | 160 | + decl.getSourceFile() === sourceFile && | 
|  | 161 | + decl.getStart() >= constructor.getStart() && | 
|  | 162 | + decl.getEnd() <= constructor.getEnd() && | 
|  | 163 | + !isInsideInlineFunction(decl, constructor), | 
|  | 164 | + ); | 
|  | 165 | + | 
|  | 166 | + if (isReferencingLocalSymbol) { | 
|  | 167 | + hasLocalRefs = true; | 
|  | 168 | + } | 
|  | 169 | + } | 
|  | 170 | + | 
|  | 171 | + if (!hasLocalRefs) { | 
|  | 172 | + node.forEachChild(walk); | 
|  | 173 | + } | 
|  | 174 | + }); | 
|  | 175 | + | 
|  | 176 | + return hasLocalRefs; | 
|  | 177 | +} | 
|  | 178 | + | 
|  | 179 | +/** | 
|  | 180 | + * Determines if a node is defined inside of an inline function. | 
|  | 181 | + * @param startNode Node from which to start checking for inline functions. | 
|  | 182 | + * @param boundary Node at which to stop searching. | 
|  | 183 | + */ | 
|  | 184 | +function isInsideInlineFunction(startNode: ts.Node, boundary: ts.Node): boolean { | 
|  | 185 | + let current = startNode; | 
|  | 186 | + | 
|  | 187 | + while (current) { | 
|  | 188 | + if (current === boundary) { | 
|  | 189 | + return false; | 
|  | 190 | + } | 
|  | 191 | + | 
|  | 192 | + if ( | 
|  | 193 | + ts.isFunctionDeclaration(current) || | 
|  | 194 | + ts.isFunctionExpression(current) || | 
|  | 195 | + ts.isArrowFunction(current) | 
|  | 196 | + ) { | 
|  | 197 | + return true; | 
|  | 198 | + } | 
|  | 199 | + | 
|  | 200 | + current = current.parent; | 
|  | 201 | + } | 
|  | 202 | + | 
|  | 203 | + return false; | 
|  | 204 | +} | 
0 commit comments