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

Added support for setting null-conditional access receiver state. #28722

Conversation

333fred
Copy link
Member

@333fred 333fred commented Jul 20, 2018

For something like

if (c?.d != null)
{
    c.d.ToString();
}

we will now track nullability of conditionally accessed expressions.

@dotnet/roslyn-compiler @jcouv @cston for review.

@333fred 333fred requested review from cston, jcouv and a team July 20, 2018 01:07
@333fred 333fred force-pushed the null-conditional-operators branch from 7e63107 to 5b1d18a Compare July 20, 2018 01:11
}
}

ImmutableArray<int> getOperandSlots(BoundExpression operand)
Copy link
Member

@cston cston Jul 20, 2018

Choose a reason for hiding this comment

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

Consider having the caller pass an ArrayBuilder argument rather than creating an ImmutableArray result. #Resolved

{
// Due to the nature of binding, if there are conditional access they will be at the top of the bound tree,
// potentially with a conversion on top of it. We go through any conditional accesses, adding slots for the
// condtional receivers if they have them. If we ever get to a receiver that MakeSlot doesn't return a slot
Copy link
Member

@cston cston Jul 20, 2018

Choose a reason for hiding this comment

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

condtional [](start = 23, length = 10)

Typo. #Resolved

// a?.GetB()?.C // a is a field, GetB is a method, and C is a property
//
// The top of the tree is the a?.GetB() conditional call. We'll ask for a slot for a, and we'll get one because
// locals have slots. The AccessExpression of the BoundCondtionalAccess is another BoundCondtionalAccess, this time
Copy link
Member

@cston cston Jul 20, 2018

Choose a reason for hiding this comment

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

Condtional [](start = 75, length = 10)

Typo, here and later in the line. #Resolved

//
// The top of the tree is the a?.GetB() conditional call. We'll ask for a slot for a, and we'll get one because
// locals have slots. The AccessExpression of the BoundCondtionalAccess is another BoundCondtionalAccess, this time
// with a reciever of the GetB() BoundCall. Attempting to get a slot for this receiver will fail, and we'll
Copy link
Member

@cston cston Jul 20, 2018

Choose a reason for hiding this comment

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

reciever [](start = 30, length = 8)

Typo. #Resolved

// If we got a slot we must have processed the previous conditional receiver.
Debug.Assert(_lastConditionalAccessSlot == -1);

if (slot >= this.State.Capacity) Normalize(ref this.State);
Copy link
Member

@cston cston Jul 20, 2018

Choose a reason for hiding this comment

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

Normalize(ref this.State); [](start = 65, length = 26)

Consider calling Normalize once, unconditionally, in the caller of getOperandSlots. #Closed

slotListBuilder.Add(slot);

// When MakeSlot is called on the nested AccessExpression, it will recurse through receivers
// until it gets to the BoundConditionalRecevier associated with this node. In our override
Copy link
Member

@cston cston Jul 20, 2018

Choose a reason for hiding this comment

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

In our override [](start = 108, length = 15)

This sentence stops short. #Resolved

slotListBuilder.Add(slot);

// When MakeSlot is called on the nested AccessExpression, it will recurse through receivers
// until it gets to the BoundConditionalRecevier associated with this node. In our override
Copy link
Member

@cston cston Jul 20, 2018

Choose a reason for hiding this comment

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

Recevier [](start = 72, length = 8)

Typo. #Closed

@jcouv jcouv added this to the 16.0 milestone Jul 20, 2018
if (!slots.IsEmpty)
{
Split();
foreach (int slot in slots)
Copy link
Member

@cston cston Jul 20, 2018

Choose a reason for hiding this comment

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

Perhaps:

ref LocalState state = ref (op == BinaryOperatorKind.Equal) ?
    ref this.StateWhenFalse :
    ref this.StateWhenTrue;
foreach (var slot in slots)
{
    state[slot] = true;
}
``` #Resolved


if (op == BinaryOperatorKind.Equal)
if (slot >= this.State.Capacity) Normalize(ref this.State);
if (slotListBuilder == null)
Copy link
Member

@cston cston Jul 20, 2018

Choose a reason for hiding this comment

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

Let's avoid creating the builder in several places. Consider creating upfront in the caller. #Resolved

{
// If we didn't get a slot, it's possible that the current _lastConditionalSlot was never processed,
// so we reset before leaving the function.
_lastConditionalAccessSlot = -1;
Copy link
Member

@cston cston Jul 20, 2018

Choose a reason for hiding this comment

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

Can we move this comment and assignment so it only appears once? Perhaps break; out of the switch statements when done, and have _lastConditionalAccessSlot = -1; return; after the switch. #Resolved

@333fred
Copy link
Member Author

333fred commented Jul 20, 2018

@cston addressed feedback.

@@ -1461,50 +1461,46 @@ protected override void AfterLeftChildHasBeenVisited(BoundBinaryOperator binary)

if (operandComparedToNull.Type?.IsReferenceType == true)
{
ImmutableArray<int> slots = getOperandSlots(operandComparedToNull);
if (!slots.IsEmpty)
Normalize(ref this.State);
Copy link
Member

@cston cston Jul 20, 2018

Choose a reason for hiding this comment

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

Normalize should be called after getOperandSlots. #Resolved

{
this.StateWhenTrue[slot] = true;
}
state[slot] = true;
}
}
Copy link
Member

@cston cston Jul 20, 2018

Choose a reason for hiding this comment

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

slotBuilder.Free(); #Resolved

}
}
}
}
}
}

ImmutableArray<int> getOperandSlots(BoundExpression operand)
void getOperandSlots(BoundExpression operand, ArrayBuilder<int> slotListBuilder)
Copy link
Member

@cston cston Jul 20, 2018

Choose a reason for hiding this comment

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

Minor point: perhaps rename to slotBuilder or simply builder. #Resolved


// When MakeSlot is called on the nested AccessExpression, it will recurse through receivers
// until it gets to the BoundConditionalReceiver associated with this node. In our override,
// we substitute this slot when we encounter a BoundConditionalAccessReceiver, and reset the
Copy link
Member

@cston cston Jul 20, 2018

Choose a reason for hiding this comment

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

Should that be BoundConditionalReceiver? #Resolved

Copy link
Member Author

Choose a reason for hiding this comment

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

I keep getting if confused with IConditionalAccessInstanceOperation.


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

if (slot > 0)
{
// If we got a slot we must have processed the previous conditional receiver.
Debug.Assert(_lastConditionalAccessSlot == -1);
Copy link
Member

@cston cston Jul 20, 2018

Choose a reason for hiding this comment

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

Is this default case handling BoundConditionalAccess / BoundConditionalReceiver or all other operands except those? If the latter, consider moving the Debug.Assert up before MakeSlot. #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 latter.


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

Copy link
Member Author

Choose a reason for hiding this comment

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

@cston after thinking though the state changes, I'm moving this back into the conditional. In the case of a?.ToString(), we won't have processed the BoundConditionalReceiver from the ToString BoundCall by this point. It is correct as originally written.


In reply to: 204188950 [](ancestors = 204188950,204187214)

{
Debug.Assert(operand != null);
Debug.Assert(_lastConditionalAccessSlot == -1);
int slot;
Copy link
Member

@cston cston Jul 20, 2018

Choose a reason for hiding this comment

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

Consider moving slot declaration closer to the use - either inside each case statement or at least inside the do loop. #Resolved

// c6._cField.GetC().ToString(); // warn
Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "c6._cField.GetC()").WithLocation(79, 13)
);
}
Copy link
Member

@cston cston Jul 21, 2018

Choose a reason for hiding this comment

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

Please add a test for unconstrained T.

static void F<T>(T t) 
{
    if (t?.ToString() != null)
        t.ToString();
}
``` #Resolved

if (slot > 0)
var slotBuilder = ArrayBuilder<int>.GetInstance();
getOperandSlots(operandComparedToNull, slotBuilder);
Normalize(ref this.State);
Copy link
Member

@jcouv jcouv Jul 21, 2018

Choose a reason for hiding this comment

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

Can we move the Normalize call down into the conditional (only normalize if slots were returned/created)? #Resolved

Copy link
Member Author

@333fred 333fred Jul 21, 2018

Choose a reason for hiding this comment

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

I just cannot win: #28722 (comment) #Closed

Copy link
Member

Choose a reason for hiding this comment

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

I think @jcouv is just suggesting moving Normalize down two lines, into the if. Seems reasonable.


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

Copy link
Member

@jcouv jcouv Jul 22, 2018

Choose a reason for hiding this comment

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

Yes, that's what I meant. #Closed


if (slot > 0)
var slotBuilder = ArrayBuilder<int>.GetInstance();
getOperandSlots(operandComparedToNull, slotBuilder);
Copy link
Member

@jcouv jcouv Jul 21, 2018

Choose a reason for hiding this comment

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

May be good to add a comment like // ex: in 'a?.b?.c' we'll update the states for 'a', 'b' and 'c'. #Closed

else if (c3?._cField != null)
{
c3._cField.ToString();
c3._cField._cField.ToString(); // warn
Copy link
Member

@jcouv jcouv Jul 21, 2018

Choose a reason for hiding this comment

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

Nit: to maximize readability, I suggest using unique labels // warn 1, // warn 2 and 3, etc so it's really easy to match the diagnostics to the code. #Resolved

// c1.ToString(); // warn
Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "c1").WithLocation(13, 13)
);
}
Copy link
Member

