Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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

Draft
michaelnebel wants to merge 11 commits into github:main
base: main
Choose a base branch
Loading
from michaelnebel:csharp/cfgforaccess

Conversation

@michaelnebel
Copy link
Contributor

@michaelnebel michaelnebel commented Jan 13, 2026
edited
Loading

In this PR, we:

  • Add support for C# 14 null conditional assignments. While the extractor already supported this feature, updates to the C# control flow implementation were needed.
  • Simplify the control flow logic for qualified write access, which also helps with the addition of null conditional assignments.
  • Make a small improvement to the MaybeNullExpr class so that it now correctly handles null conditional (read) accesses.

Changes to existing control flow

Previously, the control flow for

ca.IntField = 42

looked like this:

ca -> 42 -> ca.IntField = 42

With this PR, we now include the field access after evaluating the right-hand side, so the control flow becomes:

ca -> 42 -> ca.IntField -> ca.IntField = 42

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

M(out ca.IntField)

looked like this:

ca -> call M

With this PR, we now include the field access after the call, so the control flow becomes:

ca -> call M -> ca.Field

New control flow.

The changes to the control flow graph implementation mean that for a statement like

ca?.Prop?.StringProp = "World";

the control flow is now built correctly to reflect the sequence of null conditional accesses.
image

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.

@michaelnebel michaelnebel force-pushed the csharp/cfgforaccess branch 3 times, most recently from 30e2a16 to 15848f3 Compare January 14, 2026 09:55
@michaelnebel michaelnebel changed the title (削除) C#: Add CFG support for null conditional assignments and include eg. ... (削除ここまで) (追記) C# 14: Null conditional assignments. (追記ここまで) Jan 14, 2026
Copy link
Contributor Author

@hvitved : Would it be possible to get some early feedback on the contents of the PR (in case we should design it differently)?

Copy link
Contributor

@hvitved hvitved left a 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.

Comment on lines 449 to 450
* A qualified write access. In a qualified write access, the access itself is
* not evaluated, only the qualifier and the indexer arguments (if any).
Copy link
Contributor

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

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`,
Copy link
Contributor

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

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

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

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

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
Loading

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
Loading

Copy link
Contributor Author

@michaelnebel michaelnebel Jan 16, 2026

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.

Copy link
Contributor Author

@michaelnebel michaelnebel Jan 16, 2026

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.

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

Reviewers

@hvitved hvitved hvitved left review comments

Copilot code review Copilot Awaiting requested review from Copilot Copilot will automatically review once the pull request is marked ready for review

At least 1 approving review is required to merge this pull request.

Assignees

No one assigned

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

AltStyle によって変換されたページ (->オリジナル) /