Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(181)

Side by Side Diff: sdk/lib/_internal/pub/lib/src/utils.dart

Issue 1165473002: Start pulling pub from its own repo. (Closed) Base URL: git@github.com:dart-lang/sdk.git@master
Patch Set: Created 5 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(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);
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698