Skip to content
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

Attributes for nullable annotations #29617

Closed
terrajobst opened this issue May 20, 2019 · 24 comments
Closed

Attributes for nullable annotations #29617

terrajobst opened this issue May 20, 2019 · 24 comments
Assignees
Labels
api-approved API was approved in API review, it can be implemented area-System.Diagnostics
Milestone

Comments

@terrajobst
Copy link
Contributor

The nullable reference types feature allows developers to express which APIs can accept and return null values. Some framework APIs, such as generic APIs, or TryXxx methods use specific patterns that the normal type system of C# can't express. This proposal lists all the attributes.

namespace System.Diagnostics.CodeAnalysis 
{
    [AttributeUsage(AttributeTargets.Field |
                    AttributeTargets.Parameter |
                    AttributeTargets.Property)]
    public sealed class AllowNullAttribute : Attribute
    {    	
    }

    [AttributeUsage(AttributeTargets.Field |
                    AttributeTargets.Parameter |
                    AttributeTargets.Property)]
    public sealed class DisallowNullAttribute : Attribute
    {
    }

    [AttributeUsage(AttributeTargets.Field |
                    AttributeTargets.Parameter |
                    AttributeTargets.Property |
                    AttributeTargets.ReturnValue)]
    public sealed class MaybeNullAttribute : Attribute
    {
    }

    [AttributeUsage(AttributeTargets.Field |
                    AttributeTargets.Parameter |
                    AttributeTargets.Property |
                    AttributeTargets.ReturnValue)]
    public sealed class NotNullAttribute : Attribute
    {    	
    }

    [AttributeUsage(AttributeTargets.Parameter)]
    public sealed class MaybeNullWhenAttribute : Attribute
    {
        public MaybeNullWhenAttribute(bool returnValue);
        public bool ReturnValue { get; }
    }

    [AttributeUsage(AttributeTargets.Parameter)]
    public sealed class NotNullWhenAttribute : Attribute
    {
        public NotNullWhenAttribute(bool returnValue);
        public bool ReturnValue { get; }
    }

    [AttributeUsage(AttributeTargets.Parameter |
                    AttributeTargets.Property |
                    AttributeTargets.ReturnValue, AllowMultiple = true)]
    public sealed class NotNullIfNotNullAttribute : Attribute
    {
        public NotNullIfNotNullAttribute(string parameterName);
        public string ParameterName { get; }
    }

    [AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor)]
    public sealed class DoesNotReturnAttribute : Attribute
    {
    }

    [AttributeUsage(AttributeTargets.Parameter)]
    public sealed class DoesNotReturnIfAttribute : Attribute
    {
        public DoesNotReturnIfAttribute(bool parameterValue);
        public bool ParameterValue { get; }
    }
}
@bartonjs
Copy link
Member

Should the "Maybe" values be "MayBe"? As in "this may be null", vs "this maybe null" (which has no verb)

@danmoseley
Copy link
Member

+1, "this maybe null" is not grammatical as far as I know

@jkotas
Copy link
Member

jkotas commented May 21, 2019

I am having hard time guessing what some of the attributes do, e.g. AllowNullAttribute. Is there a spec with more details? Alternative, add a example of an existing framework method for each attribute.

DoesNotReturnAttribute

This is generally useful. It is not tied to nullable annotations. Should it be regular attribute, not related to NullableAnnotationAttribute?

@jkotas
Copy link
Member

jkotas commented May 21, 2019

they can be grouped

Where do we expect the grouping to be used? Null somewhere in the name is a good enough clue for humans that the attribute has something to do with nullable annotations.

@stephentoub
Copy link
Member

stephentoub commented May 21, 2019

@jcouv's LDM notes at https://github.com/dotnet/csharplang/blob/master/meetings/2019/LDM-2019-05-15.md include more in-depth descriptions from @MadsTorgersen as well as examples I compiled from around Corelib.

@stephentoub
Copy link
Member

This is generally useful. It is not tied to nullable annotations. Should it be regular attribute, not related to NullableAnnotationAttribute?

Yes, [DoesNotReturn] is not directly tied to nullability. It impacts reachability in general.

