Default language version (#2748)

* Added a default language version of 2.7

* Fixed tests
diff --git a/lib/src/entrypoint.dart b/lib/src/entrypoint.dart
index 574c25e..b9f9eed 100644
--- a/lib/src/entrypoint.dart
+++ b/lib/src/entrypoint.dart
@@ -18,6 +18,7 @@
 import 'executable.dart';
 import 'http.dart' as http;
 import 'io.dart';
+import 'language_version.dart';
 import 'lock_file.dart';
 import 'log.dart' as log;
 import 'package.dart';
@@ -753,7 +754,7 @@
       try {
         // Load `pubspec.yaml` and extract language version to compare with the
         // language version from `package_config.json`.
-        final languageVersion = extractLanguageVersion(
+        final languageVersion = LanguageVersion.fromSdkConstraint(
           cache.load(id).pubspec.sdkConstraints[sdk.identifier],
         );
         if (pkg.languageVersion != languageVersion) {
diff --git a/lib/src/language_version.dart b/lib/src/language_version.dart
index 4018e3c..ff7a67c 100644
--- a/lib/src/language_version.dart
+++ b/lib/src/language_version.dart
@@ -5,6 +5,8 @@
 import 'package:analyzer/dart/ast/token.dart';
 import 'package:pub_semver/pub_semver.dart';
 
+final _languageVersionPattern = RegExp(r'^(\d+)\.(\d+)$');
+
 /// A Dart language version as defined by
 /// https://github.com/dart-lang/language/blob/master/accepted/future-releases/language-versioning/feature-specification.md
 class LanguageVersion implements Comparable<LanguageVersion> {
@@ -14,20 +16,52 @@
   const LanguageVersion(this.major, this.minor);
 
   /// The language version implied by a Dart sdk version.
-  factory LanguageVersion.fromVersion(Version version) =>
-      LanguageVersion(version.major, version.minor);
+  factory LanguageVersion.fromVersion(Version version) {
+    ArgumentError.checkNotNull(version, 'version');
+    return LanguageVersion(version.major, version.minor);
+  }
 
-  /// The language version implied by a Dart sdk version range.
-  ///
-  /// Throws if the versionRange has no lower bound.
-  factory LanguageVersion.fromVersionRange(VersionRange range) {
-    final min = range.min;
-    if (min == null) {
-      // TODO(sigurdm): is this right?
-      throw ArgumentError(
-          'Version range with no lower bound does not imply a language version');
+  /// Parse language version from string.
+  factory LanguageVersion.parse(String languageVersion) {
+    ArgumentError.checkNotNull(languageVersion, 'languageVersion');
+    final m = _languageVersionPattern.firstMatch(languageVersion);
+    if (m == null) {
+      throw FormatException(
+        'Invalid language version string',
+        languageVersion,
+      );
     }
-    return LanguageVersion(min.major, min.minor);
+    return LanguageVersion(
+      int.parse(m.group(1)),
+      int.parse(m.group(2)),
+    );
+  }
+
+  /// The language version implied by a Dart SDK constraint in `pubspec.yaml`.
+  /// (this is `environment: {sdk: '>=2.0.0 <3.0.0'}` from `pubspec.yaml`)
+  ///
+  /// Fallbacks to [defaultLanguageVersion] if there is no [sdkConstraint] or
+  /// the [sdkConstraint] has no lower-bound.
+  factory LanguageVersion.fromSdkConstraint(VersionConstraint sdkConstraint) {
+    if (sdkConstraint == null || sdkConstraint.isEmpty) {
+      return defaultLanguageVersion;
+    } else if (sdkConstraint is Version) {
+      return LanguageVersion.fromVersion(sdkConstraint);
+    } else if (sdkConstraint is VersionRange) {
+      if (sdkConstraint.min != null) {
+        return LanguageVersion.fromVersion(sdkConstraint.min);
+      }
+      return defaultLanguageVersion;
+    } else if (sdkConstraint is VersionUnion) {
+      // `ranges` is non-empty and sorted.
+      final min = sdkConstraint.ranges.first.min;
+      if (min != null) {
+        return LanguageVersion.fromVersion(min);
+      }
+      return defaultLanguageVersion;
+    } else {
+      throw ArgumentError('Unknown VersionConstraint type $sdkConstraint.');
+    }
   }
 
   /// The language version implied by a Dart sdk version.
@@ -48,8 +82,18 @@
   bool operator <=(LanguageVersion other) => compareTo(other) <= 0;
   bool operator >=(LanguageVersion other) => compareTo(other) >= 0;
 
+  @override
+  int get hashCode => major ^ minor;
+
+  @override
+  bool operator ==(Object other) =>
+      other is LanguageVersion && other.minor == minor && other.major == major;
+
+  static const defaultLanguageVersion = LanguageVersion(2, 7);
   static const firstVersionWithNullSafety = LanguageVersion(2, 12);
 
+  /// Transform language version to string that can be parsed with
+  /// [LanguageVersion.parse].
   @override
   String toString() => '$major.$minor';
 }
diff --git a/lib/src/lock_file.dart b/lib/src/lock_file.dart
index da677d0..23cf4e9 100644
--- a/lib/src/lock_file.dart
+++ b/lib/src/lock_file.dart
@@ -14,6 +14,7 @@
 import 'package:yaml/yaml.dart';
 
 import 'io.dart';
+import 'language_version.dart';
 import 'package_config.dart';
 import 'package_name.dart';
 import 'sdk.dart' show sdk;
@@ -268,7 +269,7 @@
         name: name,
         rootUri: rootUri,
         packageUri: p.toUri('lib/'),
-        languageVersion: extractLanguageVersion(sdkConstraint),
+        languageVersion: LanguageVersion.fromSdkConstraint(sdkConstraint),
       ));
     }
 
@@ -277,7 +278,9 @@
         name: entrypoint,
         rootUri: p.toUri('../'),
         packageUri: p.toUri('lib/'),
-        languageVersion: extractLanguageVersion(entrypointSdkConstraint),
+        languageVersion: LanguageVersion.fromSdkConstraint(
+          entrypointSdkConstraint,
+        ),
       ));
     }
 
