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

View File

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

View File

@ -10,7 +10,7 @@ import MagicString from 'magic-string';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {absoluteFromSourceFile, AbsoluteFsPath, dirname, relative, toRelativeImport} from '../../../src/ngtsc/file_system'; 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 {Import, ImportManager, translateStatement} from '../../../src/ngtsc/translator';
import {isDtsPath} from '../../../src/ngtsc/util/src/typescript'; import {isDtsPath} from '../../../src/ngtsc/util/src/typescript';
import {ModuleWithProvidersInfo} from '../analysis/module_with_providers_analyzer'; 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). * @return The JavaScript code corresponding to `stmt` (in the appropriate format).
*/ */
printStatement(stmt: Statement, sourceFile: ts.SourceFile, importManager: ImportManager): string { printStatement(stmt: Statement, sourceFile: ts.SourceFile, importManager: ImportManager): string {
const node = translateStatement( const node = translateStatement(stmt, importManager);
stmt, importManager, NOOP_DEFAULT_IMPORT_RECORDER, ts.ScriptTarget.ES2015);
const code = this.printer.printNode(ts.EmitHint.Unspecified, node, sourceFile); const code = this.printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);
return code; return code;
@ -264,8 +263,6 @@ export class EsmRenderingFormatter implements RenderingFormatter {
return 0; return 0;
} }
/** /**
* Check whether the given type is the core Angular `ModuleWithProviders` interface. * Check whether the given type is the core Angular `ModuleWithProviders` interface.
* @param typeName The type to check. * @param typeName The type to check.
@ -292,7 +289,8 @@ function findStatement(node: ts.Node): ts.Statement|undefined {
function generateImportString( function generateImportString(
importManager: ImportManager, importPath: string|null, importName: string) { importManager: ImportManager, importPath: string|null, importName: string) {
const importAs = importPath ? importManager.generateNamedImport(importPath, importName) : null; 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 { 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 ? const namedImport = entryPointBasePath !== basePath ?
importManager.generateNamedImport(relativePath, e.identifier) : importManager.generateNamedImport(relativePath, e.identifier) :
{symbol: e.identifier, moduleImport: null}; {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};`; const exportStr = `\nexports.${e.identifier} = ${importNamespace}${namedImport.symbol};`;
output.appendRight(insertionPoint, exportStr); output.appendRight(insertionPoint, exportStr);
}); });
@ -111,7 +111,7 @@ export class UmdRenderingFormatter extends Esm5RenderingFormatter {
lastStatement ? lastStatement.getEnd() : factoryFunction.body.getEnd() - 1; lastStatement ? lastStatement.getEnd() : factoryFunction.body.getEnd() - 1;
for (const e of exports) { for (const e of exports) {
const namedImport = importManager.generateNamedImport(e.fromModule, e.symbolName); 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};`; const exportStr = `\nexports.${e.asAlias} = ${importNamespace}${namedImport.symbol};`;
output.appendRight(insertionPoint, exportStr); output.appendRight(insertionPoint, exportStr);
} }

View File

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

View File

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

View File

@ -11,7 +11,7 @@ import * as ts from 'typescript';
import {DefaultImportRecorder, ImportRewriter} from '../../imports'; import {DefaultImportRecorder, ImportRewriter} from '../../imports';
import {Decorator, ReflectionHost} from '../../reflection'; 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 {visit, VisitListEntryResult, Visitor} from '../../util/src/visitor';
import {CompileResult} from './api'; import {CompileResult} from './api';
@ -35,11 +35,12 @@ export function ivyTransformFactory(
compilation: TraitCompiler, reflector: ReflectionHost, importRewriter: ImportRewriter, compilation: TraitCompiler, reflector: ReflectionHost, importRewriter: ImportRewriter,
defaultImportRecorder: DefaultImportRecorder, isCore: boolean, defaultImportRecorder: DefaultImportRecorder, isCore: boolean,
isClosureCompilerEnabled: boolean): ts.TransformerFactory<ts.SourceFile> { isClosureCompilerEnabled: boolean): ts.TransformerFactory<ts.SourceFile> {
const recordWrappedNodeExpr = createRecorderFn(defaultImportRecorder);
return (context: ts.TransformationContext): ts.Transformer<ts.SourceFile> => { return (context: ts.TransformationContext): ts.Transformer<ts.SourceFile> => {
return (file: ts.SourceFile): ts.SourceFile => { return (file: ts.SourceFile): ts.SourceFile => {
return transformIvySourceFile( return transformIvySourceFile(
compilation, context, reflector, importRewriter, file, isCore, isClosureCompilerEnabled, compilation, context, reflector, importRewriter, file, isCore, isClosureCompilerEnabled,
defaultImportRecorder); recordWrappedNodeExpr);
}; };
}; };
} }
@ -77,7 +78,7 @@ class IvyTransformationVisitor extends Visitor {
private compilation: TraitCompiler, private compilation: TraitCompiler,
private classCompilationMap: Map<ts.ClassDeclaration, CompileResult[]>, private classCompilationMap: Map<ts.ClassDeclaration, CompileResult[]>,
private reflector: ReflectionHost, private importManager: ImportManager, private reflector: ReflectionHost, private importManager: ImportManager,
private defaultImportRecorder: DefaultImportRecorder, private recordWrappedNodeExpr: RecordWrappedNodeExprFn<ts.Expression>,
private isClosureCompilerEnabled: boolean, private isCore: boolean) { private isClosureCompilerEnabled: boolean, private isCore: boolean) {
super(); super();
} }
@ -97,8 +98,8 @@ class IvyTransformationVisitor extends Visitor {
for (const field of this.classCompilationMap.get(node)!) { for (const field of this.classCompilationMap.get(node)!) {
// Translate the initializer for the field into TS nodes. // Translate the initializer for the field into TS nodes.
const exprNode = translateExpression( const exprNode = translateExpression(
field.initializer, this.importManager, this.defaultImportRecorder, field.initializer, this.importManager,
ts.ScriptTarget.ES2015); {recordWrappedNodeExpr: this.recordWrappedNodeExpr});
// Create a static property declaration for the new field. // Create a static property declaration for the new field.
const property = ts.createProperty( const property = ts.createProperty(
@ -118,7 +119,7 @@ class IvyTransformationVisitor extends Visitor {
field.statements field.statements
.map( .map(
stmt => translateStatement( stmt => translateStatement(
stmt, this.importManager, this.defaultImportRecorder, ts.ScriptTarget.ES2015)) stmt, this.importManager, {recordWrappedNodeExpr: this.recordWrappedNodeExpr}))
.forEach(stmt => statements.push(stmt)); .forEach(stmt => statements.push(stmt));
members.push(property); members.push(property);
@ -248,7 +249,7 @@ function transformIvySourceFile(
compilation: TraitCompiler, context: ts.TransformationContext, reflector: ReflectionHost, compilation: TraitCompiler, context: ts.TransformationContext, reflector: ReflectionHost,
importRewriter: ImportRewriter, file: ts.SourceFile, isCore: boolean, importRewriter: ImportRewriter, file: ts.SourceFile, isCore: boolean,
isClosureCompilerEnabled: boolean, isClosureCompilerEnabled: boolean,
defaultImportRecorder: DefaultImportRecorder): ts.SourceFile { recordWrappedNodeExpr: RecordWrappedNodeExprFn<ts.Expression>): ts.SourceFile {
const constantPool = new ConstantPool(isClosureCompilerEnabled); const constantPool = new ConstantPool(isClosureCompilerEnabled);
const importManager = new ImportManager(importRewriter); const importManager = new ImportManager(importRewriter);
@ -270,14 +271,18 @@ function transformIvySourceFile(
// results obtained at Step 1. // results obtained at Step 1.
const transformationVisitor = new IvyTransformationVisitor( const transformationVisitor = new IvyTransformationVisitor(
compilation, compilationVisitor.classCompilationMap, reflector, importManager, compilation, compilationVisitor.classCompilationMap, reflector, importManager,
defaultImportRecorder, isClosureCompilerEnabled, isCore); recordWrappedNodeExpr, isClosureCompilerEnabled, isCore);
let sf = visit(file, transformationVisitor, context); let sf = visit(file, transformationVisitor, context);
// Generate the constant statements first, as they may involve adding additional imports // Generate the constant statements first, as they may involve adding additional imports
// to the ImportManager. // to the ImportManager.
const constants = constantPool.statements.map( const downlevelTranslatedCode = getLocalizeCompileTarget(context) < ts.ScriptTarget.ES2015;
stmt => translateStatement( const constants =
stmt, importManager, defaultImportRecorder, getLocalizeCompileTarget(context))); 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 // Preserve @fileoverview comments required by Closure, since the location might change as a
// result of adding extra imports and constant pool statements. // result of adding extra imports and constant pool statements.
@ -360,3 +365,12 @@ function maybeFilterDecorator(
function isFromAngularCore(decorator: Decorator): boolean { function isFromAngularCore(decorator: Decorator): boolean {
return decorator.import !== null && decorator.import.from === '@angular/core'; 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 * found in the LICENSE file at https://angular.io/license
*/ */
export {Import, ImportManager, NamedImport} from './src/import_manager'; export {AstFactory, BinaryOperator, LeadingComment, ObjectLiteralProperty, SourceMapLocation, SourceMapRange, TemplateElement, TemplateLiteral, UnaryOperator} from './src/api/ast_factory';
export {attachComments, translateExpression, translateStatement} from './src/translator'; 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 {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 * 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 * 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';
/** export class ImportManager implements ImportGenerator<ts.Identifier> {
* Information about an import that has been added to a module. private specifierToIdentifier = new Map<string, ts.Identifier>();
*/
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>();
private nextIndex = 0; private nextIndex = 0;
constructor(protected rewriter: ImportRewriter = new NoopImportRewriter(), private prefix = 'i') { 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. // First, rewrite the symbol name.
const symbol = this.rewriter.rewriteSymbol(originalSymbol, moduleName); const symbol = this.rewriter.rewriteSymbol(originalSymbol, moduleName);
@ -46,12 +35,8 @@ export class ImportManager {
return {moduleImport: null, symbol}; return {moduleImport: null, symbol};
} }
// If not, this symbol will be imported. Allocate a prefix for the imported module if needed. // If not, this symbol will be imported using a generated namespace import.
const moduleImport = this.generateNamespaceImport(moduleName);
if (!this.specifierToIdentifier.has(moduleName)) {
this.specifierToIdentifier.set(moduleName, `${this.prefix}${this.nextIndex++}`);
}
const moduleImport = this.specifierToIdentifier.get(moduleName)!;
return {moduleImport, symbol}; return {moduleImport, symbol};
} }
@ -60,7 +45,7 @@ export class ImportManager {
const imports: {specifier: string, qualifier: string}[] = []; const imports: {specifier: string, qualifier: string}[] = [];
this.specifierToIdentifier.forEach((qualifier, specifier) => { this.specifierToIdentifier.forEach((qualifier, specifier) => {
specifier = this.rewriter.rewriteSpecifier(specifier, contextPath); specifier = this.rewriter.rewriteSpecifier(specifier, contextPath);
imports.push({specifier, qualifier}); imports.push({specifier, qualifier: qualifier.text});
}); });
return imports; return imports;
} }

