Validate language version opt-ins (#2338)

Makes it an error to opt-in to a newer language version that what you declare in pubspec.
diff --git a/lib/src/validator.dart b/lib/src/validator.dart
index 1317860..51fa569 100644
--- a/lib/src/validator.dart
+++ b/lib/src/validator.dart
@@ -19,6 +19,7 @@
 import 'validator/directory.dart';
 import 'validator/executable.dart';
 import 'validator/flutter_plugin_format.dart';
+import 'validator/language_version.dart';
 import 'validator/license.dart';
 import 'validator/name.dart';
 import 'validator/pubspec.dart';
@@ -126,6 +127,7 @@
       SdkConstraintValidator(entrypoint),
       StrictDependenciesValidator(entrypoint),
       FlutterPluginFormatValidator(entrypoint),
+      LanguageVersionValidator(entrypoint),
     ];
     if (packageSize != null) {
       validators.add(SizeValidator(entrypoint, packageSize));
diff --git a/lib/src/validator/language_version.dart b/lib/src/validator/language_version.dart
new file mode 100644
index 0000000..09bee71
--- /dev/null
+++ b/lib/src/validator/language_version.dart
@@ -0,0 +1,74 @@
+// Copyright (c) 2020, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:analyzer/dart/ast/ast.dart';
+import 'package:path/path.dart' as p;
+import 'package:pub_semver/pub_semver.dart';
+import 'package:stack_trace/stack_trace.dart';
+
+import '../dart.dart';
+import '../entrypoint.dart';
+import '../log.dart' as log;
+import '../utils.dart';
+import '../validator.dart';
+
+/// Validates that libraries do not opt into newer language versions than what
+/// they declare in their pubspec.
+class LanguageVersionValidator extends Validator {
+  final AnalysisContextManager analysisContextManager =
+      AnalysisContextManager();
+
+  LanguageVersionValidator(Entrypoint entrypoint) : super(entrypoint) {
+    var packagePath = p.normalize(p.absolute(entrypoint.root.dir));
+    analysisContextManager.createContextsForDirectory(packagePath);
+  }
+
+  @override
+  Future validate() async {
+    final sdkConstraint = entrypoint.root.pubspec.originalDartSdkConstraint;
+
+    /// If the sdk constraint is not a `VersionRange` something is wrong, and
+    /// we cannot deduce the language version.
+    ///
+    /// This will hopefully be detected elsewhere.
+    ///
+    /// A single `Version` is also a `VersionRange`.
+    if (sdkConstraint is! VersionRange) return;
+
+    final packageSdkMinVersion = (sdkConstraint as VersionRange).min;
+    for (final path in ['lib', 'bin']
+        .map((path) => entrypoint.root.listFiles(beneath: path))
+        .expand((files) => files)
+        .map(p.absolute)
+        .where((String file) => p.extension(file) == '.dart')) {
+      CompilationUnit unit;
+      try {
+        unit = analysisContextManager.parse(path);
+      } on AnalyzerErrorGroup catch (e, s) {
+        // Ignore files that do not parse.
+        log.fine(getErrorMessage(e));
+        log.fine(Chain.forTrace(s).terse);
+        continue;
+      }
+
+      final unitLanguageVersion = unit.languageVersion;
+      if (unitLanguageVersion != null) {
+        if (Version(unitLanguageVersion.major, unitLanguageVersion.minor, 0) >
+            packageSdkMinVersion) {
+          final packageLanguageVersionString =
+              '${packageSdkMinVersion.major}.${packageSdkMinVersion.minor}';
+          final unitLanguageVersionString =
+              '${unitLanguageVersion.major}.${unitLanguageVersion.minor}';
+          final relativePath = p.relative(path);
+          errors.add('$relativePath is declaring language version '
+              '$unitLanguageVersionString that is newer than the SDK '
+              'constraint $packageLanguageVersionString declared in '
+              '`pubspec.yaml`.');
+        }
+      }
+    }
+  }
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index 6b74cd3..44b3e26 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -33,3 +33,16 @@
   test: ^1.3.0
   test_descriptor: ^1.0.0
   test_process: ^1.0.0
+
+dependency_overrides:
+  # Use a never version of these packages while waiting for a release.
+  analyzer:
+    git:
+      url: https://github.com/dart-lang/sdk.git
+      ref: bbb449dfa44ff40d3192959731b5893cbf66b205
+      path: pkg/analyzer
+  _fe_analyzer_shared:
+    git:
+      url: https://github.com/dart-lang/sdk.git
+      ref: bbb449dfa44ff40d3192959731b5893cbf66b205
+      path: pkg/_fe_analyzer_shared
diff --git a/test/validator/language_version_test.dart b/test/validator/language_version_test.dart
new file mode 100644
index 0000000..304b68a
--- /dev/null
+++ b/test/validator/language_version_test.dart
@@ -0,0 +1,75 @@
+// Copyright (c) 2020, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:test/test.dart';
+
+import 'package:pub/src/entrypoint.dart';
+import 'package:pub/src/validator.dart';
+import 'package:pub/src/validator/language_version.dart';
+
+import '../descriptor.dart' as d;
+import '../test_pub.dart';
+import 'utils.dart';
+
+Validator validator(Entrypoint entrypoint) =>
+    LanguageVersionValidator(entrypoint);
+
+Future<void> setup(
+    {String sdkConstraint, String libraryLanguageVersion}) async {
+  await d.validPackage.create();
+  await d.dir(appPath, [
+    d.pubspec({
+      'name': 'test_pkg',
+      'version': '1.0.0',
+      'environment': {'sdk': sdkConstraint},
+    }),
+    d.dir('lib', [
+      if (libraryLanguageVersion != null)
+        d.file('library.dart', '// @dart = $libraryLanguageVersion\n'),
+    ])
+  ]).create();
+  await pubGet(environment: {'_PUB_TEST_SDK_VERSION': '2.7.0'});
+  print(await d.file('.dart_tool/package_config.json').read());
+}
+
+void main() {
+  group('should consider a package valid if it', () {
+    test('has no library-level language version annotations', () async {
+      await setup(sdkConstraint: '>=2.4.0 <3.0.0');
+      expectNoValidationError(validator);
+    });
+
+    test('opts in to older language versions', () async {
+      await setup(
+          sdkConstraint: '>=2.4.0 <3.0.0', libraryLanguageVersion: '2.0');
+      await d.dir(appPath, []).create();
+      expectNoValidationError(validator);
+    });
+    test('opts in to same language versions', () async {
+      await setup(
+          sdkConstraint: '>=2.4.0 <3.0.0', libraryLanguageVersion: '2.4');
+      await d.dir(appPath, []).create();
+      expectNoValidationError(validator);
+    });
+
+    test('opts in to older language version, with non-range constraint',
+        () async {
+      await setup(sdkConstraint: '2.7.0', libraryLanguageVersion: '2.3');
+      await d.dir(appPath, []).create();
+      expectNoValidationError(validator);
+    });
+  });
+
+  group('should error if it', () {
+    test('opts in to a newer version.', () async {
+      await setup(
+          sdkConstraint: '>=2.4.1 <3.0.0', libraryLanguageVersion: '2.5');
+      expectValidationError(validator);
+    });
+    test('opts in to a newer version, with non-range constraint.', () async {
+      await setup(sdkConstraint: '2.7.0', libraryLanguageVersion: '2.8');
+      expectValidationError(validator);
+    });
+  });
+}