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
18 changes: 12 additions & 6 deletions Docs/pages/02-filters.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,8 +181,8 @@ properties (including inherited ones) have no setter or an `init`-only setter. S
immutability. Failure messages list the offending mutable members for actionable feedback.

`OnlyHasNullableMembers` / `OnlyHaveNullableMembers` (and the non-nullable counterparts) verify the
[nullability](#nullability) of all declared fields and properties of the type; the failure message lists the
non-compliant members per type:
[nullability](#nullability) of all declared fields, properties and events of the type; the failure message
lists the non-compliant members per type:

```csharp
await Expect.That(In.AssemblyContaining<MyRequest>()
Expand Down Expand Up @@ -425,7 +425,8 @@ and `IsNot…` / `AreNot…` on assertions (e.g. `WhichAreNotConstant()`, `IsNot

A property or field counts as *nullable* when its type is a `Nullable<T>` value type (e.g. `int?`) or a
reference type annotated as nullable (e.g. `string?`, based on the nullable reference type metadata emitted
by the compiler). The check follows the declared annotation on every target framework: reference types
by the compiler). The same applies to [events](#events), whose handler type is always a delegate (reference)
type. The check follows the declared annotation on every target framework: reference types
without nullability annotations (oblivious code compiled without `<Nullable>enable</Nullable>`) and
unconstrained generic type parameters (`T`, as opposed to `T?`) count as non-nullable, and post-condition
attributes like `[AllowNull]` or `[MaybeNull]` are ignored.
Expand Down Expand Up @@ -470,15 +471,20 @@ In addition to [access modifiers](#access-modifiers),
| static | `.WhichAreStatic()` | `.IsStatic()` | `.AreStatic()` |
| virtual | `.WhichAreVirtual()` | `.IsVirtual()` | `.AreVirtual()` |
| override | `.WhichOverride()` | `.Overrides()` | `.Override()` |
| nullable | `.WhichAreNullable()` | `.IsNullable()` | `.AreNullable()` |

The `OfType` / `IsOfType` / `AreOfType` filters and assertions match the event's handler type (its
`EventHandlerType`, e.g. `EventHandler<T>`); the `…ExactType` variants match only the exact handler type.
Use `OrOfType<T>()` / `OrOfExactType<T>()` to allow several handler types.

An event counts as *nullable* when its handler type is annotated as nullable
(e.g. `event EventHandler? Changed;`), following the same [nullability](#nullability) rules as
properties and fields.

:::note[Negation]
The `abstract`, `sealed`, `static` and `virtual` rows have a negated form: `WhichAreNot…` on filters and
`IsNot…` / `AreNot…` on assertions (e.g. `WhichAreNotSealed()`, `IsNotSealed()`, `AreNotSealed()`);
`override` uses `WhichDoNotOverride()` / `DoesNotOverride()` / `DoNotOverride()`.
The `abstract`, `sealed`, `static`, `virtual` and `nullable` rows have a negated form: `WhichAreNot…` on
filters and `IsNot…` / `AreNot…` on assertions (e.g. `WhichAreNotSealed()`, `IsNotSealed()`,
`AreNotSealed()`); `override` uses `WhichDoNotOverride()` / `DoesNotOverride()` / `DoNotOverride()`.
:::

```csharp
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.Reflection;
using aweXpect.Reflection.Collections;
using aweXpect.Reflection.Helpers;

namespace aweXpect.Reflection;

public static partial class EventFilters
{
/// <summary>
/// Filters for events that are nullable.
/// </summary>
public static Filtered.Events WhichAreNullable(this Filtered.Events @this)
=> @this.Which(Filter.Prefix<EventInfo>(
eventInfo => eventInfo.IsNullable(),
"nullable "));

/// <summary>
/// Filters for events that are not nullable.
/// </summary>
public static Filtered.Events WhichAreNotNullable(this Filtered.Events @this)
=> @this.Which(Filter.Prefix<EventInfo>(
eventInfo => !eventInfo.IsNullable(),
"non-nullable "));
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace aweXpect.Reflection;
public static partial class TypeFilters
{
/// <summary>
/// Filters for types whose fields and properties are all nullable, including inherited members or only
/// Filters for types whose fields, properties and events are all nullable, including inherited members or only
/// those declared directly on the type according to the <paramref name="memberScope" />.
/// </summary>
public static Filtered.Types WhichOnlyHaveNullableMembers(this Filtered.Types @this,
Expand All @@ -17,8 +17,8 @@ public static Filtered.Types WhichOnlyHaveNullableMembers(this Filtered.Types @t
"which only have nullable members "));

/// <summary>
/// Filters for types whose fields and properties are all non-nullable, including inherited members or only
/// those declared directly on the type according to the <paramref name="memberScope" />.
/// Filters for types whose fields, properties and events are all non-nullable, including inherited members or
/// only those declared directly on the type according to the <paramref name="memberScope" />.
/// </summary>
public static Filtered.Types WhichOnlyHaveNonNullableMembers(this Filtered.Types @this,
MemberScope memberScope = MemberScope.DeclaredOnly)
Expand Down
30 changes: 27 additions & 3 deletions Source/aweXpect.Reflection/Helpers/NullabilityHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,23 +68,41 @@ public static bool IsNullable(this FieldInfo? fieldInfo)
}

/// <summary>
/// Returns the nullable fields and properties of the <paramref name="type" />, including inherited
/// Checks if the <paramref name="eventInfo" /> is nullable.
/// </summary>
/// <remarks>
/// An event is considered nullable if its handler type is annotated as nullable (according to the
/// nullable reference type metadata). Event handler types are always delegate (reference) types,
/// so the <see cref="Nullable{T}" /> value type check does not apply.
/// </remarks>
public static bool IsNullable(this EventInfo? eventInfo)
{
if (eventInfo is null)
{
return false;
}

return IsNullableReferenceType(eventInfo);
}

/// <summary>
/// Returns the nullable fields, properties and events of the <paramref name="type" />, including inherited
/// members or only those declared directly on the type according to the <paramref name="memberScope" />.
/// </summary>
public static MemberInfo[] GetNullableMembers(this Type type,
MemberScope memberScope = MemberScope.DeclaredOnly)
=> type.GetMembersByNullability(memberScope).Nullable;

/// <summary>
/// Returns the non-nullable fields and properties of the <paramref name="type" />, including inherited
/// Returns the non-nullable fields, properties and events of the <paramref name="type" />, including inherited
/// members or only those declared directly on the type according to the <paramref name="memberScope" />.
/// </summary>
public static MemberInfo[] GetNotNullableMembers(this Type type,
MemberScope memberScope = MemberScope.DeclaredOnly)
=> type.GetMembersByNullability(memberScope).NotNullable;

/// <summary>
/// Partitions the fields and properties of the <paramref name="type" /> into nullable and
/// Partitions the fields, properties and events of the <paramref name="type" /> into nullable and
/// non-nullable members in a single pass, including inherited members or only those declared
/// directly on the type according to the <paramref name="memberScope" />.
/// </summary>
Expand All @@ -103,6 +121,11 @@ public static (MemberInfo[] Nullable, MemberInfo[] NotNullable) GetMembersByNull
(property.IsNullable() ? nullable : notNullable).Add(property);
}

foreach (EventInfo @event in type.GetDeclaredEvents(memberScope))
{
(@event.IsNullable() ? nullable : notNullable).Add(@event);
}

return (nullable.ToArray(), notNullable.ToArray());
}

Expand Down Expand Up @@ -250,6 +273,7 @@ private static bool IsNullableViaGenericArgument(MemberInfo memberInfo)
{
FieldInfo fieldInfo => fieldInfo.FieldType,
PropertyInfo propertyInfo => propertyInfo.PropertyType,
EventInfo eventInfo => eventInfo.EventHandlerType,
_ => null,
};
}
Expand Down
68 changes: 68 additions & 0 deletions Source/aweXpect.Reflection/ThatEvent.IsNullable.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using System.Reflection;
using System.Text;
using aweXpect.Core;
using aweXpect.Core.Constraints;
using aweXpect.Reflection.Helpers;
using aweXpect.Results;

namespace aweXpect.Reflection;

public static partial class ThatEvent
{
/// <summary>
/// Verifies that the <see cref="EventInfo" /> is nullable.
/// </summary>
/// <remarks>
/// An event is considered nullable if its handler type is annotated as nullable
/// (according to the nullable reference type metadata).
/// </remarks>
public static AndOrResult<EventInfo?, IThat<EventInfo?>> IsNullable(
this IThat<EventInfo?> subject)
=> new(subject.Get().ExpectationBuilder.AddConstraint((it, grammars)
=> new IsNullableConstraint(it, grammars)),
subject);

/// <summary>
/// Verifies that the <see cref="EventInfo" /> is not nullable.
/// </summary>
/// <remarks>
/// An event is considered nullable if its handler type is annotated as nullable
/// (according to the nullable reference type metadata).
/// Events without nullability annotations (oblivious code) count as non-nullable.
/// </remarks>
public static AndOrResult<EventInfo?, IThat<EventInfo?>> IsNotNullable(
this IThat<EventInfo?> subject)
=> new(subject.Get().ExpectationBuilder.AddConstraint((it, grammars)
=> new IsNullableConstraint(it, grammars).Invert()),
subject);

private sealed class IsNullableConstraint(string it, ExpectationGrammars grammars)
: ConstraintResult.WithNotNullValue<EventInfo?>(it, grammars),
IValueConstraint<EventInfo?>
{
public ConstraintResult IsMetBy(EventInfo? actual)
{
Actual = actual;
Outcome = actual.IsNullable() ? Outcome.Success : Outcome.Failure;
return this;
}

protected override void AppendNormalExpectation(StringBuilder stringBuilder, string? indentation = null)
=> stringBuilder.Append("is nullable");

protected override void AppendNormalResult(StringBuilder stringBuilder, string? indentation = null)
{
stringBuilder.Append(It).Append(" was non-nullable ");
Formatter.Format(stringBuilder, Actual);
}

protected override void AppendNegatedExpectation(StringBuilder stringBuilder, string? indentation = null)
=> stringBuilder.Append("is not nullable");

protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null)
{
stringBuilder.Append(It).Append(" was nullable ");
Formatter.Format(stringBuilder, Actual);
}
}
}
128 changes: 128 additions & 0 deletions Source/aweXpect.Reflection/ThatEvents.AreNullable.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
using System.Collections.Generic;
using System.Reflection;
using System.Text;
using aweXpect.Core;
using aweXpect.Core.Constraints;
using aweXpect.Reflection.Helpers;
using aweXpect.Results;
#if NET8_0_OR_GREATER
using System.Threading;
using System.Threading.Tasks;
#endif

// ReSharper disable PossibleMultipleEnumeration

namespace aweXpect.Reflection;

public static partial class ThatEvents
{
/// <summary>
/// Verifies that all items in the filtered collection of <see cref="EventInfo" /> are nullable.
/// </summary>
public static AndOrResult<IEnumerable<EventInfo?>, IThat<IEnumerable<EventInfo?>>> AreNullable(
this IThat<IEnumerable<EventInfo?>> subject)
=> new(subject.Get().ExpectationBuilder.AddConstraint<IEnumerable<EventInfo?>>((it, grammars)
=> new AreNullableConstraint(it, grammars)),
subject);

#if NET8_0_OR_GREATER
/// <summary>
/// Verifies that all items in the filtered collection of <see cref="EventInfo" /> are nullable.
/// </summary>
public static AndOrResult<IAsyncEnumerable<EventInfo?>, IThat<IAsyncEnumerable<EventInfo?>>> AreNullable(
this IThat<IAsyncEnumerable<EventInfo?>> subject)
=> new(subject.Get().ExpectationBuilder.AddConstraint<IAsyncEnumerable<EventInfo?>>((it, grammars)
=> new AreNullableConstraint(it, grammars)),
subject);
#endif

/// <summary>
/// Verifies that all items in the filtered collection of <see cref="EventInfo" /> are not nullable.
/// </summary>
public static AndOrResult<IEnumerable<EventInfo?>, IThat<IEnumerable<EventInfo?>>> AreNotNullable(
this IThat<IEnumerable<EventInfo?>> subject)
=> new(subject.Get().ExpectationBuilder.AddConstraint<IEnumerable<EventInfo?>>((it, grammars)
=> new AreNotNullableConstraint(it, grammars)),
subject);

#if NET8_0_OR_GREATER
/// <summary>
/// Verifies that all items in the filtered collection of <see cref="EventInfo" /> are not nullable.
/// </summary>
public static AndOrResult<IAsyncEnumerable<EventInfo?>, IThat<IAsyncEnumerable<EventInfo?>>> AreNotNullable(
this IThat<IAsyncEnumerable<EventInfo?>> subject)
=> new(subject.Get().ExpectationBuilder.AddConstraint<IAsyncEnumerable<EventInfo?>>((it, grammars)
=> new AreNotNullableConstraint(it, grammars)),
subject);
#endif

private sealed class AreNullableConstraint(string it, ExpectationGrammars grammars)
: CollectionConstraintResult<EventInfo?>(grammars),
IValueConstraint<IEnumerable<EventInfo?>>
#if NET8_0_OR_GREATER
, IAsyncConstraint<IAsyncEnumerable<EventInfo?>>
#endif
{
#if NET8_0_OR_GREATER
public async Task<ConstraintResult> IsMetBy(IAsyncEnumerable<EventInfo?> actual,
CancellationToken cancellationToken)
=> await SetAsyncValue(actual, @event => @event.IsNullable());
#endif

public ConstraintResult IsMetBy(IEnumerable<EventInfo?> actual)
=> SetValue(actual, @event => @event.IsNullable());

protected override void AppendNormalExpectation(StringBuilder stringBuilder, string? indentation = null)
=> stringBuilder.Append("are all nullable");

protected override void AppendNormalResult(StringBuilder stringBuilder, string? indentation = null)
{
stringBuilder.Append(it).Append(" contained non-nullable events ");
Formatter.Format(stringBuilder, NotMatching, FormattingOptions.Indented(indentation));
}

protected override void AppendNegatedExpectation(StringBuilder stringBuilder, string? indentation = null)
=> stringBuilder.Append("are not all nullable");

protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null)
{
stringBuilder.Append(it).Append(" only contained nullable events ");
Formatter.Format(stringBuilder, Matching, FormattingOptions.Indented(indentation));
}
}

private sealed class AreNotNullableConstraint(string it, ExpectationGrammars grammars)
: CollectionConstraintResult<EventInfo?>(grammars),
IValueConstraint<IEnumerable<EventInfo?>>
#if NET8_0_OR_GREATER
, IAsyncConstraint<IAsyncEnumerable<EventInfo?>>
#endif
{
#if NET8_0_OR_GREATER
public async Task<ConstraintResult> IsMetBy(IAsyncEnumerable<EventInfo?> actual,
CancellationToken cancellationToken)
=> await SetAsyncValue(actual, @event => @event?.IsNullable() == false);
#endif

public ConstraintResult IsMetBy(IEnumerable<EventInfo?> actual)
=> SetValue(actual, @event => @event?.IsNullable() == false);

protected override void AppendNormalExpectation(StringBuilder stringBuilder, string? indentation = null)
=> stringBuilder.Append("are all not nullable");

protected override void AppendNormalResult(StringBuilder stringBuilder, string? indentation = null)
{
stringBuilder.Append(it).Append(" contained nullable events ");
Formatter.Format(stringBuilder, NotMatching, FormattingOptions.Indented(indentation));
}

protected override void AppendNegatedExpectation(StringBuilder stringBuilder, string? indentation = null)
=> stringBuilder.Append("also contain a nullable event");

protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null)
{
stringBuilder.Append(it).Append(" only contained non-nullable events ");
Formatter.Format(stringBuilder, Matching, FormattingOptions.Indented(indentation));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ namespace aweXpect.Reflection;
public static partial class ThatType
{
/// <summary>
/// Verifies that all fields and properties of the <see cref="Type" /> are non-nullable, including inherited
/// members or only those declared directly on the type according to the <paramref name="memberScope" />.
/// Verifies that all fields, properties and events of the <see cref="Type" /> are non-nullable, including
/// inherited members or only those declared directly on the type according to the
/// <paramref name="memberScope" />.
/// </summary>
/// <remarks>
/// A member is considered nullable if its type is a <see cref="Nullable{T}" /> value type or a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ namespace aweXpect.Reflection;
public static partial class ThatType
{
/// <summary>
/// Verifies that all fields and properties of the <see cref="Type" /> are nullable, including inherited
/// Verifies that all fields, properties and events of the <see cref="Type" /> are nullable, including inherited
/// members or only those declared directly on the type according to the <paramref name="memberScope" />.
/// </summary>
/// <remarks>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ namespace aweXpect.Reflection;
public static partial class ThatTypes
{
/// <summary>
/// Verifies that all fields and properties of all items in the filtered collection of <see cref="Type" />
/// Verifies that all fields, properties and events of all items in the filtered collection of <see cref="Type" />
/// are non-nullable, including inherited members or only those declared directly on the type according to
/// the <paramref name="memberScope" />.
/// </summary>
Expand All @@ -35,7 +35,7 @@ public static partial class ThatTypes

#if NET8_0_OR_GREATER
/// <summary>
/// Verifies that all fields and properties of all items in the filtered collection of <see cref="Type" />
/// Verifies that all fields, properties and events of all items in the filtered collection of <see cref="Type" />
/// are non-nullable, including inherited members or only those declared directly on the type according to
/// the <paramref name="memberScope" />.
/// </summary>
Expand Down
Loading
Loading