Don't throw for missing directories (#60)

Closes #59

Treated as non-breaking because it only turns errors into non-errors.

Ignore all missing directory errors from the filesystem, instead of only
ignoring those which come from a directory operation after the first
wildcard.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7dd6382..dbef334 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 2.1.0-dev
+
+* Return empty results instead of throwing when trying to list a path that does
+  not exist.
+
 ## 2.0.2
 
 * Drop package:pedantic dependency, use package:lints instead.
diff --git a/lib/src/list_tree.dart b/lib/src/list_tree.dart
index ece6f09..2a6c020 100644
--- a/lib/src/list_tree.dart
+++ b/lib/src/list_tree.dart
@@ -319,6 +319,7 @@
       return fileSystem
           .directory(dir)
           .list(recursive: true, followLinks: followLinks)
+          .ignoreMissing()
           .where((entity) => _matches(p.relative(entity.path, from: dir)));
     }
 
@@ -340,6 +341,7 @@
       var entities = await fileSystem
           .directory(dir)
           .list(followLinks: followLinks)
+          .ignoreMissing()
           .toList();
       await _validateIntermediateChildrenAsync(dir, entities, fileSystem);
 
@@ -353,17 +355,8 @@
         children!.forEach((sequence, child) {
           if (entity is! Directory) return;
           if (!sequence.matches(basename)) return;
-          var stream = child
-              .list(p.join(dir, basename), fileSystem, followLinks: followLinks)
-              .handleError((_) {}, test: (error) {
-            // Ignore errors from directories not existing. We do this here so
-            // that we only ignore warnings below wild cards. For example, the
-            // glob "foo/bar/*/baz" should fail if "foo/bar" doesn't exist but
-            // succeed if "foo/bar/qux/baz" doesn't exist.
-            return error is FileSystemException &&
-                (error.osError!.errorCode == _enoent ||
-                    error.osError!.errorCode == _enoentWin);
-          });
+          var stream = child.list(p.join(dir, basename), fileSystem,
+              followLinks: followLinks);
           resultGroup.add(stream);
         });
       }
@@ -409,10 +402,15 @@
   Iterable<FileSystemEntity> listSync(String dir, FileSystem fileSystem,
       {bool followLinks = true}) {
     if (isRecursive) {
-      return fileSystem
-          .directory(dir)
-          .listSync(recursive: true, followLinks: followLinks)
-          .where((entity) => _matches(p.relative(entity.path, from: dir)));
+      try {
+        return fileSystem
+            .directory(dir)
+            .listSync(recursive: true, followLinks: followLinks)
+            .where((entity) => _matches(p.relative(entity.path, from: dir)));
+      } on FileSystemException catch (error) {
+        if (error.isMissing) return const [];
+        rethrow;
+      }
     }
 
     // Don't spawn extra [Directory.listSync] calls when we already know exactly
@@ -428,7 +426,13 @@
       });
     }
 
-    var entities = fileSystem.directory(dir).listSync(followLinks: followLinks);
+    List<FileSystemEntity> entities;
+    try {
+      entities = fileSystem.directory(dir).listSync(followLinks: followLinks);
+    } on FileSystemException catch (error) {
+      if (error.isMissing) return const [];
+      rethrow;
+    }
     _validateIntermediateChildrenSync(dir, entities, fileSystem);
 
     return entities.expand((entity) {
@@ -440,23 +444,10 @@
       entities.addAll(children!.keys
           .where((sequence) => sequence.matches(basename))
           .expand((sequence) {
-        try {
-          return children![sequence]!
-              .listSync(p.join(dir, basename), fileSystem,
-                  followLinks: followLinks)
-              .toList();
-        } on FileSystemException catch (error) {
-          // Ignore errors from directories not existing. We do this here so
-          // that we only ignore warnings below wild cards. For example, the
-          // glob "foo/bar/*/baz" should fail if "foo/bar" doesn't exist but
-          // succeed if "foo/bar/qux/baz" doesn't exist.
-          if (error.osError!.errorCode == _enoent ||
-              error.osError!.errorCode == _enoentWin) {
-            return const [];
-          } else {
-            rethrow;
-          }
-        }
+        return children![sequence]!
+            .listSync(p.join(dir, basename), fileSystem,
+                followLinks: followLinks)
+            .toList();
       }));
 
       return entities;
@@ -509,3 +500,15 @@
   }
   return SequenceNode(nodes, caseSensitive: first.caseSensitive);
 }
