UNPKG

ts2dart

Version:

Transpile TypeScript code to Dart

868 lines (792 loc) 31.9 kB
import * as path from 'path'; import * as ts from 'typescript'; import * as base from './base'; import {Transpiler} from './main'; type CallHandler = (c: ts.CallExpression, context: ts.Expression) => void; type PropertyHandler = (c: ts.PropertyAccessExpression) => void; type Set = { [s: string]: boolean }; const FACADE_DEBUG = false; const DEFAULT_LIB_MARKER = '__ts2dart_default_lib'; const PROVIDER_IMPORT_MARKER = '__ts2dart_has_provider_import'; const TS2DART_PROVIDER_COMMENT = '@ts2dart_Provider'; function merge(...args: {[key: string]: any}[]): {[key: string]: any} { let returnObject: {[key: string]: any} = {}; for (let arg of args) { for (let key of Object.getOwnPropertyNames(arg)) { returnObject[key] = arg[key]; } } return returnObject; } export class FacadeConverter extends base.TranspilerBase { private tc: ts.TypeChecker; private defaultLibLocation: string; private candidateProperties: {[propertyName: string]: boolean} = {}; private candidateTypes: {[typeName: string]: boolean} = {}; private genericMethodDeclDepth = 0; constructor(transpiler: Transpiler) { super(transpiler); this.extractPropertyNames(this.callHandlers, this.candidateProperties); this.extractPropertyNames(this.propertyHandlers, this.candidateProperties); this.extractPropertyNames(this.tsToDartTypeNames, this.candidateTypes); } initializeTypeBasedConversion( tc: ts.TypeChecker, opts: ts.CompilerOptions, host: ts.CompilerHost) { this.tc = tc; this.defaultLibLocation = ts.getDefaultLibFilePath(opts).replace(/\.d\.ts$/, ''); this.resolveModuleNames(opts, host, this.callHandlers); this.resolveModuleNames(opts, host, this.propertyHandlers); this.resolveModuleNames(opts, host, this.tsToDartTypeNames); this.resolveModuleNames(opts, host, this.callHandlerReplaceNew); } private extractPropertyNames(m: ts.Map<ts.Map<any>>, candidates: {[k: string]: boolean}) { for (let fileName of Object.keys(m)) { const file = m[fileName]; Object.keys(file) .map((propName) => propName.substring(propName.lastIndexOf('.') + 1)) .forEach((propName) => candidates[propName] = true); } } private resolveModuleNames( opts: ts.CompilerOptions, host: ts.CompilerHost, m: ts.Map<ts.Map<any>>) { for (let mn of Object.keys(m)) { let actual: string; let absolute: string; if (mn === DEFAULT_LIB_MARKER) { actual = this.defaultLibLocation; } else { let resolved = ts.resolveModuleName(mn, '', opts, host); if (!resolved.resolvedModule) continue; actual = resolved.resolvedModule.resolvedFileName.replace(/(\.d)?\.ts$/, ''); // TypeScript's resolution returns relative paths here, but uses absolute ones in // SourceFile.fileName later. Make sure to hit both use cases. absolute = path.resolve(actual); } if (FACADE_DEBUG) console.log('Resolved module', mn, '->', actual); m[actual] = m[mn]; if (absolute) m[absolute] = m[mn]; } } /** * To avoid strongly referencing the Provider class (which could bloat binary size), Angular 2 * write providers as object literals. However the Dart transformers don't recognize this, so * ts2dart translates the special syntax `/* @ts2dart_Provider * / {provide: Class, param1: ...}` * into `const Provider(Class, param1: ...)`. */ maybeHandleProvider(ole: ts.ObjectLiteralExpression): boolean { if (!this.hasMarkerComment(ole, TS2DART_PROVIDER_COMMENT)) return false; let classParam: ts.Expression; let remaining = ole.properties.filter((e) => { if (e.kind !== ts.SyntaxKind.PropertyAssignment) { this.reportError(e, TS2DART_PROVIDER_COMMENT + ' elements must be property assignments'); } if ('provide' === base.ident(e.name)) { classParam = (e as ts.PropertyAssignment).initializer; return false; } return true; // include below. }); if (!classParam) { this.reportError(ole, 'missing provide: element'); return false; } this.emit('const Provider('); this.visit(classParam); if (remaining.length > 0) { this.emit(','); for (let i = 0; i < remaining.length; i++) { let e = remaining[i]; if (e.kind !== ts.SyntaxKind.PropertyAssignment) this.visit(e.name); this.emit(base.ident(e.name)); this.emit(':'); this.visit((e as ts.PropertyAssignment).initializer); if ((i + 1) < remaining.length) this.emit(','); } this.emit(')'); } return true; } maybeHandleCall(c: ts.CallExpression): boolean { if (!this.tc) return false; let {context, symbol} = this.getCallInformation(c); if (!symbol) { // getCallInformation returns a symbol if we understand this call. return false; } let handler = this.getHandler(c, symbol, this.callHandlers); return handler && !handler(c, context); } handlePropertyAccess(pa: ts.PropertyAccessExpression): boolean { if (!this.tc) return; let ident = pa.name.text; if (!this.candidateProperties.hasOwnProperty(ident)) return false; let symbol = this.tc.getSymbolAtLocation(pa.name); if (!symbol) { this.reportMissingType(pa, ident); return false; } let handler = this.getHandler(pa, symbol, this.propertyHandlers); return handler && !handler(pa); } /** * Searches for type references that require extra imports and emits the imports as necessary. */ emitExtraImports(sourceFile: ts.SourceFile) { let libraries = <ts.Map<string>>{ 'XMLHttpRequest': 'dart:html', 'KeyboardEvent': 'dart:html', 'Uint8Array': 'dart:typed_arrays', 'ArrayBuffer': 'dart:typed_arrays', 'Promise': 'dart:async', }; let emitted: Set = {}; this.emitImports(sourceFile, libraries, emitted, sourceFile); } private emitImports( n: ts.Node, libraries: ts.Map<string>, emitted: Set, sourceFile: ts.SourceFile): void { if (n.kind === ts.SyntaxKind.TypeReference) { let type = base.ident((<ts.TypeReferenceNode>n).typeName); if (libraries.hasOwnProperty(type)) { let toEmit = libraries[type]; if (!emitted[toEmit]) { this.emit(`import "${toEmit}";`); emitted[toEmit] = true; } } } // Support for importing "Provider" in case /* @ts2dart_Provider */ comments are present. if (n.kind === ts.SyntaxKind.ImportDeclaration) { // See if there is already code importing 'Provider' from angular2/core. let id = n as ts.ImportDeclaration; if ((id.moduleSpecifier as ts.StringLiteral).text === 'angular2/core') { if (id.importClause.namedBindings.kind === ts.SyntaxKind.NamedImports) { let ni = id.importClause.namedBindings as ts.NamedImports; for (let nb of ni.elements) { if (base.ident(nb.name) === 'Provider') { emitted[PROVIDER_IMPORT_MARKER] = true; break; } } } } } if (!emitted[PROVIDER_IMPORT_MARKER] && this.hasMarkerComment(n, TS2DART_PROVIDER_COMMENT)) { // if 'Provider' has not been imported yet, and there's a @ts2dart_Provider, add it. this.emit(`import "package:angular2/core.dart" show Provider;`); emitted[PROVIDER_IMPORT_MARKER] = true; } n.getChildren(sourceFile) .forEach((child: ts.Node) => this.emitImports(child, libraries, emitted, sourceFile)); } pushTypeParameterNames(n: ts.FunctionLikeDeclaration) { if (!n.typeParameters) return; this.genericMethodDeclDepth++; } popTypeParameterNames(n: ts.FunctionLikeDeclaration) { if (!n.typeParameters) return; this.genericMethodDeclDepth--; } resolvePropertyTypes(tn: ts.TypeNode): ts.Map<ts.PropertyDeclaration> { let res: ts.Map<ts.PropertyDeclaration> = {}; if (!tn || !this.tc) return res; let t = this.tc.getTypeAtLocation(tn); for (let sym of this.tc.getPropertiesOfType(t)) { let decl = sym.valueDeclaration || (sym.declarations && sym.declarations[0]); if (decl.kind !== ts.SyntaxKind.PropertyDeclaration && decl.kind !== ts.SyntaxKind.PropertySignature) { let msg = this.tc.getFullyQualifiedName(sym) + ' used for named parameter definition must be a property'; this.reportError(decl, msg); continue; } res[sym.name] = <ts.PropertyDeclaration>decl; } return res; } /** * The Dart Development Compiler (DDC) has a syntax extension that uses comments to emulate * generic methods in Dart. ts2dart has to hack around this and keep track of which type names * in the current scope are actually DDC type parameters and need to be emitted in comments. * * TODO(martinprobst): Remove this once the DDC hack has made it into Dart proper. */ private isGenericMethodTypeParameterName(name: ts.EntityName): boolean { // Avoid checking this unless needed. if (this.genericMethodDeclDepth === 0 || !this.tc) return false; // Check if the type of the name is a TypeParameter. let t = this.tc.getTypeAtLocation(name); if (!t || (t.flags & ts.TypeFlags.TypeParameter) === 0) return false; // Check if the symbol we're looking at is the type parameter. let symbol = this.tc.getSymbolAtLocation(name); if (symbol !== t.symbol) return false; // Check that the Type Parameter has been declared by a function declaration. return symbol.declarations.some( // Constructors are handled separately. d => d.parent.kind === ts.SyntaxKind.FunctionDeclaration || d.parent.kind === ts.SyntaxKind.MethodDeclaration || d.parent.kind === ts.SyntaxKind.MethodSignature); } visitTypeName(typeName: ts.EntityName) { if (typeName.kind !== ts.SyntaxKind.Identifier) { this.visit(typeName); return; } let ident = base.ident(typeName); if (this.isGenericMethodTypeParameterName(typeName)) { // DDC generic methods hack - all names that are type parameters to generic methods have to be // emitted in comments. this.emit('dynamic/*='); this.emit(ident); this.emit('*/'); return; } if (this.candidateTypes.hasOwnProperty(ident) && this.tc) { let symbol = this.tc.getSymbolAtLocation(typeName); if (!symbol) { this.reportMissingType(typeName, ident); return; } let fileAndName = this.getFileAndName(typeName, symbol); if (fileAndName) { let fileSubs = this.tsToDartTypeNames[fileAndName.fileName]; if (fileSubs && fileSubs.hasOwnProperty(fileAndName.qname)) { this.emit(fileSubs[fileAndName.qname]); return; } } } this.emit(ident); } shouldEmitNew(c: ts.CallExpression): boolean { if (!this.tc) return true; let ci = this.getCallInformation(c); let symbol = ci.symbol; // getCallInformation returns a symbol if we understand this call. if (!symbol) return true; let loc = this.getFileAndName(c, symbol); if (!loc) return true; let {fileName, qname} = loc; let fileSubs = this.callHandlerReplaceNew[fileName]; if (!fileSubs) return true; return !fileSubs[qname]; } private getCallInformation(c: ts.CallExpression): {context?: ts.Expression, symbol?: ts.Symbol} { let symbol: ts.Symbol; let context: ts.Expression; let ident: string; let expr = c.expression; if (expr.kind === ts.SyntaxKind.Identifier) { // Function call. ident = base.ident(expr); if (!this.candidateProperties.hasOwnProperty(ident)) return {}; symbol = this.tc.getSymbolAtLocation(expr); if (!symbol) { this.reportMissingType(c, ident); return {}; } context = null; } else if (expr.kind === ts.SyntaxKind.PropertyAccessExpression) { // Method call. let pa = <ts.PropertyAccessExpression>expr; ident = base.ident(pa.name); if (!this.candidateProperties.hasOwnProperty(ident)) return {}; symbol = this.tc.getSymbolAtLocation(pa); // Error will be reported by PropertyAccess handling below. if (!symbol) return {}; context = pa.expression; } return {context, symbol}; } private getHandler<T>(n: ts.Node, symbol: ts.Symbol, m: ts.Map<ts.Map<T>>): T { let loc = this.getFileAndName(n, symbol); if (!loc) return null; let {fileName, qname} = loc; let fileSubs = m[fileName]; if (!fileSubs) return null; return fileSubs[qname]; } private getFileAndName(n: ts.Node, originalSymbol: ts.Symbol): {fileName: string, qname: string} { let symbol = originalSymbol; while (symbol.flags & ts.SymbolFlags.Alias) symbol = this.tc.getAliasedSymbol(symbol); let decl = symbol.valueDeclaration; if (!decl) { // In the case of a pure declaration with no assignment, there is no value declared. // Just grab the first declaration, hoping it is declared once. if (!symbol.declarations || symbol.declarations.length === 0) { this.reportError(n, 'no declarations for symbol ' + originalSymbol.name); return null; } decl = symbol.declarations[0]; } const canonicalFileName = decl.getSourceFile().fileName.replace(/(\.d)?\.ts$/, ''); let qname = this.tc.getFullyQualifiedName(symbol); // Some Qualified Names include their file name. Might be a bug in TypeScript, // for the time being just special case. if (symbol.flags & (ts.SymbolFlags.Class | ts.SymbolFlags.Function | ts.SymbolFlags.Variable)) { qname = symbol.getName(); } if (FACADE_DEBUG) console.error('cfn:', canonicalFileName, 'qn:', qname); return {fileName: canonicalFileName, qname}; } private isNamedDefaultLibType(node: ts.Node, qname: string): boolean { let symbol = this.tc.getTypeAtLocation(node).getSymbol(); if (!symbol) return false; let actual = this.getFileAndName(node, symbol); return actual.fileName === this.defaultLibLocation && qname === actual.qname; } private reportMissingType(n: ts.Node, ident: string) { this.reportError( n, `Untyped property access to "${ident}" which could be ` + `a special ts2dart builtin. ` + `Please add type declarations to disambiguate.`); } private static DECLARATIONS: {[k: number]: boolean} = { [ts.SyntaxKind.ClassDeclaration]: true, [ts.SyntaxKind.FunctionDeclaration]: true, [ts.SyntaxKind.InterfaceDeclaration]: true, [ts.SyntaxKind.MethodDeclaration]: true, [ts.SyntaxKind.PropertyDeclaration]: true, [ts.SyntaxKind.PropertyDeclaration]: true, [ts.SyntaxKind.VariableDeclaration]: true, }; isInsideConstExpr(node: ts.Node): boolean { while (node.parent) { if (node.parent.kind === ts.SyntaxKind.Parameter && (node.parent as ts.ParameterDeclaration).initializer === node) { // initializers of parameters must be const in Dart. return true; } if (this.isConstExpr(node)) return true; node = node.parent; if (FacadeConverter.DECLARATIONS[node.kind]) { // Stop walking upwards when hitting a declaration - @ts2dart_const should only propagate // to the immediate declaration it applies to (but should be transitive in expressions). return false; } } return false; } isConstClass(decl: base.ClassLike) { return this.hasConstComment(decl) || this.hasAnnotation(decl.decorators, 'CONST') || (<ts.NodeArray<ts.Declaration>>decl.members).some((m) => { if (m.kind !== ts.SyntaxKind.Constructor) return false; return this.hasAnnotation(m.decorators, 'CONST'); }); } /** * isConstExpr returns true if the passed in expression itself is a const expression. const * expressions are marked by the special comment @ts2dart_const (expr), or by the special * function call CONST_EXPR. */ isConstExpr(node: ts.Node): boolean { if (!node) return false; if (this.hasConstComment(node)) { return true; } return node.kind === ts.SyntaxKind.CallExpression && base.ident((<ts.CallExpression>node).expression) === 'CONST_EXPR'; } hasConstComment(node: ts.Node): boolean { return this.hasMarkerComment(node, '@ts2dart_const'); } private hasMarkerComment(node: ts.Node, markerText: string): boolean { let text = node.getFullText(); let comments = ts.getLeadingCommentRanges(text, 0); if (!comments) return false; for (let c of comments) { let commentText = text.substring(c.pos, c.end); if (commentText.indexOf(markerText) !== -1) { return true; } } return false; } private emitMethodCall(name: string, args?: ts.Expression[]) { this.emit('.'); this.emitCall(name, args); } private emitCall(name: string, args?: ts.Expression[]) { this.emit(name); this.emit('('); if (args) this.visitList(args); this.emit(')'); } private stdlibTypeReplacements: ts.Map<string> = { 'Date': 'DateTime', 'Array': 'List', 'XMLHttpRequest': 'HttpRequest', 'Uint8Array': 'Uint8List', 'ArrayBuffer': 'ByteBuffer', 'Promise': 'Future', // Dart has two different incompatible DOM APIs // https://github.com/angular/angular/issues/2770 'Node': 'dynamic', 'Text': 'dynamic', 'Element': 'dynamic', 'Event': 'dynamic', 'HTMLElement': 'dynamic', 'HTMLAnchorElement': 'dynamic', 'HTMLStyleElement': 'dynamic', 'HTMLInputElement': 'dynamic', 'HTMLDocument': 'dynamic', 'History': 'dynamic', 'Location': 'dynamic', }; private tsToDartTypeNames: ts.Map<ts.Map<string>> = { [DEFAULT_LIB_MARKER]: this.stdlibTypeReplacements, 'angular2/src/facade/lang': {'Date': 'DateTime'}, 'rxjs/Observable': {'Observable': 'Stream'}, 'es6-promise/es6-promise': {'Promise': 'Future'}, 'es6-shim/es6-shim': {'Promise': 'Future'}, }; private es6Promises: ts.Map<CallHandler> = { 'Promise.catch': (c: ts.CallExpression, context: ts.Expression) => { this.visit(context); this.emit('.catchError('); this.visitList(c.arguments); this.emit(')'); }, 'Promise.then': (c: ts.CallExpression, context: ts.Expression) => { // then() in Dart doesn't support 2 arguments. this.visit(context); this.emit('.then('); this.visit(c.arguments[0]); this.emit(')'); if (c.arguments.length > 1) { this.emit('.catchError('); this.visit(c.arguments[1]); this.emit(')'); } }, 'Promise': (c: ts.CallExpression, context: ts.Expression) => { if (c.kind !== ts.SyntaxKind.NewExpression) return true; this.assert(c, c.arguments.length === 1, 'Promise construction must take 2 arguments.'); this.assert( c, c.arguments[0].kind === ts.SyntaxKind.ArrowFunction || c.arguments[0].kind === ts.SyntaxKind.FunctionExpression, 'Promise argument must be a function expression (or arrow function).'); let callback: ts.FunctionLikeDeclaration; if (c.arguments[0].kind === ts.SyntaxKind.ArrowFunction) { callback = <ts.FunctionLikeDeclaration>(<ts.ArrowFunction>c.arguments[0]); } else if (c.arguments[0].kind === ts.SyntaxKind.FunctionExpression) { callback = <ts.FunctionLikeDeclaration>(<ts.FunctionExpression>c.arguments[0]); } this.assert( c, callback.parameters.length > 0 && callback.parameters.length < 3, 'Promise executor must take 1 or 2 arguments (resolve and reject).'); const completerVarName = this.uniqueId('completer'); this.assert( c, callback.parameters[0].name.kind === ts.SyntaxKind.Identifier, 'First argument of the Promise executor is not a straight parameter.'); let resolveParameterIdent = <ts.Identifier>(callback.parameters[0].name); this.emit('(() {'); // Create a new scope. this.emit(`Completer ${completerVarName} = new Completer();`); this.emit('var'); this.emit(resolveParameterIdent.text); this.emit(`= ${completerVarName}.complete;`); if (callback.parameters.length === 2) { this.assert( c, callback.parameters[1].name.kind === ts.SyntaxKind.Identifier, 'First argument of the Promise executor is not a straight parameter.'); let rejectParameterIdent = <ts.Identifier>(callback.parameters[1].name); this.emit('var'); this.emit(rejectParameterIdent.text); this.emit(`= ${completerVarName}.completeError;`); } this.emit('(()'); this.visit(callback.body); this.emit(')();'); this.emit(`return ${completerVarName}.future;`); this.emit('})()'); }, }; private es6Collections: ts.Map<CallHandler> = { 'Map.set': (c: ts.CallExpression, context: ts.Expression) => { this.visit(context); this.emit('['); this.visit(c.arguments[0]); this.emit(']'); this.emit('='); this.visit(c.arguments[1]); }, 'Map.get': (c: ts.CallExpression, context: ts.Expression) => { this.visit(context); this.emit('['); this.visit(c.arguments[0]); this.emit(']'); }, 'Map.has': (c: ts.CallExpression, context: ts.Expression) => { this.visit(context); this.emitMethodCall('containsKey', c.arguments); }, 'Map.delete': (c: ts.CallExpression, context: ts.Expression) => { // JS Map.delete(k) returns whether k was present in the map, // convert to: // (Map.containsKey(k) && (Map.remove(k) !== null || true)) // (Map.remove(k) !== null || true) is required to always returns true // when Map.containsKey(k) this.emit('('); this.visit(context); this.emitMethodCall('containsKey', c.arguments); this.emit('&& ('); this.visit(context); this.emitMethodCall('remove', c.arguments); this.emit('!= null || true ) )'); }, 'Map.forEach': (c: ts.CallExpression, context: ts.Expression) => { let cb: any; let params: any; switch (c.arguments[0].kind) { case ts.SyntaxKind.FunctionExpression: cb = <ts.FunctionExpression>(c.arguments[0]); params = cb.parameters; if (params.length !== 2) { this.reportError(c, 'Map.forEach callback requires exactly two arguments'); return; } this.visit(context); this.emit('. forEach ( ('); this.visit(params[1]); this.emit(','); this.visit(params[0]); this.emit(')'); this.visit(cb.body); this.emit(')'); break; case ts.SyntaxKind.ArrowFunction: cb = <ts.ArrowFunction>(c.arguments[0]); params = cb.parameters; if (params.length !== 2) { this.reportError(c, 'Map.forEach callback requires exactly two arguments'); return; } this.visit(context); this.emit('. forEach ( ('); this.visit(params[1]); this.emit(','); this.visit(params[0]); this.emit(')'); if (cb.body.kind !== ts.SyntaxKind.Block) { this.emit('=>'); } this.visit(cb.body); this.emit(')'); break; default: this.visit(context); this.emit('. forEach ( ( k , v ) => ('); this.visit(c.arguments[0]); this.emit(') ( v , k ) )'); break; } }, 'Array.find': (c: ts.CallExpression, context: ts.Expression) => { this.visit(context); this.emit('. firstWhere ('); this.visit(c.arguments[0]); this.emit(', orElse : ( ) => null )'); }, }; private stdlibHandlers: ts.Map<CallHandler> = merge(this.es6Promises, this.es6Collections, { 'Array.push': (c: ts.CallExpression, context: ts.Expression) => { this.visit(context); this.emitMethodCall('add', c.arguments); }, 'Array.pop': (c: ts.CallExpression, context: ts.Expression) => { this.visit(context); this.emitMethodCall('removeLast'); }, 'Array.shift': (c: ts.CallExpression, context: ts.Expression) => { this.visit(context); this.emit('. removeAt ( 0 )'); }, 'Array.unshift': (c: ts.CallExpression, context: ts.Expression) => { this.emit('('); this.visit(context); if (c.arguments.length === 1) { this.emit('.. insert ( 0,'); this.visit(c.arguments[0]); this.emit(') ) . length'); } else { this.emit('.. insertAll ( 0, ['); this.visitList(c.arguments); this.emit(']) ) . length'); } }, 'Array.map': (c: ts.CallExpression, context: ts.Expression) => { this.visit(context); this.emitMethodCall('map', c.arguments); this.emitMethodCall('toList'); }, 'Array.filter': (c: ts.CallExpression, context: ts.Expression) => { this.visit(context); this.emitMethodCall('where', c.arguments); this.emitMethodCall('toList'); }, 'Array.some': (c: ts.CallExpression, context: ts.Expression) => { this.visit(context); this.emitMethodCall('any', c.arguments); }, 'Array.slice': (c: ts.CallExpression, context: ts.Expression) => { this.emitCall('ListWrapper.slice', [context, ...c.arguments]); }, 'Array.splice': (c: ts.CallExpression, context: ts.Expression) => { this.emitCall('ListWrapper.splice', [context, ...c.arguments]); }, 'Array.concat': (c: ts.CallExpression, context: ts.Expression) => { this.emit('( new List . from ('); this.visit(context); this.emit(')'); c.arguments.forEach(arg => { if (!this.isNamedDefaultLibType(arg, 'Array')) { this.reportError(arg, 'Array.concat only takes Array arguments'); } this.emit('.. addAll ('); this.visit(arg); this.emit(')'); }); this.emit(')'); }, 'Array.join': (c: ts.CallExpression, context: ts.Expression) => { this.visit(context); if (c.arguments.length) { this.emitMethodCall('join', c.arguments); } else { this.emit('. join ( "," )'); } }, 'Array.reduce': (c: ts.CallExpression, context: ts.Expression) => { this.visit(context); if (c.arguments.length >= 2) { this.emitMethodCall('fold', [c.arguments[1], c.arguments[0]]); } else { this.emit('. fold ( null ,'); this.visit(c.arguments[0]); this.emit(')'); } }, 'ArrayConstructor.isArray': (c: ts.CallExpression, context: ts.Expression) => { this.emit('( ('); this.visitList(c.arguments); // Should only be 1. this.emit(')'); this.emit('is List'); this.emit(')'); }, 'Console.log': (c: ts.CallExpression, context: ts.Expression) => { this.emit('print('); if (c.arguments.length === 1) { this.visit(c.arguments[0]); } else { this.emit('['); this.visitList(c.arguments); this.emit('].join(" ")'); } this.emit(')'); }, 'RegExp.exec': (c: ts.CallExpression, context: ts.Expression) => { if (context.kind !== ts.SyntaxKind.RegularExpressionLiteral) { // Fail if the exec call isn't made directly on a regexp literal. // Multiple exec calls on the same global regexp have side effects // (each return the next match), which we can't reproduce with a simple // Dart RegExp (users should switch to some facade / wrapper instead). this.reportError( c, 'exec is only supported on regexp literals, ' + 'to avoid side-effect of multiple calls on global regexps.'); } if (c.parent.kind === ts.SyntaxKind.ElementAccessExpression) { // The result of the exec call is used for immediate indexed access: // this use-case can be accommodated by RegExp.firstMatch, which returns // a Match instance with operator[] which returns groups (special index // 0 returns the full text of the match). this.visit(context); this.emitMethodCall('firstMatch', c.arguments); } else { // In the general case, we want to return a List. To transform a Match // into a List of its groups, we alias it in a local closure that we // call with the Match value. We are then able to use the group method // to generate a List large enough to hold groupCount groups + the // full text of the match at special group index 0. this.emit('((match) => new List.generate(1 + match.groupCount, match.group))('); this.visit(context); this.emitMethodCall('firstMatch', c.arguments); this.emit(')'); } }, 'RegExp.test': (c: ts.CallExpression, context: ts.Expression) => { this.visit(context); this.emitMethodCall('hasMatch', c.arguments); }, 'String.substr': (c: ts.CallExpression, context: ts.Expression) => { this.reportError( c, 'substr is unsupported, use substring (but beware of the different semantics!)'); this.visit(context); this.emitMethodCall('substr', c.arguments); }, }); private callHandlerReplaceNew: ts.Map<ts.Map<boolean>> = { [DEFAULT_LIB_MARKER]: {'Promise': true}, }; private callHandlers: ts.Map<ts.Map<CallHandler>> = { [DEFAULT_LIB_MARKER]: this.stdlibHandlers, 'angular2/manual_typings/globals': this.es6Collections, 'angular2/src/facade/collection': { 'Map': (c: ts.CallExpression, context: ts.Expression): boolean => { // The actual Map constructor is special cased for const calls. if (!this.isInsideConstExpr(c)) return true; if (c.arguments.length) { this.reportError(c, 'Arguments on a Map constructor in a const are unsupported'); } if (c.typeArguments) { this.emit('<'); this.visitList(c.typeArguments); this.emit('>'); } this.emit('{ }'); return false; }, }, 'angular2/src/core/di/forward_ref': { 'forwardRef': (c: ts.CallExpression, context: ts.Expression) => { // The special function forwardRef translates to an unwrapped value in Dart. const callback = <ts.FunctionExpression>c.arguments[0]; if (callback.kind !== ts.SyntaxKind.ArrowFunction) { this.reportError(c, 'forwardRef takes only arrow functions'); return; } this.visit(callback.body); }, }, 'angular2/src/facade/lang': { 'CONST_EXPR': (c: ts.CallExpression, context: ts.Expression) => { // `const` keyword is emitted in the array literal handling, as it needs to be transitive. this.visitList(c.arguments); }, 'normalizeBlank': (c: ts.CallExpression, context: ts.Expression) => { // normalizeBlank is a noop in Dart, so erase it. this.visitList(c.arguments); }, }, }; private es6CollectionsProp: ts.Map<PropertyHandler> = { 'Map.size': (p: ts.PropertyAccessExpression) => { this.visit(p.expression); this.emit('.'); this.emit('length'); }, }; private es6PromisesProp: ts.Map<PropertyHandler> = { 'PromiseConstructor.resolve': (p: ts.PropertyAccessExpression) => { this.emit('new '); this.visit(p.expression); this.emit('.value'); }, 'PromiseConstructor.reject': (p: ts.PropertyAccessExpression) => { this.emit('new '); this.visit(p.expression); this.emit('.error'); }, }; private propertyHandlers: ts.Map<ts.Map<PropertyHandler>> = { [DEFAULT_LIB_MARKER]: merge(this.es6CollectionsProp, this.es6PromisesProp), }; }