Implement changeFile() and recompute available libraries and declarations.

Some renames in tests.

R=brianwilkerson@google.com

Change-Id: Ia54752c816bbc62f99a200d62658b6f4dfb26480
Reviewed-on: https://dart-review.googlesource.com/c/91381
Commit-Queue: Konstantin Shcheglov <scheglov@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
diff --git a/pkg/analyzer/lib/src/services/available_declarations.dart b/pkg/analyzer/lib/src/services/available_declarations.dart
index 268c8dd..fd417fc 100644
--- a/pkg/analyzer/lib/src/services/available_declarations.dart
+++ b/pkg/analyzer/lib/src/services/available_declarations.dart
@@ -339,6 +339,9 @@
 
   final _changesController = _StreamController<LibraryChange>();
 
+  /// The list of changed file paths.
+  final List<String> _changedPaths = [];
+
   /// 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 = [];
@@ -350,7 +353,9 @@
 
   /// Return `true` if there is scheduled work to do, as a result of adding
   /// new contexts, or changes to files.
-  bool get hasWork => _scheduledFiles.isNotEmpty;
+  bool get hasWork {
+    return _changedPaths.isNotEmpty || _scheduledFiles.isNotEmpty;
+  }
 
   /// Add the [analysisContext], so that its libraries are reported via the
   /// [changes] stream, and return the [DeclarationsContext] that can be used
@@ -370,7 +375,7 @@
     return declarationsContext;
   }
 
-  /// The file with the given [path] was changed - updated or added.
+  /// The file with the given [path] was changed - added, updated, or removed.
   ///
   /// The [path] must be absolute and normalized.
   ///
@@ -378,7 +383,7 @@
   /// be invoked to send updates to [changes] that reflect changes to the
   /// library of the file, and other libraries that export it.
   void changeFile(String path) {
-    // TODO(scheglov) implement
+    _changedPaths.add(path);
   }
 
   /// Do a single piece of work.
@@ -387,28 +392,34 @@
   /// This would mean that all previous changes have been processed, and
   /// updates scheduled to be delivered via the [changes] stream.
   void doWork() {
-    if (_scheduledFiles.isEmpty) return;
-
-    var scheduledFile = _scheduledFiles.removeLast();
-    var file = _getFileByPath(scheduledFile.context, scheduledFile.path);
-
-    if (!file.isLibrary) return;
-
-    if (file.exportedDeclarations == null) {
-      new _LibraryWalker().walkLibrary(file);
-      assert(file.exportedDeclarations != null);
+    if (_changedPaths.isNotEmpty) {
+      var path = _changedPaths.removeLast();
+      _performChangeFile(path);
+      return;
     }
 
-    var library = Library._(
-      file.id,
-      file.path,
-      file.uri,
-      file.exportedDeclarations,
-    );
-    _idToLibrary[file.id] = library;
-    _changesController.add(
-      LibraryChange._([library], []),
-    );
+    if (_scheduledFiles.isNotEmpty) {
+      var scheduledFile = _scheduledFiles.removeLast();
+      var file = _getFileByPath(scheduledFile.context, scheduledFile.path);
+
+      if (!file.isLibrary) return;
+
+      if (file.exportedDeclarations == null) {
+        new _LibraryWalker().walkLibrary(file);
+        assert(file.exportedDeclarations != null);
+      }
+
+      var library = Library._(
+        file.id,
+        file.path,
+        file.uri,
+        file.exportedDeclarations,
+      );
+      _idToLibrary[file.id] = library;
+      _changesController.add(
+        LibraryChange._([library], []),
+      );
+    }
   }
 
   /// The [analysisContext] is being disposed, it does not need declarations.
@@ -420,23 +431,23 @@
     });
   }
 
-  /// The file with the given [path] was removed.
-  ///
-  /// The [path] must be absolute and normalized.
-  ///
-  /// Usually causes [hasWork] to return `true`, so that [doWork] should
-  /// be invoked to send updates to [changes] that reflect removing of the
-  /// file, its defining library, other libraries that export it.
-  void removeFile(String path) {
-    // TODO(scheglov) implement
-  }
-
   void _addFile(DeclarationsContext context, String path) {
     if (path.endsWith('.dart')) {
       _scheduledFiles.add(_ScheduledFile(context, path));
     }
   }
 
+  /// Compute exported declarations for the given [libraries].
+  void _computeExportedDeclarations(Set<_File> libraries) {
+    var walker = new _LibraryWalker();
+    for (var library in libraries) {
+      if (library.isLibrary && library.exportedDeclarations == null) {
+        walker.walkLibrary(library);
+        assert(library.exportedDeclarations != null);
+      }
+    }
+  }
+
   _File _getFileByPath(DeclarationsContext context, String path) {
     var file = _pathToFile[path];
     if (file == null) {
@@ -464,6 +475,64 @@
     }
     return file;
   }
