Previously, when the NgModule scope resolver discovered semantic errors within a users NgModules, it would throw assertion errors. TODOs in the codebase indicated these should become ts.Diagnostics eventually. Besides producing better-looking errors, there is another reason to make this change asap: these assertions were shadowing actual errors, via an interesting mechanism: 1) a component would produce a ts.Diagnostic during its analyze() step 2) as a result, it wouldn't register component metadata with the scope resolver 3) the NgModule for the component references it in exports, which was detected as an invalid export (no metadata registering it as a component). 4) the resulting assertion error would crash the compiler, hiding the real cause of the problem (an invalid component). This commit should mitigate this problem by converting scoping errors to proper ts.Diagnostics. Additionally, we should consider registering some marker indicating a class is a directive/component/pipe without actually requiring full metadata to be produced for it, which would allow suppression of errors like "invalid export" for such invalid types. PR Close #29191
169 lines
6.7 KiB
TypeScript
169 lines
6.7 KiB
TypeScript
/**
|
|
* @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 * as ts from 'typescript';
|
|
|
|
import {Reference, ReferenceEmitter} from '../../imports';
|
|
import {ScopeData, ScopeDirective, ScopePipe} from '../src/api';
|
|
import {DtsModuleScopeResolver} from '../src/dependency';
|
|
import {LocalModuleScopeRegistry} from '../src/local';
|
|
|
|
function registerFakeRefs(registry: LocalModuleScopeRegistry):
|
|
{[name: string]: Reference<ts.ClassDeclaration>} {
|
|
const get = (target: {}, name: string): Reference<ts.ClassDeclaration> => {
|
|
const sf = ts.createSourceFile(
|
|
name + '.ts', `export class ${name} {}`, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
const clazz = sf.statements[0] as ts.ClassDeclaration;
|
|
const ref = new Reference(clazz);
|
|
if (name.startsWith('Dir') || name.startsWith('Cmp')) {
|
|
registry.registerDirective(fakeDirective(ref));
|
|
} else if (name.startsWith('Pipe')) {
|
|
registry.registerPipe(fakePipe(ref));
|
|
}
|
|
return ref;
|
|
};
|
|
return new Proxy({}, {get});
|
|
}
|
|
|
|
describe('LocalModuleScopeRegistry', () => {
|
|
const refEmitter = new ReferenceEmitter([]);
|
|
let registry !: LocalModuleScopeRegistry;
|
|
|
|
beforeEach(() => {
|
|
registry = new LocalModuleScopeRegistry(new MockDtsModuleScopeResolver(), refEmitter, null);
|
|
});
|
|
|
|
it('should produce an accurate LocalModuleScope for a basic NgModule', () => {
|
|
const {Dir1, Dir2, Pipe1, Module} = registerFakeRefs(registry);
|
|
|
|
registry.registerNgModule(Module.node, {
|
|
imports: [],
|
|
declarations: [Dir1, Dir2, Pipe1],
|
|
exports: [Dir1, Pipe1],
|
|
});
|
|
|
|
const scope = registry.getScopeOfModule(Module.node) !;
|
|
expect(scopeToRefs(scope.compilation)).toEqual([Dir1, Dir2, Pipe1]);
|
|
expect(scopeToRefs(scope.exported)).toEqual([Dir1, Pipe1]);
|
|
});
|
|
|
|
it('should produce accurate LocalModuleScopes for a complex module chain', () => {
|
|
const {DirA, DirB, DirCI, DirCE, ModuleA, ModuleB, ModuleC} = registerFakeRefs(registry);
|
|
|
|
registry.registerNgModule(
|
|
ModuleA.node, {imports: [ModuleB], declarations: [DirA], exports: []});
|
|
registry.registerNgModule(
|
|
ModuleB.node, {exports: [ModuleC, DirB], declarations: [DirB], imports: []});
|
|
registry.registerNgModule(
|
|
ModuleC.node, {declarations: [DirCI, DirCE], exports: [DirCE], imports: []});
|
|
|
|
const scopeA = registry.getScopeOfModule(ModuleA.node) !;
|
|
expect(scopeToRefs(scopeA.compilation)).toEqual([DirA, DirB, DirCE]);
|
|
expect(scopeToRefs(scopeA.exported)).toEqual([]);
|
|
});
|
|
|
|
it('should not treat exported modules as imported', () => {
|
|
const {Dir, ModuleA, ModuleB} = registerFakeRefs(registry);
|
|
|
|
registry.registerNgModule(ModuleA.node, {exports: [ModuleB], imports: [], declarations: []});
|
|
registry.registerNgModule(ModuleB.node, {declarations: [Dir], exports: [Dir], imports: []});
|
|
|
|
const scopeA = registry.getScopeOfModule(ModuleA.node) !;
|
|
expect(scopeToRefs(scopeA.compilation)).toEqual([]);
|
|
expect(scopeToRefs(scopeA.exported)).toEqual([Dir]);
|
|
});
|
|
|
|
it('should deduplicate declarations and exports', () => {
|
|
const {DirA, ModuleA, DirB, ModuleB, ModuleC} = registerFakeRefs(registry);
|
|
|
|
registry.registerNgModule(ModuleA.node, {
|
|
declarations: [DirA, DirA],
|
|
imports: [ModuleB, ModuleC],
|
|
exports: [DirA, DirA, DirB, ModuleB],
|
|
});
|
|
registry.registerNgModule(ModuleB.node, {declarations: [DirB], imports: [], exports: [DirB]});
|
|
registry.registerNgModule(ModuleC.node, {declarations: [], imports: [], exports: [ModuleB]});
|
|
|
|
const scope = registry.getScopeOfModule(ModuleA.node) !;
|
|
expect(scopeToRefs(scope.compilation)).toEqual([DirA, DirB]);
|
|
expect(scopeToRefs(scope.exported)).toEqual([DirA, DirB]);
|
|
});
|
|
|
|
it('should preserve reference identities in module metadata', () => {
|
|
const {Dir, Module} = registerFakeRefs(registry);
|
|
const idSf = ts.createSourceFile('id.ts', 'var id;', ts.ScriptTarget.Latest, true);
|
|
|
|
// Create a new Reference to Dir, with a special `ts.Identifier`, and register the directive
|
|
// using it. This emulates what happens when an NgModule declares a Directive.
|
|
const idVar = idSf.statements[0] as ts.VariableStatement;
|
|
const id = idVar.declarationList.declarations[0].name as ts.Identifier;
|
|
const DirInModule = new Reference(Dir.node);
|
|
DirInModule.addIdentifier(id);
|
|
registry.registerNgModule(Module.node, {exports: [], imports: [], declarations: [DirInModule]});
|
|
|
|
const scope = registry.getScopeOfModule(Module.node) !;
|
|
expect(scope.compilation.directives[0].ref.getIdentityIn(idSf)).toBe(id);
|
|
});
|
|
|
|
it('should allow directly exporting a directive that\'s not imported', () => {
|
|
const {Dir, ModuleA, ModuleB} = registerFakeRefs(registry);
|
|
|
|
registry.registerNgModule(ModuleA.node, {exports: [Dir], imports: [ModuleB], declarations: []});
|
|
registry.registerNgModule(ModuleB.node, {declarations: [Dir], exports: [Dir], imports: []});
|
|
|
|
const scopeA = registry.getScopeOfModule(ModuleA.node) !;
|
|
expect(scopeToRefs(scopeA.exported)).toEqual([Dir]);
|
|
});
|
|
|
|
it('should not allow directly exporting a directive that\'s not imported', () => {
|
|
const {Dir, ModuleA, ModuleB} = registerFakeRefs(registry);
|
|
|
|
registry.registerNgModule(ModuleA.node, {exports: [Dir], imports: [], declarations: []});
|
|
registry.registerNgModule(ModuleB.node, {declarations: [Dir], exports: [Dir], imports: []});
|
|
|
|
expect(registry.getScopeOfModule(ModuleA.node)).toBe(null);
|
|
|
|
// ModuleA should have associated diagnostics as it exports `Dir` without declaring it.
|
|
expect(registry.getDiagnosticsOfModule(ModuleA.node)).not.toBeNull();
|
|
|
|
// ModuleB should have no diagnostics as it correctly declares `Dir`.
|
|
expect(registry.getDiagnosticsOfModule(ModuleB.node)).toBeNull();
|
|
});
|
|
});
|
|
|
|
function fakeDirective(ref: Reference<ts.ClassDeclaration>): ScopeDirective {
|
|
const name = ref.debugName !;
|
|
return {
|
|
ref,
|
|
name,
|
|
selector: `[${ref.debugName}]`,
|
|
isComponent: name.startsWith('Cmp'),
|
|
inputs: {},
|
|
outputs: {},
|
|
exportAs: null,
|
|
queries: [],
|
|
hasNgTemplateContextGuard: false,
|
|
ngTemplateGuards: [],
|
|
};
|
|
}
|
|
|
|
function fakePipe(ref: Reference<ts.ClassDeclaration>): ScopePipe {
|
|
const name = ref.debugName !;
|
|
return {ref, name};
|
|
}
|
|
|
|
class MockDtsModuleScopeResolver implements DtsModuleScopeResolver {
|
|
resolve(ref: Reference<ts.ClassDeclaration>): null { return null; }
|
|
}
|
|
|
|
function scopeToRefs(scopeData: ScopeData): Reference<ts.Declaration>[] {
|
|
const directives = scopeData.directives.map(dir => dir.ref);
|
|
const pipes = scopeData.pipes.map(pipe => pipe.ref);
|
|
return [...directives, ...pipes].sort((a, b) => a.debugName !.localeCompare(b.debugName !));
|
|
}
|