diff --git a/lib/src/null_safety_analysis.dart b/lib/src/null_safety_analysis.dart
index 204b4b2..ab9777e 100644
--- a/lib/src/null_safety_analysis.dart
+++ b/lib/src/null_safety_analysis.dart
@@ -150,8 +150,7 @@
           packageDir = boundSource.getDirectory(dependencyId);
         }
 
-        final languageVersion = pubspec.languageVersion;
-        if (languageVersion == null || !languageVersion.supportsNullSafety) {
+        if (!pubspec.languageVersion.supportsNullSafety) {
           final span =
               _tryGetSpanFromYamlMap(pubspec.fields['environment'], 'sdk');
           final where = span == null
diff --git a/lib/src/package_config.dart b/lib/src/package_config.dart
index 0c876b3..340860b 100644
--- a/lib/src/package_config.dart
+++ b/lib/src/package_config.dart
@@ -6,6 +6,8 @@
 
 import 'package:pub_semver/pub_semver.dart';
 
+import 'language_version.dart';
+
 /// Contents of a `.dart_tool/package_config.json` file.
 class PackageConfig {
   /// Version of the configuration in the `.dart_tool/package_config.json` file.
@@ -139,8 +141,6 @@
       }..addAll(additionalProperties ?? {});
 }
 
-final _languageVersionPattern = RegExp(r'^\d+\.\d+$');
-
 class PackageConfigEntry {
   /// Package name.
   String name;
@@ -166,7 +166,7 @@
   /// in the `pubspec.yaml` for the given package.
   ///
   /// This property is **optional** and may be `null` if not given.
-  String languageVersion;
+  LanguageVersion languageVersion;
 
   /// Additional properties not in the specification for the
   /// `.dart_tool/package_config.json` file.
@@ -225,12 +225,15 @@
       }
     }
 