+
+  /// Recursively invalidate exported declarations of the given [library]
+  /// and libraries that export it.
+  void _invalidateExportedDeclarations(Set<_File> libraries, _File library) {
+    if (libraries.add(library)) {
+      library.exportedDeclarations = null;
+      for (var exporter in library.directExporters) {
+        _invalidateExportedDeclarations(libraries, exporter);
+      }
+    }
+  }
+
+  void _performChangeFile(String path) {
+    DeclarationsContext containingContext;
+    for (var context in _contexts.values) {
+      var uri = context._restoreUri(path);
+      if (uri != null) {
+        containingContext = context;
+        break;
+      }
+    }
+    if (containingContext == null) return;
+
+    var file = _getFileByPath(containingContext, path);
+    if (file == null) return;
+
+    var isLibrary = file.isLibrary;
+    var library = isLibrary ? file : file.library;
+
+    if (isLibrary) {
+      file.refresh(containingContext);
+    } else {
+      file.refresh(containingContext);
+      library.refresh(containingContext);
+    }
+
+    var invalidatedLibraries = Set<_File>();
+    _invalidateExportedDeclarations(invalidatedLibraries, library);
+    _computeExportedDeclarations(invalidatedLibraries);
+
+    var changedLibraries = <Library>[];
+    var removedLibraries = <int>[];
+    for (var library in invalidatedLibraries) {
+      if (library.exists) {
+        changedLibraries.add(Library._(
+          library.id,
+          library.path,
+          library.uri,
+          library.exportedDeclarations,
+        ));
+      } else {
+        removedLibraries.add(library.id);
+      }
+    }
+    _changesController.add(
+      LibraryChange._(changedLibraries, removedLibraries),
+    );
+  }
 }
 
 class Libraries {
@@ -552,10 +621,17 @@
   final String path;
   final Uri uri;
 
+  bool exists = false;
   bool isLibrary = false;
   List<_Export> exports = [];
   List<_Part> parts = [];
 
+  /// If this file is a part, the containing library.
+  _File library;
+
+  /// If this file is a library, libraries that export it.
+  List<_File> directExporters = [];
+
   List<Declaration> fileDeclarations = [];
   List<Declaration> libraryDeclarations = [];
   List<Declaration> exportedDeclarations;
@@ -568,8 +644,10 @@
     int modificationStamp;
     try {
       modificationStamp = resource.modificationStamp;
+      exists = true;
     } catch (e) {
       modificationStamp = -1;
+      exists = false;
     }
 
     // When a file changes, its modification stamp changes.
@@ -624,6 +702,15 @@
     exports.removeWhere((e) => e.file == null);
     parts.removeWhere((e) => e.file == null);
 
+    // Set back pointers.
+    for (var export in exports) {
+      export.file.directExporters.add(this);
+    }
+    for (var part in parts) {
+      part.file.library = this;
+      part.file.isLibrary = false;
+    }
+
     // Compute library declarations.
     if (isLibrary) {
       libraryDeclarations = <Declaration>[];
diff --git a/pkg/analyzer/test/src/services/available_declarations_test.dart b/pkg/analyzer/test/src/services/available_declarations_test.dart
index 83d627d..8d2834a 100644
--- a/pkg/analyzer/test/src/services/available_declarations_test.dart
+++ b/pkg/analyzer/test/src/services/available_declarations_test.dart
@@ -100,6 +100,384 @@
     _createTracker();
   }
 
+  test_changeFile_added_exported() 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');
+    var d = convertPath('/home/test/lib/d.dart');
+
+    newFile(a, content: r'''
+export 'b.dart';
+class A {}
+''');
+    newFile(b, content: r'''
+export 'c.dart';
+class B {}
+''');
+    newFile(d, content: r'''
+class D {}
+''');
+    tracker.addContext(testAnalysisContext);
+
+    await _doAllTrackerWork();
+    _assertHasLibrary('package:test/a.dart', declarations: [
+      _ExpectedDeclaration('A', DeclarationKind.CLASS),
+      _ExpectedDeclaration('B', DeclarationKind.CLASS),
+    ]);
+    _assertHasLibrary('package:test/b.dart', declarations: [
+      _ExpectedDeclaration('B', DeclarationKind.CLASS),
+    ]);
+    _assertHasNoLibrary('package:test/c.dart');
+    _assertHasLibrary('package:test/d.dart', declarations: [
+      _ExpectedDeclaration('D', DeclarationKind.CLASS),
+    ]);
+
+    newFile(c, content: r'''
+class C {}
+''');
+    tracker.changeFile(c);
+    await _doAllTrackerWork();
+
+    _assertHasLibrary('package:test/a.dart', declarations: [
+      _ExpectedDeclaration('A', DeclarationKind.CLASS),
+      _ExpectedDeclaration('B', DeclarationKind.CLASS),
+      _ExpectedDeclaration('C', DeclarationKind.CLASS),
+    ]);
+    _assertHasLibrary('package:test/b.dart', declarations: [
+      _ExpectedDeclaration('B', DeclarationKind.CLASS),
+      _ExpectedDeclaration('C', DeclarationKind.CLASS),
+    ]);
+    _assertHasLibrary('package:test/c.dart', declarations: [
+      _ExpectedDeclaration('C', DeclarationKind.CLASS),
+    ]);
+    _assertHasLibrary('package:test/d.dart', declarations: [
+      _ExpectedDeclaration('D', DeclarationKind.CLASS),
+    ]);
+  }
+
+  test_changeFile_added_library() async {
+    var a = convertPath('/home/test/lib/a.dart');
+    var b = convertPath('/home/test/lib/b.dart');
+
+    newFile(a, content: r'''
+class A {}
+''');
+    tracker.addContext(testAnalysisContext);
+
+    await _doAllTrackerWork();
+    _assertHasLibrary('package:test/a.dart', declarations: [
+      _ExpectedDeclaration('A', DeclarationKind.CLASS),
+    ]);
+    _assertHasNoLibrary('package:test/b.dart');
+
+    newFile(b, content: r'''
+class B {}
+''');
+    tracker.changeFile(b);
+    await _doAllTrackerWork();
+
+    _assertHasLibrary('package:test/a.dart', declarations: [
+      _ExpectedDeclaration('A', DeclarationKind.CLASS),
+    ]);
+    _assertHasLibrary('package:test/b.dart', declarations: [
+      _ExpectedDeclaration('B', DeclarationKind.CLASS),
+    ]);
+  }
+
+  test_changeFile_added_part() 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'''
+part 'b.dart';
+class A {}
+''');
+    newFile(c, content: r'''
+class C {}
+''');
+    tracker.addContext(testAnalysisContext);
+
+    await _doAllTrackerWork();
+    _assertHasLibrary('package:test/a.dart', declarations: [
+      _ExpectedDeclaration('A', DeclarationKind.CLASS),
+    ]);
+    _assertHasNoLibrary('package:test/b.dart');
+    _assertHasLibrary('package:test/c.dart', declarations: [
+      _ExpectedDeclaration('C', DeclarationKind.CLASS),
+    ]);
+
+    newFile(b, content: r'''
+part of 'a.dart';
+class B {}
+''');
+    tracker.changeFile(b);
+    await _doAllTrackerWork();
+
+    _assertHasLibrary('package:test/a.dart', declarations: [
+      _ExpectedDeclaration('A', DeclarationKind.CLASS),
+      _ExpectedDeclaration('B', DeclarationKind.CLASS),
+    ]);
+    _assertHasNoLibrary('package:test/b.dart');
+    _assertHasLibrary('package:test/c.dart', declarations: [
+      _ExpectedDeclaration('C', DeclarationKind.CLASS),
+    ]);
+  }
+
+  test_changeFile_deleted_exported() 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');
+    var d = convertPath('/home/test/lib/d.dart');
+
+    newFile(a, content: r'''
+export 'b.dart';
+class A {}
+''');
+    newFile(b, content: r'''
+export 'c.dart';
+class B {}
+''');
+    newFile(c, content: r'''
+class C {}
+''');
+    newFile(d, content: r'''
+class D {}
+''');
+    tracker.addContext(testAnalysisContext);
+
+    await _doAllTrackerWork();
+    _assertHasLibrary('package:test/a.dart', declarations: [
+      _ExpectedDeclaration('A', DeclarationKind.CLASS),
+      _ExpectedDeclaration('B', DeclarationKind.CLASS),
+      _ExpectedDeclaration('C', DeclarationKind.CLASS),
+    ]);
+    _assertHasLibrary('package:test/b.dart', declarations: [
+      _ExpectedDeclaration('B', DeclarationKind.CLASS),
+      _ExpectedDeclaration('C', DeclarationKind.CLASS),
+    ]);
+    _assertHasLibrary('package:test/c.dart', declarations: [
+      _ExpectedDeclaration('C', DeclarationKind.CLASS),
+    ]);
+    _assertHasLibrary('package:test/d.dart', declarations: [
+      _ExpectedDeclaration('D', DeclarationKind.CLASS),
+    ]);
+
+    deleteFile(c);
+    tracker.changeFile(c);
+    await _doAllTrackerWork();
+
+    _assertHasLibrary('package:test/a.dart', declarations: [
+      _ExpectedDeclaration('A', DeclarationKind.CLASS),
+      _ExpectedDeclaration('B', DeclarationKind.CLASS),
+    ]);
+    _assertHasLibrary('package:test/b.dart', declarations: [
+      _ExpectedDeclaration('B', DeclarationKind.CLASS),
+    ]);
+    _assertHasNoLibrary('package:test/c.dart');
+    _assertHasLibrary('package:test/d.dart', declarations: [
+      _ExpectedDeclaration('D', DeclarationKind.CLASS),
+    ]);
+  }
+
+  test_changeFile_deleted_library() async {
+    var a = convertPath('/home/test/lib/a.dart');
+    var b = convertPath('/home/test/lib/b.dart');
+
+    newFile(a, content: '');
+    newFile(b, content: '');
+    tracker.addContext(testAnalysisContext);
+
+    await _doAllTrackerWork();
+    _assertHasLibrary('package:test/a.dart');
+    _assertHasLibrary('package:test/b.dart');
+
+    deleteFile(a);
+    tracker.changeFile(a);
+    await _doAllTrackerWork();
+
+    _assertHasNoLibrary('package:test/a.dart');
+    _assertHasLibrary('package:test/b.dart');
+  }
+
+  test_changeFile_deleted_part() 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'''
+part 'b.dart';
+class A {}
+''');
+    newFile(b, content: r'''
+part of 'a.dart';
+class B {}
+''');
+    newFile(c, content: r'''
+class C {}
+''');
+    tracker.addContext(testAnalysisContext);
+
+    await _doAllTrackerWork();
+    _assertHasLibrary('package:test/a.dart', declarations: [
+      _ExpectedDeclaration('A', DeclarationKind.CLASS),
+      _ExpectedDeclaration('B', DeclarationKind.CLASS),
+    ]);
+    _assertHasLibrary('package:test/c.dart', declarations: [
+      _ExpectedDeclaration('C', DeclarationKind.CLASS),
+    ]);
+
+    deleteFile(b);
+    tracker.changeFile(b);
+    await _doAllTrackerWork();
+
+    _assertHasLibrary('package:test/a.dart', declarations: [
+      _ExpectedDeclaration('A', DeclarationKind.CLASS),
+    ]);
+    _assertHasLibrary('package:test/c.dart', declarations: [
+      _ExpectedDeclaration('C', DeclarationKind.CLASS),
+    ]);
+  }
+
+  test_changeFile_updated_exported() 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');
+    var d = convertPath('/home/test/lib/d.dart');
+
+    newFile(a, content: r'''
+export 'b.dart';
+class A {}
+''');
+    newFile(b, content: r'''
+export 'c.dart';
+class B {}
+''');
+    newFile(c, content: r'''
+class C {}
+''');
+    newFile(d, content: r'''
+class D {}
+''');
+    tracker.addContext(testAnalysisContext);
+
+    await _doAllTrackerWork();
+    _assertHasLibrary('package:test/a.dart', declarations: [
+      _ExpectedDeclaration('A', DeclarationKind.CLASS),
+      _ExpectedDeclaration('B', DeclarationKind.CLASS),
+      _ExpectedDeclaration('C', DeclarationKind.CLASS),
+    ]);
+    _assertHasLibrary('package:test/b.dart', declarations: [
+      _ExpectedDeclaration('B', DeclarationKind.CLASS),
+      _ExpectedDeclaration('C', DeclarationKind.CLASS),
+    ]);
+    _assertHasLibrary('package:test/c.dart', declarations: [
+      _ExpectedDeclaration('C', DeclarationKind.CLASS),
+    ]);
+    _assertHasLibrary('package:test/d.dart', declarations: [
+      _ExpectedDeclaration('D', DeclarationKind.CLASS),
+    ]);
+
+    newFile(c, content: r'''
+class C2 {}
+''');
+    tracker.changeFile(c);
+    await _doAllTrackerWork();
+
+    _assertHasLibrary('package:test/a.dart', declarations: [
+      _ExpectedDeclaration('A', DeclarationKind.CLASS),
+      _ExpectedDeclaration('B', DeclarationKind.CLASS),
+      _ExpectedDeclaration('C2', DeclarationKind.CLASS),
+    ]);
+    _assertHasLibrary('package:test/b.dart', declarations: [
+      _ExpectedDeclaration('B', DeclarationKind.CLASS),
+      _ExpectedDeclaration('C2', DeclarationKind.CLASS),
+    ]);
+    _assertHasLibrary('package:test/c.dart', declarations: [
+      _ExpectedDeclaration('C2', DeclarationKind.CLASS),
+    ]);
+    _assertHasLibrary('package:test/d.dart', declarations: [
+      _ExpectedDeclaration('D', DeclarationKind.CLASS),
+    ]);
+  }
+
+  test_changeFile_updated_library() async {
+    var a = convertPath('/home/test/lib/a.dart');
+    var b = convertPath('/home/test/lib/b.dart');
+
+    newFile(a, content: r'''
+class A {}
+''');
+    newFile(b, content: r'''
+class B {}
+''');
+    tracker.addContext(testAnalysisContext);
+
+    await _doAllTrackerWork();
+    _assertHasLibrary('package:test/a.dart', declarations: [
+      _ExpectedDeclaration('A', DeclarationKind.CLASS),
+    ]);
+    _assertHasLibrary('package:test/b.dart', declarations: [
+      _ExpectedDeclaration('B', DeclarationKind.CLASS),
+    ]);
+
+    newFile(a, content: r'''
+class A2 {}
+''');
+    tracker.changeFile(a);
+    await _doAllTrackerWork();
+
+    _assertHasLibrary('package:test/a.dart', declarations: [
+      _ExpectedDeclaration('A2', DeclarationKind.CLASS),
+    ]);
+    _assertHasLibrary('package:test/b.dart', declarations: [
+      _ExpectedDeclaration('B', DeclarationKind.CLASS),
+    ]);
+  }
+
+  test_changeFile_updated_part() 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'''
+part 'b.dart';
+class A {}
+''');
+    newFile(b, content: r'''
+part of 'a.dart';
+class B {}
+''');
+    newFile(c, content: r'''
+class C {}
+''');
+    tracker.addContext(testAnalysisContext);
+
+    await _doAllTrackerWork();
+    _assertHasLibrary('package:test/a.dart', declarations: [
+      _ExpectedDeclaration('A', DeclarationKind.CLASS),
+      _ExpectedDeclaration('B', DeclarationKind.CLASS),
+    ]);
+    _assertHasLibrary('package:test/c.dart', declarations: [
+      _ExpectedDeclaration('C', DeclarationKind.CLASS),
+    ]);
+
+    newFile(b, content: r'''
+part of 'a.dart';
+class B2 {}
+''');
+    tracker.changeFile(b);
+    await _doAllTrackerWork();
+
+    _assertHasLibrary('package:test/a.dart', declarations: [
+      _ExpectedDeclaration('A', DeclarationKind.CLASS),
+      _ExpectedDeclaration('B2', DeclarationKind.CLASS),
+    ]);
+    _assertHasLibrary('package:test/c.dart', declarations: [
+      _ExpectedDeclaration('C', DeclarationKind.CLASS),
+    ]);
+  }
+
   test_export() async {
     newFile('/home/test/lib/a.dart', content: r'''
 class A {}
@@ -112,11 +490,11 @@
     tracker.addContext(testAnalysisContext);
 
     await _doAllTrackerWork();
-    _assertLibraryDeclarations([
+    _assertHasLibrary('package:test/test.dart', declarations: [
       _ExpectedDeclaration.class_('A'),
       _ExpectedDeclaration.class_('B'),
       _ExpectedDeclaration.class_('C'),
-    ], uriStr: 'package:test/test.dart');
+    ]);
   }
 
   test_export_combinators_hide() async {
@@ -132,11 +510,11 @@
     tracker.addContext(testAnalysisContext);
 
     await _doAllTrackerWork();
-    _assertLibraryDeclarations([
+    _assertHasLibrary('package:test/test.dart', declarations: [
       _ExpectedDeclaration.class_('A'),
       _ExpectedDeclaration.class_('C'),
       _ExpectedDeclaration.class_('D'),
-    ], uriStr: 'package:test/test.dart');
+    ]);
   }
 
   test_export_combinators_show() async {
@@ -152,10 +530,10 @@
     tracker.addContext(testAnalysisContext);
 
     await _doAllTrackerWork();
-    _assertLibraryDeclarations([
+    _assertHasLibrary('package:test/test.dart', declarations: [
       _ExpectedDeclaration.class_('B'),
       _ExpectedDeclaration.class_('D'),
-    ], uriStr: 'package:test/test.dart');
+    ]);
   }
 
   test_export_cycle() async {
@@ -175,21 +553,21 @@
 
     await _doAllTrackerWork();
 
-    _assertLibraryDeclarations([
+    _assertHasLibrary('package:test/a.dart', declarations: [
       _ExpectedDeclaration.class_('A'),
       _ExpectedDeclaration.class_('B'),
-    ], uriStr: 'package:test/a.dart');
+    ]);
 
-    _assertLibraryDeclarations([
+    _assertHasLibrary('package:test/b.dart', declarations: [
       _ExpectedDeclaration.class_('A'),
       _ExpectedDeclaration.class_('B'),
-    ], uriStr: 'package:test/b.dart');
+    ]);
 
-    _assertLibraryDeclarations([
+    _assertHasLibrary('package:test/test.dart', declarations: [
       _ExpectedDeclaration.class_('A'),
       _ExpectedDeclaration.class_('B'),
       _ExpectedDeclaration.class_('C'),
-    ], uriStr: 'package:test/test.dart');
+    ]);
   }
 
   test_export_missing() async {
@@ -200,9 +578,9 @@
     tracker.addContext(testAnalysisContext);
 
     await _doAllTrackerWork();
-    _assertLibraryDeclarations([
+    _assertHasLibrary('package:test/test.dart', declarations: [
       _ExpectedDeclaration.class_('C'),
-    ], uriStr: 'package:test/test.dart');
+    ]);
   }
 
   test_export_sequence() async {
@@ -221,20 +599,20 @@
 
     await _doAllTrackerWork();
 
-    _assertLibraryDeclarations([
+    _assertHasLibrary('package:test/a.dart', declarations: [
       _ExpectedDeclaration.class_('A'),
-    ], uriStr: 'package:test/a.dart');
+    ]);
 
-    _assertLibraryDeclarations([
+    _assertHasLibrary('package:test/b.dart', declarations: [
       _ExpectedDeclaration.class_('A'),
       _ExpectedDeclaration.class_('B'),
-    ], uriStr: 'package:test/b.dart');
+    ]);
 
-    _assertLibraryDeclarations([
+    _assertHasLibrary('package:test/test.dart', declarations: [
       _ExpectedDeclaration.class_('A'),
       _ExpectedDeclaration.class_('B'),
       _ExpectedDeclaration.class_('C'),
-    ], uriStr: 'package:test/test.dart');
+    ]);
   }
 
   test_export_shadowedByLocal() async {
@@ -250,10 +628,10 @@
     tracker.addContext(testAnalysisContext);
 
     await _doAllTrackerWork();
-    _assertLibraryDeclarations([
+    _assertHasLibrary('package:test/test.dart', declarations: [
       _ExpectedDeclaration.class_('A'),
       _ExpectedDeclaration.mixin('B'),
-    ], uriStr: 'package:test/test.dart');
+    ]);
   }
 
   test_getLibraries_bazel() async {
@@ -306,25 +684,31 @@
     });
     await _doAllTrackerWork();
 
-    _assertLibraryDeclarations([
+    _assertHasLibrary('package:aaa/a.dart', declarations: [
       _ExpectedDeclaration.class_('A'),
-    ], uriStr: 'package:aaa/a.dart');
-    _assertNoLibrary('package:aaa/src/a2.dart');
+    ]);
+    _assertHasNoLibrary('package:aaa/src/a2.dart');
 
-    _assertLibraryDeclarations([
+    _assertHasLibrary('package:bbb/b.dart', declarations: [
       _ExpectedDeclaration.class_('B'),
-    ], uriStr: 'package:bbb/b.dart');
-    _assertNoLibrary('package:bbb/src/b2.dart');
+    ]);
+    _assertHasNoLibrary('package:bbb/src/b2.dart');
 
-    _assertLibraryDeclarations([
+    _assertHasLibrary('package:material_button/button.dart', declarations: [
       _ExpectedDeclaration.class_('MaterialButton'),
-    ], uriStr: 'package:material_button/button.dart');
-    _assertLibraryDeclarations([
-      _ExpectedDeclaration.class_('MaterialButtonTest'),
-    ], uriStr: toUri('/home/material_button/test/button_test.dart').toString());
-    _assertLibraryDeclarations([
-      _ExpectedDeclaration.class_('MaterialButtonPO'),
-    ], uriStr: 'package:material_button_testing/material_button_po.dart');
+    ]);
+    _assertHasLibrary(
+      toUri('/home/material_button/test/button_test.dart').toString(),
+      declarations: [
+        _ExpectedDeclaration.class_('MaterialButtonTest'),
+      ],
+    );
+    _assertHasLibrary(
+      'package:material_button_testing/material_button_po.dart',
+      declarations: [
+        _ExpectedDeclaration.class_('MaterialButtonPO'),
+      ],
+    );
 
     {
       var path = convertPath('/home/material_button/lib/_.dart');
@@ -432,31 +816,31 @@
     var context = tracker.addContext(testAnalysisContext);
     await _doAllTrackerWork();
 
-    _assertLibraryDeclarations([
+    _assertHasLibrary('package:aaa/a.dart', declarations: [
       _ExpectedDeclaration.class_('A'),
-    ], uriStr: 'package:aaa/a.dart');
-    _assertNoLibrary('package:aaa/src/a2.dart');
+    ]);
+    _assertHasNoLibrary('package:aaa/src/a2.dart');
 
-    _assertLibraryDeclarations([
+    _assertHasLibrary('package:bbb/b.dart', declarations: [
       _ExpectedDeclaration.class_('B'),
-    ], uriStr: 'package:bbb/b.dart');
-    _assertNoLibrary('package:bbb/src/b2.dart');
+    ]);
+    _assertHasNoLibrary('package:bbb/src/b2.dart');
 
-    _assertLibraryDeclarations([
+    _assertHasLibrary('package:ccc/c.dart', declarations: [
       _ExpectedDeclaration.class_('C'),
-    ], uriStr: 'package:ccc/c.dart');
-    _assertNoLibrary('package:ccc/src/c2.dart');
+    ]);
+    _assertHasNoLibrary('package:ccc/src/c2.dart');
 
-    _assertLibraryDeclarations([
+    _assertHasLibrary('package:test/t.dart', declarations: [
       _ExpectedDeclaration.class_('T'),
-    ], uriStr: 'package:test/t.dart');
-    _assertLibraryDeclarations([
+    ]);
+    _assertHasLibrary('package:test/src/t2.dart', declarations: [
       _ExpectedDeclaration.class_('T2'),
-    ], uriStr: 'package:test/src/t2.dart');
+    ]);
 
-    _assertLibraryDeclarations([
+    _assertHasLibrary('package:basic/s.dart', declarations: [
       _ExpectedDeclaration.class_('S'),
-    ], uriStr: 'package:basic/s.dart');
+    ]);
 
     {
       var path = convertPath('/home/test/lib/_.dart');
@@ -593,23 +977,26 @@
 
     await _doAllTrackerWork();
 
-    _assertLibraryDeclarations([
+    _assertHasLibrary('package:aaa/a.dart', declarations: [
       _ExpectedDeclaration.class_('A1'),
       _ExpectedDeclaration.class_('A2'),
-    ], uriStr: 'package:aaa/a.dart');
-    _assertNoLibrary('package:aaa/src/a2.dart');
-    _assertLibraryDeclarations([
+    ]);
+    _assertHasNoLibrary('package:aaa/src/a2.dart');
+    _assertHasLibrary('package:bbb/b.dart', declarations: [
       _ExpectedDeclaration.class_('B'),
-    ], uriStr: 'package:bbb/b.dart');
-    _assertLibraryDeclarations([
+    ]);
+    _assertHasLibrary('package:test/t.dart', declarations: [
       _ExpectedDeclaration.class_('T'),
-    ], uriStr: 'package:test/t.dart');
-    _assertLibraryDeclarations([
+    ]);
+    _assertHasLibrary('package:test/src/t2.dart', declarations: [
       _ExpectedDeclaration.class_('T2'),
-    ], uriStr: 'package:test/src/t2.dart');
-    _assertLibraryDeclarations([
-      _ExpectedDeclaration.class_('T3'),
-    ], uriStr: toUri('/home/test/test/t3.dart').toString());
+    ]);
+    _assertHasLibrary(
+      toUri('/home/test/test/t3.dart').toString(),
+      declarations: [
+        _ExpectedDeclaration.class_('T3'),
+      ],
+    );
 
     // `lib/` is configured to see `package:aaa`.
     {
@@ -682,10 +1069,10 @@
     });
     await _doAllTrackerWork();
 
-    _assertLibraryDeclarations([
+    _assertHasLibrary(aUri, declarations: [
       _ExpectedDeclaration.class_('A'),
-    ], uriStr: aUri);
-    _assertNoLibrary(bUri);
+    ]);
+    _assertHasNoLibrary(bUri);
 
     // The package can see package:aaa, but not package:bbb
     {
@@ -703,12 +1090,12 @@
     });
     await _doAllTrackerWork();
 
-    _assertLibraryDeclarations([
+    _assertHasLibrary(aUri, declarations: [
       _ExpectedDeclaration.class_('A'),
-    ], uriStr: aUri);
-    _assertLibraryDeclarations([
+    ]);
+    _assertHasLibrary(bUri, declarations: [
       _ExpectedDeclaration.class_('B'),
-    ], uriStr: bUri);
+    ]);
 
     // The package can see package:bbb, but not package:aaa
     {
@@ -735,7 +1122,7 @@
     tracker.addContext(testAnalysisContext);
 
     await _doAllTrackerWork();
-    _assertLibraryDeclarations([
+    _assertHasLibrary('package:test/test.dart', declarations: [
       _ExpectedDeclaration.class_('MyClass'),
       _ExpectedDeclaration.classTypeAlias('MyClassTypeAlias'),
       _ExpectedDeclaration.enum_('MyEnum'),
@@ -744,7 +1131,7 @@
       _ExpectedDeclaration.mixin('MyMixin'),
       _ExpectedDeclaration.variable('myVariable1'),
       _ExpectedDeclaration.variable('myVariable2'),
-    ], uriStr: 'package:test/test.dart');
+    ]);
   }
 
   test_parts() async {
@@ -764,11 +1151,11 @@
     tracker.addContext(testAnalysisContext);
 
     await _doAllTrackerWork();
-    _assertLibraryDeclarations([
+    _assertHasLibrary('package:test/test.dart', declarations: [
       _ExpectedDeclaration.class_('A'),
       _ExpectedDeclaration.class_('B'),
       _ExpectedDeclaration.class_('C'),
-    ], uriStr: 'package:test/test.dart');
+    ]);
   }
 
   test_publicOnly() async {
@@ -785,10 +1172,10 @@
     tracker.addContext(testAnalysisContext);
 
     await _doAllTrackerWork();
-    _assertLibraryDeclarations([
+    _assertHasLibrary('package:test/test.dart', declarations: [
       _ExpectedDeclaration.class_('A'),
       _ExpectedDeclaration.class_('B'),
-    ], uriStr: 'package:test/test.dart');
+    ]);
   }
 
   test_readByteStore() async {
@@ -813,11 +1200,11 @@
     tracker.addContext(testAnalysisContext);
     await _doAllTrackerWork();
 
-    _assertLibraryDeclarations([
+    _assertHasLibrary('package:test/test.dart', declarations: [
       _ExpectedDeclaration.class_('A'),
       _ExpectedDeclaration.class_('B'),
       _ExpectedDeclaration.class_('C'),
-    ], uriStr: 'package:test/test.dart');
+    ]);
   }
 
   test_removeContext_afterAddContext() async {
@@ -840,22 +1227,7 @@
     expect(uriToLibrary, isEmpty);
   }
 
-  void _assertLibraryDeclarations(
-      List<_ExpectedDeclaration> expectedDeclarations,
-      {String uriStr}) {
-    var library = uriToLibrary[uriStr];
-    expect(library, isNotNull);
-    expect(library.declarations, hasLength(expectedDeclarations.length));
-    for (var expected in expectedDeclarations) {
-      _asyncHasDeclaration(library, expected);
-    }
-  }
-
-  void _assertNoLibrary(String uriStr) {
-    expect(uriToLibrary, isNot(contains(uriStr)));
-  }
-
-  void _asyncHasDeclaration(Library library, _ExpectedDeclaration expected) {
+  void _assertHasDeclaration(Library library, _ExpectedDeclaration expected) {
     expect(
       library.declarations,
       contains(predicate((Declaration d) {
@@ -865,11 +1237,32 @@
     );
   }
 
+  /// Assert that the current state has the library with the given [uri].
+  ///
+  /// If [declarations] provided, also checks that the library has exactly
+  /// these declarations.
+  void _assertHasLibrary(String uri,
+      {List<_ExpectedDeclaration> declarations}) {
+    var library = uriToLibrary[uri];
+    expect(library, isNotNull);
+    if (declarations != null) {
+      expect(library.declarations, hasLength(declarations.length));
+      for (var expected in declarations) {
+        _assertHasDeclaration(library, expected);
+      }
+    }
+  }
+
+  void _assertHasNoLibrary(String uri) {
+    expect(uriToLibrary, isNot(contains(uri)));
+  }
+
   void _createTracker() {
     uriToLibrary.clear();
 
     tracker = DeclarationsTracker(byteStore, resourceProvider);
     tracker.changes.listen((change) {
+      changes.add(change);
       for (var library in change.changed) {
         var uriStr = library.uri.toString();
         idToLibrary[library.id] = library;