fix(ngcc): detect synthesized delegate constructors for downleveled ES2015 classes (#38463)

Similarly to the change we landed in the `@angular/core` reflection
capabilities, we need to make sure that ngcc can detect pass-through
delegate constructors for classes using downleveled ES2015 output.

More details can be found in the preceding commit, and in the issue
outlining the problem: #38453.

Fixes #38453.

PR Close #38463
This commit is contained in:
Paul Gschwendtner 2020-08-14 20:43:59 +02:00 committed by Andrew Scott
parent ca07da4563
commit 3b9c802dee
5 changed files with 834 additions and 162 deletions

View File

@ -8,7 +8,7 @@
import * as ts from 'typescript';
import {ClassDeclaration, ClassMember, ClassMemberKind, Declaration, Decorator, FunctionDefinition, Parameter, reflectObjectLiteral} from '../../../src/ngtsc/reflection';
import {ClassDeclaration, ClassMember, ClassMemberKind, Declaration, Decorator, FunctionDefinition, KnownDeclaration, Parameter, reflectObjectLiteral} from '../../../src/ngtsc/reflection';
import {getTsHelperFnFromDeclaration, getTsHelperFnFromIdentifier, hasNameIdentifier} from '../utils';
import {Esm2015ReflectionHost, getClassDeclarationFromInnerDeclaration, getPropertyValueFromSymbol, isAssignmentStatement, ParamInfo} from './esm2015_host';
@ -219,7 +219,7 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
return Array.from(constructor.parameters);
}
if (isSynthesizedConstructor(constructor)) {
if (this.isSynthesizedConstructor(constructor)) {
return null;
}
@ -352,6 +352,219 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
const classDeclarationParent = classSymbol.implementation.valueDeclaration.parent;
return ts.isBlock(classDeclarationParent) ? Array.from(classDeclarationParent.statements) : [];
}
///////////// Host Private Helpers /////////////
/**
* A constructor function may have been "synthesized" by TypeScript during JavaScript emit,
* in the case no user-defined constructor exists and e.g. property initializers are used.
* Those initializers need to be emitted into a constructor in JavaScript, so the TypeScript
* compiler generates a synthetic constructor.
*
* We need to identify such constructors as ngcc needs to be able to tell if a class did
* originally have a constructor in the TypeScript source. For ES5, we can not tell an
* empty constructor apart from a synthesized constructor, but fortunately that does not
* matter for the code generated by ngtsc.
*
* When a class has a superclass however, a synthesized constructor must not be considered
* as a user-defined constructor as that prevents a base factory call from being created by
* ngtsc, resulting in a factory function that does not inject the dependencies of the
* superclass. Hence, we identify a default synthesized super call in the constructor body,
* according to the structure that TypeScript's ES2015 to ES5 transformer generates in
* https://github.com/Microsoft/TypeScript/blob/v3.2.2/src/compiler/transformers/es2015.ts#L1082-L1098
*
* Additionally, we handle synthetic delegate constructors that are emitted when TypeScript
* downlevel's ES2015 synthetically generated to ES5. These vary slightly from the default
* structure mentioned above because the ES2015 output uses a spread operator, for delegating
* to the parent constructor, that is preserved through a TypeScript helper in ES5. e.g.
*
* ```
* return _super.apply(this, tslib.__spread(arguments)) || this;
* ```
*
* Such constructs can be still considered as synthetic delegate constructors as they are
* the product of a common TypeScript to ES5 synthetic constructor, just being downleveled
* to ES5 using `tsc`. See: https://github.com/angular/angular/issues/38453.
*
*
* @param constructor a constructor function to test
* @returns true if the constructor appears to have been synthesized
*/
private isSynthesizedConstructor(constructor: ts.FunctionDeclaration): boolean {
if (!constructor.body) return false;
const firstStatement = constructor.body.statements[0];
if (!firstStatement) return false;
return this.isSynthesizedSuperThisAssignment(firstStatement) ||
this.isSynthesizedSuperReturnStatement(firstStatement);
}
/**
* Identifies synthesized super calls which pass-through function arguments directly and are
* being assigned to a common `_this` variable. The following patterns we intend to match:
*
* 1. Delegate call emitted by TypeScript when it emits ES5 directly.
* ```
* var _this = _super !== null && _super.apply(this, arguments) || this;
* ```
*
* 2. Delegate call emitted by TypeScript when it downlevel's ES2015 to ES5.
* ```
* var _this = _super.apply(this, tslib.__spread(arguments)) || this;
* ```
*
*
* @param statement a statement that may be a synthesized super call
* @returns true if the statement looks like a synthesized super call
*/
private isSynthesizedSuperThisAssignment(statement: ts.Statement): boolean {
if (!ts.isVariableStatement(statement)) return false;
const variableDeclarations = statement.declarationList.declarations;
if (variableDeclarations.length !== 1) return false;
const variableDeclaration = variableDeclarations[0];
if (!ts.isIdentifier(variableDeclaration.name) ||
!variableDeclaration.name.text.startsWith('_this'))
return false;
const initializer = variableDeclaration.initializer;
if (!initializer) return false;
return this.isSynthesizedDefaultSuperCall(initializer);
}
/**
* Identifies synthesized super calls which pass-through function arguments directly and
* are being returned. The following patterns correspond to synthetic super return calls:
*
* 1. Delegate call emitted by TypeScript when it emits ES5 directly.
* ```
* return _super !== null && _super.apply(this, arguments) || this;
* ```
*
* 2. Delegate call emitted by TypeScript when it downlevel's ES2015 to ES5.
* ```
* return _super.apply(this, tslib.__spread(arguments)) || this;
* ```
*
* @param statement a statement that may be a synthesized super call
* @returns true if the statement looks like a synthesized super call
*/
private isSynthesizedSuperReturnStatement(statement: ts.Statement): boolean {
if (!ts.isReturnStatement(statement)) return false;
const expression = statement.expression;
if (!expression) return false;
return this.isSynthesizedDefaultSuperCall(expression);
}
/**
* Identifies synthesized super calls which pass-through function arguments directly. The
* synthetic delegate super call match the following patterns we intend to match:
*
* 1. Delegate call emitted by TypeScript when it emits ES5 directly.
* ```
* _super !== null && _super.apply(this, arguments) || this;
* ```
*
* 2. Delegate call emitted by TypeScript when it downlevel's ES2015 to ES5.
* ```
* _super.apply(this, tslib.__spread(arguments)) || this;
* ```
*
* @param expression an expression that may represent a default super call
* @returns true if the expression corresponds with the above form
*/
private isSynthesizedDefaultSuperCall(expression: ts.Expression): boolean {
if (!isBinaryExpr(expression, ts.SyntaxKind.BarBarToken)) return false;
if (expression.right.kind !== ts.SyntaxKind.ThisKeyword) return false;
const left = expression.left;
if (isBinaryExpr(left, ts.SyntaxKind.AmpersandAmpersandToken)) {
return isSuperNotNull(left.left) && this.isSuperApplyCall(left.right);
} else {
return this.isSuperApplyCall(left);
}
}
/**
* Tests whether the expression corresponds to a `super` call passing through
* function arguments without any modification. e.g.
*
* ```
* _super !== null && _super.apply(this, arguments) || this;
* ```
*
* This structure is generated by TypeScript when transforming ES2015 to ES5, see
* https://github.com/Microsoft/TypeScript/blob/v3.2.2/src/compiler/transformers/es2015.ts#L1148-L1163
*
* Additionally, we also handle cases where `arguments` are wrapped by a TypeScript spread helper.
* This can happen if ES2015 class output contain auto-generated constructors due to class
* members. The ES2015 output will be using `super(...arguments)` to delegate to the superclass,
* but once downleveled to ES5, the spread operator will be persisted through a TypeScript spread
* helper. For example:
*
* ```
* _super.apply(this, __spread(arguments)) || this;
* ```
*
* More details can be found in: https://github.com/angular/angular/issues/38453.
*
* @param expression an expression that may represent a default super call
* @returns true if the expression corresponds with the above form
*/
private isSuperApplyCall(expression: ts.Expression): boolean {
if (!ts.isCallExpression(expression) || expression.arguments.length !== 2) return false;
const targetFn = expression.expression;
if (!ts.isPropertyAccessExpression(targetFn)) return false;
if (!isSuperIdentifier(targetFn.expression)) return false;
if (targetFn.name.text !== 'apply') return false;
const thisArgument = expression.arguments[0];
if (thisArgument.kind !== ts.SyntaxKind.ThisKeyword) return false;
const argumentsExpr = expression.arguments[1];
// If the super is directly invoked with `arguments`, return `true`. This represents the
// common TypeScript output where the delegate constructor super call matches the following
// pattern: `super.apply(this, arguments)`.
if (isArgumentsIdentifier(argumentsExpr)) {
return true;
}
// The other scenario we intend to detect: The `arguments` variable might be wrapped with the
// TypeScript spread helper (either through tslib or inlined). This can happen if an explicit
// delegate constructor uses `super(...arguments)` in ES2015 and is downleveled to ES5 using
// `--downlevelIteration`. The output in such cases would not directly pass the function
// `arguments` to the `super` call, but wrap it in a TS spread helper. The output would match
// the following pattern: `super.apply(this, tslib.__spread(arguments))`. We check for such
// constructs below, but perform the detection of the call expression definition as last as
// that is the most expensive operation here.
if (!ts.isCallExpression(argumentsExpr) || argumentsExpr.arguments.length !== 1 ||
!isArgumentsIdentifier(argumentsExpr.arguments[0])) {
return false;
}
const argumentsCallExpr = argumentsExpr.expression;
let argumentsCallDeclaration: Declaration|null = null;
// The `__spread` helper could be globally available, or accessed through a namespaced
// import. Hence we support a property access here as long as it resolves to the actual
// known TypeScript spread helper.
if (ts.isIdentifier(argumentsCallExpr)) {
argumentsCallDeclaration = this.getDeclarationOfIdentifier(argumentsCallExpr);
} else if (
ts.isPropertyAccessExpression(argumentsCallExpr) &&
ts.isIdentifier(argumentsCallExpr.name)) {
argumentsCallDeclaration = this.getDeclarationOfIdentifier(argumentsCallExpr.name);
}
return argumentsCallDeclaration !== null &&
argumentsCallDeclaration.known === KnownDeclaration.TsHelperSpread;
}
}
///////////// Internal Helpers /////////////
@ -422,103 +635,8 @@ function reflectArrayElement(element: ts.Expression) {
return ts.isObjectLiteralExpression(element) ? reflectObjectLiteral(element) : null;
}
/**
* A constructor function may have been "synthesized" by TypeScript during JavaScript emit,
* in the case no user-defined constructor exists and e.g. property initializers are used.
* Those initializers need to be emitted into a constructor in JavaScript, so the TypeScript
* compiler generates a synthetic constructor.
*
* We need to identify such constructors as ngcc needs to be able to tell if a class did
* originally have a constructor in the TypeScript source. For ES5, we can not tell an
* empty constructor apart from a synthesized constructor, but fortunately that does not
* matter for the code generated by ngtsc.
*
* When a class has a superclass however, a synthesized constructor must not be considered
* as a user-defined constructor as that prevents a base factory call from being created by
* ngtsc, resulting in a factory function that does not inject the dependencies of the
* superclass. Hence, we identify a default synthesized super call in the constructor body,
* according to the structure that TypeScript's ES2015 to ES5 transformer generates in
* https://github.com/Microsoft/TypeScript/blob/v3.2.2/src/compiler/transformers/es2015.ts#L1082-L1098
*
* @param constructor a constructor function to test
* @returns true if the constructor appears to have been synthesized
*/
function isSynthesizedConstructor(constructor: ts.FunctionDeclaration): boolean {
if (!constructor.body) return false;
const firstStatement = constructor.body.statements[0];
if (!firstStatement) return false;
return isSynthesizedSuperThisAssignment(firstStatement) ||
isSynthesizedSuperReturnStatement(firstStatement);
}
/**
* Identifies a synthesized super call of the form:
*
* ```
* var _this = _super !== null && _super.apply(this, arguments) || this;
* ```
*
* @param statement a statement that may be a synthesized super call
* @returns true if the statement looks like a synthesized super call
*/
function isSynthesizedSuperThisAssignment(statement: ts.Statement): boolean {
if (!ts.isVariableStatement(statement)) return false;
const variableDeclarations = statement.declarationList.declarations;
if (variableDeclarations.length !== 1) return false;
const variableDeclaration = variableDeclarations[0];
if (!ts.isIdentifier(variableDeclaration.name) ||
!variableDeclaration.name.text.startsWith('_this'))
return false;
const initializer = variableDeclaration.initializer;
if (!initializer) return false;
return isSynthesizedDefaultSuperCall(initializer);
}
/**
* Identifies a synthesized super call of the form:
*
* ```
* return _super !== null && _super.apply(this, arguments) || this;
* ```
*
* @param statement a statement that may be a synthesized super call
* @returns true if the statement looks like a synthesized super call
*/
function isSynthesizedSuperReturnStatement(statement: ts.Statement): boolean {
if (!ts.isReturnStatement(statement)) return false;
const expression = statement.expression;
if (!expression) return false;
return isSynthesizedDefaultSuperCall(expression);
}
/**
* Tests whether the expression is of the form:
*
* ```
* _super !== null && _super.apply(this, arguments) || this;
* ```
*
* This structure is generated by TypeScript when transforming ES2015 to ES5, see
* https://github.com/Microsoft/TypeScript/blob/v3.2.2/src/compiler/transformers/es2015.ts#L1148-L1163
*
* @param expression an expression that may represent a default super call
* @returns true if the expression corresponds with the above form
*/
function isSynthesizedDefaultSuperCall(expression: ts.Expression): boolean {
if (!isBinaryExpr(expression, ts.SyntaxKind.BarBarToken)) return false;
if (expression.right.kind !== ts.SyntaxKind.ThisKeyword) return false;
const left = expression.left;
if (!isBinaryExpr(left, ts.SyntaxKind.AmpersandAmpersandToken)) return false;
return isSuperNotNull(left.left) && isSuperApplyCall(left.right);
function isArgumentsIdentifier(expression: ts.Expression): boolean {
return ts.isIdentifier(expression) && expression.text === 'arguments';
}
function isSuperNotNull(expression: ts.Expression): boolean {
@ -526,31 +644,6 @@ function isSuperNotNull(expression: ts.Expression): boolean {
isSuperIdentifier(expression.left);
}
/**
* Tests whether the expression is of the form
*
* ```
* _super.apply(this, arguments)
* ```
*
* @param expression an expression that may represent a default super call
* @returns true if the expression corresponds with the above form
*/
function isSuperApplyCall(expression: ts.Expression): boolean {
if (!ts.isCallExpression(expression) || expression.arguments.length !== 2) return false;
const targetFn = expression.expression;
if (!ts.isPropertyAccessExpression(targetFn)) return false;
if (!isSuperIdentifier(targetFn.expression)) return false;
if (targetFn.name.text !== 'apply') return false;
const thisArgument = expression.arguments[0];
if (thisArgument.kind !== ts.SyntaxKind.ThisKeyword) return false;
const argumentsArgument = expression.arguments[1];
return ts.isIdentifier(argumentsArgument) && argumentsArgument.text === 'arguments';
}
function isBinaryExpr(
expression: ts.Expression, operator: ts.BinaryOperator): expression is ts.BinaryExpression {
return ts.isBinaryExpression(expression) && expression.operatorToken.kind === operator;

View File

@ -1456,6 +1456,210 @@ exports.MissingClass2 = MissingClass2;
expect(decorators[0].args).toEqual([]);
});
});
function getConstructorParameters(
constructor: string, mode?: 'inlined'|'inlined_with_suffix'|'imported') {
let fileHeader = '';
switch (mode) {
case 'imported':
fileHeader = `const tslib = require('tslib');`;
break;
case 'inlined':
fileHeader =
`var __spread = (this && this.__spread) || function (...args) { /* ... */ }`;
break;
case 'inlined_with_suffix':
fileHeader =
`var __spread$1 = (this && this.__spread$1) || function (...args) { /* ... */ }`;
break;
}
const file = {
name: _('/synthesized_constructors.js'),
contents: `
${fileHeader}
var TestClass = /** @class */ (function (_super) {
__extends(TestClass, _super);
${constructor}
return TestClass;
}(null));
exports.TestClass = TestClass;`,
};
loadTestFiles([file]);
const bundle = makeTestBundleProgram(file.name);
const host =
createHost(bundle, new CommonJsReflectionHost(new MockLogger(), false, bundle));
const classNode =
getDeclaration(bundle.program, file.name, 'TestClass', isNamedVariableDeclaration);
return host.getConstructorParameters(classNode);
}
describe('TS -> ES5: synthesized constructors', () => {
it('recognizes _this assignment from super call', () => {
const parameters = getConstructorParameters(`
function TestClass() {
var _this = _super !== null && _super.apply(this, arguments) || this;
_this.synthesizedProperty = null;
return _this;
}
`);
expect(parameters).toBeNull();
});
it('recognizes super call as return statement', () => {
const parameters = getConstructorParameters(`
function TestClass() {
return _super !== null && _super.apply(this, arguments) || this;
}
`);
expect(parameters).toBeNull();
});
it('handles the case where a unique name was generated for _super or _this', () => {
const parameters = getConstructorParameters(`
function TestClass() {
var _this_1 = _super_1 !== null && _super_1.apply(this, arguments) || this;
_this_1._this = null;
_this_1._super = null;
return _this_1;
}
`);
expect(parameters).toBeNull();
});
it('does not consider constructors with parameters as synthesized', () => {
const parameters = getConstructorParameters(`
function TestClass(arg) {
return _super !== null && _super.apply(this, arguments) || this;
}
`);
expect(parameters!.length).toBe(1);
});
it('does not consider manual super calls as synthesized', () => {
const parameters = getConstructorParameters(`
function TestClass() {
return _super.call(this) || this;
}
`);
expect(parameters!.length).toBe(0);
});
it('does not consider empty constructors as synthesized', () => {
const parameters = getConstructorParameters(`function TestClass() {}`);
expect(parameters!.length).toBe(0);
});
});
// See: https://github.com/angular/angular/issues/38453.
describe('ES2015 -> ES5: synthesized constructors through TSC downleveling', () => {
it('recognizes delegate super call using inline spread helper', () => {
const parameters = getConstructorParameters(
`
function TestClass() {
return _super.apply(this, __spread(arguments)) || this;
}`,
'inlined');
expect(parameters).toBeNull();
});
it('recognizes delegate super call using inline spread helper with suffix', () => {
const parameters = getConstructorParameters(
`
function TestClass() {
return _super.apply(this, __spread$1(arguments)) || this;
}`,
'inlined_with_suffix');
expect(parameters).toBeNull();
});
it('recognizes delegate super call using imported spread helper', () => {
const parameters = getConstructorParameters(
`
function TestClass() {
return _super.apply(this, tslib.__spread(arguments)) || this;
}`,
'imported');
expect(parameters).toBeNull();
});
describe('with class member assignment', () => {
it('recognizes delegate super call using inline spread helper', () => {
const parameters = getConstructorParameters(
`
function TestClass() {
var _this = _super.apply(this, __spread(arguments)) || this;
_this.synthesizedProperty = null;
return _this;
}`,
'inlined');
expect(parameters).toBeNull();
});
it('recognizes delegate super call using inline spread helper with suffix', () => {
const parameters = getConstructorParameters(
`
function TestClass() {
var _this = _super.apply(this, __spread$1(arguments)) || this;
_this.synthesizedProperty = null;
return _this;
}`,
'inlined_with_suffix');
expect(parameters).toBeNull();
});
it('recognizes delegate super call using imported spread helper', () => {
const parameters = getConstructorParameters(
`
function TestClass() {
var _this = _super.apply(this, tslib.__spread(arguments)) || this;
_this.synthesizedProperty = null;
return _this;
}`,
'imported');
expect(parameters).toBeNull();
});
});
it('handles the case where a unique name was generated for _super or _this', () => {
const parameters = getConstructorParameters(
`
function TestClass() {
var _this_1 = _super_1.apply(this, __spread(arguments)) || this;
_this_1._this = null;
_this_1._super = null;
return _this_1;
}`,
'inlined');
expect(parameters).toBeNull();
});
it('does not consider constructors with parameters as synthesized', () => {
const parameters = getConstructorParameters(
`
function TestClass(arg) {
return _super.apply(this, __spread(arguments)) || this;
}`,
'inlined');
expect(parameters!.length).toBe(1);
});
});
});
describe('getDefinitionOfFunction()', () => {

View File

@ -1417,11 +1417,32 @@ runInEachFileSystem(() => {
});
});
describe('synthesized constructors', () => {
function getConstructorParameters(constructor: string) {
function getConstructorParameters(
constructor: string,
mode?: 'inlined'|'inlined_with_suffix'|'imported'|'imported_namespace') {
let fileHeader = '';
switch (mode) {
case 'imported':
fileHeader = `import {__spread} from 'tslib';`;
break;
case 'imported_namespace':
fileHeader = `import * as tslib from 'tslib';`;
break;
case 'inlined':
fileHeader =
`var __spread = (this && this.__spread) || function (...args) { /* ... */ }`;
break;
case 'inlined_with_suffix':
fileHeader =
`var __spread$1 = (this && this.__spread$1) || function (...args) { /* ... */ }`;
break;
}
const file = {
name: _('/synthesized_constructors.js'),
contents: `
${fileHeader}
var TestClass = /** @class */ (function (_super) {
__extends(TestClass, _super);
${constructor}
@ -1438,13 +1459,15 @@ runInEachFileSystem(() => {
return host.getConstructorParameters(classNode);
}
describe('TS -> ES5: synthesized constructors', () => {
it('recognizes _this assignment from super call', () => {
const parameters = getConstructorParameters(`
function TestClass() {
var _this = _super !== null && _super.apply(this, arguments) || this;
_this.synthesizedProperty = null;
return _this;
}`);
}
`);
expect(parameters).toBeNull();
});
@ -1453,7 +1476,8 @@ runInEachFileSystem(() => {
const parameters = getConstructorParameters(`
function TestClass() {
return _super !== null && _super.apply(this, arguments) || this;
}`);
}
`);
expect(parameters).toBeNull();
});
@ -1465,7 +1489,8 @@ runInEachFileSystem(() => {
_this_1._this = null;
_this_1._super = null;
return _this_1;
}`);
}
`);
expect(parameters).toBeNull();
});
@ -1474,7 +1499,8 @@ runInEachFileSystem(() => {
const parameters = getConstructorParameters(`
function TestClass(arg) {
return _super !== null && _super.apply(this, arguments) || this;
}`);
}
`);
expect(parameters!.length).toBe(1);
});
@ -1483,20 +1509,144 @@ runInEachFileSystem(() => {
const parameters = getConstructorParameters(`
function TestClass() {
return _super.call(this) || this;
}`);
}
`);
expect(parameters!.length).toBe(0);
});
it('does not consider empty constructors as synthesized', () => {
const parameters = getConstructorParameters(`
function TestClass() {
}`);
const parameters = getConstructorParameters(`function TestClass() {}`);
expect(parameters!.length).toBe(0);
});
});
// See: https://github.com/angular/angular/issues/38453.
describe('ES2015 -> ES5: synthesized constructors through TSC downleveling', () => {
it('recognizes delegate super call using inline spread helper', () => {
const parameters = getConstructorParameters(
`
function TestClass() {
return _super.apply(this, __spread(arguments)) || this;
}`,
'inlined');
expect(parameters).toBeNull();
});
it('recognizes delegate super call using inline spread helper with suffix', () => {
const parameters = getConstructorParameters(
`
function TestClass() {
return _super.apply(this, __spread$1(arguments)) || this;
}`,
'inlined_with_suffix');
expect(parameters).toBeNull();
});
it('recognizes delegate super call using imported spread helper', () => {
const parameters = getConstructorParameters(
`
function TestClass() {
return _super.apply(this, __spread(arguments)) || this;
}`,
'imported');
expect(parameters).toBeNull();
});
it('recognizes delegate super call using namespace imported spread helper', () => {
const parameters = getConstructorParameters(
`
function TestClass() {
return _super.apply(this, tslib.__spread(arguments)) || this;
}`,
'imported_namespace');
expect(parameters).toBeNull();
});
describe('with class member assignment', () => {
it('recognizes delegate super call using inline spread helper', () => {
const parameters = getConstructorParameters(
`
function TestClass() {
var _this = _super.apply(this, __spread(arguments)) || this;
_this.synthesizedProperty = null;
return _this;
}`,
'inlined');
expect(parameters).toBeNull();
});
it('recognizes delegate super call using inline spread helper with suffix', () => {
const parameters = getConstructorParameters(
`
function TestClass() {
var _this = _super.apply(this, __spread$1(arguments)) || this;
_this.synthesizedProperty = null;
return _this;
}`,
'inlined_with_suffix');
expect(parameters).toBeNull();
});
it('recognizes delegate super call using imported spread helper', () => {
const parameters = getConstructorParameters(
`
function TestClass() {
var _this = _super.apply(this, __spread(arguments)) || this;
_this.synthesizedProperty = null;
return _this;
}`,
'imported');
expect(parameters).toBeNull();
});
it('recognizes delegate super call using namespace imported spread helper', () => {
const parameters = getConstructorParameters(
`
function TestClass() {
var _this = _super.apply(this, tslib.__spread(arguments)) || this;
_this.synthesizedProperty = null;
return _this;
}`,
'imported_namespace');
expect(parameters).toBeNull();
});
});
it('handles the case where a unique name was generated for _super or _this', () => {
const parameters = getConstructorParameters(
`
function TestClass() {
var _this_1 = _super_1.apply(this, __spread(arguments)) || this;
_this_1._this = null;
_this_1._super = null;
return _this_1;
}`,
'inlined');
expect(parameters).toBeNull();
});
it('does not consider constructors with parameters as synthesized', () => {
const parameters = getConstructorParameters(
`
function TestClass(arg) {
return _super.apply(this, __spread(arguments)) || this;
}`,
'inlined');
expect(parameters!.length).toBe(1);
});
});
describe('(returned parameters `decorators.args`)', () => {
it('should be an empty array if param decorator has no `args` property', () => {
loadTestFiles([INVALID_CTOR_DECORATOR_ARGS_FILE]);

View File

@ -1564,6 +1564,231 @@ runInEachFileSystem(() => {
expect(decorators[0].args).toEqual([]);
});
});
function getConstructorParameters(
constructor: string, mode: 'inlined'|'inlined_with_suffix'|'imported' = 'imported') {
let fileHeaderWithUmd = '';
switch (mode) {
case 'imported':
fileHeaderWithUmd = `
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('tslib'))) :
typeof define === 'function' && define.amd ? define('test', ['exports', 'tslib'], factory) :
(factory(global.test, global.tslib));
}(this, (function (exports, tslib) { 'use strict';
`;
break;
case 'inlined':
fileHeaderWithUmd = `
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports)) :
typeof define === 'function' && define.amd ? define('test', ['exports'], factory) :
(factory(global.test));
}(this, (function (exports) { 'use strict';
var __spread = (this && this.__spread) || function (...args) { /* ... */ }
`;
break;
case 'inlined_with_suffix':
fileHeaderWithUmd = `
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports)) :
typeof define === 'function' && define.amd ? define('test', ['exports'], factory) :
(factory(global.test));
}(this, (function (exports) { 'use strict';
var __spread$1 = (this && this.__spread$1) || function (...args) { /* ... */ }
`;
break;
}
const file = {
name: _('/synthesized_constructors.js'),
contents: `
${fileHeaderWithUmd}
var TestClass = /** @class */ (function (_super) {
__extends(TestClass, _super);
${constructor}
return TestClass;
}(null));
exports.TestClass = TestClass;
})));
`,
};
loadTestFiles([file]);
const bundle = makeTestBundleProgram(file.name);
const host = createHost(bundle, new UmdReflectionHost(new MockLogger(), false, bundle));
const classNode =
getDeclaration(bundle.program, file.name, 'TestClass', isNamedVariableDeclaration);
return host.getConstructorParameters(classNode);
}
describe('TS -> ES5: synthesized constructors', () => {
it('recognizes _this assignment from super call', () => {
const parameters = getConstructorParameters(`
function TestClass() {
var _this = _super !== null && _super.apply(this, arguments) || this;
_this.synthesizedProperty = null;
return _this;
}
`);
expect(parameters).toBeNull();
});
it('recognizes super call as return statement', () => {
const parameters = getConstructorParameters(`
function TestClass() {
return _super !== null && _super.apply(this, arguments) || this;
}
`);
expect(parameters).toBeNull();
});
it('handles the case where a unique name was generated for _super or _this', () => {
const parameters = getConstructorParameters(`
function TestClass() {
var _this_1 = _super_1 !== null && _super_1.apply(this, arguments) || this;
_this_1._this = null;
_this_1._super = null;
return _this_1;
}
`);
expect(parameters).toBeNull();
});
it('does not consider constructors with parameters as synthesized', () => {
const parameters = getConstructorParameters(`
function TestClass(arg) {
return _super !== null && _super.apply(this, arguments) || this;
}
`);
expect(parameters!.length).toBe(1);
});
it('does not consider manual super calls as synthesized', () => {
const parameters = getConstructorParameters(`
function TestClass() {
return _super.call(this) || this;
}
`);
expect(parameters!.length).toBe(0);
});
it('does not consider empty constructors as synthesized', () => {
const parameters = getConstructorParameters(`function TestClass() {}`);
expect(parameters!.length).toBe(0);
});
});
// See: https://github.com/angular/angular/issues/38453.
describe('ES2015 -> ES5: synthesized constructors through TSC downleveling', () => {
it('recognizes delegate super call using inline spread helper', () => {
const parameters = getConstructorParameters(
`
function TestClass() {
return _super.apply(this, __spread(arguments)) || this;
}`,
'inlined');
expect(parameters).toBeNull();
});
it('recognizes delegate super call using inline spread helper with suffix', () => {
const parameters = getConstructorParameters(
`
function TestClass() {
return _super.apply(this, __spread$1(arguments)) || this;
}`,
'inlined_with_suffix');
expect(parameters).toBeNull();
});
it('recognizes delegate super call using imported spread helper', () => {
const parameters = getConstructorParameters(
`
function TestClass() {
return _super.apply(this, tslib_1.__spread(arguments)) || this;
}`,
'imported');
expect(parameters).toBeNull();
});
describe('with class member assignment', () => {
it('recognizes delegate super call using inline spread helper', () => {
const parameters = getConstructorParameters(
`
function TestClass() {
var _this = _super.apply(this, __spread(arguments)) || this;
_this.synthesizedProperty = null;
return _this;
}`,
'inlined');
expect(parameters).toBeNull();
});
it('recognizes delegate super call using inline spread helper with suffix', () => {
const parameters = getConstructorParameters(
`
function TestClass() {
var _this = _super.apply(this, __spread$1(arguments)) || this;
_this.synthesizedProperty = null;
return _this;
}`,
'inlined_with_suffix');
expect(parameters).toBeNull();
});
it('recognizes delegate super call using imported spread helper', () => {
const parameters = getConstructorParameters(
`
function TestClass() {
var _this = _super.apply(this, tslib_1.__spread(arguments)) || this;
_this.synthesizedProperty = null;
return _this;
}`,
'imported');
expect(parameters).toBeNull();
});
});
it('handles the case where a unique name was generated for _super or _this', () => {
const parameters = getConstructorParameters(
`
function TestClass() {
var _this_1 = _super_1.apply(this, __spread(arguments)) || this;
_this_1._this = null;
_this_1._super = null;
return _this_1;
}`,
'inlined');
expect(parameters).toBeNull();
});
it('does not consider constructors with parameters as synthesized', () => {
const parameters = getConstructorParameters(
`
function TestClass(arg) {
return _super.apply(this, __spread(arguments)) || this;
}`,
'inlined');
expect(parameters!.length).toBe(1);
});
});
});
describe('getDefinitionOfFunction()', () => {

View File

@ -29,7 +29,7 @@ import {GetterFn, MethodFn, SetterFn} from './types';
* it intends to capture the pattern where existing constructors have been downleveled from
* ES2015 to ES5 using TypeScript w/ downlevel iteration. e.g.
*
* * ```
* ```
* function MyClass() {
* var _this = _super.apply(this, arguments) || this;
* ```