From a3cd89079301e1c09bdea6690ec9717c15340ec5 Mon Sep 17 00:00:00 2001 From: Muhammed Bayraktar Date: Tue, 25 Aug 2026 11:09:53 +0300 Subject: [PATCH 1/2] [pigeon] Treat NSNull as null for non-null Flutter API returns `FlutterStandardReader` substitutes `NSNull` for a `nil` element of a list, so a null reply for a non-null return value arrives as `NSNull` rather than as `nil`. The generated `listResponse[0] == nil` check did not catch that, and the following force cast aborted the process. In `webview_flutter_wkwebview` this crashes apps whenever the native instance manager cannot resolve the returned instance, which the plugin itself causes by clearing the manager on scene disconnect and app termination while an authentication challenge is in flight. --- packages/pigeon/CHANGELOG.md | 5 ++ .../example/app/ios/Runner/Messages.g.swift | 2 +- packages/pigeon/lib/src/generator_tools.dart | 2 +- .../pigeon/lib/src/swift/swift_generator.dart | 22 +++++--- .../Sources/test_plugin/CoreTests.gen.swift | 56 +++++++++---------- .../test_plugin/ProxyApiTests.gen.swift | 24 ++++---- .../RunnerTests/NullableReturnsTests.swift | 26 +++++++++ packages/pigeon/pubspec.yaml | 2 +- .../pigeon/test/swift_generator_test.dart | 29 ++++++++++ 9 files changed, 118 insertions(+), 50 deletions(-) diff --git a/packages/pigeon/CHANGELOG.md b/packages/pigeon/CHANGELOG.md index 86575bae5d6e..342c5dbfbdeb 100644 --- a/packages/pigeon/CHANGELOG.md +++ b/packages/pigeon/CHANGELOG.md @@ -1,3 +1,8 @@ +## 28.0.1 + +* [swift] Fixes a crash when a Flutter API returns null for a non-null return + value and the null arrives as `NSNull`. + ## 28.0.0 * **Breaking Change** Updates Kotlin and Swift generators to generate `suspend` functions and `async throws` signatures for `@FlutterApi` methods by default, and for `@HostApi` methods annotated with `@async`. diff --git a/packages/pigeon/example/app/ios/Runner/Messages.g.swift b/packages/pigeon/example/app/ios/Runner/Messages.g.swift index 5e527c7f538f..7d89f83a890c 100644 --- a/packages/pigeon/example/app/ios/Runner/Messages.g.swift +++ b/packages/pigeon/example/app/ios/Runner/Messages.g.swift @@ -401,7 +401,7 @@ class MessageFlutterApi: MessageFlutterApiProtocol { let message: String? = nilOrValue(listResponse[1]) let details: String? = nilOrValue(listResponse[2]) continuation.resume(throwing: PigeonError(code: code, message: message, details: details)) - } else if listResponse[0] == nil { + } else if listResponse[0] == nil || listResponse[0] is NSNull { continuation.resume( throwing: PigeonError( code: "null-error", diff --git a/packages/pigeon/lib/src/generator_tools.dart b/packages/pigeon/lib/src/generator_tools.dart index 6259b92c5ce3..97cee4572cea 100644 --- a/packages/pigeon/lib/src/generator_tools.dart +++ b/packages/pigeon/lib/src/generator_tools.dart @@ -15,7 +15,7 @@ import 'generator.dart'; /// The current version of pigeon. /// /// This must match the version in pubspec.yaml. -const String pigeonVersion = '28.0.0'; +const String pigeonVersion = '28.0.1'; /// Default plugin package name. const String defaultPluginPackageName = 'dev.flutter.pigeon'; diff --git a/packages/pigeon/lib/src/swift/swift_generator.dart b/packages/pigeon/lib/src/swift/swift_generator.dart index 17f96ca56c48..34710e6a5b7e 100644 --- a/packages/pigeon/lib/src/swift/swift_generator.dart +++ b/packages/pigeon/lib/src/swift/swift_generator.dart @@ -1751,13 +1751,21 @@ static func deepHash(value: Any?, hasher: inout Hasher) { ); }, addTrailingNewline: false); if (!returnType.isNullable && !returnType.isVoid) { - indent.addScoped('else if listResponse[0] == nil {', '} ', () { - indent.writeln( - resumeError( - '${_getErrorClassName(generatorOptions)}(code: "null-error", message: "Flutter api returned null value for non-null return value.", details: "")', - ), - ); - }, addTrailingNewline: false); + // `FlutterStandardReader` substitutes `NSNull` for a `nil` element of + // a list, so a null reply can arrive as either. See + // https://github.com/flutter/flutter/issues/191254. + indent.addScoped( + 'else if listResponse[0] == nil || listResponse[0] is NSNull {', + '} ', + () { + indent.writeln( + resumeError( + '${_getErrorClassName(generatorOptions)}(code: "null-error", message: "Flutter api returned null value for non-null return value.", details: "")', + ), + ); + }, + addTrailingNewline: false, + ); } indent.addScoped('else {', '}', () { if (returnType.isVoid) { diff --git a/packages/pigeon/platform_tests/test_plugin/darwin/test_plugin/Sources/test_plugin/CoreTests.gen.swift b/packages/pigeon/platform_tests/test_plugin/darwin/test_plugin/Sources/test_plugin/CoreTests.gen.swift index 4afcc7a806cb..0bc6ec3e5204 100644 --- a/packages/pigeon/platform_tests/test_plugin/darwin/test_plugin/Sources/test_plugin/CoreTests.gen.swift +++ b/packages/pigeon/platform_tests/test_plugin/darwin/test_plugin/Sources/test_plugin/CoreTests.gen.swift @@ -4815,7 +4815,7 @@ class FlutterCallbackCoreApi: FlutterCallbackCoreApiProtocol { let message: String? = nilOrValue(listResponse[1]) let details: String? = nilOrValue(listResponse[2]) completion(.failure(PigeonError(code: code, message: message, details: details))) - } else if listResponse[0] == nil { + } else if listResponse[0] == nil || listResponse[0] is NSNull { completion( .failure( PigeonError( @@ -5098,7 +5098,7 @@ class FlutterIntegrationCoreApi: FlutterIntegrationCoreApiProtocol { let message: String? = nilOrValue(listResponse[1]) let details: String? = nilOrValue(listResponse[2]) continuation.resume(throwing: PigeonError(code: code, message: message, details: details)) - } else if listResponse[0] == nil { + } else if listResponse[0] == nil || listResponse[0] is NSNull { continuation.resume( throwing: PigeonError( code: "null-error", @@ -5157,7 +5157,7 @@ class FlutterIntegrationCoreApi: FlutterIntegrationCoreApiProtocol { let message: String? = nilOrValue(listResponse[1]) let details: String? = nilOrValue(listResponse[2]) continuation.resume(throwing: PigeonError(code: code, message: message, details: details)) - } else if listResponse[0] == nil { + } else if listResponse[0] == nil || listResponse[0] is NSNull { continuation.resume( throwing: PigeonError( code: "null-error", @@ -5218,7 +5218,7 @@ class FlutterIntegrationCoreApi: FlutterIntegrationCoreApiProtocol { let message: String? = nilOrValue(listResponse[1]) let details: String? = nilOrValue(listResponse[2]) continuation.resume(throwing: PigeonError(code: code, message: message, details: details)) - } else if listResponse[0] == nil { + } else if listResponse[0] == nil || listResponse[0] is NSNull { continuation.resume( throwing: PigeonError( code: "null-error", @@ -5247,7 +5247,7 @@ class FlutterIntegrationCoreApi: FlutterIntegrationCoreApiProtocol { let message: String? = nilOrValue(listResponse[1]) let details: String? = nilOrValue(listResponse[2]) continuation.resume(throwing: PigeonError(code: code, message: message, details: details)) - } else if listResponse[0] == nil { + } else if listResponse[0] == nil || listResponse[0] is NSNull { continuation.resume( throwing: PigeonError( code: "null-error", @@ -5276,7 +5276,7 @@ class FlutterIntegrationCoreApi: FlutterIntegrationCoreApiProtocol { let message: String? = nilOrValue(listResponse[1]) let details: String? = nilOrValue(listResponse[2]) continuation.resume(throwing: PigeonError(code: code, message: message, details: details)) - } else if listResponse[0] == nil { + } else if listResponse[0] == nil || listResponse[0] is NSNull { continuation.resume( throwing: PigeonError( code: "null-error", @@ -5305,7 +5305,7 @@ class FlutterIntegrationCoreApi: FlutterIntegrationCoreApiProtocol { let message: String? = nilOrValue(listResponse[1]) let details: String? = nilOrValue(listResponse[2]) continuation.resume(throwing: PigeonError(code: code, message: message, details: details)) - } else if listResponse[0] == nil { + } else if listResponse[0] == nil || listResponse[0] is NSNull { continuation.resume( throwing: PigeonError( code: "null-error", @@ -5334,7 +5334,7 @@ class FlutterIntegrationCoreApi: FlutterIntegrationCoreApiProtocol { let message: String? = nilOrValue(listResponse[1]) let details: String? = nilOrValue(listResponse[2]) continuation.resume(throwing: PigeonError(code: code, message: message, details: details)) - } else if listResponse[0] == nil { + } else if listResponse[0] == nil || listResponse[0] is NSNull { continuation.resume( throwing: PigeonError( code: "null-error", @@ -5363,7 +5363,7 @@ class FlutterIntegrationCoreApi: FlutterIntegrationCoreApiProtocol { let message: String? = nilOrValue(listResponse[1]) let details: String? = nilOrValue(listResponse[2]) continuation.resume(throwing: PigeonError(code: code, message: message, details: details)) - } else if listResponse[0] == nil { + } else if listResponse[0] == nil || listResponse[0] is NSNull { continuation.resume( throwing: PigeonError( code: "null-error", @@ -5392,7 +5392,7 @@ class FlutterIntegrationCoreApi: FlutterIntegrationCoreApiProtocol { let message: String? = nilOrValue(listResponse[1]) let details: String? = nilOrValue(listResponse[2]) continuation.resume(throwing: PigeonError(code: code, message: message, details: details)) - } else if listResponse[0] == nil { + } else if listResponse[0] == nil || listResponse[0] is NSNull { continuation.resume( throwing: PigeonError( code: "null-error", @@ -5421,7 +5421,7 @@ class FlutterIntegrationCoreApi: FlutterIntegrationCoreApiProtocol { let message: String? = nilOrValue(listResponse[1]) let details: String? = nilOrValue(listResponse[2]) continuation.resume(throwing: PigeonError(code: code, message: message, details: details)) - } else if listResponse[0] == nil { + } else if listResponse[0] == nil || listResponse[0] is NSNull { continuation.resume( throwing: PigeonError( code: "null-error", @@ -5450,7 +5450,7 @@ class FlutterIntegrationCoreApi: FlutterIntegrationCoreApiProtocol { let message: String? = nilOrValue(listResponse[1]) let details: String? = nilOrValue(listResponse[2]) continuation.resume(throwing: PigeonError(code: code, message: message, details: details)) - } else if listResponse[0] == nil { + } else if listResponse[0] == nil || listResponse[0] is NSNull { continuation.resume( throwing: PigeonError( code: "null-error", @@ -5479,7 +5479,7 @@ class FlutterIntegrationCoreApi: FlutterIntegrationCoreApiProtocol { let message: String? = nilOrValue(listResponse[1]) let details: String? = nilOrValue(listResponse[2]) continuation.resume(throwing: PigeonError(code: code, message: message, details: details)) - } else if listResponse[0] == nil { + } else if listResponse[0] == nil || listResponse[0] is NSNull { continuation.resume( throwing: PigeonError( code: "null-error", @@ -5508,7 +5508,7 @@ class FlutterIntegrationCoreApi: FlutterIntegrationCoreApiProtocol { let message: String? = nilOrValue(listResponse[1]) let details: String? = nilOrValue(listResponse[2]) continuation.resume(throwing: PigeonError(code: code, message: message, details: details)) - } else if listResponse[0] == nil { + } else if listResponse[0] == nil || listResponse[0] is NSNull { continuation.resume( throwing: PigeonError( code: "null-error", @@ -5537,7 +5537,7 @@ class FlutterIntegrationCoreApi: FlutterIntegrationCoreApiProtocol { let message: String? = nilOrValue(listResponse[1]) let details: String? = nilOrValue(listResponse[2]) continuation.resume(throwing: PigeonError(code: code, message: message, details: details)) - } else if listResponse[0] == nil { + } else if listResponse[0] == nil || listResponse[0] is NSNull { continuation.resume( throwing: PigeonError( code: "null-error", @@ -5566,7 +5566,7 @@ class FlutterIntegrationCoreApi: FlutterIntegrationCoreApiProtocol { let message: String? = nilOrValue(listResponse[1]) let details: String? = nilOrValue(listResponse[2]) continuation.resume(throwing: PigeonError(code: code, message: message, details: details)) - } else if listResponse[0] == nil { + } else if listResponse[0] == nil || listResponse[0] is NSNull { continuation.resume( throwing: PigeonError( code: "null-error", @@ -5595,7 +5595,7 @@ class FlutterIntegrationCoreApi: FlutterIntegrationCoreApiProtocol { let message: String? = nilOrValue(listResponse[1]) let details: String? = nilOrValue(listResponse[2]) continuation.resume(throwing: PigeonError(code: code, message: message, details: details)) - } else if listResponse[0] == nil { + } else if listResponse[0] == nil || listResponse[0] is NSNull { continuation.resume( throwing: PigeonError( code: "null-error", @@ -5624,7 +5624,7 @@ class FlutterIntegrationCoreApi: FlutterIntegrationCoreApiProtocol { let message: String? = nilOrValue(listResponse[1]) let details: String? = nilOrValue(listResponse[2]) continuation.resume(throwing: PigeonError(code: code, message: message, details: details)) - } else if listResponse[0] == nil { + } else if listResponse[0] == nil || listResponse[0] is NSNull { continuation.resume( throwing: PigeonError( code: "null-error", @@ -5655,7 +5655,7 @@ class FlutterIntegrationCoreApi: FlutterIntegrationCoreApiProtocol { let message: String? = nilOrValue(listResponse[1]) let details: String? = nilOrValue(listResponse[2]) continuation.resume(throwing: PigeonError(code: code, message: message, details: details)) - } else if listResponse[0] == nil { + } else if listResponse[0] == nil || listResponse[0] is NSNull { continuation.resume( throwing: PigeonError( code: "null-error", @@ -5684,7 +5684,7 @@ class FlutterIntegrationCoreApi: FlutterIntegrationCoreApiProtocol { let message: String? = nilOrValue(listResponse[1]) let details: String? = nilOrValue(listResponse[2]) continuation.resume(throwing: PigeonError(code: code, message: message, details: details)) - } else if listResponse[0] == nil { + } else if listResponse[0] == nil || listResponse[0] is NSNull { continuation.resume( throwing: PigeonError( code: "null-error", @@ -5713,7 +5713,7 @@ class FlutterIntegrationCoreApi: FlutterIntegrationCoreApiProtocol { let message: String? = nilOrValue(listResponse[1]) let details: String? = nilOrValue(listResponse[2]) continuation.resume(throwing: PigeonError(code: code, message: message, details: details)) - } else if listResponse[0] == nil { + } else if listResponse[0] == nil || listResponse[0] is NSNull { continuation.resume( throwing: PigeonError( code: "null-error", @@ -5742,7 +5742,7 @@ class FlutterIntegrationCoreApi: FlutterIntegrationCoreApiProtocol { let message: String? = nilOrValue(listResponse[1]) let details: String? = nilOrValue(listResponse[2]) continuation.resume(throwing: PigeonError(code: code, message: message, details: details)) - } else if listResponse[0] == nil { + } else if listResponse[0] == nil || listResponse[0] is NSNull { continuation.resume( throwing: PigeonError( code: "null-error", @@ -5773,7 +5773,7 @@ class FlutterIntegrationCoreApi: FlutterIntegrationCoreApiProtocol { let message: String? = nilOrValue(listResponse[1]) let details: String? = nilOrValue(listResponse[2]) continuation.resume(throwing: PigeonError(code: code, message: message, details: details)) - } else if listResponse[0] == nil { + } else if listResponse[0] == nil || listResponse[0] is NSNull { continuation.resume( throwing: PigeonError( code: "null-error", @@ -5802,7 +5802,7 @@ class FlutterIntegrationCoreApi: FlutterIntegrationCoreApiProtocol { let message: String? = nilOrValue(listResponse[1]) let details: String? = nilOrValue(listResponse[2]) continuation.resume(throwing: PigeonError(code: code, message: message, details: details)) - } else if listResponse[0] == nil { + } else if listResponse[0] == nil || listResponse[0] is NSNull { continuation.resume( throwing: PigeonError( code: "null-error", @@ -5831,7 +5831,7 @@ class FlutterIntegrationCoreApi: FlutterIntegrationCoreApiProtocol { let message: String? = nilOrValue(listResponse[1]) let details: String? = nilOrValue(listResponse[2]) continuation.resume(throwing: PigeonError(code: code, message: message, details: details)) - } else if listResponse[0] == nil { + } else if listResponse[0] == nil || listResponse[0] is NSNull { continuation.resume( throwing: PigeonError( code: "null-error", @@ -6401,7 +6401,7 @@ class FlutterIntegrationCoreApi: FlutterIntegrationCoreApiProtocol { let message: String? = nilOrValue(listResponse[1]) let details: String? = nilOrValue(listResponse[2]) continuation.resume(throwing: PigeonError(code: code, message: message, details: details)) - } else if listResponse[0] == nil { + } else if listResponse[0] == nil || listResponse[0] is NSNull { continuation.resume( throwing: PigeonError( code: "null-error", @@ -6700,7 +6700,7 @@ class FlutterSmallApi: FlutterSmallApiProtocol { let message: String? = nilOrValue(listResponse[1]) let details: String? = nilOrValue(listResponse[2]) continuation.resume(throwing: PigeonError(code: code, message: message, details: details)) - } else if listResponse[0] == nil { + } else if listResponse[0] == nil || listResponse[0] is NSNull { continuation.resume( throwing: PigeonError( code: "null-error", @@ -6728,7 +6728,7 @@ class FlutterSmallApi: FlutterSmallApiProtocol { let message: String? = nilOrValue(listResponse[1]) let details: String? = nilOrValue(listResponse[2]) continuation.resume(throwing: PigeonError(code: code, message: message, details: details)) - } else if listResponse[0] == nil { + } else if listResponse[0] == nil || listResponse[0] is NSNull { continuation.resume( throwing: PigeonError( code: "null-error", diff --git a/packages/pigeon/platform_tests/test_plugin/darwin/test_plugin/Sources/test_plugin/ProxyApiTests.gen.swift b/packages/pigeon/platform_tests/test_plugin/darwin/test_plugin/Sources/test_plugin/ProxyApiTests.gen.swift index cdcddc324439..1b670d1dd086 100644 --- a/packages/pigeon/platform_tests/test_plugin/darwin/test_plugin/Sources/test_plugin/ProxyApiTests.gen.swift +++ b/packages/pigeon/platform_tests/test_plugin/darwin/test_plugin/Sources/test_plugin/ProxyApiTests.gen.swift @@ -3019,7 +3019,7 @@ final class PigeonApiProxyApiTestClass: PigeonApiProtocolProxyApiTestClass { let message: String? = nilOrValue(listResponse[1]) let details: String? = nilOrValue(listResponse[2]) completion(.failure(ProxyApiTestsError(code: code, message: message, details: details))) - } else if listResponse[0] == nil { + } else if listResponse[0] == nil || listResponse[0] is NSNull { completion( .failure( ProxyApiTestsError( @@ -3070,7 +3070,7 @@ final class PigeonApiProxyApiTestClass: PigeonApiProtocolProxyApiTestClass { let message: String? = nilOrValue(listResponse[1]) let details: String? = nilOrValue(listResponse[2]) completion(.failure(ProxyApiTestsError(code: code, message: message, details: details))) - } else if listResponse[0] == nil { + } else if listResponse[0] == nil || listResponse[0] is NSNull { completion( .failure( ProxyApiTestsError( @@ -3121,7 +3121,7 @@ final class PigeonApiProxyApiTestClass: PigeonApiProtocolProxyApiTestClass { let message: String? = nilOrValue(listResponse[1]) let details: String? = nilOrValue(listResponse[2]) completion(.failure(ProxyApiTestsError(code: code, message: message, details: details))) - } else if listResponse[0] == nil { + } else if listResponse[0] == nil || listResponse[0] is NSNull { completion( .failure( ProxyApiTestsError( @@ -3172,7 +3172,7 @@ final class PigeonApiProxyApiTestClass: PigeonApiProtocolProxyApiTestClass { let message: String? = nilOrValue(listResponse[1]) let details: String? = nilOrValue(listResponse[2]) completion(.failure(ProxyApiTestsError(code: code, message: message, details: details))) - } else if listResponse[0] == nil { + } else if listResponse[0] == nil || listResponse[0] is NSNull { completion( .failure( ProxyApiTestsError( @@ -3223,7 +3223,7 @@ final class PigeonApiProxyApiTestClass: PigeonApiProtocolProxyApiTestClass { let message: String? = nilOrValue(listResponse[1]) let details: String? = nilOrValue(listResponse[2]) completion(.failure(ProxyApiTestsError(code: code, message: message, details: details))) - } else if listResponse[0] == nil { + } else if listResponse[0] == nil || listResponse[0] is NSNull { completion( .failure( ProxyApiTestsError( @@ -3274,7 +3274,7 @@ final class PigeonApiProxyApiTestClass: PigeonApiProtocolProxyApiTestClass { let message: String? = nilOrValue(listResponse[1]) let details: String? = nilOrValue(listResponse[2]) completion(.failure(ProxyApiTestsError(code: code, message: message, details: details))) - } else if listResponse[0] == nil { + } else if listResponse[0] == nil || listResponse[0] is NSNull { completion( .failure( ProxyApiTestsError( @@ -3326,7 +3326,7 @@ final class PigeonApiProxyApiTestClass: PigeonApiProtocolProxyApiTestClass { let message: String? = nilOrValue(listResponse[1]) let details: String? = nilOrValue(listResponse[2]) completion(.failure(ProxyApiTestsError(code: code, message: message, details: details))) - } else if listResponse[0] == nil { + } else if listResponse[0] == nil || listResponse[0] is NSNull { completion( .failure( ProxyApiTestsError( @@ -3377,7 +3377,7 @@ final class PigeonApiProxyApiTestClass: PigeonApiProtocolProxyApiTestClass { let message: String? = nilOrValue(listResponse[1]) let details: String? = nilOrValue(listResponse[2]) completion(.failure(ProxyApiTestsError(code: code, message: message, details: details))) - } else if listResponse[0] == nil { + } else if listResponse[0] == nil || listResponse[0] is NSNull { completion( .failure( ProxyApiTestsError( @@ -3430,7 +3430,7 @@ final class PigeonApiProxyApiTestClass: PigeonApiProtocolProxyApiTestClass { let message: String? = nilOrValue(listResponse[1]) let details: String? = nilOrValue(listResponse[2]) completion(.failure(ProxyApiTestsError(code: code, message: message, details: details))) - } else if listResponse[0] == nil { + } else if listResponse[0] == nil || listResponse[0] is NSNull { completion( .failure( ProxyApiTestsError( @@ -3481,7 +3481,7 @@ final class PigeonApiProxyApiTestClass: PigeonApiProtocolProxyApiTestClass { let message: String? = nilOrValue(listResponse[1]) let details: String? = nilOrValue(listResponse[2]) completion(.failure(ProxyApiTestsError(code: code, message: message, details: details))) - } else if listResponse[0] == nil { + } else if listResponse[0] == nil || listResponse[0] is NSNull { completion( .failure( ProxyApiTestsError( @@ -3532,7 +3532,7 @@ final class PigeonApiProxyApiTestClass: PigeonApiProtocolProxyApiTestClass { let message: String? = nilOrValue(listResponse[1]) let details: String? = nilOrValue(listResponse[2]) completion(.failure(ProxyApiTestsError(code: code, message: message, details: details))) - } else if listResponse[0] == nil { + } else if listResponse[0] == nil || listResponse[0] is NSNull { completion( .failure( ProxyApiTestsError( @@ -4034,7 +4034,7 @@ final class PigeonApiProxyApiTestClass: PigeonApiProtocolProxyApiTestClass { let message: String? = nilOrValue(listResponse[1]) let details: String? = nilOrValue(listResponse[2]) completion(.failure(ProxyApiTestsError(code: code, message: message, details: details))) - } else if listResponse[0] == nil { + } else if listResponse[0] == nil || listResponse[0] is NSNull { completion( .failure( ProxyApiTestsError( diff --git a/packages/pigeon/platform_tests/test_plugin/example/ios/RunnerTests/NullableReturnsTests.swift b/packages/pigeon/platform_tests/test_plugin/example/ios/RunnerTests/NullableReturnsTests.swift index 930a4ceb1d4a..14434a26ac35 100644 --- a/packages/pigeon/platform_tests/test_plugin/example/ios/RunnerTests/NullableReturnsTests.swift +++ b/packages/pigeon/platform_tests/test_plugin/example/ios/RunnerTests/NullableReturnsTests.swift @@ -53,3 +53,29 @@ struct NullableReturnsTests { #expect(api.x == nil) } } + +/// Regression test for https://github.com/flutter/flutter/issues/191254. +/// +/// `FlutterStandardReader` substitutes `NSNull` for a `nil` element of a list, +/// so a null reply for a non-null return value arrives as `NSNull` rather than +/// as `nil`. Before the fix that value was force-cast to the return type, which +/// aborted the process instead of reporting an error. +@MainActor +struct NullReplyForNonNullReturnTests { + let codec = FlutterStandardMessageCodec.sharedInstance() + + @Test + func nullReplyForNonNullReturnFailsWithoutCrashing() async throws { + let binaryMessenger = MockBinaryMessenger(codec: codec) + binaryMessenger.result = NSNull() + let api = FlutterIntegrationCoreApi(binaryMessenger: binaryMessenger) + + do { + // `sendMultipleNullableTypes` has a non-null return value. + _ = try await api.sendMultipleNullableTypes(aBool: nil, anInt: nil, aString: nil) + Issue.record("Expected a null-error but the call succeeded.") + } catch let error as PigeonError { + #expect(error.code == "null-error") + } + } +} diff --git a/packages/pigeon/pubspec.yaml b/packages/pigeon/pubspec.yaml index 0f754e2878fe..bbe32babac38 100644 --- a/packages/pigeon/pubspec.yaml +++ b/packages/pigeon/pubspec.yaml @@ -2,7 +2,7 @@ name: pigeon 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%3A%22p%3A+pigeon%22 -version: 28.0.0 # This must match the version in lib/src/generator_tools.dart +version: 28.0.1 # This must match the version in lib/src/generator_tools.dart environment: sdk: ^3.11.0 diff --git a/packages/pigeon/test/swift_generator_test.dart b/packages/pigeon/test/swift_generator_test.dart index 05ab0cdafa9a..a37fe9be5af7 100644 --- a/packages/pigeon/test/swift_generator_test.dart +++ b/packages/pigeon/test/swift_generator_test.dart @@ -1149,6 +1149,35 @@ void main() { expect(code, contains('channel.sendMessage([xArg, yArg] as [Any?]) { response in')); }); + test('non-null return from flutter api treats NSNull as null', () { + final root = Root( + apis: [ + AstFlutterApi( + name: 'Api', + methods: [ + Method( + name: 'doit', + location: ApiLocation.flutter, + parameters: [], + returnType: const TypeDeclaration(baseName: 'int', isNullable: false), + ), + ], + ), + ], + classes: [], + enums: [], + ); + final sink = StringBuffer(); + const swiftOptions = InternalSwiftOptions(swiftOut: ''); + const generator = SwiftGenerator(); + generator.generate(swiftOptions, root, sink, dartPackageName: DEFAULT_PACKAGE_NAME); + final code = sink.toString(); + // `FlutterStandardReader` substitutes `NSNull` for a `nil` element of a + // list, so both need to be treated as a null reply before the value is + // cast. See https://github.com/flutter/flutter/issues/191254. + expect(code, contains('else if listResponse[0] == nil || listResponse[0] is NSNull {')); + }); + test('return nullable host', () { final root = Root( apis: [ From 06c1df54b362478f4407081d14dc2451ac414880 Mon Sep 17 00:00:00 2001 From: Muhammed Bayraktar Date: Thu, 3 Sep 2026 09:12:42 +0300 Subject: [PATCH 2/2] Address review: drop explanatory comments, fold the regression test into NullableReturnsTests --- packages/pigeon/lib/src/swift/swift_generator.dart | 3 --- .../ios/RunnerTests/NullableReturnsTests.swift | 14 +------------- packages/pigeon/test/swift_generator_test.dart | 3 --- 3 files changed, 1 insertion(+), 19 deletions(-) diff --git a/packages/pigeon/lib/src/swift/swift_generator.dart b/packages/pigeon/lib/src/swift/swift_generator.dart index 34710e6a5b7e..3278d1db3744 100644 --- a/packages/pigeon/lib/src/swift/swift_generator.dart +++ b/packages/pigeon/lib/src/swift/swift_generator.dart @@ -1751,9 +1751,6 @@ static func deepHash(value: Any?, hasher: inout Hasher) { ); }, addTrailingNewline: false); if (!returnType.isNullable && !returnType.isVoid) { - // `FlutterStandardReader` substitutes `NSNull` for a `nil` element of - // a list, so a null reply can arrive as either. See - // https://github.com/flutter/flutter/issues/191254. indent.addScoped( 'else if listResponse[0] == nil || listResponse[0] is NSNull {', '} ', diff --git a/packages/pigeon/platform_tests/test_plugin/example/ios/RunnerTests/NullableReturnsTests.swift b/packages/pigeon/platform_tests/test_plugin/example/ios/RunnerTests/NullableReturnsTests.swift index 14434a26ac35..fb7fe0fb7a7a 100644 --- a/packages/pigeon/platform_tests/test_plugin/example/ios/RunnerTests/NullableReturnsTests.swift +++ b/packages/pigeon/platform_tests/test_plugin/example/ios/RunnerTests/NullableReturnsTests.swift @@ -52,26 +52,14 @@ struct NullableReturnsTests { #expect(api.didCall) #expect(api.x == nil) } -} - -/// Regression test for https://github.com/flutter/flutter/issues/191254. -/// -/// `FlutterStandardReader` substitutes `NSNull` for a `nil` element of a list, -/// so a null reply for a non-null return value arrives as `NSNull` rather than -/// as `nil`. Before the fix that value was force-cast to the return type, which -/// aborted the process instead of reporting an error. -@MainActor -struct NullReplyForNonNullReturnTests { - let codec = FlutterStandardMessageCodec.sharedInstance() @Test - func nullReplyForNonNullReturnFailsWithoutCrashing() async throws { + func nonNullReturnFailsOnNSNullResponse() async throws { let binaryMessenger = MockBinaryMessenger(codec: codec) binaryMessenger.result = NSNull() let api = FlutterIntegrationCoreApi(binaryMessenger: binaryMessenger) do { - // `sendMultipleNullableTypes` has a non-null return value. _ = try await api.sendMultipleNullableTypes(aBool: nil, anInt: nil, aString: nil) Issue.record("Expected a null-error but the call succeeded.") } catch let error as PigeonError { diff --git a/packages/pigeon/test/swift_generator_test.dart b/packages/pigeon/test/swift_generator_test.dart index a37fe9be5af7..4802e12619c6 100644 --- a/packages/pigeon/test/swift_generator_test.dart +++ b/packages/pigeon/test/swift_generator_test.dart @@ -1172,9 +1172,6 @@ void main() { const generator = SwiftGenerator(); generator.generate(swiftOptions, root, sink, dartPackageName: DEFAULT_PACKAGE_NAME); final code = sink.toString(); - // `FlutterStandardReader` substitutes `NSNull` for a `nil` element of a - // list, so both need to be treated as a null reply before the value is - // cast. See https://github.com/flutter/flutter/issues/191254. expect(code, contains('else if listResponse[0] == nil || listResponse[0] is NSNull {')); });