| Index: lib/src/utils.dart
 | 
| diff --git a/lib/src/utils.dart b/lib/src/utils.dart
 | 
| index 5d35fd21505607b5f39c1c1c60f62e0ad951dda7..2bead98e47458e926c7cfae2970b725fba986bba 100644
 | 
| --- a/lib/src/utils.dart
 | 
| +++ b/lib/src/utils.dart
 | 
| @@ -55,47 +55,6 @@ class Pair<E, F> {
 | 
|    int get hashCode => first.hashCode ^ last.hashCode;
 | 
|  }
 | 
|  
 | 
| -/// A completer that waits until all added [Future]s complete.
 | 
| -// TODO(rnystrom): Copied from web_components. Remove from here when it gets
 | 
| -// added to dart:core. (See #6626.)
 | 
| -class FutureGroup<T> {
 | 
| -  int _pending = 0;
 | 
| -  Completer<List<T>> _completer = new Completer<List<T>>();
 | 
| -  final List<Future<T>> futures = <Future<T>>[];
 | 
| -  bool completed = false;
 | 
| -
 | 
| -  final List<T> _values = <T>[];
 | 
| -
 | 
| -  /// Wait for [task] to complete.
 | 
| -  Future<T> add(Future<T> task) {
 | 
| -    if (completed) {
 | 
| -      throw new StateError("The FutureGroup has already completed.");
 | 
| -    }
 | 
| -
 | 
| -    _pending++;
 | 
| -    futures.add(task.then((value) {
 | 
| -      if (completed) return;
 | 
| -
 | 
| -      _pending--;
 | 
| -      _values.add(value);
 | 
| -
 | 
| -      if (_pending <= 0) {
 | 
| -        completed = true;
 | 
| -        _completer.complete(_values);
 | 
| -      }
 | 
| -    }).catchError((e, stackTrace) {
 | 
| -      if (completed) return;
 | 
| -
 | 
| -      completed = true;
 | 
| -      _completer.completeError(e, stackTrace);
 | 
| -    }));
 | 
| -
 | 
| -    return task;
 | 
| -  }
 | 
| -
 | 
| -  Future<List> get future => _completer.future;
 | 
| -}
 | 
| -
 | 
|  /// Like [new Future], but avoids around issue 11911 by using [new Future.value]
 | 
|  /// under the covers.
 | 
|  Future newFuture(callback()) => new Future.value().then((_) => callback());
 | 
