Fix watching when path has a trailing slash. (#2294)

diff --git a/pkgs/watcher/CHANGELOG.md b/pkgs/watcher/CHANGELOG.md
index ab621ca..0d64856 100644
--- a/pkgs/watcher/CHANGELOG.md
+++ b/pkgs/watcher/CHANGELOG.md
@@ -1,3 +1,12 @@
+## 1.2.1-wip
+
+- Bug fix: versions before 1.2.0 would allow and ignore a trailing path
+  separator passed to `DirectoryWatcher` or `FileWatcher` constructors, restore
+  that behavior.
+- In paths passed to `DirectoryWatcher` or `FileWatcher` constructors, remove
+  multiple adjacent separators and `.` and `..`, so they will not be returned in
+  events.
+
 ## 1.2.0
 
 - Polling watchers now check file sizes as well as "last modified" times, so
diff --git a/pkgs/watcher/lib/src/directory_watcher/linux/linux_directory_watcher.dart b/pkgs/watcher/lib/src/directory_watcher/linux/linux_directory_watcher.dart
index eb93cc2..c24b390 100644
--- a/pkgs/watcher/lib/src/directory_watcher/linux/linux_directory_watcher.dart
+++ b/pkgs/watcher/lib/src/directory_watcher/linux/linux_directory_watcher.dart
@@ -17,7 +17,7 @@
   String get directory => path;
 
   LinuxDirectoryWatcher(String directory)
-      : super(directory, () => _LinuxDirectoryWatcher(directory));
+      : super(directory, _LinuxDirectoryWatcher.new);
 }
 
 /// Linux directory watcher that watches using [WatchTreeRoot].
diff --git a/pkgs/watcher/lib/src/directory_watcher/polling/polling_directory_watcher.dart b/pkgs/watcher/lib/src/directory_watcher/polling/polling_directory_watcher.dart
index 835a0b5..4beb556 100644
--- a/pkgs/watcher/lib/src/directory_watcher/polling/polling_directory_watcher.dart
+++ b/pkgs/watcher/lib/src/directory_watcher/polling/polling_directory_watcher.dart
@@ -28,9 +28,9 @@
   /// shorter will give more immediate feedback at the expense of doing more IO
   /// and higher CPU usage. Defaults to one second.
   PollingDirectoryWatcher(String directory, {Duration? pollingDelay})
-      : super(directory, () {
+      : super(directory, (path) {
           return _PollingDirectoryWatcher(
-              directory, pollingDelay ?? const Duration(seconds: 1));
+              path, pollingDelay ?? const Duration(seconds: 1));
         });
 }
 
diff --git a/pkgs/watcher/lib/src/directory_watcher/recursive/recursive_directory_watcher.dart b/pkgs/watcher/lib/src/directory_watcher/recursive/recursive_directory_watcher.dart
index 3971f34..6c4bf5b 100644
--- a/pkgs/watcher/lib/src/directory_watcher/recursive/recursive_directory_watcher.dart
+++ b/pkgs/watcher/lib/src/directory_watcher/recursive/recursive_directory_watcher.dart
@@ -25,9 +25,9 @@
   RecursiveDirectoryWatcher(String directory, {required bool runInIsolate})
       : super(
             directory,
-            () => runInIsolate
-                ? IsolateRecursiveDirectoryWatcher(directory)
-                : ManuallyClosedRecursiveDirectoryWatcher(directory));
+            (path) => runInIsolate
+                ? IsolateRecursiveDirectoryWatcher(path)
+                : ManuallyClosedRecursiveDirectoryWatcher(path));
 }
 
 /// Manually closed directory watcher that watches using [WatchedDirectoryTree].
