Issue 47804. Recompute invlaidated libraries after processing all changed files.

The issue has the discussion.

Bug: https://github.com/dart-lang/sdk/issues/47804
Change-Id: I301fe8fe44b0e8e7e3b413df0363fabc44061c7d
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/221564
Reviewed-by: Samuel Rawlins <srawlins@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Konstantin Shcheglov <scheglov@google.com>
diff --git a/pkg/analyzer/lib/src/services/available_declarations.dart b/pkg/analyzer/lib/src/services/available_declarations.dart
index 588b150..b5a3097 100644
--- a/pkg/analyzer/lib/src/services/available_declarations.dart
+++ b/pkg/analyzer/lib/src/services/available_declarations.dart
@@ -504,6 +504,10 @@
   /// The list of changed file paths.
   final List<String> _changedPaths = [];
 
+  /// While processing [_changedPaths] we accumulate libraries here.
+  /// Then we process all of them at once, and reset to the empty set.
+  Set<_File> _invalidatedLibraries = {};
+
   /// The list of files scheduled for processing.  It may include parts and
   /// libraries, but parts are ignored when we detect them.
   final List<_ScheduledFile> _scheduledFiles = [];
@@ -529,7 +533,9 @@
       _whenKnownFilesPulled = now;
       pullKnownFiles();
     }
-    return _changedPaths.isNotEmpty || _scheduledFiles.isNotEmpty;
+    return _changedPaths.isNotEmpty ||
+        _invalidatedLibraries.isNotEmpty ||
+        _scheduledFiles.isNotEmpty;
   }
 
   /// Add the [analysisContext], so that its libraries are reported via the
@@ -595,6 +601,13 @@
       return;
     }
 
+    // There are no more changes as far as we know.
+    // So, recompute exported declarations for all invalidated libraries.
+    if (_invalidatedLibraries.isNotEmpty) {
+      _processInvalidatedLibraries(_invalidatedLibraries);
+      _invalidatedLibraries = {};
+    }
+
     if (_scheduledFiles.isNotEmpty) {
       var scheduledFile = _scheduledFiles.removeLast();
       var file = _getFileByPath(scheduledFile.context, [], scheduledFile.path)!;
@@ -794,11 +807,33 @@
         _invalidateExportedDeclarations(invalidatedLibraries, newLibrary);
       }
     }
-    _computeExportedDeclarations(invalidatedLibraries);
 
-    var changedLibraries = <Library>[];
+    // Don't compute exported declarations now, there might be more changes.
+    // Instead, accumulate invalidated libraries, and recompute all later.
+    _invalidatedLibraries.addAll(invalidatedLibraries);
+
     var removedLibraries = <int>[];
     for (var libraryFile in invalidatedLibraries) {
+      if (!libraryFile.exists) {
+        _idToLibrary.remove(libraryFile.id);
+        removedLibraries.add(libraryFile.id);
+      }
+    }
+    for (var file in notLibraries) {
+      _idToLibrary.remove(file.id);
+      removedLibraries.add(file.id);
+    }
+    if (removedLibraries.isNotEmpty) {
+      _changesController.add(
+        LibraryChange._([], removedLibraries),
+      );
+    }
+  }
+
+  void _processInvalidatedLibraries(Set<_File> invalidatedLibraries) {
+    _computeExportedDeclarations(invalidatedLibraries);
+    var changedLibraries = <Library>[];
+    for (var libraryFile in invalidatedLibraries) {
       if (libraryFile.exists) {
         var library = Library._(
           libraryFile.id,
@@ -809,17 +844,10 @@
         );
         _idToLibrary[library.id] = library;
         changedLibraries.add(library);
-      } else {
-        _idToLibrary.remove(libraryFile.id);
-        removedLibraries.add(libraryFile.id);
       }
     }
-    for (var file in notLibraries) {
-      _idToLibrary.remove(file.id);
-      removedLibraries.add(file.id);
-    }
     _changesController.add(
-      LibraryChange._(changedLibraries, removedLibraries),
+      LibraryChange._(changedLibraries, []),
     );
   }
 
diff --git a/pkg/analyzer/test/src/services/available_declarations_test.dart b/pkg/analyzer/test/src/services/available_declarations_test.dart
index 17908dd..357ca89 100644
--- a/pkg/analyzer/test/src/services/available_declarations_test.dart
+++ b/pkg/analyzer/test/src/services/available_declarations_test.dart
@@ -914,6 +914,75 @@
     ]);
   }
 
+  /// https://github.com/dart-lang/sdk/issues/47804
+  test_updated_exported2() async {
+    var a = convertPath('/home/test/lib/a.dart');
+    var b = convertPath('/home/test/lib/b.dart');
+    var c = convertPath('/home/test/lib/c.dart');
+
+    newFile(a, content: r'''
+class A {}
+''');
+    newFile(b, content: r'''
+class B {}
+''');
+    newFile(c, content: r'''
+export 'a.dart';
+export 'b.dart';
+class C {}
+''');
+    tracker.addContext(testAnalysisContext);
+
+    await _doAllTrackerWork();
+    _assertHasLibrary('package:test/c.dart', declarations: [
+      _ExpectedDeclaration.class_('A', [
+        _ExpectedDeclaration.constructor(''),
+      ]),
+      _ExpectedDeclaration.class_('B', [
+        _ExpectedDeclaration.constructor(''),
+      ]),
+      _ExpectedDeclaration.class_('C', [
+        _ExpectedDeclaration.constructor(''),
+      ]),
+    ]);
+
+    changes.clear();
+
+    newFile(a, content: r'''
+class A2 {}
+''');
+    newFile(b, content: r'''
+class B2 {}
+''');
+    tracker.changeFile(a);
+    tracker.changeFile(b);
+    await _doAllTrackerWork();
+
+    // In general it is OK to get duplicate libraries.
+    // But here we notified about both `a.dart` and `b.dart` changes before
+    // performing any work. So, there is no reason do handle `c.dart` twice.
+    var uniquePathSet = <String>{};
+    for (var change in changes) {
+      for (var library in change.changed) {
+        if (!uniquePathSet.add(library.path)) {
+          fail('Not unique path: ${library.path}');
+        }
+      }
+    }
+
+    _assertHasLibrary('package:test/c.dart', declarations: [
+      _ExpectedDeclaration.class_('A2', [
+        _ExpectedDeclaration.constructor(''),
+      ]),
+      _ExpectedDeclaration.class_('B2', [
+        _ExpectedDeclaration.constructor(''),
+      ]),
+      _ExpectedDeclaration.class_('C', [
+        _ExpectedDeclaration.constructor(''),
+      ]),
+    ]);
+  }
+
   test_updated_library() async {
     var a = convertPath('/home/test/lib/a.dart');
     var b = convertPath('/home/test/lib/b.dart');