feat(ivy): implement `getDefinitionOfFunction` on ES2015 and ES5 reflection hosts (#25406)

PR Close #25406
This commit is contained in:
Pete Bacon Darwin 2018-07-25 07:57:35 +01:00 committed by Matias Niemelä
parent a45f2bfb8f
commit f87b499dde
4 changed files with 260 additions and 8 deletions

View File

@ -8,9 +8,10 @@
import * as ts from 'typescript';
import {ClassMember, ClassMemberKind, Decorator, Parameter} from '../../../ngtsc/host';
import {ClassMember, ClassMemberKind, CtorParameter, Decorator} from '../../../ngtsc/host';
import {TypeScriptReflectionHost, reflectObjectLiteral} from '../../../ngtsc/metadata';
import {getNameText} from '../utils';
import {NgccReflectionHost} from './ngcc_host';
export const DECORATORS = 'decorators' as ts.__String;
@ -162,7 +163,7 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
*
* @throws if `declaration` does not resolve to a class declaration.
*/
getConstructorParameters(clazz: ts.Declaration): Parameter[]|null {
getConstructorParameters(clazz: ts.Declaration): CtorParameter[]|null {
const classSymbol = this.getClassSymbol(clazz);
if (!classSymbol) {
throw new Error(
@ -170,7 +171,7 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
}
const parameterNodes = this.getConstructorParameterDeclarations(classSymbol);
if (parameterNodes) {
const parameters: Parameter[] = [];
const parameters: CtorParameter[] = [];
const decoratorInfo = this.getConstructorDecorators(classSymbol);
parameterNodes.forEach((node, index) => {
const info = decoratorInfo[index];

View File

@ -7,8 +7,9 @@
*/
import * as ts from 'typescript';
import {ClassMember, ClassMemberKind, Decorator} from '../../../ngtsc/host';
import {ClassMember, ClassMemberKind, Decorator, FunctionDefinition, Parameter} from '../../../ngtsc/host';
import {reflectObjectLiteral} from '../../../ngtsc/metadata';
import {getNameText} from '../utils';
import {CONSTRUCTOR_PARAMS, Esm2015ReflectionHost, getPropertyValueFromSymbol} from './esm2015_host';
/**
@ -54,6 +55,29 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
return undefined;
}
/**
* Parse a function declaration to find the relevant metadata about it.
* In ESM5 we need to do special work with optional arguments to the function, since they get
* their own initializer statement that needs to be parsed and then not included in the "body"
* statements of the function.
* @param node the function declaration to parse.
*/
getDefinitionOfFunction<T extends ts.FunctionDeclaration|ts.MethodDeclaration|
ts.FunctionExpression>(node: T): FunctionDefinition<T> {
const parameters =
node.parameters.map(p => ({name: getNameText(p.name), node: p, initializer: null}));
let lookingForParamInitializers = true;
const statements = node.body && node.body.statements.filter(s => {
lookingForParamInitializers =
lookingForParamInitializers && reflectParamInitializer(s, parameters);
// If we are no longer looking for parameter initializers then we include this statement
return !lookingForParamInitializers;
});
return {node, body: statements || null, parameters};
}
/**
* Find the declarations of the constructor parameters of a class identified by its symbol.
* In ESM5 there is no "class" so the constructor that we want is actually the declaration
@ -135,3 +159,51 @@ function getReturnStatement(declaration: ts.Expression | undefined): ts.ReturnSt
function reflectArrayElement(element: ts.Expression) {
return ts.isObjectLiteralExpression(element) ? reflectObjectLiteral(element) : null;
}
/**
* Parse the statement to extract the ESM5 parameter initializer if there is one.
* If one is found, add it to the appropriate parameter in the `parameters` collection.
*
* The form we are looking for is:
*
* ```
* if (arg === void 0) { arg = initializer; }
* ```
*
* @param statement A statement that may be initializing an optional parameter
* @param parameters The collection of parameters that were found in the function definition
* @returns true if the statement was a parameter initializer
*/
function reflectParamInitializer(statement: ts.Statement, parameters: Parameter[]) {
if (ts.isIfStatement(statement) && isUndefinedComparison(statement.expression) &&
ts.isBlock(statement.thenStatement) && statement.thenStatement.statements.length === 1) {
const ifStatementComparison = statement.expression; // (arg === void 0)
const thenStatement = statement.thenStatement.statements[0]; // arg = initializer;
if (isAssignment(thenStatement)) {
const comparisonName = ifStatementComparison.left.text;
const assignmentName = thenStatement.expression.left.text;
if (comparisonName === assignmentName) {
const parameter = parameters.find(p => p.name === comparisonName);
if (parameter) {
parameter.initializer = thenStatement.expression.right;
return true;
}
}
}
}
return false;
}
function isUndefinedComparison(expression: ts.Expression): expression is ts.Expression&
{left: ts.Identifier, right: ts.Expression} {
return ts.isBinaryExpression(expression) &&
expression.operatorToken.kind === ts.SyntaxKind.EqualsEqualsEqualsToken &&
ts.isVoidExpression(expression.right) && ts.isIdentifier(expression.left);
}
function isAssignment(statement: ts.Statement): statement is ts.ExpressionStatement&
{expression: {left: ts.Identifier, right: ts.Expression}} {
return ts.isExpressionStatement(statement) && ts.isBinaryExpression(statement.expression) &&
statement.expression.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
ts.isIdentifier(statement.expression.left);
}

View File

@ -353,6 +353,38 @@ const EXPORTS_FILES = [
},
];
const FUNCTION_BODY_FILE = {
name: '/function_body.js',
contents: `
function foo(x) {
return x;
}
function bar(x, y = 42) {
return x + y;
}
function baz(x) {
let y;
if (y === void 0) { y = 42; }
return x;
}
let y;
function qux(x) {
if (x === void 0) { y = 42; }
return y;
}
function moo() {
let x;
if (x === void 0) { x = 42; }
return x;
}
let x;
function juu() {
if (x === void 0) { x = 42; }
return x;
}
`
};
describe('Esm2015ReflectionHost', () => {
describe('getDecoratorsOfDeclaration()', () => {
@ -701,7 +733,7 @@ describe('Esm2015ReflectionHost', () => {
});
});
describe('getConstructorParameters', () => {
describe('getConstructorParameters()', () => {
it('should find the decorated constructor parameters', () => {
const program = makeProgram(SOME_DIRECTIVE_FILE);
const host = new Esm2015ReflectionHost(program.getTypeChecker());
@ -897,7 +929,69 @@ describe('Esm2015ReflectionHost', () => {
});
});
describe('getImportOfIdentifier', () => {
describe('getDefinitionOfFunction()', () => {
it('should return an object describing the function declaration passed as an argument', () => {
const program = makeProgram(FUNCTION_BODY_FILE);
const host = new Esm2015ReflectionHost(program.getTypeChecker());
const fooNode =
getDeclaration(program, FUNCTION_BODY_FILE.name, 'foo', ts.isFunctionDeclaration) !;
const fooDef = host.getDefinitionOfFunction(fooNode);
expect(fooDef.node).toBe(fooNode);
expect(fooDef.body !.length).toEqual(1);
expect(fooDef.body ![0].getText()).toEqual(`return x;`);
expect(fooDef.parameters.length).toEqual(1);
expect(fooDef.parameters[0].name).toEqual('x');
expect(fooDef.parameters[0].initializer).toBe(null);
const barNode =
getDeclaration(program, FUNCTION_BODY_FILE.name, 'bar', ts.isFunctionDeclaration) !;
const barDef = host.getDefinitionOfFunction(barNode);
expect(barDef.node).toBe(barNode);
expect(barDef.body !.length).toEqual(1);
expect(ts.isReturnStatement(barDef.body ![0])).toBeTruthy();
expect(barDef.body ![0].getText()).toEqual(`return x + y;`);
expect(barDef.parameters.length).toEqual(2);
expect(barDef.parameters[0].name).toEqual('x');
expect(fooDef.parameters[0].initializer).toBe(null);
expect(barDef.parameters[1].name).toEqual('y');
expect(barDef.parameters[1].initializer !.getText()).toEqual('42');
const bazNode =
getDeclaration(program, FUNCTION_BODY_FILE.name, 'baz', ts.isFunctionDeclaration) !;
const bazDef = host.getDefinitionOfFunction(bazNode);
expect(bazDef.node).toBe(bazNode);
expect(bazDef.body !.length).toEqual(3);
expect(bazDef.parameters.length).toEqual(1);
expect(bazDef.parameters[0].name).toEqual('x');
expect(bazDef.parameters[0].initializer).toBe(null);
const quxNode =
getDeclaration(program, FUNCTION_BODY_FILE.name, 'qux', ts.isFunctionDeclaration) !;
const quxDef = host.getDefinitionOfFunction(quxNode);
expect(quxDef.node).toBe(quxNode);
expect(quxDef.body !.length).toEqual(2);
expect(quxDef.parameters.length).toEqual(1);
expect(quxDef.parameters[0].name).toEqual('x');
expect(quxDef.parameters[0].initializer).toBe(null);
const mooNode =
getDeclaration(program, FUNCTION_BODY_FILE.name, 'moo', ts.isFunctionDeclaration) !;
const mooDef = host.getDefinitionOfFunction(mooNode);
expect(mooDef.node).toBe(mooNode);
expect(mooDef.body !.length).toEqual(3);
expect(mooDef.parameters).toEqual([]);
const juuNode =
getDeclaration(program, FUNCTION_BODY_FILE.name, 'juu', ts.isFunctionDeclaration) !;
const juuDef = host.getDefinitionOfFunction(juuNode);
expect(juuDef.node).toBe(juuNode);
expect(juuDef.body !.length).toEqual(2);
expect(juuDef.parameters).toEqual([]);
});
});
describe('getImportOfIdentifier()', () => {
it('should find the import of an identifier', () => {
const program = makeProgram(...IMPORTS_FILES);
const host = new Esm2015ReflectionHost(program.getTypeChecker());
@ -929,7 +1023,7 @@ describe('Esm2015ReflectionHost', () => {
});
});
describe('getDeclarationOfIdentifier', () => {
describe('getDeclarationOfIdentifier()', () => {
it('should return the declaration of a locally defined identifier', () => {
const program = makeProgram(SOME_DIRECTIVE_FILE);
const host = new Esm2015ReflectionHost(program.getTypeChecker());

View File

@ -406,6 +406,43 @@ const EXPORTS_FILES = [
},
];
const FUNCTION_BODY_FILE = {
name: '/function_body.js',
contents: `
function foo(x) {
return x;
}
function bar(x, y) {
if (y === void 0) { y = 42; }
return x + y;
}
function complex() {
var x = 42;
return 42;
}
function baz(x) {
var y;
if (x === void 0) { y = 42; }
return y;
}
var y;
function qux(x) {
if (x === void 0) { y = 42; }
return y;
}
function moo() {
var x;
if (x === void 0) { x = 42; }
return x;
}
var x;
function juu() {
if (x === void 0) { x = 42; }
return x;
}
`
};
describe('Esm5ReflectionHost', () => {
describe('getDecoratorsOfDeclaration()', () => {
@ -928,6 +965,54 @@ describe('Esm5ReflectionHost', () => {
});
});
describe('getDefinitionOfFunction()', () => {
it('should return an object describing the function declaration passed as an argument', () => {
const program = makeProgram(FUNCTION_BODY_FILE);
const host = new Esm5ReflectionHost(program.getTypeChecker());
const fooNode =
getDeclaration(program, FUNCTION_BODY_FILE.name, 'foo', ts.isFunctionDeclaration) !;
const fooDef = host.getDefinitionOfFunction(fooNode);
expect(fooDef.node).toBe(fooNode);
expect(fooDef.body !.length).toEqual(1);
expect(fooDef.body ![0].getText()).toEqual(`return x;`);
expect(fooDef.parameters.length).toEqual(1);
expect(fooDef.parameters[0].name).toEqual('x');
expect(fooDef.parameters[0].initializer).toBe(null);
const barNode =
getDeclaration(program, FUNCTION_BODY_FILE.name, 'bar', ts.isFunctionDeclaration) !;
const barDef = host.getDefinitionOfFunction(barNode);
expect(barDef.node).toBe(barNode);
expect(barDef.body !.length).toEqual(1);
expect(ts.isReturnStatement(barDef.body ![0])).toBeTruthy();
expect(barDef.body ![0].getText()).toEqual(`return x + y;`);
expect(barDef.parameters.length).toEqual(2);
expect(barDef.parameters[0].name).toEqual('x');
expect(fooDef.parameters[0].initializer).toBe(null);
expect(barDef.parameters[1].name).toEqual('y');
expect(barDef.parameters[1].initializer !.getText()).toEqual('42');
const bazNode =
getDeclaration(program, FUNCTION_BODY_FILE.name, 'baz', ts.isFunctionDeclaration) !;
const bazDef = host.getDefinitionOfFunction(bazNode);
expect(bazDef.node).toBe(bazNode);
expect(bazDef.body !.length).toEqual(3);
expect(bazDef.parameters.length).toEqual(1);
expect(bazDef.parameters[0].name).toEqual('x');
expect(bazDef.parameters[0].initializer).toBe(null);
const quxNode =
getDeclaration(program, FUNCTION_BODY_FILE.name, 'qux', ts.isFunctionDeclaration) !;
const quxDef = host.getDefinitionOfFunction(quxNode);
expect(quxDef.node).toBe(quxNode);
expect(quxDef.body !.length).toEqual(2);
expect(quxDef.parameters.length).toEqual(1);
expect(quxDef.parameters[0].name).toEqual('x');
expect(quxDef.parameters[0].initializer).toBe(null);
});
});
describe('getImportOfIdentifier', () => {
it('should find the import of an identifier', () => {
const program = makeProgram(...IMPORTS_FILES);