Skip to content

Use non-null initial state for null-resilient properties in constructors - #84991

Merged
RikkiGibson merged 5 commits into
dotnet:mainfrom
RikkiGibson:issue77991
Sep 9, 2026
Merged

Use non-null initial state for null-resilient properties in constructors#84991
RikkiGibson merged 5 commits into
dotnet:mainfrom
RikkiGibson:issue77991

Conversation

@RikkiGibson

@RikkiGibson RikkiGibson commented Aug 21, 2026

Copy link
Copy Markdown
Member

Closes #77991

2 principles in this change:

  1. We do not share a slot when the property is null-resilient. This differs from the usual case where properties using field keyword use a shared slot in constructors. The reason is that there is no strong connection between their respective flow states in this situation. Our analysis decided that the field's value doesn't affect the nullability of the get accessor.
    • This allows us to use the property's type, instead of the field's type, etc. to get the property's initial state, in a fairly natural way. I tried some other approaches but this really made things fall out much better and seemed to stand to reason to me fairly well. Once I started doing this, a bunch of other changes I had became unnecessary.
  2. Avoid using knowledge about a backing field in nullable analysis, when the field is not accessible in the method currently being analyzed. This mostly affects required properties+backing fields+inheritance, and, honestly, mostly affects corner cases. But, trying to stick to this principle helped iron some bugs out. For example, when encountering issues with different behaviors occurring when a type comes from source versus metadata.

This also adds verification, that when a slot is shared, that we only ever lookup the slot for a variable using the "owning" symbol. Otherwise we can get really painful bugs where we might have a slot for both "owning" and "non-owning" when we meant to share a single one, or, we can report no state is tracked because we forgot to use the "owning" symbol for the lookup.

Microsoft Reviewers: Open in CodeFlow

@RikkiGibson
RikkiGibson requested a lite review from Copilot August 21, 2026 04:17
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).
1 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

Copilot stopped reviewing on behalf of RikkiGibson due to an error August 21, 2026 04:38
@RikkiGibson RikkiGibson changed the title Use non-null state for initial state of null-resilient properties in constructors Use non-null initial state for null-resilient properties in constructors Aug 29, 2026
@RikkiGibson
RikkiGibson requested a lite review from Copilot and removed request for Copilot September 2, 2026 01:45

This comment was marked as resolved.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

There are verified issues in NullableWalker (debug assertion side effects, accessibility assumptions, and malformed XML doc comments) that should be corrected before approval.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 6/6 changed files
  • Comments generated: 3
  • Review effort level: Lite

Comment thread src/Compilers/CSharp/Portable/FlowAnalysis/NullableWalker.cs
Comment thread src/Compilers/CSharp/Portable/FlowAnalysis/NullableWalker.cs
Comment thread src/Compilers/CSharp/Portable/FlowAnalysis/NullableWalker.cs
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@RikkiGibson
RikkiGibson marked this pull request as ready for review September 3, 2026 00:24
@RikkiGibson
RikkiGibson requested a review from a team as a code owner September 3, 2026 00:24
Copilot AI review requested due to automatic review settings September 3, 2026 00:24
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 2 pipeline(s).
There may be pipelines that require an authorized user to comment /azp run to run.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

It changes core nullable flow analysis slot/ownership behavior in the compiler, which is regression-prone and warrants final review by a domain expert with full CI signal.

Review details
  • Files reviewed: 6/6 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

public C(D3 d3) // 3
{
Prop1 = null;
Prop2 = null; // 4

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should we also read the properties after assignment in these new tests to observe any possible nullability warnings there?

return builder.ToImmutableAndFree();
}

public static ImmutableArray<TResult> SelectManyAsArray<TItem, TArg, TResult>(this IReadOnlyCollection<TItem>? source, Func<TItem, TArg, OneOrMany<TResult>> selector, TArg arg)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should we have an overload that takes ImmutableArray receiver too? Looks like this is currently being called on ImmutableArray receivers, causing boxing.

