Change the deferred library check tool to allow unspecified packages to be in
 any part. Also allow part specifications to specifically exclude packages.

BUG=
R=sigmund@google.com

Review URL: https://codereview.chromium.org//1535893002 .
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a6dee54..b33cd42 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
 # Changelog
 
+## 0.2.5
+- Changed the `deferred_library_check` tool to allow parts to exclude packages
+  and to not assume that unspecified packages are in the main part.
+
 ## 0.2.4
 - Added `imports` field for `OutputUnitInfo`
 
diff --git a/README.md b/README.md
index 524a38b..667ac96 100644
--- a/README.md
+++ b/README.md
@@ -207,32 +207,38 @@
 
 ```yaml
 main:
-  packages:
+  include:
     - some_package
     - other_package
+  exclude:
+    - some_other_package
 
 foo:
-  packages:
+  include:
     - foo
     - bar
 
 baz:
-  packages:
+  include:
     - baz
     - quux
+  exclude:
+    - zardoz
 ```
 
 The YAML file consists of a list of declarations, one for each deferred
 part expected in the output. At least one of these parts must be named
 "main"; this is the main part that contains the program entrypoint. Each
 top-level part contains a list of package names that are expected to be
-contained in that part. Any package that is not explicitly listed is
-expected to be in the main part. For instance, in the example YAML above
-the part named "baz" is expected to contain the packages "baz" and "quux".
+contained in that part, a list of package names that are expected to be in
+another part, or both. For instance, in the example YAML above the part named
+"baz" is expected to contain the packages "baz" and "quux" and exclude the
+package "zardoz".
 
 The names for parts given in the specification YAML file (besides "main")
-are arbitrary and just used for reporting when the output does not meet the
-specification.
+are the same as the name given to the deferred import in the dart file. For
+instance, if you have `import 'package:foo/bar.dart' deferred as baz;` in your
+dart file, then the corresponding name in the specification file is 'baz'.
 
 ### Function size analysis tool
 
diff --git a/lib/deferred_library_check.dart b/lib/deferred_library_check.dart
index d472eae..71a4e5f 100644
--- a/lib/deferred_library_check.dart
+++ b/lib/deferred_library_check.dart
@@ -6,31 +6,36 @@
 /// given in a YAML file. The format of the YAML file is:
 ///
 ///     main:
-///       packages:
+///       include:
 ///         - some_package
 ///         - other_package
 ///
 ///     foo:
-///       packages:
+///       include:
 ///         - foo
 ///         - bar
 ///
 ///     baz:
-///       packages:
+///       include:
 ///         - baz
 ///         - quux
+///       exclude:
+///         - zardoz
 ///
 /// The YAML file consists of a list of declarations, one for each deferred
 /// part expected in the output. At least one of these parts must be named
 /// "main"; this is the main part that contains the program entrypoint. Each
 /// top-level part contains a list of package names that are expected to be
-/// contained in that part. Any package that is not explicitly listed is
-/// expected to be in the main part. For instance, in the example YAML above
-/// the part named "baz" is expected to contain the packages "baz" and "quux".
+/// contained in that part, a list of package names that are expected to be in
+/// another part, or both. For instance, in the example YAML above the part
+/// named "baz" is expected to contain the packages "baz" and "quux" and not to
+/// contain the package "zardoz".
 ///
 /// The names for parts given in the specification YAML file (besides "main")
-/// are arbitrary and just used for reporting when the output does not meet the
-/// specification.
+/// are the same as the name given to the deferred import in the dart file. For
+/// instance, if you have `import 'package:foo/bar.dart' deferred as baz;` in
+/// your dart file, then the corresponding name in the specification file is
+/// 'baz'.
 library dart2js_info.deferred_library_check;
 
 import 'info.dart';
