/
local.ts
521 lines (470 loc) Β· 21.3 KB
/
local.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
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
/**
* @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 {ExternalExpr, SchemaMetadata} from '@angular/compiler';
import * as ts from 'typescript';
import {ErrorCode, makeDiagnostic} from '../../diagnostics';
import {AliasingHost, Reexport, Reference, ReferenceEmitter} from '../../imports';
import {DirectiveMeta, MetadataReader, MetadataRegistry, NgModuleMeta, PipeMeta} from '../../metadata';
import {ClassDeclaration} from '../../reflection';
import {identifierOfNode, nodeNameForError} from '../../util/src/typescript';
import {ExportScope, ScopeData} from './api';
import {ComponentScopeReader, ComponentScopeRegistry, NoopComponentScopeRegistry} from './component_scope';
import {DtsModuleScopeResolver} from './dependency';
export interface LocalNgModuleData {
declarations: Reference<ClassDeclaration>[];
imports: Reference<ClassDeclaration>[];
exports: Reference<ClassDeclaration>[];
}
export interface LocalModuleScope extends ExportScope {
compilation: ScopeData;
reexports: Reexport[]|null;
schemas: SchemaMetadata[];
}
/**
* Information about the compilation scope of a registered declaration.
*/
export interface CompilationScope extends ScopeData {
/** The declaration whose compilation scope is described here. */
declaration: ClassDeclaration;
/** The declaration of the NgModule that declares this `declaration`. */
ngModule: ClassDeclaration;
}
/**
* A registry which collects information about NgModules, Directives, Components, and Pipes which
* are local (declared in the ts.Program being compiled), and can produce `LocalModuleScope`s
* which summarize the compilation scope of a component.
*
* This class implements the logic of NgModule declarations, imports, and exports and can produce,
* for a given component, the set of directives and pipes which are "visible" in that component's
* template.
*
* The `LocalModuleScopeRegistry` has two "modes" of operation. During analysis, data for each
* individual NgModule, Directive, Component, and Pipe is added to the registry. No attempt is made
* to traverse or validate the NgModule graph (imports, exports, etc). After analysis, one of
* `getScopeOfModule` or `getScopeForComponent` can be called, which traverses the NgModule graph
* and applies the NgModule logic to generate a `LocalModuleScope`, the full scope for the given
* module or component.
*
* The `LocalModuleScopeRegistry` is also capable of producing `ts.Diagnostic` errors when Angular
* semantics are violated.
*/
export class LocalModuleScopeRegistry implements MetadataRegistry, ComponentScopeReader {
/**
* Tracks whether the registry has been asked to produce scopes for a module or component. Once
* this is true, the registry cannot accept registrations of new directives/pipes/modules as it
* would invalidate the cached scope data.
*/
private sealed = false;
/**
* A map of components from the current compilation unit to the NgModule which declared them.
*
* As components and directives are not distinguished at the NgModule level, this map may also
* contain directives. This doesn't cause any problems but isn't useful as there is no concept of
* a directive's compilation scope.
*/
private declarationToModule = new Map<ClassDeclaration, ClassDeclaration>();
private moduleToRef = new Map<ClassDeclaration, Reference<ClassDeclaration>>();
/**
* A cache of calculated `LocalModuleScope`s for each NgModule declared in the current program.
*
* A value of `undefined` indicates the scope was invalid and produced errors (therefore,
* diagnostics should exist in the `scopeErrors` map).
*/
private cache = new Map<ClassDeclaration, LocalModuleScope|undefined|null>();
/**
* Tracks whether a given component requires "remote scoping".
*
* Remote scoping is when the set of directives which apply to a given component is set in the
* NgModule's file instead of directly on the component def (which is sometimes needed to get
* around cyclic import issues). This is not used in calculation of `LocalModuleScope`s, but is
* tracked here for convenience.
*/
private remoteScoping = new Set<ClassDeclaration>();
/**
* Tracks errors accumulated in the processing of scopes for each module declaration.
*/
private scopeErrors = new Map<ClassDeclaration, ts.Diagnostic[]>();
constructor(
private localReader: MetadataReader, private dependencyScopeReader: DtsModuleScopeResolver,
private refEmitter: ReferenceEmitter, private aliasingHost: AliasingHost|null,
private componentScopeRegistry: ComponentScopeRegistry = new NoopComponentScopeRegistry()) {}
/**
* Add an NgModule's data to the registry.
*/
registerNgModuleMetadata(data: NgModuleMeta): void {
this.assertCollecting();
this.moduleToRef.set(data.ref.node, data.ref);
for (const decl of data.declarations) {
this.declarationToModule.set(decl.node, data.ref.node);
}
}
registerDirectiveMetadata(directive: DirectiveMeta): void {}
registerPipeMetadata(pipe: PipeMeta): void {}
getScopeForComponent(clazz: ClassDeclaration): LocalModuleScope|null {
const scope = !this.declarationToModule.has(clazz) ?
null :
this.getScopeOfModule(this.declarationToModule.get(clazz) !);
if (scope !== null) {
this.componentScopeRegistry.registerComponentScope(clazz, scope);
}
return scope;
}
/**
* Collects registered data for a module and its directives/pipes and convert it into a full
* `LocalModuleScope`.
*
* This method implements the logic of NgModule imports and exports. It returns the
* `LocalModuleScope` for the given NgModule if one can be produced, and `null` if no scope is
* available or the scope contains errors.
*/
getScopeOfModule(clazz: ClassDeclaration): LocalModuleScope|null {
const scope = this.moduleToRef.has(clazz) ?
this.getScopeOfModuleReference(this.moduleToRef.get(clazz) !) :
null;
// Translate undefined -> null.
return scope !== undefined ? scope : null;
}
/**
* Retrieves any `ts.Diagnostic`s produced during the calculation of the `LocalModuleScope` for
* the given NgModule, or `null` if no errors were present.
*/
getDiagnosticsOfModule(clazz: ClassDeclaration): ts.Diagnostic[]|null {
// Required to ensure the errors are populated for the given class. If it has been processed
// before, this will be a no-op due to the scope cache.
this.getScopeOfModule(clazz);
if (this.scopeErrors.has(clazz)) {
return this.scopeErrors.get(clazz) !;
} else {
return null;
}
}
/**
* Returns a collection of the compilation scope for each registered declaration.
*/
getCompilationScopes(): CompilationScope[] {
const scopes: CompilationScope[] = [];
this.declarationToModule.forEach((ngModule, declaration) => {
const scope = this.getScopeOfModule(ngModule);
if (scope !== null) {
scopes.push({declaration, ngModule, ...scope.compilation});
}
});
return scopes;
}
/**
* Implementation of `getScopeOfModule` which accepts a reference to a class and differentiates
* between:
*
* * no scope being available (returns `null`)
* * a scope being produced with errors (returns `undefined`).
*/
private getScopeOfModuleReference(ref: Reference<ClassDeclaration>): LocalModuleScope|null
|undefined {
if (this.cache.has(ref.node)) {
return this.cache.get(ref.node);
}
// Seal the registry to protect the integrity of the `LocalModuleScope` cache.
this.sealed = true;
// `ref` should be an NgModule previously added to the registry. If not, a scope for it
// cannot be produced.
const ngModule = this.localReader.getNgModuleMetadata(ref);
if (ngModule === null) {
this.cache.set(ref.node, null);
return null;
}
// Errors produced during computation of the scope are recorded here. At the end, if this array
// isn't empty then `undefined` will be cached and returned to indicate this scope is invalid.
const diagnostics: ts.Diagnostic[] = [];
// At this point, the goal is to produce two distinct transitive sets:
// - the directives and pipes which are visible to components declared in the NgModule.
// - the directives and pipes which are exported to any NgModules which import this one.
// Directives and pipes in the compilation scope.
const compilationDirectives = new Map<ts.Declaration, DirectiveMeta>();
const compilationPipes = new Map<ts.Declaration, PipeMeta>();
const declared = new Set<ts.Declaration>();
// Directives and pipes exported to any importing NgModules.
const exportDirectives = new Map<ts.Declaration, DirectiveMeta>();
const exportPipes = new Map<ts.Declaration, PipeMeta>();
// The algorithm is as follows:
// 1) Add directives/pipes declared in the NgModule to the compilation scope.
// 2) Add all of the directives/pipes from each NgModule imported into the current one to the
// compilation scope. At this point, the compilation scope is complete.
// 3) For each entry in the NgModule's exports:
// a) Attempt to resolve it as an NgModule with its own exported directives/pipes. If it is
// one, add them to the export scope of this NgModule.
// b) Otherwise, it should be a class in the compilation scope of this NgModule. If it is,
// add it to the export scope.
// c) If it's neither an NgModule nor a directive/pipe in the compilation scope, then this
// is an error.
// 1) add declarations.
for (const decl of ngModule.declarations) {
const directive = this.localReader.getDirectiveMetadata(decl);
const pipe = this.localReader.getPipeMetadata(decl);
if (directive !== null) {
compilationDirectives.set(decl.node, {...directive, ref: decl});
} else if (pipe !== null) {
compilationPipes.set(decl.node, {...pipe, ref: decl});
} else {
// TODO(alxhub): produce a ts.Diagnostic. This can't be an error right now since some
// ngtools tests rely on analysis of broken components.
continue;
}
declared.add(decl.node);
}
// 2) process imports.
for (const decl of ngModule.imports) {
const importScope = this.getExportedScope(decl, diagnostics, ref.node, 'import');
if (importScope === null) {
// An import wasn't an NgModule, so record an error.
diagnostics.push(invalidRef(ref.node, decl, 'import'));
continue;
} else if (importScope === undefined) {
// An import was an NgModule but contained errors of its own. Record this as an error too,
// because this scope is always going to be incorrect if one of its imports could not be
// read.
diagnostics.push(invalidTransitiveNgModuleRef(ref.node, decl, 'import'));
continue;
}
for (const directive of importScope.exported.directives) {
compilationDirectives.set(directive.ref.node, directive);
}
for (const pipe of importScope.exported.pipes) {
compilationPipes.set(pipe.ref.node, pipe);
}
}
// 3) process exports.
// Exports can contain modules, components, or directives. They're processed differently.
// Modules are straightforward. Directives and pipes from exported modules are added to the
// export maps. Directives/pipes are different - they might be exports of declared types or
// imported types.
for (const decl of ngModule.exports) {
// Attempt to resolve decl as an NgModule.
const importScope = this.getExportedScope(decl, diagnostics, ref.node, 'export');
if (importScope === undefined) {
// An export was an NgModule but contained errors of its own. Record this as an error too,
// because this scope is always going to be incorrect if one of its exports could not be
// read.
diagnostics.push(invalidTransitiveNgModuleRef(ref.node, decl, 'export'));
continue;
} else if (importScope !== null) {
// decl is an NgModule.
for (const directive of importScope.exported.directives) {
exportDirectives.set(directive.ref.node, directive);
}
for (const pipe of importScope.exported.pipes) {
exportPipes.set(pipe.ref.node, pipe);
}
} else if (compilationDirectives.has(decl.node)) {
// decl is a directive or component in the compilation scope of this NgModule.
const directive = compilationDirectives.get(decl.node) !;
exportDirectives.set(decl.node, directive);
} else if (compilationPipes.has(decl.node)) {
// decl is a pipe in the compilation scope of this NgModule.
const pipe = compilationPipes.get(decl.node) !;
exportPipes.set(decl.node, pipe);
} else {
// decl is an unknown export.
if (this.localReader.getDirectiveMetadata(decl) !== null ||
this.localReader.getPipeMetadata(decl) !== null) {
diagnostics.push(invalidReexport(ref.node, decl));
} else {
diagnostics.push(invalidRef(ref.node, decl, 'export'));
}
continue;
}
}
const exported = {
directives: Array.from(exportDirectives.values()),
pipes: Array.from(exportPipes.values()),
};
const reexports = this.getReexports(ngModule, ref, declared, exported, diagnostics);
// Check if this scope had any errors during production.
if (diagnostics.length > 0) {
// Cache undefined, to mark the fact that the scope is invalid.
this.cache.set(ref.node, undefined);
// Save the errors for retrieval.
this.scopeErrors.set(ref.node, diagnostics);
// Return undefined to indicate the scope is invalid.
this.cache.set(ref.node, undefined);
return undefined;
}
// Finally, produce the `LocalModuleScope` with both the compilation and export scopes.
const scope = {
compilation: {
directives: Array.from(compilationDirectives.values()),
pipes: Array.from(compilationPipes.values()),
},
exported,
reexports,
schemas: ngModule.schemas,
};
this.cache.set(ref.node, scope);
return scope;
}
/**
* Check whether a component requires remote scoping.
*/
getRequiresRemoteScope(node: ClassDeclaration): boolean { return this.remoteScoping.has(node); }
/**
* Set a component as requiring remote scoping.
*/
setComponentAsRequiringRemoteScoping(node: ClassDeclaration): void {
this.remoteScoping.add(node);
this.componentScopeRegistry.setComponentAsRequiringRemoteScoping(node);
}
/**
* Look up the `ExportScope` of a given `Reference` to an NgModule.
*
* The NgModule in question may be declared locally in the current ts.Program, or it may be
* declared in a .d.ts file.
*
* @returns `null` if no scope could be found, or `undefined` if an invalid scope
* was found.
*
* May also contribute diagnostics of its own by adding to the given `diagnostics`
* array parameter.
*/
private getExportedScope(
ref: Reference<ClassDeclaration>, diagnostics: ts.Diagnostic[],
ownerForErrors: ts.Declaration, type: 'import'|'export'): ExportScope|null|undefined {
if (ref.node.getSourceFile().isDeclarationFile) {
// The NgModule is declared in a .d.ts file. Resolve it with the `DependencyScopeReader`.
if (!ts.isClassDeclaration(ref.node)) {
// The NgModule is in a .d.ts file but is not declared as a ts.ClassDeclaration. This is an
// error in the .d.ts metadata.
const code = type === 'import' ? ErrorCode.NGMODULE_INVALID_IMPORT :
ErrorCode.NGMODULE_INVALID_EXPORT;
diagnostics.push(makeDiagnostic(
code, identifierOfNode(ref.node) || ref.node,
`Appears in the NgModule.${type}s of ${nodeNameForError(ownerForErrors)}, but could not be resolved to an NgModule`));
return undefined;
}
return this.dependencyScopeReader.resolve(ref);
} else {
// The NgModule is declared locally in the current program. Resolve it from the registry.
return this.getScopeOfModuleReference(ref);
}
}
private getReexports(
ngModule: NgModuleMeta, ref: Reference<ClassDeclaration>, declared: Set<ts.Declaration>,
exported: {directives: DirectiveMeta[], pipes: PipeMeta[]},
diagnostics: ts.Diagnostic[]): Reexport[]|null {
let reexports: Reexport[]|null = null;
const sourceFile = ref.node.getSourceFile();
if (this.aliasingHost === null) {
return null;
}
reexports = [];
// Track re-exports by symbol name, to produce diagnostics if two alias re-exports would share
// the same name.
const reexportMap = new Map<string, Reference<ClassDeclaration>>();
// Alias ngModuleRef added for readability below.
const ngModuleRef = ref;
const addReexport = (exportRef: Reference<ClassDeclaration>) => {
if (exportRef.node.getSourceFile() === sourceFile) {
return;
}
const isReExport = !declared.has(exportRef.node);
const exportName = this.aliasingHost !.maybeAliasSymbolAs(
exportRef, sourceFile, ngModule.ref.node.name.text, isReExport);
if (exportName === null) {
return;
}
if (!reexportMap.has(exportName)) {
if (exportRef.alias && exportRef.alias instanceof ExternalExpr) {
reexports !.push({
fromModule: exportRef.alias.value.moduleName !,
symbolName: exportRef.alias.value.name !,
asAlias: exportName,
});
} else {
const expr = this.refEmitter.emit(exportRef.cloneWithNoIdentifiers(), sourceFile);
if (!(expr instanceof ExternalExpr) || expr.value.moduleName === null ||
expr.value.name === null) {
throw new Error('Expected ExternalExpr');
}
reexports !.push({
fromModule: expr.value.moduleName,
symbolName: expr.value.name,
asAlias: exportName,
});
}
reexportMap.set(exportName, exportRef);
} else {
// Another re-export already used this name. Produce a diagnostic.
const prevRef = reexportMap.get(exportName) !;
diagnostics.push(reexportCollision(ngModuleRef.node, prevRef, exportRef));
}
};
for (const {ref} of exported.directives) {
addReexport(ref);
}
for (const {ref} of exported.pipes) {
addReexport(ref);
}
return reexports;
}
private assertCollecting(): void {
if (this.sealed) {
throw new Error(`Assertion: LocalModuleScopeRegistry is not COLLECTING`);
}
}
}
/**
* Produce a `ts.Diagnostic` for an invalid import or export from an NgModule.
*/
function invalidRef(
clazz: ts.Declaration, decl: Reference<ts.Declaration>,
type: 'import' | 'export'): ts.Diagnostic {
const code =
type === 'import' ? ErrorCode.NGMODULE_INVALID_IMPORT : ErrorCode.NGMODULE_INVALID_EXPORT;
const resolveTarget = type === 'import' ? 'NgModule' : 'NgModule, Component, Directive, or Pipe';
return makeDiagnostic(
code, identifierOfNode(decl.node) || decl.node,
`Appears in the NgModule.${type}s of ${nodeNameForError(clazz)}, but could not be resolved to an ${resolveTarget} class`);
}
/**
* Produce a `ts.Diagnostic` for an import or export which itself has errors.
*/
function invalidTransitiveNgModuleRef(
clazz: ts.Declaration, decl: Reference<ts.Declaration>,
type: 'import' | 'export'): ts.Diagnostic {
const code =
type === 'import' ? ErrorCode.NGMODULE_INVALID_IMPORT : ErrorCode.NGMODULE_INVALID_EXPORT;
return makeDiagnostic(
code, identifierOfNode(decl.node) || decl.node,
`Appears in the NgModule.${type}s of ${nodeNameForError(clazz)}, but itself has errors`);
}
/**
* Produce a `ts.Diagnostic` for an exported directive or pipe which was not declared or imported
* by the NgModule in question.
*/
function invalidReexport(clazz: ts.Declaration, decl: Reference<ts.Declaration>): ts.Diagnostic {
return makeDiagnostic(
ErrorCode.NGMODULE_INVALID_REEXPORT, identifierOfNode(decl.node) || decl.node,
`Present in the NgModule.exports of ${nodeNameForError(clazz)} but neither declared nor imported`);
}
/**
* Produce a `ts.Diagnostic` for a collision in re-export names between two directives/pipes.
*/
function reexportCollision(
module: ClassDeclaration, refA: Reference<ClassDeclaration>,
refB: Reference<ClassDeclaration>): ts.Diagnostic {
const childMessageText =
`This directive/pipe is part of the exports of '${module.name.text}' and shares the same name as another exported directive/pipe.`;
return makeDiagnostic(
ErrorCode.NGMODULE_REEXPORT_NAME_COLLISION, module.name, `
There was a name collision between two classes named '${refA.node.name.text}', which are both part of the exports of '${module.name.text}'.
Angular generates re-exports of an NgModule's exported directives/pipes from the module's source file in certain cases, using the declared name of the class. If two classes of the same name are exported, this automatic naming does not work.
To fix this problem please re-export one or both classes directly from this file.
`.trim(),
[
{node: refA.node.name, messageText: childMessageText},
{node: refB.node.name, messageText: childMessageText},
]);
}