-
Notifications
You must be signed in to change notification settings - Fork 1.9k
C# 14: Null conditional assignments. #21158
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
base: main
Are you sure you want to change the base?
Conversation
30e2a16 to
15848f3
Compare
438153d to
15e9685
Compare
8877b1e to
3c42988
Compare
|
@hvitved : Would it be possible to get some early feedback on the contents of the PR (in case we should design it differently)? |
…field access in the CFG.
3c42988 to
86198e3
Compare
hvitved
left a comment
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.
Overall LGTM, I mostly have a concern about CFGs for out arguments.
| * A qualified write access. In a qualified write access, the access itself is | ||
| * not evaluated, only the qualifier and the indexer arguments (if any). |
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 QL doc is now incorrect. Perhaps just remove the entire In a qualified write access, the access itself is not evaluated, only the qualifier and the indexer arguments (if any).
| exists(int i | | ||
| last(getExprChild(this, i), pred, c) and | ||
| c instanceof NormalCompletion and | ||
| (i != 0 or not c.(NullnessCompletion).isNull()) and |
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.
if i = 0 then not c.(NullnessCompletion).isNull() else any() is slightly better as it avoids tuple duplication during evaluation (the case where i != 0 and c is not (NullnessCompletion).isNull() is currently valid for both disjuncts.
|
|
||
| /** | ||
| * An expression that writes via an accessor call, for example `x.Prop = 0`, | ||
| * An expression that writes via an accessor, for example `x.Prop = 0`, |
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.
Accessor means stuff like get and set, so that is no longer the right word to use as it also includes fields.
| exists(int i | | ||
| last(getExprChild(this, i), pred, c) and | ||
| c instanceof NormalCompletion and | ||
| (i != 0 or not c.(NullnessCompletion).isNull()) and |
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.
Same comment.
| result = | ||
| any(QualifiableExpr qe | | ||
| qe.isConditional() and | ||
| qe.getQualifier() = maybeNullExpr(reason) |
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 actually needed? I would think that an expression like x?.M() can always potentially be null, regardless of what we know about x.
| * In the example above, this means we want a CFG that looks like | ||
| * | ||
| * ```csharp | ||
| * x -> call M -> x.Field -> s = M(out x.Field) |
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.
call M -> M(out x.Field)
| * In the example above, this means we want a CFG that looks like | ||
| * | ||
| * ```csharp | ||
| * x -> call M -> x.Field -> s = M(out x.Field) |
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.
Actually, I'm not convinced that this is the CFG we want in this case. Consider this example:
void MyTest()
{
if (Set(out this.IntField))
return;
Guarded();
}It gets the following CFG
flowchart TD
1["enter MyTest"]
10["return ...;"]
11["call to method Guarded"]
12["this access"]
13["...;"]
2["exit MyTest"]
3["exit MyTest (normal)"]
4["{...}"]
5["if (...) ..."]
6["call to method Set"]
7["this access"]
8["this access"]
9["access to field IntField"]
1 --> 4
3 --> 2
4 --> 5
5 --> 7
6 -- false, true --> 9
7 --> 8
8 --> 6
9 -- false --> 13
9 -- true --> 10
10 -- return --> 3
11 --> 3
12 --> 11
13 --> 12
Note how get the join at access to field IntField, which means that we can no longer tell that Guarded is guarded by Set(out this.IntField) evaluating to true. I think what we want is simply the same as if there was no out, i.e.
flowchart TD
1["enter MyTest"]
10["return ...;"]
11["call to method Guarded"]
12["this access"]
13["...;"]
2["exit MyTest"]
3["exit MyTest (normal)"]
4["{...}"]
5["if (...) ..."]
6["call to method Set"]
7["this access"]
8["this access"]
9["access to field IntField"]
1 --> 4
3 --> 2
4 --> 5
5 --> 7
9 --> 6
7 --> 8
8 --> 9
6 -- false --> 13
6 -- true --> 10
10 -- return --> 3
11 --> 3
12 --> 11
13 --> 12
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, we can do it like that. However, then it looks like a field read instead.
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 will also significantly simplify the implementation.
In this PR, we:
MaybeNullExprclass so that it now correctly handles null conditional (read) accesses.Changes to existing control flow
Previously, the control flow for
looked like this:
With this PR, we now include the field access after evaluating the right-hand side, so the control flow becomes:
This matches how property assignments are handled, where it’s important for the property access to be included in the control flow after evaluating the right-hand side, since accessing the property corresponds to invoking the property setter.
Previously, the control flow for
looked like this:
With this PR, we now include the field access after the call, so the control flow becomes:
New control flow.
The changes to the control flow graph implementation mean that for a statement like
the control flow is now built correctly to reflect the sequence of null conditional accesses.

That is, if the qualifier uses a null conditional, we add a null edge from the qualifier to the point after the assignment. This reflects the fact that if the qualifier is null, the right-hand side of the assignment isn’t evaluated.