Add a PathSet class
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3dd398c..61e1788 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,8 @@
* Add a `PathMap` class that uses path equality for its keys.
+* Add a `PathSet` class that uses path equality for its contents.
+
## 1.5.1
* Fix a number of bugs that occurred when the current working directory was `/`
diff --git a/lib/path.dart b/lib/path.dart
index 7348a34..ed70f9d 100644
--- a/lib/path.dart
+++ b/lib/path.dart
@@ -50,6 +50,7 @@
export 'src/context.dart' hide createInternal;
export 'src/path_exception.dart';
export 'src/path_map.dart';
+export 'src/path_set.dart';
export 'src/style.dart';
/// A default context for manipulating POSIX paths.
diff --git a/lib/src/path_set.dart b/lib/src/path_set.dart
new file mode 100644
index 0000000..98f08a9
--- /dev/null
+++ b/lib/src/path_set.dart
@@ -0,0 +1,83 @@
+// Copyright (c) 2018, 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.
+
+import 'dart:collection';
+
+import '../path.dart' as p;
+
+/// A set containing paths, compared using [equals] and [hash].
+class PathSet extends IterableBase<String> implements Set<String> {
+ /// The set to which we forward implementation methods.
+ final Set<String> _inner;
+
+ /// Creates an empty [PathSet] whose contents are compared using
+ /// `context.equals` and `context.hash`.
+ ///
+ /// The [context] defaults to the current path context.
+ PathSet({p.Context context}) : _inner = _create(context);
+
+ /// Creates a [PathSet] with the same contents as [other] whose elements are
+ /// compared using `context.equals` and `context.hash`.
+ ///
+ /// The [context] defaults to the current path context. If multiple elements
+ /// in [other] represent the same logical path, the first value will be
+ /// used.
+ PathSet.of(Iterable<String> other, {p.Context context})
+ : _inner = _create(context)..addAll(other);
+
+ /// Creates a set that uses [context] for equality and hashing.
+ static Set<String> _create(p.Context context) {
+ context ??= p.context;
+ return new LinkedHashSet(
+ equals: (path1, path2) {
+ if (path1 == null) return path2 == null;
+ if (path2 == null) return false;
+ return context.equals(path1, path2);
+ },
+ hashCode: (path) => path == null ? 0 : context.hash(path),
+ isValidKey: (path) => path is String || path == null);
+ }
+
+ // Normally we'd use DelegatingSetView from the collection package to
+ // implement these, but we want to avoid adding dependencies from path because
+ // it's so widely used that even brief version skew can be very painful.
+
+ Iterator<String> get iterator => _inner.iterator;
+
+ int get length => _inner.length;
+
+ bool add(String value) => _inner.add(value);
+
+ void addAll(Iterable<String> elements) => _inner.addAll(elements);
+
+ Set<T> cast<T>() => _inner.cast<T>();
+
+ void clear() => _inner.clear();
+
+ bool contains(Object other) => _inner.contains(other);
+
+ bool containsAll(Iterable<Object> other) => _inner.containsAll(other);
+
+ Set<String> difference(Set<Object> other) => _inner.difference(other);
+
+ Set<String> intersection(Set<Object> other) => _inner.intersection(other);
+
+ String lookup(Object element) => _inner.lookup(element);
+
+ bool remove(Object value) => _inner.remove(value);
+
+ void removeAll(Iterable<Object> elements) => _inner.removeAll(elements);
+
+ void removeWhere(bool test(String element)) => _inner.removeWhere(test);
+
+ void retainAll(Iterable<Object> elements) => _inner.retainAll(elements);
+
+ Set<T> retype<T>() => _inner.retype<T>();
+
+ void retainWhere(bool test(String element)) => _inner.retainWhere(test);
+
+ Set<String> union(Set<String> other) => _inner.union(other);
+
+ Set<String> toSet() => _inner.toSet();
+}
diff --git a/test/path_set_test.dart b/test/path_set_test.dart
new file mode 100644
index 0000000..884c184
--- /dev/null
+++ b/test/path_set_test.dart
@@ -0,0 +1,81 @@
+// Copyright (c) 2018, 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.
+
+import 'package:test/test.dart';
+
+import 'package:path/path.dart';
+
+void main() {
+ group("considers equal", () {
+ test("two identical paths", () {
+ var set = new PathSet();
+ expect(set.add(join("foo", "bar")), isTrue);
+ expect(set.add(join("foo", "bar")), isFalse);
+ expect(set, hasLength(1));
+ expect(set, contains(join("foo", "bar")));
+ });
+
+ test("two logically equivalent paths", () {
+ var set = new PathSet();
+ expect(set.add("foo"), isTrue);
+ expect(set.add(absolute("foo")), isFalse);
+ expect(set, hasLength(1));
+ expect(set, contains("foo"));
+ expect(set, contains(absolute("foo")));
+ });
+
+ test("two nulls", () {
+ var set = new PathSet();
+ expect(set.add(null), isTrue);
+ expect(set.add(null), isFalse);
+ expect(set, hasLength(1));
+ expect(set, contains(null));
+ });
+ });
+
+ group("considers unequal", () {
+ test("two distinct paths", () {
+ var set = new PathSet();
+ expect(set.add("foo"), isTrue);
+ expect(set.add("bar"), isTrue);
+ expect(set, hasLength(2));
+ expect(set, contains("foo"));
+ expect(set, contains("bar"));
+ });
+
+ test("a path and null", () {
+ var set = new PathSet();
+ expect(set.add("foo"), isTrue);
+ expect(set.add(null), isTrue);
+ expect(set, hasLength(2));
+ expect(set, contains("foo"));
+ expect(set, contains(null));
+ });
+ });
+
+ test("uses the custom context", () {
+ var set = new PathSet(context: windows);
+ expect(set.add("FOO"), isTrue);
+ expect(set.add("foo"), isFalse);
+ expect(set, hasLength(1));
+ expect(set, contains("fOo"));
+ });
+
+ group(".of()", () {
+ test("copies the existing set's keys", () {
+ var set = new PathSet.of(["foo", "bar"]);
+ expect(set, hasLength(2));
+ expect(set, contains("foo"));
+ expect(set, contains("bar"));
+ });
+
+ test("uses the first value in the case of duplicates", () {
+ var set = new PathSet.of(["foo", absolute("foo")]);
+ expect(set, hasLength(1));
+ expect(set, contains("foo"));
+ expect(set, contains(absolute("foo")));
+ expect(set.first, "foo");
+ });
+ });
+}