Previously the `ConcreteDeclaration` and `InlineDeclaration` had different properties for the underlying node type. And the `InlineDeclaration` did not store a value that represented its declaration. It turns out that a natural declaration node for an inline type is the expression. For example in UMD/CommonJS this would be the `exports.<name>` property access node. So this expression is now used for the `node` of `InlineDeclaration` types and the `expression` property is dropped. To support this the codebase has been refactored to use a new `DeclarationNode` type which is a union of `ts.Declaration|ts.Expression` instead of `ts.Declaration` throughout. PR Close #38959
730 lines
30 KiB
TypeScript
730 lines
30 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright Google LLC All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
* found in the LICENSE file at https://angular.io/license
|
|
*/
|
|
|
|
import * as ts from 'typescript';
|
|
|
|
import {ClassDeclaration, ClassMember, ClassMemberKind, Declaration, DeclarationKind, Decorator, FunctionDefinition, isNamedFunctionDeclaration, KnownDeclaration, Parameter, reflectObjectLiteral} from '../../../src/ngtsc/reflection';
|
|
import {getTsHelperFnFromDeclaration, getTsHelperFnFromIdentifier, hasNameIdentifier} from '../utils';
|
|
|
|
import {Esm2015ReflectionHost, getOuterNodeFromInnerDeclaration, getPropertyValueFromSymbol, isAssignmentStatement, ParamInfo} from './esm2015_host';
|
|
import {NgccClassSymbol} from './ngcc_host';
|
|
|
|
|
|
/**
|
|
* ESM5 packages contain ECMAScript IIFE functions that act like classes. For example:
|
|
*
|
|
* ```
|
|
* var CommonModule = (function () {
|
|
* function CommonModule() {
|
|
* }
|
|
* CommonModule.decorators = [ ... ];
|
|
* return CommonModule;
|
|
* ```
|
|
*
|
|
* * "Classes" are decorated if they have a static property called `decorators`.
|
|
* * Members are decorated if there is a matching key on a static property
|
|
* called `propDecorators`.
|
|
* * Constructor parameters decorators are found on an object returned from
|
|
* a static method called `ctorParameters`.
|
|
*
|
|
*/
|
|
export class Esm5ReflectionHost extends Esm2015ReflectionHost {
|
|
getBaseClassExpression(clazz: ClassDeclaration): ts.Expression|null {
|
|
const superBaseClassExpression = super.getBaseClassExpression(clazz);
|
|
if (superBaseClassExpression !== null) {
|
|
return superBaseClassExpression;
|
|
}
|
|
|
|
const iife = getIifeFn(this.getClassSymbol(clazz));
|
|
if (iife === null) return null;
|
|
|
|
if (iife.parameters.length !== 1 || !isSuperIdentifier(iife.parameters[0].name)) {
|
|
return null;
|
|
}
|
|
|
|
if (!ts.isCallExpression(iife.parent)) {
|
|
return null;
|
|
}
|
|
|
|
return iife.parent.arguments[0];
|
|
}
|
|
|
|
/**
|
|
* Trace an identifier to its declaration, if possible.
|
|
*
|
|
* This method attempts to resolve the declaration of the given identifier, tracing back through
|
|
* imports and re-exports until the original declaration statement is found. A `Declaration`
|
|
* object is returned if the original declaration is found, or `null` is returned otherwise.
|
|
*
|
|
* In ES5, the implementation of a class is a function expression that is hidden inside an IIFE.
|
|
* If we are looking for the declaration of the identifier of the inner function expression, we
|
|
* will get hold of the outer "class" variable declaration and return its identifier instead. See
|
|
* `getClassDeclarationFromInnerFunctionDeclaration()` for more info.
|
|
*
|
|
* @param id a TypeScript `ts.Identifier` to trace back to a declaration.
|
|
*
|
|
* @returns metadata about the `Declaration` if the original declaration is found, or `null`
|
|
* otherwise.
|
|
*/
|
|
getDeclarationOfIdentifier(id: ts.Identifier): Declaration|null {
|
|
const declaration = super.getDeclarationOfIdentifier(id);
|
|
|
|
if (declaration === null) {
|
|
const nonEmittedNorImportedTsHelperDeclaration = getTsHelperFnFromIdentifier(id);
|
|
if (nonEmittedNorImportedTsHelperDeclaration !== null) {
|
|
// No declaration could be found for this identifier and its name matches a known TS helper
|
|
// function. This can happen if a package is compiled with `noEmitHelpers: true` and
|
|
// `importHelpers: false` (the default). This is, for example, the case with
|
|
// `@nativescript/angular@9.0.0-next-2019-11-12-155500-01`.
|
|
return {
|
|
kind: DeclarationKind.Inline,
|
|
node: id,
|
|
known: nonEmittedNorImportedTsHelperDeclaration,
|
|
viaModule: null,
|
|
};
|
|
}
|
|
}
|
|
|
|
if (declaration === null || declaration.node === null || declaration.known !== null) {
|
|
return declaration;
|
|
}
|
|
|
|
if (!ts.isVariableDeclaration(declaration.node) || declaration.node.initializer !== undefined ||
|
|
// VariableDeclaration => VariableDeclarationList => VariableStatement => IIFE Block
|
|
!ts.isBlock(declaration.node.parent.parent.parent)) {
|
|
return declaration;
|
|
}
|
|
|
|
// We might have an alias to another variable declaration.
|
|
// Search the containing iife body for it.
|
|
const block = declaration.node.parent.parent.parent;
|
|
const aliasSymbol = this.checker.getSymbolAtLocation(declaration.node.name);
|
|
for (let i = 0; i < block.statements.length; i++) {
|
|
const statement = block.statements[i];
|
|
// Looking for statement that looks like: `AliasedVariable = OriginalVariable;`
|
|
if (isAssignmentStatement(statement) && ts.isIdentifier(statement.expression.left) &&
|
|
ts.isIdentifier(statement.expression.right) &&
|
|
this.checker.getSymbolAtLocation(statement.expression.left) === aliasSymbol) {
|
|
return this.getDeclarationOfIdentifier(statement.expression.right);
|
|
}
|
|
}
|
|
|
|
return declaration;
|
|
}
|
|
|
|
/**
|
|
* Parse a function declaration to find the relevant metadata about it.
|
|
*
|
|
* In ESM5 we need to do special work with optional arguments to the function, since they get
|
|
* their own initializer statement that needs to be parsed and then not included in the "body"
|
|
* statements of the function.
|
|
*
|
|
* @param node the function declaration to parse.
|
|
* @returns an object containing the node, statements and parameters of the function.
|
|
*/
|
|
getDefinitionOfFunction(node: ts.Node): FunctionDefinition|null {
|
|
const definition = super.getDefinitionOfFunction(node);
|
|
if (definition === null) {
|
|
return null;
|
|
}
|
|
|
|
// Filter out and capture parameter initializers
|
|
if (definition.body !== null) {
|
|
let lookingForInitializers = true;
|
|
const statements = definition.body.filter(s => {
|
|
lookingForInitializers =
|
|
lookingForInitializers && captureParamInitializer(s, definition.parameters);
|
|
// If we are no longer looking for parameter initializers then we include this statement
|
|
return !lookingForInitializers;
|
|
});
|
|
definition.body = statements;
|
|
}
|
|
|
|
return definition;
|
|
}
|
|
|
|
/**
|
|
* Check whether a `Declaration` corresponds with a known declaration, such as a TypeScript helper
|
|
* function, and set its `known` property to the appropriate `KnownDeclaration`.
|
|
*
|
|
* @param decl The `Declaration` to check.
|
|
* @return The passed in `Declaration` (potentially enhanced with a `KnownDeclaration`).
|
|
*/
|
|
detectKnownDeclaration<T extends Declaration>(decl: T): T {
|
|
decl = super.detectKnownDeclaration(decl);
|
|
|
|
// Also check for TS helpers
|
|
if (decl.known === null && decl.node !== null) {
|
|
decl.known = getTsHelperFnFromDeclaration(decl.node);
|
|
}
|
|
|
|
return decl;
|
|
}
|
|
|
|
|
|
///////////// Protected Helpers /////////////
|
|
|
|
/**
|
|
* In ES5, the implementation of a class is a function expression that is hidden inside an IIFE,
|
|
* whose value is assigned to a variable (which represents the class to the rest of the program).
|
|
* So we might need to dig around to get hold of the "class" declaration.
|
|
*
|
|
* This method extracts a `NgccClassSymbol` if `declaration` is the function declaration inside
|
|
* the IIFE. Otherwise, undefined is returned.
|
|
*
|
|
* @param declaration the declaration whose symbol we are finding.
|
|
* @returns the symbol for the node or `undefined` if it is not a "class" or has no symbol.
|
|
*/
|
|
protected getClassSymbolFromInnerDeclaration(declaration: ts.Node): NgccClassSymbol|undefined {
|
|
const classSymbol = super.getClassSymbolFromInnerDeclaration(declaration);
|
|
if (classSymbol !== undefined) {
|
|
return classSymbol;
|
|
}
|
|
|
|
if (!isNamedFunctionDeclaration(declaration)) {
|
|
return undefined;
|
|
}
|
|
|
|
const outerNode = getOuterNodeFromInnerDeclaration(declaration);
|
|
if (outerNode === null || !hasNameIdentifier(outerNode)) {
|
|
return undefined;
|
|
}
|
|
|
|
return this.createClassSymbol(outerNode.name, declaration);
|
|
}
|
|
|
|
/**
|
|
* Find the declarations of the constructor parameters of a class identified by its symbol.
|
|
*
|
|
* In ESM5, there is no "class" so the constructor that we want is actually the inner function
|
|
* declaration inside the IIFE, whose return value is assigned to the outer variable declaration
|
|
* (that represents the class to the rest of the program).
|
|
*
|
|
* @param classSymbol the symbol of the class (i.e. the outer variable declaration) whose
|
|
* parameters we want to find.
|
|
* @returns an array of `ts.ParameterDeclaration` objects representing each of the parameters in
|
|
* the class's constructor or `null` if there is no constructor.
|
|
*/
|
|
protected getConstructorParameterDeclarations(classSymbol: NgccClassSymbol):
|
|
ts.ParameterDeclaration[]|null {
|
|
const constructor = classSymbol.implementation.valueDeclaration;
|
|
if (!ts.isFunctionDeclaration(constructor)) return null;
|
|
|
|
if (constructor.parameters.length > 0) {
|
|
return Array.from(constructor.parameters);
|
|
}
|
|
|
|
if (this.isSynthesizedConstructor(constructor)) {
|
|
return null;
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* Get the parameter type and decorators for the constructor of a class,
|
|
* where the information is stored on a static method of the class.
|
|
*
|
|
* In this case the decorators are stored in the body of a method
|
|
* (`ctorParatemers`) attached to the constructor function.
|
|
*
|
|
* Note that unlike ESM2015 this is a function expression rather than an arrow
|
|
* function:
|
|
*
|
|
* ```
|
|
* SomeDirective.ctorParameters = function() { return [
|
|
* { type: ViewContainerRef, },
|
|
* { type: TemplateRef, },
|
|
* { type: IterableDiffers, },
|
|
* { type: undefined, decorators: [{ type: Inject, args: [INJECTED_TOKEN,] },] },
|
|
* ]; };
|
|
* ```
|
|
*
|
|
* @param paramDecoratorsProperty the property that holds the parameter info we want to get.
|
|
* @returns an array of objects containing the type and decorators for each parameter.
|
|
*/
|
|
protected getParamInfoFromStaticProperty(paramDecoratorsProperty: ts.Symbol): ParamInfo[]|null {
|
|
const paramDecorators = getPropertyValueFromSymbol(paramDecoratorsProperty);
|
|
// The decorators array may be wrapped in a function. If so unwrap it.
|
|
const returnStatement = getReturnStatement(paramDecorators);
|
|
const expression = returnStatement ? returnStatement.expression : paramDecorators;
|
|
if (expression && ts.isArrayLiteralExpression(expression)) {
|
|
const elements = expression.elements;
|
|
return elements.map(reflectArrayElement).map(paramInfo => {
|
|
const typeExpression = paramInfo && paramInfo.has('type') ? paramInfo.get('type')! : null;
|
|
const decoratorInfo =
|
|
paramInfo && paramInfo.has('decorators') ? paramInfo.get('decorators')! : null;
|
|
const decorators = decoratorInfo && this.reflectDecorators(decoratorInfo);
|
|
return {typeExpression, decorators};
|
|
});
|
|
} else if (paramDecorators !== undefined) {
|
|
this.logger.warn(
|
|
'Invalid constructor parameter decorator in ' + paramDecorators.getSourceFile().fileName +
|
|
':\n',
|
|
paramDecorators.getText());
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Reflect over a symbol and extract the member information, combining it with the
|
|
* provided decorator information, and whether it is a static member.
|
|
*
|
|
* If a class member uses accessors (e.g getters and/or setters) then it gets downleveled
|
|
* in ES5 to a single `Object.defineProperty()` call. In that case we must parse this
|
|
* call to extract the one or two ClassMember objects that represent the accessors.
|
|
*
|
|
* @param symbol the symbol for the member to reflect over.
|
|
* @param decorators an array of decorators associated with the member.
|
|
* @param isStatic true if this member is static, false if it is an instance property.
|
|
* @returns the reflected member information, or null if the symbol is not a member.
|
|
*/
|
|
protected reflectMembers(symbol: ts.Symbol, decorators?: Decorator[], isStatic?: boolean):
|
|
ClassMember[]|null {
|
|
const node = symbol.valueDeclaration || symbol.declarations && symbol.declarations[0];
|
|
const propertyDefinition = node && getPropertyDefinition(node);
|
|
if (propertyDefinition) {
|
|
const members: ClassMember[] = [];
|
|
if (propertyDefinition.setter) {
|
|
members.push({
|
|
node,
|
|
implementation: propertyDefinition.setter,
|
|
kind: ClassMemberKind.Setter,
|
|
type: null,
|
|
name: symbol.name,
|
|
nameNode: null,
|
|
value: null,
|
|
isStatic: isStatic || false,
|
|
decorators: decorators || [],
|
|
});
|
|
|
|
// Prevent attaching the decorators to a potential getter. In ES5, we can't tell where the
|
|
// decorators were originally attached to, however we only want to attach them to a single
|
|
// `ClassMember` as otherwise ngtsc would handle the same decorators twice.
|
|
decorators = undefined;
|
|
}
|
|
if (propertyDefinition.getter) {
|
|
members.push({
|
|
node,
|
|
implementation: propertyDefinition.getter,
|
|
kind: ClassMemberKind.Getter,
|
|
type: null,
|
|
name: symbol.name,
|
|
nameNode: null,
|
|
value: null,
|
|
isStatic: isStatic || false,
|
|
decorators: decorators || [],
|
|
});
|
|
}
|
|
return members;
|
|
}
|
|
|
|
const members = super.reflectMembers(symbol, decorators, isStatic);
|
|
members && members.forEach(member => {
|
|
if (member && member.kind === ClassMemberKind.Method && member.isStatic && member.node &&
|
|
ts.isPropertyAccessExpression(member.node) && member.node.parent &&
|
|
ts.isBinaryExpression(member.node.parent) &&
|
|
ts.isFunctionExpression(member.node.parent.right)) {
|
|
// Recompute the implementation for this member:
|
|
// ES5 static methods are variable declarations so the declaration is actually the
|
|
// initializer of the variable assignment
|
|
member.implementation = member.node.parent.right;
|
|
}
|
|
});
|
|
return members;
|
|
}
|
|
|
|
/**
|
|
* Find statements related to the given class that may contain calls to a helper.
|
|
*
|
|
* In ESM5 code the helper calls are hidden inside the class's IIFE.
|
|
*
|
|
* @param classSymbol the class whose helper calls we are interested in. We expect this symbol
|
|
* to reference the inner identifier inside the IIFE.
|
|
* @returns an array of statements that may contain helper calls.
|
|
*/
|
|
protected getStatementsForClass(classSymbol: NgccClassSymbol): ts.Statement[] {
|
|
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 /////////////
|
|
|
|
/**
|
|
* Represents the details about property definitions that were set using `Object.defineProperty`.
|
|
*/
|
|
interface PropertyDefinition {
|
|
setter: ts.FunctionExpression|null;
|
|
getter: ts.FunctionExpression|null;
|
|
}
|
|
|
|
/**
|
|
* In ES5, getters and setters have been downleveled into call expressions of
|
|
* `Object.defineProperty`, such as
|
|
*
|
|
* ```
|
|
* Object.defineProperty(Clazz.prototype, "property", {
|
|
* get: function () {
|
|
* return 'value';
|
|
* },
|
|
* set: function (value) {
|
|
* this.value = value;
|
|
* },
|
|
* enumerable: true,
|
|
* configurable: true
|
|
* });
|
|
* ```
|
|
*
|
|
* This function inspects the given node to determine if it corresponds with such a call, and if so
|
|
* extracts the `set` and `get` function expressions from the descriptor object, if they exist.
|
|
*
|
|
* @param node The node to obtain the property definition from.
|
|
* @returns The property definition if the node corresponds with accessor, null otherwise.
|
|
*/
|
|
function getPropertyDefinition(node: ts.Node): PropertyDefinition|null {
|
|
if (!ts.isCallExpression(node)) return null;
|
|
|
|
const fn = node.expression;
|
|
if (!ts.isPropertyAccessExpression(fn) || !ts.isIdentifier(fn.expression) ||
|
|
fn.expression.text !== 'Object' || fn.name.text !== 'defineProperty')
|
|
return null;
|
|
|
|
const descriptor = node.arguments[2];
|
|
if (!descriptor || !ts.isObjectLiteralExpression(descriptor)) return null;
|
|
|
|
return {
|
|
setter: readPropertyFunctionExpression(descriptor, 'set'),
|
|
getter: readPropertyFunctionExpression(descriptor, 'get'),
|
|
};
|
|
}
|
|
|
|
function readPropertyFunctionExpression(object: ts.ObjectLiteralExpression, name: string) {
|
|
const property = object.properties.find(
|
|
(p): p is ts.PropertyAssignment =>
|
|
ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === name);
|
|
|
|
return property && ts.isFunctionExpression(property.initializer) && property.initializer || null;
|
|
}
|
|
|
|
function getReturnStatement(declaration: ts.Expression|undefined): ts.ReturnStatement|undefined {
|
|
return declaration && ts.isFunctionExpression(declaration) ?
|
|
declaration.body.statements.find(ts.isReturnStatement) :
|
|
undefined;
|
|
}
|
|
|
|
function reflectArrayElement(element: ts.Expression) {
|
|
return ts.isObjectLiteralExpression(element) ? reflectObjectLiteral(element) : null;
|
|
}
|
|
|
|
function isArgumentsIdentifier(expression: ts.Expression): boolean {
|
|
return ts.isIdentifier(expression) && expression.text === 'arguments';
|
|
}
|
|
|
|
function isSuperNotNull(expression: ts.Expression): boolean {
|
|
return isBinaryExpr(expression, ts.SyntaxKind.ExclamationEqualsEqualsToken) &&
|
|
isSuperIdentifier(expression.left);
|
|
}
|
|
|
|
function isBinaryExpr(
|
|
expression: ts.Expression, operator: ts.BinaryOperator): expression is ts.BinaryExpression {
|
|
return ts.isBinaryExpression(expression) && expression.operatorToken.kind === operator;
|
|
}
|
|
|
|
function isSuperIdentifier(node: ts.Node): boolean {
|
|
// Verify that the identifier is prefixed with `_super`. We don't test for equivalence
|
|
// as TypeScript may have suffixed the name, e.g. `_super_1` to avoid name conflicts.
|
|
// Requiring only a prefix should be sufficiently accurate.
|
|
return ts.isIdentifier(node) && node.text.startsWith('_super');
|
|
}
|
|
|
|
/**
|
|
* Parse the statement to extract the ESM5 parameter initializer if there is one.
|
|
* If one is found, add it to the appropriate parameter in the `parameters` collection.
|
|
*
|
|
* The form we are looking for is:
|
|
*
|
|
* ```
|
|
* if (arg === void 0) { arg = initializer; }
|
|
* ```
|
|
*
|
|
* @param statement a statement that may be initializing an optional parameter
|
|
* @param parameters the collection of parameters that were found in the function definition
|
|
* @returns true if the statement was a parameter initializer
|
|
*/
|
|
function captureParamInitializer(statement: ts.Statement, parameters: Parameter[]) {
|
|
if (ts.isIfStatement(statement) && isUndefinedComparison(statement.expression) &&
|
|
ts.isBlock(statement.thenStatement) && statement.thenStatement.statements.length === 1) {
|
|
const ifStatementComparison = statement.expression; // (arg === void 0)
|
|
const thenStatement = statement.thenStatement.statements[0]; // arg = initializer;
|
|
if (isAssignmentStatement(thenStatement)) {
|
|
const comparisonName = ifStatementComparison.left.text;
|
|
const assignmentName = thenStatement.expression.left.text;
|
|
if (comparisonName === assignmentName) {
|
|
const parameter = parameters.find(p => p.name === comparisonName);
|
|
if (parameter) {
|
|
parameter.initializer = thenStatement.expression.right;
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function isUndefinedComparison(expression: ts.Expression): expression is ts.Expression&
|
|
{left: ts.Identifier, right: ts.Expression} {
|
|
return ts.isBinaryExpression(expression) &&
|
|
expression.operatorToken.kind === ts.SyntaxKind.EqualsEqualsEqualsToken &&
|
|
ts.isVoidExpression(expression.right) && ts.isIdentifier(expression.left);
|
|
}
|
|
|
|
/**
|
|
* Parse the declaration of the given `classSymbol` to find the IIFE wrapper function.
|
|
*
|
|
* This function may accept a `_super` argument if there is a base class.
|
|
*
|
|
* ```
|
|
* var TestClass = (function (_super) {
|
|
* __extends(TestClass, _super);
|
|
* function TestClass() {}
|
|
* return TestClass;
|
|
* }(BaseClass));
|
|
* ```
|
|
*
|
|
* @param classSymbol the class whose iife wrapper function we want to get.
|
|
* @returns the IIFE function or null if it could not be parsed.
|
|
*/
|
|
function getIifeFn(classSymbol: NgccClassSymbol|undefined): ts.FunctionExpression|null {
|
|
if (classSymbol === undefined) {
|
|
return null;
|
|
}
|
|
|
|
const innerDeclaration = classSymbol.implementation.valueDeclaration;
|
|
const iifeBody = innerDeclaration.parent;
|
|
if (!ts.isBlock(iifeBody)) {
|
|
return null;
|
|
}
|
|
|
|
const iifeWrapper = iifeBody.parent;
|
|
return iifeWrapper && ts.isFunctionExpression(iifeWrapper) ? iifeWrapper : null;
|
|
}
|