| @@ -145,7 +104,7 @@ Future captureErrors(Future callback(), {bool captureStackChains: false}) {
 | 
|  /// only returns once all Futures have completed, successfully or not.
 | 
|  ///
 | 
|  /// This will wrap the first error thrown in a [SilentException] and rethrow it.
 | 
| -Future waitAndPrintErrors(Iterable<Future> futures) {
 | 
| +Future<List/*<T>*/> waitAndPrintErrors/*<T>*/(Iterable<Future/*<T>*/> futures) {
 | 
|    return Future.wait(futures.map((future) {
 | 
|      return future.catchError((error, stackTrace) {
 | 
|        log.exception(error, stackTrace);
 | 
| @@ -160,8 +119,8 @@ Future waitAndPrintErrors(Iterable<Future> futures) {
 | 
|  /// completes.
 | 
|  ///
 | 
|  /// The stream will be passed through unchanged.
 | 
| -StreamTransformer onDoneTransformer(void onDone()) {
 | 
| -  return new StreamTransformer.fromHandlers(handleDone: (sink) {
 | 
| +StreamTransformer/*<T, T>*/ onDoneTransformer/*<T>*/(void onDone()) {
 | 
| +  return new StreamTransformer/*<T, T>*/.fromHandlers(handleDone: (sink) {
 | 
|      onDone();
 | 
|      sink.close();
 | 
|    });
 | 
| @@ -274,23 +233,6 @@ bool isLoopback(String host) {
 | 
|    }
 | 
|  }
 | 
|  
 | 
| -/// Flattens nested lists inside an iterable into a single list containing only
 | 
| -/// non-list elements.
 | 
| -List flatten(Iterable nested) {
 | 
| -  var result = [];
 | 
| -  helper(list) {
 | 
| -    for (var element in list) {
 | 
| -      if (element is List) {
 | 
| -        helper(element);
 | 
| -      } else {
 | 
| -        result.add(element);
 | 
| -      }
 | 
| -    }
 | 
| -  }
 | 
| -  helper(nested);
 | 
| -  return result;
 | 
| -}
 | 
| -
 | 
|  /// Randomly chooses a single element in [elements].
 | 
|  /*=T*/ choose/*<T>*/(List/*<T>*/ elements) =>
 | 
|      elements[random.nextInt(elements.length)];
 | 
| @@ -312,7 +254,7 @@ bool overlaps(Set set1, Set set2) {
 | 
|  }
 | 
|  
 | 
|  /// Returns a list containing the sorted elements of [iter].
 | 
| -List ordered(Iterable<Comparable> iter) {
 | 
| +List/*<T>*/ ordered/*<T extends Comparable<T>>*/(Iterable/*<T>*/ iter) {
 | 
|    var list = iter.toList();
 | 
|    list.sort();
 | 
|    return list;
 | 
| @@ -346,84 +288,13 @@ Iterable<Pair> pairs(Iterable iter) {
 | 
|    });
 | 
|  }
 | 
|  
 | 
| -/// Creates a new map from [map] with new keys and values.
 | 
| -///
 | 
| -/// The return values of [key] are used as the keys and the return values of
 | 
| -/// [value] are used as the values for the new map.
 | 
| -///
 | 
| -/// [key] defaults to returning the original key and [value] defaults to
 | 
| -/// returning the original value.
 | 
| -Map mapMap(Map map, {key(key, value), value(key, value)}) {
 | 
| -  if (key == null) key = (key, _) => key;
 | 
| -  if (value == null) value = (_, value) => value;
 | 
| -
 | 
| -  var result = {};
 | 
| -  map.forEach((mapKey, mapValue) {
 | 
| -    result[key(mapKey, mapValue)] = value(mapKey, mapValue);
 | 
| -  });
 | 
| -  return result;
 | 
| -}
 | 
| -
 | 
| -/// Like [Map.fromIterable], but [key] and [value] may return [Future]s.
 | 
| -Future<Map> mapFromIterableAsync(Iterable iter, {key(element),
 | 
| -    value(element)}) {
 | 
| -  if (key == null) key = (element) => element;
 | 
| -  if (value == null) value = (element) => element;
 | 
| -
 | 
| -  var map = new Map();
 | 
| -  return Future.wait(iter.map((element) {
 | 
| -    return Future.wait([
 | 
| -      new Future.sync(() => key(element)),
 | 
| -      new Future.sync(() => value(element))
 | 
| -    ]).then((results) {
 | 
| -      map[results[0]] = results[1];
 | 
| -    });
 | 
| -  })).then((_) => map);
 | 
| -}
 | 
| -
 | 
| -/// Returns a new map with all entries in both [map1] and [map2].
 | 
| -///
 | 
| -/// If there are overlapping keys, [map2]'s value wins.
 | 
| -Map mergeMaps(Map map1, Map map2) {
 | 
| -  var result = {};
 | 
| -  result.addAll(map1);
 | 
| -  result.addAll(map2);
 | 
| -  return result;
 | 
| -}
 | 
| -
 | 
| -/// Returns the transitive closure of [graph].
 | 
| -///
 | 
| -/// This assumes [graph] represents a graph with a vertex for each key and an
 | 
| -/// edge betweek each key and the values for that key.
 | 
| -Map<dynamic, Set> transitiveClosure(Map<dynamic, Iterable> graph) {
 | 
| -  // This uses the Floyd-Warshall algorithm
 | 
| -  // (https://en.wikipedia.org/wiki/Floyd%E2%80%93Warshall_algorithm).
 | 
| -  var result = {};
 | 
| -  graph.forEach((vertex, edges) {
 | 
| -    result[vertex] = new Set.from(edges)..add(vertex);
 | 
| -  });
 | 
| -
 | 
| -  for (var vertex1 in graph.keys) {
 | 
| -    for (var vertex2 in graph.keys) {
 | 
| -      for (var vertex3 in graph.keys) {
 | 
| -        if (result[vertex2].contains(vertex1) &&
 | 
| -            result[vertex1].contains(vertex3)) {
 | 
| -          result[vertex2].add(vertex3);
 | 
| -        }
 | 
| -      }
 | 
| -    }
 | 
| -  }
 | 
| -
 | 
| -  return result;
 | 
| -}
 | 
| -
 | 
|  /// Given a list of filenames, returns a set of patterns that can be used to
 | 
|  /// filter for those filenames.
 | 
|  ///
 | 
|  /// For a given path, that path ends with some string in the returned set if
 | 
|  /// and only if that path's basename is in [files].
 | 
|  Set<String> createFileFilter(Iterable<String> files) {
 | 
| -  return files.expand((file) {
 | 
| +  return files.expand/*<String>*/((file) {
 | 
|      var result = ["/$file"];
 | 
|      if (Platform.operatingSystem == 'windows') result.add("\\$file");
 | 
|      return result;
 | 
| @@ -436,7 +307,7 @@ Set<String> createFileFilter(Iterable<String> files) {
 | 
|  /// For a given path, that path contains some string in the returned set if
 | 
|  /// and only if one of that path's components is in [dirs].
 | 
|  Set<String> createDirectoryFilter(Iterable<String> dirs) {
 | 
| -  return dirs.expand((dir) {
 | 
| +  return dirs.expand/*<String>*/((dir) {
 | 
|      var result = ["/$dir/"];
 | 
|      if (Platform.operatingSystem == 'windows') {
 | 
|        result..add("/$dir\\")..add("\\$dir/")..add("\\$dir\\");
 | 
| @@ -533,7 +404,7 @@ void chainToCompleter(Future future, Completer completer) {
 | 
|  /// emitting the same values and errors as [stream], but only if at least one
 | 
|  /// value can be read successfully. If an error occurs before any values are
 | 
|  /// emitted, the returned Future completes to that error.
 | 
| -Future<Stream> validateStream(Stream stream) {
 | 
| +Future<Stream/*<T>*/> validateStream/*<T>*/(Stream/*<T>*/ stream) {
 | 
|    var completer = new Completer<Stream>();
 | 
|    var controller = new StreamController(sync: true);
 | 
|  
 | 
| @@ -594,66 +465,6 @@ Pair<Stream, StreamSubscription> streamWithSubscription(Stream stream) {
 | 
|    return new Pair<Stream, StreamSubscription>(controller.stream, subscription);
 | 
|  }
 | 
|  
 | 
| -// TODO(nweiz): remove this when issue 7787 is fixed.
 | 
| -/// Creates two single-subscription [Stream]s that each emit all values and
 | 
| -/// errors from [stream].
 | 
| -///
 | 
| -/// This is useful if [stream] is single-subscription but multiple subscribers
 | 
| -/// are necessary.
 | 
| -Pair<Stream, Stream> tee(Stream stream) {
 | 
| -  var controller1 = new StreamController(sync: true);
 | 
| -  var controller2 = new StreamController(sync: true);
 | 
| -  stream.listen((value) {
 | 
| -    controller1.add(value);
 | 
| -    controller2.add(value);
 | 
| -  }, onError: (error, [stackTrace]) {
 | 
| -    controller1.addError(error, stackTrace);
 | 
| -    controller2.addError(error, stackTrace);
 | 
| -  }, onDone: () {
 | 
| -    controller1.close();
 | 
| -    controller2.close();
 | 
| -  });
 | 
| -  return new Pair<Stream, Stream>(controller1.stream, controller2.stream);
 | 
| -}
 | 
| -
 | 
| -/// Merges [stream1] and [stream2] into a single stream that emits events from
 | 
| -/// both sources.
 | 
| -Stream mergeStreams(Stream stream1, Stream stream2) {
 | 
| -  var doneCount = 0;
 | 
| -  var controller = new StreamController(sync: true);
 | 
| -
 | 
| -  for (var stream in [stream1, stream2]) {
 | 
| -    stream.listen(
 | 
| -        controller.add,
 | 
| -        onError: controller.addError,
 | 
| -        onDone: () {
 | 
| -      doneCount++;
 | 
| -      if (doneCount == 2) controller.close();
 | 
| -    });
 | 
| -  }
 | 
| -
 | 
| -  return controller.stream;
 | 
| -}
 | 
| -
 | 
| -/// Returns a [Stream] that will emit the same values as the stream returned by
 | 
| -/// [callback].
 | 
| -///
 | 
| -/// [callback] will only be called when the returned [Stream] gets a subscriber.
 | 
| -Stream callbackStream(Stream callback()) {
 | 
| -  var subscription;
 | 
| -  var controller;
 | 
| -  controller = new StreamController(onListen: () {
 | 
| -    subscription = callback().listen(controller.add,
 | 
| -        onError: controller.addError,
 | 
| -        onDone: controller.close);
 | 
| -  },
 | 
| -      onCancel: () => subscription.cancel(),
 | 
| -      onPause: () => subscription.pause(),
 | 
| -      onResume: () => subscription.resume(),
 | 
| -      sync: true);
 | 
| -  return controller.stream;
 | 
| -}
 | 
| -
 | 
|  /// A regular expression matching a trailing CR character.
 | 
|  final _trailingCR = new RegExp(r"\r$");
 | 
|  
 | 
| @@ -728,7 +539,7 @@ Uri addQueryParameters(Uri url, Map<String, String> parameters) {
 | 
|  /// Convert a URL query string (or `application/x-www-form-urlencoded` body)
 | 
|  /// into a [Map] from parameter names to values.
 | 
|  Map<String, String> queryToMap(String queryList) {
 | 
| -  var map = {};
 | 
| +  var map = <String, String>{};
 | 
|    for (var pair in queryList.split("&")) {
 | 
|      var split = split1(pair, "=");
 | 
|      if (split.isEmpty) continue;
 | 
| @@ -755,7 +566,7 @@ String mapToQuery(Map<String, String> map) {
 | 
|  }
 | 
|  
 | 
|  /// Returns the union of all elements in each set in [sets].
 | 
| -Set unionAll(Iterable<Set> sets) =>
 | 
| +Set/*<T>*/ unionAll/*<T>*/(Iterable<Set/*<T>*/> sets) =>
 | 
|    sets.fold(new Set(), (union, set) => union.union(set));
 | 
|  
 | 
|  // TODO(nweiz): remove this when issue 9068 has been fixed.
 | 
| @@ -795,13 +606,12 @@ String niceDuration(Duration duration) {
 | 
|  
 | 
|    // If we're using verbose logging, be more verbose but more accurate when
 | 
|    // reporting timing information.
 | 
| -  if (log.verbosity.isLevelVisible(log.Level.FINE)) {
 | 
| -    ms = padLeft(ms.toString(), 3, '0');
 | 
| -  } else {
 | 
| -    ms ~/= 100;
 | 
| -  }
 | 
| +  var msString = log.verbosity.isLevelVisible(log.Level.FINE)
 | 
| +      ? padLeft(ms.toString(), 3, '0')
 | 
| +      : (ms ~/ 100).toString();
 | 
|  
 | 
| -  return "$result${hasMinutes ? padLeft(s.toString(), 2, '0') : s}.${ms}s";
 | 
| +  return "$result${hasMinutes ? padLeft(s.toString(), 2, '0') : s}"
 | 
| +      ".${msString}s";
 | 
|  }
 | 
|  
 | 
|  /// Decodes a URL-encoded string.
 | 
| @@ -815,26 +625,27 @@ String urlDecode(String encoded) =>
 | 
|  /// within.
 | 
|  ///
 | 
|  /// Completes with the fully resolved structure.
 | 
| -Future awaitObject(object) {
 | 
| +Future/*<T>*/ awaitObject/*<T>*/(/*=T*/ object) async {
 | 
|    // Unroll nested futures.
 | 
| -  if (object is Future) return object.then(awaitObject);
 | 
| +  if (object is Future) return await awaitObject(await object);
 | 
| +
 | 
|    if (object is Iterable) {
 | 
| -    return Future.wait(object.map(awaitObject).toList());
 | 
| +    // TODO(nweiz): Remove the unnecessary as check when sdk#26965 is fixed.
 | 
| +    return await Future.wait((object as Iterable).map(awaitObject))
 | 
| +        as List/*=T*/;
 | 
|    }
 | 
| -  if (object is! Map) return new Future.value(object);
 | 
|  
 | 
| -  var pairs = <Future<Pair>>[];
 | 
| -  object.forEach((key, value) {
 | 
| -    pairs.add(awaitObject(value)
 | 
| -        .then((resolved) => new Pair(key, resolved)));
 | 
| -  });
 | 
| -  return Future.wait(pairs).then((resolvedPairs) {
 | 
| -    var map = {};
 | 
| -    for (var pair in resolvedPairs) {
 | 
| -      map[pair.first] = pair.last;
 | 
| -    }
 | 
| -    return map;
 | 
| -  });
 | 
| +  if (object is Map) {
 | 
| +    // TODO(nweiz): Remove the unnecessary as check when sdk#26965 is fixed.
 | 
| +    var oldMap = object as Map;
 | 
| +    var newMap = {};
 | 
| +    await Future.wait(oldMap.keys.map((key) async {
 | 
| +      newMap[key] = await awaitObject(await oldMap[key]);
 | 
| +    }));
 | 
| +    return newMap as Map/*=T*/;
 | 
| +  }
 | 
| +
 | 
| +  return object;
 | 
|  }
 | 
|  
 | 
|  /// Whether "special" strings such as Unicode characters or color escapes are
 | 
| 
 |