Enable null safety (#33)

diff --git a/.travis.yml b/.travis.yml
index 59d861d..cc94606 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,24 +1,34 @@
 language: dart
 
 dart:
-  - 2.7.0
   - dev
 
-dart_task:
-  - test: --platform vm
-    xvfb: false
-  - test: --platform firefox
-  - dartanalyzer: --fatal-infos --fatal-warnings .
-
-matrix:
+jobs:
   include:
-  # Only validate formatting using the dev release
-  - dart: dev
-    dart_task: dartfmt
+    - stage: analyze_and_format
+      name: "Analyze"
+      os: linux
+      script: dartanalyzer --enable-experiment=non-nullable --fatal-warnings --fatal-infos .
+    - stage: analyze_and_format
+      name: "Format"
+      os: linux
+      script: dartfmt -n --set-exit-if-changed .
+    - stage: test
+      name: "Vm Tests"
+      os: linux
+      script: pub run --enable-experiment=non-nullable test -p vm
+    - stage: test
+      name: "Web Tests"
+      os: linux
+      script: pub run --enable-experiment=non-nullable test -p chrome
+
+stages:
+  - analyze_and_format
+  - test
 
 # Only building master means that we don't run two builds for each pull request.
 branches:
-  only: [master]
+  only: [master, null_safety]
 
 cache:
   directories:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b9ad843..1bd1ae5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,13 @@
-## 3.1.5-dev
+## 3.2.0-nullsafety
 
-* Require minimum Dart SDK of `2.7.0`.
+Pre-release for the null safety migration of this package.
+
+Note that 3.2.0 may not be the final stable null safety release version, we
+reserve the right to release it as a 4.0.0 breaking change.
+
+This release will be pinned to only allow pre-release sdk versions starting from
+2.10.0-2.0.dev, which is the first version where this package will appear in the
+null safety allow list.
 
 ## 3.1.4
 
diff --git a/analysis_options.yaml b/analysis_options.yaml
index 69009d3..f98644b 100644
--- a/analysis_options.yaml
+++ b/analysis_options.yaml
@@ -3,6 +3,8 @@
 analyzer:
   strong-mode:
     implicit-casts: false
+  enable-experiment:
+  - non-nullable
 
 linter:
   rules:
diff --git a/lib/src/authentication_challenge.dart b/lib/src/authentication_challenge.dart
index 9b554e8..356e0a2 100644
--- a/lib/src/authentication_challenge.dart
+++ b/lib/src/authentication_challenge.dart
@@ -59,7 +59,7 @@
             if (scanner.matches(',') || scanner.isDone) continue;
 
             scanner.expect(token, name: 'a token');
-            final name = scanner.lastMatch[0];
+            final name = scanner.lastMatch![0]!;
             scanner.scan(whitespace);
 
             // If there's no "=", then this is another challenge rather than a
@@ -72,7 +72,7 @@
             scanner.scan(whitespace);
 
             if (scanner.scan(token)) {
-              params[name] = scanner.lastMatch[0];
+              params[name] = scanner.lastMatch![0]!;
             } else {
               params[name] = expectQuotedString(scanner,
                   name: 'a token or a quoted string');
@@ -109,15 +109,15 @@
   ///
   /// If [whitespaceName] is passed, it's used as the name for exceptions thrown
   /// due to invalid trailing whitespace.
-  static String _scanScheme(StringScanner scanner, {String whitespaceName}) {
+  static String _scanScheme(StringScanner scanner, {String? whitespaceName}) {
     scanner.expect(token, name: 'a token');
-    final scheme = scanner.lastMatch[0].toLowerCase();
+    final scheme = scanner.lastMatch![0]!.toLowerCase();
 
     scanner.scan(whitespace);
 
     // The spec specifically requires a space between the scheme and its
     // params.
-    if (scanner.lastMatch == null || !scanner.lastMatch[0].contains(' ')) {
+    if (scanner.lastMatch == null || !scanner.lastMatch![0]!.contains(' ')) {
       scanner.expect(' ', name: whitespaceName);
     }
 
@@ -127,13 +127,13 @@
   /// Scans a single authentication parameter and stores its result in [params].
   static void _scanAuthParam(StringScanner scanner, Map params) {
     scanner.expect(token, name: 'a token');
-    final name = scanner.lastMatch[0];
+    final name = scanner.lastMatch![0];
     scanner.scan(whitespace);
     scanner.expect('=');
     scanner.scan(whitespace);
 
     if (scanner.scan(token)) {
-      params[name] = scanner.lastMatch[0];
+      params[name] = scanner.lastMatch![0];
     } else {
       params[name] =
           expectQuotedString(scanner, name: 'a token or a quoted string');
diff --git a/lib/src/case_insensitive_map.dart b/lib/src/case_insensitive_map.dart
index 870f84c..88a190e 100644
--- a/lib/src/case_insensitive_map.dart
+++ b/lib/src/case_insensitive_map.dart
@@ -8,10 +8,8 @@
 ///
 /// Much of HTTP is case-insensitive, so this is useful to have pre-defined.
 class CaseInsensitiveMap<V> extends CanonicalizedMap<String, String, V> {
-  CaseInsensitiveMap()
-      : super((key) => key.toLowerCase(), isValidKey: (key) => key != null);
+  CaseInsensitiveMap() : super((key) => key.toLowerCase());
 
   CaseInsensitiveMap.from(Map<String, V> other)
-      : super.from(other, (key) => key.toLowerCase(),
-            isValidKey: (key) => key != null);
+      : super.from(other, (key) => key.toLowerCase());
 }
diff --git a/lib/src/chunked_coding/decoder.dart b/lib/src/chunked_coding/decoder.dart
index dcc0188..a916870 100644
--- a/lib/src/chunked_coding/decoder.dart
+++ b/lib/src/chunked_coding/decoder.dart
@@ -2,6 +2,7 @@
 // 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 'dart:convert';
 import 'dart:math' as math;
 import 'dart:typed_data';
@@ -18,7 +19,7 @@
 
   @override
   List<int> convert(List<int> input) {
-    final sink = _Sink(null);
+    final sink = _Sink(StreamController());
     final output = sink._decode(input, 0, input.length);
     if (sink._state == _State.end) return output;
 
@@ -38,9 +39,10 @@
   /// The current state of the sink's parsing.
   var _state = _State.boundary;
 
-  /// The size of the chunk being parsed, or `null` if the size hasn't been
-  /// parsed yet.
-  int _size;
+  /// The size of the chunk being parsed.
+  ///
+  /// Only assigned and used within [_decode].
+  late int _size;
 
   _Sink(this._sink);
 
@@ -60,7 +62,7 @@
 
   /// Like [close], but includes [chunk] and [index] in the [FormatException] if
   /// one is thrown.
-  void _close([List<int> chunk, int index]) {
+  void _close([List<int>? chunk, int? index]) {
     if (_state != _State.end) {
       throw FormatException('Input ended unexpectedly.', chunk, index);
     }
diff --git a/lib/src/http_date.dart b/lib/src/http_date.dart
index 71f6bab..0cedd9a 100644
--- a/lib/src/http_date.dart
+++ b/lib/src/http_date.dart
@@ -113,17 +113,17 @@
 int _parseMonth(StringScanner scanner) {
   scanner.expect(_monthRegExp);
   // DateTime uses 1-indexed months.
-  return _months.indexOf(scanner.lastMatch[0]) + 1;
+  return _months.indexOf(scanner.lastMatch![0]!) + 1;
 }
 
 /// Parses an int an enforces that it has exactly [digits] digits.
 int _parseInt(StringScanner scanner, int digits) {
   scanner.expect(_digitRegExp);
-  if (scanner.lastMatch[0].length != digits) {
+  if (scanner.lastMatch![0]!.length != digits) {
     scanner.error('expected a $digits-digit number.');
   }
 
-  return int.parse(scanner.lastMatch[0]);
+  return int.parse(scanner.lastMatch![0]!);
 }
 
 /// Parses an timestamp of the form "HH:MM:SS" on a 24-hour clock.
diff --git a/lib/src/media_type.dart b/lib/src/media_type.dart
index 71b16b2..59e6928 100644
--- a/lib/src/media_type.dart
+++ b/lib/src/media_type.dart
@@ -47,22 +47,22 @@
         final scanner = StringScanner(mediaType);
         scanner.scan(whitespace);
         scanner.expect(token);
-        final type = scanner.lastMatch[0];
+        final type = scanner.lastMatch![0]!;
         scanner.expect('/');
         scanner.expect(token);
-        final subtype = scanner.lastMatch[0];
+        final subtype = scanner.lastMatch![0]!;
         scanner.scan(whitespace);
 
         final parameters = <String, String>{};
         while (scanner.scan(';')) {
           scanner.scan(whitespace);
           scanner.expect(token);
-          final attribute = scanner.lastMatch[0];
+          final attribute = scanner.lastMatch![0]!;
           scanner.expect('=');
 
           String value;
           if (scanner.scan(token)) {
-            value = scanner.lastMatch[0];
+            value = scanner.lastMatch![0]!;
           } else {
             value = expectQuotedString(scanner);
           }
@@ -75,7 +75,7 @@
         return MediaType(type, subtype, parameters);
       });
 
-  MediaType(String type, String subtype, [Map<String, String> parameters])
+  MediaType(String type, String subtype, [Map<String, String>? parameters])
       : type = type.toLowerCase(),
         subtype = subtype.toLowerCase(),
         parameters = UnmodifiableMapView(
@@ -91,10 +91,10 @@
   /// [clearParameters] is passed, it replaces the corresponding field entirely
   /// instead.
   MediaType change(
-      {String type,
-      String subtype,
-      String mimeType,
-      Map<String, String> parameters,
+      {String? type,
+      String? subtype,
+      String? mimeType,
+      Map<String, String>? parameters,
       bool clearParameters = false}) {
     if (mimeType != null) {
       if (type != null) {
diff --git a/lib/src/scan.dart b/lib/src/scan.dart
index 1bdf877..d7de54d 100644
--- a/lib/src/scan.dart
+++ b/lib/src/scan.dart
@@ -58,11 +58,11 @@
 ///
 /// If [name] is passed, it's used to describe the expected value if it's not
 /// found.
-String expectQuotedString(StringScanner scanner, {String name}) {
+String expectQuotedString(StringScanner scanner, {String? name}) {
   name ??= 'quoted string';
   scanner.expect(_quotedString, name: name);
-  final string = scanner.lastMatch[0];
+  final string = scanner.lastMatch![0]!;
   return string
       .substring(1, string.length - 1)
-      .replaceAllMapped(_quotedPair, (match) => match[1]);
+      .replaceAllMapped(_quotedPair, (match) => match[1]!);
 }
diff --git a/pubspec.yaml b/pubspec.yaml
index eccff2c..d292ea1 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,19 +1,19 @@
 name: http_parser
-version: 3.1.5-dev
+version: 3.2.0-nullsafety
 description: >-
   A platform-independent package for parsing and serializing HTTP formats.
 homepage: https://github.com/dart-lang/http_parser
 
 environment:
-  sdk: '>=2.7.0 <3.0.0'
+  sdk: '>=2.10.0-0 <2.10.0'
 
 dependencies:
-  charcode: ^1.1.0
-  collection: '>=0.9.1 <2.0.0'
-  source_span: ^1.0.0
-  string_scanner: '>=0.0.0 <2.0.0'
-  typed_data: ^1.1.0
+  charcode: ^1.2.0-nullsafety
+  collection: ^1.15.0-nullsafety.2
+  source_span: ^1.8.0-nullsafety
+  string_scanner: ^1.1.0-nullsafety
+  typed_data: ^1.3.0-nullsafety.2
 
 dev_dependencies:
-  pedantic: ^1.0.0
-  test: ^1.0.0
+  pedantic: ^1.10.0-nullsafety
+  test: ^1.16.0-nullsafety.4
diff --git a/test/chunked_coding_test.dart b/test/chunked_coding_test.dart
index 0b178ec..045557a 100644
--- a/test/chunked_coding_test.dart
+++ b/test/chunked_coding_test.dart
@@ -30,8 +30,8 @@
     });
 
     group('with chunked conversion', () {
-      List<List<int>> results;
-      ByteConversionSink sink;
+      late List<List<int>> results;
+      late ByteConversionSink sink;
       setUp(() {
         results = [];
         final controller = StreamController<List<int>>(sync: true);
@@ -255,8 +255,8 @@
     });
 
     group('with chunked conversion', () {
-      List<List<int>> results;
-      ByteConversionSink sink;
+      late List<List<int>> results;
+      late ByteConversionSink sink;
       setUp(() {
         results = [];
         final controller = StreamController<List<int>>(sync: true);
diff --git a/test/example_test.dart b/test/example_test.dart
index ef61060..bbf869f 100644
--- a/test/example_test.dart
+++ b/test/example_test.dart
@@ -6,7 +6,10 @@
   test('validate example', () {
     final result = Process.runSync(
       Platform.executable,
-      ['example/example.dart'],
+      [
+        '--enable-experiment=non-nullable',
+        'example/example.dart',
+      ],
     );
 
     expect(result.exitCode, 0);
diff --git a/test/media_type_test.dart b/test/media_type_test.dart
index ceca7b9..5ee9313 100644
--- a/test/media_type_test.dart
+++ b/test/media_type_test.dart
@@ -83,7 +83,7 @@
   });
 
   group('change', () {
-    MediaType type;
+    late MediaType type;
     setUp(() {
       type = MediaType.parse('text/plain; foo=bar; baz=bang');
     });