-
Notifications
You must be signed in to change notification settings - Fork 3.3k
[pigeon] fix swift nsnull casting crash #3545
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Are you able to repro this from the pigeon file in the issue? If not we should figure out how to get an integration test for it first before we try to fix it. |
final String nullableConditionPrefix = | ||
type.isNullable ? '$value == nil ? nil : ' : ''; | ||
return '$nullableConditionPrefix${_swiftTypeForDartType(type)}(rawValue: $value as! Int)$forceUnwrap'; | ||
return '${_swiftTypeForDartType(type)}(rawValue: $value as! Int)!'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is wrong for nullable types without the nullableConditionPrefix you removed; if value
is nil
or NSNull
this cast is invalid. Are we not using this codepath?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this code path is already after a nil check for nullable enums, so it is irrelevant at this point.
A previous iterration of this code moved the entirety of that logic into this method, but it ended up not working properly, and really didn't match the use of this method in other instances so I relocated it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When you say "this code path" do you mean the way it's currently called, rather than something in the method? Because if so, that seems very dangerous; it would be easy to violate that later without even knowing it wasn't intended to work.
If we can't make something self-contained work here, can we instead move this elsewhere, and assert
that type
isn't an enum here so this isn't a danger?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This line of code is all that's needed for non-nullable enums.
Nullable enums need extra code to check for nil before creating the enum from the int that is provided.
I had merged these lines into a single ternary before, but that was causing some other issues. I think I've resolved those now though, so I'll give it another try
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I fixed this in a really jank way, going to send up a better version tomorrow
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This still has the issue that if you call it with a nullable enum it will happily output crashing code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's true, I can nest this method inside the other to make it uncallable. It is never used except in the context of _writeDecodeCasting
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
or this function could just be removed entirely, since it is not really needed any longer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think
oops typo - added generic function example in separate comment. the generic function should allow re-using the casting part
packages/pigeon/platform_tests/test_plugin/macos/Classes/CoreTests.gen.swift
Outdated
Show resolved
Hide resolved
packages/pigeon/platform_tests/test_plugin/macos/Classes/CoreTests.gen.swift
Outdated
Show resolved
Hide resolved
I think you can do a generic version like this:
Then in the caller you can infer the type like this:
|
I'm fairly certain I tried this exact code and it wouldn't work. I'll give it another attempt though for good measure! |
In case it's helpful, you can try out this code snippet in Swift playground:
|
I just get a lot of this |
|
You have to remove |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM with a couple of nits.
} else if (type.baseName == 'int') { | ||
final String orString = | ||
type.isNullable ? 'nilOrValue($value)' : '$value as! Int64'; | ||
return '($value is Int32) ? Int64($value as! Int32) : $orString'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It occurs to me that we should reverse the conditional based on what I found about NSNumber
; as long as the underlying value actually is an NSNumber
, "casting" to Int64
directly will work, so checking for Int32
first (which will also work) will result in two conversions (NSNumber
-> Int32
> Int64
) instead of one.
So:
final String int64String = type.isNullable ? 'nilOrValue($value)' : '$value as! Int64';
return '($value is Int64) ? $int64String : Int64($value as! Int32);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That doesn't work for nullables though
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And I'm fairly certain it doesn't lower the conversion amount, since casting checking the cast type doesn't perform an actual casting
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And I'm fairly certain it doesn't lower the conversion amount, since casting checking the cast type doesn't perform an actual casting
But the cast does. In the version you have in the PR now:
(args[0] is Int32) ? Int64(args[0] as! Int32) : args[0] as! Int64
The flow for a NSNumber (what the codec currently produces) with a value that fits in 32 bits (which is going to be ~all of them in most usage) is, according to the evolution doc and observed behavior:
- Check
is Int32
-> true, because the value can be safely converted toInt32
(despite appearances, this is not actually just doing a type check;NSNumber
is not anInt32
or anInt64
, butis
will return true for it if it can convert). - Do
as! Int32
-> does a conversion, not a cast (again, despite appearances), creating anInt32
from theNSNumber
's value. - Do
Int64(theAnonymousNewInt32)
-> does a conversion from Int32 toInt64
, creating anInt64
and discarding theInt32
.
What we want is to convert the NSNumber
directly to an Int64
, which always work.
That doesn't work for nullables though
Ah, right. Maybe we should just make ints fully custom then:
non-nullable: let foo : Int64 = $value is Int64 ? $value as! Int64 : Int64($value as! Int32)
nullable: let foo : Int64? = $value is NSNull ? nil : $value is Int64? ? $value as! Int64? : Int64($value as! Int32)
(Usually I think nested ternaries are a crime against nature, but here it's short and in generated code, and we can put a comment in the Dart explaining the logic, so I think it's okay, and it avoids the mess of generating extra variables.)
@@ -607,6 +589,75 @@ import FlutterMacOS | |||
indent.newln(); | |||
} | |||
|
|||
/// Writes decode and casting code for any type. | |||
/// | |||
/// Optional parameters are necessary for class decoding only. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's more subtle than that; I'm pretty sure that passing customClassNames
would actively break things (creating essentially the bug I fixed for C++ in #3573). I was actually confused at first about how the PR wasn't causing a regression there until I noticed that the call sites weren't passing the class list.
If I'm correct about that, I think it would be better to make things much more explicit. E.g., you could rename customClassNames
to listEncodedClassNames
, and change this comment to explicitly say that a value for that must be provided only in the case of class decoding, with a link to flutter/flutter#119351 for context. (Alternately you could leave the name, but add an explicit bool like I did in the C++ generator.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I renamed them. And made the comment more specific.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM!
Let's land it quick before we find any more unusual is
or as
behavior 😁
auto label is removed for flutter/packages, pr: 3545, due to This PR has not met approval requirements for merging. Changes were requested by {stuartmorgan}, please make the needed changes and resubmit this PR.
|
Hm, this looks like flutter/flutter#99033 happening again. @godofredoc @CaseyHillers Any idea why this would be happening again? |
My guess is that it is related to how the pr was approved, then changes requested, then approved again. |
The universe is against allowing this pr to ever land |
You can land it manually once the tests runs have all finished; hopefully the autosubmit bug can be investigated after the fact (and if not, getting this particular fix in trumps the autosubmit issue in importance). |
## The problem This PR fixes a weird casting behavior discussed [here](#3545 (comment)): ``` private func nilOrValue<T>(_ value: Any?) -> T? { if value is NSNull { return nil } return (value as Any) as! T? // <- HERE } ``` Without this intermediate `as Any` cast, [these 3 tests](https://github.com/flutter/packages/blob/5662a7e5799c723f76e9589a75c9d9310e2ba8c1/packages/pigeon/platform_tests/test_plugin/example/ios/RunnerTests/RunnerTests.swift#L10-L29) would crash with `SIGABRT: Could not cast value of type 'Swift.Optional<Any>' (0x7ff865e84b38) to 'Swift.String' (0x7ff865e7de08).` ## Investigation The crash happens because `value` here is actually of type `Any??` (nested optional!). When it crashes, the debugger simply shows `value` is `nil`. But if we print in `lldb`, the `value` here is actually an inner `Optional.none` case nested by an outer `Optional.some` case. ### Why does `Any??` crash Since outer case is `some`, it fails to force cast to `T?` (e.g. `String?`) due to type mismatch. ### How did we end up with `Any??` It's related to the signature of these 3 functions: - `func toList() -> [Any?]` - `func fromList(args: [Any])` - `func nilOrValue<T>(_ value: Any?) -> T?` Firstly `toList` returns `nil` (of type `Any?`) as the first element of array. Then the type gets coerced as an `Any` type in `fromList`. Then because `nilOrValue` takes `Any?`, this `nil` value gets wrapped by an `Optional.some`. Hence the nested optional. ## Workarounds ### Workaround 1: `as Any` This is the current code [in this PR](https://github.com/flutter/packages/pull/3545/files#r1155061282). When casting `Optional.some(nil) as Any`, it erases the outer Optional, so no problem casting to `T?`. ### Workaround 2: Handle with nested optional directly: ``` private func nilOrValue<T>(_ value: Any?) -> T? { if value is NSNull { return nil } // `if let` deals with "outer some" case and then erase the outer Optional if let val = value { // here returns "outer some + inner some" or "outer some + inner none" return val as! T? } // here returns "outer none" return nil } ``` A similar version of this was also [attempted in this PR](https://github.com/flutter/packages/pull/3545/files/241f0e31e32917f5501dab11f81ab0fbf064687f#diff-bfdb6a91beb03a906435e77e0168117f3f3977ee4d6f8bcaa1724156ae4dc27cR647-R650). It just that we did not know why that worked previously, and now we know! ### Workaround 3 Casting value to nested optional (`T??`), then stripe the outer optional ``` private func nilOrValue<T>(_ value: Any?) -> T? { if value is NSNull { return nil } return (value as! T??) ?? nil } ``` ## Solutions These above workarounds handle nested optionals. However, **a real solution should prevent nested optionals from happening in the first place**, since they are so tricky. ### Solution 1 (This PR) The nested optional happens when we do cast from `Any?` to `Any` and then wrapped into `Any?`. (Refer to "How did we end up with Any??" section). So the easiest way is just to use `func fromList(args: [Any?])` to match the types of `func toList` and `func nilOrValue`. ### Solution 2 Solution 2 is the opposite - avoid using `Any?` as much as possible. Drawbacks compare to Solution 1: a. When inter-op with ObjC, `nullable id` is exported as `Any?`. So we can't 100% prevent `Any?` usage. Though this can be addressed by immediately cast it to `Any`. b. Losing of semantic meaning of `Any?` that it <s>can</s> must be optional. The hidden/implicit optional **is** the culprit here in the first place. c. While this solution fixes the nested optional issue, it does not address all issues related to implicit optional. For example: https://github.com/flutter/packages/blob/c53db71f496b436e48629a8f3e4152c48e63cd66/packages/pigeon/platform_tests/test_plugin/ios/Classes/CoreTests.gen.swift#L563-L564 This is supposed to crash if `args[0]` is `nil`. However, the crash is silenced because `as! [Any]` will make `args[0]` an implicit optional! The correct codegen should instead be: ``` let args = message as! [Any?] let anObjectArg = args[0]! ``` ### Solution 3 Just remove `as Any` and update the test. The nested optional won't happen in production code, because ObjC `NSArray` contains `NSNull` rather than `nil` when exporting to Swift. We can simply fix [the tests](https://github.com/flutter/packages/blob/5662a7e5799c723f76e9589a75c9d9310e2ba8c1/packages/pigeon/platform_tests/test_plugin/example/ios/RunnerTests/RunnerTests.swift#L10-L29) by replacing `nil`s with `NSNull`s. However, if we were to re-write engine's codec to Swift, it's actually better practice to use `nil` and not `NSNull` in the array. ## Additional TODO We would've caught this earlier if this were an error rather than warning in our unit test.  *List which issues are fixed by this PR. You must list at least one issue.* #3545 (comment) *If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].*
## The problem This PR fixes a weird casting behavior discussed [here](flutter#3545 (comment)): ``` private func nilOrValue<T>(_ value: Any?) -> T? { if value is NSNull { return nil } return (value as Any) as! T? // <- HERE } ``` Without this intermediate `as Any` cast, [these 3 tests](https://github.com/flutter/packages/blob/5662a7e5799c723f76e9589a75c9d9310e2ba8c1/packages/pigeon/platform_tests/test_plugin/example/ios/RunnerTests/RunnerTests.swift#L10-L29) would crash with `SIGABRT: Could not cast value of type 'Swift.Optional<Any>' (0x7ff865e84b38) to 'Swift.String' (0x7ff865e7de08).` ## Investigation The crash happens because `value` here is actually of type `Any??` (nested optional!). When it crashes, the debugger simply shows `value` is `nil`. But if we print in `lldb`, the `value` here is actually an inner `Optional.none` case nested by an outer `Optional.some` case. ### Why does `Any??` crash Since outer case is `some`, it fails to force cast to `T?` (e.g. `String?`) due to type mismatch. ### How did we end up with `Any??` It's related to the signature of these 3 functions: - `func toList() -> [Any?]` - `func fromList(args: [Any])` - `func nilOrValue<T>(_ value: Any?) -> T?` Firstly `toList` returns `nil` (of type `Any?`) as the first element of array. Then the type gets coerced as an `Any` type in `fromList`. Then because `nilOrValue` takes `Any?`, this `nil` value gets wrapped by an `Optional.some`. Hence the nested optional. ## Workarounds ### Workaround 1: `as Any` This is the current code [in this PR](https://github.com/flutter/packages/pull/3545/files#r1155061282). When casting `Optional.some(nil) as Any`, it erases the outer Optional, so no problem casting to `T?`. ### Workaround 2: Handle with nested optional directly: ``` private func nilOrValue<T>(_ value: Any?) -> T? { if value is NSNull { return nil } // `if let` deals with "outer some" case and then erase the outer Optional if let val = value { // here returns "outer some + inner some" or "outer some + inner none" return val as! T? } // here returns "outer none" return nil } ``` A similar version of this was also [attempted in this PR](https://github.com/flutter/packages/pull/3545/files/241f0e31e32917f5501dab11f81ab0fbf064687f#diff-bfdb6a91beb03a906435e77e0168117f3f3977ee4d6f8bcaa1724156ae4dc27cR647-R650). It just that we did not know why that worked previously, and now we know! ### Workaround 3 Casting value to nested optional (`T??`), then stripe the outer optional ``` private func nilOrValue<T>(_ value: Any?) -> T? { if value is NSNull { return nil } return (value as! T??) ?? nil } ``` ## Solutions These above workarounds handle nested optionals. However, **a real solution should prevent nested optionals from happening in the first place**, since they are so tricky. ### Solution 1 (This PR) The nested optional happens when we do cast from `Any?` to `Any` and then wrapped into `Any?`. (Refer to "How did we end up with Any??" section). So the easiest way is just to use `func fromList(args: [Any?])` to match the types of `func toList` and `func nilOrValue`. ### Solution 2 Solution 2 is the opposite - avoid using `Any?` as much as possible. Drawbacks compare to Solution 1: a. When inter-op with ObjC, `nullable id` is exported as `Any?`. So we can't 100% prevent `Any?` usage. Though this can be addressed by immediately cast it to `Any`. b. Losing of semantic meaning of `Any?` that it <s>can</s> must be optional. The hidden/implicit optional **is** the culprit here in the first place. c. While this solution fixes the nested optional issue, it does not address all issues related to implicit optional. For example: https://github.com/flutter/packages/blob/c53db71f496b436e48629a8f3e4152c48e63cd66/packages/pigeon/platform_tests/test_plugin/ios/Classes/CoreTests.gen.swift#L563-L564 This is supposed to crash if `args[0]` is `nil`. However, the crash is silenced because `as! [Any]` will make `args[0]` an implicit optional! The correct codegen should instead be: ``` let args = message as! [Any?] let anObjectArg = args[0]! ``` ### Solution 3 Just remove `as Any` and update the test. The nested optional won't happen in production code, because ObjC `NSArray` contains `NSNull` rather than `nil` when exporting to Swift. We can simply fix [the tests](https://github.com/flutter/packages/blob/5662a7e5799c723f76e9589a75c9d9310e2ba8c1/packages/pigeon/platform_tests/test_plugin/example/ios/RunnerTests/RunnerTests.swift#L10-L29) by replacing `nil`s with `NSNull`s. However, if we were to re-write engine's codec to Swift, it's actually better practice to use `nil` and not `NSNull` in the array. ## Additional TODO We would've caught this earlier if this were an error rather than warning in our unit test.  *List which issues are fixed by this PR. You must list at least one issue.* flutter#3545 (comment) *If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].*
fixes flutter/flutter#123387 by forcing NSNull to nil also simplifies Int casting, since it was overly complex ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [relevant style guides] and ran the auto-formatter. (Unlike the flutter/flutter repo, the flutter/packages repo does use `dart format`.) - [x] I signed the [CLA]. - [x] The title of the PR starts with the name of the package surrounded by square brackets, e.g. `[shared_preferences]` - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated `pubspec.yaml` with an appropriate new version according to the [pub versioning philosophy], or this PR is [exempt from version changes]. - [x] I updated `CHANGELOG.md` to add a description of the change, [following repository CHANGELOG style]. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/packages/blob/main/CONTRIBUTING.md [Tree Hygiene]: https://github.com/flutter/flutter/wiki/Tree-hygiene [relevant style guides]: https://github.com/flutter/packages/blob/main/CONTRIBUTING.md#style [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/wiki/Tree-hygiene#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/wiki/Chat [pub versioning philosophy]: https://dart.dev/tools/pub/versioning [exempt from version changes]: https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages#version-and-changelog-updates [following repository CHANGELOG style]: https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages#changelog-style [test-exempt]: https://github.com/flutter/flutter/wiki/Tree-hygiene#tests
## The problem This PR fixes a weird casting behavior discussed [here](flutter#3545 (comment)): ``` private func nilOrValue<T>(_ value: Any?) -> T? { if value is NSNull { return nil } return (value as Any) as! T? // <- HERE } ``` Without this intermediate `as Any` cast, [these 3 tests](https://github.com/flutter/packages/blob/5662a7e5799c723f76e9589a75c9d9310e2ba8c1/packages/pigeon/platform_tests/test_plugin/example/ios/RunnerTests/RunnerTests.swift#L10-L29) would crash with `SIGABRT: Could not cast value of type 'Swift.Optional<Any>' (0x7ff865e84b38) to 'Swift.String' (0x7ff865e7de08).` ## Investigation The crash happens because `value` here is actually of type `Any??` (nested optional!). When it crashes, the debugger simply shows `value` is `nil`. But if we print in `lldb`, the `value` here is actually an inner `Optional.none` case nested by an outer `Optional.some` case. ### Why does `Any??` crash Since outer case is `some`, it fails to force cast to `T?` (e.g. `String?`) due to type mismatch. ### How did we end up with `Any??` It's related to the signature of these 3 functions: - `func toList() -> [Any?]` - `func fromList(args: [Any])` - `func nilOrValue<T>(_ value: Any?) -> T?` Firstly `toList` returns `nil` (of type `Any?`) as the first element of array. Then the type gets coerced as an `Any` type in `fromList`. Then because `nilOrValue` takes `Any?`, this `nil` value gets wrapped by an `Optional.some`. Hence the nested optional. ## Workarounds ### Workaround 1: `as Any` This is the current code [in this PR](https://github.com/flutter/packages/pull/3545/files#r1155061282). When casting `Optional.some(nil) as Any`, it erases the outer Optional, so no problem casting to `T?`. ### Workaround 2: Handle with nested optional directly: ``` private func nilOrValue<T>(_ value: Any?) -> T? { if value is NSNull { return nil } // `if let` deals with "outer some" case and then erase the outer Optional if let val = value { // here returns "outer some + inner some" or "outer some + inner none" return val as! T? } // here returns "outer none" return nil } ``` A similar version of this was also [attempted in this PR](https://github.com/flutter/packages/pull/3545/files/241f0e31e32917f5501dab11f81ab0fbf064687f#diff-bfdb6a91beb03a906435e77e0168117f3f3977ee4d6f8bcaa1724156ae4dc27cR647-R650). It just that we did not know why that worked previously, and now we know! ### Workaround 3 Casting value to nested optional (`T??`), then stripe the outer optional ``` private func nilOrValue<T>(_ value: Any?) -> T? { if value is NSNull { return nil } return (value as! T??) ?? nil } ``` ## Solutions These above workarounds handle nested optionals. However, **a real solution should prevent nested optionals from happening in the first place**, since they are so tricky. ### Solution 1 (This PR) The nested optional happens when we do cast from `Any?` to `Any` and then wrapped into `Any?`. (Refer to "How did we end up with Any??" section). So the easiest way is just to use `func fromList(args: [Any?])` to match the types of `func toList` and `func nilOrValue`. ### Solution 2 Solution 2 is the opposite - avoid using `Any?` as much as possible. Drawbacks compare to Solution 1: a. When inter-op with ObjC, `nullable id` is exported as `Any?`. So we can't 100% prevent `Any?` usage. Though this can be addressed by immediately cast it to `Any`. b. Losing of semantic meaning of `Any?` that it <s>can</s> must be optional. The hidden/implicit optional **is** the culprit here in the first place. c. While this solution fixes the nested optional issue, it does not address all issues related to implicit optional. For example: https://github.com/flutter/packages/blob/c53db71f496b436e48629a8f3e4152c48e63cd66/packages/pigeon/platform_tests/test_plugin/ios/Classes/CoreTests.gen.swift#L563-L564 This is supposed to crash if `args[0]` is `nil`. However, the crash is silenced because `as! [Any]` will make `args[0]` an implicit optional! The correct codegen should instead be: ``` let args = message as! [Any?] let anObjectArg = args[0]! ``` ### Solution 3 Just remove `as Any` and update the test. The nested optional won't happen in production code, because ObjC `NSArray` contains `NSNull` rather than `nil` when exporting to Swift. We can simply fix [the tests](https://github.com/flutter/packages/blob/5662a7e5799c723f76e9589a75c9d9310e2ba8c1/packages/pigeon/platform_tests/test_plugin/example/ios/RunnerTests/RunnerTests.swift#L10-L29) by replacing `nil`s with `NSNull`s. However, if we were to re-write engine's codec to Swift, it's actually better practice to use `nil` and not `NSNull` in the array. ## Additional TODO We would've caught this earlier if this were an error rather than warning in our unit test.  *List which issues are fixed by this PR. You must list at least one issue.* flutter#3545 (comment) *If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].*
fixes flutter/flutter#123387 by forcing NSNull to nil
also simplifies Int casting, since it was overly complex
Pre-launch Checklist
dart format
.)[shared_preferences]
pubspec.yaml
with an appropriate new version according to the pub versioning philosophy, or this PR is exempt from version changes.CHANGELOG.md
to add a description of the change, following repository CHANGELOG style.///
).If you need help, consider asking for advice on the #hackers-new channel on Discord.