@stephentoub
Copy link
Member

stephentoub commented May 21, 2019

For reference, I have a local draft of a change that employs these in corelib. Appx usage counts end up being something like this:

  • DoesNotReturn: 318
  • AllowNull: 42
  • MaybeNull: 28
  • MaybeNullWhen(false): 23
  • NotNullIfNotNull(paramName): 22
  • NotNullWhen(true): 12
  • NotNullWhen(false): 3
  • DisallowNull: 11
  • NotNull: 8
  • DoesNotReturnIf(false): 8

@marek-safar
Copy link
Contributor

Could you include attributes target in the proposal? It would clarify the intention where they can be used.

@stephentoub
Copy link
Member

Here's what I have in my local branch. Obviously this can be tweaked, it's just what I put in place to be able to make progress deploying the attributes across Corelib:

namespace System.Runtime.CompilerServices
{
    /// <summary>Specifies that null is allowed as an input even if the corresponding type disallows it.</summary>
    [AttributeUsage(AttributeTargets.Field | AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true)]
    public sealed class AllowNullAttribute : Attribute { }

    /// <summary>Specifies that null is disallowed as an input even if the corresponding type allows it.</summary>
    [AttributeUsage(AttributeTargets.Field | AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true)]
    public sealed class DisallowNullAttribute : Attribute { }

    /// <summary>Specifies that an output may be null even if the corresponding type disallows it.</summary>
    [AttributeUsage(AttributeTargets.Field | AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true)]
    public sealed class MaybeNullAttribute : Attribute { }

    /// <summary>Specifies that an output will not be null even if the corresponding type allows it.</summary>
    [AttributeUsage(AttributeTargets.Field | AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true)]
    public sealed class NotNullAttribute : Attribute { }

    /// <summary>Specifies that when a method returns <see cref="ReturnValue"/>, the parameter may be null even if the corresponding type disallows it.</summary>
    [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)]
    public sealed class MaybeNullWhenAttribute : Attribute
    {
        /// <summary>Initializes the attribute with the specified return value condition.</summary>
        /// <param name="returnValue">
        /// The return value condition. If the method returns this value, the associated parameter may be null.
        /// </param>
        public MaybeNullWhenAttribute(bool returnValue) => ReturnValue = returnValue;

        /// <summary>Gets the return value condition.</summary>
        public bool ReturnValue { get; }
    }

    /// <summary>Specifies that when a method returns <see cref="ReturnValue"/>, the parameter will not be null even if the corresponding type allows it.</summary>
    [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)]
    public sealed class NotNullWhenAttribute : Attribute
    {
        /// <summary>Initializes the attribute with the specified return value condition.</summary>
        /// <param name="returnValue">
        /// The return value condition. If the method returns this value, the associated parameter will not be null.
        /// </param>
        public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue;

        /// <summary>Gets the return value condition.</summary>
        public bool ReturnValue { get; }
    }

    /// <summary>Specifies that the output will be non-null if the named parameter is non-null.</summary>
    [AttributeUsage(AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true)]
    public sealed class NotNullIfNotNullAttribute : Attribute
    {
        /// <summary>Initializes the attribute with the associated parameter name.</summary>
        /// <param name="parameterName">
        /// The associated parameter name.  The output will be non-null if the argument to the parameter specified is non-null.
        /// </param>
        public NotNullIfNotNullAttribute(string parameterName)
        {
            ParameterName = parameterName ?? throw new ArgumentNullException(nameof(parameterName));
        }

        /// <summary>Gets the associated parameter name.</summary>
        public string ParameterName { get; }
    }

    /// <summary>Applied to a method that will never return under any circumstance.</summary>
    [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
    public sealed class DoesNotReturnAttribute : Attribute { }

    /// <summary>Specifies that the method will not return if the associated Boolean property is passed the specified value.</summary>
    [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = true)]
    public sealed class DoesNotReturnIfAttribute : Attribute
    {
        /// <summary>Initializes the attribute.</summary>
        /// <param name="parameterValue">
        /// The condition parameter value.  Code after the method will be unreachable if the argument to the associated parameter
        /// matches this value.
        /// </param>
        public DoesNotReturnIfAttribute(bool parameterValue) => ParameterValue = parameterValue;

        /// <summary>Gets the condition parameter value.</summary>
        public bool ParameterValue { get; }
    }
}

