1ef4399df0
This uses optional new/const and `=` in named argument defaults. All changes are automated, except for: - utils/dartdevc/BUILD.gn: run DDC build scripts with --preview-dart-2 - pkg/dev_compiler/tool/patch_sdk.dart: add a TODO that Analyzer doesn't supporting implicit const in libraries.dart - pkg/dev_compiler/tool/input_sdk/libraries.dart: was not formatted due to the aforementioned Analyzer bug - tools/bots/test_matrix.json: run DDC sourcemap suite in Dart 2 mode - pkg/pkg.status: skip pkg/dev_compiler if running in Dart 1 mode Change-Id: I9b80ccba0c2cc7b66efc662a0b16562e3660aee3 Reviewed-on: https://dart-review.googlesource.com/60402 Commit-Queue: Jenny Messerly <jmesserly@google.com> Reviewed-by: Bob Nystrom <rnystrom@google.com>
498 lines
16 KiB
Dart
498 lines
16 KiB
Dart
// 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.
|
|
|
|
// Patch file for dart:convert library.
|
|
|
|
import 'dart:_js_helper' show argumentErrorValue, patch;
|
|
import 'dart:_foreign_helper' show JS;
|
|
import 'dart:_interceptors' show JSExtendableArray;
|
|
import 'dart:_internal' show MappedIterable, ListIterable;
|
|
import 'dart:collection' show Maps, LinkedHashMap, MapBase;
|
|
import 'dart:_native_typed_data' show NativeUint8List;
|
|
|
|
/**
|
|
* Parses [json] and builds the corresponding parsed JSON value.
|
|
*
|
|
* Parsed JSON values Nare of the types [num], [String], [bool], [Null],
|
|
* [List]s of parsed JSON values or [Map]s from [String] to parsed
|
|
* JSON values.
|
|
*
|
|
* The optional [reviver] function, if provided, is called once for each object
|
|
* or list property parsed. The arguments are the property name ([String]) or
|
|
* list index ([int]), and the value is the parsed value. The return value of
|
|
* the reviver will be used as the value of that property instead of the parsed
|
|
* value. The top level value is passed to the reviver with the empty string as
|
|
* a key.
|
|
*
|
|
* Throws [FormatException] if the input is not valid JSON text.
|
|
*/
|
|
@patch
|
|
_parseJson(String source, reviver(Object key, Object value)) {
|
|
if (source is! String) throw argumentErrorValue(source);
|
|
|
|
var parsed;
|
|
try {
|
|
parsed = JS('=Object|JSExtendableArray|Null|bool|num|String',
|
|
'JSON.parse(#)', source);
|
|
} catch (e) {
|
|
throw FormatException(JS('String', 'String(#)', e));
|
|
}
|
|
|
|
if (reviver == null) {
|
|
return _convertJsonToDartLazy(parsed);
|
|
} else {
|
|
return _convertJsonToDart(parsed, reviver);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Walks the raw JavaScript value [json], replacing JavaScript Objects with
|
|
* Maps. [json] is expected to be freshly allocated so elements can be replaced
|
|
* in-place.
|
|
*/
|
|
_convertJsonToDart(json, reviver(Object key, Object value)) {
|
|
assert(reviver != null);
|
|
walk(e) {
|
|
// JavaScript null, string, number, bool are in the correct representation.
|
|
if (JS('bool', '# == null', e) || JS('bool', 'typeof # != "object"', e)) {
|
|
return e;
|
|
}
|
|
|
|
// This test is needed to avoid identifying '{"__proto__":[]}' as an Array.
|
|
// TODO(sra): Replace this test with cheaper '#.constructor === Array' when
|
|
// bug 621 below is fixed.
|
|
if (JS('bool', 'Object.getPrototypeOf(#) === Array.prototype', e)) {
|
|
// In-place update of the elements since JS Array is a Dart List.
|
|
for (int i = 0; i < JS('int', '#.length', e); i++) {
|
|
// Use JS indexing to avoid range checks. We know this is the only
|
|
// reference to the list, but the compiler will likely never be able to
|
|
// tell that this instance of the list cannot have its length changed by
|
|
// the reviver even though it later will be passed to the reviver at the
|
|
// outer level.
|
|
var item = JS('', '#[#]', e, i);
|
|
JS('', '#[#]=#', e, i, reviver(i, walk(item)));
|
|
}
|
|
return e;
|
|
}
|
|
|
|
// Otherwise it is a plain object, so copy to a JSON map, so we process
|
|
// and revive all entries recursively.
|
|
_JsonMap map = _JsonMap(e);
|
|
var processed = map._processed;
|
|
List<String> keys = map._computeKeys();
|
|
for (int i = 0; i < keys.length; i++) {
|
|
String key = keys[i];
|
|
var revived = reviver(key, walk(JS('', '#[#]', e, key)));
|
|
JS('', '#[#]=#', processed, key, revived);
|
|
}
|
|
|
|
// Update the JSON map structure so future access is cheaper.
|
|
map._original = processed; // Don't keep two objects around.
|
|
return map;
|
|
}
|
|
|
|
return reviver(null, walk(json));
|
|
}
|
|
|
|
_convertJsonToDartLazy(object) {
|
|
// JavaScript null and undefined are represented as null.
|
|
if (object == null) return null;
|
|
|
|
// JavaScript string, number, bool already has the correct representation.
|
|
if (JS('bool', 'typeof # != "object"', object)) {
|
|
return object;
|
|
}
|
|
|
|
// This test is needed to avoid identifying '{"__proto__":[]}' as an array.
|
|
// TODO(sra): Replace this test with cheaper '#.constructor === Array' when
|
|
// bug https://code.google.com/p/v8/issues/detail?id=621 is fixed.
|
|
if (JS('bool', 'Object.getPrototypeOf(#) !== Array.prototype', object)) {
|
|
return _JsonMap(object);
|
|
}
|
|
|
|
// Update the elements in place since JS arrays are Dart lists.
|
|
for (int i = 0; i < JS('int', '#.length', object); i++) {
|
|
// Use JS indexing to avoid range checks. We know this is the only
|
|
// reference to the list, but the compiler will likely never be able to
|
|
// tell that this instance of the list cannot have its length changed by
|
|
// the reviver even though it later will be passed to the reviver at the
|
|
// outer level.
|
|
var item = JS('', '#[#]', object, i);
|
|
JS('', '#[#]=#', object, i, _convertJsonToDartLazy(item));
|
|
}
|
|
return object;
|
|
}
|
|
|
|
class _JsonMap extends MapBase<String, dynamic> {
|
|
// The original JavaScript object remains unchanged until
|
|
// the map is eventually upgraded, in which case we null it
|
|
// out to reclaim the memory used by it.
|
|
var _original;
|
|
|
|
// We keep track of the map entries that we have already
|
|
// processed by adding them to a separate JavaScript object.
|
|
var _processed = _newJavaScriptObject();
|
|
|
|
// If the data slot isn't null, it represents either the list
|
|
// of keys (for non-upgraded JSON maps) or the upgraded map.
|
|
var _data = null;
|
|
|
|
_JsonMap(this._original);
|
|
|
|
operator [](key) {
|
|
if (_isUpgraded) {
|
|
return _upgradedMap[key];
|
|
} else if (key is! String) {
|
|
return null;
|
|
} else {
|
|
var result = _getProperty(_processed, key);
|
|
if (_isUnprocessed(result)) result = _process(key);
|
|
return result;
|
|
}
|
|
}
|
|
|
|
int get length => _isUpgraded ? _upgradedMap.length : _computeKeys().length;
|
|
|
|
bool get isEmpty => length == 0;
|
|
bool get isNotEmpty => length > 0;
|
|
|
|
Iterable<String> get keys {
|
|
if (_isUpgraded) return _upgradedMap.keys;
|
|
return _JsonMapKeyIterable(this);
|
|
}
|
|
|
|
Iterable get values {
|
|
if (_isUpgraded) return _upgradedMap.values;
|
|
return MappedIterable(_computeKeys(), (each) => this[each]);
|
|
}
|
|
|
|
operator []=(key, value) {
|
|
if (_isUpgraded) {
|
|
_upgradedMap[key] = value;
|
|
} else if (containsKey(key)) {
|
|
var processed = _processed;
|
|
_setProperty(processed, key, value);
|
|
var original = _original;
|
|
if (!identical(original, processed)) {
|
|
_setProperty(original, key, null); // Reclaim memory.
|
|
}
|
|
} else {
|
|
_upgrade()[key] = value;
|
|
}
|
|
}
|
|
|
|
void addAll(Map<String, dynamic> other) {
|
|
other.forEach((key, value) {
|
|
this[key] = value;
|
|
});
|
|
}
|
|
|
|
bool containsValue(value) {
|
|
if (_isUpgraded) return _upgradedMap.containsValue(value);
|
|
List<String> keys = _computeKeys();
|
|
for (int i = 0; i < keys.length; i++) {
|
|
String key = keys[i];
|
|
if (this[key] == value) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool containsKey(key) {
|
|
if (_isUpgraded) return _upgradedMap.containsKey(key);
|
|
if (key is! String) return false;
|
|
return _hasProperty(_original, key);
|
|
}
|
|
|
|
putIfAbsent(key, ifAbsent()) {
|
|
if (containsKey(key)) return this[key];
|
|
var value = ifAbsent();
|
|
this[key] = value;
|
|
return value;
|
|
}
|
|
|
|
remove(Object key) {
|
|
if (!_isUpgraded && !containsKey(key)) return null;
|
|
return _upgrade().remove(key);
|
|
}
|
|
|
|
void clear() {
|
|
if (_isUpgraded) {
|
|
_upgradedMap.clear();
|
|
} else {
|
|
if (_data != null) {
|
|
// Clear the list of keys to make sure we force
|
|
// a concurrent modification error if anyone is
|
|
// currently iterating over it.
|
|
_data.clear();
|
|
}
|
|
_original = _processed = null;
|
|
_data = {};
|
|
}
|
|
}
|
|
|
|
void forEach(void f(String key, value)) {
|
|
if (_isUpgraded) return _upgradedMap.forEach(f);
|
|
List<String> keys = _computeKeys();
|
|
for (int i = 0; i < keys.length; i++) {
|
|
String key = keys[i];
|
|
|
|
// Compute the value under the assumption that the property
|
|
// is present but potentially not processed.
|
|
var value = _getProperty(_processed, key);
|
|
if (_isUnprocessed(value)) {
|
|
value = _convertJsonToDartLazy(_getProperty(_original, key));
|
|
_setProperty(_processed, key, value);
|
|
}
|
|
|
|
// Do the callback.
|
|
f(key, value);
|
|
|
|
// Check if invoking the callback function changed
|
|
// the key set. If so, throw an exception.
|
|
if (!identical(keys, _data)) {
|
|
throw ConcurrentModificationError(this);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------
|
|
// Private helper methods.
|
|
// ------------------------------------------
|
|
|
|
bool get _isUpgraded => _processed == null;
|
|
|
|
Map<String, dynamic> get _upgradedMap {
|
|
assert(_isUpgraded);
|
|
// 'cast' the union type to LinkedHashMap. It would be even better if we
|
|
// could 'cast' to the implementation type, since LinkedHashMap includes
|
|
// _JsonMap.
|
|
return JS('LinkedHashMap', '#', _data);
|
|
}
|
|
|
|
List<String> _computeKeys() {
|
|
assert(!_isUpgraded);
|
|
List keys = _data;
|
|
if (keys == null) {
|
|
keys = _data = _getPropertyNames(_original);
|
|
}
|
|
return JS('JSExtendableArray', '#', keys);
|
|
}
|
|
|
|
Map<String, dynamic> _upgrade() {
|
|
if (_isUpgraded) return _upgradedMap;
|
|
|
|
// Copy all the (key, value) pairs to a freshly allocated
|
|
// linked hash map thus preserving the ordering.
|
|
var result = <String, dynamic>{};
|
|
List<String> keys = _computeKeys();
|
|
for (int i = 0; i < keys.length; i++) {
|
|
String key = keys[i];
|
|
result[key] = this[key];
|
|
}
|
|
|
|
// We only upgrade when we need to extend the map, so we can
|
|
// safely force a concurrent modification error in case
|
|
// someone is iterating over the map here.
|
|
if (keys.isEmpty) {
|
|
keys.add(null);
|
|
} else {
|
|
keys.clear();
|
|
}
|
|
|
|
// Clear out the associated JavaScript objects and mark the
|
|
// map as having been upgraded.
|
|
_original = _processed = null;
|
|
_data = result;
|
|
assert(_isUpgraded);
|
|
return result;
|
|
}
|
|
|
|
_process(String key) {
|
|
if (!_hasProperty(_original, key)) return null;
|
|
var result = _convertJsonToDartLazy(_getProperty(_original, key));
|
|
return _setProperty(_processed, key, result);
|
|
}
|
|
|
|
// ------------------------------------------
|
|
// Private JavaScript helper methods.
|
|
// ------------------------------------------
|
|
|
|
static bool _hasProperty(object, String key) =>
|
|
JS('bool', 'Object.prototype.hasOwnProperty.call(#,#)', object, key);
|
|
static _getProperty(object, String key) => JS('', '#[#]', object, key);
|
|
static _setProperty(object, String key, value) =>
|
|
JS('', '#[#]=#', object, key, value);
|
|
static List _getPropertyNames(object) =>
|
|
JS('JSExtendableArray', 'Object.keys(#)', object);
|
|
static bool _isUnprocessed(object) =>
|
|
JS('bool', 'typeof(#)=="undefined"', object);
|
|
static _newJavaScriptObject() => JS('=Object', 'Object.create(null)');
|
|
}
|
|
|
|
class _JsonMapKeyIterable extends ListIterable<String> {
|
|
final _JsonMap _parent;
|
|
|
|
_JsonMapKeyIterable(this._parent);
|
|
|
|
int get length => _parent.length;
|
|
|
|
String elementAt(int index) {
|
|
return _parent._isUpgraded
|
|
? _parent.keys.elementAt(index)
|
|
: _parent._computeKeys()[index];
|
|
}
|
|
|
|
/// Although [ListIterable] defines its own iterator, we return the iterator
|
|
/// of the underlying list [_keys] in order to propagate
|
|
/// [ConcurrentModificationError]s.
|
|
Iterator<String> get iterator {
|
|
return _parent._isUpgraded
|
|
? _parent.keys.iterator
|
|
: _parent._computeKeys().iterator;
|
|
}
|
|
|
|
/// Delegate to [parent.containsKey] to ensure the performance expected
|
|
/// from [Map.keys.containsKey].
|
|
bool contains(Object key) => _parent.containsKey(key);
|
|
}
|
|
|
|
@patch
|
|
class JsonDecoder {
|
|
@patch
|
|
StringConversionSink startChunkedConversion(Sink<Object> sink) {
|
|
return _JsonDecoderSink(_reviver, sink);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Implements the chunked conversion from a JSON string to its corresponding
|
|
* object.
|
|
*
|
|
* The sink only creates one object, but its input can be chunked.
|
|
*/
|
|
// TODO(floitsch): don't accumulate everything before starting to decode.
|
|
class _JsonDecoderSink extends _StringSinkConversionSink {
|
|
final _Reviver _reviver;
|
|
final Sink<Object> _sink;
|
|
|
|
_JsonDecoderSink(this._reviver, this._sink) : super(StringBuffer(''));
|
|
|
|
void close() {
|
|
super.close();
|
|
StringBuffer buffer = _stringSink;
|
|
String accumulated = buffer.toString();
|
|
buffer.clear();
|
|
Object decoded = _parseJson(accumulated, _reviver);
|
|
_sink.add(decoded);
|
|
_sink.close();
|
|
}
|
|
}
|
|
|
|
@patch
|
|
class Utf8Decoder {
|
|
@patch
|
|
Converter<List<int>, T> fuse<T>(Converter<String, T> next) {
|
|
return super.fuse(next);
|
|
}
|
|
|
|
// Currently not intercepting UTF8 decoding.
|
|
@patch
|
|
static String _convertIntercepted(
|
|
bool allowMalformed, List<int> codeUnits, int start, int end) {
|
|
// Test `codeUnits is NativeUint8List`. Dart's NativeUint8List is
|
|
// implemented by JavaScript's Uint8Array.
|
|
if (JS('bool', '# instanceof Uint8Array', codeUnits)) {
|
|
// JS 'cast' to avoid a downcast equivalent to the is-check we hand-coded.
|
|
NativeUint8List casted = JS('NativeUint8List', '#', codeUnits);
|
|
return _convertInterceptedUint8List(allowMalformed, casted, start, end);
|
|
}
|
|
}
|
|
|
|
static String _convertInterceptedUint8List(
|
|
bool allowMalformed, NativeUint8List codeUnits, int start, int end) {
|
|
if (allowMalformed) {
|
|
// TextDecoder with option {fatal: false} does not produce the same result
|
|
// as [Utf8Decoder]. It disagrees on the number of `U+FFFD` (REPLACEMENT
|
|
// CHARACTER) generated for some malformed sequences. We could use
|
|
// TextDecoder with option {fatal: true}, catch the error, and re-try
|
|
// without acceleration. That turns out to be extremely slow (the Error
|
|
// captures a stack trace).
|
|
// TODO(31370): Bring Utf8Decoder into alignment with TextDecoder.
|
|
// TODO(sra): If we can't do that, can we detect valid input fast enough
|
|
// to use a check like the [_unsafe] check below?
|
|
return null;
|
|
}
|
|
|
|
var decoder = _decoder;
|
|
if (decoder == null) return null;
|
|
if (0 == start && end == null) {
|
|
return _useTextDecoderChecked(decoder, codeUnits);
|
|
}
|
|
|
|
int length = codeUnits.length;
|
|
end = RangeError.checkValidRange(start, end, length);
|
|
|
|
if (0 == start && end == codeUnits.length) {
|
|
return _useTextDecoderChecked(decoder, codeUnits);
|
|
}
|
|
|
|
return _useTextDecoderChecked(decoder,
|
|
JS('NativeUint8List', '#.subarray(#, #)', codeUnits, start, end));
|
|
}
|
|
|
|
static String _useTextDecoderChecked(decoder, NativeUint8List codeUnits) {
|
|
if (_unsafe(codeUnits)) return null;
|
|
return _useTextDecoderUnchecked(decoder, codeUnits);
|
|
}
|
|
|
|
static String _useTextDecoderUnchecked(decoder, NativeUint8List codeUnits) {
|
|
// If the input is malformed, catch the exception and return `null` to fall
|
|
// back on unintercepted decoder. The fallback will either succeed in
|
|
// decoding, or report the problem better than TextDecoder.
|
|
try {
|
|
return JS('String', '#.decode(#)', decoder, codeUnits);
|
|
} catch (e) {}
|
|
return null;
|
|
}
|
|
|
|
/// Returns `true` if [codeUnits] contains problematic encodings.
|
|
///
|
|
/// TextDecoder behaves differently to [Utf8Encoder] when the input encodes a
|
|
/// surrogate (U+D800 through U+DFFF). TextDecoder considers the surrogate to
|
|
/// be an encoding error and, depending on the `fatal` option, either throws
|
|
/// and Error or encodes the surrogate as U+FFFD. [Utf8Decoder] does not
|
|
/// consider the surrogate to be an error and returns the code unit encoded by
|
|
/// the surrogate.
|
|
///
|
|
/// Throwing an `Error` captures the stack, whoch makes it so expensive that
|
|
/// it is worth checking the input for surrogates and avoiding TextDecoder in
|
|
/// this case.
|
|
static bool _unsafe(NativeUint8List codeUnits) {
|
|
// Surrogates encode as (hex) ED Ax xx or ED Bx xx.
|
|
int limit = codeUnits.length - 2;
|
|
for (int i = 0; i < limit; i++) {
|
|
int unit1 = codeUnits[i];
|
|
if (unit1 == 0xED) {
|
|
int unit2 = JS('!', '#', codeUnits[i + 1]);
|
|
if ((unit2 & 0xE0) == 0xA0) return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
//// TextDecoder is not defined on some browsers and on the stand-alone d8 and
|
|
/// jsshell engines. Use a lazy initializer to do feature detection once.
|
|
static final _decoder = () {
|
|
try {
|
|
// Use `{fatal: true}`. 'fatal' does not correspond exactly to
|
|
// `!allowMalformed`: TextDecoder rejects unpaired surrogates which
|
|
// [Utf8Decoder] accepts. In non-fatal mode, TextDecoder translates
|
|
// unpaired surrogates to REPLACEMENT CHARACTER (U+FFFD) whereas
|
|
// [Utf8Decoder] leaves the surrogate intact.
|
|
return JS('', 'new TextDecoder("utf-8", {fatal: true})');
|
|
} catch (e) {}
|
|
return null;
|
|
}();
|
|
}
|