refactor(compiler-cli): make the output AST translator generic (#38775)

This commit refactors the `ExpressionTranslatorVisitor` so that it
is not tied directly to the TypeScript AST. Instead it uses generic
`TExpression` and `TStatement` types that are then converted
to concrete types by the `TypeScriptAstFactory`.

This paves the way for a `BabelAstFactory` that can be used to
generate Babel AST nodes instead of TypeScript, which will be
part of the new linker tool.

PR Close #38775
This commit is contained in:
Pete Bacon Darwin 2020-09-09 22:00:27 +01:00 committed by Misko Hevery
parent a93605f2a4
commit 297b123151
17 changed files with 1403 additions and 439 deletions

View File

@ -55,7 +55,7 @@ export class CommonJsRenderingFormatter extends Esm5RenderingFormatter {
const namedImport = entryPointBasePath !== basePath ?
importManager.generateNamedImport(relativePath, e.identifier) :
{symbol: e.identifier, moduleImport: null};
const importNamespace = namedImport.moduleImport ? `${namedImport.moduleImport}.` : '';
const importNamespace = namedImport.moduleImport ? `${namedImport.moduleImport.text}.` : '';
const exportStr = `\nexports.${e.identifier} = ${importNamespace}${namedImport.symbol};`;
output.append(exportStr);
});
@ -66,7 +66,7 @@ export class CommonJsRenderingFormatter extends Esm5RenderingFormatter {
file: ts.SourceFile): void {
for (const e of exports) {
const namedImport = importManager.generateNamedImport(e.fromModule, e.symbolName);
const importNamespace = namedImport.moduleImport ? `${namedImport.moduleImport}.` : '';
const importNamespace = namedImport.moduleImport ? `${namedImport.moduleImport.text}.` : '';
const exportStr = `\nexports.${e.asAlias} = ${importNamespace}${namedImport.symbol};`;
output.append(exportStr);
}

View File

@ -9,7 +9,6 @@ import {Statement} from '@angular/compiler';
import MagicString from 'magic-string';
import * as ts from 'typescript';
import {NOOP_DEFAULT_IMPORT_RECORDER} from '../../../src/ngtsc/imports';
import {ImportManager, translateStatement} from '../../../src/ngtsc/translator';
import {CompiledClass} from '../analysis/types';
import {getContainingStatement} from '../host/esm2015_host';
@ -65,8 +64,9 @@ export class Esm5RenderingFormatter extends EsmRenderingFormatter {
* @return The JavaScript code corresponding to `stmt` (in the appropriate format).
*/
printStatement(stmt: Statement, sourceFile: ts.SourceFile, importManager: ImportManager): string {
const node =
translateStatement(stmt, importManager, NOOP_DEFAULT_IMPORT_RECORDER, ts.ScriptTarget.ES5);
const node = translateStatement(
stmt, importManager,
{downlevelLocalizedStrings: true, downlevelVariableDeclarations: true});
const code = this.printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);
return code;

View File

@ -10,7 +10,7 @@ import MagicString from 'magic-string';
import * as ts from 'typescript';
import {absoluteFromSourceFile, AbsoluteFsPath, dirname, relative, toRelativeImport} from '../../../src/ngtsc/file_system';
import {NOOP_DEFAULT_IMPORT_RECORDER, Reexport} from '../../../src/ngtsc/imports';
import {Reexport} from '../../../src/ngtsc/imports';
import {Import, ImportManager, translateStatement} from '../../../src/ngtsc/translator';
import {isDtsPath} from '../../../src/ngtsc/util/src/typescript';
import {ModuleWithProvidersInfo} from '../analysis/module_with_providers_analyzer';
@ -247,8 +247,7 @@ export class EsmRenderingFormatter implements RenderingFormatter {
* @return The JavaScript code corresponding to `stmt` (in the appropriate format).
*/
printStatement(stmt: Statement, sourceFile: ts.SourceFile, importManager: ImportManager): string {
const node = translateStatement(
stmt, importManager, NOOP_DEFAULT_IMPORT_RECORDER, ts.ScriptTarget.ES2015);
const node = translateStatement(stmt, importManager);
const code = this.printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);
return code;
@ -264,8 +263,6 @@ export class EsmRenderingFormatter implements RenderingFormatter {
return 0;
}
/**
* Check whether the given type is the core Angular `ModuleWithProviders` interface.
* @param typeName The type to check.
@ -292,7 +289,8 @@ function findStatement(node: ts.Node): ts.Statement|undefined {
function generateImportString(
importManager: ImportManager, importPath: string|null, importName: string) {
const importAs = importPath ? importManager.generateNamedImport(importPath, importName) : null;
return importAs ? `${importAs.moduleImport}.${importAs.symbol}` : `${importName}`;
return importAs && importAs.moduleImport ? `${importAs.moduleImport.text}.${importAs.symbol}` :
`${importName}`;
}
function getNextSiblingInArray<T extends ts.Node>(node: T, array: ts.NodeArray<T>): T|null {

View File

@ -91,7 +91,7 @@ export class UmdRenderingFormatter extends Esm5RenderingFormatter {
const namedImport = entryPointBasePath !== basePath ?
importManager.generateNamedImport(relativePath, e.identifier) :
{symbol: e.identifier, moduleImport: null};
const importNamespace = namedImport.moduleImport ? `${namedImport.moduleImport}.` : '';
const importNamespace = namedImport.moduleImport ? `${namedImport.moduleImport.text}.` : '';
const exportStr = `\nexports.${e.identifier} = ${importNamespace}${namedImport.symbol};`;
output.appendRight(insertionPoint, exportStr);
});
@ -111,7 +111,7 @@ export class UmdRenderingFormatter extends Esm5RenderingFormatter {
lastStatement ? lastStatement.getEnd() : factoryFunction.body.getEnd() - 1;
for (const e of exports) {
const namedImport = importManager.generateNamedImport(e.fromModule, e.symbolName);
const importNamespace = namedImport.moduleImport ? `${namedImport.moduleImport}.` : '';
const importNamespace = namedImport.moduleImport ? `${namedImport.moduleImport.text}.` : '';
const exportStr = `\nexports.${e.asAlias} = ${importNamespace}${namedImport.symbol};`;
output.appendRight(insertionPoint, exportStr);
}

View File

@ -13,7 +13,7 @@ import * as ts from 'typescript';
import {absoluteFrom, getFileSystem} from '../../../src/ngtsc/file_system';
import {runInEachFileSystem, TestFile} from '../../../src/ngtsc/file_system/testing';
import {NOOP_DEFAULT_IMPORT_RECORDER, Reexport} from '../../../src/ngtsc/imports';
import {Reexport} from '../../../src/ngtsc/imports';
import {MockLogger} from '../../../src/ngtsc/logging/testing';
import {Import, ImportManager, translateStatement} from '../../../src/ngtsc/translator';
import {loadTestFiles} from '../../../test/helpers';
@ -65,8 +65,8 @@ class TestRenderingFormatter implements RenderingFormatter {
}
printStatement(stmt: Statement, sourceFile: ts.SourceFile, importManager: ImportManager): string {
const node = translateStatement(
stmt, importManager, NOOP_DEFAULT_IMPORT_RECORDER,
this.isEs5 ? ts.ScriptTarget.ES5 : ts.ScriptTarget.ES2015);
stmt, importManager,
{downlevelLocalizedStrings: this.isEs5, downlevelVariableDeclarations: this.isEs5});
const code = this.printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);
return `// TRANSPILED\n${code}`;

View File

@ -133,8 +133,7 @@ runInEachFileSystem(() => {
}
const sf = getSourceFileOrError(program, _('/index.ts'));
const im = new ImportManager(new NoopImportRewriter(), 'i');
const tsStatement =
translateStatement(call, im, NOOP_DEFAULT_IMPORT_RECORDER, ts.ScriptTarget.ES2015);
const tsStatement = translateStatement(call, im);
const res = ts.createPrinter().printNode(ts.EmitHint.Unspecified, tsStatement, sf);
return res.replace(/\s+/g, ' ');
}

View File

@ -11,7 +11,7 @@ import * as ts from 'typescript';
import {DefaultImportRecorder, ImportRewriter} from '../../imports';
import {Decorator, ReflectionHost} from '../../reflection';
import {ImportManager, translateExpression, translateStatement} from '../../translator';
import {ImportManager, RecordWrappedNodeExprFn, translateExpression, translateStatement} from '../../translator';
import {visit, VisitListEntryResult, Visitor} from '../../util/src/visitor';
import {CompileResult} from './api';
@ -35,11 +35,12 @@ export function ivyTransformFactory(
compilation: TraitCompiler, reflector: ReflectionHost, importRewriter: ImportRewriter,
defaultImportRecorder: DefaultImportRecorder, isCore: boolean,
isClosureCompilerEnabled: boolean): ts.TransformerFactory<ts.SourceFile> {
const recordWrappedNodeExpr = createRecorderFn(defaultImportRecorder);
return (context: ts.TransformationContext): ts.Transformer<ts.SourceFile> => {
return (file: ts.SourceFile): ts.SourceFile => {
return transformIvySourceFile(
compilation, context, reflector, importRewriter, file, isCore, isClosureCompilerEnabled,
defaultImportRecorder);
recordWrappedNodeExpr);
};
};
}
@ -77,7 +78,7 @@ class IvyTransformationVisitor extends Visitor {
private compilation: TraitCompiler,
private classCompilationMap: Map<ts.ClassDeclaration, CompileResult[]>,
private reflector: ReflectionHost, private importManager: ImportManager,
private defaultImportRecorder: DefaultImportRecorder,
private recordWrappedNodeExpr: RecordWrappedNodeExprFn<ts.Expression>,
private isClosureCompilerEnabled: boolean, private isCore: boolean) {
super();
}
@ -97,8 +98,8 @@ class IvyTransformationVisitor extends Visitor {
for (const field of this.classCompilationMap.get(node)!) {
// Translate the initializer for the field into TS nodes.
const exprNode = translateExpression(
field.initializer, this.importManager, this.defaultImportRecorder,
ts.ScriptTarget.ES2015);
field.initializer, this.importManager,
{recordWrappedNodeExpr: this.recordWrappedNodeExpr});
// Create a static property declaration for the new field.
const property = ts.createProperty(
@ -118,7 +119,7 @@ class IvyTransformationVisitor extends Visitor {
field.statements
.map(
stmt => translateStatement(
stmt, this.importManager, this.defaultImportRecorder, ts.ScriptTarget.ES2015))
stmt, this.importManager, {recordWrappedNodeExpr: this.recordWrappedNodeExpr}))
.forEach(stmt => statements.push(stmt));
members.push(property);
@ -248,7 +249,7 @@ function transformIvySourceFile(
compilation: TraitCompiler, context: ts.TransformationContext, reflector: ReflectionHost,
importRewriter: ImportRewriter, file: ts.SourceFile, isCore: boolean,
isClosureCompilerEnabled: boolean,
defaultImportRecorder: DefaultImportRecorder): ts.SourceFile {
recordWrappedNodeExpr: RecordWrappedNodeExprFn<ts.Expression>): ts.SourceFile {
const constantPool = new ConstantPool(isClosureCompilerEnabled);
const importManager = new ImportManager(importRewriter);
@ -270,14 +271,18 @@ function transformIvySourceFile(
// results obtained at Step 1.
const transformationVisitor = new IvyTransformationVisitor(
compilation, compilationVisitor.classCompilationMap, reflector, importManager,
defaultImportRecorder, isClosureCompilerEnabled, isCore);
recordWrappedNodeExpr, isClosureCompilerEnabled, isCore);
let sf = visit(file, transformationVisitor, context);
// Generate the constant statements first, as they may involve adding additional imports
// to the ImportManager.
const constants = constantPool.statements.map(
stmt => translateStatement(
stmt, importManager, defaultImportRecorder, getLocalizeCompileTarget(context)));
const downlevelTranslatedCode = getLocalizeCompileTarget(context) < ts.ScriptTarget.ES2015;
const constants =
constantPool.statements.map(stmt => translateStatement(stmt, importManager, {
recordWrappedNodeExpr,
downlevelLocalizedStrings: downlevelTranslatedCode,
downlevelVariableDeclarations: downlevelTranslatedCode,
}));
// Preserve @fileoverview comments required by Closure, since the location might change as a
// result of adding extra imports and constant pool statements.
@ -360,3 +365,12 @@ function maybeFilterDecorator(
function isFromAngularCore(decorator: Decorator): boolean {
return decorator.import !== null && decorator.import.from === '@angular/core';
}
function createRecorderFn(defaultImportRecorder: DefaultImportRecorder):
RecordWrappedNodeExprFn<ts.Expression> {
return expr => {
if (ts.isIdentifier(expr)) {
defaultImportRecorder.recordUsedIdentifier(expr);
}
};
}

View File

@ -6,6 +6,10 @@
* found in the LICENSE file at https://angular.io/license
*/
export {Import, ImportManager, NamedImport} from './src/import_manager';
export {attachComments, translateExpression, translateStatement} from './src/translator';
export {AstFactory, BinaryOperator, LeadingComment, ObjectLiteralProperty, SourceMapLocation, SourceMapRange, TemplateElement, TemplateLiteral, UnaryOperator} from './src/api/ast_factory';
export {Import, ImportGenerator, NamedImport} from './src/api/import_generator';
export {ImportManager} from './src/import_manager';
export {RecordWrappedNodeExprFn} from './src/translator';
export {translateType} from './src/type_translator';
export {attachComments, TypeScriptAstFactory} from './src/typescript_ast_factory';
export {translateExpression, translateStatement} from './src/typescript_translator';

View File

@ -0,0 +1,320 @@
/**
* @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
*/
/**
* Used to create transpiler specific AST nodes from Angular Output AST nodes in an abstract way.
*
* Note that the `AstFactory` makes no assumptions about the target language being generated.
* It is up to the caller to do this - e.g. only call `createTaggedTemplate()` or pass `let`|`const`
* to `createVariableDeclaration()` if the final JS will allow it.
*/
export interface AstFactory<TStatement, TExpression> {
/**
* Attach the `leadingComments` to the given `statement` node.
*
* @param statement the statement where the comment is to be attached.
* @param leadingComments the comments to attach.
* @returns the node passed in as `statement` with the comments attached.
*/
attachComments(statement: TStatement, leadingComments?: LeadingComment[]): TStatement;
/**
* Create a literal array expresion (e.g. `[expr1, expr2]`).
*
* @param elements a collection of the expressions to appear in each array slot.
*/
createArrayLiteral(elements: TExpression[]): TExpression;
/**
* Create an assignment expression (e.g. `lhsExpr = rhsExpr`).
*
* @param target an expression that evaluates to the left side of the assignment.
* @param value an expression that evaluates to the right side of the assignment.
*/
createAssignment(target: TExpression, value: TExpression): TExpression;
/**
* Create a binary expression (e.g. `lhs && rhs`).
*
* @param leftOperand an expression that will appear on the left of the operator.
* @param operator the binary operator that will be applied.
* @param rightOperand an expression that will appear on the right of the operator.
*/
createBinaryExpression(
leftOperand: TExpression, operator: BinaryOperator, rightOperand: TExpression): TExpression;
/**
* Create a block of statements (e.g. `{ stmt1; stmt2; }`).
*
* @param body an array of statements to be wrapped in a block.
*/
createBlock(body: TStatement[]): TStatement;
/**
* Create an expression that is calling the `callee` with the given `args`.
*
* @param callee an expression that evaluates to a function to be called.
* @param args the arugments to be passed to the call.
* @param pure whether to mark the call as pure (having no side-effects).
*/
createCallExpression(callee: TExpression, args: TExpression[], pure: boolean): TExpression;
/**
* Create a ternary expression (e.g. `testExpr ? trueExpr : falseExpr`).
*
* @param condition an expression that will be tested for truthiness.
* @param thenExpression an expression that is executed if `condition` is truthy.
* @param elseExpression an expression that is executed if `condition` is falsy.
*/
createConditional(
condition: TExpression, thenExpression: TExpression,
elseExpression: TExpression): TExpression;
/**
* Create an element access (e.g. `obj[expr]`).
*
* @param expression an expression that evaluates to the object to be accessed.
* @param element an expression that evaluates to the element on the object.
*/
createElementAccess(expression: TExpression, element: TExpression): TExpression;
/**
* Create a statement that is simply executing the given `expression` (e.g. `x = 10;`).
*
* @param expression the expression to be converted to a statement.
*/
createExpressionStatement(expression: TExpression): TStatement;
/**
* Create a statement that declares a function (e.g. `function foo(param1, param2) { stmt; }`).
*
* @param functionName the name of the function.
* @param parameters the names of the function's parameters.
* @param body a statement (or a block of statements) that are the body of the function.
*/
createFunctionDeclaration(functionName: string|null, parameters: string[], body: TStatement):
TStatement;
/**
* Create an expression that represents a function
* (e.g. `function foo(param1, param2) { stmt; }`).
*
* @param functionName the name of the function.
* @param parameters the names of the function's parameters.
* @param body a statement (or a block of statements) that are the body of the function.
*/
createFunctionExpression(functionName: string|null, parameters: string[], body: TStatement):
TExpression;
/**
* Create an identifier.
*
* @param name the name of the identifier.
*/
createIdentifier(name: string): TExpression;
/**
* Create an if statement (e.g. `if (testExpr) { trueStmt; } else { falseStmt; }`).
*
* @param condition an expression that will be tested for truthiness.
* @param thenStatement a statement (or block of statements) that is executed if `condition` is
* truthy.
* @param elseStatement a statement (or block of statements) that is executed if `condition` is
* falsy.
*/
createIfStatement(
condition: TExpression, thenStatement: TStatement,
elseStatement: TStatement|null): TStatement;
/**
* Create a simple literal (e.g. `"string"`, `123`, `false`, etc).
*
* @param value the value of the literal.
*/
createLiteral(value: string|number|boolean|null|undefined): TExpression;
/**
* Create an expression that is instantiating the `expression` as a class.
*
* @param expression an expression that evaluates to a constructor to be instantiated.
* @param args the arguments to be passed to the constructor.
*/
createNewExpression(expression: TExpression, args: TExpression[]): TExpression;
/**
* Create a literal object expression (e.g. `{ prop1: expr1, prop2: expr2 }`).
*
* @param properties the properties (key and value) to appear in the object.
*/
createObjectLiteral(properties: ObjectLiteralProperty<TExpression>[]): TExpression;
/**
* Wrap an expression in parentheses.
*
* @param expression the expression to wrap in parentheses.
*/
createParenthesizedExpression(expression: TExpression): TExpression;
/**
* Create a property access (e.g. `obj.prop`).
*
* @param expression an expression that evaluates to the object to be accessed.
* @param propertyName the name of the property to access.
*/
createPropertyAccess(expression: TExpression, propertyName: string): TExpression;
/**
* Create a return statement (e.g `return expr;`).
*
* @param expression the expression to be returned.
*/
createReturnStatement(expression: TExpression|null): TStatement;
/**
* Create a tagged template literal string. E.g.
*
* ```
* tag`str1${expr1}str2${expr2}str3`
* ```
*
* @param tag an expression that is applied as a tag handler for this template string.
* @param template the collection of strings and expressions that constitute an interpolated
* template literal.
*/
createTaggedTemplate(tag: TExpression, template: TemplateLiteral<TExpression>): TExpression;
/**
* Create a throw statement (e.g. `throw expr;`).
*
* @param expression the expression to be thrown.
*/
createThrowStatement(expression: TExpression): TStatement;
/**
* Create an expression that extracts the type of an expression (e.g. `typeof expr`).
*
* @param expression the expression whose type we want.
*/
createTypeOfExpression(expression: TExpression): TExpression;
/**
* Prefix the `operand` with the given `operator` (e.g. `-expr`).
*
* @param operator the text of the operator to apply (e.g. `+`, `-` or `!`).
* @param operand the expression that the operator applies to.
*/
createUnaryExpression(operator: UnaryOperator, operand: TExpression): TExpression;
/**
* Create an expression that declares a new variable, possibly initialized to `initializer`.
*
* @param variableName the name of the variable.
* @param initializer if not `null` then this expression is assigned to the declared variable.
* @param type whether this variable should be declared as `var`, `let` or `const`.
*/
createVariableDeclaration(
variableName: string, initializer: TExpression|null,
type: VariableDeclarationType): TStatement;
/**
* Attach a source map range to the given node.
*
* @param node the node to which the range should be attached.
* @param sourceMapRange the range to attach to the node, or null if there is no range to attach.
* @returns the `node` with the `sourceMapRange` attached.
*/
setSourceMapRange<T extends TStatement|TExpression>(node: T, sourceMapRange: SourceMapRange|null):
T;
}
/**
* The type of a variable declaration.
*/
export type VariableDeclarationType = 'const'|'let'|'var';
/**
* The unary operators supported by the `AstFactory`.
*/
export type UnaryOperator = '+'|'-'|'!';
/**
* The binary operators supported by the `AstFactory`.
*/
export type BinaryOperator =
'&&'|'>'|'>='|'&'|'/'|'=='|'==='|'<'|'<='|'-'|'%'|'*'|'!='|'!=='|'||'|'+';
/**
* The original location of the start or end of a node created by the `AstFactory`.
*/
export interface SourceMapLocation {
/** 0-based character position of the location in the original source file. */
offset: number;
/** 0-based line index of the location in the original source file. */
line: number;
/** 0-based column position of the location in the original source file. */
column: number;
}
/**
* The original range of a node created by the `AstFactory`.
*/
export interface SourceMapRange {
url: string;
content: string;
start: SourceMapLocation;
end: SourceMapLocation;
}
/**
* Information used by the `AstFactory` to create a property on an object literal expression.
*/
export interface ObjectLiteralProperty<TExpression> {
propertyName: string;
value: TExpression;
/**
* Whether the `propertyName` should be enclosed in quotes.
*/
quoted: boolean;
}
/**
* Information used by the `AstFactory` to create a template literal string (i.e. a back-ticked
* string with interpolations).
*/
export interface TemplateLiteral<TExpression> {
/**
* A collection of the static string pieces of the interpolated template literal string.
*/
elements: TemplateElement[];
/**
* A collection of the interpolated expressions that are interleaved between the elements.
*/
expressions: TExpression[];
}
/**
* Information about a static string piece of an interpolated template literal string.
*/
export interface TemplateElement {
/** The raw string as it was found in the original source code. */
raw: string;
/** The parsed string, with escape codes etc processed. */
cooked: string;
/** The original location of this piece of the template literal string. */
range: SourceMapRange|null;
}
/**
* Information used by the `AstFactory` to prepend a comment to a statement that was created by the
* `AstFactory`.
*/
export interface LeadingComment {
toString(): string;
multiline: boolean;
trailingNewline: boolean;
}

View File

@ -0,0 +1,39 @@
/**
* @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
*/
/**
* Information about an import that has been added to a module.
*/
export interface Import {
/** The name of the module that has been imported. */
specifier: string;
/** The alias of the imported module. */
qualifier: string;
}
/**
* The symbol name and import namespace of an imported symbol,
* which has been registered through the ImportGenerator.
*/
export interface NamedImport<TExpression> {
/** The import namespace containing this imported symbol. */
moduleImport: TExpression|null;
/** The (possibly rewritten) name of the imported symbol. */
symbol: string;
}
/**
* Generate import information based on the context of the code being generated.
*
* Implementations of these methods return a specific identifier that corresponds to the imported
* module.
*/
export interface ImportGenerator<TExpression> {
generateNamespaceImport(moduleName: string): TExpression;
generateNamedImport(moduleName: string, originalSymbol: string): NamedImport<TExpression>;
}

View File

@ -5,37 +5,26 @@
* 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 {ImportRewriter, NoopImportRewriter} from '../../imports/src/core';
import * as ts from 'typescript';
import {ImportRewriter, NoopImportRewriter} from '../../imports';
import {Import, ImportGenerator, NamedImport} from './api/import_generator';
/**
* Information about an import that has been added to a module.
*/
export interface Import {
/** The name of the module that has been imported. */
specifier: string;
/** The alias of the imported module. */
qualifier: string;
}
/**
* The symbol name and import namespace of an imported symbol,
* which has been registered through the ImportManager.
*/
export interface NamedImport {
/** The import namespace containing this imported symbol. */
moduleImport: string|null;
/** The (possibly rewritten) name of the imported symbol. */
symbol: string;
}
export class ImportManager {
private specifierToIdentifier = new Map<string, string>();
export class ImportManager implements ImportGenerator<ts.Identifier> {
private specifierToIdentifier = new Map<string, ts.Identifier>();
private nextIndex = 0;
constructor(protected rewriter: ImportRewriter = new NoopImportRewriter(), private prefix = 'i') {
}
generateNamedImport(moduleName: string, originalSymbol: string): NamedImport {
generateNamespaceImport(moduleName: string): ts.Identifier {
if (!this.specifierToIdentifier.has(moduleName)) {
this.specifierToIdentifier.set(
moduleName, ts.createIdentifier(`${this.prefix}${this.nextIndex++}`));
}
return this.specifierToIdentifier.get(moduleName)!;
}
generateNamedImport(moduleName: string, originalSymbol: string): NamedImport<ts.Identifier> {
// First, rewrite the symbol name.
const symbol = this.rewriter.rewriteSymbol(originalSymbol, moduleName);
@ -46,12 +35,8 @@ export class ImportManager {
return {moduleImport: null, symbol};
}
// If not, this symbol will be imported. Allocate a prefix for the imported module if needed.
if (!this.specifierToIdentifier.has(moduleName)) {
this.specifierToIdentifier.set(moduleName, `${this.prefix}${this.nextIndex++}`);
}
const moduleImport = this.specifierToIdentifier.get(moduleName)!;
// If not, this symbol will be imported using a generated namespace import.
const moduleImport = this.generateNamespaceImport(moduleName);
return {moduleImport, symbol};
}
@ -60,7 +45,7 @@ export class ImportManager {
const imports: {specifier: string, qualifier: string}[] = [];
this.specifierToIdentifier.forEach((qualifier, specifier) => {
specifier = this.rewriter.rewriteSpecifier(specifier, contextPath);
imports.push({specifier, qualifier});
imports.push({specifier, qualifier: qualifier.text});
});
return imports;
}

View File

@ -5,214 +5,249 @@
* 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 o from '@angular/compiler';
import {AssertNotNull, BinaryOperator, BinaryOperatorExpr, CastExpr, ClassStmt, CommaExpr, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, Expression, ExpressionStatement, ExpressionVisitor, ExternalExpr, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, LeadingComment, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, NotExpr, ParseSourceSpan, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, Statement, StatementVisitor, StmtModifier, ThrowStmt, TryCatchStmt, TypeofExpr, WrappedNodeExpr, WriteKeyExpr, WritePropExpr, WriteVarExpr} from '@angular/compiler';
import {LocalizedString, UnaryOperator, UnaryOperatorExpr} from '@angular/compiler/src/output/output_ast';
import * as ts from 'typescript';
import {DefaultImportRecorder} from '../../imports';
import {AstFactory, BinaryOperator, ObjectLiteralProperty, SourceMapRange, TemplateElement, TemplateLiteral, UnaryOperator} from './api/ast_factory';
import {ImportGenerator} from './api/import_generator';
import {Context} from './context';
import {ImportManager} from './import_manager';
const UNARY_OPERATORS = new Map<UnaryOperator, ts.PrefixUnaryOperator>([
[UnaryOperator.Minus, ts.SyntaxKind.MinusToken],
[UnaryOperator.Plus, ts.SyntaxKind.PlusToken],
const UNARY_OPERATORS = new Map<o.UnaryOperator, UnaryOperator>([
[o.UnaryOperator.Minus, '-'],
[o.UnaryOperator.Plus, '+'],
]);
const BINARY_OPERATORS = new Map<BinaryOperator, ts.BinaryOperator>([
[BinaryOperator.And, ts.SyntaxKind.AmpersandAmpersandToken],
[BinaryOperator.Bigger, ts.SyntaxKind.GreaterThanToken],
[BinaryOperator.BiggerEquals, ts.SyntaxKind.GreaterThanEqualsToken],
[BinaryOperator.BitwiseAnd, ts.SyntaxKind.AmpersandToken],
[BinaryOperator.Divide, ts.SyntaxKind.SlashToken],
[BinaryOperator.Equals, ts.SyntaxKind.EqualsEqualsToken],
[BinaryOperator.Identical, ts.SyntaxKind.EqualsEqualsEqualsToken],
[BinaryOperator.Lower, ts.SyntaxKind.LessThanToken],
[BinaryOperator.LowerEquals, ts.SyntaxKind.LessThanEqualsToken],
[BinaryOperator.Minus, ts.SyntaxKind.MinusToken],
[BinaryOperator.Modulo, ts.SyntaxKind.PercentToken],
[BinaryOperator.Multiply, ts.SyntaxKind.AsteriskToken],
[BinaryOperator.NotEquals, ts.SyntaxKind.ExclamationEqualsToken],
[BinaryOperator.NotIdentical, ts.SyntaxKind.ExclamationEqualsEqualsToken],
[BinaryOperator.Or, ts.SyntaxKind.BarBarToken],
[BinaryOperator.Plus, ts.SyntaxKind.PlusToken],
const BINARY_OPERATORS = new Map<o.BinaryOperator, BinaryOperator>([
[o.BinaryOperator.And, '&&'],
[o.BinaryOperator.Bigger, '>'],
[o.BinaryOperator.BiggerEquals, '>='],
[o.BinaryOperator.BitwiseAnd, '&'],
[o.BinaryOperator.Divide, '/'],
[o.BinaryOperator.Equals, '=='],
[o.BinaryOperator.Identical, '==='],
[o.BinaryOperator.Lower, '<'],
[o.BinaryOperator.LowerEquals, '<='],
[o.BinaryOperator.Minus, '-'],
[o.BinaryOperator.Modulo, '%'],
[o.BinaryOperator.Multiply, '*'],
[o.BinaryOperator.NotEquals, '!='],
[o.BinaryOperator.NotIdentical, '!=='],
[o.BinaryOperator.Or, '||'],
[o.BinaryOperator.Plus, '+'],
]);
export function translateExpression(
expression: Expression, imports: ImportManager, defaultImportRecorder: DefaultImportRecorder,
scriptTarget: Exclude<ts.ScriptTarget, ts.ScriptTarget.JSON>): ts.Expression {
return expression.visitExpression(
new ExpressionTranslatorVisitor(imports, defaultImportRecorder, scriptTarget),
new Context(false));
export type RecordWrappedNodeExprFn<TExpression> = (expr: TExpression) => void;
export interface TranslatorOptions<TExpression> {
downlevelLocalizedStrings?: boolean;
downlevelVariableDeclarations?: boolean;
recordWrappedNodeExpr?: RecordWrappedNodeExprFn<TExpression>;
}
export function translateStatement(
statement: Statement, imports: ImportManager, defaultImportRecorder: DefaultImportRecorder,
scriptTarget: Exclude<ts.ScriptTarget, ts.ScriptTarget.JSON>): ts.Statement {
return statement.visitStatement(
new ExpressionTranslatorVisitor(imports, defaultImportRecorder, scriptTarget),
new Context(true));
}
export class ExpressionTranslatorVisitor<TStatement, TExpression> implements o.ExpressionVisitor,
o.StatementVisitor {
private downlevelLocalizedStrings: boolean;
private downlevelVariableDeclarations: boolean;
private recordWrappedNodeExpr: RecordWrappedNodeExprFn<TExpression>;
class ExpressionTranslatorVisitor implements ExpressionVisitor, StatementVisitor {
private externalSourceFiles = new Map<string, ts.SourceMapSource>();
constructor(
private imports: ImportManager, private defaultImportRecorder: DefaultImportRecorder,
private scriptTarget: Exclude<ts.ScriptTarget, ts.ScriptTarget.JSON>) {}
visitDeclareVarStmt(stmt: DeclareVarStmt, context: Context): ts.VariableStatement {
const varType = this.scriptTarget < ts.ScriptTarget.ES2015 ?
ts.NodeFlags.None :
stmt.hasModifier(StmtModifier.Final) ? ts.NodeFlags.Const : ts.NodeFlags.Let;
const varDeclaration = ts.createVariableDeclaration(
/* name */ stmt.name,
/* type */ undefined,
/* initializer */ stmt.value?.visitExpression(this, context.withExpressionMode));
const declarationList = ts.createVariableDeclarationList(
/* declarations */[varDeclaration],
/* flags */ varType);
const varStatement = ts.createVariableStatement(undefined, declarationList);
return attachComments(varStatement, stmt.leadingComments);
private factory: AstFactory<TStatement, TExpression>,
private imports: ImportGenerator<TExpression>, options: TranslatorOptions<TExpression>) {
this.downlevelLocalizedStrings = options.downlevelLocalizedStrings === true;
this.downlevelVariableDeclarations = options.downlevelVariableDeclarations === true;
this.recordWrappedNodeExpr = options.recordWrappedNodeExpr || (() => {});
}
visitDeclareFunctionStmt(stmt: DeclareFunctionStmt, context: Context): ts.FunctionDeclaration {
const fnDeclaration = ts.createFunctionDeclaration(
/* decorators */ undefined,
/* modifiers */ undefined,
/* asterisk */ undefined,
/* name */ stmt.name,
/* typeParameters */ undefined,
/* parameters */
stmt.params.map(param => ts.createParameter(undefined, undefined, undefined, param.name)),
/* type */ undefined,
/* body */
ts.createBlock(
stmt.statements.map(child => child.visitStatement(this, context.withStatementMode))));
return attachComments(fnDeclaration, stmt.leadingComments);
}
visitExpressionStmt(stmt: ExpressionStatement, context: Context): ts.ExpressionStatement {
return attachComments(
ts.createStatement(stmt.expr.visitExpression(this, context.withStatementMode)),
visitDeclareVarStmt(stmt: o.DeclareVarStmt, context: Context): TStatement {
const varType = this.downlevelVariableDeclarations ?
'var' :
stmt.hasModifier(o.StmtModifier.Final) ? 'const' : 'let';
return this.factory.attachComments(
this.factory.createVariableDeclaration(
stmt.name, stmt.value?.visitExpression(this, context.withExpressionMode), varType),
stmt.leadingComments);
}
visitReturnStmt(stmt: ReturnStatement, context: Context): ts.ReturnStatement {
return attachComments(
ts.createReturn(stmt.value.visitExpression(this, context.withExpressionMode)),
visitDeclareFunctionStmt(stmt: o.DeclareFunctionStmt, context: Context): TStatement {
return this.factory.attachComments(
this.factory.createFunctionDeclaration(
stmt.name, stmt.params.map(param => param.name),
this.factory.createBlock(
this.visitStatements(stmt.statements, context.withStatementMode))),
stmt.leadingComments);
}
visitDeclareClassStmt(stmt: ClassStmt, context: Context) {
if (this.scriptTarget < ts.ScriptTarget.ES2015) {
throw new Error(
`Unsupported mode: Visiting a "declare class" statement (class ${stmt.name}) while ` +
`targeting ${ts.ScriptTarget[this.scriptTarget]}.`);
}
visitExpressionStmt(stmt: o.ExpressionStatement, context: Context): TStatement {
return this.factory.attachComments(
this.factory.createExpressionStatement(
stmt.expr.visitExpression(this, context.withStatementMode)),
stmt.leadingComments);
}
visitReturnStmt(stmt: o.ReturnStatement, context: Context): TStatement {
return this.factory.attachComments(
this.factory.createReturnStatement(
stmt.value.visitExpression(this, context.withExpressionMode)),
stmt.leadingComments);
}
visitDeclareClassStmt(_stmt: o.ClassStmt, _context: Context): never {
throw new Error('Method not implemented.');
}
visitIfStmt(stmt: IfStmt, context: Context): ts.IfStatement {
const thenBlock = ts.createBlock(
stmt.trueCase.map(child => child.visitStatement(this, context.withStatementMode)));
const elseBlock = stmt.falseCase.length > 0 ?
ts.createBlock(
stmt.falseCase.map(child => child.visitStatement(this, context.withStatementMode))) :
undefined;
const ifStatement =
ts.createIf(stmt.condition.visitExpression(this, context), thenBlock, elseBlock);
return attachComments(ifStatement, stmt.leadingComments);
}
visitTryCatchStmt(stmt: TryCatchStmt, context: Context) {
throw new Error('Method not implemented.');
}
visitThrowStmt(stmt: ThrowStmt, context: Context): ts.ThrowStatement {
return attachComments(
ts.createThrow(stmt.error.visitExpression(this, context.withExpressionMode)),
visitIfStmt(stmt: o.IfStmt, context: Context): TStatement {
return this.factory.attachComments(
this.factory.createIfStatement(
stmt.condition.visitExpression(this, context),
this.factory.createBlock(
this.visitStatements(stmt.trueCase, context.withStatementMode)),
stmt.falseCase.length > 0 ? this.factory.createBlock(this.visitStatements(
stmt.falseCase, context.withStatementMode)) :
null),
stmt.leadingComments);
}
visitReadVarExpr(ast: ReadVarExpr, context: Context): ts.Identifier {
const identifier = ts.createIdentifier(ast.name!);
visitTryCatchStmt(_stmt: o.TryCatchStmt, _context: Context): never {
throw new Error('Method not implemented.');
}
visitThrowStmt(stmt: o.ThrowStmt, context: Context): TStatement {
return this.factory.attachComments(
this.factory.createThrowStatement(
stmt.error.visitExpression(this, context.withExpressionMode)),
stmt.leadingComments);
}
visitReadVarExpr(ast: o.ReadVarExpr, _context: Context): TExpression {
const identifier = this.factory.createIdentifier(ast.name!);
this.setSourceMapRange(identifier, ast.sourceSpan);
return identifier;
}
visitWriteVarExpr(expr: WriteVarExpr, context: Context): ts.Expression {
const result: ts.Expression = ts.createBinary(
ts.createIdentifier(expr.name), ts.SyntaxKind.EqualsToken,
expr.value.visitExpression(this, context));
return context.isStatement ? result : ts.createParen(result);
visitWriteVarExpr(expr: o.WriteVarExpr, context: Context): TExpression {
const assignment = this.factory.createAssignment(
this.setSourceMapRange(this.factory.createIdentifier(expr.name), expr.sourceSpan),
expr.value.visitExpression(this, context),
);
return context.isStatement ? assignment :
this.factory.createParenthesizedExpression(assignment);
}
visitWriteKeyExpr(expr: WriteKeyExpr, context: Context): ts.Expression {
visitWriteKeyExpr(expr: o.WriteKeyExpr, context: Context): TExpression {
const exprContext = context.withExpressionMode;
const lhs = ts.createElementAccess(
const target = this.factory.createElementAccess(
expr.receiver.visitExpression(this, exprContext),
expr.index.visitExpression(this, exprContext));
const rhs = expr.value.visitExpression(this, exprContext);
const result: ts.Expression = ts.createBinary(lhs, ts.SyntaxKind.EqualsToken, rhs);
return context.isStatement ? result : ts.createParen(result);
expr.index.visitExpression(this, exprContext),
);
const assignment =
this.factory.createAssignment(target, expr.value.visitExpression(this, exprContext));
return context.isStatement ? assignment :
this.factory.createParenthesizedExpression(assignment);
}
visitWritePropExpr(expr: WritePropExpr, context: Context): ts.BinaryExpression {
return ts.createBinary(
ts.createPropertyAccess(expr.receiver.visitExpression(this, context), expr.name),
ts.SyntaxKind.EqualsToken, expr.value.visitExpression(this, context));
visitWritePropExpr(expr: o.WritePropExpr, context: Context): TExpression {
const target =
this.factory.createPropertyAccess(expr.receiver.visitExpression(this, context), expr.name);
return this.factory.createAssignment(target, expr.value.visitExpression(this, context));
}
visitInvokeMethodExpr(ast: InvokeMethodExpr, context: Context): ts.CallExpression {
visitInvokeMethodExpr(ast: o.InvokeMethodExpr, context: Context): TExpression {
const target = ast.receiver.visitExpression(this, context);
const call = ts.createCall(
ast.name !== null ? ts.createPropertyAccess(target, ast.name) : target, undefined,
ast.args.map(arg => arg.visitExpression(this, context)));
this.setSourceMapRange(call, ast.sourceSpan);
return call;
return this.setSourceMapRange(
this.factory.createCallExpression(
ast.name !== null ? this.factory.createPropertyAccess(target, ast.name) : target,
ast.args.map(arg => arg.visitExpression(this, context)),
/* pure */ false),
ast.sourceSpan);
}
visitInvokeFunctionExpr(ast: InvokeFunctionExpr, context: Context): ts.CallExpression {
const expr = ts.createCall(
ast.fn.visitExpression(this, context), undefined,
visitInvokeFunctionExpr(ast: o.InvokeFunctionExpr, context: Context): TExpression {
return this.setSourceMapRange(
this.factory.createCallExpression(
ast.fn.visitExpression(this, context),
ast.args.map(arg => arg.visitExpression(this, context)), ast.pure),
ast.sourceSpan);
}
visitInstantiateExpr(ast: o.InstantiateExpr, context: Context): TExpression {
return this.factory.createNewExpression(
ast.classExpr.visitExpression(this, context),
ast.args.map(arg => arg.visitExpression(this, context)));
if (ast.pure) {
ts.addSyntheticLeadingComment(expr, ts.SyntaxKind.MultiLineCommentTrivia, '@__PURE__', false);
}
visitLiteralExpr(ast: o.LiteralExpr, _context: Context): TExpression {
return this.setSourceMapRange(this.factory.createLiteral(ast.value), ast.sourceSpan);
}
visitLocalizedString(ast: o.LocalizedString, context: Context): TExpression {
// A `$localize` message consists of `messageParts` and `expressions`, which get interleaved
// together. The interleaved pieces look like:
// `[messagePart0, expression0, messagePart1, expression1, messagePart2]`
//
// Note that there is always a message part at the start and end, and so therefore
// `messageParts.length === expressions.length + 1`.
//
// Each message part may be prefixed with "metadata", which is wrapped in colons (:) delimiters.
// The metadata is attached to the first and subsequent message parts by calls to
// `serializeI18nHead()` and `serializeI18nTemplatePart()` respectively.
//
// The first message part (i.e. `ast.messageParts[0]`) is used to initialize `messageParts`
// array.
const elements: TemplateElement[] = [createTemplateElement(ast.serializeI18nHead())];
const expressions: TExpression[] = [];
for (let i = 0; i < ast.expressions.length; i++) {
const placeholder = this.setSourceMapRange(
ast.expressions[i].visitExpression(this, context), ast.getPlaceholderSourceSpan(i));
expressions.push(placeholder);
elements.push(createTemplateElement(ast.serializeI18nTemplatePart(i + 1)));
}
this.setSourceMapRange(expr, ast.sourceSpan);
return expr;
const localizeTag = this.factory.createIdentifier('$localize');
// Now choose which implementation to use to actually create the necessary AST nodes.
const localizeCall = this.downlevelLocalizedStrings ?
this.createES5TaggedTemplateFunctionCall(localizeTag, {elements, expressions}) :
this.factory.createTaggedTemplate(localizeTag, {elements, expressions});
return this.setSourceMapRange(localizeCall, ast.sourceSpan);
}
visitInstantiateExpr(ast: InstantiateExpr, context: Context): ts.NewExpression {
return ts.createNew(
ast.classExpr.visitExpression(this, context), undefined,
ast.args.map(arg => arg.visitExpression(this, context)));
}
/**
* Translate the tagged template literal into a call that is compatible with ES5, using the
* imported `__makeTemplateObject` helper for ES5 formatted output.
*/
private createES5TaggedTemplateFunctionCall(
tagHandler: TExpression, {elements, expressions}: TemplateLiteral<TExpression>): TExpression {
// Ensure that the `__makeTemplateObject()` helper has been imported.
const {moduleImport, symbol} =
this.imports.generateNamedImport('tslib', '__makeTemplateObject');
const __makeTemplateObjectHelper = (moduleImport === null) ?
this.factory.createIdentifier(symbol) :
this.factory.createPropertyAccess(moduleImport, symbol);
visitLiteralExpr(ast: LiteralExpr, context: Context): ts.Expression {
let expr: ts.Expression;
if (ast.value === undefined) {
expr = ts.createIdentifier('undefined');
} else if (ast.value === null) {
expr = ts.createNull();
} else {
expr = ts.createLiteral(ast.value);
// Collect up the cooked and raw strings into two separate arrays.
const cooked: TExpression[] = [];
const raw: TExpression[] = [];
for (const element of elements) {
cooked.push(this.factory.setSourceMapRange(
this.factory.createLiteral(element.cooked), element.range));
raw.push(
this.factory.setSourceMapRange(this.factory.createLiteral(element.raw), element.range));
}
this.setSourceMapRange(expr, ast.sourceSpan);
return expr;
// Generate the helper call in the form: `__makeTemplateObject([cooked], [raw]);`
const templateHelperCall = this.factory.createCallExpression(
__makeTemplateObjectHelper,
[this.factory.createArrayLiteral(cooked), this.factory.createArrayLiteral(raw)],
/* pure */ false);
// Finally create the tagged handler call in the form:
// `tag(__makeTemplateObject([cooked], [raw]), ...expressions);`
return this.factory.createCallExpression(
tagHandler, [templateHelperCall, ...expressions],
/* pure */ false);
}
visitLocalizedString(ast: LocalizedString, context: Context): ts.Expression {
const localizedString = this.scriptTarget >= ts.ScriptTarget.ES2015 ?
this.createLocalizedStringTaggedTemplate(ast, context) :
this.createLocalizedStringFunctionCall(ast, context);
this.setSourceMapRange(localizedString, ast.sourceSpan);
return localizedString;
}
visitExternalExpr(ast: ExternalExpr, context: Context): ts.PropertyAccessExpression
|ts.Identifier {
visitExternalExpr(ast: o.ExternalExpr, _context: Context): TExpression {
if (ast.value.name === null) {
throw new Error(`Import unknown module or symbol ${ast.value}`);
}
@ -224,19 +259,18 @@ class ExpressionTranslatorVisitor implements ExpressionVisitor, StatementVisitor
this.imports.generateNamedImport(ast.value.moduleName, ast.value.name);
if (moduleImport === null) {
// The symbol was ambient after all.
return ts.createIdentifier(symbol);
return this.factory.createIdentifier(symbol);
} else {
return ts.createPropertyAccess(
ts.createIdentifier(moduleImport), ts.createIdentifier(symbol));
return this.factory.createPropertyAccess(moduleImport, symbol);
}
} else {
// The symbol is ambient, so just reference it.
return ts.createIdentifier(ast.value.name);
return this.factory.createIdentifier(ast.value.name);
}
}
visitConditionalExpr(ast: ConditionalExpr, context: Context): ts.ConditionalExpression {
let cond: ts.Expression = ast.condition.visitExpression(this, context);
visitConditionalExpr(ast: o.ConditionalExpr, context: Context): TExpression {
let cond: TExpression = ast.condition.visitExpression(this, context);
// Ordinarily the ternary operator is right-associative. The following are equivalent:
// `a ? b : c ? d : e` => `a ? b : (c ? d : e)`
@ -258,259 +292,128 @@ class ExpressionTranslatorVisitor implements ExpressionVisitor, StatementVisitor
// conditional expression is directly used as the condition of another.
//
// TODO(alxhub): investigate better logic for precendence of conditional operators
if (ast.condition instanceof ConditionalExpr) {
if (ast.condition instanceof o.ConditionalExpr) {
// The condition of this ternary needs to be wrapped in parentheses to maintain
// left-associativity.
cond = ts.createParen(cond);
cond = this.factory.createParenthesizedExpression(cond);
}
return ts.createConditional(
return this.factory.createConditional(
cond, ast.trueCase.visitExpression(this, context),
ast.falseCase!.visitExpression(this, context));
}
visitNotExpr(ast: NotExpr, context: Context): ts.PrefixUnaryExpression {
return ts.createPrefix(
ts.SyntaxKind.ExclamationToken, ast.condition.visitExpression(this, context));
visitNotExpr(ast: o.NotExpr, context: Context): TExpression {
return this.factory.createUnaryExpression('!', ast.condition.visitExpression(this, context));
}
visitAssertNotNullExpr(ast: AssertNotNull, context: Context): ts.NonNullExpression {
visitAssertNotNullExpr(ast: o.AssertNotNull, context: Context): TExpression {
return ast.condition.visitExpression(this, context);
}
visitCastExpr(ast: CastExpr, context: Context): ts.Expression {
visitCastExpr(ast: o.CastExpr, context: Context): TExpression {
return ast.value.visitExpression(this, context);
}
visitFunctionExpr(ast: FunctionExpr, context: Context): ts.FunctionExpression {
return ts.createFunctionExpression(
undefined, undefined, ast.name || undefined, undefined,
ast.params.map(
param => ts.createParameter(
undefined, undefined, undefined, param.name, undefined, undefined, undefined)),
undefined, ts.createBlock(ast.statements.map(stmt => stmt.visitStatement(this, context))));
visitFunctionExpr(ast: o.FunctionExpr, context: Context): TExpression {
return this.factory.createFunctionExpression(
ast.name ?? null, ast.params.map(param => param.name),
this.factory.createBlock(this.visitStatements(ast.statements, context)));
}
visitUnaryOperatorExpr(ast: UnaryOperatorExpr, context: Context): ts.Expression {
if (!UNARY_OPERATORS.has(ast.operator)) {
throw new Error(`Unknown unary operator: ${UnaryOperator[ast.operator]}`);
}
return ts.createPrefix(
UNARY_OPERATORS.get(ast.operator)!, ast.expr.visitExpression(this, context));
}
visitBinaryOperatorExpr(ast: BinaryOperatorExpr, context: Context): ts.Expression {
visitBinaryOperatorExpr(ast: o.BinaryOperatorExpr, context: Context): TExpression {
if (!BINARY_OPERATORS.has(ast.operator)) {
throw new Error(`Unknown binary operator: ${BinaryOperator[ast.operator]}`);
throw new Error(`Unknown binary operator: ${o.BinaryOperator[ast.operator]}`);
}
return ts.createBinary(
ast.lhs.visitExpression(this, context), BINARY_OPERATORS.get(ast.operator)!,
ast.rhs.visitExpression(this, context));
return this.factory.createBinaryExpression(
ast.lhs.visitExpression(this, context),
BINARY_OPERATORS.get(ast.operator)!,
ast.rhs.visitExpression(this, context),
);
}
visitReadPropExpr(ast: ReadPropExpr, context: Context): ts.PropertyAccessExpression {
return ts.createPropertyAccess(ast.receiver.visitExpression(this, context), ast.name);
visitReadPropExpr(ast: o.ReadPropExpr, context: Context): TExpression {
return this.factory.createPropertyAccess(ast.receiver.visitExpression(this, context), ast.name);
}
visitReadKeyExpr(ast: ReadKeyExpr, context: Context): ts.ElementAccessExpression {
return ts.createElementAccess(
visitReadKeyExpr(ast: o.ReadKeyExpr, context: Context): TExpression {
return this.factory.createElementAccess(
ast.receiver.visitExpression(this, context), ast.index.visitExpression(this, context));
}
visitLiteralArrayExpr(ast: LiteralArrayExpr, context: Context): ts.ArrayLiteralExpression {
const expr =
ts.createArrayLiteral(ast.entries.map(expr => expr.visitExpression(this, context)));
this.setSourceMapRange(expr, ast.sourceSpan);
return expr;
visitLiteralArrayExpr(ast: o.LiteralArrayExpr, context: Context): TExpression {
return this.factory.createArrayLiteral(ast.entries.map(
expr => this.setSourceMapRange(expr.visitExpression(this, context), ast.sourceSpan)));
}
visitLiteralMapExpr(ast: LiteralMapExpr, context: Context): ts.ObjectLiteralExpression {
const entries = ast.entries.map(
entry => ts.createPropertyAssignment(
entry.quoted ? ts.createLiteral(entry.key) : ts.createIdentifier(entry.key),
entry.value.visitExpression(this, context)));
const expr = ts.createObjectLiteral(entries);
this.setSourceMapRange(expr, ast.sourceSpan);
return expr;
visitLiteralMapExpr(ast: o.LiteralMapExpr, context: Context): TExpression {
const properties: ObjectLiteralProperty<TExpression>[] = ast.entries.map(entry => {
return {
propertyName: entry.key,
quoted: entry.quoted,
value: entry.value.visitExpression(this, context)
};
});
return this.setSourceMapRange(this.factory.createObjectLiteral(properties), ast.sourceSpan);
}
visitCommaExpr(ast: CommaExpr, context: Context): never {
visitCommaExpr(ast: o.CommaExpr, context: Context): never {
throw new Error('Method not implemented.');
}
visitWrappedNodeExpr(ast: WrappedNodeExpr<any>, context: Context): any {
if (ts.isIdentifier(ast.node)) {
this.defaultImportRecorder.recordUsedIdentifier(ast.node);
}
visitWrappedNodeExpr(ast: o.WrappedNodeExpr<any>, _context: Context): any {
this.recordWrappedNodeExpr(ast.node);
return ast.node;
}
visitTypeofExpr(ast: TypeofExpr, context: Context): ts.TypeOfExpression {
return ts.createTypeOf(ast.expr.visitExpression(this, context));
visitTypeofExpr(ast: o.TypeofExpr, context: Context): TExpression {
return this.factory.createTypeOfExpression(ast.expr.visitExpression(this, context));
}
/**
* Translate the `LocalizedString` node into a `TaggedTemplateExpression` for ES2015 formatted
* output.
*/
private createLocalizedStringTaggedTemplate(ast: LocalizedString, context: Context):
ts.TaggedTemplateExpression {
let template: ts.TemplateLiteral;
const length = ast.messageParts.length;
const metaBlock = ast.serializeI18nHead();
if (length === 1) {
template = ts.createNoSubstitutionTemplateLiteral(metaBlock.cooked, metaBlock.raw);
this.setSourceMapRange(template, ast.getMessagePartSourceSpan(0));
} else {
// Create the head part
const head = ts.createTemplateHead(metaBlock.cooked, metaBlock.raw);
this.setSourceMapRange(head, ast.getMessagePartSourceSpan(0));
const spans: ts.TemplateSpan[] = [];
// Create the middle parts
for (let i = 1; i < length - 1; i++) {
const resolvedExpression = ast.expressions[i - 1].visitExpression(this, context);
this.setSourceMapRange(resolvedExpression, ast.getPlaceholderSourceSpan(i - 1));
const templatePart = ast.serializeI18nTemplatePart(i);
const templateMiddle = createTemplateMiddle(templatePart.cooked, templatePart.raw);
this.setSourceMapRange(templateMiddle, ast.getMessagePartSourceSpan(i));
const templateSpan = ts.createTemplateSpan(resolvedExpression, templateMiddle);
spans.push(templateSpan);
}
// Create the tail part
const resolvedExpression = ast.expressions[length - 2].visitExpression(this, context);
this.setSourceMapRange(resolvedExpression, ast.getPlaceholderSourceSpan(length - 2));
const templatePart = ast.serializeI18nTemplatePart(length - 1);
const templateTail = createTemplateTail(templatePart.cooked, templatePart.raw);
this.setSourceMapRange(templateTail, ast.getMessagePartSourceSpan(length - 1));
spans.push(ts.createTemplateSpan(resolvedExpression, templateTail));
// Put it all together
template = ts.createTemplateExpression(head, spans);
visitUnaryOperatorExpr(ast: o.UnaryOperatorExpr, context: Context): TExpression {
if (!UNARY_OPERATORS.has(ast.operator)) {
throw new Error(`Unknown unary operator: ${o.UnaryOperator[ast.operator]}`);
}
const expression = ts.createTaggedTemplate(ts.createIdentifier('$localize'), template);
this.setSourceMapRange(expression, ast.sourceSpan);
return expression;
return this.factory.createUnaryExpression(
UNARY_OPERATORS.get(ast.operator)!, ast.expr.visitExpression(this, context));
}
/**
* Translate the `LocalizedString` node into a `$localize` call using the imported
* `__makeTemplateObject` helper for ES5 formatted output.
*/
private createLocalizedStringFunctionCall(ast: LocalizedString, context: Context) {
// A `$localize` message consists `messageParts` and `expressions`, which get interleaved
// together. The interleaved pieces look like:
// `[messagePart0, expression0, messagePart1, expression1, messagePart2]`
//
// Note that there is always a message part at the start and end, and so therefore
// `messageParts.length === expressions.length + 1`.
//
// Each message part may be prefixed with "metadata", which is wrapped in colons (:) delimiters.
// The metadata is attached to the first and subsequent message parts by calls to
// `serializeI18nHead()` and `serializeI18nTemplatePart()` respectively.
// The first message part (i.e. `ast.messageParts[0]`) is used to initialize `messageParts`
// array.
const messageParts = [ast.serializeI18nHead()];
const expressions: any[] = [];
// The rest of the `ast.messageParts` and each of the expressions are `ast.expressions` pushed
// into the arrays. Note that `ast.messagePart[i]` corresponds to `expressions[i-1]`
for (let i = 1; i < ast.messageParts.length; i++) {
expressions.push(ast.expressions[i - 1].visitExpression(this, context));
messageParts.push(ast.serializeI18nTemplatePart(i));
}
// The resulting downlevelled tagged template string uses a call to the `__makeTemplateObject()`
// helper, so we must ensure it has been imported.
const {moduleImport, symbol} =
this.imports.generateNamedImport('tslib', '__makeTemplateObject');
const __makeTemplateObjectHelper = (moduleImport === null) ?
ts.createIdentifier(symbol) :
ts.createPropertyAccess(ts.createIdentifier(moduleImport), ts.createIdentifier(symbol));
// Generate the call in the form:
// `$localize(__makeTemplateObject(cookedMessageParts, rawMessageParts), ...expressions);`
const cookedLiterals = messageParts.map(
(messagePart, i) =>
this.createLiteral(messagePart.cooked, ast.getMessagePartSourceSpan(i)));
const rawLiterals = messageParts.map(
(messagePart, i) => this.createLiteral(messagePart.raw, ast.getMessagePartSourceSpan(i)));
return ts.createCall(
/* expression */ ts.createIdentifier('$localize'),
/* typeArguments */ undefined,
/* argumentsArray */[
ts.createCall(
/* expression */ __makeTemplateObjectHelper,
/* typeArguments */ undefined,
/* argumentsArray */
[
ts.createArrayLiteral(cookedLiterals),
ts.createArrayLiteral(rawLiterals),
]),
...expressions,
]);
private visitStatements(statements: o.Statement[], context: Context): TStatement[] {
return statements.map(stmt => stmt.visitStatement(this, context))
.filter(stmt => stmt !== undefined);
}
private setSourceMapRange(expr: ts.Node, sourceSpan: ParseSourceSpan|null) {
if (sourceSpan) {
const {start, end} = sourceSpan;
const {url, content} = start.file;
if (url) {
if (!this.externalSourceFiles.has(url)) {
this.externalSourceFiles.set(url, ts.createSourceMapSource(url, content, pos => pos));
}
const source = this.externalSourceFiles.get(url);
ts.setSourceMapRange(expr, {pos: start.offset, end: end.offset, source});
}
}
private setSourceMapRange<T extends TExpression|TStatement>(ast: T, span: o.ParseSourceSpan|null):
T {
return this.factory.setSourceMapRange(ast, createRange(span));
}
private createLiteral(text: string, span: ParseSourceSpan|null) {
const literal = ts.createStringLiteral(text);
this.setSourceMapRange(literal, span);
return literal;
}
}
// HACK: Use this in place of `ts.createTemplateMiddle()`.
// Revert once https://github.com/microsoft/TypeScript/issues/35374 is fixed
function createTemplateMiddle(cooked: string, raw: string): ts.TemplateMiddle {
const node: ts.TemplateLiteralLikeNode = ts.createTemplateHead(cooked, raw);
(node.kind as ts.SyntaxKind) = ts.SyntaxKind.TemplateMiddle;
return node as ts.TemplateMiddle;
}
// HACK: Use this in place of `ts.createTemplateTail()`.
// Revert once https://github.com/microsoft/TypeScript/issues/35374 is fixed
function createTemplateTail(cooked: string, raw: string): ts.TemplateTail {
const node: ts.TemplateLiteralLikeNode = ts.createTemplateHead(cooked, raw);
(node.kind as ts.SyntaxKind) = ts.SyntaxKind.TemplateTail;
return node as ts.TemplateTail;
}
/**
* Attach the given `leadingComments` to the `statement` node.
*
* @param statement The statement that will have comments attached.
* @param leadingComments The comments to attach to the statement.
* Convert a cooked-raw string object into one that can be used by the AST factories.
*/
export function attachComments<T extends ts.Statement>(
statement: T, leadingComments?: LeadingComment[]): T {
if (leadingComments === undefined) {
return statement;
}
for (const comment of leadingComments) {
const commentKind = comment.multiline ? ts.SyntaxKind.MultiLineCommentTrivia :
ts.SyntaxKind.SingleLineCommentTrivia;
if (comment.multiline) {
ts.addSyntheticLeadingComment(
statement, commentKind, comment.toString(), comment.trailingNewline);
} else {
for (const line of comment.text.split('\n')) {
ts.addSyntheticLeadingComment(statement, commentKind, line, comment.trailingNewline);
}
}
}
return statement;
function createTemplateElement(
{cooked, raw, range}: {cooked: string, raw: string, range: o.ParseSourceSpan|null}):
TemplateElement {
return {cooked, raw, range: createRange(range)};
}
/**
* Convert an OutputAST source-span into a range that can be used by the AST factories.
*/
function createRange(span: o.ParseSourceSpan|null): SourceMapRange|null {
if (span === null) {
return null;
}
const {start, end} = span;
const {url, content} = start.file;
if (!url) {
return null;
}
return {
url,
content,
start: {offset: start.offset, line: start.line, column: start.col},
end: {offset: end.offset, line: end.line, column: end.col},
};
}

View File

@ -0,0 +1,256 @@
/**
* @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 {AstFactory, BinaryOperator, LeadingComment, ObjectLiteralProperty, SourceMapRange, TemplateLiteral, UnaryOperator, VariableDeclarationType} from './api/ast_factory';
const UNARY_OPERATORS: Record<UnaryOperator, ts.PrefixUnaryOperator> = {
'+': ts.SyntaxKind.PlusToken,
'-': ts.SyntaxKind.MinusToken,
'!': ts.SyntaxKind.ExclamationToken,
};
const BINARY_OPERATORS: Record<BinaryOperator, ts.BinaryOperator> = {
'&&': ts.SyntaxKind.AmpersandAmpersandToken,
'>': ts.SyntaxKind.GreaterThanToken,
'>=': ts.SyntaxKind.GreaterThanEqualsToken,
'&': ts.SyntaxKind.AmpersandToken,
'/': ts.SyntaxKind.SlashToken,
'==': ts.SyntaxKind.EqualsEqualsToken,
'===': ts.SyntaxKind.EqualsEqualsEqualsToken,
'<': ts.SyntaxKind.LessThanToken,
'<=': ts.SyntaxKind.LessThanEqualsToken,
'-': ts.SyntaxKind.MinusToken,
'%': ts.SyntaxKind.PercentToken,
'*': ts.SyntaxKind.AsteriskToken,
'!=': ts.SyntaxKind.ExclamationEqualsToken,
'!==': ts.SyntaxKind.ExclamationEqualsEqualsToken,
'||': ts.SyntaxKind.BarBarToken,
'+': ts.SyntaxKind.PlusToken,
};
const VAR_TYPES: Record<VariableDeclarationType, ts.NodeFlags> = {
'const': ts.NodeFlags.Const,
'let': ts.NodeFlags.Let,
'var': ts.NodeFlags.None,
};
/**
* A TypeScript flavoured implementation of the AstFactory.
*/
export class TypeScriptAstFactory implements AstFactory<ts.Statement, ts.Expression> {
private externalSourceFiles = new Map<string, ts.SourceMapSource>();
attachComments = attachComments;
createArrayLiteral = ts.createArrayLiteral;
createAssignment(target: ts.Expression, value: ts.Expression): ts.Expression {
return ts.createBinary(target, ts.SyntaxKind.EqualsToken, value);
}
createBinaryExpression(
leftOperand: ts.Expression, operator: BinaryOperator,
rightOperand: ts.Expression): ts.Expression {
return ts.createBinary(leftOperand, BINARY_OPERATORS[operator], rightOperand);
}
createBlock(body: ts.Statement[]): ts.Statement {
return ts.createBlock(body);
}
createCallExpression(callee: ts.Expression, args: ts.Expression[], pure: boolean): ts.Expression {
const call = ts.createCall(callee, undefined, args);
if (pure) {
ts.addSyntheticLeadingComment(
call, ts.SyntaxKind.MultiLineCommentTrivia, '@__PURE__', /* trailing newline */ false);
}
return call;
}
createConditional = ts.createConditional;
createElementAccess = ts.createElementAccess;
createExpressionStatement = ts.createExpressionStatement;
createFunctionDeclaration(functionName: string|null, parameters: string[], body: ts.Statement):
ts.Statement {
if (!ts.isBlock(body)) {
throw new Error(`Invalid syntax, expected a block, but got ${ts.SyntaxKind[body.kind]}.`);
}
return ts.createFunctionDeclaration(
undefined, undefined, undefined, functionName ?? undefined, undefined,
parameters.map(param => ts.createParameter(undefined, undefined, undefined, param)),
undefined, body);
}
createFunctionExpression(functionName: string|null, parameters: string[], body: ts.Statement):
ts.Expression {
if (!ts.isBlock(body)) {
throw new Error(`Invalid syntax, expected a block, but got ${ts.SyntaxKind[body.kind]}.`);
}
return ts.createFunctionExpression(
undefined, undefined, functionName ?? undefined, undefined,
parameters.map(param => ts.createParameter(undefined, undefined, undefined, param)),
undefined, body);
}
createIdentifier = ts.createIdentifier;
createIfStatement(
condition: ts.Expression, thenStatement: ts.Statement,
elseStatement: ts.Statement|null): ts.Statement {
return ts.createIf(condition, thenStatement, elseStatement ?? undefined);
}
createLiteral(value: string|number|boolean|null|undefined): ts.Expression {
if (value === undefined) {
return ts.createIdentifier('undefined');
} else if (value === null) {
return ts.createNull();
} else {
return ts.createLiteral(value);
}
}
createNewExpression(expression: ts.Expression, args: ts.Expression[]): ts.Expression {
return ts.createNew(expression, undefined, args);
}
createObjectLiteral(properties: ObjectLiteralProperty<ts.Expression>[]): ts.Expression {
return ts.createObjectLiteral(properties.map(
prop => ts.createPropertyAssignment(
prop.quoted ? ts.createLiteral(prop.propertyName) :
ts.createIdentifier(prop.propertyName),
prop.value)));
}
createParenthesizedExpression = ts.createParen;
createPropertyAccess = ts.createPropertyAccess;
createReturnStatement(expression: ts.Expression|null): ts.Statement {
return ts.createReturn(expression ?? undefined);
}
createTaggedTemplate(tag: ts.Expression, template: TemplateLiteral<ts.Expression>):
ts.Expression {
let templateLiteral: ts.TemplateLiteral;
const length = template.elements.length;
const head = template.elements[0];
if (length === 1) {
templateLiteral = ts.createNoSubstitutionTemplateLiteral(head.cooked, head.raw);
} else {
const spans: ts.TemplateSpan[] = [];
// Create the middle parts
for (let i = 1; i < length - 1; i++) {
const {cooked, raw, range} = template.elements[i];
const middle = createTemplateMiddle(cooked, raw);
if (range !== null) {
this.setSourceMapRange(middle, range);
}
spans.push(ts.createTemplateSpan(template.expressions[i - 1], middle));
}
// Create the tail part
const resolvedExpression = template.expressions[length - 2];
const templatePart = template.elements[length - 1];
const templateTail = createTemplateTail(templatePart.cooked, templatePart.raw);
if (templatePart.range !== null) {
this.setSourceMapRange(templateTail, templatePart.range);
}
spans.push(ts.createTemplateSpan(resolvedExpression, templateTail));
// Put it all together
templateLiteral =
ts.createTemplateExpression(ts.createTemplateHead(head.cooked, head.raw), spans);
}
if (head.range !== null) {
this.setSourceMapRange(templateLiteral, head.range);
}
return ts.createTaggedTemplate(tag, templateLiteral);
}
createThrowStatement = ts.createThrow;
createTypeOfExpression = ts.createTypeOf;
createUnaryExpression(operator: UnaryOperator, operand: ts.Expression): ts.Expression {
return ts.createPrefix(UNARY_OPERATORS[operator], operand);
}
createVariableDeclaration(
variableName: string, initializer: ts.Expression|null,
type: VariableDeclarationType): ts.Statement {
return ts.createVariableStatement(
undefined,
ts.createVariableDeclarationList(
[ts.createVariableDeclaration(variableName, undefined, initializer ?? undefined)],
VAR_TYPES[type]),
);
}
setSourceMapRange<T extends ts.Node>(node: T, sourceMapRange: SourceMapRange|null): T {
if (sourceMapRange === null) {
return node;
}
const url = sourceMapRange.url;
if (!this.externalSourceFiles.has(url)) {
this.externalSourceFiles.set(
url, ts.createSourceMapSource(url, sourceMapRange.content, pos => pos));
}
const source = this.externalSourceFiles.get(url);
ts.setSourceMapRange(
node, {pos: sourceMapRange.start.offset, end: sourceMapRange.end.offset, source});
return node;
}
}
// HACK: Use this in place of `ts.createTemplateMiddle()`.
// Revert once https://github.com/microsoft/TypeScript/issues/35374 is fixed.
export function createTemplateMiddle(cooked: string, raw: string): ts.TemplateMiddle {
const node: ts.TemplateLiteralLikeNode = ts.createTemplateHead(cooked, raw);
(node.kind as ts.SyntaxKind) = ts.SyntaxKind.TemplateMiddle;
return node as ts.TemplateMiddle;
}
// HACK: Use this in place of `ts.createTemplateTail()`.
// Revert once https://github.com/microsoft/TypeScript/issues/35374 is fixed.
export function createTemplateTail(cooked: string, raw: string): ts.TemplateTail {
const node: ts.TemplateLiteralLikeNode = ts.createTemplateHead(cooked, raw);
(node.kind as ts.SyntaxKind) = ts.SyntaxKind.TemplateTail;
return node as ts.TemplateTail;
}
/**
* Attach the given `leadingComments` to the `statement` node.
*
* @param statement The statement that will have comments attached.
* @param leadingComments The comments to attach to the statement.
*/
export function attachComments<T extends ts.Statement>(
statement: T, leadingComments?: LeadingComment[]): T {
if (leadingComments === undefined) {
return statement;
}
for (const comment of leadingComments) {
const commentKind = comment.multiline ? ts.SyntaxKind.MultiLineCommentTrivia :
ts.SyntaxKind.SingleLineCommentTrivia;
if (comment.multiline) {
ts.addSyntheticLeadingComment(
statement, commentKind, comment.toString(), comment.trailingNewline);
} else {
for (const line of comment.toString().split('\n')) {
ts.addSyntheticLeadingComment(statement, commentKind, line, comment.trailingNewline);
}
}
}
return statement;
}

View File

@ -0,0 +1,33 @@
/**
* @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 o from '@angular/compiler';
import * as ts from 'typescript';
import {ImportGenerator} from './api/import_generator';
import {Context} from './context';
import {ExpressionTranslatorVisitor, TranslatorOptions} from './translator';
import {TypeScriptAstFactory} from './typescript_ast_factory';
export function translateExpression(
expression: o.Expression, imports: ImportGenerator<ts.Expression>,
options: TranslatorOptions<ts.Expression> = {}): ts.Expression {
return expression.visitExpression(
new ExpressionTranslatorVisitor<ts.Statement, ts.Expression>(
new TypeScriptAstFactory(), imports, options),
new Context(false));
}
export function translateStatement(
statement: o.Statement, imports: ImportGenerator<ts.Expression>,
options: TranslatorOptions<ts.Expression> = {}): ts.Statement {
return statement.visitStatement(
new ExpressionTranslatorVisitor<ts.Statement, ts.Expression>(
new TypeScriptAstFactory(), imports, options),
new Context(true));
}

View File

@ -0,0 +1,25 @@
load("//tools:defaults.bzl", "jasmine_node_test", "ts_library")
package(default_visibility = ["//visibility:public"])
ts_library(
name = "test_lib",
testonly = True,
srcs = glob([
"**/*.ts",
]),
deps = [
"//packages:types",
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/translator",
"@npm//typescript",
],
)
jasmine_node_test(
name = "test",
bootstrap = ["//tools/testing:node_no_angular_es5"],
deps = [
":test_lib",
],
)

View File

@ -0,0 +1,389 @@
/**
* @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 {leadingComment} from '@angular/compiler';
import * as ts from 'typescript';
import {TypeScriptAstFactory} from '../src/typescript_ast_factory';
describe('TypeScriptAstFactory', () => {
let factory: TypeScriptAstFactory;
beforeEach(() => factory = new TypeScriptAstFactory());
describe('attachComments()', () => {
it('should add the comments to the given statement', () => {
const {items: [stmt], generate} = setupStatements('x = 10;');
factory.attachComments(
stmt, [leadingComment('comment 1', true), leadingComment('comment 2', false)]);
expect(generate(stmt)).toEqual([
'/* comment 1 */',
'//comment 2',
'x = 10;',
].join('\n'));
});
});
describe('createArrayLiteral()', () => {
it('should create an array node containing the provided expressions', () => {
const {items: [expr1, expr2], generate} = setupExpressions(`42`, '"moo"');
const array = factory.createArrayLiteral([expr1, expr2]);
expect(generate(array)).toEqual('[42, "moo"]');
});
});
describe('createAssignment()', () => {
it('should create an assignment node using the target and value expressions', () => {
const {items: [target, value], generate} = setupExpressions(`x`, `42`);
const assignment = factory.createAssignment(target, value);
expect(generate(assignment)).toEqual('x = 42');
});
});
describe('createBinaryExpression()', () => {
it('should create a binary operation node using the left and right expressions', () => {
const {items: [left, right], generate} = setupExpressions(`17`, `42`);
const assignment = factory.createBinaryExpression(left, '+', right);
expect(generate(assignment)).toEqual('17 + 42');
});
});
describe('createBlock()', () => {
it('should create a block statement containing the given statements', () => {
const {items: stmts, generate} = setupStatements('x = 10; y = 20;');
const block = factory.createBlock(stmts);
expect(generate(block)).toEqual([
'{',
' x = 10;',
' y = 20;',
'}',
].join('\n'));
});
});
describe('createCallExpression()', () => {
it('should create a call on the `callee` with the given `args`', () => {
const {items: [callee, arg1, arg2], generate} = setupExpressions('foo', '42', '"moo"');
const call = factory.createCallExpression(callee, [arg1, arg2], false);
expect(generate(call)).toEqual('foo(42, "moo")');
});
it('should create a call marked with a PURE comment if `pure` is true', () => {
const {items: [callee, arg1, arg2], generate} = setupExpressions(`foo`, `42`, `"moo"`);
const call = factory.createCallExpression(callee, [arg1, arg2], true);
expect(generate(call)).toEqual('/*@__PURE__*/ foo(42, "moo")');
});
});
describe('createConditional()', () => {
it('should create a condition expression', () => {
const {items: [test, thenExpr, elseExpr], generate} =
setupExpressions(`!test`, `42`, `"moo"`);
const conditional = factory.createConditional(test, thenExpr, elseExpr);
expect(generate(conditional)).toEqual('!test ? 42 : "moo"');
});
});
describe('createElementAccess()', () => {
it('should create an expression accessing the element of an array/object', () => {
const {items: [expr, element], generate} = setupExpressions(`obj`, `"moo"`);
const access = factory.createElementAccess(expr, element);
expect(generate(access)).toEqual('obj["moo"]');
});
});
describe('createExpressionStatement()', () => {
it('should create a statement node from the given expression', () => {
const {items: [expr], generate} = setupExpressions(`x = 10`);
const stmt = factory.createExpressionStatement(expr);
expect(ts.isExpressionStatement(stmt)).toBe(true);
expect(generate(stmt)).toEqual('x = 10;');
});
});
describe('createFunctionDeclaration()', () => {
it('should create a function declaration node with the given name, parameters and body statements',
() => {
const {items: [body], generate} = setupStatements('{x = 10; y = 20;}');
const fn = factory.createFunctionDeclaration('foo', ['arg1', 'arg2'], body);
expect(generate(fn))
.toEqual(
'function foo(arg1, arg2) { x = 10; y = 20; }',
);
});
});
describe('createFunctionExpression()', () => {
it('should create a function expression node with the given name, parameters and body statements',
() => {
const {items: [body], generate} = setupStatements('{x = 10; y = 20;}');
const fn = factory.createFunctionExpression('foo', ['arg1', 'arg2'], body);
expect(ts.isExpressionStatement(fn)).toBe(false);
expect(generate(fn)).toEqual('function foo(arg1, arg2) { x = 10; y = 20; }');
});
it('should create an anonymous function expression node if the name is null', () => {
const {items: [body], generate} = setupStatements('{x = 10; y = 20;}');
const fn = factory.createFunctionExpression(null, ['arg1', 'arg2'], body);
expect(generate(fn)).toEqual('function (arg1, arg2) { x = 10; y = 20; }');
});
});
describe('createIdentifier()', () => {
it('should create an identifier with the given name', () => {
const id = factory.createIdentifier('someId') as ts.Identifier;
expect(ts.isIdentifier(id)).toBe(true);
expect(id.text).toEqual('someId');
});
});
describe('createIfStatement()', () => {
it('should create an if-else statement', () => {
const {items: [testStmt, thenStmt, elseStmt], generate} =
setupStatements('!test;x = 10;x = 42;');
const test = (testStmt as ts.ExpressionStatement).expression;
const ifStmt = factory.createIfStatement(test, thenStmt, elseStmt);
expect(generate(ifStmt)).toEqual([
'if (!test)',
' x = 10;',
'else',
' x = 42;',
].join('\n'));
});
it('should create an if statement if the else expression is null', () => {
const {items: [testStmt, thenStmt], generate} = setupStatements('!test;x = 10;');
const test = (testStmt as ts.ExpressionStatement).expression;
const ifStmt = factory.createIfStatement(test, thenStmt, null);
expect(generate(ifStmt)).toEqual([
'if (!test)',
' x = 10;',
].join('\n'));
});
});
describe('createLiteral()', () => {
it('should create a string literal', () => {
const {generate} = setupStatements();
const literal = factory.createLiteral('moo');
expect(ts.isStringLiteral(literal)).toBe(true);
expect(generate(literal)).toEqual('"moo"');
});
it('should create a number literal', () => {
const {generate} = setupStatements();
const literal = factory.createLiteral(42);
expect(ts.isNumericLiteral(literal)).toBe(true);
expect(generate(literal)).toEqual('42');
});
it('should create a number literal for `NaN`', () => {
const {generate} = setupStatements();
const literal = factory.createLiteral(NaN);
expect(ts.isNumericLiteral(literal)).toBe(true);
expect(generate(literal)).toEqual('NaN');
});
it('should create a boolean literal', () => {
const {generate} = setupStatements();
const literal = factory.createLiteral(true);
expect(ts.isToken(literal)).toBe(true);
expect(generate(literal)).toEqual('true');
});
it('should create an `undefined` literal', () => {
const {generate} = setupStatements();
const literal = factory.createLiteral(undefined);
expect(ts.isIdentifier(literal)).toBe(true);
expect(generate(literal)).toEqual('undefined');
});
it('should create a `null` literal', () => {
const {generate} = setupStatements();
const literal = factory.createLiteral(null);
expect(ts.isToken(literal)).toBe(true);
expect(generate(literal)).toEqual('null');
});
});
describe('createNewExpression()', () => {
it('should create a `new` operation on the constructor `expression` with the given `args`',
() => {
const {items: [expr, arg1, arg2], generate} = setupExpressions('Foo', '42', '"moo"');
const call = factory.createNewExpression(expr, [arg1, arg2]);
expect(generate(call)).toEqual('new Foo(42, "moo")');
});
});
describe('createObjectLiteral()', () => {
it('should create an object literal node, with the given properties', () => {
const {items: [prop1, prop2], generate} = setupExpressions('42', '"moo"');
const obj = factory.createObjectLiteral([
{propertyName: 'prop1', value: prop1, quoted: false},
{propertyName: 'prop2', value: prop2, quoted: true},
]);
expect(generate(obj)).toEqual('{ prop1: 42, "prop2": "moo" }');
});
});
describe('createParenthesizedExpression()', () => {
it('should add parentheses around the given expression', () => {
const {items: [expr], generate} = setupExpressions(`a + b`);
const paren = factory.createParenthesizedExpression(expr);
expect(generate(paren)).toEqual('(a + b)');
});
});
describe('createPropertyAccess()', () => {
it('should create a property access expression node', () => {
const {items: [expr], generate} = setupExpressions(`obj`);
const access = factory.createPropertyAccess(expr, 'moo');
expect(generate(access)).toEqual('obj.moo');
});
});
describe('createReturnStatement()', () => {
it('should create a return statement returning the given expression', () => {
const {items: [expr], generate} = setupExpressions(`42`);
const returnStmt = factory.createReturnStatement(expr);
expect(generate(returnStmt)).toEqual('return 42;');
});
it('should create a void return statement if the expression is null', () => {
const {generate} = setupStatements();
const returnStmt = factory.createReturnStatement(null);
expect(generate(returnStmt)).toEqual('return;');
});
});
describe('createTaggedTemplate()', () => {
it('should create a tagged template node from the tag, elements and expressions', () => {
const elements = [
{raw: 'raw\\n1', cooked: 'raw\n1', range: null},
{raw: 'raw\\n2', cooked: 'raw\n2', range: null},
{raw: 'raw\\n3', cooked: 'raw\n3', range: null},
];
const {items: [tag, ...expressions], generate} = setupExpressions('tagFn', '42', '"moo"');
const template = factory.createTaggedTemplate(tag, {elements, expressions});
expect(generate(template)).toEqual('tagFn `raw\\n1${42}raw\\n2${"moo"}raw\\n3`');
});
});
describe('createThrowStatement()', () => {
it('should create a throw statement, throwing the given expression', () => {
const {items: [expr], generate} = setupExpressions(`new Error("bad")`);
const throwStmt = factory.createThrowStatement(expr);
expect(generate(throwStmt)).toEqual('throw new Error("bad");');
});
});
describe('createTypeOfExpression()', () => {
it('should create a typeof expression node', () => {
const {items: [expr], generate} = setupExpressions(`42`);
const typeofExpr = factory.createTypeOfExpression(expr);
expect(generate(typeofExpr)).toEqual('typeof 42');
});
});
describe('createUnaryExpression()', () => {
it('should create a unary expression with the operator and operand', () => {
const {items: [expr], generate} = setupExpressions(`value`);
const unaryExpr = factory.createUnaryExpression('!', expr);
expect(generate(unaryExpr)).toEqual('!value');
});
});
describe('createVariableDeclaration()', () => {
it('should create a variable declaration statement node for the given variable name and initializer',
() => {
const {items: [initializer], generate} = setupExpressions(`42`);
const varDecl = factory.createVariableDeclaration('foo', initializer, 'let');
expect(generate(varDecl)).toEqual('let foo = 42;');
});
it('should create a constant declaration statement node for the given variable name and initializer',
() => {
const {items: [initializer], generate} = setupExpressions(`42`);
const varDecl = factory.createVariableDeclaration('foo', initializer, 'const');
expect(generate(varDecl)).toEqual('const foo = 42;');
});
it('should create a downleveled declaration statement node for the given variable name and initializer',
() => {
const {items: [initializer], generate} = setupExpressions(`42`);
const varDecl = factory.createVariableDeclaration('foo', initializer, 'var');
expect(generate(varDecl)).toEqual('var foo = 42;');
});
it('should create an uninitialized variable declaration statement node for the given variable name and a null initializer',
() => {
const {generate} = setupStatements();
const varDecl = factory.createVariableDeclaration('foo', null, 'let');
expect(generate(varDecl)).toEqual('let foo;');
});
});
describe('setSourceMapRange()', () => {
it('should attach the `sourceMapRange` to the given `node`', () => {
const {items: [expr]} = setupExpressions(`42`);
factory.setSourceMapRange(expr, {
start: {line: 0, column: 1, offset: 1},
end: {line: 2, column: 3, offset: 15},
content: '-****\n*****\n****',
url: 'original.ts'
});
const range = ts.getSourceMapRange(expr);
expect(range.pos).toEqual(1);
expect(range.end).toEqual(15);
expect(range.source?.getLineAndCharacterOfPosition(range.pos))
.toEqual({line: 0, character: 1});
expect(range.source?.getLineAndCharacterOfPosition(range.end))
.toEqual({line: 2, character: 3});
});
});
});
/**
* Setup some statements to use in a test, along with a generate function to print the created nodes
* out.
*
* The TypeScript printer requires access to the original source of non-synthesized nodes.
* It uses the source content to output things like text between parts of nodes, which it doesn't
* store in the AST node itself.
*
* So this helper (and its sister `setupExpressions()`) capture the original source file used to
* provide the original statements/expressions that are used in the tests so that the printing will
* work via the returned `generate()` function.
*/
function setupStatements(stmts: string = ''): SetupResult<ts.Statement> {
const printer = ts.createPrinter();
const sf = ts.createSourceFile('test.ts', stmts, ts.ScriptTarget.ES2015, true);
return {
items: Array.from(sf.statements),
generate: (node: ts.Node) => printer.printNode(ts.EmitHint.Unspecified, node, sf),
};
}
/**
* Setup some statements to use in a test, along with a generate function to print the created nodes
* out.
*
* See `setupStatements()` for more information about this helper function.
*/
function setupExpressions(...exprs: string[]): SetupResult<ts.Expression> {
const {items: [arrayStmt], generate} = setupStatements(`[${exprs.join(',')}];`);
const expressions = Array.from(
((arrayStmt as ts.ExpressionStatement).expression as ts.ArrayLiteralExpression).elements);
return {items: expressions, generate};
}
interface SetupResult<TNode extends ts.Node> {
items: TNode[];
generate(node: ts.Node): string;
}

View File

@ -9,7 +9,7 @@
import {ExpressionType, ExternalExpr, Type, WrappedNodeExpr} from '@angular/compiler';
import * as ts from 'typescript';
import {ImportFlags, NOOP_DEFAULT_IMPORT_RECORDER, Reference, ReferenceEmitter} from '../../imports';
import {ImportFlags, Reference, ReferenceEmitter} from '../../imports';
import {ClassDeclaration, ReflectionHost} from '../../reflection';
import {ImportManager, translateExpression, translateType} from '../../translator';
import {TypeCheckableDirectiveMeta, TypeCheckingConfig, TypeCtorMetadata} from '../api';
@ -213,8 +213,7 @@ export class Environment {
const ngExpr = this.refEmitter.emit(ref, this.contextFile, ImportFlags.NoAliasing);
// Use `translateExpression` to convert the `Expression` into a `ts.Expression`.
return translateExpression(
ngExpr, this.importManager, NOOP_DEFAULT_IMPORT_RECORDER, ts.ScriptTarget.ES2015);
return translateExpression(ngExpr, this.importManager);
}
/**