Migrate to null safety (dart-lang/json_rpc_2#72)

diff --git a/pkgs/json_rpc_2/.github/workflows/test-package.yml b/pkgs/json_rpc_2/.github/workflows/test-package.yml
index 2792265..21a3c50 100644
--- a/pkgs/json_rpc_2/.github/workflows/test-package.yml
+++ b/pkgs/json_rpc_2/.github/workflows/test-package.yml
@@ -59,27 +59,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: 2.2.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.2.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/pkgs/json_rpc_2/CHANGELOG.md b/pkgs/json_rpc_2/CHANGELOG.md
index f27cacb..126ba00 100644
--- a/pkgs/json_rpc_2/CHANGELOG.md
+++ b/pkgs/json_rpc_2/CHANGELOG.md
@@ -1,5 +1,6 @@
-## 2.2.3-dev (unreleased)
+## 3.0.0-dev
 
+* Migrate to null safety.
 * Accept responses even if the server converts the ID to a String.
 
 ## 2.2.2
diff --git a/pkgs/json_rpc_2/lib/error_code.dart b/pkgs/json_rpc_2/lib/error_code.dart
index 4bb0808..f4ca462 100644
--- a/pkgs/json_rpc_2/lib/error_code.dart
+++ b/pkgs/json_rpc_2/lib/error_code.dart
@@ -37,7 +37,7 @@
 /// JSON-RPC 2.0 spec.
 ///
 /// If [errorCode] isn't defined in the JSON-RPC 2.0 spec, returns null.
-String name(int errorCode) {
+String? name(int errorCode) {
   switch (errorCode) {
     case PARSE_ERROR:
       return 'parse error';
diff --git a/pkgs/json_rpc_2/lib/src/client.dart b/pkgs/json_rpc_2/lib/src/client.dart
index ccc4c39..7e040cc 100644
--- a/pkgs/json_rpc_2/lib/src/client.dart
+++ b/pkgs/json_rpc_2/lib/src/client.dart
@@ -24,7 +24,7 @@
   /// The current batch of requests to be sent together.
   ///
   /// Each element is a JSON RPC spec compliant message.
-  List<Map<String, dynamic>> _batch;
+  List<Map<String, dynamic>>? _batch;
 
   /// The map of request ids to pending requests.
   final _pendingRequests = <int, _Request>{};
@@ -141,7 +141,7 @@
   ///
   /// Sends a request to invoke [method] with [parameters]. If [id] is given,
   /// the request uses that id.
-  void _send(String method, parameters, [int id]) {
+  void _send(String method, parameters, [int? id]) {
     if (parameters is Iterable) parameters = parameters.toList();
     if (parameters is! Map && parameters is! List && parameters != null) {
       throw ArgumentError('Only maps and lists may be used as JSON-RPC '
@@ -154,7 +154,7 @@
     if (parameters != null) message['params'] = parameters;
 
     if (_batch != null) {
-      _batch.add(message);
+      _batch!.add(message);
     } else {
       _channel.sink.add(message);
     }
@@ -197,7 +197,7 @@
     if (!_isResponseValid(response)) return;
     var id = response['id'];
     id = (id is String) ? int.parse(id) : id;
-    var request = _pendingRequests.remove(id);
+    var request = _pendingRequests.remove(id)!;
     if (response.containsKey('result')) {
       request.completer.complete(response['result']);
     } else {
diff --git a/pkgs/json_rpc_2/lib/src/peer.dart b/pkgs/json_rpc_2/lib/src/peer.dart
index 4cf6aae..a11cff2 100644
--- a/pkgs/json_rpc_2/lib/src/peer.dart
+++ b/pkgs/json_rpc_2/lib/src/peer.dart
@@ -21,11 +21,11 @@
 
   /// The underlying client that handles request-sending and response-receiving
   /// logic.
-  Client _client;
+  late final Client _client;
 
   /// The underlying server that handles request-receiving and response-sending
   /// logic.
-  Server _server;
+  late final Server _server;
 
   /// A stream controller that forwards incoming messages to [_server] if
   /// they're requests.
@@ -35,14 +35,14 @@
   /// they're responses.
   final _clientIncomingForwarder = StreamController(sync: true);
 
-  Future<void> _done;
   @override
-  Future get done => _done ??= Future.wait([_client.done, _server.done]);
+  late final Future done = Future.wait([_client.done, _server.done]);
+
   @override
   bool get isClosed => _client.isClosed || _server.isClosed;
 
   @override
-  ErrorCallback get onUnhandledError => _server?.onUnhandledError;
+  ErrorCallback? get onUnhandledError => _server.onUnhandledError;
 
   @override
   bool get strictProtocolChecks => _server.strictProtocolChecks;
@@ -60,7 +60,7 @@
   /// specification. In particular, requests missing the `jsonrpc` parameter
   /// will be accepted.
   Peer(StreamChannel<String> channel,
-      {ErrorCallback onUnhandledError, bool strictProtocolChecks = true})
+      {ErrorCallback? onUnhandledError, bool strictProtocolChecks = true})
       : this.withoutJson(
             jsonDocument.bind(channel).transform(respondToFormatExceptions),
             onUnhandledError: onUnhandledError,
@@ -82,7 +82,7 @@
   /// specification. In particular, requests missing the `jsonrpc` parameter
   /// will be accepted.
   Peer.withoutJson(this._channel,
-      {ErrorCallback onUnhandledError, bool strictProtocolChecks = true}) {
+      {ErrorCallback? onUnhandledError, bool strictProtocolChecks = true}) {
     _server = Server.withoutJson(
         StreamChannel(_serverIncomingForwarder.stream, _channel.sink),
         onUnhandledError: onUnhandledError,
diff --git a/pkgs/json_rpc_2/lib/src/server.dart b/pkgs/json_rpc_2/lib/src/server.dart
index 553b67b..a2b4dc1 100644
--- a/pkgs/json_rpc_2/lib/src/server.dart
+++ b/pkgs/json_rpc_2/lib/src/server.dart
@@ -60,7 +60,7 @@
   /// In the case where a user provided callback results in an exception that
   /// cannot be properly routed back to the client, this handler will be
   /// invoked. If it is not set, the exception will be swallowed.
-  final ErrorCallback onUnhandledError;
+  final ErrorCallback? onUnhandledError;
 
   /// Whether to strictly enforce the JSON-RPC 2.0 specification for received
   /// messages.
@@ -82,7 +82,7 @@
   /// requests which are not conformant with the JSON-RPC 2.0 specification. In
   /// particular, requests missing the `jsonrpc` parameter will be accepted.
   Server(StreamChannel<String> channel,
-      {ErrorCallback onUnhandledError, bool strictProtocolChecks = true})
+      {ErrorCallback? onUnhandledError, bool strictProtocolChecks = true})
       : this.withoutJson(
             jsonDocument.bind(channel).transform(respondToFormatExceptions),
             onUnhandledError: onUnhandledError,
diff --git a/pkgs/json_rpc_2/lib/src/utils.dart b/pkgs/json_rpc_2/lib/src/utils.dart
index 3f86f01..37523f0 100644
--- a/pkgs/json_rpc_2/lib/src/utils.dart
+++ b/pkgs/json_rpc_2/lib/src/utils.dart
@@ -40,25 +40,25 @@
     whenComplete();
     return result;
   } else {
-    return result.whenComplete(whenComplete);
+    result.whenComplete(whenComplete);
   }
 }
 
 /// A transformer that silently drops [FormatException]s.
-final ignoreFormatExceptions = StreamTransformer<Object, Object>.fromHandlers(
+final ignoreFormatExceptions = StreamTransformer<Object?, Object?>.fromHandlers(
     handleError: (error, stackTrace, sink) {
   if (error is FormatException) return;
   sink.addError(error, stackTrace);
 });
 
 /// A transformer that sends error responses on [FormatException]s.
-final StreamChannelTransformer<Object, Object> respondToFormatExceptions =
+final StreamChannelTransformer<Object?, Object?> respondToFormatExceptions =
     _RespondToFormatExceptionsTransformer();
 
 class _RespondToFormatExceptionsTransformer
-    implements StreamChannelTransformer<Object, Object> {
+    implements StreamChannelTransformer<Object?, Object?> {
   @override
-  StreamChannel<Object> bind(StreamChannel<Object> channel) {
+  StreamChannel<Object?> bind(StreamChannel<Object?> channel) {
     return channel.changeStream((stream) {
       return stream.handleError((dynamic error) {
         final formatException = error as FormatException;
diff --git a/pkgs/json_rpc_2/pubspec.yaml b/pkgs/json_rpc_2/pubspec.yaml
index c0c8008..e8d1b15 100644
--- a/pkgs/json_rpc_2/pubspec.yaml
+++ b/pkgs/json_rpc_2/pubspec.yaml
@@ -1,17 +1,17 @@
 name: json_rpc_2
-version: 2.2.3-dev
+version: 3.0.0-dev
 description: >-
   Utilities to write a client or server using the JSON-RPC 2.0 spec.
 homepage: https://github.com/dart-lang/json_rpc_2
 
 environment:
-  sdk: ">=2.2.0 <3.0.0"
+  sdk: ">=2.12.0 <3.0.0"
 
 dependencies:
-  stack_trace: ^1.0.0
+  stack_trace: ^1.10.0
   stream_channel: ">=1.1.0 <3.0.0"
 
 dev_dependencies:
-  pedantic: ^1.8.0
-  test: ^1.0.0
-  web_socket_channel: ^1.1.0
+  pedantic: ^1.11.0
+  test: ^1.16.0
+  web_socket_channel: ^2.0.0
diff --git a/pkgs/json_rpc_2/test/client/client_test.dart b/pkgs/json_rpc_2/test/client/client_test.dart
index 334ddad..1c9ce10 100644
--- a/pkgs/json_rpc_2/test/client/client_test.dart
+++ b/pkgs/json_rpc_2/test/client/client_test.dart
@@ -174,14 +174,12 @@
       };
     });
 
-    expect(controller.client.sendRequest('foo', {'param': 'value'}),
-        throwsA(predicate((exception) {
-      expect(exception, TypeMatcher<json_rpc.RpcException>());
-      expect(exception.code, equals(error_code.SERVER_ERROR));
-      expect(exception.message, equals('you are bad at requests'));
-      expect(exception.data, equals('some junk'));
-      return true;
-    })));
+    expect(
+        controller.client.sendRequest('foo', {'param': 'value'}),
+        throwsA(TypeMatcher<json_rpc.RpcException>()
+            .having((e) => e.code, 'code', error_code.SERVER_ERROR)
+            .having((e) => e.message, 'message', 'you are bad at requests')
+            .having((e) => e.data, 'data', 'some junk')));
   });
 
   test('requests throw StateErrors if the client is closed', () {
diff --git a/pkgs/json_rpc_2/test/client/utils.dart b/pkgs/json_rpc_2/test/client/utils.dart
index 1684b37..398c558 100644
--- a/pkgs/json_rpc_2/test/client/utils.dart
+++ b/pkgs/json_rpc_2/test/client/utils.dart
@@ -19,13 +19,12 @@
   final _requestController = StreamController<String>();
 
   /// The client.
-  json_rpc.Client get client => _client;
-  json_rpc.Client _client;
+  late final json_rpc.Client client;
 
   ClientController() {
-    _client = json_rpc.Client(
+    client = json_rpc.Client(
         StreamChannel(_responseController.stream, _requestController.sink));
-    _client.listen();
+    client.listen();
   }
 
   /// Expects that the client will send a request.
diff --git a/pkgs/json_rpc_2/test/server/utils.dart b/pkgs/json_rpc_2/test/server/utils.dart
index 7ff491c..92f1d32 100644
--- a/pkgs/json_rpc_2/test/server/utils.dart
+++ b/pkgs/json_rpc_2/test/server/utils.dart
@@ -20,17 +20,16 @@
   final _responseController = StreamController<String>();
 
   /// The server.
-  json_rpc.Server get server => _server;
-  json_rpc.Server _server;
+  late final json_rpc.Server server;
 
   ServerController(
-      {json_rpc.ErrorCallback onUnhandledError,
+      {json_rpc.ErrorCallback? onUnhandledError,
       bool strictProtocolChecks = true}) {
-    _server = json_rpc.Server(
+    server = json_rpc.Server(
         StreamChannel(_requestController.stream, _responseController.sink),
         onUnhandledError: onUnhandledError,
         strictProtocolChecks: strictProtocolChecks);
-    _server.listen();
+    server.listen();
   }
 
   /// Passes [request], a decoded request, to [server] and returns its decoded
@@ -66,11 +65,7 @@
 
 /// Returns a matcher that matches a [json_rpc.RpcException] with an
 /// `invalid_params` error code.
-Matcher throwsInvalidParams(String message) {
-  return throwsA(predicate((error) {
-    expect(error, TypeMatcher<json_rpc.RpcException>());
-    expect(error.code, equals(error_code.INVALID_PARAMS));
-    expect(error.message, equals(message));
-    return true;
-  }));
-}
+Matcher throwsInvalidParams(String message) =>
+    throwsA(TypeMatcher<json_rpc.RpcException>()
+        .having((e) => e.code, 'code', error_code.INVALID_PARAMS)
+        .having((e) => e.message, 'message', message));