-
-
Notifications
You must be signed in to change notification settings - Fork 427
Nullable reference types #581
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
Conversation
facfc3e to
787f73c
Compare
src/Shouldly.Tests/ConventionTests/ApprovePublicApi.ShouldlyApi.approved.cs
Outdated
Show resolved
Hide resolved
| <PackageReference Include="PublicApiGenerator" Version="8.0.1" /> | ||
| <PackageReference Include="PublicApiGenerator" Version="10.0.0-beta1" /> | ||
| <PackageReference Include="TestStack.ConventionTests" Version="3.0.1" /> | ||
| <DotNetCliToolReference Include="dotnet-xunit" Version="2.3.1" /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
dotnet-xunit was discontinued: https://github.com/xunit/xunit/issues/1835#issuecomment-431199286
Version 2.3.1 has no rollforward policy and can only run on .NET Core 2.0 which is EOL.
| <PropertyGroup> | ||
| <TargetFrameworks>netcoreapp2.1</TargetFrameworks> | ||
| <TargetFrameworks Condition=" '$(OS)' == 'Windows_NT' ">$(TargetFrameworks);net472;</TargetFrameworks> | ||
| <LangVersion>latest</LangVersion> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved to Directory.Build.props so that it could be next to <Nullable>enable</Nullable>
| if (object.Equals(x, default(T))) | ||
| return object.Equals(y, default(T)); | ||
| if (object.Equals(x, null)) | ||
| return object.Equals(y, null); | ||
|
|
||
| if (object.Equals(y, default(T))) | ||
| if (object.Equals(y, null)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
default(T) is always null given the if condition. This is nicer than doing default(T)!.
| return true; | ||
|
|
||
| // Null? | ||
| if (!type.IsValueType() || (type.IsGenericType() && type.GetGenericTypeDefinition().IsAssignableFrom(NullableType))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This check would be simpler as if (default(T)! is null). I could find all references of IsValueType and NullableType in a separate PR, but I'd like this PR to be as noise-free as possible.
| public new bool Equals(object? x, object? y) | ||
| { | ||
| return _innerComparer.Equals((T) x, (T) y); | ||
| return _innerComparer.Equals((T)x!, (T)y!); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The cast will always succeed when T is a nullable type at runtime (either a reference type or Nullable<>). The ! operator suppresses the warning that an exception will be thrown if x or y is null and T is a non-nullable value type. Shouldly expects this, just as an exception will be thrown if an object of the wrong type is passed.
8009c7a to
1774735
Compare
| internal static class Is | ||
| { | ||
| public static bool InRange<T>(T comparable, T @from, T to) where T : IComparable<T> | ||
| public static bool InRange<T>([DisallowNull] T comparable, [AllowNull] T @from, [AllowNull] T to) where T : IComparable<T> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[AllowNull] and [DisallowNull] are the only way to annotate these parameters since C# still does not allow T? here. (Allowing T? without a class or struct constraint would be a CLR change.)
| } | ||
|
|
||
| public static void ShouldBeEmpty<T>(this IEnumerable<T> actual) | ||
| public static void ShouldBeEmpty<T>([NotNull] this IEnumerable<T>? actual) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[NotNull] initially seems to conflict with the ?, but there is no conflict. [NotNull] is indicating that if this call returns, the expression that had been passed to actual cannot have been null. At the same time, the method accepts null inputs without a compile-time warning.
You could also choose to go with ShouldBeEmpty<T>(this IEnumerable<T> actual) in which case the compiler would warn if the user does foo.ShouldBeEmpty(); without first proving to the compiler that foo can't be null (or using !).
| /// <param name="obj">The object to check</param> | ||
| /// <returns>true if the object is a numeric type</returns> | ||
| public static bool IsNumericType(Object obj) | ||
| public static bool IsNumericType([NotNullWhen(true)] object? obj) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NotNullWhen is used to tell the compiler's flow state analysis what happens after a call to this method:
nullableThing = ...
if (IsNumericType(nullableThing))
{
nullableThing.GetHashCode(); // No warning because IsNumericType returned true
}
nullableThing.GetHashCode(); // Compiler warning because `nullableThing` could be null at this point| System.Diagnostics.StackTrace stackTrace = null, | ||
| [CallerMemberName] string shouldlyMethod = null) : base(shouldlyMethod, expected, actual, stackTrace) | ||
| System.Diagnostics.StackTrace? stackTrace = null, | ||
| [CallerMemberName] string shouldlyMethod = null!) : base(shouldlyMethod, expected, actual, stackTrace) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think there's a situation where the default value null could be used.
https://docs.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.callermembernameattribute#remarks
| var codeLines = string.Join("\n", File.ReadAllLines(FileName).Skip(LineNumber).ToArray()); | ||
|
|
||
| var indexOf = codeLines.IndexOf(_shouldMethod); | ||
| var indexOf = codeLines.IndexOf(_shouldMethod!); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The way to avoid all these dammit operators (!) would be to pass the value between methods rather than making it a class field. The state of a mutable class field is also harder to reason about between methods for people as well as compilers, so I would recommend this.
| static string DelimitWith<T>(this IEnumerable<T> enumerable, string? separator) where T : class? | ||
| { | ||
| return string.Join(separator, enumerable.Select(i => Equals(i, default(T)) ? null : i.ToString()).ToArray()); | ||
| return string.Join(separator, enumerable.Select(i => Equals(i, null) ? null : i.ToString()).ToArray()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
default(T) was always null already because of the class constraint. Using null is even nicer now that the other option is default(T)!.
| Debug.Assert(context.Actual is IDictionary); | ||
| Debug.Assert(context.Key is object); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
⚠ Not sure how you'd like to handle classes like these. This method is relying on the fragile assumption that remote code will have set these fields, and these asserts silence the warnings this causes now that NRTs are enabled. The way I would handle it would be to require these values to be passed to the DictionaryShouldContainKeyAndValueMessageGenerator constructor.
|
|
||
| [ContractAnnotation("actual:null,expected:null => halt")] | ||
| public static void ShouldNotBe<T>(this T actual, T expected) | ||
| public static void ShouldNotBe<T>([AllowNull] this T actual, [AllowNull] T expected) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[AllowNull] is needed on each so that generic inference doesn't get upset while deciding whether T is nullable or not when one expression is nullable and the other isn't.
| { | ||
| public static partial class ShouldBeStringTestExtensions | ||
| { | ||
| [ContractAnnotation("actual:notnull => halt")] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Side note: I think these JetBrains annotations might actually be incorrect. If actual is "", a non-null value, the method does not halt.
| public static class ShouldBeBooleanExtensions | ||
| { | ||
| public static void ShouldBeTrue(this bool actual) | ||
| public static void ShouldBeTrue([DoesNotReturnIf(false)] this bool actual) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DoesNotReturnIf is the annotation that powers scenarios like this:
nullableThing.GetHashCode(); // Compiler warning, possible NRE
(nullableThing != null).ShouldBeTrue();
nullableThing.GetHashCode(); // No warning because ShouldBeTrue did not throw an exception| public static class ShouldBeDictionaryTestExtensions | ||
| { | ||
| public static void ShouldContainKey<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key) | ||
| where TKey : notnull |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is necessary in order to use TKey for IDictionary<TKey, TValue> due to the generic constraint added to IDictionary<,> in the BCL.
| { | ||
| [ContractAnnotation("actual:notnull => halt")] | ||
| public static void ShouldBeNull<T>(this T actual) | ||
| public static void ShouldBeNull<T>([MaybeNull] this T actual) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this method returns, the actual expression is definitely null. [MaybeNull] is the strongest the .NET annotations get. Threw it in for completeness.
var nonnullableThing = new object();
nonnullableThing.GetHashCode(); // No warning
nonnullableThing.ShouldBeNull();
nonnullableThing.GetHashCode(); // Warning that nonnullableThing may be null| } | ||
| } | ||
|
|
||
| internal class ExpectedEquvalenceShouldlyMessage : ShouldlyMessage |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a typo.
e0cd649 to
bfff1ba
Compare
…ception has no inner exceptions
…the structure of the search
…ing GetMethod unnecessarily
… sorting methods by parameter count)
4573ed5 to
e74d529
Compare
|
@josephwoodward New force push. Two of the commits have already been merged in #596 and I fixed merge conflicts caused by #595. |
|
@jnm2 Nice! So is this good to go? (pending review of course) |
|
@josephwoodward Yes! |
|
@josephwoodward I merged with master to fix merge commits, then I recognized the build failure as being related to certain versions of Mono and Cake pre-0.37 and fixed that, then I fixed some new warnings that will crop up in VS 16.5. I'm not sure reviewing and merging will get more appealing as time goes on, and I don't know what to do to make that easier. |
|
@SimonCropp I was going to push a merge commit to resolve the conflicts. Would it make more sense to wait till the dust settles from the PRs you're merging, or would there be another approach altogether that would work better for you? |
|
i can hold off for a few hours. or i can handle the conflicts and merge for u |
|
@SimonCropp You might have better context than me for fixing the conflicts since you are familiar with what's happened on master in the meantime. That sounds best, but I'm also happy to do it. Once this PR merges, there is a good chance that most outstanding PRs will have merge conflicts too. The tradeoff is fixing things individually versus adding the equivalent work in bulk to this PR. Is one better than the other? |
|
@SimonCropp For sure! |
Closes #579.
I tried to keep the big initial annotation commit as straightforward as possible, saving more complex fixes for later commits. For files where the changes aren't absolutely obvious, please consider reviewing single commits at a time.
➡ Please comment if you'd like any of the types in these APIs to be annotated differently. I simply annotated them as nullable if
nullwas tolerated in previous releases and left them non-nullable ifnullwould have caused an exception. All nullable annotations (?and attributes) are a description of my understanding of the past rather than a statement from me about the future.