UNPKG

ts2dart

Version:

Transpile TypeScript code to Dart

516 lines (492 loc) 19.8 kB
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(';'); } }