/
transform.ts
197 lines (167 loc) Β· 7.4 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
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
/**
* @license
* Copyright Google Inc. 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 {forwardRefResolver} from '@angular/compiler-cli/src/ngtsc/annotations/src/util';
import {Reference} from '@angular/compiler-cli/src/ngtsc/imports';
import {DynamicValue, PartialEvaluator, ResolvedValue} from '@angular/compiler-cli/src/ngtsc/partial_evaluator';
import {TypeScriptReflectionHost} from '@angular/compiler-cli/src/ngtsc/reflection';
import * as ts from 'typescript';
import {getAngularDecorators} from '../../utils/ng_decorators';
import {ResolvedDirective, ResolvedNgModule} from './definition_collector';
import {ImportManager} from './import_manager';
import {UpdateRecorder} from './update_recorder';
/** Name of decorators which imply that a given class does not need to be migrated. */
const NO_MIGRATE_DECORATORS = ['Injectable', 'Directive', 'Component', 'Pipe'];
export interface AnalysisFailure {
node: ts.Node;
message: string;
}
export class MissingInjectableTransform {
private printer = ts.createPrinter();
private importManager = new ImportManager(this.getUpdateRecorder, this.printer);
private partialEvaluator: PartialEvaluator;
/** Set of provider class declarations which were already checked or migrated. */
private visitedProviderClasses = new Set<ts.ClassDeclaration>();
constructor(
private typeChecker: ts.TypeChecker,
private getUpdateRecorder: (sf: ts.SourceFile) => UpdateRecorder) {
this.partialEvaluator =
new PartialEvaluator(new TypeScriptReflectionHost(typeChecker), typeChecker);
}
recordChanges() { this.importManager.recordChanges(); }
/**
* Migrates all specified NgModule's by walking through referenced providers
* and decorating them with "@Injectable" if needed.
*/
migrateModules(modules: ResolvedNgModule[]): AnalysisFailure[] {
return modules.reduce(
(failures, node) => failures.concat(this.migrateModule(node)), [] as AnalysisFailure[]);
}
/**
* Migrates all specified directives by walking through referenced providers
* and decorating them with "@Injectable" if needed.
*/
migrateDirectives(directives: ResolvedDirective[]): AnalysisFailure[] {
return directives.reduce(
(failures, node) => failures.concat(this.migrateDirective(node)), [] as AnalysisFailure[]);
}
/** Migrates a given NgModule by walking through the referenced providers. */
migrateModule(module: ResolvedNgModule): AnalysisFailure[] {
if (module.providersExpr === null) {
return [];
}
const evaluatedExpr = this._evaluateExpression(module.providersExpr);
if (!Array.isArray(evaluatedExpr)) {
return [{
node: module.providersExpr,
message: 'Providers of module are not statically analyzable.'
}];
}
return this._visitProviderResolvedValue(evaluatedExpr, module);
}
/**
* Migrates a given directive by walking through defined providers. This method
* also handles components with "viewProviders" defined.
*/
migrateDirective(directive: ResolvedDirective): AnalysisFailure[] {
const failures: AnalysisFailure[] = [];
// Migrate "providers" on directives and components if defined.
if (directive.providersExpr) {
const evaluatedExpr = this._evaluateExpression(directive.providersExpr);
if (!Array.isArray(evaluatedExpr)) {
return [
{node: directive.providersExpr, message: `Providers are not statically analyzable.`}
];
}
failures.push(...this._visitProviderResolvedValue(evaluatedExpr, directive));
}
// Migrate "viewProviders" on components if defined.
if (directive.viewProvidersExpr) {
const evaluatedExpr = this._evaluateExpression(directive.viewProvidersExpr);
if (!Array.isArray(evaluatedExpr)) {
return [
{node: directive.viewProvidersExpr, message: `Providers are not statically analyzable.`}
];
}
failures.push(...this._visitProviderResolvedValue(evaluatedExpr, directive));
}
return failures;
}
/**
* Migrates a given provider class if it is not decorated with
* any Angular decorator.
*/
migrateProviderClass(node: ts.ClassDeclaration, context: ResolvedNgModule|ResolvedDirective) {
if (this.visitedProviderClasses.has(node)) {
return;
}
this.visitedProviderClasses.add(node);
const sourceFile = node.getSourceFile();
// We cannot migrate provider classes outside of source files. This is because the
// migration for third-party library files should happen in "ngcc", and in general
// would also involve metadata parsing.
if (sourceFile.isDeclarationFile) {
return;
}
const ngDecorators =
node.decorators ? getAngularDecorators(this.typeChecker, node.decorators) : null;
if (ngDecorators !== null &&
ngDecorators.some(d => NO_MIGRATE_DECORATORS.indexOf(d.name) !== -1)) {
return;
}
const updateRecorder = this.getUpdateRecorder(sourceFile);
const importExpr =
this.importManager.addImportToSourceFile(sourceFile, 'Injectable', '@angular/core');
const newDecoratorExpr = ts.createDecorator(ts.createCall(importExpr, undefined, undefined));
const newDecoratorText =
this.printer.printNode(ts.EmitHint.Unspecified, newDecoratorExpr, sourceFile);
// In case the class is already decorated with "@Inject(..)", we replace the "@Inject"
// decorator with "@Injectable()" since using "@Inject(..)" on a class is a noop and
// most likely was meant to be "@Injectable()".
const existingInjectDecorator =
ngDecorators !== null ? ngDecorators.find(d => d.name === 'Inject') : null;
if (existingInjectDecorator) {
updateRecorder.replaceDecorator(existingInjectDecorator.node, newDecoratorText, context.name);
} else {
updateRecorder.addClassDecorator(node, newDecoratorText, context.name);
}
}
/**
* Evaluates the given TypeScript expression using the partial evaluator with
* the foreign function resolver for handling "forwardRef" calls.
*/
private _evaluateExpression(expr: ts.Expression): ResolvedValue {
return this.partialEvaluator.evaluate(expr, forwardRefResolver);
}
/**
* Visits the given resolved value of a provider. Providers can be nested in
* arrays and we need to recursively walk through the providers to be able to
* migrate all referenced provider classes. e.g. "providers: [[A, [B]]]".
*/
private _visitProviderResolvedValue(value: ResolvedValue, module: ResolvedNgModule):
AnalysisFailure[] {
if (value instanceof Reference && ts.isClassDeclaration(value.node)) {
this.migrateProviderClass(value.node, module);
} else if (value instanceof Map) {
if (!value.has('provide') || value.has('useValue') || value.has('useFactory') ||
value.has('useExisting')) {
return [];
}
if (value.has('useClass')) {
return this._visitProviderResolvedValue(value.get('useClass') !, module);
} else {
return this._visitProviderResolvedValue(value.get('provide') !, module);
}
} else if (Array.isArray(value)) {
return value.reduce((res, v) => res.concat(this._visitProviderResolvedValue(v, module)), [
] as AnalysisFailure[]);
} else if (value instanceof DynamicValue) {
return [{node: value.node, message: `Provider is not statically analyzable.`}];
}
return [];
}
}