Migrate to null safety (#47)

Fixes #41
diff --git a/.github/workflows/test-package.yml b/.github/workflows/test-package.yml
index 779e3d2..2ae7ee0 100644
--- a/.github/workflows/test-package.yml
+++ b/.github/workflows/test-package.yml
@@ -58,27 +58,3 @@
       - name: Run VM tests
         run: dart test --platform vm
         if: always() && steps.install.outcome == 'success'
-
-  # Run tests on a matrix consisting of two dimensions:
-  # 1. OS: ubuntu-latest, (macos-latest, windows-latest)
-  # 2. release channel: 2.3.0
-  test-legacy-sdk:
-    needs: analyze
-    runs-on: ${{ matrix.os }}
-    strategy:
-      fail-fast: false
-      matrix:
-        # Add macos-latest and/or windows-latest if relevant for this package.
-        os: [ubuntu-latest]
-        sdk: [2.3.0]
-    steps:
-      - uses: actions/checkout@v2
-      - uses: dart-lang/setup-dart@v0.3
-        with:
-          sdk: ${{ matrix.sdk }}
-      - id: install
-        name: Install dependencies
-        run: pub get
-      - name: Run VM tests
-        run: pub run test --platform vm
-        if: always() && steps.install.outcome == 'success'
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 040c4d2..ab642b7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,10 @@
+## 1.0.0-dev
+
+* Migrate to null safety.
+
 ## 0.2.9+2
 
-- Change version constraint for the `shelf` dependency, so it accepts null-safe versions.
+* Change version constraint for the `shelf` dependency, so it accepts null-safe versions.
 
 ## 0.2.9+1
 
diff --git a/example/example_server.dart b/example/example_server.dart
index 11dd0b3..288d630 100644
--- a/example/example_server.dart
+++ b/example/example_server.dart
@@ -23,8 +23,7 @@
     logging = result['logging'] as bool;
     listDirectories = result['list-directories'] as bool;
   } on FormatException catch (e) {
-    stderr.writeln(e.message);
-    stderr.writeln(parser.usage);
+    stderr..writeln(e.message)..writeln(parser.usage);
     // http://linux.die.net/include/sysexits.h
     // #define EX_USAGE	64	/* command line usage error */
     exit(64);
@@ -40,7 +39,7 @@
     pipeline = pipeline.addMiddleware(shelf.logRequests());
   }
 
-  var defaultDoc = _defaultDoc;
+  String? defaultDoc = _defaultDoc;
   if (listDirectories) {
     defaultDoc = null;
   }
