@@ -16,7 +16,7 @@ import 'package:_fe_analyzer_shared/src/messages/codes.dart'
16
16
LocatedMessage,
17
17
templateJsInteropStaticInteropMockMissingOverride,
18
18
templateJsInteropStaticInteropMockExternalExtensionMemberConflict;
19
- import 'package:_js_interop_checks/src/js_interop.dart' ;
19
+ import 'package:_js_interop_checks/src/js_interop.dart' as js_interop ;
20
20
21
21
class _ExtensionVisitor extends RecursiveVisitor {
22
22
final Map <Reference , Extension > staticInteropClassesWithExtensions;
@@ -30,7 +30,7 @@ class _ExtensionVisitor extends RecursiveVisitor {
30
30
// and this code needs to be refactored to handle multiple extensions.
31
31
var onType = extension .onType;
32
32
if (onType is InterfaceType &&
33
- hasStaticInteropAnnotation (onType.classNode)) {
33
+ js_interop. hasStaticInteropAnnotation (onType.classNode)) {
34
34
if (! staticInteropClassesWithExtensions.containsKey (onType.className)) {
35
35
staticInteropClassesWithExtensions[onType.className] = extension ;
36
36
}
@@ -40,15 +40,34 @@ class _ExtensionVisitor extends RecursiveVisitor {
40
40
}
41
41
42
42
class StaticInteropMockCreator extends Transformer {
43
+ final Procedure _allowInterop;
44
+ final Procedure _callMethod;
45
+ final Procedure _createStaticInteropMock;
46
+ final DiagnosticReporter <Message , LocatedMessage > _diagnosticReporter;
43
47
late final _ExtensionVisitor _extensionVisitor;
48
+ final InterfaceType _functionType;
49
+ final Procedure _getProperty;
50
+ final Procedure _globalThis;
51
+ final InterfaceType _objectType;
52
+ final Procedure _setProperty;
44
53
final Map <Reference , Extension > _staticInteropClassesWithExtensions = {};
45
54
final TypeEnvironment _typeEnvironment;
46
- final DiagnosticReporter <Message , LocatedMessage > _diagnosticReporter;
47
- final Procedure _createStaticInteropMock;
48
55
49
56
StaticInteropMockCreator (this ._typeEnvironment, this ._diagnosticReporter)
50
- : _createStaticInteropMock = _typeEnvironment.coreTypes.index
51
- .getTopLevelProcedure ('dart:js_util' , 'createStaticInteropMock' ) {
57
+ : _allowInterop = _typeEnvironment.coreTypes.index
58
+ .getTopLevelProcedure ('dart:js' , 'allowInterop' ),
59
+ _callMethod = _typeEnvironment.coreTypes.index
60
+ .getTopLevelProcedure ('dart:js_util' , 'callMethod' ),
61
+ _createStaticInteropMock = _typeEnvironment.coreTypes.index
62
+ .getTopLevelProcedure ('dart:js_util' , 'createStaticInteropMock' ),
63
+ _functionType = _typeEnvironment.coreTypes.functionNonNullableRawType,
64
+ _getProperty = _typeEnvironment.coreTypes.index
65
+ .getTopLevelProcedure ('dart:js_util' , 'getProperty' ),
66
+ _globalThis = _typeEnvironment.coreTypes.index
67
+ .getTopLevelProcedure ('dart:js_util' , 'get:globalThis' ),
68
+ _objectType = _typeEnvironment.coreTypes.objectNonNullableRawType,
69
+ _setProperty = _typeEnvironment.coreTypes.index
70
+ .getTopLevelProcedure ('dart:js_util' , 'setProperty' ) {
52
71
_extensionVisitor = _ExtensionVisitor (_staticInteropClassesWithExtensions);
53
72
}
54
73
@@ -64,7 +83,8 @@ class StaticInteropMockCreator extends Transformer {
64
83
var dartType = typeArguments[1 ];
65
84
var typeArgumentsError = false ;
66
85
if (staticInteropType is ! InterfaceType ||
67
- ! hasStaticInteropAnnotation (staticInteropType.classNode)) {
86
+ staticInteropType.declaredNullability != Nullability .nonNullable ||
87
+ ! js_interop.hasStaticInteropAnnotation (staticInteropType.classNode)) {
68
88
_diagnosticReporter.report (
69
89
templateJsInteropStaticInteropMockNotStaticInteropType.withArguments (
70
90
staticInteropType, true ),
@@ -74,9 +94,10 @@ class StaticInteropMockCreator extends Transformer {
74
94
typeArgumentsError = true ;
75
95
}
76
96
if (dartType is ! InterfaceType ||
77
- hasJSInteropAnnotation (dartType.classNode) ||
78
- hasStaticInteropAnnotation (dartType.classNode) ||
79
- hasAnonymousAnnotation (dartType.classNode)) {
97
+ dartType.declaredNullability != Nullability .nonNullable ||
98
+ js_interop.hasJSInteropAnnotation (dartType.classNode) ||
99
+ js_interop.hasStaticInteropAnnotation (dartType.classNode) ||
100
+ js_interop.hasAnonymousAnnotation (dartType.classNode)) {
80
101
_diagnosticReporter.report (
81
102
templateJsInteropStaticInteropMockNotDartInterfaceType.withArguments (
82
103
dartType, true ),
@@ -233,7 +254,7 @@ class StaticInteropMockCreator extends Transformer {
233
254
}
234
255
235
256
// CFE creates static procedures for each extension member.
236
- var interopMember = interopDescriptor.member.node as Procedure ;
257
+ var interopMember = interopDescriptor.member.asProcedure ;
237
258
DartType getGetterFunctionType (DartType getterType) {
238
259
return FunctionType ([], getterType, Nullability .nonNullable);
239
260
}
@@ -281,8 +302,225 @@ class StaticInteropMockCreator extends Transformer {
281
302
}
282
303
// The interfaces do not conform and therefore we can't create a mock.
283
304
if (conformanceError) return node;
284
- // TODO(srujzs): Create a mocking object.
285
- return super .visitStaticInvocation (node);
305
+ // Everything conforms, we can safely create a mock and replace this
306
+ // invocation with it.
307
+ return _createMock (
308
+ node, nameToDescriptors, descriptorToClass, dartMemberMap);
309
+ }
310
+
311
+ TreeNode _createMock (
312
+ StaticInvocation node,
313
+ Map <String , List <ExtensionMemberDescriptor >> nameToDescriptors,
314
+ Map <ExtensionMemberDescriptor , Class > descriptorToClass,
315
+ Map <String , Member > dartMemberMap) {
316
+ var block = < Statement > [];
317
+ assert (node.arguments.positional.length == 1 );
318
+ var interopType = node.arguments.types[0 ];
319
+ var dartType = node.arguments.types[1 ];
320
+
321
+ var dartMock = VariableDeclaration ('#dartMock' ,
322
+ initializer: node.arguments.positional[0 ], type: dartType)
323
+ ..fileOffset = node.fileOffset
324
+ ..parent = node.parent;
325
+ block.add (dartMock);
326
+
327
+ // Get the global 'Object' property.
328
+ StaticInvocation getObjectProperty () => StaticInvocation (
329
+ _getProperty,
330
+ Arguments ([StaticGet (_globalThis), StringLiteral ('Object' )],
331
+ types: [_objectType]));
332
+
333
+ // Get a fresh object literal.
334
+ // TODO(srujzs): Add prototype option for instance checks.
335
+ StaticInvocation getLiteral () {
336
+ return StaticInvocation (
337
+ _callMethod,
338
+ Arguments ([
339
+ getObjectProperty (),
340
+ StringLiteral ('create' ),
341
+ ListLiteral ([NullLiteral ()]),
342
+ ], types: [
343
+ _objectType
344
+ ]));
345
+ }
346
+
347
+ var jsMock = VariableDeclaration ('#jsMock' ,
348
+ initializer: AsExpression (getLiteral (), interopType), type: interopType)
349
+ ..fileOffset = node.fileOffset
350
+ ..parent = node.parent;
351
+ block.add (jsMock);
352
+
353
+ // Keep a map of all the mappings we use for `Object.defineProperty`. It's
354
+ // possible that different descriptors might have the same rename, and it's
355
+ // invalid to redefine a property. This is used in `createAndOrAddToMapping`
356
+ // below.
357
+ var jsNameToGetSetMap = < String , VariableDeclaration > {};
358
+ for (var descriptorName in nameToDescriptors.keys) {
359
+ var descriptors = nameToDescriptors[descriptorName]! ;
360
+ var descriptor = descriptors[0 ];
361
+ // Do any necessary renaming from the `@JS()` annotation.
362
+ String getJSName (ExtensionMemberDescriptor desc) {
363
+ var name = js_interop.getJSName (desc.member.asProcedure);
364
+ return name.isEmpty ? descriptorName : name;
365
+ }
366
+
367
+ ExpressionStatement setProperty (VariableGet jsObject, String propertyName,
368
+ StaticInvocation wrappedValue) {
369
+ // `setProperty(jsObject, propertyName, wrappedValue)`
370
+ return ExpressionStatement (StaticInvocation (
371
+ _setProperty,
372
+ Arguments ([jsObject, StringLiteral (propertyName), wrappedValue],
373
+ types: [_objectType])))
374
+ ..fileOffset = node.fileOffset
375
+ ..parent = node.parent;
376
+ }
377
+
378
+ var jsName = getJSName (descriptor);
379
+ if (descriptor.isMethod) {
380
+ var target = dartMemberMap[descriptorName]! as Procedure ;
381
+ // `setProperty(jsMock, jsName, allowInterop(dartMock.tearoffMethod))`
382
+ block.add (setProperty (
383
+ VariableGet (jsMock),
384
+ jsName,
385
+ StaticInvocation (
386
+ _allowInterop,
387
+ Arguments ([
388
+ InstanceTearOff (InstanceAccessKind .Instance ,
389
+ VariableGet (dartMock), target.name,
390
+ interfaceTarget: target, resultType: target.getterType)
391
+ ], types: [
392
+ _functionType
393
+ ]))));
394
+ } else {
395
+ // Create the mapping from `get` and `set` to their `dartMock` calls to
396
+ // be used in `Object.defineProperty`.
397
+
398
+ // Add the given descriptor to the mapping that corresponds to the given
399
+ // JS name that is used by `Object.defineProperty`. In order to conform
400
+ // to that API, this function defines 'get' or 'set' properties on a
401
+ // given object literal.
402
+ // The AST code looks like:
403
+ //
404
+ // ```
405
+ // setProperty(getSetMap, 'get', allowInterop(() {
406
+ // return dartMock.getter;
407
+ // }));
408
+ // ```
409
+ //
410
+ // in the case of a getter and:
411
+ //
412
+ // ```
413
+ // setProperty(getSetMap, 'set', allowInterop((val) {
414
+ // dartMock.setter = val;
415
+ // }));
416
+ // ```
417
+ //
418
+ // in the case of a setter.
419
+ //
420
+ // In the case where a mapping does not exist yet for the JS name, a new
421
+ // VariableDeclaration is created and added to the block of statements.
422
+ ExpressionStatement createAndOrAddToMapping (
423
+ ExtensionMemberDescriptor desc,
424
+ String jsName,
425
+ List <Statement > block) {
426
+ if (! jsNameToGetSetMap.containsKey (jsName)) {
427
+ jsNameToGetSetMap[jsName] = VariableDeclaration ('#${jsName }Mapping' ,
428
+ initializer: getLiteral (), type: _objectType)
429
+ ..fileOffset = node.fileOffset
430
+ ..parent = node.parent;
431
+ block.add (jsNameToGetSetMap[jsName]! );
432
+ }
433
+ var getSetMap = jsNameToGetSetMap[jsName]! ;
434
+ var dartTarget = desc.isGetter
435
+ ? dartMemberMap[descriptorName]!
436
+ : dartMemberMap[descriptorName + '=' ]! ;
437
+ // Parameter needed in case the descriptor is a setter.
438
+ var setterParameter =
439
+ VariableDeclaration ('#val' , type: dartTarget.setterType)
440
+ ..fileOffset = node.fileOffset
441
+ ..parent = node.parent;
442
+ return setProperty (
443
+ VariableGet (getSetMap),
444
+ desc.isGetter ? 'get' : 'set' ,
445
+ desc.isGetter
446
+ ? StaticInvocation (
447
+ _allowInterop,
448
+ Arguments ([
449
+ FunctionExpression (FunctionNode (ReturnStatement (
450
+ InstanceGet (InstanceAccessKind .Instance ,
451
+ VariableGet (dartMock), dartTarget.name,
452
+ interfaceTarget: dartTarget,
453
+ resultType: dartTarget.getterType))))
454
+ ], types: [
455
+ _functionType
456
+ ]))
457
+ : StaticInvocation (
458
+ _allowInterop,
459
+ Arguments ([
460
+ FunctionExpression (FunctionNode (
461
+ ExpressionStatement (InstanceSet (
462
+ InstanceAccessKind .Instance ,
463
+ VariableGet (dartMock),
464
+ dartTarget.name,
465
+ VariableGet (setterParameter),
466
+ interfaceTarget: dartTarget)),
467
+ positionalParameters: [setterParameter]))
468
+ ], types: [
469
+ _functionType
470
+ ])));
471
+ }
472
+
473
+ var jsName = getJSName (descriptor);
474
+ block.add (createAndOrAddToMapping (descriptor, jsName, block));
475
+ if (descriptors.length == 2 ) {
476
+ var secondDescriptor = descriptors[1 ];
477
+ var secondJsName = getJSName (secondDescriptor);
478
+ block.add (
479
+ createAndOrAddToMapping (secondDescriptor, secondJsName, block));
480
+ if (secondJsName != jsName) {
481
+ // Getter and setter's JS names don't match, we will use the new
482
+ // mapping. This is likely a bug, so print a warning but proceed
483
+ // anyways.
484
+ var classRef =
485
+ descriptorToClass[nameToDescriptors[descriptorName]! [0 ]]! ;
486
+ print ('WARNING: ${classRef .name } has getter and setter named '
487
+ '$descriptorName , but do not share the same JS name: $jsName '
488
+ 'and $secondJsName . Proceeding anyways...' );
489
+ }
490
+ }
491
+ }
492
+ }
493
+ // Call `Object.defineProperty` to define the descriptor name with the 'get'
494
+ // and/or 'set' mapping. This allows us to treat get/set semantics as
495
+ // methods.
496
+ for (var jsName in jsNameToGetSetMap.keys) {
497
+ block.add (ExpressionStatement (StaticInvocation (
498
+ _callMethod,
499
+ Arguments ([
500
+ getObjectProperty (),
501
+ StringLiteral ('defineProperty' ),
502
+ ListLiteral ([
503
+ VariableGet (jsMock),
504
+ StringLiteral (jsName),
505
+ VariableGet (jsNameToGetSetMap[jsName]! )
506
+ ])
507
+ ], types: [
508
+ VoidType ()
509
+ ])))
510
+ ..fileOffset = node.fileOffset
511
+ ..parent = node.parent);
512
+ }
513
+
514
+ block.add (ReturnStatement (VariableGet (jsMock)));
515
+ // Return a call to evaluate the entire block of code and return the JS mock
516
+ // that was created.
517
+ return FunctionInvocation (
518
+ FunctionAccessKind .Function ,
519
+ FunctionExpression (FunctionNode (Block (block), returnType: interopType)),
520
+ Arguments ([]),
521
+ functionType: FunctionType ([], interopType, Nullability .nonNullable))
522
+ ..fileOffset = node.fileOffset
523
+ ..parent = node.parent;
286
524
}
287
525
}
288
526
@@ -352,7 +590,7 @@ extension _StaticInteropClassExtension on Class {
352
590
Map <ExtensionMemberDescriptor , Class > descriptorToClass,
353
591
Map <Reference , Extension > staticInteropClassesWithExtensions,
354
592
TypeEnvironment typeEnvironment) {
355
- assert (hasStaticInteropAnnotation (this ));
593
+ assert (js_interop. hasStaticInteropAnnotation (this ));
356
594
var classes = < Class > {};
357
595
// Compute a map of all the possible descriptors available in this type and
358
596
// the supertypes.
@@ -431,5 +669,5 @@ extension ExtensionMemberDescriptorExtension on ExtensionMemberDescriptor {
431
669
bool get isSetter => this .kind == ExtensionMemberKind .Setter ;
432
670
bool get isMethod => this .kind == ExtensionMemberKind .Method ;
433
671
434
- bool get isExternal => (this .member.node as Procedure ).isExternal;
672
+ bool get isExternal => (this .member.asProcedure ).isExternal;
435
673
}
0 commit comments