| OLD | NEW |
| (Empty) |
| 1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | |
| 2 // for details. All rights reserved. Use of this source code is governed by a | |
| 3 // BSD-style license that can be found in the LICENSE file. | |
| 4 | |
| 5 /// Generic utility functions. Stuff that should possibly be in core. | |
| 6 library pub.utils; | |
| 7 | |
| 8 import 'dart:async'; | |
| 9 import "dart:convert"; | |
| 10 import 'dart:io'; | |
| 11 | |
| 12 // This is used by [libraryPath]. It must be kept up-to-date with all libraries | |
| 13 // whose paths are looked up using that function. | |
| 14 @MirrorsUsed(targets: const ['pub.io', 'test_pub']) | |
| 15 import 'dart:mirrors'; | |
| 16 | |
| 17 import "package:crypto/crypto.dart"; | |
| 18 import 'package:path/path.dart' as path; | |
| 19 import "package:stack_trace/stack_trace.dart"; | |
| 20 | |
| 21 import 'exceptions.dart'; | |
| 22 import 'log.dart' as log; | |
| 23 | |
| 24 export '../../asset/dart/utils.dart'; | |
| 25 | |
| 26 /// A regular expression matching a Dart identifier. | |
| 27 /// | |
| 28 /// This also matches a package name, since they must be Dart identifiers. | |
| 29 final identifierRegExp = new RegExp(r"[a-zA-Z_][a-zA-Z0-9_]+"); | |
| 30 | |
| 31 /// Like [identifierRegExp], but anchored so that it only matches strings that | |
| 32 /// are *just* Dart identifiers. | |
| 33 final onlyIdentifierRegExp = new RegExp("^${identifierRegExp.pattern}\$"); | |
| 34 | |
| 35 /// A pair of values. | |
| 36 class Pair<E, F> { | |
| 37 E first; | |
| 38 F last; | |
| 39 | |
| 40 Pair(this.first, this.last); | |
| 41 | |
| 42 String toString() => '($first, $last)'; | |
| 43 | |
| 44 bool operator==(other) { | |
| 45 if (other is! Pair) return false; | |
| 46 return other.first == first && other.last == last; | |
| 47 } | |
| 48 | |
| 49 int get hashCode => first.hashCode ^ last.hashCode; | |
| 50 } | |
| 51 | |
| 52 /// A completer that waits until all added [Future]s complete. | |
| 53 // TODO(rnystrom): Copied from web_components. Remove from here when it gets | |
| 54 // added to dart:core. (See #6626.) | |
| 55 class FutureGroup<T> { | |
| 56 int _pending = 0; | |
| 57 Completer<List<T>> _completer = new Completer<List<T>>(); | |
| 58 final List<Future<T>> futures = <Future<T>>[]; | |
| 59 bool completed = false; | |
| 60 | |
| 61 final List<T> _values = <T>[]; | |
| 62 | |
| 63 /// Wait for [task] to complete. | |
| 64 Future<T> add(Future<T> task) { | |
| 65 if (completed) { | |
| 66 throw new StateError("The FutureGroup has already completed."); | |
| 67 } | |
| 68 | |
| 69 _pending++; | |
| 70 futures.add(task.then((value) { | |
| 71 if (completed) return; | |
| 72 | |
| 73 _pending--; | |
| 74 _values.add(value); | |
| 75 | |
| 76 if (_pending <= 0) { | |
| 77 completed = true; | |
| 78 _completer.complete(_values); | |
| 79 } | |
| 80 }).catchError((e, stackTrace) { | |
| 81 if (completed) return; | |
| 82 | |
| 83 completed = true; | |
| 84 _completer.completeError(e, stackTrace); | |
| 85 })); | |
| 86 | |
| 87 return task; | |
| 88 } | |
| 89 | |
| 90 Future<List> get future => _completer.future; | |
| 91 } | |
| 92 | |
| 93 /// Like [new Future], but avoids around issue 11911 by using [new Future.value] | |
| 94 /// under the covers. | |
| 95 Future newFuture(callback()) => new Future.value().then((_) => callback()); | |
| 96 | |
| 97 /// Runs [callback] in an error zone and pipes any unhandled error to the | |
| 98 /// returned [Future]. | |
| 99 /// | |
| 100 /// If the returned [Future] produces an error, its stack trace will always be a | |
| 101 /// [Chain]. By default, this chain will contain only the local stack trace, but | |
| 102 /// if [captureStackChains] is passed, it will contain the full stack chain for | |
| 103 /// the error. | |
| 104 Future captureErrors(Future callback(), {bool captureStackChains: false}) { | |
| 105 var completer = new Completer(); | |
| 106 var wrappedCallback = () { | |
| 107 new Future.sync(callback).then(completer.complete) | |
| 108 .catchError((e, stackTrace) { | |
| 109 // [stackTrace] can be null if we're running without [captureStackChains], | |
| 110 // since dart:io will often throw errors without stack traces. | |
| 111 if (stackTrace != null) { | |
| 112 stackTrace = new Chain.forTrace(stackTrace); | |
| 113 } else { | |
| 114 stackTrace = new Chain([]); | |
| 115 } | |
| 116 if (!completer.isCompleted) completer.completeError(e, stackTrace); | |
| 117 }); | |
| 118 }; | |
| 119 | |
| 120 if (captureStackChains) { | |
| 121 Chain.capture(wrappedCallback, onError: (error, stackTrace) { | |
| 122 if (!completer.isCompleted) completer.completeError(error, stackTrace); | |
| 123 }); | |
| 124 } else { | |
| 125 runZoned(wrappedCallback, onError: (e, stackTrace) { | |
| 126 if (stackTrace == null) { | |
| 127 stackTrace = new Chain.current(); | |
| 128 } else { | |
| 129 stackTrace = new Chain([new Trace.from(stackTrace)]); | |
| 130 } | |
| 131 if (!completer.isCompleted) completer.completeError(e, stackTrace); | |
| 132 }); | |
| 133 } | |
| 134 | |
| 135 return completer.future; | |
| 136 } | |
| 137 | |
| 138 /// Like [Future.wait], but prints all errors from the futures as they occur and | |
| 139 /// only returns once all Futures have completed, successfully or not. | |
| 140 /// | |
| 141 /// This will wrap the first error thrown in a [SilentException] and rethrow it. | |
| 142 Future waitAndPrintErrors(Iterable<Future> futures) { | |
| 143 return Future.wait(futures.map((future) { | |
| 144 return future.catchError((error, stackTrace) { | |
| 145 log.exception(error, stackTrace); | |
| 146 throw error; | |
| 147 }); | |
| 148 })).catchError((error, stackTrace) { | |
| 149 throw new SilentException(error, stackTrace); | |
| 150 }); | |
| 151 } | |
| 152 | |
| 153 /// Returns a [StreamTransformer] that will call [onDone] when the stream | |
| 154 /// completes. | |
| 155 /// | |
| 156 /// The stream will be passed through unchanged. | |
| 157 StreamTransformer onDoneTransformer(void onDone()) { | |
| 158 return new StreamTransformer.fromHandlers(handleDone: (sink) { | |
| 159 onDone(); | |
| 160 sink.close(); | |
| 161 }); | |
| 162 } | |
| 163 | |
| 164 // TODO(rnystrom): Move into String? | |
| 165 /// Pads [source] to [length] by adding spaces at the end. | |
| 166 String padRight(String source, int length) { | |
| 167 final result = new StringBuffer(); | |
| 168 result.write(source); | |
| 169 | |
| 170 while (result.length < length) { | |
| 171 result.write(' '); | |
| 172 } | |
| 173 | |
| 174 return result.toString(); | |
| 175 } | |
| 176 | |
| 177 /// Pads [source] to [length] by adding [char]s at the beginning. | |
| 178 /// | |
| 179 /// If [char] is `null`, it defaults to a space. | |
| 180 String padLeft(String source, int length, [String char]) { | |
| 181 if (char == null) char = ' '; | |
| 182 if (source.length >= length) return source; | |
| 183 | |
| 184 return char * (length - source.length) + source; | |
| 185 } | |
| 186 | |
| 187 /// Returns a labelled sentence fragment starting with [name] listing the | |
| 188 /// elements [iter]. | |
| 189 /// | |
| 190 /// If [iter] does not have one item, name will be pluralized by adding "s" or | |
| 191 /// using [plural], if given. | |
| 192 String namedSequence(String name, Iterable iter, [String plural]) { | |
| 193 if (iter.length == 1) return "$name ${iter.single}"; | |
| 194 | |
| 195 if (plural == null) plural = "${name}s"; | |
| 196 return "$plural ${toSentence(iter)}"; | |
| 197 } | |
| 198 | |
| 199 /// Returns a sentence fragment listing the elements of [iter]. | |
| 200 /// | |
| 201 /// This converts each element of [iter] to a string and separates them with | |
| 202 /// commas and/or "and" where appropriate. | |
| 203 String toSentence(Iterable iter) { | |
| 204 if (iter.length == 1) return iter.first.toString(); | |
| 205 return iter.take(iter.length - 1).join(", ") + " and ${iter.last}"; | |
| 206 } | |
| 207 | |
| 208 /// Returns [name] if [number] is 1, or the plural of [name] otherwise. | |
| 209 /// | |
| 210 /// By default, this just adds "s" to the end of [name] to get the plural. If | |
| 211 /// [plural] is passed, that's used instead. | |
| 212 String pluralize(String name, int number, {String plural}) { | |
| 213 if (number == 1) return name; | |
| 214 if (plural != null) return plural; | |
| 215 return '${name}s'; | |
| 216 } | |
| 217 | |
| 218 /// Escapes any regex metacharacters in [string] so that using as a [RegExp] | |
| 219 /// pattern will match the string literally. | |
| 220 // TODO(rnystrom): Remove when #4706 is fixed. | |
| 221 String quoteRegExp(String string) { | |
| 222 // Note: make sure "\" is done first so that we don't escape the other | |
| 223 // escaped characters. We could do all of the replaces at once with a regexp | |
| 224 // but string literal for regex that matches all regex metacharacters would | |
| 225 // be a bit hard to read. | |
| 226 for (var metacharacter in r"\^$.*+?()[]{}|".split("")) { | |
| 227 string = string.replaceAll(metacharacter, "\\$metacharacter"); | |
| 228 } | |
| 229 | |
| 230 return string; | |
| 231 } | |
| 232 | |
| 233 /// Creates a URL string for [address]:[port]. | |
| 234 /// | |
| 235 /// Handles properly formatting IPv6 addresses. | |
| 236 Uri baseUrlForAddress(InternetAddress address, int port) { | |
| 237 if (address.isLoopback) { | |
| 238 return new Uri(scheme: "http", host: "localhost", port: port); | |
| 239 } | |
| 240 | |
| 241 // IPv6 addresses in URLs need to be enclosed in square brackets to avoid | |
| 242 // URL ambiguity with the ":" in the address. | |
| 243 if (address.type == InternetAddressType.IP_V6) { | |
| 244 return new Uri(scheme: "http", host: "[${address.address}]", port: port); | |
| 245 } | |
| 246 | |
| 247 return new Uri(scheme: "http", host: address.address, port: port); | |
| 248 } | |
| 249 | |
| 250 /// Returns whether [host] is a host for a localhost or loopback URL. | |
| 251 /// | |
| 252 /// Unlike [InternetAddress.isLoopback], this hostnames from URLs as well as | |
| 253 /// from [InternetAddress]es, including "localhost". | |
| 254 bool isLoopback(String host) { | |
| 255 if (host == 'localhost') return true; | |
| 256 | |
| 257 // IPv6 hosts in URLs are surrounded by square brackets. | |
| 258 if (host.startsWith("[") && host.endsWith("]")) { | |
| 259 host = host.substring(1, host.length - 1); | |
| 260 } | |
| 261 | |
| 262 try { | |
| 263 return new InternetAddress(host).isLoopback; | |
| 264 } on ArgumentError catch (_) { | |
| 265 // The host isn't an IP address and isn't "localhost', so it's almost | |
| 266 // certainly not a loopback host. | |
| 267 return false; | |
| 268 } | |
| 269 } | |
| 270 | |
| 271 /// Flattens nested lists inside an iterable into a single list containing only | |
| 272 /// non-list elements. | |
| 273 List flatten(Iterable nested) { | |
| 274 var result = []; | |
| 275 helper(list) { | |
| 276 for (var element in list) { | |
| 277 if (element is List) { | |
| 278 helper(element); | |
| 279 } else { | |
| 280 result.add(element); | |
| 281 } | |
| 282 } | |
| 283 } | |
| 284 helper(nested); | |
| 285 return result; | |
| 286 } | |
| 287 | |
| 288 /// Returns a set containing all elements in [minuend] that are not in | |
| 289 /// [subtrahend]. | |
| 290 Set setMinus(Iterable minuend, Iterable subtrahend) { | |
| 291 var minuendSet = new Set.from(minuend); | |
| 292 minuendSet.removeAll(subtrahend); | |
| 293 return minuendSet; | |
| 294 } | |
| 295 | |
| 296 /// Returns whether there's any overlap between [set1] and [set2]. | |
| 297 bool overlaps(Set set1, Set set2) { | |
| 298 // Iterate through the smaller set. | |
| 299 var smaller = set1.length > set2.length ? set1 : set2; | |
| 300 var larger = smaller == set1 ? set2 : set1; | |
| 301 return smaller.any(larger.contains); | |
| 302 } | |
| 303 | |
| 304 /// Returns a list containing the sorted elements of [iter]. | |
| 305 List ordered(Iterable<Comparable> iter) { | |
| 306 var list = iter.toList(); | |
| 307 list.sort(); | |
| 308 return list; | |
| 309 } | |
| 310 | |
| 311 /// Returns the element of [iter] for which [f] returns the minimum value. | |
| 312 minBy(Iterable iter, Comparable f(element)) { | |
| 313 var min = null; | |
| 314 var minComparable = null; | |
| 315 for (var element in iter) { | |
| 316 var comparable = f(element); | |
| 317 if (minComparable == null || | |
| 318 comparable.compareTo(minComparable) < 0) { | |
| 319 min = element; | |
| 320 minComparable = comparable; | |
| 321 } | |
| 322 } | |
| 323 return min; | |
| 324 } | |
| 325 | |
| 326 /// Returns every pair of consecutive elements in [iter]. | |
| 327 /// | |
| 328 /// For example, if [iter] is `[1, 2, 3, 4]`, this will return `[(1, 2), (2, 3), | |
| 329 /// (3, 4)]`. | |
| 330 Iterable<Pair> pairs(Iterable iter) { | |
| 331 var previous = iter.first; | |
| 332 return iter.skip(1).map((element) { | |
| 333 var oldPrevious = previous; | |
| 334 previous = element; | |
| 335 return new Pair(oldPrevious, element); | |
| 336 }); | |
| 337 } | |
| 338 | |
| 339 /// Creates a new map from [map] with new keys and values. | |
| 340 /// | |
| 341 /// The return values of [key] are used as the keys and the return values of | |
| 342 /// [value] are used as the values for the new map. | |
| 343 /// | |
| 344 /// [key] defaults to returning the original key and [value] defaults to | |
| 345 /// returning the original value. | |
| 346 Map mapMap(Map map, {key(key, value), value(key, value)}) { | |
| 347 if (key == null) key = (key, _) => key; | |
| 348 if (value == null) value = (_, value) => value; | |
| 349 | |
| 350 var result = {}; | |
| 351 map.forEach((mapKey, mapValue) { | |
| 352 result[key(mapKey, mapValue)] = value(mapKey, mapValue); | |
| 353 }); | |
| 354 return result; | |
| 355 } | |
| 356 | |
| 357 /// Like [Map.fromIterable], but [key] and [value] may return [Future]s. | |
| 358 Future<Map> mapFromIterableAsync(Iterable iter, {key(element), | |
| 359 value(element)}) { | |
| 360 if (key == null) key = (element) => element; | |
| 361 if (value == null) value = (element) => element; | |
| 362 | |
| 363 var map = new Map(); | |
| 364 return Future.wait(iter.map((element) { | |
| 365 return Future.wait([ | |
| 366 new Future.sync(() => key(element)), | |
| 367 new Future.sync(() => value(element)) | |
| 368 ]).then((results) { | |
| 369 map[results[0]] = results[1]; | |
| 370 }); | |
| 371 })).then((_) => map); | |
| 372 } | |
| 373 | |
| 374 /// Returns a new map with all entries in both [map1] and [map2]. | |
| 375 /// | |
| 376 /// If there are overlapping keys, [map2]'s value wins. | |
| 377 Map mergeMaps(Map map1, Map map2) { | |
| 378 var result = {}; | |
| 379 result.addAll(map1); | |
| 380 result.addAll(map2); | |
| 381 return result; | |
| 382 } | |
| 383 | |
| 384 /// Returns the transitive closure of [graph]. | |
| 385 /// | |
| 386 /// This assumes [graph] represents a graph with a vertex for each key and an | |
| 387 /// edge betweek each key and the values for that key. | |
| 388 Map<dynamic, Set> transitiveClosure(Map<dynamic, Iterable> graph) { | |
| 389 // This uses the Floyd-Warshall algorithm | |
| 390 // (https://en.wikipedia.org/wiki/Floyd%E2%80%93Warshall_algorithm). | |
| 391 var result = {}; | |
| 392 graph.forEach((vertex, edges) { | |
| 393 result[vertex] = new Set.from(edges)..add(vertex); | |
| 394 }); | |
| 395 | |
| 396 for (var vertex1 in graph.keys) { | |
| 397 for (var vertex2 in graph.keys) { | |
| 398 for (var vertex3 in graph.keys) { | |
| 399 if (result[vertex2].contains(vertex1) && | |
| 400 result[vertex1].contains(vertex3)) { | |
| 401 result[vertex2].add(vertex3); | |
| 402 } | |
| 403 } | |
| 404 } | |
| 405 } | |
| 406 | |
| 407 return result; | |
| 408 } | |
| 409 | |
| 410 /// Given a list of filenames, returns a set of patterns that can be used to | |
| 411 /// filter for those filenames. | |
| 412 /// | |
| 413 /// For a given path, that path ends with some string in the returned set if | |
| 414 /// and only if that path's basename is in [files]. | |
| 415 Set<String> createFileFilter(Iterable<String> files) { | |
| 416 return files.expand((file) { | |
| 417 var result = ["/$file"]; | |
| 418 if (Platform.operatingSystem == 'windows') result.add("\\$file"); | |
| 419 return result; | |
| 420 }).toSet(); | |
| 421 } | |
| 422 | |
| 423 /// Given a blacklist of directory names, returns a set of patterns that can | |
| 424 /// be used to filter for those directory names. | |
| 425 /// | |
| 426 /// For a given path, that path contains some string in the returned set if | |
| 427 /// and only if one of that path's components is in [dirs]. | |
| 428 Set<String> createDirectoryFilter(Iterable<String> dirs) { | |
| 429 return dirs.expand((dir) { | |
| 430 var result = ["/$dir/"]; | |
| 431 if (Platform.operatingSystem == 'windows') { | |
| 432 result..add("/$dir\\")..add("\\$dir/")..add("\\$dir\\"); | |
| 433 } | |
| 434 return result; | |
| 435 }).toSet(); | |
| 436 } | |
| 437 | |
| 438 /// Returns the maximum value in [iter] by [compare]. | |
| 439 /// | |
| 440 /// [compare] defaults to [Comparable.compare]. | |
| 441 maxAll(Iterable iter, [int compare(element1, element2)]) { | |
| 442 if (compare == null) compare = Comparable.compare; | |
| 443 return iter.reduce((max, element) => | |
| 444 compare(element, max) > 0 ? element : max); | |
| 445 } | |
| 446 | |
| 447 /// Returns the minimum value in [iter] by [compare]. | |
| 448 /// | |
| 449 /// [compare] defaults to [Comparable.compare]. | |
| 450 minAll(Iterable iter, [int compare(element1, element2)]) { | |
| 451 if (compare == null) compare = Comparable.compare; | |
| 452 return iter.reduce((max, element) => | |
| 453 compare(element, max) < 0 ? element : max); | |
| 454 } | |
| 455 | |
| 456 /// Replace each instance of [matcher] in [source] with the return value of | |
| 457 /// [fn]. | |
| 458 String replace(String source, Pattern matcher, String fn(Match)) { | |
| 459 var buffer = new StringBuffer(); | |
| 460 var start = 0; | |
| 461 for (var match in matcher.allMatches(source)) { | |
| 462 buffer.write(source.substring(start, match.start)); | |
| 463 start = match.end; | |
| 464 buffer.write(fn(match)); | |
| 465 } | |
| 466 buffer.write(source.substring(start)); | |
| 467 return buffer.toString(); | |
| 468 } | |
| 469 | |
| 470 /// Returns whether or not [str] ends with [matcher]. | |
| 471 bool endsWithPattern(String str, Pattern matcher) { | |
| 472 for (var match in matcher.allMatches(str)) { | |
| 473 if (match.end == str.length) return true; | |
| 474 } | |
| 475 return false; | |
| 476 } | |
| 477 | |
| 478 /// Returns the hex-encoded sha1 hash of [source]. | |
| 479 String sha1(String source) { | |
| 480 var sha = new SHA1(); | |
| 481 sha.add(source.codeUnits); | |
| 482 return CryptoUtils.bytesToHex(sha.close()); | |
| 483 } | |
| 484 | |
| 485 /// Configures [future] so that its result (success or exception) is passed on | |
| 486 /// to [completer]. | |
| 487 void chainToCompleter(Future future, Completer completer) { | |
| 488 future.then(completer.complete, onError: completer.completeError); | |
| 489 } | |
| 490 | |
| 491 /// Ensures that [stream] can emit at least one value successfully (or close | |
| 492 /// without any values). | |
| 493 /// | |
| 494 /// For example, reading asynchronously from a non-existent file will return a | |
| 495 /// stream that fails on the first chunk. In order to handle that more | |
| 496 /// gracefully, you may want to check that the stream looks like it's working | |
| 497 /// before you pipe the stream to something else. | |
| 498 /// | |
| 499 /// This lets you do that. It returns a [Future] that completes to a [Stream] | |
| 500 /// emitting the same values and errors as [stream], but only if at least one | |
| 501 /// value can be read successfully. If an error occurs before any values are | |
| 502 /// emitted, the returned Future completes to that error. | |
| 503 Future<Stream> validateStream(Stream stream) { | |
| 504 var completer = new Completer<Stream>(); | |
| 505 var controller = new StreamController(sync: true); | |
| 506 | |
| 507 StreamSubscription subscription; | |
| 508 subscription = stream.listen((value) { | |
| 509 // We got a value, so the stream is valid. | |
| 510 if (!completer.isCompleted) completer.complete(controller.stream); | |
| 511 controller.add(value); | |
| 512 }, onError: (error, [stackTrace]) { | |
| 513 // If the error came after values, it's OK. | |
| 514 if (completer.isCompleted) { | |
| 515 controller.addError(error, stackTrace); | |
| 516 return; | |
| 517 } | |
| 518 | |
| 519 // Otherwise, the error came first and the stream is invalid. | |
| 520 completer.completeError(error, stackTrace); | |
| 521 | |
| 522 // We don't be returning the stream at all in this case, so unsubscribe | |
| 523 // and swallow the error. | |
| 524 subscription.cancel(); | |
| 525 }, onDone: () { | |
| 526 // It closed with no errors, so the stream is valid. | |
| 527 if (!completer.isCompleted) completer.complete(controller.stream); | |
| 528 controller.close(); | |
| 529 }); | |
| 530 | |
| 531 return completer.future; | |
| 532 } | |
| 533 | |
| 534 // TODO(nweiz): remove this when issue 7964 is fixed. | |
| 535 /// Returns a [Future] that will complete to the first element of [stream]. | |
| 536 /// | |
| 537 /// Unlike [Stream.first], this is safe to use with single-subscription streams. | |
| 538 Future streamFirst(Stream stream) { | |
| 539 var completer = new Completer(); | |
| 540 var subscription; | |
| 541 subscription = stream.listen((value) { | |
| 542 subscription.cancel(); | |
| 543 completer.complete(value); | |
| 544 }, onError: (e, [stackTrace]) { | |
| 545 completer.completeError(e, stackTrace); | |
| 546 }, onDone: () { | |
| 547 completer.completeError(new StateError("No elements"), new Chain.current()); | |
| 548 }, cancelOnError: true); | |
| 549 return completer.future; | |
| 550 } | |
| 551 | |
| 552 /// Returns a wrapped version of [stream] along with a [StreamSubscription] that | |
| 553 /// can be used to control the wrapped stream. | |
| 554 Pair<Stream, StreamSubscription> streamWithSubscription(Stream stream) { | |
| 555 var controller = | |
| 556 stream.isBroadcast ? new StreamController.broadcast(sync: true) | |
| 557 : new StreamController(sync: true); | |
| 558 var subscription = stream.listen(controller.add, | |
| 559 onError: controller.addError, | |
| 560 onDone: controller.close); | |
| 561 return new Pair<Stream, StreamSubscription>(controller.stream, subscription); | |
| 562 } | |
| 563 | |
| 564 // TODO(nweiz): remove this when issue 7787 is fixed. | |
| 565 /// Creates two single-subscription [Stream]s that each emit all values and | |
| 566 /// errors from [stream]. | |
| 567 /// | |
| 568 /// This is useful if [stream] is single-subscription but multiple subscribers | |
| 569 /// are necessary. | |
| 570 Pair<Stream, Stream> tee(Stream stream) { | |
| 571 var controller1 = new StreamController(sync: true); | |
| 572 var controller2 = new StreamController(sync: true); | |
| 573 stream.listen((value) { | |
| 574 controller1.add(value); | |
| 575 controller2.add(value); | |
| 576 }, onError: (error, [stackTrace]) { | |
| 577 controller1.addError(error, stackTrace); | |
| 578 controller2.addError(error, stackTrace); | |
| 579 }, onDone: () { | |
| 580 controller1.close(); | |
| 581 controller2.close(); | |
| 582 }); | |
| 583 return new Pair<Stream, Stream>(controller1.stream, controller2.stream); | |
| 584 } | |
| 585 | |
| 586 /// Merges [stream1] and [stream2] into a single stream that emits events from | |
| 587 /// both sources. | |
| 588 Stream mergeStreams(Stream stream1, Stream stream2) { | |
| 589 var doneCount = 0; | |
| 590 var controller = new StreamController(sync: true); | |
| 591 | |
| 592 for (var stream in [stream1, stream2]) { | |
| 593 stream.listen( | |
| 594 controller.add, | |
| 595 onError: controller.addError, | |
| 596 onDone: () { | |
| 597 doneCount++; | |
| 598 if (doneCount == 2) controller.close(); | |
| 599 }); | |
| 600 } | |
| 601 | |
| 602 return controller.stream; | |
| 603 } | |
| 604 | |
| 605 /// A regular expression matching a trailing CR character. | |
| 606 final _trailingCR = new RegExp(r"\r$"); | |
| 607 | |
| 608 // TODO(nweiz): Use `text.split(new RegExp("\r\n?|\n\r?"))` when issue 9360 is | |
| 609 // fixed. | |
| 610 /// Splits [text] on its line breaks in a Windows-line-break-friendly way. | |
| 611 List<String> splitLines(String text) => | |
| 612 text.split("\n").map((line) => line.replaceFirst(_trailingCR, "")).toList(); | |
| 613 | |
| 614 /// Converts a stream of arbitrarily chunked strings into a line-by-line stream. | |
| 615 /// | |
| 616 /// The lines don't include line termination characters. A single trailing | |
| 617 /// newline is ignored. | |
| 618 Stream<String> streamToLines(Stream<String> stream) { | |
| 619 var buffer = new StringBuffer(); | |
| 620 return stream.transform(new StreamTransformer.fromHandlers( | |
| 621 handleData: (chunk, sink) { | |
| 622 var lines = splitLines(chunk); | |
| 623 var leftover = lines.removeLast(); | |
| 624 for (var line in lines) { | |
| 625 if (!buffer.isEmpty) { | |
| 626 buffer.write(line); | |
| 627 line = buffer.toString(); | |
| 628 buffer = new StringBuffer(); | |
| 629 } | |
| 630 | |
| 631 sink.add(line); | |
| 632 } | |
| 633 buffer.write(leftover); | |
| 634 }, | |
| 635 handleDone: (sink) { | |
| 636 if (!buffer.isEmpty) sink.add(buffer.toString()); | |
| 637 sink.close(); | |
| 638 })); | |
| 639 } | |
| 640 | |
| 641 /// Like [Iterable.where], but allows [test] to return [Future]s and uses the | |
| 642 /// results of those [Future]s as the test. | |
| 643 Future<Iterable> futureWhere(Iterable iter, test(value)) { | |
| 644 return Future.wait(iter.map((e) { | |
| 645 var result = test(e); | |
| 646 if (result is! Future) result = new Future.value(result); | |
| 647 return result.then((result) => new Pair(e, result)); | |
| 648 })) | |
| 649 .then((pairs) => pairs.where((pair) => pair.last)) | |
| 650 .then((pairs) => pairs.map((pair) => pair.first)); | |
| 651 } | |
| 652 | |
| 653 // TODO(nweiz): unify the following functions with the utility functions in | |
| 654 // pkg/http. | |
| 655 | |
| 656 /// Like [String.split], but only splits on the first occurrence of the pattern. | |
| 657 /// | |
| 658 /// This always returns an array of two elements or fewer. | |
| 659 List<String> split1(String toSplit, String pattern) { | |
| 660 if (toSplit.isEmpty) return <String>[]; | |
| 661 | |
| 662 var index = toSplit.indexOf(pattern); | |
| 663 if (index == -1) return [toSplit]; | |
| 664 return [toSplit.substring(0, index), | |
| 665 toSplit.substring(index + pattern.length)]; | |
| 666 } | |
| 667 | |
| 668 /// Adds additional query parameters to [url], overwriting the original | |
| 669 /// parameters if a name conflict occurs. | |
| 670 Uri addQueryParameters(Uri url, Map<String, String> parameters) { | |
| 671 var queryMap = queryToMap(url.query); | |
| 672 queryMap.addAll(parameters); | |
| 673 return url.resolve("?${mapToQuery(queryMap)}"); | |
| 674 } | |
| 675 | |
| 676 /// Convert a URL query string (or `application/x-www-form-urlencoded` body) | |
| 677 /// into a [Map] from parameter names to values. | |
| 678 Map<String, String> queryToMap(String queryList) { | |
| 679 var map = {}; | |
| 680 for (var pair in queryList.split("&")) { | |
| 681 var split = split1(pair, "="); | |
| 682 if (split.isEmpty) continue; | |
| 683 var key = urlDecode(split[0]); | |
| 684 var value = split.length > 1 ? urlDecode(split[1]) : ""; | |
| 685 map[key] = value; | |
| 686 } | |
| 687 return map; | |
| 688 } | |
| 689 | |
| 690 /// Convert a [Map] from parameter names to values to a URL query string. | |
| 691 String mapToQuery(Map<String, String> map) { | |
| 692 var pairs = <List<String>>[]; | |
| 693 map.forEach((key, value) { | |
| 694 key = Uri.encodeQueryComponent(key); | |
| 695 value = (value == null || value.isEmpty) | |
| 696 ? null : Uri.encodeQueryComponent(value); | |
| 697 pairs.add([key, value]); | |
| 698 }); | |
| 699 return pairs.map((pair) { | |
| 700 if (pair[1] == null) return pair[0]; | |
| 701 return "${pair[0]}=${pair[1]}"; | |
| 702 }).join("&"); | |
| 703 } | |
| 704 | |
| 705 /// Returns the union of all elements in each set in [sets]. | |
| 706 Set unionAll(Iterable<Set> sets) => | |
| 707 sets.fold(new Set(), (union, set) => union.union(set)); | |
| 708 | |
| 709 // TODO(nweiz): remove this when issue 9068 has been fixed. | |
| 710 /// Whether [uri1] and [uri2] are equal. | |
| 711 /// | |
| 712 /// This consider HTTP URIs to default to port 80, and HTTPs URIs to default to | |
| 713 /// port 443. | |
| 714 bool urisEqual(Uri uri1, Uri uri2) => | |
| 715 canonicalizeUri(uri1) == canonicalizeUri(uri2); | |
| 716 | |
| 717 /// Return [uri] with redundant port information removed. | |
| 718 Uri canonicalizeUri(Uri uri) { | |
| 719 return uri; | |
| 720 } | |
| 721 | |
| 722 /// Returns a human-friendly representation of [inputPath]. | |
| 723 /// | |
| 724 /// If [inputPath] isn't too distant from the current working directory, this | |
| 725 /// will return the relative path to it. Otherwise, it will return the absolute | |
| 726 /// path. | |
| 727 String nicePath(String inputPath) { | |
| 728 var relative = path.relative(inputPath); | |
| 729 var split = path.split(relative); | |
| 730 if (split.length > 1 && split[0] == '..' && split[1] == '..') { | |
| 731 return path.absolute(inputPath); | |
| 732 } | |
| 733 return relative; | |
| 734 } | |
| 735 | |
| 736 /// Returns a human-friendly representation of [duration]. | |
| 737 String niceDuration(Duration duration) { | |
| 738 var result = duration.inMinutes > 0 ? "${duration.inMinutes}:" : ""; | |
| 739 | |
| 740 var s = duration.inSeconds % 59; | |
| 741 var ms = duration.inMilliseconds % 1000; | |
| 742 | |
| 743 // If we're using verbose logging, be more verbose but more accurate when | |
| 744 // reporting timing information. | |
| 745 if (log.verbosity.isLevelVisible(log.Level.FINE)) { | |
| 746 ms = padLeft(ms.toString(), 3, '0'); | |
| 747 } else { | |
| 748 ms ~/= 100; | |
| 749 } | |
| 750 | |
| 751 return "$result$s.${ms}s"; | |
| 752 } | |
| 753 | |
| 754 /// Decodes a URL-encoded string. | |
| 755 /// | |
| 756 /// Unlike [Uri.decodeComponent], this includes replacing `+` with ` `. | |
| 757 String urlDecode(String encoded) => | |
| 758 Uri.decodeComponent(encoded.replaceAll("+", " ")); | |
| 759 | |
| 760 /// Takes a simple data structure (composed of [Map]s, [Iterable]s, scalar | |
| 761 /// objects, and [Future]s) and recursively resolves all the [Future]s contained | |
| 762 /// within. | |
| 763 /// | |
| 764 /// Completes with the fully resolved structure. | |
| 765 Future awaitObject(object) { | |
| 766 // Unroll nested futures. | |
| 767 if (object is Future) return object.then(awaitObject); | |
| 768 if (object is Iterable) { | |
| 769 return Future.wait(object.map(awaitObject).toList()); | |
| 770 } | |
| 771 if (object is! Map) return new Future.value(object); | |
| 772 | |
| 773 var pairs = <Future<Pair>>[]; | |
| 774 object.forEach((key, value) { | |
| 775 pairs.add(awaitObject(value) | |
| 776 .then((resolved) => new Pair(key, resolved))); | |
| 777 }); | |
| 778 return Future.wait(pairs).then((resolvedPairs) { | |
| 779 var map = {}; | |
| 780 for (var pair in resolvedPairs) { | |
| 781 map[pair.first] = pair.last; | |
| 782 } | |
| 783 return map; | |
| 784 }); | |
| 785 } | |
| 786 | |
| 787 /// Returns the path to the library named [libraryName]. | |
| 788 /// | |
| 789 /// The library name must be globally unique, or the wrong library path may be | |
| 790 /// returned. Any libraries accessed must be added to the [MirrorsUsed] | |
| 791 /// declaration in the import above. | |
| 792 String libraryPath(String libraryName) { | |
| 793 var lib = currentMirrorSystem().findLibrary(new Symbol(libraryName)); | |
| 794 return path.fromUri(lib.uri); | |
| 795 } | |
| 796 | |
| 797 /// Whether "special" strings such as Unicode characters or color escapes are | |
| 798 /// safe to use. | |
| 799 /// | |
| 800 /// On Windows or when not printing to a terminal, only printable ASCII | |
| 801 /// characters should be used. | |
| 802 bool get canUseSpecialChars => !runningAsTest && | |
| 803 Platform.operatingSystem != 'windows' && | |
| 804 stdioType(stdout) == StdioType.TERMINAL; | |
| 805 | |
| 806 /// Gets a "special" string (ANSI escape or Unicode). | |
| 807 /// | |
| 808 /// On Windows or when not printing to a terminal, returns something else since | |
| 809 /// those aren't supported. | |
| 810 String getSpecial(String special, [String onWindows = '']) => | |
| 811 canUseSpecialChars ? special : onWindows; | |
| 812 | |
| 813 /// Prepends each line in [text] with [prefix]. | |
| 814 /// | |
| 815 /// If [firstPrefix] is passed, the first line is prefixed with that instead. | |
| 816 String prefixLines(String text, {String prefix: '| ', String firstPrefix}) { | |
| 817 var lines = text.split('\n'); | |
| 818 if (firstPrefix == null) { | |
| 819 return lines.map((line) => '$prefix$line').join('\n'); | |
| 820 } | |
| 821 | |
| 822 var firstLine = "$firstPrefix${lines.first}"; | |
| 823 lines = lines.skip(1).map((line) => '$prefix$line').toList(); | |
| 824 lines.insert(0, firstLine); | |
| 825 return lines.join('\n'); | |
| 826 } | |
| 827 | |
| 828 /// Whether pub is running as a subprocess in an integration test or in a unit | |
| 829 /// test that has explicitly set this. | |
| 830 bool runningAsTest = Platform.environment.containsKey('_PUB_TESTING'); | |
| 831 | |
| 832 /// Whether today is April Fools' day. | |
| 833 bool get isAprilFools { | |
| 834 // Tests should never see April Fools' output. | |
| 835 if (runningAsTest) return false; | |
| 836 | |
| 837 var date = new DateTime.now(); | |
| 838 return date.month == 4 && date.day == 1; | |
| 839 } | |
| 840 | |
| 841 /// Wraps [fn] to guard against several different kinds of stack overflow | |
| 842 /// exceptions: | |
| 843 /// | |
| 844 /// * A sufficiently long [Future] chain can cause a stack overflow if there are | |
| 845 /// no asynchronous operations in it (issue 9583). | |
| 846 /// * A recursive function that recurses too deeply without an asynchronous | |
| 847 /// operation can cause a stack overflow. | |
| 848 /// * Even if the former is guarded against by adding asynchronous operations, | |
| 849 /// returning a value through the [Future] chain can still cause a stack | |
| 850 /// overflow. | |
| 851 Future resetStack(fn()) { | |
| 852 // Using a [Completer] breaks the [Future] chain for the return value and | |
| 853 // avoids the third case described above. | |
| 854 var completer = new Completer(); | |
| 855 | |
| 856 // Using [new Future] adds an asynchronous operation that works around the | |
| 857 // first and second cases described above. | |
| 858 newFuture(fn).then((val) { | |
| 859 scheduleMicrotask(() => completer.complete(val)); | |
| 860 }).catchError((err, stackTrace) { | |
| 861 scheduleMicrotask(() => completer.completeError(err, stackTrace)); | |
| 862 }); | |
| 863 return completer.future; | |
| 864 } | |
| 865 | |
| 866 /// The subset of strings that don't need quoting in YAML. | |
| 867 /// | |
| 868 /// This pattern does not strictly follow the plain scalar grammar of YAML, | |
| 869 /// which means some strings may be unnecessarily quoted, but it's much simpler. | |
| 870 final _unquotableYamlString = new RegExp(r"^[a-zA-Z_-][a-zA-Z_0-9-]*$"); | |
| 871 | |
| 872 /// Converts [data], which is a parsed YAML object, to a pretty-printed string, | |
| 873 /// using indentation for maps. | |
| 874 String yamlToString(data) { | |
| 875 var buffer = new StringBuffer(); | |
| 876 | |
| 877 _stringify(bool isMapValue, String indent, data) { | |
| 878 // TODO(nweiz): Serialize using the YAML library once it supports | |
| 879 // serialization. | |
| 880 | |
| 881 // Use indentation for (non-empty) maps. | |
| 882 if (data is Map && !data.isEmpty) { | |
| 883 if (isMapValue) { | |
| 884 buffer.writeln(); | |
| 885 indent += ' '; | |
| 886 } | |
| 887 | |
| 888 // Sort the keys. This minimizes deltas in diffs. | |
| 889 var keys = data.keys.toList(); | |
| 890 keys.sort((a, b) => a.toString().compareTo(b.toString())); | |
| 891 | |
| 892 var first = true; | |
| 893 for (var key in keys) { | |
| 894 if (!first) buffer.writeln(); | |
| 895 first = false; | |
| 896 | |
| 897 var keyString = key; | |
| 898 if (key is! String || !_unquotableYamlString.hasMatch(key)) { | |
| 899 keyString = JSON.encode(key); | |
| 900 } | |
| 901 | |
| 902 buffer.write('$indent$keyString:'); | |
| 903 _stringify(true, indent, data[key]); | |
| 904 } | |
| 905 | |
| 906 return; | |
| 907 } | |
| 908 | |
| 909 // Everything else we just stringify using JSON to handle escapes in | |
| 910 // strings and number formatting. | |
| 911 var string = data; | |
| 912 | |
| 913 // Don't quote plain strings if not needed. | |
| 914 if (data is! String || !_unquotableYamlString.hasMatch(data)) { | |
| 915 string = JSON.encode(data); | |
| 916 } | |
| 917 | |
| 918 if (isMapValue) { | |
| 919 buffer.write(' $string'); | |
| 920 } else { | |
| 921 buffer.write('$indent$string'); | |
| 922 } | |
| 923 } | |
| 924 | |
| 925 _stringify(false, '', data); | |
| 926 return buffer.toString(); | |
| 927 } | |
| 928 | |
| 929 /// Throw a [ApplicationException] with [message]. | |
| 930 void fail(String message, [innerError, StackTrace innerTrace]) { | |
| 931 if (innerError != null) { | |
| 932 throw new WrappedException(message, innerError, innerTrace); | |
| 933 } else { | |
| 934 throw new ApplicationException(message); | |
| 935 } | |
| 936 } | |
| 937 | |
| 938 /// Throw a [DataException] with [message] to indicate that the command has | |
| 939 /// failed because of invalid input data. | |
| 940 /// | |
| 941 /// This will report the error and cause pub to exit with [exit_codes.DATA]. | |
| 942 void dataError(String message) => throw new DataException(message); | |
| OLD | NEW |