[analysis_server] Remove pendingFilesToRemoveOverlay when cancelling timer

This prevents an issue where the overlay can be removed unexpectedly because the watch event sees the old (cancelled) timer, and believes there is a pending removal, so removes it.

Change-Id: Ib193ba4d6b4ba3d55a0d793554a19476c8f40e47
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/238087
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Konstantin Shcheglov <scheglov@google.com>
diff --git a/pkg/analysis_server/lib/src/analysis_server.dart b/pkg/analysis_server/lib/src/analysis_server.dart
index 0279042..2aec83b 100644
--- a/pkg/analysis_server/lib/src/analysis_server.dart
+++ b/pkg/analysis_server/lib/src/analysis_server.dart
@@ -569,10 +569,11 @@
                   'Invalid overlay change')));
         }
       } else if (change is RemoveContentOverlay) {
-        _pendingFilesToRemoveOverlay[file]?.cancel();
+        _pendingFilesToRemoveOverlay.remove(file)?.cancel();
         _pendingFilesToRemoveOverlay[file] = Timer(
           pendingFilesRemoveOverlayDelay,
           () {
+            _pendingFilesToRemoveOverlay.remove(file);
             resourceProvider.removeOverlay(file);
             _changeFileInDrivers(file);
           },
@@ -583,7 +584,7 @@
         throw AnalysisException('Illegal change type');
       }
 
-      _pendingFilesToRemoveOverlay[file]?.cancel();
+      _pendingFilesToRemoveOverlay.remove(file)?.cancel();
       resourceProvider.setOverlay(
         file,
         content: newContents,
diff --git a/pkg/analysis_server/test/domain_analysis_test.dart b/pkg/analysis_server/test/domain_analysis_test.dart
index 776c72d..5c2c45e 100644
--- a/pkg/analysis_server/test/domain_analysis_test.dart
+++ b/pkg/analysis_server/test/domain_analysis_test.dart
@@ -884,6 +884,62 @@
     assertNoErrorsNotification(a_path);
   }
 
+  /// This test ensures that when an `addOverlay` cancels any pending
+  /// `removeOverlay` timer, it also removes it, so that a subsequent watch
+  /// event does not still try to process it.
+  Future<void>
+      test_fileSystem_removeOverlay_addOverlay_changeFile_changeOverlay() async {
+    // Use long delay, so that it does not happen.
+    server.pendingFilesRemoveOverlayDelay = const Duration(seconds: 300);
+
+    newFile2(testFilePath, '');
+
+    // Add an overlay without errors.
+    await handleSuccessfulRequest(
+      AnalysisUpdateContentParams({
+        testFilePathPlatform: AddContentOverlay(''),
+      }).toRequest('0'),
+    );
+
+    await setRoots(included: [workspaceRootPath], excluded: []);
+
+    // The test file (overlay) is analyzed, no errors.
+    await _waitAnalysisComplete();
+    _assertAnalyzedFiles(
+      hasErrors: [],
+      noErrors: [testFilePathPlatform],
+      notAnalyzed: [],
+    );
+
+    // Ask to remove the overlay, still active, start a timer.
+    await handleSuccessfulRequest(
+      AnalysisUpdateContentParams({
+        testFilePathPlatform: RemoveContentOverlay(),
+      }).toRequest('0'),
+    );
+
+    // Re-add an overlay. Should cancel the timer and replace the overlay.
+    await handleSuccessfulRequest(
+      AnalysisUpdateContentParams({
+        testFilePathPlatform: AddContentOverlay(''),
+      }).toRequest('0'),
+    );
+
+    // Change the file to trigger the watcher. Since the request above should
+    // have cancelled (and removed) the timer, this should not do anything
+    // (specifically, it should not remove the new overlay).
+    modifyFile(testFilePath, 'error');
+
+    // The overlay should still be present, so we should be able to change it.
+    await handleSuccessfulRequest(
+      AnalysisUpdateContentParams({
+        testFilePathPlatform: ChangeContentOverlay(
+          [SourceEdit(0, 0, '//')],
+        ),
+      }).toRequest('0'),
+    );
+  }
+
   Future<void> test_setPriorityFiles() async {
     var a = getFile('$workspaceRootPath/foo/lib/a.dart');
     var b = getFile('$workspaceRootPath/foo/lib/b.dart');