fix(ngcc): detect synthesized constructors that have been downleveled using TS 4.2 (#41305)

TypeScript 4.2 has changed its emitted syntax for synthetic constructors
when using `downlevelIteration`, which affects ES5 bundles that have
been downleveled from ES2015 bundles. This is typically the case for UMD
bundles in the APF spec, as they are generated by downleveling the
ESM2015 bundle into ES5. ngcc needs to detect the new syntax in order to
correctly identify synthesized constructor functions in ES5 bundles.

Fixes #41298

PR Close #41305
This commit is contained in:
JoostK 2021-03-21 18:33:22 +01:00 committed by Joey Perrott
parent 65f7d5380d
commit 8d3da56eda
5 changed files with 428 additions and 27 deletions

View File

@ -382,6 +382,12 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
* return _super.apply(this, tslib.__spread(arguments)) || this;
* ```
*
* or, since TypeScript 4.2 it would be
*
* ```
* return _super.apply(this, tslib.__spreadArray([], tslib.__read(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.
@ -413,7 +419,10 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
* ```
* var _this = _super.apply(this, tslib.__spread(arguments)) || this;
* ```
*
* or using the syntax emitted since TypeScript 4.2:
* ```
* return _super.apply(this, tslib.__spreadArray([], tslib.__read(arguments))) || this;
* ```
*
* @param statement a statement that may be a synthesized super call
* @returns true if the statement looks like a synthesized super call
@ -447,6 +456,10 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
* ```
* return _super.apply(this, tslib.__spread(arguments)) || this;
* ```
* or using the syntax emitted since TypeScript 4.2:
* ```
* return _super.apply(this, tslib.__spreadArray([], tslib.__read(arguments))) || this;
* ```
*
* @param statement a statement that may be a synthesized super call
* @returns true if the statement looks like a synthesized super call
@ -473,6 +486,10 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
* ```
* _super.apply(this, tslib.__spread(arguments)) || this;
* ```
* or using the syntax emitted since TypeScript 4.2:
* ```
* return _super.apply(this, tslib.__spreadArray([], tslib.__read(arguments))) || this;
* ```
*
* @param expression an expression that may represent a default super call
* @returns true if the expression corresponds with the above form
@ -500,7 +517,8 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
* 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.
* 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
@ -510,6 +528,12 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
* _super.apply(this, __spread(arguments)) || this;
* ```
*
* or, since TypeScript 4.2 it would be
*
* ```
* _super.apply(this, tslib.__spreadArray([], tslib.__read(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
@ -538,32 +562,79 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
// 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])) {
// `--downlevelIteration`.
return this.isSpreadArgumentsExpression(argumentsExpr);
}
/**
* Determines if the provided expression is one of the following call expressions:
*
* 1. `__spread(arguments)`
* 2. `__spreadArray([], __read(arguments))`
*
* The tslib helpers may have been emitted inline as in the above example, or they may be read
* from a namespace import.
*/
private isSpreadArgumentsExpression(expression: ts.Expression): boolean {
const call = this.extractKnownHelperCall(expression);
if (call === null) {
return false;
}
const argumentsCallExpr = argumentsExpr.expression;
let argumentsCallDeclaration: Declaration|null = null;
if (call.helper === KnownDeclaration.TsHelperSpread) {
// `__spread(arguments)`
return call.args.length === 1 && isArgumentsIdentifier(call.args[0]);
} else if (call.helper === KnownDeclaration.TsHelperSpreadArray) {
// `__spreadArray([], __read(arguments))`
if (call.args.length !== 2) {
return false;
}
// 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);
const firstArg = call.args[0];
if (!ts.isArrayLiteralExpression(firstArg) || firstArg.elements.length !== 0) {
return false;
}
const secondArg = this.extractKnownHelperCall(call.args[1]);
if (secondArg === null || secondArg.helper !== KnownDeclaration.TsHelperRead) {
return false;
}
return secondArg.args.length === 1 && isArgumentsIdentifier(secondArg.args[0]);
} else {
return false;
}
}
/**
* Inspects the provided expression and determines if it corresponds with a known helper function
* as receiver expression.
*/
private extractKnownHelperCall(expression: ts.Expression):
{helper: KnownDeclaration, args: ts.NodeArray<ts.Expression>}|null {
if (!ts.isCallExpression(expression)) {
return null;
}
return argumentsCallDeclaration !== null &&
argumentsCallDeclaration.known === KnownDeclaration.TsHelperSpread;
const receiverExpr = expression.expression;
// The 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 helper.
let receiver: Declaration|null = null;
if (ts.isIdentifier(receiverExpr)) {
receiver = this.getDeclarationOfIdentifier(receiverExpr);
} else if (ts.isPropertyAccessExpression(receiverExpr) && ts.isIdentifier(receiverExpr.name)) {
receiver = this.getDeclarationOfIdentifier(receiverExpr.name);
}
if (receiver === null || receiver.known === null) {
return null;
}
return {
helper: receiver.known,
args: expression.arguments,
};
}
}

View File

@ -1530,11 +1530,15 @@ exports.MissingClass2 = MissingClass2;
break;
case 'inlined':
fileHeader =
`var __spread = (this && this.__spread) || function (...args) { /* ... */ }`;
`var __spread = (this && this.__spread) || function (...args) { /* ... */ };\n` +
`var __spreadArray = (this && this.__spreadArray) || function (...args) { /* ... */ };\n` +
`var __read = (this && this.__read) || function (...args) { /* ... */ };\n`;
break;
case 'inlined_with_suffix':
fileHeader =
`var __spread$1 = (this && this.__spread$1) || function (...args) { /* ... */ }`;
`var __spread$1 = (this && this.__spread$1) || function (...args) { /* ... */ };\n` +
`var __spreadArray$1 = (this && this.__spreadArray$1) || function (...args) { /* ... */ };\n` +
`var __read$2 = (this && this.__read$2) || function (...args) { /* ... */ };\n`;
break;
}
const file = {
@ -1635,6 +1639,17 @@ exports.MissingClass2 = MissingClass2;
expect(parameters).toBeNull();
});
it('recognizes delegate super call using inline spreadArray helper', () => {
const parameters = getConstructorParameters(
`
function TestClass() {
return _super.apply(this, __spreadArray([], __read(arguments))) || this;
}`,
'inlined');
expect(parameters).toBeNull();
});
it('recognizes delegate super call using inline spread helper with suffix', () => {
const parameters = getConstructorParameters(
`
@ -1646,6 +1661,17 @@ exports.MissingClass2 = MissingClass2;
expect(parameters).toBeNull();
});
it('recognizes delegate super call using inline spreadArray helper with suffix', () => {
const parameters = getConstructorParameters(
`
function TestClass() {
return _super.apply(this, __spreadArray$1([], __read$2(arguments))) || this;
}`,
'inlined_with_suffix');
expect(parameters).toBeNull();
});
it('recognizes delegate super call using imported spread helper', () => {
const parameters = getConstructorParameters(
`
@ -1657,6 +1683,17 @@ exports.MissingClass2 = MissingClass2;
expect(parameters).toBeNull();
});
it('recognizes delegate super call using imported spreadArray helper', () => {
const parameters = getConstructorParameters(
`
function TestClass() {
return _super.apply(this, tslib.__spreadArray([], tslib.__read(arguments))) || this;
}`,
'imported');
expect(parameters).toBeNull();
});
describe('with class member assignment', () => {
it('recognizes delegate super call using inline spread helper', () => {
const parameters = getConstructorParameters(
@ -1671,6 +1708,19 @@ exports.MissingClass2 = MissingClass2;
expect(parameters).toBeNull();
});
it('recognizes delegate super call using inline spreadArray helper', () => {
const parameters = getConstructorParameters(
`
function TestClass() {
var _this = _super.apply(this, __spreadArray([], __read(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(
`
@ -1684,6 +1734,19 @@ exports.MissingClass2 = MissingClass2;
expect(parameters).toBeNull();
});
it('recognizes delegate super call using inline spreadArray helper with suffix', () => {
const parameters = getConstructorParameters(
`
function TestClass() {
var _this = _super.apply(this, __spreadArray$1([], __read$2(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(
`
@ -1696,6 +1759,19 @@ exports.MissingClass2 = MissingClass2;
expect(parameters).toBeNull();
});
it('recognizes delegate super call using imported spreadArray helper', () => {
const parameters = getConstructorParameters(
`
function TestClass() {
var _this = _super.apply(this, tslib.__spreadArray([], tslib.__read(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', () => {

View File

@ -1485,18 +1485,22 @@ runInEachFileSystem(() => {
switch (mode) {
case 'imported':
fileHeader = `import {__spread} from 'tslib';`;
fileHeader = `import {__spread, __spreadArray, __read} from 'tslib';`;
break;
case 'imported_namespace':
fileHeader = `import * as tslib from 'tslib';`;
break;
case 'inlined':
fileHeader =
`var __spread = (this && this.__spread) || function (...args) { /* ... */ }`;
`var __spread = (this && this.__spread) || function (...args) { /* ... */ };\n` +
`var __spreadArray = (this && this.__spreadArray) || function (...args) { /* ... */ };\n` +
`var __read = (this && this.__read) || function (...args) { /* ... */ };\n`;
break;
case 'inlined_with_suffix':
fileHeader =
`var __spread$1 = (this && this.__spread$1) || function (...args) { /* ... */ }`;
`var __spread$1 = (this && this.__spread$1) || function (...args) { /* ... */ };\n` +
`var __spreadArray$1 = (this && this.__spreadArray$1) || function (...args) { /* ... */ };\n` +
`var __read$2 = (this && this.__read$2) || function (...args) { /* ... */ };\n`;
break;
}
@ -1595,6 +1599,17 @@ runInEachFileSystem(() => {
expect(parameters).toBeNull();
});
it('recognizes delegate super call using inline spreadArray helper', () => {
const parameters = getConstructorParameters(
`
function TestClass() {
return _super.apply(this, __spreadArray([], __read(arguments))) || this;
}`,
'inlined');
expect(parameters).toBeNull();
});
it('recognizes delegate super call using inline spread helper with suffix', () => {
const parameters = getConstructorParameters(
`
@ -1606,6 +1621,17 @@ runInEachFileSystem(() => {
expect(parameters).toBeNull();
});
it('recognizes delegate super call using inline spreadArray helper with suffix', () => {
const parameters = getConstructorParameters(
`
function TestClass() {
return _super.apply(this, __spreadArray$1([], __read$2(arguments))) || this;
}`,
'inlined_with_suffix');
expect(parameters).toBeNull();
});
it('recognizes delegate super call using imported spread helper', () => {
const parameters = getConstructorParameters(
`
@ -1617,6 +1643,17 @@ runInEachFileSystem(() => {
expect(parameters).toBeNull();
});
it('recognizes delegate super call using imported spreadArray helper', () => {
const parameters = getConstructorParameters(
`
function TestClass() {
return _super.apply(this, __spreadArray([], __read(arguments))) || this;
}`,
'imported');
expect(parameters).toBeNull();
});
it('recognizes delegate super call using namespace imported spread helper', () => {
const parameters = getConstructorParameters(
`
@ -1628,6 +1665,17 @@ runInEachFileSystem(() => {
expect(parameters).toBeNull();
});
it('recognizes delegate super call using namespace imported spreadArray helper', () => {
const parameters = getConstructorParameters(
`
function TestClass() {
return _super.apply(this, tslib.__spreadArray([], tslib.__read(arguments))) || this;
}`,
'imported_namespace');
expect(parameters).toBeNull();
});
describe('with class member assignment', () => {
it('recognizes delegate super call using inline spread helper', () => {
const parameters = getConstructorParameters(
@ -1642,6 +1690,19 @@ runInEachFileSystem(() => {
expect(parameters).toBeNull();
});
it('recognizes delegate super call using inline spreadArray helper', () => {
const parameters = getConstructorParameters(
`
function TestClass() {
var _this = _super.apply(this, __spreadArray([], __read(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(
`
@ -1655,6 +1716,19 @@ runInEachFileSystem(() => {
expect(parameters).toBeNull();
});
it('recognizes delegate super call using inline spreadArray helper with suffix', () => {
const parameters = getConstructorParameters(
`
function TestClass() {
var _this = _super.apply(this, __spreadArray$1([], __read$2(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(
`
@ -1668,6 +1742,19 @@ runInEachFileSystem(() => {
expect(parameters).toBeNull();
});
it('recognizes delegate super call using imported spreadArray helper', () => {
const parameters = getConstructorParameters(
`
function TestClass() {
var _this = _super.apply(this, __spreadArray([], __read(arguments))) || this;
_this.synthesizedProperty = null;
return _this;
}`,
'imported');
expect(parameters).toBeNull();
});
it('recognizes delegate super call using namespace imported spread helper', () => {
const parameters = getConstructorParameters(
`
@ -1680,6 +1767,19 @@ runInEachFileSystem(() => {
expect(parameters).toBeNull();
});
it('recognizes delegate super call using namespace imported spreadArray helper', () => {
const parameters = getConstructorParameters(
`
function TestClass() {
var _this = _super.apply(this, tslib.__spreadArray([], tslib.__read(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', () => {

View File

@ -1784,6 +1784,8 @@ runInEachFileSystem(() => {
}(this, (function (exports) { 'use strict';
var __spread = (this && this.__spread) || function (...args) { /* ... */ }
var __spreadArray = (this && this.__spreadArray) || function (...args) { /* ... */ }
var __read = (this && this.__read) || function (...args) { /* ... */ }
`;
break;
case 'inlined_with_suffix':
@ -1795,6 +1797,8 @@ runInEachFileSystem(() => {
}(this, (function (exports) { 'use strict';
var __spread$1 = (this && this.__spread$1) || function (...args) { /* ... */ }
var __spreadArray$1 = (this && this.__spreadArray$1) || function (...args) { /* ... */ }
var __read$2 = (this && this.__read$2) || function (...args) { /* ... */ }
`;
break;
}
@ -1897,6 +1901,17 @@ runInEachFileSystem(() => {
expect(parameters).toBeNull();
});
it('recognizes delegate super call using inline spreadArray helper', () => {
const parameters = getConstructorParameters(
`
function TestClass() {
return _super.apply(this, __spreadArray([], __read(arguments))) || this;
}`,
'inlined');
expect(parameters).toBeNull();
});
it('recognizes delegate super call using inline spread helper with suffix', () => {
const parameters = getConstructorParameters(
`
@ -1908,6 +1923,17 @@ runInEachFileSystem(() => {
expect(parameters).toBeNull();
});
it('recognizes delegate super call using inline spreadArray helper with suffix', () => {
const parameters = getConstructorParameters(
`
function TestClass() {
return _super.apply(this, __spreadArray$1([], __read$2(arguments))) || this;
}`,
'inlined_with_suffix');
expect(parameters).toBeNull();
});
it('recognizes delegate super call using imported spread helper', () => {
const parameters = getConstructorParameters(
`
@ -1919,6 +1945,17 @@ runInEachFileSystem(() => {
expect(parameters).toBeNull();
});
it('recognizes delegate super call using imported spreadArray helper', () => {
const parameters = getConstructorParameters(
`
function TestClass() {
return _super.apply(this, tslib_1.__spreadArray([], tslib.__read(arguments))) || this;
}`,
'imported');
expect(parameters).toBeNull();
});
describe('with class member assignment', () => {
it('recognizes delegate super call using inline spread helper', () => {
const parameters = getConstructorParameters(
@ -1933,6 +1970,19 @@ runInEachFileSystem(() => {
expect(parameters).toBeNull();
});
it('recognizes delegate super call using inline spreadArray helper', () => {
const parameters = getConstructorParameters(
`
function TestClass() {
var _this = _super.apply(this, __spreadArray([], __read(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(
`
@ -1946,6 +1996,19 @@ runInEachFileSystem(() => {
expect(parameters).toBeNull();
});
it('recognizes delegate super call using inline spreadArray helper with suffix', () => {
const parameters = getConstructorParameters(
`
function TestClass() {
var _this = _super.apply(this, __spreadArray$1([], __read$2(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(
`
@ -1958,6 +2021,19 @@ runInEachFileSystem(() => {
expect(parameters).toBeNull();
});
it('recognizes delegate super call using imported spreadArray helper', () => {
const parameters = getConstructorParameters(
`
function TestClass() {
var _this = _super.apply(this, tslib_1.__spreadArray([], tslib.__read(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', () => {

View File

@ -348,6 +348,84 @@ runInEachFileSystem(() => {
});
});
it(`should be able to detect synthesized constructors in ES5 with downlevelIteration enabled (imported helpers)`,
() => {
setupAngularCoreEsm5();
compileIntoApf(
'test-package', {
'/index.ts': `
import {Injectable} from '@angular/core';
@Injectable()
export class Base {}
@Injectable()
export class SubClass extends Base {
constructor() {
// Note: mimic the situation where TS is first emitted into ES2015, resulting
// in the spread super call below, and then downleveled into ES5 using the
// "downlevelIteration" option.
super(...arguments);
this.foo = 'bar';
}
}
`,
},
{importHelpers: true, noEmitHelpers: true, downlevelIteration: true});
mainNgcc({
basePath: '/node_modules',
targetEntryPointPath: 'test-package',
propertiesToConsider: ['esm5'],
});
const jsContents = fs.readFile(_(`/node_modules/test-package/esm5/src/index.js`));
// Verify that the ES5 bundle does contain the expected downleveling syntax.
expect(jsContents).toContain('__spreadArray([], __read(arguments))');
expect(jsContents)
.toContain(
'var ɵSubClass_BaseFactory = /*@__PURE__*/ ɵngcc0.ɵɵgetInheritedFactory(SubClass);');
});
it(`should be able to detect synthesized constructors in ES5 with downlevelIteration enabled (emitted helpers)`,
() => {
setupAngularCoreEsm5();
compileIntoApf(
'test-package', {
'/index.ts': `
import {Injectable} from '@angular/core';
@Injectable()
export class Base {}
@Injectable()
export class SubClass extends Base {
constructor() {
// Note: mimic the situation where TS is first emitted into ES2015, resulting
// in the spread super call below, and then downleveled into ES5 using the
// "downlevelIteration" option.
super(...arguments);
this.foo = 'bar';
}
}
`,
},
{importHelpers: false, noEmitHelpers: false, downlevelIteration: true});
mainNgcc({
basePath: '/node_modules',
targetEntryPointPath: 'test-package',
propertiesToConsider: ['esm5'],
});
const jsContents = fs.readFile(_(`/node_modules/test-package/esm5/src/index.js`));
// Verify that the ES5 bundle does contain the expected downleveling syntax.
expect(jsContents).toContain('__spreadArray([], __read(arguments))');
expect(jsContents)
.toContain(
'var ɵSubClass_BaseFactory = /*@__PURE__*/ ɵngcc0.ɵɵgetInheritedFactory(SubClass);');
});
it('should not add `const` in ES5 generated code', () => {
setupAngularCoreEsm5();
compileIntoFlatEs5Package('test-package', {