From 35cae2d4dac4fb7add6c74e1705120c43932d64d Mon Sep 17 00:00:00 2001 From: Srujan Gaddam Date: Wed, 26 Oct 2022 16:34:38 +0000 Subject: [PATCH] [pkg:js] Rename export and mock transformers and separate them Per previous review comment - this separates the three components into separate files. Change-Id: I601bd9ac29ddd032e913a60ce2b5b7dd81e39efc Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/264603 Reviewed-by: Riley Porter Reviewed-by: Joshua Litt --- .../lib/js_interop_checks.dart | 2 +- .../src/transformations/export_checker.dart | 305 ++++++ .../src/transformations/export_creator.dart | 350 +++++++ .../static_interop_mock_creator.dart | 932 ------------------ .../static_interop_mock_validator.dart | 303 ++++++ .../lib/src/kernel/dart2js_target.dart | 10 +- pkg/dart2wasm/lib/target.dart | 10 +- pkg/dev_compiler/lib/src/kernel/target.dart | 10 +- 8 files changed, 971 insertions(+), 951 deletions(-) create mode 100644 pkg/_js_interop_checks/lib/src/transformations/export_checker.dart create mode 100644 pkg/_js_interop_checks/lib/src/transformations/export_creator.dart delete mode 100644 pkg/_js_interop_checks/lib/src/transformations/static_interop_mock_creator.dart create mode 100644 pkg/_js_interop_checks/lib/src/transformations/static_interop_mock_validator.dart diff --git a/pkg/_js_interop_checks/lib/js_interop_checks.dart b/pkg/_js_interop_checks/lib/js_interop_checks.dart index cac8b428897..6f4937dccbf 100644 --- a/pkg/_js_interop_checks/lib/js_interop_checks.dart +++ b/pkg/_js_interop_checks/lib/js_interop_checks.dart @@ -28,7 +28,7 @@ import 'package:_fe_analyzer_shared/src/messages/codes.dart' templateJsInteropNativeClassInAnnotation, templateJsInteropStaticInteropTrustTypesUsageNotAllowed, templateJsInteropStaticInteropTrustTypesUsedWithoutStaticInterop; -import 'package:_js_interop_checks/src/transformations/static_interop_mock_creator.dart'; +import 'package:_js_interop_checks/src/transformations/export_checker.dart'; import 'src/js_interop.dart'; diff --git a/pkg/_js_interop_checks/lib/src/transformations/export_checker.dart b/pkg/_js_interop_checks/lib/src/transformations/export_checker.dart new file mode 100644 index 00000000000..eaaf8a6533b --- /dev/null +++ b/pkg/_js_interop_checks/lib/src/transformations/export_checker.dart @@ -0,0 +1,305 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:kernel/ast.dart'; +import 'package:kernel/target/targets.dart'; +import 'package:_fe_analyzer_shared/src/messages/codes.dart' + show + Message, + LocatedMessage, + templateJsInteropExportDartInterfaceHasNonEmptyJSExportValue, + templateJsInteropExportDisallowedMember, + templateJsInteropExportMemberCollision, + templateJsInteropExportNoExportableMembers; +import 'package:_js_interop_checks/src/js_interop.dart' as js_interop; + +enum ExportStatus { + EXPORT_ERROR, + EXPORTABLE, + NON_EXPORTABLE, +} + +class _GetSet { + Member? getter; + Member? setter; + + _GetSet(this.getter, this.setter); +} + +class ExportChecker { + final DiagnosticReporter _diagnosticReporter; + final Map>> exportClassToMemberMap = {}; + final Map exportStatus = {}; + final Class _objectClass; + final Map> _overrideMap = {}; + // Store map of libraries to @staticInterop extensions, so that we can compute + // the class to extension map later. Prefer to do it this way so that modular + // compilation can invalidate recompiled extensions. + static final Map> libraryExtensionMap = {}; + + ExportChecker(this._diagnosticReporter, this._objectClass); + + /// Gets the getter and setter from the given [exports]. + /// + /// [exports] should be a set of members from the [exportClassToMemberMap]. If + /// missing a getter and/or setter, the corresponding field will be `null`. + _GetSet getGetterSetter(Set exports) { + assert(exports.isNotEmpty && exports.length <= 2); + Member? getter; + Member? setter; + + var firstExport = exports.first; + if (exports.length == 1) { + if (firstExport.isGetter) { + getter = firstExport; + } + if (firstExport.isSetter) { + setter = firstExport; + } + } else if (exports.length == 2) { + var secondExport = exports.elementAt(1); + // One of them could be a partially overridden non-final field, so + // determine the strict getter or setter first. + if (firstExport.isStrictGetter || secondExport.isStrictSetter) { + getter = firstExport; + setter = secondExport; + } else { + getter = secondExport; + setter = firstExport; + } + } + + return _GetSet(getter, setter); + } + + /// Calculates the overrides, including inheritance, for [cls]. + /// + /// Note that we use a map from the unique name (with setter renaming) to + /// avoid duplicate checks on classes, and to store the overrides. + void _collectOverrides(Class cls) { + if (_overrideMap.containsKey(cls.reference)) return; + Map memberMap; + var superclass = cls.superclass; + if (superclass != null && superclass != _objectClass) { + _collectOverrides(superclass); + memberMap = Map.from(_overrideMap[superclass.reference]!); + } else { + memberMap = {}; + } + // If this is a mixin application, fetch the members from the mixin. + var demangledCls = cls.isMixinApplication ? cls.mixin : cls; + for (var member in [ + ...demangledCls.procedures.where((proc) => proc.exportable), + ...demangledCls.fields.where((field) => field.exportable) + ]) { + var memberName = member.name.text; + if (member is Procedure && member.isSetter) { + memberMap[memberName + '='] = member; + } else { + if (member is Field && !member.isFinal) { + memberMap[memberName + '='] = member; + } + memberMap[memberName] = member; + } + } + _overrideMap[cls.reference] = memberMap; + } + + /// Determine if [cls] is exportable, and if so, compute the export members. + /// + /// + /// Check the following: + /// - If the class has a `@JSExport` annotation, the value should be empty. + /// - If the class has the annotation, it should have at least one exportable + /// member in the class or in any superclass (ignoring `Object`). + /// - Accounting for Dart overrides, the export member map of the class or + /// any of its superclasses do not contain unresolvable name collisions. An + /// explanation of the resolvable collisions is below. + void visitClass(Class cls) { + var classHasJSExport = js_interop.hasJSExportAnnotation(cls); + // If the class doesn't have the annotation or if the class wasn't marked + // when we visited the members and checked their annotations, there's + // nothing to do for this class. + if (!classHasJSExport && + exportStatus[cls.reference] != ExportStatus.EXPORTABLE) { + exportStatus[cls.reference] = ExportStatus.NON_EXPORTABLE; + return; + } + + if (classHasJSExport && js_interop.getJSExportName(cls).isNotEmpty) { + _diagnosticReporter.report( + templateJsInteropExportDartInterfaceHasNonEmptyJSExportValue + .withArguments(cls.name), + cls.fileOffset, + cls.name.length, + cls.location?.file); + exportStatus[cls.reference] = ExportStatus.EXPORT_ERROR; + } + + _collectOverrides(cls); + + var allExportableMembers = _overrideMap[cls.reference]!.values.where( + (member) => + // Only members that qualify are those that are exportable, and + // either their class has the annotation or they have it themselves. + member.exportable && + (js_interop.hasJSExportAnnotation(member) || + js_interop.hasJSExportAnnotation(member.enclosingClass!))); + var exports = >{}; + + // Store the exportable members. + for (var member in allExportableMembers) { + var exportName = member.exportPropertyName; + exports.putIfAbsent(exportName, () => {}).add(member); + } + + // Walk through the export map and determine if there are any unresolvable + // conflicts. + for (var exportName in exports.keys) { + var existingMembers = exports[exportName]!; + if (existingMembers.length == 1) continue; + if (existingMembers.length == 2) { + // There are two instances where you can resolve collisions: + // 1. One of the members is a non-final field, and the other one is + // either a strict getter or a strict setter that overrides part of + // that field. + // 2. One of the members is a strict getter, and the other one is a + // strict setter or vice versa. + // Any other case is an error to have more than 1 member per name. + bool isCollisionOkay(Member m1, Member m2) { + if (m1.isNonFinalField && + (m2.isStrictGetter || m2.isStrictSetter) && + // Is an override if the same name and across different classes. + (m1.name.text == m2.name.text && + m1.enclosingClass != m2.enclosingClass)) { + return true; + } else if (m1.isStrictGetter && m2.isStrictSetter) { + return true; + } + return false; + } + + var first = existingMembers.elementAt(0); + var second = existingMembers.elementAt(1); + if (isCollisionOkay(first, second) || isCollisionOkay(second, first)) { + continue; + } + } + // Sort to get deterministic order. + var sortedExistingMembers = + existingMembers.map((member) => member.toString()).toList()..sort(); + _diagnosticReporter.report( + templateJsInteropExportMemberCollision.withArguments( + exportName, sortedExistingMembers.join(', ')), + cls.fileOffset, + cls.name.length, + cls.location?.file); + exportStatus[cls.reference] = ExportStatus.EXPORT_ERROR; + } + + if (exports.isEmpty) { + _diagnosticReporter.report( + templateJsInteropExportNoExportableMembers.withArguments(cls.name), + cls.fileOffset, + cls.name.length, + cls.location?.file); + exportStatus[cls.reference] = ExportStatus.EXPORT_ERROR; + } + + exportClassToMemberMap[cls.reference] = exports; + exportStatus[cls.reference] ??= ExportStatus.EXPORTABLE; + } + + /// Check that the [member] can be exportable if it has an annotation, and if + /// so, mark the enclosing class as exportable. + void visitMember(Member member) { + var memberHasJSExportAnnotation = js_interop.hasJSExportAnnotation(member); + var cls = member.enclosingClass; + if (memberHasJSExportAnnotation) { + if (!member.exportable) { + _diagnosticReporter.report( + templateJsInteropExportDisallowedMember + .withArguments(member.name.text), + member.fileOffset, + member.name.text.length, + member.location?.file); + if (cls != null) { + exportStatus[cls.reference] = ExportStatus.EXPORT_ERROR; + } + } else { + // Mark as exportable so we know that the class has an exportable member + // when we process the class later. + if (cls != null) exportStatus[cls.reference] = ExportStatus.EXPORTABLE; + } + } + } + + void visitLibrary(Library library) { + for (var extension in library.extensions) { + var onType = extension.onType; + if (onType is InterfaceType && + js_interop.hasStaticInteropAnnotation(onType.classNode)) { + libraryExtensionMap + .putIfAbsent(library.reference, () => {}) + .add(extension); + } + } + } +} + +extension ExtensionMemberDescriptorExtension on ExtensionMemberDescriptor { + bool get isGetter => this.kind == ExtensionMemberKind.Getter; + bool get isSetter => this.kind == ExtensionMemberKind.Setter; + bool get isMethod => this.kind == ExtensionMemberKind.Method; + + bool get isExternal => (this.member.asProcedure).isExternal; +} + +extension ProcedureExtension on Procedure { + // We only care about concrete instance procedures. + bool get exportable => + !this.isAbstract && + !this.isStatic && + !this.isExtensionMember && + !this.isFactory && + !this.isExternal && + this.kind != ProcedureKind.Operator; +} + +extension FieldExtension on Field { + // We only care about concrete instance fields. + bool get exportable => !this.isAbstract && !this.isStatic && !this.isExternal; +} + +extension MemberExtension on Member { + // Get the property name that this member will be exported as. + String get exportPropertyName { + var rename = js_interop.getJSExportName(this); + return rename.isEmpty ? this.name.text : rename; + } + + bool get exportable => + (this is Procedure && (this as Procedure).exportable) || + (this is Field && (this as Field).exportable); + + // Only a getter and not a setter. + bool get isStrictGetter => + (this is Procedure && (this as Procedure).isGetter) || + (this is Field && (this as Field).isFinal); + + // Only a setter and not a getter. + bool get isStrictSetter => this is Procedure && (this as Procedure).isSetter; + + bool get isNonFinalField => this is Field && !(this as Field).isFinal; + + bool get isGetter => + this is Field || (this is Procedure && (this as Procedure).isGetter); + + bool get isSetter => + this.isNonFinalField || + (this is Procedure && (this as Procedure).isSetter); + + bool get isMethod => + this is Procedure && (this as Procedure).kind == ProcedureKind.Method; +} diff --git a/pkg/_js_interop_checks/lib/src/transformations/export_creator.dart b/pkg/_js_interop_checks/lib/src/transformations/export_creator.dart new file mode 100644 index 00000000000..f7942b9caf2 --- /dev/null +++ b/pkg/_js_interop_checks/lib/src/transformations/export_creator.dart @@ -0,0 +1,350 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:front_end/src/fasta/fasta_codes.dart' + show + templateJsInteropExportInvalidInteropTypeArgument, + templateJsInteropExportInvalidTypeArgument; +import 'package:kernel/ast.dart'; +import 'package:kernel/target/targets.dart'; +import 'package:kernel/type_environment.dart'; +import 'package:_fe_analyzer_shared/src/messages/codes.dart' + show + Message, + LocatedMessage, + templateJsInteropExportClassNotMarkedExportable; +import 'package:_js_interop_checks/src/js_interop.dart' as js_interop; + +import 'export_checker.dart'; +import 'static_interop_mock_validator.dart'; + +class ExportCreator extends Transformer { + final Procedure _allowInterop; + final Procedure _createDartExport; + final Procedure _createStaticInteropMock; + final DiagnosticReporter _diagnosticReporter; + final ExportChecker _exportChecker; + final InterfaceType _functionType; + final Procedure _getProperty; + final Procedure _globalThis; + final InterfaceType _objectType; + final Procedure _setProperty; + final StaticInteropMockValidator _staticInteropMockValidator; + final TypeEnvironment _typeEnvironment; + + ExportCreator( + this._typeEnvironment, this._diagnosticReporter, this._exportChecker) + : _allowInterop = _typeEnvironment.coreTypes.index + .getTopLevelProcedure('dart:js', 'allowInterop'), + _createDartExport = _typeEnvironment.coreTypes.index + .getTopLevelProcedure('dart:js_util', 'createDartExport'), + _createStaticInteropMock = _typeEnvironment.coreTypes.index + .getTopLevelProcedure('dart:js_util', 'createStaticInteropMock'), + _functionType = _typeEnvironment.coreTypes.functionNonNullableRawType, + _getProperty = (_typeEnvironment.coreTypes.index.tryGetTopLevelMember( + 'dart:js_util', '_getPropertyTrustType') ?? + _typeEnvironment.coreTypes.index.getTopLevelProcedure( + 'dart:js_util', 'getProperty')) as Procedure, + _globalThis = _typeEnvironment.coreTypes.index + .getTopLevelProcedure('dart:js_util', 'get:globalThis'), + _objectType = _typeEnvironment.coreTypes.objectNonNullableRawType, + _setProperty = (_typeEnvironment.coreTypes.index.tryGetTopLevelMember( + 'dart:js_util', '_setPropertyUnchecked') ?? + _typeEnvironment.coreTypes.index.getTopLevelProcedure( + 'dart:js_util', 'setProperty')) as Procedure, + _staticInteropMockValidator = StaticInteropMockValidator( + _diagnosticReporter, _exportChecker, _typeEnvironment); + + @override + TreeNode visitStaticInvocation(StaticInvocation node) { + if (node.target == _createDartExport) { + var typeArguments = node.arguments.types; + assert(typeArguments.length == 1); + if (_verifyExportable(node, typeArguments[0])) { + return _createExport(node, typeArguments[0] as InterfaceType); + } + } else if (node.target == _createStaticInteropMock) { + var typeArguments = node.arguments.types; + assert(typeArguments.length == 2); + var staticInteropType = typeArguments[0]; + var dartType = typeArguments[1]; + + var exportable = _verifyExportable(node, dartType); + var staticInteropTypeArgumentCorrect = _staticInteropMockValidator + .validateStaticInteropTypeArgument(node, staticInteropType); + if (exportable && + staticInteropTypeArgumentCorrect && + _staticInteropMockValidator.validateCreateStaticInteropMock( + node, + (staticInteropType as InterfaceType).classNode, + (dartType as InterfaceType).classNode)) { + var arguments = node.arguments.positional; + assert(arguments.length == 1 || arguments.length == 2); + var proto = arguments.length == 2 ? arguments[1] : null; + + return _createExport(node, dartType, staticInteropType, proto); + } + } + return node; + } + + /// Validate that the [dartType] provided via `createDartExport` can be + /// exported safely. + /// + /// Checks that: + /// - Type argument is a valid Dart interface type. + /// - Type argument is not a JS interop type. + /// - Type argument was not marked as non-exportable. + /// + /// If there were no errors with processing the class, returns true. + /// Otherwise, returns false. + bool _verifyExportable(StaticInvocation node, DartType dartType) { + if (dartType is! InterfaceType) { + _diagnosticReporter.report( + templateJsInteropExportInvalidTypeArgument.withArguments( + dartType, true), + node.fileOffset, + node.name.text.length, + node.location?.file); + return false; + } + var dartClass = dartType.classNode; + if (js_interop.hasJSInteropAnnotation(dartClass) || + js_interop.hasStaticInteropAnnotation(dartClass) || + js_interop.hasAnonymousAnnotation(dartClass)) { + _diagnosticReporter.report( + templateJsInteropExportInvalidInteropTypeArgument.withArguments( + dartType, true), + node.fileOffset, + node.name.text.length, + node.location?.file); + return false; + } + if (!_exportChecker.exportStatus.containsKey(dartClass.reference)) { + // This occurs when we deserialize previously compiled modules. Those + // modules may contain export classes, so we need to revisit the classes + // in those previously compiled modules if they are used. + dartClass.procedures + .forEach((member) => _exportChecker.visitMember(member)); + dartClass.fields.forEach((member) => _exportChecker.visitMember(member)); + _exportChecker.visitClass(dartClass); + } + var exportStatus = _exportChecker.exportStatus[dartClass.reference]; + if (exportStatus == ExportStatus.NON_EXPORTABLE) { + _diagnosticReporter.report( + templateJsInteropExportClassNotMarkedExportable + .withArguments(dartClass.name), + node.fileOffset, + node.name.text.length, + node.location?.file); + return false; + } + return exportStatus == ExportStatus.EXPORTABLE; + } + + /// Create the object literal using the export map that was computed from the + /// interface in [dartType]. + /// + /// [node] is either a call to `createStaticInteropMock` or + /// `createDartExport`. [dartType] is assumed to be a valid exportable class. + /// [returnType] is the type that the object literal will be casted to. + /// [proto] is an optional prototype object that users can pass to instantiate + /// the object literal. + /// + /// The export map is already validated, so this method simply iterates over + /// it and either assigns a method for a given property name, or assigns a + /// getter and/or setter. + /// + /// Returns a call to the block of code that instantiates this object literal + /// and returns it. + TreeNode _createExport(StaticInvocation node, InterfaceType dartType, + [DartType? returnType, Expression? proto]) { + var exportMap = + _exportChecker.exportClassToMemberMap[dartType.classNode.reference]!; + + var block = []; + returnType ??= _typeEnvironment.coreTypes.objectNonNullableRawType; + + var dartInstance = VariableDeclaration('#dartInstance', + initializer: node.arguments.positional[0], type: dartType) + ..fileOffset = node.fileOffset + ..parent = node.parent; + block.add(dartInstance); + + // Get the global 'Object' property. + StaticInvocation getObjectProperty() => StaticInvocation( + _getProperty, + Arguments([StaticGet(_globalThis), StringLiteral('Object')], + types: [_objectType])); + + // Get a fresh object literal, using the proto to create it if one was + // given. + StaticInvocation getLiteral([Expression? proto]) { + return _callMethod(getObjectProperty(), StringLiteral('create'), + [proto ?? NullLiteral()], _objectType); + } + + var jsExporter = VariableDeclaration('#jsExporter', + initializer: AsExpression(getLiteral(proto), returnType), + type: returnType) + ..fileOffset = node.fileOffset + ..parent = node.parent; + block.add(jsExporter); + + for (var exportName in exportMap.keys) { + var exports = exportMap[exportName]!; + ExpressionStatement setProperty(VariableGet jsObject, String propertyName, + StaticInvocation wrappedValue) { + // `setProperty(jsObject, propertyName, wrappedValue)` + return ExpressionStatement(StaticInvocation( + _setProperty, + Arguments([jsObject, StringLiteral(propertyName), wrappedValue], + types: [_objectType]))) + ..fileOffset = node.fileOffset + ..parent = node.parent; + } + + var firstExport = exports.first; + // With methods, there's only one export per export name. + if (firstExport is Procedure && + firstExport.kind == ProcedureKind.Method) { + // `setProperty(jsMock, jsName, allowInterop(dartMock.tearoffMethod))` + block.add(setProperty( + VariableGet(jsExporter), + exportName, + StaticInvocation( + _allowInterop, + Arguments([ + InstanceTearOff(InstanceAccessKind.Instance, + VariableGet(dartInstance), firstExport.name, + interfaceTarget: firstExport, + resultType: firstExport.getterType) + ], types: [ + _functionType + ])))); + } else { + // Create the mapping from `get` and `set` to their `dartInstance` calls + // to be used in `Object.defineProperty`. + + // Add the given exports to the mapping that corresponds to the given + // exportName that is used by `Object.defineProperty`. In order to + // conform to that API, this function defines 'get' or 'set' properties + // on a given object literal. + // The AST code looks like: + // + // ``` + // setProperty(getSetMap, 'get', allowInterop(() { + // return dartInstance.getter; + // })); + // ``` + // + // in the case of a getter and: + // + // ``` + // setProperty(getSetMap, 'set', allowInterop((val) { + // dartInstance.setter = val; + // })); + // ``` + // + // in the case of a setter. + // + // A new map VariableDeclaration is created and added to the block of + // statements for each export name. + var getSetMap = VariableDeclaration('#${exportName}Mapping', + initializer: getLiteral(), type: _objectType) + ..fileOffset = node.fileOffset + ..parent = node.parent; + block.add(getSetMap); + var getSet = _exportChecker.getGetterSetter(exports); + var getter = getSet.getter; + var setter = getSet.setter; + if (getter != null) { + block.add(setProperty( + VariableGet(getSetMap), + 'get', + StaticInvocation( + _allowInterop, + Arguments([ + FunctionExpression(FunctionNode(ReturnStatement(InstanceGet( + InstanceAccessKind.Instance, + VariableGet(dartInstance), + getter.name, + interfaceTarget: getter, + resultType: getter.getterType)))) + ], types: [ + _functionType + ])))); + } + if (setter != null) { + var setterParameter = + VariableDeclaration('#val', type: setter.setterType) + ..fileOffset = node.fileOffset + ..parent = node.parent; + block.add(setProperty( + VariableGet(getSetMap), + 'set', + StaticInvocation( + _allowInterop, + Arguments([ + FunctionExpression(FunctionNode( + ExpressionStatement(InstanceSet( + InstanceAccessKind.Instance, + VariableGet(dartInstance), + setter.name, + VariableGet(setterParameter), + interfaceTarget: setter)), + positionalParameters: [setterParameter])) + ], types: [ + _functionType + ])))); + } + // Call `Object.defineProperty` to define the export name with the + // 'get' and/or 'set' mapping. This allows us to treat get/set + // semantics as methods. + block.add(ExpressionStatement(_callMethod( + getObjectProperty(), + StringLiteral('defineProperty'), + [ + VariableGet(jsExporter), + StringLiteral(exportName), + VariableGet(getSetMap) + ], + VoidType())) + ..fileOffset = node.fileOffset + ..parent = node.parent); + } + } + + block.add(ReturnStatement(VariableGet(jsExporter))); + // Return a call to evaluate the entire block of code and return the JS mock + // that was created. + return FunctionInvocation( + FunctionAccessKind.Function, + FunctionExpression(FunctionNode(Block(block), returnType: returnType)), + Arguments([]), + functionType: FunctionType([], returnType, Nullability.nonNullable)) + ..fileOffset = node.fileOffset + ..parent = node.parent; + } + + // Optimize `callMethod` calls if possible. + StaticInvocation _callMethod(Expression object, StringLiteral methodName, + List args, DartType returnType) { + var index = args.length; + var callMethodOptimized = _typeEnvironment.coreTypes.index + .tryGetTopLevelMember( + 'dart:js_util', '_callMethodUncheckedTrustType$index'); + if (callMethodOptimized == null) { + var callMethod = _typeEnvironment.coreTypes.index + .getTopLevelProcedure('dart:js_util', 'callMethod'); + return StaticInvocation( + callMethod, + Arguments([object, methodName, ListLiteral(args)], + types: [returnType])); + } else { + return StaticInvocation(callMethodOptimized as Procedure, + Arguments([object, methodName, ...args], types: [returnType])); + } + } +} diff --git a/pkg/_js_interop_checks/lib/src/transformations/static_interop_mock_creator.dart b/pkg/_js_interop_checks/lib/src/transformations/static_interop_mock_creator.dart deleted file mode 100644 index a2135df4ca2..00000000000 --- a/pkg/_js_interop_checks/lib/src/transformations/static_interop_mock_creator.dart +++ /dev/null @@ -1,932 +0,0 @@ -// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. - -import 'package:front_end/src/fasta/fasta_codes.dart' - show - templateJsInteropExportInvalidInteropTypeArgument, - templateJsInteropExportInvalidTypeArgument, - templateJsInteropStaticInteropMockNotStaticInteropType; -import 'package:kernel/ast.dart'; -import 'package:kernel/target/targets.dart'; -import 'package:kernel/type_environment.dart'; -import 'package:_fe_analyzer_shared/src/messages/codes.dart' - show - Message, - LocatedMessage, - templateJsInteropExportClassNotMarkedExportable, - templateJsInteropExportDartInterfaceHasNonEmptyJSExportValue, - templateJsInteropExportDisallowedMember, - templateJsInteropExportMemberCollision, - templateJsInteropExportNoExportableMembers, - templateJsInteropStaticInteropMockMissingGetterOrSetter, - templateJsInteropStaticInteropMockMissingImplements; -import 'package:_js_interop_checks/src/js_interop.dart' as js_interop; - -enum _ExportStatus { - EXPORT_ERROR, - NON_EXPORTABLE, - EXPORTABLE, -} - -class _GetSet { - Member? getter; - Member? setter; - - _GetSet(this.getter, this.setter); -} - -class ExportChecker { - final DiagnosticReporter _diagnosticReporter; - final Map>> exportClassToMemberMap = {}; - final Map exportStatus = {}; - final Class _objectClass; - final Map> _overrideMap = {}; - // Store map of libraries to @staticInterop extensions, so that we can compute - // the class to extension map later. Prefer to do it this way so that modular - // compilation can invalidate recompiled extensions. - static final Map> libraryExtensionMap = {}; - - ExportChecker(this._diagnosticReporter, this._objectClass); - - /// Gets the getter and setter from the given [exports]. - /// - /// [exports] should be a set of members from the [exportClassToMemberMap]. If - /// missing a getter and/or setter, the corresponding field will be `null`. - _GetSet getGetterSetter(Set exports) { - assert(exports.isNotEmpty && exports.length <= 2); - Member? getter; - Member? setter; - - var firstExport = exports.first; - if (exports.length == 1) { - if (firstExport.isGetter) { - getter = firstExport; - } - if (firstExport.isSetter) { - setter = firstExport; - } - } else if (exports.length == 2) { - var secondExport = exports.elementAt(1); - // One of them could be a partially overridden non-final field, so - // determine the strict getter or setter first. - if (firstExport.isStrictGetter || secondExport.isStrictSetter) { - getter = firstExport; - setter = secondExport; - } else { - getter = secondExport; - setter = firstExport; - } - } - - return _GetSet(getter, setter); - } - - /// Calculates the overrides, including inheritance, for [cls]. - /// - /// Note that we use a map from the unique name (with setter renaming) to - /// avoid duplicate checks on classes, and to store the overrides. - void _collectOverrides(Class cls) { - if (_overrideMap.containsKey(cls.reference)) return; - Map memberMap; - var superclass = cls.superclass; - if (superclass != null && superclass != _objectClass) { - _collectOverrides(superclass); - memberMap = Map.from(_overrideMap[superclass.reference]!); - } else { - memberMap = {}; - } - // If this is a mixin application, fetch the members from the mixin. - var demangledCls = cls.isMixinApplication ? cls.mixin : cls; - for (var member in [ - ...demangledCls.procedures.where((proc) => proc.exportable), - ...demangledCls.fields.where((field) => field.exportable) - ]) { - var memberName = member.name.text; - if (member is Procedure && member.isSetter) { - memberMap[memberName + '='] = member; - } else { - if (member is Field && !member.isFinal) { - memberMap[memberName + '='] = member; - } - memberMap[memberName] = member; - } - } - _overrideMap[cls.reference] = memberMap; - } - - /// Determine if [cls] is exportable, and if so, compute the export members. - /// - /// - /// Check the following: - /// - If the class has a `@JSExport` annotation, the value should be empty. - /// - If the class has the annotation, it should have at least one exportable - /// member in the class or in any superclass (ignoring `Object`). - /// - Accounting for Dart overrides, the export member map of the class or - /// any of its superclasses do not contain unresolvable name collisions. An - /// explanation of the resolvable collisions is below. - void visitClass(Class cls) { - var classHasJSExport = js_interop.hasJSExportAnnotation(cls); - // If the class doesn't have the annotation or if the class wasn't marked - // when we visited the members and checked their annotations, there's - // nothing to do for this class. - if (!classHasJSExport && - exportStatus[cls.reference] != _ExportStatus.EXPORTABLE) { - exportStatus[cls.reference] = _ExportStatus.NON_EXPORTABLE; - return; - } - - if (classHasJSExport && js_interop.getJSExportName(cls).isNotEmpty) { - _diagnosticReporter.report( - templateJsInteropExportDartInterfaceHasNonEmptyJSExportValue - .withArguments(cls.name), - cls.fileOffset, - cls.name.length, - cls.location?.file); - exportStatus[cls.reference] = _ExportStatus.EXPORT_ERROR; - } - - _collectOverrides(cls); - - var allExportableMembers = _overrideMap[cls.reference]!.values.where( - (member) => - // Only members that qualify are those that are exportable, and - // either their class has the annotation or they have it themselves. - member.exportable && - (js_interop.hasJSExportAnnotation(member) || - js_interop.hasJSExportAnnotation(member.enclosingClass!))); - var exports = >{}; - - // Store the exportable members. - for (var member in allExportableMembers) { - var exportName = member.exportPropertyName; - exports.putIfAbsent(exportName, () => {}).add(member); - } - - // Walk through the export map and determine if there are any unresolvable - // conflicts. - for (var exportName in exports.keys) { - var existingMembers = exports[exportName]!; - if (existingMembers.length == 1) continue; - if (existingMembers.length == 2) { - // There are two instances where you can resolve collisions: - // 1. One of the members is a non-final field, and the other one is - // either a strict getter or a strict setter that overrides part of - // that field. - // 2. One of the members is a strict getter, and the other one is a - // strict setter or vice versa. - // Any other case is an error to have more than 1 member per name. - bool isCollisionOkay(Member m1, Member m2) { - if (m1.isNonFinalField && - (m2.isStrictGetter || m2.isStrictSetter) && - // Is an override if the same name and across different classes. - (m1.name.text == m2.name.text && - m1.enclosingClass != m2.enclosingClass)) { - return true; - } else if (m1.isStrictGetter && m2.isStrictSetter) { - return true; - } - return false; - } - - var first = existingMembers.elementAt(0); - var second = existingMembers.elementAt(1); - if (isCollisionOkay(first, second) || isCollisionOkay(second, first)) { - continue; - } - } - // Sort to get deterministic order. - var sortedExistingMembers = - existingMembers.map((member) => member.toString()).toList()..sort(); - _diagnosticReporter.report( - templateJsInteropExportMemberCollision.withArguments( - exportName, sortedExistingMembers.join(', ')), - cls.fileOffset, - cls.name.length, - cls.location?.file); - exportStatus[cls.reference] = _ExportStatus.EXPORT_ERROR; - } - - if (exports.isEmpty) { - _diagnosticReporter.report( - templateJsInteropExportNoExportableMembers.withArguments(cls.name), - cls.fileOffset, - cls.name.length, - cls.location?.file); - exportStatus[cls.reference] = _ExportStatus.EXPORT_ERROR; - } - - exportClassToMemberMap[cls.reference] = exports; - exportStatus[cls.reference] ??= _ExportStatus.EXPORTABLE; - } - - /// Check that the [member] can be exportable if it has an annotation, and if - /// so, mark the enclosing class as exportable. - void visitMember(Member member) { - var memberHasJSExportAnnotation = js_interop.hasJSExportAnnotation(member); - var cls = member.enclosingClass; - if (memberHasJSExportAnnotation) { - if (!member.exportable) { - _diagnosticReporter.report( - templateJsInteropExportDisallowedMember - .withArguments(member.name.text), - member.fileOffset, - member.name.text.length, - member.location?.file); - if (cls != null) { - exportStatus[cls.reference] = _ExportStatus.EXPORT_ERROR; - } - } else { - // Mark as exportable so we know that the class has an exportable member - // when we process the class later. - if (cls != null) exportStatus[cls.reference] = _ExportStatus.EXPORTABLE; - } - } - } - - void visitLibrary(Library library) { - for (var extension in library.extensions) { - var onType = extension.onType; - if (onType is InterfaceType && - js_interop.hasStaticInteropAnnotation(onType.classNode)) { - libraryExtensionMap - .putIfAbsent(library.reference, () => {}) - .add(extension); - } - } - } -} - -class StaticInteropMockValidator { - final Map _descriptorToExtensionName = {}; - final DiagnosticReporter _diagnosticReporter; - final ExportChecker _exportChecker; - // Cache of @staticInterop classes to a mapping between their extension - // members and those members' export names. - final Map>> - _staticInteropExportNameToDescriptorMap = {}; - final TypeEnvironment _typeEnvironment; - late final Map> - _staticInteropClassesWithExtensions = _computeStaticInteropExtensionMap(); - StaticInteropMockValidator( - this._diagnosticReporter, this._exportChecker, this._typeEnvironment); - - bool validateStaticInteropTypeArgument( - StaticInvocation node, DartType staticInteropType) { - if (staticInteropType is! InterfaceType || - !js_interop.hasStaticInteropAnnotation(staticInteropType.classNode)) { - _diagnosticReporter.report( - templateJsInteropStaticInteropMockNotStaticInteropType.withArguments( - staticInteropType, true), - node.fileOffset, - node.name.text.length, - node.location?.file); - return false; - } - return true; - } - - /// Given an invocation [node] of `js_util.createStaticInteropMock`, and its - /// type arguments [staticInteropClass] and [dartClass], checks that the - /// [dartClass] has sufficient members to be exported in place of - /// [staticInteropClass]. - bool validateCreateStaticInteropMock( - StaticInvocation node, Class staticInteropClass, Class dartClass) { - var conformanceError = false; - var exportNameToDescriptors = - _computeImplementableExtensionMembers(staticInteropClass); - var exportMap = _exportChecker.exportClassToMemberMap[dartClass.reference]!; - - for (var exportName in exportNameToDescriptors.keys) { - var descriptors = exportNameToDescriptors[exportName]!; - - String getAsErrorString(Iterable descriptors) { - var withExtensionNameAndType = descriptors.map((descriptor) { - var extension = _descriptorToExtensionName[descriptor]!; - var name = descriptor.name.text; - var type = _getTypeOfDescriptor(descriptor); - if (descriptor.isGetter) { - type = FunctionType([], type, Nullability.nonNullable); - } else if (descriptor.isSetter) { - type = FunctionType([type], VoidType(), Nullability.nonNullable); - name += '='; - } - return '$extension.$name ($type)'; - }).toList() - ..sort(); - return withExtensionNameAndType.join(', '); - } - - // Unlike with class members, there's no guarantee that there aren't - // conflicting members. We take a conservative approach with our error - // checking, and just require one of the extension members with the export - // name be implemented in the mocking class. It's typically unusual to - // have conflicting members for the same interface, so this should be - // satisfactory in most cases. - var hasImplementation = false; - var dartMembers = exportMap[exportName]; - if (dartMembers != null) { - var firstMember = dartMembers.first; - if (firstMember.isMethod) { - hasImplementation = descriptors - .any((descriptor) => _implements(firstMember, descriptor)); - } else { - var getSet = _exportChecker.getGetterSetter(dartMembers); - - var getters = {}; - var setters = {}; - - var implementsGetter = false; - var implementsSetter = false; - for (var descriptor in descriptors) { - if (descriptor.isGetter) { - implementsGetter |= _implements(getSet.getter, descriptor); - getters.add(descriptor); - } else if (descriptor.isSetter) { - implementsSetter |= _implements(getSet.setter, descriptor); - setters.add(descriptor); - } - } - - hasImplementation = implementsGetter || implementsSetter; - - // If there is both a getter and setter descriptor, then we require - // users to provide both a getter and setter that are subtypes. - // It's likely that declaring one but not the other when both are used - // in the @staticInterop class is a bug. - if (getters.isNotEmpty && - setters.isNotEmpty && - (implementsGetter ^ implementsSetter)) { - _diagnosticReporter.report( - templateJsInteropStaticInteropMockMissingGetterOrSetter - .withArguments( - dartClass.name, - implementsGetter ? 'getter' : 'setter', - implementsGetter ? 'setter' : 'getter', - exportName, - getAsErrorString(implementsGetter ? setters : getters)), - node.fileOffset, - node.name.text.length, - node.location?.file); - // While we do have an implementation, this is still an error. - conformanceError = true; - } - } - } - - if (!hasImplementation) { - _diagnosticReporter.report( - templateJsInteropStaticInteropMockMissingImplements.withArguments( - dartClass.name, exportName, getAsErrorString(descriptors)), - node.fileOffset, - node.name.text.length, - node.location?.file); - conformanceError = true; - } - } - return !conformanceError; - } - - // Get the corresponding function type of the given descriptor. Getters and - // setters return their return and parameter types, respectively. - DartType _getTypeOfDescriptor(ExtensionMemberDescriptor interopDescriptor) { - // CFE creates static procedures for each extension member. - var interopMember = interopDescriptor.member.asProcedure; - - if (interopDescriptor.isGetter) { - return interopMember.function.returnType; - } else if (interopDescriptor.isSetter) { - // Ignore the first argument `this` in the generated procedure. - return interopMember.function.positionalParameters[1].type; - } else { - assert(interopDescriptor.isMethod); - var interopMemberType = - interopMember.function.computeFunctionType(Nullability.nonNullable); - // Ignore the first argument `this` in the generated procedure. - return FunctionType( - interopMemberType.positionalParameters.skip(1).toList(), - interopMemberType.returnType, - interopMemberType.declaredNullability, - namedParameters: interopMemberType.namedParameters, - typeParameters: interopMemberType.typeParameters, - requiredParameterCount: interopMemberType.requiredParameterCount - 1); - } - } - - // Determine if the given Dart member is the right kind and subtype to - // implement the descriptor. - bool _implements( - Member? dartMember, ExtensionMemberDescriptor interopDescriptor) { - if (dartMember == null) return false; - - // If it isn't even the right kind, don't continue. - if (interopDescriptor.isGetter && !dartMember.isGetter) { - return false; - } else if (interopDescriptor.isSetter && !dartMember.isSetter) { - return false; - } else if (interopDescriptor.isMethod && dartMember is! Procedure) { - return false; - } - - bool isSubtypeOf(DartType dartType, DartType interopType) { - return _typeEnvironment.isSubtypeOf( - dartType, interopType, SubtypeCheckMode.withNullabilities); - } - - var interopType = _getTypeOfDescriptor(interopDescriptor); - - if (interopDescriptor.isGetter) { - if (!isSubtypeOf(dartMember.getterType, interopType)) { - return false; - } - } else if (interopDescriptor.isSetter) { - if (!isSubtypeOf(interopType, dartMember.setterType)) { - return false; - } - } else if (interopDescriptor.isMethod) { - if (!isSubtypeOf( - (dartMember as Procedure) - .function - .computeFunctionType(Nullability.nonNullable), - interopType)) { - return false; - } - } - return true; - } - - /// Compute a mapping between all the @staticInterop classes and their - /// extensions. - /// - /// We do this here instead of in the export checker for two reasons: - /// 1. Modular compilation may invalidate extensions, so we need some way to - /// get rid of old extensions. - /// 2. The work to do this is only done when you use the - /// `createStaticInteropMock` API, leaving unrelated libraries alone. - /// - /// TODO(srujzs): This does not take into account any scoping. This might mean - /// that if another library defines an extension on the @staticInterop class - /// that is outside of the scope of the current library, this API will report - /// an error. Considering this API should primarily be used in tests, such a - /// compilation will be unlikely, but we should revisit this. - Map> _computeStaticInteropExtensionMap() { - // Process the stored libaries, and create a mapping between @staticInterop - // classes and their extensions. - var staticInteropClassesWithExtensions = >{}; - for (var library in ExportChecker.libraryExtensionMap.keys) { - for (var extension in ExportChecker.libraryExtensionMap[library]!) { - var onType = extension.onType as InterfaceType; - staticInteropClassesWithExtensions - .putIfAbsent(onType.className, () => {}) - .add(extension); - } - } - return staticInteropClassesWithExtensions; - } - - /// Returns a map between all the implementable external extension member - /// names and the descriptors that have that name for [staticInteropClass]. - /// - /// Also computes a mapping between descriptors and their name for error - /// reporting. - Map> - _computeImplementableExtensionMembers(Class staticInteropClass) { - assert(js_interop.hasStaticInteropAnnotation(staticInteropClass)); - - // Get the cached result if we've already processed this class. - var exportNameToDescriptors = - _staticInteropExportNameToDescriptorMap[staticInteropClass]; - if (exportNameToDescriptors != null) { - return exportNameToDescriptors; - } else { - exportNameToDescriptors = >{}; - } - - var classes = {}; - // Compute a map of all the possible descriptors available in this type and - // the supertypes. - void getAllDescriptors(Class cls) { - if (classes.add(cls)) { - var extensions = _staticInteropClassesWithExtensions[cls.reference]; - if (extensions != null) { - for (var extension in extensions) { - for (var descriptor in extension.members) { - if (!descriptor.isExternal || descriptor.isStatic) continue; - // No need to handle external fields - they are transformed to - // external getters/setters by the CFE. - if (!descriptor.isGetter && - !descriptor.isSetter && - !descriptor.isMethod) { - continue; - } - _descriptorToExtensionName[descriptor] = - extension.isUnnamedExtension ? '' : extension.name; - var name = js_interop.getJSName(descriptor.member.asMember); - if (name.isEmpty) name = descriptor.name.text; - exportNameToDescriptors! - .putIfAbsent(name, () => {}) - .add(descriptor); - } - } - } - cls.supers.forEach((Supertype supertype) { - getAllDescriptors(supertype.classNode); - }); - } - } - - getAllDescriptors(staticInteropClass); - - return _staticInteropExportNameToDescriptorMap[staticInteropClass] = - exportNameToDescriptors; - } -} - -// TODO(srujzs): Rename this class and file to focus on exports. Separate out -// the export creation, export validation, and mock validation into three -// separate files to make this cleaner. -class StaticInteropMockCreator extends Transformer { - final Procedure _allowInterop; - final Procedure _createDartExport; - final Procedure _createStaticInteropMock; - final DiagnosticReporter _diagnosticReporter; - final ExportChecker _exportChecker; - final InterfaceType _functionType; - final Procedure _getProperty; - final Procedure _globalThis; - final InterfaceType _objectType; - final Procedure _setProperty; - final StaticInteropMockValidator _staticInteropMockValidator; - final TypeEnvironment _typeEnvironment; - - StaticInteropMockCreator( - this._typeEnvironment, this._diagnosticReporter, this._exportChecker) - : _allowInterop = _typeEnvironment.coreTypes.index - .getTopLevelProcedure('dart:js', 'allowInterop'), - _createDartExport = _typeEnvironment.coreTypes.index - .getTopLevelProcedure('dart:js_util', 'createDartExport'), - _createStaticInteropMock = _typeEnvironment.coreTypes.index - .getTopLevelProcedure('dart:js_util', 'createStaticInteropMock'), - _functionType = _typeEnvironment.coreTypes.functionNonNullableRawType, - _getProperty = (_typeEnvironment.coreTypes.index.tryGetTopLevelMember( - 'dart:js_util', '_getPropertyTrustType') ?? - _typeEnvironment.coreTypes.index.getTopLevelProcedure( - 'dart:js_util', 'getProperty')) as Procedure, - _globalThis = _typeEnvironment.coreTypes.index - .getTopLevelProcedure('dart:js_util', 'get:globalThis'), - _objectType = _typeEnvironment.coreTypes.objectNonNullableRawType, - _setProperty = (_typeEnvironment.coreTypes.index.tryGetTopLevelMember( - 'dart:js_util', '_setPropertyUnchecked') ?? - _typeEnvironment.coreTypes.index.getTopLevelProcedure( - 'dart:js_util', 'setProperty')) as Procedure, - _staticInteropMockValidator = StaticInteropMockValidator( - _diagnosticReporter, _exportChecker, _typeEnvironment); - - @override - TreeNode visitStaticInvocation(StaticInvocation node) { - if (node.target == _createDartExport) { - var typeArguments = node.arguments.types; - assert(typeArguments.length == 1); - if (_verifyExportable(node, typeArguments[0])) { - return _createExport(node, typeArguments[0] as InterfaceType); - } - } else if (node.target == _createStaticInteropMock) { - var typeArguments = node.arguments.types; - assert(typeArguments.length == 2); - var staticInteropType = typeArguments[0]; - var dartType = typeArguments[1]; - - var exportable = _verifyExportable(node, dartType); - var staticInteropTypeArgumentCorrect = _staticInteropMockValidator - .validateStaticInteropTypeArgument(node, staticInteropType); - if (exportable && - staticInteropTypeArgumentCorrect && - _staticInteropMockValidator.validateCreateStaticInteropMock( - node, - (staticInteropType as InterfaceType).classNode, - (dartType as InterfaceType).classNode)) { - var arguments = node.arguments.positional; - assert(arguments.length == 1 || arguments.length == 2); - var proto = arguments.length == 2 ? arguments[1] : null; - - return _createExport(node, dartType, staticInteropType, proto); - } - } - return node; - } - - /// Validate that the [dartType] provided via `createDartExport` can be - /// exported safely. - /// - /// Checks that: - /// - Type argument is a valid Dart interface type. - /// - Type argument is not a JS interop type. - /// - Type argument was not marked as non-exportable. - /// - /// If there were no errors with processing the class, returns true. - /// Otherwise, returns false. - bool _verifyExportable(StaticInvocation node, DartType dartType) { - if (dartType is! InterfaceType) { - _diagnosticReporter.report( - templateJsInteropExportInvalidTypeArgument.withArguments( - dartType, true), - node.fileOffset, - node.name.text.length, - node.location?.file); - return false; - } - var dartClass = dartType.classNode; - if (js_interop.hasJSInteropAnnotation(dartClass) || - js_interop.hasStaticInteropAnnotation(dartClass) || - js_interop.hasAnonymousAnnotation(dartClass)) { - _diagnosticReporter.report( - templateJsInteropExportInvalidInteropTypeArgument.withArguments( - dartType, true), - node.fileOffset, - node.name.text.length, - node.location?.file); - return false; - } - if (!_exportChecker.exportStatus.containsKey(dartClass.reference)) { - // This occurs when we deserialize previously compiled modules. Those - // modules may contain export classes, so we need to revisit the classes - // in those previously compiled modules if they are used. - dartClass.procedures - .forEach((member) => _exportChecker.visitMember(member)); - dartClass.fields.forEach((member) => _exportChecker.visitMember(member)); - _exportChecker.visitClass(dartClass); - } - var exportStatus = _exportChecker.exportStatus[dartClass.reference]; - if (exportStatus == _ExportStatus.NON_EXPORTABLE) { - _diagnosticReporter.report( - templateJsInteropExportClassNotMarkedExportable - .withArguments(dartClass.name), - node.fileOffset, - node.name.text.length, - node.location?.file); - return false; - } - return exportStatus == _ExportStatus.EXPORTABLE; - } - - /// Create the object literal using the export map that was computed from the - /// interface in [dartType]. - /// - /// [node] is either a call to `createStaticInteropMock` or - /// `createDartExport`. [dartType] is assumed to be a valid exportable class. - /// [returnType] is the type that the object literal will be casted to. - /// [proto] is an optional prototype object that users can pass to instantiate - /// the object literal. - /// - /// The export map is already validated, so this method simply iterates over - /// it and either assigns a method for a given property name, or assigns a - /// getter and/or setter. - /// - /// Returns a call to the block of code that instantiates this object literal - /// and returns it. - TreeNode _createExport(StaticInvocation node, InterfaceType dartType, - [DartType? returnType, Expression? proto]) { - var exportMap = - _exportChecker.exportClassToMemberMap[dartType.classNode.reference]!; - - var block = []; - returnType ??= _typeEnvironment.coreTypes.objectNonNullableRawType; - - var dartInstance = VariableDeclaration('#dartInstance', - initializer: node.arguments.positional[0], type: dartType) - ..fileOffset = node.fileOffset - ..parent = node.parent; - block.add(dartInstance); - - // Get the global 'Object' property. - StaticInvocation getObjectProperty() => StaticInvocation( - _getProperty, - Arguments([StaticGet(_globalThis), StringLiteral('Object')], - types: [_objectType])); - - // Get a fresh object literal, using the proto to create it if one was - // given. - StaticInvocation getLiteral([Expression? proto]) { - return _callMethod(getObjectProperty(), StringLiteral('create'), - [proto ?? NullLiteral()], _objectType); - } - - var jsExporter = VariableDeclaration('#jsExporter', - initializer: AsExpression(getLiteral(proto), returnType), - type: returnType) - ..fileOffset = node.fileOffset - ..parent = node.parent; - block.add(jsExporter); - - for (var exportName in exportMap.keys) { - var exports = exportMap[exportName]!; - ExpressionStatement setProperty(VariableGet jsObject, String propertyName, - StaticInvocation wrappedValue) { - // `setProperty(jsObject, propertyName, wrappedValue)` - return ExpressionStatement(StaticInvocation( - _setProperty, - Arguments([jsObject, StringLiteral(propertyName), wrappedValue], - types: [_objectType]))) - ..fileOffset = node.fileOffset - ..parent = node.parent; - } - - var firstExport = exports.first; - // With methods, there's only one export per export name. - if (firstExport is Procedure && - firstExport.kind == ProcedureKind.Method) { - // `setProperty(jsMock, jsName, allowInterop(dartMock.tearoffMethod))` - block.add(setProperty( - VariableGet(jsExporter), - exportName, - StaticInvocation( - _allowInterop, - Arguments([ - InstanceTearOff(InstanceAccessKind.Instance, - VariableGet(dartInstance), firstExport.name, - interfaceTarget: firstExport, - resultType: firstExport.getterType) - ], types: [ - _functionType - ])))); - } else { - // Create the mapping from `get` and `set` to their `dartInstance` calls - // to be used in `Object.defineProperty`. - - // Add the given exports to the mapping that corresponds to the given - // exportName that is used by `Object.defineProperty`. In order to - // conform to that API, this function defines 'get' or 'set' properties - // on a given object literal. - // The AST code looks like: - // - // ``` - // setProperty(getSetMap, 'get', allowInterop(() { - // return dartInstance.getter; - // })); - // ``` - // - // in the case of a getter and: - // - // ``` - // setProperty(getSetMap, 'set', allowInterop((val) { - // dartInstance.setter = val; - // })); - // ``` - // - // in the case of a setter. - // - // A new map VariableDeclaration is created and added to the block of - // statements for each export name. - var getSetMap = VariableDeclaration('#${exportName}Mapping', - initializer: getLiteral(), type: _objectType) - ..fileOffset = node.fileOffset - ..parent = node.parent; - block.add(getSetMap); - var getSet = _exportChecker.getGetterSetter(exports); - var getter = getSet.getter; - var setter = getSet.setter; - if (getter != null) { - block.add(setProperty( - VariableGet(getSetMap), - 'get', - StaticInvocation( - _allowInterop, - Arguments([ - FunctionExpression(FunctionNode(ReturnStatement(InstanceGet( - InstanceAccessKind.Instance, - VariableGet(dartInstance), - getter.name, - interfaceTarget: getter, - resultType: getter.getterType)))) - ], types: [ - _functionType - ])))); - } - if (setter != null) { - var setterParameter = - VariableDeclaration('#val', type: setter.setterType) - ..fileOffset = node.fileOffset - ..parent = node.parent; - block.add(setProperty( - VariableGet(getSetMap), - 'set', - StaticInvocation( - _allowInterop, - Arguments([ - FunctionExpression(FunctionNode( - ExpressionStatement(InstanceSet( - InstanceAccessKind.Instance, - VariableGet(dartInstance), - setter.name, - VariableGet(setterParameter), - interfaceTarget: setter)), - positionalParameters: [setterParameter])) - ], types: [ - _functionType - ])))); - } - // Call `Object.defineProperty` to define the export name with the - // 'get' and/or 'set' mapping. This allows us to treat get/set - // semantics as methods. - block.add(ExpressionStatement(_callMethod( - getObjectProperty(), - StringLiteral('defineProperty'), - [ - VariableGet(jsExporter), - StringLiteral(exportName), - VariableGet(getSetMap) - ], - VoidType())) - ..fileOffset = node.fileOffset - ..parent = node.parent); - } - } - - block.add(ReturnStatement(VariableGet(jsExporter))); - // Return a call to evaluate the entire block of code and return the JS mock - // that was created. - return FunctionInvocation( - FunctionAccessKind.Function, - FunctionExpression(FunctionNode(Block(block), returnType: returnType)), - Arguments([]), - functionType: FunctionType([], returnType, Nullability.nonNullable)) - ..fileOffset = node.fileOffset - ..parent = node.parent; - } - - // Optimize `callMethod` calls if possible. - StaticInvocation _callMethod(Expression object, StringLiteral methodName, - List args, DartType returnType) { - var index = args.length; - var callMethodOptimized = _typeEnvironment.coreTypes.index - .tryGetTopLevelMember( - 'dart:js_util', '_callMethodUncheckedTrustType$index'); - if (callMethodOptimized == null) { - var callMethod = _typeEnvironment.coreTypes.index - .getTopLevelProcedure('dart:js_util', 'callMethod'); - return StaticInvocation( - callMethod, - Arguments([object, methodName, ListLiteral(args)], - types: [returnType])); - } else { - return StaticInvocation(callMethodOptimized as Procedure, - Arguments([object, methodName, ...args], types: [returnType])); - } - } -} - -extension ExtensionMemberDescriptorExtension on ExtensionMemberDescriptor { - bool get isGetter => this.kind == ExtensionMemberKind.Getter; - bool get isSetter => this.kind == ExtensionMemberKind.Setter; - bool get isMethod => this.kind == ExtensionMemberKind.Method; - - bool get isExternal => (this.member.asProcedure).isExternal; -} - -extension ProcedureExtension on Procedure { - // We only care about concrete instance procedures. - bool get exportable => - !this.isAbstract && - !this.isStatic && - !this.isExtensionMember && - !this.isFactory && - !this.isExternal && - this.kind != ProcedureKind.Operator; -} - -extension FieldExtension on Field { - // We only care about concrete instance fields. - bool get exportable => !this.isAbstract && !this.isStatic && !this.isExternal; -} - -extension MemberExtension on Member { - // Get the property name that this member will be exported as. - String get exportPropertyName { - var rename = js_interop.getJSExportName(this); - return rename.isEmpty ? this.name.text : rename; - } - - bool get exportable => - (this is Procedure && (this as Procedure).exportable) || - (this is Field && (this as Field).exportable); - - // Only a getter and not a setter. - bool get isStrictGetter => - (this is Procedure && (this as Procedure).isGetter) || - (this is Field && (this as Field).isFinal); - - // Only a setter and not a getter. - bool get isStrictSetter => this is Procedure && (this as Procedure).isSetter; - - bool get isNonFinalField => this is Field && !(this as Field).isFinal; - - bool get isGetter => - this is Field || (this is Procedure && (this as Procedure).isGetter); - - bool get isSetter => - this.isNonFinalField || - (this is Procedure && (this as Procedure).isSetter); - - bool get isMethod => - this is Procedure && (this as Procedure).kind == ProcedureKind.Method; -} diff --git a/pkg/_js_interop_checks/lib/src/transformations/static_interop_mock_validator.dart b/pkg/_js_interop_checks/lib/src/transformations/static_interop_mock_validator.dart new file mode 100644 index 00000000000..c7f177067a2 --- /dev/null +++ b/pkg/_js_interop_checks/lib/src/transformations/static_interop_mock_validator.dart @@ -0,0 +1,303 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:front_end/src/fasta/fasta_codes.dart' + show templateJsInteropStaticInteropMockNotStaticInteropType; +import 'package:kernel/ast.dart'; +import 'package:kernel/target/targets.dart'; +import 'package:kernel/type_environment.dart'; +import 'package:_fe_analyzer_shared/src/messages/codes.dart' + show + Message, + LocatedMessage, + templateJsInteropStaticInteropMockMissingGetterOrSetter, + templateJsInteropStaticInteropMockMissingImplements; +import 'package:_js_interop_checks/src/js_interop.dart' as js_interop; + +import 'export_checker.dart'; + +class StaticInteropMockValidator { + final Map _descriptorToExtensionName = {}; + final DiagnosticReporter _diagnosticReporter; + final ExportChecker _exportChecker; + // Cache of @staticInterop classes to a mapping between their extension + // members and those members' export names. + final Map>> + _staticInteropExportNameToDescriptorMap = {}; + final TypeEnvironment _typeEnvironment; + late final Map> + _staticInteropClassesWithExtensions = _computeStaticInteropExtensionMap(); + StaticInteropMockValidator( + this._diagnosticReporter, this._exportChecker, this._typeEnvironment); + + bool validateStaticInteropTypeArgument( + StaticInvocation node, DartType staticInteropType) { + if (staticInteropType is! InterfaceType || + !js_interop.hasStaticInteropAnnotation(staticInteropType.classNode)) { + _diagnosticReporter.report( + templateJsInteropStaticInteropMockNotStaticInteropType.withArguments( + staticInteropType, true), + node.fileOffset, + node.name.text.length, + node.location?.file); + return false; + } + return true; + } + + /// Given an invocation [node] of `js_util.createStaticInteropMock`, and its + /// type arguments [staticInteropClass] and [dartClass], checks that the + /// [dartClass] has sufficient members to be exported in place of + /// [staticInteropClass]. + bool validateCreateStaticInteropMock( + StaticInvocation node, Class staticInteropClass, Class dartClass) { + var conformanceError = false; + var exportNameToDescriptors = + _computeImplementableExtensionMembers(staticInteropClass); + var exportMap = _exportChecker.exportClassToMemberMap[dartClass.reference]!; + + for (var exportName in exportNameToDescriptors.keys) { + var descriptors = exportNameToDescriptors[exportName]!; + + String getAsErrorString(Iterable descriptors) { + var withExtensionNameAndType = descriptors.map((descriptor) { + var extension = _descriptorToExtensionName[descriptor]!; + var name = descriptor.name.text; + var type = _getTypeOfDescriptor(descriptor); + if (descriptor.isGetter) { + type = FunctionType([], type, Nullability.nonNullable); + } else if (descriptor.isSetter) { + type = FunctionType([type], VoidType(), Nullability.nonNullable); + name += '='; + } + return '$extension.$name ($type)'; + }).toList() + ..sort(); + return withExtensionNameAndType.join(', '); + } + + // Unlike with class members, there's no guarantee that there aren't + // conflicting members. We take a conservative approach with our error + // checking, and just require one of the extension members with the export + // name be implemented in the mocking class. It's typically unusual to + // have conflicting members for the same interface, so this should be + // satisfactory in most cases. + var hasImplementation = false; + var dartMembers = exportMap[exportName]; + if (dartMembers != null) { + var firstMember = dartMembers.first; + if (firstMember.isMethod) { + hasImplementation = descriptors + .any((descriptor) => _implements(firstMember, descriptor)); + } else { + var getSet = _exportChecker.getGetterSetter(dartMembers); + + var getters = {}; + var setters = {}; + + var implementsGetter = false; + var implementsSetter = false; + for (var descriptor in descriptors) { + if (descriptor.isGetter) { + implementsGetter |= _implements(getSet.getter, descriptor); + getters.add(descriptor); + } else if (descriptor.isSetter) { + implementsSetter |= _implements(getSet.setter, descriptor); + setters.add(descriptor); + } + } + + hasImplementation = implementsGetter || implementsSetter; + + // If there is both a getter and setter descriptor, then we require + // users to provide both a getter and setter that are subtypes. + // It's likely that declaring one but not the other when both are used + // in the @staticInterop class is a bug. + if (getters.isNotEmpty && + setters.isNotEmpty && + (implementsGetter ^ implementsSetter)) { + _diagnosticReporter.report( + templateJsInteropStaticInteropMockMissingGetterOrSetter + .withArguments( + dartClass.name, + implementsGetter ? 'getter' : 'setter', + implementsGetter ? 'setter' : 'getter', + exportName, + getAsErrorString(implementsGetter ? setters : getters)), + node.fileOffset, + node.name.text.length, + node.location?.file); + // While we do have an implementation, this is still an error. + conformanceError = true; + } + } + } + + if (!hasImplementation) { + _diagnosticReporter.report( + templateJsInteropStaticInteropMockMissingImplements.withArguments( + dartClass.name, exportName, getAsErrorString(descriptors)), + node.fileOffset, + node.name.text.length, + node.location?.file); + conformanceError = true; + } + } + return !conformanceError; + } + + // Get the corresponding function type of the given descriptor. Getters and + // setters return their return and parameter types, respectively. + DartType _getTypeOfDescriptor(ExtensionMemberDescriptor interopDescriptor) { + // CFE creates static procedures for each extension member. + var interopMember = interopDescriptor.member.asProcedure; + + if (interopDescriptor.isGetter) { + return interopMember.function.returnType; + } else if (interopDescriptor.isSetter) { + // Ignore the first argument `this` in the generated procedure. + return interopMember.function.positionalParameters[1].type; + } else { + assert(interopDescriptor.isMethod); + var interopMemberType = + interopMember.function.computeFunctionType(Nullability.nonNullable); + // Ignore the first argument `this` in the generated procedure. + return FunctionType( + interopMemberType.positionalParameters.skip(1).toList(), + interopMemberType.returnType, + interopMemberType.declaredNullability, + namedParameters: interopMemberType.namedParameters, + typeParameters: interopMemberType.typeParameters, + requiredParameterCount: interopMemberType.requiredParameterCount - 1); + } + } + + // Determine if the given Dart member is the right kind and subtype to + // implement the descriptor. + bool _implements( + Member? dartMember, ExtensionMemberDescriptor interopDescriptor) { + if (dartMember == null) return false; + + // If it isn't even the right kind, don't continue. + if (interopDescriptor.isGetter && !dartMember.isGetter) { + return false; + } else if (interopDescriptor.isSetter && !dartMember.isSetter) { + return false; + } else if (interopDescriptor.isMethod && dartMember is! Procedure) { + return false; + } + + bool isSubtypeOf(DartType dartType, DartType interopType) { + return _typeEnvironment.isSubtypeOf( + dartType, interopType, SubtypeCheckMode.withNullabilities); + } + + var interopType = _getTypeOfDescriptor(interopDescriptor); + + if (interopDescriptor.isGetter) { + if (!isSubtypeOf(dartMember.getterType, interopType)) { + return false; + } + } else if (interopDescriptor.isSetter) { + if (!isSubtypeOf(interopType, dartMember.setterType)) { + return false; + } + } else if (interopDescriptor.isMethod) { + if (!isSubtypeOf( + (dartMember as Procedure) + .function + .computeFunctionType(Nullability.nonNullable), + interopType)) { + return false; + } + } + return true; + } + + /// Compute a mapping between all the @staticInterop classes and their + /// extensions. + /// + /// We do this here instead of in the export checker for two reasons: + /// 1. Modular compilation may invalidate extensions, so we need some way to + /// get rid of old extensions. + /// 2. The work to do this is only done when you use the + /// `createStaticInteropMock` API, leaving unrelated libraries alone. + /// + /// TODO(srujzs): This does not take into account any scoping. This might mean + /// that if another library defines an extension on the @staticInterop class + /// that is outside of the scope of the current library, this API will report + /// an error. Considering this API should primarily be used in tests, such a + /// compilation will be unlikely, but we should revisit this. + Map> _computeStaticInteropExtensionMap() { + // Process the stored libaries, and create a mapping between @staticInterop + // classes and their extensions. + var staticInteropClassesWithExtensions = >{}; + for (var library in ExportChecker.libraryExtensionMap.keys) { + for (var extension in ExportChecker.libraryExtensionMap[library]!) { + var onType = extension.onType as InterfaceType; + staticInteropClassesWithExtensions + .putIfAbsent(onType.className, () => {}) + .add(extension); + } + } + return staticInteropClassesWithExtensions; + } + + /// Returns a map between all the implementable external extension member + /// names and the descriptors that have that name for [staticInteropClass]. + /// + /// Also computes a mapping between descriptors and their name for error + /// reporting. + Map> + _computeImplementableExtensionMembers(Class staticInteropClass) { + assert(js_interop.hasStaticInteropAnnotation(staticInteropClass)); + + // Get the cached result if we've already processed this class. + var exportNameToDescriptors = + _staticInteropExportNameToDescriptorMap[staticInteropClass]; + if (exportNameToDescriptors != null) { + return exportNameToDescriptors; + } else { + exportNameToDescriptors = >{}; + } + + var classes = {}; + // Compute a map of all the possible descriptors available in this type and + // the supertypes. + void getAllDescriptors(Class cls) { + if (classes.add(cls)) { + var extensions = _staticInteropClassesWithExtensions[cls.reference]; + if (extensions != null) { + for (var extension in extensions) { + for (var descriptor in extension.members) { + if (!descriptor.isExternal || descriptor.isStatic) continue; + // No need to handle external fields - they are transformed to + // external getters/setters by the CFE. + if (!descriptor.isGetter && + !descriptor.isSetter && + !descriptor.isMethod) { + continue; + } + _descriptorToExtensionName[descriptor] = + extension.isUnnamedExtension ? '' : extension.name; + var name = js_interop.getJSName(descriptor.member.asMember); + if (name.isEmpty) name = descriptor.name.text; + exportNameToDescriptors! + .putIfAbsent(name, () => {}) + .add(descriptor); + } + } + } + cls.supers.forEach((Supertype supertype) { + getAllDescriptors(supertype.classNode); + }); + } + } + + getAllDescriptors(staticInteropClass); + + return _staticInteropExportNameToDescriptorMap[staticInteropClass] = + exportNameToDescriptors; + } +} diff --git a/pkg/compiler/lib/src/kernel/dart2js_target.dart b/pkg/compiler/lib/src/kernel/dart2js_target.dart index 591d8b6bcaa..1cc334ffb87 100644 --- a/pkg/compiler/lib/src/kernel/dart2js_target.dart +++ b/pkg/compiler/lib/src/kernel/dart2js_target.dart @@ -11,9 +11,9 @@ library compiler.src.kernel.dart2js_target; import 'package:_fe_analyzer_shared/src/messages/codes.dart' show Message, LocatedMessage; import 'package:_js_interop_checks/js_interop_checks.dart'; +import 'package:_js_interop_checks/src/transformations/export_creator.dart'; import 'package:_js_interop_checks/src/transformations/js_util_optimizer.dart'; import 'package:_js_interop_checks/src/transformations/static_interop_class_eraser.dart'; -import 'package:_js_interop_checks/src/transformations/static_interop_mock_creator.dart'; import 'package:kernel/ast.dart' as ir; import 'package:kernel/class_hierarchy.dart'; import 'package:kernel/core_types.dart'; @@ -160,13 +160,11 @@ class Dart2jsTarget extends Target { for (var library in libraries) { jsInteropChecks.visitLibrary(library); } - var staticInteropMockCreator = StaticInteropMockCreator( - TypeEnvironment(coreTypes, hierarchy), - diagnosticReporter, - jsInteropChecks.exportChecker); + var exportCreator = ExportCreator(TypeEnvironment(coreTypes, hierarchy), + diagnosticReporter, jsInteropChecks.exportChecker); var jsUtilOptimizer = JsUtilOptimizer(coreTypes, hierarchy); for (var library in libraries) { - staticInteropMockCreator.visitLibrary(library); + exportCreator.visitLibrary(library); // TODO (rileyporter): Merge js_util optimizations with other lowerings // in the single pass in `transformations/lowering.dart`. jsUtilOptimizer.visitLibrary(library); diff --git a/pkg/dart2wasm/lib/target.dart b/pkg/dart2wasm/lib/target.dart index 1ad8f26cd75..44117dfa9bc 100644 --- a/pkg/dart2wasm/lib/target.dart +++ b/pkg/dart2wasm/lib/target.dart @@ -6,9 +6,9 @@ import 'package:_fe_analyzer_shared/src/messages/codes.dart' show Message, LocatedMessage; import 'package:_js_interop_checks/js_interop_checks.dart'; import 'package:_js_interop_checks/src/js_interop.dart' as jsInteropHelper; +import 'package:_js_interop_checks/src/transformations/export_creator.dart'; import 'package:_js_interop_checks/src/transformations/js_util_wasm_optimizer.dart'; import 'package:_js_interop_checks/src/transformations/static_interop_class_eraser.dart'; -import 'package:_js_interop_checks/src/transformations/static_interop_mock_creator.dart'; import 'package:kernel/ast.dart'; import 'package:kernel/class_hierarchy.dart'; import 'package:kernel/clone.dart'; @@ -109,13 +109,11 @@ class WasmTarget extends Target { for (Library library in interopDependentLibraries) { jsInteropChecks.visitLibrary(library); } - final staticInteropMockCreator = StaticInteropMockCreator( - TypeEnvironment(coreTypes, hierarchy), - diagnosticReporter, - jsInteropChecks.exportChecker); + final exportCreator = ExportCreator(TypeEnvironment(coreTypes, hierarchy), + diagnosticReporter, jsInteropChecks.exportChecker); final jsUtilOptimizer = JsUtilWasmOptimizer(coreTypes, hierarchy); for (Library library in interopDependentLibraries) { - staticInteropMockCreator.visitLibrary(library); + exportCreator.visitLibrary(library); jsUtilOptimizer.visitLibrary(library); } // Do the erasure after any possible mock creation to avoid erasing types diff --git a/pkg/dev_compiler/lib/src/kernel/target.dart b/pkg/dev_compiler/lib/src/kernel/target.dart index cc47eb96141..5274209895c 100644 --- a/pkg/dev_compiler/lib/src/kernel/target.dart +++ b/pkg/dev_compiler/lib/src/kernel/target.dart @@ -7,9 +7,9 @@ import 'dart:collection'; import 'package:_fe_analyzer_shared/src/messages/codes.dart' show Message, LocatedMessage; import 'package:_js_interop_checks/js_interop_checks.dart'; +import 'package:_js_interop_checks/src/transformations/export_creator.dart'; import 'package:_js_interop_checks/src/transformations/js_util_optimizer.dart'; import 'package:_js_interop_checks/src/transformations/static_interop_class_eraser.dart'; -import 'package:_js_interop_checks/src/transformations/static_interop_mock_creator.dart'; import 'package:kernel/class_hierarchy.dart'; import 'package:kernel/core_types.dart'; import 'package:kernel/kernel.dart'; @@ -174,14 +174,12 @@ class DevCompilerTarget extends Target { for (var library in libraries) { jsInteropChecks.visitLibrary(library); } - var staticInteropMockCreator = StaticInteropMockCreator( - TypeEnvironment(coreTypes, hierarchy), - diagnosticReporter, - jsInteropChecks.exportChecker); + var exportCreator = ExportCreator(TypeEnvironment(coreTypes, hierarchy), + diagnosticReporter, jsInteropChecks.exportChecker); var jsUtilOptimizer = JsUtilOptimizer(coreTypes, hierarchy); for (var library in libraries) { _CovarianceTransformer(library).transform(); - staticInteropMockCreator.visitLibrary(library); + exportCreator.visitLibrary(library); jsUtilOptimizer.visitLibrary(library); } // Do the erasure after any possible mock creation to avoid erasing types