Skip to content

Commit 3ecd9fb

Browse files
devversionprofanis
authored andcommitted
fix(ngcc): detect synthesized delegate constructors for downleveled ES2015 classes (angular#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: angular#38453. Fixes angular#38453. PR Close angular#38463
1 parent 51e5cb2 commit 3ecd9fb

File tree

5 files changed

+834
-162
lines changed

5 files changed

+834
-162
lines changed

packages/compiler-cli/ngcc/src/host/esm5_host.ts

Lines changed: 217 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import * as ts from 'typescript';
1010

11-
import {ClassDeclaration, ClassMember, ClassMemberKind, Declaration, Decorator, FunctionDefinition, Parameter, reflectObjectLiteral} from '../../../src/ngtsc/reflection';
11+
import {ClassDeclaration, ClassMember, ClassMemberKind, Declaration, Decorator, FunctionDefinition, KnownDeclaration, Parameter, reflectObjectLiteral} from '../../../src/ngtsc/reflection';
1212
import {getTsHelperFnFromDeclaration, getTsHelperFnFromIdentifier, hasNameIdentifier} from '../utils';
1313

1414
import {Esm2015ReflectionHost, getClassDeclarationFromInnerDeclaration, getPropertyValueFromSymbol, isAssignmentStatement, ParamInfo} from './esm2015_host';
@@ -219,7 +219,7 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
219219
return Array.from(constructor.parameters);
220220
}
221221

