-
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
Added support for setting null-conditional access receiver state. #28722
Added support for setting null-conditional access receiver state. #28722
Conversation
7e63107
to
5b1d18a
Compare
} | ||
} | ||
|
||
ImmutableArray<int> getOperandSlots(BoundExpression operand) |
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.
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 |
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.
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 |
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.
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 |
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.
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); |
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.
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 |
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.
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 |
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.
Recevier [](start = 72, length = 8)
Typo. #Closed
if (!slots.IsEmpty) | ||
{ | ||
Split(); | ||
foreach (int slot in slots) |
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.
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) |
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.
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; |
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.
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
@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); |
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.
Normalize
should be called after getOperandSlots
. #Resolved
{ | ||
this.StateWhenTrue[slot] = true; | ||
} | ||
state[slot] = true; | ||
} | ||
} |
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.
slotBuilder.Free();
#Resolved
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
ImmutableArray<int> getOperandSlots(BoundExpression operand) | ||
void getOperandSlots(BoundExpression operand, ArrayBuilder<int> slotListBuilder) |
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.
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 |
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.
Should that be BoundConditionalReceiver
? #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.
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); |
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.
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
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.
@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; |
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.
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) | ||
); | ||
} |
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.
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); |
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.
Can we move the Normalize
call down into the conditional (only normalize if slots were returned/created)? #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.
I just cannot win: #28722 (comment) #Closed
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.
Yes, that's what I meant. #Closed
|
||
if (slot > 0) | ||
var slotBuilder = ArrayBuilder<int>.GetInstance(); | ||
getOperandSlots(operandComparedToNull, slotBuilder); |
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.
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 |
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.
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) | ||
); | ||
} |
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.
Could you add a test with a nested null test with an null-conditional operator?
if (a?.GetB(c?.d == null).e == null) ...
``` #Resolved
@jcouv addressed feedback. |
Split(); | ||
|
||
if (op == BinaryOperatorKind.Equal) | ||
ref LocalState state = ref (op == BinaryOperatorKind.Equal) ? ref this.StateWhenFalse : ref this.StateWhenTrue; |
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.
= ref [](start = 53, length = 5)
Random thought: this may be the first ref-conditional in our codebase.
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.
This one already existed: https://github.com/dotnet/roslyn/blob/master/src/Compilers/Core/Portable/Operations/ControlFlowGraphBuilder.cs#L2458 #Closed
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 Thanks
c2.Prop.ToString(); // warn 1 2 | ||
} | ||
} | ||
}", parseOptions: TestOptions.Regular8); |
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.
Is the following case interesting?
static void M(object? x, object? y)
{
if ((x = y)?.GetHashCode() != null)
x.ToString();
}
``` #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.
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)
…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) |
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.
Are we handling unconstrained type parameter types? #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.
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.
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()); |
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.
(object)operandType != null &&
de4c697
to
4ca10c2
Compare
I forgot to ask: could you update the speclet to reflect this new behavior? There is a section on "null tests". |
For something like
we will now track nullability of conditionally accessed expressions.
@dotnet/roslyn-compiler @jcouv @cston for review.