George Kalpakas bb6a3632f6 refactor(ivy): correctly type class declarations in ngtsc/ngcc (#29209)
Previously, several `ngtsc` and `ngcc` APIs dealing with class
declaration nodes used inconsistent types. For example, some methods of
the `DecoratorHandler` interface expected a `ts.Declaration` argument,
but actual `DecoratorHandler` implementations specified a stricter
`ts.ClassDeclaration` type.

As a result, the stricter methods would operate under the incorrect
assumption that their arguments were of type `ts.ClassDeclaration`,
while the actual arguments might be of different types (e.g. `ngcc`
would call them with `ts.FunctionDeclaration` or
`ts.VariableDeclaration` arguments, when compiling ES5 code).

Additionally, since we need those class declarations to be referenced in
other parts of the program, `ngtsc`/`ngcc` had to either repeatedly
check for `ts.isIdentifier(node.name)` or assume there was a `name`
identifier and use `node.name!`. While this assumption happens to be
true in the current implementation, working around type-checking is
error-prone (e.g. the assumption might stop being true in the future).

This commit fixes this by introducing a new type to be used for such
class declarations (`ts.Declaration & {name: ts.Identifier}`) and using
it consistently throughput the code.

PR Close #29209
2019-03-21 22:20:23 +00:00

170 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 {ClassDeclaration} from '../../reflection';
import {ScopeData, ScopeDirective, ScopePipe} from '../src/api';
import {DtsModuleScopeResolver} from '../src/dependency';
import {LocalModuleScopeRegistry} from '../src/local';
function registerFakeRefs(registry: LocalModuleScopeRegistry):
{[name: string]: Reference<ClassDeclaration>} {
const get = (target: {}, name: string): Reference<ClassDeclaration> => {
const sf = ts.createSourceFile(
name + '.ts', `export class ${name} {}`, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
const clazz = sf.statements[0] as unknown as 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<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<ClassDeclaration>): ScopePipe {
const name = ref.debugName !;
return {ref, name};
}
class MockDtsModuleScopeResolver implements DtsModuleScopeResolver {
resolve(ref: Reference<ClassDeclaration>): null { return null; }
}
function scopeToRefs(scopeData: ScopeData): Reference<ClassDeclaration>[] {
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 !));
}