Skip to content

Commit c1762b4

Browse files
authored
Handled overloaded Promise-returning methods correctly (dart-archive#57)
* Handled overloaded Promise-returning methods correctly * Emitted null checks in extension methods with optional parameters to call the class method of the correct arity
1 parent 5d515cb commit c1762b4

File tree

3 files changed

+161
-30
lines changed

3 files changed

+161
-30
lines changed

lib/declaration.ts

Lines changed: 81 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as ts from 'typescript';
33
import * as base from './base';
44
import {FacadeConverter} from './facade_converter';
55
import {Transpiler} from './main';
6-
import {MergedParameter, MergedType, MergedTypeParameters} from './merge';
6+
import {MergedMember, MergedParameter, MergedType, MergedTypeParameters} from './merge';
77

88
export function isFunctionLikeProperty(
99
decl: ts.VariableDeclaration|ts.ParameterDeclaration|ts.PropertyDeclaration|
@@ -20,8 +20,9 @@ export function isFunctionLikeProperty(
2020
export default class DeclarationTranspiler extends base.TranspilerBase {
2121
private tc: ts.TypeChecker;
2222
private extendsClass = false;
23+
private visitPromises = false;
2324
private containsPromises = false;
24-
private promiseMethods: Set<ts.FunctionLikeDeclaration> = new Set();
25+
private promiseMethods: ts.SignatureDeclaration[] = [];
2526

2627
static NUM_FAKE_REST_PARAMETERS = 5;
2728

@@ -243,7 +244,16 @@ export default class DeclarationTranspiler extends base.TranspilerBase {
243244
}
244245
}
245246

246-
visitMergingOverloads(members: ts.NodeArray<ts.Node>) {
247+
/**
248+
* Visits an array of class members and merges overloads.
249+
*
250+
* @returns An updated version of the members array. All overloaded methods are grouped into
251+
* MergedMember objects that contain all original declarations, as well as the merged result.
252+
* Other non-overloaded members are represented by MergedMembers with only one constituent,
253+
* which is the single declaration of the member.
254+
*/
255+
visitMergingOverloads(members: ts.NodeArray<ts.Node>): MergedMember[] {
256+
const result: MergedMember[] = [];
247257
// TODO(jacobr): merge method overloads.
248258
let groups: Map<string, ts.Node[]> = new Map();
249259
let orderedGroups: Array<ts.Node[]> = [];
@@ -315,9 +325,24 @@ export default class DeclarationTranspiler extends base.TranspilerBase {
315325
group.push(node);
316326
});
317327

318-
orderedGroups.forEach((group: Array<ts.Node>) => {
328+
orderedGroups.forEach((group: Array<ts.SignatureDeclaration>) => {
329+
const first = group[0];
330+
// If the methods in this group return Promises and this.visitPromises is false, skip
331+
// visiting these methods and add them to this.promiseMethods. If the methods in this group
332+
// return Promises and this.visitPromises is true, it means that this function is being called
333+
// from emitMethodsAsExtensions and the methods should now be visited.
334+
if (!this.visitPromises && base.isPromise(first.type)) {
335+
if (!this.containsPromises) {
336+
this.containsPromises = true;
337+
}
338+
group.forEach((declaration: ts.SignatureDeclaration) => {
339+
this.promiseMethods.push(declaration);
340+
});
341+
return;
342+
}
319343
if (group.length === 1) {
320-
this.visit(group[0]);
344+
this.visit(first);
345+
result.push(new MergedMember(group, first));
321346
return;
322347
}
323348
group.forEach((fn: ts.Node) => {
@@ -330,7 +355,6 @@ export default class DeclarationTranspiler extends base.TranspilerBase {
330355
this.maybeLineBreak();
331356
});
332357
// TODO: actually merge.
333-
let first = <ts.SignatureDeclaration>group[0];
334358
let kind = first.kind;
335359
let merged = <ts.SignatureDeclaration>ts.createNode(kind);
336360
merged.parent = first.parent;
@@ -384,7 +408,9 @@ export default class DeclarationTranspiler extends base.TranspilerBase {
384408
merged.typeParameters = mergedTypeParams.toTypeParameters();
385409

386410
this.fc.visit(merged);
411+
result.push(new MergedMember(group, merged));
387412
});
413+
return result;
388414
}
389415

390416

@@ -686,16 +712,6 @@ export default class DeclarationTranspiler extends base.TranspilerBase {
686712
break;
687713
case ts.SyntaxKind.MethodSignature:
688714
let methodSignatureDecl = <ts.FunctionLikeDeclaration>node;
689-
if (base.isPromise(methodSignatureDecl.type)) {
690-
if (this.promiseMethods.has(methodSignatureDecl)) {
691-
break;
692-
}
693-
if (!this.containsPromises) {
694-
this.containsPromises = true;
695-
}
696-
this.promiseMethods.add(methodSignatureDecl);
697-
break;
698-
}
699715
this.visitDeclarationMetadata(methodSignatureDecl);
700716
this.visitFunctionLike(methodSignatureDecl);
701717
break;
@@ -865,15 +881,15 @@ export default class DeclarationTranspiler extends base.TranspilerBase {
865881

866882
this.visitClassBody(decl, name);
867883
this.emit('}\n');
868-
if (this.promiseMethods.size) {
884+
if (this.promiseMethods.length) {
869885
const visitName = () => {
870886
this.visitClassLikeName(name, typeParameters, ts.createNodeArray(), false);
871887
};
872888
const visitNameOfExtensions = () => {
873889
this.visitClassLikeName(name, typeParameters, ts.createNodeArray(), true);
874890
};
875891
this.emitMethodsAsExtensions(name, visitName, visitNameOfExtensions, this.promiseMethods);
876-
this.promiseMethods.clear();
892+
this.promiseMethods = [];
877893
}
878894
this.emit('\n');
879895
}
@@ -949,15 +965,14 @@ export default class DeclarationTranspiler extends base.TranspilerBase {
949965

950966
private emitMethodsAsExtensions(
951967
className: ts.Identifier, visitName: () => void, visitNameOfExtensions: () => void,
952-
methods: Set<ts.FunctionLikeDeclaration>) {
968+
methods: ts.SignatureDeclaration[]) {
969+
this.visitPromises = true;
953970
// Emit private class containing external methods
954971
this.emit(`@JS('${base.ident(className)}')`);
955972
this.emit(`abstract class _`);
956973
visitName();
957974
this.emit('{');
958-
for (const declaration of methods) {
959-
this.visitFunctionLike(declaration);
960-
}
975+
const mergedMembers = this.visitMergingOverloads(ts.createNodeArray(Array.from(methods)));
961976
this.emit('}\n');
962977

963978
// Emit extensions on public class to expose methods
@@ -966,7 +981,8 @@ export default class DeclarationTranspiler extends base.TranspilerBase {
966981
this.emit('on');
967982
visitName();
968983
this.emit('{');
969-
for (const declaration of methods) {
984+
for (const merged of mergedMembers) {
985+
const declaration = merged.mergedDeclaration;
970986
if (!base.isPromise(declaration.type)) {
971987
continue;
972988
}
@@ -981,10 +997,49 @@ export default class DeclarationTranspiler extends base.TranspilerBase {
981997
this.emit('final _');
982998
this.fc.visitTypeName(className);
983999
this.emit('tt = t;\n');
984-
this.emit(`return promiseToFuture(tt.${base.ident(declaration.name)}`);
985-
this.visitParameters(declaration.parameters, {namesOnly: true});
986-
this.emit(');}\n');
1000+
this.emitExtensionBody(merged);
1001+
this.emit('}\n');
9871002
}
9881003
this.emit('}\n');
1004+
this.visitPromises = false;
1005+
}
1006+
1007+
private emitExtensionBody({constituents, mergedDeclaration}: MergedMember) {
1008+
// Determine all valid arties of this method by going through the overloaded signatures
1009+
const arities: Set<number> = new Set();
1010+
for (const constituent of constituents) {
1011+
const arity = constituent.parameters.length;
1012+
arities.add(arity);
1013+
}
1014+
const sortedArities = Array.from(arities).sort();
1015+
for (const arity of sortedArities) {
1016+
if (arity < mergedDeclaration.parameters.length) {
1017+
const firstOptionalIndex = arity;
1018+
const suppliedParameters = mergedDeclaration.parameters.slice(0, firstOptionalIndex);
1019+
const omittedParameters = mergedDeclaration.parameters.slice(
1020+
firstOptionalIndex, mergedDeclaration.parameters.length);
1021+
// Emit null checks to verify the number of omitted parameters
1022+
this.emit('if (');
1023+
let isFirst = true;
1024+
for (const omitted of omittedParameters) {
1025+
if (isFirst) {
1026+
isFirst = false;
1027+
} else {
1028+
this.emit('&&');
1029+
}
1030+
this.visit(omitted.name);
1031+
this.emit('== null');
1032+
}
1033+
this.emit(') {');
1034+
this.emit(`return promiseToFuture(tt.${base.ident(mergedDeclaration.name)}`);
1035+
this.visitParameters(ts.createNodeArray(suppliedParameters), {namesOnly: true});
1036+
this.emit('); }\n');
1037+
} else {
1038+
// No parameters were omitted, no null checks are necessary for this call
1039+
this.emit(`return promiseToFuture(tt.${base.ident(mergedDeclaration.name)}`);
1040+
this.visitParameters(ts.createNodeArray(mergedDeclaration.parameters), {namesOnly: true});
1041+
this.emit(');\n');
1042+
}
1043+
}
9891044
}
9901045
}

lib/merge.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export class MergedType {
3131
this.merge((t as ts.TypePredicateNode).type);
3232
return;
3333
case ts.SyntaxKind.TypeReference:
34-
// We need to follow Alais types as Dart does not support them for non
34+
// We need to follow Alias types as Dart does not support them for non
3535
// function types. TODO(jacobr): handle them for Function types?
3636
let typeRef = <ts.TypeReferenceNode>t;
3737
let decl = this.fc.getDeclaration(typeRef.typeName);
@@ -224,6 +224,22 @@ export class MergedTypeParameters {
224224
}
225225
}
226226

227+
/**
228+
* Represents a merged class member. If the member was not overloaded, the constituents array
229+
* will contain only the original declaration and mergedDeclaration will just be the original
230+
* declaration. If the member was an overloaded method, the constituents array will contain all
231+
* overloaded declarations and mergedDeclaration will contain the result of merging the overloads.
232+
*/
233+
export class MergedMember {
234+
constructor(
235+
public constituents: ts.SignatureDeclaration[],
236+
public mergedDeclaration: ts.SignatureDeclaration) {}
237+
238+
isOverloaded(): boolean {
239+
return this.constituents.length > 1;
240+
}
241+
}
242+
227243
/**
228244
* Normalize a SourceFile
229245
*/

test/declaration_test.ts

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ abstract class MyMath {}
268268
269269
@JS('MyMath')
270270
abstract class _MyMath {
271-
Promise<num> randomInRange(num start, num end);
271+
external Promise<num> randomInRange(num start, num end);
272272
}
273273
274274
extension MyMathExtensions on MyMath {
@@ -291,7 +291,7 @@ abstract class X<T> {}
291291
292292
@JS('X')
293293
abstract class _X<T> {
294-
Promise<T> f(T a);
294+
external Promise<T> f(T a);
295295
}
296296
297297
extension XExtensions<T> on X<T> {
@@ -326,7 +326,7 @@ abstract class Z implements Y {}
326326
327327
@JS('Z')
328328
abstract class _Z {
329-
Promise<String> f();
329+
external Promise<String> f();
330330
}
331331
332332
extension ZExtensions on Z {
@@ -337,6 +337,66 @@ extension ZExtensions on Z {
337337
}
338338
}
339339
340+
@JS()
341+
abstract class Promise<T> {}`);
342+
expectTranslate(`declare interface X {
343+
f(a: string): Promise<number>;
344+
f(a: string, b: number): Promise<number>;
345+
}`).to.equal(`import "package:js/js_util.dart" show promiseToFuture;
346+
347+
@anonymous
348+
@JS()
349+
abstract class X {}
350+
351+
@JS('X')
352+
abstract class _X {
353+
/*external Promise<num> f(String a);*/
354+
/*external Promise<num> f(String a, num b);*/
355+
external Promise<num> f(String a, [num b]);
356+
}
357+
358+
extension XExtensions on X {
359+
Future<num> f(String a, [num b]) {
360+
final Object t = this;
361+
final _X tt = t;
362+
if (b == null) {
363+
return promiseToFuture(tt.f(a));
364+
}
365+
return promiseToFuture(tt.f(a, b));
366+
}
367+
}
368+
369+
@JS()
370+
abstract class Promise<T> {}`);
371+
expectTranslate(`declare interface X {
372+
f(a: string): Promise<number>;
373+
f(a: number, b: number): Promise<number>;
374+
f(c: number[]): Promise<number>;
375+
}`).to.equal(`import "package:js/js_util.dart" show promiseToFuture;
376+
377+
@anonymous
378+
@JS()
379+
abstract class X {}
380+
381+
@JS('X')
382+
abstract class _X {
383+
/*external Promise<num> f(String a);*/
384+
/*external Promise<num> f(num a, num b);*/
385+
/*external Promise<num> f(List<num> c);*/
386+
external Promise<num> f(dynamic /*String|num|List<num>*/ a_c, [num b]);
387+
}
388+
389+
extension XExtensions on X {
390+
Future<num> f(dynamic /*String|num|List<num>*/ a_c, [num b]) {
391+
final Object t = this;
392+
final _X tt = t;
393+
if (b == null) {
394+
return promiseToFuture(tt.f(a_c));
395+
}
396+
return promiseToFuture(tt.f(a_c, b));
397+
}
398+
}
399+
340400
@JS()
341401
abstract class Promise<T> {}`);
342402
});

0 commit comments

Comments
 (0)