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:
parent
d21471e24e
commit
1e1e242570
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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'));
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue