/
transform.ts
171 lines (142 loc) Β· 6.58 KB
/
transform.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {UpdateRecorder} from '@angular-devkit/schematics';
import type {ResolvedValue, ResolvedValueMap} from '@angular/compiler-cli/private/migrations';
import ts from 'typescript';
import {ResolvedNgModule} from './collector';
import {createModuleWithProvidersType} from './util';
export interface AnalysisFailure {
node: ts.Node;
message: string;
}
const TODO_COMMENT = 'TODO: The following node requires a generic type for `ModuleWithProviders`';
export class ModuleWithProvidersTransform {
private printer = ts.createPrinter();
private partialEvaluator = new this.compilerCliMigrationsModule.PartialEvaluator(
new this.compilerCliMigrationsModule.TypeScriptReflectionHost(this.typeChecker),
this.typeChecker,
/* dependencyTracker */ null);
constructor(
private typeChecker: ts.TypeChecker,
private getUpdateRecorder: (sf: ts.SourceFile) => UpdateRecorder,
private compilerCliMigrationsModule:
typeof import('@angular/compiler-cli/private/migrations')) {}
/** Migrates a given NgModule by walking through the referenced providers and static methods. */
migrateModule(module: ResolvedNgModule): AnalysisFailure[] {
return module.staticMethodsWithoutType.map(this._migrateStaticNgModuleMethod.bind(this))
.filter(v => v) as AnalysisFailure[];
}
/** Migrates a ModuleWithProviders type definition that has no explicit generic type */
migrateType(type: ts.TypeReferenceNode): AnalysisFailure[] {
const parent = type.parent;
let moduleText: string|undefined;
if ((ts.isFunctionDeclaration(parent) || ts.isMethodDeclaration(parent)) && parent.body) {
const returnStatement = parent.body.statements.find(ts.isReturnStatement);
// No return type found, exit
if (!returnStatement || !returnStatement.expression) {
return [{node: parent, message: `Return type is not statically analyzable.`}];
}
moduleText = this._getNgModuleTypeOfExpression(returnStatement.expression);
} else if (ts.isPropertyDeclaration(parent) || ts.isVariableDeclaration(parent)) {
if (!parent.initializer) {
addTodoToNode(type, TODO_COMMENT);
this._updateNode(type, type);
return [{node: parent, message: `Unable to determine type for declaration.`}];
}
moduleText = this._getNgModuleTypeOfExpression(parent.initializer);
}
if (moduleText) {
this._addGenericToTypeReference(type, moduleText);
return [];
}
return [{node: parent, message: `Type is not statically analyzable.`}];
}
/** Add a given generic to a type reference node */
private _addGenericToTypeReference(node: ts.TypeReferenceNode, typeName: string) {
const newGenericExpr = createModuleWithProvidersType(typeName, node);
this._updateNode(node, newGenericExpr);
}
/**
* Migrates a given static method if its ModuleWithProviders does not provide
* a generic type.
*/
private _updateStaticMethodType(method: ts.MethodDeclaration, typeName: string) {
const newGenericExpr =
createModuleWithProvidersType(typeName, method.type as ts.TypeReferenceNode);
const newMethodDecl = ts.updateMethod(
method, method.decorators, method.modifiers, method.asteriskToken, method.name,
method.questionToken, method.typeParameters, method.parameters, newGenericExpr,
method.body);
this._updateNode(method, newMethodDecl);
}
/** Whether the resolved value map represents a ModuleWithProviders object */
isModuleWithProvidersType(value: ResolvedValueMap): boolean {
const ngModule = value.get('ngModule') !== undefined;
const providers = value.get('providers') !== undefined;
return ngModule && (value.size === 1 || (providers && value.size === 2));
}
/**
* Determine the generic type of a suspected ModuleWithProviders return type and add it
* explicitly
*/
private _migrateStaticNgModuleMethod(node: ts.MethodDeclaration): AnalysisFailure|null {
const returnStatement = node.body &&
node.body.statements.find(n => ts.isReturnStatement(n)) as ts.ReturnStatement | undefined;
// No return type found, exit
if (!returnStatement || !returnStatement.expression) {
return {node: node, message: `Return type is not statically analyzable.`};
}
const moduleText = this._getNgModuleTypeOfExpression(returnStatement.expression);
if (moduleText) {
this._updateStaticMethodType(node, moduleText);
return null;
}
return {node: node, message: `Method type is not statically analyzable.`};
}
/** Evaluate and return the ngModule type from an expression */
private _getNgModuleTypeOfExpression(expr: ts.Expression): string|undefined {
const evaluatedExpr = this.partialEvaluator.evaluate(expr);
return this._getTypeOfResolvedValue(evaluatedExpr);
}
/**
* Visits a given object literal expression to determine the ngModule type. If the expression
* cannot be resolved, add a TODO to alert the user.
*/
private _getTypeOfResolvedValue(value: ResolvedValue): string|undefined {
if (value instanceof Map && this.isModuleWithProvidersType(value)) {
const mapValue = value.get('ngModule')!;
if (mapValue instanceof this.compilerCliMigrationsModule.Reference &&
ts.isClassDeclaration(mapValue.node) && mapValue.node.name) {
return mapValue.node.name.text;
} else if (mapValue instanceof this.compilerCliMigrationsModule.DynamicValue) {
addTodoToNode(mapValue.node, TODO_COMMENT);
this._updateNode(mapValue.node, mapValue.node);
}
}
return undefined;
}
private _updateNode(node: ts.Node, newNode: ts.Node) {
const newText = this.printer.printNode(ts.EmitHint.Unspecified, newNode, node.getSourceFile());
const recorder = this.getUpdateRecorder(node.getSourceFile());
recorder.remove(node.getStart(), node.getWidth());
recorder.insertRight(node.getStart(), newText);
}
}
/**
* Adds a to-do to the given TypeScript node which alerts developers to fix
* potential issues identified by the migration.
*/
function addTodoToNode(node: ts.Node, text: string) {
ts.setSyntheticLeadingComments(node, [{
pos: -1,
end: -1,
hasTrailingNewLine: false,
kind: ts.SyntaxKind.MultiLineCommentTrivia,
text: ` ${text} `
}]);
}