Skip to content

Commit 7155f16

Browse files
srujzsCommit Queue
authored andcommitted
[dart2wasm] Support non-identifiers in @js renames
dart2js and ddc both have a rich enough JS AST to use dot or bracket notation when necessary. Therefore, both compilers have and do support @js renames that contain characters that aren't supported in dot notation e.g. `my-lib`. We should have dart2wasm also support such characters to be consistent, but prefer dot notation and avoid adding extra `globalThis` when possible. Change-Id: Iabf4d6f88dd23321388841f3a6b64758f856c68f Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/447964 Reviewed-by: Ömer Ağacan <[email protected]> Commit-Queue: Srujan Gaddam <[email protected]>
1 parent fe4c2c3 commit 7155f16

8 files changed

+207
-104
lines changed

pkg/dart2wasm/lib/js/interop_specializer.dart

Lines changed: 97 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ abstract class _Specializer {
2525
final InteropSpecializerFactory factory;
2626
final Procedure interopMethod;
2727
final String jsString;
28-
late final bool firstParameterIsObject =
28+
late final bool isInstanceMember =
2929
factory._extensionIndex.isInstanceInteropMember(interopMethod);
3030

3131
_Specializer(this.factory, this.interopMethod, this.jsString);
@@ -53,25 +53,31 @@ abstract class _Specializer {
5353

5454
/// Returns the string that will be the body of the JS trampoline.
5555
///
56-
/// [object] is the callee if there is one for this config. [callArguments] is
57-
/// the remaining arguments of the `interopMethod`.
58-
String bodyString(String object, List<String> callArguments);
56+
/// [receiver] is the callee if there is one for this config. If empty, it's
57+
/// assumed to be `globalThis`, which the implementation may elide.
58+
/// [callArguments] is the remaining arguments of the `interopMethod`.
59+
String bodyString(String receiver, List<String> callArguments);
5960

6061
/// Compute and return the JS trampoline string needed for this method
6162
/// lowering.
63+
// TODO(srujzs): We check whether this specializer is a constructor, setter,
64+
// and an instance member, as well as assert that we don't pass the wrong
65+
// receivers into `bodyString` calls. This feel like a code smell and likely
66+
// means we should push this method further down into the implementations,
67+
// possibly with more intermediate types to reduce code duplication.
6268
String generateJS(List<String> parameterNames) {
63-
final object = isConstructor
69+
final receiver = isConstructor
6470
? ''
65-
: firstParameterIsObject
71+
: isInstanceMember
6672
? parameterNames[0]
6773
: 'globalThis';
6874
final callArguments =
69-
firstParameterIsObject ? parameterNames.sublist(1) : parameterNames;
75+
isInstanceMember ? parameterNames.sublist(1) : parameterNames;
7076
final callArgumentsString = callArguments.join(',');
71-
String functionParameters = firstParameterIsObject
72-
? '$object${callArguments.isEmpty ? '' : ',$callArgumentsString'}'
77+
String functionParameters = isInstanceMember
78+
? '$receiver${callArguments.isEmpty ? '' : ',$callArgumentsString'}'
7379
: callArgumentsString;
74-
final body = bodyString(object, callArguments);
80+
final body = bodyString(receiver, callArguments);
7581

7682
if (parametersNeedParens(parameterNames)) {
7783
functionParameters = '($functionParameters)';
@@ -135,6 +141,46 @@ abstract class _Specializer {
135141
interopMethod, () => {})[parameters.length] = dartProcedure;
136142
return dartProcedure;
137143
}
144+
145+
// Determines if a selector in an `@JS` rename can be used in dot notation as
146+
// an identifier. Note that this is conservative, and it's possible to have
147+
// other valid identifiers (such as Unicode characters) that don't match this
148+
// regex. Enumerating all such characters in `ID_Start` and `ID_Continue`
149+
// would make this a long regex. Almost any "real" rename will be ASCII, so
150+
// being conservative here is okay.
151+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#identifiers
152+
static final RegExp _dotNotationable = RegExp(r'^[A-Za-z_$][\w$]*$');
153+
154+
/// Given a [fullDottedString] that represents a target for a JS interop call,
155+
/// splits it and recombines the string into a valid JS property access,
156+
/// including the provider [receiver] at the beginning.
157+
///
158+
/// If [receiver] is empty, potentially adds a `globalThis` at the beginning
159+
/// if needed for bracket notation.
160+
String _splitSelectorsAndRecombine(
161+
{required String fullDottedString, required String receiver}) {
162+
final selectors = jsString.split('.');
163+
final validPropertyStr = StringBuffer('');
164+
validPropertyStr.write(receiver);
165+
for (var i = 0; i < selectors.length; i++) {
166+
final selector = selectors[i];
167+
if (_dotNotationable.hasMatch(selector)) {
168+
// Prefer dot notation when possible as it's fewer characters.
169+
if (validPropertyStr.isEmpty) {
170+
validPropertyStr.write(selector);
171+
} else {
172+
validPropertyStr.write(".$selector");
173+
}
174+
} else {
175+
if (validPropertyStr.isEmpty) {
176+
// Bracket notation needs a receiver.
177+
validPropertyStr.write('globalThis');
178+
}
179+
validPropertyStr.write("['$selector']");
180+
}
181+
}
182+
return validPropertyStr.toString();
183+
}
138184
}
139185

140186
/// Config class for interop members that get lowered on the procedure side.
@@ -176,8 +222,12 @@ class _ConstructorSpecializer extends _ProcedureSpecializer {
176222
bool get isSetter => false;
177223

178224
@override
179-
String bodyString(String object, List<String> callArguments) =>
180-
"new $jsString(${callArguments.join(',')})";
225+
String bodyString(String receiver, List<String> callArguments) {
226+
assert(receiver.isEmpty);
227+
final constructorStr = _splitSelectorsAndRecombine(
228+
fullDottedString: jsString, receiver: receiver);
229+
return "new $constructorStr(${callArguments.join(',')})";
230+
}
181231
}
182232

183233
class _GetterSpecializer extends _ProcedureSpecializer {
@@ -190,8 +240,11 @@ class _GetterSpecializer extends _ProcedureSpecializer {
190240
bool get isSetter => false;
191241

192242
@override
193-
String bodyString(String object, List<String> callArguments) =>
194-
'$object.$jsString';
243+
String bodyString(String receiver, List<String> callArguments) {
244+
assert(receiver.isNotEmpty);
245+
return _splitSelectorsAndRecombine(
246+
fullDottedString: jsString, receiver: receiver);
247+
}
195248
}
196249

197250
class _SetterSpecializer extends _ProcedureSpecializer {
@@ -204,8 +257,12 @@ class _SetterSpecializer extends _ProcedureSpecializer {
204257
bool get isSetter => true;
205258

206259
@override
207-
String bodyString(String object, List<String> callArguments) =>
208-
'$object.$jsString = ${callArguments[0]}';
260+
String bodyString(String receiver, List<String> callArguments) {
261+
assert(receiver.isNotEmpty);
262+
final setterStr = _splitSelectorsAndRecombine(
263+
fullDottedString: jsString, receiver: receiver);
264+
return '$setterStr = ${callArguments[0]}';
265+
}
209266
}
210267

211268
class _MethodSpecializer extends _ProcedureSpecializer {
@@ -218,8 +275,12 @@ class _MethodSpecializer extends _ProcedureSpecializer {
218275
bool get isSetter => false;
219276

220277
@override
221-
String bodyString(String object, List<String> callArguments) =>
222-
"$object.$jsString(${callArguments.join(',')})";
278+
String bodyString(String receiver, List<String> callArguments) {
279+
assert(receiver.isNotEmpty);
280+
final methodStr = _splitSelectorsAndRecombine(
281+
fullDottedString: jsString, receiver: receiver);
282+
return '$methodStr(${callArguments.join(',')})';
283+
}
223284
}
224285

225286
class _OperatorSpecializer extends _ProcedureSpecializer {
@@ -238,10 +299,11 @@ class _OperatorSpecializer extends _ProcedureSpecializer {
238299
};
239300

240301
@override
241-
String bodyString(String object, List<String> callArguments) {
302+
String bodyString(String receiver, List<String> callArguments) {
303+
assert(receiver.isNotEmpty);
242304
return isSetter
243-
? '$object[${callArguments[0]}] = ${callArguments[1]}'
244-
: '$object[${callArguments[0]}]';
305+
? '$receiver[${callArguments[0]}] = ${callArguments[1]}'
306+
: '$receiver[${callArguments[0]}]';
245307
}
246308
}
247309

@@ -302,8 +364,12 @@ class _ConstructorInvocationSpecializer
302364
bool get isSetter => false;
303365

304366
@override
305-
String bodyString(String object, List<String> callArguments) =>
306-
"new $jsString(${callArguments.join(',')})";
367+
String bodyString(String receiver, List<String> callArguments) {
368+
assert(receiver.isEmpty);
369+
final constructorStr = _splitSelectorsAndRecombine(
370+
fullDottedString: jsString, receiver: receiver);
371+
return "new $constructorStr(${callArguments.join(',')})";
372+
}
307373
}
308374

309375
class _MethodInvocationSpecializer extends _PositionalInvocationSpecializer {
@@ -317,8 +383,12 @@ class _MethodInvocationSpecializer extends _PositionalInvocationSpecializer {
317383
bool get isSetter => false;
318384

319385
@override
320-
String bodyString(String object, List<String> callArguments) =>
321-
"$object.$jsString(${callArguments.join(',')})";
386+
String bodyString(String receiver, List<String> callArguments) {
387+
assert(receiver.isNotEmpty);
388+
final methodStr = _splitSelectorsAndRecombine(
389+
fullDottedString: jsString, receiver: receiver);
390+
return '$methodStr(${callArguments.join(',')})';
391+
}
322392
}
323393

324394
/// Config class for object literals, which only use named arguments and are
@@ -361,7 +431,8 @@ class _ObjectLiteralSpecializer extends _InvocationSpecializer {
361431
}
362432

363433
@override
364-
String bodyString(String object, List<String> callArguments) {
434+
String bodyString(String receiver, List<String> callArguments) {
435+
assert(receiver.isEmpty);
365436
final keys = parameters.map(_jsKey).toList();
366437
final keyValuePairs = <String>[];
367438
for (int i = 0; i < callArguments.length; i++) {

tests/lib/js/static_interop_test/extension_type/external_extension_member_test.dart

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ extension E on ExtensionType {
1616
external String field;
1717
@JS('field')
1818
external String renamedField;
19-
@JS('nestedField.foo.field')
19+
@JS('nested-field.foo.field')
2020
external String nestedField;
2121
external final String finalField;
2222

@@ -26,32 +26,32 @@ extension E on ExtensionType {
2626
external String get renamedGetSet;
2727
@JS('getSet')
2828
external set renamedGetSet(String val);
29-
@JS('nestedGetSet.bar.getSet')
29+
@JS('nestedGetSet.1.getSet')
3030
external String get nestedGetSet;
31-
@JS('nestedGetSet.bar.getSet')
31+
@JS('nestedGetSet.1.getSet')
3232
external set nestedGetSet(String val);
3333

3434
external String method();
3535
external String differentArgsMethod(String a, [String b = '']);
3636
@JS('method')
3737
external String renamedMethod();
38-
@JS('nestedMethod.method')
38+
@JS('nested^method.method')
3939
external String nestedMethod();
4040
}
4141

4242
void main() {
4343
eval('''
4444
globalThis.ExtensionType = function ExtensionType() {
4545
this.field = 'field';
46-
this.nestedField = {
46+
this['nested-field'] = {
4747
foo: {
4848
field: 'nestedField'
4949
}
5050
};
5151
this.finalField = 'finalField';
5252
this.getSet = 'getSet';
5353
this.nestedGetSet = {
54-
bar: {
54+
'1': {
5555
getSet: 'nestedGetSet'
5656
}
5757
};
@@ -61,7 +61,7 @@ void main() {
6161
this.differentArgsMethod = function(a, b) {
6262
return a + b;
6363
}
64-
this.nestedMethod = {
64+
this['nested^method'] = {
6565
method: function() {
6666
return 'nestedMethod';
6767
}

tests/lib/js/static_interop_test/extension_type/external_member_test.dart

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ extension type External<T extends JSAny?, U extends Nested>._(JSObject _) {
1818
external String field;
1919
@JS('field')
2020
external String renamedField;
21-
@JS('nestedField.foo.field')
21+
@JS('nested-field.foo.field')
2222
external String nestedField;
2323
external final String finalField;
2424

@@ -28,16 +28,16 @@ extension type External<T extends JSAny?, U extends Nested>._(JSObject _) {
2828
external String get renamedGetSet;
2929
@JS('getSet')
3030
external set renamedGetSet(String val);
31-
@JS('nestedGetSet.bar.getSet')
31+
@JS('nestedGetSet.1.getSet')
3232
external String get nestedGetSet;
33-
@JS('nestedGetSet.bar.getSet')
33+
@JS('nestedGetSet.1.getSet')
3434
external set nestedGetSet(String val);
3535

3636
external String method();
3737
external String addMethod(String a, [String b]);
3838
@JS('method')
3939
external String renamedMethod();
40-
@JS('nestedMethod.method')
40+
@JS('nested^method.method')
4141
external String nestedMethod();
4242

4343
@JS('field')
@@ -72,15 +72,15 @@ void main() {
7272
eval('''
7373
globalThis.External = function External() {
7474
this.field = 'field';
75-
this.nestedField = {
75+
this['nested-field'] = {
7676
foo: {
7777
field: 'nestedField'
7878
}
7979
};
8080
this.finalField = 'finalField';
8181
this.getSet = 'getSet';
8282
this.nestedGetSet = {
83-
bar: {
83+
'1': {
8484
getSet: 'nestedGetSet'
8585
}
8686
};
@@ -93,7 +93,7 @@ void main() {
9393
}
9494
return a + b;
9595
}
96-
this.nestedMethod = {
96+
this['nested^method'] = {
9797
method: function() {
9898
return 'nestedMethod';
9999
}

0 commit comments

Comments
 (0)