Skip to content
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

Merged
merged 7 commits into from
Oct 22, 2020
Merged

Conversation

jcouv
Copy link
Member

@jcouv jcouv commented Oct 8, 2020

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); or return p != null;, while avoiding annoying warnings on return M(); or return TryGetValue(..., out _);.

Fixes #48126
Original issue: #44080

@jcouv jcouv self-assigned this Oct 8, 2020
@jcouv jcouv marked this pull request as ready for review October 8, 2020 17:33
@jcouv jcouv requested a review from a team as a code owner October 8, 2020 17:33
@RikkiGibson RikkiGibson self-assigned this Oct 8, 2020
@RikkiGibson
Copy link
Contributor

RikkiGibson commented Oct 8, 2020

we split the state for non-constant values (!expr and method invocations involving attributes)

It feels like !expr should only have a split state if we entered a split state after visiting expr.

we don't split the state for constant values

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 !true or true || false).

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

@jcouv
Copy link
Member Author

jcouv commented Oct 8, 2020

It feels like !expr should only have a split state if we entered a split state after visiting expr.
It feels like if we don't maintain the split state for constant-valued bool expressions in general, there will be other bugs.

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.

I do wonder if there is additional tuning that should be done in order to check methods which return in a conditional state

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

@jcouv jcouv requested a review from RikkiGibson October 8, 2020 23:57
@RikkiGibson
Copy link
Contributor

RikkiGibson commented Oct 9, 2020

It feels like in order to catch return s == null; or return !TryInitializeThing();, before giving a diagnostic you'd have to check that you're in a split state and that the state for the referenced output (e.g. out parameter, member) is actually different between the StateWhenFalse and StateWhenTrue (as a proxy to answer the question: was the return expr performing a test or conditional assignment to this variable?). I think we would have to reason very carefully through some scenarios to decide if we're happy with all the consequences of something like that.

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

@RikkiGibson
Copy link
Contributor

RikkiGibson commented Oct 9, 2020

It feels like some of the scenarios where we warn or not can feel a little arbitrary in practice, such as in M4 vs M6 in the above example. People may end up filing bugs for it. But I suppose that is the nature of doing a "best-effort"/pragmatic analysis. #Resolved

@jcouv
Copy link
Member Author

jcouv commented Oct 9, 2020

was the return expr performing a test or conditional assignment to this variable?

I like that. Let me take a stab. #Resolved

@jcouv jcouv requested a review from RikkiGibson October 9, 2020 22:56
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])
Copy link
Contributor

@RikkiGibson RikkiGibson Oct 9, 2020

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

Copy link
Contributor

@RikkiGibson RikkiGibson left a 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)
Copy link
Member

@333fred 333fred Oct 13, 2020

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

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same for the other tests of this condition.


In reply to: 503602424 [](ancestors = 503602424)

Copy link
Member Author

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)
Copy link
Member

@333fred 333fred Oct 13, 2020

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!;
Copy link
Member

@333fred 333fred Oct 13, 2020

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;
Copy link
Member

@333fred 333fred Oct 13, 2020

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

Copy link
Member Author

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
Copy link
Member

@333fred 333fred Oct 13, 2020

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

Copy link
Member Author

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)

@333fred
Copy link
Member

333fred commented Oct 13, 2020

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

@333fred
Copy link
Member

333fred commented Oct 13, 2020

Done review pass (commit 6)

@jcouv jcouv requested a review from 333fred October 16, 2020 22:51
}
";
var comp = CreateNullableCompilation(new[] { source, NotNullWhenAttributeDefinition });
comp.VerifyDiagnostics();
Copy link
Member

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Missing nullability warnings for [MemberNotNullWhen] parameters
4 participants