@jcouv jcouv Jul 21, 2018

Choose a reason for hiding this comment

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

Could you add a test with a nested null test with an null-conditional operator?

if (a?.GetB(c?.d == null).e == null) ...
``` #Resolved

@333fred
Copy link
Member Author

333fred commented Jul 23, 2018

@jcouv addressed feedback.

Split();

if (op == BinaryOperatorKind.Equal)
ref LocalState state = ref (op == BinaryOperatorKind.Equal) ? ref this.StateWhenFalse : ref this.StateWhenTrue;
Copy link
Member

Choose a reason for hiding this comment

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

= ref [](start = 53, length = 5)

Random thought: this may be the first ref-conditional in our codebase.

Copy link
Member Author

@333fred 333fred Jul 23, 2018

Choose a reason for hiding this comment

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

Copy link
Member

@jcouv jcouv left a comment

Choose a reason for hiding this comment

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

LGTM Thanks

c2.Prop.ToString(); // warn 1 2
}
}
}", parseOptions: TestOptions.Regular8);
Copy link
Member

@cston cston Jul 23, 2018

Choose a reason for hiding this comment

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

Is the following case interesting?

static void M(object? x, object? y)
{
    if ((x = y)?.GetHashCode() != null)
        x.ToString();
}
``` #Resolved

