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…
Reference in New Issue