Skip to content

Commit

Permalink
Supporting C# 8 Nullable Reference Type attributes (#229)
Browse files Browse the repository at this point in the history
* Supporting C# 8 Nullable Reference Type attributes

- Method parameters and return values that use nullable reference types don't have null guards inserted

Implements #203

* Add documentation
  • Loading branch information
flcdrg authored and SimonCropp committed Dec 31, 2019
1 parent eed6f12 commit 28a0c73
Show file tree
Hide file tree
Showing 6 changed files with 118 additions and 5 deletions.
40 changes: 40 additions & 0 deletions AssemblyToProcess/NullableReferenceTypeClass.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

#nullable enable
public class NullableReferenceTypeClass
{
/*
public void SomeMethod(string nonNullArg, [Nullable(2)] string nullArg)
{
Console.WriteLine(nonNullArg);
}
*/
public void SomeMethod(string nonNullArg, string? nullArg)
{
Console.WriteLine(nonNullArg);
}

public string MethodWithReturnValue(bool returnNull)
{
#pragma warning disable CS8603 // Possible null reference return.
return returnNull ? null : "";
#pragma warning restore CS8603 // Possible null reference return.
}

/*
[NullableContext(2)]
public string MethodAllowsNullReturnValue()
{
return (string) null;
}
*/
public string? MethodAllowsNullReturnValue()
{
return null;
}
}
#nullable disable
24 changes: 20 additions & 4 deletions NullGuard.Fody/CecilExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ static class CecilExtensions
const string AllowNullAttributeTypeName = "AllowNullAttribute";
const string CanBeNullAttributeTypeName = "CanBeNullAttribute";

// https://github.com/dotnet/roslyn/blob/master/docs/features/nullable-metadata.md
const string NullableContextAttributeTypeName = "NullableContextAttribute";
const string NullableAttributeTypeName = "NullableAttribute";
const byte NullableAnnotated = 2;
private const string SystemByteFullTypeName = "System.Byte";

public static bool HasInterface(this TypeDefinition type, string interfaceFullName)
{
return type.Interfaces.Any(i => i.InterfaceType.FullName.Equals(interfaceFullName))
Expand Down Expand Up @@ -61,9 +67,18 @@ public static bool AllowsNull(this PropertyDefinition property, ExplicitMode exp
return property.ImplicitAllowsNull();
}

static bool HasNullableReferenceType(this Mono.Collections.Generic.Collection<CustomAttribute> value, string attributeTypeName)
=> value.Where(a => a.AttributeType.Name == attributeTypeName)
.SelectMany(a => a.ConstructorArguments)
.Where(ca => ca.Type.FullName == SystemByteFullTypeName)
.Where(ca => (byte)ca.Value == NullableAnnotated)
.Any();

public static bool ImplicitAllowsNull(this ICustomAttributeProvider value)
{
return value.CustomAttributes.Any(a => a.AttributeType.Name == AllowNullAttributeTypeName || a.AttributeType.Name == CanBeNullAttributeTypeName);
return value.CustomAttributes.HasNullableReferenceType(NullableAttributeTypeName) ||
value.CustomAttributes.Any(a => a.AttributeType.Name == AllowNullAttributeTypeName ||
a.AttributeType.Name == CanBeNullAttributeTypeName);
}

public static bool AllowsNullReturnValue(this MethodDefinition methodDefinition, ExplicitMode explicitMode)
Expand All @@ -74,9 +89,10 @@ public static bool AllowsNullReturnValue(this MethodDefinition methodDefinition,
return explicitMode.AllowsNull(methodDefinition);
}

return methodDefinition.MethodReturnType.CustomAttributes.Any(a => a.AttributeType.Name == AllowNullAttributeTypeName) ||
// ReSharper uses a *method* attribute for CanBeNull for the return value
methodDefinition.CustomAttributes.Any(a => a.AttributeType.Name == CanBeNullAttributeTypeName);
return methodDefinition.CustomAttributes.HasNullableReferenceType(NullableContextAttributeTypeName) ||
methodDefinition.MethodReturnType.CustomAttributes.Any(a => a.AttributeType.Name == AllowNullAttributeTypeName) ||
// ReSharper uses a *method* attribute for CanBeNull for the return value
methodDefinition.CustomAttributes.Any(a => a.AttributeType.Name == CanBeNullAttributeTypeName);
}

