fix(ngcc): consistently delegate to TypeScript host for typing files (#36089)

When ngcc is compiling an entry-point, it uses a `ReflectionHost` that
is specific to its format, e.g. ES2015, ES5, UMD or CommonJS. During the
compilation of that entry-point however, the reflector may be used to
reflect into external libraries using their declaration files.

Up until now this was achieved by letting all `ReflectionHost` classes
consider their parent class for reflector queries, thereby ending up in
the `TypeScriptReflectionHost` that is a common base class for all
reflector hosts. This approach has proven to be prone to bugs, as
failing to call into the base class would cause incompatibilities with
reading from declaration files.

The observation can be made that there's only two distinct kinds of
reflection host queries:
1. the reflector query is about code that is part of the entry-point
   that is being compiled, or
2. the reflector query is for an external library that the entry-point
   depends on, in which case the information is reflected
   from the declaration files.

The `ReflectionHost` that was chosen for the entry-point should serve
only reflector queries for the first case, whereas a regular
`TypeScriptReflectionHost` should be used for the second case. This
avoids the problem where a format-specific `ReflectionHost` fails to
handle the second case correctly, as it isn't even considered for such
reflector queries.

This commit introduces a `ReflectionHost` that delegates to the
`TypeScriptReflectionHost` for AST nodes within declaration files,
otherwise delegating to the format-specific `ReflectionHost`.

Fixes #35078
Resolves FW-1859

PR Close #36089
This commit is contained in:
JoostK
2020-02-04 22:15:06 +01:00
committed by Andrew Kushnir
parent 1bc3893c65
commit 9e70bcb34f
8 changed files with 229 additions and 136 deletions

View File

@ -1607,30 +1607,6 @@ exports.ExternalModule = ExternalModule;
expect(importOfIdent).toEqual({name: 'a', from: './file_a'});
});
it('should find the import of an identifier in a declaration file', () => {
loadTestFiles([
{
name: _('/index.d.ts'),
contents: `
import {MyClass} from './myclass.d.ts';
export declare const a: MyClass;`
},
{
name: _('/myclass.d.ts'),
contents: `export declare class MyClass {}`,
}
]);
const bundle = makeTestBundleProgram(_('/index.d.ts'));
const host = new CommonJsReflectionHost(new MockLogger(), false, bundle);
const variableNode =
getDeclaration(bundle.program, _('/index.d.ts'), 'a', isNamedVariableDeclaration);
const identifier =
((variableNode.type as ts.TypeReferenceNode).typeName as ts.Identifier);
const importOfIdent = host.getImportOfIdentifier(identifier !);
expect(importOfIdent).toEqual({name: 'MyClass', from: './myclass.d.ts'});
});
it('should return null if the identifier was not imported', () => {
loadTestFiles(IMPORTS_FILES);
const bundle = makeTestBundleProgram(_('/index.js'));
@ -1816,39 +1792,6 @@ exports.ExternalModule = ExternalModule;
expect(importOfIdent.viaModule).toBe('lib');
});
it('should return the correct declaration of an identifier imported in a typings file',
() => {
const files = [
{
name: _('/node_modules/test-package/index.d.ts'),
contents: `
import {SubModule} from 'sub_module';
export const x = SubModule;
`,
},
{
name: _('/node_modules/sub_module/index.d.ts'),
contents: 'export class SubModule {}',
}
];
loadTestFiles(files);
const bundle = makeTestBundleProgram(files[0].name);
const host = new CommonJsReflectionHost(new MockLogger(), false, bundle);
const expectedDeclaration = getDeclaration(
bundle.program, files[1].name, 'SubModule', isNamedClassDeclaration);
const x =
getDeclaration(bundle.program, files[0].name, 'x', isNamedVariableDeclaration);
if (x.initializer === undefined || !ts.isIdentifier(x.initializer)) {
return fail('Expected constant `x` to have an identifer as an initializer.');
}
const decl = host.getDeclarationOfIdentifier(x.initializer);
if (decl === null) {
return fail('Expected to find a declaration for ' + x.initializer.getText());
}
expect(decl.viaModule).toEqual('sub_module');
expect(decl.node).toBe(expectedDeclaration);
});
it('should recognize TypeScript helpers (as function declarations)', () => {
const file: TestFile = {
name: _('/test.js'),

View File

@ -1776,29 +1776,6 @@ runInEachFileSystem(() => {
expect(importOfIdent).toEqual({name: 'a', from: './file_a'});
});
it('should find the import of an identifier in a declaration file', () => {
loadTestFiles([
{
name: _('/index.d.ts'),
contents: `
import {MyClass} from './myclass.d.ts';
export declare const a: MyClass;`
},
{
name: _('/myclass.d.ts'),
contents: `export declare class MyClass {}`,
}
]);
const bundle = makeTestBundleProgram(_('/index.d.ts'));
const host = new UmdReflectionHost(new MockLogger(), false, bundle);
const variableNode =
getDeclaration(bundle.program, _('/index.d.ts'), 'a', isNamedVariableDeclaration);
const identifier = ((variableNode.type as ts.TypeReferenceNode).typeName as ts.Identifier);
const importOfIdent = host.getImportOfIdentifier(identifier !);
expect(importOfIdent).toEqual({name: 'MyClass', from: './myclass.d.ts'});
});
it('should return null if the identifier was not imported', () => {
loadTestFiles(IMPORTS_FILES);
const bundle = makeTestBundleProgram(_('/index.js'));
@ -1942,39 +1919,6 @@ runInEachFileSystem(() => {
expect(actualDeclaration !.viaModule).toBe('@angular/core');
});
it('should return the correct declaration of an identifier imported in a typings file',
() => {
const FILES = [
{
name: _('/node_modules/test-package/index.d.ts'),
contents: `
import {SubModule} from 'sub_module';
export const x = SubModule;
`,
},
{
name: _('/node_modules/sub_module/index.d.ts'),
contents: `export class SubModule {}`,
}
];
loadTestFiles(FILES);
const bundle = makeTestBundleProgram(FILES[0].name);
const host = new UmdReflectionHost(new MockLogger(), false, bundle);
const expectedDeclaration =
getDeclaration(bundle.program, FILES[1].name, 'SubModule', isNamedClassDeclaration);
const x = getDeclaration(bundle.program, FILES[0].name, 'x', isNamedVariableDeclaration);
if (x.initializer === undefined || !ts.isIdentifier(x.initializer)) {
return fail('Expected constant `x` to have an identifer as an initializer.');
}
const decl = host.getDeclarationOfIdentifier(x.initializer);
if (decl === null) {
return fail('Expected to find a declaration for ' + x.initializer.getText());
}
expect(decl.viaModule).toEqual('sub_module');
expect(decl.node).toBe(expectedDeclaration);
});
it('should recognize TypeScript helpers (as function declarations)', () => {
const file: TestFile = {
name: _('/test.js'),

View File

@ -249,6 +249,70 @@ runInEachFileSystem(() => {
expect(jsContents).not.toMatch(/\bconst \w+\s*=/);
});
it('should be able to reflect into external libraries', () => {
compileIntoApf('lib', {
'/index.ts': `
export * from './constants';
export * from './module';
`,
'/constants.ts': `
export const selectorA = '[selector-a]';
export class Selectors {
static readonly B = '[selector-b]';
}
`,
'/module.ts': `
import {NgModule, ModuleWithProviders} from '@angular/core';
@NgModule()
export class MyOtherModule {}
export class MyModule {
static forRoot(): ModuleWithProviders<MyOtherModule> {
return {ngModule: MyOtherModule};
}
}
`
});
compileIntoFlatEs5Package('test-package', {
'/index.ts': `
import {Directive, Input, NgModule} from '@angular/core';
import * as lib from 'lib';
@Directive({
selector: lib.selectorA,
})
export class DirectiveA {
}
@Directive({
selector: lib.Selectors.B,
})
export class DirectiveB {
}
@NgModule({
imports: [lib.MyModule.forRoot()],
declarations: [DirectiveA, DirectiveB],
})
export class FooModule {}
`,
});
mainNgcc({
basePath: '/node_modules',
targetEntryPointPath: 'test-package',
propertiesToConsider: ['module'],
});
const jsContents = fs.readFile(_(`/node_modules/test-package/index.js`));
expect(jsContents).toContain('"selector-a"');
expect(jsContents).toContain('"selector-b"');
expect(jsContents).toContain('imports: [ɵngcc1.MyOtherModule]');
});
it('should add ɵfac but not duplicate ɵprov properties on injectables', () => {
compileIntoFlatEs5Package('test-package', {
'/index.ts': `