| // Copyright (c) 2016, 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. | |
| library; | |
| import 'package:kernel/ast.dart' as ir; | |
| import '../common.dart'; | |
| import '../common/elements.dart' show ElementEnvironment; | |
| import '../elements/entities.dart'; | |
| import '../elements/names.dart'; | |
| import '../elements/types.dart'; | |
| import '../ir/annotations.dart'; | |
| import '../js_model/js_to_frontend_map.dart' show identity, JsToFrontendMap; | |
| import '../kernel/element_map.dart'; | |
| import '../native/behavior.dart' show NativeBehavior; | |
| import '../serialization/serialization.dart'; | |
| import '../universe/call_structure.dart'; | |
| import '../universe/selector.dart'; | |
| import '../util/util.dart'; | |
| class NativeBasicDataBuilder { | |
| bool _closed = false; | |
| /// Tag info for native JavaScript classes names. See | |
| /// [setNativeClassTagInfo]. | |
| final Map<ClassEntity, NativeClassTag> _nativeClassTagInfo = {}; | |
| /// The JavaScript libraries implemented via typed JavaScript interop. | |
| final Map<LibraryEntity, String> _jsInteropLibraries = {}; | |
| /// The JavaScript classes implemented via typed JavaScript interop. | |
| final Map<ClassEntity, String> _jsInteropClasses = {}; | |
| /// JavaScript interop classes annotated with `@anonymous`. | |
| final Set<ClassEntity> _anonymousJsInteropClasses = {}; | |
| /// JavaScript interop classes annotated with `@staticInterop`. | |
| final Set<ClassEntity> _staticInteropClasses = {}; | |
| /// The JavaScript members implemented via typed JavaScript interop. | |
| final Map<MemberEntity, String> _jsInteropMembers = {}; | |
| /// Sets the native tag info for [cls]. | |
| /// | |
| /// The tag info string contains comma-separated 'words' which are either | |
| /// dispatch tags (having JavaScript identifier syntax) and directives that | |
| /// begin with `!`. | |
| void setNativeClassTagInfo(ClassEntity cls, String tagText) { | |
| assert( | |
| !_closed, | |
| failedAt( | |
| cls, | |
| "NativeBasicDataBuilder is closed. " | |
| "Trying to mark $cls as a native class.", | |
| ), | |
| ); | |
| // TODO(johnniwinther): Assert that this is only called once. The memory | |
| // compiler copies pre-processed elements into a new compiler through | |
| // [Compiler.onLibraryScanned] and thereby causes multiple calls to this | |
| // method. | |
| assert( | |
| !_nativeClassTagInfo.containsKey(cls) || | |
| _nativeClassTagInfo[cls]!.text == tagText, | |
| failedAt( | |
| cls, | |
| "Native tag info set inconsistently on $cls: " | |
| "Existing tag info '${_nativeClassTagInfo[cls]}', " | |
| "new tag info '$tagText'.", | |
| ), | |
| ); | |
| _nativeClassTagInfo[cls] = NativeClassTag(tagText); | |
| } | |
| /// Marks [element] as an explicit part of js interop. | |
| /// | |
| /// If [name] is provided, it sets the explicit js interop name for the | |
| /// library [element], other the js interop name is expected to be computed | |
| /// later. | |
| void markAsJsInteropLibrary(LibraryEntity element, {required String name}) { | |
| assert( | |
| !_closed, | |
| failedAt( | |
| element, | |
| "NativeBasicDataBuilder is closed. " | |
| "Trying to mark $element as a js-interop library.", | |
| ), | |
| ); | |
| _jsInteropLibraries[element] = name; | |
| } | |
| /// Marks [element] as an explicit part of js interop. | |
| /// | |
| /// If [name] is provided, it sets the explicit js interop name for the | |
| /// class [element], other the js interop name is expected to be computed | |
| /// later. | |
| void markAsJsInteropClass( | |
| ClassEntity element, { | |
| required String name, | |
| required bool isAnonymous, | |
| required bool isStaticInterop, | |
| }) { | |
| assert( | |
| !_closed, | |
| failedAt( | |
| element, | |
| "NativeBasicDataBuilder is closed. " | |
| "Trying to mark $element as a js-interop class.", | |
| ), | |
| ); | |
| _jsInteropClasses[element] = name; | |
| if (isAnonymous) { | |
| _anonymousJsInteropClasses.add(element); | |
| } | |
| if (isStaticInterop) { | |
| _staticInteropClasses.add(element); | |
| } | |
| } | |
| /// Marks [element] as an explicit part of js interop and sets the explicit js | |
| /// interop [name] for the member [element]. | |
| void markAsJsInteropMember(MemberEntity element, String name) { | |
| assert( | |
| !_closed, | |
| failedAt( | |
| element, | |
| "NativeBasicDataBuilder is closed. " | |
| "Trying to mark $element as a js-interop member.", | |
| ), | |
| ); | |
| _jsInteropMembers[element] = name; | |
| } | |
| /// Creates the [NativeBasicData] object for the data collected in this | |
| /// builder. | |
| NativeBasicData close(ElementEnvironment environment) { | |
| _closed = true; | |
| return NativeBasicData( | |
| environment, | |
| false, | |
| _nativeClassTagInfo, | |
| _jsInteropLibraries, | |
| _jsInteropClasses, | |
| _anonymousJsInteropClasses, | |
| _staticInteropClasses, | |
| _jsInteropMembers, | |
| ); | |
| } | |
| void reopenForTesting() { | |
| _closed = false; | |
| } | |
| } | |
| /// Basic information for native classes and js-interop libraries and classes. | |
| /// | |
| /// This information is computed during loading using [NativeBasicDataBuilder]. | |
| class NativeBasicData { | |
| /// Tag used for identifying serialized [NativeBasicData] objects in a | |
| /// debugging data stream. | |
| static const String tag = 'native-basic-data'; | |
| final ElementEnvironment _env; | |
| bool _isAllowInteropUsed; | |
| /// Tag info for native JavaScript classes names. See | |
| /// [setNativeClassTagInfo]. | |
| final Map<ClassEntity, NativeClassTag> _nativeClassTagInfo; | |
| /// The JavaScript libraries implemented via typed JavaScript interop. | |
| final Map<LibraryEntity, String> _jsInteropLibraries; | |
| /// The JavaScript classes implemented via typed JavaScript interop. | |
| final Map<ClassEntity, String> _jsInteropClasses; | |
| /// JavaScript interop classes annotated with `@anonymous` | |
| final Set<ClassEntity> _anonymousJsInteropClasses; | |
| /// JavaScript interop classes annotated with `@staticInterop` | |
| final Set<ClassEntity> _staticInteropClasses; | |
| /// The JavaScript members implemented via typed JavaScript interop. | |
| final Map<MemberEntity, String?> _jsInteropMembers; | |
| NativeBasicData( | |
| this._env, | |
| this._isAllowInteropUsed, | |
| this._nativeClassTagInfo, | |
| this._jsInteropLibraries, | |
| this._jsInteropClasses, | |
| this._anonymousJsInteropClasses, | |
| this._staticInteropClasses, | |
| this._jsInteropMembers, | |
| ); | |
| factory NativeBasicData.fromIr( | |
| KernelToElementMap map, | |
| IrAnnotationData data, | |
| ) { | |
| ElementEnvironment env = map.elementEnvironment; | |
| Map<ClassEntity, NativeClassTag> nativeClassTagInfo = {}; | |
| Map<LibraryEntity, String> jsInteropLibraries = {}; | |
| Map<ClassEntity, String> jsInteropClasses = {}; | |
| Set<ClassEntity> anonymousJsInteropClasses = {}; | |
| Set<ClassEntity> staticInteropClasses = {}; | |
| Map<MemberEntity, String?> jsInteropMembers = {}; | |
| data.forEachNativeClass((ir.Class node, String text) { | |
| nativeClassTagInfo[map.getClass(node)] = NativeClassTag(text); | |
| }); | |
| data.forEachJsInteropLibrary((ir.Library node, String name) { | |
| jsInteropLibraries[env.lookupLibrary(node.importUri, required: true)!] = | |
| name; | |
| }); | |
| data.forEachJsInteropClass(( | |
| ir.Class node, | |
| String name, { | |
| required bool isAnonymous, | |
| required bool isStaticInterop, | |
| }) { | |
| ClassEntity cls = map.getClass(node); | |
| jsInteropClasses[cls] = name; | |
| if (isAnonymous) { | |
| anonymousJsInteropClasses.add(cls); | |
| } | |
| if (isStaticInterop) { | |
| staticInteropClasses.add(cls); | |
| } | |
| }); | |
| data.forEachJsInteropMember((ir.Member node, String? name) { | |
| // TODO(49428): Are there other members that we should ignore here? | |
| // There are non-external and unannotated members because the source code | |
| // doesn't contain them. (e.g. default constructor) Does it make sense to | |
| // consider these valid JS members? | |
| if (memberIsIgnorable(node)) return; | |
| jsInteropMembers[map.getMember(node)] = name; | |
| }); | |
| return NativeBasicData( | |
| env, | |
| false, | |
| nativeClassTagInfo, | |
| jsInteropLibraries, | |
| jsInteropClasses, | |
| anonymousJsInteropClasses, | |
| staticInteropClasses, | |
| jsInteropMembers, | |
| ); | |
| } | |
| /// Deserializes a [NativeBasicData] object from [source]. | |
| factory NativeBasicData.readFromDataSource( | |
| DataSourceReader source, | |
| ElementEnvironment elementEnvironment, | |
| ) { | |
| source.begin(tag); | |
| bool isAllowInteropUsed = source.readBool(); | |
| Map<ClassEntity, NativeClassTag> nativeClassTagInfo = source.readClassMap( | |
| () { | |
| final names = source.readStrings(); | |
| bool isNonLeaf = source.readBool(); | |
| return NativeClassTag.internal(names, isNonLeaf); | |
| }, | |
| ); | |
| Map<LibraryEntity, String> jsInteropLibraries = source.readLibraryMap( | |
| source.readString, | |
| ); | |
| Map<ClassEntity, String> jsInteropClasses = source.readClassMap( | |
| source.readString, | |
| ); | |
| Set<ClassEntity> anonymousJsInteropClasses = source.readClasses().toSet(); | |
| Set<ClassEntity> staticInteropClasses = source.readClasses().toSet(); | |
| Map<MemberEntity, String?> jsInteropMembers = source.readMemberMap( | |
| (MemberEntity member) => source.readStringOrNull(), | |
| ); | |
| source.end(tag); | |
| return NativeBasicData( | |
| elementEnvironment, | |
| isAllowInteropUsed, | |
| nativeClassTagInfo, | |
| jsInteropLibraries, | |
| jsInteropClasses, | |
| anonymousJsInteropClasses, | |
| staticInteropClasses, | |
| jsInteropMembers, | |
| ); | |
| } | |
| /// Serializes this [NativeBasicData] to [sink]. | |
| void writeToDataSink(DataSinkWriter sink) { | |
| sink.begin(tag); | |
| sink.writeBool(isAllowInteropUsed); | |
| sink.writeClassMap(_nativeClassTagInfo, (NativeClassTag tag) { | |
| sink.writeStrings(tag.names); | |
| sink.writeBool(tag.isNonLeaf); | |
| }); | |
| sink.writeLibraryMap(_jsInteropLibraries, sink.writeString); | |
| sink.writeClassMap(_jsInteropClasses, sink.writeString); | |
| sink.writeClasses(_anonymousJsInteropClasses); | |
| sink.writeClasses(_staticInteropClasses); | |
| sink.writeMemberMap( | |
| _jsInteropMembers, | |
| (MemberEntity member, String? name) => sink.writeStringOrNull(name), | |
| ); | |
| sink.end(tag); | |
| } | |
| /// Returns `true` if `allowInterop()` is invoked. | |
| bool get isAllowInteropUsed => _isAllowInteropUsed; | |
| /// Marks `allowInterop()` as used. | |
| /// | |
| /// [isAllowInteropUsed] is initially false on the closed world, and is only | |
| /// set during codegen enqueuing. | |
| void registerAllowInterop() { | |
| _isAllowInteropUsed = true; | |
| } | |
| /// Returns `true` if [cls] corresponds to a native JavaScript class. | |
| /// | |
| /// A class is marked as native either through the `@Native(...)` annotation | |
| /// allowed for internal libraries or via the typed JavaScriptInterop | |
| /// mechanism allowed for user libraries. | |
| bool isNativeClass(ClassEntity element) { | |
| if (isJsInteropClass(element)) return true; | |
| return _nativeClassTagInfo.containsKey(element); | |
| } | |
| /// Returns the list of non-directive native tag words for [cls]. | |
| List<String> getNativeTagsOfClass(ClassEntity cls) { | |
| return _nativeClassTagInfo[cls]!.names; | |
| } | |
| /// Returns `true` if [cls] has a `!nonleaf` tag word. | |
| bool hasNativeTagsForcedNonLeaf(ClassEntity cls) { | |
| return _nativeClassTagInfo[cls]!.isNonLeaf; | |
| } | |
| /// Returns `true` if js interop features are used. | |
| bool get isJsInteropUsed => | |
| _jsInteropLibraries.isNotEmpty || _jsInteropClasses.isNotEmpty; | |
| /// Returns `true` if [element] is a JsInterop library. | |
| bool isJsInteropLibrary(LibraryEntity element) { | |
| return _jsInteropLibraries.containsKey(element); | |
| } | |
| /// Returns `true` if [element] is a JsInterop class. | |
| bool isJsInteropClass(ClassEntity element) { | |
| return _jsInteropClasses.containsKey(element); | |
| } | |
| /// Returns `true` if [element] is explicitly marked as part of JsInterop. | |
| bool _isJsInteropMember(MemberEntity element) { | |
| return _jsInteropMembers.containsKey(element); | |
| } | |
| /// Returns `true` if [element] is a JsInterop member. | |
| bool isJsInteropMember(MemberEntity element) { | |
| // TODO(johnniwinther): Share this with [NativeData.isJsInteropMember]. | |
| if (element.isFunction || | |
| element is ConstructorEntity || | |
| element.isGetter || | |
| element.isSetter) { | |
| final function = element as FunctionEntity; | |
| if (!function.isExternal) return false; | |
| if (_isJsInteropMember(function)) return true; | |
| if (function.enclosingClass != null) { | |
| return isJsInteropClass(function.enclosingClass!); | |
| } | |
| if (function.isTopLevel) { | |
| return isJsInteropLibrary(function.library); | |
| } | |
| return false; | |
| } else { | |
| return _isJsInteropMember(element); | |
| } | |
| } | |
| /// Returns `true` if [element] or any of its superclasses is native. Supports | |
| /// nullable element for checks on nonexistent enclosing class of top-level | |
| /// functions. | |
| bool isNativeOrExtendsNative(ClassEntity? element) { | |
| if (element == null) return false; | |
| if (isNativeClass(element) || isJsInteropClass(element)) { | |
| return true; | |
| } | |
| return isNativeOrExtendsNative(_env.getSuperClass(element)); | |
| } | |
| NativeBasicData convert(JsToFrontendMap map, ElementEnvironment environment) { | |
| Map<ClassEntity, NativeClassTag> nativeClassTagInfo = | |
| <ClassEntity, NativeClassTag>{}; | |
| _nativeClassTagInfo.forEach((ClassEntity cls, NativeClassTag tag) { | |
| final backendClass = map.toBackendClass(cls); | |
| nativeClassTagInfo[backendClass] = tag; | |
| }); | |
| Map<LibraryEntity, String> jsInteropLibraries = map.toBackendLibraryMap( | |
| _jsInteropLibraries, | |
| identity, | |
| ); | |
| Map<ClassEntity, String> jsInteropClasses = map.toBackendClassMap( | |
| _jsInteropClasses, | |
| identity, | |
| ); | |
| Set<ClassEntity> anonymousJsInteropClasses = map.toBackendClassSet( | |
| _anonymousJsInteropClasses, | |
| ); | |
| Set<ClassEntity> staticInteropClasses = map.toBackendClassSet( | |
| _staticInteropClasses, | |
| ); | |
| Map<MemberEntity, String?> jsInteropMembers = map.toBackendMemberMap( | |
| _jsInteropMembers, | |
| identity, | |
| ); | |
| return NativeBasicData( | |
| environment, | |
| isAllowInteropUsed, | |
| nativeClassTagInfo, | |
| jsInteropLibraries, | |
| jsInteropClasses, | |
| anonymousJsInteropClasses, | |
| staticInteropClasses, | |
| jsInteropMembers, | |
| ); | |
| } | |
| } | |
| class NativeDataBuilder { | |
| final NativeBasicData _nativeBasicData; | |
| /// The JavaScript names for native JavaScript elements implemented. | |
| final Map<MemberEntity, String> _nativeMemberName = {}; | |
| /// Cache for [NativeBehavior]s for calling native methods. | |
| final Map<FunctionEntity, NativeBehavior> _nativeMethodBehavior = {}; | |
| /// Cache for [NativeBehavior]s for reading from native fields. | |
| final Map<MemberEntity, NativeBehavior> _nativeFieldLoadBehavior = {}; | |
| /// Cache for [NativeBehavior]s for writing to native fields. | |
| final Map<MemberEntity, NativeBehavior> _nativeFieldStoreBehavior = {}; | |
| NativeDataBuilder(this._nativeBasicData); | |
| /// Sets the native [name] for the member [element]. | |
| /// | |
| /// This name is used for [element] in the generated JavaScript. | |
| void setNativeMemberName(MemberEntity element, String name) { | |
| // TODO(johnniwinther): Avoid setting this more than once. The enqueuer | |
| // might enqueue [element] several times (before processing it) and computes | |
| // name on each call to `internalAddToWorkList`. | |
| assert( | |
| !_nativeMemberName.containsKey(element) || | |
| _nativeMemberName[element] == name, | |
| failedAt( | |
| element, | |
| "Native member name set inconsistently on $element: " | |
| "Existing name '${_nativeMemberName[element]}', " | |
| "new name '$name'.", | |
| ), | |
| ); | |
| _nativeMemberName[element] = name; | |
| } | |
| /// Registers the [behavior] for calling the native [method]. | |
| void setNativeMethodBehavior(FunctionEntity method, NativeBehavior behavior) { | |
| _nativeMethodBehavior[method] = behavior; | |
| } | |
| /// Registers the [behavior] for reading from the native [field]. | |
| void setNativeFieldLoadBehavior(FieldEntity field, NativeBehavior behavior) { | |
| _nativeFieldLoadBehavior[field] = behavior; | |
| } | |
| /// Registers the [behavior] for writing to the native [field]. | |
| void setNativeFieldStoreBehavior(FieldEntity field, NativeBehavior behavior) { | |
| _nativeFieldStoreBehavior[field] = behavior; | |
| } | |
| /// Closes this builder and creates the resulting [NativeData] object. | |
| NativeData close(DiagnosticReporter reporter) { | |
| final data = NativeData( | |
| _nativeBasicData, | |
| _nativeMemberName, | |
| _nativeMethodBehavior, | |
| _nativeFieldLoadBehavior, | |
| _nativeFieldStoreBehavior, | |
| {}, | |
| ); | |
| if (reporter.options.interopNullAssertions) { | |
| // We can enforce the return type nullability of an interop API in two | |
| // ways: by putting the null check on the invocation in the caller, or by | |
| // putting the null check on the return value in the callee body | |
| // (generally an interceptor method). It is only safe to do the latter if | |
| // all interop bindings that share the interceptor method have consistent | |
| // nullabilities. | |
| final environment = _nativeBasicData._env; | |
| final dartTypes = environment.elementMap.commonElements.dartTypes; | |
| bool returnTypeIsNonNullable( | |
| FunctionEntity member, { | |
| required bool callthrough, | |
| }) { | |
| final memberType = environment.getFunctionType(member); | |
| final functionType = callthrough | |
| ? memberType.returnType as FunctionType | |
| : memberType; | |
| return dartTypes.isNonNullable(functionType.returnType); | |
| } | |
| // Intercepted methods keyed by selector. | |
| final jsNameMap = <Selector, List<FunctionEntity>>{}; | |
| for (final (member as FunctionEntity) | |
| in _nativeBasicData._jsInteropMembers.keys) { | |
| if (!member.isInstanceMember) continue; | |
| if (!member.isFunction && !member.isGetter) continue; | |
| // The program builder uses the unescaped name for interceptor methods. | |
| // We can only perform null checks in the interceptor method body if all | |
| // the intercepted methods with the same name have consistent return | |
| // type nullabilities. | |
| // We use a public name because the interceptor will not distinguish | |
| // methods from different libraries, even if they have a leading | |
| // underscore. | |
| final name = PublicName( | |
| data.computeUnescapedJSInteropName(member.name!), | |
| ); | |
| void addAllPossibleInvocations(FunctionType type) { | |
| final requiredPositionalCount = type.parameterTypes.length; | |
| final optionalPositionalCount = type.optionalParameterTypes.length; | |
| // We do not yet know which invocations are actually live in the | |
| // program, so we conservatively allow for any number of optional | |
| // arguments to be passed. Named parameters are not supported. | |
| for (var i = 0; i <= optionalPositionalCount; i++) { | |
| (jsNameMap[Selector.call( | |
| name, | |
| CallStructure.unnamed(requiredPositionalCount + i), | |
| )] ??= | |
| []) | |
| .add(member); | |
| } | |
| } | |
| if (member.isGetter) { | |
| (jsNameMap[Selector.getter(name)] ??= []).add(member); | |
| final returnType = environment.getFunctionType(member).returnType; | |
| if (returnType is FunctionType) { | |
| addAllPossibleInvocations(returnType); | |
| } | |
| } else if (member.isFunction) { | |
| final functionType = environment.getFunctionType(member); | |
| addAllPossibleInvocations(functionType); | |
| } | |
| } | |
| jsNameMap.forEach((selector, members) { | |
| final canCheckInCallee = members.every( | |
| (FunctionEntity member) => returnTypeIsNonNullable( | |
| member, | |
| callthrough: member.isGetter && selector.kind == SelectorKind.call, | |
| ), | |
| ); | |
| data.interopNullChecks[selector] = canCheckInCallee | |
| ? InteropNullCheckKind.calleeCheck | |
| : InteropNullCheckKind.callerCheck; | |
| }); | |
| } | |
| return data; | |
| } | |
| } | |
| enum InteropNullCheckKind { calleeCheck, callerCheck } | |
| /// Additional element information for native classes and methods and js-interop | |
| /// methods. | |
| /// | |
| /// This information is computed during resolution using [NativeDataBuilder]. | |
| // TODO(johnniwinther): Remove fields that overlap with [NativeBasicData], like | |
| // [anonymousJsInteropClasses]. | |
| class NativeData implements NativeBasicData { | |
| /// Tag used for identifying serialized [NativeData] objects in a | |
| /// debugging data stream. | |
| static const String tag = 'native-data'; | |
| /// Prefix used to escape JS names that are not valid Dart names | |
| /// when using JSInterop. | |
| static const String _jsInteropEscapePrefix = r'JS$'; | |
| final NativeBasicData _nativeBasicData; | |
| /// The JavaScript names for native JavaScript elements implemented. | |
| final Map<MemberEntity, String> _nativeMemberName; | |
| /// Cache for [NativeBehavior]s for calling native methods. | |
| final Map<FunctionEntity, NativeBehavior> _nativeMethodBehavior; | |
| /// Cache for [NativeBehavior]s for reading from native fields. | |
| final Map<MemberEntity, NativeBehavior> _nativeFieldLoadBehavior; | |
| /// Cache for [NativeBehavior]s for writing to native fields. | |
| final Map<MemberEntity, NativeBehavior> _nativeFieldStoreBehavior; | |
| /// A map from selectors for interop members to the type of null check | |
| /// required when `--interop-null-assertions` is passed. | |
| final Map<Selector, InteropNullCheckKind> interopNullChecks; | |
| NativeData( | |
| this._nativeBasicData, | |
| this._nativeMemberName, | |
| this._nativeMethodBehavior, | |
| this._nativeFieldLoadBehavior, | |
| this._nativeFieldStoreBehavior, | |
| this.interopNullChecks, | |
| ); | |
| factory NativeData.fromIr(KernelToElementMap map, IrAnnotationData data) { | |
| NativeBasicData nativeBasicData = NativeBasicData.fromIr(map, data); | |
| Map<MemberEntity, String> nativeMemberName = {}; | |
| Map<FunctionEntity, NativeBehavior> nativeMethodBehavior = {}; | |
| Map<MemberEntity, NativeBehavior> nativeFieldLoadBehavior = {}; | |
| Map<MemberEntity, NativeBehavior> nativeFieldStoreBehavior = {}; | |
| data.forEachNativeMethodData(( | |
| ir.Member node, | |
| String name, | |
| Iterable<String> createsAnnotations, | |
| Iterable<String> returnsAnnotations, | |
| ) { | |
| final member = map.getMember(node) as FunctionEntity; | |
| nativeMemberName[member] = name; | |
| bool isJsInterop = nativeBasicData.isJsInteropMember(member); | |
| nativeMethodBehavior[member] = map.getNativeBehaviorForMethod( | |
| node, | |
| createsAnnotations, | |
| returnsAnnotations, | |
| isJsInterop: isJsInterop, | |
| ); | |
| }); | |
| data.forEachNativeFieldData(( | |
| ir.Member node, | |
| String name, | |
| Iterable<String> createsAnnotations, | |
| Iterable<String> returnsAnnotations, | |
| ) { | |
| final field = map.getMember(node) as FieldEntity; | |
| nativeMemberName[field] = name; | |
| bool isJsInterop = nativeBasicData.isJsInteropMember(field); | |
| node as ir.Field; | |
| nativeFieldLoadBehavior[field] = map.getNativeBehaviorForFieldLoad( | |
| node, | |
| createsAnnotations, | |
| returnsAnnotations, | |
| isJsInterop: isJsInterop, | |
| ); | |
| nativeFieldStoreBehavior[field] = map.getNativeBehaviorForFieldStore( | |
| node, | |
| ); | |
| }); | |
| return NativeData( | |
| nativeBasicData, | |
| nativeMemberName, | |
| nativeMethodBehavior, | |
| nativeFieldLoadBehavior, | |
| nativeFieldStoreBehavior, | |
| const {}, | |
| ); | |
| } | |
| /// Deserializes a [NativeData] object from [source]. | |
| factory NativeData.readFromDataSource( | |
| DataSourceReader source, | |
| ElementEnvironment elementEnvironment, | |
| ) { | |
| source.begin(tag); | |
| NativeBasicData nativeBasicData = NativeBasicData.readFromDataSource( | |
| source, | |
| elementEnvironment, | |
| ); | |
| Map<MemberEntity, String> nativeMemberName = source.readMemberMap( | |
| (MemberEntity member) => source.readString(), | |
| ); | |
| Map<FunctionEntity, NativeBehavior> nativeMethodBehavior = source | |
| .readMemberMap( | |
| (MemberEntity member) => NativeBehavior.readFromDataSource(source), | |
| ); | |
| Map<MemberEntity, NativeBehavior> nativeFieldLoadBehavior = source | |
| .readMemberMap( | |
| (MemberEntity member) => NativeBehavior.readFromDataSource(source), | |
| ); | |
| Map<MemberEntity, NativeBehavior> nativeFieldStoreBehavior = source | |
| .readMemberMap( | |
| (MemberEntity member) => NativeBehavior.readFromDataSource(source), | |
| ); | |
| Map<Selector, InteropNullCheckKind> interopNullChecks = source | |
| .readSelectorMap((_) => source.readEnum(InteropNullCheckKind.values)); | |
| source.end(tag); | |
| return NativeData( | |
| nativeBasicData, | |
| nativeMemberName, | |
| nativeMethodBehavior, | |
| nativeFieldLoadBehavior, | |
| nativeFieldStoreBehavior, | |
| interopNullChecks, | |
| ); | |
| } | |
| /// Serializes this [NativeData] to [sink]. | |
| @override | |
| void writeToDataSink(DataSinkWriter sink) { | |
| sink.begin(tag); | |
| _nativeBasicData.writeToDataSink(sink); | |
| sink.writeMemberMap( | |
| _nativeMemberName, | |
| (MemberEntity member, String name) => sink.writeString(name), | |
| ); | |
| sink.writeMemberMap(_nativeMethodBehavior, ( | |
| MemberEntity member, | |
| NativeBehavior behavior, | |
| ) { | |
| behavior.writeToDataSink(sink); | |
| }); | |
| sink.writeMemberMap(_nativeFieldLoadBehavior, ( | |
| MemberEntity member, | |
| NativeBehavior behavior, | |
| ) { | |
| behavior.writeToDataSink(sink); | |
| }); | |
| sink.writeMemberMap(_nativeFieldStoreBehavior, ( | |
| MemberEntity member, | |
| NativeBehavior behavior, | |
| ) { | |
| behavior.writeToDataSink(sink); | |
| }); | |
| sink.writeSelectorMap( | |
| interopNullChecks, | |
| sink.writeEnum<InteropNullCheckKind>, | |
| ); | |
| sink.end(tag); | |
| } | |
| @override | |
| bool get _isAllowInteropUsed => _nativeBasicData._isAllowInteropUsed; | |
| @override | |
| set _isAllowInteropUsed(bool value) => | |
| _nativeBasicData._isAllowInteropUsed = value; | |
| @override | |
| bool get isAllowInteropUsed => _nativeBasicData.isAllowInteropUsed; | |
| @override | |
| void registerAllowInterop() => _nativeBasicData.registerAllowInterop(); | |
| @override | |
| Map<LibraryEntity, String> get _jsInteropLibraries => | |
| _nativeBasicData._jsInteropLibraries; | |
| @override | |
| Set<ClassEntity> get _anonymousJsInteropClasses => | |
| _nativeBasicData._anonymousJsInteropClasses; | |
| @override | |
| Set<ClassEntity> get _staticInteropClasses => | |
| _nativeBasicData._staticInteropClasses; | |
| @override | |
| Map<ClassEntity, String> get _jsInteropClasses => | |
| _nativeBasicData._jsInteropClasses; | |
| @override | |
| Map<MemberEntity, String?> get _jsInteropMembers => | |
| _nativeBasicData._jsInteropMembers; | |
| /// Returns `true` if [element] has an `@Anonymous` annotation. | |
| bool isAnonymousJsInteropClass(ClassEntity element) { | |
| return _anonymousJsInteropClasses.contains(element); | |
| } | |
| /// Returns `true` if [element] has an `@staticInterop` annotation. | |
| bool isStaticInteropClass(ClassEntity element) { | |
| return _staticInteropClasses.contains(element); | |
| } | |
| @override | |
| bool isNativeClass(ClassEntity element) => | |
| _nativeBasicData.isNativeClass(element); | |
| @override | |
| List<String> getNativeTagsOfClass(ClassEntity cls) => | |
| _nativeBasicData.getNativeTagsOfClass(cls); | |
| @override | |
| bool hasNativeTagsForcedNonLeaf(ClassEntity cls) => | |
| _nativeBasicData.hasNativeTagsForcedNonLeaf(cls); | |
| @override | |
| bool get isJsInteropUsed => _nativeBasicData.isJsInteropUsed; | |
| @override | |
| bool isJsInteropLibrary(LibraryEntity element) => | |
| _nativeBasicData.isJsInteropLibrary(element); | |
| @override | |
| bool isJsInteropClass(ClassEntity element) => | |
| _nativeBasicData.isJsInteropClass(element); | |
| @override | |
| bool isNativeOrExtendsNative(ClassEntity? element) => | |
| _nativeBasicData.isNativeOrExtendsNative(element); | |
| /// Returns the explicit js interop name for library [element]. | |
| String? getJsInteropLibraryName(LibraryEntity element) { | |
| return _jsInteropLibraries[element]; | |
| } | |
| /// Returns the explicit js interop name for class [element]. | |
| String? getJsInteropClassName(ClassEntity element) { | |
| return _jsInteropClasses[element]; | |
| } | |
| /// Returns the explicit js interop name for member [element]. | |
| String? getJsInteropMemberName(MemberEntity element) { | |
| return _jsInteropMembers[element]; | |
| } | |
| @override | |
| bool _isJsInteropMember(MemberEntity element) { | |
| return _jsInteropMembers.containsKey(element); | |
| } | |
| @override | |
| bool isJsInteropMember(MemberEntity element) { | |
| if (element.isFunction || | |
| element is ConstructorEntity || | |
| element.isGetter || | |
| element.isSetter) { | |
| final function = element as FunctionEntity; | |
| if (!function.isExternal) return false; | |
| if (_isJsInteropMember(function)) return true; | |
| if (function.enclosingClass != null) { | |
| return isJsInteropClass(function.enclosingClass!); | |
| } | |
| if (function.isTopLevel) { | |
| return isJsInteropLibrary(function.library); | |
| } | |
| return false; | |
| } else { | |
| return _isJsInteropMember(element); | |
| } | |
| } | |
| /// Returns `true` if the name of [element] is fixed for the generated | |
| /// JavaScript. | |
| bool hasFixedBackendName(MemberEntity element) { | |
| return isJsInteropMember(element) || _nativeMemberName.containsKey(element); | |
| } | |
| /// Computes the name for [element] to use in the generated JavaScript. This | |
| /// is either given through a native annotation or a js interop annotation. | |
| String? getFixedBackendName(MemberEntity element) { | |
| String? name = _nativeMemberName[element]; | |
| if (name == null && isJsInteropMember(element)) { | |
| if (element is ConstructorEntity) { | |
| name = _jsClassNameHelper(element.enclosingClass); | |
| } else { | |
| name = _jsMemberNameHelper(element); | |
| // Top-level static JS interop members can be associated with a dotted | |
| // name, if so, fixedBackendName is the last segment. | |
| if (element.isTopLevel && name.contains('.')) { | |
| name = name.substring(name.lastIndexOf('.') + 1); | |
| } | |
| } | |
| _nativeMemberName[element] = name; | |
| } | |
| return name; | |
| } | |
| String _jsLibraryNameHelper(LibraryEntity element) { | |
| String? jsInteropName = getJsInteropLibraryName(element); | |
| if (jsInteropName != null && jsInteropName.isNotEmpty) return jsInteropName; | |
| return 'self'; | |
| } | |
| String _jsClassNameHelper(ClassEntity element) { | |
| String? jsInteropName = getJsInteropClassName(element); | |
| if (jsInteropName != null && jsInteropName.isNotEmpty) return jsInteropName; | |
| return computeUnescapedJSInteropName(element.name); | |
| } | |
| String _jsMemberNameHelper(MemberEntity element) { | |
| String? jsInteropName = _jsInteropMembers[element]; | |
| assert( | |
| !(_jsInteropMembers.containsKey(element) && jsInteropName == null), | |
| failedAt( | |
| element, | |
| 'Member $element is js interop but js interop name has not yet ' | |
| 'been computed.', | |
| ), | |
| ); | |
| if (jsInteropName != null && jsInteropName.isNotEmpty) { | |
| return jsInteropName; | |
| } | |
| return computeUnescapedJSInteropName(element.name!); | |
| } | |
| /// Returns a JavaScript path specifying the context in which | |
| /// [element.fixedBackendName] should be evaluated. Only applicable for | |
| /// elements using typed JavaScript interop. | |
| /// For example: fixedBackendPath for the static method createMap in the | |
| /// Map class of the goog.map JavaScript library would have path | |
| /// "goog.maps.Map". | |
| String? getFixedBackendMethodPath(FunctionEntity element) { | |
| if (!isJsInteropMember(element)) return null; | |
| if (element.isInstanceMember) return 'this'; | |
| if (element is ConstructorEntity) { | |
| return _fixedBackendClassPath(element.enclosingClass); | |
| } | |
| StringBuffer sb = StringBuffer(); | |
| sb.write(_jsLibraryNameHelper(element.library)); | |
| if (element.enclosingClass != null) { | |
| sb | |
| ..write('.') | |
| ..write(_jsClassNameHelper(element.enclosingClass!)); | |
| } | |
| // Top-level static JS interop members can be associated with a dotted | |
| // name, if so, fixedBackendPath includes all but the last segment. | |
| final name = _jsMemberNameHelper(element); | |
| if (element.isTopLevel && name.contains('.')) { | |
| sb | |
| ..write('.') | |
| ..write(name.substring(0, name.lastIndexOf('.'))); | |
| } | |
| return sb.toString(); | |
| } | |
| String? _fixedBackendClassPath(ClassEntity element) { | |
| if (!isJsInteropClass(element)) return null; | |
| return _jsLibraryNameHelper(element.library); | |
| } | |
| /// Returns `true` if [element] corresponds to a native JavaScript member. | |
| /// | |
| /// A member is marked as native either through the native mechanism | |
| /// (`@Native(...)` or the `native` pseudo keyword) allowed for internal | |
| /// libraries or via the typed JavaScriptInterop mechanism allowed for user | |
| /// libraries. | |
| bool isNativeMember(MemberEntity element) { | |
| if (isJsInteropMember(element)) return true; | |
| return _nativeMemberName.containsKey(element); | |
| } | |
| /// Returns the [NativeBehavior] for calling the native [method]. | |
| NativeBehavior getNativeMethodBehavior(FunctionEntity method) { | |
| assert( | |
| _nativeMethodBehavior.containsKey(method), | |
| failedAt( | |
| method, | |
| "No native method behavior has been computed for $method.", | |
| ), | |
| ); | |
| return _nativeMethodBehavior[method]!; | |
| } | |
| /// Returns the [NativeBehavior] for reading from the native [field]. | |
| NativeBehavior getNativeFieldLoadBehavior(FieldEntity field) { | |
| assert( | |
| _nativeFieldLoadBehavior.containsKey(field), | |
| failedAt( | |
| field, | |
| "No native field load behavior has been " | |
| "computed for $field.", | |
| ), | |
| ); | |
| return _nativeFieldLoadBehavior[field]!; | |
| } | |
| /// Returns the [NativeBehavior] for writing to the native [field]. | |
| NativeBehavior getNativeFieldStoreBehavior(FieldEntity field) { | |
| assert( | |
| _nativeFieldStoreBehavior.containsKey(field), | |
| failedAt( | |
| field, | |
| "No native field store behavior has been computed for $field.", | |
| ), | |
| ); | |
| return _nativeFieldStoreBehavior[field]!; | |
| } | |
| /// Apply JS$ escaping scheme to convert possible escaped Dart names into | |
| /// JS names. | |
| String computeUnescapedJSInteropName(String name) { | |
| return name.startsWith(_jsInteropEscapePrefix) | |
| ? name.substring(_jsInteropEscapePrefix.length) | |
| : name; | |
| } | |
| @override | |
| Map<ClassEntity, NativeClassTag> get _nativeClassTagInfo => | |
| _nativeBasicData._nativeClassTagInfo; | |
| @override | |
| ElementEnvironment get _env => _nativeBasicData._env; | |
| @override | |
| NativeData convert(JsToFrontendMap map, ElementEnvironment environment) { | |
| NativeBasicData nativeBasicData = _nativeBasicData.convert( | |
| map, | |
| environment, | |
| ); | |
| Map<MemberEntity, String> nativeMemberName = map.toBackendMemberMap( | |
| _nativeMemberName, | |
| identity, | |
| ); | |
| final nativeMethodBehavior = <FunctionEntity, NativeBehavior>{}; | |
| _nativeMethodBehavior.forEach(( | |
| FunctionEntity method, | |
| NativeBehavior behavior, | |
| ) { | |
| final backendMethod = map.toBackendMember(method) as FunctionEntity?; | |
| if (backendMethod != null) { | |
| // If [method] isn't used it doesn't have a corresponding backend | |
| // method. | |
| nativeMethodBehavior[backendMethod] = behavior.convert(map); | |
| } | |
| }); | |
| NativeBehavior convertNativeBehavior(NativeBehavior behavior) => | |
| behavior.convert(map); | |
| Map<MemberEntity, NativeBehavior> nativeFieldLoadBehavior = map | |
| .toBackendMemberMap(_nativeFieldLoadBehavior, convertNativeBehavior); | |
| Map<MemberEntity, NativeBehavior> nativeFieldStoreBehavior = map | |
| .toBackendMemberMap(_nativeFieldStoreBehavior, convertNativeBehavior); | |
| return NativeData( | |
| nativeBasicData, | |
| nativeMemberName, | |
| nativeMethodBehavior, | |
| nativeFieldLoadBehavior, | |
| nativeFieldStoreBehavior, | |
| interopNullChecks, | |
| ); | |
| } | |
| } | |
| class NativeClassTag { | |
| final List<String> names; | |
| final bool isNonLeaf; | |
| factory NativeClassTag(String tagText) { | |
| List<String> tags = tagText.split(','); | |
| List<String> names = tags | |
| .where((s) => s.isNotEmpty && !s.startsWith('!')) | |
| .toList(); | |
| bool isNonLeaf = tags.contains('!nonleaf'); | |
| return NativeClassTag.internal(names, isNonLeaf); | |
| } | |
| NativeClassTag.internal(this.names, this.isNonLeaf); | |
| String get text { | |
| StringBuffer sb = StringBuffer(); | |
| sb.write(names.join(',')); | |
| if (isNonLeaf) { | |
| if (names.isNotEmpty) { | |
| sb.write(','); | |
| } | |
| sb.write('!nonleaf'); | |
| } | |
| return sb.toString(); | |
| } | |
| @override | |
| int get hashCode => Hashing.listHash(names, isNonLeaf.hashCode); | |
| @override | |
| bool operator ==(other) { | |
| if (identical(this, other)) return true; | |
| if (other is! NativeClassTag) return false; | |
| return equalElements(names, other.names) && isNonLeaf == other.isNonLeaf; | |
| } | |
| @override | |
| String toString() => text; | |
| } |