public static bool ContainsAllowNullAttribute(this ICustomAttributeProvider definition)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[NullGuard] nonNullArg is null.
Parameter name: nonNullArg
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[NullGuard] Return value of method 'System.String NullableReferenceTypeClass::MethodWithReturnValue(System.Boolean)' is null.
35 changes: 35 additions & 0 deletions Tests/RewritingMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,24 @@ public void AllowsNullWhenAttributeApplied()
sample.SomeMethod("", null);
}

[Fact]
public Task RequiresNonNullArgumentWhenNullableReferenceTypeNotUsed()
{
var type = AssemblyWeaver.Assembly.GetType(nameof(NullableReferenceTypeClass));
var sample = (dynamic)Activator.CreateInstance(type);
var exception = Assert.Throws<ArgumentNullException>(() => { sample.SomeMethod(null, ""); });
Assert.Equal("nonNullArg", exception.ParamName);
return Verify(exception.Message);
}

[Fact]
public void AllowsNullWhenNullableReferenceTypeUsed()
{
var type = AssemblyWeaver.Assembly.GetType(nameof(NullableReferenceTypeClass));
var sample = (dynamic)Activator.CreateInstance(type);
sample.SomeMethod("", null);
}

[Fact]
public Task RequiresNonNullMethodReturnValue()
{
Expand All @@ -86,6 +104,15 @@ public Task RequiresNonNullMethodReturnValue()
return Verify(exception.Message);
}

[Fact]
public Task RequiresNonNullMethodReturnValueWhenNullableReferenceTypeNotUsed()
{
var type = AssemblyWeaver.Assembly.GetType(nameof(NullableReferenceTypeClass));
var sample = (dynamic)Activator.CreateInstance(type);
var exception = Assert.Throws<InvalidOperationException>(() => sample.MethodWithReturnValue(true));
return Verify(exception.Message);
}

[Fact]
public Task RequiresNonNullGenericMethodReturnValue()
{
Expand All @@ -103,6 +130,14 @@ public void AllowsNullReturnValueWhenAttributeApplied()
sample.MethodAllowsNullReturnValue();
}

[Fact]
public void AllowsNullReturnValueWhenNullableReferenceType()
{
var type = AssemblyWeaver.Assembly.GetType(nameof(NullableReferenceTypeClass));
var sample = (dynamic)Activator.CreateInstance(type);
sample.MethodAllowsNullReturnValue();
}

[Fact]
public Task RequiresNonNullOutValue()
{
Expand Down
21 changes: 20 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ The `Install-Package Fody` is required since NuGet always defaults to the oldest

NullGuard supports two modes of operations, [*implicit*](#implicit-mode) and [*explicit*](#explicit-mode).

* In [*implicit*](#implicit-mode) mode everything is assumed to be not-null, unless attributed with `[AllowNull]`. This is how NullGuard has been working always.
* In [*implicit*](#implicit-mode) mode everything is assumed to be not-null, unless attributed with `[AllowNull]`. This is how NullGuard has been working always. C# 8 nullable reference types are also used to determine if a type may be null.
* In the new [*explicit*](#explicit-mode) mode everything is assumed to be nullable, unless attributed with `[NotNull]`. This mode is designed to support the R# nullability analysis, using pessimistic mode.

If not configured explicitly, NullGuard will auto-detect the mode as follows:
Expand All @@ -50,6 +50,11 @@ public class Sample
// arg may be null here
}

public void AndAnotherMethod(string? arg)
{
// arg may be null here
}

public string MethodWithReturn()
{
return SomeOtherClass.SomeMethod();
Expand All @@ -61,6 +66,11 @@ public class Sample
return null;
}

public string? MethodAlsoAllowsNullReturnValue()
{
return null;
}

// Null checking works for automatic properties too.
public string SomeProperty { get; set; }

Expand Down Expand Up @@ -104,6 +114,11 @@ public class SampleOutput
return null;
}

public string MethodAlsoAllowsNullReturnValue()
{
return null;
}

string someProperty;
public string SomeProperty
{
Expand All @@ -129,6 +144,10 @@ public class SampleOutput
{
}

public void AndAnotherMethod(string arg)
{
}

public string MethodWithReturn()
{
var returnValue = SomeOtherClass.SomeMethod();
Expand Down

0 comments on commit 28a0c73

Please sign in to comment.