mirror of
https://github.com/angular/angular-cli.git
synced 2025-05-25 16:57:51 +08:00
507 lines
16 KiB
TypeScript
507 lines
16 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright Google Inc. 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 { addPureComment } from '../helpers/ast-utils';
|
|
|
|
function isBlockLike(node: ts.Node): node is ts.BlockLike {
|
|
return node.kind === ts.SyntaxKind.Block
|
|
|| node.kind === ts.SyntaxKind.ModuleBlock
|
|
|| node.kind === ts.SyntaxKind.CaseClause
|
|
|| node.kind === ts.SyntaxKind.DefaultClause
|
|
|| node.kind === ts.SyntaxKind.SourceFile;
|
|
}
|
|
|
|
export function getWrapEnumsTransformer(): ts.TransformerFactory<ts.SourceFile> {
|
|
return (context: ts.TransformationContext): ts.Transformer<ts.SourceFile> => {
|
|
const transformer: ts.Transformer<ts.SourceFile> = sf => {
|
|
const result = visitBlockStatements(sf.statements, context);
|
|
|
|
return ts.updateSourceFileNode(sf, ts.setTextRange(result, sf.statements));
|
|
};
|
|
|
|
return transformer;
|
|
};
|
|
}
|
|
|
|
function visitBlockStatements(
|
|
statements: ts.NodeArray<ts.Statement>,
|
|
context: ts.TransformationContext,
|
|
): ts.NodeArray<ts.Statement> {
|
|
|
|
// copy of statements to modify; lazy initialized
|
|
let updatedStatements: Array<ts.Statement> | undefined;
|
|
|
|
const visitor: ts.Visitor = (node) => {
|
|
if (isBlockLike(node)) {
|
|
let result = visitBlockStatements(node.statements, context);
|
|
if (result === node.statements) {
|
|
return node;
|
|
}
|
|
result = ts.setTextRange(result, node.statements);
|
|
switch (node.kind) {
|
|
case ts.SyntaxKind.Block:
|
|
return ts.updateBlock(node, result);
|
|
case ts.SyntaxKind.ModuleBlock:
|
|
return ts.updateModuleBlock(node, result);
|
|
case ts.SyntaxKind.CaseClause:
|
|
return ts.updateCaseClause(node, node.expression, result);
|
|
case ts.SyntaxKind.DefaultClause:
|
|
return ts.updateDefaultClause(node, result);
|
|
default:
|
|
return node;
|
|
}
|
|
} else {
|
|
return ts.visitEachChild(node, visitor, context);
|
|
}
|
|
};
|
|
|
|
// 'oIndex' is the original statement index; 'uIndex' is the updated statement index
|
|
for (let oIndex = 0, uIndex = 0; oIndex < statements.length; oIndex++, uIndex++) {
|
|
const currentStatement = statements[oIndex];
|
|
|
|
// these can't contain an enum declaration
|
|
if (currentStatement.kind === ts.SyntaxKind.ImportDeclaration) {
|
|
continue;
|
|
}
|
|
|
|
// enum declarations must:
|
|
// * not be last statement
|
|
// * be a variable statement
|
|
// * have only one declaration
|
|
// * have an identifer as a declaration name
|
|
if (oIndex < statements.length - 1
|
|
&& ts.isVariableStatement(currentStatement)
|
|
&& currentStatement.declarationList.declarations.length === 1) {
|
|
|
|
const variableDeclaration = currentStatement.declarationList.declarations[0];
|
|
if (ts.isIdentifier(variableDeclaration.name)) {
|
|
const name = variableDeclaration.name.text;
|
|
|
|
if (!variableDeclaration.initializer) {
|
|
const iife = findTs2_3EnumIife(name, statements[oIndex + 1]);
|
|
if (iife) {
|
|
// found an enum
|
|
if (!updatedStatements) {
|
|
updatedStatements = statements.slice();
|
|
}
|
|
// update IIFE and replace variable statement and old IIFE
|
|
updatedStatements.splice(uIndex, 2, updateEnumIife(
|
|
currentStatement,
|
|
iife[0],
|
|
iife[1],
|
|
));
|
|
// skip IIFE statement
|
|
oIndex++;
|
|
continue;
|
|
}
|
|
} else if (ts.isObjectLiteralExpression(variableDeclaration.initializer)
|
|
&& variableDeclaration.initializer.properties.length === 0) {
|
|
const enumStatements = findTs2_2EnumStatements(name, statements, oIndex + 1);
|
|
if (enumStatements.length > 0) {
|
|
// found an enum
|
|
if (!updatedStatements) {
|
|
updatedStatements = statements.slice();
|
|
}
|
|
// create wrapper and replace variable statement and enum member statements
|
|
updatedStatements.splice(uIndex, enumStatements.length + 1, createWrappedEnum(
|
|
name,
|
|
currentStatement,
|
|
enumStatements,
|
|
variableDeclaration.initializer,
|
|
));
|
|
// skip enum member declarations
|
|
oIndex += enumStatements.length;
|
|
continue;
|
|
}
|
|
} else if (ts.isObjectLiteralExpression(variableDeclaration.initializer)
|
|
&& variableDeclaration.initializer.properties.length !== 0) {
|
|
const literalPropertyCount = variableDeclaration.initializer.properties.length;
|
|
|
|
// tsickle es2015 enums first statement is an export declaration
|
|
const isPotentialEnumExport = ts.isExportDeclaration(statements[oIndex + 1]);
|
|
if (isPotentialEnumExport) {
|
|
// skip the export
|
|
oIndex ++;
|
|
}
|
|
|
|
const enumStatements = findEnumNameStatements(name, statements, oIndex + 1);
|
|
if (enumStatements.length === literalPropertyCount) {
|
|
// found an enum
|
|
if (!updatedStatements) {
|
|
updatedStatements = statements.slice();
|
|
}
|
|
// create wrapper and replace variable statement and enum member statements
|
|
const deleteCount = enumStatements.length + (isPotentialEnumExport ? 2 : 1);
|
|
updatedStatements.splice(uIndex, deleteCount, createWrappedEnum(
|
|
name,
|
|
currentStatement,
|
|
enumStatements,
|
|
variableDeclaration.initializer,
|
|
isPotentialEnumExport,
|
|
));
|
|
// skip enum member declarations
|
|
oIndex += enumStatements.length;
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const result = ts.visitNode(currentStatement, visitor);
|
|
if (result !== currentStatement) {
|
|
if (!updatedStatements) {
|
|
updatedStatements = statements.slice();
|
|
}
|
|
updatedStatements[uIndex] = result;
|
|
}
|
|
}
|
|
|
|
// if changes, return updated statements
|
|
// otherwise, return original array instance
|
|
return updatedStatements ? ts.createNodeArray(updatedStatements) : statements;
|
|
}
|
|
|
|
// TS 2.3 enums have statements that are inside a IIFE.
|
|
function findTs2_3EnumIife(
|
|
name: string,
|
|
statement: ts.Statement,
|
|
): [ts.CallExpression, ts.Expression | undefined] | null {
|
|
if (!ts.isExpressionStatement(statement)) {
|
|
return null;
|
|
}
|
|
|
|
let expression = statement.expression;
|
|
while (ts.isParenthesizedExpression(expression)) {
|
|
expression = expression.expression;
|
|
}
|
|
|
|
if (!expression || !ts.isCallExpression(expression) || expression.arguments.length !== 1) {
|
|
return null;
|
|
}
|
|
|
|
const callExpression = expression;
|
|
let exportExpression;
|
|
|
|
let argument = expression.arguments[0];
|
|
if (!ts.isBinaryExpression(argument)) {
|
|
return null;
|
|
}
|
|
|
|
if (!ts.isIdentifier(argument.left) || argument.left.text !== name) {
|
|
return null;
|
|
}
|
|
|
|
let potentialExport = false;
|
|
if (argument.operatorToken.kind === ts.SyntaxKind.FirstAssignment) {
|
|
if (!ts.isBinaryExpression(argument.right)
|
|
|| argument.right.operatorToken.kind !== ts.SyntaxKind.BarBarToken) {
|
|
return null;
|
|
}
|
|
|
|
potentialExport = true;
|
|
argument = argument.right;
|
|
}
|
|
|
|
if (!ts.isBinaryExpression(argument)) {
|
|
return null;
|
|
}
|
|
|
|
if (argument.operatorToken.kind !== ts.SyntaxKind.BarBarToken) {
|
|
return null;
|
|
}
|
|
|
|
if (potentialExport && !ts.isIdentifier(argument.left)) {
|
|
exportExpression = argument.left;
|
|
}
|
|
|
|
expression = expression.expression;
|
|
while (ts.isParenthesizedExpression(expression)) {
|
|
expression = expression.expression;
|
|
}
|
|
|
|
if (!expression || !ts.isFunctionExpression(expression) || expression.parameters.length !== 1) {
|
|
return null;
|
|
}
|
|
|
|
const parameter = expression.parameters[0];
|
|
if (!ts.isIdentifier(parameter.name)) {
|
|
return null;
|
|
}
|
|
|
|
// The name of the parameter can be different than the name of the enum if it was renamed
|
|
// due to scope hoisting.
|
|
const parameterName = parameter.name.text;
|
|
|
|
// In TS 2.3 enums, the IIFE contains only expressions with a certain format.
|
|
// If we find any that is different, we ignore the whole thing.
|
|
for (let bodyIndex = 0; bodyIndex < expression.body.statements.length; ++bodyIndex) {
|
|
const bodyStatement = expression.body.statements[bodyIndex];
|
|
|
|
if (!ts.isExpressionStatement(bodyStatement) || !bodyStatement.expression) {
|
|
return null;
|
|
}
|
|
|
|
if (!ts.isBinaryExpression(bodyStatement.expression)
|
|
|| bodyStatement.expression.operatorToken.kind !== ts.SyntaxKind.FirstAssignment) {
|
|
return null;
|
|
}
|
|
|
|
const assignment = bodyStatement.expression.left;
|
|
const value = bodyStatement.expression.right;
|
|
if (!ts.isElementAccessExpression(assignment) || !ts.isStringLiteral(value)) {
|
|
return null;
|
|
}
|
|
|
|
if (!ts.isIdentifier(assignment.expression) || assignment.expression.text !== parameterName) {
|
|
return null;
|
|
}
|
|
|
|
const memberArgument = assignment.argumentExpression;
|
|
if (!memberArgument || !ts.isBinaryExpression(memberArgument)
|
|
|| memberArgument.operatorToken.kind !== ts.SyntaxKind.FirstAssignment) {
|
|
return null;
|
|
}
|
|
|
|
|
|
if (!ts.isElementAccessExpression(memberArgument.left)) {
|
|
return null;
|
|
}
|
|
|
|
if (!ts.isIdentifier(memberArgument.left.expression)
|
|
|| memberArgument.left.expression.text !== parameterName) {
|
|
return null;
|
|
}
|
|
|
|
if (!memberArgument.left.argumentExpression
|
|
|| !ts.isStringLiteral(memberArgument.left.argumentExpression)) {
|
|
return null;
|
|
}
|
|
|
|
if (memberArgument.left.argumentExpression.text !== value.text) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
return [callExpression, exportExpression];
|
|
}
|
|
|
|
// TS 2.2 enums have statements after the variable declaration, with index statements followed
|
|
// by value statements.
|
|
function findTs2_2EnumStatements(
|
|
name: string,
|
|
statements: ts.NodeArray<ts.Statement>,
|
|
statementOffset: number,
|
|
): ts.Statement[] {
|
|
const enumValueStatements: ts.Statement[] = [];
|
|
const memberNames: string[] = [];
|
|
|
|
let index = statementOffset;
|
|
for (; index < statements.length; ++index) {
|
|
// Ensure all statements are of the expected format and using the right identifer.
|
|
// When we find a statement that isn't part of the enum, return what we collected so far.
|
|
const current = statements[index];
|
|
if (!ts.isExpressionStatement(current) || !ts.isBinaryExpression(current.expression)) {
|
|
break;
|
|
}
|
|
|
|
const property = current.expression.left;
|
|
if (!property || !ts.isPropertyAccessExpression(property)) {
|
|
break;
|
|
}
|
|
|
|
if (!ts.isIdentifier(property.expression) || property.expression.text !== name) {
|
|
break;
|
|
}
|
|
|
|
memberNames.push(property.name.text);
|
|
enumValueStatements.push(current);
|
|
}
|
|
|
|
if (enumValueStatements.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
const enumNameStatements = findEnumNameStatements(name, statements, index, memberNames);
|
|
if (enumNameStatements.length !== enumValueStatements.length) {
|
|
return [];
|
|
}
|
|
|
|
return enumValueStatements.concat(enumNameStatements);
|
|
}
|
|
|
|
// Tsickle enums have a variable statement with indexes, followed by value statements.
|
|
// See https://github.com/angular/devkit/issues/229#issuecomment-338512056 fore more information.
|
|
function findEnumNameStatements(
|
|
name: string,
|
|
statements: ts.NodeArray<ts.Statement>,
|
|
statementOffset: number,
|
|
memberNames?: string[],
|
|
): ts.Statement[] {
|
|
const enumStatements: ts.Statement[] = [];
|
|
|
|
for (let index = statementOffset; index < statements.length; ++index) {
|
|
// Ensure all statements are of the expected format and using the right identifer.
|
|
// When we find a statement that isn't part of the enum, return what we collected so far.
|
|
const current = statements[index];
|
|
if (!ts.isExpressionStatement(current) || !ts.isBinaryExpression(current.expression)) {
|
|
break;
|
|
}
|
|
|
|
const access = current.expression.left;
|
|
const value = current.expression.right;
|
|
if (!access || !ts.isElementAccessExpression(access) || !value || !ts.isStringLiteral(value)) {
|
|
break;
|
|
}
|
|
|
|
if (memberNames && !memberNames.includes(value.text)) {
|
|
break;
|
|
}
|
|
|
|
if (!ts.isIdentifier(access.expression) || access.expression.text !== name) {
|
|
break;
|
|
}
|
|
|
|
if (!access.argumentExpression || !ts.isPropertyAccessExpression(access.argumentExpression)) {
|
|
break;
|
|
}
|
|
|
|
const enumExpression = access.argumentExpression.expression;
|
|
if (!ts.isIdentifier(enumExpression) || enumExpression.text !== name) {
|
|
break;
|
|
}
|
|
|
|
if (value.text !== access.argumentExpression.name.text) {
|
|
break;
|
|
}
|
|
|
|
enumStatements.push(current);
|
|
}
|
|
|
|
return enumStatements;
|
|
}
|
|
|
|
function updateHostNode(
|
|
hostNode: ts.VariableStatement,
|
|
expression: ts.Expression,
|
|
): ts.Statement {
|
|
|
|
// Update existing host node with the pure comment before the variable declaration initializer.
|
|
const variableDeclaration = hostNode.declarationList.declarations[0];
|
|
const outerVarStmt = ts.updateVariableStatement(
|
|
hostNode,
|
|
hostNode.modifiers,
|
|
ts.updateVariableDeclarationList(
|
|
hostNode.declarationList,
|
|
[
|
|
ts.updateVariableDeclaration(
|
|
variableDeclaration,
|
|
variableDeclaration.name,
|
|
variableDeclaration.type,
|
|
expression,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
return outerVarStmt;
|
|
}
|
|
|
|
function updateEnumIife(
|
|
hostNode: ts.VariableStatement,
|
|
iife: ts.CallExpression,
|
|
exportAssignment?: ts.Expression,
|
|
): ts.Statement {
|
|
if (!ts.isParenthesizedExpression(iife.expression)
|
|
|| !ts.isFunctionExpression(iife.expression.expression)) {
|
|
throw new Error('Invalid IIFE Structure');
|
|
}
|
|
|
|
// Ignore export assignment if variable is directly exported
|
|
if (hostNode.modifiers
|
|
&& hostNode.modifiers.findIndex(m => m.kind == ts.SyntaxKind.ExportKeyword) != -1) {
|
|
exportAssignment = undefined;
|
|
}
|
|
|
|
const expression = iife.expression.expression;
|
|
const updatedFunction = ts.updateFunctionExpression(
|
|
expression,
|
|
expression.modifiers,
|
|
expression.asteriskToken,
|
|
expression.name,
|
|
expression.typeParameters,
|
|
expression.parameters,
|
|
expression.type,
|
|
ts.updateBlock(
|
|
expression.body,
|
|
[
|
|
...expression.body.statements,
|
|
ts.createReturn(expression.parameters[0].name as ts.Identifier),
|
|
],
|
|
),
|
|
);
|
|
|
|
let arg: ts.Expression = ts.createObjectLiteral();
|
|
if (exportAssignment) {
|
|
arg = ts.createBinary(exportAssignment, ts.SyntaxKind.BarBarToken, arg);
|
|
}
|
|
const updatedIife = ts.updateCall(
|
|
iife,
|
|
ts.updateParen(
|
|
iife.expression,
|
|
updatedFunction,
|
|
),
|
|
iife.typeArguments,
|
|
[arg],
|
|
);
|
|
|
|
let value: ts.Expression = addPureComment(updatedIife);
|
|
if (exportAssignment) {
|
|
value = ts.createBinary(
|
|
exportAssignment,
|
|
ts.SyntaxKind.FirstAssignment,
|
|
updatedIife);
|
|
}
|
|
|
|
return updateHostNode(hostNode, value);
|
|
}
|
|
|
|
function createWrappedEnum(
|
|
name: string,
|
|
hostNode: ts.VariableStatement,
|
|
statements: Array<ts.Statement>,
|
|
literalInitializer: ts.ObjectLiteralExpression | undefined,
|
|
addExportModifier = false,
|
|
): ts.Statement {
|
|
literalInitializer = literalInitializer || ts.createObjectLiteral();
|
|
|
|
const node = addExportModifier
|
|
? ts.updateVariableStatement(
|
|
hostNode,
|
|
[ts.createToken(ts.SyntaxKind.ExportKeyword)],
|
|
hostNode.declarationList,
|
|
)
|
|
: hostNode;
|
|
|
|
const innerVarStmt = ts.createVariableStatement(
|
|
undefined,
|
|
ts.createVariableDeclarationList([
|
|
ts.createVariableDeclaration(name, undefined, literalInitializer),
|
|
]),
|
|
);
|
|
|
|
const innerReturn = ts.createReturn(ts.createIdentifier(name));
|
|
|
|
const iife = ts.createImmediatelyInvokedFunctionExpression([
|
|
innerVarStmt,
|
|
...statements,
|
|
innerReturn,
|
|
]);
|
|
|
|
return updateHostNode(node, addPureComment(ts.createParen(iife)));
|
|
}
|