Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,17 @@ To get more metadata from a `PropertyInfo`, you can use extensions methods like:
* `IsIndexer`
* `HasAttribute` and `HasAttributeInHierarchy`
* `IsPublic`, `IsInternal` or `IsAbstract` to check either the getter or setters matches the criteria
* `GetNullability` and `IsNullableReference` to determine the nullable reference type annotation of the property

Similarly, you can find indexers using `FindIndexers`, conversion operators through `FindImplicitConversionOperators`
and `FindExplicitConversionOperators`, and methods via `FindMethod`, `FindParameterlessMethod` and `HasMethod`.

For a `FieldInfo`, you can use `GetNullability` and `IsNullableReference` in the same way as for `PropertyInfo`.

For `ParameterInfo`, you can use:

* `HasAttribute` and `HasAttributeInHierarchy` to check whether a parameter is decorated with a specific attribute, with an optional predicate to filter on attribute properties.
* `GetNullability` and `IsNullableReference` to determine the nullable reference type annotation of the parameter.

Other extension methods act on `Type` directly and include:

Expand All @@ -102,6 +106,12 @@ Additionally, Reflectify offers some helpers such as

* `NullableOrActualType` to get the actual type of a nullable type or the type itself if it's not nullable.

`GetNullability` returns a `Nullability` enum (`Unknown`, `NotNull` or `Nullable`) that reflects the compiler-emitted
nullable reference type metadata for reference types, and treats value types consistently with `NullableOrActualType`
(e.g. `int` is `NotNull`, `int?` is `Nullable`). `IsNullableReference` is a convenience shortcut for
`GetNullability() == Nullability.Nullable`. On .NET 6 and later this is backed by `NullabilityInfoContext`; on older
targets it's determined by reading the compiler's `NullableAttribute`/`NullableContextAttribute` metadata directly.

## Download

