@@ -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
183233class _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
197250class _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
211268class _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
225286class _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
309375class _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++ ) {
0 commit comments