From 3b9c802deefcc53ab2b759ac8650449bed75aa9a Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Fri, 14 Aug 2020 20:43:59 +0200 Subject: [PATCH] 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 --- .../compiler-cli/ngcc/src/host/esm5_host.ts | 341 +++++++++++------- .../ngcc/test/host/commonjs_host_spec.ts | 204 +++++++++++ .../ngcc/test/host/esm5_host_spec.ts | 224 ++++++++++-- .../ngcc/test/host/umd_host_spec.ts | 225 ++++++++++++ .../src/reflection/reflection_capabilities.ts | 2 +- 5 files changed, 834 insertions(+), 162 deletions(-) diff --git a/packages/compiler-cli/ngcc/src/host/esm5_host.ts b/packages/compiler-cli/ngcc/src/host/esm5_host.ts index fbaf423b7c..5464fbcff4 100644 --- a/packages/compiler-cli/ngcc/src/host/esm5_host.ts +++ b/packages/compiler-cli/ngcc/src/host/esm5_host.ts @@ -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; diff --git a/packages/compiler-cli/ngcc/test/host/commonjs_host_spec.ts b/packages/compiler-cli/ngcc/test/host/commonjs_host_spec.ts index 922b88f777..d2e1fcf02b 100644 --- a/packages/compiler-cli/ngcc/test/host/commonjs_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/commonjs_host_spec.ts @@ -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()', () => { diff --git a/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts b/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts index 6e219658c5..0fb7428196 100644 --- a/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts @@ -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]); diff --git a/packages/compiler-cli/ngcc/test/host/umd_host_spec.ts b/packages/compiler-cli/ngcc/test/host/umd_host_spec.ts index 01a10eb4bb..a25b09fb5a 100644 --- a/packages/compiler-cli/ngcc/test/host/umd_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/umd_host_spec.ts @@ -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()', () => { diff --git a/packages/core/src/reflection/reflection_capabilities.ts b/packages/core/src/reflection/reflection_capabilities.ts index b4880c4188..75575ebb13 100644 --- a/packages/core/src/reflection/reflection_capabilities.ts +++ b/packages/core/src/reflection/reflection_capabilities.ts @@ -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; * ```