fix(ivy): ngcc - support typings (d.ts) classes that are not publicly exported (#26906)

If a decorated class is not publicly exported via an entry-point then the
previous approach to finding the associated typings file failed.

Now we ensure that we extract all the class declarations from the
dtsTypings program, even if they are not exported from the entry-point.
This is achieved by also parsing statements of each source file, rather
than just parsing classes that are exported from the entry-point.

Because we now look at all the files, it is possible for there to be multiple
class declarations with the same local name. In this case, only the first
declaration with a given name is added to the map; subsequent classes are
ignored.

We are most interested in classes that are publicly exported from the
entry-point, so these are added to the map first, to ensure that they are
not ignored.

PR Close #26906
This commit is contained in:
Pete Bacon Darwin 2018-11-21 20:52:27 +00:00 committed by Igor Minar
parent 64a34616d8
commit 0c6e1f4a1b
2 changed files with 103 additions and 37 deletions

View File

@ -356,12 +356,7 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
throw new Error(
`Cannot get the dts file for a class declaration that has no indetifier: ${declaration.getText()} in ${declaration.getSourceFile().fileName}`);
}
const dtsDeclaration = this.dtsClassMap.get(declaration.name.text);
if (!dtsDeclaration) {
throw new Error(
`Unable to find matching typings (.d.ts) declaration for ${declaration.name.text} in ${declaration.getSourceFile().fileName}`);
}
return dtsDeclaration;
return this.dtsClassMap.get(declaration.name.text) || null;
}
}
return null;
@ -993,31 +988,37 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
}
}
/**
* Extract all the class declarations from the dtsTypings program, storing them in a map
* where the key is the declared name of the class and the value is the declaration itself.
*
* It is possible for there to be multiple class declarations with the same local name.
* Only the first declaration with a given name is added to the map; subsequent classes will be
* ignored.
*
* We are most interested in classes that are publicly exported from the entry point, so these are
* added to the map first, to ensure that they are not ignored.
*
* @param dtsRootFileName The filename of the entry-point to the `dtsTypings` program.
* @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>();
const checker = dtsProgram.getTypeChecker();
const dtsRootFile = dtsProgram.getSourceFile(dtsRootFileName);
const rootModule = dtsRootFile && checker.getSymbolAtLocation(dtsRootFile);
const moduleExports = rootModule && checker.getExportsOfModule(rootModule);
if (moduleExports) {
moduleExports.forEach(exportedSymbol => {
if (exportedSymbol.flags & ts.SymbolFlags.Alias) {
exportedSymbol = checker.getAliasedSymbol(exportedSymbol);
}
const declaration = exportedSymbol.declarations[0];
if (declaration && ts.isClassDeclaration(declaration)) {
const name = exportedSymbol.name;
const previousDeclaration = dtsClassMap.get(name);
if (previousDeclaration && previousDeclaration !== declaration) {
console.warn(
`Ambiguous class name ${name} in typings files: ${previousDeclaration.getSourceFile().fileName} and ${declaration.getSourceFile().fileName}`);
} else {
dtsClassMap.set(name, declaration);
}
}
});
// First add all the classes that are publicly exported from the entry-point
const rootFile = dtsProgram.getSourceFile(dtsRootFileName);
if (!rootFile) {
throw new Error(`The given file ${dtsRootFileName} is not part of the typings program.`);
}
collectExportedClasses(checker, dtsClassMap, 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;
}
}
@ -1151,3 +1152,28 @@ function getFarLeftIdentifier(propertyAccess: ts.PropertyAccessExpression): ts.I
}
return ts.isIdentifier(propertyAccess.expression) ? propertyAccess.expression : null;
}
/**
* 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.
*/
function collectExportedClasses(
checker: ts.TypeChecker, dtsClassMap: Map<string, ts.ClassDeclaration>,
srcFile: ts.SourceFile): void {
const srcModule = srcFile && checker.getSymbolAtLocation(srcFile);
const moduleExports = srcModule && checker.getExportsOfModule(srcModule);
if (moduleExports) {
moduleExports.forEach(exportedSymbol => {
if (exportedSymbol.flags & ts.SymbolFlags.Alias) {
exportedSymbol = checker.getAliasedSymbol(exportedSymbol);
}
const declaration = exportedSymbol.valueDeclaration;
const name = exportedSymbol.name;
if (declaration && ts.isClassDeclaration(declaration) && !dtsClassMap.has(name)) {
dtsClassMap.set(name, declaration);
}
});
}
}

View File

