diff --git a/packages/compiler-cli/src/ngcc/src/host/esm2015_host.ts b/packages/compiler-cli/src/ngcc/src/host/esm2015_host.ts index a2dff5d7df..97605c5538 100644 --- a/packages/compiler-cli/src/ngcc/src/host/esm2015_host.ts +++ b/packages/compiler-cli/src/ngcc/src/host/esm2015_host.ts @@ -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]; diff --git a/packages/compiler-cli/src/ngcc/src/host/esm5_host.ts b/packages/compiler-cli/src/ngcc/src/host/esm5_host.ts index b79b13d0de..db03b43db2 100644 --- a/packages/compiler-cli/src/ngcc/src/host/esm5_host.ts +++ b/packages/compiler-cli/src/ngcc/src/host/esm5_host.ts @@ -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(node: T): FunctionDefinition { + 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 @@ -134,4 +158,52 @@ function getReturnStatement(declaration: ts.Expression | undefined): ts.ReturnSt function reflectArrayElement(element: ts.Expression) { return ts.isObjectLiteralExpression(element) ? reflectObjectLiteral(element) : null; -} \ No newline at end of file +} + +/** + * 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); +} diff --git a/packages/compiler-cli/src/ngcc/test/host/esm2015_host_spec.ts b/packages/compiler-cli/src/ngcc/test/host/esm2015_host_spec.ts index 03a35831cd..b693399b81 100644 --- a/packages/compiler-cli/src/ngcc/test/host/esm2015_host_spec.ts +++ b/packages/compiler-cli/src/ngcc/test/host/esm2015_host_spec.ts @@ -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()); diff --git a/packages/compiler-cli/src/ngcc/test/host/esm5_host_spec.ts b/packages/compiler-cli/src/ngcc/test/host/esm5_host_spec.ts index 172eefb98f..623bcbef8e 100644 --- a/packages/compiler-cli/src/ngcc/test/host/esm5_host_spec.ts +++ b/packages/compiler-cli/src/ngcc/test/host/esm5_host_spec.ts @@ -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);