@@ -38,51 +43,64 @@
 
 List<ManifestComplianceFailure> checkDeferredLibraryManifest(
     AllInfo info, Map manifest) {
-  // For each part in the manifest, record the expected "packages" for that
-  // part.
-  var packages = <String, String>{};
+  var includedPackages = new Multimap<String, String>();
+  var excludedPackages = new Multimap<String, String>();
   for (var part in manifest.keys) {
-    for (var package in manifest[part]['packages']) {
-      if (packages.containsKey(package)) {
-        throw new ArgumentError.value(
-            manifest,
-            'manifest',
-            'You cannot specify that package "$package" maps to both parts '
-            '"$part" and "${packages[package]}".');
-      }
-      packages[package] = part;
+    for (var package in manifest[part]['include'] ?? []) {
+      includedPackages.add(part, package);
+    }
+    for (var package in manifest[part]['exclude'] ?? []) {
+      excludedPackages.add(part, package);
     }
   }
 
-  var guessedPartMapping = new BiMap<String, String>();
-  guessedPartMapping['main'] = 'main';
+  // There are 2 types of parts that are valid to mention in the specification
+  // file. These are the main part and directly imported deferred parts. The
+  // main part is always named 'main'; the directly imported deferred parts are
+  // the outputUnits whose list of 'imports' contains a single import. If the
+  // part is shared, it will have more than one import since it will include the
+  // imports of all the top-level deferred parts that will load the shared part.
+  List<String> validParts = ['main']
+    ..addAll(info.outputUnits
+        .where((unit) => unit.imports.length == 1)
+        .map((unit) => unit.imports.single));
+  List<String> mentionedParts = []
+    ..addAll(includedPackages.keys)
+    ..addAll(excludedPackages.keys);
+  var partNameFailures = <_InvalidPartName>[];
+  for (var part in mentionedParts) {
+    if (!validParts.contains(part)) {
+      partNameFailures.add(new _InvalidPartName(part, validParts));
+    }
+  }
+  if (partNameFailures.isNotEmpty) {
+    return partNameFailures;
+  }
+
+  var mentionedPackages = new Set()
+    ..addAll(includedPackages.values)
+    ..addAll(excludedPackages.values);
+  var actualIncludedPackages = new Multimap<String, String>();
 
   var failures = <ManifestComplianceFailure>[];
 
   checkInfo(BasicInfo info) {
+    if (info.size == 0) return;
     var lib = _getLibraryOf(info);
     if (lib != null && _isPackageUri(lib.uri)) {
       var packageName = _getPackageName(lib.uri);
-      var outputUnitName = info.outputUnit.name;
-      var expectedPart;
-      if (packages.containsKey(packageName)) {
-        expectedPart = packages[packageName];
+      if (!mentionedPackages.contains(packageName)) return;
+      var containingParts = <String>[];
+      if (info.outputUnit.name == 'main') {
+        containingParts.add('main');
       } else {
-        expectedPart = 'main';
+        containingParts.addAll(info.outputUnit.imports);
       }
-      var expectedOutputUnit = guessedPartMapping[expectedPart];
-      if (expectedOutputUnit == null) {
-        guessedPartMapping[expectedPart] = outputUnitName;
-      } else {
-        if (expectedOutputUnit != outputUnitName) {
-          // TODO(het): add options for how to treat unspecified packages
-          if (!packages.containsKey(packageName)) {
-            failures.add(new ManifestComplianceFailure(info.name, packageName));
-          } else {
-            var actualPart = guessedPartMapping.inverse[outputUnitName];
-            failures.add(new ManifestComplianceFailure(
-                info.name, packageName, expectedPart, actualPart));
-          }
+      for (var part in containingParts) {
+        actualIncludedPackages.add(part, packageName);
+        if (excludedPackages[part].contains(packageName)) {
+          failures
+              .add(new _PartContainedExcludedPackage(part, packageName, info));
         }
       }
     }
@@ -91,6 +109,12 @@
   info.functions.forEach(checkInfo);
   info.fields.forEach(checkInfo);
 
+  includedPackages.forEach((part, package) {
+    if (!actualIncludedPackages.containsKey(part) ||
+        !actualIncludedPackages[part].contains(package)) {
+      failures.add(new _PartDidNotContainPackage(part, package));
+    }
+  });
   return failures;
 }
 
@@ -113,21 +137,40 @@
 }
 
 class ManifestComplianceFailure {
-  final String infoName;
-  final String packageName;
-  final String expectedPart;
-  final String actualPart;
+  const ManifestComplianceFailure();
+}
 
-  const ManifestComplianceFailure(this.infoName, this.packageName,
-      [this.expectedPart, this.actualPart]);
+class _InvalidPartName extends ManifestComplianceFailure {
+  final String part;
+  final List<String> validPartNames;
+  const _InvalidPartName(this.part, this.validPartNames);
 
   String toString() {
-    if (expectedPart == null && actualPart == null) {
-      return '"$infoName" from package "$packageName" was not declared '
-          'to be in an explicit part but was not in the main part';
-    } else {
-      return '"$infoName" from package "$packageName" was specified to '
-          'be in part $expectedPart but is in part $actualPart';
-    }
+    return 'Manifest file declares invalid part "$part". '
+        'Valid part names are: $validPartNames';
+  }
+}
+
+class _PartContainedExcludedPackage extends ManifestComplianceFailure {
+  final String part;
+  final String package;
+  final BasicInfo info;
+  const _PartContainedExcludedPackage(this.part, this.package, this.info);
+
+  String toString() {
+    return 'Part "$part" was specified to exclude package "$package" but it '
+        'actually contains ${kindToString(info.kind)} "${info.name}" which '
+        'is from package "$package"';
+  }
+}
+
+class _PartDidNotContainPackage extends ManifestComplianceFailure {
+  final String part;
+  final String package;
+  const _PartDidNotContainPackage(this.part, this.package);
+
+  String toString() {
+    return 'Part "$part" was specified to include package "$package" but it '
+        'does not contain any elements from that package.';
   }
 }
diff --git a/lib/info.dart b/lib/info.dart
index 0ca08ca..f258275 100644
--- a/lib/info.dart
+++ b/lib/info.dart
@@ -52,7 +52,7 @@
   int size;
   Info parent;
 
-  String get serializedId => '${_kindToString(kind)}/$id';
+  String get serializedId => '${kindToString(kind)}/$id';
 
   String name;
 
@@ -419,7 +419,7 @@
   typedef,
 }
 
-String _kindToString(InfoKind kind) {
+String kindToString(InfoKind kind) {
   switch (kind) {
     case InfoKind.library:
       return 'library';
@@ -444,9 +444,9 @@
     int.parse(serializedId.substring(serializedId.indexOf('/') + 1));
 
 InfoKind _kindFromSerializedId(String serializedId) =>
-    _kindFromString(serializedId.substring(0, serializedId.indexOf('/')));
+    kindFromString(serializedId.substring(0, serializedId.indexOf('/')));
 
-InfoKind _kindFromString(String kind) {
+InfoKind kindFromString(String kind) {
   switch (kind) {
     case 'library':
       return InfoKind.library;
diff --git a/lib/json_info_codec.dart b/lib/json_info_codec.dart
index 1308d4d..b042a40 100644
--- a/lib/json_info_codec.dart
+++ b/lib/json_info_codec.dart
@@ -300,7 +300,7 @@
   Map _visitBasicInfo(BasicInfo info) {
     var res = {
       'id': info.serializedId,
-      'kind': _kindToString(info.kind),
+      'kind': kindToString(info.kind),
       'name': info.name,
       'size': info.size,
     };
diff --git a/pubspec.yaml b/pubspec.yaml
index 88b1336..1a43066 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: dart2js_info
-version: 0.2.4
+version: 0.2.5
 description: >
   Libraries and tools to process data produced when running dart2js with
   --dump-info.