-
Notifications
You must be signed in to change notification settings - Fork 15.6k
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
.NET: Add nullable annotations to Protobuf library #9801
Conversation
Thanks for the PR. I'll try to review it this week, or at least make a start. We'll need to figure out what we do about the compatibility tests, which are failing on Kokoro. (I need to remind myself exactly what they're doing, for a start...) |
Thanks @jskeet . My take on the failures is that the nullable changes would require updating the compatibility csproj to use C# 8.0 at minimum. I hadn't realized nullable was not backward compatible to older C# versions. If true, the implication of my change is that it requires users to update their language settings as well, which at minimum would imply additional release note info, and likely only makes sense to do at a minor version number update. |
It's not compatible in terms of source code, but it is binary compatible. We won't actually be requiring users to change their language settings (and if we did I'd suggest we shouldn't do this at all)... but we may need to change the settings for the compatibility project file. Basically I don't expect any other users to be compiling Google.Protobuf themselves, so it's fine to need a change for those language settings. |
411b074
to
557d86a
Compare
9ecf0fd
to
8a75f14
Compare
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.
First chunk of review - I've reviewed as far as the end of the tests. (Somewhat skimming, admittedly - for the production code I'll be looking more closely.)
csharp/src/Google.Protobuf.Test/Collections/ProtobufEqualityComparersTest.cs
Show resolved
Hide resolved
@jskeet if you like I can break this PR up into a series of smaller ones with less cognitive overload. Probably starting with updating to C#10 and some new feature use, then follow with turning on nullable a few files at a time. |
@Erikma: Let me see if I can get to it next week. (It won't be on Monday as I'm at a conference.) It's on my list, just not high up on my list of priorities - that's what's taking time rather than the size of the PR. |
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.
Next set of comments. Apologies that this is taking a long time. I've now viewed everything under Collections and Compatibility.
6a2847e
to
10be4a4
Compare
Note this is for the Protobuf library itself, not related to generated code. Large but shallow changes: - Added Directory.Build.props with C# 10 targeted and nullable enabled across all projects. - Updated C# README to note already-existing dependency on VS2022 and .NET 6. - Added nullable annotations across all C# code, drawing down to zero warnings/issues. - Made use of a few newer C# features like pattern matching while fixing nullable issues. Wider use of other C# features in a few cases but mostly deferred to later PR(s). Validations: - Unit tests passing including null param exceptions. - Manually verified existing calls to null param check utility code (ProtoPreconditions.CheckNotNull*) line up with non-null annotations. - Dropped main assembly into a private codebase with several gRPC contracts, generated protobuf code, and nullable issues set to error. Verified nullable issues match expected results, i.e. cause no unexpected errors.
10be4a4
to
276b26e
Compare
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.
Next set of comments, and some thoughts about how we deal with this.
I want to reiterate that I'm immensely grateful to you for this effort, and that I do want to get it done. But I think that doing it correctly is going to require splitting it up into multiple PRs simply because my eyes are glazing over too often.
Do you think it's feasible to do this over the course of multiple PRs, even if there's a release during that time? I would hope that if we do this carefully, that won't actually cause a problem other than customers potentially seeing multiple "phases" of warnings. I can look into whether we can effectively make this all internal (i.e. not "advertise" the NRT annotations until we're ready) if that would be useful.
(I'd definitely suggest keeping this branch around as a sort of "reference point" so we can see what's left at any point.)
Do you have a sense of an ordering which would make sense to do this in, e.g. some "core" parts that everything else refers to, then "outer" parts?
Finally, I think there will be some places where we want to make a judgement call about whether introducing a null check would make sense. For example, currently we allow an Extension<TTarget, TValue>
to be created with a null codec, but that will always throw an exception if you then call CreateValue
on it. I think it's reasonable to consider that a bug, and tighten it up to validate the codec on construction. That may make several aspects easier.
public sealed class RepeatedExtension<TTarget, TValue> : Extension where TTarget : IExtendableMessage<TTarget> | ||
public sealed class RepeatedExtension<TTarget, TValue> : Extension | ||
where TTarget : IExtendableMessage<TTarget> | ||
where TValue : notnull |
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.
Same concerns as earlier about whether adding a new generic constraint could be a breaking change. I need to revisit that side of things in general.
@@ -78,7 +77,7 @@ public static class ExtensionSet | |||
{ | |||
return extensionValue.GetValue(); | |||
} | |||
else if (value.GetValue() is TValue underlyingValue) | |||
else if (value!.GetValue() is TValue underlyingValue) |
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.
If we know this is non-null due to TryGetValue returning true, we should add an appropriate attribute to that method (NotNullWhen I suspect) so that we don't require consumers (this and others) to use the ! operator or check.
If it's actually because this is a special case, that's a different matter.
Looking at the code, I think it would be appropriate to add the NotNullWhen annotation. What do you think?
{ | ||
if (value is RepeatedExtensionValue<TValue> extensionValue) | ||
{ | ||
return extensionValue.GetValue(); | ||
} | ||
else | ||
{ | ||
var valueType = value.GetType().GetTypeInfo(); | ||
var valueType = value!.GetType().GetTypeInfo(); |
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.
(Again, this wouldn't be necessary with an annotation.)
{ | ||
IExtensionValue value; | ||
IExtensionValue? value; |
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.
Do we actually need this to be nullable? We're assuming that it's non-null later on, so it would be good to lean on the compiler here.
@@ -77,7 +77,7 @@ public Extension(int fieldNumber, FieldCodec<TValue> codec) : base(fieldNumber) | |||
this.codec = codec; | |||
} | |||
|
|||
internal TValue DefaultValue => codec != null ? codec.DefaultValue : default(TValue); | |||
internal TValue? DefaultValue => codec != null ? codec.DefaultValue : default; |
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 not clear to me why the codec could be null, to be honest. We do use that a couple of times in tests, but I'm not sure whether it's actually valid. If we could tighten that up, we could potentially make this non-nullable, which would be good. (I haven't looked at FieldCodec yet.)
{ | ||
IExtensionValue value = extension.CreateValue(); | ||
IExtensionValue value = extension!.CreateValue(); |
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 we should be able to get rid of this one too.
} | ||
|
||
internal sealed class ExtensionValue<T> : IExtensionValue | ||
{ | ||
private T field; | ||
private FieldCodec<T> codec; | ||
private T? field; |
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 we should be able to make this non-nullable, if we can make codec.DefaultValue
non-nullable. It looks like we're relying on it not being null anyway (e.g. in GetHashCode).
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 then changes a bunch of things below as well.)
@@ -206,7 +205,7 @@ public static FieldCodec<double> ForDouble(uint tag) | |||
/// <param name="toInt32">A conversion function from <see cref="Int32"/> to the enum type.</param> | |||
/// <param name="fromInt32">A conversion function from the enum type to <see cref="Int32"/>.</param> | |||
/// <returns>A codec for the given tag.</returns> | |||
public static FieldCodec<T> ForEnum<T>(uint tag, Func<T, int> toInt32, Func<int, T> fromInt32) | |||
public static FieldCodec<T?> ForEnum<T>(uint tag, Func<T?, int> toInt32, Func<int, T> fromInt32) |
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.
Does this need to be nullable? It's only used for enums, isn't it? (It's unfortunate that we can't add constraints for that now in terms of compatibility.)
@jskeet no worries on redoing incrementally. Give me a couple of days to restart, I'll link each sub-PR into this one for tracking. Initial guess at ordering:
|
Thanks @Erikma - and I can faithfully promise that with smaller changes, I'll get round to them quite quickly. I'm terrible when it comes to putting off large reviews, I'm afraid - sorry you've caught that! |
Closing this PR in favor of completed, open, and future PRs as slices of this one. |
Note this is for the Protobuf library itself, not related to generated code. Large but shallow changes:
Validations: