diff --git a/csharp/ql/lib/change-notes/2026-01-06-null-conditional-assignments.md b/csharp/ql/lib/change-notes/2026-01-06-null-conditional-assignments.md new file mode 100644 index 000000000000..e9c5a94478a6 --- /dev/null +++ b/csharp/ql/lib/change-notes/2026-01-06-null-conditional-assignments.md @@ -0,0 +1,4 @@ +--- +category: minorAnalysis +--- +* The `MaybeNullExpr` class now takes null-conditional access (such as `?.` and `?[]`) into account when modeling potential null values. diff --git a/csharp/ql/lib/semmle/code/csharp/dataflow/Nullness.qll b/csharp/ql/lib/semmle/code/csharp/dataflow/Nullness.qll index 6a211e71f456..756fd6a4e3a0 100644 --- a/csharp/ql/lib/semmle/code/csharp/dataflow/Nullness.qll +++ b/csharp/ql/lib/semmle/code/csharp/dataflow/Nullness.qll @@ -43,6 +43,12 @@ private Expr maybeNullExpr(Expr reason) { ) or result.(NullCoalescingExpr).getRightOperand() = maybeNullExpr(reason) + or + result = + any(QualifiableExpr qe | + qe.isConditional() and + qe.getQualifier() = maybeNullExpr(reason) + ) } /** An expression that may be `null`. */ diff --git a/csharp/ql/test/library-tests/nullconditional/NullConditional.cs b/csharp/ql/test/library-tests/nullconditional/NullConditional.cs new file mode 100644 index 000000000000..04392fb4e626 --- /dev/null +++ b/csharp/ql/test/library-tests/nullconditional/NullConditional.cs @@ -0,0 +1,32 @@ +using System; + +public class C +{ + public int Field; + + public string Property { get; set; } = ""; + + public void M(C c, int[] numbers, bool b) + { + // Get values. + var fieldValue = c?.Field; + var propertyValue = c?.Property; + var n = numbers?[0]; + + // Set values + c?.Field = 42; + c?.Property = "Hello"; + numbers?[0] = 7; + + // Set values using operators + c?.Field -= 1; + c?.Property += " World"; + + // Using the return of an assignment + var x = c?.Field = 10; + + // Using in a conditional expression + int? maybenull = 0; + maybenull = (b ? c : null)?.Field; + } +} diff --git a/csharp/ql/test/library-tests/nullconditional/maybenull.expected b/csharp/ql/test/library-tests/nullconditional/maybenull.expected new file mode 100644 index 000000000000..06c5c376b8e6 --- /dev/null +++ b/csharp/ql/test/library-tests/nullconditional/maybenull.expected @@ -0,0 +1,4 @@ +| NullConditional.cs:30:9:30:41 | ... = ... | +| NullConditional.cs:30:21:30:41 | access to field Field | +| NullConditional.cs:30:22:30:33 | ... ? ... : ... | +| NullConditional.cs:30:30:30:33 | null | diff --git a/csharp/ql/test/library-tests/nullconditional/maybenull.ql b/csharp/ql/test/library-tests/nullconditional/maybenull.ql new file mode 100644 index 000000000000..a11fd2a9151c --- /dev/null +++ b/csharp/ql/test/library-tests/nullconditional/maybenull.ql @@ -0,0 +1,6 @@ +import csharp +import semmle.code.csharp.dataflow.Nullness + +from MaybeNullExpr mne +where mne.getFile().getBaseName() = "NullConditional.cs" +select mne diff --git a/csharp/ql/test/library-tests/nullconditional/nullconditional.expected b/csharp/ql/test/library-tests/nullconditional/nullconditional.expected new file mode 100644 index 000000000000..80d51ab9361d --- /dev/null +++ b/csharp/ql/test/library-tests/nullconditional/nullconditional.expected @@ -0,0 +1,12 @@ +| NullConditional.cs:12:26:12:33 | access to field Field | +| NullConditional.cs:13:29:13:39 | access to property Property | +| NullConditional.cs:14:17:14:27 | access to array element | +| NullConditional.cs:17:9:17:16 | access to field Field | +| NullConditional.cs:18:9:18:19 | access to property Property | +| NullConditional.cs:19:9:19:19 | access to array element | +| NullConditional.cs:22:9:22:16 | access to field Field | +| NullConditional.cs:22:9:22:16 | access to field Field | +| NullConditional.cs:23:9:23:19 | access to property Property | +| NullConditional.cs:23:9:23:19 | access to property Property | +| NullConditional.cs:26:17:26:24 | access to field Field | +| NullConditional.cs:30:21:30:41 | access to field Field | diff --git a/csharp/ql/test/library-tests/nullconditional/nullconditional.ql b/csharp/ql/test/library-tests/nullconditional/nullconditional.ql new file mode 100644 index 000000000000..77735091f79f --- /dev/null +++ b/csharp/ql/test/library-tests/nullconditional/nullconditional.ql @@ -0,0 +1,5 @@ +import csharp + +from QualifiableExpr e +where e.isConditional() +select e diff --git a/csharp/ql/test/library-tests/nullconditional/options b/csharp/ql/test/library-tests/nullconditional/options new file mode 100644 index 000000000000..77b22963f5c8 --- /dev/null +++ b/csharp/ql/test/library-tests/nullconditional/options @@ -0,0 +1,2 @@ +semmle-extractor-options: /nostdlib /noconfig +semmle-extractor-options: --load-sources-from-project:${testdir}/../../resources/stubs/_frameworks/Microsoft.NETCore.App/Microsoft.NETCore.App.csproj