Skip to content

Commit 05c3750

Browse files
crisbetoAndrewKushnir
authored andcommitted
refactor(migrations): add internal cleanup logic (angular#57315)
Expands the `inject` migration to add some cleanups that are only relevant internally. Externally this isn't exposed to users. PR Close angular#57315
1 parent e20cd4a commit 05c3750

File tree

4 files changed

+676
-16
lines changed

4 files changed

+676
-16
lines changed

packages/core/schematics/ng-generate/inject-migration/analysis.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import ts from 'typescript';
1010
import {getAngularDecorators} from '../../utils/ng_decorators';
1111
import {getNamedImports} from '../../utils/typescript/imports';
12-
import {isReferenceToImport} from '../../utils/typescript/symbol';
1312

1413
/** Names of decorators that enable DI on a class declaration. */
1514
const DECORATORS_SUPPORTING_DI = new Set([
@@ -124,10 +123,12 @@ export function analyzeFile(sourceFile: ts.SourceFile, localTypeChecker: ts.Type
124123
* Returns the parameters of a function that aren't used within its body.
125124
* @param declaration Function in which to search for unused parameters.
126125
* @param localTypeChecker Type checker scoped to the file in which the function was declared.
126+
* @param removedStatements Statements that were already removed from the constructor.
127127
*/
128128
export function getConstructorUnusedParameters(
129129
declaration: ts.ConstructorDeclaration,
130130
localTypeChecker: ts.TypeChecker,
131+
removedStatements: Set<ts.Statement> | null,
131132
): Set<ts.Declaration> {
132133
const accessedTopLevelParameters = new Set<ts.Declaration>();
133134
const topLevelParameters = new Set<ts.Declaration>();
@@ -147,18 +148,19 @@ export function getConstructorUnusedParameters(
147148
}
148149

149150
declaration.body.forEachChild(function walk(node) {
151+
// Don't descend into statements that were removed already.
152+
if (removedStatements && ts.isStatement(node) && removedStatements.has(node)) {
153+
return;
154+
}
155+
150156
if (!ts.isIdentifier(node) || !topLevelParameterNames.has(node.text)) {
151157
node.forEachChild(walk);
152158
return;
153159
}
154160

155161
// Don't consider `this.<name>` accesses as being references to
156162
// parameters since they'll be moved to property declarations.
157-
if (
158-
ts.isPropertyAccessExpression(node.parent) &&
159-
node.parent.expression.kind === ts.SyntaxKind.ThisKeyword &&
160-
node.parent.name === node
161-
) {
163+
if (isAccessedViaThis(node)) {
162164
return;
163165
}
164166

@@ -287,6 +289,15 @@ export function hasGenerics(node: ts.TypeNode): boolean {
287289
return false;
288290
}
289291

292+
/** Checks whether an identifier is accessed through `this`, e.g. `this.<some identifier>`. */
293+
export function isAccessedViaThis(node: ts.Identifier): boolean {
294+
return (
295+
ts.isPropertyAccessExpression(node.parent) &&
296+
node.parent.expression.kind === ts.SyntaxKind.ThisKeyword &&
297+
node.parent.name === node
298+
);
299+
}
300+
290301
/** Finds a `super` call inside of a specific node. */
291302
function findSuperCall(root: ts.Node): ts.CallExpression | null {
292303
let result: ts.CallExpression | null = null;
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
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

Comments
 (0)