-
Notifications
You must be signed in to change notification settings - Fork 24.8k
/
scanner.ts
166 lines (142 loc) Β· 6 KB
/
scanner.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
/**
* @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 {ExpressionType, ExternalExpr, R3Identifiers as Identifiers, Type} from '@angular/compiler';
import * as ts from 'typescript';
import {ImportFlags, Reference, ReferenceEmitter} from '../../imports';
import {PartialEvaluator, ResolvedValueMap} from '../../partial_evaluator';
import {ReflectionHost} from '../../reflection';
export interface DtsHandler {
addTypeReplacement(node: ts.Declaration, type: Type): void;
}
export class ModuleWithProvidersScanner {
constructor(
private host: ReflectionHost, private evaluator: PartialEvaluator,
private emitter: ReferenceEmitter) {}
scan(sf: ts.SourceFile, dts: DtsHandler): void {
for (const stmt of sf.statements) {
this.visitStatement(dts, stmt);
}
}
private visitStatement(dts: DtsHandler, stmt: ts.Statement): void {
// Detect whether a statement is exported, which is used as one of the hints whether to look
// more closely at possible MWP functions within. This is a syntactic check, not a semantic
// check, so it won't detect cases like:
//
// var X = ...;
// export {X}
//
// This is intentional, because the alternative is slow and this will catch 99% of the cases we
// need to handle.
const isExported = stmt.modifiers !== undefined &&
stmt.modifiers.some(mod => mod.kind === ts.SyntaxKind.ExportKeyword);
if (!isExported) {
return;
}
if (ts.isClassDeclaration(stmt)) {
for (const member of stmt.members) {
if (!ts.isMethodDeclaration(member) || !isStatic(member)) {
continue;
}
this.visitFunctionOrMethodDeclaration(dts, member);
}
} else if (ts.isFunctionDeclaration(stmt)) {
this.visitFunctionOrMethodDeclaration(dts, stmt);
}
}
private visitFunctionOrMethodDeclaration(
dts: DtsHandler, decl: ts.MethodDeclaration|ts.FunctionDeclaration): void {
// First, some sanity. This should have a method body with a single return statement.
if (decl.body === undefined || decl.body.statements.length !== 1) {
return;
}
const retStmt = decl.body.statements[0];
if (!ts.isReturnStatement(retStmt) || retStmt.expression === undefined) {
return;
}
const retValue = retStmt.expression;
// Now, look at the return type of the method. Maybe bail if the type is already marked, or if
// it's incompatible with a MWP function.
const returnType = this.returnTypeOf(decl);
if (returnType === ReturnType.OTHER || returnType === ReturnType.MWP_WITH_TYPE) {
// Don't process this declaration, it either already declares the right return type, or an
// incompatible one.
return;
}
const value = this.evaluator.evaluate(retValue);
if (!(value instanceof Map) || !value.has('ngModule')) {
// The return value does not provide sufficient information to be able to add a generic type.
return;
}
if (returnType === ReturnType.INFERRED && !isModuleWithProvidersType(value)) {
// The return type is inferred but the returned object is not of the correct shape, so we
// shouldn's modify the return type to become `ModuleWithProviders`.
return;
}
// The return type has been verified to represent the `ModuleWithProviders` type, but either the
// return type is inferred or the generic type argument is missing. In both cases, a new return
// type is created where the `ngModule` type is included as generic type argument.
const ngModule = value.get('ngModule');
if (!(ngModule instanceof Reference) || !ts.isClassDeclaration(ngModule.node)) {
return;
}
const ngModuleExpr =
this.emitter.emit(ngModule, decl.getSourceFile(), ImportFlags.ForceNewImport);
const ngModuleType = new ExpressionType(ngModuleExpr);
const mwpNgType = new ExpressionType(
new ExternalExpr(Identifiers.ModuleWithProviders), [/* modifiers */], [ngModuleType]);
dts.addTypeReplacement(decl, mwpNgType);
}
private returnTypeOf(decl: ts.FunctionDeclaration|ts.MethodDeclaration|
ts.VariableDeclaration): ReturnType {
if (decl.type === undefined) {
return ReturnType.INFERRED;
} else if (!ts.isTypeReferenceNode(decl.type)) {
return ReturnType.OTHER;
}
// Try to figure out if the type is of a familiar form, something that looks like it was
// imported.
let typeId: ts.Identifier;
if (ts.isIdentifier(decl.type.typeName)) {
// def: ModuleWithProviders
typeId = decl.type.typeName;
} else if (ts.isQualifiedName(decl.type.typeName) && ts.isIdentifier(decl.type.typeName.left)) {
// def: i0.ModuleWithProviders
typeId = decl.type.typeName.right;
} else {
return ReturnType.OTHER;
}
const importDecl = this.host.getImportOfIdentifier(typeId);
if (importDecl === null || importDecl.from !== '@angular/core' ||
importDecl.name !== 'ModuleWithProviders') {
return ReturnType.OTHER;
}
if (decl.type.typeArguments === undefined || decl.type.typeArguments.length === 0) {
// The return type is indeed ModuleWithProviders, but no generic type parameter was found.
return ReturnType.MWP_NO_TYPE;
} else {
// The return type is ModuleWithProviders, and the user has already specified a generic type.
return ReturnType.MWP_WITH_TYPE;
}
}
}
enum ReturnType {
INFERRED,
MWP_NO_TYPE,
MWP_WITH_TYPE,
OTHER,
}
/** Whether the resolved value map represents a ModuleWithProviders object */
function isModuleWithProvidersType(value: ResolvedValueMap): boolean {
const ngModule = value.has('ngModule');
const providers = value.has('providers');
return ngModule && (value.size === 1 || (providers && value.size === 2));
}
function isStatic(node: ts.Node): boolean {
return node.modifiers !== undefined &&
node.modifiers.some(mod => mod.kind === ts.SyntaxKind.StaticKeyword);
}