From 93a23b9ae0b0b609af764c4b44f1cbec4e69330b Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Fri, 1 Nov 2019 16:55:10 +0000 Subject: [PATCH] fix(ngcc): override `getInternalNameOfClass()` and `getAdjacentNameOfClass()` for ES5 (#33533) In ES5 the class consists of an outer variable declaration that is initialised by an IIFE. Inside the IIFE the class is implemented by an inner function declaration that is returned from the IIFE. This inner declaration may have a different name to the outer declaration. This commit overrides `getInternalNameOfClass()` and `getAdjacentNameOfClass()` in `Esm5ReflectionHost` with methods that can find the correct inner declaration name identifier. PR Close #33533 --- .../compiler-cli/ngcc/src/host/esm5_host.ts | 17 +++++ .../ngcc/test/host/commonjs_host_spec.ts | 66 +++++++++++++++++++ .../ngcc/test/host/esm2015_host_spec.ts | 22 +++++++ .../ngcc/test/host/esm5_host_spec.ts | 66 +++++++++++++++++++ .../ngcc/test/host/umd_host_spec.ts | 66 +++++++++++++++++++ 5 files changed, 237 insertions(+) diff --git a/packages/compiler-cli/ngcc/src/host/esm5_host.ts b/packages/compiler-cli/ngcc/src/host/esm5_host.ts index 18570124d5643..e8d6754588ee7 100644 --- a/packages/compiler-cli/ngcc/src/host/esm5_host.ts +++ b/packages/compiler-cli/ngcc/src/host/esm5_host.ts @@ -86,6 +86,23 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost { return iife.parent.arguments[0]; } + getInternalNameOfClass(clazz: ClassDeclaration): ts.Identifier { + const innerClass = this.getInnerFunctionDeclarationFromClassDeclaration(clazz); + if (innerClass === undefined) { + throw new Error( + `getInternalNameOfClass() called on a non-ES5 class: expected ${clazz.name.text} to have an inner class declaration`); + } + if (innerClass.name === undefined) { + throw new Error( + `getInternalNameOfClass() called on a class with an anonymous inner declaration: expected a name on:\n${innerClass.getText()}`); + } + return innerClass.name; + } + + getAdjacentNameOfClass(clazz: ClassDeclaration): ts.Identifier { + return this.getInternalNameOfClass(clazz); + } + /** * In ES5, the implementation of a class is a function expression that is hidden inside an IIFE, * whose value is assigned to a variable (which represents the class to the rest of the program). diff --git a/packages/compiler-cli/ngcc/test/host/commonjs_host_spec.ts b/packages/compiler-cli/ngcc/test/host/commonjs_host_spec.ts index 72f01d82c8243..ed755b25d0814 100644 --- a/packages/compiler-cli/ngcc/test/host/commonjs_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/commonjs_host_spec.ts @@ -147,6 +147,24 @@ var NoDecoratorConstructorClass = (function() { } return NoDecoratorConstructorClass; }()); +var OuterClass1 = (function() { + function InnerClass1() { + } + return InnerClass1; +}()); +var OuterClass2 = (function() { + function InnerClass2() { + } + InnerClass2_1 = InnerClass12 + var InnerClass2_1; + return InnerClass2; +}()); +var SuperClass = (function() { function SuperClass() {} return SuperClass; }()); +var ChildClass = /** @class */ (function (_super) { + __extends(ChildClass, _super); + function InnerChildClass() {} + return InnerChildClass; +}(SuperClass); exports.EmptyClass = EmptyClass; exports.NoDecoratorConstructorClass = NoDecoratorConstructorClass; `, @@ -2212,6 +2230,54 @@ exports.ExternalModule = ExternalModule; }); }); + describe('getInternalNameOfClass()', () => { + it('should return the name of the inner class declaration', () => { + loadTestFiles([SIMPLE_CLASS_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + + const emptyClass = getDeclaration( + program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration); + expect(host.getInternalNameOfClass(emptyClass).text).toEqual('EmptyClass'); + + const class1 = getDeclaration( + program, SIMPLE_CLASS_FILE.name, 'OuterClass1', isNamedVariableDeclaration); + expect(host.getInternalNameOfClass(class1).text).toEqual('InnerClass1'); + + const class2 = getDeclaration( + program, SIMPLE_CLASS_FILE.name, 'OuterClass2', isNamedVariableDeclaration); + expect(host.getInternalNameOfClass(class2).text).toEqual('InnerClass2'); + + const childClass = getDeclaration( + program, SIMPLE_CLASS_FILE.name, 'ChildClass', isNamedVariableDeclaration); + expect(host.getInternalNameOfClass(childClass).text).toEqual('InnerChildClass'); + }); + }); + + describe('getAdjacentNameOfClass()', () => { + it('should return the name of the inner class declaration', () => { + loadTestFiles([SIMPLE_CLASS_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + + const emptyClass = getDeclaration( + program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration); + expect(host.getAdjacentNameOfClass(emptyClass).text).toEqual('EmptyClass'); + + const class1 = getDeclaration( + program, SIMPLE_CLASS_FILE.name, 'OuterClass1', isNamedVariableDeclaration); + expect(host.getAdjacentNameOfClass(class1).text).toEqual('InnerClass1'); + + const class2 = getDeclaration( + program, SIMPLE_CLASS_FILE.name, 'OuterClass2', isNamedVariableDeclaration); + expect(host.getAdjacentNameOfClass(class2).text).toEqual('InnerClass2'); + + const childClass = getDeclaration( + program, SIMPLE_CLASS_FILE.name, 'ChildClass', isNamedVariableDeclaration); + expect(host.getAdjacentNameOfClass(childClass).text).toEqual('InnerChildClass'); + }); + }); + describe('getModuleWithProvidersFunctions', () => { it('should find every exported function that returns an object that looks like a ModuleWithProviders object', () => { diff --git a/packages/compiler-cli/ngcc/test/host/esm2015_host_spec.ts b/packages/compiler-cli/ngcc/test/host/esm2015_host_spec.ts index f17eea2e9dc77..b4f7609ba2fe3 100644 --- a/packages/compiler-cli/ngcc/test/host/esm2015_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/esm2015_host_spec.ts @@ -2088,6 +2088,28 @@ runInEachFileSystem(() => { }); }); + describe('getInternalNameOfClass()', () => { + it('should return the name of the class (there is no separate inner class in ES2015)', () => { + 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); + expect(host.getInternalNameOfClass(node).text).toEqual('EmptyClass'); + }); + }); + + describe('getAdjacentNameOfClass()', () => { + it('should return the name of the class (there is no separate inner class in ES2015)', () => { + 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); + expect(host.getAdjacentNameOfClass(node).text).toEqual('EmptyClass'); + }); + }); + describe('getModuleWithProvidersFunctions()', () => { it('should find every exported function that returns an object that looks like a ModuleWithProviders object', () => { diff --git a/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts b/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts index d46e6a0639295..16f1b9b5c5736 100644 --- a/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts @@ -192,6 +192,24 @@ runInEachFileSystem(() => { } return NoDecoratorConstructorClass; }()); + var OuterClass1 = (function() { + function InnerClass1() { + } + return InnerClass1; + }()); + var OuterClass2 = (function() { + function InnerClass2() { + } + InnerClass2_1 = InnerClass12 + var InnerClass2_1; + return InnerClass2; + }()); + var SuperClass = (function() { function SuperClass() {} return SuperClass; }()); + var ChildClass = /** @class */ (function (_super) { + __extends(ChildClass, _super); + function InnerChildClass() {} + return InnerChildClass; + }(SuperClass); `, }; @@ -2331,6 +2349,54 @@ runInEachFileSystem(() => { }); }); + describe('getInternalNameOfClass()', () => { + it('should return the name of the inner class declaration', () => { + loadTestFiles([SIMPLE_CLASS_FILE]); + const {program} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + + const emptyClass = getDeclaration( + program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration); + expect(host.getInternalNameOfClass(emptyClass).text).toEqual('EmptyClass'); + + const class1 = getDeclaration( + program, SIMPLE_CLASS_FILE.name, 'OuterClass1', isNamedVariableDeclaration); + expect(host.getInternalNameOfClass(class1).text).toEqual('InnerClass1'); + + const class2 = getDeclaration( + program, SIMPLE_CLASS_FILE.name, 'OuterClass2', isNamedVariableDeclaration); + expect(host.getInternalNameOfClass(class2).text).toEqual('InnerClass2'); + + const childClass = getDeclaration( + program, SIMPLE_CLASS_FILE.name, 'ChildClass', isNamedVariableDeclaration); + expect(host.getInternalNameOfClass(childClass).text).toEqual('InnerChildClass'); + }); + }); + + describe('getAdjacentNameOfClass()', () => { + it('should return the name of the inner class declaration', () => { + loadTestFiles([SIMPLE_CLASS_FILE]); + const {program} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + + const emptyClass = getDeclaration( + program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration); + expect(host.getAdjacentNameOfClass(emptyClass).text).toEqual('EmptyClass'); + + const class1 = getDeclaration( + program, SIMPLE_CLASS_FILE.name, 'OuterClass1', isNamedVariableDeclaration); + expect(host.getAdjacentNameOfClass(class1).text).toEqual('InnerClass1'); + + const class2 = getDeclaration( + program, SIMPLE_CLASS_FILE.name, 'OuterClass2', isNamedVariableDeclaration); + expect(host.getAdjacentNameOfClass(class2).text).toEqual('InnerClass2'); + + const childClass = getDeclaration( + program, SIMPLE_CLASS_FILE.name, 'ChildClass', isNamedVariableDeclaration); + expect(host.getAdjacentNameOfClass(childClass).text).toEqual('InnerChildClass'); + }); + }); + describe('getModuleWithProvidersFunctions', () => { it('should find every exported function that returns an object that looks like a ModuleWithProviders object', () => { diff --git a/packages/compiler-cli/ngcc/test/host/umd_host_spec.ts b/packages/compiler-cli/ngcc/test/host/umd_host_spec.ts index 52a5ec58b9cbd..51349c598f312 100644 --- a/packages/compiler-cli/ngcc/test/host/umd_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/umd_host_spec.ts @@ -163,6 +163,24 @@ runInEachFileSystem(() => { } return NoDecoratorConstructorClass; }()); + var OuterClass1 = (function() { + function InnerClass1() { + } + return InnerClass1; + }()); + var OuterClass2 = (function() { + function InnerClass2() { + } + InnerClass2_1 = InnerClass12 + var InnerClass2_1; + return InnerClass2; + }()); + var SuperClass = (function() { function SuperClass() {} return SuperClass; }()); + var ChildClass = /** @class */ (function (_super) { + __extends(ChildClass, _super); + function InnerChildClass() {} + return InnerChildClass; + }(SuperClass); exports.EmptyClass = EmptyClass; exports.NoDecoratorConstructorClass = NoDecoratorConstructorClass; })));`, @@ -2221,6 +2239,54 @@ runInEachFileSystem(() => { }); }); + describe('getInternalNameOfClass()', () => { + it('should return the name of the inner class declaration', () => { + loadTestFiles([SIMPLE_CLASS_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + + const emptyClass = getDeclaration( + program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration); + expect(host.getInternalNameOfClass(emptyClass).text).toEqual('EmptyClass'); + + const class1 = getDeclaration( + program, SIMPLE_CLASS_FILE.name, 'OuterClass1', isNamedVariableDeclaration); + expect(host.getInternalNameOfClass(class1).text).toEqual('InnerClass1'); + + const class2 = getDeclaration( + program, SIMPLE_CLASS_FILE.name, 'OuterClass2', isNamedVariableDeclaration); + expect(host.getInternalNameOfClass(class2).text).toEqual('InnerClass2'); + + const childClass = getDeclaration( + program, SIMPLE_CLASS_FILE.name, 'ChildClass', isNamedVariableDeclaration); + expect(host.getInternalNameOfClass(childClass).text).toEqual('InnerChildClass'); + }); + }); + + describe('getAdjacentNameOfClass()', () => { + it('should return the name of the inner class declaration', () => { + loadTestFiles([SIMPLE_CLASS_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + + const emptyClass = getDeclaration( + program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration); + expect(host.getAdjacentNameOfClass(emptyClass).text).toEqual('EmptyClass'); + + const class1 = getDeclaration( + program, SIMPLE_CLASS_FILE.name, 'OuterClass1', isNamedVariableDeclaration); + expect(host.getAdjacentNameOfClass(class1).text).toEqual('InnerClass1'); + + const class2 = getDeclaration( + program, SIMPLE_CLASS_FILE.name, 'OuterClass2', isNamedVariableDeclaration); + expect(host.getAdjacentNameOfClass(class2).text).toEqual('InnerClass2'); + + const childClass = getDeclaration( + program, SIMPLE_CLASS_FILE.name, 'ChildClass', isNamedVariableDeclaration); + expect(host.getAdjacentNameOfClass(childClass).text).toEqual('InnerChildClass'); + }); + }); + describe('getModuleWithProvidersFunctions', () => { it('should find every exported function that returns an object that looks like a ModuleWithProviders object', () => {