feat(ivy): ngcc - map functions as well as classes from source to typings (#27326)

To support updating `ModuleWithProviders` calls,
we need to be able to map exported functions between
source and typings files, as well as classes.

PR Close #27326
This commit is contained in:
Pete Bacon Darwin 2018-11-29 08:26:00 +00:00 committed by Matias Niemelä
parent 99d0e27587
commit cfb8c17511
7 changed files with 62 additions and 53 deletions

View File

@ -52,7 +52,7 @@ export class PrivateDeclarationsAnalyzer {
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 dtsDeclaration = this.host.getDtsDeclaration(declaration.node);
const dtsFrom = dtsDeclaration && dtsDeclaration.getSourceFile().fileName;
return {identifier: id.text, from, dtsFrom};
});

View File

@ -49,10 +49,10 @@ export const CONSTRUCTOR_PARAMS = 'ctorParameters' as ts.__String;
* a static method called `ctorParameters`.
*/
export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements NgccReflectionHost {
protected dtsClassMap: Map<string, ts.ClassDeclaration>|null;
protected dtsDeclarationMap: Map<string, ts.Declaration>|null;
constructor(protected isCore: boolean, checker: ts.TypeChecker, dts?: BundleProgram|null) {
super(checker);
this.dtsClassMap = dts && this.computeDtsClassMap(dts.path, dts.program) || null;
this.dtsDeclarationMap = dts && this.computeDtsDeclarationMap(dts.path, dts.program) || null;
}
/**
@ -327,15 +327,15 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
* is not a class or has an unknown number of type parameters.
*/
getGenericArityOfClass(clazz: ts.Declaration): number|null {
const dtsClass = this.getDtsDeclarationOfClass(clazz);
if (dtsClass) {
return dtsClass.typeParameters ? dtsClass.typeParameters.length : 0;
const dtsDeclaration = this.getDtsDeclaration(clazz);
if (dtsDeclaration && ts.isClassDeclaration(dtsDeclaration)) {
return dtsDeclaration.typeParameters ? dtsDeclaration.typeParameters.length : 0;
}
return null;
}
/**
* Take an exported declaration of a class (maybe downleveled to a variable) and look up the
* Take an exported declaration of a class (maybe down-leveled to a variable) and look up the
* declaration of its type in a separate .d.ts tree.
*
* This function is allowed to return `null` if the current compilation unit does not have a
@ -346,20 +346,17 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
* Note that the `ts.ClassDeclaration` returned from this function may not be from the same
* `ts.Program` as the input declaration.
*/
getDtsDeclarationOfClass(declaration: ts.Declaration): ts.ClassDeclaration|null {
if (this.dtsClassMap) {
if (ts.isClassDeclaration(declaration)) {
if (!declaration.name || !ts.isIdentifier(declaration.name)) {
throw new Error(
`Cannot get the dts file for a class declaration that has no indetifier: ${declaration.getText()} in ${declaration.getSourceFile().fileName}`);
}
return this.dtsClassMap.get(declaration.name.text) || null;
}
getDtsDeclaration(declaration: ts.Declaration): ts.Declaration|null {
if (!this.dtsDeclarationMap) {
return null;
}
return null;
if (!isNamedDeclaration(declaration)) {
throw new Error(
`Cannot get the dts file for a declaration that has no name: ${declaration.getText()} in ${declaration.getSourceFile().fileName}`);
}
return this.dtsDeclarationMap.get(declaration.name.text) || null;
}
///////////// Protected Helpers /////////////
protected getDecoratorsOfSymbol(symbol: ts.Symbol): Decorator[]|null {
@ -738,7 +735,7 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
}
if (!name) {
if (isNamedDeclaration(node) && node.name && ts.isIdentifier(node.name)) {
if (isNamedDeclaration(node)) {
name = node.name.text;
nameNode = node.name;
} else {
@ -846,8 +843,8 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
}
/**
* Get the parameter type and decorators for a class where the information is stored on
* in calls to `__decorate` helpers.
* Get the parameter type and decorators for a class where the information is stored via
* calls to `__decorate` helpers.
*
* Reflect over the helpers to find the decorators and types about each of
* the class's constructor parameters.
@ -1002,9 +999,9 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
* @param dtsProgram The program containing all the typings files.
* @returns a map of class names to class declarations.
*/
protected computeDtsClassMap(dtsRootFileName: string, dtsProgram: ts.Program):
Map<string, ts.ClassDeclaration> {
const dtsClassMap = new Map<string, ts.ClassDeclaration>();
protected computeDtsDeclarationMap(dtsRootFileName: string, dtsProgram: ts.Program):
Map<string, ts.Declaration> {
const dtsDeclarationMap = new Map<string, ts.Declaration>();
const checker = dtsProgram.getTypeChecker();
// First add all the classes that are publicly exported from the entry-point
@ -1012,13 +1009,13 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
if (!rootFile) {
throw new Error(`The given file ${dtsRootFileName} is not part of the typings program.`);
}
collectExportedClasses(checker, dtsClassMap, rootFile);
collectExportedDeclarations(checker, dtsDeclarationMap, rootFile);
// Now add any additional classes that are exported from individual dts files,
// but are not publicly exported from the entry-point.
dtsProgram.getSourceFiles().forEach(
sourceFile => { collectExportedClasses(checker, dtsClassMap, sourceFile); });
return dtsClassMap;
sourceFile => { collectExportedDeclarations(checker, dtsDeclarationMap, sourceFile); });
return dtsDeclarationMap;
}
}
@ -1129,8 +1126,10 @@ function isThisAssignment(node: ts.Declaration): node is ts.BinaryExpression&
node.left.expression.kind === ts.SyntaxKind.ThisKeyword;
}
function isNamedDeclaration(node: ts.Declaration): node is ts.NamedDeclaration {
return !!(node as any).name;
function isNamedDeclaration(node: ts.Declaration): node is ts.NamedDeclaration&
{name: ts.Identifier} {
const anyNode: any = node;
return !!anyNode.name && ts.isIdentifier(anyNode.name);
}
@ -1153,13 +1152,11 @@ function getFarLeftIdentifier(propertyAccess: ts.PropertyAccessExpression): ts.I
}
/**
* Search a source file for exported classes, storing them in the provided `dtsClassMap`.
* @param checker The typechecker for the source program.
* @param dtsClassMap The map in which to store the collected exported classes.
* @param srcFile The source file to search for exported classes.
* Collect mappings between exported declarations in a source file and its associated
* declaration in the typings program.
*/
function collectExportedClasses(
checker: ts.TypeChecker, dtsClassMap: Map<string, ts.ClassDeclaration>,
function collectExportedDeclarations(
checker: ts.TypeChecker, dtsDeclarationMap: Map<string, ts.Declaration>,
srcFile: ts.SourceFile): void {
const srcModule = srcFile && checker.getSymbolAtLocation(srcFile);
const moduleExports = srcModule && checker.getExportsOfModule(srcModule);
@ -1170,8 +1167,8 @@ function collectExportedClasses(
}
const declaration = exportedSymbol.valueDeclaration;
const name = exportedSymbol.name;
if (declaration && ts.isClassDeclaration(declaration) && !dtsClassMap.has(name)) {
dtsClassMap.set(name, declaration);
if (declaration && !dtsDeclarationMap.has(name)) {
dtsDeclarationMap.set(name, declaration);
}
});
}

View File

@ -307,7 +307,7 @@ export abstract class Renderer {
const dtsMap = new Map<ts.SourceFile, DtsClassInfo[]>();
analyses.forEach(compiledFile => {
compiledFile.compiledClasses.forEach(compiledClass => {
const dtsDeclaration = this.host.getDtsDeclarationOfClass(compiledClass.declaration);
const dtsDeclaration = this.host.getDtsDeclaration(compiledClass.declaration);
if (dtsDeclaration) {
const dtsFile = dtsDeclaration.getSourceFile();
const classes = dtsMap.get(dtsFile) || [];

View File

@ -453,6 +453,7 @@ const TYPINGS_SRC_FILES = [
},
{name: '/src/class1.js', contents: 'export class Class1 {}\nexport class MissingClass1 {}'},
{name: '/src/class2.js', contents: 'export class Class2 {}'},
{name: '/src/func1.js', contents: 'export function mooFn() {}'},
{name: '/src/internal.js', contents: 'export class InternalClass {}\nexport class Class2 {}'},
{name: '/src/missing-class.js', contents: 'export class MissingClass2 {}'}, {
name: '/src/flat-file.js',
@ -476,6 +477,7 @@ const TYPINGS_DTS_FILES = [
contents:
`export declare class Class2 {}\nexport declare interface SomeInterface {}\nexport {Class3 as xClass3} from './class3';`
},
{name: '/typings/func1.d.ts', contents: 'export declare function mooFn(): void;'},
{
name: '/typings/internal.d.ts',
contents: `export declare class InternalClass {}\nexport declare class Class2 {}`
@ -1286,10 +1288,20 @@ describe('Fesm2015ReflectionHost', () => {
const class1 = getDeclaration(srcProgram, '/src/class1.js', 'Class1', ts.isClassDeclaration);
const host = new Esm2015ReflectionHost(false, srcProgram.getTypeChecker(), dts);
const dtsDeclaration = host.getDtsDeclarationOfClass(class1);
const dtsDeclaration = host.getDtsDeclaration(class1);
expect(dtsDeclaration !.getSourceFile().fileName).toEqual('/typings/class1.d.ts');
});
it('should find the dts declaration for exported functions', () => {
const srcProgram = makeTestProgram(...TYPINGS_SRC_FILES);
const dtsProgram = makeTestBundleProgram(TYPINGS_DTS_FILES);
const mooFn = getDeclaration(srcProgram, '/src/func1.js', 'mooFn', ts.isFunctionDeclaration);
const host = new Esm2015ReflectionHost(false, srcProgram.getTypeChecker(), dtsProgram);
const dtsDeclaration = host.getDtsDeclaration(mooFn);
expect(dtsDeclaration !.getSourceFile().fileName).toEqual('/typings/func1.d.ts');
});
it('should return null if there is no matching class in the matching dts file', () => {
const srcProgram = makeTestProgram(...TYPINGS_SRC_FILES);
const dts = makeTestBundleProgram(TYPINGS_DTS_FILES);
@ -1297,7 +1309,7 @@ describe('Fesm2015ReflectionHost', () => {
getDeclaration(srcProgram, '/src/class1.js', 'MissingClass1', ts.isClassDeclaration);
const host = new Esm2015ReflectionHost(false, srcProgram.getTypeChecker(), dts);
expect(host.getDtsDeclarationOfClass(missingClass)).toBe(null);
expect(host.getDtsDeclaration(missingClass)).toBe(null);
});
it('should return null if there is no matching dts file', () => {
@ -1307,7 +1319,7 @@ describe('Fesm2015ReflectionHost', () => {
srcProgram, '/src/missing-class.js', 'MissingClass2', ts.isClassDeclaration);
const host = new Esm2015ReflectionHost(false, srcProgram.getTypeChecker(), dts);
expect(host.getDtsDeclarationOfClass(missingClass)).toBe(null);
expect(host.getDtsDeclaration(missingClass)).toBe(null);
});
it('should find the dts file that contains a matching class declaration, even if the source files do not match',
@ -1318,7 +1330,7 @@ describe('Fesm2015ReflectionHost', () => {
getDeclaration(srcProgram, '/src/flat-file.js', 'Class1', ts.isClassDeclaration);
const host = new Esm2015ReflectionHost(false, srcProgram.getTypeChecker(), dts);
const dtsDeclaration = host.getDtsDeclarationOfClass(class1);
const dtsDeclaration = host.getDtsDeclaration(class1);
expect(dtsDeclaration !.getSourceFile().fileName).toEqual('/typings/class1.d.ts');
});
@ -1329,7 +1341,7 @@ describe('Fesm2015ReflectionHost', () => {
getDeclaration(srcProgram, '/src/flat-file.js', 'Class3', ts.isClassDeclaration);
const host = new Esm2015ReflectionHost(false, srcProgram.getTypeChecker(), dts);
const dtsDeclaration = host.getDtsDeclarationOfClass(class3);
const dtsDeclaration = host.getDtsDeclaration(class3);
expect(dtsDeclaration !.getSourceFile().fileName).toEqual('/typings/class3.d.ts');
});
@ -1341,7 +1353,7 @@ describe('Fesm2015ReflectionHost', () => {
getDeclaration(srcProgram, '/src/internal.js', 'InternalClass', ts.isClassDeclaration);
const host = new Esm2015ReflectionHost(false, srcProgram.getTypeChecker(), dts);
const dtsDeclaration = host.getDtsDeclarationOfClass(internalClass);
const dtsDeclaration = host.getDtsDeclaration(internalClass);
expect(dtsDeclaration !.getSourceFile().fileName).toEqual('/typings/internal.d.ts');
});
@ -1355,10 +1367,10 @@ describe('Fesm2015ReflectionHost', () => {
getDeclaration(srcProgram, '/src/internal.js', 'Class2', ts.isClassDeclaration);
const host = new Esm2015ReflectionHost(false, srcProgram.getTypeChecker(), dts);
const class2DtsDeclaration = host.getDtsDeclarationOfClass(class2);
const class2DtsDeclaration = host.getDtsDeclaration(class2);
expect(class2DtsDeclaration !.getSourceFile().fileName).toEqual('/typings/class2.d.ts');
const internalClass2DtsDeclaration = host.getDtsDeclarationOfClass(internalClass2);
const internalClass2DtsDeclaration = host.getDtsDeclaration(internalClass2);
expect(internalClass2DtsDeclaration !.getSourceFile().fileName)
.toEqual('/typings/class2.d.ts');
});

View File

@ -109,7 +109,7 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalys
const valueContext = node.getSourceFile();
let typeContext = valueContext;
const typeNode = this.reflector.getDtsDeclarationOfClass(node);
const typeNode = this.reflector.getDtsDeclaration(node);
if (typeNode !== null) {
typeContext = typeNode.getSourceFile();
}
@ -183,8 +183,8 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalys
return toR3Reference(valueRef, valueRef, valueContext, valueContext);
} else {
let typeRef = valueRef;
let typeNode = this.reflector.getDtsDeclarationOfClass(typeRef.node);
if (typeNode !== null) {
let typeNode = this.reflector.getDtsDeclaration(typeRef.node);
if (typeNode !== null && ts.isClassDeclaration(typeNode)) {
typeRef = new ResolvedReference(typeNode, typeNode.name !);
}
return toR3Reference(valueRef, typeRef, valueContext, typeContext);

View File

@ -448,7 +448,7 @@ export interface ReflectionHost {
getVariableValue(declaration: ts.VariableDeclaration): ts.Expression|null;
/**
* Take an exported declaration of a class (maybe downleveled to a variable) and look up the
* Take an exported declaration (maybe a class down-leveled to a variable) and look up the
* declaration of its type in a separate .d.ts tree.
*
* This function is allowed to return `null` if the current compilation unit does not have a
@ -456,8 +456,8 @@ export interface ReflectionHost {
* are produced only during the emit of such a compilation. When compiling .js code, however,
* there is frequently a parallel .d.ts tree which this method exposes.
*
* Note that the `ts.ClassDeclaration` returned from this function may not be from the same
* Note that the `ts.Declaration` returned from this function may not be from the same
* `ts.Program` as the input declaration.
*/
getDtsDeclarationOfClass(declaration: ts.Declaration): ts.ClassDeclaration|null;
getDtsDeclaration(declaration: ts.Declaration): ts.Declaration|null;
}

View File

@ -188,7 +188,7 @@ export class TypeScriptReflectionHost implements ReflectionHost {
return declaration.initializer || null;
}
getDtsDeclarationOfClass(_: ts.Declaration): ts.ClassDeclaration|null { return null; }
getDtsDeclaration(_: ts.Declaration): ts.Declaration|null { return null; }
/**
* Resolve a `ts.Symbol` to its declaration, keeping track of the `viaModule` along the way.