Skip to content

Commit 2f6bfa1

Browse files
authored
Added support for Promise class properties (dart-archive#60)
* Added support for Promise class properties Emitted extension getters and setters to expose them as Futures in the generated facade Refactored emission of getters and setters
1 parent 6f463b2 commit 2f6bfa1

File tree

3 files changed

+294
-84
lines changed

3 files changed

+294
-84
lines changed

lib/declaration.ts

Lines changed: 169 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,21 @@ import {FacadeConverter} from './facade_converter';
55
import {Transpiler} from './main';
66
import {MergedMember, MergedParameter, MergedType, MergedTypeParameters} from './merge';
77

8+
const PRIVATE_CLASS_INSTANCE_IN_EXTENSIONS = 'tt';
9+
10+
const enum emitPropertyMode {
11+
getter,
12+
setter
13+
}
14+
type emitPropertyOptions = {
15+
mode: emitPropertyMode,
16+
declaration: ts.PropertyDeclaration|ts.PropertySignature|ts.VariableDeclaration|
17+
ts.ParameterDeclaration,
18+
emitJsAnnotation: boolean,
19+
isExternal: boolean,
20+
emitBody?: (declaration: ts.PropertyDeclaration|ts.PropertySignature) => void
21+
};
22+
823
export function isFunctionLikeProperty(
924
decl: ts.VariableDeclaration|ts.ParameterDeclaration|ts.PropertyDeclaration|
1025
ts.PropertySignature,
@@ -22,7 +37,7 @@ export default class DeclarationTranspiler extends base.TranspilerBase {
2237
private extendsClass = false;
2338
private visitPromises = false;
2439
private containsPromises = false;
25-
private promiseMethods: ts.SignatureDeclaration[] = [];
40+
private promiseMembers: ts.SignatureDeclaration[] = [];
2641

2742
static NUM_FAKE_REST_PARAMETERS = 5;
2843

@@ -128,13 +143,13 @@ export default class DeclarationTranspiler extends base.TranspilerBase {
128143
}
129144
}
130145

131-
private visitName(name: ts.Node) {
146+
private visitName(name: ts.PropertyName|ts.BindingName) {
132147
if (base.getEnclosingClass(name) != null) {
133148
this.visit(name);
134149
return;
135150
}
136151
// Have to rewrite names in this case as we could have conflicts
137-
// due to needing to support multiple JS modules in a single JS module
152+
// due to needing to support multiple JS modules in a single Dart module
138153
if (!ts.isIdentifier(name)) {
139154
throw 'Internal error: unexpected function name kind:' + name.kind;
140155
}
@@ -327,16 +342,17 @@ export default class DeclarationTranspiler extends base.TranspilerBase {
327342

328343
orderedGroups.forEach((group: Array<ts.SignatureDeclaration>) => {
329344
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.
345+
// If the members in this group are Promise properties or Promise-returning methods and
346+
// this.visitPromises is false, skip visiting these members and add them to
347+
// this.promiseMembers. If the members in this group are/return Promises and
348+
// this.visitPromises is true, it means that this function is being called from
349+
// emitMembersAsExtensions and the members should now be visited.
334350
if (!this.visitPromises && base.isPromise(first.type)) {
335351
if (!this.containsPromises) {
336352
this.containsPromises = true;
337353
}
338354
group.forEach((declaration: ts.SignatureDeclaration) => {
339-
this.promiseMethods.push(declaration);
355+
this.promiseMembers.push(declaration);
340356
});
341357
return;
342358
}
@@ -529,7 +545,12 @@ export default class DeclarationTranspiler extends base.TranspilerBase {
529545
this.visitMergingOverloads(sourceFile.statements);
530546
if (this.containsPromises) {
531547
this.addImport('package:js/js_util.dart', 'promiseToFuture');
532-
this.emit(`@JS() abstract class Promise<T> {}\n`);
548+
// TODO(derekx): Move this definition of the Promise class to a package
549+
this.emit(`@JS()
550+
abstract class Promise<T> {
551+
external factory Promise(void executor(void resolve(T result), Function reject));
552+
external Promise then(void onFulfilled(T result), [Function onRejected]);
553+
}\n`);
533554
}
534555
break;
535556
case ts.SyntaxKind.ModuleBlock: {
@@ -540,26 +561,25 @@ export default class DeclarationTranspiler extends base.TranspilerBase {
540561
// We have to handle variable declaration lists differently in the case
541562
// of JS interop because Dart does not support external variables.
542563
let varDeclList = <ts.VariableDeclarationList>node;
543-
this.visitList(varDeclList.declarations, ';');
564+
this.visitList(varDeclList.declarations, ' ');
544565
} break;
545566
case ts.SyntaxKind.VariableDeclaration: {
546567
// We have to handle variable declarations differently in the case of JS
547568
// interop because Dart does not support external variables.
548569
let varDecl = <ts.VariableDeclaration>node;
549-
this.maybeEmitJsAnnotation(varDecl);
550-
this.emit('external');
551-
this.visit(varDecl.type);
552-
this.emit('get');
553-
this.visitName(varDecl.name);
570+
this.emitProperty({
571+
mode: emitPropertyMode.getter,
572+
declaration: varDecl,
573+
emitJsAnnotation: true,
574+
isExternal: true
575+
});
554576
if (!this.hasNodeFlag(varDecl, ts.NodeFlags.Const)) {
555-
this.emitNoSpace(';');
556-
this.maybeEmitJsAnnotation(varDecl);
557-
this.emit('external');
558-
this.emit('set');
559-
this.visitName(varDecl.name);
560-
this.emitNoSpace('(');
561-
this.visit(varDecl.type);
562-
this.emit('v)');
577+
this.emitProperty({
578+
mode: emitPropertyMode.setter,
579+
declaration: varDecl,
580+
emitJsAnnotation: true,
581+
isExternal: true
582+
});
563583
}
564584
} break;
565585
case ts.SyntaxKind.StringLiteral: {
@@ -651,9 +671,9 @@ export default class DeclarationTranspiler extends base.TranspilerBase {
651671
case ts.SyntaxKind.ExpressionWithTypeArguments: {
652672
let exprWithTypeArgs = <ts.ExpressionWithTypeArguments>node;
653673
let expr = exprWithTypeArgs.expression;
654-
if (expr.kind === ts.SyntaxKind.Identifier || expr.kind === ts.SyntaxKind.QualifiedName ||
655-
expr.kind === ts.SyntaxKind.PropertyAccessExpression) {
656-
this.fc.visitTypeName(expr as (ts.EntityName | ts.PropertyAccessExpression));
674+
if (ts.isIdentifier(expr) || ts.isQualifiedName(expr) ||
675+
ts.isPropertyAccessExpression(expr)) {
676+
this.fc.visitTypeName(expr);
657677
} else {
658678
this.visit(expr);
659679
}
@@ -743,7 +763,6 @@ export default class DeclarationTranspiler extends base.TranspilerBase {
743763
case ts.SyntaxKind.VariableStatement:
744764
let variableStmt = <ts.VariableStatement>node;
745765
this.visit(variableStmt.declarationList);
746-
this.emitNoSpace(';');
747766
break;
748767
case ts.SyntaxKind.SwitchStatement:
749768
case ts.SyntaxKind.ArrayLiteralExpression:
@@ -771,7 +790,7 @@ export default class DeclarationTranspiler extends base.TranspilerBase {
771790
if (name.kind !== ts.SyntaxKind.Identifier) {
772791
this.reportError(name, 'Unexpected name kind:' + name.kind);
773792
}
774-
this.fc.visitTypeName(<ts.Identifier>name);
793+
this.fc.visitTypeName(name);
775794
}
776795

777796
if (fn.typeParameters) {
@@ -815,30 +834,26 @@ export default class DeclarationTranspiler extends base.TranspilerBase {
815834
decl: ts.PropertyDeclaration|ts.ParameterDeclaration, isParameter?: boolean) {
816835
const hasValidName =
817836
!ts.isStringLiteral(decl.name) || base.isValidDartIdentifier(decl.name.text);
818-
const isStatic = base.isStatic(decl);
819837

820838
// TODO(derekx): Properties with names that contain special characters are currently ignored by
821839
// commenting them out. Determine a way to rename these properties in the future.
822840
this.maybeWrapInCodeComment({shouldWrap: !hasValidName, newLine: true}, () => {
823-
this.emit('external');
824-
if (isStatic) this.emit('static');
825-
this.visit(decl.type);
826-
this.emit('get');
827-
this.visitName(decl.name);
828-
this.emitNoSpace(';');
841+
this.emitProperty({
842+
mode: emitPropertyMode.getter,
843+
declaration: decl,
844+
emitJsAnnotation: false,
845+
isExternal: true
846+
});
829847
});
830848

831849
if (!base.isReadonly(decl)) {
832850
this.maybeWrapInCodeComment({shouldWrap: !hasValidName, newLine: true}, () => {
833-
this.emit('external');
834-
if (isStatic) this.emit('static');
835-
this.emit('set');
836-
this.visitName(decl.name);
837-
this.emitNoSpace('(');
838-
this.visit(decl.type);
839-
this.emit('v');
840-
this.emitNoSpace(')');
841-
this.emitNoSpace(';');
851+
this.emitProperty({
852+
mode: emitPropertyMode.setter,
853+
declaration: decl,
854+
emitJsAnnotation: false,
855+
isExternal: true
856+
});
842857
});
843858
}
844859
}
@@ -881,15 +896,15 @@ export default class DeclarationTranspiler extends base.TranspilerBase {
881896

882897
this.visitClassBody(decl, name);
883898
this.emit('}\n');
884-
if (this.promiseMethods.length) {
899+
if (this.promiseMembers.length) {
885900
const visitName = () => {
886901
this.visitClassLikeName(name, typeParameters, ts.createNodeArray(), false);
887902
};
888903
const visitNameOfExtensions = () => {
889904
this.visitClassLikeName(name, typeParameters, ts.createNodeArray(), true);
890905
};
891-
this.emitMethodsAsExtensions(name, visitName, visitNameOfExtensions, this.promiseMethods);
892-
this.promiseMethods = [];
906+
this.emitMembersAsExtensions(name, visitName, visitNameOfExtensions, this.promiseMembers);
907+
this.promiseMembers = [];
893908
}
894909
this.emit('\n');
895910
}
@@ -963,48 +978,106 @@ export default class DeclarationTranspiler extends base.TranspilerBase {
963978
this.emitNoSpace(';');
964979
}
965980

966-
private emitMethodsAsExtensions(
967-
className: ts.Identifier, visitName: () => void, visitNameOfExtensions: () => void,
981+
private emitProperty({mode, declaration, emitJsAnnotation, isExternal, emitBody}:
982+
emitPropertyOptions) {
983+
const {name, type} = declaration;
984+
985+
if (emitJsAnnotation) {
986+
this.maybeEmitJsAnnotation(declaration);
987+
}
988+
if (isExternal) {
989+
this.emit('external');
990+
}
991+
if (base.isStatic(declaration)) {
992+
this.emit('static');
993+
}
994+
if (mode === emitPropertyMode.getter) {
995+
this.visit(type);
996+
this.emit('get');
997+
this.visitName(name);
998+
} else if (mode === emitPropertyMode.setter) {
999+
this.emit('set');
1000+
this.visitName(name);
1001+
this.emitNoSpace('(');
1002+
this.visit(type);
1003+
this.emit('v)');
1004+
}
1005+
if (emitBody && !ts.isVariableDeclaration(declaration) && !ts.isParameter(declaration)) {
1006+
this.emit('{');
1007+
emitBody(declaration);
1008+
this.emit('}');
1009+
} else {
1010+
this.emitNoSpace(';');
1011+
}
1012+
}
1013+
1014+
private emitCastThisToPrivateClass(visitClassName: () => void) {
1015+
this.emit('final Object t = this;');
1016+
this.emit('final _');
1017+
visitClassName();
1018+
this.emit('tt = t;\n');
1019+
}
1020+
1021+
private emitMembersAsExtensions(
1022+
className: ts.Identifier, visitClassName: () => void, visitNameOfExtensions: () => void,
9681023
methods: ts.SignatureDeclaration[]) {
9691024
this.visitPromises = true;
1025+
this.fc.emitPromisesAsFutures = false;
9701026
// Emit private class containing external methods
9711027
this.emit(`@JS('${base.ident(className)}')`);
9721028
this.emit(`abstract class _`);
973-
visitName();
1029+
visitClassName();
9741030
this.emit('{');
9751031
const mergedMembers = this.visitMergingOverloads(ts.createNodeArray(Array.from(methods)));
9761032
this.emit('}\n');
1033+
this.fc.emitPromisesAsFutures = true;
9771034

9781035
// Emit extensions on public class to expose methods
9791036
this.emit('extension');
9801037
visitNameOfExtensions();
9811038
this.emit('on');
982-
visitName();
1039+
visitClassName();
9831040
this.emit('{');
9841041
for (const merged of mergedMembers) {
9851042
const declaration = merged.mergedDeclaration;
986-
if (!base.isPromise(declaration.type)) {
987-
continue;
988-
}
989-
this.emit('Future');
990-
if (ts.isTypeReferenceNode(declaration.type)) {
991-
this.maybeVisitTypeArguments(declaration.type);
1043+
if (ts.isPropertyDeclaration(declaration) || ts.isPropertySignature(declaration)) {
1044+
this.emitProperty({
1045+
mode: emitPropertyMode.getter,
1046+
declaration,
1047+
emitJsAnnotation: false,
1048+
isExternal: false,
1049+
emitBody: () => {
1050+
this.emitCastThisToPrivateClass(visitClassName);
1051+
this.emitExtensionGetterBody(declaration);
1052+
}
1053+
});
1054+
if (!base.isReadonly(declaration)) {
1055+
this.emitProperty({
1056+
mode: emitPropertyMode.setter,
1057+
declaration,
1058+
emitJsAnnotation: false,
1059+
isExternal: false,
1060+
emitBody: () => {
1061+
this.emitCastThisToPrivateClass(visitClassName);
1062+
this.emitExtensionSetterBody(declaration);
1063+
}
1064+
});
1065+
}
1066+
} else if (ts.isMethodDeclaration(declaration) || ts.isMethodSignature(declaration)) {
1067+
this.visit(declaration.type);
1068+
this.visitName(declaration.name);
1069+
this.visitParameters(declaration.parameters, {namesOnly: false});
1070+
this.emit('{');
1071+
this.emitCastThisToPrivateClass(visitClassName);
1072+
this.emitExtensionMethodBody(merged);
1073+
this.emit('}\n');
9921074
}
993-
this.emit(base.ident(declaration.name));
994-
this.visitParameters(declaration.parameters, {namesOnly: false});
995-
this.emit('{');
996-
this.emit('final Object t = this;');
997-
this.emit('final _');
998-
this.fc.visitTypeName(className);
999-
this.emit('tt = t;\n');
1000-
this.emitExtensionBody(merged);
1001-
this.emit('}\n');
10021075
}
10031076
this.emit('}\n');
10041077
this.visitPromises = false;
10051078
}
10061079

1007-
private emitExtensionBody({constituents, mergedDeclaration}: MergedMember) {
1080+
private emitExtensionMethodBody({constituents, mergedDeclaration}: MergedMember) {
10081081
// Determine all valid arties of this method by going through the overloaded signatures
10091082
const arities: Set<number> = new Set();
10101083
for (const constituent of constituents) {
@@ -1031,15 +1104,42 @@ export default class DeclarationTranspiler extends base.TranspilerBase {
10311104
this.emit('== null');
10321105
}
10331106
this.emit(') {');
1034-
this.emit(`return promiseToFuture(tt.${base.ident(mergedDeclaration.name)}`);
1107+
this.emit('return promiseToFuture(');
1108+
this.emit(PRIVATE_CLASS_INSTANCE_IN_EXTENSIONS);
1109+
this.emit('.');
1110+
this.visitName(mergedDeclaration.name);
10351111
this.visitParameters(ts.createNodeArray(suppliedParameters), {namesOnly: true});
10361112
this.emit('); }\n');
10371113
} else {
10381114
// No parameters were omitted, no null checks are necessary for this call
1039-
this.emit(`return promiseToFuture(tt.${base.ident(mergedDeclaration.name)}`);
1115+
this.emit('return promiseToFuture(');
1116+
this.emit(PRIVATE_CLASS_INSTANCE_IN_EXTENSIONS);
1117+
this.emit('.');
1118+
this.visitName(mergedDeclaration.name);
10401119
this.visitParameters(ts.createNodeArray(mergedDeclaration.parameters), {namesOnly: true});
10411120
this.emit(');\n');
10421121
}
10431122
}
10441123
}
1124+
1125+
private emitExtensionGetterBody(declaration: ts.PropertyDeclaration|ts.PropertySignature) {
1126+
this.emit('return promiseToFuture(');
1127+
this.emit(PRIVATE_CLASS_INSTANCE_IN_EXTENSIONS);
1128+
this.emit('.');
1129+
this.visitName(declaration.name);
1130+
this.emit(');');
1131+
}
1132+
1133+
private emitExtensionSetterBody(declaration: ts.PropertyDeclaration|ts.PropertySignature) {
1134+
this.emit(PRIVATE_CLASS_INSTANCE_IN_EXTENSIONS);
1135+
this.emit('.');
1136+
this.visitName(declaration.name);
1137+
this.emit('=');
1138+
// To emit the call to the Promise constructor, we need to temporarily disable
1139+
// this.fc.emitPromisesAsFutures
1140+
this.fc.emitPromisesAsFutures = false;
1141+
this.visit(declaration.type);
1142+
this.fc.emitPromisesAsFutures = true;
1143+
this.emit('(allowInterop((resolve, reject) { v.then(resolve, onError: reject); }));');
1144+
}
10451145
}

0 commit comments

Comments
 (0)