diff --git a/pkg/_fe_analyzer_shared/lib/src/messages/codes_generated.dart b/pkg/_fe_analyzer_shared/lib/src/messages/codes_generated.dart index a96cb8e35f7..ca3aca0620f 100644 --- a/pkg/_fe_analyzer_shared/lib/src/messages/codes_generated.dart +++ b/pkg/_fe_analyzer_shared/lib/src/messages/codes_generated.dart @@ -7121,6 +7121,76 @@ const MessageCode messageJsInteropOperatorsNotSupported = const MessageCode( problemMessage: r"""JS interop classes do not support operator methods.""", correctionMessage: r"""Try replacing this with a normal method."""); +// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE. +const Template + templateJsInteropStaticInteropMockExternalExtensionMemberConflict = + const Template( + problemMessageTemplate: + r"""External extension member with name '#name' is defined in the following extensions and none are more specific: #string.""", + correctionMessageTemplate: + r"""Try using the `@JS` annotation to rename conflicting members.""", + withArguments: + _withArgumentsJsInteropStaticInteropMockExternalExtensionMemberConflict); + +// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE. +const Code + codeJsInteropStaticInteropMockExternalExtensionMemberConflict = + const Code( + "JsInteropStaticInteropMockExternalExtensionMemberConflict", +); + +// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE. +Message _withArgumentsJsInteropStaticInteropMockExternalExtensionMemberConflict( + String name, String string) { + if (name.isEmpty) throw 'No name provided'; + name = demangleMixinApplicationName(name); + if (string.isEmpty) throw 'No string provided'; + return new Message( + codeJsInteropStaticInteropMockExternalExtensionMemberConflict, + problemMessage: + """External extension member with name '${name}' is defined in the following extensions and none are more specific: ${string}.""", + correctionMessage: """Try using the `@JS` annotation to rename conflicting members.""", + arguments: {'name': name, 'string': string}); +} + +// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE. +const Template< + Message Function( + String name, + String name2, + String + name3)> templateJsInteropStaticInteropMockMissingOverride = const Template< + Message Function( + String name, String name2, String name3)>( + problemMessageTemplate: + r"""`@staticInterop` class '#name' has external extension member '#name2', but Dart class '#name3' does not have an overriding instance member.""", + correctionMessageTemplate: + r"""Add a Dart instance member in '#name3' that overrides '#name.#name2'.""", + withArguments: _withArgumentsJsInteropStaticInteropMockMissingOverride); + +// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE. +const Code + codeJsInteropStaticInteropMockMissingOverride = + const Code( + "JsInteropStaticInteropMockMissingOverride", +); + +// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE. +Message _withArgumentsJsInteropStaticInteropMockMissingOverride( + String name, String name2, String name3) { + if (name.isEmpty) throw 'No name provided'; + name = demangleMixinApplicationName(name); + if (name2.isEmpty) throw 'No name provided'; + name2 = demangleMixinApplicationName(name2); + if (name3.isEmpty) throw 'No name provided'; + name3 = demangleMixinApplicationName(name3); + return new Message(codeJsInteropStaticInteropMockMissingOverride, + problemMessage: + """`@staticInterop` class '${name}' has external extension member '${name2}', but Dart class '${name3}' does not have an overriding instance member.""", + correctionMessage: """Add a Dart instance member in '${name3}' that overrides '${name}.${name2}'.""", + arguments: {'name': name, 'name2': name2, 'name3': name3}); +} + // DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE. const Template templateJsInteropStaticInteropTrustTypesUsageNotAllowed = 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 new file mode 100644 index 00000000000..f73a0aaabe1 --- /dev/null +++ b/pkg/_js_interop_checks/lib/src/transformations/static_interop_mock_creator.dart @@ -0,0 +1,435 @@ +// 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 + templateJsInteropStaticInteropMockMemberNotSubtype, + templateJsInteropStaticInteropMockNotDartInterfaceType, + 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, + templateJsInteropStaticInteropMockMissingOverride, + templateJsInteropStaticInteropMockExternalExtensionMemberConflict; +import 'package:_js_interop_checks/src/js_interop.dart'; + +class _ExtensionVisitor extends RecursiveVisitor { + final Map staticInteropClassesWithExtensions; + + _ExtensionVisitor(this.staticInteropClassesWithExtensions); + + @override + void visitExtension(Extension extension) { + // TODO(srujzs): This code was written with the assumption there would be + // one single extension per `@staticInterop` class. This is no longer true + // and this code needs to be refactored to handle multiple extensions. + var onType = extension.onType; + if (onType is InterfaceType && + hasStaticInteropAnnotation(onType.classNode)) { + if (!staticInteropClassesWithExtensions.containsKey(onType.className)) { + staticInteropClassesWithExtensions[onType.className] = extension; + } + } + super.visitExtension(extension); + } +} + +class StaticInteropMockCreator extends Transformer { + late final _ExtensionVisitor _extensionVisitor; + final Map _staticInteropClassesWithExtensions = {}; + final TypeEnvironment _typeEnvironment; + final DiagnosticReporter _diagnosticReporter; + final Procedure _createStaticInteropMock; + + StaticInteropMockCreator(this._typeEnvironment, this._diagnosticReporter) + : _createStaticInteropMock = _typeEnvironment.coreTypes.index + .getTopLevelProcedure('dart:js_util', 'createStaticInteropMock') { + _extensionVisitor = _ExtensionVisitor(_staticInteropClassesWithExtensions); + } + + void processExtensions(Library library) => + _extensionVisitor.visitLibrary(library); + + @override + TreeNode visitStaticInvocation(StaticInvocation node) { + if (node.target != _createStaticInteropMock) return node; + var typeArguments = node.arguments.types; + assert(typeArguments.length == 2); + var staticInteropType = typeArguments[0]; + var dartType = typeArguments[1]; + var typeArgumentsError = false; + if (staticInteropType is! InterfaceType || + !hasStaticInteropAnnotation(staticInteropType.classNode)) { + _diagnosticReporter.report( + templateJsInteropStaticInteropMockNotStaticInteropType.withArguments( + staticInteropType, true), + node.fileOffset, + node.name.text.length, + node.location?.file); + typeArgumentsError = true; + } + if (dartType is! InterfaceType || + hasJSInteropAnnotation(dartType.classNode) || + hasStaticInteropAnnotation(dartType.classNode) || + hasAnonymousAnnotation(dartType.classNode)) { + _diagnosticReporter.report( + templateJsInteropStaticInteropMockNotDartInterfaceType.withArguments( + dartType, true), + node.fileOffset, + node.name.text.length, + node.location?.file); + typeArgumentsError = true; + } + // Can't proceed with these errors. + if (typeArgumentsError) return node; + + var staticInteropClass = (staticInteropType as InterfaceType).classNode; + var dartClass = (dartType as InterfaceType).classNode; + + var dartMemberMap = {}; + for (var procedure in dartClass.allInstanceProcedures) { + // We only care about concrete instance getters, setters, and methods. + if (procedure.isAbstract || + procedure.isStatic || + procedure.isExtensionMember || + procedure.isFactory) { + continue; + } + var name = procedure.name.text; + // Add a suffix to differentiate getters and setters. + if (procedure.isSetter) name += '='; + dartMemberMap[name] = procedure; + } + for (var field in dartClass.allInstanceFields) { + // We only care about concrete instance fields. + if (field.isAbstract || field.isStatic) continue; + var name = field.name.text; + dartMemberMap[name] = field; + if (!field.isFinal) { + // Add the setter. + name += '='; + dartMemberMap[name] = field; + } + } + + var conformanceError = false; + var nameToDescriptors = >{}; + var descriptorToClass = {}; + staticInteropClass.computeAllNonStaticExternalExtensionMembers( + nameToDescriptors, + descriptorToClass, + _staticInteropClassesWithExtensions, + _typeEnvironment); + for (var descriptorName in nameToDescriptors.keys) { + var descriptors = nameToDescriptors[descriptorName]!; + // In the case of a getter/setter, we may have 2 descriptors per extension + // with the same name, and therefore per class. So, only get one + // descriptor per class to determine if there are conflicts. + var visitedClasses = {}; + var descriptorConflicts = {}; + for (var descriptor in descriptors) { + if (visitedClasses.add(descriptorToClass[descriptor]!)) { + descriptorConflicts.add(descriptor); + } + } + if (descriptorConflicts.length > 1) { + // Conflict, report an error. + var violations = []; + for (var descriptor in descriptorConflicts) { + var cls = descriptorToClass[descriptor]!; + var extension = _staticInteropClassesWithExtensions[cls.reference]!; + var extensionName = + extension.isUnnamedExtension ? 'unnamed' : extension.name; + violations.add("'${cls.name}.$extensionName'"); + } + // Sort violations so error expectations can be deterministic. + violations.sort(); + _diagnosticReporter.report( + templateJsInteropStaticInteropMockExternalExtensionMemberConflict + .withArguments(descriptorName, violations.join(', ')), + node.fileOffset, + node.name.text.length, + node.location?.file); + conformanceError = true; + continue; + } + // With no conflicts, there should be either just 1 entry or 2 entries + // where one is a getter and the other is a setter in the same extension + // (and therefore the same @staticInterop class). + assert(descriptors.length == 1 || descriptors.length == 2); + if (descriptors.length == 2) { + var first = descriptors[0]; + var second = descriptors[1]; + assert(descriptorToClass[first]! == descriptorToClass[second]!); + assert((first.isGetter && second.isSetter) || + (first.isSetter && second.isGetter)); + } + for (var interopDescriptor in descriptors) { + var dartMemberName = descriptorName; + // Distinguish getters and setters for overriding conformance. + if (interopDescriptor.isSetter) dartMemberName += '='; + + // Determine whether the Dart instance member with the same name as the + // `@staticInterop` procedure is the right type of member such that it + // can be considered an override. + bool validOverridingMemberType() { + var dartMember = dartMemberMap[dartMemberName]!; + if (interopDescriptor.isGetter && + dartMember is! Field && + !(dartMember as Procedure).isGetter) { + return false; + } else if (interopDescriptor.isSetter && + dartMember is! Field && + !(dartMember as Procedure).isSetter) { + return false; + } else if (interopDescriptor.isMethod && dartMember is! Procedure) { + return false; + } + return true; + } + + if (!dartMemberMap.containsKey(dartMemberName) || + !validOverridingMemberType()) { + _diagnosticReporter.report( + templateJsInteropStaticInteropMockMissingOverride.withArguments( + staticInteropClass.name, dartMemberName, dartClass.name), + node.fileOffset, + node.name.text.length, + node.location?.file); + conformanceError = true; + continue; + } + var dartMember = dartMemberMap[dartMemberName]!; + + // Determine if the given type of the Dart member is a valid subtype of + // the given type of the `@staticInterop` member. If not, report an + // error to the user. + bool overrideIsSubtype(DartType? dartType, DartType? interopType) { + if (dartType == null || + interopType == null || + !_typeEnvironment.isSubtypeOf( + dartType, interopType, SubtypeCheckMode.withNullabilities)) { + _diagnosticReporter.report( + templateJsInteropStaticInteropMockMemberNotSubtype + .withArguments( + dartClass.name, + dartMemberName, + dartType ?? NullType(), + staticInteropClass.name, + dartMemberName, + interopType ?? NullType(), + true), + node.fileOffset, + node.name.text.length, + node.location?.file); + return false; + } + return true; + } + + // CFE creates static procedures for each extension member. + var interopMember = interopDescriptor.member.node as Procedure; + DartType getGetterFunctionType(DartType getterType) { + return FunctionType([], getterType, Nullability.nonNullable); + } + + DartType getSetterFunctionType(DartType setterType) { + return FunctionType( + [setterType], VoidType(), Nullability.nonNullable); + } + + if (interopDescriptor.isGetter && + !overrideIsSubtype(getGetterFunctionType(dartMember.getterType), + getGetterFunctionType(interopMember.function.returnType))) { + conformanceError = true; + continue; + } else if (interopDescriptor.isSetter && + !overrideIsSubtype( + getSetterFunctionType(dartMember.setterType), + // Ignore the first argument `this` in the generated procedure. + getSetterFunctionType( + interopMember.function.positionalParameters[1].type))) { + conformanceError = true; + continue; + } else if (interopDescriptor.isMethod) { + var interopMemberType = interopMember.function + .computeFunctionType(Nullability.nonNullable); + // Ignore the first argument `this` in the generated procedure. + interopMemberType = FunctionType( + interopMemberType.positionalParameters.skip(1).toList(), + interopMemberType.returnType, + interopMemberType.declaredNullability, + namedParameters: interopMemberType.namedParameters, + typeParameters: interopMemberType.typeParameters, + requiredParameterCount: + interopMemberType.requiredParameterCount - 1); + if (!overrideIsSubtype( + (dartMember as Procedure) + .function + .computeFunctionType(Nullability.nonNullable), + interopMemberType)) { + conformanceError = true; + continue; + } + } + } + } + // The interfaces do not conform and therefore we can't create a mock. + if (conformanceError) return node; + // TODO(srujzs): Create a mocking object. + return super.visitStaticInvocation(node); + } +} + +extension _DartClassExtension on Class { + List get allInstanceProcedures { + var allProcs = []; + Class? cls = this; + // We only care about instance procedures that have a body. + bool isInstanceProcedure(Procedure proc) => + !proc.isAbstract && + !proc.isStatic && + !proc.isExtensionMember && + !proc.isFactory; + while (cls != null) { + allProcs.addAll(cls.procedures.where(isInstanceProcedure)); + // Mixin members override the given superclass' members, but are + // overridden by the class' instance members, so they are inserted next. + if (cls.isMixinApplication) { + allProcs.addAll(cls.mixin.procedures.where(isInstanceProcedure)); + } + cls = cls.superclass; + } + // We inserted procedures from subtype to supertypes, so reverse them so + // that overridden members come first, with their overrides last. + return allProcs.reversed.toList(); + } + + List get allInstanceFields { + var allFields = []; + Class? cls = this; + bool isInstanceField(Field field) => !field.isAbstract && !field.isStatic; + while (cls != null) { + allFields.addAll(cls.fields.where(isInstanceField)); + if (cls.isMixinApplication) { + allFields.addAll(cls.mixin.fields.where(isInstanceField)); + } + cls = cls.superclass; + } + return allFields.reversed.toList(); + } +} + +extension _StaticInteropClassExtension on Class { + /// Sets [nameToDescriptors] to be a map between all the available external + /// extension member names and the descriptors that have that name, and also + /// sets [descriptorToClass] to be a mapping between every external extension + /// member and its on-type. + /// + /// [staticInteropClassesWithExtensions] is a map between all the + /// `@staticInterop` classes and their singular extension. [typeEnvironment] + /// is the current component's `TypeEnvironment`. + /// + /// Note: The algorithm to determine the most-specific extension member in the + /// event of name collisions does not conform to the specificity rules + /// described here: + /// https://github.com/dart-lang/language/blob/master/accepted/2.7/static-extension-methods/feature-specification.md#specificity. + /// Instead, it only uses subtype checking of the on-types to find the most + /// specific member. This is mostly benign as: + /// 1. There's a single extension per @staticInterop class, so conflicts occur + /// between classes and not within them. + /// 2. Generics in the context of interop are by design supposed to be more + /// rare, and external extension members are already disallowed from using + /// type parameters. This lowers the importance of checking for instantiation + /// to bounds. + void computeAllNonStaticExternalExtensionMembers( + Map> nameToDescriptors, + Map descriptorToClass, + Map staticInteropClassesWithExtensions, + TypeEnvironment typeEnvironment) { + assert(hasStaticInteropAnnotation(this)); + 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)) { + if (staticInteropClassesWithExtensions.containsKey(cls.reference)) { + for (var descriptor + in staticInteropClassesWithExtensions[cls.reference]!.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; + } + descriptorToClass[descriptor] = cls; + nameToDescriptors + .putIfAbsent(descriptor.name.text, () => []) + .add(descriptor); + } + } + cls.supers.forEach((Supertype supertype) { + getAllDescriptors(supertype.classNode); + }); + } + } + + getAllDescriptors(this); + + InterfaceType getOnType(ExtensionMemberDescriptor desc) => + InterfaceType(descriptorToClass[desc]!, Nullability.nonNullable); + + bool isStrictSubtypeOf(InterfaceType s, InterfaceType t) { + if (s.className == t.className) return false; + return typeEnvironment.isSubtypeOf( + s, t, SubtypeCheckMode.withNullabilities); + } + + // Try and find the most specific member amongst duplicate names using + // subtype checks. + for (var name in nameToDescriptors.keys) { + // The set of potential targets whose on-types are not strict subtypes of + // any other target's on-type. As we iterate through the descriptors, this + // invariant will hold true. + var targets = []; + for (var descriptor in nameToDescriptors[name]!) { + if (targets.isEmpty) { + targets.add(descriptor); + } else { + var newOnType = getOnType(descriptor); + // For each existing target, if the new descriptor's on-type is a + // strict subtype of the target's on-type, then the new descriptor is + // more specific. If any of the existing targets' on-types are a + // strict subtype of the new descriptor's on-type, then the new + // descriptor is never more specific, and therefore can be ignored. + if (!targets.any( + (target) => isStrictSubtypeOf(getOnType(target), newOnType))) { + targets = [ + descriptor, + // Not a supertype or a subtype, potential conflict or simply a + // setter and getter. + ...targets.where( + (target) => !isStrictSubtypeOf(newOnType, getOnType(target))), + ]; + } + } + } + nameToDescriptors[name] = targets; + } + } +} + +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.node as Procedure).isExternal; +} diff --git a/pkg/_js_interop_checks/pubspec.yaml b/pkg/_js_interop_checks/pubspec.yaml index 786fd8d1465..fcb1de53407 100644 --- a/pkg/_js_interop_checks/pubspec.yaml +++ b/pkg/_js_interop_checks/pubspec.yaml @@ -8,6 +8,7 @@ environment: # Use 'any' constraints here; we get our versions from the DEPS file. dependencies: _fe_analyzer_shared: any + front_end: any kernel: any # Use 'any' constraints here; we get our versions from the DEPS file. diff --git a/pkg/compiler/lib/src/kernel/dart2js_target.dart b/pkg/compiler/lib/src/kernel/dart2js_target.dart index 96a9081989b..e4bda648d72 100644 --- a/pkg/compiler/lib/src/kernel/dart2js_target.dart +++ b/pkg/compiler/lib/src/kernel/dart2js_target.dart @@ -13,12 +13,14 @@ import 'package:_fe_analyzer_shared/src/messages/codes.dart' import 'package:_js_interop_checks/js_interop_checks.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'; import 'package:kernel/reference_from_index.dart'; import 'package:kernel/target/changed_structure_notifier.dart'; import 'package:kernel/target/targets.dart'; +import 'package:kernel/type_environment.dart'; import '../options.dart'; import 'invocation_mirror_constants.dart'; @@ -154,19 +156,28 @@ class Dart2jsTarget extends Target { coreTypes, diagnosticReporter as DiagnosticReporter, _nativeClasses!); + var staticInteropMockCreator = StaticInteropMockCreator( + TypeEnvironment(coreTypes, hierarchy), diagnosticReporter); var jsUtilOptimizer = JsUtilOptimizer(coreTypes, hierarchy); - var staticInteropClassEraser = - StaticInteropClassEraser(coreTypes, referenceFromIndex); + // Cache extensions for entire component before creating mock. + for (var library in libraries) { + staticInteropMockCreator.processExtensions(library); + } for (var library in libraries) { jsInteropChecks.visitLibrary(library); + staticInteropMockCreator.visitLibrary(library); // TODO (rileyporter): Merge js_util optimizations with other lowerings // in the single pass in `transformations/lowering.dart`. jsUtilOptimizer.visitLibrary(library); } + var staticInteropClassEraser = + StaticInteropClassEraser(coreTypes, referenceFromIndex); lowering.transformLibraries(libraries, coreTypes, hierarchy, options); logger?.call("Lowering transformations performed"); if (canPerformGlobalTransforms) { transformMixins.transformLibraries(libraries); + // Do the erasure after any possible mock creation to avoid erasing types + // that need to be used during mock conformance checking. for (var library in libraries) { staticInteropClassEraser.visitLibrary(library); } diff --git a/pkg/dart2wasm/lib/target.dart b/pkg/dart2wasm/lib/target.dart index 44745ce84d1..2c65c2d2cbb 100644 --- a/pkg/dart2wasm/lib/target.dart +++ b/pkg/dart2wasm/lib/target.dart @@ -8,6 +8,7 @@ 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/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'; @@ -15,6 +16,7 @@ import 'package:kernel/core_types.dart'; import 'package:kernel/reference_from_index.dart'; import 'package:kernel/target/changed_structure_notifier.dart'; import 'package:kernel/target/targets.dart'; +import 'package:kernel/type_environment.dart'; import 'package:vm/transformations/mixin_full_resolution.dart' as transformMixins show transformLibraries; import 'package:vm/transformations/ffi/common.dart' as ffiHelper @@ -102,14 +104,25 @@ class WasmTarget extends Target { coreTypes, diagnosticReporter as DiagnosticReporter, _nativeClasses!); + final staticInteropMockCreator = StaticInteropMockCreator( + TypeEnvironment(coreTypes, hierarchy), diagnosticReporter); final jsUtilOptimizer = JsUtilWasmOptimizer(coreTypes, hierarchy); + // Cache extensions for entire component before creating mock. + for (Library library in interopDependentLibraries) { + staticInteropMockCreator.processExtensions(library); + } + for (Library library in interopDependentLibraries) { + jsInteropChecks.visitLibrary(library); + staticInteropMockCreator.visitLibrary(library); + jsUtilOptimizer.visitLibrary(library); + } + // Do the erasure after any possible mock creation to avoid erasing types + // that need to be used during mock conformance checking. final staticInteropClassEraser = StaticInteropClassEraser( coreTypes, referenceFromIndex, libraryForJavaScriptObject: 'dart:_js_helper', classNameOfJavaScriptObject: 'JSValue'); for (Library library in interopDependentLibraries) { - jsInteropChecks.visitLibrary(library); - jsUtilOptimizer.visitLibrary(library); staticInteropClassEraser.visitLibrary(library); } } diff --git a/pkg/dev_compiler/lib/src/kernel/target.dart b/pkg/dev_compiler/lib/src/kernel/target.dart index 3b2416f8412..70b8712e903 100644 --- a/pkg/dev_compiler/lib/src/kernel/target.dart +++ b/pkg/dev_compiler/lib/src/kernel/target.dart @@ -9,6 +9,7 @@ import 'package:_fe_analyzer_shared/src/messages/codes.dart' import 'package:_js_interop_checks/js_interop_checks.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'; @@ -16,6 +17,7 @@ import 'package:kernel/reference_from_index.dart'; import 'package:kernel/target/changed_structure_notifier.dart'; import 'package:kernel/target/targets.dart'; import 'package:kernel/transformations/track_widget_constructor_locations.dart'; +import 'package:kernel/type_environment.dart'; import 'constants.dart' show DevCompilerConstantsBackend; import 'kernel_helpers.dart'; @@ -168,13 +170,24 @@ class DevCompilerTarget extends Target { coreTypes, diagnosticReporter as DiagnosticReporter, _nativeClasses!); + var staticInteropMockCreator = StaticInteropMockCreator( + TypeEnvironment(coreTypes, hierarchy), diagnosticReporter); var jsUtilOptimizer = JsUtilOptimizer(coreTypes, hierarchy); - var staticInteropClassEraser = - StaticInteropClassEraser(coreTypes, referenceFromIndex); + // Cache extensions for entire component before creating mock. + for (var library in libraries) { + staticInteropMockCreator.processExtensions(library); + } for (var library in libraries) { _CovarianceTransformer(library).transform(); jsInteropChecks.visitLibrary(library); + staticInteropMockCreator.visitLibrary(library); jsUtilOptimizer.visitLibrary(library); + } + // Do the erasure after any possible mock creation to avoid erasing types + // that need to be used during mock conformance checking. + var staticInteropClassEraser = + StaticInteropClassEraser(coreTypes, referenceFromIndex); + for (var library in libraries) { staticInteropClassEraser.visitLibrary(library); } } diff --git a/pkg/front_end/lib/src/fasta/fasta_codes_cfe_generated.dart b/pkg/front_end/lib/src/fasta/fasta_codes_cfe_generated.dart index 15788e80e2c..b55c1177e9f 100644 --- a/pkg/front_end/lib/src/fasta/fasta_codes_cfe_generated.dart +++ b/pkg/front_end/lib/src/fasta/fasta_codes_cfe_generated.dart @@ -3658,6 +3658,154 @@ Message _withArgumentsInvalidReturnPartNullability( }); } +// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE. +const Template< + Message Function( + String name, + String name2, + DartType _type, + String name3, + String name4, + DartType _type2, + bool isNonNullableByDefault)> + templateJsInteropStaticInteropMockMemberNotSubtype = const Template< + Message Function( + String name, + String name2, + DartType _type, + String name3, + String name4, + DartType _type2, + bool isNonNullableByDefault)>( + problemMessageTemplate: + r"""Dart class member '#name.#name2' with type '#type' is not a subtype of `@staticInterop` external extension member '#name3.#name4' with type '#type2'.""", + correctionMessageTemplate: + r"""Change '#name.#name2' to be a subtype of '#name3.#name4'.""", + withArguments: + _withArgumentsJsInteropStaticInteropMockMemberNotSubtype); + +// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE. +const Code< + Message Function( + String name, + String name2, + DartType _type, + String name3, + String name4, + DartType _type2, + bool isNonNullableByDefault)> + codeJsInteropStaticInteropMockMemberNotSubtype = const Code< + Message Function( + String name, + String name2, + DartType _type, + String name3, + String name4, + DartType _type2, + bool isNonNullableByDefault)>( + "JsInteropStaticInteropMockMemberNotSubtype", +); + +// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE. +Message _withArgumentsJsInteropStaticInteropMockMemberNotSubtype( + String name, + String name2, + DartType _type, + String name3, + String name4, + DartType _type2, + bool isNonNullableByDefault) { + if (name.isEmpty) throw 'No name provided'; + name = demangleMixinApplicationName(name); + if (name2.isEmpty) throw 'No name provided'; + name2 = demangleMixinApplicationName(name2); + TypeLabeler labeler = new TypeLabeler(isNonNullableByDefault); + List typeParts = labeler.labelType(_type); + if (name3.isEmpty) throw 'No name provided'; + name3 = demangleMixinApplicationName(name3); + if (name4.isEmpty) throw 'No name provided'; + name4 = demangleMixinApplicationName(name4); + List type2Parts = labeler.labelType(_type2); + String type = typeParts.join(); + String type2 = type2Parts.join(); + return new Message(codeJsInteropStaticInteropMockMemberNotSubtype, + problemMessage: + """Dart class member '${name}.${name2}' with type '${type}' is not a subtype of `@staticInterop` external extension member '${name3}.${name4}' with type '${type2}'.""" + + labeler.originMessages, + correctionMessage: + """Change '${name}.${name2}' to be a subtype of '${name3}.${name4}'.""", + arguments: { + 'name': name, + 'name2': name2, + 'type': _type, + 'name3': name3, + 'name4': name4, + 'type2': _type2 + }); +} + +// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE. +const Template + templateJsInteropStaticInteropMockNotDartInterfaceType = const Template< + Message Function(DartType _type, bool isNonNullableByDefault)>( + problemMessageTemplate: + r"""Second type argument '#type' is not a Dart interface type.""", + correctionMessageTemplate: r"""Use a Dart class instead.""", + withArguments: + _withArgumentsJsInteropStaticInteropMockNotDartInterfaceType); + +// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE. +const Code + codeJsInteropStaticInteropMockNotDartInterfaceType = + const Code( + "JsInteropStaticInteropMockNotDartInterfaceType", +); + +// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE. +Message _withArgumentsJsInteropStaticInteropMockNotDartInterfaceType( + DartType _type, bool isNonNullableByDefault) { + TypeLabeler labeler = new TypeLabeler(isNonNullableByDefault); + List typeParts = labeler.labelType(_type); + String type = typeParts.join(); + return new Message(codeJsInteropStaticInteropMockNotDartInterfaceType, + problemMessage: + """Second type argument '${type}' is not a Dart interface type.""" + + labeler.originMessages, + correctionMessage: """Use a Dart class instead.""", + arguments: {'type': _type}); +} + +// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE. +const Template + templateJsInteropStaticInteropMockNotStaticInteropType = const Template< + Message Function(DartType _type, bool isNonNullableByDefault)>( + problemMessageTemplate: + r"""First type argument '#type' is not a `@staticInterop` type.""", + correctionMessageTemplate: r"""Use a `@staticInterop` class instead.""", + withArguments: + _withArgumentsJsInteropStaticInteropMockNotStaticInteropType); + +// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE. +const Code + codeJsInteropStaticInteropMockNotStaticInteropType = + const Code( + "JsInteropStaticInteropMockNotStaticInteropType", +); + +// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE. +Message _withArgumentsJsInteropStaticInteropMockNotStaticInteropType( + DartType _type, bool isNonNullableByDefault) { + TypeLabeler labeler = new TypeLabeler(isNonNullableByDefault); + List typeParts = labeler.labelType(_type); + String type = typeParts.join(); + return new Message(codeJsInteropStaticInteropMockNotStaticInteropType, + problemMessage: + """First type argument '${type}' is not a `@staticInterop` type.""" + + labeler.originMessages, + correctionMessage: """Use a `@staticInterop` class instead.""", + arguments: {'type': _type}); +} + // DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE. const Template< Message Function( diff --git a/pkg/front_end/messages.status b/pkg/front_end/messages.status index 3fa00126107..d90b483bffe 100644 --- a/pkg/front_end/messages.status +++ b/pkg/front_end/messages.status @@ -579,6 +579,20 @@ JsInteropNonExternalMember/analyzerCode: Fail # Web compiler specific JsInteropNonExternalMember/example: Fail # Web compiler specific JsInteropOperatorsNotSupported/analyzerCode: Fail # Web compiler specific JsInteropOperatorsNotSupported/example: Fail # Web compiler specific +JsInteropStaticInteropMockExternalExtensionMemberConflict/analyzerCode: Fail # Web compiler specific +JsInteropStaticInteropMockExternalExtensionMemberConflict/example: Fail # Web compiler specific +JsInteropStaticInteropMockMemberNotSubtype/analyzerCode: Fail # Web compiler specific +JsInteropStaticInteropMockMemberNotSubtype/example: Fail # Web compiler specific +JsInteropStaticInteropMockMissingOverride/analyzerCode: Fail # Web compiler specific +JsInteropStaticInteropMockMissingOverride/example: Fail # Web compiler specific +JsInteropStaticInteropMockNotDartInterfaceType/analyzerCode: Fail # Web compiler specific +JsInteropStaticInteropMockNotDartInterfaceType/example: Fail # Web compiler specific +JsInteropStaticInteropMockNotStaticInteropType/analyzerCode: Fail # Web compiler specific +JsInteropStaticInteropMockNotStaticInteropType/example: Fail # Web compiler specific +JsInteropStaticInteropWithInstanceMembers/analyzerCode: Fail # Web compiler specific +JsInteropStaticInteropWithInstanceMembers/example: Fail # Web compiler specific +JsInteropStaticInteropWithNonStaticSupertype/analyzerCode: Fail # Web compiler specific +JsInteropStaticInteropWithNonStaticSupertype/example: Fail # Web compiler specific JsInteropStaticInteropTrustTypesUsageNotAllowed/analyzerCode: Fail # Web compiler specific JsInteropStaticInteropTrustTypesUsageNotAllowed/example: Fail # Web compiler specific JsInteropStaticInteropTrustTypesUsedWithoutStaticInterop/analyzerCode: Fail # Web compiler specific diff --git a/pkg/front_end/messages.yaml b/pkg/front_end/messages.yaml index 47824cca37f..b194ebb09a2 100644 --- a/pkg/front_end/messages.yaml +++ b/pkg/front_end/messages.yaml @@ -5231,6 +5231,26 @@ JsInteropOperatorsNotSupported: JsInteropInvalidStaticClassMemberName: problemMessage: "JS interop static class members cannot have '.' in their JS name." +JsInteropStaticInteropMockExternalExtensionMemberConflict: + problemMessage: "External extension member with name '#name' is defined in the following extensions and none are more specific: #string." + correctionMessage: "Try using the `@JS` annotation to rename conflicting members." + +JsInteropStaticInteropMockMemberNotSubtype: + problemMessage: "Dart class member '#name.#name2' with type '#type' is not a subtype of `@staticInterop` external extension member '#name3.#name4' with type '#type2'." + correctionMessage: "Change '#name.#name2' to be a subtype of '#name3.#name4'." + +JsInteropStaticInteropMockMissingOverride: + problemMessage: "`@staticInterop` class '#name' has external extension member '#name2', but Dart class '#name3' does not have an overriding instance member." + correctionMessage: "Add a Dart instance member in '#name3' that overrides '#name.#name2'." + +JsInteropStaticInteropMockNotDartInterfaceType: + problemMessage: "Second type argument '#type' is not a Dart interface type." + correctionMessage: "Use a Dart class instead." + +JsInteropStaticInteropMockNotStaticInteropType: + problemMessage: "First type argument '#type' is not a `@staticInterop` type." + correctionMessage: "Use a `@staticInterop` class instead." + JsInteropStaticInteropWithInstanceMembers: problemMessage: "JS interop class '#name' with `@staticInterop` annotation cannot declare instance members." correctionMessage: "Try moving the instance member to a static extension." diff --git a/pkg/front_end/test/spell_checking_list_messages.txt b/pkg/front_end/test/spell_checking_list_messages.txt index 41f1bc8b8aa..575ec3d32b6 100644 --- a/pkg/front_end/test/spell_checking_list_messages.txt +++ b/pkg/front_end/test/spell_checking_list_messages.txt @@ -54,6 +54,7 @@ migrate n name.#name name.stack +name3.#name nameokempty native('native nativefieldwrapperclass diff --git a/sdk/lib/js_util/js_util.dart b/sdk/lib/js_util/js_util.dart index e47e6eb2fa7..d3a45107f4c 100644 --- a/sdk/lib/js_util/js_util.dart +++ b/sdk/lib/js_util/js_util.dart @@ -159,3 +159,39 @@ external bool isJavaScriptSimpleObject(value); /// converts it to a Dart based object. Only JS primitives, arrays, or 'map' /// like JS objects are supported. external Object? dartify(Object? o); + +/// DO NOT USE - THIS IS UNIMPLEMENTED. +/// +/// Given a `@staticInterop` type T and an instance [dartMock] of a Dart class +/// U that implements the external extension members of T, creates a forwarding +/// mock. +/// +/// When external extension members are called, they will forward to the +/// corresponding implementing member in [dartMock]. If U does not implement all +/// the external extension members of T, or if U does not properly override +/// them, it will be considered a compile-time error. +/// +/// For example: +/// +/// ``` +/// @JS() +/// @staticInterop +/// class JSClass {} +/// +/// extension JSClassExtension on JSClass { +/// external String stringify(int param); +/// } +/// +/// class DartClass { +/// String stringify(num param) => param.toString(); +/// } +/// +/// ... +/// +/// JSClass mock = createStaticInteropMock(DartClass()); +/// ``` +/// +/// TODO(srujzs): Add more detail on how inherited extension members need to be +/// implemented, as well as how conflicts are resolved (if they are resolvable). +/// The semantics here tries to conform to the view type specification. +external T createStaticInteropMock(U dartMock);