-
Notifications
You must be signed in to change notification settings - Fork 4k
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
Recognize annotations on methods that affect nullability analysis #26761
Comments
cc @jcouv |
Any plans to also include an annotation for marking a method as not returning (dotnet/csharplang#538)/always throwing (dotnet/csharplang#739)? Since throw helpers are still pretty common, it'd be useful if the flow analysis could be informed of those. public int M(string s)
{
if (s == null)
ThrowHelper.ThrowArgNullException(nameof(s));
//'s' is definitely not null here, but how will the compiler know?
return s.Length;
} |
I second @Joe4evr's request. I use throw helper methods extensively in Web API code with HttpResponseException such as
|
@Eirenarch Looking at #26656, |
As a long time user of ReSharper's annotations and nullability analysis, I'm very happy to see nullability annotations coming with the nullable reference types feature. However, the numbers of attributes used to represent the various true/false/null/not-null/throws state combinations could grow quite large if you want to handle most cases, and I believe that most cases should be handled to ease adoption of this feature. The goal isn't to recreate code contracts, but to allow the nullability analysis to handle 99% of cases while keeping the attributes readable. ReSharper solves this with ContractAnnotation, having its own syntax. I understand that introducing a completely new syntax for this in Roslyn would be quite out of scope. That's why I propose the following [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = true)]
public sealed class ImpliedNullabilityAttribute : Attribute
{
ImpliedNullabilityAttribute(SourceArgumentKind source, TargetResultKind target);
ImpliedNullabilityAttribute(SourceResultKind source, TargetArgumentKind target);
}
public enum SourceArgumentKind { True, False, Null, NotNull }
public enum TargetResultKind { True, False, NotNull, Throws }
public enum SourceResultKind { True, False, Null, NotNull, Any }
public enum TargetArgumentKind { True, False, NotNull } Naming is hard, and I'm open to anything.
Applied to the examples above, the syntax would be: // ensures arg is true
void Assert(
[ImpliedNullability(SourceArgumentKind.False, TargetResultKind.Throws)] bool b
)
// false if arg is not null
bool IsNullOrEmpty(
[ImpliedNullability(SourceArgumentKind.NotNull, TargetResultKind.False)]
string? s
)
// result nullability matches arg
string? NormalizePath(
[ImpliedNullability(SourceArgumentKind.NotNull, TargetResultKind.NotNull)]
string? path
)
// ref arg not reset to null
void InitializeAndAdd<T>(
[ImpliedNullability(SourceResultKind.Any, TargetResultKind.NotNull)] ref List<T>? list,
T item
) Other common use cases: // ensures arg is not null
void ThrowIfNull(
[ImpliedNullability(SourceArgumentKind.Null, TargetResultKind.Throws)] object? o
);
// not null if arg is null
string? GetValidationError(
[ImpliedNullability(SourceArgumentKind.Null, TargetResultKind.NotNull)] object? o
) Or even more sophisticated cases for existing classes: bool Version.TryParse(
[ImpliedNullability(SourceArgumentKind.Null, TargetResultKind.False)] string? input,
[ImpliedNullability(SourceResultKind.True, TargetArgumentKind.NotNull)] Version? output
)
string? input = ...;
if (Version.TryParse(input, out Version? version)) {
// input is known to be not null
// version is known to be not null
} Various points:
I believe that this would handle most of the nullability cases. What do you think? |
I fail to see how this is better than different attributes. It seems more complex and less readable. With separate attributes which are appropriately named things will be much simpler even if there are a lot of them |
I believe that these Attribute names must be absolutely clear and unambiguous to be useful. Examples above are inprecise: // false if arg is not null // must be the other way: arg is not null if false
static bool IsNullOrEmpty([NotNullWhenFalse] string? s) // result nullability matches arg // equivalence?
static string? NormalizePath([NullInNullOut] string? path) // implication? |
Do these have compile-time guarantees or it's merely used for metadata? i.e. could give error if the control leaves the method while b is not statically proved to be true. That sounds like method contracts (#119) static void Assert(bool b) ensures b
static void Fail(string msg) ensures false
static bool IsNullOrEmpty(string? s) returns s != null
static bool Equals(object? x, object? y) returns x == y
static string? NormalizePath(string? path) returns path
static void InitializeAndAdd<T>(ref List<T>? list, T item) ensures list != null The question is how we're going to formulate contracts in metadata (in a general way?) |
After playing around with the VS 2019 preview, I'm definitely looking forward to this being implemented. It will make null warnings much more intelligent & useful. Mads mentioned in this video that a mini language of attributes is being developed to allow the compiler to analyze across assemblies. My question is whether this can be done transparently by the compiler. I love the prospect of intelligent non-nullable analysis, but I don't want to learn a mini language of attributes and I don't want to repeat & maintain my code logic in the mini language. I really think this needs to be auto generated for the majority of uses. For the 2% of cases where the compiler can't analyze it properly, manually written annotations could be set to override the compiler's. This auto generation could be implemented by a 3rd party "Analyzer with Code Fix" nuget, but I'd really prefer it was baked in for all users as I'd be concerned with method attributes going stale / rotting and then essentially being untrustworthy. |
For methods that do not return, we are considering adding dotnet/csharplang#538 to the scope of C# 8. |
Here's another case we're hitting with some frequency in coreclr/corefx... It's a fairly common pattern for methods that do callbacks to take Consider a method like: public CancellationTokenRegistration Register(Action<object> callback, object state) on public CancellationTokenRegistration Register(Action<object?> callback, object? state) but that means when I write code like: ct.Register(thisRef => ((CurrentType)thisRef).Foo(), this); I'm warned that I may be dereferencing a null public CancellationTokenRegistration Register(Action<[NotNullWhenNotNull(nameof(state))]object?> callback, object? state) then the compiler would be able to see that the argument As it stands, most code using such methods (which often know statically that the state argument isn't null) will need to use ct.Register(thisRef => ((CurrentType)thisRef)!.Foo(), this); cc: @dotnet/nullablefc |
Another situation... There are places where the nullability of the return value is based on a Boolean argument to the method, e.g. private ReaderWriterCount GetThreadRWCount(bool dontAllocate) This can return Related, we have methods like: public static Delegate CreateDelegate(Type type, Type target, string method, bool ignoreCase, bool throwOnBindFailure) that will never return null if a Boolean argument (in this case, cc: @dotnet/nullablefc |
Another case, private void LazyCreateEvent(ref EventWaitHandle waitEvent, EnterLockType enterLockType) cc: @dotnet/nullablefc |
@stephentoub Regarding In other words, if you changed the method to |
I think it does. It also might actually be all the type information available.
As you say, we can't change the signature. Even if we were to add a new one, the existing one would still be there, and if you actually had something typed as object, that overload would continue to be used. Plus, adding a new version of some of these APIs isn't as simple as just adding an overload, at least not to do it "correctly", as it then means the implementation needs to support multiple (or more) kinds of callbacks, needs to type test for them, and so on, which might be fine or might add problematic overhead. (And some are on interfaces, which would require DIM to augment.) |
The following pattern is common and currently requires a if (errorCode < 0)
{
throw Marshal.GetExceptionForHR(errorCode);
} Providing a way to say: "EnsuresNotNull when errorCode < 0" would be useful. I also logged #34754, as I'm not sure we should warn for |
|
I agree with Jan
|
I'm not sure what the attribute would be here, but here's a case that I'm expecting will cause a fair number of spurious warnings...
if (buffer.TryGetArray(out ArraySegment<T> arraySegment)
{
Use(arraySegment.Array);
} the |
I've come across a few cases now where code does effectively: if (!ReferenceEquals(something, null)) something.Foo(); and the |
Is there a problem with changing this to |
No, that's what I've been doing as I encounter them. I'm just calling out examples where code changes are required in order to silence warnings (either using |
@stephentoub Regarding the
For the consumer, it'd be nice if it simply ended up working like this: var withNull = new Timer(o => o.GetType(), //warning: potential dereference of a 'null' reference
null, //because this is the null-literal (or a nullable variable is passed in) 'o' is treated as object?
TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10));
var withNonNull = new Timer(o => o.GetType(), //no warning
"Definite non-null object", //because this is a non-null variable, 'o' is treated as object!
TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10)); This might be way too complicated to work, but I'm not letting that stop me from putting the idea out there. |
Exactly my thoughts. Seems in line with the other attributes though. |
This issue was superseded by #35816 which describes our plan for C# 8. Also see the language proposal which describes the attributes and their meaning: https://github.com/dotnet/csharplang/blob/master/meetings/2019/LDM-2019-05-15.md I'll go ahead and close the present issue. |
Examples, with placeholder attributes:
(jcouv updated): We also need to handle:
Interlocked.CompareExchange
FilterAndAppendAndFreeDiagnostics
). This way the caller would be warned if passing a maybe-null argument. (Debug.Assert
would not do that)TryGetValue
onWeakReference
object.ReferenceEquals
AsNode()
is safe ifIsNode
is true?var x = EnsuresNotNull(something);
M(x = y, y)
whereEnsuresNotNull
is on the second parameter, orM(y, x = y)
where it is on the first?Filed https://github.com/dotnet/corefx/issues/33880 to track work to annotate the BCL and collect ideas of APIs needing annotations.
We should confirm whether the nullable attributes should be added to mono or not.
The text was updated successfully, but these errors were encountered: