Merge pull request #40 from dart-lang/path-map

Add PathMap and PathSet classes
diff --git a/.travis.yml b/.travis.yml
index 63f5e5e..140ce59 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -2,16 +2,17 @@
 
 dart:
   - dev
-  - stable
 
 dart_task:
   - test
-  - dartanalyzer
 
 matrix:
   include:
     - dart: dev
       dart_task: dartfmt
+  include:
+    - dart: dev
+      dart_task: dartanalyzer
 
 # Only building master means that we don't run two builds for each pull request.
 branches:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e23e380..61e1788 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,9 @@
+## 1.6.0
+
+* 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 9885859..ed70f9d 100644
--- a/lib/path.dart
+++ b/lib/path.dart
@@ -49,6 +49,8 @@
 
 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_map.dart b/lib/src/path_map.dart
new file mode 100644
index 0000000..53205ad
--- /dev/null
+++ b/lib/src/path_map.dart
@@ -0,0 +1,38 @@
+// 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 map whose keys are paths, compared using [equals] and [hash].
+class PathMap<V> extends MapView<String, V> {
+  /// Creates an empty [PathMap] whose keys are compared using `context.equals`
+  /// and `context.hash`.
+  ///
+  /// The [context] defaults to the current path context.
+  PathMap({p.Context context}) : super(_create(context));
+
+  /// Creates a [PathMap] with the same keys and values as [other] whose keys
+  /// are compared using `context.equals` and `context.hash`.
+  ///
+  /// The [context] defaults to the current path context. If multiple keys in
+  /// [other] represent the same logical path, the last key's value will be
+  /// used.
+  PathMap.of(Map<String, V> other, {p.Context context})
+      : super(_create(context)..addAll(other));
+
+  /// Creates a map that uses [context] for equality and hashing.
+  static Map<String, V> _create<V>(p.Context context) {
+    context ??= p.context;
+    return new LinkedHashMap(
+        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);
+  }
+}
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/pubspec.yaml b/pubspec.yaml
index 30a8dff..a9a4cdb 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: path
-version: 1.5.1
+version: 1.6.0
 author: Dart Team <misc@dartlang.org>
 description: >
  A string-based path manipulation library. All of the path operations you know
@@ -9,4 +9,4 @@
 dev_dependencies:
   test: ">=0.12.0 <0.13.0"
 environment:
-  sdk: ">=1.0.0 <2.0.0"
+  sdk: ">=2.0.0-dev.35.0 <2.0.0"
diff --git a/test/path_map_test.dart b/test/path_map_test.dart
new file mode 100644
index 0000000..ce025db
--- /dev/null
+++ b/test/path_map_test.dart
@@ -0,0 +1,80 @@
+// 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 map = new PathMap<int>();
+      map[join("foo", "bar")] = 1;
+      map[join("foo", "bar")] = 2;
+      expect(map, hasLength(1));
+      expect(map, containsPair(join("foo", "bar"), 2));
+    });
+
+    test("two logically equivalent paths", () {
+      var map = new PathMap<int>();
+      map["foo"] = 1;
+      map[absolute("foo")] = 2;
+      expect(map, hasLength(1));
+      expect(map, containsPair("foo", 2));
+      expect(map, containsPair(absolute("foo"), 2));
+    });
+
+    test("two nulls", () {
+      var map = new PathMap<int>();
+      map[null] = 1;
+      map[null] = 2;
+      expect(map, hasLength(1));
+      expect(map, containsPair(null, 2));
+    });
+  });
+
+  group("considers unequal", () {
+    test("two distinct paths", () {
+      var map = new PathMap<int>();
+      map["foo"] = 1;
+      map["bar"] = 2;
+      expect(map, hasLength(2));
+      expect(map, containsPair("foo", 1));
+      expect(map, containsPair("bar", 2));
+    });
+
+    test("a path and null", () {
+      var map = new PathMap<int>();
+      map["foo"] = 1;
+      map[null] = 2;
+      expect(map, hasLength(2));
+      expect(map, containsPair("foo", 1));
+      expect(map, containsPair(null, 2));
+    });
+  });
+
+  test("uses the custom context", () {
+    var map = new PathMap<int>(context: windows);
+    map["FOO"] = 1;
+    map["foo"] = 2;
+    expect(map, hasLength(1));
+    expect(map, containsPair("fOo", 2));
+  });
+
+  group(".of()", () {
+    test("copies the existing map's keys", () {
+      var map = new PathMap.of({"foo": 1, "bar": 2});
+      expect(map, hasLength(2));
+      expect(map, containsPair("foo", 1));
+      expect(map, containsPair("bar", 2));
+    });
+
+    test("uses the second value in the case of duplicates", () {
+      var map = new PathMap.of({"foo": 1, absolute("foo"): 2});
+      expect(map, hasLength(1));
+      expect(map, containsPair("foo", 2));
+      expect(map, containsPair(absolute("foo"), 2));
+    });
+  });
+}
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");
+    });
+  });
+}