Skip to content

Conversation

@michaelnebel
Copy link
Contributor

@michaelnebel michaelnebel commented Jan 13, 2026

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 -> M(out ca.IntField)

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

ca -> ca.IntField -> M(out ca.IntField)

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.

@github-actions github-actions bot added the C# label Jan 13, 2026
@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
@michaelnebel
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)?

@michaelnebel michaelnebel requested a review from hvitved January 16, 2026 10:26
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.

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, if x is variable (or other stuff that is not special cased in the MaybeNullExpr class), but it appears we need to propgate the maybe null information for casts and conditionals (stuff that is explicitly handled in the MaybeNullExpr class itself). That is, to properly handle (x as C).GetInt().
An example of an extra finding: https://github.com/dotnet/roslyn/blob/6afbfb45ccc9691167206bf29482a99b1d6d469c/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenTupleTest.cs#L24398

Copy link
Contributor

Choose a reason for hiding this comment

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

What I meant was why not

result = any(QualifiableExpr qe | qe.isConditional() and reason = qe.getQualifier())

That should give us strictly more results, and I would assume that any x?.M() expression can be potentially null because it is conditionally qualified.

Copy link
Contributor Author

@michaelnebel michaelnebel Jan 19, 2026

Choose a reason for hiding this comment

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

That is somewhat less conservative. It means that stuff like

void M1(object o)
{
    var t = o?.GetType();
    Console.WriteLine(t.FullName);
}

gets flagged (which is good), but it also means that we introduce false positives in cases like

void M2()
{
    var o = new object();
    var t = o?.GetType();
    Console.WriteLine(t.FullName); // GOOD
}

Maybe that is acceptable for a "maybe" query (to consider the intent of the use of ?). I will try and make the change and see what results DCA produce.

Copy link
Contributor

Choose a reason for hiding this comment

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

I would think that was OK; if the qualifier cannot be null then there is no need to use ?.

@michaelnebel michaelnebel marked this pull request as ready for review January 19, 2026 09:49
Copilot AI review requested due to automatic review settings January 19, 2026 09:49
@michaelnebel michaelnebel requested a review from a team as a code owner January 19, 2026 09:49
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This pull request adds support for C# 14 null conditional assignments and improves control flow graph handling for qualified write access. The changes update the control flow logic to match how property assignments work, ensuring field/property accesses are included in the control flow graph after evaluating the right-hand side.

Changes:

  • Updated control flow graph to include field/property access nodes before assignments
  • Added support for null conditional assignments (e.g., ca?.Prop = value)
  • Improved MaybeNullExpr class to handle null conditional read accesses
  • Added comprehensive test coverage for the new features

Reviewed changes

Copilot reviewed 23 out of 24 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
csharp/ql/test/library-tests/standalone/controlflow/cfg.expected Updated expected control flow for array element access assignments
csharp/ql/test/library-tests/obinit/ObInit.expected Updated control flow for object initializer field assignments
csharp/ql/test/library-tests/dataflow/signanalysis/SignAnalysis.expected Added entries for field access in assignments
csharp/ql/test/library-tests/dataflow/nullness/options Added extractor options for nullness test
csharp/ql/test/library-tests/dataflow/nullness/maybeNullExpr.ql New query for testing MaybeNullExpr class
csharp/ql/test/library-tests/dataflow/nullness/maybeNullExpr.expected Expected results for MaybeNullExpr test
csharp/ql/test/library-tests/dataflow/nullness/MaybeNullExpr.cs Test cases for null conditional expressions
csharp/ql/test/library-tests/dataflow/local/TaintTrackingStep.expected Updated taint tracking for array element assignments
csharp/ql/test/library-tests/csharp8/*.expected Updated control flow for various C# 8 features with new assignment handling
csharp/ql/test/library-tests/controlflow/graph/*.expected Comprehensive updates to control flow graph expected results
csharp/ql/test/library-tests/controlflow/graph/ConditionalAccess.cs New test cases for null conditional assignments (M9 method)
csharp/ql/test/library-tests/controlflow/graph/Assignments.cs New test cases for out parameter assignments with fields
Comments suppressed due to low confidence (3)

csharp/ql/test/library-tests/dataflow/nullness/MaybeNullExpr.cs:13

  • This assignment to nullCoalescing is useless, since its value is never read.
        var nullCoalescing = o ?? null;

csharp/ql/test/library-tests/dataflow/nullness/MaybeNullExpr.cs:16

  • This assignment to c is useless, since its value is never read.
        var c = o as C;

csharp/ql/test/library-tests/dataflow/nullness/MaybeNullExpr.cs:19

  • This assignment to s1 is useless, since its value is never read.
        var s1 = (o as C)?.Prop;

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

var c = o as C;

// Conditional access might be null as the qualifier might be null.
var s1 = (o as C)?.Prop;
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

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

Local scope variable 's1' shadows C.s1.

Suggested change
var s1 = (o as C)?.Prop;
var propValue = (o as C)?.Prop;

Copilot uses AI. Check for mistakes.
void M(object o, bool b)
{
// Conditional expr might be null.
var conditionalExpr = b ? new object() : null;
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

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

This assignment to conditionalExpr is useless, since its value is never read.

This issue also appears in the following locations of the same file:

  • line 13
  • line 16
  • line 19

Copilot uses AI. Check for mistakes.
Comment on lines +40 to +43
string this[int index]
{
get { return null; }
set { }
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

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

Value ignored when setting property.

Suggested change
string this[int index]
{
get { return null; }
set { }
string[] _items = new string[1];
string this[int index]
{
get { return index == 0 ? _items[0] : null; }
set
{
if (index == 0)
_items[0] = value;
}

Copilot uses AI. Check for mistakes.
hvitved
hvitved previously approved these changes Jan 19, 2026
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.

LGTM, just the one comment in Nullness.qll.

@michaelnebel
Copy link
Contributor Author

@hvitved : Thank you so much for all the feedback and the time you have spent on this!
Running DCA one more time.

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.

2 participants