View File

@ -5,214 +5,249 @@
* Use of this source code is governed by an MIT-style license that can be * 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 * 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 {AstFactory, BinaryOperator, ObjectLiteralProperty, SourceMapRange, TemplateElement, TemplateLiteral, UnaryOperator} from './api/ast_factory';
import {LocalizedString, UnaryOperator, UnaryOperatorExpr} from '@angular/compiler/src/output/output_ast'; import {ImportGenerator} from './api/import_generator';
import * as ts from 'typescript';
import {DefaultImportRecorder} from '../../imports';
import {Context} from './context'; import {Context} from './context';
import {ImportManager} from './import_manager';
const UNARY_OPERATORS = new Map<UnaryOperator, ts.PrefixUnaryOperator>([ const UNARY_OPERATORS = new Map<o.UnaryOperator, UnaryOperator>([
[UnaryOperator.Minus, ts.SyntaxKind.MinusToken], [o.UnaryOperator.Minus, '-'],
[UnaryOperator.Plus, ts.SyntaxKind.PlusToken], [o.UnaryOperator.Plus, '+'],
]); ]);
const BINARY_OPERATORS = new Map<BinaryOperator, ts.BinaryOperator>([ const BINARY_OPERATORS = new Map<o.BinaryOperator, BinaryOperator>([
[BinaryOperator.And, ts.SyntaxKind.AmpersandAmpersandToken], [o.BinaryOperator.And, '&&'],
[BinaryOperator.Bigger, ts.SyntaxKind.GreaterThanToken], [o.BinaryOperator.Bigger, '>'],
[BinaryOperator.BiggerEquals, ts.SyntaxKind.GreaterThanEqualsToken], [o.BinaryOperator.BiggerEquals, '>='],
[BinaryOperator.BitwiseAnd, ts.SyntaxKind.AmpersandToken], [o.BinaryOperator.BitwiseAnd, '&'],
[BinaryOperator.Divide, ts.SyntaxKind.SlashToken], [o.BinaryOperator.Divide, '/'],
[BinaryOperator.Equals, ts.SyntaxKind.EqualsEqualsToken], [o.BinaryOperator.Equals, '=='],
[BinaryOperator.Identical, ts.SyntaxKind.EqualsEqualsEqualsToken], [o.BinaryOperator.Identical, '==='],
[BinaryOperator.Lower, ts.SyntaxKind.LessThanToken], [o.BinaryOperator.Lower, '<'],
[BinaryOperator.LowerEquals, ts.SyntaxKind.LessThanEqualsToken], [o.BinaryOperator.LowerEquals, '<='],
[BinaryOperator.Minus, ts.SyntaxKind.MinusToken], [o.BinaryOperator.Minus, '-'],
[BinaryOperator.Modulo, ts.SyntaxKind.PercentToken], [o.BinaryOperator.Modulo, '%'],
[BinaryOperator.Multiply, ts.SyntaxKind.AsteriskToken], [o.BinaryOperator.Multiply, '*'],
[BinaryOperator.NotEquals, ts.SyntaxKind.ExclamationEqualsToken], [o.BinaryOperator.NotEquals, '!='],
[BinaryOperator.NotIdentical, ts.SyntaxKind.ExclamationEqualsEqualsToken], [o.BinaryOperator.NotIdentical, '!=='],
[BinaryOperator.Or, ts.SyntaxKind.BarBarToken], [o.BinaryOperator.Or, '||'],
[BinaryOperator.Plus, ts.SyntaxKind.PlusToken], [o.BinaryOperator.Plus, '+'],
]); ]);
export function translateExpression( export type RecordWrappedNodeExprFn<TExpression> = (expr: TExpression) => void;
expression: Expression, imports: ImportManager, defaultImportRecorder: DefaultImportRecorder,
scriptTarget: Exclude<ts.ScriptTarget, ts.ScriptTarget.JSON>): ts.Expression { export interface TranslatorOptions<TExpression> {
return expression.visitExpression( downlevelLocalizedStrings?: boolean;
new ExpressionTranslatorVisitor(imports, defaultImportRecorder, scriptTarget), downlevelVariableDeclarations?: boolean;
new Context(false)); recordWrappedNodeExpr?: RecordWrappedNodeExprFn<TExpression>;
} }
export function translateStatement( export class ExpressionTranslatorVisitor<TStatement, TExpression> implements o.ExpressionVisitor,
statement: Statement, imports: ImportManager, defaultImportRecorder: DefaultImportRecorder, o.StatementVisitor {
scriptTarget: Exclude<ts.ScriptTarget, ts.ScriptTarget.JSON>): ts.Statement { private downlevelLocalizedStrings: boolean;
return statement.visitStatement( private downlevelVariableDeclarations: boolean;
new ExpressionTranslatorVisitor(imports, defaultImportRecorder, scriptTarget), private recordWrappedNodeExpr: RecordWrappedNodeExprFn<TExpression>;
new Context(true));
}
class ExpressionTranslatorVisitor implements ExpressionVisitor, StatementVisitor {
private externalSourceFiles = new Map<string, ts.SourceMapSource>();
constructor( constructor(
private imports: ImportManager, private defaultImportRecorder: DefaultImportRecorder, private factory: AstFactory<TStatement, TExpression>,
private scriptTarget: Exclude<ts.ScriptTarget, ts.ScriptTarget.JSON>) {} private imports: ImportGenerator<TExpression>, options: TranslatorOptions<TExpression>) {
this.downlevelLocalizedStrings = options.downlevelLocalizedStrings === true;
visitDeclareVarStmt(stmt: DeclareVarStmt, context: Context): ts.VariableStatement { this.downlevelVariableDeclarations = options.downlevelVariableDeclarations === true;
const varType = this.scriptTarget < ts.ScriptTarget.ES2015 ? this.recordWrappedNodeExpr = options.recordWrappedNodeExpr || (() => {});
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);
} }
visitDeclareFunctionStmt(stmt: DeclareFunctionStmt, context: Context): ts.FunctionDeclaration { visitDeclareVarStmt(stmt: o.DeclareVarStmt, context: Context): TStatement {
const fnDeclaration = ts.createFunctionDeclaration( const varType = this.downlevelVariableDeclarations ?
/* decorators */ undefined, 'var' :
/* modifiers */ undefined, stmt.hasModifier(o.StmtModifier.Final) ? 'const' : 'let';
/* asterisk */ undefined, return this.factory.attachComments(
/* name */ stmt.name, this.factory.createVariableDeclaration(
/* typeParameters */ undefined, stmt.name, stmt.value?.visitExpression(this, context.withExpressionMode), varType),
/* 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)),
stmt.leadingComments); stmt.leadingComments);
} }
visitReturnStmt(stmt: ReturnStatement, context: Context): ts.ReturnStatement { visitDeclareFunctionStmt(stmt: o.DeclareFunctionStmt, context: Context): TStatement {
return attachComments( return this.factory.attachComments(
ts.createReturn(stmt.value.visitExpression(this, context.withExpressionMode)), this.factory.createFunctionDeclaration(
stmt.name, stmt.params.map(param => param.name),
this.factory.createBlock(
this.visitStatements(stmt.statements, context.withStatementMode))),
stmt.leadingComments); stmt.leadingComments);
} }
visitDeclareClassStmt(stmt: ClassStmt, context: Context) { visitExpressionStmt(stmt: o.ExpressionStatement, context: Context): TStatement {
if (this.scriptTarget < ts.ScriptTarget.ES2015) { return this.factory.attachComments(
throw new Error( this.factory.createExpressionStatement(
`Unsupported mode: Visiting a "declare class" statement (class ${stmt.name}) while ` + stmt.expr.visitExpression(this, context.withStatementMode)),
`targeting ${ts.ScriptTarget[this.scriptTarget]}.`); 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.'); throw new Error('Method not implemented.');
} }
visitIfStmt(stmt: IfStmt, context: Context): ts.IfStatement { visitIfStmt(stmt: o.IfStmt, context: Context): TStatement {
const thenBlock = ts.createBlock( return this.factory.attachComments(
stmt.trueCase.map(child => child.visitStatement(this, context.withStatementMode))); this.factory.createIfStatement(
const elseBlock = stmt.falseCase.length > 0 ? stmt.condition.visitExpression(this, context),
ts.createBlock( this.factory.createBlock(
stmt.falseCase.map(child => child.visitStatement(this, context.withStatementMode))) : this.visitStatements(stmt.trueCase, context.withStatementMode)),
undefined; stmt.falseCase.length > 0 ? this.factory.createBlock(this.visitStatements(
const ifStatement = stmt.falseCase, context.withStatementMode)) :
ts.createIf(stmt.condition.visitExpression(this, context), thenBlock, elseBlock); null),
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)),
stmt.leadingComments); stmt.leadingComments);
} }
visitReadVarExpr(ast: ReadVarExpr, context: Context): ts.Identifier { visitTryCatchStmt(_stmt: o.TryCatchStmt, _context: Context): never {
const identifier = ts.createIdentifier(ast.name!); 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); this.setSourceMapRange(identifier, ast.sourceSpan);
return identifier; return identifier;
} }
visitWriteVarExpr(expr: WriteVarExpr, context: Context): ts.Expression { visitWriteVarExpr(expr: o.WriteVarExpr, context: Context): TExpression {
const result: ts.Expression = ts.createBinary( const assignment = this.factory.createAssignment(
ts.createIdentifier(expr.name), ts.SyntaxKind.EqualsToken, this.setSourceMapRange(this.factory.createIdentifier(expr.name), expr.sourceSpan),
expr.value.visitExpression(this, context)); expr.value.visitExpression(this, context),
return context.isStatement ? result : ts.createParen(result); );
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 exprContext = context.withExpressionMode;
const lhs = ts.createElementAccess( const target = this.factory.createElementAccess(
expr.receiver.visitExpression(this, exprContext), expr.receiver.visitExpression(this, exprContext),
expr.index.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); const assignment =
return context.isStatement ? result : ts.createParen(result); this.factory.createAssignment(target, expr.value.visitExpression(this, exprContext));
return context.isStatement ? assignment :
this.factory.createParenthesizedExpression(assignment);
} }
visitWritePropExpr(expr: WritePropExpr, context: Context): ts.BinaryExpression { visitWritePropExpr(expr: o.WritePropExpr, context: Context): TExpression {
return ts.createBinary( const target =
ts.createPropertyAccess(expr.receiver.visitExpression(this, context), expr.name), this.factory.createPropertyAccess(expr.receiver.visitExpression(this, context), expr.name);
ts.SyntaxKind.EqualsToken, expr.value.visitExpression(this, context)); 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 target = ast.receiver.visitExpression(this, context);
const call = ts.createCall( return this.setSourceMapRange(
ast.name !== null ? ts.createPropertyAccess(target, ast.name) : target, undefined, this.factory.createCallExpression(
ast.args.map(arg => arg.visitExpression(this, context))); ast.name !== null ? this.factory.createPropertyAccess(target, ast.name) : target,
this.setSourceMapRange(call, ast.sourceSpan); ast.args.map(arg => arg.visitExpression(this, context)),
return call; /* pure */ false),
ast.sourceSpan);
} }
visitInvokeFunctionExpr(ast: InvokeFunctionExpr, context: Context): ts.CallExpression { visitInvokeFunctionExpr(ast: o.InvokeFunctionExpr, context: Context): TExpression {
const expr = ts.createCall( return this.setSourceMapRange(
ast.fn.visitExpression(this, context), undefined, 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))); 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( * Translate the tagged template literal into a call that is compatible with ES5, using the
ast.classExpr.visitExpression(this, context), undefined, * imported `__makeTemplateObject` helper for ES5 formatted output.
ast.args.map(arg => arg.visitExpression(this, context))); */
} 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 { // Collect up the cooked and raw strings into two separate arrays.
let expr: ts.Expression; const cooked: TExpression[] = [];
if (ast.value === undefined) { const raw: TExpression[] = [];
expr = ts.createIdentifier('undefined'); for (const element of elements) {
} else if (ast.value === null) { cooked.push(this.factory.setSourceMapRange(
expr = ts.createNull(); this.factory.createLiteral(element.cooked), element.range));
} else { raw.push(
expr = ts.createLiteral(ast.value); 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 { visitExternalExpr(ast: o.ExternalExpr, _context: Context): TExpression {
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 {
if (ast.value.name === null) { if (ast.value.name === null) {
throw new Error(`Import unknown module or symbol ${ast.value}`); 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); this.imports.generateNamedImport(ast.value.moduleName, ast.value.name);
if (moduleImport === null) { if (moduleImport === null) {
// The symbol was ambient after all. // The symbol was ambient after all.
return ts.createIdentifier(symbol); return this.factory.createIdentifier(symbol);
} else { } else {
return ts.createPropertyAccess( return this.factory.createPropertyAccess(moduleImport, symbol);
ts.createIdentifier(moduleImport), ts.createIdentifier(symbol));
} }
} else { } else {
// The symbol is ambient, so just reference it. // 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 { visitConditionalExpr(ast: o.ConditionalExpr, context: Context): TExpression {
let cond: ts.Expression = ast.condition.visitExpression(this, context); let cond: TExpression = ast.condition.visitExpression(this, context);
// Ordinarily the ternary operator is right-associative. The following are equivalent: // Ordinarily the ternary operator is right-associative. The following are equivalent:
// `a ? b : c ? d : e` => `a ? b : (c ? d : e)` // `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. // conditional expression is directly used as the condition of another.
// //
// TODO(alxhub): investigate better logic for precendence of conditional operators // 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 // The condition of this ternary needs to be wrapped in parentheses to maintain
// left-associativity. // left-associativity.
cond = ts.createParen(cond); cond = this.factory.createParenthesizedExpression(cond);
} }
return ts.createConditional( return this.factory.createConditional(
cond, ast.trueCase.visitExpression(this, context), cond, ast.trueCase.visitExpression(this, context),
ast.falseCase!.visitExpression(this, context)); ast.falseCase!.visitExpression(this, context));
} }
visitNotExpr(ast: NotExpr, context: Context): ts.PrefixUnaryExpression { visitNotExpr(ast: o.NotExpr, context: Context): TExpression {
return ts.createPrefix( return this.factory.createUnaryExpression('!', ast.condition.visitExpression(this, context));
ts.SyntaxKind.ExclamationToken, 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); 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); return ast.value.visitExpression(this, context);
} }
visitFunctionExpr(ast: FunctionExpr, context: Context): ts.FunctionExpression { visitFunctionExpr(ast: o.FunctionExpr, context: Context): TExpression {
return ts.createFunctionExpression( return this.factory.createFunctionExpression(
undefined, undefined, ast.name || undefined, undefined, ast.name ?? null, ast.params.map(param => param.name),
ast.params.map( this.factory.createBlock(this.visitStatements(ast.statements, context)));
param => ts.createParameter(
undefined, undefined, undefined, param.name, undefined, undefined, undefined)),
undefined, ts.createBlock(ast.statements.map(stmt => stmt.visitStatement(this, context))));
} }
visitUnaryOperatorExpr(ast: UnaryOperatorExpr, context: Context): ts.Expression { visitBinaryOperatorExpr(ast: o.BinaryOperatorExpr, context: Context): TExpression {
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 {
if (!BINARY_OPERATORS.has(ast.operator)) { 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( return this.factory.createBinaryExpression(
ast.lhs.visitExpression(this, context), BINARY_OPERATORS.get(ast.operator)!, ast.lhs.visitExpression(this, context),
ast.rhs.visitExpression(this, context)); BINARY_OPERATORS.get(ast.operator)!,
ast.rhs.visitExpression(this, context),
);
} }
visitReadPropExpr(ast: ReadPropExpr, context: Context): ts.PropertyAccessExpression { visitReadPropExpr(ast: o.ReadPropExpr, context: Context): TExpression {
return ts.createPropertyAccess(ast.receiver.visitExpression(this, context), ast.name); return this.factory.createPropertyAccess(ast.receiver.visitExpression(this, context), ast.name);
} }
visitReadKeyExpr(ast: ReadKeyExpr, context: Context): ts.ElementAccessExpression { visitReadKeyExpr(ast: o.ReadKeyExpr, context: Context): TExpression {
return ts.createElementAccess( return this.factory.createElementAccess(
ast.receiver.visitExpression(this, context), ast.index.visitExpression(this, context)); ast.receiver.visitExpression(this, context), ast.index.visitExpression(this, context));
} }
visitLiteralArrayExpr(ast: LiteralArrayExpr, context: Context): ts.ArrayLiteralExpression { visitLiteralArrayExpr(ast: o.LiteralArrayExpr, context: Context): TExpression {
const expr = return this.factory.createArrayLiteral(ast.entries.map(
ts.createArrayLiteral(ast.entries.map(expr => expr.visitExpression(this, context))); expr => this.setSourceMapRange(expr.visitExpression(this, context), ast.sourceSpan)));
this.setSourceMapRange(expr, ast.sourceSpan);
return expr;
} }
visitLiteralMapExpr(ast: LiteralMapExpr, context: Context): ts.ObjectLiteralExpression { visitLiteralMapExpr(ast: o.LiteralMapExpr, context: Context): TExpression {
const entries = ast.entries.map( const properties: ObjectLiteralProperty<TExpression>[] = ast.entries.map(entry => {
entry => ts.createPropertyAssignment( return {
entry.quoted ? ts.createLiteral(entry.key) : ts.createIdentifier(entry.key), propertyName: entry.key,
entry.value.visitExpression(this, context))); quoted: entry.quoted,
const expr = ts.createObjectLiteral(entries); value: entry.value.visitExpression(this, context)
this.setSourceMapRange(expr, ast.sourceSpan); };
return expr; });
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.'); throw new Error('Method not implemented.');
} }
visitWrappedNodeExpr(ast: WrappedNodeExpr<any>, context: Context): any { visitWrappedNodeExpr(ast: o.WrappedNodeExpr<any>, _context: Context): any {
if (ts.isIdentifier(ast.node)) { this.recordWrappedNodeExpr(ast.node);
this.defaultImportRecorder.recordUsedIdentifier(ast.node);
}
return ast.node; return ast.node;
} }
visitTypeofExpr(ast: TypeofExpr, context: Context): ts.TypeOfExpression { visitTypeofExpr(ast: o.TypeofExpr, context: Context): TExpression {
return ts.createTypeOf(ast.expr.visitExpression(this, context)); return this.factory.createTypeOfExpression(ast.expr.visitExpression(this, context));
} }
/** visitUnaryOperatorExpr(ast: o.UnaryOperatorExpr, context: Context): TExpression {
* Translate the `LocalizedString` node into a `TaggedTemplateExpression` for ES2015 formatted if (!UNARY_OPERATORS.has(ast.operator)) {
* output. throw new Error(`Unknown unary operator: ${o.UnaryOperator[ast.operator]}`);
*/
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);
} }
const expression = ts.createTaggedTemplate(ts.createIdentifier('$localize'), template); return this.factory.createUnaryExpression(
this.setSourceMapRange(expression, ast.sourceSpan); UNARY_OPERATORS.get(ast.operator)!, ast.expr.visitExpression(this, context));
return expression;
} }
/** private visitStatements(statements: o.Statement[], context: Context): TStatement[] {
* Translate the `LocalizedString` node into a `$localize` call using the imported return statements.map(stmt => stmt.visitStatement(this, context))
* `__makeTemplateObject` helper for ES5 formatted output. .filter(stmt => stmt !== undefined);
*/
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 setSourceMapRange<T extends TExpression|TStatement>(ast: T, span: o.ParseSourceSpan|null):
private setSourceMapRange(expr: ts.Node, sourceSpan: ParseSourceSpan|null) { T {
if (sourceSpan) { return this.factory.setSourceMapRange(ast, createRange(span));
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 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. * Convert a cooked-raw string object into one that can be used by the AST factories.
*
* @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>( function createTemplateElement(
statement: T, leadingComments?: LeadingComment[]): T { {cooked, raw, range}: {cooked: string, raw: string, range: o.ParseSourceSpan|null}):
if (leadingComments === undefined) { TemplateElement {
return statement; return {cooked, raw, range: createRange(range)};
} }
for (const comment of leadingComments) { /**
const commentKind = comment.multiline ? ts.SyntaxKind.MultiLineCommentTrivia : * Convert an OutputAST source-span into a range that can be used by the AST factories.
ts.SyntaxKind.SingleLineCommentTrivia; */
if (comment.multiline) { function createRange(span: o.ParseSourceSpan|null): SourceMapRange|null {
ts.addSyntheticLeadingComment( if (span === null) {
statement, commentKind, comment.toString(), comment.trailingNewline); return null;
} else { }
for (const line of comment.text.split('\n')) { const {start, end} = span;
ts.addSyntheticLeadingComment(statement, commentKind, line, comment.trailingNewline); const {url, content} = start.file;
} if (!url) {
} return null;
} }
return statement; 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 {ExpressionType, ExternalExpr, Type, WrappedNodeExpr} from '@angular/compiler';
import * as ts from 'typescript'; 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 {ClassDeclaration, ReflectionHost} from '../../reflection';
import {ImportManager, translateExpression, translateType} from '../../translator'; import {ImportManager, translateExpression, translateType} from '../../translator';
import {TypeCheckableDirectiveMeta, TypeCheckingConfig, TypeCtorMetadata} from '../api'; import {TypeCheckableDirectiveMeta, TypeCheckingConfig, TypeCtorMetadata} from '../api';
@ -213,8 +213,7 @@ export class Environment {
const ngExpr = this.refEmitter.emit(ref, this.contextFile, ImportFlags.NoAliasing); const ngExpr = this.refEmitter.emit(ref, this.contextFile, ImportFlags.NoAliasing);
// Use `translateExpression` to convert the `Expression` into a `ts.Expression`. // Use `translateExpression` to convert the `Expression` into a `ts.Expression`.
return translateExpression( return translateExpression(ngExpr, this.importManager);
ngExpr, this.importManager, NOOP_DEFAULT_IMPORT_RECORDER, ts.ScriptTarget.ES2015);
} }
/** /**