diff --git a/packages/compiler-cli/ngcc/src/rendering/commonjs_rendering_formatter.ts b/packages/compiler-cli/ngcc/src/rendering/commonjs_rendering_formatter.ts index 4213d605a4..962f94ad29 100644 --- a/packages/compiler-cli/ngcc/src/rendering/commonjs_rendering_formatter.ts +++ b/packages/compiler-cli/ngcc/src/rendering/commonjs_rendering_formatter.ts @@ -55,7 +55,7 @@ export class CommonJsRenderingFormatter extends Esm5RenderingFormatter { const namedImport = entryPointBasePath !== basePath ? importManager.generateNamedImport(relativePath, e.identifier) : {symbol: e.identifier, moduleImport: null}; - const importNamespace = namedImport.moduleImport ? `${namedImport.moduleImport}.` : ''; + const importNamespace = namedImport.moduleImport ? `${namedImport.moduleImport.text}.` : ''; const exportStr = `\nexports.${e.identifier} = ${importNamespace}${namedImport.symbol};`; output.append(exportStr); }); @@ -66,7 +66,7 @@ export class CommonJsRenderingFormatter extends Esm5RenderingFormatter { file: ts.SourceFile): void { for (const e of exports) { const namedImport = importManager.generateNamedImport(e.fromModule, e.symbolName); - const importNamespace = namedImport.moduleImport ? `${namedImport.moduleImport}.` : ''; + const importNamespace = namedImport.moduleImport ? `${namedImport.moduleImport.text}.` : ''; const exportStr = `\nexports.${e.asAlias} = ${importNamespace}${namedImport.symbol};`; output.append(exportStr); } diff --git a/packages/compiler-cli/ngcc/src/rendering/esm5_rendering_formatter.ts b/packages/compiler-cli/ngcc/src/rendering/esm5_rendering_formatter.ts index a2cefd7d42..8cb3e68310 100644 --- a/packages/compiler-cli/ngcc/src/rendering/esm5_rendering_formatter.ts +++ b/packages/compiler-cli/ngcc/src/rendering/esm5_rendering_formatter.ts @@ -9,7 +9,6 @@ import {Statement} from '@angular/compiler'; import MagicString from 'magic-string'; import * as ts from 'typescript'; -import {NOOP_DEFAULT_IMPORT_RECORDER} from '../../../src/ngtsc/imports'; import {ImportManager, translateStatement} from '../../../src/ngtsc/translator'; import {CompiledClass} from '../analysis/types'; import {getContainingStatement} from '../host/esm2015_host'; @@ -65,8 +64,9 @@ export class Esm5RenderingFormatter extends EsmRenderingFormatter { * @return The JavaScript code corresponding to `stmt` (in the appropriate format). */ printStatement(stmt: Statement, sourceFile: ts.SourceFile, importManager: ImportManager): string { - const node = - translateStatement(stmt, importManager, NOOP_DEFAULT_IMPORT_RECORDER, ts.ScriptTarget.ES5); + const node = translateStatement( + stmt, importManager, + {downlevelLocalizedStrings: true, downlevelVariableDeclarations: true}); const code = this.printer.printNode(ts.EmitHint.Unspecified, node, sourceFile); return code; diff --git a/packages/compiler-cli/ngcc/src/rendering/esm_rendering_formatter.ts b/packages/compiler-cli/ngcc/src/rendering/esm_rendering_formatter.ts index 5f113661fb..28aeaec3b1 100644 --- a/packages/compiler-cli/ngcc/src/rendering/esm_rendering_formatter.ts +++ b/packages/compiler-cli/ngcc/src/rendering/esm_rendering_formatter.ts @@ -10,7 +10,7 @@ import MagicString from 'magic-string'; import * as ts from 'typescript'; import {absoluteFromSourceFile, AbsoluteFsPath, dirname, relative, toRelativeImport} from '../../../src/ngtsc/file_system'; -import {NOOP_DEFAULT_IMPORT_RECORDER, Reexport} from '../../../src/ngtsc/imports'; +import {Reexport} from '../../../src/ngtsc/imports'; import {Import, ImportManager, translateStatement} from '../../../src/ngtsc/translator'; import {isDtsPath} from '../../../src/ngtsc/util/src/typescript'; import {ModuleWithProvidersInfo} from '../analysis/module_with_providers_analyzer'; @@ -247,8 +247,7 @@ export class EsmRenderingFormatter implements RenderingFormatter { * @return The JavaScript code corresponding to `stmt` (in the appropriate format). */ printStatement(stmt: Statement, sourceFile: ts.SourceFile, importManager: ImportManager): string { - const node = translateStatement( - stmt, importManager, NOOP_DEFAULT_IMPORT_RECORDER, ts.ScriptTarget.ES2015); + const node = translateStatement(stmt, importManager); const code = this.printer.printNode(ts.EmitHint.Unspecified, node, sourceFile); return code; @@ -264,8 +263,6 @@ export class EsmRenderingFormatter implements RenderingFormatter { return 0; } - - /** * Check whether the given type is the core Angular `ModuleWithProviders` interface. * @param typeName The type to check. @@ -292,7 +289,8 @@ function findStatement(node: ts.Node): ts.Statement|undefined { function generateImportString( importManager: ImportManager, importPath: string|null, importName: string) { const importAs = importPath ? importManager.generateNamedImport(importPath, importName) : null; - return importAs ? `${importAs.moduleImport}.${importAs.symbol}` : `${importName}`; + return importAs && importAs.moduleImport ? `${importAs.moduleImport.text}.${importAs.symbol}` : + `${importName}`; } function getNextSiblingInArray(node: T, array: ts.NodeArray): T|null { diff --git a/packages/compiler-cli/ngcc/src/rendering/umd_rendering_formatter.ts b/packages/compiler-cli/ngcc/src/rendering/umd_rendering_formatter.ts index 0a696b0e0d..3fbf9755de 100644 --- a/packages/compiler-cli/ngcc/src/rendering/umd_rendering_formatter.ts +++ b/packages/compiler-cli/ngcc/src/rendering/umd_rendering_formatter.ts @@ -91,7 +91,7 @@ export class UmdRenderingFormatter extends Esm5RenderingFormatter { const namedImport = entryPointBasePath !== basePath ? importManager.generateNamedImport(relativePath, e.identifier) : {symbol: e.identifier, moduleImport: null}; - const importNamespace = namedImport.moduleImport ? `${namedImport.moduleImport}.` : ''; + const importNamespace = namedImport.moduleImport ? `${namedImport.moduleImport.text}.` : ''; const exportStr = `\nexports.${e.identifier} = ${importNamespace}${namedImport.symbol};`; output.appendRight(insertionPoint, exportStr); }); @@ -111,7 +111,7 @@ export class UmdRenderingFormatter extends Esm5RenderingFormatter { lastStatement ? lastStatement.getEnd() : factoryFunction.body.getEnd() - 1; for (const e of exports) { const namedImport = importManager.generateNamedImport(e.fromModule, e.symbolName); - const importNamespace = namedImport.moduleImport ? `${namedImport.moduleImport}.` : ''; + const importNamespace = namedImport.moduleImport ? `${namedImport.moduleImport.text}.` : ''; const exportStr = `\nexports.${e.asAlias} = ${importNamespace}${namedImport.symbol};`; output.appendRight(insertionPoint, exportStr); } diff --git a/packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts b/packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts index 6d3e87e44e..78121b5dad 100644 --- a/packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts +++ b/packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts @@ -13,7 +13,7 @@ import * as ts from 'typescript'; import {absoluteFrom, getFileSystem} from '../../../src/ngtsc/file_system'; import {runInEachFileSystem, TestFile} from '../../../src/ngtsc/file_system/testing'; -import {NOOP_DEFAULT_IMPORT_RECORDER, Reexport} from '../../../src/ngtsc/imports'; +import {Reexport} from '../../../src/ngtsc/imports'; import {MockLogger} from '../../../src/ngtsc/logging/testing'; import {Import, ImportManager, translateStatement} from '../../../src/ngtsc/translator'; import {loadTestFiles} from '../../../test/helpers'; @@ -65,8 +65,8 @@ class TestRenderingFormatter implements RenderingFormatter { } printStatement(stmt: Statement, sourceFile: ts.SourceFile, importManager: ImportManager): string { const node = translateStatement( - stmt, importManager, NOOP_DEFAULT_IMPORT_RECORDER, - this.isEs5 ? ts.ScriptTarget.ES5 : ts.ScriptTarget.ES2015); + stmt, importManager, + {downlevelLocalizedStrings: this.isEs5, downlevelVariableDeclarations: this.isEs5}); const code = this.printer.printNode(ts.EmitHint.Unspecified, node, sourceFile); return `// TRANSPILED\n${code}`; diff --git a/packages/compiler-cli/src/ngtsc/annotations/test/metadata_spec.ts b/packages/compiler-cli/src/ngtsc/annotations/test/metadata_spec.ts index 1745af51a7..9a09329bad 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/test/metadata_spec.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/test/metadata_spec.ts @@ -133,8 +133,7 @@ runInEachFileSystem(() => { } const sf = getSourceFileOrError(program, _('/index.ts')); const im = new ImportManager(new NoopImportRewriter(), 'i'); - const tsStatement = - translateStatement(call, im, NOOP_DEFAULT_IMPORT_RECORDER, ts.ScriptTarget.ES2015); + const tsStatement = translateStatement(call, im); const res = ts.createPrinter().printNode(ts.EmitHint.Unspecified, tsStatement, sf); return res.replace(/\s+/g, ' '); } diff --git a/packages/compiler-cli/src/ngtsc/transform/src/transform.ts b/packages/compiler-cli/src/ngtsc/transform/src/transform.ts index b93af50e39..96ea10d553 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/transform.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/transform.ts @@ -11,7 +11,7 @@ import * as ts from 'typescript'; import {DefaultImportRecorder, ImportRewriter} from '../../imports'; import {Decorator, ReflectionHost} from '../../reflection'; -import {ImportManager, translateExpression, translateStatement} from '../../translator'; +import {ImportManager, RecordWrappedNodeExprFn, translateExpression, translateStatement} from '../../translator'; import {visit, VisitListEntryResult, Visitor} from '../../util/src/visitor'; import {CompileResult} from './api'; @@ -35,11 +35,12 @@ export function ivyTransformFactory( compilation: TraitCompiler, reflector: ReflectionHost, importRewriter: ImportRewriter, defaultImportRecorder: DefaultImportRecorder, isCore: boolean, isClosureCompilerEnabled: boolean): ts.TransformerFactory { + const recordWrappedNodeExpr = createRecorderFn(defaultImportRecorder); return (context: ts.TransformationContext): ts.Transformer => { return (file: ts.SourceFile): ts.SourceFile => { return transformIvySourceFile( compilation, context, reflector, importRewriter, file, isCore, isClosureCompilerEnabled, - defaultImportRecorder); + recordWrappedNodeExpr); }; }; } @@ -77,7 +78,7 @@ class IvyTransformationVisitor extends Visitor { private compilation: TraitCompiler, private classCompilationMap: Map, private reflector: ReflectionHost, private importManager: ImportManager, - private defaultImportRecorder: DefaultImportRecorder, + private recordWrappedNodeExpr: RecordWrappedNodeExprFn, private isClosureCompilerEnabled: boolean, private isCore: boolean) { super(); } @@ -97,8 +98,8 @@ class IvyTransformationVisitor extends Visitor { for (const field of this.classCompilationMap.get(node)!) { // Translate the initializer for the field into TS nodes. const exprNode = translateExpression( - field.initializer, this.importManager, this.defaultImportRecorder, - ts.ScriptTarget.ES2015); + field.initializer, this.importManager, + {recordWrappedNodeExpr: this.recordWrappedNodeExpr}); // Create a static property declaration for the new field. const property = ts.createProperty( @@ -118,7 +119,7 @@ class IvyTransformationVisitor extends Visitor { field.statements .map( stmt => translateStatement( - stmt, this.importManager, this.defaultImportRecorder, ts.ScriptTarget.ES2015)) + stmt, this.importManager, {recordWrappedNodeExpr: this.recordWrappedNodeExpr})) .forEach(stmt => statements.push(stmt)); members.push(property); @@ -248,7 +249,7 @@ function transformIvySourceFile( compilation: TraitCompiler, context: ts.TransformationContext, reflector: ReflectionHost, importRewriter: ImportRewriter, file: ts.SourceFile, isCore: boolean, isClosureCompilerEnabled: boolean, - defaultImportRecorder: DefaultImportRecorder): ts.SourceFile { + recordWrappedNodeExpr: RecordWrappedNodeExprFn): ts.SourceFile { const constantPool = new ConstantPool(isClosureCompilerEnabled); const importManager = new ImportManager(importRewriter); @@ -270,14 +271,18 @@ function transformIvySourceFile( // results obtained at Step 1. const transformationVisitor = new IvyTransformationVisitor( compilation, compilationVisitor.classCompilationMap, reflector, importManager, - defaultImportRecorder, isClosureCompilerEnabled, isCore); + recordWrappedNodeExpr, isClosureCompilerEnabled, isCore); let sf = visit(file, transformationVisitor, context); // Generate the constant statements first, as they may involve adding additional imports // to the ImportManager. - const constants = constantPool.statements.map( - stmt => translateStatement( - stmt, importManager, defaultImportRecorder, getLocalizeCompileTarget(context))); + const downlevelTranslatedCode = getLocalizeCompileTarget(context) < ts.ScriptTarget.ES2015; + const constants = + constantPool.statements.map(stmt => translateStatement(stmt, importManager, { + recordWrappedNodeExpr, + downlevelLocalizedStrings: downlevelTranslatedCode, + downlevelVariableDeclarations: downlevelTranslatedCode, + })); // Preserve @fileoverview comments required by Closure, since the location might change as a // result of adding extra imports and constant pool statements. @@ -360,3 +365,12 @@ function maybeFilterDecorator( function isFromAngularCore(decorator: Decorator): boolean { return decorator.import !== null && decorator.import.from === '@angular/core'; } + +function createRecorderFn(defaultImportRecorder: DefaultImportRecorder): + RecordWrappedNodeExprFn { + return expr => { + if (ts.isIdentifier(expr)) { + defaultImportRecorder.recordUsedIdentifier(expr); + } + }; +} diff --git a/packages/compiler-cli/src/ngtsc/translator/index.ts b/packages/compiler-cli/src/ngtsc/translator/index.ts index 102b3d406a..0df3249b78 100644 --- a/packages/compiler-cli/src/ngtsc/translator/index.ts +++ b/packages/compiler-cli/src/ngtsc/translator/index.ts @@ -6,6 +6,10 @@ * found in the LICENSE file at https://angular.io/license */ -export {Import, ImportManager, NamedImport} from './src/import_manager'; -export {attachComments, translateExpression, translateStatement} from './src/translator'; -export {translateType} from './src/type_translator'; \ No newline at end of file +export {AstFactory, BinaryOperator, LeadingComment, ObjectLiteralProperty, SourceMapLocation, SourceMapRange, TemplateElement, TemplateLiteral, UnaryOperator} from './src/api/ast_factory'; +export {Import, ImportGenerator, NamedImport} from './src/api/import_generator'; +export {ImportManager} from './src/import_manager'; +export {RecordWrappedNodeExprFn} from './src/translator'; +export {translateType} from './src/type_translator'; +export {attachComments, TypeScriptAstFactory} from './src/typescript_ast_factory'; +export {translateExpression, translateStatement} from './src/typescript_translator'; diff --git a/packages/compiler-cli/src/ngtsc/translator/src/api/ast_factory.ts b/packages/compiler-cli/src/ngtsc/translator/src/api/ast_factory.ts new file mode 100644 index 0000000000..d8182f3885 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/translator/src/api/ast_factory.ts @@ -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 { + /** + * 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; + + /** + * 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; + + /** + * 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(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 { + 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 { + /** + * 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; +} diff --git a/packages/compiler-cli/src/ngtsc/translator/src/api/import_generator.ts b/packages/compiler-cli/src/ngtsc/translator/src/api/import_generator.ts new file mode 100644 index 0000000000..5e5fd3474d --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/translator/src/api/import_generator.ts @@ -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 { + /** 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 { + generateNamespaceImport(moduleName: string): TExpression; + generateNamedImport(moduleName: string, originalSymbol: string): NamedImport; +} diff --git a/packages/compiler-cli/src/ngtsc/translator/src/import_manager.ts b/packages/compiler-cli/src/ngtsc/translator/src/import_manager.ts index 2674b2d297..f5eeec0f4b 100644 --- a/packages/compiler-cli/src/ngtsc/translator/src/import_manager.ts +++ b/packages/compiler-cli/src/ngtsc/translator/src/import_manager.ts @@ -5,37 +5,26 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -import {ImportRewriter, NoopImportRewriter} from '../../imports/src/core'; +import * as ts from 'typescript'; +import {ImportRewriter, NoopImportRewriter} from '../../imports'; +import {Import, ImportGenerator, NamedImport} from './api/import_generator'; -/** - * Information about an import that has been added to a module. - */ -export interface Import { - /** The name of the module that has been imported. */ - specifier: string; - /** The alias of the imported module. */ - qualifier: string; -} - -/** - * The symbol name and import namespace of an imported symbol, - * which has been registered through the ImportManager. - */ -export interface NamedImport { - /** The import namespace containing this imported symbol. */ - moduleImport: string|null; - /** The (possibly rewritten) name of the imported symbol. */ - symbol: string; -} - -export class ImportManager { - private specifierToIdentifier = new Map(); +export class ImportManager implements ImportGenerator { + private specifierToIdentifier = new Map(); private nextIndex = 0; constructor(protected rewriter: ImportRewriter = new NoopImportRewriter(), private prefix = 'i') { } - generateNamedImport(moduleName: string, originalSymbol: string): NamedImport { + generateNamespaceImport(moduleName: string): ts.Identifier { + if (!this.specifierToIdentifier.has(moduleName)) { + this.specifierToIdentifier.set( + moduleName, ts.createIdentifier(`${this.prefix}${this.nextIndex++}`)); + } + return this.specifierToIdentifier.get(moduleName)!; + } + + generateNamedImport(moduleName: string, originalSymbol: string): NamedImport { // First, rewrite the symbol name. const symbol = this.rewriter.rewriteSymbol(originalSymbol, moduleName); @@ -46,12 +35,8 @@ export class ImportManager { return {moduleImport: null, symbol}; } - // If not, this symbol will be imported. Allocate a prefix for the imported module if needed. - - if (!this.specifierToIdentifier.has(moduleName)) { - this.specifierToIdentifier.set(moduleName, `${this.prefix}${this.nextIndex++}`); - } - const moduleImport = this.specifierToIdentifier.get(moduleName)!; + // If not, this symbol will be imported using a generated namespace import. + const moduleImport = this.generateNamespaceImport(moduleName); return {moduleImport, symbol}; } @@ -60,7 +45,7 @@ export class ImportManager { const imports: {specifier: string, qualifier: string}[] = []; this.specifierToIdentifier.forEach((qualifier, specifier) => { specifier = this.rewriter.rewriteSpecifier(specifier, contextPath); - imports.push({specifier, qualifier}); + imports.push({specifier, qualifier: qualifier.text}); }); return imports; } diff --git a/packages/compiler-cli/src/ngtsc/translator/src/translator.ts b/packages/compiler-cli/src/ngtsc/translator/src/translator.ts index c859923976..d644ec4cfc 100644 --- a/packages/compiler-cli/src/ngtsc/translator/src/translator.ts +++ b/packages/compiler-cli/src/ngtsc/translator/src/translator.ts @@ -5,214 +5,249 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ +import * as o from '@angular/compiler'; -import {AssertNotNull, BinaryOperator, BinaryOperatorExpr, CastExpr, ClassStmt, CommaExpr, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, Expression, ExpressionStatement, ExpressionVisitor, ExternalExpr, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, LeadingComment, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, NotExpr, ParseSourceSpan, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, Statement, StatementVisitor, StmtModifier, ThrowStmt, TryCatchStmt, TypeofExpr, WrappedNodeExpr, WriteKeyExpr, WritePropExpr, WriteVarExpr} from '@angular/compiler'; -import {LocalizedString, UnaryOperator, UnaryOperatorExpr} from '@angular/compiler/src/output/output_ast'; -import * as ts from 'typescript'; - -import {DefaultImportRecorder} from '../../imports'; +import {AstFactory, BinaryOperator, ObjectLiteralProperty, SourceMapRange, TemplateElement, TemplateLiteral, UnaryOperator} from './api/ast_factory'; +import {ImportGenerator} from './api/import_generator'; import {Context} from './context'; -import {ImportManager} from './import_manager'; -const UNARY_OPERATORS = new Map([ - [UnaryOperator.Minus, ts.SyntaxKind.MinusToken], - [UnaryOperator.Plus, ts.SyntaxKind.PlusToken], +const UNARY_OPERATORS = new Map([ + [o.UnaryOperator.Minus, '-'], + [o.UnaryOperator.Plus, '+'], ]); -const BINARY_OPERATORS = new Map([ - [BinaryOperator.And, ts.SyntaxKind.AmpersandAmpersandToken], - [BinaryOperator.Bigger, ts.SyntaxKind.GreaterThanToken], - [BinaryOperator.BiggerEquals, ts.SyntaxKind.GreaterThanEqualsToken], - [BinaryOperator.BitwiseAnd, ts.SyntaxKind.AmpersandToken], - [BinaryOperator.Divide, ts.SyntaxKind.SlashToken], - [BinaryOperator.Equals, ts.SyntaxKind.EqualsEqualsToken], - [BinaryOperator.Identical, ts.SyntaxKind.EqualsEqualsEqualsToken], - [BinaryOperator.Lower, ts.SyntaxKind.LessThanToken], - [BinaryOperator.LowerEquals, ts.SyntaxKind.LessThanEqualsToken], - [BinaryOperator.Minus, ts.SyntaxKind.MinusToken], - [BinaryOperator.Modulo, ts.SyntaxKind.PercentToken], - [BinaryOperator.Multiply, ts.SyntaxKind.AsteriskToken], - [BinaryOperator.NotEquals, ts.SyntaxKind.ExclamationEqualsToken], - [BinaryOperator.NotIdentical, ts.SyntaxKind.ExclamationEqualsEqualsToken], - [BinaryOperator.Or, ts.SyntaxKind.BarBarToken], - [BinaryOperator.Plus, ts.SyntaxKind.PlusToken], +const BINARY_OPERATORS = new Map([ + [o.BinaryOperator.And, '&&'], + [o.BinaryOperator.Bigger, '>'], + [o.BinaryOperator.BiggerEquals, '>='], + [o.BinaryOperator.BitwiseAnd, '&'], + [o.BinaryOperator.Divide, '/'], + [o.BinaryOperator.Equals, '=='], + [o.BinaryOperator.Identical, '==='], + [o.BinaryOperator.Lower, '<'], + [o.BinaryOperator.LowerEquals, '<='], + [o.BinaryOperator.Minus, '-'], + [o.BinaryOperator.Modulo, '%'], + [o.BinaryOperator.Multiply, '*'], + [o.BinaryOperator.NotEquals, '!='], + [o.BinaryOperator.NotIdentical, '!=='], + [o.BinaryOperator.Or, '||'], + [o.BinaryOperator.Plus, '+'], ]); -export function translateExpression( - expression: Expression, imports: ImportManager, defaultImportRecorder: DefaultImportRecorder, - scriptTarget: Exclude): ts.Expression { - return expression.visitExpression( - new ExpressionTranslatorVisitor(imports, defaultImportRecorder, scriptTarget), - new Context(false)); +export type RecordWrappedNodeExprFn = (expr: TExpression) => void; + +export interface TranslatorOptions { + downlevelLocalizedStrings?: boolean; + downlevelVariableDeclarations?: boolean; + recordWrappedNodeExpr?: RecordWrappedNodeExprFn; } -export function translateStatement( - statement: Statement, imports: ImportManager, defaultImportRecorder: DefaultImportRecorder, - scriptTarget: Exclude): ts.Statement { - return statement.visitStatement( - new ExpressionTranslatorVisitor(imports, defaultImportRecorder, scriptTarget), - new Context(true)); -} +export class ExpressionTranslatorVisitor implements o.ExpressionVisitor, + o.StatementVisitor { + private downlevelLocalizedStrings: boolean; + private downlevelVariableDeclarations: boolean; + private recordWrappedNodeExpr: RecordWrappedNodeExprFn; - -class ExpressionTranslatorVisitor implements ExpressionVisitor, StatementVisitor { - private externalSourceFiles = new Map(); constructor( - private imports: ImportManager, private defaultImportRecorder: DefaultImportRecorder, - private scriptTarget: Exclude) {} - - visitDeclareVarStmt(stmt: DeclareVarStmt, context: Context): ts.VariableStatement { - const varType = this.scriptTarget < ts.ScriptTarget.ES2015 ? - ts.NodeFlags.None : - stmt.hasModifier(StmtModifier.Final) ? ts.NodeFlags.Const : ts.NodeFlags.Let; - const varDeclaration = ts.createVariableDeclaration( - /* name */ stmt.name, - /* type */ undefined, - /* initializer */ stmt.value?.visitExpression(this, context.withExpressionMode)); - const declarationList = ts.createVariableDeclarationList( - /* declarations */[varDeclaration], - /* flags */ varType); - const varStatement = ts.createVariableStatement(undefined, declarationList); - return attachComments(varStatement, stmt.leadingComments); + private factory: AstFactory, + private imports: ImportGenerator, options: TranslatorOptions) { + this.downlevelLocalizedStrings = options.downlevelLocalizedStrings === true; + this.downlevelVariableDeclarations = options.downlevelVariableDeclarations === true; + this.recordWrappedNodeExpr = options.recordWrappedNodeExpr || (() => {}); } - visitDeclareFunctionStmt(stmt: DeclareFunctionStmt, context: Context): ts.FunctionDeclaration { - const fnDeclaration = ts.createFunctionDeclaration( - /* decorators */ undefined, - /* modifiers */ undefined, - /* asterisk */ undefined, - /* name */ stmt.name, - /* typeParameters */ undefined, - /* parameters */ - stmt.params.map(param => ts.createParameter(undefined, undefined, undefined, param.name)), - /* type */ undefined, - /* body */ - ts.createBlock( - stmt.statements.map(child => child.visitStatement(this, context.withStatementMode)))); - return attachComments(fnDeclaration, stmt.leadingComments); - } - - visitExpressionStmt(stmt: ExpressionStatement, context: Context): ts.ExpressionStatement { - return attachComments( - ts.createStatement(stmt.expr.visitExpression(this, context.withStatementMode)), + visitDeclareVarStmt(stmt: o.DeclareVarStmt, context: Context): TStatement { + const varType = this.downlevelVariableDeclarations ? + 'var' : + stmt.hasModifier(o.StmtModifier.Final) ? 'const' : 'let'; + return this.factory.attachComments( + this.factory.createVariableDeclaration( + stmt.name, stmt.value?.visitExpression(this, context.withExpressionMode), varType), stmt.leadingComments); } - visitReturnStmt(stmt: ReturnStatement, context: Context): ts.ReturnStatement { - return attachComments( - ts.createReturn(stmt.value.visitExpression(this, context.withExpressionMode)), + visitDeclareFunctionStmt(stmt: o.DeclareFunctionStmt, context: Context): TStatement { + return this.factory.attachComments( + this.factory.createFunctionDeclaration( + stmt.name, stmt.params.map(param => param.name), + this.factory.createBlock( + this.visitStatements(stmt.statements, context.withStatementMode))), stmt.leadingComments); } - visitDeclareClassStmt(stmt: ClassStmt, context: Context) { - if (this.scriptTarget < ts.ScriptTarget.ES2015) { - throw new Error( - `Unsupported mode: Visiting a "declare class" statement (class ${stmt.name}) while ` + - `targeting ${ts.ScriptTarget[this.scriptTarget]}.`); - } + visitExpressionStmt(stmt: o.ExpressionStatement, context: Context): TStatement { + return this.factory.attachComments( + this.factory.createExpressionStatement( + stmt.expr.visitExpression(this, context.withStatementMode)), + stmt.leadingComments); + } + + visitReturnStmt(stmt: o.ReturnStatement, context: Context): TStatement { + return this.factory.attachComments( + this.factory.createReturnStatement( + stmt.value.visitExpression(this, context.withExpressionMode)), + stmt.leadingComments); + } + + visitDeclareClassStmt(_stmt: o.ClassStmt, _context: Context): never { throw new Error('Method not implemented.'); } - visitIfStmt(stmt: IfStmt, context: Context): ts.IfStatement { - const thenBlock = ts.createBlock( - stmt.trueCase.map(child => child.visitStatement(this, context.withStatementMode))); - const elseBlock = stmt.falseCase.length > 0 ? - ts.createBlock( - stmt.falseCase.map(child => child.visitStatement(this, context.withStatementMode))) : - undefined; - const ifStatement = - ts.createIf(stmt.condition.visitExpression(this, context), thenBlock, elseBlock); - return attachComments(ifStatement, stmt.leadingComments); - } - - visitTryCatchStmt(stmt: TryCatchStmt, context: Context) { - throw new Error('Method not implemented.'); - } - - visitThrowStmt(stmt: ThrowStmt, context: Context): ts.ThrowStatement { - return attachComments( - ts.createThrow(stmt.error.visitExpression(this, context.withExpressionMode)), + visitIfStmt(stmt: o.IfStmt, context: Context): TStatement { + return this.factory.attachComments( + this.factory.createIfStatement( + stmt.condition.visitExpression(this, context), + this.factory.createBlock( + this.visitStatements(stmt.trueCase, context.withStatementMode)), + stmt.falseCase.length > 0 ? this.factory.createBlock(this.visitStatements( + stmt.falseCase, context.withStatementMode)) : + null), stmt.leadingComments); } - visitReadVarExpr(ast: ReadVarExpr, context: Context): ts.Identifier { - const identifier = ts.createIdentifier(ast.name!); + visitTryCatchStmt(_stmt: o.TryCatchStmt, _context: Context): never { + throw new Error('Method not implemented.'); + } + + visitThrowStmt(stmt: o.ThrowStmt, context: Context): TStatement { + return this.factory.attachComments( + this.factory.createThrowStatement( + stmt.error.visitExpression(this, context.withExpressionMode)), + stmt.leadingComments); + } + + visitReadVarExpr(ast: o.ReadVarExpr, _context: Context): TExpression { + const identifier = this.factory.createIdentifier(ast.name!); this.setSourceMapRange(identifier, ast.sourceSpan); return identifier; } - visitWriteVarExpr(expr: WriteVarExpr, context: Context): ts.Expression { - const result: ts.Expression = ts.createBinary( - ts.createIdentifier(expr.name), ts.SyntaxKind.EqualsToken, - expr.value.visitExpression(this, context)); - return context.isStatement ? result : ts.createParen(result); + visitWriteVarExpr(expr: o.WriteVarExpr, context: Context): TExpression { + const assignment = this.factory.createAssignment( + this.setSourceMapRange(this.factory.createIdentifier(expr.name), expr.sourceSpan), + expr.value.visitExpression(this, context), + ); + return context.isStatement ? assignment : + this.factory.createParenthesizedExpression(assignment); } - visitWriteKeyExpr(expr: WriteKeyExpr, context: Context): ts.Expression { + visitWriteKeyExpr(expr: o.WriteKeyExpr, context: Context): TExpression { const exprContext = context.withExpressionMode; - const lhs = ts.createElementAccess( + const target = this.factory.createElementAccess( expr.receiver.visitExpression(this, exprContext), - expr.index.visitExpression(this, exprContext)); - const rhs = expr.value.visitExpression(this, exprContext); - const result: ts.Expression = ts.createBinary(lhs, ts.SyntaxKind.EqualsToken, rhs); - return context.isStatement ? result : ts.createParen(result); + expr.index.visitExpression(this, exprContext), + ); + const assignment = + this.factory.createAssignment(target, expr.value.visitExpression(this, exprContext)); + return context.isStatement ? assignment : + this.factory.createParenthesizedExpression(assignment); } - visitWritePropExpr(expr: WritePropExpr, context: Context): ts.BinaryExpression { - return ts.createBinary( - ts.createPropertyAccess(expr.receiver.visitExpression(this, context), expr.name), - ts.SyntaxKind.EqualsToken, expr.value.visitExpression(this, context)); + visitWritePropExpr(expr: o.WritePropExpr, context: Context): TExpression { + const target = + this.factory.createPropertyAccess(expr.receiver.visitExpression(this, context), expr.name); + return this.factory.createAssignment(target, expr.value.visitExpression(this, context)); } - visitInvokeMethodExpr(ast: InvokeMethodExpr, context: Context): ts.CallExpression { + visitInvokeMethodExpr(ast: o.InvokeMethodExpr, context: Context): TExpression { const target = ast.receiver.visitExpression(this, context); - const call = ts.createCall( - ast.name !== null ? ts.createPropertyAccess(target, ast.name) : target, undefined, - ast.args.map(arg => arg.visitExpression(this, context))); - this.setSourceMapRange(call, ast.sourceSpan); - return call; + return this.setSourceMapRange( + this.factory.createCallExpression( + ast.name !== null ? this.factory.createPropertyAccess(target, ast.name) : target, + ast.args.map(arg => arg.visitExpression(this, context)), + /* pure */ false), + ast.sourceSpan); } - visitInvokeFunctionExpr(ast: InvokeFunctionExpr, context: Context): ts.CallExpression { - const expr = ts.createCall( - ast.fn.visitExpression(this, context), undefined, + visitInvokeFunctionExpr(ast: o.InvokeFunctionExpr, context: Context): TExpression { + return this.setSourceMapRange( + this.factory.createCallExpression( + ast.fn.visitExpression(this, context), + ast.args.map(arg => arg.visitExpression(this, context)), ast.pure), + ast.sourceSpan); + } + + visitInstantiateExpr(ast: o.InstantiateExpr, context: Context): TExpression { + return this.factory.createNewExpression( + ast.classExpr.visitExpression(this, context), ast.args.map(arg => arg.visitExpression(this, context))); - if (ast.pure) { - ts.addSyntheticLeadingComment(expr, ts.SyntaxKind.MultiLineCommentTrivia, '@__PURE__', false); + } + + visitLiteralExpr(ast: o.LiteralExpr, _context: Context): TExpression { + return this.setSourceMapRange(this.factory.createLiteral(ast.value), ast.sourceSpan); + } + + visitLocalizedString(ast: o.LocalizedString, context: Context): TExpression { + // A `$localize` message consists of `messageParts` and `expressions`, which get interleaved + // together. The interleaved pieces look like: + // `[messagePart0, expression0, messagePart1, expression1, messagePart2]` + // + // Note that there is always a message part at the start and end, and so therefore + // `messageParts.length === expressions.length + 1`. + // + // Each message part may be prefixed with "metadata", which is wrapped in colons (:) delimiters. + // The metadata is attached to the first and subsequent message parts by calls to + // `serializeI18nHead()` and `serializeI18nTemplatePart()` respectively. + // + // The first message part (i.e. `ast.messageParts[0]`) is used to initialize `messageParts` + // array. + const elements: TemplateElement[] = [createTemplateElement(ast.serializeI18nHead())]; + const expressions: TExpression[] = []; + for (let i = 0; i < ast.expressions.length; i++) { + const placeholder = this.setSourceMapRange( + ast.expressions[i].visitExpression(this, context), ast.getPlaceholderSourceSpan(i)); + expressions.push(placeholder); + elements.push(createTemplateElement(ast.serializeI18nTemplatePart(i + 1))); } - this.setSourceMapRange(expr, ast.sourceSpan); - return expr; + + const localizeTag = this.factory.createIdentifier('$localize'); + + // Now choose which implementation to use to actually create the necessary AST nodes. + const localizeCall = this.downlevelLocalizedStrings ? + this.createES5TaggedTemplateFunctionCall(localizeTag, {elements, expressions}) : + this.factory.createTaggedTemplate(localizeTag, {elements, expressions}); + + return this.setSourceMapRange(localizeCall, ast.sourceSpan); } - visitInstantiateExpr(ast: InstantiateExpr, context: Context): ts.NewExpression { - return ts.createNew( - ast.classExpr.visitExpression(this, context), undefined, - ast.args.map(arg => arg.visitExpression(this, context))); - } + /** + * Translate the tagged template literal into a call that is compatible with ES5, using the + * imported `__makeTemplateObject` helper for ES5 formatted output. + */ + private createES5TaggedTemplateFunctionCall( + tagHandler: TExpression, {elements, expressions}: TemplateLiteral): TExpression { + // Ensure that the `__makeTemplateObject()` helper has been imported. + const {moduleImport, symbol} = + this.imports.generateNamedImport('tslib', '__makeTemplateObject'); + const __makeTemplateObjectHelper = (moduleImport === null) ? + this.factory.createIdentifier(symbol) : + this.factory.createPropertyAccess(moduleImport, symbol); - visitLiteralExpr(ast: LiteralExpr, context: Context): ts.Expression { - let expr: ts.Expression; - if (ast.value === undefined) { - expr = ts.createIdentifier('undefined'); - } else if (ast.value === null) { - expr = ts.createNull(); - } else { - expr = ts.createLiteral(ast.value); + // Collect up the cooked and raw strings into two separate arrays. + const cooked: TExpression[] = []; + const raw: TExpression[] = []; + for (const element of elements) { + cooked.push(this.factory.setSourceMapRange( + this.factory.createLiteral(element.cooked), element.range)); + raw.push( + this.factory.setSourceMapRange(this.factory.createLiteral(element.raw), element.range)); } - this.setSourceMapRange(expr, ast.sourceSpan); - return expr; + + // Generate the helper call in the form: `__makeTemplateObject([cooked], [raw]);` + const templateHelperCall = this.factory.createCallExpression( + __makeTemplateObjectHelper, + [this.factory.createArrayLiteral(cooked), this.factory.createArrayLiteral(raw)], + /* pure */ false); + + // Finally create the tagged handler call in the form: + // `tag(__makeTemplateObject([cooked], [raw]), ...expressions);` + return this.factory.createCallExpression( + tagHandler, [templateHelperCall, ...expressions], + /* pure */ false); } - visitLocalizedString(ast: LocalizedString, context: Context): ts.Expression { - const localizedString = this.scriptTarget >= ts.ScriptTarget.ES2015 ? - this.createLocalizedStringTaggedTemplate(ast, context) : - this.createLocalizedStringFunctionCall(ast, context); - this.setSourceMapRange(localizedString, ast.sourceSpan); - return localizedString; - } - - visitExternalExpr(ast: ExternalExpr, context: Context): ts.PropertyAccessExpression - |ts.Identifier { + visitExternalExpr(ast: o.ExternalExpr, _context: Context): TExpression { if (ast.value.name === null) { throw new Error(`Import unknown module or symbol ${ast.value}`); } @@ -224,19 +259,18 @@ class ExpressionTranslatorVisitor implements ExpressionVisitor, StatementVisitor this.imports.generateNamedImport(ast.value.moduleName, ast.value.name); if (moduleImport === null) { // The symbol was ambient after all. - return ts.createIdentifier(symbol); + return this.factory.createIdentifier(symbol); } else { - return ts.createPropertyAccess( - ts.createIdentifier(moduleImport), ts.createIdentifier(symbol)); + return this.factory.createPropertyAccess(moduleImport, symbol); } } else { // The symbol is ambient, so just reference it. - return ts.createIdentifier(ast.value.name); + return this.factory.createIdentifier(ast.value.name); } } - visitConditionalExpr(ast: ConditionalExpr, context: Context): ts.ConditionalExpression { - let cond: ts.Expression = ast.condition.visitExpression(this, context); + visitConditionalExpr(ast: o.ConditionalExpr, context: Context): TExpression { + let cond: TExpression = ast.condition.visitExpression(this, context); // Ordinarily the ternary operator is right-associative. The following are equivalent: // `a ? b : c ? d : e` => `a ? b : (c ? d : e)` @@ -258,259 +292,128 @@ class ExpressionTranslatorVisitor implements ExpressionVisitor, StatementVisitor // conditional expression is directly used as the condition of another. // // TODO(alxhub): investigate better logic for precendence of conditional operators - if (ast.condition instanceof ConditionalExpr) { + if (ast.condition instanceof o.ConditionalExpr) { // The condition of this ternary needs to be wrapped in parentheses to maintain // left-associativity. - cond = ts.createParen(cond); + cond = this.factory.createParenthesizedExpression(cond); } - return ts.createConditional( + return this.factory.createConditional( cond, ast.trueCase.visitExpression(this, context), ast.falseCase!.visitExpression(this, context)); } - visitNotExpr(ast: NotExpr, context: Context): ts.PrefixUnaryExpression { - return ts.createPrefix( - ts.SyntaxKind.ExclamationToken, ast.condition.visitExpression(this, context)); + visitNotExpr(ast: o.NotExpr, context: Context): TExpression { + return this.factory.createUnaryExpression('!', ast.condition.visitExpression(this, context)); } - visitAssertNotNullExpr(ast: AssertNotNull, context: Context): ts.NonNullExpression { + visitAssertNotNullExpr(ast: o.AssertNotNull, context: Context): TExpression { return ast.condition.visitExpression(this, context); } - visitCastExpr(ast: CastExpr, context: Context): ts.Expression { + visitCastExpr(ast: o.CastExpr, context: Context): TExpression { return ast.value.visitExpression(this, context); } - visitFunctionExpr(ast: FunctionExpr, context: Context): ts.FunctionExpression { - return ts.createFunctionExpression( - undefined, undefined, ast.name || undefined, undefined, - ast.params.map( - param => ts.createParameter( - undefined, undefined, undefined, param.name, undefined, undefined, undefined)), - undefined, ts.createBlock(ast.statements.map(stmt => stmt.visitStatement(this, context)))); + visitFunctionExpr(ast: o.FunctionExpr, context: Context): TExpression { + return this.factory.createFunctionExpression( + ast.name ?? null, ast.params.map(param => param.name), + this.factory.createBlock(this.visitStatements(ast.statements, context))); } - visitUnaryOperatorExpr(ast: UnaryOperatorExpr, context: Context): ts.Expression { - if (!UNARY_OPERATORS.has(ast.operator)) { - throw new Error(`Unknown unary operator: ${UnaryOperator[ast.operator]}`); - } - return ts.createPrefix( - UNARY_OPERATORS.get(ast.operator)!, ast.expr.visitExpression(this, context)); - } - - visitBinaryOperatorExpr(ast: BinaryOperatorExpr, context: Context): ts.Expression { + visitBinaryOperatorExpr(ast: o.BinaryOperatorExpr, context: Context): TExpression { if (!BINARY_OPERATORS.has(ast.operator)) { - throw new Error(`Unknown binary operator: ${BinaryOperator[ast.operator]}`); + throw new Error(`Unknown binary operator: ${o.BinaryOperator[ast.operator]}`); } - return ts.createBinary( - ast.lhs.visitExpression(this, context), BINARY_OPERATORS.get(ast.operator)!, - ast.rhs.visitExpression(this, context)); + return this.factory.createBinaryExpression( + ast.lhs.visitExpression(this, context), + BINARY_OPERATORS.get(ast.operator)!, + ast.rhs.visitExpression(this, context), + ); } - visitReadPropExpr(ast: ReadPropExpr, context: Context): ts.PropertyAccessExpression { - return ts.createPropertyAccess(ast.receiver.visitExpression(this, context), ast.name); + visitReadPropExpr(ast: o.ReadPropExpr, context: Context): TExpression { + return this.factory.createPropertyAccess(ast.receiver.visitExpression(this, context), ast.name); } - visitReadKeyExpr(ast: ReadKeyExpr, context: Context): ts.ElementAccessExpression { - return ts.createElementAccess( + visitReadKeyExpr(ast: o.ReadKeyExpr, context: Context): TExpression { + return this.factory.createElementAccess( ast.receiver.visitExpression(this, context), ast.index.visitExpression(this, context)); } - visitLiteralArrayExpr(ast: LiteralArrayExpr, context: Context): ts.ArrayLiteralExpression { - const expr = - ts.createArrayLiteral(ast.entries.map(expr => expr.visitExpression(this, context))); - this.setSourceMapRange(expr, ast.sourceSpan); - return expr; + visitLiteralArrayExpr(ast: o.LiteralArrayExpr, context: Context): TExpression { + return this.factory.createArrayLiteral(ast.entries.map( + expr => this.setSourceMapRange(expr.visitExpression(this, context), ast.sourceSpan))); } - visitLiteralMapExpr(ast: LiteralMapExpr, context: Context): ts.ObjectLiteralExpression { - const entries = ast.entries.map( - entry => ts.createPropertyAssignment( - entry.quoted ? ts.createLiteral(entry.key) : ts.createIdentifier(entry.key), - entry.value.visitExpression(this, context))); - const expr = ts.createObjectLiteral(entries); - this.setSourceMapRange(expr, ast.sourceSpan); - return expr; + visitLiteralMapExpr(ast: o.LiteralMapExpr, context: Context): TExpression { + const properties: ObjectLiteralProperty[] = ast.entries.map(entry => { + return { + propertyName: entry.key, + quoted: entry.quoted, + value: entry.value.visitExpression(this, context) + }; + }); + return this.setSourceMapRange(this.factory.createObjectLiteral(properties), ast.sourceSpan); } - visitCommaExpr(ast: CommaExpr, context: Context): never { + visitCommaExpr(ast: o.CommaExpr, context: Context): never { throw new Error('Method not implemented.'); } - visitWrappedNodeExpr(ast: WrappedNodeExpr, context: Context): any { - if (ts.isIdentifier(ast.node)) { - this.defaultImportRecorder.recordUsedIdentifier(ast.node); - } + visitWrappedNodeExpr(ast: o.WrappedNodeExpr, _context: Context): any { + this.recordWrappedNodeExpr(ast.node); return ast.node; } - visitTypeofExpr(ast: TypeofExpr, context: Context): ts.TypeOfExpression { - return ts.createTypeOf(ast.expr.visitExpression(this, context)); + visitTypeofExpr(ast: o.TypeofExpr, context: Context): TExpression { + return this.factory.createTypeOfExpression(ast.expr.visitExpression(this, context)); } - /** - * Translate the `LocalizedString` node into a `TaggedTemplateExpression` for ES2015 formatted - * output. - */ - private createLocalizedStringTaggedTemplate(ast: LocalizedString, context: Context): - ts.TaggedTemplateExpression { - let template: ts.TemplateLiteral; - const length = ast.messageParts.length; - const metaBlock = ast.serializeI18nHead(); - if (length === 1) { - template = ts.createNoSubstitutionTemplateLiteral(metaBlock.cooked, metaBlock.raw); - this.setSourceMapRange(template, ast.getMessagePartSourceSpan(0)); - } else { - // Create the head part - const head = ts.createTemplateHead(metaBlock.cooked, metaBlock.raw); - this.setSourceMapRange(head, ast.getMessagePartSourceSpan(0)); - const spans: ts.TemplateSpan[] = []; - // Create the middle parts - for (let i = 1; i < length - 1; i++) { - const resolvedExpression = ast.expressions[i - 1].visitExpression(this, context); - this.setSourceMapRange(resolvedExpression, ast.getPlaceholderSourceSpan(i - 1)); - const templatePart = ast.serializeI18nTemplatePart(i); - const templateMiddle = createTemplateMiddle(templatePart.cooked, templatePart.raw); - this.setSourceMapRange(templateMiddle, ast.getMessagePartSourceSpan(i)); - const templateSpan = ts.createTemplateSpan(resolvedExpression, templateMiddle); - spans.push(templateSpan); - } - // Create the tail part - const resolvedExpression = ast.expressions[length - 2].visitExpression(this, context); - this.setSourceMapRange(resolvedExpression, ast.getPlaceholderSourceSpan(length - 2)); - const templatePart = ast.serializeI18nTemplatePart(length - 1); - const templateTail = createTemplateTail(templatePart.cooked, templatePart.raw); - this.setSourceMapRange(templateTail, ast.getMessagePartSourceSpan(length - 1)); - spans.push(ts.createTemplateSpan(resolvedExpression, templateTail)); - // Put it all together - template = ts.createTemplateExpression(head, spans); + visitUnaryOperatorExpr(ast: o.UnaryOperatorExpr, context: Context): TExpression { + if (!UNARY_OPERATORS.has(ast.operator)) { + throw new Error(`Unknown unary operator: ${o.UnaryOperator[ast.operator]}`); } - const expression = ts.createTaggedTemplate(ts.createIdentifier('$localize'), template); - this.setSourceMapRange(expression, ast.sourceSpan); - return expression; + return this.factory.createUnaryExpression( + UNARY_OPERATORS.get(ast.operator)!, ast.expr.visitExpression(this, context)); } - /** - * Translate the `LocalizedString` node into a `$localize` call using the imported - * `__makeTemplateObject` helper for ES5 formatted output. - */ - private createLocalizedStringFunctionCall(ast: LocalizedString, context: Context) { - // A `$localize` message consists `messageParts` and `expressions`, which get interleaved - // together. The interleaved pieces look like: - // `[messagePart0, expression0, messagePart1, expression1, messagePart2]` - // - // Note that there is always a message part at the start and end, and so therefore - // `messageParts.length === expressions.length + 1`. - // - // Each message part may be prefixed with "metadata", which is wrapped in colons (:) delimiters. - // The metadata is attached to the first and subsequent message parts by calls to - // `serializeI18nHead()` and `serializeI18nTemplatePart()` respectively. - - // The first message part (i.e. `ast.messageParts[0]`) is used to initialize `messageParts` - // array. - const messageParts = [ast.serializeI18nHead()]; - const expressions: any[] = []; - - // The rest of the `ast.messageParts` and each of the expressions are `ast.expressions` pushed - // into the arrays. Note that `ast.messagePart[i]` corresponds to `expressions[i-1]` - for (let i = 1; i < ast.messageParts.length; i++) { - expressions.push(ast.expressions[i - 1].visitExpression(this, context)); - messageParts.push(ast.serializeI18nTemplatePart(i)); - } - - // The resulting downlevelled tagged template string uses a call to the `__makeTemplateObject()` - // helper, so we must ensure it has been imported. - const {moduleImport, symbol} = - this.imports.generateNamedImport('tslib', '__makeTemplateObject'); - const __makeTemplateObjectHelper = (moduleImport === null) ? - ts.createIdentifier(symbol) : - ts.createPropertyAccess(ts.createIdentifier(moduleImport), ts.createIdentifier(symbol)); - - // Generate the call in the form: - // `$localize(__makeTemplateObject(cookedMessageParts, rawMessageParts), ...expressions);` - const cookedLiterals = messageParts.map( - (messagePart, i) => - this.createLiteral(messagePart.cooked, ast.getMessagePartSourceSpan(i))); - const rawLiterals = messageParts.map( - (messagePart, i) => this.createLiteral(messagePart.raw, ast.getMessagePartSourceSpan(i))); - return ts.createCall( - /* expression */ ts.createIdentifier('$localize'), - /* typeArguments */ undefined, - /* argumentsArray */[ - ts.createCall( - /* expression */ __makeTemplateObjectHelper, - /* typeArguments */ undefined, - /* argumentsArray */ - [ - ts.createArrayLiteral(cookedLiterals), - ts.createArrayLiteral(rawLiterals), - ]), - ...expressions, - ]); + private visitStatements(statements: o.Statement[], context: Context): TStatement[] { + return statements.map(stmt => stmt.visitStatement(this, context)) + .filter(stmt => stmt !== undefined); } - - private setSourceMapRange(expr: ts.Node, sourceSpan: ParseSourceSpan|null) { - if (sourceSpan) { - const {start, end} = sourceSpan; - const {url, content} = start.file; - if (url) { - if (!this.externalSourceFiles.has(url)) { - this.externalSourceFiles.set(url, ts.createSourceMapSource(url, content, pos => pos)); - } - const source = this.externalSourceFiles.get(url); - ts.setSourceMapRange(expr, {pos: start.offset, end: end.offset, source}); - } - } + private setSourceMapRange(ast: T, span: o.ParseSourceSpan|null): + T { + return this.factory.setSourceMapRange(ast, createRange(span)); } - - private createLiteral(text: string, span: ParseSourceSpan|null) { - const literal = ts.createStringLiteral(text); - this.setSourceMapRange(literal, span); - return literal; - } -} - -// HACK: Use this in place of `ts.createTemplateMiddle()`. -// Revert once https://github.com/microsoft/TypeScript/issues/35374 is fixed -function createTemplateMiddle(cooked: string, raw: string): ts.TemplateMiddle { - const node: ts.TemplateLiteralLikeNode = ts.createTemplateHead(cooked, raw); - (node.kind as ts.SyntaxKind) = ts.SyntaxKind.TemplateMiddle; - return node as ts.TemplateMiddle; -} - -// HACK: Use this in place of `ts.createTemplateTail()`. -// Revert once https://github.com/microsoft/TypeScript/issues/35374 is fixed -function createTemplateTail(cooked: string, raw: string): ts.TemplateTail { - const node: ts.TemplateLiteralLikeNode = ts.createTemplateHead(cooked, raw); - (node.kind as ts.SyntaxKind) = ts.SyntaxKind.TemplateTail; - return node as ts.TemplateTail; } /** - * Attach the given `leadingComments` to the `statement` node. - * - * @param statement The statement that will have comments attached. - * @param leadingComments The comments to attach to the statement. + * Convert a cooked-raw string object into one that can be used by the AST factories. */ -export function attachComments( - statement: T, leadingComments?: LeadingComment[]): T { - if (leadingComments === undefined) { - return statement; - } - - for (const comment of leadingComments) { - const commentKind = comment.multiline ? ts.SyntaxKind.MultiLineCommentTrivia : - ts.SyntaxKind.SingleLineCommentTrivia; - if (comment.multiline) { - ts.addSyntheticLeadingComment( - statement, commentKind, comment.toString(), comment.trailingNewline); - } else { - for (const line of comment.text.split('\n')) { - ts.addSyntheticLeadingComment(statement, commentKind, line, comment.trailingNewline); - } - } - } - return statement; +function createTemplateElement( + {cooked, raw, range}: {cooked: string, raw: string, range: o.ParseSourceSpan|null}): + TemplateElement { + return {cooked, raw, range: createRange(range)}; +} + +/** + * Convert an OutputAST source-span into a range that can be used by the AST factories. + */ +function createRange(span: o.ParseSourceSpan|null): SourceMapRange|null { + if (span === null) { + return null; + } + const {start, end} = span; + const {url, content} = start.file; + if (!url) { + return null; + } + return { + url, + content, + start: {offset: start.offset, line: start.line, column: start.col}, + end: {offset: end.offset, line: end.line, column: end.col}, + }; } diff --git a/packages/compiler-cli/src/ngtsc/translator/src/typescript_ast_factory.ts b/packages/compiler-cli/src/ngtsc/translator/src/typescript_ast_factory.ts new file mode 100644 index 0000000000..06a86d7c56 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/translator/src/typescript_ast_factory.ts @@ -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 = { + '+': ts.SyntaxKind.PlusToken, + '-': ts.SyntaxKind.MinusToken, + '!': ts.SyntaxKind.ExclamationToken, +}; + +const BINARY_OPERATORS: Record = { + '&&': 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 = { + 'const': ts.NodeFlags.Const, + 'let': ts.NodeFlags.Let, + 'var': ts.NodeFlags.None, +}; + +/** + * A TypeScript flavoured implementation of the AstFactory. + */ +export class TypeScriptAstFactory implements AstFactory { + private externalSourceFiles = new Map(); + + 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 { + 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 { + 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(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( + 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; +} diff --git a/packages/compiler-cli/src/ngtsc/translator/src/typescript_translator.ts b/packages/compiler-cli/src/ngtsc/translator/src/typescript_translator.ts new file mode 100644 index 0000000000..5e71dd760f --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/translator/src/typescript_translator.ts @@ -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, + options: TranslatorOptions = {}): ts.Expression { + return expression.visitExpression( + new ExpressionTranslatorVisitor( + new TypeScriptAstFactory(), imports, options), + new Context(false)); +} + +export function translateStatement( + statement: o.Statement, imports: ImportGenerator, + options: TranslatorOptions = {}): ts.Statement { + return statement.visitStatement( + new ExpressionTranslatorVisitor( + new TypeScriptAstFactory(), imports, options), + new Context(true)); +} diff --git a/packages/compiler-cli/src/ngtsc/translator/test/BUILD.bazel b/packages/compiler-cli/src/ngtsc/translator/test/BUILD.bazel new file mode 100644 index 0000000000..ab5851162f --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/translator/test/BUILD.bazel @@ -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", + ], +) diff --git a/packages/compiler-cli/src/ngtsc/translator/test/typescript_ast_factory_spec.ts b/packages/compiler-cli/src/ngtsc/translator/test/typescript_ast_factory_spec.ts new file mode 100644 index 0000000000..b128a58432 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/translator/test/typescript_ast_factory_spec.ts @@ -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 { + 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 { + 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 { + items: TNode[]; + generate(node: ts.Node): string; +} diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/environment.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/environment.ts index 48b1bca849..d0c25504ba 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/environment.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/environment.ts @@ -9,7 +9,7 @@ import {ExpressionType, ExternalExpr, Type, WrappedNodeExpr} from '@angular/compiler'; import * as ts from 'typescript'; -import {ImportFlags, NOOP_DEFAULT_IMPORT_RECORDER, Reference, ReferenceEmitter} from '../../imports'; +import {ImportFlags, Reference, ReferenceEmitter} from '../../imports'; import {ClassDeclaration, ReflectionHost} from '../../reflection'; import {ImportManager, translateExpression, translateType} from '../../translator'; import {TypeCheckableDirectiveMeta, TypeCheckingConfig, TypeCtorMetadata} from '../api'; @@ -213,8 +213,7 @@ export class Environment { const ngExpr = this.refEmitter.emit(ref, this.contextFile, ImportFlags.NoAliasing); // Use `translateExpression` to convert the `Expression` into a `ts.Expression`. - return translateExpression( - ngExpr, this.importManager, NOOP_DEFAULT_IMPORT_RECORDER, ts.ScriptTarget.ES2015); + return translateExpression(ngExpr, this.importManager); } /**