// Copyright (c) 2012, 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 dart._interceptors; import 'dart:collection'; import 'dart:_internal' hide Symbol; import 'dart:_js_helper'; import 'dart:_foreign_helper' show JS, JS_GET_FLAG, TYPE_REF, JS_RTI_PARAMETER; import 'dart:math' show Random, ln2; import 'dart:_rti' as rti show createRuntimeType, Rti; import 'dart:_runtime' as dart; import 'dart:_js_shared_embedded_names' show ARRAY_RTI_PROPERTY; part 'js_array.dart'; part 'js_number.dart'; part 'js_string.dart'; // TODO(jmesserly): remove, this doesn't do anything for us. abstract class Interceptor { const Interceptor(); // Use native JS toString method instead of standard Dart Object.toString. String toString() => JS('!', '#.toString()', this); } /// The interceptor class for [bool]. @JsPeerInterface(name: 'Boolean') final class JSBool extends Interceptor implements bool, TrustedGetRuntimeType { const JSBool(); // Note: if you change this, also change the function [S]. @notNull String toString() => JS('!', r'String(#)', this); // The values here are SMIs, co-prime and differ about half of the bit // positions, including the low bit, so they are different mod 2^k. @notNull int get hashCode => this ? (2 * 3 * 23 * 3761) : (269 * 811); @notNull bool operator &(@nullCheck bool other) => JS('!', "# && #", other, this); @notNull bool operator |(@nullCheck bool other) => JS('!', "# || #", other, this); @notNull bool operator ^(@nullCheck bool other) => !identical(this, other); Type get runtimeType => bool; } /// The supertype for JSString and JSArray. Used by the backend as to /// have a type mask that contains the objects that we can use the /// native JS [] operator and length on. abstract class JSIndexable { int get length; E operator [](int index); } /// The supertype for JSMutableArray and JavaScriptIndexingBehavior. /// // TODO(nshahan) Use as a type mask that contains the objects we can use the JS // []= operator on. abstract class JSMutableIndexable extends JSIndexable { operator []=(int index, E value); } /// The interface implemented by JavaScript objects. /// /// These are methods in addition to the regular Dart Object methods like /// [Object.hashCode]. This is the type that should be exported by a JavaScript /// interop library. abstract class JSObject {} /// Superclass of all interop objects and native types defined in the web /// libraries. /// /// This is the class @staticInterop classes erase to. class JavaScriptObject extends Interceptor implements JSObject { const JavaScriptObject(); } /// Interceptor base class for JavaScript objects not recognized as some more /// specific native type. /// /// Note that this used to be `JavaScriptObject`. class LegacyJavaScriptObject extends JavaScriptObject { const LegacyJavaScriptObject(); } /// Interceptor for plain JavaScript objects created as JavaScript object /// literals or `new Object()`. /// /// Note that this isn't being used today in ddc. Instead of using interceptors, /// we have other type logic to distinguish JS types. class PlainJavaScriptObject extends LegacyJavaScriptObject { const PlainJavaScriptObject(); } /// Interceptor for unclassified JavaScript objects, typically objects with a /// non-trivial prototype chain. /// /// This class also serves as a fallback for unknown JavaScript exceptions. /// Note that this isn't being used today in ddc. Instead of using interceptors, /// we have other type logic to distinguish JS types. class UnknownJavaScriptObject extends LegacyJavaScriptObject { const UnknownJavaScriptObject(); } @JsPeerInterface(name: 'Error') class NativeError extends JavaScriptObject { String dartStack() => JS('!', '#.stack', this); } // Note that this needs to be in interceptors.dart in order for // it to be picked up as an extension type. @JsPeerInterface(name: 'TypeError') class JSNoSuchMethodError extends NativeError implements NoSuchMethodError { static final _nullError = RegExp(r"^Cannot read property '(.+)' of null$"); static final _notAFunction = RegExp(r"^(.+) is not a function$"); static final _extensionName = RegExp(r"^Symbol\(dartx\.(.+)\)$"); static final _privateName = RegExp(r"^Symbol\((_.+)\)$"); String? _fieldName(String message) { RegExpMatch? match = _nullError.firstMatch(message); if (match == null) return null; String name = match[1]!; match = _extensionName.firstMatch(name) ?? _privateName.firstMatch(name); return match != null ? match[1] : name; } String? _functionCallTarget(String message) { var match = _notAFunction.firstMatch(message); return match != null ? match[1] : null; } String dartStack() { var stack = super.dartStack(); // Strip TypeError from first line. stack = toString() + '\n' + stack.split('\n').sublist(1).join('\n'); return stack; } StackTrace get stackTrace => dart.stackTrace(this); String toString() { String message = JS('!', '#.message', this); var callTarget = _functionCallTarget(message); if (callTarget != null) { return "NoSuchMethodError: tried to call a non-function, such as null: " "'$callTarget'"; } // TODO(vsm): Distinguish between null reference errors and other // TypeErrors. We should not get non-null TypeErrors from DDC code, // but we may from native JavaScript. var name = _fieldName(message); if (name == null) { // Not a Null NSM error: fallback to JS. return JS('!', '#.toString()', this); } return "NoSuchMethodError: invalid member on null: '$name'"; } } @JsPeerInterface(name: 'Function') class JSFunction extends Interceptor { toString() { if (dart.isDartClass(this)) { // If the function is a Dart class, we should just display the class name. // Regular Dart code shouldn't be able to access these values to call // `.toString()` but they might be accessed through debugging tools. return 'Dart class: ${JS('!', '#.name', this)}'; } return JS( '!', r'"Closure: " + # + " from: " + #', dart.typeName(dart.getReifiedType(this)), this, ); } // TODO(nshahan): We can remove these if we canonicalize all tearoffs and no // longer support weak null safety "same type" equality. operator ==(other) { // Basic function values (no generics, no bound instances) are represented // as the original functions so reference equality is sufficient. if (JS('!', '# === #', this, other)) return true; dynamic boundObj; dynamic otherFn; var originalFn = JS('', '#._originalFn', this); if (originalFn == null) { // No generic instantiation present. boundObj = JS('', '#._boundObject', this); if (boundObj == null) return false; originalFn = this; otherFn = other; } else { // Generic instantiation was present. var typeArgs = JS('!', '#._typeArgs', this); var otherTypeArgs = JS('', '#._typeArgs', other); // Test if all instantiated type arguments are equal. if (JS_GET_FLAG('SOUND_NULL_SAFETY')) { // The list has been canonicalized on creation so reference equality // is sufficient. if (JS('!', '# !== #', typeArgs, otherTypeArgs)) return false; } else { // In weak null safety all types arguments must be compared in a way // that is agnostic to legacy. var typeArgCount = JS('!', '#.length', typeArgs); if (JS('!', '!#', otherTypeArgs) || typeArgCount != JS('', '#.length', otherTypeArgs)) { return false; } for (var i = 0; i < typeArgCount; i++) { var typeArg = JS('!', '#[#]', typeArgs, i); var otherTypeArg = JS('!', '#[#]', otherTypeArgs, i); if (JS_GET_FLAG('SOUND_NULL_SAFETY')) { if (typeArg != otherTypeArg) return false; } else { if (rti.Rti.getLegacyErasedRecipe(typeArg) != rti.Rti.getLegacyErasedRecipe(otherTypeArg)) { return false; } } } } boundObj = JS('', '#._boundObject', originalFn); otherFn = JS('', '#._originalFn', other); if (boundObj == null) { // This isn't an instance tearoff, test if the original uninstantiated // methods are equal. return JS('!', '# === #', originalFn, otherFn); } } // This is an instance tearoff, test if the bound instances and methods // are equal. return JS( '!', '# === #._boundObject && #._boundMethod === #._boundMethod', boundObj, otherFn, originalFn, otherFn, ); } get hashCode { var boundObj = JS('', '#._boundObject', this); if (boundObj == null) return identityHashCode(this); var boundMethod = JS('!', '#._boundMethod', this); int hash = (17 * 31 + boundObj.hashCode) & 0x1fffffff; return (hash * 31 + identityHashCode(boundMethod)) & 0x1fffffff; } Type get runtimeType => rti.createRuntimeType(JS('!', '#', dart.getReifiedType(this))); } /// A class used for implementing `null` tear-offs. class JSNull { toString() => 'null'; noSuchMethod(Invocation i) => dart.defaultNoSuchMethod(null, i); } final Object jsNull = JSNull(); // Note that this needs to be in interceptors.dart in order for // it to be picked up as an extension type. @JsPeerInterface(name: 'RangeError') class JSRangeError extends JavaScriptObject implements ArgumentError { StackTrace get stackTrace => dart.stackTrace(this); get invalidValue => null; get name => null; get message => JS('!', '#.message', this); String toString() => "Invalid argument: $message"; } // Obsolete in dart dev compiler. Added only so that the same version of // dart:html can be used in dart2js an dev compiler. // Warning: calls to these methods need to be removed before custom elements // and cross-frame dom objects behave correctly in ddc. // See https://github.com/dart-lang/sdk/issues/28326 findInterceptorConstructorForType(Type? type) {} findConstructorForNativeSubclassType(Type? type, String name) {} getNativeInterceptor(object) {} setDispatchProperty(object, value) {} // Added to allow dart2js and dartdevc to share tests // TODO(sigmund): revisit whether this method is still needed after reorganizing // all web tests. findInterceptorForType(Type? type) {} /// Interceptor class for JavaScript function objects and Dart functions that /// have been converted to JavaScript functions. /// /// A reference to this class is only used by `getInterceptor()` to return to /// the dart:rti library because stores information used for type checks. class JavaScriptFunction extends LegacyJavaScriptObject implements Function {} /// Interceptor for JavaScript BigInt primitive values, i.e. values `x` for /// which `typeof x == "bigint"`. @JsPeerInterface(name: 'BigInt') final class JavaScriptBigInt extends Interceptor { const JavaScriptBigInt(); // JavaScript BigInt objects don't have any operations that look efficient // enough to generate a good hash code. // // TODO(https://dartbug.com/53162): "bigint" primitive values might be used as // keys without using the `hashCode`, but that will not help for compound hash // values, e.g. produced by `Object.hash`. int get hashCode => 0; /// Returns the result of the JavaScript BigInt's `toString` method. String toString() => JS('String', 'String(#)', this); } /// Interceptor for JavaScript Symbol primitive values, i.e. values `x` for /// which `typeof x == "symbol"`. @JsPeerInterface(name: 'Symbol') final class JavaScriptSymbol extends Interceptor { const JavaScriptSymbol(); // It is not clear what to do for a Symbol's hashCode. Registered Symbols // [can't be keys of WeakMaps][1]. Using a property won't work because the // property will be added to the ephmeral Object wrapper, either being an // incorrect operation or an error in strict mode. // // TODO(https://dartbug.com/53162): "symbol" primitive values might be used as // keys without using the `hashCode`, but that will not help for compound hash // values, e.g. produced by `Object.hash`. // // [1]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol#shared_symbols_in_the_global_symbol_registry int get hashCode => 0; /// Returns the result of the JavaScript Symbol's `toString` method. String toString() => JS('String', 'String(#)', this); }