+
+extension on Stream<FileSystemEntity> {
+  Stream<FileSystemEntity> ignoreMissing() => handleError((_) {},
+      test: (error) => error is FileSystemException && error.isMissing);
+}
+
+extension on FileSystemException {
+  bool get isMissing {
+    final errorCode = osError?.errorCode;
+    return errorCode == _enoent || errorCode == _enoentWin;
+  }
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index 728c0d3..37a37fd 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: glob
-version: 2.0.2
+version: 2.1.0-dev
 
 description: Bash-style filename globbing.
 repository: https://github.com/dart-lang/glob
diff --git a/test/list_test.dart b/test/list_test.dart
index 512d901..5714f25 100644
--- a/test/list_test.dart
+++ b/test/list_test.dart
@@ -26,15 +26,17 @@
       expect(Glob('*', context: p.url).list, throwsStateError);
     });
 
-    test('reports exceptions for non-existent case-sensitive directories', () {
-      expect(Glob('non/existent/**', caseSensitive: true).list().toList(),
-          throwsA(isA<FileSystemException>()));
+    test('returns empty list for non-existent case-sensitive directories',
+        () async {
+      expect(await Glob('non/existent/**', caseSensitive: true).list().toList(),
+          []);
     });
 
-    test('reports exceptions for non-existent case-insensitive directories',
-        () {
-      expect(Glob('non/existent/**', caseSensitive: false).list().toList(),
-          throwsA(isA<FileSystemException>()));
+    test('returns empty list for non-existent case-insensitive directories',
+        () async {
+      expect(
+          await Glob('non/existent/**', caseSensitive: false).list().toList(),
+          []);
     });
   });
 
@@ -43,32 +45,27 @@
       expect(Glob('*', context: p.url).listSync, throwsStateError);
     });
 
-    test('reports exceptions for non-existent case-sensitive directories', () {
-      expect(Glob('non/existent/**', caseSensitive: true).listSync,
-          throwsA(isA<FileSystemException>()));
+    test('returns empty list for non-existent case-sensitive directories', () {
+      expect(Glob('non/existent/**', caseSensitive: true).listSync(), []);
     });
 
-    test('reports exceptions for non-existent case-insensitive directories',
+    test('returns empty list for non-existent case-insensitive directories',
         () {
-      expect(Glob('non/existent/**', caseSensitive: false).listSync,
-          throwsA(isA<FileSystemException>()));
+      expect(Glob('non/existent/**', caseSensitive: false).listSync(), []);
     });
   });
 
   group('when case-sensitive', () {
     test('lists literals case-sensitively', () {
-      expect(Glob('foo/BAZ/qux', caseSensitive: true).listSync,
-          throwsA(isA<FileSystemException>()));
+      expect(Glob('foo/BAZ/qux', caseSensitive: true).listSync(), []);
     });
 
     test('lists ranges case-sensitively', () {
-      expect(Glob('foo/[BX][A-Z]z/qux', caseSensitive: true).listSync,
-          throwsA(isA<FileSystemException>()));
+      expect(Glob('foo/[BX][A-Z]z/qux', caseSensitive: true).listSync(), []);
     });
 
     test('options preserve case-sensitivity', () {
-      expect(Glob('foo/{BAZ,ZAP}/qux', caseSensitive: true).listSync,
-          throwsA(isA<FileSystemException>()));
+      expect(Glob('foo/{BAZ,ZAP}/qux', caseSensitive: true).listSync(), []);
     });
   });