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:
parent
64a34616d8
commit
0c6e1f4a1b
|
@ -356,12 +356,7 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Cannot get the dts file for a class declaration that has no indetifier: ${declaration.getText()} in ${declaration.getSourceFile().fileName}`);
|
`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);
|
return this.dtsClassMap.get(declaration.name.text) || null;
|
||||||
if (!dtsDeclaration) {
|
|
||||||
throw new Error(
|
|
||||||
`Unable to find matching typings (.d.ts) declaration for ${declaration.name.text} in ${declaration.getSourceFile().fileName}`);
|
|
||||||
}
|
|
||||||
return dtsDeclaration;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return 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):
|
protected computeDtsClassMap(dtsRootFileName: string, dtsProgram: ts.Program):
|
||||||
Map<string, ts.ClassDeclaration> {
|
Map<string, ts.ClassDeclaration> {
|
||||||
const dtsClassMap = new Map<string, ts.ClassDeclaration>();
|
const dtsClassMap = new Map<string, ts.ClassDeclaration>();
|
||||||
const checker = dtsProgram.getTypeChecker();
|
const checker = dtsProgram.getTypeChecker();
|
||||||
const dtsRootFile = dtsProgram.getSourceFile(dtsRootFileName);
|
|
||||||
const rootModule = dtsRootFile && checker.getSymbolAtLocation(dtsRootFile);
|
// First add all the classes that are publicly exported from the entry-point
|
||||||
const moduleExports = rootModule && checker.getExportsOfModule(rootModule);
|
const rootFile = dtsProgram.getSourceFile(dtsRootFileName);
|
||||||
if (moduleExports) {
|
if (!rootFile) {
|
||||||
moduleExports.forEach(exportedSymbol => {
|
throw new Error(`The given file ${dtsRootFileName} is not part of the typings program.`);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
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;
|
return dtsClassMap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1151,3 +1152,28 @@ function getFarLeftIdentifier(propertyAccess: ts.PropertyAccessExpression): ts.I
|
||||||
}
|
}
|
||||||
return ts.isIdentifier(propertyAccess.expression) ? propertyAccess.expression : null;
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -445,9 +445,14 @@ const ARITY_CLASSES = [
|
||||||
];
|
];
|
||||||
|
|
||||||
const TYPINGS_SRC_FILES = [
|
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/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/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',
|
||||||
contents:
|
contents:
|
||||||
|
@ -456,7 +461,11 @@ const TYPINGS_SRC_FILES = [
|
||||||
];
|
];
|
||||||
|
|
||||||
const TYPINGS_DTS_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',
|
name: '/typings/class1.d.ts',
|
||||||
contents: `export declare class Class1 {}\nexport declare class OtherClass {}`
|
contents: `export declare class Class1 {}\nexport declare class OtherClass {}`
|
||||||
|
@ -466,6 +475,10 @@ 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/internal.d.ts',
|
||||||
|
contents: `export declare class InternalClass {}\nexport declare class Class2 {}`
|
||||||
|
},
|
||||||
{name: '/typings/class3.d.ts', contents: `export declare class Class3 {}`},
|
{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');
|
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 srcProgram = makeProgram(...TYPINGS_SRC_FILES);
|
||||||
const dtsProgram = makeProgram(...TYPINGS_DTS_FILES);
|
const dtsProgram = makeProgram(...TYPINGS_DTS_FILES);
|
||||||
const missingClass =
|
const missingClass =
|
||||||
|
@ -1285,12 +1298,10 @@ describe('Fesm2015ReflectionHost', () => {
|
||||||
const host = new Esm2015ReflectionHost(
|
const host = new Esm2015ReflectionHost(
|
||||||
false, srcProgram.getTypeChecker(), TYPINGS_DTS_FILES[0].name, dtsProgram);
|
false, srcProgram.getTypeChecker(), TYPINGS_DTS_FILES[0].name, dtsProgram);
|
||||||
|
|
||||||
expect(() => host.getDtsDeclarationOfClass(missingClass))
|
expect(host.getDtsDeclarationOfClass(missingClass)).toBe(null);
|
||||||
.toThrowError(
|
|
||||||
'Unable to find matching typings (.d.ts) declaration for MissingClass1 in /src/class1.js');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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 srcProgram = makeProgram(...TYPINGS_SRC_FILES);
|
||||||
const dtsProgram = makeProgram(...TYPINGS_DTS_FILES);
|
const dtsProgram = makeProgram(...TYPINGS_DTS_FILES);
|
||||||
const missingClass = getDeclaration(
|
const missingClass = getDeclaration(
|
||||||
|
@ -1298,9 +1309,7 @@ describe('Fesm2015ReflectionHost', () => {
|
||||||
const host = new Esm2015ReflectionHost(
|
const host = new Esm2015ReflectionHost(
|
||||||
false, srcProgram.getTypeChecker(), TYPINGS_DTS_FILES[0].name, dtsProgram);
|
false, srcProgram.getTypeChecker(), TYPINGS_DTS_FILES[0].name, dtsProgram);
|
||||||
|
|
||||||
expect(() => host.getDtsDeclarationOfClass(missingClass))
|
expect(host.getDtsDeclarationOfClass(missingClass)).toBe(null);
|
||||||
.toThrowError(
|
|
||||||
'Unable to find matching typings (.d.ts) declaration for MissingClass2 in /src/missing-class.js');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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',
|
||||||
|
@ -1327,6 +1336,37 @@ describe('Fesm2015ReflectionHost', () => {
|
||||||
const dtsDeclaration = host.getDtsDeclarationOfClass(class3);
|
const dtsDeclaration = host.getDtsDeclarationOfClass(class3);
|
||||||
expect(dtsDeclaration !.getSourceFile().fileName).toEqual('/typings/class3.d.ts');
|
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');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue