/** * @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 {Declaration} from '../../../src/ngtsc/reflection'; import {NgccReflectionHost} from '../host/ngcc_host'; import {hasNameIdentifier, isDefined} from '../utils'; import {NgccReferencesRegistry} from './ngcc_references_registry'; export interface ExportInfo { identifier: string; from: string; dtsFrom?: string|null; alias?: 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: NgccReferencesRegistry) {} 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); const exportAliasDeclarations: Map = new Map(); rootFiles.forEach(f => { const exports = this.host.getExportsOfModule(f); if (exports) { exports.forEach((declaration, exportedName) => { if (hasNameIdentifier(declaration.node)) { const privateDeclaration = privateDeclarations.get(declaration.node.name); if (privateDeclaration) { if (privateDeclaration.node !== declaration.node) { throw new Error(`${declaration.node.name.text} is declared multiple times.`); } if (declaration.node.name.text === exportedName) { // This declaration is public so we can remove it from the list privateDeclarations.delete(declaration.node.name); } else if (!this.host.getDtsDeclaration(declaration.node)) { // The referenced declaration is exported publicly but via an alias. // In some cases the original declaration is missing from the dts program, such as // when rolling up (flattening) the dts files. // This is because the original declaration gets renamed to the exported alias. // There is a constraint on this which we cannot handle. Consider the following // code: // // /src/entry_point.js: // export {MyComponent as aliasedMyComponent} from './a'; // export {MyComponent} from './b';` // // /src/a.js: // export class MyComponent {} // // /src/b.js: // export class MyComponent {} // // //typings/entry_point.d.ts: // export declare class aliasedMyComponent {} // export declare class MyComponent {} // // In this case we would end up matching the `MyComponent` from `/src/a.js` to the // `MyComponent` declared in `/typings/entry_point.d.ts` even though that // declaration is actually for the `MyComponent` in `/src/b.js`. exportAliasDeclarations.set(declaration.node.name, exportedName); } } } }); } }); return Array.from(privateDeclarations.keys()).map(id => { const from = id.getSourceFile().fileName; const declaration = privateDeclarations.get(id) !; const alias = exportAliasDeclarations.get(id) || null; const dtsDeclaration = this.host.getDtsDeclaration(declaration.node); const dtsFrom = dtsDeclaration && dtsDeclaration.getSourceFile().fileName; return {identifier: id.text, from, dtsFrom, alias}; }); } }