// Copyright (c) 2013, 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._js_helper; import 'dart:async' show Zone; import 'dart:collection'; import 'dart:_foreign_helper' show DART_RUNTIME_LIBRAY, JS, JS_CLASS_REF, JS_GET_FLAG, JSExportName; import 'dart:_interceptors'; import 'dart:_internal' show EfficientLengthIterable, HideEfficientLengthIterable, MappedIterable, IterableElementError, SubListIterable, patch; import 'dart:_native_typed_data'; import 'dart:_rti' as rti show pairwiseIsTest, evaluateRtiForRecord, Rti; import 'dart:_runtime' as dart; part 'annotations.dart'; part 'linked_hash_map.dart'; part 'linked_hash_set.dart'; part 'identity_hash_map.dart'; part 'identity_hash_set.dart'; part 'custom_hash_map.dart'; part 'custom_hash_set.dart'; part 'native_helper.dart'; part 'regexp_helper.dart'; part 'string_helper.dart'; part 'js_rti.dart'; /// Adapts a JS `[Symbol.iterator]` to a Dart `get iterator`. /// /// This is the inverse of `JsIterator`, for classes where we can more /// efficiently obtain a JS iterator instead of a Dart one. /// // TODO(jmesserly): this adapter is to work around // https://github.com/dart-lang/sdk/issues/28320 class DartIterator implements Iterator { final _jsIterator; E? _current; DartIterator(this._jsIterator); E get current => _current as E; bool moveNext() { final ret = JS('', '#.next()', _jsIterator); _current = JS('', '#.value', ret); return JS('!', '!#.done', ret); } } /// Used to compile `sync*`. class SyncIterable extends Iterable { final Function() _initGenerator; SyncIterable(this._initGenerator); @JSExportName('Symbol.iterator') _jsIterator() => _initGenerator(); get iterator => DartIterator(_initGenerator()); } class Primitives { static int? parseInt(@nullCheck String source, int? _radix) { var re = JS('', r'/^\s*[+-]?((0x[a-f0-9]+)|(\d+)|([a-z0-9]+))\s*$/i'); // This isn't reified List?, but it's safe to use as long as we use // it locally and don't expose it to user code. var match = JS?>('', '#.exec(#)', re, source); int digitsIndex = 1; int hexIndex = 2; int decimalIndex = 3; if (match == null) { // TODO(sra): It might be that the match failed due to unrecognized U+0085 // spaces. We could replace them with U+0020 spaces and try matching // again. return null; } var decimalMatch = match[decimalIndex]; if (_radix == null) { if (decimalMatch != null) { // Cannot fail because we know that the digits are all decimal. return JS('!', r'parseInt(#, 10)', source); } if (match[hexIndex] != null) { // Cannot fail because we know that the digits are all hex. return JS('!', r'parseInt(#, 16)', source); } return null; } @notNull var radix = _radix; if (radix < 2 || radix > 36) { throw RangeError.range(radix, 2, 36, 'radix'); } if (radix == 10 && decimalMatch != null) { // Cannot fail because we know that the digits are all decimal. return JS('!', r'parseInt(#, 10)', source); } // If radix >= 10 and we have only decimal digits the string is safe. // Otherwise we need to check the digits. if (radix < 10 || decimalMatch == null) { // We know that the characters must be ASCII as otherwise the // regexp wouldn't have matched. Lowercasing by doing `| 0x20` is thus // guaranteed to be a safe operation, since it preserves digits // and lower-cases ASCII letters. int maxCharCode; if (radix <= 10) { // Allow all digits less than the radix. For example 0, 1, 2 for // radix 3. // "0".codeUnitAt(0) + radix - 1; maxCharCode = (0x30 - 1) + radix; } else { // Letters are located after the digits in ASCII. Therefore we // only check for the character code. The regexp above made already // sure that the string does not contain anything but digits or // letters. // "a".codeUnitAt(0) + (radix - 10) - 1; maxCharCode = (0x61 - 10 - 1) + radix; } assert(match[digitsIndex] is String); String digitsPart = JS('!', '#[#]', match, digitsIndex); for (int i = 0; i < digitsPart.length; i++) { int characterCode = digitsPart.codeUnitAt(i) | 0x20; if (characterCode > maxCharCode) { return null; } } } // The above matching and checks ensures the source has at least one digits // and all digits are suitable for the radix, so parseInt cannot return NaN. return JS('!', r'parseInt(#, #)', source, radix); } static double? parseDouble(@nullCheck String source) { // Notice that JS parseFloat accepts garbage at the end of the string. // Accept only: // - [+/-]NaN // - [+/-]Infinity // - a Dart double literal // We do allow leading or trailing whitespace. if (!JS( 'bool', r'/^\s*[+-]?(?:Infinity|NaN|' r'(?:\.\d+|\d+(?:\.\d*)?)(?:[eE][+-]?\d+)?)\s*$/.test(#)', source, )) { return null; } var result = JS('!', r'parseFloat(#)', source); if (result.isNaN) { var trimmed = source.trim(); if (trimmed == 'NaN' || trimmed == '+NaN' || trimmed == '-NaN') { return result; } return null; } return result; } static bool? parseBool( @nullCheck String source, @nullCheck bool caseSensitive, ) { if (caseSensitive) { return JS('bool', r'# == "true" || # != "false" && null', source, source); } return _compareIgnoreCase(source, "true") ? true : _compareIgnoreCase(source, "false") ? false : null; } /// Compares a string against an ASCII lower-case letter-only string. /// /// Returns `true` if the [input] has the same length and same letters /// as [lowerCaseTarget], `false` if not. static bool _compareIgnoreCase(String input, String lowerCaseTarget) { if (input.length != lowerCaseTarget.length) return false; var delta = 0x20; for (var i = 0; i < input.length; i++) { delta |= input.codeUnitAt(i) ^ lowerCaseTarget.codeUnitAt(i); } return delta == 0x20; } static String stringSafeToString(String str) { return JS('!', 'JSON.stringify(#)', str); } static String safeToString(obj) { if (obj == null || obj is num || obj is bool) { return obj.toString(); } if (obj is String) { return stringSafeToString(obj); } if (obj is dart.RecordImpl) { return dart.recordSafeToString(obj); } return "Instance of '${dart.typeName(dart.getReifiedType(obj))}'"; } /// `r"$".codeUnitAt(0)` static const int DOLLAR_CHAR_VALUE = 36; static int dateNow() => JS('!', r'Date.now()'); static void initTicker() { if (timerFrequency != 0) return; // Start with low-resolution. We overwrite the fields if we find better. timerFrequency = 1000; if (JS('!', 'typeof window == "undefined"')) return; var jsWindow = JS('var', 'window'); if (jsWindow == null) return; var performance = JS('var', '#.performance', jsWindow); if (performance == null) return; if (JS('!', 'typeof #.now != "function"', performance)) return; timerFrequency = 1000000; timerTicks = () => (1000 * JS('!', '#.now()', performance)).floor(); } /// 0 frequency indicates the default uninitialized state. static int timerFrequency = 0; static int Function() timerTicks = dateNow; // Low-resolution version. static bool get isD8 { return JS( 'bool', 'typeof version == "function"' ' && typeof os == "object" && "system" in os', ); } static bool get isJsshell { return JS( 'bool', 'typeof version == "function" && typeof system == "function"', ); } static String currentUri() { // In a browser return self.location.href. if (JS('!', '!!#.location', dart.global_)) { return JS('!', '#.location.href', dart.global_); } // TODO(vsm): Consider supporting properly in non-browser settings. return ''; } // This is to avoid stack overflows due to very large argument arrays in // apply(). It fixes http://dartbug.com/6919 @notNull static String _fromCharCodeApply(List array) { const kMaxApply = 500; @nullCheck int end = array.length; if (end <= kMaxApply) { return JS('!', r'String.fromCharCode.apply(null, #)', array); } String result = ''; for (int i = 0; i < end; i += kMaxApply) { int chunkEnd = (i + kMaxApply < end) ? i + kMaxApply : end; result = JS( 'String', r'# + String.fromCharCode.apply(null, #.slice(#, #))', result, array, i, chunkEnd, ); } return result; } @notNull static String stringFromCodePoints(JSArray codePoints) { List a = []; for (@nullCheck var i in codePoints) { if (i <= 0xffff) { a.add(i); } else if (i <= 0x10ffff) { a.add(0xd800 + ((((i - 0x10000) >> 10) & 0x3ff))); a.add(0xdc00 + (i & 0x3ff)); } else { throw argumentErrorValue(i); } } return _fromCharCodeApply(a); } @notNull static String stringFromCharCodes(JSArray charCodes) { for (@nullCheck var i in charCodes) { if (i < 0) throw argumentErrorValue(i); if (i > 0xffff) return stringFromCodePoints(charCodes); } return _fromCharCodeApply(charCodes); } // [start] and [end] are validated. @notNull static String stringFromNativeUint8List( NativeUint8List charCodes, @nullCheck int start, @nullCheck int end, ) { const kMaxApply = 500; if (end <= kMaxApply && start == 0 && end == charCodes.length) { return JS('!', r'String.fromCharCode.apply(null, #)', charCodes); } String result = ''; for (int i = start; i < end; i += kMaxApply) { int chunkEnd = (i + kMaxApply < end) ? i + kMaxApply : end; result = JS( 'String', r'# + String.fromCharCode.apply(null, #.subarray(#, #))', result, charCodes, i, chunkEnd, ); } return result; } @notNull static String stringFromCharCode(@nullCheck int charCode) { if (0 <= charCode) { if (charCode <= 0xffff) { return JS('!', 'String.fromCharCode(#)', charCode); } if (charCode <= 0x10ffff) { var bits = charCode - 0x10000; var low = 0xDC00 | (bits & 0x3ff); var high = 0xD800 | (bits >> 10); return JS('!', 'String.fromCharCode(#, #)', high, low); } } throw RangeError.range(charCode, 0, 0x10ffff); } static String flattenString(String str) { return JS('!', "#.charCodeAt(0) == 0 ? # : #", str, str, str); } static String getTimeZoneName(DateTime receiver) { // Firefox and Chrome emit the timezone in parenthesis. // Example: "Wed May 16 2012 21:13:00 GMT+0200 (CEST)". // We extract this name using a regexp. var d = lazyAsJsDate(receiver); // In this method all calls to `exec()` include a single capture group and // it is only read if there is a match so a value will be present. To avoid // extra null checks or casts from dynamic we type the return type of // `exec()` to always contain non-nullable Strings. var match = JS?>('', r'/\((.*)\)/.exec(#.toString())', d); if (match != null) return match[1]; // Internet Explorer 10+ emits the zone name without parenthesis: // Example: Thu Oct 31 14:07:44 PDT 2013 match = JS?>( '', // Thu followed by a space. r'/^[A-Z,a-z]{3}\s' // Oct 31 followed by space. r'[A-Z,a-z]{3}\s\d+\s' // Time followed by a space. r'\d{2}:\d{2}:\d{2}\s' // The time zone name followed by a space. r'([A-Z]{3,5})\s' // The year. r'\d{4}$/' '.exec(#.toString())', d, ); if (match != null) return match[1]; // IE 9 and Opera don't provide the zone name. We fall back to emitting the // UTC/GMT offset. // Example (IE9): Wed Nov 20 09:51:00 UTC+0100 2013 // (Opera): Wed Nov 20 2013 11:03:38 GMT+0100 match = JS?>( '', r'/(?:GMT|UTC)[+-]\d{4}/.exec(#.toString())', d, ); if (match != null) return match[0]; return ""; } static int getTimeZoneOffsetInSeconds(DateTime receiver) { // Note that JavaScript's Date and and Dart's DateTime disagree on the sign // of the offset. Subtract to avoid -0.0. The offset in minutes could // contain 'seconds' as fractional minutes. num offsetInMinutes = JS( 'num', r'#.getTimezoneOffset()', lazyAsJsDate(receiver), ); return (0 - offsetInMinutes * 60).toInt(); } static int? valueFromDecomposedDate( @nullCheck int years, @nullCheck int month, @nullCheck int day, @nullCheck int hours, @nullCheck int minutes, @nullCheck int seconds, @nullCheck int milliseconds, @nullCheck int microseconds, @nullCheck bool isUtc, ) { final int MAX_MILLISECONDS_SINCE_EPOCH = 8640000000000000; var jsMonth = month - 1; // The JavaScript Date constructor 'corrects' year NN to 19NN. Sidestep that // correction by adjusting years out of that range and compensating with an // adjustment of months. This hack should not be sensitive to leap years but // use 400 just in case. if (0 <= years && years < 100) { years += 400; jsMonth -= 400 * 12; } // JavaScript `Date` does not handle microseconds, so ensure the provided // microseconds is in range [0..999]. final remainder = microseconds % 1000; milliseconds += (microseconds - remainder) ~/ 1000; microseconds = remainder; int value; if (isUtc) { value = JS( '!', r'Date.UTC(#, #, #, #, #, #, #)', years, jsMonth, day, hours, minutes, seconds, milliseconds, ); } else { value = JS( '!', r'new Date(#, #, #, #, #, #, #).valueOf()', years, jsMonth, day, hours, minutes, seconds, milliseconds, ); } if (value.isNaN || value < -MAX_MILLISECONDS_SINCE_EPOCH || value > MAX_MILLISECONDS_SINCE_EPOCH || value == MAX_MILLISECONDS_SINCE_EPOCH && microseconds != 0) { return null; } return value; } // Lazily keep a JS Date stored in the JS object. static lazyAsJsDate(DateTime receiver) { if (JS('!', r'#.date === (void 0)', receiver)) { JS( 'void', r'#.date = new Date(#)', receiver, receiver.millisecondsSinceEpoch, ); } return JS('var', r'#.date', receiver); } // The getters for date and time parts below add a positive integer to ensure // that the result is really an integer, because the JavaScript implementation // may return -0.0 instead of 0. static int getYear(DateTime receiver) { return (receiver.isUtc) ? JS('!', r'(#.getUTCFullYear() + 0)', lazyAsJsDate(receiver)) : JS('!', r'(#.getFullYear() + 0)', lazyAsJsDate(receiver)); } static int getMonth(DateTime receiver) { return (receiver.isUtc) ? JS('!', r'#.getUTCMonth() + 1', lazyAsJsDate(receiver)) : JS('!', r'#.getMonth() + 1', lazyAsJsDate(receiver)); } static int getDay(DateTime receiver) { return (receiver.isUtc) ? JS('!', r'(#.getUTCDate() + 0)', lazyAsJsDate(receiver)) : JS('!', r'(#.getDate() + 0)', lazyAsJsDate(receiver)); } static int getHours(DateTime receiver) { return (receiver.isUtc) ? JS('!', r'(#.getUTCHours() + 0)', lazyAsJsDate(receiver)) : JS('!', r'(#.getHours() + 0)', lazyAsJsDate(receiver)); } static int getMinutes(DateTime receiver) { return (receiver.isUtc) ? JS('!', r'(#.getUTCMinutes() + 0)', lazyAsJsDate(receiver)) : JS('!', r'(#.getMinutes() + 0)', lazyAsJsDate(receiver)); } static int getSeconds(DateTime receiver) { return (receiver.isUtc) ? JS('!', r'(#.getUTCSeconds() + 0)', lazyAsJsDate(receiver)) : JS('!', r'(#.getSeconds() + 0)', lazyAsJsDate(receiver)); } static int getMilliseconds(DateTime receiver) { return (receiver.isUtc) ? JS('!', r'(#.getUTCMilliseconds() + 0)', lazyAsJsDate(receiver)) : JS('!', r'(#.getMilliseconds() + 0)', lazyAsJsDate(receiver)); } static int getWeekday(DateTime receiver) { int weekday = (receiver.isUtc) ? JS('!', r'#.getUTCDay() + 0', lazyAsJsDate(receiver)) : JS('!', r'#.getDay() + 0', lazyAsJsDate(receiver)); // Adjust by one because JS weeks start on Sunday. return (weekday + 6) % 7 + 1; } static Object? getProperty(Object? object, Object key) { if (object == null || object is bool || object is num || object is String) { throw argumentErrorValue(object); } return JS('var', '#[#]', object, key); } static void setProperty(object, key, value) { if (object == null || object is bool || object is num || object is String) { throw argumentErrorValue(object); } JS('void', '#[#] = #', object, key, value); } } /// Diagnoses an indexing error. Returns the ArgumentError or RangeError that /// describes the problem. Error diagnoseIndexError(indexable, int index) { int length = indexable.length; // The following returns the same error that would be thrown by calling // [IndexError.check] with no optional parameters // provided. if (index < 0 || index >= length) { return IndexError.withLength( index, length, indexable: indexable, name: 'index', ); } // The above should always match, but if it does not, use the following. return RangeError.value(index, 'index'); } /// Diagnoses a range error. Returns the ArgumentError or RangeError that /// describes the problem. Error diagnoseRangeError(int? start, int? end, int length) { if (start == null) { return ArgumentError.value(start, 'start'); } if (start < 0 || start > length) { return RangeError.range(start, 0, length, 'start'); } if (end != null) { if (end < start || end > length) { return RangeError.range(end, start, length, 'end'); } } // The above should always match, but if it does not, use the following. return ArgumentError.value(end, "end"); } @notNull int stringLastIndexOfUnchecked(receiver, element, start) => JS('!', r'#.lastIndexOf(#, #)', receiver, element, start); /// 'factory' for constructing ArgumentError.value to keep the call sites small. ArgumentError argumentErrorValue(object) { return ArgumentError.value(object); } void throwArgumentErrorValue(value) { throw argumentErrorValue(value); } checkInt(value) { if (value is! int) throw argumentErrorValue(value); return value; } throwRuntimeError(message) { throw RuntimeError(message); } throwConcurrentModificationError(collection) { throw ConcurrentModificationError(collection); } class JsNoSuchMethodError extends Error implements NoSuchMethodError { final String? _message; final String? _method; final String? _receiver; JsNoSuchMethodError(this._message, match) : _method = match == null ? null : JS('String|Null', '#.method', match), _receiver = match == null ? null : JS('String|Null', '#.receiver', match); String toString() { if (_method == null) return 'NoSuchMethodError: $_message'; if (_receiver == null) { return "NoSuchMethodError: method not found: '$_method' ($_message)"; } return "NoSuchMethodError: " "method not found: '$_method' on '$_receiver' ($_message)"; } } class UnknownJsTypeError extends Error { final String _message; UnknownJsTypeError(this._message); String toString() => _message.isEmpty ? 'Error' : 'Error: $_message'; } /// Called by generated code to build a map literal. [keyValuePairs] is /// a list of key, value, key, value, ..., etc. fillLiteralMap(keyValuePairs, Map result) { // TODO(johnniwinther): Use JSArray to optimize this code instead of calling // [getLength] and [getIndex]. int index = 0; int length = getLength(keyValuePairs); while (index < length) { var key = getIndex(keyValuePairs, index++); var value = getIndex(keyValuePairs, index++); result[key] = value; } return result; } bool jsHasOwnProperty(jsObject, String property) { return JS('!', r'#.hasOwnProperty(#)', jsObject, property); } jsPropertyAccess(jsObject, String property) { return JS('var', r'#[#]', jsObject, property); } /// A metadata annotation describing the types instantiated by a native element. /// /// The annotation is valid on a native method and a field of a native class. /// /// By default, a field of a native class is seen as an instantiation point for /// all native classes that are a subtype of the field's type, and a native /// method is seen as an instantiation point fo all native classes that are a /// subtype of the method's return type, or the argument types of the declared /// type of the method's callback parameter. /// /// An @[Creates] annotation overrides the default set of instantiated types. /// If one or more @[Creates] annotations are present, the type of the native /// element is ignored, and the union of @[Creates] annotations is used instead. /// The names in the strings are resolved and the program will fail to compile /// with dart2js if they do not name types. /// /// The argument to [Creates] is a string. The string is parsed as the names of /// one or more types, separated by vertical bars `|`. There are some special /// names: /// /// * `=Object`. This means 'exactly Object', which is a plain JavaScript object /// with properties and none of the subtypes of Object. /// /// Example: we may know that a method always returns a specific implementation: /// /// @Creates('_NodeList') /// List getElementsByTagName(String tag) native; /// /// Useful trick: A method can be marked as not instantiating any native classes /// with the annotation `@Creates('Null')`. This is useful for fields on native /// classes that are used only in Dart code. /// /// @Creates('Null') /// var _cachedFoo; class Creates { final String types; const Creates(this.types); } /// A metadata annotation describing the types returned or yielded by a native /// element. /// /// The annotation is valid on a native method and a field of a native class. /// /// By default, a native method or field is seen as returning or yielding all /// subtypes if the method return type or field type. This annotation allows a /// more precise set of types to be specified. /// /// See [Creates] for the syntax of the argument. /// /// Example: IndexedDB keys are numbers, strings and JavaScript Arrays of keys. /// /// @Returns('String|num|JSExtendableArray') /// dynamic key; /// /// // Equivalent: /// @Returns('String') @Returns('num') @Returns('JSExtendableArray') /// dynamic key; class Returns { final String types; const Returns(this.types); } /// A metadata annotation placed on native methods and fields of native classes /// to specify the JavaScript name. /// /// This example declares a Dart field + getter + setter called `$dom_title` /// that corresponds to the JavaScript property `title`. /// /// class Document native "*Foo" { /// @JSName('title') /// String $dom_title; /// } class JSName { final String name; const JSName(this.name); } /// Special interface recognized by the compiler and implemented by DOM /// objects that support integer indexing. This interface is not /// visible to anyone, and is only injected into special libraries. abstract class JavaScriptIndexingBehavior extends JSMutableIndexable {} /// Thrown by type assertions that fail. class TypeErrorImpl extends Error implements TypeError { final String _message; TypeErrorImpl(this._message); String toString() => _message; } /// Error thrown when a runtime error occurs. class RuntimeError extends Error { final message; RuntimeError(this.message); String toString() => "RuntimeError: $message"; } class DeferredNotLoadedError extends Error implements NoSuchMethodError { String enclosingLibrary; String importPrefix; DeferredNotLoadedError(this.enclosingLibrary, this.importPrefix); String toString() { return 'Deferred import $importPrefix (from $enclosingLibrary) was not loaded.'; } } /// Error thrown by DDC when an `assert()` fails (with or without a message). class AssertionErrorImpl extends AssertionError { final String? _fileUri; final int? _line; final int? _column; final String? _conditionSource; AssertionErrorImpl( Object? message, [ this._fileUri, this._line, this._column, this._conditionSource, ]) : super(message); String toString() { var failureMessage = ""; if (_fileUri != null && _line != null && _column != null && _conditionSource != null) { failureMessage += "$_fileUri:${_line}:${_column}\n$_conditionSource\n"; } failureMessage += message != null ? Error.safeToString(message) : "is not true"; return "Assertion failed: $failureMessage"; } } /// Creates a random number with 64 bits of randomness. /// /// This will be truncated to the 53 bits available in a double. int random64() { // TODO(lrn): Use a secure random source. int int32a = JS("int", "(Math.random() * 0x100000000) >>> 0"); int int32b = JS("int", "(Math.random() * 0x100000000) >>> 0"); return int32a + int32b * 0x100000000; } class BooleanConversionAssertionError extends AssertionError { toString() => 'Failed assertion: boolean expression must not be null'; } // Hook to register new global object. This is invoked from dart:html // whenever a new window is accessed for the first time. void registerGlobalObject(object) { try { if (dart.polyfill(object)) { dart.applyAllExtensions(object); } } catch (e) { // This may fail due to cross-origin errors. In that case, we shouldn't // need to polyfill as we can't get objects from that frame. // TODO(vsm): Detect this more robustly - ideally before we try to polyfill. } } /// Expose browser JS classes. void applyExtension(name, nativeObject) { dart.applyExtension(name, nativeObject); } /// Hook to apply extensions on native JS classes defined in a native unit test. void applyTestExtensions(List names) { names.forEach(dart.applyExtensionForTesting); } /// Used internally by DDC to map ES6 symbols to Dart. class PrivateSymbol implements Symbol { // TODO(jmesserly): could also get this off the native symbol instead of // storing it. Mirrors already does this conversion. final String _name; final Object _nativeSymbol; const PrivateSymbol(this._name, this._nativeSymbol); static String getName(Symbol symbol) => (symbol as PrivateSymbol)._name; static Object? getNativeSymbol(Symbol symbol) { if (symbol is PrivateSymbol) return symbol._nativeSymbol; return null; } bool operator ==(other) => other is PrivateSymbol && _name == other._name && identical(_nativeSymbol, other._nativeSymbol); get hashCode => _name.hashCode; // TODO(jmesserly): is this equivalent to _nativeSymbol toString? toString() => 'Symbol("$_name")'; } /// Asserts that if [value] is a function, it is a JavaScript function or has /// been wrapped by [allowInterop]. /// /// This function does not recurse if [value] is a collection. void assertInterop(Object? value) { if (value is Function) dart.assertInterop(value); } /// Like [assertInterop], except iterates over a list of arguments /// non-recursively. void assertInteropArgs(List args) => args.forEach(assertInterop); /// Wraps the given [callback] within the current Zone. void Function(T)? wrapZoneUnaryCallback(void Function(T)? callback) { // For performance reasons avoid wrapping if we are in the root zone. if (Zone.current == Zone.root) return callback; if (callback == null) return null; return Zone.current.bindUnaryCallbackGuarded(callback); } /// Returns a JavaScript predicate that tests if the argument is a record with /// the given shape and fields types. /// /// Only called from the `dart:_rti` library but requires specific knowledge of /// the record representation in DDC. There is a duplicate version of this /// method in the dart2js version of this library. /// /// The shape is determined by the number of fields and the [partialShapeTag]. /// [fieldRtis] contains the Rti type objects for each field in order of /// positionals followed by the sorted named elements. Object? createRecordTypePredicate(String partialShapeTag, JSArray fieldRtis) { var shapeKey = JS( '!', '#.length + ";" + #', fieldRtis, partialShapeTag, ); return (obj) { return JS( '!', '# instanceof #', obj, JS_CLASS_REF(dart.RecordImpl), ) && JS('!', '#[#]', obj, dart.shapeProperty) == JS('', '#.get(#)', dart.shapes, shapeKey) && rti.pairwiseIsTest( fieldRtis, JS('!', '#[#]', obj, dart.valuesProperty), ); }; } /// Returns the Rti for the provided [record]. /// /// Is called from the `dart:_rti` library but requires specific knowledge of /// the record representation in DDC. There is a duplicate version of this /// method in the dart2js version of this library. /// /// Calls [rti.evaluateRtiForRecord] with components of the [record]. rti.Rti getRtiForRecord(Object? record) { var recordObj = JS('!', '#', record); var recipeBuffer = StringBuffer('+'); var shape = JS('!', '#[#]', recordObj, dart.shapeProperty); var values = JS('!', '#[#]', recordObj, dart.valuesProperty); var named = shape.named; if (named != null) recipeBuffer.writeAll(named, ','); recipeBuffer.write('('); var elementCount = values.length; recipeBuffer.writeAll([for (var i = 1; i <= elementCount; i++) i], ','); recipeBuffer.write(')'); return rti.evaluateRtiForRecord(recipeBuffer.toString(), values); } /// A marker interface for classes with 'trustworthy' implementations of `get /// runtimeType`. /// /// Generally, overrides of `get runtimeType` are not used in displaying the /// types of irritants in TypeErrors or computing the structural `runtimeType` /// of records. Instead the Rti (aka 'true') type is used. /// /// The 'true' type is sometimes confusing because it shows implementation /// details, e.g. the true type of `42` is `JSInt` and `2.1` is `JSNumNotInt`. /// /// For a limited number of implementation classes we tell a 'white lie' that /// the value is of another type, e.g. that `42` is an `int` and `2.1` is /// `double`. This is achieved by overriding `get runtimeType` to return the /// desired type, and marking the implementation class type with `implements /// [TrustedGetRuntimeType]`. /// /// [TrustedGetRuntimeType] is not exposed outside the `dart:` libraries so /// users cannot tell lies. /// /// The `Type` returned by a trusted `get runtimeType` must be an instance of /// the system `Type`, which is guaranteed by using a type literal. Type /// literals can be generic and dependent on type variables, e.g. `List`. /// /// Care needs to taken to ensure that the runtime does not get caught telling /// lies. Generally, a class's `runtimeType` lies by returning an abstract /// supertype of the class. Since both the the marker interface and `get /// runtimeType` are inherited, there should be no way in which a user can /// extend the class or implement interface of the class. // TODO(48585): Move this class back to the dart:_rti library when old DDC // runtime type system has been removed. abstract class TrustedGetRuntimeType {} /// Wraps the JavaScript `Object.getPrototypeOf()` method returning the /// `__proto__` of [obj]. /// /// This method is equivalent to: /// /// JS('', 'Object.getPrototypeOf(#)', obj); /// /// but the code is generated by the compiler directly (a low-tech way of /// inlining). /// /// This helper should always be used in place of `JS('', '#.__proto__', obj)` /// to avoid prototype pollution issues. /// See: https://github.com/tc39/proposal-symbol-proto external Object? jsObjectGetPrototypeOf(@notNull Object obj); /// Wraps the JavaScript `Object.setPrototypeOf()` method setting the /// `__proto__` of [obj] to [prototype] and returning [obj]. /// /// This method is equivalent to: /// /// JS('!', 'Object.setPrototypeOf(#, #)', obj, prototype); /// /// but the code is generated by the compiler directly (a low-tech way of /// inlining). /// /// This helper should always be used in place of /// `JS('', '#.__proto__ = #', obj, prototype)` to avoid prototype /// pollution issues. /// See: https://github.com/tc39/proposal-symbol-proto @notNull external Object jsObjectSetPrototypeOf(@notNull Object obj, Object? prototype); /// The global context that "static interop" members use for namespaces. /// /// For example, an interop library with no library-level `@JS` annotation and a /// top-level external member named or renamed to 'foo' will lower a call to /// that member as `.foo`. The same applies for any /// external constructors or class/extension type static members. /// /// If the library does have a `@JS` annotation with a value, the call then gets /// lowered to `..foo`. /// /// To see which members get lowered with this, see the transformation in /// `pkg/_js_interop_checks/lib/src/js_util_optimizer.dart`. /// /// This should match the global context that non-static interop members use. /// Note that this is external. We could implement it here, but DDC will not /// inline the call so this will be an extra level of indirection. DDC manually /// inlines this method in the compiler instead. external Object get staticInteropGlobalContext; /// Return a fresh object literal. T createObjectLiteral() => JS('PlainJavaScriptObject', '{}');