| Index: lib/base.ts
|
| diff --git a/lib/base.ts b/lib/base.ts
|
| index dcc5d5f047617f6ba11a1d8025a6341a881aa1fe..9267d1d90b113fa9bdddbfe3b829720c08c113b2 100644
|
| --- a/lib/base.ts
|
| +++ b/lib/base.ts
|
| @@ -1,18 +1,63 @@
|
| import * as dartStyle from 'dart-style';
|
| +import * as path from 'path';
|
| import * as ts from 'typescript';
|
|
|
| import {OutputContext, Transpiler} from './main';
|
|
|
| +export type ResolvedTypeMap = {
|
| + [name: string]: ts.TypeNode
|
| +};
|
| +
|
| +/***
|
| + * Options for how we display a TypeScript type as a Dart type.
|
| + */
|
| +export interface TypeDisplayOptions {
|
| + /**
|
| + * We are displaying the type inside a comment so we don't have to restrict to valid Dart syntax.
|
| + * For example, we can display string literal type using the regular TypeScript syntax.
|
| + */
|
| + insideComment?: boolean;
|
| + /**
|
| + * Dart has additional restrictions for what types are valid to emit inside a type argument. For
|
| + * example, "void" is not valid inside a type argument so Null has to be used instead.
|
| + */
|
| + insideTypeArgument?: boolean;
|
| + /**
|
| + * Indicates that we should not append an additional comment indicating what the true TypeScript
|
| + * type was for cases where Dart cannot express the type precisely.
|
| + */
|
| + hideComment?: boolean;
|
| +
|
| + /**
|
| + * Parameter declarations to substitute
|
| + */
|
| + typeArguments?: ts.TypeNode[];
|
| +
|
| + resolvedTypeArguments?: ResolvedTypeMap;
|
| +}
|
| +
|
| export type ClassLike = ts.ClassDeclaration | ts.InterfaceDeclaration;
|
| export type NamedDeclaration = ClassLike | ts.PropertyDeclaration | ts.VariableDeclaration |
|
| ts.MethodDeclaration | ts.ModuleDeclaration | ts.FunctionDeclaration;
|
|
|
| +export interface ExtendedInterfaceDeclaration extends ts.InterfaceDeclaration {
|
| + /**
|
| + * VariableDeclaration associated with this interface that we want to treat as the concrete
|
| + * location of this interface to enable interfaces that act like constructors.
|
| + * Because Dart does not permit calling objects like constructors we have to add this workaround.
|
| + */
|
| + classLikeVariableDeclaration?: ts.VariableDeclaration;
|
| +}
|
| +
|
| export type Set = {
|
| [s: string]: boolean
|
| };
|
|
|
| export function ident(n: ts.Node): string {
|
| if (n.kind === ts.SyntaxKind.Identifier) return (<ts.Identifier>n).text;
|
| + if (n.kind === ts.SyntaxKind.FirstLiteralToken) {
|
| + return (n as ts.LiteralLikeNode).text;
|
| + }
|
| if (n.kind === ts.SyntaxKind.QualifiedName) {
|
| let qname = (<ts.QualifiedName>n);
|
| let leftName = ident(qname.left);
|
| @@ -60,6 +105,11 @@ export function isCallableType(type: ts.TypeNode, tc: ts.TypeChecker): boolean {
|
| return false;
|
| }
|
|
|
| +export function supportedAliasType(alias: ts.TypeAliasDeclaration): boolean {
|
| + let kind = alias.type.kind;
|
| + return kind === ts.SyntaxKind.TypeLiteral || kind === ts.SyntaxKind.FunctionType;
|
| +}
|
| +
|
| export function isFunctionType(type: ts.TypeNode, tc: ts.TypeChecker): boolean {
|
| let kind = type.kind;
|
| if (kind === ts.SyntaxKind.FunctionType) return true;
|
| @@ -68,6 +118,16 @@ export function isFunctionType(type: ts.TypeNode, tc: ts.TypeChecker): boolean {
|
| if (t.symbol && t.symbol.flags & ts.SymbolFlags.Function) return true;
|
| }
|
|
|
| + if (kind === ts.SyntaxKind.IntersectionType) {
|
| + let types = (<ts.IntersectionTypeNode>type).types;
|
| + for (let i = 0; i < types.length; ++i) {
|
| + if (isFunctionType(types[i], tc)) {
|
| + return true;
|
| + }
|
| + }
|
| + return false;
|
| + }
|
| +
|
| if (kind === ts.SyntaxKind.UnionType) {
|
| let types = (<ts.UnionTypeNode>type).types;
|
| for (let i = 0; i < types.length; ++i) {
|
| @@ -92,9 +152,30 @@ export function isFunctionType(type: ts.TypeNode, tc: ts.TypeChecker): boolean {
|
| return false;
|
| }
|
|
|
| +export function isThisParameter(param: ts.ParameterDeclaration): boolean {
|
| + return param.name && param.name.kind === ts.SyntaxKind.Identifier &&
|
| + (param.name as ts.Identifier).text === 'this';
|
| +}
|
| +
|
| +/**
|
| + * Dart does not have a concept of binding the type of the "this" parameter to a method.
|
| + */
|
| +export function filterThisParameter(params: ts.ParameterDeclaration[]): ts.ParameterDeclaration[] {
|
| + let ret: ts.ParameterDeclaration[] = [];
|
| + for (let i = 0; i < params.length; i++) {
|
| + let param = params[i];
|
| + if (!isThisParameter(param)) {
|
| + ret.push(param);
|
| + }
|
| + }
|
| + return ret;
|
| +}
|
| +
|
| export function isTypeNode(node: ts.Node): boolean {
|
| switch (node.kind) {
|
| + case ts.SyntaxKind.IntersectionType:
|
| case ts.SyntaxKind.UnionType:
|
| + case ts.SyntaxKind.ParenthesizedType:
|
| case ts.SyntaxKind.TypeReference:
|
| case ts.SyntaxKind.TypeLiteral:
|
| case ts.SyntaxKind.LastTypeNode:
|
| @@ -105,9 +186,12 @@ export function isTypeNode(node: ts.Node): boolean {
|
| case ts.SyntaxKind.NumberKeyword:
|
| case ts.SyntaxKind.StringKeyword:
|
| case ts.SyntaxKind.VoidKeyword:
|
| + case ts.SyntaxKind.NullKeyword:
|
| + case ts.SyntaxKind.UndefinedKeyword:
|
| case ts.SyntaxKind.BooleanKeyword:
|
| case ts.SyntaxKind.AnyKeyword:
|
| case ts.SyntaxKind.FunctionType:
|
| + case ts.SyntaxKind.ThisType:
|
| return true;
|
| default:
|
| return false;
|
| @@ -141,11 +225,12 @@ export function getAncestor(n: ts.Node, kind: ts.SyntaxKind): ts.Node {
|
| }
|
|
|
| export function getEnclosingClass(n: ts.Node): ClassLike {
|
| - for (let parent = n.parent; parent; parent = parent.parent) {
|
| - if (parent.kind === ts.SyntaxKind.ClassDeclaration ||
|
| - parent.kind === ts.SyntaxKind.InterfaceDeclaration) {
|
| - return <ClassLike>parent;
|
| + while (n) {
|
| + if (n.kind === ts.SyntaxKind.ClassDeclaration ||
|
| + n.kind === ts.SyntaxKind.InterfaceDeclaration) {
|
| + return <ClassLike>n;
|
| }
|
| + n = n.parent;
|
| }
|
| return null;
|
| }
|
| @@ -158,10 +243,10 @@ export function isInsideConstExpr(node: ts.Node): boolean {
|
| return isConstCall(<ts.CallExpression>getAncestor(node, ts.SyntaxKind.CallExpression));
|
| }
|
|
|
| -export function formatType(s: string, comment: string, insideCodeComment: boolean): string {
|
| - if (!comment) {
|
| +export function formatType(s: string, comment: string, options: TypeDisplayOptions): string {
|
| + if (!comment || options.hideComment) {
|
| return s;
|
| - } else if (insideCodeComment) {
|
| + } else if (options.insideComment) {
|
| // When inside a comment we only need to emit the comment version which
|
| // is the syntax we would like to use if Dart supported all language
|
| // features we would like to use for interop.
|
| @@ -202,17 +287,34 @@ export class TranspilerBase {
|
| maybeLineBreak() { return this.transpiler.maybeLineBreak(); }
|
| enterCodeComment() { return this.transpiler.enterCodeComment(); }
|
| exitCodeComment() { return this.transpiler.exitCodeComment(); }
|
| +
|
| + enterTypeArguments() { this.transpiler.enterTypeArgument(); }
|
| + exitTypeArguments() { this.transpiler.exitTypeArgument(); }
|
| + get insideTypeArgument() { return this.transpiler.insideTypeArgument; }
|
| +
|
| get insideCodeComment() { return this.transpiler.insideCodeComment; }
|
|
|
| emitImport(toEmit: string) {
|
| if (!this.transpiler.importsEmitted[toEmit]) {
|
| this.pushContext(OutputContext.Import);
|
| - this.emit(`import "${toEmit}";`);
|
| + this.emit(`import "${toEmit}";\n`);
|
| this.transpiler.importsEmitted[toEmit] = true;
|
| this.popContext();
|
| }
|
| }
|
|
|
| + emitImportForSourceFile(sourceFile: ts.SourceFile, context: ts.SourceFile) {
|
| + if (sourceFile === context) return;
|
| + if (sourceFile.hasNoDefaultLib) {
|
| + // We don't want to emit imports to default lib libraries as we replace with Dart equivalents
|
| + // such as dart:html, etc.
|
| + return;
|
| + }
|
| + let relativePath = path.relative(path.dirname(context.fileName), sourceFile.fileName);
|
| + this.emitImport(this.transpiler.getDartFileName(relativePath));
|
| + }
|
| +
|
| +
|
| reportError(n: ts.Node, message: string) { this.transpiler.reportError(n, message); }
|
|
|
| visitNode(n: ts.Node): boolean { throw new Error('not implemented'); }
|
| @@ -230,6 +332,33 @@ export class TranspilerBase {
|
| }
|
| }
|
|
|
| + /**
|
| + * Returns whether any parameters were actually emitted.
|
| + * @param nodes
|
| + * @param separator
|
| + */
|
| + visitParameterList(nodes: ts.ParameterDeclaration[]): boolean {
|
| + let emittedParameters = false;
|
| + for (let i = 0; i < nodes.length; ++i) {
|
| + let param = nodes[i];
|
| + if (!this.insideCodeComment && isThisParameter(param)) {
|
| + // Emit the this type in a comment as it could be of interest to Dart users who are
|
| + // calling allowInteropCaptureThis to bind a Dart method.
|
| + this.enterCodeComment();
|
| + this.visit(param.type);
|
| + this.emit('this');
|
| + this.exitCodeComment();
|
| + continue;
|
| + }
|
| + if (emittedParameters) {
|
| + this.emitNoSpace(',');
|
| + }
|
| + this.visit(param);
|
| + emittedParameters = true;
|
| + }
|
| + return emittedParameters;
|
| + }
|
| +
|
| uniqueId(name: string): string {
|
| const id = this.idCounter++;
|
| return `_${name}\$\$js_facade_gen\$${id}`;
|
| @@ -271,16 +400,14 @@ export class TranspilerBase {
|
| return this.transpiler.getRelativeFileName(fileName);
|
| }
|
|
|
| + getDartFileName(fileName?: string): string { return this.transpiler.getDartFileName(fileName); }
|
| +
|
| maybeVisitTypeArguments(n: {typeArguments?: ts.NodeArray<ts.TypeNode>}) {
|
| if (n.typeArguments) {
|
| - // If it's a single type argument `<void>`, ignore it and emit nothing.
|
| - // This is particularly useful for `Promise<void>`, see
|
| - // https://github.com/dart-lang/sdk/issues/2231#issuecomment-108313639
|
| - if (n.typeArguments.length === 1 && n.typeArguments[0].kind === ts.SyntaxKind.VoidKeyword) {
|
| - return;
|
| - }
|
| this.emitNoSpace('<');
|
| + this.enterTypeArguments();
|
| this.visitList(n.typeArguments);
|
| + this.exitTypeArguments();
|
| this.emitNoSpace('>');
|
| }
|
| }
|
| @@ -298,16 +425,17 @@ export class TranspilerBase {
|
| }
|
| }
|
|
|
| + let hasValidParameters = false;
|
| if (firstInitParamIdx !== 0) {
|
| let requiredParams = parameters.slice(0, firstInitParamIdx);
|
| - this.visitList(requiredParams);
|
| + hasValidParameters = this.visitParameterList(requiredParams);
|
| }
|
|
|
| if (firstInitParamIdx !== parameters.length) {
|
| - if (firstInitParamIdx !== 0) this.emitNoSpace(',');
|
| + if (hasValidParameters) this.emitNoSpace(',');
|
| let positionalOptional = parameters.slice(firstInitParamIdx, parameters.length);
|
| this.emit('[');
|
| - this.visitList(positionalOptional);
|
| + this.visitParameterList(positionalOptional);
|
| this.emitNoSpace(']');
|
| }
|
|
|
|
|