Version Used:
Branch master (14 Oct 2020)
Latest commit 3fca2e8 by msftbot[bot]:
Merge pull request #48555 from CyrusNajmabadi/splitFollowingComment
Do not start a new comment when hitting enter before an existing comment
Steps to Reproduce:
Compile and run the following code:
#nullable enable
using System.Diagnostics.CodeAnalysis;
string? s = null;
AcceptNullable(s);
s.ToString();
void AcceptNullable([AllowNull] string s) { }
Expected Behavior:
Warning for possible null dereference in s.ToString();
Actual Behavior:
No warnings. The program crashes at runtime with a NullReferenceException
This is a scenario we discussed at some length internally a few months ago. At the time we concluded that the flow state of the argument for such a parameter should be updated to not-null after the call, as part of our change to reduce redundant warnings for maybe-null arguments to not-null parameters (#43383).
(I feel the need to note that I didn't agree with this decision, but that my point of view did not win the day 馃槈)
Tagging @333fred.
At the time we concluded that the flow state of the argument for such a parameter should be updated to not-null after the call,
TBH I fail to see why this would be considered logical behavior. I wonder if you remember any arguments for it
(I feel the need to note that I didn't agree with this decision, but that my point of view did not win the day 馃槈)
鉂わ笍
The arguments for this are that we made a change to update the flow state of a parameter to the declared state of a parameter after such a method is called. Generally speaking, we didn't consider this case to be realistic code, and weren't too concerned with the simpler rule (always update the state) being implemented. Can you share a real scenario that is actually hurt by this?
While I can't come up with a real scenario here, I agree with @TessenR and @RikkiGibson that this should produce a warning.
For a real scenario, imagine that I called WriteJson from Newtonsoft.Json library:
The parameter value is marked with [AllowNull]. After the call, if someone attempts to dereference the passed value, they will get no warnings.
@333fred
weren't too concerned with the simpler rule (always update the state)
I'm not sure why "update the state to not nullable if parameter only allows not nullable values" isn't simple enough.
Can you share a real scenario that is actually hurt by this?
The original design of nullable reference types all but ensured that there's a ton of API out there using these attributes. One of the examples is linked above. The reasons for this are:
[AllowNull] to annotate generics[AllowNull] T e.g. in IEqualityComparer.Equals with [AllowNull] NotNullable parameter, so it feels like a natural way of implementing such an interfaceThis leads to a situation where e.g. there's an implementation of the following interface from netcore3.1:
public interface IEqualityComparer<in T>
{
bool Equals([AllowNull] T x, [AllowNull] T y);
int GetHashCode([DisallowNull] T obj);
}
With the following type:
class StringPalindromeComparer : IEqualityComparer<string>
{
public bool Equals([AllowNull] string x, [AllowNull] string y) => StringUtils.IsPalindrome(x, y);
public int GetHashCode([DisallowNull] string obj) => obj.Length;
}
And I'll miss warnings in the following scenario which will crash with an NRE
var comparer = new StringPalindromeComparer();
var s1 = GetMeAString(); // can it return null? well I hope the compiler will warn me if it is
var s2 = GetMeAString();
if (comparer.Equals(s1, s2))
{
Console.WriteLine("Palindrome length: " + s1.Length); // no warnings, oops, runtime crash with a 'NullReferenceException'
Console.WriteLine("Palindrome strings: {0}, {1}", s1, s2);
}
string? GetMeAString() => null;
Note that
StringPalindromeComparer and it has no warnings regardless of the compiler's versionSo I'd consider this scenario a very real-world one.
While this all can be solved by tremendous effort of software engineers across the world e.g.
[AllowNull] attiributes in your code and all used third-party librariesetc
I think the better solution would be to just support this attribute in the compiler especially given that this looks like the most obvious thing ever - the parameter allows null values, why would the compiler think it guarantees the argument won't be null after the call?
flow state of a parameter to the declared state of a parameter after such a method is called
I think I've got a bit lost in the terminology here. What exactly do you mean by the declared state of a parameter?
Because it can't be neither parameter type nullability as the compiler does update the argument's state to not nullable upon passing it to [DisallowNull] string? s parameter. Nor the initial state of the parameter in the method since using [AllowNull] string s will warn on using it within a method so its state is nullable.
What I find especially confusing is that the compiler already understands [DisallowNull] and updates the argument' state in the following code:
#nullable enable
using System.Diagnostics.CodeAnalysis;
string? s = null;
L(s);
L(s); // no warnings
void L([DisallowNull] string? s) => s.ToString();
But for some reason just ignores its [AllowNull] counterpart. This seems completely illogical
Not yet certain how we are resolving this, but I am self-assigning in order to keep it on my radar.
Trying to get this on the LDM agenda for this / next week so we can get a resolution.
It looks like we will probably change analysis according to https://github.com/dotnet/csharplang/discussions/4102#discussioncomment-128346