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 => { return Array.from(privateDeclarations.keys()).map(id => {
const from = id.getSourceFile().fileName; const from = id.getSourceFile().fileName;
const declaration = privateDeclarations.get(id) !; 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; const dtsFrom = dtsDeclaration && dtsDeclaration.getSourceFile().fileName;
return {identifier: id.text, from, dtsFrom}; 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`. * a static method called `ctorParameters`.
*/ */
export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements NgccReflectionHost { 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) { constructor(protected isCore: boolean, checker: ts.TypeChecker, dts?: BundleProgram|null) {
super(checker); 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. * is not a class or has an unknown number of type parameters.
*/ */
getGenericArityOfClass(clazz: ts.Declaration): number|null { getGenericArityOfClass(clazz: ts.Declaration): number|null {
const dtsClass = this.getDtsDeclarationOfClass(clazz); const dtsDeclaration = this.getDtsDeclaration(clazz);
if (dtsClass) { if (dtsDeclaration && ts.isClassDeclaration(dtsDeclaration)) {
return dtsClass.typeParameters ? dtsClass.typeParameters.length : 0; return dtsDeclaration.typeParameters ? dtsDeclaration.typeParameters.length : 0;
} }
return null; 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. * 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 * 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 * Note that the `ts.ClassDeclaration` returned from this function may not be from the same
* `ts.Program` as the input declaration. * `ts.Program` as the input declaration.
*/ */
getDtsDeclarationOfClass(declaration: ts.Declaration): ts.ClassDeclaration|null { getDtsDeclaration(declaration: ts.Declaration): ts.Declaration|null {
if (this.dtsClassMap) { if (!this.dtsDeclarationMap) {
if (ts.isClassDeclaration(declaration)) { return null;
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;
}
} }
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 Helpers /////////////
protected getDecoratorsOfSymbol(symbol: ts.Symbol): Decorator[]|null { protected getDecoratorsOfSymbol(symbol: ts.Symbol): Decorator[]|null {
@ -738,7 +735,7 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
} }
if (!name) { if (!name) {
if (isNamedDeclaration(node) && node.name && ts.isIdentifier(node.name)) { if (isNamedDeclaration(node)) {
name = node.name.text; name = node.name.text;
nameNode = node.name; nameNode = node.name;
} else { } 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 * Get the parameter type and decorators for a class where the information is stored via
* in calls to `__decorate` helpers. * calls to `__decorate` helpers.
* *
* Reflect over the helpers to find the decorators and types about each of * Reflect over the helpers to find the decorators and types about each of
* the class's constructor parameters. * 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. * @param dtsProgram The program containing all the typings files.
* @returns a map of class names to class declarations. * @returns a map of class names to class declarations.
*/ */
protected computeDtsClassMap(dtsRootFileName: string, dtsProgram: ts.Program): protected computeDtsDeclarationMap(dtsRootFileName: string, dtsProgram: ts.Program):
Map<string, ts.ClassDeclaration> { Map<string, ts.Declaration> {
const dtsClassMap = new Map<string, ts.ClassDeclaration>(); const dtsDeclarationMap = new Map<string, ts.Declaration>();
const checker = dtsProgram.getTypeChecker(); const checker = dtsProgram.getTypeChecker();
// First add all the classes that are publicly exported from the entry-point // 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) { if (!rootFile) {
throw new Error(`The given file ${dtsRootFileName} is not part of the typings program.`); 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, // Now add any additional classes that are exported from individual dts files,
// but are not publicly exported from the entry-point. // but are not publicly exported from the entry-point.
dtsProgram.getSourceFiles().forEach( dtsProgram.getSourceFiles().forEach(
sourceFile => { collectExportedClasses(checker, dtsClassMap, sourceFile); }); sourceFile => { collectExportedDeclarations(checker, dtsDeclarationMap, sourceFile); });
return dtsClassMap; return dtsDeclarationMap;
} }
} }
@ -1129,8 +1126,10 @@ function isThisAssignment(node: ts.Declaration): node is ts.BinaryExpression&
node.left.expression.kind === ts.SyntaxKind.ThisKeyword; node.left.expression.kind === ts.SyntaxKind.ThisKeyword;
} }
function isNamedDeclaration(node: ts.Declaration): node is ts.NamedDeclaration { function isNamedDeclaration(node: ts.Declaration): node is ts.NamedDeclaration&
return !!(node as any).name; {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`. * Collect mappings between exported declarations in a source file and its associated
* @param checker The typechecker for the source program. * declaration in the typings program.
* @param dtsClassMap The map in which to store the collected exported classes.
* @param srcFile The source file to search for exported classes.
*/ */
function collectExportedClasses( function collectExportedDeclarations(
checker: ts.TypeChecker, dtsClassMap: Map<string, ts.ClassDeclaration>, checker: ts.TypeChecker, dtsDeclarationMap: Map<string, ts.Declaration>,
srcFile: ts.SourceFile): void { srcFile: ts.SourceFile): void {
const srcModule = srcFile && checker.getSymbolAtLocation(srcFile); const srcModule = srcFile && checker.getSymbolAtLocation(srcFile);
const moduleExports = srcModule && checker.getExportsOfModule(srcModule); const moduleExports = srcModule && checker.getExportsOfModule(srcModule);
@ -1170,8 +1167,8 @@ function collectExportedClasses(
} }
const declaration = exportedSymbol.valueDeclaration; const declaration = exportedSymbol.valueDeclaration;
const name = exportedSymbol.name; const name = exportedSymbol.name;
if (declaration && ts.isClassDeclaration(declaration) && !dtsClassMap.has(name)) { if (declaration && !dtsDeclarationMap.has(name)) {
dtsClassMap.set(name, declaration); dtsDeclarationMap.set(name, declaration);
} }
}); });
} }