Copy link
Member Author

Choose a reason for hiding this comment

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

It is, but because GetHashCode() returns a nullable int, and not a reference type. Should be a simple fix.


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

333fred added 2 commits July 23, 2018 14:35
…operators

* features/NullableReferenceTypes:
  `is` operator informs nullability analysis (dotnet#28686)
  Make module-level NonNullTypes explicit in tests (dotnet#28769)
  Lazily evaluate NonNullTypes (dotnet#28736)
  Calculate TypeSymbolWithAnnotations.IsNullable lazily (dotnet#28687)
slotBuilder.Add(slot);
// We need to continue the walk regardless of whether the receiver is a value
// type, but we only want to update the slots of reference types
if (conditional.Receiver.Type?.IsReferenceType == true)
Copy link
Member

@cston cston Jul 23, 2018

Choose a reason for hiding this comment

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

Are we handling unconstrained type parameter types? #Resolved

Copy link
Member Author

Choose a reason for hiding this comment

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

I'll verify this when I address #28792, as I'm not changing our existing behavior much here.


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

Copy link
Member Author

Choose a reason for hiding this comment

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

Turns out I need to address this now, as it's breaking a test.


In reply to: 204573537 [](ancestors = 204573537,204568700)


bool shouldUpdateType(TypeSymbol operandType) =>
operandType != null &&
(operandType.IsReferenceType == true || operandType.IsUnconstrainedTypeParameter());
Copy link
Member

Choose a reason for hiding this comment

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

(object)operandType != null &&

@333fred 333fred force-pushed the null-conditional-operators branch from de4c697 to 4ca10c2 Compare July 24, 2018 00:03
@333fred 333fred merged commit a818e66 into dotnet:features/NullableReferenceTypes Jul 24, 2018
@333fred 333fred deleted the null-conditional-operators branch July 24, 2018 17:48
@jcouv
Copy link
Member

jcouv commented Jul 25, 2018

I forgot to ask: could you update the speclet to reflect this new behavior? There is a section on "null tests".

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.

3 participants