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
This commit is contained in:
		
							parent
							
								
									399935c32b
								
							
						
					
					
						commit
						8a470b9af9
					
				| @ -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); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|  | ||||
| @ -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. | ||||
|    * | ||||
|  | ||||
| @ -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); | ||||
|  | ||||
| @ -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); | ||||
|  | ||||
| @ -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); | ||||
|  | ||||
| @ -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); | ||||
|  | ||||
| @ -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. | ||||
|    * | ||||
|  | ||||
| @ -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); | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user