@stephentoub
Copy link
Member

A full draft change for Corelib is here:
dotnet/coreclr#24679

@terrajobst
Copy link
Contributor Author

terrajobst commented May 21, 2019

Video

Summary:

  • We didn't want to put them in compiler services, because most APIs aren't meant to be used by developers.
  • NullableAttribute (emitted by the compiler for question marks) will still go to compiler services.
  • We decided to go with the existing System.Diagnostics.CodeAnalysis namespace
  • Since the new namespace is low in terms of types, we decided that we don't need a base type

Here is the API shape we agreed on:

namespace System.Diagnostics.CodeAnalysis 
{
    [AttributeUsage(AttributeTargets.Field |
                    AttributeTargets.Parameter |
                    AttributeTargets.Property)]
    public sealed class AllowNullAttribute : Attribute
    {    	
    }

    [AttributeUsage(AttributeTargets.Field |
                    AttributeTargets.Parameter |
                    AttributeTargets.Property)]
    public sealed class DisallowNullAttribute : Attribute
    {
    }

    [AttributeUsage(AttributeTargets.Field |
                    AttributeTargets.Parameter |
                    AttributeTargets.Property |
                    AttributeTargets.ReturnValue)]
    public sealed class MaybeNullAttribute : Attribute
    {
    }

    [AttributeUsage(AttributeTargets.Field |
                    AttributeTargets.Parameter |
                    AttributeTargets.Property |
                    AttributeTargets.ReturnValue)]
    public sealed class NotNullAttribute : Attribute
    {    	
    }

    [AttributeUsage(AttributeTargets.Parameter)]
    public sealed class MaybeNullWhenAttribute : Attribute
    {
        public MaybeNullWhenAttribute(bool returnValue);
        public bool ReturnValue { get; }
    }

    [AttributeUsage(AttributeTargets.Parameter)]
    public sealed class NotNullWhenAttribute : Attribute
    {
        public NotNullWhenAttribute(bool returnValue);
        public bool ReturnValue { get; }
    }

    [AttributeUsage(AttributeTargets.Parameter |
                    AttributeTargets.Property |
                    AttributeTargets.ReturnValue, AllowMultiple = true)]
    public sealed class NotNullIfNotNullAttribute : Attribute
    {
        public NotNullIfNotNullAttribute(string parameterName);
        public string ParameterName { get; }
    }

    [AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor)]
    public sealed class DoesNotReturnAttribute : Attribute
    {
    }

    [AttributeUsage(AttributeTargets.Parameter)]
    public sealed class DoesNotReturnIfAttribute : Attribute
    {
        public DoesNotReturnIfAttribute(bool parameterValue);
        public bool ParameterValue { get; }
    }
}

@danmoseley
Copy link
Member

@bartonjs @terrajobst was maybe vs may be (ie Maybe vs MayBe) discussed in the review? maybe it's just me but it sounds wrong as Maybe

@jcouv
Copy link
Member

jcouv commented May 22, 2019

Looping @cston. A prompt resolution on naming will be appreciated to avoid blocking his work to recognize the attributes in the compiler. Thanks

@stephentoub
Copy link
Member

maybe it's just me but it sounds wrong as Maybe