222-
if (isSynthesizedConstructor(constructor)) {
222+
if (this.isSynthesizedConstructor(constructor)) {
223223
return null;
224224
}
225225

@@ -352,6 +352,219 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
352352
const classDeclarationParent = classSymbol.implementation.valueDeclaration.parent;
353353
return ts.isBlock(classDeclarationParent) ? Array.from(classDeclarationParent.statements) : [];
354354
}
355+
356+
///////////// Host Private Helpers /////////////
357+
358+
/**
359+
* A constructor function may have been "synthesized" by TypeScript during JavaScript emit,
360+
* in the case no user-defined constructor exists and e.g. property initializers are used.
361+
* Those initializers need to be emitted into a constructor in JavaScript, so the TypeScript
362+
* compiler generates a synthetic constructor.
363+
*
364+
* We need to identify such constructors as ngcc needs to be able to tell if a class did
365+
* originally have a constructor in the TypeScript source. For ES5, we can not tell an
366+
* empty constructor apart from a synthesized constructor, but fortunately that does not
367+
* matter for the code generated by ngtsc.
368+
*
369+
* When a class has a superclass however, a synthesized constructor must not be considered
370+
* as a user-defined constructor as that prevents a base factory call from being created by
371+
* ngtsc, resulting in a factory function that does not inject the dependencies of the
372+
* superclass. Hence, we identify a default synthesized super call in the constructor body,
373+
* according to the structure that TypeScript's ES2015 to ES5 transformer generates in
374+
* https://github.com/Microsoft/TypeScript/blob/v3.2.2/src/compiler/transformers/es2015.ts#L1082-L1098
375+
*
376+
* Additionally, we handle synthetic delegate constructors that are emitted when TypeScript
377+
* downlevel's ES2015 synthetically generated to ES5. These vary slightly from the default
378+
* structure mentioned above because the ES2015 output uses a spread operator, for delegating
379+
* to the parent constructor, that is preserved through a TypeScript helper in ES5. e.g.
380+
*
381+
* ```
382+
* return _super.apply(this, tslib.__spread(arguments)) || this;
383+
* ```
384+
*
385+
* Such constructs can be still considered as synthetic delegate constructors as they are
386+
* the product of a common TypeScript to ES5 synthetic constructor, just being downleveled
387+
* to ES5 using `tsc`. See: https://github.com/angular/angular/issues/38453.
388+
*
389+
*
390+
* @param constructor a constructor function to test
391+
* @returns true if the constructor appears to have been synthesized
392+
*/
393+
private isSynthesizedConstructor(constructor: ts.FunctionDeclaration): boolean {
394+
if (!constructor.body) return false;
395+
396+
const firstStatement = constructor.body.statements[0];
397+
if (!firstStatement) return false;
398+
399+
return this.isSynthesizedSuperThisAssignment(firstStatement) ||
400+
this.isSynthesizedSuperReturnStatement(firstStatement);
401+
}
402+
403+
/**
404+
* Identifies synthesized super calls which pass-through function arguments directly and are
405+
* being assigned to a common `_this` variable. The following patterns we intend to match:
406+
*
407+
* 1. Delegate call emitted by TypeScript when it emits ES5 directly.
408+
* ```
409+
* var _this = _super !== null && _super.apply(this, arguments) || this;
410+
* ```
411+
*
412+
* 2. Delegate call emitted by TypeScript when it downlevel's ES2015 to ES5.
413+
* ```
414+
* var _this = _super.apply(this, tslib.__spread(arguments)) || this;
415+
* ```
416+
*
417+
*
418+
* @param statement a statement that may be a synthesized super call
419+
* @returns true if the statement looks like a synthesized super call
420+
*/
421+
private isSynthesizedSuperThisAssignment(statement: ts.Statement): boolean {
422+
if (!ts.isVariableStatement(statement)) return false;
423+
424+
const variableDeclarations = statement.declarationList.declarations;
425+
if (variableDeclarations.length !== 1) return false;
426+
427+
const variableDeclaration = variableDeclarations[0];
428+
if (!ts.isIdentifier(variableDeclaration.name) ||
429+
!variableDeclaration.name.text.startsWith('_this'))
430+
return false;
431+
432+
const initializer = variableDeclaration.initializer;
433+
if (!initializer) return false;
434+
435+
return this.isSynthesizedDefaultSuperCall(initializer);
436+
}
437+
/**
438+
* Identifies synthesized super calls which pass-through function arguments directly and
439+
* are being returned. The following patterns correspond to synthetic super return calls:
440+
*
441+
* 1. Delegate call emitted by TypeScript when it emits ES5 directly.
442+
* ```
443+
* return _super !== null && _super.apply(this, arguments) || this;
444+
* ```
445+
*
446+
* 2. Delegate call emitted by TypeScript when it downlevel's ES2015 to ES5.
447+
* ```
448+
* return _super.apply(this, tslib.__spread(arguments)) || this;
449+
* ```
450+
*
451+
* @param statement a statement that may be a synthesized super call
452+
* @returns true if the statement looks like a synthesized super call
453+
*/
454+
private isSynthesizedSuperReturnStatement(statement: ts.Statement): boolean {
455+
if (!ts.isReturnStatement(statement)) return false;
456+
457+
const expression = statement.expression;
458+
if (!expression) return false;
459+
460+
return this.isSynthesizedDefaultSuperCall(expression);
461+
}
462+
463+
/**
464+
* Identifies synthesized super calls which pass-through function arguments directly. The
465+
* synthetic delegate super call match the following patterns we intend to match:
466+
*
467+
* 1. Delegate call emitted by TypeScript when it emits ES5 directly.
468+
* ```
469+
* _super !== null && _super.apply(this, arguments) || this;
470+
* ```
471+
*
472+
* 2. Delegate call emitted by TypeScript when it downlevel's ES2015 to ES5.
473+
* ```
474+
* _super.apply(this, tslib.__spread(arguments)) || this;
475+
* ```
476+
*
477+
* @param expression an expression that may represent a default super call
478+
* @returns true if the expression corresponds with the above form
479+
*/
480+
private isSynthesizedDefaultSuperCall(expression: ts.Expression): boolean {
481+
if (!isBinaryExpr(expression, ts.SyntaxKind.BarBarToken)) return false;
482+
if (expression.right.kind !== ts.SyntaxKind.ThisKeyword) return false;
483+
484+
const left = expression.left;
485+
if (isBinaryExpr(left, ts.SyntaxKind.AmpersandAmpersandToken)) {
486+
return isSuperNotNull(left.left) && this.isSuperApplyCall(left.right);
487+
} else {
488+
return this.isSuperApplyCall(left);
489+
}
490+
}
491+
492+
/**
493+
* Tests whether the expression corresponds to a `super` call passing through
494+
* function arguments without any modification. e.g.
495+
*
496+
* ```
497+
* _super !== null && _super.apply(this, arguments) || this;
498+
* ```
499+
*
500+
* This structure is generated by TypeScript when transforming ES2015 to ES5, see
501+
* https://github.com/Microsoft/TypeScript/blob/v3.2.2/src/compiler/transformers/es2015.ts#L1148-L1163
502+
*
503+
* Additionally, we also handle cases where `arguments` are wrapped by a TypeScript spread helper.
504+
* This can happen if ES2015 class output contain auto-generated constructors due to class
505+
* members. The ES2015 output will be using `super(...arguments)` to delegate to the superclass,
506+
* but once downleveled to ES5, the spread operator will be persisted through a TypeScript spread
507+
* helper. For example:
508+
*
509+
* ```
510+
* _super.apply(this, __spread(arguments)) || this;
511+
* ```
512+
*
513+
* More details can be found in: https://github.com/angular/angular/issues/38453.
514+
*
515+
* @param expression an expression that may represent a default super call
516+
* @returns true if the expression corresponds with the above form
517+
*/
518+
private isSuperApplyCall(expression: ts.Expression): boolean {
519+
if (!ts.isCallExpression(expression) || expression.arguments.length !== 2) return false;
520+
521+
const targetFn = expression.expression;
522+
if (!ts.isPropertyAccessExpression(targetFn)) return false;
523+
if (!isSuperIdentifier(targetFn.expression)) return false;
524+
if (targetFn.name.text !== 'apply') return false;
525+
526+
const thisArgument = expression.arguments[0];
527+
if (thisArgument.kind !== ts.SyntaxKind.ThisKeyword) return false;
528+
529+
const argumentsExpr = expression.arguments[1];
530+
531+
// If the super is directly invoked with `arguments`, return `true`. This represents the
532+
// common TypeScript output where the delegate constructor super call matches the following
533+
// pattern: `super.apply(this, arguments)`.
534+
if (isArgumentsIdentifier(argumentsExpr)) {
535+
return true;
536+
}
537+
538+
// The other scenario we intend to detect: The `arguments` variable might be wrapped with the
539+
// TypeScript spread helper (either through tslib or inlined). This can happen if an explicit
540+
// delegate constructor uses `super(...arguments)` in ES2015 and is downleveled to ES5 using
541+
// `--downlevelIteration`. The output in such cases would not directly pass the function
542+
// `arguments` to the `super` call, but wrap it in a TS spread helper. The output would match
543+
// the following pattern: `super.apply(this, tslib.__spread(arguments))`. We check for such
544+
// constructs below, but perform the detection of the call expression definition as last as
545+
// that is the most expensive operation here.
546+
if (!ts.isCallExpression(argumentsExpr) || argumentsExpr.arguments.length !== 1 ||
547+
!isArgumentsIdentifier(argumentsExpr.arguments[0])) {
548+
return false;
549+
}
550+
551+
const argumentsCallExpr = argumentsExpr.expression;
552+
let argumentsCallDeclaration: Declaration|null = null;
553+
554+
// The `__spread` helper could be globally available, or accessed through a namespaced
555+
// import. Hence we support a property access here as long as it resolves to the actual
556+
// known TypeScript spread helper.
557+
if (ts.isIdentifier(argumentsCallExpr)) {
558+
argumentsCallDeclaration = this.getDeclarationOfIdentifier(argumentsCallExpr);
559+
} else if (
560+
ts.isPropertyAccessExpression(argumentsCallExpr) &&
561+
ts.isIdentifier(argumentsCallExpr.name)) {
562+
argumentsCallDeclaration = this.getDeclarationOfIdentifier(argumentsCallExpr.name);
563+
}
564+
565+
return argumentsCallDeclaration !== null &&
566+
argumentsCallDeclaration.known === KnownDeclaration.TsHelperSpread;
567+
}
355568
}
356569

