From 18bf77204e580d35437d1b9da183f2e2c126ad18 Mon Sep 17 00:00:00 2001 From: Chuck Jazdzewski Date: Tue, 30 May 2017 11:43:13 -0600 Subject: [PATCH] feat(compiler): emit typescript nodes from an output ast (#16823) --- .../src/transformers/node_emitter.ts | 472 ++++++++++++++++++ .../test/transformers/node_emitter_spec.ts | 402 +++++++++++++++ packages/compiler/src/compiler.ts | 1 + 3 files changed, 875 insertions(+) create mode 100644 packages/compiler-cli/src/transformers/node_emitter.ts create mode 100644 packages/compiler-cli/test/transformers/node_emitter_spec.ts diff --git a/packages/compiler-cli/src/transformers/node_emitter.ts b/packages/compiler-cli/src/transformers/node_emitter.ts new file mode 100644 index 0000000000..6ab65ac2b8 --- /dev/null +++ b/packages/compiler-cli/src/transformers/node_emitter.ts @@ -0,0 +1,472 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {AssertNotNull, BinaryOperator, BinaryOperatorExpr, BuiltinMethod, BuiltinVar, CastExpr, ClassStmt, CommaExpr, CommentStmt, CompileIdentifierMetadata, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, ExpressionStatement, ExpressionVisitor, ExternalExpr, ExternalReference, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, NotExpr, ParseSourceSpan, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, Statement, StatementVisitor, StaticSymbol, StmtModifier, ThrowStmt, TryCatchStmt, WriteKeyExpr, WritePropExpr, WriteVarExpr} from '@angular/compiler'; +import * as ts from 'typescript'; + +export interface Node { sourceSpan: ParseSourceSpan|null; } + +const METHOD_THIS_NAME = 'this'; +const CATCH_ERROR_NAME = 'error'; +const CATCH_STACK_NAME = 'stack'; + +export class TypeScriptNodeEmitter { + updateSourceFile( + sourceFile: ts.SourceFile, srcFilePath: string, genFilePath: string, stmts: Statement[], + exportedVars: string[], preamble?: string): [ts.SourceFile, Map] { + const converter = new _NodeEmitterVisitor(); + const statements = + stmts.map(stmt => stmt.visitStatement(converter, null)).filter(stmt => stmt != null); + const newSourceFile = ts.updateSourceFileNode( + sourceFile, [...converter.getReexports(), ...converter.getImports(), ...statements]); + if (preamble) { + if (preamble.startsWith('/*') && preamble.endsWith('*/')) { + preamble = preamble.substr(2, preamble.length - 4); + } + if (!statements.length) { + statements.push(ts.createEmptyStatement()); + } + statements[0] = ts.setSyntheticLeadingComments( + statements[0], + [{kind: ts.SyntaxKind.MultiLineCommentTrivia, text: preamble, pos: -1, end: -1}]) + } + return [newSourceFile, converter.getNodeMap()]; + } +} + +// A recorded node is a subtype of the node that is marked as being recoreded. This is used +// to ensure that NodeEmitterVisitor.record has been called on all nodes returned by the +// NodeEmitterVisitor +type RecordedNode = (T & { __recorded: any; }) | null; + +function createLiteral(value: any) { + if (value === null) { + return ts.createNull(); + } else if (value === undefined) { + return ts.createIdentifier('undefined'); + } else { + return ts.createLiteral(value); + } +} + +/** + * Visits an output ast and produces the corresponding TypeScript synthetic nodes. + */ +class _NodeEmitterVisitor implements StatementVisitor, ExpressionVisitor { + private _nodeMap = new Map(); + private _importsWithPrefixes = new Map(); + private _reexports = new Map(); + + getReexports(): ts.Statement[] { + return Array.from(this._reexports.entries()) + .map( + ([exportedFilePath, reexports]) => ts.createExportDeclaration( + /* decorators */ undefined, + /* modifiers */ undefined, ts.createNamedExports(reexports.map( + ({name, as}) => ts.createExportSpecifier(name, as))), + /* moduleSpecifier */ createLiteral(exportedFilePath))); + } + + getImports(): ts.Statement[] { + return Array.from(this._importsWithPrefixes.entries()) + .map( + ([namespace, prefix]) => ts.createImportDeclaration( + /* decorators */ undefined, + /* modifiers */ undefined, + /* importClause */ ts.createImportClause( + /* name */(undefined as any), + ts.createNamespaceImport(ts.createIdentifier(prefix))), + /* moduleSpecifier */ createLiteral(namespace))); + } + + getNodeMap() { return this._nodeMap; } + + private record(ngNode: Node, tsNode: T|null): RecordedNode { + if (tsNode && !this._nodeMap.has(tsNode)) { + this._nodeMap.set(tsNode, ngNode); + ts.forEachChild(tsNode, child => this.record(ngNode, tsNode)); + } + return tsNode as RecordedNode; + } + + private getModifiers(stmt: Statement) { + let modifiers: ts.Modifier[] = []; + if (stmt.hasModifier(StmtModifier.Exported)) { + modifiers.push(ts.createToken(ts.SyntaxKind.ExportKeyword)); + } + if (stmt.hasModifier(StmtModifier.Final)) { + modifiers.push(ts.createToken(ts.SyntaxKind.ConstKeyword)); + } + return modifiers; + } + + // StatementVisitor + visitDeclareVarStmt(stmt: DeclareVarStmt) { + if (stmt.hasModifier(StmtModifier.Exported) && stmt.value instanceof ExternalExpr && + !stmt.type) { + // check for a reexport + const {name, moduleName} = stmt.value.value; + if (moduleName) { + let reexports = this._reexports.get(moduleName); + if (!reexports) { + reexports = []; + this._reexports.set(moduleName, reexports); + } + reexports.push({name: name !, as: stmt.name}); + return null; + } + } + + return this.record( + stmt, ts.createVariableStatement( + this.getModifiers(stmt), + ts.createVariableDeclarationList([ts.createVariableDeclaration( + ts.createIdentifier(stmt.name), + /* type */ undefined, + (stmt.value && stmt.value.visitExpression(this, null)) || undefined)]))); + } + + visitDeclareFunctionStmt(stmt: DeclareFunctionStmt, context: any) { + return this.record( + stmt, ts.createFunctionDeclaration( + /* decorators */ undefined, this.getModifiers(stmt), + /* astrictToken */ undefined, stmt.name, /* typeParameters */ undefined, + stmt.params.map( + p => ts.createParameter( + /* decorators */ undefined, /* modifiers */ undefined, + /* dotDotDotToken */ undefined, p.name)), + undefined, this._visitStatements(stmt.statements))); + } + + visitExpressionStmt(stmt: ExpressionStatement) { + return this.record(stmt, ts.createStatement(stmt.expr.visitExpression(this, null))); + } + + visitReturnStmt(stmt: ReturnStatement) { + return this.record( + stmt, ts.createReturn(stmt.value ? stmt.value.visitExpression(this, null) : undefined)); + } + + visitDeclareClassStmt(stmt: ClassStmt) { + const modifiers = this.getModifiers(stmt); + const fields = stmt.fields.map( + field => ts.createProperty( + /* decorators */ undefined, /* modifiers */ undefined, field.name, + /* questionToken */ undefined, + /* type */ undefined, ts.createNull())); + const getters = stmt.getters.map( + getter => ts.createGetAccessor( + /* decorators */ undefined, /* modifiers */ undefined, getter.name, /* parameters */[], + /* type */ undefined, this._visitStatements(getter.body))); + + const constructor = + (stmt.constructorMethod && [ts.createConstructor( + /* decorators */ undefined, + /* modifiers */ undefined, + /* parameters */ stmt.constructorMethod.params.map( + p => ts.createParameter( + /* decorators */ undefined, + /* modifiers */ undefined, + /* dotDotDotToken */ undefined, p.name)), + this._visitStatements(stmt.constructorMethod.body))]) || + []; + + // TODO {chuckj}: Determine what should be done for a method with a null name. + const methods = stmt.methods.filter(method => method.name) + .map( + method => ts.createMethodDeclaration( + /* decorators */ undefined, /* modifiers */ undefined, + /* astriskToken */ undefined, method.name !/* guarded by filter */, + /* questionToken */ undefined, /* typeParameters */ undefined, + method.params.map( + p => ts.createParameter( + /* decorators */ undefined, /* modifiers */ undefined, + /* dotDotDotToken */ undefined, p.name)), + undefined, this._visitStatements(method.body))); + return this.record( + stmt, ts.createClassDeclaration( + /* decorators */ undefined, modifiers, stmt.name, /* typeParameters*/ undefined, + stmt.parent && [ts.createHeritageClause( + ts.SyntaxKind.ExtendsKeyword, + [stmt.parent.visitExpression(this, null)])] || + [], + [...fields, ...getters, ...constructor, ...methods])); + } + + visitIfStmt(stmt: IfStmt) { + return this.record( + stmt, + ts.createIf( + stmt.condition.visitExpression(this, null), this._visitStatements(stmt.trueCase), + stmt.falseCase && stmt.falseCase.length && this._visitStatements(stmt.falseCase) || + undefined)); + } + + visitTryCatchStmt(stmt: TryCatchStmt): RecordedNode { + return this.record( + stmt, ts.createTry( + this._visitStatements(stmt.bodyStmts), + ts.createCatchClause( + CATCH_ERROR_NAME, this._visitStatementsPrefix( + [ts.createVariableStatement( + /* modifiers */ undefined, + [ts.createVariableDeclaration( + CATCH_STACK_NAME, /* type */ undefined, + ts.createPropertyAccess( + ts.createIdentifier(CATCH_ERROR_NAME), + ts.createIdentifier(CATCH_STACK_NAME)))])], + stmt.catchStmts)), + undefined)); + } + + visitThrowStmt(stmt: ThrowStmt) { + return this.record(stmt, ts.createThrow(stmt.error.visitExpression(this, null))); + } + + visitCommentStmt(stmt: CommentStmt) { return null; } + + // ExpressionVisitor + visitReadVarExpr(expr: ReadVarExpr) { + switch (expr.builtin) { + case BuiltinVar.This: + return this.record(expr, ts.createIdentifier(METHOD_THIS_NAME)); + case BuiltinVar.CatchError: + return this.record(expr, ts.createIdentifier(CATCH_ERROR_NAME)); + case BuiltinVar.CatchStack: + return this.record(expr, ts.createIdentifier(CATCH_STACK_NAME)); + case BuiltinVar.Super: + return this.record(expr, ts.createSuper()); + } + if (expr.name) { + return this.record(expr, ts.createIdentifier(expr.name)); + } + throw Error(`Unexpected ReadVarExpr form`); + } + + visitWriteVarExpr(expr: WriteVarExpr): RecordedNode { + return this.record( + expr, ts.createAssignment( + ts.createIdentifier(expr.name), expr.value.visitExpression(this, null))); + } + + visitWriteKeyExpr(expr: WriteKeyExpr): RecordedNode { + return this.record( + expr, + ts.createAssignment( + ts.createElementAccess( + expr.receiver.visitExpression(this, null), expr.index.visitExpression(this, null)), + expr.value.visitExpression(this, null))); + } + + visitWritePropExpr(expr: WritePropExpr): RecordedNode { + return this.record( + expr, ts.createAssignment( + ts.createPropertyAccess(expr.receiver.visitExpression(this, null), expr.name), + expr.value.visitExpression(this, null))); + } + + visitInvokeMethodExpr(expr: InvokeMethodExpr): RecordedNode { + const methodName = getMethodName(expr); + return this.record( + expr, + ts.createCall( + ts.createPropertyAccess(expr.receiver.visitExpression(this, null), methodName), + /* typeArguments */ undefined, expr.args.map(arg => arg.visitExpression(this, null)))); + } + + visitInvokeFunctionExpr(expr: InvokeFunctionExpr): RecordedNode { + return this.record( + expr, ts.createCall( + expr.fn.visitExpression(this, null), /* typeArguments */ undefined, + expr.args.map(arg => arg.visitExpression(this, null)))) + } + + visitInstantiateExpr(expr: InstantiateExpr): RecordedNode { + return this.record( + expr, ts.createNew( + expr.classExpr.visitExpression(this, null), /* typeArguments */ undefined, + expr.args.map(arg => arg.visitExpression(this, null)))); + } + + visitLiteralExpr(expr: LiteralExpr) { return this.record(expr, createLiteral(expr.value)); } + + visitExternalExpr(expr: ExternalExpr) { + return this.record(expr, this._visitIdentifier(expr.value)); + } + + visitConditionalExpr(expr: ConditionalExpr): RecordedNode { + // TODO {chuckj}: Review use of ! on flaseCase. Should it be non-nullable? + return this.record( + expr, + ts.createConditional( + expr.condition.visitExpression(this, null), expr.trueCase.visitExpression(this, null), + expr.falseCase !.visitExpression(this, null))); + ; + } + + visitNotExpr(expr: NotExpr): RecordedNode { + return this.record( + expr, ts.createPrefix( + ts.SyntaxKind.ExclamationToken, expr.condition.visitExpression(this, null))); + } + + visitAssertNotNullExpr(expr: AssertNotNull): RecordedNode { + return expr.condition.visitExpression(this, null); + } + + visitCastExpr(expr: CastExpr): RecordedNode { + return expr.value.visitExpression(this, null); + } + + visitFunctionExpr(expr: FunctionExpr) { + return this.record( + expr, ts.createFunctionExpression( + /* modifiers */ undefined, /* astriskToken */ undefined, /* name */ undefined, + /* typeParameters */ undefined, + expr.params.map( + p => ts.createParameter( + /* decorators */ undefined, /* modifiers */ undefined, + /* dotDotDotToken */ undefined, p.name)), + /* type */ undefined, this._visitStatements(expr.statements))); + } + + visitBinaryOperatorExpr(expr: BinaryOperatorExpr): RecordedNode { + let binaryOperator: ts.BinaryOperator; + switch (expr.operator) { + case BinaryOperator.And: + binaryOperator = ts.SyntaxKind.AmpersandAmpersandToken; + break; + case BinaryOperator.Bigger: + binaryOperator = ts.SyntaxKind.GreaterThanToken; + break; + case BinaryOperator.BiggerEquals: + binaryOperator = ts.SyntaxKind.GreaterThanEqualsToken; + break; + case BinaryOperator.Divide: + binaryOperator = ts.SyntaxKind.SlashToken; + break; + case BinaryOperator.Equals: + binaryOperator = ts.SyntaxKind.EqualsEqualsToken; + break; + case BinaryOperator.Identical: + binaryOperator = ts.SyntaxKind.EqualsEqualsEqualsToken; + break; + case BinaryOperator.Lower: + binaryOperator = ts.SyntaxKind.LessThanToken; + break; + case BinaryOperator.LowerEquals: + binaryOperator = ts.SyntaxKind.LessThanEqualsToken; + break; + case BinaryOperator.Minus: + binaryOperator = ts.SyntaxKind.MinusToken; + break; + case BinaryOperator.Modulo: + binaryOperator = ts.SyntaxKind.PercentToken; + break; + case BinaryOperator.Multiply: + binaryOperator = ts.SyntaxKind.AsteriskToken; + break; + case BinaryOperator.NotEquals: + binaryOperator = ts.SyntaxKind.ExclamationEqualsToken; + break; + case BinaryOperator.NotIdentical: + binaryOperator = ts.SyntaxKind.ExclamationEqualsEqualsToken; + break; + case BinaryOperator.Or: + binaryOperator = ts.SyntaxKind.BarBarToken; + break; + case BinaryOperator.Plus: + binaryOperator = ts.SyntaxKind.PlusToken; + break; + default: + throw new Error(`Unknown operator: ${expr.operator}`); + } + return this.record( + expr, ts.createBinary( + expr.lhs.visitExpression(this, null), binaryOperator, + expr.rhs.visitExpression(this, null))); + } + + visitReadPropExpr(expr: ReadPropExpr): RecordedNode { + return this.record( + expr, ts.createPropertyAccess(expr.receiver.visitExpression(this, null), expr.name)); + } + + visitReadKeyExpr(expr: ReadKeyExpr): RecordedNode { + return this.record( + expr, + ts.createElementAccess( + expr.receiver.visitExpression(this, null), expr.index.visitExpression(this, null))); + } + + visitLiteralArrayExpr(expr: LiteralArrayExpr): RecordedNode { + return this.record( + expr, ts.createArrayLiteral(expr.entries.map(entry => entry.visitExpression(this, null)))); + } + + visitLiteralMapExpr(expr: LiteralMapExpr): RecordedNode { + return this.record( + expr, ts.createObjectLiteral(expr.entries.map( + entry => ts.createPropertyAssignment( + entry.quoted ? ts.createLiteral(entry.key) : entry.key, + entry.value.visitExpression(this, null))))); + } + + visitCommaExpr(expr: CommaExpr): RecordedNode { + return this.record( + expr, expr.parts.map(e => e.visitExpression(this, null)) + .reduce( + (left, right) => + left ? ts.createBinary(left, ts.SyntaxKind.CommaToken, right) : right, + null)); + } + + private _visitStatements(statements: Statement[]): ts.Block { + return this._visitStatementsPrefix([], statements); + } + + private _visitStatementsPrefix(prefix: ts.Statement[], statements: Statement[]) { + return ts.createBlock([ + ...prefix, ...statements.map(stmt => stmt.visitStatement(this, null)).filter(f => f != null) + ]); + } + + private _visitIdentifier(value: ExternalReference): ts.Expression { + const {name, moduleName} = value; + let prefixIdent: ts.Identifier|null = null; + if (moduleName) { + let prefix = this._importsWithPrefixes.get(moduleName); + if (prefix == null) { + prefix = `i${this._importsWithPrefixes.size}`; + this._importsWithPrefixes.set(moduleName, prefix); + } + prefixIdent = ts.createIdentifier(prefix); + } + // name can only be null during JIT which never executes this code. + let result: ts.Expression = + prefixIdent ? ts.createPropertyAccess(prefixIdent, name !) : ts.createIdentifier(name !); + return result; + } +} + + +function getMethodName(methodRef: {name: string | null; builtin: BuiltinMethod | null}): string { + if (methodRef.name) { + return methodRef.name; + } else { + switch (methodRef.builtin) { + case BuiltinMethod.Bind: + return 'bind'; + case BuiltinMethod.ConcatArray: + return 'concat'; + case BuiltinMethod.SubscribeObservable: + return 'subscribe'; + } + } + throw new Error('Unexpected method reference form'); +} diff --git a/packages/compiler-cli/test/transformers/node_emitter_spec.ts b/packages/compiler-cli/test/transformers/node_emitter_spec.ts new file mode 100644 index 0000000000..9dbfde4d06 --- /dev/null +++ b/packages/compiler-cli/test/transformers/node_emitter_spec.ts @@ -0,0 +1,402 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import * as o from '@angular/compiler/src/output/output_ast'; +import * as ts from 'typescript'; + +import {TypeScriptNodeEmitter} from '../../src/transformers/node_emitter'; +import {Directory, MockAotContext, MockCompilerHost} from '../mocks'; + +const someGenFilePath = '/somePackage/someGenFile'; +const someGenFileName = someGenFilePath + '.ts'; +const someSourceFilePath = '/somePackage/someSourceFile'; +const anotherModuleUrl = '/somePackage/someOtherPath'; + +const sameModuleIdentifier = new o.ExternalReference(null, 'someLocalId', null); + +const externalModuleIdentifier = new o.ExternalReference(anotherModuleUrl, 'someExternalId', null); + +describe('TypeScriptEmitter', () => { + let context: MockAotContext; + let host: MockCompilerHost; + let emitter: TypeScriptNodeEmitter; + let someVar: o.ReadVarExpr; + + beforeEach(() => { + context = new MockAotContext('/', FILES); + host = new MockCompilerHost(context); + emitter = new TypeScriptNodeEmitter(); + someVar = o.variable('someVar', null, null); + }); + + function emitStmt(stmt: o.Statement | o.Statement[], preamble?: string): string { + const stmts = Array.isArray(stmt) ? stmt : [stmt]; + + const program = ts.createProgram( + [someGenFileName], {module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2017}, host); + const moduleSourceFile = program.getSourceFile(someGenFileName); + const transformers: ts.CustomTransformers = { + before: [context => { + return sourceFile => { + const [newSourceFile] = emitter.updateSourceFile( + sourceFile, someGenFileName, someGenFilePath, stmts, [], preamble); + return newSourceFile; + }; + }] + }; + let result: string = ''; + const emitResult = program.emit( + moduleSourceFile, (fileName, data, writeByteOrderMark, onError, sourceFiles) => { + if (fileName.startsWith(someGenFilePath)) { + result = data; + } + }, undefined, undefined, transformers); + return normalizeResult(result); + } + + it('should declare variables', () => { + expect(emitStmt(someVar.set(o.literal(1)).toDeclStmt())).toEqual(`var someVar = 1;`); + expect(emitStmt(someVar.set(o.literal(1)).toDeclStmt(null, [o.StmtModifier.Final]))) + .toEqual(`var someVar = 1;`); + expect(emitStmt(someVar.set(o.literal(1)).toDeclStmt(null, [o.StmtModifier.Exported]))) + .toEqual(`exports.someVar = 1;`); + }); + + describe('declare variables with ExternExpressions as values', () => { + it('should create no reexport if the identifier is in the same module', () => { + // identifier is in the same module -> no reexport + expect(emitStmt(someVar.set(o.importExpr(sameModuleIdentifier)).toDeclStmt(null, [ + o.StmtModifier.Exported + ]))).toEqual('exports.someVar = someLocalId;'); + }); + + it('should create no reexport if the variable is not exported', () => { + expect(emitStmt(someVar.set(o.importExpr(externalModuleIdentifier)).toDeclStmt())) + .toEqual( + `const i0 = require("/somePackage/someOtherPath"); var someVar = i0.someExternalId;`); + }); + + it('should create no reexport if the variable is typed', () => { + expect(emitStmt(someVar.set(o.importExpr(externalModuleIdentifier)) + .toDeclStmt(o.DYNAMIC_TYPE, [o.StmtModifier.Exported]))) + .toEqual( + `const i0 = require("/somePackage/someOtherPath"); exports.someVar = i0.someExternalId;`); + }); + + it('should create a reexport', () => { + expect(emitStmt(someVar.set(o.importExpr(externalModuleIdentifier)) + .toDeclStmt(null, [o.StmtModifier.Exported]))) + .toEqual( + `var someOtherPath_1 = require("/somePackage/someOtherPath"); exports.someVar = someOtherPath_1.someExternalId;`); + }); + + it('should create multiple reexports from the same file', () => { + const someVar2 = o.variable('someVar2'); + const externalModuleIdentifier2 = + new o.ExternalReference(anotherModuleUrl, 'someExternalId2', null); + expect(emitStmt([ + someVar.set(o.importExpr(externalModuleIdentifier)) + .toDeclStmt(null, [o.StmtModifier.Exported]), + someVar2.set(o.importExpr(externalModuleIdentifier2)) + .toDeclStmt(null, [o.StmtModifier.Exported]) + ])) + .toEqual( + `var someOtherPath_1 = require("/somePackage/someOtherPath"); exports.someVar = someOtherPath_1.someExternalId; exports.someVar2 = someOtherPath_1.someExternalId2;`); + }); + }); + + it('should read and write variables', () => { + expect(emitStmt(someVar.toStmt())).toEqual(`someVar;`); + expect(emitStmt(someVar.set(o.literal(1)).toStmt())).toEqual(`someVar = 1;`); + expect(emitStmt(someVar.set(o.variable('someOtherVar').set(o.literal(1))).toStmt())) + .toEqual(`someVar = someOtherVar = 1;`); + }); + + it('should read and write keys', () => { + expect(emitStmt(o.variable('someMap').key(o.variable('someKey')).toStmt())) + .toEqual(`someMap[someKey];`); + expect(emitStmt(o.variable('someMap').key(o.variable('someKey')).set(o.literal(1)).toStmt())) + .toEqual(`someMap[someKey] = 1;`); + }); + + it('should read and write properties', () => { + expect(emitStmt(o.variable('someObj').prop('someProp').toStmt())).toEqual(`someObj.someProp;`); + expect(emitStmt(o.variable('someObj').prop('someProp').set(o.literal(1)).toStmt())) + .toEqual(`someObj.someProp = 1;`); + }); + + it('should invoke functions and methods and constructors', () => { + expect(emitStmt(o.variable('someFn').callFn([o.literal(1)]).toStmt())).toEqual('someFn(1);'); + expect(emitStmt(o.variable('someObj').callMethod('someMethod', [o.literal(1)]).toStmt())) + .toEqual('someObj.someMethod(1);'); + expect(emitStmt(o.variable('SomeClass').instantiate([o.literal(1)]).toStmt())) + .toEqual('new SomeClass(1);'); + }); + + it('should invoke functions and methods and constructors', () => { + expect(emitStmt(o.variable('someFn').callFn([o.literal(1)]).toStmt())).toEqual('someFn(1);'); + expect(emitStmt(o.variable('someObj').callMethod('someMethod', [o.literal(1)]).toStmt())) + .toEqual('someObj.someMethod(1);'); + expect(emitStmt(o.variable('SomeClass').instantiate([o.literal(1)]).toStmt())) + .toEqual('new SomeClass(1);'); + }); + + it('should support builtin methods', () => { + expect(emitStmt(o.variable('arr1') + .callMethod(o.BuiltinMethod.ConcatArray, [o.variable('arr2')]) + .toStmt())) + .toEqual('arr1.concat(arr2);'); + + expect(emitStmt(o.variable('observable') + .callMethod(o.BuiltinMethod.SubscribeObservable, [o.variable('listener')]) + .toStmt())) + .toEqual('observable.subscribe(listener);'); + + expect(emitStmt( + o.variable('fn').callMethod(o.BuiltinMethod.Bind, [o.variable('someObj')]).toStmt())) + .toEqual('fn.bind(someObj);'); + }); + + it('should support literals', () => { + expect(emitStmt(o.literal(0).toStmt())).toEqual('0;'); + expect(emitStmt(o.literal(true).toStmt())).toEqual('true;'); + expect(emitStmt(o.literal('someStr').toStmt())).toEqual(`"someStr";`); + expect(emitStmt(o.literalArr([o.literal(1)]).toStmt())).toEqual(`[1];`); + expect(emitStmt(o.literalMap([['someKey', o.literal(1)]]).toStmt())) + .toEqual(`({ someKey: 1 });`); + }); + + it('should apply quotes to each entry within a map produced with literalMap when true', () => { + expect(emitStmt( + o.literalMap([['a', o.literal('a')], ['*', o.literal('star')]], null, true).toStmt()) + .replace(/\s+/gm, '')) + .toEqual(`({"a":"a","*":"star"});`); + }); + + it('should support blank literals', () => { + expect(emitStmt(o.literal(null).toStmt())).toEqual('null;'); + expect(emitStmt(o.literal(undefined).toStmt())).toEqual('undefined;'); + expect(emitStmt(o.variable('a', null).isBlank().toStmt())).toEqual('a == null;'); + }); + + it('should support external identifiers', () => { + expect(emitStmt(o.importExpr(sameModuleIdentifier).toStmt())).toEqual('someLocalId;'); + expect(emitStmt(o.importExpr(externalModuleIdentifier).toStmt())) + .toEqual(`const i0 = require("/somePackage/someOtherPath"); i0.someExternalId;`); + }); + + it('should support operators', () => { + const lhs = o.variable('lhs'); + const rhs = o.variable('rhs'); + expect(emitStmt(someVar.cast(o.INT_TYPE).toStmt())).toEqual('someVar;'); + expect(emitStmt(o.not(someVar).toStmt())).toEqual('!someVar;'); + expect(emitStmt(o.assertNotNull(someVar).toStmt())).toEqual('someVar;'); + expect(emitStmt(someVar.conditional(o.variable('trueCase'), o.variable('falseCase')).toStmt())) + .toEqual('someVar ? trueCase : falseCase;'); + + expect(emitStmt(lhs.equals(rhs).toStmt())).toEqual('lhs == rhs;'); + expect(emitStmt(lhs.notEquals(rhs).toStmt())).toEqual('lhs != rhs;'); + expect(emitStmt(lhs.identical(rhs).toStmt())).toEqual('lhs === rhs;'); + expect(emitStmt(lhs.notIdentical(rhs).toStmt())).toEqual('lhs !== rhs;'); + expect(emitStmt(lhs.minus(rhs).toStmt())).toEqual('lhs - rhs;'); + expect(emitStmt(lhs.plus(rhs).toStmt())).toEqual('lhs + rhs;'); + expect(emitStmt(lhs.divide(rhs).toStmt())).toEqual('lhs / rhs;'); + expect(emitStmt(lhs.multiply(rhs).toStmt())).toEqual('lhs * rhs;'); + expect(emitStmt(lhs.modulo(rhs).toStmt())).toEqual('lhs % rhs;'); + expect(emitStmt(lhs.and(rhs).toStmt())).toEqual('lhs && rhs;'); + expect(emitStmt(lhs.or(rhs).toStmt())).toEqual('lhs || rhs;'); + expect(emitStmt(lhs.lower(rhs).toStmt())).toEqual('lhs < rhs;'); + expect(emitStmt(lhs.lowerEquals(rhs).toStmt())).toEqual('lhs <= rhs;'); + expect(emitStmt(lhs.bigger(rhs).toStmt())).toEqual('lhs > rhs;'); + expect(emitStmt(lhs.biggerEquals(rhs).toStmt())).toEqual('lhs >= rhs;'); + }); + + it('should support function expressions', () => { + expect(emitStmt(o.fn([], []).toStmt())).toEqual(`(function () { });`); + expect(emitStmt(o.fn([], [new o.ReturnStatement(o.literal(1))], o.INT_TYPE).toStmt())) + .toEqual(`(function () { return 1; });`); + expect(emitStmt(o.fn([new o.FnParam('param1', o.INT_TYPE)], []).toStmt())) + .toEqual(`(function (param1) { });`); + }); + + it('should support function statements', () => { + expect(emitStmt(new o.DeclareFunctionStmt('someFn', [], []))).toEqual('function someFn() { }'); + expect(emitStmt(new o.DeclareFunctionStmt('someFn', [], [], null, [o.StmtModifier.Exported]))) + .toEqual(`function someFn() { } exports.someFn = someFn;`); + expect(emitStmt(new o.DeclareFunctionStmt( + 'someFn', [], [new o.ReturnStatement(o.literal(1))], o.INT_TYPE))) + .toEqual(`function someFn() { return 1; }`); + expect(emitStmt(new o.DeclareFunctionStmt('someFn', [new o.FnParam('param1', o.INT_TYPE)], [ + ]))).toEqual(`function someFn(param1) { }`); + }); + + it('should support comments', () => { expect(emitStmt(new o.CommentStmt('a\nb'))).toEqual(''); }); + + it('should support if stmt', () => { + const trueCase = o.variable('trueCase').callFn([]).toStmt(); + const falseCase = o.variable('falseCase').callFn([]).toStmt(); + expect(emitStmt(new o.IfStmt(o.variable('cond'), [trueCase]))) + .toEqual('if (cond) { trueCase(); }'); + expect(emitStmt(new o.IfStmt(o.variable('cond'), [trueCase], [falseCase]))) + .toEqual('if (cond) { trueCase(); } else { falseCase(); }'); + }); + + it('should support try/catch', () => { + const bodyStmt = o.variable('body').callFn([]).toStmt(); + const catchStmt = o.variable('catchFn').callFn([o.CATCH_ERROR_VAR, o.CATCH_STACK_VAR]).toStmt(); + expect(emitStmt(new o.TryCatchStmt([bodyStmt], [catchStmt]))) + .toEqual( + `try { body(); } catch (error) { var stack = error.stack; catchFn(error, stack); }`); + }); + + it('should support support throwing', + () => { expect(emitStmt(new o.ThrowStmt(someVar))).toEqual('throw someVar;'); }); + + describe('classes', () => { + let callSomeMethod: o.Statement; + + beforeEach(() => { callSomeMethod = o.THIS_EXPR.callMethod('someMethod', []).toStmt(); }); + + + it('should support declaring classes', () => { + expect(emitStmt(new o.ClassStmt('SomeClass', null !, [], [], null !, [ + ]))).toEqual('class SomeClass { }'); + expect(emitStmt(new o.ClassStmt('SomeClass', null !, [], [], null !, [], [ + o.StmtModifier.Exported + ]))).toEqual('class SomeClass { } exports.SomeClass = SomeClass;'); + expect(emitStmt(new o.ClassStmt('SomeClass', o.variable('SomeSuperClass'), [], [], null !, [ + ]))).toEqual('class SomeClass extends SomeSuperClass { }'); + }); + + it('should support declaring constructors', () => { + const superCall = o.SUPER_EXPR.callFn([o.variable('someParam')]).toStmt(); + expect(emitStmt(new o.ClassStmt( + 'SomeClass', null !, [], [], new o.ClassMethod(null !, [], []), []))) + .toEqual(`class SomeClass { constructor() { } }`); + expect(emitStmt(new o.ClassStmt( + 'SomeClass', null !, [], [], + new o.ClassMethod(null !, [new o.FnParam('someParam', o.INT_TYPE)], []), []))) + .toEqual(`class SomeClass { constructor(someParam) { } }`); + expect(emitStmt(new o.ClassStmt( + 'SomeClass', null !, [], [], new o.ClassMethod(null !, [], [superCall]), []))) + .toEqual(`class SomeClass { constructor() { super(someParam); } }`); + expect(emitStmt(new o.ClassStmt( + 'SomeClass', null !, [], [], new o.ClassMethod(null !, [], [callSomeMethod]), []))) + .toEqual(`class SomeClass { constructor() { this.someMethod(); } }`); + }); + + it('should support declaring fields', () => { + expect(emitStmt(new o.ClassStmt( + 'SomeClass', null !, [new o.ClassField('someField')], [], null !, []))) + .toEqual(`class SomeClass { constructor() { this.someField = null; } }`); + expect(emitStmt(new o.ClassStmt( + 'SomeClass', null !, [new o.ClassField('someField', o.INT_TYPE)], [], null !, []))) + .toEqual(`class SomeClass { constructor() { this.someField = null; } }`); + expect(emitStmt(new o.ClassStmt( + 'SomeClass', null !, + [new o.ClassField('someField', o.INT_TYPE, [o.StmtModifier.Private])], [], null !, + []))) + .toEqual(`class SomeClass { constructor() { this.someField = null; } }`); + }); + + it('should support declaring getters', () => { + expect(emitStmt(new o.ClassStmt( + 'SomeClass', null !, [], [new o.ClassGetter('someGetter', [])], null !, []))) + .toEqual(`class SomeClass { get someGetter() { } }`); + expect(emitStmt(new o.ClassStmt( + 'SomeClass', null !, [], [new o.ClassGetter('someGetter', [], o.INT_TYPE)], null !, + []))) + .toEqual(`class SomeClass { get someGetter() { } }`); + expect(emitStmt(new o.ClassStmt( + 'SomeClass', null !, [], [new o.ClassGetter('someGetter', [callSomeMethod])], + null !, []))) + .toEqual(`class SomeClass { get someGetter() { this.someMethod(); } }`); + expect( + emitStmt(new o.ClassStmt( + 'SomeClass', null !, [], + [new o.ClassGetter('someGetter', [], null !, [o.StmtModifier.Private])], null !, []))) + .toEqual(`class SomeClass { get someGetter() { } }`); + }); + + it('should support methods', () => { + expect(emitStmt(new o.ClassStmt('SomeClass', null !, [], [], null !, [ + new o.ClassMethod('someMethod', [], []) + ]))).toEqual(`class SomeClass { someMethod() { } }`); + expect(emitStmt(new o.ClassStmt('SomeClass', null !, [], [], null !, [ + new o.ClassMethod('someMethod', [], [], o.INT_TYPE) + ]))).toEqual(`class SomeClass { someMethod() { } }`); + expect(emitStmt(new o.ClassStmt('SomeClass', null !, [], [], null !, [ + new o.ClassMethod('someMethod', [new o.FnParam('someParam', o.INT_TYPE)], []) + ]))).toEqual(`class SomeClass { someMethod(someParam) { } }`); + expect(emitStmt(new o.ClassStmt('SomeClass', null !, [], [], null !, [ + new o.ClassMethod('someMethod', [], [callSomeMethod]) + ]))).toEqual(`class SomeClass { someMethod() { this.someMethod(); } }`); + }); + }); + + it('should support builtin types', () => { + const writeVarExpr = o.variable('a').set(o.NULL_EXPR); + expect(emitStmt(writeVarExpr.toDeclStmt(o.DYNAMIC_TYPE))).toEqual('var a = null;'); + expect(emitStmt(writeVarExpr.toDeclStmt(o.BOOL_TYPE))).toEqual('var a = null;'); + expect(emitStmt(writeVarExpr.toDeclStmt(o.INT_TYPE))).toEqual('var a = null;'); + expect(emitStmt(writeVarExpr.toDeclStmt(o.NUMBER_TYPE))).toEqual('var a = null;'); + expect(emitStmt(writeVarExpr.toDeclStmt(o.STRING_TYPE))).toEqual('var a = null;'); + expect(emitStmt(writeVarExpr.toDeclStmt(o.FUNCTION_TYPE))).toEqual('var a = null;'); + }); + + it('should support external types', () => { + const writeVarExpr = o.variable('a').set(o.NULL_EXPR); + expect(emitStmt(writeVarExpr.toDeclStmt(o.importType(sameModuleIdentifier)))) + .toEqual('var a = null;'); + expect(emitStmt(writeVarExpr.toDeclStmt(o.importType(externalModuleIdentifier)))) + .toEqual(`var a = null;`); + }); + + it('should support expression types', () => { + expect(emitStmt(o.variable('a').set(o.NULL_EXPR).toDeclStmt(o.expressionType(o.variable('b'))))) + .toEqual('var a = null;'); + }); + + it('should support expressions with type parameters', () => { + expect(emitStmt(o.variable('a') + .set(o.NULL_EXPR) + .toDeclStmt(o.importType(externalModuleIdentifier, [o.STRING_TYPE])))) + .toEqual(`var a = null;`); + }); + + it('should support combined types', () => { + const writeVarExpr = o.variable('a').set(o.NULL_EXPR); + expect(emitStmt(writeVarExpr.toDeclStmt(new o.ArrayType(null !)))).toEqual('var a = null;'); + expect(emitStmt(writeVarExpr.toDeclStmt(new o.ArrayType(o.INT_TYPE)))).toEqual('var a = null;'); + + expect(emitStmt(writeVarExpr.toDeclStmt(new o.MapType(null)))).toEqual('var a = null;'); + expect(emitStmt(writeVarExpr.toDeclStmt(new o.MapType(o.INT_TYPE)))).toEqual('var a = null;'); + }); + + it('should support a preamble', () => { + expect(emitStmt(o.variable('a').toStmt(), '/* SomePreamble */')).toBe('/* SomePreamble */ a;'); + }); +}); + +const FILES: Directory = { + somePackage: {'someGenFile.ts': `export var a: number;`} +}; + +function normalizeResult(result: string): string { + // Remove TypeScript prefixes + // Remove new lines + // Squish adjacent spaces + // Remove prefix and postfix spaces + return result.replace('"use strict";', ' ') + .replace('exports.__esModule = true;', ' ') + .replace('Object.defineProperty(exports, "__esModule", { value: true });', ' ') + .replace(/\n/g, ' ') + .replace(/ +/g, ' ') + .replace(/^ /g, '') + .replace(/ $/g, ''); +} diff --git a/packages/compiler/src/compiler.ts b/packages/compiler/src/compiler.ts index af7c9e79bf..d126a872fc 100644 --- a/packages/compiler/src/compiler.ts +++ b/packages/compiler/src/compiler.ts @@ -60,6 +60,7 @@ export * from './ml_parser/html_tags'; export * from './ml_parser/interpolation_config'; export * from './ml_parser/tags'; export {NgModuleCompiler} from './ng_module_compiler'; +export {AssertNotNull, BinaryOperator, BinaryOperatorExpr, BuiltinMethod, BuiltinVar, CastExpr, ClassStmt, CommaExpr, CommentStmt, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, ExpressionStatement, ExpressionVisitor, ExternalExpr, ExternalReference, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, NotExpr, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, StatementVisitor, ThrowStmt, TryCatchStmt, WriteKeyExpr, WritePropExpr, WriteVarExpr, StmtModifier, Statement} from './output/output_ast'; export * from './output/ts_emitter'; export * from './parse_util'; export * from './schema/dom_element_schema_registry';