ts2dart
Version:
Transpile TypeScript code to Dart
868 lines (792 loc) • 31.9 kB
text/typescript
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),
};
}