diff --git a/lib/src/static_handler.dart b/lib/src/static_handler.dart
index e1545a0..23ac17f 100644
--- a/lib/src/static_handler.dart
+++ b/lib/src/static_handler.dart
@@ -41,10 +41,10 @@
 /// detection.
 Handler createStaticHandler(String fileSystemPath,
     {bool serveFilesOutsidePath = false,
-    String defaultDocument,
+    String? defaultDocument,
     bool listDirectories = false,
     bool useHeaderBytesForContentType = false,
-    MimeTypeResolver contentTypeResolver}) {
+    MimeTypeResolver? contentTypeResolver}) {
   final rootDir = Directory(fileSystemPath);
   if (!rootDir.existsSync()) {
     throw ArgumentError('A directory corresponding to fileSystemPath '
@@ -59,31 +59,32 @@
     }
   }
 
-  contentTypeResolver ??= _defaultMimeTypeResolver;
+  final mimeResolver = contentTypeResolver ?? _defaultMimeTypeResolver;
 
   return (Request request) {
     final segs = [fileSystemPath, ...request.url.pathSegments];
 
     final fsPath = p.joinAll(segs);
 
-    final entityType = FileSystemEntity.typeSync(fsPath, followLinks: true);
+    final entityType = FileSystemEntity.typeSync(fsPath);
 
-    File file;
+    File? fileFound;
 
     if (entityType == FileSystemEntityType.file) {
-      file = File(fsPath);
+      fileFound = File(fsPath);
     } else if (entityType == FileSystemEntityType.directory) {
-      file = _tryDefaultFile(fsPath, defaultDocument);
-      if (file == null && listDirectories) {
+      fileFound = _tryDefaultFile(fsPath, defaultDocument);
+      if (fileFound == null && listDirectories) {
         final uri = request.requestedUri;
         if (!uri.path.endsWith('/')) return _redirectToAddTrailingSlash(uri);
         return listDirectory(fileSystemPath, fsPath);
       }
     }
 
-    if (file == null) {
+    if (fileFound == null) {
       return Response.notFound('Not Found');
     }
+    final file = fileFound;
 
     if (!serveFilesOutsidePath) {
       final resolvedPath = file.resolveSymbolicLinksSync();
@@ -104,17 +105,16 @@
 
     return _handleFile(request, file, () async {
       if (useHeaderBytesForContentType) {
-        final length = math.min(
-            contentTypeResolver.magicNumbersMaxLength, file.lengthSync());
+        final length =
+            math.min(mimeResolver.magicNumbersMaxLength, file.lengthSync());
 
         final byteSink = ByteAccumulatorSink();
 
         await file.openRead(0, length).listen(byteSink.add).asFuture();
 
-        return contentTypeResolver.lookup(file.path,
-            headerBytes: byteSink.bytes);
+        return mimeResolver.lookup(file.path, headerBytes: byteSink.bytes);
       } else {
-        return contentTypeResolver.lookup(file.path);
+        return mimeResolver.lookup(file.path);
       }
     });
   };
@@ -132,7 +132,7 @@
   return Response.movedPermanently(location.toString());
 }
 
-File _tryDefaultFile(String dirPath, String defaultFile) {
+File? _tryDefaultFile(String dirPath, String? defaultFile) {
   if (defaultFile == null) return null;
 
   final filePath = p.join(dirPath, defaultFile);
@@ -154,7 +154,7 @@
 /// This uses the given [contentType] for the Content-Type header. It defaults
 /// to looking up a content type based on [path]'s file extension, and failing
 /// that doesn't sent a [contentType] header at all.
-Handler createFileHandler(String path, {String url, String contentType}) {
+Handler createFileHandler(String path, {String? url, String? contentType}) {
   final file = File(path);
   if (!file.existsSync()) {
     throw ArgumentError.value(path, 'path', 'does not exist.');
@@ -162,12 +162,12 @@
     throw ArgumentError.value(url, 'url', 'must be relative.');
   }
 
-  contentType ??= _defaultMimeTypeResolver.lookup(path);
+  final mimeType = contentType ?? _defaultMimeTypeResolver.lookup(path);
   url ??= p.toUri(p.basename(path)).toString();
 
   return (request) {
     if (request.url.path != url) return Response.notFound('Not Found');
-    return _handleFile(request, file, () => contentType);
+    return _handleFile(request, file, () => mimeType);
   };
 }
 
@@ -177,7 +177,7 @@
 /// indicates that it has the latest version of a file. Otherwise, it calls
 /// [getContentType] and uses it to populate the Content-Type header.
 Future<Response> _handleFile(Request request, File file,
-    FutureOr<String> Function() getContentType) async {
+    FutureOr<String?> Function() getContentType) async {
   final stat = file.statSync();
   final ifModifiedSince = request.ifModifiedSince;
 
diff --git a/pubspec.yaml b/pubspec.yaml
index f828265..e945e97 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,19 +1,23 @@
 name: shelf_static
-version: 0.2.9+2
+version: 1.0.0-dev
 description: Static file server support for shelf
 repository: https://github.com/dart-lang/shelf_static
 
 environment:
-  sdk: '>=2.3.0 <3.0.0'
+  sdk: '>=2.12.0-0 <3.0.0'
 
 dependencies:
-  convert: '>=1.0.0 <4.0.0'
-  http_parser: '>=0.0.2+2 <5.0.0'
-  mime: '>=0.9.0 <=1.0.0'
-  path: '>=1.1.0 <2.0.0'
-  shelf: '>=0.5.7 <2.0.0'
+  convert: ^3.0.0
+  http_parser: ^4.0.0
+  mime: ^1.0.0
+  path: ^1.8.0
+  shelf: ^1.0.0
 
 dev_dependencies:
-  args: '^1.0.0'
-  test: '^1.2.0'
-  test_descriptor: '^1.0.0'
+  args: ^2.0.0
+  test: ^1.16.0
+  test_descriptor: ^2.0.0
+
+dependency_overrides:
+  shelf_packages_handler: ^2.0.1
+  test: ^1.16.0
diff --git a/test/basic_file_test.dart b/test/basic_file_test.dart
index ec5ab46..ab89efc 100644
--- a/test/basic_file_test.dart
+++ b/test/basic_file_test.dart
@@ -153,7 +153,7 @@
       final rootPath = p.join(d.sandbox, 'root.txt');
 
       final response1 = await makeRequest(handler, '/root.txt');
-      final originalModificationDate = response1.lastModified;
+      final originalModificationDate = response1.lastModified!;
 
       // Ensure the timestamp change is > 1s.
       await Future<void>.delayed(const Duration(seconds: 2));
@@ -167,7 +167,7 @@
       final response2 =
           await makeRequest(handler, '/root.txt', headers: headers);
       expect(response2.statusCode, HttpStatus.ok);
-      expect(response2.lastModified.millisecondsSinceEpoch,
+      expect(response2.lastModified!.millisecondsSinceEpoch,
           greaterThan(originalModificationDate.millisecondsSinceEpoch));
     });
   });
diff --git a/test/test_util.dart b/test/test_util.dart
index f42f6d1..e496394 100644
--- a/test/test_util.dart
+++ b/test/test_util.dart
@@ -11,15 +11,15 @@
 
 /// Makes a simple GET request to [handler] and returns the result.
 Future<Response> makeRequest(Handler handler, String path,
-    {String handlerPath, Map<String, String> headers}) {
+    {String? handlerPath, Map<String, String>? headers}) {
   final rootedHandler = _rootHandler(handlerPath, handler);
   return Future.sync(() => rootedHandler(_fromPath(path, headers)));
 }
 
-Request _fromPath(String path, Map<String, String> headers) =>
+Request _fromPath(String path, Map<String, String>? headers) =>
     Request('GET', Uri.parse('http://localhost$path'), headers: headers);
 
-Handler _rootHandler(String path, Handler handler) {
+Handler _rootHandler(String? path, Handler handler) {
   if (path == null || path.isEmpty) {
     return handler;
   }
@@ -49,7 +49,7 @@
   bool matches(dynamic item, Map matchState) {
     if (item is! DateTime) return false;
 
-    return _datesEqualToSecond(_target, item as DateTime);
+    return _datesEqualToSecond(_target, item);
   }
 
   @override