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:
Pete Bacon Darwin 2019-07-18 21:05:31 +01:00 committed by Misko Hevery
parent 399935c32b
commit 8a470b9af9
8 changed files with 408 additions and 4 deletions

View File

@ -190,9 +190,21 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
return false; return false;
} }
return innerClassDeclaration.heritageClauses !== undefined && return super.hasBaseClass(innerClassDeclaration);
innerClassDeclaration.heritageClauses.some( }
clause => clause.token === ts.SyntaxKind.ExtendsKeyword);
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);
} }
/** /**

View File

@ -56,6 +56,32 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
return iife.parameters.length === 1 && isSuperIdentifier(iife.parameters[0].name); 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. * Find the declaration of a class given a node that we think represents the class.
* *

View File

@ -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()', () => { describe('findClassSymbols()', () => {
it('should return an array of all classes in the given source file', () => { it('should return an array of all classes in the given source file', () => {
loadTestFiles(DECORATED_FILES); loadTestFiles(DECORATED_FILES);

View File

@ -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()', () => { describe('getGenericArityOfClass()', () => {
it('should properly count type parameters', () => { it('should properly count type parameters', () => {
loadTestFiles(ARITY_CLASSES); loadTestFiles(ARITY_CLASSES);

View File

@ -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()', () => { describe('findClassSymbols()', () => {
it('should return an array of all classes in the given source file', () => { it('should return an array of all classes in the given source file', () => {
loadTestFiles(DECORATED_FILES); loadTestFiles(DECORATED_FILES);

View File

@ -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()', () => { describe('findClassSymbols()', () => {
it('should return an array of all classes in the given source file', () => { it('should return an array of all classes in the given source file', () => {
loadTestFiles(DECORATED_FILES); loadTestFiles(DECORATED_FILES);

View File

@ -505,6 +505,16 @@ export interface ReflectionHost {
*/ */
hasBaseClass(clazz: ClassDeclaration): boolean; 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. * Get the number of generic type parameters of a given class.
* *

View File

@ -121,10 +121,28 @@ export class TypeScriptReflectionHost implements ReflectionHost {
} }
hasBaseClass(clazz: ClassDeclaration): boolean { 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); 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 { getDeclarationOfIdentifier(id: ts.Identifier): Declaration|null {
// Resolve the identifier to a Symbol, and return the declaration of that. // Resolve the identifier to a Symbol, and return the declaration of that.
let symbol: ts.Symbol|undefined = this.checker.getSymbolAtLocation(id); let symbol: ts.Symbol|undefined = this.checker.getSymbolAtLocation(id);