-
Notifications
You must be signed in to change notification settings - Fork 4.1k
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
Only enforce Member/NotNullWhen on constants #48425
Conversation
It feels like
It feels like if we don't maintain the split state for constant-valued bool expressions in general, there will be other bugs (granted, probably related to uninteresting scenarios such as Regardless of the above comments, I agree with the strategy to check for a constant-valued bool expression when deciding whether to give the warning. I do wonder if there is additional tuning that should be done in order to check methods which return in a conditional state: SharpLab using System;
using System.Diagnostics.CodeAnalysis;
#nullable enable
public class C {
public string? s;
[MemberNotNullWhen(true, "s")]
public bool M1() {
s = "a";
return true;
}
[MemberNotNullWhen(false, "s")]
public bool M2() {
s = "a";
return false;
}
[MemberNotNullWhen(true, "s")]
public bool M3() {
return M1(); // ok
}
[MemberNotNullWhen(true, "s")]
public bool M4() {
return M2(); // bad
}
[MemberNotNullWhen(true, "s")]
public bool M5() {
return !M2(); // ok
}
[MemberNotNullWhen(true, "s")]
public bool M6() {
// if we only warn on constant bools, then this case will warn while `M4` does not.
if (M2())
return true;
else
return false;
}
[MemberNotNullWhen(true, "s")]
public bool M7() {
return s == null; // bad -- it would be unfortunate to lose this warning IMO.
}
[MemberNotNullWhen(true, "s")]
public bool M8() {
return s != null; // ok
}
} #Resolved |
I toyed with that both ideas, but ended pulling them out of this change since not necessary to fix and not observable. I'm happy to add back and we can discuss.
Yes, we're losing some useful warnings for sure. I tried splitting the problem different ways to do something better, but I don't have a solution... #Resolved |
It feels like in order to catch I'm slightly sad to see some of the tests where we dropped warnings here, but I defer to your judgment on whether this is the best trade-off (considering that present unexpected warnings are perhaps more annoying than missing expected warnings here?) #Resolved |
It feels like some of the scenarios where we warn or not can feel a little arbitrary in practice, such as in |
I like that. Let me take a stab. #Resolved |
foreach (var parameter in parameters) | ||
{ | ||
// For non-constant values, only complain if we were able to analyze a difference for this parameter between two branches | ||
if (GetOrCreateSlot(parameter) is > 0 and var slot && pendingReturn.StateWhenTrue[slot] != pendingReturn.StateWhenFalse[slot]) |
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.
(doesn't need to be addressed in this PR) I have been thinking that it doesn't make sense for a parameter to ever not have a slot--it will never be constant, for example. Perhaps we should use a helper for getting parameter slots that throws if we fail to get a slot for it. #WontFix
src/Compilers/CSharp/Test/Semantic/Semantics/NullableReferenceTypesTests.cs
Show resolved
Hide resolved
src/Compilers/CSharp/Test/Semantic/Semantics/NullableReferenceTypesTests.cs
Show resolved
Hide resolved
src/Compilers/CSharp/Test/Semantic/Semantics/NullableReferenceTypesTests.cs
Show resolved
Hide resolved
src/Compilers/CSharp/Test/Semantic/Semantics/NullableReferenceTypesTests.cs
Show resolved
Hide resolved
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!
return; | ||
} | ||
|
||
if (!pendingReturn.StateWhenTrue.Reachable || !pendingReturn.StateWhenFalse.Reachable) |
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.
Shouldn't we only suppress when both states are unreachable? We could have just done a test and thrown in the other branch, but accidentally messed up the order of the branches and returned incorrectly. #ByDesign
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.
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.
The only way to have one state being unreachable is using a constant condition (which is case above).
In reply to: 503602558 [](ancestors = 503602558,503602424)
if (node.ConstantValue is { IsBoolean: true }) | ||
{ | ||
Split(); | ||
if (node.ConstantValue.BooleanValue) |
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.
node.ConstantValue.BooleanValue [](start = 20, length = 31)
Consider extracting this in the pattern like the other additions in this file. #Closed
return !Init(); // 2, 3 | ||
} | ||
|
||
bool M([MaybeNullWhen(true)]out string s) => throw null!; |
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.
Unused? #Resolved
@@ -37146,7 +37295,7 @@ static bool TryGetValue<T>([AllowNull]T x, [MaybeNullWhen(true)]out T y, [MaybeN | |||
{ | |||
y = x; | |||
z = x; | |||
return y != null || z != null; // 1, 2 | |||
return y != null || z != null; |
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.
Why would this not have warnings? There should be different state here between the branches? Or is it because it's nested inside a ||
? #ByDesign
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.
The when-true and when-false states for the return
are identical (every variable may be null), so the expression as a whole is not considered to inform the nullability of y
or z
.
In reply to: 503605598 [](ancestors = 503605598)
static bool TryGetValue2B([NotNullWhen(true)] out TYPE y) | ||
{ | ||
y = null; | ||
return y == null; // 2 |
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.
The fact that these work but putting them inside a ||
does not is really confusing for me. I don't think this will be obvious at all for users. #ByDesign
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.
Agreed. Still, in terms of maximizing useful warnings and minimizing bad ones, this design seems the best option so far...
In reply to: 503605817 [](ancestors = 503605817)
Please add a test for something like this scenario: using System.Diagnostics.CodeAnalysis;
static bool M([NotNullWhen(true)] object? o)
{
return o == null ? true : throw null!;
} #Resolved |
Done review pass (commit 6) |
} | ||
"; | ||
var comp = CreateNullableCompilation(new[] { source, NotNullWhenAttributeDefinition }); | ||
comp.VerifyDiagnostics(); |
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 super don't like this behavior. It's obviously incorrect code.
My previous PR (#46201) to relax enforcement of those conditional attribute on non-constants returns was incorrect. It relied on the state being conditional or not, but (1) we split the state for non-constant values (
!expr
and method invocations involving attributes), and (2) we don't split the state for constant values.This PR corrects that, by directly checking for the return having a constant value.Update: this PR now only enforced those warnings if the target of the attribute (ie. parameter or member) has a different state in the two branches of analysis of the return statement. This allows to warn on statements that we can analyze like
return !TryGetValue(..., out p);
orreturn p != null;
, while avoiding annoying warnings onreturn M();
orreturn TryGetValue(..., out _);
.Fixes #48126
Original issue: #44080