enable null safety (#23)

Also remove lint redundant with latest pedantic – and add a few favorites

Co-authored-by: Nate Bosch <nbosch@google.com>
diff --git a/.github/workflows/test-package.yml b/.github/workflows/test-package.yml
index c3808a4..ce7b153 100644
--- a/.github/workflows/test-package.yml
+++ b/.github/workflows/test-package.yml
@@ -20,21 +20,20 @@
     strategy:
       fail-fast: false
       matrix:
-        sdk: [dev]
+        sdk: [2.12.0, dev]
     steps:
       - uses: actions/checkout@v2
-      - uses: dart-lang/setup-dart@v0.3
+      - uses: dart-lang/setup-dart@v1.0
         with:
           sdk: ${{ matrix.sdk }}
       - id: install
-        name: Install dependencies
         run: dart pub get
-      - name: Check formatting
-        run: dart format --output=none --set-exit-if-changed .
-        if: always() && steps.install.outcome == 'success'
-      - name: Analyze code
-        run: dart analyze --fatal-infos
-        if: always() && steps.install.outcome == 'success'
+      - run: dart format --output=none --set-exit-if-changed .
+        if: matrix.sdk == 'dev' && steps.install.outcome == 'success'
+      - run: dart analyze --fatal-infos
+        if: matrix.sdk == 'dev' && steps.install.outcome == 'success'
+      - run: dart analyze
+        if: matrix.sdk != 'dev' && steps.install.output == 'success'
 
   # Run tests on a matrix consisting of two dimensions:
   # 1. OS: ubuntu-latest, (macos-latest, windows-latest)
@@ -47,39 +46,13 @@
       matrix:
         # Add macos-latest and/or windows-latest if relevant for this package.
         os: [ubuntu-latest]
-        sdk: [dev]
+        sdk: [2.12.0, dev]
     steps:
       - uses: actions/checkout@v2
-      - uses: dart-lang/setup-dart@v0.3
+      - uses: dart-lang/setup-dart@v1.0
         with:
           sdk: ${{ matrix.sdk }}
       - id: install
-        name: Install dependencies
         run: dart pub get
-      - 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.0.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.0.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
+      - run: dart test
         if: always() && steps.install.outcome == 'success'
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 43863e3..8618814 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,9 @@
+## 1.0.0
+
+- Require Dart `2.12`.
+- Enable null safety.
+- Removed deprecated `createProxyHandler`.
+
 ## 0.1.0+7
 
 * Added example.
diff --git a/analysis_options.yaml b/analysis_options.yaml
index 57afaff..c9b76ab 100644
--- a/analysis_options.yaml
+++ b/analysis_options.yaml
@@ -1,79 +1,75 @@
 include: package:pedantic/analysis_options.yaml
+
 analyzer:
   strong-mode:
     implicit-casts: false
+
 linter:
   rules:
-    - always_declare_return_types
-    - annotate_overrides
+    - avoid_bool_literals_in_conditional_expressions
+    - avoid_catching_errors
+    - avoid_classes_with_only_static_members
+    - avoid_dynamic_calls
     - avoid_empty_else
     - avoid_function_literals_in_foreach_calls
-    - avoid_init_to_null
-    - avoid_null_checks_in_equality_operators
-    - avoid_relative_lib_imports
+    - avoid_private_typedef_functions
+    - avoid_redundant_argument_values
     - avoid_renaming_method_parameters
-    - avoid_return_types_on_setters
     - avoid_returning_null
     - avoid_returning_null_for_future
-    - avoid_shadowing_type_parameters
-    - avoid_types_as_parameter_names
+    - avoid_returning_null_for_void
+    - avoid_returning_this
     - avoid_unused_constructor_parameters
-    - await_only_futures
+    - avoid_void_async
     - camel_case_types
     - cancel_subscriptions
+    - cascade_invocations
     - comment_references
     - constant_identifier_names
     - control_flow_in_finally
     - directives_ordering
-    - empty_catches
-    - empty_constructor_bodies
     - empty_statements
+    - file_names
     - hash_and_equals
     - implementation_imports
     - invariant_booleans
     - iterable_contains_unrelated_type
-    - library_names
-    - library_prefixes
+    - join_return_with_assignment
+    - lines_longer_than_80_chars
     - list_remove_unrelated_type
     - literal_only_boolean_expressions
+    - missing_whitespace_between_adjacent_strings
     - no_adjacent_strings_in_list
-    - no_duplicate_case_values
+    - no_runtimeType_toString
     - non_constant_identifier_names
-    - null_closures
-    - omit_local_variable_types
     - only_throw_errors
     - overridden_fields
     - package_api_docs
     - package_names
     - package_prefixed_library_names
-    - prefer_adjacent_string_concatenation
-    - prefer_collection_literals
-    - prefer_conditional_assignment
+    - prefer_asserts_in_initializer_lists
     - prefer_const_constructors
-    - prefer_contains
-    - prefer_equal_for_default_values
-    - prefer_final_fields
-    - prefer_generic_function_type_aliases
+    - prefer_const_declarations
+    - prefer_expression_function_bodies
+    - prefer_final_locals
+    - prefer_function_declarations_over_variables
     - prefer_initializing_formals
-    - prefer_is_empty
-    - prefer_is_not_empty
+    - prefer_interpolation_to_compose_strings
+    - prefer_is_not_operator
+    - prefer_null_aware_operators
+    - prefer_relative_imports
     - prefer_typing_uninitialized_variables
-    - recursive_getters
-    - slash_for_doc_comments
+    - prefer_void_to_null
+    - provide_deprecation_message
+    - sort_pub_dependencies
     - test_types_in_equals
     - throw_in_finally
-    - type_init_formals
     - unnecessary_await_in_return
-    - unnecessary_brace_in_string_interps
-    - unnecessary_const
-    - unnecessary_getters_setters
     - unnecessary_lambdas
-    - unnecessary_new
     - unnecessary_null_aware_assignments
+    - unnecessary_overrides
     - unnecessary_parenthesis
     - unnecessary_statements
-    - unnecessary_this
-    - unrelated_type_equality_checks
-    - use_function_type_syntax_for_parameters
-    - use_rethrow_when_possible
-    - valid_regexps
+    - unnecessary_string_interpolations
+    - use_string_buffers
+    - void_checks
diff --git a/example/example.dart b/example/example.dart
index 02da1a3..2b1af31 100644
--- a/example/example.dart
+++ b/example/example.dart
@@ -5,8 +5,8 @@
 import 'package:shelf/shelf_io.dart' as shelf_io;
 import 'package:shelf_proxy/shelf_proxy.dart';
 
-void main() async {
-  var server = await shelf_io.serve(
+Future<void> main() async {
+  final server = await shelf_io.serve(
     proxyHandler('https://dart.dev'),
     'localhost',
     8080,
diff --git a/lib/shelf_proxy.dart b/lib/shelf_proxy.dart
index a59623d..0f2d3c7 100644
--- a/lib/shelf_proxy.dart
+++ b/lib/shelf_proxy.dart
@@ -21,7 +21,7 @@
 ///
 /// [proxyName] is used in headers to identify this proxy. It should be a valid
 /// HTTP token or a hostname. It defaults to `shelf_proxy`.
-Handler proxyHandler(url, {http.Client client, String proxyName}) {
+Handler proxyHandler(url, {http.Client? client, String? proxyName}) {
   Uri uri;
   if (url is String) {
     uri = Uri.parse(url);
@@ -30,7 +30,7 @@
   } else {
     throw ArgumentError.value(url, 'url', 'url must be a String or Uri.');
   }
-  client ??= http.Client();
+  final nonNullClient = client ?? http.Client();
   proxyName ??= 'shelf_proxy';
 
   return (serverRequest) async {
@@ -38,11 +38,11 @@
 
     // TODO(nweiz): Handle TRACE requests correctly. See
     // http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.8
-    var requestUrl = uri.resolve(serverRequest.url.toString());
-    var clientRequest = http.StreamedRequest(serverRequest.method, requestUrl);
-    clientRequest.followRedirects = false;
-    clientRequest.headers.addAll(serverRequest.headers);
-    clientRequest.headers['Host'] = uri.authority;
+    final requestUrl = uri.resolve(serverRequest.url.toString());
+    final clientRequest = http.StreamedRequest(serverRequest.method, requestUrl)
+      ..followRedirects = false
+      ..headers.addAll(serverRequest.headers)
+      ..headers['Host'] = uri.authority;
 
     // Add a Via header. See
     // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.45
@@ -54,7 +54,7 @@
         .forEach(clientRequest.sink.add)
         .catchError(clientRequest.sink.addError)
         .whenComplete(clientRequest.sink.close));
-    var clientResponse = await client.send(clientRequest);
+    final clientResponse = await nonNullClient.send(clientRequest);
     // Add a Via header. See
     // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.45
     _addHeader(clientResponse.headers, 'via', '1.1 $proxyName');
@@ -79,11 +79,11 @@
     // than the destination server, if possible.
     if (clientResponse.isRedirect &&
         clientResponse.headers.containsKey('location')) {
-      var location =
-          requestUrl.resolve(clientResponse.headers['location']).toString();
+      final location =
+          requestUrl.resolve(clientResponse.headers['location']!).toString();
       if (p.url.isWithin(uri.toString(), location)) {
         clientResponse.headers['location'] =
-            '/' + p.url.relative(location, from: uri.toString());
+            '/${p.url.relative(location, from: uri.toString())}';
       } else {
         clientResponse.headers['location'] = location;
       }
@@ -94,17 +94,10 @@
   };
 }
 
-/// Use [proxyHandler] instead.
-@deprecated
-Handler createProxyHandler(Uri rootUri) => proxyHandler(rootUri);
-
 // TODO(nweiz): use built-in methods for this when http and shelf support them.
 /// Add a header with [name] and [value] to [headers], handling existing headers
 /// gracefully.
 void _addHeader(Map<String, String> headers, String name, String value) {
-  if (headers.containsKey(name)) {
-    headers[name] += ', $value';
-  } else {
-    headers[name] = value;
-  }
+  final existing = headers[name];
+  headers[name] = existing == null ? value : '$existing, $value';
 }
diff --git a/pubspec.yaml b/pubspec.yaml
index b6be43b..dc9cb6b 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,16 +1,16 @@
 name: shelf_proxy
-version: 0.1.0+8-dev
+version: 1.0.0
 description: A shelf handler for proxying HTTP requests to another server.
-homepage: https://github.com/dart-lang/shelf_proxy
+repository: https://github.com/dart-lang/shelf_proxy
 
 environment:
-  sdk: '>=2.0.0 <3.0.0'
+  sdk: '>=2.12.0 <3.0.0'
 
 dependencies:
-  http: '>=0.9.0 <0.13.0'
-  path: '>=1.0.0 <2.0.0'
+  http: ^0.13.0
+  path: ^1.8.0
   pedantic: ^1.0.0
-  shelf: '>=0.5.2 <0.8.0'
+  shelf: ^1.0.0
 
 dev_dependencies:
-  test: ^1.3.0
+  test: ^1.6.0
diff --git a/test/shelf_proxy_test.dart b/test/shelf_proxy_test.dart
index 588efda..b1ec3fd 100644
--- a/test/shelf_proxy_test.dart
+++ b/test/shelf_proxy_test.dart
@@ -12,10 +12,10 @@
 import 'package:test/test.dart';
 
 /// The URI of the server the current proxy server is proxying to.
-Uri targetUri;
+late Uri targetUri;
 
 /// The URI of the current proxy server.
-Uri proxyUri;
+late Uri proxyUri;
 
 void main() {
   group('forwarding', () {
@@ -48,30 +48,24 @@
     });
 
     test('forwards response status', () async {
-      await createProxy((request) {
-        return shelf.Response(567);
-      });
+      await createProxy((request) => shelf.Response(567));
 
-      var response = await get();
+      final response = await get();
       expect(response.statusCode, equals(567));
     });
 
     test('forwards response headers', () async {
-      await createProxy((request) {
-        return shelf.Response.ok(':)',
-            headers: {'foo': 'bar', 'accept': '*/*'});
-      });
+      await createProxy((request) =>
+          shelf.Response.ok(':)', headers: {'foo': 'bar', 'accept': '*/*'}));
 
-      var response = await get();
+      final response = await get();
 
       expect(response.headers, containsPair('foo', 'bar'));
       expect(response.headers, containsPair('accept', '*/*'));
     });
 
     test('forwards response body', () async {
-      await createProxy((request) {
-        return shelf.Response.ok('hello, client');
-      });
+      await createProxy((request) => shelf.Response.ok('hello, client'));
 
       expect(await http.read(proxyUri), equals('hello, client'));
     });
@@ -109,16 +103,15 @@
     test('adds a Via header to the response', () async {
       await createProxy((request) => shelf.Response.ok(':)'));
 
-      var response = await get();
+      final response = await get();
       expect(response.headers, containsPair('via', '1.1 shelf_proxy'));
     });
 
     test("adds to a response's existing Via header", () async {
-      await createProxy((request) {
-        return shelf.Response.ok(':)', headers: {'via': '1.0 something'});
-      });
+      await createProxy((request) =>
+          shelf.Response.ok(':)', headers: {'via': '1.0 something'}));
 
-      var response = await get();
+      final response = await get();
       expect(response.headers,
           containsPair('via', '1.0 something, 1.1 shelf_proxy'));
     });
@@ -126,40 +119,36 @@
 
   group('redirects', () {
     test("doesn't modify a Location for a foreign server", () async {
-      await createProxy((request) {
-        return shelf.Response.found('http://dartlang.org');
-      });
+      await createProxy(
+          (request) => shelf.Response.found('http://dartlang.org'));
 
-      var response = await get();
+      final response = await get();
       expect(response.headers, containsPair('location', 'http://dartlang.org'));
     });
 
     test('relativizes a reachable root-relative Location', () async {
-      await createProxy((request) {
-        return shelf.Response.found('/foo/bar');
-      }, targetPath: '/foo');
+      await createProxy((request) => shelf.Response.found('/foo/bar'),
+          targetPath: '/foo');
 
-      var response = await get();
+      final response = await get();
       expect(response.headers, containsPair('location', '/bar'));
     });
 
     test('absolutizes an unreachable root-relative Location', () async {
-      await createProxy((request) {
-        return shelf.Response.found('/baz');
-      }, targetPath: '/foo');
+      await createProxy((request) => shelf.Response.found('/baz'),
+          targetPath: '/foo');
 
-      var response = await get();
+      final response = await get();
       expect(response.headers,
           containsPair('location', targetUri.resolve('/baz').toString()));
     });
   });
 
   test('removes a transfer-encoding header', () async {
-    var handler = mockHandler((request) {
-      return http.Response('', 200, headers: {'transfer-encoding': 'chunked'});
-    });
+    final handler = mockHandler((request) =>
+        http.Response('', 200, headers: {'transfer-encoding': 'chunked'}));
 
-    var response =
+    final response =
         await handler(shelf.Request('GET', Uri.parse('http://localhost/')));
 
     expect(response.headers, isNot(contains('transfer-encoding')));
@@ -167,12 +156,10 @@
 
   test('removes content-length and content-encoding for a gzipped response',
       () async {
-    var handler = mockHandler((request) {
-      return http.Response('', 200,
-          headers: {'content-encoding': 'gzip', 'content-length': '1234'});
-    });
+    final handler = mockHandler((request) => http.Response('', 200,
+        headers: {'content-encoding': 'gzip', 'content-length': '1234'}));
 
-    var response =
+    final response =
         await handler(shelf.Request('GET', Uri.parse('http://localhost/')));
 
     expect(response.headers, isNot(contains('content-encoding')));
@@ -186,15 +173,15 @@
 ///
 /// [targetPath] is the root-relative path on the target server to proxy to. It
 /// defaults to `/`.
-Future createProxy(shelf.Handler handler, {String targetPath}) async {
+Future createProxy(shelf.Handler handler, {String? targetPath}) async {
   handler = expectAsync1(handler, reason: 'target server handler');
-  var targetServer = await shelf_io.serve(handler, 'localhost', 0);
+  final targetServer = await shelf_io.serve(handler, 'localhost', 0);
   targetUri = Uri.parse('http://localhost:${targetServer.port}');
   if (targetPath != null) targetUri = targetUri.resolve(targetPath);
-  var proxyServerHandler =
+  final proxyServerHandler =
       expectAsync1(proxyHandler(targetUri), reason: 'proxy server handler');
 
-  var proxyServer = await shelf_io.serve(proxyServerHandler, 'localhost', 0);
+  final proxyServer = await shelf_io.serve(proxyServerHandler, 'localhost', 0);
   proxyUri = Uri.parse('http://localhost:${proxyServer.port}');
 
   addTearDown(() {
@@ -207,14 +194,14 @@
 /// [callback].
 shelf.Handler mockHandler(
     FutureOr<http.Response> Function(http.Request) callback) {
-  var client = MockClient((request) async => await callback(request));
+  final client = MockClient((request) async => await callback(request));
   return proxyHandler('http://dartlang.org', client: client);
 }
 
 /// Schedules a GET request with [headers] to the proxy server.
-Future<http.Response> get({Map<String, String> headers}) {
-  var uri = proxyUri;
-  var request = http.Request('GET', uri);
+Future<http.Response> get({Map<String, String>? headers}) {
+  final uri = proxyUri;
+  final request = http.Request('GET', uri);
   if (headers != null) request.headers.addAll(headers);
   request.followRedirects = false;
   return request.send().then(http.Response.fromStream);