From 8a470b9af9accaa9355c947ec566e8bd39f95266 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Thu, 18 Jul 2019 21:05:31 +0100 Subject: [PATCH] feat(ivy): add `getBaseClassIdentifier()` to `ReflectionHost` (#31544) This method will be useful for writing ngcc `Migrations` that need to be able to find base classes. PR Close #31544 --- .../ngcc/src/host/esm2015_host.ts | 18 +++- .../compiler-cli/ngcc/src/host/esm5_host.ts | 26 ++++++ .../ngcc/test/host/commonjs_host_spec.ts | 89 +++++++++++++++++++ .../ngcc/test/host/esm2015_host_spec.ts | 71 +++++++++++++++ .../ngcc/test/host/esm5_host_spec.ts | 89 +++++++++++++++++++ .../ngcc/test/host/umd_host_spec.ts | 89 +++++++++++++++++++ .../src/ngtsc/reflection/src/host.ts | 10 +++ .../src/ngtsc/reflection/src/typescript.ts | 20 ++++- 8 files changed, 408 insertions(+), 4 deletions(-) diff --git a/packages/compiler-cli/ngcc/src/host/esm2015_host.ts b/packages/compiler-cli/ngcc/src/host/esm2015_host.ts index fea902b793..7ac8a2bf91 100644 --- a/packages/compiler-cli/ngcc/src/host/esm2015_host.ts +++ b/packages/compiler-cli/ngcc/src/host/esm2015_host.ts @@ -190,9 +190,21 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N return false; } - return innerClassDeclaration.heritageClauses !== undefined && - innerClassDeclaration.heritageClauses.some( - clause => clause.token === ts.SyntaxKind.ExtendsKeyword); + return super.hasBaseClass(innerClassDeclaration); + } + + getBaseClassExpression(clazz: ClassDeclaration): ts.Expression|null { + // First try getting the base class from the "outer" declaration + const superBaseClassIdentifier = super.getBaseClassExpression(clazz); + if (superBaseClassIdentifier) { + return superBaseClassIdentifier; + } + // That didn't work so now try getting it from the "inner" declaration. + const innerClassDeclaration = getInnerClassDeclaration(clazz); + if (innerClassDeclaration === null) { + return null; + } + return super.getBaseClassExpression(innerClassDeclaration); } /** diff --git a/packages/compiler-cli/ngcc/src/host/esm5_host.ts b/packages/compiler-cli/ngcc/src/host/esm5_host.ts index 2bf6960f0f..a18a30bd4e 100644 --- a/packages/compiler-cli/ngcc/src/host/esm5_host.ts +++ b/packages/compiler-cli/ngcc/src/host/esm5_host.ts @@ -56,6 +56,32 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost { return iife.parameters.length === 1 && isSuperIdentifier(iife.parameters[0].name); } + getBaseClassExpression(clazz: ClassDeclaration): ts.Expression|null { + const superBaseClassIdentifier = super.getBaseClassExpression(clazz); + if (superBaseClassIdentifier) { + return superBaseClassIdentifier; + } + + const classDeclaration = this.getClassDeclaration(clazz); + if (!classDeclaration) return null; + + const iifeBody = getIifeBody(classDeclaration); + if (!iifeBody) return null; + + const iife = iifeBody.parent; + if (!iife || !ts.isFunctionExpression(iife)) return null; + + if (iife.parameters.length !== 1 || !isSuperIdentifier(iife.parameters[0].name)) { + return null; + } + + if (!ts.isCallExpression(iife.parent)) { + return null; + } + + return iife.parent.arguments[0]; + } + /** * Find the declaration of a class given a node that we think represents the class. * 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 0e9145bf98..620ca1014e 100644 --- a/packages/compiler-cli/ngcc/test/host/commonjs_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/commonjs_host_spec.ts @@ -1855,6 +1855,95 @@ exports.ExternalModule = ExternalModule; }); }); + describe('getBaseClassExpression()', () => { + function getBaseClassIdentifier(source: string): ts.Identifier|null { + const file = { + name: _('/synthesized_constructors.js'), + contents: source, + }; + + loadTestFiles([file]); + const {program, host: compilerHost} = makeTestBundleProgram(file.name); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = + getDeclaration(program, file.name, 'TestClass', isNamedVariableDeclaration); + const expression = host.getBaseClassExpression(classNode); + if (expression !== null && !ts.isIdentifier(expression)) { + throw new Error( + 'Expected class to inherit via an identifier but got: ' + expression.getText()); + } + return expression; + } + + it('should find the base class of an IIFE with _super parameter', () => { + const identifier = getBaseClassIdentifier(` + var BaseClass = /** @class */ (function () { + function BaseClass() {} + return BaseClass; + }()); + var TestClass = /** @class */ (function (_super) { + __extends(TestClass, _super); + function TestClass() {} + return TestClass; + }(BaseClass));`); + expect(identifier !.text).toBe('BaseClass'); + }); + + it('should find the base class of an IIFE with a unique name generated for the _super parameter', + () => { + const identifier = getBaseClassIdentifier(` + var BaseClass = /** @class */ (function () { + function BaseClass() {} + return BaseClass; + }()); + var TestClass = /** @class */ (function (_super_1) { + __extends(TestClass, _super_1); + function TestClass() {} + return TestClass; + }(BaseClass));`); + expect(identifier !.text).toBe('BaseClass'); + }); + + it('should not find a base class for an IIFE without parameter', () => { + const identifier = getBaseClassIdentifier(` + var BaseClass = /** @class */ (function () { + function BaseClass() {} + return BaseClass; + }()); + var TestClass = /** @class */ (function () { + __extends(TestClass, _super); + function TestClass() {} + return TestClass; + }(BaseClass));`); + expect(identifier).toBe(null); + }); + + it('should find a dynamic base class expression of an IIFE', () => { + const file = { + name: _('/synthesized_constructors.js'), + contents: ` + var BaseClass = /** @class */ (function () { + function BaseClass() {} + return BaseClass; + }()); + function foo() { return BaseClass; } + var TestClass = /** @class */ (function (_super) { + __extends(TestClass, _super); + function TestClass() {} + return TestClass; + }(foo()));`, + }; + + loadTestFiles([file]); + const {program, host: compilerHost} = makeTestBundleProgram(file.name); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = + getDeclaration(program, file.name, 'TestClass', isNamedVariableDeclaration); + const expression = host.getBaseClassExpression(classNode) !; + expect(expression.getText()).toBe('foo()'); + }); + }); + describe('findClassSymbols()', () => { it('should return an array of all classes in the given source file', () => { loadTestFiles(DECORATED_FILES); 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 505cfda15c..2c5c507ad8 100644 --- a/packages/compiler-cli/ngcc/test/host/esm2015_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/esm2015_host_spec.ts @@ -1680,6 +1680,77 @@ runInEachFileSystem(() => { }); }); + describe('getBaseClassExpression()', () => { + it('should not consider a class without extends clause as having a base class', () => { + const file = { + name: _('/base_class.js'), + contents: `class TestClass {}`, + }; + loadTestFiles([file]); + const {program} = makeTestBundleProgram(file.name); + const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration(program, file.name, 'TestClass', isNamedClassDeclaration); + expect(host.getBaseClassExpression(classNode)).toBe(null); + }); + + it('should find the base class of a class with an `extends` clause', () => { + const file = { + name: _('/base_class.js'), + contents: ` + class BaseClass {} + class TestClass extends BaseClass {}`, + }; + loadTestFiles([file]); + const {program} = makeTestBundleProgram(file.name); + const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration(program, file.name, 'TestClass', isNamedClassDeclaration); + const baseIdentifier = host.getBaseClassExpression(classNode) !; + if (!ts.isIdentifier(baseIdentifier)) { + throw new Error(`Expected ${baseIdentifier.getText()} to be an identifier.`); + } + expect(baseIdentifier.text).toEqual('BaseClass'); + }); + + it('should find the base class of an aliased class with an `extends` clause', () => { + const file = { + name: _('/base_class.js'), + contents: ` + let TestClass_1; + class BaseClass {} + let TestClass = TestClass_1 = class TestClass extends BaseClass {}`, + }; + loadTestFiles([file]); + const {program} = makeTestBundleProgram(file.name); + const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = + getDeclaration(program, file.name, 'TestClass', isNamedVariableDeclaration); + const baseIdentifier = host.getBaseClassExpression(classNode) !; + if (!ts.isIdentifier(baseIdentifier)) { + throw new Error(`Expected ${baseIdentifier.getText()} to be an identifier.`); + } + expect(baseIdentifier.text).toEqual('BaseClass'); + }); + + it('should find the base class expression of a class with a dynamic `extends` expression', + () => { + const file = { + name: _('/base_class.js'), + contents: ` + class BaseClass {} + function foo() { return BaseClass; } + class TestClass extends foo() {}`, + }; + loadTestFiles([file]); + const {program} = makeTestBundleProgram(file.name); + const host = + new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = + getDeclaration(program, file.name, 'TestClass', isNamedClassDeclaration); + const baseExpression = host.getBaseClassExpression(classNode) !; + expect(baseExpression.getText()).toEqual('foo()'); + }); + }); + describe('getGenericArityOfClass()', () => { it('should properly count type parameters', () => { loadTestFiles(ARITY_CLASSES); 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 3b2e34b027..a898b190cb 100644 --- a/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts @@ -2022,6 +2022,95 @@ runInEachFileSystem(() => { }); }); + describe('getBaseClassExpression()', () => { + function getBaseClassIdentifier(source: string): ts.Identifier|null { + const file = { + name: _('/synthesized_constructors.js'), + contents: source, + }; + + loadTestFiles([file]); + const {program} = makeTestBundleProgram(file.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = + getDeclaration(program, file.name, 'TestClass', isNamedVariableDeclaration); + const expression = host.getBaseClassExpression(classNode); + if (expression !== null && !ts.isIdentifier(expression)) { + throw new Error( + 'Expected class to inherit via an identifier but got: ' + expression.getText()); + } + return expression; + } + + it('should find the base class of an IIFE with _super parameter', () => { + const identifier = getBaseClassIdentifier(` + var BaseClass = /** @class */ (function () { + function BaseClass() {} + return BaseClass; + }()); + var TestClass = /** @class */ (function (_super) { + __extends(TestClass, _super); + function TestClass() {} + return TestClass; + }(BaseClass));`); + expect(identifier !.text).toBe('BaseClass'); + }); + + it('should find the base class of an IIFE with a unique name generated for the _super parameter', + () => { + const identifier = getBaseClassIdentifier(` + var BaseClass = /** @class */ (function () { + function BaseClass() {} + return BaseClass; + }()); + var TestClass = /** @class */ (function (_super_1) { + __extends(TestClass, _super_1); + function TestClass() {} + return TestClass; + }(BaseClass));`); + expect(identifier !.text).toBe('BaseClass'); + }); + + it('should not find a base class for an IIFE without parameter', () => { + const identifier = getBaseClassIdentifier(` + var BaseClass = /** @class */ (function () { + function BaseClass() {} + return BaseClass; + }()); + var TestClass = /** @class */ (function () { + __extends(TestClass, _super); + function TestClass() {} + return TestClass; + }(BaseClass));`); + expect(identifier).toBe(null); + }); + + it('should find a dynamic base class expression of an IIFE', () => { + const file = { + name: _('/synthesized_constructors.js'), + contents: ` + var BaseClass = /** @class */ (function () { + function BaseClass() {} + return BaseClass; + }()); + function foo() { return BaseClass; } + var TestClass = /** @class */ (function (_super) { + __extends(TestClass, _super); + function TestClass() {} + return TestClass; + }(foo()));`, + }; + + loadTestFiles([file]); + const {program} = makeTestBundleProgram(file.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = + getDeclaration(program, file.name, 'TestClass', isNamedVariableDeclaration); + const expression = host.getBaseClassExpression(classNode) !; + expect(expression.getText()).toBe('foo()'); + }); + }); + describe('findClassSymbols()', () => { it('should return an array of all classes in the given source file', () => { loadTestFiles(DECORATED_FILES); 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 74bbce8dfc..c62815f997 100644 --- a/packages/compiler-cli/ngcc/test/host/umd_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/umd_host_spec.ts @@ -1934,6 +1934,95 @@ runInEachFileSystem(() => { }); }); + describe('getBaseClassExpression()', () => { + function getBaseClassIdentifier(source: string): ts.Identifier|null { + const file = { + name: _('/synthesized_constructors.js'), + contents: source, + }; + + loadTestFiles([file]); + const {program, host: compilerHost} = makeTestBundleProgram(file.name); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = + getDeclaration(program, file.name, 'TestClass', isNamedVariableDeclaration); + const expression = host.getBaseClassExpression(classNode); + if (expression !== null && !ts.isIdentifier(expression)) { + throw new Error( + 'Expected class to inherit via an identifier but got: ' + expression.getText()); + } + return expression; + } + + it('should find the base class of an IIFE with _super parameter', () => { + const identifier = getBaseClassIdentifier(` + var BaseClass = /** @class */ (function () { + function BaseClass() {} + return BaseClass; + }()); + var TestClass = /** @class */ (function (_super) { + __extends(TestClass, _super); + function TestClass() {} + return TestClass; + }(BaseClass));`); + expect(identifier !.text).toBe('BaseClass'); + }); + + it('should find the base class of an IIFE with a unique name generated for the _super parameter', + () => { + const identifier = getBaseClassIdentifier(` + var BaseClass = /** @class */ (function () { + function BaseClass() {} + return BaseClass; + }()); + var TestClass = /** @class */ (function (_super_1) { + __extends(TestClass, _super_1); + function TestClass() {} + return TestClass; + }(BaseClass));`); + expect(identifier !.text).toBe('BaseClass'); + }); + + it('should not find a base class for an IIFE without parameter', () => { + const identifier = getBaseClassIdentifier(` + var BaseClass = /** @class */ (function () { + function BaseClass() {} + return BaseClass; + }()); + var TestClass = /** @class */ (function () { + __extends(TestClass, _super); + function TestClass() {} + return TestClass; + }(BaseClass));`); + expect(identifier).toBe(null); + }); + + it('should find a dynamic base class expression of an IIFE', () => { + const file = { + name: _('/synthesized_constructors.js'), + contents: ` + var BaseClass = /** @class */ (function () { + function BaseClass() {} + return BaseClass; + }()); + function foo() { return BaseClass; } + var TestClass = /** @class */ (function (_super) { + __extends(TestClass, _super); + function TestClass() {} + return TestClass; + }(foo()));`, + }; + + loadTestFiles([file]); + const {program, host: compilerHost} = makeTestBundleProgram(file.name); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = + getDeclaration(program, file.name, 'TestClass', isNamedVariableDeclaration); + const expression = host.getBaseClassExpression(classNode) !; + expect(expression.getText()).toBe('foo()'); + }); + }); + describe('findClassSymbols()', () => { it('should return an array of all classes in the given source file', () => { loadTestFiles(DECORATED_FILES); diff --git a/packages/compiler-cli/src/ngtsc/reflection/src/host.ts b/packages/compiler-cli/src/ngtsc/reflection/src/host.ts index 004ac0c2b8..2349911919 100644 --- a/packages/compiler-cli/src/ngtsc/reflection/src/host.ts +++ b/packages/compiler-cli/src/ngtsc/reflection/src/host.ts @@ -505,6 +505,16 @@ export interface ReflectionHost { */ hasBaseClass(clazz: ClassDeclaration): boolean; + /** + * Get an expression representing the base class (if any) of the given `clazz`. + * + * This expression is most commonly an Identifier, but is possible to inherit from a more dynamic + * expression. + * + * @param clazz the class whose base we want to get. + */ + getBaseClassExpression(clazz: ClassDeclaration): ts.Expression|null; + /** * Get the number of generic type parameters of a given class. * diff --git a/packages/compiler-cli/src/ngtsc/reflection/src/typescript.ts b/packages/compiler-cli/src/ngtsc/reflection/src/typescript.ts index 0d63d3d8dc..4c9fc0c027 100644 --- a/packages/compiler-cli/src/ngtsc/reflection/src/typescript.ts +++ b/packages/compiler-cli/src/ngtsc/reflection/src/typescript.ts @@ -121,10 +121,28 @@ export class TypeScriptReflectionHost implements ReflectionHost { } hasBaseClass(clazz: ClassDeclaration): boolean { - return ts.isClassDeclaration(clazz) && clazz.heritageClauses !== undefined && + return (ts.isClassDeclaration(clazz) || ts.isClassExpression(clazz)) && + clazz.heritageClauses !== undefined && clazz.heritageClauses.some(clause => clause.token === ts.SyntaxKind.ExtendsKeyword); } + getBaseClassExpression(clazz: ClassDeclaration): ts.Expression|null { + if (!(ts.isClassDeclaration(clazz) || ts.isClassExpression(clazz)) || + clazz.heritageClauses === undefined) { + return null; + } + const extendsClause = + clazz.heritageClauses.find(clause => clause.token === ts.SyntaxKind.ExtendsKeyword); + if (extendsClause === undefined) { + return null; + } + const extendsType = extendsClause.types[0]; + if (extendsType === undefined) { + return null; + } + return extendsType.expression; + } + getDeclarationOfIdentifier(id: ts.Identifier): Declaration|null { // Resolve the identifier to a Symbol, and return the declaration of that. let symbol: ts.Symbol|undefined = this.checker.getSymbolAtLocation(id);