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);
+ });
+ });
+}