|
8 | 8 |
|
9 | 9 | import * as ts from 'typescript';
|
10 | 10 |
|
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'; |
12 | 12 | import {getTsHelperFnFromDeclaration, getTsHelperFnFromIdentifier, hasNameIdentifier} from '../utils';
|
13 | 13 |
|
14 | 14 | import {Esm2015ReflectionHost, getClassDeclarationFromInnerDeclaration, getPropertyValueFromSymbol, isAssignmentStatement, ParamInfo} from './esm2015_host';
|
@@ -219,7 +219,7 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
|
219 | 219 | return Array.from(constructor.parameters);
|
220 | 220 | }
|
221 | 221 |
|
222 |
| - if (isSynthesizedConstructor(constructor)) { |
| 222 | + if (this.isSynthesizedConstructor(constructor)) { |
223 | 223 | return null;
|
224 | 224 | }
|
225 | 225 |
|
@@ -352,6 +352,219 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
|
352 | 352 | const classDeclarationParent = classSymbol.implementation.valueDeclaration.parent;
|
353 | 353 | return ts.isBlock(classDeclarationParent) ? Array.from(classDeclarationParent.statements) : [];
|
354 | 354 | }
|
| 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 | + } |
355 | 568 | }
|
356 | 569 |
|
357 | 570 | ///////////// Internal Helpers /////////////
|
@@ -422,135 +635,15 @@ function reflectArrayElement(element: ts.Expression) {
|
422 | 635 | return ts.isObjectLiteralExpression(element) ? reflectObjectLiteral(element) : null;
|
423 | 636 | }
|
424 | 637 |
|
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'; |
522 | 640 | }
|
523 | 641 |
|
524 | 642 | function isSuperNotNull(expression: ts.Expression): boolean {
|
525 | 643 | return isBinaryExpr(expression, ts.SyntaxKind.ExclamationEqualsEqualsToken) &&
|
526 | 644 | isSuperIdentifier(expression.left);
|
527 | 645 | }
|
528 | 646 |
|
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 |
| - |
554 | 647 | function isBinaryExpr(
|
555 | 648 | expression: ts.Expression, operator: ts.BinaryOperator): expression is ts.BinaryExpression {
|
556 | 649 | return ts.isBinaryExpression(expression) && expression.operatorToken.kind === operator;
|
|
0 commit comments