ts2dart
Version:
Transpile TypeScript code to Dart
516 lines (492 loc) • 19.8 kB
text/typescript
import * as ts from 'typescript';
import * as base from './base';
import {FacadeConverter} from './facade_converter';
import {Transpiler} from './main';
export default class DeclarationTranspiler extends base.TranspilerBase {
constructor(
tr: Transpiler, private fc: FacadeConverter, private enforceUnderscoreConventions: boolean) {
super(tr);
}
visitNode(node: ts.Node): boolean {
switch (node.kind) {
case ts.SyntaxKind.VariableDeclarationList:
// Note: VariableDeclarationList can only occur as part of a for loop.
let varDeclList = <ts.VariableDeclarationList>node;
this.visitList(varDeclList.declarations);
break;
case ts.SyntaxKind.VariableDeclaration:
let varDecl = <ts.VariableDeclaration>node;
this.visitVariableDeclarationType(varDecl);
this.visit(varDecl.name);
if (varDecl.initializer) {
this.emit('=');
this.visit(varDecl.initializer);
}
break;
case ts.SyntaxKind.ClassDeclaration:
let classDecl = <ts.ClassDeclaration>node;
if (classDecl.modifiers && (classDecl.modifiers.flags & ts.NodeFlags.Abstract)) {
this.visitClassLike('abstract class', classDecl);
} else {
this.visitClassLike('class', classDecl);
}
break;
case ts.SyntaxKind.InterfaceDeclaration:
let ifDecl = <ts.InterfaceDeclaration>node;
// Function type interface in an interface with a single declaration
// of a call signature (http://goo.gl/ROC5jN).
if (ifDecl.members.length === 1 && ifDecl.members[0].kind === ts.SyntaxKind.CallSignature) {
let member = <ts.CallSignatureDeclaration>ifDecl.members[0];
this.visitFunctionTypedefInterface(ifDecl.name.text, member, ifDecl.typeParameters);
} else {
this.visitClassLike('abstract class', ifDecl);
}
break;
case ts.SyntaxKind.HeritageClause:
let heritageClause = <ts.HeritageClause>node;
if (heritageClause.token === ts.SyntaxKind.ExtendsKeyword &&
heritageClause.parent.kind !== ts.SyntaxKind.InterfaceDeclaration) {
this.emit('extends');
} else {
this.emit('implements');
}
// Can only have one member for extends clauses.
this.visitList(heritageClause.types);
break;
case ts.SyntaxKind.ExpressionWithTypeArguments:
let exprWithTypeArgs = <ts.ExpressionWithTypeArguments>node;
this.visit(exprWithTypeArgs.expression);
this.maybeVisitTypeArguments(exprWithTypeArgs);
break;
case ts.SyntaxKind.EnumDeclaration:
let decl = <ts.EnumDeclaration>node;
// The only legal modifier for an enum decl is const.
let isConst = decl.modifiers && (decl.modifiers.flags & ts.NodeFlags.Const);
if (isConst) {
this.reportError(node, 'const enums are not supported');
}
this.emit('enum');
this.fc.visitTypeName(decl.name);
this.emit('{');
// Enums can be empty in TS ...
if (decl.members.length === 0) {
// ... but not in Dart.
this.reportError(node, 'empty enums are not supported');
}
this.visitList(decl.members);
this.emit('}');
break;
case ts.SyntaxKind.EnumMember:
let member = <ts.EnumMember>node;
this.visit(member.name);
if (member.initializer) {
this.reportError(node, 'enum initializers are not supported');
}
break;
case ts.SyntaxKind.Constructor:
let ctorDecl = <ts.ConstructorDeclaration>node;
// Find containing class name.
let className: ts.Identifier;
for (let parent = ctorDecl.parent; parent; parent = parent.parent) {
if (parent.kind === ts.SyntaxKind.ClassDeclaration) {
className = (<ts.ClassDeclaration>parent).name;
break;
}
}
if (!className) this.reportError(ctorDecl, 'cannot find outer class node');
this.visitDeclarationMetadata(ctorDecl);
if (this.fc.isConstClass(<base.ClassLike>ctorDecl.parent)) {
this.emit('const');
}
this.visit(className);
this.visitParameters(ctorDecl.parameters);
this.visit(ctorDecl.body);
break;
case ts.SyntaxKind.PropertyDeclaration:
this.visitProperty(<ts.PropertyDeclaration>node);
break;
case ts.SyntaxKind.SemicolonClassElement:
// No-op, don't emit useless declarations.
break;
case ts.SyntaxKind.MethodDeclaration:
this.visitDeclarationMetadata(<ts.MethodDeclaration>node);
this.visitFunctionLike(<ts.MethodDeclaration>node);
break;
case ts.SyntaxKind.GetAccessor:
this.visitDeclarationMetadata(<ts.MethodDeclaration>node);
this.visitFunctionLike(<ts.AccessorDeclaration>node, 'get');
break;
case ts.SyntaxKind.SetAccessor:
this.visitDeclarationMetadata(<ts.MethodDeclaration>node);
this.visitFunctionLike(<ts.AccessorDeclaration>node, 'set');
break;
case ts.SyntaxKind.FunctionDeclaration:
let funcDecl = <ts.FunctionDeclaration>node;
this.visitDecorators(funcDecl.decorators);
this.visitFunctionLike(funcDecl);
break;
case ts.SyntaxKind.ArrowFunction:
let arrowFunc = <ts.FunctionExpression>node;
// Dart only allows expressions following the fat arrow operator.
// If the body is a block, we have to drop the fat arrow and emit an
// anonymous function instead.
if (arrowFunc.body.kind === ts.SyntaxKind.Block) {
this.visitFunctionLike(arrowFunc);
} else {
this.visitParameters(arrowFunc.parameters);
this.emit('=>');
this.visit(arrowFunc.body);
}
break;
case ts.SyntaxKind.FunctionExpression:
let funcExpr = <ts.FunctionExpression>node;
this.visitFunctionLike(funcExpr);
break;
case ts.SyntaxKind.PropertySignature:
let propSig = <ts.PropertyDeclaration>node;
this.visitProperty(propSig);
break;
case ts.SyntaxKind.MethodSignature:
let methodSignatureDecl = <ts.FunctionLikeDeclaration>node;
this.visitEachIfPresent(methodSignatureDecl.modifiers);
this.visitFunctionLike(methodSignatureDecl);
break;
case ts.SyntaxKind.Parameter:
let paramDecl = <ts.ParameterDeclaration>node;
// Property parameters will have an explicit property declaration, so we just
// need the dart assignment shorthand to reference the property.
if (this.hasFlag(paramDecl.modifiers, ts.NodeFlags.Public) ||
this.hasFlag(paramDecl.modifiers, ts.NodeFlags.Private) ||
this.hasFlag(paramDecl.modifiers, ts.NodeFlags.Protected)) {
this.visitDeclarationMetadata(paramDecl);
this.emit('this .');
this.visit(paramDecl.name);
if (paramDecl.initializer) {
this.emit('=');
this.visit(paramDecl.initializer);
}
break;
}
if (paramDecl.dotDotDotToken) this.reportError(node, 'rest parameters are unsupported');
if (paramDecl.name.kind === ts.SyntaxKind.ObjectBindingPattern) {
this.visitNamedParameter(paramDecl);
break;
}
this.visitDecorators(paramDecl.decorators);
if (paramDecl.type && paramDecl.type.kind === ts.SyntaxKind.FunctionType) {
// Dart uses "returnType paramName ( parameters )" syntax.
let fnType = <ts.FunctionOrConstructorTypeNode>paramDecl.type;
let hasRestParameter = fnType.parameters.some(p => !!p.dotDotDotToken);
if (hasRestParameter) {
// Dart does not support rest parameters/varargs, degenerate to just "Function".
this.emit('Function');
this.visit(paramDecl.name);
} else {
this.visit(fnType.type);
this.visit(paramDecl.name);
this.visitParameters(fnType.parameters);
}
} else {
if (paramDecl.type) this.visit(paramDecl.type);
this.visit(paramDecl.name);
}
if (paramDecl.initializer) {
this.emit('=');
this.visit(paramDecl.initializer);
}
break;
case ts.SyntaxKind.StaticKeyword:
this.emit('static');
break;
case ts.SyntaxKind.AbstractKeyword:
// Abstract methods in Dart simply lack implementation,
// and don't use the 'abstract' modifier
// Abstract classes are handled in `case ts.SyntaxKind.ClassDeclaration` above.
break;
case ts.SyntaxKind.PrivateKeyword:
// no-op, handled through '_' naming convention in Dart.
break;
case ts.SyntaxKind.PublicKeyword:
// Handled in `visitDeclarationMetadata` below.
break;
case ts.SyntaxKind.ProtectedKeyword:
// Handled in `visitDeclarationMetadata` below.
break;
default:
return false;
}
return true;
}
private visitVariableDeclarationType(varDecl: ts.VariableDeclaration) {
/* Note: VariableDeclarationList can only occur as part of a for loop. This helper method
* is meant for processing for-loop variable declaration types only.
*
* In Dart, all variables in a variable declaration list must have the same type. Since
* we are doing syntax directed translation, we cannot reliably determine if distinct
* variables are declared with the same type or not. Hence we support the following cases:
*
* - A variable declaration list with a single variable can be explicitly typed.
* - When more than one variable is in the list, all must be implicitly typed.
*/
let firstDecl = varDecl.parent.declarations[0];
let msg = 'Variables in a declaration list of more than one variable cannot by typed';
let isFinal = this.hasFlag(varDecl.parent, ts.NodeFlags.Const);
let isConst = false;
if (isFinal && varDecl.initializer) {
// "const" in TypeScript/ES6 corresponds to "final" in Dart, i.e. reference constness.
// If a "const" variable is immediately initialized to a CONST_EXPR(), special case it to be
// a deeply const constant, and generate "const ...".
isConst = varDecl.initializer.kind === ts.SyntaxKind.StringLiteral ||
varDecl.initializer.kind === ts.SyntaxKind.NumericLiteral ||
this.fc.isConstExpr(varDecl.initializer);
}
if (firstDecl === varDecl) {
if (isConst) {
this.emit('const');
} else if (isFinal) {
this.emit('final');
}
if (!varDecl.type) {
if (!isFinal) this.emit('var');
} else if (varDecl.parent.declarations.length > 1) {
this.reportError(varDecl, msg);
} else {
this.visit(varDecl.type);
}
} else if (varDecl.type) {
this.reportError(varDecl, msg);
}
}
private visitFunctionLike(fn: ts.FunctionLikeDeclaration, accessor?: string) {
this.fc.pushTypeParameterNames(fn);
try {
if (fn.type) {
if (fn.kind === ts.SyntaxKind.ArrowFunction ||
fn.kind === ts.SyntaxKind.FunctionExpression) {
// The return type is silently dropped for function expressions (including arrow
// functions), it is not supported in Dart.
this.emit('/*');
this.visit(fn.type);
this.emit('*/');
} else {
this.visit(fn.type);
}
}
if (accessor) this.emit(accessor);
if (fn.name) this.visit(fn.name);
if (fn.typeParameters) {
this.emit('/*<');
// Emit the names literally instead of visiting, otherwise they will be replaced with the
// comment hack themselves.
this.emit(fn.typeParameters.map(p => base.ident(p.name)).join(', '));
this.emit('>*/');
}
// Dart does not even allow the parens of an empty param list on getter
if (accessor !== 'get') {
this.visitParameters(fn.parameters);
} else {
if (fn.parameters && fn.parameters.length > 0) {
this.reportError(fn, 'getter should not accept parameters');
}
}
if (fn.body) {
this.visit(fn.body);
} else {
this.emit(';');
}
} finally {
this.fc.popTypeParameterNames(fn);
}
}
private visitParameters(parameters: ts.ParameterDeclaration[]) {
this.emit('(');
let firstInitParamIdx = 0;
for (; firstInitParamIdx < parameters.length; firstInitParamIdx++) {
// ObjectBindingPatterns are handled within the parameter visit.
let isOpt =
parameters[firstInitParamIdx].initializer || parameters[firstInitParamIdx].questionToken;
if (isOpt && parameters[firstInitParamIdx].name.kind !== ts.SyntaxKind.ObjectBindingPattern) {
break;
}
}
if (firstInitParamIdx !== 0) {
let requiredParams = parameters.slice(0, firstInitParamIdx);
this.visitList(requiredParams);
}
if (firstInitParamIdx !== parameters.length) {
if (firstInitParamIdx !== 0) this.emit(',');
let positionalOptional = parameters.slice(firstInitParamIdx, parameters.length);
this.emit('[');
this.visitList(positionalOptional);
this.emit(']');
}
this.emit(')');
}
/**
* Visit a property declaration.
* In the special case of property parameters in a constructor, we also allow a parameter to be
* emitted as a property.
*/
private visitProperty(decl: ts.PropertyDeclaration|ts.ParameterDeclaration, isParameter = false) {
if (!isParameter) this.visitDeclarationMetadata(decl);
let containingClass = <base.ClassLike>(isParameter ? decl.parent.parent : decl.parent);
let isConstField =
this.fc.hasConstComment(decl) || this.hasAnnotation(decl.decorators, 'CONST');
let hasConstCtor = this.fc.isConstClass(containingClass);
if (isConstField) {
// const implies final
this.emit('const');
} else {
if (hasConstCtor) {
this.emit('final');
}
}
if (decl.type) {
this.visit(decl.type);
} else if (!isConstField && !hasConstCtor) {
this.emit('var');
}
this.visit(decl.name);
if (decl.initializer && !isParameter) {
this.emit('=');
this.visit(decl.initializer);
}
this.emit(';');
}
private visitClassLike(keyword: string, decl: base.ClassLike) {
this.visitDecorators(decl.decorators);
this.emit(keyword);
this.fc.visitTypeName(decl.name);
if (decl.typeParameters) {
this.emit('<');
this.visitList(decl.typeParameters);
this.emit('>');
}
this.visitEachIfPresent(decl.heritageClauses);
this.emit('{');
// Synthesize explicit properties for ctor with 'property parameters'
let synthesizePropertyParam = (param: ts.ParameterDeclaration) => {
if (this.hasFlag(param.modifiers, ts.NodeFlags.Public) ||
this.hasFlag(param.modifiers, ts.NodeFlags.Private) ||
this.hasFlag(param.modifiers, ts.NodeFlags.Protected)) {
// TODO: we should enforce the underscore prefix on privates
this.visitProperty(param, true);
}
};
(<ts.NodeArray<ts.Declaration>>decl.members)
.filter((m) => m.kind === ts.SyntaxKind.Constructor)
.forEach(
(ctor) =>
(<ts.ConstructorDeclaration>ctor).parameters.forEach(synthesizePropertyParam));
this.visitEachIfPresent(decl.members);
// Generate a constructor to host the const modifier, if needed
if (this.fc.isConstClass(decl) &&
!(<ts.NodeArray<ts.Declaration>>decl.members)
.some((m) => m.kind === ts.SyntaxKind.Constructor)) {
this.emit('const');
this.fc.visitTypeName(decl.name);
this.emit('();');
}
this.emit('}');
}
private visitDecorators(decorators: ts.NodeArray<ts.Decorator>) {
if (!decorators) return;
decorators.forEach((d) => {
// Special case @CONST
let name = base.ident(d.expression);
if (!name && d.expression.kind === ts.SyntaxKind.CallExpression) {
// Unwrap @CONST()
let callExpr = (<ts.CallExpression>d.expression);
name = base.ident(callExpr.expression);
}
// Make sure these match IGNORED_ANNOTATIONS below.
if (name === 'CONST') {
// Ignore @CONST - it is handled above in visitClassLike.
return;
}
this.emit('@');
this.visit(d.expression);
});
}
private visitDeclarationMetadata(decl: ts.Declaration) {
this.visitDecorators(decl.decorators);
this.visitEachIfPresent(decl.modifiers);
if (this.hasFlag(decl.modifiers, ts.NodeFlags.Protected)) {
this.reportError(decl, 'protected declarations are unsupported');
return;
}
if (!this.enforceUnderscoreConventions) return;
// Early return in case this is a decl with no name, such as a constructor
if (!decl.name) return;
let name = base.ident(decl.name);
if (!name) return;
let isPrivate = this.hasFlag(decl.modifiers, ts.NodeFlags.Private);
let matchesPrivate = !!name.match(/^_/);
if (isPrivate && !matchesPrivate) {
this.reportError(decl, 'private members must be prefixed with "_"');
}
if (!isPrivate && matchesPrivate) {
this.reportError(decl, 'public members must not be prefixed with "_"');
}
}
private visitNamedParameter(paramDecl: ts.ParameterDeclaration) {
this.visitDecorators(paramDecl.decorators);
let bp = <ts.BindingPattern>paramDecl.name;
let propertyTypes = this.fc.resolvePropertyTypes(paramDecl.type);
let initMap = this.getInitializers(paramDecl);
this.emit('{');
for (let i = 0; i < bp.elements.length; i++) {
let elem = bp.elements[i];
let propDecl = propertyTypes[base.ident(elem.name)];
if (propDecl && propDecl.type) this.visit(propDecl.type);
this.visit(elem.name);
if (elem.initializer && initMap[base.ident(elem.name)]) {
this.reportError(elem, 'cannot have both an inner and outer initializer');
}
let init = elem.initializer || initMap[base.ident(elem.name)];
if (init) {
this.emit(':');
this.visit(init);
}
if (i + 1 < bp.elements.length) this.emit(',');
}
this.emit('}');
}
private getInitializers(paramDecl: ts.ParameterDeclaration) {
let res: ts.Map<ts.Expression> = {};
if (!paramDecl.initializer) return res;
if (paramDecl.initializer.kind !== ts.SyntaxKind.ObjectLiteralExpression) {
this.reportError(paramDecl, 'initializers for named parameters must be object literals');
return res;
}
for (let i of (<ts.ObjectLiteralExpression>paramDecl.initializer).properties) {
if (i.kind !== ts.SyntaxKind.PropertyAssignment) {
this.reportError(i, 'named parameter initializers must be properties, got ' + i.kind);
continue;
}
let ole = <ts.PropertyAssignment>i;
res[base.ident(ole.name)] = ole.initializer;
}
return res;
}
/**
* Handles a function typedef-like interface, i.e. an interface that only declares a single
* call signature, by translating to a Dart `typedef`.
*/
private visitFunctionTypedefInterface(
name: string, signature: ts.CallSignatureDeclaration,
typeParameters: ts.NodeArray<ts.TypeParameterDeclaration>) {
this.emit('typedef');
if (signature.type) {
this.visit(signature.type);
}
this.emit(name);
if (typeParameters) {
this.emit('<');
this.visitList(typeParameters);
this.emit('>');
}
this.visitParameters(signature.parameters);
this.emit(';');
}
}