feat(ivy): ngcc - add `PrivateDeclarationsAnalyzer` (#26906)

This analyzer searches the source for declared classes that are not
exported publicly from the entry-point.

PR Close #26906
This commit is contained in:
Pete Bacon Darwin 2018-11-21 09:05:27 +00:00 committed by Igor Minar
parent b55e1c2ba9
commit bf3ac41e36
2 changed files with 218 additions and 0 deletions

View File

@ -0,0 +1,60 @@
/**
* @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 {ReferencesRegistry} from '../../../ngtsc/annotations';
import {Declaration} from '../../../ngtsc/host';
import {NgccReflectionHost} from '../host/ngcc_host';
import {hasNameIdentifier, isDefined} from '../utils';
export interface ExportInfo {
identifier: string;
from: string;
dtsFrom: string|null;
}
export type PrivateDeclarationsAnalyses = ExportInfo[];
/**
* This class will analyze a program to find all the declared classes
* (i.e. on an NgModule) that are not publicly exported via an entry-point.
*/
export class PrivateDeclarationsAnalyzer {
constructor(private host: NgccReflectionHost, private referencesRegistry: ReferencesRegistry) {}
analyzeProgram(program: ts.Program): PrivateDeclarationsAnalyses {
const rootFiles = this.getRootFiles(program);
return this.getPrivateDeclarations(rootFiles, this.referencesRegistry.getDeclarationMap());
}
private getRootFiles(program: ts.Program): ts.SourceFile[] {
return program.getRootFileNames().map(f => program.getSourceFile(f)).filter(isDefined);
}
private getPrivateDeclarations(
rootFiles: ts.SourceFile[],
declarations: Map<ts.Identifier, Declaration>): PrivateDeclarationsAnalyses {
const privateDeclarations: Map<ts.Identifier, Declaration> = new Map(declarations);
rootFiles.forEach(f => {
const exports = this.host.getExportsOfModule(f);
if (exports) {
exports.forEach((declaration, exportedName) => {
if (hasNameIdentifier(declaration.node) && declaration.node.name.text === exportedName) {
privateDeclarations.delete(declaration.node.name);
}
});
}
});
return Array.from(privateDeclarations.keys()).map(id => {
const from = id.getSourceFile().fileName;
const declaration = privateDeclarations.get(id) !;
const dtsDeclaration = this.host.getDtsDeclarationOfClass(declaration.node);
const dtsFrom = dtsDeclaration && dtsDeclaration.getSourceFile().fileName;
return {identifier: id.text, from, dtsFrom};
});
}
}

View File

@ -0,0 +1,158 @@
/**
* @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 {ResolvedReference} from '@angular/compiler-cli/src/ngtsc/metadata';
import * as ts from 'typescript';
import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry';
import {PrivateDeclarationsAnalyzer} from '../../src/analysis/private_declarations_analyzer';
import {Esm2015ReflectionHost} from '../../src/host/esm2015_host';
import {getDeclaration, makeTestBundleProgram, makeTestProgram} from '../helpers/utils';
const TEST_PROGRAM = [
{
name: '/src/entry_point.js',
isRoot: true,
contents: `
export {PublicComponent} from './a';
export {ModuleA} from './mod';
export {ModuleB} from './b';
`
},
{
name: '/src/a.js',
isRoot: false,
contents: `
import {Component} from '@angular/core';
export class PublicComponent {}
PublicComponent.decorators = [
{type: Component, args: [{selectors: 'a', template: ''}]}
];
`
},
{
name: '/src/b.js',
isRoot: false,
contents: `
import {Component, NgModule} from '@angular/core';
class PrivateComponent {}
PrivateComponent.decorators = [
{type: Component, args: [{selectors: 'b', template: ''}]}
];
export class ModuleB {}
ModuleB.decorators = [
{type: NgModule, args: [{declarations: [PrivateComponent]}]}
];
`
},
{
name: '/src/c.js',
isRoot: false,
contents: `
import {Component} from '@angular/core';
export class InternalComponent {}
InternalComponent.decorators = [
{type: Component, args: [{selectors: 'c', template: ''}]}
];
`
},
{
name: '/src/mod.js',
isRoot: false,
contents: `
import {Component, NgModule} from '@angular/core';
import {PublicComponent} from './a';
import {ModuleB} from './b';
import {InternalComponent} from './c';
export class ModuleA {}
ModuleA.decorators = [
{type: NgModule, args: [{
declarations: [PublicComponent, InternalComponent],
imports: [ModuleB]
}]}
];
`
}
];
const TEST_DTS_PROGRAM = [
{
name: '/typings/entry_point.d.ts',
isRoot: true,
contents: `
export {PublicComponent} from './a';
export {ModuleA} from './mod';
export {ModuleB} from './b';
`
},
{
name: '/typings/a.d.ts',
isRoot: false,
contents: `
export declare class PublicComponent {}
`
},
{
name: '/typings/b.d.ts',
isRoot: false,
contents: `
export declare class ModuleB {}
`
},
{
name: '/typings/c.d.ts',
isRoot: false,
contents: `
export declare class InternalComponent {}
`
},
{
name: '/typings/mod.d.ts',
isRoot: false,
contents: `
import {PublicComponent} from './a';
import {ModuleB} from './b';
import {InternalComponent} from './c';
export declare class ModuleA {}
`
},
];
describe('PrivateDeclarationsAnalyzer', () => {
describe('analyzeProgram()', () => {
it('should find all NgModule declarations that were not publicly exported from the entry-point',
() => {
const program = makeTestProgram(...TEST_PROGRAM);
const dts = makeTestBundleProgram(TEST_DTS_PROGRAM);
const host = new Esm2015ReflectionHost(false, program.getTypeChecker(), dts);
const referencesRegistry = new NgccReferencesRegistry(host);
const analyzer = new PrivateDeclarationsAnalyzer(host, referencesRegistry);
// Set up the registry with references - this would normally be done by the
// decoration handlers in the `DecorationAnalyzer`.
const publicComponentDeclaration =
getDeclaration(program, '/src/a.js', 'PublicComponent', ts.isClassDeclaration);
referencesRegistry.add(
new ResolvedReference(publicComponentDeclaration, publicComponentDeclaration.name !));
const privateComponentDeclaration =
getDeclaration(program, '/src/b.js', 'PrivateComponent', ts.isClassDeclaration);
referencesRegistry.add(new ResolvedReference(
privateComponentDeclaration, privateComponentDeclaration.name !));
const internalComponentDeclaration =
getDeclaration(program, '/src/c.js', 'InternalComponent', ts.isClassDeclaration);
referencesRegistry.add(new ResolvedReference(
internalComponentDeclaration, internalComponentDeclaration.name !));
const analyses = analyzer.analyzeProgram(program);
expect(analyses.length).toEqual(2);
expect(analyses).toEqual([
{identifier: 'PrivateComponent', from: '/src/b.js', dtsFrom: null},
{identifier: 'InternalComponent', from: '/src/c.js', dtsFrom: '/typings/c.d.ts'},
]);
});
});
});