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:
parent
ca07da4563
commit
3b9c802dee
|
@ -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;
|
||||
|
|
|
@ -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()', () => {
|
||||
|
|
|
@ -1417,86 +1417,236 @@ runInEachFileSystem(() => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('synthesized constructors', () => {
|
||||
function getConstructorParameters(constructor: string) {
|
||||
const file = {
|
||||
name: _('/synthesized_constructors.js'),
|
||||
contents: `
|
||||
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}
|
||||
return TestClass;
|
||||
}(null));
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
||||
loadTestFiles([file]);
|
||||
const bundle = makeTestBundleProgram(file.name);
|
||||
const host = createHost(bundle, new Esm5ReflectionHost(new MockLogger(), false, bundle));
|
||||
const classNode =
|
||||
getDeclaration(bundle.program, file.name, 'TestClass', isNamedVariableDeclaration);
|
||||
return host.getConstructorParameters(classNode);
|
||||
}
|
||||
loadTestFiles([file]);
|
||||
const bundle = makeTestBundleProgram(file.name);
|
||||
const host = createHost(bundle, new Esm5ReflectionHost(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;
|
||||
}`);
|
||||
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;
|
||||
}`);
|
||||
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;
|
||||
}`);
|
||||
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;
|
||||
}`);
|
||||
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;
|
||||
}`);
|
||||
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]);
|
||||
|
|
|
@ -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()', () => {
|
||||
|
|
|
@ -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;
|
||||
* ```
|
||||
|
|
Loading…
Reference in New Issue