fix(ngcc): consistently use outer declaration for classes (#32539)
In ngcc's reflection hosts for compiled JS bundles, such as ESM2015, special care needs to be taken for classes as there may be an outer declaration (referred to as "declaration") and an inner declaration (referred to as "implementation") for a given class. Therefore, there will also be two `ts.Symbol`s bound per class, and ngcc needs to switch between those declarations and symbols depending on where certain information can be found. Prior to this commit, the `NgccReflectionHost` interface had methods `getClassSymbol` and `findClassSymbols` that would return a `ts.Symbol`. These class symbols would be used to kick off compilation of components using ngtsc, so it is important for these symbols to correspond with the publicly visible outer declaration of the class. However, the ESM2015 reflection host used to return the `ts.Symbol` for the inner declaration, if the class was declared as follows: ```javascript var MyClass = class MyClass {}; ``` For the above code, `Esm2015ReflectionHost.getClassSymbol` would return the `ts.Symbol` corresponding with the `class MyClass {}` declaration, whereas it should have corresponded with the `var MyClass` declaration. As a consequence, no `NgModule` could be resolved for the component, so no components/directives would be in scope for the component. This resulted in errors during runtime. This commit resolves the issue by introducing a `NgccClassSymbol` that contains references to both the outer and inner `ts.Symbol`, instead of just a single `ts.Symbol`. This avoids the unclarity of whether a `ts.Symbol` corresponds with the outer or inner declaration. More details can be found here: https://hackmd.io/7nkgWOFWQlSRAuIW_8KPPw Fixes #32078 Closes FW-1507 PR Close #32539
This commit is contained in:
@ -1565,6 +1565,95 @@ runInEachFileSystem(() => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getClassSymbol()', () => {
|
||||
it('should return the class symbol for an ES2015 class', () => {
|
||||
loadTestFiles([SIMPLE_CLASS_FILE]);
|
||||
const {program} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name);
|
||||
const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
|
||||
const node =
|
||||
getDeclaration(program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedClassDeclaration);
|
||||
const classSymbol = host.getClassSymbol(node);
|
||||
|
||||
expect(classSymbol).toBeDefined();
|
||||
expect(classSymbol !.declaration.valueDeclaration).toBe(node);
|
||||
expect(classSymbol !.implementation.valueDeclaration).toBe(node);
|
||||
});
|
||||
|
||||
it('should return the class symbol for a class expression (outer variable declaration)',
|
||||
() => {
|
||||
loadTestFiles([CLASS_EXPRESSION_FILE]);
|
||||
const {program} = makeTestBundleProgram(CLASS_EXPRESSION_FILE.name);
|
||||
const host =
|
||||
new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
|
||||
const outerNode = getDeclaration(
|
||||
program, CLASS_EXPRESSION_FILE.name, 'EmptyClass', isNamedVariableDeclaration);
|
||||
const innerNode = (outerNode.initializer as ts.ClassExpression);
|
||||
const classSymbol = host.getClassSymbol(outerNode);
|
||||
|
||||
expect(classSymbol).toBeDefined();
|
||||
expect(classSymbol !.declaration.valueDeclaration).toBe(outerNode);
|
||||
expect(classSymbol !.implementation.valueDeclaration).toBe(innerNode);
|
||||
});
|
||||
|
||||
it('should return the class symbol for a class expression (inner class expression)', () => {
|
||||
loadTestFiles([CLASS_EXPRESSION_FILE]);
|
||||
const {program} = makeTestBundleProgram(CLASS_EXPRESSION_FILE.name);
|
||||
const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
|
||||
const outerNode = getDeclaration(
|
||||
program, CLASS_EXPRESSION_FILE.name, 'EmptyClass', isNamedVariableDeclaration);
|
||||
const innerNode = (outerNode.initializer as ts.ClassExpression);
|
||||
const classSymbol = host.getClassSymbol(innerNode);
|
||||
|
||||
expect(classSymbol).toBeDefined();
|
||||
expect(classSymbol !.declaration.valueDeclaration).toBe(outerNode);
|
||||
expect(classSymbol !.implementation.valueDeclaration).toBe(innerNode);
|
||||
});
|
||||
|
||||
it('should return the same class symbol (of the outer declaration) for outer and inner declarations',
|
||||
() => {
|
||||
loadTestFiles([CLASS_EXPRESSION_FILE]);
|
||||
const {program} = makeTestBundleProgram(CLASS_EXPRESSION_FILE.name);
|
||||
const host =
|
||||
new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
|
||||
const outerNode = getDeclaration(
|
||||
program, CLASS_EXPRESSION_FILE.name, 'EmptyClass', isNamedVariableDeclaration);
|
||||
const innerNode = (outerNode.initializer as ts.ClassExpression);
|
||||
|
||||
const innerSymbol = host.getClassSymbol(innerNode) !;
|
||||
const outerSymbol = host.getClassSymbol(outerNode) !;
|
||||
expect(innerSymbol.declaration).toBe(outerSymbol.declaration);
|
||||
expect(innerSymbol.implementation).toBe(outerSymbol.implementation);
|
||||
});
|
||||
|
||||
it('should return undefined if node is not a class', () => {
|
||||
loadTestFiles([FOO_FUNCTION_FILE]);
|
||||
const {program} = makeTestBundleProgram(FOO_FUNCTION_FILE.name);
|
||||
const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
|
||||
const node =
|
||||
getDeclaration(program, FOO_FUNCTION_FILE.name, 'foo', isNamedFunctionDeclaration);
|
||||
const classSymbol = host.getClassSymbol(node);
|
||||
|
||||
expect(classSymbol).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined if variable declaration is not initialized using a class expression',
|
||||
() => {
|
||||
const testFile = {
|
||||
name: _('/test.js'),
|
||||
contents: `var MyClass = null;`,
|
||||
};
|
||||
loadTestFiles([testFile]);
|
||||
const {program} = makeTestBundleProgram(testFile.name);
|
||||
const host =
|
||||
new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
|
||||
const node =
|
||||
getDeclaration(program, testFile.name, 'MyClass', isNamedVariableDeclaration);
|
||||
const classSymbol = host.getClassSymbol(node);
|
||||
|
||||
expect(classSymbol).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isClass()', () => {
|
||||
it('should return true if a given node is a TS class declaration', () => {
|
||||
loadTestFiles([SIMPLE_CLASS_FILE]);
|
||||
|
Reference in New Issue
Block a user