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:
parent
b55e1c2ba9
commit
bf3ac41e36
|
@ -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};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -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'},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue