Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions packages/compiler-cli/src/ngcc/src/host/esm2015_host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -162,15 +163,15 @@ 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(
`Attempted to get constructor parameters of a non-class: "${clazz.getText()}"`);
}
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];
Expand Down
74 changes: 72 additions & 2 deletions packages/compiler-cli/src/ngcc/src/host/esm5_host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand Down Expand Up @@ -54,6 +55,27 @@ 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 statements: ts.Statement[]|null = null;
if (node.body) {
const firstNonInitializer =
node.body.statements.findIndex(s => !reflectParamInitializer(s, parameters));
statements = node.body.statements.slice(firstNonInitializer);
}
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
Expand Down Expand Up @@ -134,4 +156,52 @@ 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);
}
100 changes: 97 additions & 3 deletions packages/compiler-cli/src/ngcc/test/host/esm2015_host_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()', () => {
Expand Down Expand Up @@ -702,7 +734,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());
Expand Down Expand Up @@ -898,7 +930,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());
Expand Down Expand Up @@ -930,7 +1024,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());
Expand Down
85 changes: 85 additions & 0 deletions packages/compiler-cli/src/ngcc/test/host/esm5_host_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()', () => {
Expand Down Expand Up @@ -930,6 +967,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);
Expand Down
Loading