Change logic for asserting package_config.json is up-to-date. (#2334)

* Change logic for asserting package_config.json is up-to-date.

 * Documented the reasoning behind these assertions.
 * Changed how we assert to forbid packages that are not in the
   lockfile.
diff --git a/lib/src/entrypoint.dart b/lib/src/entrypoint.dart
index e6c1ad5..52a913f 100644
--- a/lib/src/entrypoint.dart
+++ b/lib/src/entrypoint.dart
@@ -430,6 +430,12 @@
 
     var touchedLockFile = false;
     if (lockFileModified.isBefore(pubspecModified) || hasPathDependencies) {
+      // If `pubspec.lock` is newer than `pubspec.yaml` or we have path
+      // dependencies, then we check that `pubspec.lock` is a correct solution
+      // for the requirements in `pubspec.yaml`. This aims to:
+      //  * Prevent missing packages when `pubspec.lock` is checked into git.
+      //  * Mitigate missing transitive dependencies when the `pubspec.yaml` in
+      //    a path dependency is changed.
       _assertLockFileUpToDate();
       if (_arePackagesAvailable()) {
         touchedLockFile = true;
@@ -451,6 +457,11 @@
     var packageConfigModified = File(packageConfigFile).lastModifiedSync();
     if (packageConfigModified.isBefore(lockFileModified) ||
         hasPathDependencies) {
+      // If `package_config.json` is newer than `pubspec.lock` or we have
+      // path dependencies, then we check that `package_config.json` is a
+      // correct configuration on the local machine. This aims to:
+      //  * Mitigate issues when copying a folder from one machine to another.
+      //  * Force `pub get` if a path dependency has changed language verison.
       _checkPackageConfigUpToDate();
       touch(packageConfigFile);
     } else if (touchedLockFile) {
@@ -537,45 +548,60 @@
     });
   }
 
-  /// Determines [lockfile] agrees with the given [packagePathsMapping].
+  /// Determines [lockFile] agrees with the given [packagePathsMapping].
   ///
   /// The [packagePathsMapping] is a mapping from package names to paths where
   /// the packages are located. (The library is located under
   /// `lib/` relative to the path given).
   bool _isPackagePathsMappingUpToDateWithLockfile(
-          Map<String, String> packagePathsMapping) =>
-      lockFile.packages.values.every((lockFileId) {
-        // It's very unlikely that the lockfile is invalid here, but it's not
-        // impossible—for example, the user may have a very old application
-        // package with a checked-in lockfile that's newer than the pubspec, but
-        // that contains SDK dependencies.
-        if (lockFileId.source is UnknownSource) return false;
+    Map<String, String> packagePathsMapping,
+  ) {
+    // Check that [packagePathsMapping] does not contain more packages than what
+    // is required. This could lead to import statements working, when they are
+    // not supposed to work.
+    final hasExtraMappings = packagePathsMapping.keys.every((packageName) {
+      return packageName == root.name ||
+          lockFile.packages.containsKey(packageName);
+    });
+    if (!hasExtraMappings) {
+      return false;
+    }
 
-        final packagePath = packagePathsMapping[lockFileId.name];
-        if (packagePath == null) {
-          return false;
-        }
+    // Check that all packages in the [lockFile] are reflected in the
+    // [packagePathsMapping].
+    return lockFile.packages.values.every((lockFileId) {
+      // It's very unlikely that the lockfile is invalid here, but it's not
+      // impossible—for example, the user may have a very old application
+      // package with a checked-in lockfile that's newer than the pubspec, but
+      // that contains SDK dependencies.
+      if (lockFileId.source is UnknownSource) return false;
 
-        final source = cache.source(lockFileId.source);
-        final lockFilePackagePath =
-            p.join(root.dir, source.getDirectory(lockFileId));
+      final packagePath = packagePathsMapping[lockFileId.name];
+      if (packagePath == null) {
+        return false;
+      }
 
-        // Make sure that the packagePath agrees with the lock file about the
-        // path to the package.
-        if (p.normalize(packagePath) != p.normalize(lockFilePackagePath)) {
-          return false;
-        }
+      final source = cache.source(lockFileId.source);
+      final lockFilePackagePath =
+          p.join(root.dir, source.getDirectory(lockFileId));
 
-        // For cached sources, make sure the directory exists and looks like a
-        // package. This is also done by [_arePackagesAvailable] but that may not
-        // be run if the lockfile is newer than the pubspec.
-        if (source is CachedSource && !dirExists(lockFilePackagePath) ||
-            !fileExists(p.join(lockFilePackagePath, 'pubspec.yaml'))) {
-          return false;
-        }
+      // Make sure that the packagePath agrees with the lock file about the
+      // path to the package.
+      if (p.normalize(packagePath) != p.normalize(lockFilePackagePath)) {
+        return false;
+      }
 
-        return true;
-      });
+      // For cached sources, make sure the directory exists and looks like a
+      // package. This is also done by [_arePackagesAvailable] but that may not
+      // be run if the lockfile is newer than the pubspec.
+      if (source is CachedSource && !dirExists(lockFilePackagePath) ||
+          !fileExists(p.join(lockFilePackagePath, 'pubspec.yaml'))) {
+        return false;
+      }
+
+      return true;
+    });
+  }
 
   /// Checks whether or not the `.packages` file is out of date with respect
   /// to the [lockfile].
@@ -646,9 +672,12 @@
     } on FormatException {
       badPackageConfig();
     }
-    if (cfg.configVersion != 2 ||
-        cfg.generator != 'pub' ||
-        cfg.generatorVersion != sdk.version) {
+
+    // Version 2 is the initial version number for `package_config.json`,
+    // because `.packages` was version 1 (even if it was a different file).
+    // If the version is different from 2, then it must be a newer incompatible
+    // version, hence, the user should run `pub get` with the downgraded SDK.
+    if (cfg.configVersion != 2) {
       badPackageConfig();
     }
 
@@ -665,18 +694,29 @@
       outOfDate();
     }
 
+    // Check if language version specified in the `package_config.json` is
+    // correct. This is important for path dependencies as these can mutate.
     for (final pkg in cfg.packages) {
       final id = lockFile.packages[pkg.name];
       if (id == null) {
-        continue; // Ignore entries that aren't in the lockFile
+        assert(
+          false,
+          'unnecessary package_config.json entries should be forbidden by '
+          '_isPackagePathsMappingUpToDateWithLockfile',
+        );
+        continue;
       }
 
+      // If a package is cached, then it's universally immutable and we need
+      // not check if the language version is correct.
       final source = cache.source(id.source);
       if (source is CachedSource) {
         continue;
       }
 
       try {
+        // Load `pubspec.yaml` and extract language version to compare with the
+        // language version from `package_config.json`.
         final languageVersion = extractLanguageVersion(
           cache.load(id).pubspec.sdkConstraints[sdk.identifier],
         );