This library is available as [a NuGet package](https://www.nuget.org/packages/Reflectify) on https://nuget.org. To install it, use the following command-line:
Expand Down
63 changes: 63 additions & 0 deletions src/Reflectify/FieldInfoExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#if !REFLECTIFY_COMPILE
// <autogenerated />
#pragma warning disable
#endif

#nullable disable

using System;
using System.Reflection;

namespace Reflectify;

#if REFLECTIFY_COMPILE
public static class FieldInfoExtensions
#else
[global::Microsoft.CodeAnalysis.Embedded]
[global::System.Diagnostics.DebuggerNonUserCode]
internal static class FieldInfoExtensions
#endif
{
/// <summary>
/// Determines the nullability of the field, taking into account nullable reference type metadata as well as
/// nullable value types (see <see cref="TypeExtensions.NullableOrActualType"/>).
/// </summary>
public static Nullability GetNullability(this FieldInfo field)
{
Type fieldType = field.FieldType;

Type actualType = fieldType.NullableOrActualType();

if (actualType != fieldType)
{
return Nullability.Nullable;
}

if (fieldType.IsValueType)
{
return Nullability.NotNull;
}

#if NET6_0_OR_GREATER
NullabilityInfo info = new NullabilityInfoContext().Create(field);

return info.ReadState switch
{
NullabilityState.NotNull => Nullability.NotNull,
NullabilityState.Nullable => Nullability.Nullable,
_ => Nullability.Unknown
};
#else
return NullabilityMetadataReader.GetNullability(fieldType, field.GetCustomAttributes(inherit: false), field.DeclaringType);
#endif
}

/// <summary>
/// Returns <see langword="true" /> if the field is annotated as accepting <see langword="null"/>, either
/// because it is a nullable reference type or a nullable value type, or <see langword="false" /> otherwise.
/// </summary>
public static bool IsNullableReference(this FieldInfo field)
{
return field.GetNullability() == Nullability.Nullable;
}
}
36 changes: 36 additions & 0 deletions src/Reflectify/Nullability.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#if !REFLECTIFY_COMPILE
// <autogenerated />
#pragma warning disable
#endif

#nullable disable

namespace Reflectify;

/// <summary>
/// Represents the nullability of a member, as determined from the compiler-emitted nullable reference type
/// metadata, or from the fact that the member is a value type (or <see cref="System.Nullable{T}"/>).
/// </summary>
#if REFLECTIFY_COMPILE
public enum Nullability
#else
[global::Microsoft.CodeAnalysis.Embedded]
internal enum Nullability
#endif
{
/// <summary>
/// The nullability of the member could not be determined, typically because it was compiled without
/// nullable reference types enabled (the "oblivious" context).
/// </summary>
Unknown,

/// <summary>
/// The member is annotated as not allowing <see langword="null"/>.
/// </summary>
NotNull,

/// <summary>
/// The member is annotated as allowing <see langword="null"/>.
/// </summary>
Nullable
}
149 changes: 149 additions & 0 deletions src/Reflectify/NullabilityMetadataReader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
#if !REFLECTIFY_COMPILE
// <autogenerated />
#pragma warning disable
#endif

#nullable disable

using System;
using System.Reflection;

namespace Reflectify;

// This fallback is only needed on frameworks that lack System.Reflection.NullabilityInfoContext (pre-.NET 6). On
// net6.0-or-greater the extension methods use that API directly, so this whole type is excluded there to avoid it
// being reported as uninstrumented/uncovered dead code.
#if !NET6_0_OR_GREATER
/// <summary>
/// Provides the manual, attribute-based fallback used to determine nullable reference type metadata on target
/// frameworks that don't have access to <c>System.Reflection.NullabilityInfoContext</c> (i.e. everything
/// before .NET 6).
/// </summary>
/// <remarks>
/// The C# compiler encodes nullable reference type information using two compiler-internal attributes that are not
/// part of the public BCL surface: <c>System.Runtime.CompilerServices.NullableAttribute</c> and
/// <c>System.Runtime.CompilerServices.NullableContextAttribute</c>. Because these attributes are embedded
/// per-assembly (rather than shared through a common reference), they must be matched by their full type name
/// instead of via <c>typeof(...)</c>, since a <c>NullableAttribute</c> emitted into one assembly is a different CLR
/// type than one emitted into another. See
/// https://github.com/dotnet/roslyn/blob/main/docs/features/nullable-metadata.md for the specification this logic
/// is based on.
/// </remarks>
#if !REFLECTIFY_COMPILE
[global::Microsoft.CodeAnalysis.Embedded]
#endif
[global::System.Diagnostics.DebuggerNonUserCode]
internal static class NullabilityMetadataReader
{
private const string NullableAttributeFullName = "System.Runtime.CompilerServices.NullableAttribute";
private const string NullableContextAttributeFullName = "System.Runtime.CompilerServices.NullableContextAttribute";

/// <summary>
/// Determines the <see cref="Nullability"/> of a member of type <paramref name="memberType"/>, given the
/// attributes declared directly on that member and the type that declares it.
/// </summary>
/// <param name="memberType">The type of the property, field or parameter.</param>
/// <param name="memberAttributes">The custom attributes declared directly on the member.</param>
/// <param name="declaringType">The type that declares the member, used to walk outward for the ambient nullable context.</param>
public static Nullability GetNullability(Type memberType, object[] memberAttributes, Type declaringType)
{
if (memberType is null)
{
return Nullability.Unknown;
}

Type actualType = memberType.NullableOrActualType();

if (actualType != memberType)
{
// Nullable<T>, e.g. int?
return Nullability.Nullable;
}

if (memberType.IsValueType)
{
// Plain value types, e.g. int, can never be null.
return Nullability.NotNull;
}

byte? flag = GetNullableAttributeFlag(memberAttributes) ?? GetAmbientNullableContextFlag(declaringType);

return MapFlag(flag);
}

private static byte? GetNullableAttributeFlag(object[] attributes)
{
foreach (object attribute in attributes)
{
if (attribute.GetType().FullName == NullableAttributeFullName)
{
FieldInfo field = attribute.GetType().GetField("NullableFlags", BindingFlags.Public | BindingFlags.Instance);

// The NullableAttribute constructor always populates NullableFlags as a byte[], regardless of
// whether it was invoked with a single byte or a byte[] (nested generic arguments). The first
// element always applies to the member itself.
if (field?.GetValue(attribute) is byte[] { Length: > 0 } flags)
{
return flags[0];
}
}
}

return null;
}

private static byte? GetAmbientNullableContextFlag(Type declaringType)
{
for (Type type = declaringType; type is not null; type = type.DeclaringType)
{
byte? flag = GetNullableContextFlag(type.GetCustomAttributes(inherit: false));

if (flag is not null)
{
return flag;
}
}

if (declaringType is not null)
{
byte? flag = GetNullableContextFlag(declaringType.Module.GetCustomAttributes(inherit: false));

if (flag is not null)
{
return flag;
}
}

return null;
}

private static byte? GetNullableContextFlag(object[] attributes)
{
foreach (object attribute in attributes)
{
if (attribute.GetType().FullName == NullableContextAttributeFullName)
{
FieldInfo field = attribute.GetType().GetField("Flag", BindingFlags.Public | BindingFlags.Instance);

if (field?.GetValue(attribute) is byte value)
{
return value;
}
}
}

return null;
}

private static Nullability MapFlag(byte? flag)
{
// Per the Roslyn nullable metadata spec: 0 = oblivious, 1 = not annotated (not null), 2 = annotated (nullable).
return flag switch
{
1 => Nullability.NotNull,
2 => Nullability.Nullable,
_ => Nullability.Unknown
};
}
}
#endif
44 changes: 44 additions & 0 deletions src/Reflectify/ParameterInfoExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,48 @@ public static bool HasAttributeInHierarchy<TAttribute>(this ParameterInfo parame
{
return parameter.IsDefined(typeof(TAttribute), inherit: true);
}

/// <summary>
/// Determines the nullability of the parameter, taking into account nullable reference type metadata as well as
/// nullable value types (see <see cref="TypeExtensions.NullableOrActualType"/>).
/// </summary>
public static Nullability GetNullability(this ParameterInfo parameter)
{
Type parameterType = parameter.ParameterType;

Type actualType = parameterType.NullableOrActualType();

if (actualType != parameterType)
{
return Nullability.Nullable;
}

if (parameterType.IsValueType)
{
return Nullability.NotNull;
}

#if NET6_0_OR_GREATER
NullabilityInfo info = new NullabilityInfoContext().Create(parameter);

return info.ReadState switch
{
NullabilityState.NotNull => Nullability.NotNull,
NullabilityState.Nullable => Nullability.Nullable,
_ => Nullability.Unknown
};
#else
return NullabilityMetadataReader.GetNullability(parameterType, parameter.GetCustomAttributes(inherit: false),
parameter.Member.DeclaringType);
#endif
}

/// <summary>
/// Returns <see langword="true" /> if the parameter is annotated as accepting <see langword="null"/>, either
/// because it is a nullable reference type or a nullable value type, or <see langword="false" /> otherwise.
/// </summary>
public static bool IsNullableReference(this ParameterInfo parameter)
{
return parameter.GetNullability() == Nullability.Nullable;
}
}
45 changes: 45 additions & 0 deletions src/Reflectify/PropertyInfoExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

#nullable disable

using System;
using System.Reflection;

namespace Reflectify;
Expand Down Expand Up @@ -64,4 +65,48 @@ public static bool IsAbstract(this PropertyInfo prop)
{
return prop.GetMethod is { IsAbstract: true } || prop.SetMethod is { IsAbstract: true };
}

/// <summary>
/// Determines the nullability of the property, taking into account nullable reference type metadata as well as
/// nullable value types (see <see cref="TypeExtensions.NullableOrActualType"/>).
/// </summary>
public static Nullability GetNullability(this PropertyInfo prop)
{
Type propertyType = prop.PropertyType;

Type actualType = propertyType.NullableOrActualType();

if (actualType != propertyType)
{
return Nullability.Nullable;
}

if (propertyType.IsValueType)
{
return Nullability.NotNull;
}

#if NET6_0_OR_GREATER
NullabilityInfo info = new NullabilityInfoContext().Create(prop);
NullabilityState state = prop.GetMethod is not null ? info.ReadState : info.WriteState;

return state switch
{
NullabilityState.NotNull => Nullability.NotNull,
NullabilityState.Nullable => Nullability.Nullable,
_ => Nullability.Unknown
};
#else
return NullabilityMetadataReader.GetNullability(propertyType, prop.GetCustomAttributes(inherit: false), prop.DeclaringType);
#endif
}

/// <summary>
/// Returns <see langword="true" /> if the property is annotated as accepting <see langword="null"/>, either
/// because it is a nullable reference type or a nullable value type, or <see langword="false" /> otherwise.
/// </summary>
public static bool IsNullableReference(this PropertyInfo prop)
{
return prop.GetNullability() == Nullability.Nullable;
}
}
Loading