Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions packages/compiler-cli/ngcc/src/host/decorated_class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

import * as ts from 'typescript';
import {Decorator} from '../../../src/ngtsc/reflection';
import {ClassDeclaration, Decorator} from '../../../src/ngtsc/reflection';

/**
* A simple container that holds the details of a decorated class that has been
Expand All @@ -18,9 +17,12 @@ export class DecoratedClass {
* Initialize a `DecoratedClass` that was found in a `DecoratedFile`.
* @param name The name of the class that has been found. This is mostly used
* for informational purposes.
* @param declaration The TypeScript AST node where this class is declared
* @param declaration The TypeScript AST node where this class is declared. In ES5 code, where a
* class can be represented by both a variable declaration and a function declaration (inside an
* IIFE), `declaration` will always refer to the outer variable declaration, which represents the
* class to the rest of the program.
* @param decorators The collection of decorators that have been found on this class.
*/
constructor(
public name: string, public declaration: ts.Declaration, public decorators: Decorator[], ) {}
public name: string, public declaration: ClassDeclaration, public decorators: Decorator[]) {}
}
244 changes: 132 additions & 112 deletions packages/compiler-cli/ngcc/src/host/esm2015_host.ts

Large diffs are not rendered by default.

225 changes: 171 additions & 54 deletions packages/compiler-cli/ngcc/src/host/esm5_host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import * as ts from 'typescript';

import {ClassMember, ClassMemberKind, Declaration, Decorator, FunctionDefinition, Parameter, reflectObjectLiteral} from '../../../src/ngtsc/reflection';
import {ClassDeclaration, ClassMember, ClassMemberKind, ClassSymbol, CtorParameter, Declaration, Decorator, FunctionDefinition, Parameter, isNamedVariableDeclaration, reflectObjectLiteral} from '../../../src/ngtsc/reflection';
import {getNameText, hasNameIdentifier} from '../utils';

import {Esm2015ReflectionHost, ParamInfo, getPropertyValueFromSymbol, isAssignmentStatement} from './esm2015_host';
Expand Down Expand Up @@ -36,21 +36,23 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
/**
* Check whether the given node actually represents a class.
*/
isClass(node: ts.Node): node is ts.NamedDeclaration {
return super.isClass(node) || !!this.getClassSymbol(node);
}
isClass(node: ts.Node): node is ClassDeclaration { return !!this.getClassDeclaration(node); }

