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:
@ -13,7 +13,6 @@ import {ClassMemberKind, CtorParameter, Import, InlineDeclaration, isNamedClassD
|
||||
import {getDeclaration} from '../../../src/ngtsc/testing';
|
||||
import {loadFakeCore, loadTestFiles} from '../../../test/helpers';
|
||||
import {CommonJsReflectionHost} from '../../src/host/commonjs_host';
|
||||
import {Esm2015ReflectionHost} from '../../src/host/esm2015_host';
|
||||
import {getIifeBody} from '../../src/host/esm5_host';
|
||||
import {MockLogger} from '../helpers/mock_logger';
|
||||
import {getRootFiles, makeTestBundleProgram, makeTestDtsBundleProgram} from '../helpers/utils';
|
||||
@ -1712,19 +1711,22 @@ exports.ExternalModule = ExternalModule;
|
||||
const classSymbol = host.getClassSymbol(node);
|
||||
|
||||
expect(classSymbol).toBeDefined();
|
||||
expect(classSymbol !.valueDeclaration).toBe(node);
|
||||
expect(classSymbol !.declaration.valueDeclaration).toBe(node);
|
||||
expect(classSymbol !.implementation.valueDeclaration).toBe(node);
|
||||
});
|
||||
|
||||
it('should return the class symbol for an ES5 class (outer variable declaration)', () => {
|
||||
loadTestFiles([SIMPLE_CLASS_FILE]);
|
||||
const {program, host: compilerHost} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name);
|
||||
const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost);
|
||||
const node = getDeclaration(
|
||||
const outerNode = getDeclaration(
|
||||
program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration);
|
||||
const classSymbol = host.getClassSymbol(node);
|
||||
const innerNode = getIifeBody(outerNode) !.statements.find(isNamedFunctionDeclaration) !;
|
||||
const classSymbol = host.getClassSymbol(outerNode);
|
||||
|
||||
expect(classSymbol).toBeDefined();
|
||||
expect(classSymbol !.valueDeclaration).toBe(node);
|
||||
expect(classSymbol !.declaration.valueDeclaration).toBe(outerNode);
|
||||
expect(classSymbol !.implementation.valueDeclaration).toBe(innerNode);
|
||||
});
|
||||
|
||||
it('should return the class symbol for an ES5 class (inner function declaration)', () => {
|
||||
@ -1737,7 +1739,8 @@ exports.ExternalModule = ExternalModule;
|
||||
const classSymbol = host.getClassSymbol(innerNode);
|
||||
|
||||
expect(classSymbol).toBeDefined();
|
||||
expect(classSymbol !.valueDeclaration).toBe(outerNode);
|
||||
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',
|
||||
@ -1751,7 +1754,10 @@ exports.ExternalModule = ExternalModule;
|
||||
const innerNode =
|
||||
getIifeBody(outerNode) !.statements.find(isNamedFunctionDeclaration) !;
|
||||
|
||||
expect(host.getClassSymbol(innerNode)).toBe(host.getClassSymbol(outerNode));
|
||||
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 an ES5 class', () => {
|
||||
@ -1764,46 +1770,67 @@ exports.ExternalModule = ExternalModule;
|
||||
|
||||
expect(classSymbol).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined if variable declaration is not initialized using an IIFE',
|
||||
() => {
|
||||
const testFile = {
|
||||
name: _('/test.js'),
|
||||
contents: `var MyClass = null;`,
|
||||
};
|
||||
loadTestFiles([testFile]);
|
||||
const {program, host: compilerHost} = makeTestBundleProgram(testFile.name);
|
||||
const host =
|
||||
new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost);
|
||||
const node =
|
||||
getDeclaration(program, testFile.name, 'MyClass', isNamedVariableDeclaration);
|
||||
const classSymbol = host.getClassSymbol(node);
|
||||
|
||||
expect(classSymbol).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isClass()', () => {
|
||||
let host: CommonJsReflectionHost;
|
||||
let mockNode: ts.Node;
|
||||
let getClassDeclarationSpy: jasmine.Spy;
|
||||
let superGetClassDeclarationSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
loadTestFiles([SIMPLE_CLASS_FILE]);
|
||||
const {program, host: compilerHost} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name);
|
||||
host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost);
|
||||
mockNode = {} as any;
|
||||
|
||||
getClassDeclarationSpy = spyOn(CommonJsReflectionHost.prototype, 'getClassDeclaration');
|
||||
superGetClassDeclarationSpy =
|
||||
spyOn(Esm2015ReflectionHost.prototype, 'getClassDeclaration');
|
||||
it('should return true if a given node is a TS class declaration', () => {
|
||||
loadTestFiles([SIMPLE_ES2015_CLASS_FILE]);
|
||||
const {program, host: compilerHost} =
|
||||
makeTestBundleProgram(SIMPLE_ES2015_CLASS_FILE.name);
|
||||
const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost);
|
||||
const node = getDeclaration(
|
||||
program, SIMPLE_ES2015_CLASS_FILE.name, 'EmptyClass', isNamedClassDeclaration);
|
||||
expect(host.isClass(node)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true if superclass returns true', () => {
|
||||
superGetClassDeclarationSpy.and.returnValue(true);
|
||||
getClassDeclarationSpy.and.callThrough();
|
||||
it('should return true if a given node is the outer variable declaration of a class',
|
||||
() => {
|
||||
loadTestFiles([SIMPLE_CLASS_FILE]);
|
||||
const {program, host: compilerHost} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name);
|
||||
const host =
|
||||
new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost);
|
||||
const node = getDeclaration(
|
||||
program, SIMPLE_CLASS_FILE.name, 'EmptyClass', ts.isVariableDeclaration);
|
||||
expect(host.isClass(node)).toBe(true);
|
||||
});
|
||||
|
||||
expect(host.isClass(mockNode)).toBe(true);
|
||||
expect(getClassDeclarationSpy).toHaveBeenCalledWith(mockNode);
|
||||
expect(superGetClassDeclarationSpy).toHaveBeenCalledWith(mockNode);
|
||||
});
|
||||
it('should return true if a given node is the inner variable declaration of a class',
|
||||
() => {
|
||||
loadTestFiles([SIMPLE_CLASS_FILE]);
|
||||
const {program, host: compilerHost} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name);
|
||||
const host =
|
||||
new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost);
|
||||
const outerNode = getDeclaration(
|
||||
program, SIMPLE_CLASS_FILE.name, 'EmptyClass', ts.isVariableDeclaration);
|
||||
const innerNode =
|
||||
getIifeBody(outerNode) !.statements.find(isNamedFunctionDeclaration) !;
|
||||
expect(host.isClass(innerNode)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true if it can find a declaration for the class', () => {
|
||||
getClassDeclarationSpy.and.returnValue(true);
|
||||
|
||||
expect(host.isClass(mockNode)).toBe(true);
|
||||
expect(getClassDeclarationSpy).toHaveBeenCalledWith(mockNode);
|
||||
});
|
||||
|
||||
it('should return false if it cannot find a declaration for the class', () => {
|
||||
getClassDeclarationSpy.and.returnValue(false);
|
||||
|
||||
expect(host.isClass(mockNode)).toBe(false);
|
||||
expect(getClassDeclarationSpy).toHaveBeenCalledWith(mockNode);
|
||||
it('should return false if a given node is a function declaration', () => {
|
||||
loadTestFiles([FOO_FUNCTION_FILE]);
|
||||
const {program, host: compilerHost} = makeTestBundleProgram(FOO_FUNCTION_FILE.name);
|
||||
const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost);
|
||||
const node =
|
||||
getDeclaration(program, FOO_FUNCTION_FILE.name, 'foo', isNamedFunctionDeclaration);
|
||||
expect(host.isClass(node)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -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]);
|
||||
|
@ -1874,19 +1874,22 @@ runInEachFileSystem(() => {
|
||||
const classSymbol = host.getClassSymbol(node);
|
||||
|
||||
expect(classSymbol).toBeDefined();
|
||||
expect(classSymbol !.valueDeclaration).toBe(node);
|
||||
expect(classSymbol !.declaration.valueDeclaration).toBe(node);
|
||||
expect(classSymbol !.implementation.valueDeclaration).toBe(node);
|
||||
});
|
||||
|
||||
it('should return the class symbol for an ES5 class (outer variable declaration)', () => {
|
||||
loadTestFiles([SIMPLE_CLASS_FILE]);
|
||||
const {program} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name);
|
||||
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
|
||||
const node = getDeclaration(
|
||||
const outerNode = getDeclaration(
|
||||
program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration);
|
||||
const classSymbol = host.getClassSymbol(node);
|
||||
const innerNode = getIifeBody(outerNode) !.statements.find(isNamedFunctionDeclaration) !;
|
||||
const classSymbol = host.getClassSymbol(outerNode);
|
||||
|
||||
expect(classSymbol).toBeDefined();
|
||||
expect(classSymbol !.valueDeclaration).toBe(node);
|
||||
expect(classSymbol !.declaration.valueDeclaration).toBe(outerNode);
|
||||
expect(classSymbol !.implementation.valueDeclaration).toBe(innerNode);
|
||||
});
|
||||
|
||||
it('should return the class symbol for an ES5 class (inner function declaration)', () => {
|
||||
@ -1899,7 +1902,8 @@ runInEachFileSystem(() => {
|
||||
const classSymbol = host.getClassSymbol(innerNode);
|
||||
|
||||
expect(classSymbol).toBeDefined();
|
||||
expect(classSymbol !.valueDeclaration).toBe(outerNode);
|
||||
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',
|
||||
@ -1911,7 +1915,10 @@ runInEachFileSystem(() => {
|
||||
program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration);
|
||||
const innerNode = getIifeBody(outerNode) !.statements.find(isNamedFunctionDeclaration) !;
|
||||
|
||||
expect(host.getClassSymbol(innerNode)).toBe(host.getClassSymbol(outerNode));
|
||||
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 an ES5 class', () => {
|
||||
@ -1924,43 +1931,58 @@ runInEachFileSystem(() => {
|
||||
|
||||
expect(classSymbol).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined if variable declaration is not initialized using an IIFE', () => {
|
||||
const testFile = {
|
||||
name: _('/test.js'),
|
||||
contents: `var MyClass = null;`,
|
||||
};
|
||||
loadTestFiles([testFile]);
|
||||
const {program} = makeTestBundleProgram(testFile.name);
|
||||
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
|
||||
const node = getDeclaration(program, testFile.name, 'MyClass', isNamedVariableDeclaration);
|
||||
const classSymbol = host.getClassSymbol(node);
|
||||
|
||||
expect(classSymbol).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isClass()', () => {
|
||||
let host: Esm5ReflectionHost;
|
||||
let mockNode: ts.Node;
|
||||
let getClassDeclarationSpy: jasmine.Spy;
|
||||
let superGetClassDeclarationSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
host = new Esm5ReflectionHost(new MockLogger(), false, null as any);
|
||||
mockNode = {} as any;
|
||||
|
||||
getClassDeclarationSpy = spyOn(Esm5ReflectionHost.prototype, 'getClassDeclaration');
|
||||
superGetClassDeclarationSpy = spyOn(Esm2015ReflectionHost.prototype, 'getClassDeclaration');
|
||||
it('should return true if a given node is a TS class declaration', () => {
|
||||
loadTestFiles([SIMPLE_ES2015_CLASS_FILE]);
|
||||
const {program} = makeTestBundleProgram(SIMPLE_ES2015_CLASS_FILE.name);
|
||||
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
|
||||
const node = getDeclaration(
|
||||
program, SIMPLE_ES2015_CLASS_FILE.name, 'EmptyClass', isNamedClassDeclaration);
|
||||
expect(host.isClass(node)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true if superclass returns true', () => {
|
||||
superGetClassDeclarationSpy.and.returnValue(true);
|
||||
getClassDeclarationSpy.and.callThrough();
|
||||
|
||||
expect(host.isClass(mockNode)).toBe(true);
|
||||
expect(getClassDeclarationSpy).toHaveBeenCalledWith(mockNode);
|
||||
expect(superGetClassDeclarationSpy).toHaveBeenCalledWith(mockNode);
|
||||
it('should return true if a given node is the outer variable declaration of a class', () => {
|
||||
loadTestFiles([SIMPLE_CLASS_FILE]);
|
||||
const {program} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name);
|
||||
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
|
||||
const node =
|
||||
getDeclaration(program, SIMPLE_CLASS_FILE.name, 'EmptyClass', ts.isVariableDeclaration);
|
||||
expect(host.isClass(node)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true if it can find a declaration for the class', () => {
|
||||
getClassDeclarationSpy.and.returnValue(true);
|
||||
|
||||
expect(host.isClass(mockNode)).toBe(true);
|
||||
expect(getClassDeclarationSpy).toHaveBeenCalledWith(mockNode);
|
||||
it('should return true if a given node is the inner variable declaration of a class', () => {
|
||||
loadTestFiles([SIMPLE_CLASS_FILE]);
|
||||
const {program} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name);
|
||||
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
|
||||
const outerNode =
|
||||
getDeclaration(program, SIMPLE_CLASS_FILE.name, 'EmptyClass', ts.isVariableDeclaration);
|
||||
const innerNode = getIifeBody(outerNode) !.statements.find(isNamedFunctionDeclaration) !;
|
||||
expect(host.isClass(innerNode)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if it cannot find a declaration for the class', () => {
|
||||
getClassDeclarationSpy.and.returnValue(false);
|
||||
|
||||
expect(host.isClass(mockNode)).toBe(false);
|
||||
expect(getClassDeclarationSpy).toHaveBeenCalledWith(mockNode);
|
||||
it('should return false if a given node is a function declaration', () => {
|
||||
loadTestFiles([FOO_FUNCTION_FILE]);
|
||||
const {program} = makeTestBundleProgram(FOO_FUNCTION_FILE.name);
|
||||
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
|
||||
const node =
|
||||
getDeclaration(program, FOO_FUNCTION_FILE.name, 'foo', isNamedFunctionDeclaration);
|
||||
expect(host.isClass(node)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1783,19 +1783,22 @@ runInEachFileSystem(() => {
|
||||
const classSymbol = host.getClassSymbol(node);
|
||||
|
||||
expect(classSymbol).toBeDefined();
|
||||
expect(classSymbol !.valueDeclaration).toBe(node);
|
||||
expect(classSymbol !.declaration.valueDeclaration).toBe(node);
|
||||
expect(classSymbol !.implementation.valueDeclaration).toBe(node);
|
||||
});
|
||||
|
||||
it('should return the class symbol for an ES5 class (outer variable declaration)', () => {
|
||||
loadTestFiles([SIMPLE_CLASS_FILE]);
|
||||
const {program, host: compilerHost} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name);
|
||||
const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost);
|
||||
const node = getDeclaration(
|
||||
const outerNode = getDeclaration(
|
||||
program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration);
|
||||
const classSymbol = host.getClassSymbol(node);
|
||||
const innerNode = getIifeBody(outerNode) !.statements.find(isNamedFunctionDeclaration) !;
|
||||
const classSymbol = host.getClassSymbol(outerNode);
|
||||
|
||||
expect(classSymbol).toBeDefined();
|
||||
expect(classSymbol !.valueDeclaration).toBe(node);
|
||||
expect(classSymbol !.declaration.valueDeclaration).toBe(outerNode);
|
||||
expect(classSymbol !.implementation.valueDeclaration).toBe(innerNode);
|
||||
});
|
||||
|
||||
it('should return the class symbol for an ES5 class (inner function declaration)', () => {
|
||||
@ -1808,7 +1811,8 @@ runInEachFileSystem(() => {
|
||||
const classSymbol = host.getClassSymbol(innerNode);
|
||||
|
||||
expect(classSymbol).toBeDefined();
|
||||
expect(classSymbol !.valueDeclaration).toBe(outerNode);
|
||||
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',
|
||||
@ -1821,7 +1825,10 @@ runInEachFileSystem(() => {
|
||||
const innerNode =
|
||||
getIifeBody(outerNode) !.statements.find(isNamedFunctionDeclaration) !;
|
||||
|
||||
expect(host.getClassSymbol(innerNode)).toBe(host.getClassSymbol(outerNode));
|
||||
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 an ES5 class', () => {
|
||||
@ -1834,46 +1841,64 @@ runInEachFileSystem(() => {
|
||||
|
||||
expect(classSymbol).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined if variable declaration is not initialized using an IIFE',
|
||||
() => {
|
||||
const testFile = {
|
||||
name: _('/test.js'),
|
||||
contents: `var MyClass = null;`,
|
||||
};
|
||||
loadTestFiles([testFile]);
|
||||
const {program, host: compilerHost} = makeTestBundleProgram(testFile.name);
|
||||
const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost);
|
||||
const node =
|
||||
getDeclaration(program, testFile.name, 'MyClass', isNamedVariableDeclaration);
|
||||
const classSymbol = host.getClassSymbol(node);
|
||||
|
||||
expect(classSymbol).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isClass()', () => {
|
||||
let host: UmdReflectionHost;
|
||||
let mockNode: ts.Node;
|
||||
let getClassDeclarationSpy: jasmine.Spy;
|
||||
let superGetClassDeclarationSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
loadTestFiles([SIMPLE_CLASS_FILE]);
|
||||
const {program, host: compilerHost} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name);
|
||||
host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost);
|
||||
mockNode = {} as any;
|
||||
|
||||
getClassDeclarationSpy = spyOn(UmdReflectionHost.prototype, 'getClassDeclaration');
|
||||
superGetClassDeclarationSpy =
|
||||
spyOn(Esm2015ReflectionHost.prototype, 'getClassDeclaration');
|
||||
it('should return true if a given node is a TS class declaration', () => {
|
||||
loadTestFiles([SIMPLE_ES2015_CLASS_FILE]);
|
||||
const {program, host: compilerHost} =
|
||||
makeTestBundleProgram(SIMPLE_ES2015_CLASS_FILE.name);
|
||||
const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost);
|
||||
const node = getDeclaration(
|
||||
program, SIMPLE_ES2015_CLASS_FILE.name, 'EmptyClass', isNamedClassDeclaration);
|
||||
expect(host.isClass(node)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true if superclass returns true', () => {
|
||||
superGetClassDeclarationSpy.and.returnValue(true);
|
||||
getClassDeclarationSpy.and.callThrough();
|
||||
it('should return true if a given node is the outer variable declaration of a class',
|
||||
() => {
|
||||
loadTestFiles([SIMPLE_CLASS_FILE]);
|
||||
const {program, host: compilerHost} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name);
|
||||
const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost);
|
||||
const node = getDeclaration(
|
||||
program, SIMPLE_CLASS_FILE.name, 'EmptyClass', ts.isVariableDeclaration);
|
||||
expect(host.isClass(node)).toBe(true);
|
||||
});
|
||||
|
||||
expect(host.isClass(mockNode)).toBe(true);
|
||||
expect(getClassDeclarationSpy).toHaveBeenCalledWith(mockNode);
|
||||
expect(superGetClassDeclarationSpy).toHaveBeenCalledWith(mockNode);
|
||||
});
|
||||
it('should return true if a given node is the inner variable declaration of a class',
|
||||
() => {
|
||||
loadTestFiles([SIMPLE_CLASS_FILE]);
|
||||
const {program, host: compilerHost} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name);
|
||||
const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost);
|
||||
const outerNode = getDeclaration(
|
||||
program, SIMPLE_CLASS_FILE.name, 'EmptyClass', ts.isVariableDeclaration);
|
||||
const innerNode =
|
||||
getIifeBody(outerNode) !.statements.find(isNamedFunctionDeclaration) !;
|
||||
expect(host.isClass(innerNode)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true if it can find a declaration for the class', () => {
|
||||
getClassDeclarationSpy.and.returnValue(true);
|
||||
|
||||
expect(host.isClass(mockNode)).toBe(true);
|
||||
expect(getClassDeclarationSpy).toHaveBeenCalledWith(mockNode);
|
||||
});
|
||||
|
||||
it('should return false if it cannot find a declaration for the class', () => {
|
||||
getClassDeclarationSpy.and.returnValue(false);
|
||||
|
||||
expect(host.isClass(mockNode)).toBe(false);
|
||||
expect(getClassDeclarationSpy).toHaveBeenCalledWith(mockNode);
|
||||
it('should return false if a given node is a function declaration', () => {
|
||||
loadTestFiles([FOO_FUNCTION_FILE]);
|
||||
const {program, host: compilerHost} = makeTestBundleProgram(FOO_FUNCTION_FILE.name);
|
||||
const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost);
|
||||
const node =
|
||||
getDeclaration(program, FOO_FUNCTION_FILE.name, 'foo', isNamedFunctionDeclaration);
|
||||
expect(host.isClass(node)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
Reference in New Issue
Block a user