View File

@ -307,7 +307,7 @@ export abstract class Renderer {
const dtsMap = new Map<ts.SourceFile, DtsClassInfo[]>(); const dtsMap = new Map<ts.SourceFile, DtsClassInfo[]>();
analyses.forEach(compiledFile => { analyses.forEach(compiledFile => {
compiledFile.compiledClasses.forEach(compiledClass => { compiledFile.compiledClasses.forEach(compiledClass => {
const dtsDeclaration = this.host.getDtsDeclarationOfClass(compiledClass.declaration); const dtsDeclaration = this.host.getDtsDeclaration(compiledClass.declaration);
if (dtsDeclaration) { if (dtsDeclaration) {
const dtsFile = dtsDeclaration.getSourceFile(); const dtsFile = dtsDeclaration.getSourceFile();
const classes = dtsMap.get(dtsFile) || []; 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/class1.js', contents: 'export class Class1 {}\nexport class MissingClass1 {}'},
{name: '/src/class2.js', contents: 'export class Class2 {}'}, {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/internal.js', contents: 'export class InternalClass {}\nexport class Class2 {}'},
{name: '/src/missing-class.js', contents: 'export class MissingClass2 {}'}, { {name: '/src/missing-class.js', contents: 'export class MissingClass2 {}'}, {
name: '/src/flat-file.js', name: '/src/flat-file.js',
@ -476,6 +477,7 @@ const TYPINGS_DTS_FILES = [
contents: contents:
`export declare class Class2 {}\nexport declare interface SomeInterface {}\nexport {Class3 as xClass3} from './class3';` `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', name: '/typings/internal.d.ts',
contents: `export declare class InternalClass {}\nexport declare class Class2 {}` 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 class1 = getDeclaration(srcProgram, '/src/class1.js', 'Class1', ts.isClassDeclaration);
const host = new Esm2015ReflectionHost(false, srcProgram.getTypeChecker(), dts); 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'); 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', () => { it('should return null if there is no matching class in the matching dts file', () => {
const srcProgram = makeTestProgram(...TYPINGS_SRC_FILES); const srcProgram = makeTestProgram(...TYPINGS_SRC_FILES);
const dts = makeTestBundleProgram(TYPINGS_DTS_FILES); const dts = makeTestBundleProgram(TYPINGS_DTS_FILES);
@ -1297,7 +1309,7 @@ describe('Fesm2015ReflectionHost', () => {
getDeclaration(srcProgram, '/src/class1.js', 'MissingClass1', ts.isClassDeclaration); getDeclaration(srcProgram, '/src/class1.js', 'MissingClass1', ts.isClassDeclaration);
const host = new Esm2015ReflectionHost(false, srcProgram.getTypeChecker(), dts); 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', () => { 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); srcProgram, '/src/missing-class.js', 'MissingClass2', ts.isClassDeclaration);
const host = new Esm2015ReflectionHost(false, srcProgram.getTypeChecker(), dts); 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', 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); getDeclaration(srcProgram, '/src/flat-file.js', 'Class1', ts.isClassDeclaration);
const host = new Esm2015ReflectionHost(false, srcProgram.getTypeChecker(), dts); 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'); expect(dtsDeclaration !.getSourceFile().fileName).toEqual('/typings/class1.d.ts');
}); });
@ -1329,7 +1341,7 @@ describe('Fesm2015ReflectionHost', () => {
getDeclaration(srcProgram, '/src/flat-file.js', 'Class3', ts.isClassDeclaration); getDeclaration(srcProgram, '/src/flat-file.js', 'Class3', ts.isClassDeclaration);
const host = new Esm2015ReflectionHost(false, srcProgram.getTypeChecker(), dts); 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'); expect(dtsDeclaration !.getSourceFile().fileName).toEqual('/typings/class3.d.ts');
}); });
@ -1341,7 +1353,7 @@ describe('Fesm2015ReflectionHost', () => {
getDeclaration(srcProgram, '/src/internal.js', 'InternalClass', ts.isClassDeclaration); getDeclaration(srcProgram, '/src/internal.js', 'InternalClass', ts.isClassDeclaration);
const host = new Esm2015ReflectionHost(false, srcProgram.getTypeChecker(), dts); 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'); expect(dtsDeclaration !.getSourceFile().fileName).toEqual('/typings/internal.d.ts');
}); });
@ -1355,10 +1367,10 @@ describe('Fesm2015ReflectionHost', () => {
getDeclaration(srcProgram, '/src/internal.js', 'Class2', ts.isClassDeclaration); getDeclaration(srcProgram, '/src/internal.js', 'Class2', ts.isClassDeclaration);
const host = new Esm2015ReflectionHost(false, srcProgram.getTypeChecker(), dts); 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'); expect(class2DtsDeclaration !.getSourceFile().fileName).toEqual('/typings/class2.d.ts');
const internalClass2DtsDeclaration = host.getDtsDeclarationOfClass(internalClass2); const internalClass2DtsDeclaration = host.getDtsDeclaration(internalClass2);
expect(internalClass2DtsDeclaration !.getSourceFile().fileName) expect(internalClass2DtsDeclaration !.getSourceFile().fileName)
.toEqual('/typings/class2.d.ts'); .toEqual('/typings/class2.d.ts');
}); });

View File

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

View File

@ -448,7 +448,7 @@ export interface ReflectionHost {
getVariableValue(declaration: ts.VariableDeclaration): ts.Expression|null; 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. * 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 * 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, * 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. * 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. * `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; 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. * Resolve a `ts.Symbol` to its declaration, keeping track of the `viaModule` along the way.