[pigeon] Minor fixes to NNBD type handling in Dart (#1543)

diff --git a/packages/pigeon/CHANGELOG.md b/packages/pigeon/CHANGELOG.md
index d848818..d3d130a 100644
--- a/packages/pigeon/CHANGELOG.md
+++ b/packages/pigeon/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 3.0.2
+
+* Fixes non-nullable classes and enums as fields.
+* Fixes nullable collections as return types.
+
 ## 3.0.1
 
 * Enables NNBD for the Pigeon tool itself.
diff --git a/packages/pigeon/lib/dart_generator.dart b/packages/pigeon/lib/dart_generator.dart
index 982044c..3625b36 100644
--- a/packages/pigeon/lib/dart_generator.dart
+++ b/packages/pigeon/lib/dart_generator.dart
@@ -192,10 +192,11 @@
         final String returnType = _makeGenericTypeArguments(func.returnType);
         final String castCall = _makeGenericCastCall(func.returnType);
         const String accessor = 'replyMap[\'${Keys.result}\']';
-        final String unwrapper = func.returnType.isNullable ? '' : '!';
+        final String nullHandler =
+            func.returnType.isNullable ? (castCall.isEmpty ? '' : '?') : '!';
         final String returnStatement = func.returnType.isVoid
             ? 'return;'
-            : 'return ($accessor as $returnType?)$unwrapper$castCall;';
+            : 'return ($accessor as $returnType?)$nullHandler$castCall;';
         indent.format('''
 final Map<Object?, Object?>? replyMap =\n\t\tawait channel.send($sendArgument) as Map<Object?, Object?>?;
 if (replyMap == null) {
@@ -459,13 +460,14 @@
         );
         for (final NamedType field in klass.fields) {
           indent.write('pigeonMap[\'${field.name}\'] = ');
+          final String conditional = field.type.isNullable ? '?' : '';
           if (customClassNames.contains(field.type.baseName)) {
             indent.addln(
-              '${field.name}?.encode();',
+              '${field.name}$conditional.encode();',
             );
           } else if (customEnumNames.contains(field.type.baseName)) {
             indent.addln(
-              '${field.name}?.index;',
+              '${field.name}$conditional.index;',
             );
           } else {
             indent.addln('${field.name};');
@@ -478,15 +480,29 @@
     void writeDecode() {
       void writeValueDecode(NamedType field) {
         if (customClassNames.contains(field.type.baseName)) {
-          indent.format('''
+          final String nonNullValue =
+              "${field.type.baseName}.decode(pigeonMap['${field.name}']!)";
+          indent.format(
+              field.type.isNullable
+                  ? '''
 pigeonMap['${field.name}'] != null
-\t\t? ${field.type.baseName}.decode(pigeonMap['${field.name}']!)
-\t\t: null''', leadingSpace: false, trailingNewline: false);
+\t\t? $nonNullValue
+\t\t: null'''
+                  : nonNullValue,
+              leadingSpace: false,
+              trailingNewline: false);
         } else if (customEnumNames.contains(field.type.baseName)) {
-          indent.format('''
+          final String nonNullValue =
+              "${field.type.baseName}.values[pigeonMap['${field.name}']! as int]";
+          indent.format(
+              field.type.isNullable
+                  ? '''
 pigeonMap['${field.name}'] != null
-\t\t? ${field.type.baseName}.values[pigeonMap['${field.name}']! as int]
-\t\t: null''', leadingSpace: false, trailingNewline: false);
+\t\t? $nonNullValue
+\t\t: null'''
+                  : nonNullValue,
+              leadingSpace: false,
+              trailingNewline: false);
         } else if (field.type.typeArguments.isNotEmpty) {
           final String genericType = _makeGenericTypeArguments(field.type);
           final String castCall = _makeGenericCastCall(field.type);
diff --git a/packages/pigeon/lib/generator_tools.dart b/packages/pigeon/lib/generator_tools.dart
index 65397d9..a22c7e4 100644
--- a/packages/pigeon/lib/generator_tools.dart
+++ b/packages/pigeon/lib/generator_tools.dart
@@ -7,8 +7,8 @@
 import 'dart:mirrors';
 import 'ast.dart';
 
-/// The current version of pigeon. This must match the version in pubspec.yaml.
-const String pigeonVersion = '3.0.1';
+/// The current version of pigeon. This must match the version in pubspec.yaml.\
+const String pigeonVersion = '3.0.2';
 
 /// Read all the content from [stdin] to a String.
 String readStdin() {
diff --git a/packages/pigeon/pigeons/non_null_fields.dart b/packages/pigeon/pigeons/non_null_fields.dart
index bbce336..aa96b87 100644
--- a/packages/pigeon/pigeons/non_null_fields.dart
+++ b/packages/pigeon/pigeons/non_null_fields.dart
@@ -12,11 +12,21 @@
   String query;
 }
 
+class ExtraData {
+  ExtraData({required this.detailA, required this.detailB});
+  String detailA;
+  String detailB;
+}
+
+enum ReplyType { success, error }
+
 class SearchReply {
-  SearchReply(this.result, this.error, this.indices);
+  SearchReply(this.result, this.error, this.indices, this.extraData, this.type);
   String result;
   String error;
   List<int?> indices;
+  ExtraData extraData;
+  ReplyType type;
 }
 
 @HostApi()
diff --git a/packages/pigeon/pigeons/nullable_returns.dart b/packages/pigeon/pigeons/nullable_returns.dart
index 150d5d3..548e159 100644
--- a/packages/pigeon/pigeons/nullable_returns.dart
+++ b/packages/pigeon/pigeons/nullable_returns.dart
@@ -8,12 +8,12 @@
 import 'package:pigeon/pigeon.dart';
 
 @HostApi()
-abstract class NonNullHostApi {
+abstract class NullableReturnHostApi {
   int? doit();
 }
 
 @FlutterApi()
-abstract class NonNullFlutterApi {
+abstract class NullableReturnFlutterApi {
   int? doit();
 }
 
@@ -26,3 +26,23 @@
 abstract class NullableArgFlutterApi {
   int doit(int? x);
 }
+
+@HostApi()
+abstract class NullableCollectionReturnHostApi {
+  List<String?>? doit();
+}
+
+@FlutterApi()
+abstract class NullableCollectionReturnFlutterApi {
+  List<String?>? doit();
+}
+
+@HostApi()
+abstract class NullableCollectionArgHostApi {
+  List<String?> doit(List<String?>? x);
+}
+
+@FlutterApi()
+abstract class NullableCollectionArgFlutterApi {
+  List<String?> doit(List<String?>? x);
+}
diff --git a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/non_null_fields.gen.dart b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/non_null_fields.gen.dart
index 9dbcebe..0b7e513 100644
--- a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/non_null_fields.gen.dart
+++ b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/non_null_fields.gen.dart
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 //
-// Autogenerated from Pigeon (v2.0.4), do not edit directly.
+// Autogenerated from Pigeon (v3.0.2), do not edit directly.
 // See also: https://pub.dev/packages/pigeon
 // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name
 // @dart = 2.12
@@ -12,6 +12,11 @@
 import 'package:flutter/foundation.dart' show WriteBuffer, ReadBuffer;
 import 'package:flutter/services.dart';
 
+enum ReplyType {
+  success,
+  error,
+}
+
 class SearchRequest {
   SearchRequest({
     required this.query,
@@ -33,22 +38,53 @@
   }
 }
 
+class ExtraData {
+  ExtraData({
+    required this.detailA,
+    required this.detailB,
+  });
+
+  String detailA;
+  String detailB;
+
+  Object encode() {
+    final Map<Object?, Object?> pigeonMap = <Object?, Object?>{};
+    pigeonMap['detailA'] = detailA;
+    pigeonMap['detailB'] = detailB;
+    return pigeonMap;
+  }
+
+  static ExtraData decode(Object message) {
+    final Map<Object?, Object?> pigeonMap = message as Map<Object?, Object?>;
+    return ExtraData(
+      detailA: pigeonMap['detailA']! as String,
+      detailB: pigeonMap['detailB']! as String,
+    );
+  }
+}
+
 class SearchReply {
   SearchReply({
     required this.result,
     required this.error,
     required this.indices,
+    required this.extraData,
+    required this.type,
   });
 
   String result;
   String error;
   List<int?> indices;
+  ExtraData extraData;
+  ReplyType type;
 
   Object encode() {
     final Map<Object?, Object?> pigeonMap = <Object?, Object?>{};
     pigeonMap['result'] = result;
     pigeonMap['error'] = error;
     pigeonMap['indices'] = indices;
+    pigeonMap['extraData'] = extraData.encode();
+    pigeonMap['type'] = type.index;
     return pigeonMap;
   }
 
@@ -58,6 +94,8 @@
       result: pigeonMap['result']! as String,
       error: pigeonMap['error']! as String,
       indices: (pigeonMap['indices'] as List<Object?>?)!.cast<int?>(),
+      extraData: ExtraData.decode(pigeonMap['extraData']!),
+      type: ReplyType.values[pigeonMap['type']! as int],
     );
   }
 }
@@ -66,12 +104,15 @@
   const _NonNullHostApiCodec();
   @override
   void writeValue(WriteBuffer buffer, Object? value) {
-    if (value is SearchReply) {
+    if (value is ExtraData) {
       buffer.putUint8(128);
       writeValue(buffer, value.encode());
-    } else if (value is SearchRequest) {
+    } else if (value is SearchReply) {
       buffer.putUint8(129);
       writeValue(buffer, value.encode());
+    } else if (value is SearchRequest) {
+      buffer.putUint8(130);
+      writeValue(buffer, value.encode());
     } else {
       super.writeValue(buffer, value);
     }
@@ -81,9 +122,12 @@
   Object? readValueOfType(int type, ReadBuffer buffer) {
     switch (type) {
       case 128:
-        return SearchReply.decode(readValue(buffer)!);
+        return ExtraData.decode(readValue(buffer)!);
 
       case 129:
+        return SearchReply.decode(readValue(buffer)!);
+
+      case 130:
         return SearchRequest.decode(readValue(buffer)!);
 
       default:
@@ -137,12 +181,15 @@
   const _NonNullFlutterApiCodec();
   @override
   void writeValue(WriteBuffer buffer, Object? value) {
-    if (value is SearchReply) {
+    if (value is ExtraData) {
       buffer.putUint8(128);
       writeValue(buffer, value.encode());
-    } else if (value is SearchRequest) {
+    } else if (value is SearchReply) {
       buffer.putUint8(129);
       writeValue(buffer, value.encode());
+    } else if (value is SearchRequest) {
+      buffer.putUint8(130);
+      writeValue(buffer, value.encode());
     } else {
       super.writeValue(buffer, value);
     }
@@ -152,9 +199,12 @@
   Object? readValueOfType(int type, ReadBuffer buffer) {
     switch (type) {
       case 128:
-        return SearchReply.decode(readValue(buffer)!);
+        return ExtraData.decode(readValue(buffer)!);
 
       case 129:
+        return SearchReply.decode(readValue(buffer)!);
+
+      case 130:
         return SearchRequest.decode(readValue(buffer)!);
 
       default:
diff --git a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/nullable_returns.gen.dart b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/nullable_returns.gen.dart
index cf07297..52cf1e5 100644
--- a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/nullable_returns.gen.dart
+++ b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/nullable_returns.gen.dart
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 //
-// Autogenerated from Pigeon (v2.0.4), do not edit directly.
+// Autogenerated from Pigeon (v3.0.2), do not edit directly.
 // See also: https://pub.dev/packages/pigeon
 // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name
 // @dart = 2.12
@@ -12,24 +12,24 @@
 import 'package:flutter/foundation.dart' show WriteBuffer, ReadBuffer;
 import 'package:flutter/services.dart';
 
-class _NonNullHostApiCodec extends StandardMessageCodec {
-  const _NonNullHostApiCodec();
+class _NullableReturnHostApiCodec extends StandardMessageCodec {
+  const _NullableReturnHostApiCodec();
 }
 
-class NonNullHostApi {
-  /// Constructor for [NonNullHostApi].  The [binaryMessenger] named argument is
+class NullableReturnHostApi {
+  /// Constructor for [NullableReturnHostApi].  The [binaryMessenger] named argument is
   /// available for dependency injection.  If it is left null, the default
   /// BinaryMessenger will be used which routes to the host platform.
-  NonNullHostApi({BinaryMessenger? binaryMessenger})
+  NullableReturnHostApi({BinaryMessenger? binaryMessenger})
       : _binaryMessenger = binaryMessenger;
 
   final BinaryMessenger? _binaryMessenger;
 
-  static const MessageCodec<Object?> codec = _NonNullHostApiCodec();
+  static const MessageCodec<Object?> codec = _NullableReturnHostApiCodec();
 
   Future<int?> doit() async {
     final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
-        'dev.flutter.pigeon.NonNullHostApi.doit', codec,
+        'dev.flutter.pigeon.NullableReturnHostApi.doit', codec,
         binaryMessenger: _binaryMessenger);
     final Map<Object?, Object?>? replyMap =
         await channel.send(null) as Map<Object?, Object?>?;
@@ -52,19 +52,19 @@
   }
 }
 
-class _NonNullFlutterApiCodec extends StandardMessageCodec {
-  const _NonNullFlutterApiCodec();
+class _NullableReturnFlutterApiCodec extends StandardMessageCodec {
+  const _NullableReturnFlutterApiCodec();
 }
 
-abstract class NonNullFlutterApi {
-  static const MessageCodec<Object?> codec = _NonNullFlutterApiCodec();
+abstract class NullableReturnFlutterApi {
+  static const MessageCodec<Object?> codec = _NullableReturnFlutterApiCodec();
 
   int? doit();
-  static void setup(NonNullFlutterApi? api,
+  static void setup(NullableReturnFlutterApi? api,
       {BinaryMessenger? binaryMessenger}) {
     {
       final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
-          'dev.flutter.pigeon.NonNullFlutterApi.doit', codec,
+          'dev.flutter.pigeon.NullableReturnFlutterApi.doit', codec,
           binaryMessenger: binaryMessenger);
       if (api == null) {
         channel.setMessageHandler(null);
@@ -153,3 +153,150 @@
     }
   }
 }
+
+class _NullableCollectionReturnHostApiCodec extends StandardMessageCodec {
+  const _NullableCollectionReturnHostApiCodec();
+}
+
+class NullableCollectionReturnHostApi {
+  /// Constructor for [NullableCollectionReturnHostApi].  The [binaryMessenger] named argument is
+  /// available for dependency injection.  If it is left null, the default
+  /// BinaryMessenger will be used which routes to the host platform.
+  NullableCollectionReturnHostApi({BinaryMessenger? binaryMessenger})
+      : _binaryMessenger = binaryMessenger;
+
+  final BinaryMessenger? _binaryMessenger;
+
+  static const MessageCodec<Object?> codec =
+      _NullableCollectionReturnHostApiCodec();
+
+  Future<List<String?>?> doit() async {
+    final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+        'dev.flutter.pigeon.NullableCollectionReturnHostApi.doit', codec,
+        binaryMessenger: _binaryMessenger);
+    final Map<Object?, Object?>? replyMap =
+        await channel.send(null) as Map<Object?, Object?>?;
+    if (replyMap == null) {
+      throw PlatformException(
+        code: 'channel-error',
+        message: 'Unable to establish connection on channel.',
+      );
+    } else if (replyMap['error'] != null) {
+      final Map<Object?, Object?> error =
+          (replyMap['error'] as Map<Object?, Object?>?)!;
+      throw PlatformException(
+        code: (error['code'] as String?)!,
+        message: error['message'] as String?,
+        details: error['details'],
+      );
+    } else {
+      return (replyMap['result'] as List<Object?>?)?.cast<String?>();
+    }
+  }
+}
+
+class _NullableCollectionReturnFlutterApiCodec extends StandardMessageCodec {
+  const _NullableCollectionReturnFlutterApiCodec();
+}
+
+abstract class NullableCollectionReturnFlutterApi {
+  static const MessageCodec<Object?> codec =
+      _NullableCollectionReturnFlutterApiCodec();
+
+  List<String?>? doit();
+  static void setup(NullableCollectionReturnFlutterApi? api,
+      {BinaryMessenger? binaryMessenger}) {
+    {
+      final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+          'dev.flutter.pigeon.NullableCollectionReturnFlutterApi.doit', codec,
+          binaryMessenger: binaryMessenger);
+      if (api == null) {
+        channel.setMessageHandler(null);
+      } else {
+        channel.setMessageHandler((Object? message) async {
+          // ignore message
+          final List<String?>? output = api.doit();
+          return output;
+        });
+      }
+    }
+  }
+}
+
+class _NullableCollectionArgHostApiCodec extends StandardMessageCodec {
+  const _NullableCollectionArgHostApiCodec();
+}
+
+class NullableCollectionArgHostApi {
+  /// Constructor for [NullableCollectionArgHostApi].  The [binaryMessenger] named argument is
+  /// available for dependency injection.  If it is left null, the default
+  /// BinaryMessenger will be used which routes to the host platform.
+  NullableCollectionArgHostApi({BinaryMessenger? binaryMessenger})
+      : _binaryMessenger = binaryMessenger;
+
+  final BinaryMessenger? _binaryMessenger;
+
+  static const MessageCodec<Object?> codec =
+      _NullableCollectionArgHostApiCodec();
+
+  Future<List<String?>> doit(List<String?>? arg_x) async {
+    final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+        'dev.flutter.pigeon.NullableCollectionArgHostApi.doit', codec,
+        binaryMessenger: _binaryMessenger);
+    final Map<Object?, Object?>? replyMap =
+        await channel.send(<Object?>[arg_x]) as Map<Object?, Object?>?;
+    if (replyMap == null) {
+      throw PlatformException(
+        code: 'channel-error',
+        message: 'Unable to establish connection on channel.',
+      );
+    } else if (replyMap['error'] != null) {
+      final Map<Object?, Object?> error =
+          (replyMap['error'] as Map<Object?, Object?>?)!;
+      throw PlatformException(
+        code: (error['code'] as String?)!,
+        message: error['message'] as String?,
+        details: error['details'],
+      );
+    } else if (replyMap['result'] == null) {
+      throw PlatformException(
+        code: 'null-error',
+        message: 'Host platform returned null value for non-null return value.',
+      );
+    } else {
+      return (replyMap['result'] as List<Object?>?)!.cast<String?>();
+    }
+  }
+}
+
+class _NullableCollectionArgFlutterApiCodec extends StandardMessageCodec {
+  const _NullableCollectionArgFlutterApiCodec();
+}
+
+abstract class NullableCollectionArgFlutterApi {
+  static const MessageCodec<Object?> codec =
+      _NullableCollectionArgFlutterApiCodec();
+
+  List<String?> doit(List<String?>? x);
+  static void setup(NullableCollectionArgFlutterApi? api,
+      {BinaryMessenger? binaryMessenger}) {
+    {
+      final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+          'dev.flutter.pigeon.NullableCollectionArgFlutterApi.doit', codec,
+          binaryMessenger: binaryMessenger);
+      if (api == null) {
+        channel.setMessageHandler(null);
+      } else {
+        channel.setMessageHandler((Object? message) async {
+          assert(message != null,
+              'Argument for dev.flutter.pigeon.NullableCollectionArgFlutterApi.doit was null.');
+          final List<Object?> args = (message as List<Object?>?)!;
+          final List<String?>? arg_x =
+              (args[0] as List<Object?>?)?.cast<String?>();
+          final List<String?> output = api.doit(arg_x);
+          return output;
+        });
+      }
+    }
+  }
+}
diff --git a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/pubspec.yaml b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/pubspec.yaml
index 9b21234..dad6b7c 100644
--- a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/pubspec.yaml
+++ b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/pubspec.yaml
@@ -10,7 +10,7 @@
     sdk: flutter
 
 dev_dependencies:
-  build_runner: ^1.11.0
+  build_runner: ^2.1.10
   flutter_test:
     sdk: flutter
   mockito: ^5.0.4
diff --git a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/test/null_safe_test.dart b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/test/null_safe_test.dart
index 9c0ab20..5cd468c 100644
--- a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/test/null_safe_test.dart
+++ b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/test/null_safe_test.dart
@@ -13,7 +13,13 @@
 import 'null_safe_test.mocks.dart';
 import 'test_util.dart';
 
-@GenerateMocks(<Type>[BinaryMessenger, NullableArgFlutterApi])
+@GenerateMocks(<Type>[
+  BinaryMessenger,
+  NullableArgFlutterApi,
+  NullableReturnFlutterApi,
+  NullableCollectionArgFlutterApi,
+  NullableCollectionReturnFlutterApi,
+])
 void main() {
   TestWidgetsFlutterBinding.ensureInitialized();
 
@@ -105,6 +111,21 @@
     expect(await api.doit(null), 123);
   });
 
+  test('send null collection parameter', () async {
+    final BinaryMessenger mockMessenger = MockBinaryMessenger();
+    const String channel =
+        'dev.flutter.pigeon.NullableCollectionArgHostApi.doit';
+    when(mockMessenger.send(channel, any))
+        .thenAnswer((Invocation realInvocation) async {
+      return Api.codec.encodeMessage(<String?, Object?>{
+        'result': <String?>['123']
+      });
+    });
+    final NullableCollectionArgHostApi api =
+        NullableCollectionArgHostApi(binaryMessenger: mockMessenger);
+    expect(await api.doit(null), <String?>['123']);
+  });
+
   test('receive null parameters', () {
     final MockNullableArgFlutterApi mockFlutterApi =
         MockNullableArgFlutterApi();
@@ -115,8 +136,9 @@
     final Completer<int> resultCompleter = Completer<int>();
     // Null check operator is used because ServicesBinding.instance is nullable
     // in earlier versions of Flutter.
-    // ignore: unnecessary_non_null_assertion
-    ServicesBinding.instance!.defaultBinaryMessenger.handlePlatformMessage(
+    _ambiguate(ServicesBinding.instance)!
+        .defaultBinaryMessenger
+        .handlePlatformMessage(
       'dev.flutter.pigeon.NullableArgFlutterApi.doit',
       NullableArgFlutterApi.codec.encodeMessage(<Object?>[null]),
       (ByteData? data) {
@@ -131,4 +153,121 @@
     // Removes message handlers from global default binary messenger.
     NullableArgFlutterApi.setup(null);
   });
+
+  test('receive null collection parameters', () {
+    final MockNullableCollectionArgFlutterApi mockFlutterApi =
+        MockNullableCollectionArgFlutterApi();
+    when(mockFlutterApi.doit(null)).thenReturn(<String?>['14']);
+
+    NullableCollectionArgFlutterApi.setup(mockFlutterApi);
+
+    final Completer<List<String?>> resultCompleter = Completer<List<String?>>();
+    // Null check operator is used because ServicesBinding.instance is nullable
+    // in earlier versions of Flutter.
+    _ambiguate(ServicesBinding.instance)!
+        .defaultBinaryMessenger
+        .handlePlatformMessage(
+      'dev.flutter.pigeon.NullableCollectionArgFlutterApi.doit',
+      NullableCollectionArgFlutterApi.codec.encodeMessage(<Object?>[null]),
+      (ByteData? data) {
+        resultCompleter.complete(
+          (NullableCollectionArgFlutterApi.codec.decodeMessage(data)!
+                  as List<Object?>)
+              .cast<String>(),
+        );
+      },
+    );
+
+    expect(resultCompleter.future, completion(<String>['14']));
+
+    // Removes message handlers from global default binary messenger.
+    NullableArgFlutterApi.setup(null);
+  });
+
+  test('receive null return', () async {
+    final BinaryMessenger mockMessenger = MockBinaryMessenger();
+    const String channel = 'dev.flutter.pigeon.NullableReturnHostApi.doit';
+    when(mockMessenger.send(channel, any))
+        .thenAnswer((Invocation realInvocation) async {
+      return NullableReturnHostApi.codec
+          .encodeMessage(<String?, Object?>{'result': null});
+    });
+    final NullableReturnHostApi api =
+        NullableReturnHostApi(binaryMessenger: mockMessenger);
+    expect(await api.doit(), null);
+  });
+
+  test('receive null collection return', () async {
+    final BinaryMessenger mockMessenger = MockBinaryMessenger();
+    const String channel =
+        'dev.flutter.pigeon.NullableCollectionReturnHostApi.doit';
+    when(mockMessenger.send(channel, any))
+        .thenAnswer((Invocation realInvocation) async {
+      return NullableCollectionReturnHostApi.codec
+          .encodeMessage(<String?, Object?>{'result': null});
+    });
+    final NullableCollectionReturnHostApi api =
+        NullableCollectionReturnHostApi(binaryMessenger: mockMessenger);
+    expect(await api.doit(), null);
+  });
+
+  test('send null return', () async {
+    final MockNullableReturnFlutterApi mockFlutterApi =
+        MockNullableReturnFlutterApi();
+    when(mockFlutterApi.doit()).thenReturn(null);
+
+    NullableReturnFlutterApi.setup(mockFlutterApi);
+
+    final Completer<int?> resultCompleter = Completer<int?>();
+    // Null check operator is used because ServicesBinding.instance is nullable
+    // in earlier versions of Flutter.
+    _ambiguate(ServicesBinding.instance)!
+        .defaultBinaryMessenger
+        .handlePlatformMessage(
+      'dev.flutter.pigeon.NullableReturnFlutterApi.doit',
+      NullableReturnFlutterApi.codec.encodeMessage(<Object?>[]),
+      (ByteData? data) {
+        resultCompleter.complete(null);
+      },
+    );
+
+    expect(resultCompleter.future, completion(null));
+
+    // Removes message handlers from global default binary messenger.
+    NullableArgFlutterApi.setup(null);
+  });
+
+  test('send null collection return', () async {
+    final MockNullableCollectionReturnFlutterApi mockFlutterApi =
+        MockNullableCollectionReturnFlutterApi();
+    when(mockFlutterApi.doit()).thenReturn(null);
+
+    NullableCollectionReturnFlutterApi.setup(mockFlutterApi);
+
+    final Completer<List<String?>?> resultCompleter =
+        Completer<List<String?>?>();
+    // Null check operator is used because ServicesBinding.instance is nullable
+    // in earlier versions of Flutter.
+    _ambiguate(ServicesBinding.instance)!
+        .defaultBinaryMessenger
+        .handlePlatformMessage(
+      'dev.flutter.pigeon.NullableCollectionReturnFlutterApi.doit',
+      NullableCollectionReturnFlutterApi.codec.encodeMessage(<Object?>[]),
+      (ByteData? data) {
+        resultCompleter.complete(null);
+      },
+    );
+
+    expect(resultCompleter.future, completion(null));
+
+    // Removes message handlers from global default binary messenger.
+    NullableArgFlutterApi.setup(null);
+  });
 }
+
+/// This allows a value of type T or T? to be treated as a value of type T?.
+///
+/// We use this so that APIs that have become non-nullable can still be used
+/// with `!` and `?` on the stable branch.
+// TODO(stuartmorgan): Remove this once 2.13 or later is on the stable channel.
+T? _ambiguate<T>(T? value) => value;
diff --git a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/test/null_safe_test.mocks.dart b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/test/null_safe_test.mocks.dart
index 5f8f129..9b5bfcb 100644
--- a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/test/null_safe_test.mocks.dart
+++ b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/test/null_safe_test.mocks.dart
@@ -56,3 +56,38 @@
       (super.noSuchMethod(Invocation.method(#doit, [x]), returnValue: 0)
           as int);
 }
+
+/// A class which mocks [NullableReturnFlutterApi].
+///
+/// See the documentation for Mockito's code generation for more information.
+class MockNullableReturnFlutterApi extends _i1.Mock
+    implements _i6.NullableReturnFlutterApi {
+  MockNullableReturnFlutterApi() {
+    _i1.throwOnMissingStub(this);
+  }
+}
+
+/// A class which mocks [NullableCollectionArgFlutterApi].
+///
+/// See the documentation for Mockito's code generation for more information.
+class MockNullableCollectionArgFlutterApi extends _i1.Mock
+    implements _i6.NullableCollectionArgFlutterApi {
+  MockNullableCollectionArgFlutterApi() {
+    _i1.throwOnMissingStub(this);
+  }
+
+  @override
+  List<String?> doit(List<String?>? x) => (super
+          .noSuchMethod(Invocation.method(#doit, [x]), returnValue: <String?>[])
+      as List<String?>);
+}
+
+/// A class which mocks [NullableCollectionReturnFlutterApi].
+///
+/// See the documentation for Mockito's code generation for more information.
+class MockNullableCollectionReturnFlutterApi extends _i1.Mock
+    implements _i6.NullableCollectionReturnFlutterApi {
+  MockNullableCollectionReturnFlutterApi() {
+    _i1.throwOnMissingStub(this);
+  }
+}
diff --git a/packages/pigeon/pubspec.yaml b/packages/pigeon/pubspec.yaml
index bd17ad5..a08046a 100644
--- a/packages/pigeon/pubspec.yaml
+++ b/packages/pigeon/pubspec.yaml
@@ -2,7 +2,7 @@
 description: Code generator tool to make communication between Flutter and the host platform type-safe and easier.
 repository: https://github.com/flutter/packages/tree/main/packages/pigeon
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3Apigeon
-version: 3.0.1 # This must match the version in lib/generator_tools.dart
+version: 3.0.2 # This must match the version in lib/generator_tools.dart
 
 environment:
   sdk: ">=2.12.0 <3.0.0"
diff --git a/packages/pigeon/test/dart_generator_test.dart b/packages/pigeon/test/dart_generator_test.dart
index 4f03de6..a7c2bef 100644
--- a/packages/pigeon/test/dart_generator_test.dart
+++ b/packages/pigeon/test/dart_generator_test.dart
@@ -204,6 +204,50 @@
     );
   });
 
+  test('nested non-nullable class', () {
+    final Root root = Root(apis: <Api>[], classes: <Class>[
+      Class(
+        name: 'Input',
+        fields: <NamedType>[
+          NamedType(
+              type: const TypeDeclaration(
+                baseName: 'String',
+                isNullable: false,
+              ),
+              name: 'input',
+              offset: null)
+        ],
+      ),
+      Class(
+        name: 'Nested',
+        fields: <NamedType>[
+          NamedType(
+              type: const TypeDeclaration(
+                baseName: 'Input',
+                isNullable: false,
+              ),
+              name: 'nested',
+              offset: null)
+        ],
+      )
+    ], enums: <Enum>[]);
+    final StringBuffer sink = StringBuffer();
+    generateDart(const DartOptions(), root, sink);
+    final String code = sink.toString();
+    expect(
+      code,
+      contains(
+        'pigeonMap[\'nested\'] = nested.encode()',
+      ),
+    );
+    expect(
+      code.replaceAll('\n', ' ').replaceAll('  ', ''),
+      contains(
+        'nested: Input.decode(pigeonMap[\'nested\']!)',
+      ),
+    );
+  });
+
   test('flutterapi', () {
     final Root root = Root(apis: <Api>[
       Api(name: 'Api', location: ApiLocation.flutter, methods: <Method>[
@@ -401,6 +445,51 @@
     expect(code, contains('EnumClass doSomething(EnumClass arg0);'));
   });
 
+  test('flutter non-nullable enum argument with enum class', () {
+    final Root root = Root(apis: <Api>[
+      Api(name: 'Api', location: ApiLocation.flutter, methods: <Method>[
+        Method(
+          name: 'doSomething',
+          arguments: <NamedType>[
+            NamedType(
+                type: const TypeDeclaration(
+                  baseName: 'EnumClass',
+                  isNullable: false,
+                ),
+                name: '',
+                offset: null)
+          ],
+          returnType:
+              const TypeDeclaration(baseName: 'EnumClass', isNullable: false),
+          isAsynchronous: false,
+        )
+      ])
+    ], classes: <Class>[
+      Class(name: 'EnumClass', fields: <NamedType>[
+        NamedType(
+            type: const TypeDeclaration(
+              baseName: 'Enum',
+              isNullable: false,
+            ),
+            name: 'enum1',
+            offset: null)
+      ]),
+    ], enums: <Enum>[
+      Enum(
+        name: 'Enum',
+        members: <String>[
+          'one',
+          'two',
+        ],
+      )
+    ]);
+    final StringBuffer sink = StringBuffer();
+    generateDart(const DartOptions(), root, sink);
+    final String code = sink.toString();
+    expect(code, contains('pigeonMap[\'enum1\'] = enum1.index;'));
+    expect(code, contains('enum1: Enum.values[pigeonMap[\'enum1\']! as int]'));
+  });
+
   test('host void argument', () {
     final Root root = Root(apis: <Api>[
       Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
@@ -893,6 +982,34 @@
     expect(code, contains('return (replyMap[\'result\'] as int?);'));
   });
 
+  test('return nullable collection host', () {
+    final Root root = Root(
+      apis: <Api>[
+        Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+          Method(
+              name: 'doit',
+              returnType: const TypeDeclaration(
+                  baseName: 'List',
+                  isNullable: true,
+                  typeArguments: <TypeDeclaration>[
+                    TypeDeclaration(baseName: 'int', isNullable: true)
+                  ]),
+              arguments: <NamedType>[])
+        ])
+      ],
+      classes: <Class>[],
+      enums: <Enum>[],
+    );
+    final StringBuffer sink = StringBuffer();
+    generateDart(const DartOptions(), root, sink);
+    final String code = sink.toString();
+    expect(code, contains('Future<List<int?>?> doit()'));
+    expect(
+        code,
+        contains(
+            'return (replyMap[\'result\'] as List<Object?>?)?.cast<int?>();'));
+  });
+
   test('return nullable async host', () {
     final Root root = Root(
       apis: <Api>[