diff --git a/packages/compiler-cli/src/ngcc/src/analysis/private_declarations_analyzer.ts b/packages/compiler-cli/src/ngcc/src/analysis/private_declarations_analyzer.ts new file mode 100644 index 0000000000..26d3f839a0 --- /dev/null +++ b/packages/compiler-cli/src/ngcc/src/analysis/private_declarations_analyzer.ts @@ -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): PrivateDeclarationsAnalyses { + const privateDeclarations: Map = 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}; + }); + } +} diff --git a/packages/compiler-cli/src/ngcc/test/analysis/private_declarations_analyzer_spec.ts b/packages/compiler-cli/src/ngcc/test/analysis/private_declarations_analyzer_spec.ts new file mode 100644 index 0000000000..9188a5344b --- /dev/null +++ b/packages/compiler-cli/src/ngcc/test/analysis/private_declarations_analyzer_spec.ts @@ -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'}, + ]); + }); + }); +});