Skip to content

Commit 1ee7f74

Browse files
authored
Added logic to handle methods that return Promises (dart-archive#52)
* Added logic to handle methods that return Promises * Moved external methods that return Promises to private classes and exposed them through extension methods * Emitted opaque Dart class for Promises
1 parent 721d368 commit 1ee7f74

File tree

5 files changed

+110
-16
lines changed

5 files changed

+110
-16
lines changed

lib/base.ts

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,10 @@ export function isTypeNode(node: ts.Node): boolean {
260260
}
261261
}
262262

263+
export function isPromise(type: ts.TypeNode): boolean {
264+
return type && ts.isTypeReferenceNode(type) && ident(type.typeName) === 'Promise';
265+
}
266+
263267
export function isCallable(decl: ClassLike): boolean {
264268
let members = decl.members as ReadonlyArray<ts.ClassElement>;
265269
return members.some((member) => {
@@ -473,7 +477,7 @@ export class TranspilerBase {
473477
/**
474478
* Returns whether any parameters were actually emitted.
475479
*/
476-
visitParameterList(nodes: ts.ParameterDeclaration[]): boolean {
480+
visitParameterList(nodes: ts.ParameterDeclaration[], namesOnly: boolean): boolean {
477481
let emittedParameters = false;
478482
for (let i = 0; i < nodes.length; ++i) {
479483
let param = nodes[i];
@@ -489,7 +493,11 @@ export class TranspilerBase {
489493
if (emittedParameters) {
490494
this.emitNoSpace(',');
491495
}
492-
this.visit(param);
496+
if (namesOnly) {
497+
this.emit(ident(param.name));
498+
} else {
499+
this.visit(param);
500+
}
493501
emittedParameters = true;
494502
}
495503
return emittedParameters;
@@ -556,7 +564,7 @@ export class TranspilerBase {
556564
}
557565
}
558566

559-
visitParameters(parameters: ts.NodeArray<ts.ParameterDeclaration>) {
567+
visitParameters(parameters: ts.NodeArray<ts.ParameterDeclaration>, {namesOnly = false}) {
560568
this.emitNoSpace('(');
561569
let firstInitParamIdx = 0;
562570
for (; firstInitParamIdx < parameters.length; firstInitParamIdx++) {
@@ -572,15 +580,19 @@ export class TranspilerBase {
572580
let hasValidParameters = false;
573581
if (firstInitParamIdx !== 0) {
574582
let requiredParams = parameters.slice(0, firstInitParamIdx);
575-
hasValidParameters = this.visitParameterList(requiredParams);
583+
hasValidParameters = this.visitParameterList(requiredParams, namesOnly);
576584
}
577585

578586
if (firstInitParamIdx !== parameters.length) {
579587
if (hasValidParameters) this.emitNoSpace(',');
580588
let positionalOptional = parameters.slice(firstInitParamIdx, parameters.length);
581-
this.emit('[');
582-
this.visitParameterList(positionalOptional);
583-
this.emitNoSpace(']');
589+
if (!namesOnly) {
590+
this.emit('[');
591+
}
592+
this.visitParameterList(positionalOptional, namesOnly);
593+
if (!namesOnly) {
594+
this.emitNoSpace(']');
595+
}
584596
}
585597

586598
this.emitNoSpace(')');

lib/declaration.ts

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@ export function isFunctionLikeProperty(
1919

2020
export default class DeclarationTranspiler extends base.TranspilerBase {
2121
private tc: ts.TypeChecker;
22-
2322
private extendsClass = false;
23+
private containsPromises = false;
24+
private promiseMethods: Set<ts.FunctionLikeDeclaration> = new Set();
2425

2526
static NUM_FAKE_REST_PARAMETERS = 5;
2627

@@ -446,7 +447,7 @@ export default class DeclarationTranspiler extends base.TranspilerBase {
446447
// instead.
447448
this.visit(fnType.type);
448449
this.visit(paramDecl.name);
449-
this.visitParameters(fnType.parameters);
450+
this.visitParameters(fnType.parameters, {namesOnly: false});
450451
break;
451452
}
452453
}
@@ -500,6 +501,11 @@ export default class DeclarationTranspiler extends base.TranspilerBase {
500501
case ts.SyntaxKind.SourceFile:
501502
let sourceFile = node as ts.SourceFile;
502503
this.visitMergingOverloads(sourceFile.statements);
504+
if (this.containsPromises) {
505+
this.addImport('dart:async', 'Completer');
506+
this.addImport('package:js/js_util.dart', 'promiseToFuture');
507+
this.emit(`@JS() abstract class Promise<T> {}\n`);
508+
}
503509
break;
504510
case ts.SyntaxKind.ModuleBlock: {
505511
let block = <ts.ModuleBlock>node;
@@ -540,7 +546,7 @@ export default class DeclarationTranspiler extends base.TranspilerBase {
540546
this.emit('external');
541547
this.visit(fn.type);
542548
this.emit('call');
543-
this.visitParameters(fn.parameters);
549+
this.visitParameters(fn.parameters, {namesOnly: false});
544550
this.emitNoSpace(';');
545551
} break;
546552
case ts.SyntaxKind.IndexSignature:
@@ -641,7 +647,7 @@ export default class DeclarationTranspiler extends base.TranspilerBase {
641647
}
642648
this.visitDeclarationMetadata(ctorDecl);
643649
this.fc.visitTypeName(classDecl.name);
644-
this.visitParameters(ctorDecl.parameters);
650+
this.visitParameters(ctorDecl.parameters, {namesOnly: false});
645651
this.emitNoSpace(';');
646652
if (isAnonymous) {
647653
this.exitCodeComment();
@@ -681,6 +687,16 @@ export default class DeclarationTranspiler extends base.TranspilerBase {
681687
break;
682688
case ts.SyntaxKind.MethodSignature:
683689
let methodSignatureDecl = <ts.FunctionLikeDeclaration>node;
690+
if (base.isPromise(methodSignatureDecl.type)) {
691+
if (this.promiseMethods.has(methodSignatureDecl)) {
692+
break;
693+
}
694+
if (!this.containsPromises) {
695+
this.containsPromises = true;
696+
}
697+
this.promiseMethods.add(methodSignatureDecl);
698+
break;
699+
}
684700
this.visitDeclarationMetadata(methodSignatureDecl);
685701
this.visitFunctionLike(methodSignatureDecl);
686702
break;
@@ -759,7 +775,7 @@ export default class DeclarationTranspiler extends base.TranspilerBase {
759775
}
760776
// Dart does not even allow the parens of an empty param list on getter
761777
if (accessor !== 'get') {
762-
this.visitParameters(fn.parameters);
778+
this.visitParameters(fn.parameters, {namesOnly: false});
763779
} else {
764780
if (fn.parameters && fn.parameters.length > 0) {
765781
this.reportError(fn, 'getter should not accept parameters');
@@ -858,7 +874,12 @@ export default class DeclarationTranspiler extends base.TranspilerBase {
858874
(<ts.ConstructorDeclaration>ctor).parameters.forEach(synthesizePropertyParam));
859875

860876
this.visitClassBody(decl, name);
861-
this.emit('}\n\n');
877+
this.emit('}\n');
878+
if (this.promiseMethods.size) {
879+
this.emitMethodsAsExtensions(name, this.promiseMethods);
880+
this.promiseMethods.clear();
881+
}
882+
this.emit('\n');
862883
}
863884

864885
private visitDeclarationMetadata(decl: ts.Declaration) {
@@ -906,7 +927,42 @@ export default class DeclarationTranspiler extends base.TranspilerBase {
906927
this.exitTypeArguments();
907928
this.emitNoSpace('>');
908929
}
909-
this.visitParameters(signature.parameters);
930+
this.visitParameters(signature.parameters, {namesOnly: false});
910931
this.emitNoSpace(';');
911932
}
933+
934+
private emitMethodsAsExtensions(
935+
className: ts.Identifier, methods: Set<ts.FunctionLikeDeclaration>) {
936+
// Emit private class containing external methods
937+
this.emit(`@JS('${base.ident(className)}')`);
938+
this.emit(`abstract class _`);
939+
this.fc.visitTypeName(className);
940+
this.emit('{');
941+
for (const declaration of methods) {
942+
this.visitFunctionLike(declaration);
943+
}
944+
this.emit('}\n');
945+
946+
// Emit extensions on public class to expose methods
947+
this.emit('extension on');
948+
this.fc.visitTypeName(className);
949+
this.emit('{');
950+
for (const declaration of methods) {
951+
if (!base.isPromise(declaration.type)) {
952+
continue;
953+
}
954+
this.emit('Future');
955+
this.emit(base.ident(declaration.name));
956+
this.visitParameters(declaration.parameters, {namesOnly: false});
957+
this.emit('{');
958+
this.emit('final Object t = this;');
959+
this.emit('final _');
960+
this.fc.visitTypeName(className);
961+
this.emit('tt = t;\n');
962+
this.emit(`return promiseToFuture(tt.${base.ident(declaration.name)}`);
963+
this.visitParameters(declaration.parameters, {namesOnly: true});
964+
this.emit(');}\n');
965+
}
966+
this.emit('}\n');
967+
}
912968
}

lib/main.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -534,7 +534,8 @@ class Output {
534534
let buffer = this.insideCodeComment ? this.codeCommentResult : this.result;
535535
if (buffer.length > 0) {
536536
let lastChar = buffer.slice(-1);
537-
if (lastChar !== ' ' && lastChar !== '(' && lastChar !== '<' && lastChar !== '[') {
537+
if (lastChar !== ' ' && lastChar !== '(' && lastChar !== '<' && lastChar !== '[' &&
538+
lastChar !== '_') {
538539
// Avoid emitting a space in obvious cases where a space is not required
539540
// to make the output slightly prettier in cases where the DartFormatter
540541
// cannot run such as within a comment where code we emit is not quite

test/declaration_test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,32 @@ class X {
256256
X.fakeConstructor$();
257257
external x();
258258
}`);
259+
});
260+
it('should emit extension methods to return Futures from methods that return Promises', () => {
261+
expectTranslate(`declare interface MyMath {
262+
randomInRange(start: number, end: number): Promise<number>;
263+
}`).to.equal(`import "dart:async" show Completer;
264+
import "package:js/js_util.dart" show promiseToFuture;
265+
266+
@anonymous
267+
@JS()
268+
abstract class MyMath {}
269+
270+
@JS('MyMath')
271+
abstract class _MyMath {
272+
Promise<num> randomInRange(num start, num end);
273+
}
274+
275+
extension on MyMath {
276+
Future randomInRange(num start, num end) {
277+
final Object t = this;
278+
final _MyMath tt = t;
279+
return promiseToFuture(tt.randomInRange(start, end));
280+
}
281+
}
282+
283+
@JS()
284+
abstract class Promise<T> {}`);
259285
});
260286
it('supports abstract methods', () => {
261287
expectTranslate('abstract class X { abstract x(); }').to.equal(`@JS()

test/facade_converter_test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ function getSources(str: string): {[k: string]: string} {
77
map(x: number): string { return String(x); }
88
static get(m: any, k: string): number { return m[k]; }
99
}
10-
export class Promise {}
1110
`,
1211
};
1312
srcs[FAKE_MAIN] = str;

0 commit comments

Comments
 (0)