case PropertySymbol { IsRequired: true }:
case PropertySymbol { IsRequired: true } requiredProperty when GetAccessibleBackingField(requiredProperty, method.ContainingType) is null:
// Visit a required property, unless it has a backing field which is accessible in the current context,
// in which case we expect the field to be visited in another iteration of this loop.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It looks like the field can be skipped in some cases (in if (IsPropertyOutputMoreStrictThanInput(prop))). For example,

#nullable enable
using System.Diagnostics.CodeAnalysis;

public class C
{
    [AllowNull]
    public required string P { get; set; }

    [SetsRequiredMembers]
    public C()
    {
        P.ToString();
    }
}

correctly reported a warning on P.ToString(); before this PR, but does not report it after.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The thing is, the warning is also missing before this PR, when required and [SetsRequiredMembers] are both deleted. So, I could see about putting the warning in both scenarios. Hopefully there is now a single point of control for doing that.

@RikkiGibson RikkiGibson Sep 3, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

If you are comfortable with it, I'd like to address the AllowNull issue more holistically in a follow-up. Basically, I think that we could arrive at an overall more desirable behavior by mostly just deleting a little more implementation code. But, there is test churn involved.

Copilot AI review requested due to automatic review settings September 3, 2026 23:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

It changes core nullable flow analysis and slot-sharing behavior in NullableWalker, which is high-risk and warrants final review by a compiler maintainer despite added tests.

Review details

Suppressed comments (2)

Previously missed (2) — in code that hasn't changed since the last review.

src/Compilers/CSharp/Portable/FlowAnalysis/LocalDataFlowPass.cs:68

  • There’s a small typo in the updated XML comment: “struct constructs” should be “struct constructors”.
    src/Compilers/CSharp/Portable/FlowAnalysis/NullableWalker.cs:574
  • The DEBUG assert in TryGetVariable() is intended to enforce that the slot lookup uses the owning symbol, but tuple equality here uses Symbol.Equals (which can be overridden for non-reference equality). That can allow a different-but-equal symbol to slip through, defeating the purpose of the assert.
  • Files reviewed: 7/7 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread src/Dependencies/Collections/Extensions/ImmutableArrayExtensions.cs
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 4, 2026 00:21

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

ImmutableArrayExtensions.SelectManyAsArray has a malformed addition (missing method signature) that will break compilation.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 7/7 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread src/Dependencies/Collections/Extensions/ImmutableArrayExtensions.cs
Copilot AI review requested due to automatic review settings September 4, 2026 04:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

A new public extension method overload has incomplete XML documentation (missing <param name="arg">), which should be corrected before approval.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 7/7 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread src/Dependencies/Collections/Extensions/ImmutableArrayExtensions.cs
if (array.Length == 0)
return [];

var builder = ArrayBuilder<TResult>.GetInstance();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Interesting that we are not setting the capacity here like we are in the IReadOnlyCollection equivalent. But this matches the other ImmutableArray extensions around, so I guess it's fine.

@RikkiGibson

Copy link
Copy Markdown
Member Author

/pr-val adcc3fb

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

View PR Validation Run triggered by @RikkiGibson

Parameters
  • Validation Type: pr-val
  • Pipeline ID: 8972
  • Pipeline Version: main
  • PR Number: 84991
  • Commit SHA: adcc3fbd892fdc47ca2b42e6dd699fb32ea3f254
  • Source Branch: issue77991
  • Target Branch: main
  • Build ID: 15217868

@RikkiGibson

RikkiGibson commented Sep 7, 2026

Copy link
Copy Markdown
Member Author

CloudBuild is failing in insertion, it looked like a locked file issue, unlikely to be related to this change. I re-queued.

@RikkiGibson

Copy link
Copy Markdown
Member Author

VS build is clean.

@RikkiGibson
RikkiGibson merged commit e1dda53 into dotnet:main Sep 9, 2026
26 checks passed
@dotnet-policy-service dotnet-policy-service Bot added this to the Next milestone Sep 9, 2026
@RikkiGibson
RikkiGibson deleted the issue77991 branch September 9, 2026 22:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Null resilient props should not get nullable initial state in constructors

3 participants