-    final languageVersion = root['languageVersion'];
-    if (languageVersion != null) {
-      if (languageVersion is! String) {
+    LanguageVersion languageVersion;
+    final languageVersionRaw = root['languageVersion'];
+    if (languageVersionRaw != null) {
+      if (languageVersionRaw is! String) {
         _throw('languageVersion', 'must be a string');
       }
-      if (!_languageVersionPattern.hasMatch(languageVersion)) {
+      try {
+        languageVersion = LanguageVersion.parse(languageVersionRaw);
+      } on FormatException {
         _throw('languageVersion', 'must be on the form <major>.<minor>');
       }
     }
@@ -248,29 +251,6 @@
         'name': name,
         'rootUri': rootUri.toString(),
         if (packageUri != null) 'packageUri': packageUri?.toString(),
-        if (languageVersion != null) 'languageVersion': languageVersion,
+        if (languageVersion != null) 'languageVersion': '$languageVersion',
       }..addAll(additionalProperties ?? {});
 }
-
-/// Extract the _language version_ from an SDK constraint from `pubspec.yaml`.
-///
-/// This returns `null` if there is no language version.
-String extractLanguageVersion(VersionConstraint c) {
-  Version minVersion;
-  if (c == null || c.isEmpty) {
-    return null;
-  } else if (c is Version) {
-    minVersion = c;
-  } else if (c is VersionRange) {
-    minVersion = c.min;
-  } else if (c is VersionUnion) {
-    // `ranges` is non-empty and sorted.
-    minVersion = c.ranges.first.min;
-  } else {
-    throw ArgumentError('Unknown VersionConstraint type $c.');
-  }
-  if (minVersion == null) {
-    return null;
-  }
-  return '${minVersion.major}.${minVersion.minor}';
-}
diff --git a/lib/src/pubspec.dart b/lib/src/pubspec.dart
index 8a9ad5c..a8c3ed2 100644
--- a/lib/src/pubspec.dart
+++ b/lib/src/pubspec.dart
@@ -447,15 +447,8 @@
       name == null && version == Version.none && dependencies.isEmpty;
 
   /// The language version implied by the sdk constraint.
-  ///
-  /// Given no or unbounded constraint we assume language version 1.0.
-  LanguageVersion get languageVersion {
-    final constraint = originalDartSdkConstraint;
-    if (constraint is VersionRange && constraint.min != null) {
-      return LanguageVersion.fromVersionRange(constraint);
-    }
-    return LanguageVersion(1, 0);
-  }
+  LanguageVersion get languageVersion =>
+      LanguageVersion.fromSdkConstraint(originalDartSdkConstraint);
 
   /// Loads the pubspec for a package located in [packageDir].
   ///
diff --git a/lib/src/validator/relative_version_numbering.dart b/lib/src/validator/relative_version_numbering.dart
index 1eff1e0..fa3ae53 100644
--- a/lib/src/validator/relative_version_numbering.dart
+++ b/lib/src/validator/relative_version_numbering.dart
@@ -4,14 +4,10 @@
 
 import 'dart:async';
 
-import 'package:pub_semver/pub_semver.dart';
-
 import '../entrypoint.dart';
 import '../exceptions.dart';
-import '../language_version.dart';
 import '../null_safety_analysis.dart';
 import '../package_name.dart';
-import '../pubspec.dart';
 import '../validator.dart';
 
 /// Gives a warning when publishing a new version, if the latest published
@@ -45,8 +41,9 @@
     final previousPubspec =
         await hostedSource.bind(entrypoint.cache).describe(previousVersion);
 
-    final currentOptedIn = _optedIntoNullSafety(entrypoint.root.pubspec);
-    final previousOptedIn = _optedIntoNullSafety(previousPubspec);
+    final currentOptedIn =
+        entrypoint.root.pubspec.languageVersion.supportsNullSafety;
+    final previousOptedIn = previousPubspec.languageVersion.supportsNullSafety;
 
     if (currentOptedIn && !previousOptedIn) {
       hints.add(
@@ -61,21 +58,4 @@
           'See $semverUrl for information about versioning.');
     }
   }
