diff --git a/Docs/pages/02-filters.md b/Docs/pages/02-filters.md index d329240f..95323c3b 100644 --- a/Docs/pages/02-filters.md +++ b/Docs/pages/02-filters.md @@ -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() @@ -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` 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 `enable`) and unconstrained generic type parameters (`T`, as opposed to `T?`) count as non-nullable, and post-condition attributes like `[AllowNull]` or `[MaybeNull]` are ignored. @@ -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`); the `…ExactType` variants match only the exact handler type. Use `OrOfType()` / `OrOfExactType()` 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 diff --git a/Source/aweXpect.Reflection/Filters/EventFilters.WhichAreNullable.cs b/Source/aweXpect.Reflection/Filters/EventFilters.WhichAreNullable.cs new file mode 100644 index 00000000..314afc33 --- /dev/null +++ b/Source/aweXpect.Reflection/Filters/EventFilters.WhichAreNullable.cs @@ -0,0 +1,24 @@ +using System.Reflection; +using aweXpect.Reflection.Collections; +using aweXpect.Reflection.Helpers; + +namespace aweXpect.Reflection; + +public static partial class EventFilters +{ + /// + /// Filters for events that are nullable. + /// + public static Filtered.Events WhichAreNullable(this Filtered.Events @this) + => @this.Which(Filter.Prefix( + eventInfo => eventInfo.IsNullable(), + "nullable ")); + + /// + /// Filters for events that are not nullable. + /// + public static Filtered.Events WhichAreNotNullable(this Filtered.Events @this) + => @this.Which(Filter.Prefix( + eventInfo => !eventInfo.IsNullable(), + "non-nullable ")); +} diff --git a/Source/aweXpect.Reflection/Filters/TypeFilters.WhichOnlyHaveNullableMembers.cs b/Source/aweXpect.Reflection/Filters/TypeFilters.WhichOnlyHaveNullableMembers.cs index b996237f..f0e495a6 100644 --- a/Source/aweXpect.Reflection/Filters/TypeFilters.WhichOnlyHaveNullableMembers.cs +++ b/Source/aweXpect.Reflection/Filters/TypeFilters.WhichOnlyHaveNullableMembers.cs @@ -7,7 +7,7 @@ namespace aweXpect.Reflection; public static partial class TypeFilters { /// - /// 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 . /// public static Filtered.Types WhichOnlyHaveNullableMembers(this Filtered.Types @this, @@ -17,8 +17,8 @@ public static Filtered.Types WhichOnlyHaveNullableMembers(this Filtered.Types @t "which only have nullable members ")); /// - /// 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 . + /// 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 . /// public static Filtered.Types WhichOnlyHaveNonNullableMembers(this Filtered.Types @this, MemberScope memberScope = MemberScope.DeclaredOnly) diff --git a/Source/aweXpect.Reflection/Helpers/NullabilityHelpers.cs b/Source/aweXpect.Reflection/Helpers/NullabilityHelpers.cs index ccc13342..6cc23ec2 100644 --- a/Source/aweXpect.Reflection/Helpers/NullabilityHelpers.cs +++ b/Source/aweXpect.Reflection/Helpers/NullabilityHelpers.cs @@ -68,7 +68,25 @@ public static bool IsNullable(this FieldInfo? fieldInfo) } /// - /// Returns the nullable fields and properties of the , including inherited + /// Checks if the is nullable. + /// + /// + /// 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 value type check does not apply. + /// + public static bool IsNullable(this EventInfo? eventInfo) + { + if (eventInfo is null) + { + return false; + } + + return IsNullableReferenceType(eventInfo); + } + + /// + /// Returns the nullable fields, properties and events of the , including inherited /// members or only those declared directly on the type according to the . /// public static MemberInfo[] GetNullableMembers(this Type type, @@ -76,7 +94,7 @@ public static MemberInfo[] GetNullableMembers(this Type type, => type.GetMembersByNullability(memberScope).Nullable; /// - /// Returns the non-nullable fields and properties of the , including inherited + /// Returns the non-nullable fields, properties and events of the , including inherited /// members or only those declared directly on the type according to the . /// public static MemberInfo[] GetNotNullableMembers(this Type type, @@ -84,7 +102,7 @@ public static MemberInfo[] GetNotNullableMembers(this Type type, => type.GetMembersByNullability(memberScope).NotNullable; /// - /// Partitions the fields and properties of the into nullable and + /// Partitions the fields, properties and events of the into nullable and /// non-nullable members in a single pass, including inherited members or only those declared /// directly on the type according to the . /// @@ -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()); } @@ -250,6 +273,7 @@ private static bool IsNullableViaGenericArgument(MemberInfo memberInfo) { FieldInfo fieldInfo => fieldInfo.FieldType, PropertyInfo propertyInfo => propertyInfo.PropertyType, + EventInfo eventInfo => eventInfo.EventHandlerType, _ => null, }; } diff --git a/Source/aweXpect.Reflection/ThatEvent.IsNullable.cs b/Source/aweXpect.Reflection/ThatEvent.IsNullable.cs new file mode 100644 index 00000000..fd30aaf1 --- /dev/null +++ b/Source/aweXpect.Reflection/ThatEvent.IsNullable.cs @@ -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 +{ + /// + /// Verifies that the is nullable. + /// + /// + /// An event is considered nullable if its handler type is annotated as nullable + /// (according to the nullable reference type metadata). + /// + public static AndOrResult> IsNullable( + this IThat subject) + => new(subject.Get().ExpectationBuilder.AddConstraint((it, grammars) + => new IsNullableConstraint(it, grammars)), + subject); + + /// + /// Verifies that the is not nullable. + /// + /// + /// 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. + /// + public static AndOrResult> IsNotNullable( + this IThat subject) + => new(subject.Get().ExpectationBuilder.AddConstraint((it, grammars) + => new IsNullableConstraint(it, grammars).Invert()), + subject); + + private sealed class IsNullableConstraint(string it, ExpectationGrammars grammars) + : ConstraintResult.WithNotNullValue(it, grammars), + IValueConstraint + { + 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); + } + } +} diff --git a/Source/aweXpect.Reflection/ThatEvents.AreNullable.cs b/Source/aweXpect.Reflection/ThatEvents.AreNullable.cs new file mode 100644 index 00000000..81750187 --- /dev/null +++ b/Source/aweXpect.Reflection/ThatEvents.AreNullable.cs @@ -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 +{ + /// + /// Verifies that all items in the filtered collection of are nullable. + /// + public static AndOrResult, IThat>> AreNullable( + this IThat> subject) + => new(subject.Get().ExpectationBuilder.AddConstraint>((it, grammars) + => new AreNullableConstraint(it, grammars)), + subject); + +#if NET8_0_OR_GREATER + /// + /// Verifies that all items in the filtered collection of are nullable. + /// + public static AndOrResult, IThat>> AreNullable( + this IThat> subject) + => new(subject.Get().ExpectationBuilder.AddConstraint>((it, grammars) + => new AreNullableConstraint(it, grammars)), + subject); +#endif + + /// + /// Verifies that all items in the filtered collection of are not nullable. + /// + public static AndOrResult, IThat>> AreNotNullable( + this IThat> subject) + => new(subject.Get().ExpectationBuilder.AddConstraint>((it, grammars) + => new AreNotNullableConstraint(it, grammars)), + subject); + +#if NET8_0_OR_GREATER + /// + /// Verifies that all items in the filtered collection of are not nullable. + /// + public static AndOrResult, IThat>> AreNotNullable( + this IThat> subject) + => new(subject.Get().ExpectationBuilder.AddConstraint>((it, grammars) + => new AreNotNullableConstraint(it, grammars)), + subject); +#endif + + private sealed class AreNullableConstraint(string it, ExpectationGrammars grammars) + : CollectionConstraintResult(grammars), + IValueConstraint> +#if NET8_0_OR_GREATER + , IAsyncConstraint> +#endif + { +#if NET8_0_OR_GREATER + public async Task IsMetBy(IAsyncEnumerable actual, + CancellationToken cancellationToken) + => await SetAsyncValue(actual, @event => @event.IsNullable()); +#endif + + public ConstraintResult IsMetBy(IEnumerable 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(grammars), + IValueConstraint> +#if NET8_0_OR_GREATER + , IAsyncConstraint> +#endif + { +#if NET8_0_OR_GREATER + public async Task IsMetBy(IAsyncEnumerable actual, + CancellationToken cancellationToken) + => await SetAsyncValue(actual, @event => @event?.IsNullable() == false); +#endif + + public ConstraintResult IsMetBy(IEnumerable 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)); + } + } +} diff --git a/Source/aweXpect.Reflection/ThatType.OnlyHasNonNullableMembers.cs b/Source/aweXpect.Reflection/ThatType.OnlyHasNonNullableMembers.cs index 0db51739..7bd1710c 100644 --- a/Source/aweXpect.Reflection/ThatType.OnlyHasNonNullableMembers.cs +++ b/Source/aweXpect.Reflection/ThatType.OnlyHasNonNullableMembers.cs @@ -12,8 +12,9 @@ namespace aweXpect.Reflection; public static partial class ThatType { /// - /// Verifies that all fields and properties of the are non-nullable, including inherited - /// members or only those declared directly on the type according to the . + /// Verifies that all fields, properties and events of the are non-nullable, including + /// inherited members or only those declared directly on the type according to the + /// . /// /// /// A member is considered nullable if its type is a value type or a diff --git a/Source/aweXpect.Reflection/ThatType.OnlyHasNullableMembers.cs b/Source/aweXpect.Reflection/ThatType.OnlyHasNullableMembers.cs index d7bb91a9..b906af2e 100644 --- a/Source/aweXpect.Reflection/ThatType.OnlyHasNullableMembers.cs +++ b/Source/aweXpect.Reflection/ThatType.OnlyHasNullableMembers.cs @@ -12,7 +12,7 @@ namespace aweXpect.Reflection; public static partial class ThatType { /// - /// Verifies that all fields and properties of the are nullable, including inherited + /// Verifies that all fields, properties and events of the are nullable, including inherited /// members or only those declared directly on the type according to the . /// /// diff --git a/Source/aweXpect.Reflection/ThatTypes.OnlyHaveNonNullableMembers.cs b/Source/aweXpect.Reflection/ThatTypes.OnlyHaveNonNullableMembers.cs index ebfb295c..28973b85 100644 --- a/Source/aweXpect.Reflection/ThatTypes.OnlyHaveNonNullableMembers.cs +++ b/Source/aweXpect.Reflection/ThatTypes.OnlyHaveNonNullableMembers.cs @@ -19,7 +19,7 @@ namespace aweXpect.Reflection; public static partial class ThatTypes { /// - /// Verifies that all fields and properties of all items in the filtered collection of + /// Verifies that all fields, properties and events of all items in the filtered collection of /// are non-nullable, including inherited members or only those declared directly on the type according to /// the . /// @@ -35,7 +35,7 @@ public static partial class ThatTypes #if NET8_0_OR_GREATER /// - /// Verifies that all fields and properties of all items in the filtered collection of + /// Verifies that all fields, properties and events of all items in the filtered collection of /// are non-nullable, including inherited members or only those declared directly on the type according to /// the . /// diff --git a/Source/aweXpect.Reflection/ThatTypes.OnlyHaveNullableMembers.cs b/Source/aweXpect.Reflection/ThatTypes.OnlyHaveNullableMembers.cs index 4996d904..5cacd13b 100644 --- a/Source/aweXpect.Reflection/ThatTypes.OnlyHaveNullableMembers.cs +++ b/Source/aweXpect.Reflection/ThatTypes.OnlyHaveNullableMembers.cs @@ -19,7 +19,7 @@ namespace aweXpect.Reflection; public static partial class ThatTypes { /// - /// Verifies that all fields and properties of all items in the filtered collection of + /// Verifies that all fields, properties and events of all items in the filtered collection of /// are nullable, including inherited members or only those declared directly on the type according to /// the . /// @@ -35,7 +35,7 @@ public static partial class ThatTypes #if NET8_0_OR_GREATER /// - /// Verifies that all fields and properties of all items in the filtered collection of + /// Verifies that all fields, properties and events of all items in the filtered collection of /// are nullable, including inherited members or only those declared directly on the type according to /// the . /// diff --git a/Tests/aweXpect.Reflection.Api.Tests/Expected/aweXpect.Reflection_net10.0.txt b/Tests/aweXpect.Reflection.Api.Tests/Expected/aweXpect.Reflection_net10.0.txt index 8d724a85..ef769d1b 100644 --- a/Tests/aweXpect.Reflection.Api.Tests/Expected/aweXpect.Reflection_net10.0.txt +++ b/Tests/aweXpect.Reflection.Api.Tests/Expected/aweXpect.Reflection_net10.0.txt @@ -221,6 +221,7 @@ namespace aweXpect.Reflection public static aweXpect.Reflection.Collections.Filtered.Events WhichAreNot(this aweXpect.Reflection.Collections.Filtered.Events @this, aweXpect.Reflection.Collections.AccessModifiers accessModifier) { } public static aweXpect.Reflection.Collections.Filtered.Events WhichAreNotAbstract(this aweXpect.Reflection.Collections.Filtered.Events @this) { } public static aweXpect.Reflection.Collections.Filtered.Events WhichAreNotInternal(this aweXpect.Reflection.Collections.Filtered.Events @this) { } + public static aweXpect.Reflection.Collections.Filtered.Events WhichAreNotNullable(this aweXpect.Reflection.Collections.Filtered.Events @this) { } public static aweXpect.Reflection.Collections.Filtered.Events WhichAreNotObsolete(this aweXpect.Reflection.Collections.Filtered.Events @this) { } public static aweXpect.Reflection.Collections.Filtered.Events WhichAreNotPrivate(this aweXpect.Reflection.Collections.Filtered.Events @this) { } public static aweXpect.Reflection.Collections.Filtered.Events WhichAreNotPrivateProtected(this aweXpect.Reflection.Collections.Filtered.Events @this) { } @@ -230,6 +231,7 @@ namespace aweXpect.Reflection public static aweXpect.Reflection.Collections.Filtered.Events WhichAreNotSealed(this aweXpect.Reflection.Collections.Filtered.Events @this) { } public static aweXpect.Reflection.Collections.Filtered.Events WhichAreNotStatic(this aweXpect.Reflection.Collections.Filtered.Events @this) { } public static aweXpect.Reflection.Collections.Filtered.Events WhichAreNotVirtual(this aweXpect.Reflection.Collections.Filtered.Events @this) { } + public static aweXpect.Reflection.Collections.Filtered.Events WhichAreNullable(this aweXpect.Reflection.Collections.Filtered.Events @this) { } public static aweXpect.Reflection.Collections.Filtered.Events WhichAreObsolete(this aweXpect.Reflection.Collections.Filtered.Events @this) { } public static aweXpect.Reflection.Collections.Filtered.Events WhichArePrivate(this aweXpect.Reflection.Collections.Filtered.Events @this) { } public static aweXpect.Reflection.Collections.Filtered.Events WhichArePrivateProtected(this aweXpect.Reflection.Collections.Filtered.Events @this) { } @@ -957,9 +959,11 @@ namespace aweXpect.Reflection where TAttribute : System.Attribute { } public static aweXpect.Results.AndOrResult> IsAbstract(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsNotAbstract(this aweXpect.Core.IThat subject) { } + public static aweXpect.Results.AndOrResult> IsNotNullable(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsNotSealed(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsNotStatic(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsNotVirtual(this aweXpect.Core.IThat subject) { } + public static aweXpect.Results.AndOrResult> IsNullable(this aweXpect.Core.IThat subject) { } public static aweXpect.Reflection.ThatEvent.EventOfTypeResult> IsOfExactType(this aweXpect.Core.IThat subject, System.Type handlerType) { } public static aweXpect.Reflection.ThatEvent.EventOfTypeResult> IsOfExactType(this aweXpect.Core.IThat subject) { } public static aweXpect.Reflection.ThatEvent.EventOfTypeResult> IsOfType(this aweXpect.Core.IThat subject, System.Type handlerType) { } @@ -984,12 +988,16 @@ namespace aweXpect.Reflection public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreAbstract(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotAbstract(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotAbstract(this aweXpect.Core.IThat> subject) { } + public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotNullable(this aweXpect.Core.IThat> subject) { } + public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotNullable(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotSealed(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotSealed(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotStatic(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotStatic(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotVirtual(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotVirtual(this aweXpect.Core.IThat> subject) { } + public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNullable(this aweXpect.Core.IThat> subject) { } + public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNullable(this aweXpect.Core.IThat> subject) { } public static aweXpect.Reflection.ThatEvents.EventsOfTypeResult, aweXpect.Core.IThat>> AreOfExactType(this aweXpect.Core.IThat> subject, System.Type handlerType) { } public static aweXpect.Reflection.ThatEvents.EventsOfTypeResult, aweXpect.Core.IThat>> AreOfExactType(this aweXpect.Core.IThat> subject, System.Type handlerType) { } public static aweXpect.Reflection.ThatEvents.EventsOfTypeResult, aweXpect.Core.IThat>> AreOfExactType(this aweXpect.Core.IThat> subject) { } diff --git a/Tests/aweXpect.Reflection.Api.Tests/Expected/aweXpect.Reflection_net8.0.txt b/Tests/aweXpect.Reflection.Api.Tests/Expected/aweXpect.Reflection_net8.0.txt index 807459c2..05d6cca6 100644 --- a/Tests/aweXpect.Reflection.Api.Tests/Expected/aweXpect.Reflection_net8.0.txt +++ b/Tests/aweXpect.Reflection.Api.Tests/Expected/aweXpect.Reflection_net8.0.txt @@ -221,6 +221,7 @@ namespace aweXpect.Reflection public static aweXpect.Reflection.Collections.Filtered.Events WhichAreNot(this aweXpect.Reflection.Collections.Filtered.Events @this, aweXpect.Reflection.Collections.AccessModifiers accessModifier) { } public static aweXpect.Reflection.Collections.Filtered.Events WhichAreNotAbstract(this aweXpect.Reflection.Collections.Filtered.Events @this) { } public static aweXpect.Reflection.Collections.Filtered.Events WhichAreNotInternal(this aweXpect.Reflection.Collections.Filtered.Events @this) { } + public static aweXpect.Reflection.Collections.Filtered.Events WhichAreNotNullable(this aweXpect.Reflection.Collections.Filtered.Events @this) { } public static aweXpect.Reflection.Collections.Filtered.Events WhichAreNotObsolete(this aweXpect.Reflection.Collections.Filtered.Events @this) { } public static aweXpect.Reflection.Collections.Filtered.Events WhichAreNotPrivate(this aweXpect.Reflection.Collections.Filtered.Events @this) { } public static aweXpect.Reflection.Collections.Filtered.Events WhichAreNotPrivateProtected(this aweXpect.Reflection.Collections.Filtered.Events @this) { } @@ -230,6 +231,7 @@ namespace aweXpect.Reflection public static aweXpect.Reflection.Collections.Filtered.Events WhichAreNotSealed(this aweXpect.Reflection.Collections.Filtered.Events @this) { } public static aweXpect.Reflection.Collections.Filtered.Events WhichAreNotStatic(this aweXpect.Reflection.Collections.Filtered.Events @this) { } public static aweXpect.Reflection.Collections.Filtered.Events WhichAreNotVirtual(this aweXpect.Reflection.Collections.Filtered.Events @this) { } + public static aweXpect.Reflection.Collections.Filtered.Events WhichAreNullable(this aweXpect.Reflection.Collections.Filtered.Events @this) { } public static aweXpect.Reflection.Collections.Filtered.Events WhichAreObsolete(this aweXpect.Reflection.Collections.Filtered.Events @this) { } public static aweXpect.Reflection.Collections.Filtered.Events WhichArePrivate(this aweXpect.Reflection.Collections.Filtered.Events @this) { } public static aweXpect.Reflection.Collections.Filtered.Events WhichArePrivateProtected(this aweXpect.Reflection.Collections.Filtered.Events @this) { } @@ -957,9 +959,11 @@ namespace aweXpect.Reflection where TAttribute : System.Attribute { } public static aweXpect.Results.AndOrResult> IsAbstract(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsNotAbstract(this aweXpect.Core.IThat subject) { } + public static aweXpect.Results.AndOrResult> IsNotNullable(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsNotSealed(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsNotStatic(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsNotVirtual(this aweXpect.Core.IThat subject) { } + public static aweXpect.Results.AndOrResult> IsNullable(this aweXpect.Core.IThat subject) { } public static aweXpect.Reflection.ThatEvent.EventOfTypeResult> IsOfExactType(this aweXpect.Core.IThat subject, System.Type handlerType) { } public static aweXpect.Reflection.ThatEvent.EventOfTypeResult> IsOfExactType(this aweXpect.Core.IThat subject) { } public static aweXpect.Reflection.ThatEvent.EventOfTypeResult> IsOfType(this aweXpect.Core.IThat subject, System.Type handlerType) { } @@ -984,12 +988,16 @@ namespace aweXpect.Reflection public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreAbstract(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotAbstract(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotAbstract(this aweXpect.Core.IThat> subject) { } + public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotNullable(this aweXpect.Core.IThat> subject) { } + public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotNullable(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotSealed(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotSealed(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotStatic(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotStatic(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotVirtual(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotVirtual(this aweXpect.Core.IThat> subject) { } + public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNullable(this aweXpect.Core.IThat> subject) { } + public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNullable(this aweXpect.Core.IThat> subject) { } public static aweXpect.Reflection.ThatEvents.EventsOfTypeResult, aweXpect.Core.IThat>> AreOfExactType(this aweXpect.Core.IThat> subject, System.Type handlerType) { } public static aweXpect.Reflection.ThatEvents.EventsOfTypeResult, aweXpect.Core.IThat>> AreOfExactType(this aweXpect.Core.IThat> subject, System.Type handlerType) { } public static aweXpect.Reflection.ThatEvents.EventsOfTypeResult, aweXpect.Core.IThat>> AreOfExactType(this aweXpect.Core.IThat> subject) { } diff --git a/Tests/aweXpect.Reflection.Api.Tests/Expected/aweXpect.Reflection_netstandard2.0.txt b/Tests/aweXpect.Reflection.Api.Tests/Expected/aweXpect.Reflection_netstandard2.0.txt index 35f1fe56..fa38caae 100644 --- a/Tests/aweXpect.Reflection.Api.Tests/Expected/aweXpect.Reflection_netstandard2.0.txt +++ b/Tests/aweXpect.Reflection.Api.Tests/Expected/aweXpect.Reflection_netstandard2.0.txt @@ -221,6 +221,7 @@ namespace aweXpect.Reflection public static aweXpect.Reflection.Collections.Filtered.Events WhichAreNot(this aweXpect.Reflection.Collections.Filtered.Events @this, aweXpect.Reflection.Collections.AccessModifiers accessModifier) { } public static aweXpect.Reflection.Collections.Filtered.Events WhichAreNotAbstract(this aweXpect.Reflection.Collections.Filtered.Events @this) { } public static aweXpect.Reflection.Collections.Filtered.Events WhichAreNotInternal(this aweXpect.Reflection.Collections.Filtered.Events @this) { } + public static aweXpect.Reflection.Collections.Filtered.Events WhichAreNotNullable(this aweXpect.Reflection.Collections.Filtered.Events @this) { } public static aweXpect.Reflection.Collections.Filtered.Events WhichAreNotObsolete(this aweXpect.Reflection.Collections.Filtered.Events @this) { } public static aweXpect.Reflection.Collections.Filtered.Events WhichAreNotPrivate(this aweXpect.Reflection.Collections.Filtered.Events @this) { } public static aweXpect.Reflection.Collections.Filtered.Events WhichAreNotPrivateProtected(this aweXpect.Reflection.Collections.Filtered.Events @this) { } @@ -230,6 +231,7 @@ namespace aweXpect.Reflection public static aweXpect.Reflection.Collections.Filtered.Events WhichAreNotSealed(this aweXpect.Reflection.Collections.Filtered.Events @this) { } public static aweXpect.Reflection.Collections.Filtered.Events WhichAreNotStatic(this aweXpect.Reflection.Collections.Filtered.Events @this) { } public static aweXpect.Reflection.Collections.Filtered.Events WhichAreNotVirtual(this aweXpect.Reflection.Collections.Filtered.Events @this) { } + public static aweXpect.Reflection.Collections.Filtered.Events WhichAreNullable(this aweXpect.Reflection.Collections.Filtered.Events @this) { } public static aweXpect.Reflection.Collections.Filtered.Events WhichAreObsolete(this aweXpect.Reflection.Collections.Filtered.Events @this) { } public static aweXpect.Reflection.Collections.Filtered.Events WhichArePrivate(this aweXpect.Reflection.Collections.Filtered.Events @this) { } public static aweXpect.Reflection.Collections.Filtered.Events WhichArePrivateProtected(this aweXpect.Reflection.Collections.Filtered.Events @this) { } @@ -871,9 +873,11 @@ namespace aweXpect.Reflection where TAttribute : System.Attribute { } public static aweXpect.Results.AndOrResult> IsAbstract(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsNotAbstract(this aweXpect.Core.IThat subject) { } + public static aweXpect.Results.AndOrResult> IsNotNullable(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsNotSealed(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsNotStatic(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsNotVirtual(this aweXpect.Core.IThat subject) { } + public static aweXpect.Results.AndOrResult> IsNullable(this aweXpect.Core.IThat subject) { } public static aweXpect.Reflection.ThatEvent.EventOfTypeResult> IsOfExactType(this aweXpect.Core.IThat subject, System.Type handlerType) { } public static aweXpect.Reflection.ThatEvent.EventOfTypeResult> IsOfExactType(this aweXpect.Core.IThat subject) { } public static aweXpect.Reflection.ThatEvent.EventOfTypeResult> IsOfType(this aweXpect.Core.IThat subject, System.Type handlerType) { } @@ -896,9 +900,11 @@ namespace aweXpect.Reflection { public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreAbstract(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotAbstract(this aweXpect.Core.IThat> subject) { } + public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotNullable(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotSealed(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotStatic(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotVirtual(this aweXpect.Core.IThat> subject) { } + public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNullable(this aweXpect.Core.IThat> subject) { } public static aweXpect.Reflection.ThatEvents.EventsOfTypeResult, aweXpect.Core.IThat>> AreOfExactType(this aweXpect.Core.IThat> subject, System.Type handlerType) { } public static aweXpect.Reflection.ThatEvents.EventsOfTypeResult, aweXpect.Core.IThat>> AreOfExactType(this aweXpect.Core.IThat> subject) { } public static aweXpect.Reflection.ThatEvents.EventsOfTypeResult, aweXpect.Core.IThat>> AreOfType(this aweXpect.Core.IThat> subject, System.Type handlerType) { } diff --git a/Tests/aweXpect.Reflection.Internal.Tests/Helpers/NullabilityHelpersTests.cs b/Tests/aweXpect.Reflection.Internal.Tests/Helpers/NullabilityHelpersTests.cs index bb68015f..446ab551 100644 --- a/Tests/aweXpect.Reflection.Internal.Tests/Helpers/NullabilityHelpersTests.cs +++ b/Tests/aweXpect.Reflection.Internal.Tests/Helpers/NullabilityHelpersTests.cs @@ -9,7 +9,7 @@ namespace aweXpect.Reflection.Internal.Tests.Helpers; public sealed class NullabilityHelpersTests { [Fact] - public async Task GetNotNullableMembers_ShouldReturnNonNullableFieldsAndProperties() + public async Task GetNotNullableMembers_ShouldReturnNonNullableFieldsPropertiesAndEvents() { MemberInfo[] notNullableMembers = typeof(NullabilityTestClass).GetNotNullableMembers(); @@ -20,11 +20,13 @@ await That(notNullableMembers.Select(member => member.Name)).IsEqualTo([ nameof(NullabilityTestClass.NonNullableValueProperty), nameof(NullabilityTestClass.NonNullableReferenceProperty), nameof(NullabilityTestClass.NonNullableGenericProperty), + nameof(NullabilityTestClass.NonNullableEvent), + nameof(NullabilityTestClass.NonNullableGenericEvent), ]).InAnyOrder(); } [Fact] - public async Task GetNullableMembers_ShouldReturnNullableFieldsAndProperties() + public async Task GetNullableMembers_ShouldReturnNullableFieldsPropertiesAndEvents() { MemberInfo[] nullableMembers = typeof(NullabilityTestClass).GetNullableMembers(); @@ -36,6 +38,8 @@ await That(nullableMembers.Select(member => member.Name)).IsEqualTo([ nameof(NullabilityTestClass.NullableReferenceProperty), nameof(NullabilityTestClass.NullableGenericProperty), nameof(NullabilityTestClass.NullableWriteOnlyProperty), + nameof(NullabilityTestClass.NullableEvent), + nameof(NullabilityTestClass.NullableGenericEvent), ]).InAnyOrder(); } @@ -53,6 +57,18 @@ public async Task IsNullable_ShouldEvaluateFieldNullability(string fieldName, bo await That(fieldInfo.IsNullable()).IsEqualTo(expectNullable); } + [Theory] + [InlineData(nameof(NullabilityTestClass.NullableEvent), true)] + [InlineData(nameof(NullabilityTestClass.NonNullableEvent), false)] + [InlineData(nameof(NullabilityTestClass.NullableGenericEvent), true)] + [InlineData(nameof(NullabilityTestClass.NonNullableGenericEvent), false)] + public async Task IsNullable_ShouldEvaluateEventNullability(string eventName, bool expectNullable) + { + EventInfo eventInfo = typeof(NullabilityTestClass).GetEvent(eventName)!; + + await That(eventInfo.IsNullable()).IsEqualTo(expectNullable); + } + [Theory] [InlineData(nameof(NullabilityTestClass.NullableValueProperty), true)] [InlineData(nameof(NullabilityTestClass.NonNullableValueProperty), false)] @@ -84,6 +100,14 @@ public async Task IsNullable_WhenMemberDiffersFromContextOfDeclaringType_ShouldE await That(result).IsEqualTo(expectNullable); } + [Fact] + public async Task IsNullable_WhenEventIsOblivious_ShouldReturnFalse() + { + EventInfo eventInfo = typeof(ObliviousTestClass).GetEvent(nameof(ObliviousTestClass.ObliviousEvent))!; + + await That(eventInfo.IsNullable()).IsFalse(); + } + [Theory] [InlineData(nameof(ObliviousTestClass.ObliviousField))] [InlineData(nameof(ObliviousTestClass.ObliviousProperty))] @@ -221,6 +245,25 @@ public async Task IsNullable_WithNullableIndexer_ShouldReturnTrue() await That(propertyInfo.IsNullable()).IsTrue(); } + [Fact] + public async Task IsNullable_WithNonNullableEventInConstructedGenericType_ShouldReturnFalse() + { + // Events cannot be declared as a bare generic type parameter, so resolving the member type + // through the generic type definition always yields the (non generic-parameter) handler type. + EventInfo eventInfo = typeof(GenericTestClass) + .GetEvent(nameof(GenericTestClass.UnannotatedEvent))!; + + await That(eventInfo.IsNullable()).IsFalse(); + } + + [Fact] + public async Task IsNullable_WithNullEventInfo_ShouldReturnFalse() + { + EventInfo? eventInfo = null; + + await That(eventInfo.IsNullable()).IsFalse(); + } + [Fact] public async Task IsNullable_WithNullFieldInfo_ShouldReturnFalse() { @@ -248,6 +291,7 @@ public async Task IsNullable_WithPostConditionAttributes_ShouldIgnoreThemAndRetu } #pragma warning disable CS0649 // Field is never assigned to, and will always have its default value +#pragma warning disable CS0067 // Event is never used public class NullabilityTestClass { public List NonNullableGenericField = []; @@ -256,6 +300,10 @@ public class NullabilityTestClass public List? NullableGenericField; public string? NullableReferenceField; public int? NullableValueField; + public event EventHandler? NullableEvent; + public event EventHandler NonNullableEvent = delegate { }; + public event EventHandler? NullableGenericEvent; + public event EventHandler NonNullableGenericEvent = delegate { }; public int? NullableValueProperty { get; set; } public int NonNullableValueProperty { get; set; } public string? NullableReferenceProperty { get; set; } @@ -297,6 +345,7 @@ public class GenericTestClass { public T? AnnotatedField; public T UnannotatedField = default!; + public event EventHandler UnannotatedEvent = delegate { }; public T UnannotatedProperty { get; set; } = default!; public T? AnnotatedProperty { get; set; } } @@ -322,6 +371,7 @@ public class ObliviousTestClass { public string ObliviousField; public string ObliviousProperty { get; set; } + public event EventHandler ObliviousEvent; public class NestedObliviousTestClass { @@ -329,5 +379,6 @@ public class NestedObliviousTestClass public string NestedObliviousProperty { get; set; } } } +#pragma warning restore CS0067 #pragma warning restore CS0649 } diff --git a/Tests/aweXpect.Reflection.Tests/Filters/EventFilters.WhichAreNotNullable.Tests.cs b/Tests/aweXpect.Reflection.Tests/Filters/EventFilters.WhichAreNotNullable.Tests.cs new file mode 100644 index 00000000..4f6cc699 --- /dev/null +++ b/Tests/aweXpect.Reflection.Tests/Filters/EventFilters.WhichAreNotNullable.Tests.cs @@ -0,0 +1,36 @@ +using aweXpect.Reflection.Collections; +using aweXpect.Reflection.Tests.TestHelpers.Types; + +namespace aweXpect.Reflection.Tests.Filters; + +public sealed partial class EventFilters +{ + public sealed class WhichAreNotNullable + { + public sealed class Tests + { + [Fact] + public async Task ShouldAllowFilteringForNonNullableEvents() + { + Filtered.Events events = In.Type() + .Events().WhichAreNotNullable(); + + await That(events).AreNotNullable().And.IsNotEmpty(); + await That(events.GetDescription()) + .IsEqualTo("non-nullable events in type").AsPrefix(); + } + + [Fact] + public async Task ShouldOnlyKeepNonNullableEvents() + { + Filtered.Events events = In.Type() + .Events().WhichAreNotNullable(); + + await That(events).IsEqualTo([ + typeof(ClassWithMixedNullableEvents) + .GetEvent(nameof(ClassWithMixedNullableEvents.NonNullableEvent))!, + ]).InAnyOrder(); + } + } + } +} diff --git a/Tests/aweXpect.Reflection.Tests/Filters/EventFilters.WhichAreNullable.Tests.cs b/Tests/aweXpect.Reflection.Tests/Filters/EventFilters.WhichAreNullable.Tests.cs new file mode 100644 index 00000000..75a3b29b --- /dev/null +++ b/Tests/aweXpect.Reflection.Tests/Filters/EventFilters.WhichAreNullable.Tests.cs @@ -0,0 +1,36 @@ +using aweXpect.Reflection.Collections; +using aweXpect.Reflection.Tests.TestHelpers.Types; + +namespace aweXpect.Reflection.Tests.Filters; + +public sealed partial class EventFilters +{ + public sealed class WhichAreNullable + { + public sealed class Tests + { + [Fact] + public async Task ShouldAllowFilteringForNullableEvents() + { + Filtered.Events events = In.Type() + .Events().WhichAreNullable(); + + await That(events).AreNullable().And.IsNotEmpty(); + await That(events.GetDescription()) + .IsEqualTo("nullable events in type").AsPrefix(); + } + + [Fact] + public async Task ShouldOnlyKeepNullableEvents() + { + Filtered.Events events = In.Type() + .Events().WhichAreNullable(); + + await That(events).IsEqualTo([ + typeof(ClassWithMixedNullableEvents) + .GetEvent(nameof(ClassWithMixedNullableEvents.NullableEvent))!, + ]).InAnyOrder(); + } + } + } +} diff --git a/Tests/aweXpect.Reflection.Tests/TestHelpers/Types/TypesWithNullableMembers.cs b/Tests/aweXpect.Reflection.Tests/TestHelpers/Types/TypesWithNullableMembers.cs index 09b8c54d..db2847a5 100644 --- a/Tests/aweXpect.Reflection.Tests/TestHelpers/Types/TypesWithNullableMembers.cs +++ b/Tests/aweXpect.Reflection.Tests/TestHelpers/Types/TypesWithNullableMembers.cs @@ -3,6 +3,7 @@ namespace aweXpect.Reflection.Tests.TestHelpers.Types; #pragma warning disable CS0649 // Field is never assigned to, and will always have its default value +#pragma warning disable CS0067 // Event is never used public class ClassWithNullableMembers { public string? NullableField; @@ -71,10 +72,85 @@ public class DerivedClassWithNullableMembers : ClassWithNonNullableMembers public class ClassWithoutMembers; +public class ClassWithNullableEvents +{ + public event EventHandler? NullableEvent; + public event EventHandler? NullableGenericEvent; +} + +public class ClassWithNonNullableEvents +{ + public event EventHandler NonNullableEvent = delegate { }; + public event EventHandler NonNullableGenericEvent = delegate { }; +} + +public class ClassWithMixedNullableEvents +{ + public event EventHandler? NullableEvent; + public event EventHandler NonNullableEvent = delegate { }; +} + +public class ClassWithSingleNullableEvent +{ + public event EventHandler? NullableEvent; +} + +public class ClassWithSingleNonNullableEvent +{ + public event EventHandler NonNullableEvent = delegate { }; +} + +public class DerivedClassWithNullableEvent : ClassWithSingleNonNullableEvent +{ + public event EventHandler? DeclaredNullableEvent; +} + +public class DerivedClassWithInheritedNullableEvent : ClassWithSingleNullableEvent +{ + public event EventHandler? DeclaredNullableEvent; +} + +public class BaseClassWithPrivateNonNullableEvent +{ + // ReSharper disable once UnusedMember.Local + private event EventHandler PrivateNonNullableEvent = delegate { }; +} + +public class DerivedClassWithPrivateNonNullableBaseEvent : BaseClassWithPrivateNonNullableEvent +{ + public event EventHandler? DeclaredNullableEvent; +} + +// ReSharper disable ValueParameterNotUsed +public class ClassWithCustomNullableEvent +{ + public event EventHandler? NullableCustomEvent + { + add { } + remove { } + } +} + +public class ClassWithCustomNonNullableEvent +{ + public event EventHandler NonNullableCustomEvent + { + add { } + remove { } + } +} +// ReSharper restore ValueParameterNotUsed + #nullable disable public class ClassWithObliviousMembers { public string ObliviousField; public string ObliviousProperty { get; set; } } + +public class ClassWithObliviousEvents +{ + public event EventHandler ObliviousEvent; +} +#pragma warning restore CS0067 #pragma warning restore CS0649 diff --git a/Tests/aweXpect.Reflection.Tests/ThatEvent.IsNotNullable.Tests.cs b/Tests/aweXpect.Reflection.Tests/ThatEvent.IsNotNullable.Tests.cs new file mode 100644 index 00000000..849155e8 --- /dev/null +++ b/Tests/aweXpect.Reflection.Tests/ThatEvent.IsNotNullable.Tests.cs @@ -0,0 +1,180 @@ +using System.Reflection; +using aweXpect.Reflection.Tests.TestHelpers.Types; + +namespace aweXpect.Reflection.Tests; + +public sealed partial class ThatEvent +{ + public sealed class IsNotNullable + { + public sealed class Tests + { + [Fact] + public async Task WhenEventIsNonNullable_ShouldSucceed() + { + EventInfo subject = typeof(ClassWithNonNullableEvents) + .GetEvent(nameof(ClassWithNonNullableEvents.NonNullableEvent))!; + + async Task Act() + { + await That(subject).IsNotNullable(); + } + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenEventIsNonNullableGenericType_ShouldSucceed() + { + EventInfo subject = typeof(ClassWithNonNullableEvents) + .GetEvent(nameof(ClassWithNonNullableEvents.NonNullableGenericEvent))!; + + async Task Act() + { + await That(subject).IsNotNullable(); + } + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenEventIsNonNullableWithCustomAccessors_ShouldSucceed() + { + EventInfo subject = typeof(ClassWithCustomNonNullableEvent) + .GetEvent(nameof(ClassWithCustomNonNullableEvent.NonNullableCustomEvent))!; + + async Task Act() + { + await That(subject).IsNotNullable(); + } + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenEventIsNull_ShouldFail() + { + EventInfo? subject = null; + + async Task Act() + { + await That(subject).IsNotNullable(); + } + + await That(Act).ThrowsException() + .WithMessage(""" + Expected that subject + is not nullable, + but it was + """); + } + + [Fact] + public async Task WhenEventIsNullable_ShouldFail() + { + EventInfo subject = typeof(ClassWithNullableEvents) + .GetEvent(nameof(ClassWithNullableEvents.NullableEvent))!; + + async Task Act() + { + await That(subject).IsNotNullable(); + } + + await That(Act).ThrowsException() + .WithMessage($""" + Expected that subject + is not nullable, + but it was nullable {Formatter.Format(subject)} + """); + } + + [Fact] + public async Task WhenEventIsNullableGenericType_ShouldFail() + { + EventInfo subject = typeof(ClassWithNullableEvents) + .GetEvent(nameof(ClassWithNullableEvents.NullableGenericEvent))!; + + async Task Act() + { + await That(subject).IsNotNullable(); + } + + await That(Act).ThrowsException() + .WithMessage($""" + Expected that subject + is not nullable, + but it was nullable {Formatter.Format(subject)} + """); + } + + [Fact] + public async Task WhenEventIsNullableWithCustomAccessors_ShouldFail() + { + EventInfo subject = typeof(ClassWithCustomNullableEvent) + .GetEvent(nameof(ClassWithCustomNullableEvent.NullableCustomEvent))!; + + async Task Act() + { + await That(subject).IsNotNullable(); + } + + await That(Act).ThrowsException() + .WithMessage($""" + Expected that subject + is not nullable, + but it was nullable {Formatter.Format(subject)} + """); + } + + [Fact] + public async Task WhenEventIsOblivious_ShouldSucceed() + { + EventInfo subject = typeof(ClassWithObliviousEvents) + .GetEvent(nameof(ClassWithObliviousEvents.ObliviousEvent))!; + + async Task Act() + { + await That(subject).IsNotNullable(); + } + + await That(Act).DoesNotThrow(); + } + } + + public sealed class NegatedTests + { + [Fact] + public async Task WhenEventIsNotNullable_ShouldFail() + { + EventInfo subject = typeof(ClassWithNonNullableEvents) + .GetEvent(nameof(ClassWithNonNullableEvents.NonNullableEvent))!; + + async Task Act() + { + await That(subject).DoesNotComplyWith(it => it.IsNotNullable()); + } + + await That(Act).ThrowsException() + .WithMessage($""" + Expected that subject + is nullable, + but it was non-nullable {Formatter.Format(subject)} + """); + } + + [Fact] + public async Task WhenEventIsNullable_ShouldSucceed() + { + EventInfo subject = typeof(ClassWithNullableEvents) + .GetEvent(nameof(ClassWithNullableEvents.NullableEvent))!; + + async Task Act() + { + await That(subject).DoesNotComplyWith(it => it.IsNotNullable()); + } + + await That(Act).DoesNotThrow(); + } + } + } +} diff --git a/Tests/aweXpect.Reflection.Tests/ThatEvent.IsNullable.Tests.cs b/Tests/aweXpect.Reflection.Tests/ThatEvent.IsNullable.Tests.cs new file mode 100644 index 00000000..fa019bb5 --- /dev/null +++ b/Tests/aweXpect.Reflection.Tests/ThatEvent.IsNullable.Tests.cs @@ -0,0 +1,218 @@ +using System.Reflection; +using aweXpect.Reflection.Tests.TestHelpers.Types; + +namespace aweXpect.Reflection.Tests; + +public sealed partial class ThatEvent +{ + public sealed class IsNullable + { + public sealed class Tests + { + [Fact] + public async Task WhenEventIsNonNullable_ShouldFail() + { + EventInfo subject = typeof(ClassWithNonNullableEvents) + .GetEvent(nameof(ClassWithNonNullableEvents.NonNullableEvent))!; + + async Task Act() + { + await That(subject).IsNullable(); + } + + await That(Act).ThrowsException() + .WithMessage($""" + Expected that subject + is nullable, + but it was non-nullable {Formatter.Format(subject)} + """); + } + + [Fact] + public async Task WhenEventIsNonNullableGenericType_ShouldFail() + { + EventInfo subject = typeof(ClassWithNonNullableEvents) + .GetEvent(nameof(ClassWithNonNullableEvents.NonNullableGenericEvent))!; + + async Task Act() + { + await That(subject).IsNullable(); + } + + await That(Act).ThrowsException() + .WithMessage($""" + Expected that subject + is nullable, + but it was non-nullable {Formatter.Format(subject)} + """); + } + + [Fact] + public async Task WhenEventIsNonNullableInMixedClass_ShouldFail() + { + EventInfo subject = typeof(ClassWithMixedNullableEvents) + .GetEvent(nameof(ClassWithMixedNullableEvents.NonNullableEvent))!; + + async Task Act() + { + await That(subject).IsNullable(); + } + + await That(Act).ThrowsException() + .WithMessage($""" + Expected that subject + is nullable, + but it was non-nullable {Formatter.Format(subject)} + """); + } + + [Fact] + public async Task WhenEventIsNonNullableWithCustomAccessors_ShouldFail() + { + EventInfo subject = typeof(ClassWithCustomNonNullableEvent) + .GetEvent(nameof(ClassWithCustomNonNullableEvent.NonNullableCustomEvent))!; + + async Task Act() + { + await That(subject).IsNullable(); + } + + await That(Act).ThrowsException() + .WithMessage($""" + Expected that subject + is nullable, + but it was non-nullable {Formatter.Format(subject)} + """); + } + + [Fact] + public async Task WhenEventIsNull_ShouldFail() + { + EventInfo? subject = null; + + async Task Act() + { + await That(subject).IsNullable(); + } + + await That(Act).ThrowsException() + .WithMessage(""" + Expected that subject + is nullable, + but it was + """); + } + + [Fact] + public async Task WhenEventIsNullable_ShouldSucceed() + { + EventInfo subject = typeof(ClassWithNullableEvents) + .GetEvent(nameof(ClassWithNullableEvents.NullableEvent))!; + + async Task Act() + { + await That(subject).IsNullable(); + } + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenEventIsNullableGenericType_ShouldSucceed() + { + EventInfo subject = typeof(ClassWithNullableEvents) + .GetEvent(nameof(ClassWithNullableEvents.NullableGenericEvent))!; + + async Task Act() + { + await That(subject).IsNullable(); + } + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenEventIsNullableInMixedClass_ShouldSucceed() + { + EventInfo subject = typeof(ClassWithMixedNullableEvents) + .GetEvent(nameof(ClassWithMixedNullableEvents.NullableEvent))!; + + async Task Act() + { + await That(subject).IsNullable(); + } + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenEventIsNullableWithCustomAccessors_ShouldSucceed() + { + EventInfo subject = typeof(ClassWithCustomNullableEvent) + .GetEvent(nameof(ClassWithCustomNullableEvent.NullableCustomEvent))!; + + async Task Act() + { + await That(subject).IsNullable(); + } + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenEventIsOblivious_ShouldFail() + { + EventInfo subject = typeof(ClassWithObliviousEvents) + .GetEvent(nameof(ClassWithObliviousEvents.ObliviousEvent))!; + + async Task Act() + { + await That(subject).IsNullable(); + } + + await That(Act).ThrowsException() + .WithMessage($""" + Expected that subject + is nullable, + but it was non-nullable {Formatter.Format(subject)} + """); + } + } + + public sealed class NegatedTests + { + [Fact] + public async Task WhenEventIsNotNullable_ShouldSucceed() + { + EventInfo subject = typeof(ClassWithNonNullableEvents) + .GetEvent(nameof(ClassWithNonNullableEvents.NonNullableEvent))!; + + async Task Act() + { + await That(subject).DoesNotComplyWith(it => it.IsNullable()); + } + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenEventIsNullable_ShouldFail() + { + EventInfo subject = typeof(ClassWithNullableEvents) + .GetEvent(nameof(ClassWithNullableEvents.NullableEvent))!; + + async Task Act() + { + await That(subject).DoesNotComplyWith(it => it.IsNullable()); + } + + await That(Act).ThrowsException() + .WithMessage($""" + Expected that subject + is not nullable, + but it was nullable {Formatter.Format(subject)} + """); + } + } + } +} diff --git a/Tests/aweXpect.Reflection.Tests/ThatEvents.AreNotNullable.Tests.cs b/Tests/aweXpect.Reflection.Tests/ThatEvents.AreNotNullable.Tests.cs new file mode 100644 index 00000000..08775134 --- /dev/null +++ b/Tests/aweXpect.Reflection.Tests/ThatEvents.AreNotNullable.Tests.cs @@ -0,0 +1,152 @@ +using System.Collections.Generic; +using System.Reflection; +using aweXpect.Reflection.Tests.TestHelpers.Types; +#if NET8_0_OR_GREATER +using aweXpect.Reflection.Tests.TestHelpers; +#endif + +namespace aweXpect.Reflection.Tests; + +public sealed partial class ThatEvents +{ + public sealed class AreNotNullable + { + public sealed class Tests + { + [Fact] + public async Task WhenAllEventsAreNotNullable_ShouldSucceed() + { + IEnumerable subject = typeof(ClassWithNonNullableEvents) + .GetEvents(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly); + + async Task Act() + { + await That(subject).AreNotNullable(); + } + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenEventsContainNull_ShouldFail() + { + IEnumerable subject = [null,]; + + async Task Act() + { + await That(subject).AreNotNullable(); + } + + await That(Act).ThrowsException() + .WithMessage(""" + Expected that subject + are all not nullable, + but it contained nullable events [ + + ] + """); + } + + [Fact] + public async Task WhenEventsContainNullableEvents_ShouldFail() + { + IEnumerable subject = typeof(ClassWithMixedNullableEvents) + .GetEvents(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly); + + async Task Act() + { + await That(subject).AreNotNullable(); + } + + await That(Act).ThrowsException() + .WithMessage(""" + Expected that subject + are all not nullable, + but it contained nullable events [ + * + ] + """).AsWildcard(); + } + } + + public sealed class NegatedTests + { + [Fact] + public async Task WhenAllEventsAreNotNullable_ShouldFail() + { + IEnumerable subject = typeof(ClassWithNonNullableEvents) + .GetEvents(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly); + + async Task Act() + { + await That(subject).DoesNotComplyWith(they => they.AreNotNullable()); + } + + await That(Act).ThrowsException() + .WithMessage(""" + Expected that subject + also contain a nullable event, + but it only contained non-nullable events [ + * + ] + """).AsWildcard(); + } + + [Fact] + public async Task WhenEventsContainNullableEvents_ShouldSucceed() + { + IEnumerable subject = typeof(ClassWithMixedNullableEvents) + .GetEvents(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly); + + async Task Act() + { + await That(subject).DoesNotComplyWith(they => they.AreNotNullable()); + } + + await That(Act).DoesNotThrow(); + } + } + +#if NET8_0_OR_GREATER + public sealed class AsyncEnumerableTests + { + [Fact] + public async Task WhenAllEventsAreNotNullable_ShouldSucceed() + { + IAsyncEnumerable subject = typeof(ClassWithNonNullableEvents) + .GetEvents(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly) + .ToTestAsyncEnumerable(); + + async Task Act() + { + await That(subject).AreNotNullable(); + } + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenEventsContainNullableEvents_ShouldFail() + { + IAsyncEnumerable subject = typeof(ClassWithMixedNullableEvents) + .GetEvents(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly) + .ToTestAsyncEnumerable(); + + async Task Act() + { + await That(subject).AreNotNullable(); + } + + await That(Act).ThrowsException() + .WithMessage(""" + Expected that subject + are all not nullable, + but it contained nullable events [ + * + ] + """).AsWildcard(); + } + } +#endif + } +} diff --git a/Tests/aweXpect.Reflection.Tests/ThatEvents.AreNullable.Tests.cs b/Tests/aweXpect.Reflection.Tests/ThatEvents.AreNullable.Tests.cs new file mode 100644 index 00000000..ea326891 --- /dev/null +++ b/Tests/aweXpect.Reflection.Tests/ThatEvents.AreNullable.Tests.cs @@ -0,0 +1,132 @@ +using System.Collections.Generic; +using System.Reflection; +using aweXpect.Reflection.Tests.TestHelpers.Types; +#if NET8_0_OR_GREATER +using aweXpect.Reflection.Tests.TestHelpers; +#endif + +namespace aweXpect.Reflection.Tests; + +public sealed partial class ThatEvents +{ + public sealed class AreNullable + { + public sealed class Tests + { + [Fact] + public async Task WhenAllEventsAreNullable_ShouldSucceed() + { + IEnumerable subject = typeof(ClassWithNullableEvents) + .GetEvents(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly); + + async Task Act() + { + await That(subject).AreNullable(); + } + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenEventsContainNonNullableEvents_ShouldFail() + { + IEnumerable subject = typeof(ClassWithMixedNullableEvents) + .GetEvents(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly); + + async Task Act() + { + await That(subject).AreNullable(); + } + + await That(Act).ThrowsException() + .WithMessage(""" + Expected that subject + are all nullable, + but it contained non-nullable events [ + * + ] + """).AsWildcard(); + } + } + + public sealed class NegatedTests + { + [Fact] + public async Task WhenAllEventsAreNullable_ShouldFail() + { + IEnumerable subject = typeof(ClassWithNullableEvents) + .GetEvents(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly); + + async Task Act() + { + await That(subject).DoesNotComplyWith(they => they.AreNullable()); + } + + await That(Act).ThrowsException() + .WithMessage(""" + Expected that subject + are not all nullable, + but it only contained nullable events [ + * + ] + """).AsWildcard(); + } + + [Fact] + public async Task WhenEventsContainNonNullableEvents_ShouldSucceed() + { + IEnumerable subject = typeof(ClassWithMixedNullableEvents) + .GetEvents(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly); + + async Task Act() + { + await That(subject).DoesNotComplyWith(they => they.AreNullable()); + } + + await That(Act).DoesNotThrow(); + } + } + +#if NET8_0_OR_GREATER + public sealed class AsyncEnumerableTests + { + [Fact] + public async Task WhenAllEventsAreNullable_ShouldSucceed() + { + IAsyncEnumerable subject = typeof(ClassWithNullableEvents) + .GetEvents(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly) + .ToTestAsyncEnumerable(); + + async Task Act() + { + await That(subject).AreNullable(); + } + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenEventsContainNonNullableEvents_ShouldFail() + { + IAsyncEnumerable subject = typeof(ClassWithMixedNullableEvents) + .GetEvents(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly) + .ToTestAsyncEnumerable(); + + async Task Act() + { + await That(subject).AreNullable(); + } + + await That(Act).ThrowsException() + .WithMessage(""" + Expected that subject + are all nullable, + but it contained non-nullable events [ + * + ] + """).AsWildcard(); + } + } +#endif + } +} diff --git a/Tests/aweXpect.Reflection.Tests/ThatType.OnlyHasNonNullableMembers.Tests.cs b/Tests/aweXpect.Reflection.Tests/ThatType.OnlyHasNonNullableMembers.Tests.cs index 0591219b..dea83eaf 100644 --- a/Tests/aweXpect.Reflection.Tests/ThatType.OnlyHasNonNullableMembers.Tests.cs +++ b/Tests/aweXpect.Reflection.Tests/ThatType.OnlyHasNonNullableMembers.Tests.cs @@ -64,6 +64,28 @@ but it contained nullable members [ """); } + [Fact] + public async Task WhenTypeHasNullableEvent_ShouldFail() + { + Type subject = typeof(ClassWithSingleNullableEvent); + EventInfo @event = subject + .GetEvent(nameof(ClassWithSingleNullableEvent.NullableEvent))!; + + async Task Act() + { + await That(subject).OnlyHasNonNullableMembers(); + } + + await That(Act).ThrowsException() + .WithMessage($""" + Expected that subject + only has non-nullable members, + but it contained nullable members [ + {Formatter.Format(@event)} + ] + """); + } + [Fact] public async Task WhenTypeIsNull_ShouldFail() { @@ -82,6 +104,19 @@ but it was """); } + [Fact] + public async Task WhenTypeOnlyHasNonNullableEvent_ShouldSucceed() + { + Type subject = typeof(ClassWithSingleNonNullableEvent); + + async Task Act() + { + await That(subject).OnlyHasNonNullableMembers(); + } + + await That(Act).DoesNotThrow(); + } + [Fact] public async Task WhenTypeOnlyHasNonNullableMembers_ShouldSucceed() { diff --git a/Tests/aweXpect.Reflection.Tests/ThatType.OnlyHasNullableMembers.Tests.cs b/Tests/aweXpect.Reflection.Tests/ThatType.OnlyHasNullableMembers.Tests.cs index 5c728847..8aef3517 100644 --- a/Tests/aweXpect.Reflection.Tests/ThatType.OnlyHasNullableMembers.Tests.cs +++ b/Tests/aweXpect.Reflection.Tests/ThatType.OnlyHasNullableMembers.Tests.cs @@ -10,6 +10,19 @@ public sealed class OnlyHasNullableMembers { public sealed class Tests { + [Fact] + public async Task WhenBaseTypeHasNonNullableEvent_ShouldSucceed() + { + Type subject = typeof(DerivedClassWithNullableEvent); + + async Task Act() + { + await That(subject).OnlyHasNullableMembers(); + } + + await That(Act).DoesNotThrow(); + } + [Fact] public async Task WhenBaseTypeHasNonNullableMembers_ShouldSucceed() { @@ -78,6 +91,28 @@ but it contained non-nullable members [ """); } + [Fact] + public async Task WhenTypeHasNonNullableEvent_ShouldFail() + { + Type subject = typeof(ClassWithSingleNonNullableEvent); + EventInfo @event = subject + .GetEvent(nameof(ClassWithSingleNonNullableEvent.NonNullableEvent))!; + + async Task Act() + { + await That(subject).OnlyHasNullableMembers(); + } + + await That(Act).ThrowsException() + .WithMessage($""" + Expected that subject + only has nullable members, + but it contained non-nullable members [ + {Formatter.Format(@event)} + ] + """); + } + [Fact] public async Task WhenTypeIsNull_ShouldFail() { @@ -96,6 +131,19 @@ but it was """); } + [Fact] + public async Task WhenTypeOnlyHasNullableEvent_ShouldSucceed() + { + Type subject = typeof(ClassWithSingleNullableEvent); + + async Task Act() + { + await That(subject).OnlyHasNullableMembers(); + } + + await That(Act).DoesNotThrow(); + } + [Fact] public async Task WhenTypeOnlyHasNullableMembers_ShouldSucceed() { @@ -109,6 +157,28 @@ async Task Act() await That(Act).DoesNotThrow(); } + [Fact] + public async Task WithIncludingInherited_WhenBaseTypeHasNonNullableEvent_ShouldFail() + { + Type subject = typeof(DerivedClassWithNullableEvent); + EventInfo @event = subject + .GetEvent(nameof(ClassWithSingleNonNullableEvent.NonNullableEvent))!; + + async Task Act() + { + await That(subject).OnlyHasNullableMembers(MemberScope.IncludingInherited); + } + + await That(Act).ThrowsException() + .WithMessage($""" + Expected that subject + only has nullable members, + but it contained non-nullable members [ + {Formatter.Format(@event)} + ] + """); + } + [Fact] public async Task WithIncludingInherited_WhenBaseTypeHasNonNullableMembers_ShouldFail() { @@ -128,6 +198,41 @@ but it contained non-nullable members [ ] """).AsWildcard(); } + + [Fact] + public async Task WithIncludingInherited_WhenBaseTypeHasNullableEvent_ShouldSucceed() + { + Type subject = typeof(DerivedClassWithInheritedNullableEvent); + + async Task Act() + { + await That(subject).OnlyHasNullableMembers(MemberScope.IncludingInherited); + } + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WithIncludingInherited_WhenBaseTypeHasPrivateNonNullableEvent_ShouldFail() + { + Type subject = typeof(DerivedClassWithPrivateNonNullableBaseEvent); + EventInfo @event = typeof(BaseClassWithPrivateNonNullableEvent) + .GetEvent("PrivateNonNullableEvent", BindingFlags.NonPublic | BindingFlags.Instance)!; + + async Task Act() + { + await That(subject).OnlyHasNullableMembers(MemberScope.IncludingInherited); + } + + await That(Act).ThrowsException() + .WithMessage($""" + Expected that subject + only has nullable members, + but it contained non-nullable members [ + {Formatter.Format(@event)} + ] + """); + } } public sealed class NegatedTests