Skip to content

Commit 69221fe

Browse files
srujzsCommit Bot
authored and
Commit Bot
committed
[pkg:js] Create mock in createStaticInteropMock
Bug: #49351 After static checks have passed, adds functionality for a JS object literal to mock a @staticInterop class using a Dart implementation. Fields, getters, setters, and methods' names are added to the object literal, and their values are closures which call the Dart mock's members. Change-Id: Ie2ef27179eb79039d3aa28737b246c5091f4beb6 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/257160 Reviewed-by: Riley Porter <[email protected]>
1 parent 093cf19 commit 69221fe

File tree

1 file changed

+253
-15
lines changed

1 file changed

+253
-15
lines changed

pkg/_js_interop_checks/lib/src/transformations/static_interop_mock_creator.dart

Lines changed: 253 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import 'package:_fe_analyzer_shared/src/messages/codes.dart'
1616
LocatedMessage,
1717
templateJsInteropStaticInteropMockMissingOverride,
1818
templateJsInteropStaticInteropMockExternalExtensionMemberConflict;
19-
import 'package:_js_interop_checks/src/js_interop.dart';
19+
import 'package:_js_interop_checks/src/js_interop.dart' as js_interop;
2020

2121
class _ExtensionVisitor extends RecursiveVisitor {
2222
final Map<Reference, Extension> staticInteropClassesWithExtensions;
@@ -30,7 +30,7 @@ class _ExtensionVisitor extends RecursiveVisitor {
3030
// and this code needs to be refactored to handle multiple extensions.
3131
var onType = extension.onType;
3232
if (onType is InterfaceType &&
33-
hasStaticInteropAnnotation(onType.classNode)) {
33+
js_interop.hasStaticInteropAnnotation(onType.classNode)) {
3434
if (!staticInteropClassesWithExtensions.containsKey(onType.className)) {
3535
staticInteropClassesWithExtensions[onType.className] = extension;
3636
}
@@ -40,15 +40,34 @@ class _ExtensionVisitor extends RecursiveVisitor {
4040
}
4141

4242
class StaticInteropMockCreator extends Transformer {
43+
final Procedure _allowInterop;
44+
final Procedure _callMethod;
45+
final Procedure _createStaticInteropMock;
46+
final DiagnosticReporter<Message, LocatedMessage> _diagnosticReporter;
4347
late final _ExtensionVisitor _extensionVisitor;
48+
final InterfaceType _functionType;
49+
final Procedure _getProperty;
50+
final Procedure _globalThis;
51+
final InterfaceType _objectType;
52+
final Procedure _setProperty;
4453
final Map<Reference, Extension> _staticInteropClassesWithExtensions = {};
4554
final TypeEnvironment _typeEnvironment;
46-
final DiagnosticReporter<Message, LocatedMessage> _diagnosticReporter;
47-
final Procedure _createStaticInteropMock;
4855

4956
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') {
5271
_extensionVisitor = _ExtensionVisitor(_staticInteropClassesWithExtensions);
5372
}
5473

@@ -64,7 +83,8 @@ class StaticInteropMockCreator extends Transformer {
6483
var dartType = typeArguments[1];
6584
var typeArgumentsError = false;
6685
if (staticInteropType is! InterfaceType ||
67-
!hasStaticInteropAnnotation(staticInteropType.classNode)) {
86+
staticInteropType.declaredNullability != Nullability.nonNullable ||
87+
!js_interop.hasStaticInteropAnnotation(staticInteropType.classNode)) {
6888
_diagnosticReporter.report(
6989
templateJsInteropStaticInteropMockNotStaticInteropType.withArguments(
7090
staticInteropType, true),
@@ -74,9 +94,10 @@ class StaticInteropMockCreator extends Transformer {
7494
typeArgumentsError = true;
7595
}
7696
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)) {
80101
_diagnosticReporter.report(
81102
templateJsInteropStaticInteropMockNotDartInterfaceType.withArguments(
82103
dartType, true),
@@ -233,7 +254,7 @@ class StaticInteropMockCreator extends Transformer {
233254
}
234255

235256
// CFE creates static procedures for each extension member.
236-
var interopMember = interopDescriptor.member.node as Procedure;
257+
var interopMember = interopDescriptor.member.asProcedure;
237258
DartType getGetterFunctionType(DartType getterType) {
238259
return FunctionType([], getterType, Nullability.nonNullable);
239260
}
@@ -281,8 +302,225 @@ class StaticInteropMockCreator extends Transformer {
281302
}
282303
// The interfaces do not conform and therefore we can't create a mock.
283304
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;
286524
}
287525
}
288526

@@ -352,7 +590,7 @@ extension _StaticInteropClassExtension on Class {
352590
Map<ExtensionMemberDescriptor, Class> descriptorToClass,
353591
Map<Reference, Extension> staticInteropClassesWithExtensions,
354592
TypeEnvironment typeEnvironment) {
355-
assert(hasStaticInteropAnnotation(this));
593+
assert(js_interop.hasStaticInteropAnnotation(this));
356594
var classes = <Class>{};
357595
// Compute a map of all the possible descriptors available in this type and
358596
// the supertypes.
@@ -431,5 +669,5 @@ extension ExtensionMemberDescriptorExtension on ExtensionMemberDescriptor {
431669
bool get isSetter => this.kind == ExtensionMemberKind.Setter;
432670
bool get isMethod => this.kind == ExtensionMemberKind.Method;
433671

434-
bool get isExternal => (this.member.node as Procedure).isExternal;
672+
bool get isExternal => (this.member.asProcedure).isExternal;
435673
}

0 commit comments

Comments
 (0)