-
-  static bool _optedIntoNullSafety(Pubspec pubspec) {
-    final sdkConstraint = 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 false;
-    final constraintMin = (sdkConstraint as VersionRange).min;
-
-    if (constraintMin == null) return false;
-
-    return LanguageVersion.fromVersionRange(sdkConstraint).supportsNullSafety;
-  }
 }
diff --git a/test/descriptor.dart b/test/descriptor.dart
index 6265531..2bcd14c 100644
--- a/test/descriptor.dart
+++ b/test/descriptor.dart
@@ -6,6 +6,7 @@
 import 'dart:convert';
 
 import 'package:oauth2/oauth2.dart' as oauth2;
+import 'package:pub/src/language_version.dart';
 import 'package:pub/src/package_config.dart';
 import 'package:test_descriptor/test_descriptor.dart';
 import 'package:meta/meta.dart';
@@ -257,7 +258,8 @@
     name: name,
     rootUri: rootUri,
     packageUri: Uri(path: 'lib/'),
-    languageVersion: languageVersion,
+    languageVersion:
+        languageVersion != null ? LanguageVersion.parse(languageVersion) : null,
   );
 }
 
diff --git a/test/package_config_file_test.dart b/test/package_config_file_test.dart
index a08bc20..a1450ca 100644
--- a/test/package_config_file_test.dart
+++ b/test/package_config_file_test.dart
@@ -32,14 +32,17 @@
           d.packageConfigEntry(
             name: 'foo',
             version: '1.2.3',
+            languageVersion: '2.7',
           ),
           d.packageConfigEntry(
             name: 'bar',
             version: '3.2.1',
+            languageVersion: '2.7',
           ),
           d.packageConfigEntry(
             name: 'baz',
             version: '2.2.2',
+            languageVersion: '2.7',
           ),
           d.packageConfigEntry(
             name: 'myapp',
@@ -69,6 +72,7 @@
           d.packageConfigEntry(
             name: 'notFoo',
             version: '9.9.9',
+            languageVersion: '2.7',
           ),
         ]),
       ]);
@@ -82,14 +86,17 @@
           d.packageConfigEntry(
             name: 'foo',
             version: '1.2.3',
+            languageVersion: '2.7',
           ),
           d.packageConfigEntry(
             name: 'bar',
             version: '3.2.1',
+            languageVersion: '2.7',
           ),
           d.packageConfigEntry(
             name: 'baz',
             version: '2.2.2',
+            languageVersion: '2.7',
           ),
           d.packageConfigEntry(
             name: 'myapp',
@@ -154,10 +161,12 @@
           d.packageConfigEntry(
             name: 'foo',
             version: '1.2.3',
+            languageVersion: '2.7',
           ),
           d.packageConfigEntry(
             name: 'baz',
             path: '../local_baz',
+            languageVersion: '2.7',
           ),
           d.packageConfigEntry(
             name: 'myapp',
@@ -213,14 +222,14 @@
       ]).validate();
     });
 
-    test('package_config.json has no default language version', () async {
+    test('package_config.json has 2.7 default language version', () async {
       await servePackages((builder) {
         builder.serve(
           'foo',
           '1.2.3',
           pubspec: {
             'environment': {
-              'sdk': '>=0.0.1 <=0.2.2+2', // tests runs with '0.1.2+3'
+              'sdk': 'any',
             },
           },
           contents: [d.dir('lib', [])],
@@ -244,7 +253,7 @@
           d.packageConfigEntry(
             name: 'foo',
             version: '1.2.3',
-            languageVersion: '0.0',
+            languageVersion: '2.7',
           ),
           d.packageConfigEntry(
             name: 'myapp',