357570
///////////// Internal Helpers /////////////
@@ -422,135 +635,15 @@ function reflectArrayElement(element: ts.Expression) {
422635
return ts.isObjectLiteralExpression(element) ? reflectObjectLiteral(element) : null;
423636
}
424637

425-
/**
426-
* A constructor function may have been "synthesized" by TypeScript during JavaScript emit,
427-
* in the case no user-defined constructor exists and e.g. property initializers are used.
428-
* Those initializers need to be emitted into a constructor in JavaScript, so the TypeScript
429-
* compiler generates a synthetic constructor.
430-
*
431-
* We need to identify such constructors as ngcc needs to be able to tell if a class did
432-
* originally have a constructor in the TypeScript source. For ES5, we can not tell an
433-
* empty constructor apart from a synthesized constructor, but fortunately that does not
434-
* matter for the code generated by ngtsc.
435-
*
436-
* When a class has a superclass however, a synthesized constructor must not be considered
437-
* as a user-defined constructor as that prevents a base factory call from being created by
438-
* ngtsc, resulting in a factory function that does not inject the dependencies of the
439-
* superclass. Hence, we identify a default synthesized super call in the constructor body,
440-
* according to the structure that TypeScript's ES2015 to ES5 transformer generates in
441-
* https://github.com/Microsoft/TypeScript/blob/v3.2.2/src/compiler/transformers/es2015.ts#L1082-L1098
442-
*
443-
* @param constructor a constructor function to test
444-
* @returns true if the constructor appears to have been synthesized
445-
*/
446-
function isSynthesizedConstructor(constructor: ts.FunctionDeclaration): boolean {
447-
if (!constructor.body) return false;
448-
449-
const firstStatement = constructor.body.statements[0];
450-
if (!firstStatement) return false;
451-
452-
return isSynthesizedSuperThisAssignment(firstStatement) ||
453-
isSynthesizedSuperReturnStatement(firstStatement);
454-
}
455-
456-
/**
457-
* Identifies a synthesized super call of the form:
458-
*
459-
* ```
460-
* var _this = _super !== null && _super.apply(this, arguments) || this;
461-
* ```
462-
*
463-
* @param statement a statement that may be a synthesized super call
464-
* @returns true if the statement looks like a synthesized super call
465-
*/
466-
function isSynthesizedSuperThisAssignment(statement: ts.Statement): boolean {
467-
if (!ts.isVariableStatement(statement)) return false;
468-
469-
const variableDeclarations = statement.declarationList.declarations;
470-
if (variableDeclarations.length !== 1) return false;
471-
472-
const variableDeclaration = variableDeclarations[0];
473-
if (!ts.isIdentifier(variableDeclaration.name) ||
474-
!variableDeclaration.name.text.startsWith('_this'))
475-
return false;
476-
477-
const initializer = variableDeclaration.initializer;
478-
if (!initializer) return false;
479-
480-
return isSynthesizedDefaultSuperCall(initializer);
481-
}
482-
/**
483-
* Identifies a synthesized super call of the form:
484-
*
485-
* ```
486-
* return _super !== null && _super.apply(this, arguments) || this;
487-
* ```
488-
*
489-
* @param statement a statement that may be a synthesized super call
490-
* @returns true if the statement looks like a synthesized super call
491-
*/
492-
function isSynthesizedSuperReturnStatement(statement: ts.Statement): boolean {
493-
if (!ts.isReturnStatement(statement)) return false;
494-
495-
const expression = statement.expression;
496-
if (!expression) return false;
497-
498-
return isSynthesizedDefaultSuperCall(expression);
499-
}
500-
501-
/**
502-
* Tests whether the expression is of the form:
503-
*
504-
* ```
505-
* _super !== null && _super.apply(this, arguments) || this;
506-
* ```
507-
*
508-
* This structure is generated by TypeScript when transforming ES2015 to ES5, see
509-
* https://github.com/Microsoft/TypeScript/blob/v3.2.2/src/compiler/transformers/es2015.ts#L1148-L1163
510-
*
511-
* @param expression an expression that may represent a default super call
512-
* @returns true if the expression corresponds with the above form
513-
*/
514-
function isSynthesizedDefaultSuperCall(expression: ts.Expression): boolean {
515-
if (!isBinaryExpr(expression, ts.SyntaxKind.BarBarToken)) return false;
516-
if (expression.right.kind !== ts.SyntaxKind.ThisKeyword) return false;
517-
518-
const left = expression.left;
519-
if (!isBinaryExpr(left, ts.SyntaxKind.AmpersandAmpersandToken)) return false;
520-
521-
return isSuperNotNull(left.left) && isSuperApplyCall(left.right);
638+
function isArgumentsIdentifier(expression: ts.Expression): boolean {
639+
return ts.isIdentifier(expression) && expression.text === 'arguments';
522640
}
523641

524642
function isSuperNotNull(expression: ts.Expression): boolean {
525643
return isBinaryExpr(expression, ts.SyntaxKind.ExclamationEqualsEqualsToken) &&
526644
isSuperIdentifier(expression.left);
527645
}
528646

529-
/**
530-
* Tests whether the expression is of the form
531-
*
532-
* ```
533-
* _super.apply(this, arguments)
534-
* ```
535-
*
536-
* @param expression an expression that may represent a default super call
537-
* @returns true if the expression corresponds with the above form
538-
*/
539-
function isSuperApplyCall(expression: ts.Expression): boolean {
540-
if (!ts.isCallExpression(expression) || expression.arguments.length !== 2) return false;
541-
542-
const targetFn = expression.expression;
543-
if (!ts.isPropertyAccessExpression(targetFn)) return false;
544-
if (!isSuperIdentifier(targetFn.expression)) return false;
545-
if (targetFn.name.text !== 'apply') return false;
546-
547-
const thisArgument = expression.arguments[0];
548-
if (thisArgument.kind !== ts.SyntaxKind.ThisKeyword) return false;
549-
550-
const argumentsArgument = expression.arguments[1];
551-
return ts.isIdentifier(argumentsArgument) && argumentsArgument.text === 'arguments';
552-
}
553-
554647
function isBinaryExpr(
555648
expression: ts.Expression, operator: ts.BinaryOperator): expression is ts.BinaryExpression {
556649
return ts.isBinaryExpression(expression) && expression.operatorToken.kind === operator;

0 commit comments

Comments
 (0)