/**
* Determines whether the given declaration has a base class.
* Determines whether the given declaration, which should be a "class", has a base "class".
*
* In ES5 code, we need to determine if the IIFE wrapper takes a `_super` parameter .
*
* In ES5, we need to determine if the IIFE wrapper takes a `_super` parameter .
* @param clazz a `ClassDeclaration` representing the class over which to reflect.
*/
hasBaseClass(node: ts.Declaration): boolean {
const classSymbol = this.getClassSymbol(node);
if (!classSymbol) return false;
hasBaseClass(clazz: ClassDeclaration): boolean {
if (super.hasBaseClass(clazz)) return true;

const iifeBody = classSymbol.valueDeclaration.parent;
if (!iifeBody || !ts.isBlock(iifeBody)) return false;
const classDeclaration = this.getClassDeclaration(clazz);
if (!classDeclaration) return false;

const iifeBody = getIifeBody(classDeclaration);
if (!iifeBody) return false;

const iife = iifeBody.parent;
if (!iife || !ts.isFunctionExpression(iife)) return false;
Expand All @@ -59,38 +61,39 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
}

/**
* Find a symbol for a node that we think is a class.
* Find the declaration of a class given a node that we think represents the class.
*
* In ES5, the implementation of a class is a function expression that is hidden inside an IIFE.
* So we might need to dig around inside to get hold of the "class" symbol.
* 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.
*
* `node` might be one of:
* - A class declaration (from a declaration file).
* - A class declaration (from a typings file).
* - The declaration of the outer variable, which is assigned the result of the IIFE.
* - The function declaration inside the IIFE, which is eventually returned and assigned to the
* outer variable.
*
* @param node the top level declaration that represents an exported class or the function
* expression inside the IIFE.
* @returns the symbol for the node or `undefined` if it is not a "class" or has no symbol.
* The returned declaration is either the class declaration (from the typings file) or the outer
* variable declaration.
*
* @param node the node that represents the class whose declaration we are finding.
* @returns the declaration of the class or `undefined` if it is not a "class".
*/
getClassSymbol(node: ts.Node): ts.Symbol|undefined {
const symbol = super.getClassSymbol(node);
if (symbol) return symbol;

if (ts.isVariableDeclaration(node)) {
const iifeBody = getIifeBody(node);
if (!iifeBody) return undefined;

const innerClassIdentifier = getReturnIdentifier(iifeBody);
if (!innerClassIdentifier) return undefined;

return this.checker.getSymbolAtLocation(innerClassIdentifier);
getClassDeclaration(node: ts.Node): ClassDeclaration|undefined {
const superDeclaration = super.getClassDeclaration(node);
if (superDeclaration) return superDeclaration;

const outerClass = getClassDeclarationFromInnerFunctionDeclaration(node);
if (outerClass) return outerClass;

// At this point, `node` could be the outer variable declaration of an ES5 class.
// If so, ensure that it has a `name` identifier and the correct structure.
if (!isNamedVariableDeclaration(node) ||
!this.getInnerFunctionDeclarationFromClassDeclaration(node)) {
return undefined;
}

const outerClassNode = getClassDeclarationFromInnerFunctionDeclaration(node);

return outerClassNode && this.getClassSymbol(outerClassNode);
return node;
}

/**
Expand All @@ -113,12 +116,7 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
getDeclarationOfIdentifier(id: ts.Identifier): Declaration|null {
// Get the identifier for the outer class node (if any).
const outerClassNode = getClassDeclarationFromInnerFunctionDeclaration(id.parent);

if (outerClassNode && hasNameIdentifier(outerClassNode)) {
id = outerClassNode.name;
}

const declaration = super.getDeclarationOfIdentifier(id);
const declaration = super.getDeclarationOfIdentifier(outerClassNode ? outerClassNode.name : id);

if (!declaration || !ts.isVariableDeclaration(declaration.node) ||
declaration.node.initializer !== undefined ||
Expand Down Expand Up @@ -170,31 +168,149 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
return {node, body: statements || null, parameters};
}

/**
* Examine a declaration which should be of a class, and return metadata about the members of the
* class.
*
* @param declaration a TypeScript `ts.Declaration` node representing the class over which to
* reflect.
*
* @returns an array of `ClassMember` metadata representing the members of the class.
*
* @throws if `declaration` does not resolve to a class declaration.
*/
getMembersOfClass(clazz: ClassDeclaration): ClassMember[] {
if (super.isClass(clazz)) return super.getMembersOfClass(clazz);

// The necessary info is on the inner function declaration (inside the ES5 class IIFE).
const innerFunctionSymbol = this.getInnerFunctionSymbolFromClassDeclaration(clazz);
if (!innerFunctionSymbol) {
throw new Error(
`Attempted to get members of a non-class: "${(clazz as ClassDeclaration).getText()}"`);
}

return this.getMembersOfSymbol(innerFunctionSymbol);
}


///////////// Protected Helpers /////////////

/**
* Get the inner function declaration of an ES5-style class.
*
* In ES5, the implementation of a class is a function expression that is hidden inside an IIFE
* and returned to be assigned to a variable outside the IIFE, which is what the rest of the
* program interacts with.
*
* Given the outer variable declaration, we want to get to the inner function declaration.
*
* @param node a node that could be the variable expression outside an ES5 class IIFE.
* @param checker the TS program TypeChecker
* @returns the inner function declaration or `undefined` if it is not a "class".
*/
protected getInnerFunctionDeclarationFromClassDeclaration(node: ts.Node): ts.FunctionDeclaration
|undefined {
if (!ts.isVariableDeclaration(node)) return undefined;

// Extract the IIFE body (if any).
const iifeBody = getIifeBody(node);
if (!iifeBody) return undefined;

// Extract the function declaration from inside the IIFE.
const functionDeclaration = iifeBody.statements.find(ts.isFunctionDeclaration);
if (!functionDeclaration) return undefined;

// Extract the return identifier of the IIFE.
const returnIdentifier = getReturnIdentifier(iifeBody);
const returnIdentifierSymbol =
returnIdentifier && this.checker.getSymbolAtLocation(returnIdentifier);
if (!returnIdentifierSymbol) return undefined;

// Verify that the inner function is returned.
if (returnIdentifierSymbol.valueDeclaration !== functionDeclaration) return undefined;

return functionDeclaration;
}

/**
* Get the identifier symbol of the inner function declaration of an ES5-style class.
*
* In ES5, the implementation of a class is a function expression that is hidden inside an IIFE
* and returned to be assigned to a variable outside the IIFE, which is what the rest of the
* program interacts with.
*
* Given the outer variable declaration, we want to get to the identifier symbol of the inner
* function declaration.
*
* @param clazz a node that could be the variable expression outside an ES5 class IIFE.
* @param checker the TS program TypeChecker
* @returns the inner function declaration identifier symbol or `undefined` if it is not a "class"
* or has no identifier.
*/
protected getInnerFunctionSymbolFromClassDeclaration(clazz: ClassDeclaration): ClassSymbol
|undefined {
const innerFunctionDeclaration = this.getInnerFunctionDeclarationFromClassDeclaration(clazz);
if (!innerFunctionDeclaration || !hasNameIdentifier(innerFunctionDeclaration)) return undefined;

return this.checker.getSymbolAtLocation(innerFunctionDeclaration.name) as ClassSymbol;
}

/**
* 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 declaration
* function itself.
* 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 class whose parameters we want to find.
* @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.
* the class's constructor or `null` if there is no constructor.
*/
protected getConstructorParameterDeclarations(classSymbol: ts.Symbol):
protected getConstructorParameterDeclarations(classSymbol: ClassSymbol):
ts.ParameterDeclaration[]|null {
const constructor = classSymbol.valueDeclaration as ts.FunctionDeclaration;
const constructor =
this.getInnerFunctionDeclarationFromClassDeclaration(classSymbol.valueDeclaration);
if (!constructor) return null;

if (constructor.parameters.length > 0) {
return Array.from(constructor.parameters);
}

if (isSynthesizedConstructor(constructor)) {
return null;
}

return [];
}

/**
* Get the parameter decorators of a class constructor.
*
* @param classSymbol the symbol of the class (i.e. the outer variable declaration) whose
* parameter info we want to get.
* @param parameterNodes the array of TypeScript parameter nodes for this class's constructor.
* @returns an array of constructor parameter info objects.
*/
protected getConstructorParamInfo(
classSymbol: ClassSymbol, parameterNodes: ts.ParameterDeclaration[]): CtorParameter[] {
// The necessary info is on the inner function declaration (inside the ES5 class IIFE).
const innerFunctionSymbol =
this.getInnerFunctionSymbolFromClassDeclaration(classSymbol.valueDeclaration);
if (!innerFunctionSymbol) return [];

return super.getConstructorParamInfo(innerFunctionSymbol, parameterNodes);
}

protected getDecoratorsOfSymbol(symbol: ClassSymbol): Decorator[]|null {
// The necessary info is on the inner function declaration (inside the ES5 class IIFE).
const innerFunctionSymbol =
this.getInnerFunctionSymbolFromClassDeclaration(symbol.valueDeclaration);
if (!innerFunctionSymbol) return null;

return super.getDecoratorsOfSymbol(innerFunctionSymbol);
}

/**
* Get the parameter type and decorators for the constructor of a class,
* where the information is stored on a static method of the class.
Expand Down Expand Up @@ -310,10 +426,9 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
* to reference the inner identifier inside the IIFE.
* @returns an array of statements that may contain helper calls.
*/
protected getStatementsForClass(classSymbol: ts.Symbol): ts.Statement[] {
const classDeclaration = classSymbol.valueDeclaration;
return ts.isBlock(classDeclaration.parent) ? Array.from(classDeclaration.parent.statements) :
[];
protected getStatementsForClass(classSymbol: ClassSymbol): ts.Statement[] {
const classDeclarationParent = classSymbol.valueDeclaration.parent;
return ts.isBlock(classDeclarationParent) ? Array.from(classDeclarationParent.statements) : [];
}
}

Expand Down Expand Up @@ -388,8 +503,8 @@ function readPropertyFunctionExpression(object: ts.ObjectLiteralExpression, name
* @param node a node that could be the function expression inside an ES5 class IIFE.
* @returns the outer variable declaration or `undefined` if it is not a "class".
*/
function getClassDeclarationFromInnerFunctionDeclaration(node: ts.Node): ts.VariableDeclaration|
undefined {
function getClassDeclarationFromInnerFunctionDeclaration(node: ts.Node):
ClassDeclaration<ts.VariableDeclaration>|undefined {
if (ts.isFunctionDeclaration(node)) {
// It might be the function expression inside the IIFE. We need to go 5 levels up...

Expand All @@ -413,14 +528,16 @@ function getClassDeclarationFromInnerFunctionDeclaration(node: ts.Node): ts.Vari
outerNode = outerNode.parent;
if (!outerNode || !ts.isVariableDeclaration(outerNode)) return undefined;

return outerNode;
// Finally, ensure that the variable declaration has a `name` identifier.
return hasNameIdentifier(outerNode) ? outerNode : undefined;
}

return undefined;
}

function getIifeBody(declaration: ts.VariableDeclaration): ts.Block|undefined {
if (!declaration.initializer || !ts.isParenthesizedExpression(declaration.initializer)) {
export function getIifeBody(declaration: ts.Declaration): ts.Block|undefined {
if (!ts.isVariableDeclaration(declaration) || !declaration.initializer ||
!ts.isParenthesizedExpression(declaration.initializer)) {
return undefined;
}
const call = declaration.initializer;
Expand Down
4 changes: 2 additions & 2 deletions packages/compiler-cli/ngcc/src/host/ngcc_host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import * as ts from 'typescript';
import {ReflectionHost} from '../../../src/ngtsc/reflection';
import {ClassSymbol, ReflectionHost} from '../../../src/ngtsc/reflection';
import {DecoratedClass} from './decorated_class';

export const PRE_R3_MARKER = '__PRE_R3__';
Expand Down Expand Up @@ -52,7 +52,7 @@ export interface NgccReflectionHost extends ReflectionHost {
* @returns the symbol for the declaration or `undefined` if it is not
* a "class" or has no symbol.
*/
getClassSymbol(node: ts.Node): ts.Symbol|undefined;
getClassSymbol(node: ts.Node): ClassSymbol|undefined;

/**
* Search the given module for variable declarations in which the initializer
Expand Down
14 changes: 6 additions & 8 deletions packages/compiler-cli/ngcc/src/rendering/esm5_renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/
import * as ts from 'typescript';
import MagicString from 'magic-string';
import {getIifeBody} from '../host/esm5_host';
import {NgccReflectionHost} from '../host/ngcc_host';
import {CompiledClass} from '../analysis/decoration_analyzer';
import {EsmRenderer} from './esm_renderer';
Expand All @@ -23,21 +24,18 @@ export class Esm5Renderer extends EsmRenderer {
* Add the definitions to each decorated class
*/
addDefinitions(output: MagicString, compiledClass: CompiledClass, definitions: string): void {
const classSymbol = this.host.getClassSymbol(compiledClass.declaration);
if (!classSymbol) {
throw new Error(
`Compiled class does not have a valid symbol: ${compiledClass.name} in ${compiledClass.declaration.getSourceFile().fileName}`);
}
const parent = classSymbol.valueDeclaration && classSymbol.valueDeclaration.parent;
if (!parent || !ts.isBlock(parent)) {
const iifeBody = getIifeBody(compiledClass.declaration);
if (!iifeBody) {
throw new Error(
`Compiled class declaration is not inside an IIFE: ${compiledClass.name} in ${compiledClass.declaration.getSourceFile().fileName}`);
}
const returnStatement = parent.statements.find(statement => ts.isReturnStatement(statement));

const returnStatement = iifeBody.statements.find(ts.isReturnStatement);
if (!returnStatement) {
throw new Error(
`Compiled class wrapper IIFE does not have a return statement: ${compiledClass.name} in ${compiledClass.declaration.getSourceFile().fileName}`);
}

const insertionPoint = returnStatement.getFullStart();
output.appendLeft(insertionPoint, '\n' + definitions);
}
Expand Down
Loading