diff --git a/packages/compiler-cli/ngcc/src/host/esm2015_host.ts b/packages/compiler-cli/ngcc/src/host/esm2015_host.ts index 7ac94d1486..827eaf5a57 100644 --- a/packages/compiler-cli/ngcc/src/host/esm2015_host.ts +++ b/packages/compiler-cli/ngcc/src/host/esm2015_host.ts @@ -1104,7 +1104,8 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N * does not match. */ protected getHelperCall(statement: ts.Statement, helperNames: string[]): ts.CallExpression|null { - if (ts.isExpressionStatement(statement)) { + if ((ts.isExpressionStatement(statement) || ts.isReturnStatement(statement)) && + statement.expression) { let expression = statement.expression; while (isAssignment(expression)) { expression = expression.right; diff --git a/packages/compiler-cli/ngcc/src/host/esm5_host.ts b/packages/compiler-cli/ngcc/src/host/esm5_host.ts index 03ad05b0f9..e003921765 100644 --- a/packages/compiler-cli/ngcc/src/host/esm5_host.ts +++ b/packages/compiler-cli/ngcc/src/host/esm5_host.ts @@ -11,7 +11,7 @@ import * as ts from 'typescript'; import {ClassDeclaration, ClassMember, ClassMemberKind, Declaration, Decorator, FunctionDefinition, Parameter, TsHelperFn, isNamedVariableDeclaration, reflectObjectLiteral} from '../../../src/ngtsc/reflection'; import {getNameText, hasNameIdentifier, stripDollarSuffix} from '../utils'; -import {Esm2015ReflectionHost, ParamInfo, getPropertyValueFromSymbol, isAssignmentStatement} from './esm2015_host'; +import {Esm2015ReflectionHost, ParamInfo, getPropertyValueFromSymbol, isAssignment, isAssignmentStatement} from './esm2015_host'; import {NgccClassSymbol} from './ngcc_host'; /** @@ -602,23 +602,36 @@ function getClassDeclarationFromInnerFunctionDeclaration(node: ts.Node): } export function getIifeBody(declaration: ts.Declaration): ts.Block|undefined { - if (!ts.isVariableDeclaration(declaration) || !declaration.initializer || - !ts.isParenthesizedExpression(declaration.initializer)) { + if (!ts.isVariableDeclaration(declaration) || !declaration.initializer) { return undefined; } - const call = declaration.initializer; - return ts.isCallExpression(call.expression) && - ts.isFunctionExpression(call.expression.expression) ? - call.expression.expression.body : - undefined; + + const call = stripParentheses(declaration.initializer); + if (!ts.isCallExpression(call)) { + return undefined; + } + + const fn = stripParentheses(call.expression); + if (!ts.isFunctionExpression(fn)) { + return undefined; + } + + return fn.body; } function getReturnIdentifier(body: ts.Block): ts.Identifier|undefined { const returnStatement = body.statements.find(ts.isReturnStatement); - return returnStatement && returnStatement.expression && - ts.isIdentifier(returnStatement.expression) ? - returnStatement.expression : - undefined; + if (!returnStatement || !returnStatement.expression) { + return undefined; + } + if (ts.isIdentifier(returnStatement.expression)) { + return returnStatement.expression; + } + if (isAssignment(returnStatement.expression) && + ts.isIdentifier(returnStatement.expression.left)) { + return returnStatement.expression.left; + } + return undefined; } function getReturnStatement(declaration: ts.Expression | undefined): ts.ReturnStatement|undefined { diff --git a/packages/compiler-cli/ngcc/test/host/commonjs_host_spec.ts b/packages/compiler-cli/ngcc/test/host/commonjs_host_spec.ts index ed755b25d0..c83b3ae5bd 100644 --- a/packages/compiler-cli/ngcc/test/host/commonjs_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/commonjs_host_spec.ts @@ -142,6 +142,16 @@ var EmptyClass = (function() { } return EmptyClass; }()); +var NoParensClass = function() { + function EmptyClass() { + } + return EmptyClass; +}(); +var InnerParensClass = (function() { + function EmptyClass() { + } + return EmptyClass; +})(); var NoDecoratorConstructorClass = (function() { function NoDecoratorConstructorClass(foo) { } @@ -1855,6 +1865,40 @@ exports.ExternalModule = ExternalModule; expect(innerSymbol.implementation).toBe(outerSymbol.implementation); }); + it('should return the class symbol for an ES5 class whose IIFE is not wrapped in parens', + () => { + loadTestFiles([SIMPLE_CLASS_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name); + const host = + new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const outerNode = getDeclaration( + program, SIMPLE_CLASS_FILE.name, 'NoParensClass', isNamedVariableDeclaration); + const innerNode = + getIifeBody(outerNode) !.statements.find(isNamedFunctionDeclaration) !; + const classSymbol = host.getClassSymbol(outerNode); + + expect(classSymbol).toBeDefined(); + expect(classSymbol !.declaration.valueDeclaration).toBe(outerNode); + expect(classSymbol !.implementation.valueDeclaration).toBe(innerNode); + }); + + it('should return the class symbol for an ES5 class whose IIFE is not wrapped with inner parens', + () => { + loadTestFiles([SIMPLE_CLASS_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name); + const host = + new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const outerNode = getDeclaration( + program, SIMPLE_CLASS_FILE.name, 'InnerParensClass', isNamedVariableDeclaration); + const innerNode = + getIifeBody(outerNode) !.statements.find(isNamedFunctionDeclaration) !; + const classSymbol = host.getClassSymbol(outerNode); + + expect(classSymbol).toBeDefined(); + expect(classSymbol !.declaration.valueDeclaration).toBe(outerNode); + expect(classSymbol !.implementation.valueDeclaration).toBe(innerNode); + }); + it('should return undefined if node is not an ES5 class', () => { loadTestFiles([FOO_FUNCTION_FILE]); const {program, host: compilerHost} = makeTestBundleProgram(FOO_FUNCTION_FILE.name); diff --git a/packages/compiler-cli/ngcc/test/host/esm5_host_import_helper_spec.ts b/packages/compiler-cli/ngcc/test/host/esm5_host_import_helper_spec.ts index d16b15ee25..9abcff4561 100644 --- a/packages/compiler-cli/ngcc/test/host/esm5_host_import_helper_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/esm5_host_import_helper_spec.ts @@ -9,10 +9,10 @@ import * as ts from 'typescript'; import {absoluteFrom, getFileSystem, getSourceFileOrError} from '../../../src/ngtsc/file_system'; import {TestFile, runInEachFileSystem} from '../../../src/ngtsc/file_system/testing'; -import {ClassMemberKind, Import, isNamedVariableDeclaration} from '../../../src/ngtsc/reflection'; +import {ClassMemberKind, isNamedFunctionDeclaration, isNamedVariableDeclaration} from '../../../src/ngtsc/reflection'; import {getDeclaration} from '../../../src/ngtsc/testing'; import {loadFakeCore, loadTestFiles, loadTsLib} from '../../../test/helpers'; -import {Esm5ReflectionHost} from '../../src/host/esm5_host'; +import {Esm5ReflectionHost, getIifeBody} from '../../src/host/esm5_host'; import {MockLogger} from '../helpers/mock_logger'; import {convertToDirectTsLibImport, convertToInlineTsLib, makeTestBundleProgram} from '../helpers/utils'; @@ -166,6 +166,22 @@ export { SomeDirective }; export { HttpClientXsrfModule }; ` }, + { + name: _('/some_minified_directive.js'), + contents: ` +import * as tslib_1 from 'tslib'; +import { Directive } from '@angular/core'; +// Note that the IIFE is not in parentheses +var SomeDirective = function () { + function SomeDirective() {} + // Note that the decorator is combined with the return statment + return SomeDirective = tslib_1.__decorate([ + Directive({ selector: '[someDirective]' }), + ], SomeDirective); +}()); +export { SomeDirective }; +`, + }, ]; const DIRECT_IMPORT_FILES = convertToDirectTsLibImport(NAMESPACED_IMPORT_FILES); @@ -207,6 +223,28 @@ export { SomeDirective }; expect(decorator.args !.map(arg => arg.getText())).toEqual([ '{ selector: \'[someDirective]\' }', ]); + + }); + + it('should find the decorators on a minified class', () => { + const {program} = makeTestBundleProgram(_('/some_minified_directive.js')); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, _('/some_minified_directive.js'), 'SomeDirective', + isNamedVariableDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode) !; + + expect(decorators).toBeDefined(); + expect(decorators.length).toEqual(1); + + const decorator = decorators[0]; + expect(decorator.name).toEqual('Directive'); + expect(decorator.identifier !.getText()).toEqual('Directive'); + expect(decorator.import).toEqual({name: 'Directive', from: '@angular/core'}); + expect(decorator.args !.map(arg => arg.getText())).toEqual([ + '{ selector: \'[someDirective]\' }', + ]); + }); it('should find the decorators on a class when mixing `ctorParameters` and `__decorate`', @@ -253,6 +291,23 @@ export { SomeDirective }; }); }); + describe('getClassSymbol()', () => { + it('should find a class that has been minified', () => { + const {program} = makeTestBundleProgram(_('/some_minified_directive.js')); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, _('/some_minified_directive.js'), 'SomeDirective', + isNamedVariableDeclaration); + const innerNode = + getIifeBody(classNode) !.statements.find(isNamedFunctionDeclaration) !; + const classSymbol = host.getClassSymbol(classNode); + + expect(classSymbol).toBeDefined(); + expect(classSymbol !.declaration.valueDeclaration).toBe(classNode); + expect(classSymbol !.implementation.valueDeclaration).toBe(innerNode); + }); + }); + describe('getMembersOfClass()', () => { it('should find decorated members on a class', () => { const {program} = makeTestBundleProgram(_('/some_directive.js')); diff --git a/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts b/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts index 7883adbb76..231e602698 100644 --- a/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts @@ -187,6 +187,16 @@ runInEachFileSystem(() => { } return EmptyClass; }()); + var NoParensClass = function() { + function EmptyClass() { + } + return EmptyClass; + }(); + var InnerParensClass = (function() { + function EmptyClass() { + } + return EmptyClass; + })(); var NoDecoratorConstructorClass = (function() { function NoDecoratorConstructorClass(foo) { } @@ -2061,6 +2071,36 @@ runInEachFileSystem(() => { expect(innerSymbol.implementation).toBe(outerSymbol.implementation); }); + it('should return the class symbol for an ES5 class whose IIFE is not wrapped in parens', + () => { + loadTestFiles([SIMPLE_CLASS_FILE]); + const {program} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const outerNode = getDeclaration( + program, SIMPLE_CLASS_FILE.name, 'NoParensClass', isNamedVariableDeclaration); + const innerNode = getIifeBody(outerNode) !.statements.find(isNamedFunctionDeclaration) !; + const classSymbol = host.getClassSymbol(outerNode); + + expect(classSymbol).toBeDefined(); + expect(classSymbol !.declaration.valueDeclaration).toBe(outerNode); + expect(classSymbol !.implementation.valueDeclaration).toBe(innerNode); + }); + + it('should return the class symbol for an ES5 class whose IIFE is not wrapped with inner parens', + () => { + loadTestFiles([SIMPLE_CLASS_FILE]); + const {program} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const outerNode = getDeclaration( + program, SIMPLE_CLASS_FILE.name, 'InnerParensClass', isNamedVariableDeclaration); + const innerNode = getIifeBody(outerNode) !.statements.find(isNamedFunctionDeclaration) !; + const classSymbol = host.getClassSymbol(outerNode); + + expect(classSymbol).toBeDefined(); + expect(classSymbol !.declaration.valueDeclaration).toBe(outerNode); + expect(classSymbol !.implementation.valueDeclaration).toBe(innerNode); + }); + it('should return undefined if node is not an ES5 class', () => { loadTestFiles([FOO_FUNCTION_FILE]); const {program} = makeTestBundleProgram(FOO_FUNCTION_FILE.name); diff --git a/packages/compiler-cli/ngcc/test/host/umd_host_spec.ts b/packages/compiler-cli/ngcc/test/host/umd_host_spec.ts index 51349c598f..4cdc43c50a 100644 --- a/packages/compiler-cli/ngcc/test/host/umd_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/umd_host_spec.ts @@ -158,6 +158,16 @@ runInEachFileSystem(() => { } return EmptyClass; }()); + var NoParensClass = function() { + function EmptyClass() { + } + return EmptyClass; + }(); + var InnerParensClass = (function() { + function EmptyClass() { + } + return EmptyClass; + })(); var NoDecoratorConstructorClass = (function() { function NoDecoratorConstructorClass(foo) { } @@ -1872,6 +1882,38 @@ runInEachFileSystem(() => { expect(innerSymbol.implementation).toBe(outerSymbol.implementation); }); + it('should return the class symbol for an ES5 class whose IIFE is not wrapped in parens', + () => { + loadTestFiles([SIMPLE_CLASS_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const outerNode = getDeclaration( + program, SIMPLE_CLASS_FILE.name, 'NoParensClass', isNamedVariableDeclaration); + const innerNode = + getIifeBody(outerNode) !.statements.find(isNamedFunctionDeclaration) !; + const classSymbol = host.getClassSymbol(outerNode); + + expect(classSymbol).toBeDefined(); + expect(classSymbol !.declaration.valueDeclaration).toBe(outerNode); + expect(classSymbol !.implementation.valueDeclaration).toBe(innerNode); + }); + + it('should return the class symbol for an ES5 class whose IIFE is not wrapped with inner parens', + () => { + loadTestFiles([SIMPLE_CLASS_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const outerNode = getDeclaration( + program, SIMPLE_CLASS_FILE.name, 'InnerParensClass', isNamedVariableDeclaration); + const innerNode = + getIifeBody(outerNode) !.statements.find(isNamedFunctionDeclaration) !; + const classSymbol = host.getClassSymbol(outerNode); + + expect(classSymbol).toBeDefined(); + expect(classSymbol !.declaration.valueDeclaration).toBe(outerNode); + expect(classSymbol !.implementation.valueDeclaration).toBe(innerNode); + }); + it('should return undefined if node is not an ES5 class', () => { loadTestFiles([FOO_FUNCTION_FILE]); const {program, host: compilerHost} = makeTestBundleProgram(FOO_FUNCTION_FILE.name);