fix(ngcc): support minified ES5 scenarios (#33777)

The reflection hosts have been updated to support the following
code forms, which were found in some minified library code:

* The class IIFE not being wrapped in parentheses.
* Calls to `__decorate()` being combined with the IIFE return statement.

PR Close #33777
This commit is contained in:
Pete Bacon Darwin 2019-11-13 08:40:51 +00:00 committed by Kara Erickson
parent d21471e24e
commit 1e1e242570
6 changed files with 210 additions and 15 deletions

View File

@ -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;

View File

@ -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 {

View File

@ -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);

View File

@ -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'));

View File

@ -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);

View File

@ -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);