@ -445,9 +445,14 @@ const ARITY_CLASSES = [
];
const TYPINGS_SRC_FILES = [
{name: '/src/index.js', contents: `export * from './class1'; export * from './class2';`},
{
name: '/src/index.js',
contents:
`import {InternalClass} from './internal'; export * from './class1'; export * from './class2';`
},
{name: '/src/class1.js', contents: 'export class Class1 {}\nexport class MissingClass1 {}'},
{name: '/src/class2.js', contents: 'export class Class2 {}'},
{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',
contents:
@ -456,7 +461,11 @@ const TYPINGS_SRC_FILES = [
];
const TYPINGS_DTS_FILES = [
{name: '/typings/index.d.ts', contents: `export * from './class1'; export * from './class2';`},
{
name: '/typings/index.d.ts',
contents:
`import {InternalClass} from './internal'; export * from './class1'; export * from './class2';`
},
{
name: '/typings/class1.d.ts',
contents: `export declare class Class1 {}\nexport declare class OtherClass {}`
@ -466,6 +475,10 @@ const TYPINGS_DTS_FILES = [
contents:
`export declare class Class2 {}\nexport declare interface SomeInterface {}\nexport {Class3 as xClass3} from './class3';`
},
{
name: '/typings/internal.d.ts',
contents: `export declare class InternalClass {}\nexport declare class Class2 {}`
},
{name: '/typings/class3.d.ts', contents: `export declare class Class3 {}`},
];
@ -1277,7 +1290,7 @@ describe('Fesm2015ReflectionHost', () => {
expect(dtsDeclaration !.getSourceFile().fileName).toEqual('/typings/class1.d.ts');
});
it('should throw an error 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 = makeProgram(...TYPINGS_SRC_FILES);
const dtsProgram = makeProgram(...TYPINGS_DTS_FILES);
const missingClass =
@ -1285,12 +1298,10 @@ describe('Fesm2015ReflectionHost', () => {
const host = new Esm2015ReflectionHost(
false, srcProgram.getTypeChecker(), TYPINGS_DTS_FILES[0].name, dtsProgram);
expect(() => host.getDtsDeclarationOfClass(missingClass))
.toThrowError(
'Unable to find matching typings (.d.ts) declaration for MissingClass1 in /src/class1.js');
expect(host.getDtsDeclarationOfClass(missingClass)).toBe(null);
});
it('should throw an error if there is no matching dts file', () => {
it('should return null if there is no matching dts file', () => {
const srcProgram = makeProgram(...TYPINGS_SRC_FILES);
const dtsProgram = makeProgram(...TYPINGS_DTS_FILES);
const missingClass = getDeclaration(
@ -1298,9 +1309,7 @@ describe('Fesm2015ReflectionHost', () => {
const host = new Esm2015ReflectionHost(
false, srcProgram.getTypeChecker(), TYPINGS_DTS_FILES[0].name, dtsProgram);
expect(() => host.getDtsDeclarationOfClass(missingClass))
.toThrowError(
'Unable to find matching typings (.d.ts) declaration for MissingClass2 in /src/missing-class.js');
expect(host.getDtsDeclarationOfClass(missingClass)).toBe(null);
});
it('should find the dts file that contains a matching class declaration, even if the source files do not match',
@ -1327,6 +1336,37 @@ describe('Fesm2015ReflectionHost', () => {
const dtsDeclaration = host.getDtsDeclarationOfClass(class3);
expect(dtsDeclaration !.getSourceFile().fileName).toEqual('/typings/class3.d.ts');
});
it('should find the dts file that contains a matching class declaration, even if the class is not publicly exported',
() => {
const srcProgram = makeProgram(...TYPINGS_SRC_FILES);
const dtsProgram = makeProgram(...TYPINGS_DTS_FILES);
const internalClass =
getDeclaration(srcProgram, '/src/internal.js', 'InternalClass', ts.isClassDeclaration);
const host = new Esm2015ReflectionHost(
false, srcProgram.getTypeChecker(), TYPINGS_DTS_FILES[0].name, dtsProgram);
const dtsDeclaration = host.getDtsDeclarationOfClass(internalClass);
expect(dtsDeclaration !.getSourceFile().fileName).toEqual('/typings/internal.d.ts');
});
it('should prefer the publicly exported class if there are multiple classes with the same name',
() => {
const srcProgram = makeProgram(...TYPINGS_SRC_FILES);
const dtsProgram = makeProgram(...TYPINGS_DTS_FILES);
const class2 =
getDeclaration(srcProgram, '/src/class2.js', 'Class2', ts.isClassDeclaration);
const internalClass2 =
getDeclaration(srcProgram, '/src/internal.js', 'Class2', ts.isClassDeclaration);
const host = new Esm2015ReflectionHost(
false, srcProgram.getTypeChecker(), TYPINGS_DTS_FILES[0].name, dtsProgram);
const class2DtsDeclaration = host.getDtsDeclarationOfClass(class2);
expect(class2DtsDeclaration !.getSourceFile().fileName).toEqual('/typings/class2.d.ts');
const internalClass2DtsDeclaration = host.getDtsDeclarationOfClass(internalClass2);
expect(internalClass2DtsDeclaration !.getSourceFile().fileName)
.toEqual('/typings/class2.d.ts');
});
});
});