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.
|
* does not match.
|
||||||
*/
|
*/
|
||||||
protected getHelperCall(statement: ts.Statement, helperNames: string[]): ts.CallExpression|null {
|
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;
|
let expression = statement.expression;
|
||||||
while (isAssignment(expression)) {
|
while (isAssignment(expression)) {
|
||||||
expression = expression.right;
|
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 {ClassDeclaration, ClassMember, ClassMemberKind, Declaration, Decorator, FunctionDefinition, Parameter, TsHelperFn, isNamedVariableDeclaration, reflectObjectLiteral} from '../../../src/ngtsc/reflection';
|
||||||
import {getNameText, hasNameIdentifier, stripDollarSuffix} from '../utils';
|
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';
|
import {NgccClassSymbol} from './ngcc_host';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -602,23 +602,36 @@ function getClassDeclarationFromInnerFunctionDeclaration(node: ts.Node):
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getIifeBody(declaration: ts.Declaration): ts.Block|undefined {
|
export function getIifeBody(declaration: ts.Declaration): ts.Block|undefined {
|
||||||
if (!ts.isVariableDeclaration(declaration) || !declaration.initializer ||
|
if (!ts.isVariableDeclaration(declaration) || !declaration.initializer) {
|
||||||
!ts.isParenthesizedExpression(declaration.initializer)) {
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const call = declaration.initializer;
|
|
||||||
return ts.isCallExpression(call.expression) &&
|
const call = stripParentheses(declaration.initializer);
|
||||||
ts.isFunctionExpression(call.expression.expression) ?
|
if (!ts.isCallExpression(call)) {
|
||||||
call.expression.expression.body :
|
return undefined;
|
||||||
undefined;
|
}
|
||||||
|
|
||||||
|
const fn = stripParentheses(call.expression);
|
||||||
|
if (!ts.isFunctionExpression(fn)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fn.body;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getReturnIdentifier(body: ts.Block): ts.Identifier|undefined {
|
function getReturnIdentifier(body: ts.Block): ts.Identifier|undefined {
|
||||||
const returnStatement = body.statements.find(ts.isReturnStatement);
|
const returnStatement = body.statements.find(ts.isReturnStatement);
|
||||||
return returnStatement && returnStatement.expression &&
|
if (!returnStatement || !returnStatement.expression) {
|
||||||
ts.isIdentifier(returnStatement.expression) ?
|
return undefined;
|
||||||
returnStatement.expression :
|
}
|
||||||
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 {
|
function getReturnStatement(declaration: ts.Expression | undefined): ts.ReturnStatement|undefined {
|
||||||
|
@ -142,6 +142,16 @@ var EmptyClass = (function() {
|
|||||||
}
|
}
|
||||||
return EmptyClass;
|
return EmptyClass;
|
||||||
}());
|
}());
|
||||||
|
var NoParensClass = function() {
|
||||||
|
function EmptyClass() {
|
||||||
|
}
|
||||||
|
return EmptyClass;
|
||||||
|
}();
|
||||||
|
var InnerParensClass = (function() {
|
||||||
|
function EmptyClass() {
|
||||||
|
}
|
||||||
|
return EmptyClass;
|
||||||
|
})();
|
||||||
var NoDecoratorConstructorClass = (function() {
|
var NoDecoratorConstructorClass = (function() {
|
||||||
function NoDecoratorConstructorClass(foo) {
|
function NoDecoratorConstructorClass(foo) {
|
||||||
}
|
}
|
||||||
@ -1855,6 +1865,40 @@ exports.ExternalModule = ExternalModule;
|
|||||||
expect(innerSymbol.implementation).toBe(outerSymbol.implementation);
|
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', () => {
|
it('should return undefined if node is not an ES5 class', () => {
|
||||||
loadTestFiles([FOO_FUNCTION_FILE]);
|
loadTestFiles([FOO_FUNCTION_FILE]);
|
||||||
const {program, host: compilerHost} = makeTestBundleProgram(FOO_FUNCTION_FILE.name);
|
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 {absoluteFrom, getFileSystem, getSourceFileOrError} from '../../../src/ngtsc/file_system';
|
||||||
import {TestFile, runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
|
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 {getDeclaration} from '../../../src/ngtsc/testing';
|
||||||
import {loadFakeCore, loadTestFiles, loadTsLib} from '../../../test/helpers';
|
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 {MockLogger} from '../helpers/mock_logger';
|
||||||
import {convertToDirectTsLibImport, convertToInlineTsLib, makeTestBundleProgram} from '../helpers/utils';
|
import {convertToDirectTsLibImport, convertToInlineTsLib, makeTestBundleProgram} from '../helpers/utils';
|
||||||
|
|
||||||
@ -166,6 +166,22 @@ export { SomeDirective };
|
|||||||
export { HttpClientXsrfModule };
|
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);
|
const DIRECT_IMPORT_FILES = convertToDirectTsLibImport(NAMESPACED_IMPORT_FILES);
|
||||||
@ -207,6 +223,28 @@ export { SomeDirective };
|
|||||||
expect(decorator.args !.map(arg => arg.getText())).toEqual([
|
expect(decorator.args !.map(arg => arg.getText())).toEqual([
|
||||||
'{ selector: \'[someDirective]\' }',
|
'{ 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`',
|
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()', () => {
|
describe('getMembersOfClass()', () => {
|
||||||
it('should find decorated members on a class', () => {
|
it('should find decorated members on a class', () => {
|
||||||
const {program} = makeTestBundleProgram(_('/some_directive.js'));
|
const {program} = makeTestBundleProgram(_('/some_directive.js'));
|
||||||
|
@ -187,6 +187,16 @@ runInEachFileSystem(() => {
|
|||||||
}
|
}
|
||||||
return EmptyClass;
|
return EmptyClass;
|
||||||
}());
|
}());
|
||||||
|
var NoParensClass = function() {
|
||||||
|
function EmptyClass() {
|
||||||
|
}
|
||||||
|
return EmptyClass;
|
||||||
|
}();
|
||||||
|
var InnerParensClass = (function() {
|
||||||
|
function EmptyClass() {
|
||||||
|
}
|
||||||
|
return EmptyClass;
|
||||||
|
})();
|
||||||
var NoDecoratorConstructorClass = (function() {
|
var NoDecoratorConstructorClass = (function() {
|
||||||
function NoDecoratorConstructorClass(foo) {
|
function NoDecoratorConstructorClass(foo) {
|
||||||
}
|
}
|
||||||
@ -2061,6 +2071,36 @@ runInEachFileSystem(() => {
|
|||||||
expect(innerSymbol.implementation).toBe(outerSymbol.implementation);
|
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', () => {
|
it('should return undefined if node is not an ES5 class', () => {
|
||||||
loadTestFiles([FOO_FUNCTION_FILE]);
|
loadTestFiles([FOO_FUNCTION_FILE]);
|
||||||
const {program} = makeTestBundleProgram(FOO_FUNCTION_FILE.name);
|
const {program} = makeTestBundleProgram(FOO_FUNCTION_FILE.name);
|
||||||
|
@ -158,6 +158,16 @@ runInEachFileSystem(() => {
|
|||||||
}
|
}
|
||||||
return EmptyClass;
|
return EmptyClass;
|
||||||
}());
|
}());
|
||||||
|
var NoParensClass = function() {
|
||||||
|
function EmptyClass() {
|
||||||
|
}
|
||||||
|
return EmptyClass;
|
||||||
|
}();
|
||||||
|
var InnerParensClass = (function() {
|
||||||
|
function EmptyClass() {
|
||||||
|
}
|
||||||
|
return EmptyClass;
|
||||||
|
})();
|
||||||
var NoDecoratorConstructorClass = (function() {
|
var NoDecoratorConstructorClass = (function() {
|
||||||
function NoDecoratorConstructorClass(foo) {
|
function NoDecoratorConstructorClass(foo) {
|
||||||
}
|
}
|
||||||
@ -1872,6 +1882,38 @@ runInEachFileSystem(() => {
|
|||||||
expect(innerSymbol.implementation).toBe(outerSymbol.implementation);
|
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', () => {
|
it('should return undefined if node is not an ES5 class', () => {
|
||||||
loadTestFiles([FOO_FUNCTION_FILE]);
|
loadTestFiles([FOO_FUNCTION_FILE]);
|
||||||
const {program, host: compilerHost} = makeTestBundleProgram(FOO_FUNCTION_FILE.name);
|
const {program, host: compilerHost} = makeTestBundleProgram(FOO_FUNCTION_FILE.name);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user