We did discuss it at the review. It's not just you. It's the difference between "the state of this may be null" and "the state of this is 'maybe null'". I read it as the former, in which case MayBeNull is better. Others read it as the latter, in which case MaybeNull is better. I still prefer the former, but there were arguments made about the latter being a) consistent with "not null" (e.g. "the state of this is 'not null'" vs "the state of this not null", the latter of which obviously doesn't make sense), and b) being easier to type/read. No one had a strong enough preference on the former case to override the stronger opinions on the latter case.

@danmoseley
Copy link
Member

OK fine. Sounds good. For what it's worth, I read "if you can exchange it with 'perhaps' then maybe is potentially correct. if you can exchange it will 'could be' then may be is potentially correct". Which is basically the difference you describe.

@stephentoub stephentoub self-assigned this May 30, 2019
@stephentoub
Copy link
Member

This has been checked in.

@jskeet
Copy link

jskeet commented Jul 27, 2019

I'm struggling to work out whether any of these attributes has the meaning that I quite often want of "this method doesn't return (because it'll throw an exception) if the parameter was null".

For example, I'd want to be able to write:

string? foo = // Get value from somewhere.
Preconditions.CheckNotNull(foo, nameof(foo)); // Ideally using CallerExpressionAttribute, of course...
Console.WriteLine(foo.Length);

It feels like I want a mixture of NotNullWhen and DoesNotReturn, but I can't see how to do that.

Have I missed something, or should I file a feature request?

(I can make Preconditions.CheckNotNull accept a T? and return a T which sorts out a lot of uses, but not all.)

@svick
Copy link
Contributor

svick commented Jul 27, 2019

@jskeet The LDM notes say:

Open question: would we also need a [DoesNotReturnWhenNull(nameof(parameter))]?

I think that's what you're looking for, but it does not exist (yet?).

Others said they want to see this attribute too, see dotnet/roslyn#35816 (comment) and dotnet/csharplang#2536 (comment).

@jskeet
Copy link

jskeet commented Jul 27, 2019

@svick: Fantastic, thanks for being more on the ball than I am.

@jcouv
Copy link
Member

jcouv commented Jul 29, 2019

Filed dotnet/roslyn#37544 for tracking (post C# 8).

@jcouv
Copy link
Member

jcouv commented Jul 30, 2019

@jskeet @svick You can do void M([NotNull] string? x) today and get the effect that [DoesNotReturnWhenNull(nameof(x))] would presumably have.

@jskeet
Copy link

jskeet commented Jul 30, 2019

@jcouv: Maybe it's just the brief description of the method that confuses me, but I don't see how NotNullAttribute would do that. This is about the input of the method rather than the output of the method. Is there any more concrete documentation on this that we could read?

@jcouv
Copy link
Member

jcouv commented Jul 30, 2019

I was wrong. [NotNull] does not work on by-value/in parameters.

The best documentation we have on nullable annotation attributes is https://github.com/dotnet/csharplang/blob/master/meetings/2019/LDM-2019-05-15.md
The doc says that [NotNull] only applies to outputs (returns values, out/ref parameters) and the compiler did implement that.

Verified with the following test:

        [Fact]
        public void Test()
        {
            var source = @"
using System;
#nullable enable

public class C
{
    void M2(string? x)
    {
        M(x);
        x.ToString(); // warning
    }
    void M([NotNull] string? x) // when this method returns, `x` was not null
    {
        if (x is null) throw null!;
    }
}

[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue)]
public sealed class NotNullAttribute : Attribute
{
}";
            var comp = CreateNullableCompilation(source);
            comp.VerifyDiagnostics(
                    // (10,9): warning CS8602: Dereference of a possibly null reference.
                    //         x.ToString(); // warning
                    Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "x").WithLocation(10, 9)
                    );
        }

@jcouv
Copy link
Member

jcouv commented Jul 30, 2019

I'm investigating further...
I see code that should learn from post-conditions on inputs, which matches my recollection. But the behavior shown by test above shows no effect.

            switch (refKind)
            {
                case RefKind.None:
                case RefKind.In:
                    {
                        // learn from post-conditions [Maybe/NotNull, Maybe/NotNullWhen] without using an assignment
                        learnFromPostConditions(argument, parameterAnnotations);
                    }
                    break;

Will continue discussion in dotnet/roslyn#37544

@msftgits msftgits transferred this issue from dotnet/corefx Feb 1, 2020
@msftgits msftgits added this to the 3.0 milestone Feb 1, 2020
@ghost ghost locked as resolved and limited conversation to collaborators Dec 13, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
api-approved API was approved in API review, it can be implemented area-System.Diagnostics
Projects
None yet
Development

No branches or pull requests

10 participants