Migrate to null safety (#52)

- Switch to a major version bump.
- Reformat `.travis.yml`. Indent list elements to the same position as
  the containing key. This matches yaml style in other Google files.
- Update SDK constraints to use the `2.11.0` dev SDKs.
- Renames some variables with noise word "the" to match the field name
  and avoid the shadowing with `this.`.
- Add override for analyzer package.

Co-authored-by: Nate Bosch <nbosch1@gmail.com>
diff --git a/.travis.yml b/.travis.yml
index 0e732d0..e40520c 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,23 +1,12 @@
 language: dart
 
 dart:
-  - dev
-  - 2.0.0
+- dev
 
 dart_task:
-  - test
-
-matrix:
-  include:
-    # Only validate formatting using the dev release
-    - dart: dev
-      dart_task: dartfmt
-    - dart: dev
-      dart_task:
-        dartanalyzer: --fatal-warnings --fatal-hints .
-    - dart: 2.0.0
-      dart_task:
-        dartanalyzer: --fatal-warnings .
+- test: -p vm,chrome
+- dart_task: dartfmt
+- dartanalyzer: --fatal-infos .
 
 # Only building master means that we don't run two builds for each pull request.
 branches:
@@ -25,4 +14,4 @@
 
 cache:
  directories:
-   - $HOME/.pub-cache
+ - $HOME/.pub-cache
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 63f7285..3719707 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,7 @@
-# 1.4.5-dev
+# 2.0.0-nullsafety.0
+
+- Migrate to null safety.
+- `Version.primary` now throws `StateError` if the `versions` argument is empty.
 
 # 1.4.4
 
diff --git a/lib/src/utils.dart b/lib/src/utils.dart
index 60617f2..a9f714f 100644
--- a/lib/src/utils.dart
+++ b/lib/src/utils.dart
@@ -19,7 +19,7 @@
   if (range1.min == null) return range2.min != null;
   if (range2.min == null) return false;
 
-  var comparison = range1.min.compareTo(range2.min);
+  var comparison = range1.min!.compareTo(range2.min!);
   if (comparison == -1) return true;
   if (comparison == 1) return false;
   return range1.includeMin && !range2.includeMin;
@@ -30,7 +30,7 @@
   if (range1.max == null) return range2.max != null;
   if (range2.max == null) return false;
 
-  var comparison = range1.max.compareTo(range2.max);
+  var comparison = range1.max!.compareTo(range2.max!);
   if (comparison == 1) return true;
   if (comparison == -1) return false;
   return range1.includeMax && !range2.includeMax;
@@ -41,7 +41,7 @@
 bool strictlyLower(VersionRange range1, VersionRange range2) {
   if (range1.max == null || range2.min == null) return false;
 
-  var comparison = range1.max.compareTo(range2.min);
+  var comparison = range1.max!.compareTo(range2.min!);
   if (comparison == -1) return true;
   if (comparison == 1) return false;
   return !range1.includeMax || !range2.includeMin;
diff --git a/lib/src/version.dart b/lib/src/version.dart
index 314937d..552b498 100644
--- a/lib/src/version.dart
+++ b/lib/src/version.dart
@@ -92,8 +92,8 @@
   @override
   bool get includeMax => true;
 
-  Version._(this.major, this.minor, this.patch, String preRelease, String build,
-      this._text)
+  Version._(this.major, this.minor, this.patch, String? preRelease,
+      String? build, this._text)
       : preRelease = preRelease == null ? [] : _splitParts(preRelease),
         build = build == null ? [] : _splitParts(build) {
     if (major < 0) throw ArgumentError('Major version must be non-negative.');
@@ -102,7 +102,8 @@
   }
 
   /// Creates a new [Version] object.
-  factory Version(int major, int minor, int patch, {String pre, String build}) {
+  factory Version(int major, int minor, int patch,
+      {String? pre, String? build}) {
     var text = '$major.$minor.$patch';
     if (pre != null) text += '-$pre';
     if (build != null) text += '+$build';
@@ -118,9 +119,9 @@
     }
 
     try {
-      var major = int.parse(match[1]);
-      var minor = int.parse(match[2]);
-      var patch = int.parse(match[3]);
+      var major = int.parse(match[1]!);
+      var minor = int.parse(match[2]!);
+      var patch = int.parse(match[3]!);
 
       var preRelease = match[5];
       var build = match[8];
@@ -136,12 +137,11 @@
   /// This is the highest-numbered stable (non-prerelease) version. If there
   /// are no stable versions, it's just the highest-numbered version.
   ///
-  /// If [versions] is empty, returns `null`.
+  /// If [versions] is empty, throws a [StateError].
   static Version primary(List<Version> versions) {
-    Version primary;
-    for (var version in versions) {
-      if (primary == null ||
-          (!version.isPreRelease && primary.isPreRelease) ||
+    var primary = versions.first;
+    for (var version in versions.skip(1)) {
+      if ((!version.isPreRelease && primary.isPreRelease) ||
           (version.isPreRelease == primary.isPreRelease && version > primary)) {
         primary = version;
       }
diff --git a/lib/src/version_constraint.dart b/lib/src/version_constraint.dart
index ac39b6b..6eb6c3a 100644
--- a/lib/src/version_constraint.dart
+++ b/lib/src/version_constraint.dart
@@ -57,16 +57,16 @@
     if (text == 'any') return any;
 
     // Try to parse and consume a version number.
-    Version matchVersion() {
+    Version? matchVersion() {
       var version = startVersion.firstMatch(text);
       if (version == null) return null;
 
       text = text.substring(version.end);
-      return Version.parse(version[0]);
+      return Version.parse(version[0]!);
     }
 
     // Try to parse and consume a comparison operator followed by a version.
-    VersionRange matchComparison() {
+    VersionRange? matchComparison() {
       var comparison = startComparison.firstMatch(text);
       if (comparison == null) return null;
 
@@ -97,7 +97,7 @@
     }
 
     // Try to parse the "^" operator followed by a version.
-    VersionConstraint matchCompatibleWith() {
+    VersionConstraint? matchCompatibleWith() {
       if (!text.startsWith(compatibleWithChar)) return null;
 
       text = text.substring(compatibleWithChar.length);
@@ -120,9 +120,9 @@
     var compatibleWith = matchCompatibleWith();
     if (compatibleWith != null) return compatibleWith;
 
-    Version min;
+    Version? min;
     var includeMin = false;
-    Version max;
+    Version? max;
     var includeMax = false;
 
     for (;;) {
@@ -137,7 +137,7 @@
       }
 
       if (newRange.min != null) {
-        if (min == null || newRange.min > min) {
+        if (min == null || newRange.min! > min) {
           min = newRange.min;
           includeMin = newRange.includeMin;
         } else if (newRange.min == min && !newRange.includeMin) {
@@ -146,7 +146,7 @@
       }
 
       if (newRange.max != null) {
-        if (max == null || newRange.max < max) {
+        if (max == null || newRange.max! < max) {
           max = newRange.max;
           includeMax = newRange.includeMax;
         } else if (newRange.max == max && !newRange.includeMax) {
diff --git a/lib/src/version_range.dart b/lib/src/version_range.dart
index 78cd6d0..a2a6327 100644
--- a/lib/src/version_range.dart
+++ b/lib/src/version_range.dart
@@ -25,7 +25,7 @@
   ///
   /// This may be `null` in which case the range has no minimum end and allows
   /// any version less than the maximum.
-  final Version min;
+  final Version? min;
 
   /// The maximum end of the range.
   ///
@@ -35,7 +35,7 @@
   ///
   /// This may be `null` in which case the range has no maximum end and allows
   /// any version greater than the minimum.
-  final Version max;
+  final Version? max;
 
   /// If `true` then [min] is allowed by the range.
   final bool includeMin;
@@ -58,8 +58,8 @@
   /// pre-release versions of an exclusive [max]. Otherwise, it will use the
   /// default behavior for pre-release versions of [max].
   factory VersionRange(
-      {Version min,
-      Version max,
+      {Version? min,
+      Version? max,
       bool includeMin = false,
       bool includeMax = false,
       bool alwaysIncludeMaxPreRelease = false}) {
@@ -111,12 +111,12 @@
   @override
   bool allows(Version other) {
     if (min != null) {
-      if (other < min) return false;
+      if (other < min!) return false;
       if (!includeMin && other == min) return false;
     }
 
     if (max != null) {
-      if (other > max) return false;
+      if (other > max!) return false;
       if (!includeMax && other == max) return false;
     }
 
@@ -167,7 +167,7 @@
 
     if (other is VersionRange) {
       // Intersect the two ranges.
-      Version intersectMin;
+      Version? intersectMin;
       bool intersectIncludeMin;
       if (allowsLower(this, other)) {
         if (strictlyLower(this, other)) return VersionConstraint.empty;
@@ -179,7 +179,7 @@
         intersectIncludeMin = includeMin;
       }
 
-      Version intersectMax;
+      Version? intersectMax;
       bool intersectIncludeMax;
       if (allowsHigher(this, other)) {
         intersectMax = other.max;
@@ -199,7 +199,7 @@
         // Because we already verified that the lower range isn't strictly
         // lower, there must be some overlap.
         assert(intersectIncludeMin && intersectIncludeMax);
-        return intersectMin;
+        return intersectMin!;
       }
 
       // If we got here, there is an actual range.
@@ -251,7 +251,7 @@
         return VersionConstraint.unionOf([this, other]);
       }
 
-      Version unionMin;
+      Version? unionMin;
       bool unionIncludeMin;
       if (allowsLower(this, other)) {
         unionMin = min;
@@ -261,7 +261,7 @@
         unionIncludeMin = other.includeMin;
       }
 
-      Version unionMax;
+      Version? unionMax;
       bool unionIncludeMax;
       if (allowsHigher(this, other)) {
         unionMax = max;
@@ -326,7 +326,7 @@
     } else if (other is VersionRange) {
       if (!allowsAny(other)) return this;
 
-      VersionRange before;
+      VersionRange? before;
       if (!allowsLower(this, other)) {
         before = null;
       } else if (min == other.min) {
@@ -342,7 +342,7 @@
             alwaysIncludeMaxPreRelease: true);
       }
 
-      VersionRange after;
+      VersionRange? after;
       if (!allowsHigher(this, other)) {
         after = null;
       } else if (max == other.max) {
@@ -359,7 +359,7 @@
       }
 
       if (before == null && after == null) return VersionConstraint.empty;
-      if (before == null) return after;
+      if (before == null) return after!;
       if (after == null) return before;
       return VersionUnion.fromRanges([before, after]);
     } else if (other is VersionUnion) {
@@ -404,7 +404,7 @@
       return 1;
     }
 
-    var result = min.compareTo(other.min);
+    var result = min!.compareTo(other.min!);
     if (result != 0) return result;
     if (includeMin != other.includeMin) return includeMin ? -1 : 1;
 
@@ -420,7 +420,7 @@
       return -1;
     }
 
-    var result = max.compareTo(other.max);
+    var result = max!.compareTo(other.max!);
     if (result != 0) return result;
     if (includeMax != other.includeMax) return includeMax ? 1 : -1;
     return 0;
@@ -430,10 +430,13 @@
   String toString() {
     var buffer = StringBuffer();
 
+    final min = this.min;
     if (min != null) {
       buffer..write(includeMin ? '>=' : '>')..write(min);
     }
 
+    final max = this.max;
+
     if (max != null) {
       if (min != null) buffer.write(' ');
       if (includeMax) {
diff --git a/lib/src/version_union.dart b/lib/src/version_union.dart
index 8a23522..24b82f4 100644
--- a/lib/src/version_union.dart
+++ b/lib/src/version_union.dart
@@ -50,19 +50,19 @@
 
     // Because both lists of ranges are ordered by minimum version, we can
     // safely move through them linearly here.
-    ourRanges.moveNext();
-    theirRanges.moveNext();
-    while (ourRanges.current != null && theirRanges.current != null) {
+    var ourRangesMoved = ourRanges.moveNext();
+    var theirRangesMoved = theirRanges.moveNext();
+    while (ourRangesMoved && theirRangesMoved) {
       if (ourRanges.current.allowsAll(theirRanges.current)) {
-        theirRanges.moveNext();
+        theirRangesMoved = theirRanges.moveNext();
       } else {
-        ourRanges.moveNext();
+        ourRangesMoved = ourRanges.moveNext();
       }
     }
 
     // If our ranges have allowed all of their ranges, we'll have consumed all
     // of them.
-    return theirRanges.current == null;
+    return !theirRangesMoved;
   }
 
   @override
@@ -72,9 +72,9 @@
 
     // Because both lists of ranges are ordered by minimum version, we can
     // safely move through them linearly here.
-    ourRanges.moveNext();
-    theirRanges.moveNext();
-    while (ourRanges.current != null && theirRanges.current != null) {
+    var ourRangesMoved = ourRanges.moveNext();
+    var theirRangesMoved = theirRanges.moveNext();
+    while (ourRangesMoved && theirRangesMoved) {
       if (ourRanges.current.allowsAny(theirRanges.current)) {
         return true;
       }
@@ -82,9 +82,9 @@
       // Move the constraint with the lower max value forward. This ensures that
       // we keep both lists in sync as much as possible.
       if (allowsHigher(theirRanges.current, ourRanges.current)) {
-        ourRanges.moveNext();
+        ourRangesMoved = ourRanges.moveNext();
       } else {
-        theirRanges.moveNext();
+        theirRangesMoved = theirRanges.moveNext();
       }
     }
 
@@ -99,9 +99,9 @@
     // Because both lists of ranges are ordered by minimum version, we can
     // safely move through them linearly here.
     var newRanges = <VersionRange>[];
-    ourRanges.moveNext();
-    theirRanges.moveNext();
-    while (ourRanges.current != null && theirRanges.current != null) {
+    var ourRangesMoved = ourRanges.moveNext();
+    var theirRangesMoved = theirRanges.moveNext();
+    while (ourRangesMoved && theirRangesMoved) {
       var intersection = ourRanges.current.intersect(theirRanges.current);
 
       if (!intersection.isEmpty) newRanges.add(intersection as VersionRange);
@@ -110,9 +110,9 @@
       // we keep both lists in sync as much as possible, and that large ranges
       // have a chance to match multiple small ranges that they contain.
       if (allowsHigher(theirRanges.current, ourRanges.current)) {
-        ourRanges.moveNext();
+        ourRangesMoved = ourRanges.moveNext();
       } else {
-        theirRanges.moveNext();
+        theirRangesMoved = theirRanges.moveNext();
       }
     }
 
diff --git a/pubspec.yaml b/pubspec.yaml
index 78de107..dc2d6ac 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,16 +1,18 @@
 name: pub_semver
-version: 1.4.5-dev
-
+version: 2.0.0-nullsafety.0
 description: >-
  Versions and version constraints implementing pub's versioning policy. This
  is very similar to vanilla semver, with a few corner cases.
-homepage: https://github.com/dart-lang/pub_semver
+repository: https://github.com/dart-lang/pub_semver
 
 environment:
- sdk: '>=2.0.0 <3.0.0'
+ sdk: '>=2.12.0-0 <3.0.0'
 
 dependencies:
-  collection: ^1.0.0
+  collection: ^1.15.0-nullsafety.2
 
 dev_dependencies:
-  test: ^1.0.0
+  test: ^1.16.0-nullsafety.1
+
+dependency_overrides:
+  analyzer: any
diff --git a/test/utils.dart b/test/utils.dart
index eb3358a..595415a 100644
--- a/test/utils.dart
+++ b/test/utils.dart
@@ -79,13 +79,13 @@
 /// Gets a [Matcher] that validates that a [VersionConstraint] allows all
 /// given versions.
 Matcher allows(Version v1,
-    [Version v2,
-    Version v3,
-    Version v4,
-    Version v5,
-    Version v6,
-    Version v7,
-    Version v8]) {
+    [Version? v2,
+    Version? v3,
+    Version? v4,
+    Version? v5,
+    Version? v6,
+    Version? v7,
+    Version? v8]) {
   var versions = _makeVersionList(v1, v2, v3, v4, v5, v6, v7, v8);
   return _VersionConstraintMatcher(versions, true);
 }
@@ -93,25 +93,25 @@
 /// Gets a [Matcher] that validates that a [VersionConstraint] allows none of
 /// the given versions.
 Matcher doesNotAllow(Version v1,
-    [Version v2,
-    Version v3,
-    Version v4,
-    Version v5,
-    Version v6,
-    Version v7,
-    Version v8]) {
+    [Version? v2,
+    Version? v3,
+    Version? v4,
+    Version? v5,
+    Version? v6,
+    Version? v7,
+    Version? v8]) {
   var versions = _makeVersionList(v1, v2, v3, v4, v5, v6, v7, v8);
   return _VersionConstraintMatcher(versions, false);
 }
 
 List<Version> _makeVersionList(Version v1,
-    [Version v2,
-    Version v3,
-    Version v4,
-    Version v5,
-    Version v6,
-    Version v7,
-    Version v8]) {
+    [Version? v2,
+    Version? v3,
+    Version? v4,
+    Version? v5,
+    Version? v6,
+    Version? v7,
+    Version? v8]) {
   var versions = [v1];
   if (v2 != null) versions.add(v2);
   if (v3 != null) versions.add(v3);
diff --git a/test/version_test.dart b/test/version_test.dart
index 70d2779..6ff4b3e 100644
--- a/test/version_test.dart
+++ b/test/version_test.dart
@@ -371,10 +371,7 @@
     });
 
     test('empty', () {
-      expect(
-        _primary([]),
-        isNull,
-      );
+      expect(() => Version.primary([]), throwsStateError);
     });
   });
 }