diff --git a/pkgs/watcher/lib/src/file_watcher/native.dart b/pkgs/watcher/lib/src/file_watcher/native.dart
index 75efb76..9862092 100644
--- a/pkgs/watcher/lib/src/file_watcher/native.dart
+++ b/pkgs/watcher/lib/src/file_watcher/native.dart
@@ -16,7 +16,7 @@
 /// Single-file notifications are much simpler than those for multiple files, so
 /// this doesn't need to be split out into multiple OS-specific classes.
 class NativeFileWatcher extends ResubscribableWatcher implements FileWatcher {
-  NativeFileWatcher(String path) : super(path, () => _NativeFileWatcher(path));
+  NativeFileWatcher(String path) : super(path, _NativeFileWatcher.new);
 }
 
 class _NativeFileWatcher implements FileWatcher, ManuallyClosedWatcher {
diff --git a/pkgs/watcher/lib/src/file_watcher/polling.dart b/pkgs/watcher/lib/src/file_watcher/polling.dart
index e87649d..3b4abc2 100644
--- a/pkgs/watcher/lib/src/file_watcher/polling.dart
+++ b/pkgs/watcher/lib/src/file_watcher/polling.dart
@@ -13,7 +13,7 @@
 /// Periodically polls a file for changes.
 class PollingFileWatcher extends ResubscribableWatcher implements FileWatcher {
   PollingFileWatcher(String path, {Duration? pollingDelay})
-      : super(path, () {
+      : super(path, (path) {
           return _PollingFileWatcher(
               path, pollingDelay ?? const Duration(seconds: 1));
         });
diff --git a/pkgs/watcher/lib/src/resubscribable.dart b/pkgs/watcher/lib/src/resubscribable.dart
index a2c37f8..b67cd32 100644
--- a/pkgs/watcher/lib/src/resubscribable.dart
+++ b/pkgs/watcher/lib/src/resubscribable.dart
@@ -4,6 +4,8 @@
 
 import 'dart:async';
 
+import 'package:path/path.dart' as p;
+
 import '../watcher.dart';
 
 /// A wrapper for [ManuallyClosedWatcher] that encapsulates support for closing
@@ -21,7 +23,7 @@
 /// takes a factory function that produces instances of the inner class.
 abstract class ResubscribableWatcher implements Watcher {
   /// The factory function that produces instances of the inner class.
-  final ManuallyClosedWatcher Function() _factory;
+  final ManuallyClosedWatcher Function(String path) _factory;
 
   @override
   final String path;
@@ -39,14 +41,15 @@
 
   /// Creates a new [ResubscribableWatcher] wrapping the watchers
   /// emitted by [_factory].
-  ResubscribableWatcher(this.path, this._factory) {
+  ResubscribableWatcher(String path, this._factory)
+      : path = _normalizeAndStripTrailingSeparator(path) {
     late ManuallyClosedWatcher watcher;
     late StreamSubscription<WatchEvent> subscription;
 
     _eventsController = StreamController<WatchEvent>.broadcast(
         onListen: () async {
           final completer = _readyCompleter;
-          watcher = _factory();
+          watcher = _factory(this.path);
           subscription = watcher.events.listen(_eventsController.add,
               onError: _eventsController.addError,
               onDone: _eventsController.close);
@@ -78,3 +81,13 @@
   /// resources.
   void close();
 }
+
+/// Normalizes [path] then strips any extra trailing separator.
+///
+/// Removes adjacent separators, `.` and `..` but does not convert to absolute.
+String _normalizeAndStripTrailingSeparator(String path) {
+  path = p.normalize(path);
+  return path.length > 1 && path.endsWith(p.separator)
+      ? path.substring(0, path.length - 1)
+      : path;
+}
diff --git a/pkgs/watcher/pubspec.yaml b/pkgs/watcher/pubspec.yaml
index 0f2be5b..5faf63b 100644
--- a/pkgs/watcher/pubspec.yaml
+++ b/pkgs/watcher/pubspec.yaml
@@ -1,5 +1,5 @@
 name: watcher
-version: 1.2.0
+version: 1.2.1-wip
 description: >-
   A file system watcher. It monitors changes to contents of directories and
   sends notifications when files have been added, removed, or modified.
diff --git a/pkgs/watcher/test/directory_watcher/file_tests.dart b/pkgs/watcher/test/directory_watcher/file_tests.dart
index da9a1e9..4f4b26d 100644
--- a/pkgs/watcher/test/directory_watcher/file_tests.dart
+++ b/pkgs/watcher/test/directory_watcher/file_tests.dart
@@ -112,6 +112,25 @@
     expect(await queue3HasNext, false);
   });
 
+  // Regression test for https://github.com/dart-lang/tools/issues/2293.
+  test('works with trailing path separator', () async {
+    await startWatcher(exactPath: '${d.sandbox}${Platform.pathSeparator}');
+
+    writeFile('a.txt');
+    await expectAddEvent('a.txt');
+  });
+
+  test('normalizes many adjacent separators and ..', () async {
+    createDir('a');
+    final separator = Platform.pathSeparator;
+    await startWatcher(
+        exactPath:
+            '${d.sandbox}${separator * 5}a${separator * 4}b${separator * 3}..');
+
+    writeFile('a/a.txt');
+    await expectAddEvent('a/a.txt');
+  });
+
   test('does not notify for files that already exist when started', () async {
     // Make some pre-existing files.
     writeFile('a.txt');