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:
parent
a93605f2a4
commit
297b123151
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}`;
|
||||||
|
|
|
@ -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, ' ');
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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>;
|
||||||
|
}
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
|
@ -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",
|
||||||
|
],
|
||||||
|
)
|
|
@ -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;
|
||||||
|
}
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in New Issue