Skip to content

Conversation

RikkiGibson
Copy link
Member

@RikkiGibson RikkiGibson commented Feb 27, 2025

From simply trying the feature out I decided that 2-pass is needed. Also simplified some language relating to [NotNull] on the property which didn't seem to be necessary in practice.

- This is because when the user starts applying nullability attributes to the field, we no longer want to infer anything, we just want the nullability to be *what the user said*.
- If the containing property has ***oblivious*** or ***annotated*** nullability, then the backing field has the same nullability as the property.
- If the containing property has *not-annotated* nullability (e.g. `string` or `T`) or has the `[NotNull]` attribute, and the property is ***null-resilient***, then the backing field has ***annotated*** nullability.
- If the containing property has *not-annotated* nullability (e.g. `string` or `T`) or has the `[NotNull]` attribute, and the property is ***not null-resilient***, then the backing field has ***not-annotated*** nullability.
Copy link
Contributor

@jnm2 jnm2 Feb 27, 2025

Choose a reason for hiding this comment

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

I guess the only difference this change makes is whether this becomes a constructor warning requiring initialization instead of a "possible null reference return" warning on get?

[NotNull]
public string? Prop
{
    get;
    set => field = value ?? "";
}

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah I think so. And yes I decided that this isn’t a scenario where we need to do inference. Also, in the case of unconstrained T, inferring not nullable field may not be sufficient for things to work satisfactorily here. You’re already expressing things in a “wonky” way, go ahead and call it [AllowNull] string instead, or, attribute the field so that it works how you want.

Copy link
Contributor

@jnm2 jnm2 left a comment

Choose a reason for hiding this comment

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

🎉

- If the containing property has *not-annotated* nullability (e.g. `string` or `T`) or has the `[NotNull]` attribute, and the property is ***null-resilient***, then the backing field has ***annotated*** nullability.
- If the containing property has *not-annotated* nullability (e.g. `string` or `T`) or has the `[NotNull]` attribute, and the property is ***not null-resilient***, then the backing field has ***not-annotated*** nullability.
- If the containing property has *not-annotated* nullability (e.g. `string` or `T`), and the property is ***null-resilient***, then the backing field has ***annotated*** nullability.
- If the containing property has *not-annotated* nullability (e.g. `string` or `T`), and the property is ***not null-resilient***, then the backing field has ***not-annotated*** nullability.
Copy link
Member

Choose a reason for hiding this comment

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

Consider an alternative approach where we say that the backing field is always annotated if we get into this section. This doesn't change our ability to decide if the get works with an unannotated return type, it just changes how we emit the field. I'm curious what specific things break, from a user and roslyn API perspective, if the field is always annotated?

Copy link
Member Author

@RikkiGibson RikkiGibson Mar 6, 2025

Choose a reason for hiding this comment

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

I've been turning this idea over in my mind quite a bit and I think this is actually going to simplify implementation and improve clarity.

it just changes how we emit the field

This seems fine. If we say a backing field of reference type is always nullable, when field keyword is used, it would affect some reflection behaviors, but I would be surprised if it was anything that breaking. EF, for example, does not look at the backing field's nullability, only the property's nullability.

I'm curious what specific things break, from a user and roslyn API perspective, if the field is always annotated?

Basically it means that for field in string Prop { get => field; }, Quick Info shows string? Prop.field, instead of string Prop.field. I actually think this is a good thing. Currently the implementation always shows string Prop.field, even when we inferred a nullable annotation for it.

The current design was based on the idea that if we just infer the nullable annotation, then constructor analysis will just do the right thing, in terms of knowing whether to enforce initialization of the property. If field is string?, then no need for enforcement. If it is string!, then need to enforce.

My stretch goal was to make public API show the inferred annotation. But if we do that, I'm concerned about "Schrodinger's null", where assigning maybe-null to the field actually changes its type from string to string?. This seems confusing, as the point of saying its type is string is to convey to the user that they are not allowed to put null in it.

If we go with "field is always annotated", then we need to adjust the constructor analysis design so that it uses the "null resilient" concept for these backing fields instead. "Null-resilient" means no need for enforcement. "Not null-resilient" means need to enforce. But that's not a big deal.

Copy link
Member Author

Choose a reason for hiding this comment

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

Also, the current design is to give the field the same nullability as the property, when any nullability attributes are used on the field. If we said the field is "always annotated when reference type", then we would probably want to do that even when attributes are used on the field.

This may break some existing cases where only a precondition or only postcondition attribute is used. Cases where both are specified (e.g. [DisallowNull, NotNull] won't change. I do not think such a break is problematic.

Copy link
Member Author

Choose a reason for hiding this comment

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

I tried to pursue the on the implementation side but it just ended up being too much churn at that time. I ended up pushing a change to bring us back around to being "annotation-oriented" which reflects what we shipped.

@RikkiGibson
Copy link
Member Author

@jnm2 @CyrusNajmabadi @333fred This PR now reflects the reality of what we shipped except that setter analysis is not implemented yet (dotnet/roslyn#77215).

Copy link
Contributor

@jnm2 jnm2 left a comment

Choose a reason for hiding this comment

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

Looks great to me, thank you for the updates!

- If the get accessor is auto-implemented, the property is not null-resilient.

The nullability of the backing field is determined as follows:
- If the field has nullability attributes such as `[field: MaybeNull]`, `AllowNull`, `NotNull`, or `DisallowNull`, then the field's nullable annotation is the same as the property's nullable annotation.
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm glad we didn't go this route.

- If the property is not *null-resilient*, the nullable annotation is *not-annotated*.

> [!NOTE]
> The inferred nullable annotation of the backing field is not exposed in the Roslyn symbol APIs due to the dependency on binding and flow analysis of methods. The inferred annotation is instead used only in nullable analysis (and surfaced indirectly through APIs such as GetSymbolInfo/GetTypeInfo for expressions.) The nullable annotation of the property is used instead in the symbol APIs. See also subsection of [Nullability alternatives](#infer-the-initial-flow-state-of-the-field).
Copy link
Member

Choose a reason for hiding this comment

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

I don't really know if this note is appropriate for a csharplang spec?

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm happy to move it to any more appropriate location, I just want the fact recorded somewhere that it can be found in the future when investigating behavior of this feature area.

@RikkiGibson
Copy link
Member Author

@333fred removed references to public API. I'm going to open a separate PR to Roslyn which adjusts a doc comment to include the information that I want to preserve related to this.

@333fred
Copy link
Member

333fred commented Sep 11, 2025

@333fred removed references to public API. I'm going to open a separate PR to Roslyn which adjusts a doc comment to include the information that I want to preserve related to this.

That works for me, thanks!

@RikkiGibson RikkiGibson merged commit 6f0715c into main Sep 11, 2025
1 check passed
@RikkiGibson RikkiGibson deleted the field-nullability-revisions branch September 11, 2025 20:35
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.

5 participants