diff --git a/README.md b/README.md index d79eada7..5b6991a8 100644 --- a/README.md +++ b/README.md @@ -292,6 +292,8 @@ outside the namespace. | instantiable | `.WhichAreInstantiable()` | `.IsInstantiable()` | `.AreInstantiable()` | | immutable | `.WhichAreImmutable()` | `.IsImmutable()` | `.AreImmutable()` | | default constructor | `.WhichHaveADefaultConstructor()` | `.HasADefaultConstructor()` | `.HaveADefaultConstructor()` | +| only nullable members | `.WhichOnlyHaveNullableMembers()` | `.OnlyHasNullableMembers()` | `.OnlyHaveNullableMembers()` | +| only non-nullable members | `.WhichOnlyHaveNonNullableMembers()` | `.OnlyHasNonNullableMembers()` | `.OnlyHaveNonNullableMembers()` | | custom predicate | `.Which(t => …)` | `.Satisfies(t => …)` | `.All().Satisfy(t => …)` | `WhichInheritFrom` / `InheritsFrom` consider only the **base-class chain** (not implemented interfaces) and @@ -325,6 +327,16 @@ A type is *immutable* when all instance fields (including inherited ones) are `r properties (including inherited ones) have no setter or an `init`-only setter. Static members do not affect 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: + +```csharp +await Expect.That(In.AssemblyContaining() + .Types().WithName("Request").AsSuffix()) + .OnlyHaveNullableMembers(); +``` + > **Negation:** every kind/modifier row above has a negated form. Most use `WhichAreNot…` on filters and > `IsNot…` / `AreNot…` on assertions (e.g. `WhichAreNotSealed()`, `IsNotAClass()`, `AreNotStatic()`, > `IsNotInstantiable()`). The *default constructor* row uses `WhichDoNotHaveADefaultConstructor()`, @@ -526,6 +538,7 @@ In addition to [access modifiers](#access-modifiers), | of type (or a subtype) | `.OfType()` | `.IsOfType()` | `.AreOfType()` | | of exact type | `.OfExactType()` | `.IsOfExactType()` | `.AreOfExactType()` | | static *(properties & fields)* | `.WhichAreStatic()` | `.IsStatic()` | `.AreStatic()` | +| nullable *(properties & fields)* | `.WhichAreNullable()` | `.IsNullable()` | `.AreNullable()` | | abstract / sealed *(properties only)* | `.WhichAreAbstract()` / `.WhichAreSealed()` | `.IsAbstract()` / `.IsSealed()` | `.AreAbstract()` / `.AreSealed()` | | virtual *(properties only)* | `.WhichAreVirtual()` | `.IsVirtual()` | `.AreVirtual()` | | override *(properties only)* | `.WhichOverride()` | `.Overrides()` | `.Override()` | @@ -543,10 +556,27 @@ In addition to [access modifiers](#access-modifiers), | read-only *(fields only)* | `.WhichAreReadOnly()` | `.IsReadOnly()` | `.AreReadOnly()` | | constant *(fields only)* | `.WhichAreConstant()` | `.IsConstant()` | `.AreConstant()` | -> **Negation:** the `static`, `abstract`, `sealed`, `virtual`, `required`, `indexer`, `extension property`, -> `read-only` *(fields)* and `constant` rows have a negated form: `WhichAreNot…` on filters and `IsNot…` / -> `AreNot…` on assertions (e.g. `WhichAreNotConstant()`, `IsNotConstant()`, `AreNotConstant()`); `override` uses -> `WhichDoNotOverride()` / `DoesNotOverride()` / `DoNotOverride()`. +> **Negation:** the `static`, `nullable`, `abstract`, `sealed`, `virtual`, `required`, `indexer`, +> `extension property`, `read-only` *(fields)* and `constant` rows have a negated form: `WhichAreNot…` on filters +> and `IsNot…` / `AreNot…` on assertions (e.g. `WhichAreNotConstant()`, `IsNotConstant()`, `AreNotConstant()`); +> `override` uses `WhichDoNotOverride()` / `DoesNotOverride()` / `DoNotOverride()`. + +#### Nullability + +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 +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. + +```csharp +// All properties and fields of the request types must be nullable +await Expect.That(In.AssemblyContaining() + .Types().WithName("Request").AsSuffix() + .Properties()) + .AreNullable(); +``` `WhichAreExtensionProperties()`, `IsAnExtensionProperty()` and `AreExtensionProperties()` match extension properties declared with the C# extension block syntax (`extension(...) { … }`), both instance and static. The real properties diff --git a/Source/aweXpect.Reflection/Filters/FieldFilters.WhichAreNullable.cs b/Source/aweXpect.Reflection/Filters/FieldFilters.WhichAreNullable.cs new file mode 100644 index 00000000..73076639 --- /dev/null +++ b/Source/aweXpect.Reflection/Filters/FieldFilters.WhichAreNullable.cs @@ -0,0 +1,24 @@ +using System.Reflection; +using aweXpect.Reflection.Collections; +using aweXpect.Reflection.Helpers; + +namespace aweXpect.Reflection; + +public static partial class FieldFilters +{ + /// + /// Filters for fields that are nullable. + /// + public static Filtered.Fields WhichAreNullable(this Filtered.Fields @this) + => @this.Which(Filter.Prefix( + field => field.IsNullable(), + "nullable ")); + + /// + /// Filters for fields that are not nullable. + /// + public static Filtered.Fields WhichAreNotNullable(this Filtered.Fields @this) + => @this.Which(Filter.Prefix( + field => !field.IsNullable(), + "non-nullable ")); +} diff --git a/Source/aweXpect.Reflection/Filters/PropertyFilters.WhichAreNullable.cs b/Source/aweXpect.Reflection/Filters/PropertyFilters.WhichAreNullable.cs new file mode 100644 index 00000000..91ada16b --- /dev/null +++ b/Source/aweXpect.Reflection/Filters/PropertyFilters.WhichAreNullable.cs @@ -0,0 +1,24 @@ +using System.Reflection; +using aweXpect.Reflection.Collections; +using aweXpect.Reflection.Helpers; + +namespace aweXpect.Reflection; + +public static partial class PropertyFilters +{ + /// + /// Filters for properties that are nullable. + /// + public static Filtered.Properties WhichAreNullable(this Filtered.Properties @this) + => @this.Which(Filter.Prefix( + property => property.IsNullable(), + "nullable ")); + + /// + /// Filters for properties that are not nullable. + /// + public static Filtered.Properties WhichAreNotNullable(this Filtered.Properties @this) + => @this.Which(Filter.Prefix( + property => !property.IsNullable(), + "non-nullable ")); +} diff --git a/Source/aweXpect.Reflection/Filters/TypeFilters.WhichOnlyHaveNullableMembers.cs b/Source/aweXpect.Reflection/Filters/TypeFilters.WhichOnlyHaveNullableMembers.cs new file mode 100644 index 00000000..b996237f --- /dev/null +++ b/Source/aweXpect.Reflection/Filters/TypeFilters.WhichOnlyHaveNullableMembers.cs @@ -0,0 +1,28 @@ +using System; +using aweXpect.Reflection.Collections; +using aweXpect.Reflection.Helpers; + +namespace aweXpect.Reflection; + +public static partial class TypeFilters +{ + /// + /// Filters for types whose fields and properties 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, + MemberScope memberScope = MemberScope.DeclaredOnly) + => @this.Which(Filter.Suffix( + type => type.GetNotNullableMembers(memberScope).Length == 0, + "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 . + /// + public static Filtered.Types WhichOnlyHaveNonNullableMembers(this Filtered.Types @this, + MemberScope memberScope = MemberScope.DeclaredOnly) + => @this.Which(Filter.Suffix( + type => type.GetNullableMembers(memberScope).Length == 0, + "which only have non-nullable members ")); +} diff --git a/Source/aweXpect.Reflection/Helpers/MemberViolationRenderer.cs b/Source/aweXpect.Reflection/Helpers/MemberViolationRenderer.cs new file mode 100644 index 00000000..36a0e935 --- /dev/null +++ b/Source/aweXpect.Reflection/Helpers/MemberViolationRenderer.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text; +using aweXpect.Core; + +namespace aweXpect.Reflection.Helpers; + +/// +/// Renders the grouped failure output of the nullability member constraints: one indented line per failing +/// type, each followed by its list of violating members. +/// +/// +/// Shared between the nullable and the non-nullable member constraints, so that the formatting (indentation, +/// comma placement, null handling) cannot drift between the two. +/// +internal static class MemberViolationRenderer +{ + /// + /// Appends {it}{header}[, one indented line per type in (appending + /// {memberHeader}[…] when has an entry for it), and a closing ]. + /// + /// + /// A type has no violations to list; it fails because it cannot satisfy the + /// expectation, so it is rendered without a (contradictory empty) violation list. + /// + public static void AppendTypesWithViolatingMembers( + StringBuilder stringBuilder, + string it, + string header, + IReadOnlyList types, + IReadOnlyDictionary violations, + string memberHeader, + string? indentation) + { + string itemIndentation = (indentation ?? string.Empty) + " "; + stringBuilder.Append(it).Append(header).Append('['); + for (int index = 0; index < types.Count; index++) + { + Type? type = types[index]; + stringBuilder.Append(Environment.NewLine).Append(itemIndentation) + .Append(Formatter.Format(type)); + + if (type is not null && violations.TryGetValue(type, out MemberInfo[]? members)) + { + stringBuilder.Append(memberHeader).Append(Formatter.Format(members)); + } + + if (index < types.Count - 1) + { + stringBuilder.Append(','); + } + } + + stringBuilder.Append(Environment.NewLine).Append(indentation).Append(']'); + } +} diff --git a/Source/aweXpect.Reflection/Helpers/NullabilityHelpers.cs b/Source/aweXpect.Reflection/Helpers/NullabilityHelpers.cs new file mode 100644 index 00000000..eede8bad --- /dev/null +++ b/Source/aweXpect.Reflection/Helpers/NullabilityHelpers.cs @@ -0,0 +1,324 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using aweXpect.Reflection.Collections; + +namespace aweXpect.Reflection.Helpers; + +/// +/// Helper methods to determine the nullability of members. +/// +/// +/// A member is considered nullable if its type is a value type or a reference +/// type whose declared annotation is nullable (e.g. string?). The declared annotation is decoded +/// from the NullableAttribute / NullableContextAttribute metadata on all target frameworks, +/// so the behavior cannot diverge between them: members without annotations (oblivious code) and +/// unconstrained generic type parameters count as non-nullable, and post-condition attributes like +/// [AllowNull] or [MaybeNull] are ignored. +/// +internal static class NullabilityHelpers +{ + private const byte Annotated = 2; + private const byte NotAnnotated = 1; + private const string NullableAttributeName = "System.Runtime.CompilerServices.NullableAttribute"; + private const string NullableContextAttributeName = "System.Runtime.CompilerServices.NullableContextAttribute"; + + /// + /// Checks if the is nullable. + /// + /// + /// A property is considered nullable if its type is a value type or a + /// reference type annotated as nullable (according to the nullable reference type metadata). + /// + public static bool IsNullable(this PropertyInfo? propertyInfo) + { + if (propertyInfo is null) + { + return false; + } + + if (propertyInfo.PropertyType.IsValueType) + { + return Nullable.GetUnderlyingType(propertyInfo.PropertyType) != null; + } + + return IsNullableReferenceType(propertyInfo); + } + + /// + /// Checks if the is nullable. + /// + /// + /// A field is considered nullable if its type is a value type or a + /// reference type annotated as nullable (according to the nullable reference type metadata). + /// + public static bool IsNullable(this FieldInfo? fieldInfo) + { + if (fieldInfo is null) + { + return false; + } + + if (fieldInfo.FieldType.IsValueType) + { + return Nullable.GetUnderlyingType(fieldInfo.FieldType) != null; + } + + return IsNullableReferenceType(fieldInfo); + } + + /// + /// Returns the nullable fields and properties of the , including inherited + /// members or only those declared directly on the type according to the . + /// + public static MemberInfo[] GetNullableMembers(this Type type, + MemberScope memberScope = MemberScope.DeclaredOnly) + => type.GetMembersByNullability(memberScope).Nullable; + + /// + /// Returns the non-nullable fields and properties of the , including inherited + /// members or only those declared directly on the type according to the . + /// + public static MemberInfo[] GetNotNullableMembers(this Type type, + MemberScope memberScope = MemberScope.DeclaredOnly) + => type.GetMembersByNullability(memberScope).NotNullable; + + /// + /// Partitions the fields and properties 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 . + /// + public static (MemberInfo[] Nullable, MemberInfo[] NotNullable) GetMembersByNullability(this Type type, + MemberScope memberScope = MemberScope.DeclaredOnly) + { + List nullable = []; + List notNullable = []; + foreach (FieldInfo field in type.GetDeclaredFields(memberScope)) + { + (field.IsNullable() ? nullable : notNullable).Add(field); + } + + foreach (PropertyInfo property in type.GetDeclaredProperties(memberScope)) + { + (property.IsNullable() ? nullable : notNullable).Add(property); + } + + return (nullable.ToArray(), notNullable.ToArray()); + } + + /// + /// Determines the nullability of a reference type member from the nullable reference type metadata. + /// + /// + /// The compiler stores the annotation in a NullableAttribute on the member (a scalar + /// or a array whose first element describes the top-level type) + /// and omits it when the value equals the context stored in a NullableContextAttribute on one of + /// the declaring types. A flag value of 2 means "annotated" (nullable). + /// + /// Members declared as a bare generic type parameter (flag value 1) are additionally resolved + /// through the constructed declaring type, so that e.g. a member of type T accessed via a type + /// deriving from Base<string?> counts as nullable. + /// + /// The metadata is decoded directly (instead of using NullabilityInfoContext, which is unavailable + /// on netstandard2.0) so that the same member yields the same result on every target framework. + /// + private static bool IsNullableReferenceType(MemberInfo memberInfo) + { + byte flag = GetNullableFlag(memberInfo) + ?? GetNullableContextFlag(memberInfo.DeclaringType) + ?? 0; + if (flag == Annotated) + { + return true; + } + + return flag == NotAnnotated && IsNullableViaGenericArgument(memberInfo); + } + + /// + /// Returns the nullability flag of the NullableAttribute on the , + /// or if the member has no such attribute. + /// + private static byte? GetNullableFlag(MemberInfo memberInfo) + { + foreach (CustomAttributeData attributeData in memberInfo.CustomAttributes) + { + if (attributeData.AttributeType.FullName == NullableAttributeName && + attributeData.ConstructorArguments.Count == 1) + { + CustomAttributeTypedArgument argument = attributeData.ConstructorArguments[0]; + if (argument.Value is byte flag) + { + return flag; + } + + if (argument.Value is IReadOnlyList { Count: > 0, } flags && + flags[0].Value is byte firstFlag) + { + return firstFlag; + } + + return 0; + } + } + + return null; + } + + /// + /// Returns the nullability flag of the first NullableContextAttribute found on the + /// or one of its declaring types, or if none is found. + /// + private static byte? GetNullableContextFlag(Type? type) + { + for (Type? declaringType = type; declaringType is not null; declaringType = declaringType.DeclaringType) + { + foreach (CustomAttributeData attributeData in declaringType.CustomAttributes) + { + if (attributeData.AttributeType.FullName == NullableContextAttributeName && + attributeData.ConstructorArguments.Count == 1 && + attributeData.ConstructorArguments[0].Value is byte contextFlag) + { + return contextFlag; + } + } + } + + return null; + } + + /// + /// Checks if a member declared as a bare generic type parameter (e.g. T) is nullable because + /// the type argument of the constructed declaring type is annotated as nullable (e.g. when accessed + /// via a type deriving from Base<string?>). + /// + /// + /// The annotation of the type arguments is stored in a NullableAttribute on the derived type + /// whose base type reference instantiates the generic type definition: index 0 describes the base + /// type itself, followed by its flattened generic arguments. + /// + private static bool IsNullableViaGenericArgument(MemberInfo memberInfo) + { + if (memberInfo.DeclaringType is not { IsGenericType: true, IsGenericTypeDefinition: false, } declaringType) + { + return false; + } + + Type genericTypeDefinition = declaringType.GetGenericTypeDefinition(); + if (GetDeclaredMemberType(memberInfo, genericTypeDefinition) is not + { IsGenericParameter: true, } genericParameter) + { + return false; + } + + for (Type? derivedType = memberInfo.ReflectedType; derivedType is not null; derivedType = derivedType.BaseType) + { + Type? baseType = derivedType.BaseType; + if (baseType is not { IsGenericType: true, } || baseType.GetGenericTypeDefinition() != genericTypeDefinition) + { + continue; + } + + Type genericArgument = baseType.GetGenericArguments()[genericParameter.GenericParameterPosition]; + if (genericArgument.IsGenericParameter || genericArgument.IsValueType) + { + return false; + } + + return IsNullableBaseTypeArgument(derivedType, baseType, genericParameter.GenericParameterPosition); + } + + return false; + } + + /// + /// Returns the type of the member on the generic type definition that corresponds to the + /// of a constructed generic type. + /// + private static Type? GetDeclaredMemberType(MemberInfo memberInfo, Type genericTypeDefinition) + { + foreach (MemberInfo member in genericTypeDefinition.GetMember(memberInfo.Name, memberInfo.MemberType, + BindingFlags.DeclaredOnly | + BindingFlags.NonPublic | + BindingFlags.Public | + BindingFlags.Instance | + BindingFlags.Static)) + { + if (member.MetadataToken == memberInfo.MetadataToken) + { + return member switch + { + FieldInfo fieldInfo => fieldInfo.FieldType, + PropertyInfo propertyInfo => propertyInfo.PropertyType, + _ => null, + }; + } + } + + return null; + } + + /// + /// Checks if the generic argument at the of the + /// reference is annotated as nullable in the metadata of the . + /// + private static bool IsNullableBaseTypeArgument(Type derivedType, Type baseType, int position) + { + foreach (CustomAttributeData attributeData in derivedType.CustomAttributes) + { + if (attributeData.AttributeType.FullName == NullableAttributeName && + attributeData.ConstructorArguments.Count == 1) + { + CustomAttributeTypedArgument argument = attributeData.ConstructorArguments[0]; + if (argument.Value is byte flag) + { + return flag == Annotated; + } + + if (argument.Value is IReadOnlyList flags) + { + int index = 1; + Type[] genericArguments = baseType.GetGenericArguments(); + for (int i = 0; i < position; i++) + { + index += CountNullabilityStates(genericArguments[i]); + } + + return index < flags.Count && flags[index].Value is byte argumentFlag && argumentFlag == Annotated; + } + + return false; + } + } + + return GetNullableContextFlag(derivedType) == Annotated; + } + + /// + /// Counts the number of nullability flags the occupies in the flattened + /// NullableAttribute encoding: reference types and generic value types occupy one flag plus + /// the flags of their generic arguments, arrays one flag plus the flags of their element type, and + /// non-generic value types none. + /// + private static int CountNullabilityStates(Type type) + { + Type underlyingType = Nullable.GetUnderlyingType(type) ?? type; + if (underlyingType.IsGenericType) + { + int count = 1; + foreach (Type genericArgument in underlyingType.GetGenericArguments()) + { + count += CountNullabilityStates(genericArgument); + } + + return count; + } + + if (underlyingType.HasElementType) + { + return (underlyingType.IsArray ? 1 : 0) + CountNullabilityStates(underlyingType.GetElementType()!); + } + + return type.IsValueType ? 0 : 1; + } +} diff --git a/Source/aweXpect.Reflection/ThatField.IsNullable.cs b/Source/aweXpect.Reflection/ThatField.IsNullable.cs new file mode 100644 index 00000000..c000b51f --- /dev/null +++ b/Source/aweXpect.Reflection/ThatField.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 ThatField +{ + /// + /// Verifies that the is nullable. + /// + /// + /// A field is considered nullable if its type is a value type or a + /// reference type 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. + /// + /// + /// A field is considered nullable if its type is a value type or a + /// reference type annotated as nullable (according to the nullable reference type metadata). + /// Fields 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(FieldInfo? 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/ThatFields.AreNullable.cs b/Source/aweXpect.Reflection/ThatFields.AreNullable.cs new file mode 100644 index 00000000..adb06aa2 --- /dev/null +++ b/Source/aweXpect.Reflection/ThatFields.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 ThatFields +{ + /// + /// 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, field => field.IsNullable()); +#endif + + public ConstraintResult IsMetBy(IEnumerable actual) + => SetValue(actual, field => field.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 fields "); + 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 fields "); + 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, field => field?.IsNullable() == false); +#endif + + public ConstraintResult IsMetBy(IEnumerable actual) + => SetValue(actual, field => field?.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 fields "); + Formatter.Format(stringBuilder, NotMatching, FormattingOptions.Indented(indentation)); + } + + protected override void AppendNegatedExpectation(StringBuilder stringBuilder, string? indentation = null) + => stringBuilder.Append("also contain a nullable field"); + + protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null) + { + stringBuilder.Append(it).Append(" only contained non-nullable fields "); + Formatter.Format(stringBuilder, Matching, FormattingOptions.Indented(indentation)); + } + } +} diff --git a/Source/aweXpect.Reflection/ThatProperties.AreNullable.cs b/Source/aweXpect.Reflection/ThatProperties.AreNullable.cs new file mode 100644 index 00000000..8cc55dbd --- /dev/null +++ b/Source/aweXpect.Reflection/ThatProperties.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 ThatProperties +{ + /// + /// 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, property => property.IsNullable()); +#endif + + public ConstraintResult IsMetBy(IEnumerable actual) + => SetValue(actual, property => property.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 properties "); + 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 properties "); + 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, property => property?.IsNullable() == false); +#endif + + public ConstraintResult IsMetBy(IEnumerable actual) + => SetValue(actual, property => property?.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 properties "); + Formatter.Format(stringBuilder, NotMatching, FormattingOptions.Indented(indentation)); + } + + protected override void AppendNegatedExpectation(StringBuilder stringBuilder, string? indentation = null) + => stringBuilder.Append("also contain a nullable property"); + + protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null) + { + stringBuilder.Append(it).Append(" only contained non-nullable properties "); + Formatter.Format(stringBuilder, Matching, FormattingOptions.Indented(indentation)); + } + } +} diff --git a/Source/aweXpect.Reflection/ThatProperty.IsNullable.cs b/Source/aweXpect.Reflection/ThatProperty.IsNullable.cs new file mode 100644 index 00000000..751c66de --- /dev/null +++ b/Source/aweXpect.Reflection/ThatProperty.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 ThatProperty +{ + /// + /// Verifies that the is nullable. + /// + /// + /// A property is considered nullable if its type is a value type or a + /// reference type 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. + /// + /// + /// A property is considered nullable if its type is a value type or a + /// reference type annotated as nullable (according to the nullable reference type metadata). + /// Properties 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(PropertyInfo? 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/ThatType.OnlyHasNonNullableMembers.cs b/Source/aweXpect.Reflection/ThatType.OnlyHasNonNullableMembers.cs new file mode 100644 index 00000000..0db51739 --- /dev/null +++ b/Source/aweXpect.Reflection/ThatType.OnlyHasNonNullableMembers.cs @@ -0,0 +1,68 @@ +using System; +using System.Reflection; +using System.Text; +using aweXpect.Core; +using aweXpect.Core.Constraints; +using aweXpect.Reflection.Collections; +using aweXpect.Reflection.Helpers; +using aweXpect.Results; + +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 . + /// + /// + /// A member is considered nullable if its type is a value type or a + /// reference type annotated as nullable (according to the nullable reference type metadata). + /// + public static AndOrResult> OnlyHasNonNullableMembers( + this IThat subject, MemberScope memberScope = MemberScope.DeclaredOnly) + => new(subject.Get().ExpectationBuilder.AddConstraint((it, grammars) + => new OnlyHasNonNullableMembersConstraint(it, grammars, memberScope)), + subject); + + private sealed class OnlyHasNonNullableMembersConstraint( + string it, + ExpectationGrammars grammars, + MemberScope memberScope) + : ConstraintResult.WithNotNullValue(it, grammars), + IValueConstraint + { + private MemberInfo[] _notNullableMembers = []; + private MemberInfo[] _nullableMembers = []; + + public ConstraintResult IsMetBy(Type? actual) + { + Actual = actual; + if (actual is not null) + { + (_nullableMembers, _notNullableMembers) = actual.GetMembersByNullability(memberScope); + } + + Outcome = actual is not null && _nullableMembers.Length == 0 ? Outcome.Success : Outcome.Failure; + return this; + } + + protected override void AppendNormalExpectation(StringBuilder stringBuilder, string? indentation = null) + => stringBuilder.Append("only has non-nullable members"); + + protected override void AppendNormalResult(StringBuilder stringBuilder, string? indentation = null) + { + stringBuilder.Append(It).Append(" contained nullable members "); + Formatter.Format(stringBuilder, _nullableMembers, FormattingOptions.Indented(indentation)); + } + + protected override void AppendNegatedExpectation(StringBuilder stringBuilder, string? indentation = null) + => stringBuilder.Append("does not only have non-nullable members"); + + protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null) + { + stringBuilder.Append(It).Append(" only contained non-nullable members "); + Formatter.Format(stringBuilder, _notNullableMembers, FormattingOptions.Indented(indentation)); + } + } +} diff --git a/Source/aweXpect.Reflection/ThatType.OnlyHasNullableMembers.cs b/Source/aweXpect.Reflection/ThatType.OnlyHasNullableMembers.cs new file mode 100644 index 00000000..d7bb91a9 --- /dev/null +++ b/Source/aweXpect.Reflection/ThatType.OnlyHasNullableMembers.cs @@ -0,0 +1,68 @@ +using System; +using System.Reflection; +using System.Text; +using aweXpect.Core; +using aweXpect.Core.Constraints; +using aweXpect.Reflection.Collections; +using aweXpect.Reflection.Helpers; +using aweXpect.Results; + +namespace aweXpect.Reflection; + +public static partial class ThatType +{ + /// + /// Verifies that all fields and properties of the are 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 + /// reference type annotated as nullable (according to the nullable reference type metadata). + /// + public static AndOrResult> OnlyHasNullableMembers( + this IThat subject, MemberScope memberScope = MemberScope.DeclaredOnly) + => new(subject.Get().ExpectationBuilder.AddConstraint((it, grammars) + => new OnlyHasNullableMembersConstraint(it, grammars, memberScope)), + subject); + + private sealed class OnlyHasNullableMembersConstraint( + string it, + ExpectationGrammars grammars, + MemberScope memberScope) + : ConstraintResult.WithNotNullValue(it, grammars), + IValueConstraint + { + private MemberInfo[] _notNullableMembers = []; + private MemberInfo[] _nullableMembers = []; + + public ConstraintResult IsMetBy(Type? actual) + { + Actual = actual; + if (actual is not null) + { + (_nullableMembers, _notNullableMembers) = actual.GetMembersByNullability(memberScope); + } + + Outcome = actual is not null && _notNullableMembers.Length == 0 ? Outcome.Success : Outcome.Failure; + return this; + } + + protected override void AppendNormalExpectation(StringBuilder stringBuilder, string? indentation = null) + => stringBuilder.Append("only has nullable members"); + + protected override void AppendNormalResult(StringBuilder stringBuilder, string? indentation = null) + { + stringBuilder.Append(It).Append(" contained non-nullable members "); + Formatter.Format(stringBuilder, _notNullableMembers, FormattingOptions.Indented(indentation)); + } + + protected override void AppendNegatedExpectation(StringBuilder stringBuilder, string? indentation = null) + => stringBuilder.Append("does not only have nullable members"); + + protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null) + { + stringBuilder.Append(It).Append(" only contained nullable members "); + Formatter.Format(stringBuilder, _nullableMembers, FormattingOptions.Indented(indentation)); + } + } +} diff --git a/Source/aweXpect.Reflection/ThatTypes.OnlyHaveNonNullableMembers.cs b/Source/aweXpect.Reflection/ThatTypes.OnlyHaveNonNullableMembers.cs new file mode 100644 index 00000000..ebfb295c --- /dev/null +++ b/Source/aweXpect.Reflection/ThatTypes.OnlyHaveNonNullableMembers.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text; +using aweXpect.Core; +using aweXpect.Core.Constraints; +using aweXpect.Reflection.Collections; +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 ThatTypes +{ + /// + /// Verifies that all fields and properties 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 . + /// + /// + /// A member is considered nullable if its type is a value type or a + /// reference type annotated as nullable (according to the nullable reference type metadata). + /// + public static AndOrResult, IThat>> OnlyHaveNonNullableMembers( + this IThat> subject, MemberScope memberScope = MemberScope.DeclaredOnly) + => new(subject.Get().ExpectationBuilder.AddConstraint>((it, grammars) + => new OnlyHaveNonNullableMembersConstraint(it, grammars, memberScope)), + subject); + +#if NET8_0_OR_GREATER + /// + /// Verifies that all fields and properties 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 . + /// + /// + /// A member is considered nullable if its type is a value type or a + /// reference type annotated as nullable (according to the nullable reference type metadata). + /// + public static AndOrResult, IThat>> OnlyHaveNonNullableMembers( + this IThat> subject, MemberScope memberScope = MemberScope.DeclaredOnly) + => new(subject.Get().ExpectationBuilder.AddConstraint>((it, grammars) + => new OnlyHaveNonNullableMembersConstraint(it, grammars, memberScope)), + subject); +#endif + + private sealed class OnlyHaveNonNullableMembersConstraint( + string it, + ExpectationGrammars grammars, + MemberScope memberScope) + : CollectionConstraintResult(grammars), + IValueConstraint> +#if NET8_0_OR_GREATER + , IAsyncConstraint> +#endif + { + private readonly Dictionary _nullableMembers = new(); + + private bool OnlyHasNonNullableMembers(Type? type) + { + if (type is null) + { + return false; + } + + MemberInfo[] nullableMembers = type.GetNullableMembers(memberScope); + if (nullableMembers.Length > 0) + { + _nullableMembers[type] = nullableMembers; + } + + return nullableMembers.Length == 0; + } + +#if NET8_0_OR_GREATER + public async Task IsMetBy(IAsyncEnumerable actual, + CancellationToken cancellationToken) + => await SetAsyncValue(actual, OnlyHasNonNullableMembers); +#endif + + public ConstraintResult IsMetBy(IEnumerable actual) + => SetValue(actual, OnlyHasNonNullableMembers); + + protected override void AppendNormalExpectation(StringBuilder stringBuilder, string? indentation = null) + => stringBuilder.Append("only have non-nullable members"); + + protected override void AppendNormalResult(StringBuilder stringBuilder, string? indentation = null) + => MemberViolationRenderer.AppendTypesWithViolatingMembers(stringBuilder, it, + " contained types with nullable members ", NotMatching, _nullableMembers, + " with nullable members ", indentation); + + protected override void AppendNegatedExpectation(StringBuilder stringBuilder, string? indentation = null) + => stringBuilder.Append("not all only have non-nullable members"); + + protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null) + { + stringBuilder.Append(it).Append(" only contained types with only non-nullable members "); + Formatter.Format(stringBuilder, Matching, FormattingOptions.Indented(indentation)); + } + } +} diff --git a/Source/aweXpect.Reflection/ThatTypes.OnlyHaveNullableMembers.cs b/Source/aweXpect.Reflection/ThatTypes.OnlyHaveNullableMembers.cs new file mode 100644 index 00000000..4996d904 --- /dev/null +++ b/Source/aweXpect.Reflection/ThatTypes.OnlyHaveNullableMembers.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text; +using aweXpect.Core; +using aweXpect.Core.Constraints; +using aweXpect.Reflection.Collections; +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 ThatTypes +{ + /// + /// Verifies that all fields and properties of all items in the filtered collection of + /// are 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 + /// reference type annotated as nullable (according to the nullable reference type metadata). + /// + public static AndOrResult, IThat>> OnlyHaveNullableMembers( + this IThat> subject, MemberScope memberScope = MemberScope.DeclaredOnly) + => new(subject.Get().ExpectationBuilder.AddConstraint>((it, grammars) + => new OnlyHaveNullableMembersConstraint(it, grammars, memberScope)), + subject); + +#if NET8_0_OR_GREATER + /// + /// Verifies that all fields and properties of all items in the filtered collection of + /// are 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 + /// reference type annotated as nullable (according to the nullable reference type metadata). + /// + public static AndOrResult, IThat>> OnlyHaveNullableMembers( + this IThat> subject, MemberScope memberScope = MemberScope.DeclaredOnly) + => new(subject.Get().ExpectationBuilder.AddConstraint>((it, grammars) + => new OnlyHaveNullableMembersConstraint(it, grammars, memberScope)), + subject); +#endif + + private sealed class OnlyHaveNullableMembersConstraint( + string it, + ExpectationGrammars grammars, + MemberScope memberScope) + : CollectionConstraintResult(grammars), + IValueConstraint> +#if NET8_0_OR_GREATER + , IAsyncConstraint> +#endif + { + private readonly Dictionary _notNullableMembers = new(); + + private bool OnlyHasNullableMembers(Type? type) + { + if (type is null) + { + return false; + } + + MemberInfo[] notNullableMembers = type.GetNotNullableMembers(memberScope); + if (notNullableMembers.Length > 0) + { + _notNullableMembers[type] = notNullableMembers; + } + + return notNullableMembers.Length == 0; + } + +#if NET8_0_OR_GREATER + public async Task IsMetBy(IAsyncEnumerable actual, + CancellationToken cancellationToken) + => await SetAsyncValue(actual, OnlyHasNullableMembers); +#endif + + public ConstraintResult IsMetBy(IEnumerable actual) + => SetValue(actual, OnlyHasNullableMembers); + + protected override void AppendNormalExpectation(StringBuilder stringBuilder, string? indentation = null) + => stringBuilder.Append("only have nullable members"); + + protected override void AppendNormalResult(StringBuilder stringBuilder, string? indentation = null) + => MemberViolationRenderer.AppendTypesWithViolatingMembers(stringBuilder, it, + " contained types with non-nullable members ", NotMatching, _notNullableMembers, + " with non-nullable members ", indentation); + + protected override void AppendNegatedExpectation(StringBuilder stringBuilder, string? indentation = null) + => stringBuilder.Append("not all only have nullable members"); + + protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null) + { + stringBuilder.Append(it).Append(" only contained types with only nullable members "); + Formatter.Format(stringBuilder, Matching, FormattingOptions.Indented(indentation)); + } + } +} 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 a47147d4..9c3006e3 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 @@ -276,6 +276,7 @@ namespace aweXpect.Reflection public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreNot(this aweXpect.Reflection.Collections.Filtered.Fields @this, aweXpect.Reflection.Collections.AccessModifiers accessModifier) { } public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreNotConstant(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreNotInternal(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } + public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreNotNullable(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreNotObsolete(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreNotPrivate(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreNotPrivateProtected(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } @@ -284,6 +285,7 @@ namespace aweXpect.Reflection public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreNotPublic(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreNotReadOnly(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreNotStatic(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } + public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreNullable(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreObsolete(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } public static aweXpect.Reflection.Collections.Filtered.Fields WhichArePrivate(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } public static aweXpect.Reflection.Collections.Filtered.Fields WhichArePrivateProtected(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } @@ -578,6 +580,7 @@ namespace aweXpect.Reflection public static aweXpect.Reflection.Collections.Filtered.Properties WhichAreNotExtensionProperties(this aweXpect.Reflection.Collections.Filtered.Properties @this) { } public static aweXpect.Reflection.Collections.Filtered.Properties WhichAreNotIndexers(this aweXpect.Reflection.Collections.Filtered.Properties @this) { } public static aweXpect.Reflection.Collections.Filtered.Properties WhichAreNotInternal(this aweXpect.Reflection.Collections.Filtered.Properties @this) { } + public static aweXpect.Reflection.Collections.Filtered.Properties WhichAreNotNullable(this aweXpect.Reflection.Collections.Filtered.Properties @this) { } public static aweXpect.Reflection.Collections.Filtered.Properties WhichAreNotObsolete(this aweXpect.Reflection.Collections.Filtered.Properties @this) { } public static aweXpect.Reflection.Collections.Filtered.Properties WhichAreNotPrivate(this aweXpect.Reflection.Collections.Filtered.Properties @this) { } public static aweXpect.Reflection.Collections.Filtered.Properties WhichAreNotPrivateProtected(this aweXpect.Reflection.Collections.Filtered.Properties @this) { } @@ -588,6 +591,7 @@ namespace aweXpect.Reflection public static aweXpect.Reflection.Collections.Filtered.Properties WhichAreNotSealed(this aweXpect.Reflection.Collections.Filtered.Properties @this) { } public static aweXpect.Reflection.Collections.Filtered.Properties WhichAreNotStatic(this aweXpect.Reflection.Collections.Filtered.Properties @this) { } public static aweXpect.Reflection.Collections.Filtered.Properties WhichAreNotVirtual(this aweXpect.Reflection.Collections.Filtered.Properties @this) { } + public static aweXpect.Reflection.Collections.Filtered.Properties WhichAreNullable(this aweXpect.Reflection.Collections.Filtered.Properties @this) { } public static aweXpect.Reflection.Collections.Filtered.Properties WhichAreObsolete(this aweXpect.Reflection.Collections.Filtered.Properties @this) { } public static aweXpect.Reflection.Collections.Filtered.Properties WhichArePrivate(this aweXpect.Reflection.Collections.Filtered.Properties @this) { } public static aweXpect.Reflection.Collections.Filtered.Properties WhichArePrivateProtected(this aweXpect.Reflection.Collections.Filtered.Properties @this) { } @@ -1018,8 +1022,10 @@ namespace aweXpect.Reflection where TAttribute : System.Attribute { } public static aweXpect.Results.AndOrResult> IsConstant(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsNotConstant(this aweXpect.Core.IThat subject) { } + public static aweXpect.Results.AndOrResult> IsNotNullable(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsNotReadOnly(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsNotStatic(this aweXpect.Core.IThat subject) { } + public static aweXpect.Results.AndOrResult> IsNullable(this aweXpect.Core.IThat subject) { } public static aweXpect.Reflection.ThatField.FieldOfTypeResult> IsOfExactType(this aweXpect.Core.IThat subject, System.Type fieldType) { } public static aweXpect.Reflection.ThatField.FieldOfTypeResult> IsOfExactType(this aweXpect.Core.IThat subject) { } public static aweXpect.Reflection.ThatField.FieldOfTypeResult> IsOfType(this aweXpect.Core.IThat subject, System.Type fieldType) { } @@ -1042,10 +1048,14 @@ namespace aweXpect.Reflection public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreConstant(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotConstant(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotConstant(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>> AreNotReadOnly(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotReadOnly(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>> AreNullable(this aweXpect.Core.IThat> subject) { } + public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNullable(this aweXpect.Core.IThat> subject) { } public static aweXpect.Reflection.ThatFields.FieldsOfTypeResult, aweXpect.Core.IThat>> AreOfExactType(this aweXpect.Core.IThat> subject, System.Type fieldType) { } public static aweXpect.Reflection.ThatFields.FieldsOfTypeResult, aweXpect.Core.IThat>> AreOfExactType(this aweXpect.Core.IThat> subject, System.Type fieldType) { } public static aweXpect.Reflection.ThatFields.FieldsOfTypeResult, aweXpect.Core.IThat>> AreOfExactType(this aweXpect.Core.IThat> subject) { } @@ -1496,6 +1506,8 @@ namespace aweXpect.Reflection public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotExtensionProperties(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotIndexers(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotIndexers(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>> AreNotRequired(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotRequired(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotSealed(this aweXpect.Core.IThat> subject) { } @@ -1504,6 +1516,8 @@ namespace aweXpect.Reflection 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.ThatProperties.PropertiesOfTypeResult, aweXpect.Core.IThat>> AreOfExactType(this aweXpect.Core.IThat> subject, System.Type propertyType) { } public static aweXpect.Reflection.ThatProperties.PropertiesOfTypeResult, aweXpect.Core.IThat>> AreOfExactType(this aweXpect.Core.IThat> subject, System.Type propertyType) { } public static aweXpect.Reflection.ThatProperties.PropertiesOfTypeResult, aweXpect.Core.IThat>> AreOfExactType(this aweXpect.Core.IThat> subject) { } @@ -1580,10 +1594,12 @@ namespace aweXpect.Reflection public static aweXpect.Results.AndOrResult> IsNotAbstract(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsNotAnExtensionProperty(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsNotAnIndexer(this aweXpect.Core.IThat subject) { } + public static aweXpect.Results.AndOrResult> IsNotNullable(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsNotRequired(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.ThatProperty.PropertyOfTypeResult> IsOfExactType(this aweXpect.Core.IThat subject, System.Type propertyType) { } public static aweXpect.Reflection.ThatProperty.PropertyOfTypeResult> IsOfExactType(this aweXpect.Core.IThat subject) { } public static aweXpect.Reflection.ThatProperty.PropertyOfTypeResult> IsOfType(this aweXpect.Core.IThat subject, System.Type propertyType) { } @@ -1704,6 +1720,8 @@ namespace aweXpect.Reflection public static aweXpect.Results.AndOrResult> IsSealed(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsStatic(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsWithinNamespace(this aweXpect.Core.IThat subject, string expected) { } + public static aweXpect.Results.AndOrResult> OnlyHasNonNullableMembers(this aweXpect.Core.IThat subject, aweXpect.Reflection.Collections.MemberScope memberScope = 0) { } + public static aweXpect.Results.AndOrResult> OnlyHasNullableMembers(this aweXpect.Core.IThat subject, aweXpect.Reflection.Collections.MemberScope memberScope = 0) { } } public static class ThatTypes { @@ -1891,6 +1909,10 @@ namespace aweXpect.Reflection public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> InheritFrom(this aweXpect.Core.IThat> subject, System.Type baseType, bool forceDirect = false) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> InheritFrom(this aweXpect.Core.IThat> subject, bool forceDirect = false) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> InheritFrom(this aweXpect.Core.IThat> subject, bool forceDirect = false) { } + public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> OnlyHaveNonNullableMembers(this aweXpect.Core.IThat> subject, aweXpect.Reflection.Collections.MemberScope memberScope = 0) { } + public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> OnlyHaveNonNullableMembers(this aweXpect.Core.IThat> subject, aweXpect.Reflection.Collections.MemberScope memberScope = 0) { } + public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> OnlyHaveNullableMembers(this aweXpect.Core.IThat> subject, aweXpect.Reflection.Collections.MemberScope memberScope = 0) { } + public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> OnlyHaveNullableMembers(this aweXpect.Core.IThat> subject, aweXpect.Reflection.Collections.MemberScope memberScope = 0) { } } public static class TypeFilters { @@ -1981,6 +2003,8 @@ namespace aweXpect.Reflection public static aweXpect.Reflection.Collections.Filtered.Types WhichImplement(this aweXpect.Reflection.Collections.Filtered.Types @this, bool forceDirect = false) { } public static aweXpect.Reflection.Collections.Filtered.Types WhichInheritFrom(this aweXpect.Reflection.Collections.Filtered.Types @this, System.Type baseType, bool forceDirect = false) { } public static aweXpect.Reflection.Collections.Filtered.Types WhichInheritFrom(this aweXpect.Reflection.Collections.Filtered.Types @this, bool forceDirect = false) { } + public static aweXpect.Reflection.Collections.Filtered.Types WhichOnlyHaveNonNullableMembers(this aweXpect.Reflection.Collections.Filtered.Types @this, aweXpect.Reflection.Collections.MemberScope memberScope = 0) { } + public static aweXpect.Reflection.Collections.Filtered.Types WhichOnlyHaveNullableMembers(this aweXpect.Reflection.Collections.Filtered.Types @this, aweXpect.Reflection.Collections.MemberScope memberScope = 0) { } public static aweXpect.Reflection.TypeFilters.TypesWith With(this aweXpect.Reflection.Collections.Filtered.Types @this, bool inherit = true) where TAttribute : System.Attribute { } public static aweXpect.Reflection.TypeFilters.TypesWith With(this aweXpect.Reflection.Collections.Filtered.Types @this, System.Func? predicate, bool inherit = true, [System.Runtime.CompilerServices.CallerArgumentExpression("predicate")] string doNotPopulateThisValue = "") 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 421369cc..2f460584 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 @@ -276,6 +276,7 @@ namespace aweXpect.Reflection public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreNot(this aweXpect.Reflection.Collections.Filtered.Fields @this, aweXpect.Reflection.Collections.AccessModifiers accessModifier) { } public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreNotConstant(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreNotInternal(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } + public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreNotNullable(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreNotObsolete(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreNotPrivate(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreNotPrivateProtected(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } @@ -284,6 +285,7 @@ namespace aweXpect.Reflection public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreNotPublic(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreNotReadOnly(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreNotStatic(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } + public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreNullable(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreObsolete(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } public static aweXpect.Reflection.Collections.Filtered.Fields WhichArePrivate(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } public static aweXpect.Reflection.Collections.Filtered.Fields WhichArePrivateProtected(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } @@ -578,6 +580,7 @@ namespace aweXpect.Reflection public static aweXpect.Reflection.Collections.Filtered.Properties WhichAreNotExtensionProperties(this aweXpect.Reflection.Collections.Filtered.Properties @this) { } public static aweXpect.Reflection.Collections.Filtered.Properties WhichAreNotIndexers(this aweXpect.Reflection.Collections.Filtered.Properties @this) { } public static aweXpect.Reflection.Collections.Filtered.Properties WhichAreNotInternal(this aweXpect.Reflection.Collections.Filtered.Properties @this) { } + public static aweXpect.Reflection.Collections.Filtered.Properties WhichAreNotNullable(this aweXpect.Reflection.Collections.Filtered.Properties @this) { } public static aweXpect.Reflection.Collections.Filtered.Properties WhichAreNotObsolete(this aweXpect.Reflection.Collections.Filtered.Properties @this) { } public static aweXpect.Reflection.Collections.Filtered.Properties WhichAreNotPrivate(this aweXpect.Reflection.Collections.Filtered.Properties @this) { } public static aweXpect.Reflection.Collections.Filtered.Properties WhichAreNotPrivateProtected(this aweXpect.Reflection.Collections.Filtered.Properties @this) { } @@ -588,6 +591,7 @@ namespace aweXpect.Reflection public static aweXpect.Reflection.Collections.Filtered.Properties WhichAreNotSealed(this aweXpect.Reflection.Collections.Filtered.Properties @this) { } public static aweXpect.Reflection.Collections.Filtered.Properties WhichAreNotStatic(this aweXpect.Reflection.Collections.Filtered.Properties @this) { } public static aweXpect.Reflection.Collections.Filtered.Properties WhichAreNotVirtual(this aweXpect.Reflection.Collections.Filtered.Properties @this) { } + public static aweXpect.Reflection.Collections.Filtered.Properties WhichAreNullable(this aweXpect.Reflection.Collections.Filtered.Properties @this) { } public static aweXpect.Reflection.Collections.Filtered.Properties WhichAreObsolete(this aweXpect.Reflection.Collections.Filtered.Properties @this) { } public static aweXpect.Reflection.Collections.Filtered.Properties WhichArePrivate(this aweXpect.Reflection.Collections.Filtered.Properties @this) { } public static aweXpect.Reflection.Collections.Filtered.Properties WhichArePrivateProtected(this aweXpect.Reflection.Collections.Filtered.Properties @this) { } @@ -1018,8 +1022,10 @@ namespace aweXpect.Reflection where TAttribute : System.Attribute { } public static aweXpect.Results.AndOrResult> IsConstant(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsNotConstant(this aweXpect.Core.IThat subject) { } + public static aweXpect.Results.AndOrResult> IsNotNullable(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsNotReadOnly(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsNotStatic(this aweXpect.Core.IThat subject) { } + public static aweXpect.Results.AndOrResult> IsNullable(this aweXpect.Core.IThat subject) { } public static aweXpect.Reflection.ThatField.FieldOfTypeResult> IsOfExactType(this aweXpect.Core.IThat subject, System.Type fieldType) { } public static aweXpect.Reflection.ThatField.FieldOfTypeResult> IsOfExactType(this aweXpect.Core.IThat subject) { } public static aweXpect.Reflection.ThatField.FieldOfTypeResult> IsOfType(this aweXpect.Core.IThat subject, System.Type fieldType) { } @@ -1042,10 +1048,14 @@ namespace aweXpect.Reflection public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreConstant(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotConstant(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotConstant(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>> AreNotReadOnly(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotReadOnly(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>> AreNullable(this aweXpect.Core.IThat> subject) { } + public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNullable(this aweXpect.Core.IThat> subject) { } public static aweXpect.Reflection.ThatFields.FieldsOfTypeResult, aweXpect.Core.IThat>> AreOfExactType(this aweXpect.Core.IThat> subject, System.Type fieldType) { } public static aweXpect.Reflection.ThatFields.FieldsOfTypeResult, aweXpect.Core.IThat>> AreOfExactType(this aweXpect.Core.IThat> subject, System.Type fieldType) { } public static aweXpect.Reflection.ThatFields.FieldsOfTypeResult, aweXpect.Core.IThat>> AreOfExactType(this aweXpect.Core.IThat> subject) { } @@ -1496,6 +1506,8 @@ namespace aweXpect.Reflection public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotExtensionProperties(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotIndexers(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotIndexers(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>> AreNotRequired(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotRequired(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotSealed(this aweXpect.Core.IThat> subject) { } @@ -1504,6 +1516,8 @@ namespace aweXpect.Reflection 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.ThatProperties.PropertiesOfTypeResult, aweXpect.Core.IThat>> AreOfExactType(this aweXpect.Core.IThat> subject, System.Type propertyType) { } public static aweXpect.Reflection.ThatProperties.PropertiesOfTypeResult, aweXpect.Core.IThat>> AreOfExactType(this aweXpect.Core.IThat> subject, System.Type propertyType) { } public static aweXpect.Reflection.ThatProperties.PropertiesOfTypeResult, aweXpect.Core.IThat>> AreOfExactType(this aweXpect.Core.IThat> subject) { } @@ -1580,10 +1594,12 @@ namespace aweXpect.Reflection public static aweXpect.Results.AndOrResult> IsNotAbstract(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsNotAnExtensionProperty(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsNotAnIndexer(this aweXpect.Core.IThat subject) { } + public static aweXpect.Results.AndOrResult> IsNotNullable(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsNotRequired(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.ThatProperty.PropertyOfTypeResult> IsOfExactType(this aweXpect.Core.IThat subject, System.Type propertyType) { } public static aweXpect.Reflection.ThatProperty.PropertyOfTypeResult> IsOfExactType(this aweXpect.Core.IThat subject) { } public static aweXpect.Reflection.ThatProperty.PropertyOfTypeResult> IsOfType(this aweXpect.Core.IThat subject, System.Type propertyType) { } @@ -1704,6 +1720,8 @@ namespace aweXpect.Reflection public static aweXpect.Results.AndOrResult> IsSealed(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsStatic(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsWithinNamespace(this aweXpect.Core.IThat subject, string expected) { } + public static aweXpect.Results.AndOrResult> OnlyHasNonNullableMembers(this aweXpect.Core.IThat subject, aweXpect.Reflection.Collections.MemberScope memberScope = 0) { } + public static aweXpect.Results.AndOrResult> OnlyHasNullableMembers(this aweXpect.Core.IThat subject, aweXpect.Reflection.Collections.MemberScope memberScope = 0) { } } public static class ThatTypes { @@ -1891,6 +1909,10 @@ namespace aweXpect.Reflection public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> InheritFrom(this aweXpect.Core.IThat> subject, System.Type baseType, bool forceDirect = false) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> InheritFrom(this aweXpect.Core.IThat> subject, bool forceDirect = false) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> InheritFrom(this aweXpect.Core.IThat> subject, bool forceDirect = false) { } + public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> OnlyHaveNonNullableMembers(this aweXpect.Core.IThat> subject, aweXpect.Reflection.Collections.MemberScope memberScope = 0) { } + public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> OnlyHaveNonNullableMembers(this aweXpect.Core.IThat> subject, aweXpect.Reflection.Collections.MemberScope memberScope = 0) { } + public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> OnlyHaveNullableMembers(this aweXpect.Core.IThat> subject, aweXpect.Reflection.Collections.MemberScope memberScope = 0) { } + public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> OnlyHaveNullableMembers(this aweXpect.Core.IThat> subject, aweXpect.Reflection.Collections.MemberScope memberScope = 0) { } } public static class TypeFilters { @@ -1981,6 +2003,8 @@ namespace aweXpect.Reflection public static aweXpect.Reflection.Collections.Filtered.Types WhichImplement(this aweXpect.Reflection.Collections.Filtered.Types @this, bool forceDirect = false) { } public static aweXpect.Reflection.Collections.Filtered.Types WhichInheritFrom(this aweXpect.Reflection.Collections.Filtered.Types @this, System.Type baseType, bool forceDirect = false) { } public static aweXpect.Reflection.Collections.Filtered.Types WhichInheritFrom(this aweXpect.Reflection.Collections.Filtered.Types @this, bool forceDirect = false) { } + public static aweXpect.Reflection.Collections.Filtered.Types WhichOnlyHaveNonNullableMembers(this aweXpect.Reflection.Collections.Filtered.Types @this, aweXpect.Reflection.Collections.MemberScope memberScope = 0) { } + public static aweXpect.Reflection.Collections.Filtered.Types WhichOnlyHaveNullableMembers(this aweXpect.Reflection.Collections.Filtered.Types @this, aweXpect.Reflection.Collections.MemberScope memberScope = 0) { } public static aweXpect.Reflection.TypeFilters.TypesWith With(this aweXpect.Reflection.Collections.Filtered.Types @this, bool inherit = true) where TAttribute : System.Attribute { } public static aweXpect.Reflection.TypeFilters.TypesWith With(this aweXpect.Reflection.Collections.Filtered.Types @this, System.Func? predicate, bool inherit = true, [System.Runtime.CompilerServices.CallerArgumentExpression("predicate")] string doNotPopulateThisValue = "") 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 75d6dd11..02829e2e 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 @@ -276,6 +276,7 @@ namespace aweXpect.Reflection public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreNot(this aweXpect.Reflection.Collections.Filtered.Fields @this, aweXpect.Reflection.Collections.AccessModifiers accessModifier) { } public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreNotConstant(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreNotInternal(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } + public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreNotNullable(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreNotObsolete(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreNotPrivate(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreNotPrivateProtected(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } @@ -284,6 +285,7 @@ namespace aweXpect.Reflection public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreNotPublic(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreNotReadOnly(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreNotStatic(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } + public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreNullable(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreObsolete(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } public static aweXpect.Reflection.Collections.Filtered.Fields WhichArePrivate(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } public static aweXpect.Reflection.Collections.Filtered.Fields WhichArePrivateProtected(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } @@ -578,6 +580,7 @@ namespace aweXpect.Reflection public static aweXpect.Reflection.Collections.Filtered.Properties WhichAreNotExtensionProperties(this aweXpect.Reflection.Collections.Filtered.Properties @this) { } public static aweXpect.Reflection.Collections.Filtered.Properties WhichAreNotIndexers(this aweXpect.Reflection.Collections.Filtered.Properties @this) { } public static aweXpect.Reflection.Collections.Filtered.Properties WhichAreNotInternal(this aweXpect.Reflection.Collections.Filtered.Properties @this) { } + public static aweXpect.Reflection.Collections.Filtered.Properties WhichAreNotNullable(this aweXpect.Reflection.Collections.Filtered.Properties @this) { } public static aweXpect.Reflection.Collections.Filtered.Properties WhichAreNotObsolete(this aweXpect.Reflection.Collections.Filtered.Properties @this) { } public static aweXpect.Reflection.Collections.Filtered.Properties WhichAreNotPrivate(this aweXpect.Reflection.Collections.Filtered.Properties @this) { } public static aweXpect.Reflection.Collections.Filtered.Properties WhichAreNotPrivateProtected(this aweXpect.Reflection.Collections.Filtered.Properties @this) { } @@ -588,6 +591,7 @@ namespace aweXpect.Reflection public static aweXpect.Reflection.Collections.Filtered.Properties WhichAreNotSealed(this aweXpect.Reflection.Collections.Filtered.Properties @this) { } public static aweXpect.Reflection.Collections.Filtered.Properties WhichAreNotStatic(this aweXpect.Reflection.Collections.Filtered.Properties @this) { } public static aweXpect.Reflection.Collections.Filtered.Properties WhichAreNotVirtual(this aweXpect.Reflection.Collections.Filtered.Properties @this) { } + public static aweXpect.Reflection.Collections.Filtered.Properties WhichAreNullable(this aweXpect.Reflection.Collections.Filtered.Properties @this) { } public static aweXpect.Reflection.Collections.Filtered.Properties WhichAreObsolete(this aweXpect.Reflection.Collections.Filtered.Properties @this) { } public static aweXpect.Reflection.Collections.Filtered.Properties WhichArePrivate(this aweXpect.Reflection.Collections.Filtered.Properties @this) { } public static aweXpect.Reflection.Collections.Filtered.Properties WhichArePrivateProtected(this aweXpect.Reflection.Collections.Filtered.Properties @this) { } @@ -916,8 +920,10 @@ namespace aweXpect.Reflection where TAttribute : System.Attribute { } public static aweXpect.Results.AndOrResult> IsConstant(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsNotConstant(this aweXpect.Core.IThat subject) { } + public static aweXpect.Results.AndOrResult> IsNotNullable(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsNotReadOnly(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsNotStatic(this aweXpect.Core.IThat subject) { } + public static aweXpect.Results.AndOrResult> IsNullable(this aweXpect.Core.IThat subject) { } public static aweXpect.Reflection.ThatField.FieldOfTypeResult> IsOfExactType(this aweXpect.Core.IThat subject, System.Type fieldType) { } public static aweXpect.Reflection.ThatField.FieldOfTypeResult> IsOfExactType(this aweXpect.Core.IThat subject) { } public static aweXpect.Reflection.ThatField.FieldOfTypeResult> IsOfType(this aweXpect.Core.IThat subject, System.Type fieldType) { } @@ -938,8 +944,10 @@ namespace aweXpect.Reflection { public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreConstant(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotConstant(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>> AreNotReadOnly(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>> AreNullable(this aweXpect.Core.IThat> subject) { } public static aweXpect.Reflection.ThatFields.FieldsOfTypeResult, aweXpect.Core.IThat>> AreOfExactType(this aweXpect.Core.IThat> subject, System.Type fieldType) { } public static aweXpect.Reflection.ThatFields.FieldsOfTypeResult, aweXpect.Core.IThat>> AreOfExactType(this aweXpect.Core.IThat> subject) { } public static aweXpect.Reflection.ThatFields.FieldsOfTypeResult, aweXpect.Core.IThat>> AreOfType(this aweXpect.Core.IThat> subject, System.Type fieldType) { } @@ -1248,10 +1256,12 @@ namespace aweXpect.Reflection public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotAbstract(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotExtensionProperties(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotIndexers(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>> AreNotRequired(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.ThatProperties.PropertiesOfTypeResult, aweXpect.Core.IThat>> AreOfExactType(this aweXpect.Core.IThat> subject, System.Type propertyType) { } public static aweXpect.Reflection.ThatProperties.PropertiesOfTypeResult, aweXpect.Core.IThat>> AreOfExactType(this aweXpect.Core.IThat> subject) { } public static aweXpect.Reflection.ThatProperties.PropertiesOfTypeResult, aweXpect.Core.IThat>> AreOfType(this aweXpect.Core.IThat> subject, System.Type propertyType) { } @@ -1304,10 +1314,12 @@ namespace aweXpect.Reflection public static aweXpect.Results.AndOrResult> IsNotAbstract(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsNotAnExtensionProperty(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsNotAnIndexer(this aweXpect.Core.IThat subject) { } + public static aweXpect.Results.AndOrResult> IsNotNullable(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsNotRequired(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.ThatProperty.PropertyOfTypeResult> IsOfExactType(this aweXpect.Core.IThat subject, System.Type propertyType) { } public static aweXpect.Reflection.ThatProperty.PropertyOfTypeResult> IsOfExactType(this aweXpect.Core.IThat subject) { } public static aweXpect.Reflection.ThatProperty.PropertyOfTypeResult> IsOfType(this aweXpect.Core.IThat subject, System.Type propertyType) { } @@ -1428,6 +1440,8 @@ namespace aweXpect.Reflection public static aweXpect.Results.AndOrResult> IsSealed(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsStatic(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsWithinNamespace(this aweXpect.Core.IThat subject, string expected) { } + public static aweXpect.Results.AndOrResult> OnlyHasNonNullableMembers(this aweXpect.Core.IThat subject, aweXpect.Reflection.Collections.MemberScope memberScope = 0) { } + public static aweXpect.Results.AndOrResult> OnlyHasNullableMembers(this aweXpect.Core.IThat subject, aweXpect.Reflection.Collections.MemberScope memberScope = 0) { } } public static class ThatTypes { @@ -1523,6 +1537,8 @@ namespace aweXpect.Reflection public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> Implement(this aweXpect.Core.IThat> subject, bool forceDirect = false) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> InheritFrom(this aweXpect.Core.IThat> subject, System.Type baseType, bool forceDirect = false) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> InheritFrom(this aweXpect.Core.IThat> subject, bool forceDirect = false) { } + public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> OnlyHaveNonNullableMembers(this aweXpect.Core.IThat> subject, aweXpect.Reflection.Collections.MemberScope memberScope = 0) { } + public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> OnlyHaveNullableMembers(this aweXpect.Core.IThat> subject, aweXpect.Reflection.Collections.MemberScope memberScope = 0) { } } public static class TypeFilters { @@ -1613,6 +1629,8 @@ namespace aweXpect.Reflection public static aweXpect.Reflection.Collections.Filtered.Types WhichImplement(this aweXpect.Reflection.Collections.Filtered.Types @this, bool forceDirect = false) { } public static aweXpect.Reflection.Collections.Filtered.Types WhichInheritFrom(this aweXpect.Reflection.Collections.Filtered.Types @this, System.Type baseType, bool forceDirect = false) { } public static aweXpect.Reflection.Collections.Filtered.Types WhichInheritFrom(this aweXpect.Reflection.Collections.Filtered.Types @this, bool forceDirect = false) { } + public static aweXpect.Reflection.Collections.Filtered.Types WhichOnlyHaveNonNullableMembers(this aweXpect.Reflection.Collections.Filtered.Types @this, aweXpect.Reflection.Collections.MemberScope memberScope = 0) { } + public static aweXpect.Reflection.Collections.Filtered.Types WhichOnlyHaveNullableMembers(this aweXpect.Reflection.Collections.Filtered.Types @this, aweXpect.Reflection.Collections.MemberScope memberScope = 0) { } public static aweXpect.Reflection.TypeFilters.TypesWith With(this aweXpect.Reflection.Collections.Filtered.Types @this, bool inherit = true) where TAttribute : System.Attribute { } public static aweXpect.Reflection.TypeFilters.TypesWith With(this aweXpect.Reflection.Collections.Filtered.Types @this, System.Func? predicate, bool inherit = true, [System.Runtime.CompilerServices.CallerArgumentExpression("predicate")] string doNotPopulateThisValue = "") diff --git a/Tests/aweXpect.Reflection.Internal.Tests/Helpers/NullabilityHelpersTests.cs b/Tests/aweXpect.Reflection.Internal.Tests/Helpers/NullabilityHelpersTests.cs new file mode 100644 index 00000000..b6a6e754 --- /dev/null +++ b/Tests/aweXpect.Reflection.Internal.Tests/Helpers/NullabilityHelpersTests.cs @@ -0,0 +1,335 @@ +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using aweXpect.Reflection.Helpers; + +namespace aweXpect.Reflection.Internal.Tests.Helpers; + +public sealed class NullabilityHelpersTests +{ + [Fact] + public async Task IsNullable_WithNullFieldInfo_ShouldReturnFalse() + { + FieldInfo? fieldInfo = null; + + await That(fieldInfo.IsNullable()).IsFalse(); + } + + [Fact] + public async Task IsNullable_WithNullPropertyInfo_ShouldReturnFalse() + { + PropertyInfo? propertyInfo = null; + + await That(propertyInfo.IsNullable()).IsFalse(); + } + + [Theory] + [InlineData(nameof(NullabilityTestClass.NullableValueField), true)] + [InlineData(nameof(NullabilityTestClass.NonNullableValueField), false)] + [InlineData(nameof(NullabilityTestClass.NullableReferenceField), true)] + [InlineData(nameof(NullabilityTestClass.NonNullableReferenceField), false)] + [InlineData(nameof(NullabilityTestClass.NullableGenericField), true)] + [InlineData(nameof(NullabilityTestClass.NonNullableGenericField), false)] + public async Task IsNullable_ShouldEvaluateFieldNullability(string fieldName, bool expectNullable) + { + FieldInfo fieldInfo = typeof(NullabilityTestClass).GetField(fieldName)!; + + await That(fieldInfo.IsNullable()).IsEqualTo(expectNullable); + } + + [Theory] + [InlineData(nameof(NullabilityTestClass.NullableValueProperty), true)] + [InlineData(nameof(NullabilityTestClass.NonNullableValueProperty), false)] + [InlineData(nameof(NullabilityTestClass.NullableReferenceProperty), true)] + [InlineData(nameof(NullabilityTestClass.NonNullableReferenceProperty), false)] + [InlineData(nameof(NullabilityTestClass.NullableGenericProperty), true)] + [InlineData(nameof(NullabilityTestClass.NonNullableGenericProperty), false)] + [InlineData(nameof(NullabilityTestClass.NullableWriteOnlyProperty), true)] + public async Task IsNullable_ShouldEvaluatePropertyNullability(string propertyName, bool expectNullable) + { + PropertyInfo propertyInfo = typeof(NullabilityTestClass).GetProperty(propertyName)!; + + await That(propertyInfo.IsNullable()).IsEqualTo(expectNullable); + } + + [Theory] + [InlineData(nameof(AllNullableTestClass.FirstNullableField))] + [InlineData(nameof(AllNullableTestClass.SecondNullableField))] + [InlineData(nameof(AllNullableTestClass.FirstNullableProperty))] + public async Task IsNullable_WhenNullabilityIsStoredInContextOfDeclaringType_ShouldReturnTrue(string memberName) + { + MemberInfo memberInfo = typeof(AllNullableTestClass).GetMember(memberName).Single(); + + bool result = memberInfo is FieldInfo fieldInfo + ? fieldInfo.IsNullable() + : ((PropertyInfo)memberInfo).IsNullable(); + + await That(result).IsTrue(); + } + + [Theory] + [InlineData(nameof(NullabilityEdgeCaseTestClass.NullableArrayField), true)] + [InlineData(nameof(NullabilityEdgeCaseTestClass.ArrayOfNullableField), false)] + [InlineData(nameof(NullabilityEdgeCaseTestClass.NullableArrayProperty), true)] + [InlineData(nameof(NullabilityEdgeCaseTestClass.ArrayOfNullableProperty), false)] + public async Task IsNullable_WithArrayMembers_ShouldOnlyConsiderTheTopLevelAnnotation( + string memberName, bool expectNullable) + { + MemberInfo memberInfo = typeof(NullabilityEdgeCaseTestClass).GetMember(memberName).Single(); + + bool result = memberInfo is FieldInfo fieldInfo + ? fieldInfo.IsNullable() + : ((PropertyInfo)memberInfo).IsNullable(); + + await That(result).IsEqualTo(expectNullable); + } + + [Fact] + public async Task IsNullable_WithNullableIndexer_ShouldReturnTrue() + { + PropertyInfo propertyInfo = typeof(NullabilityEdgeCaseTestClass).GetProperty("Item")!; + + await That(propertyInfo.IsNullable()).IsTrue(); + } + + [Theory] + [InlineData(nameof(NullabilityEdgeCaseTestClass.AllowNullProperty))] + [InlineData(nameof(NullabilityEdgeCaseTestClass.MaybeNullProperty))] + public async Task IsNullable_WithPostConditionAttributes_ShouldIgnoreThemAndReturnFalse(string propertyName) + { + PropertyInfo propertyInfo = typeof(NullabilityEdgeCaseTestClass).GetProperty(propertyName)!; + + await That(propertyInfo.IsNullable()).IsFalse(); + } + + [Theory] + [InlineData(nameof(GenericTestClass.UnannotatedField), false)] + [InlineData(nameof(GenericTestClass.AnnotatedField), true)] + [InlineData(nameof(GenericTestClass.UnannotatedProperty), false)] + [InlineData(nameof(GenericTestClass.AnnotatedProperty), true)] + public async Task IsNullable_WithGenericTypeParameterMembers_ShouldOnlyConsiderTheAnnotation( + string memberName, bool expectNullable) + { + MemberInfo memberInfo = typeof(GenericTestClass<>).GetMember(memberName).Single(); + + bool result = memberInfo is FieldInfo fieldInfo + ? fieldInfo.IsNullable() + : ((PropertyInfo)memberInfo).IsNullable(); + + await That(result).IsEqualTo(expectNullable); + } + + [Theory] + [InlineData(nameof(GenericTestClass.UnannotatedField))] + [InlineData(nameof(GenericTestClass.AnnotatedField))] + [InlineData(nameof(GenericTestClass.UnannotatedProperty))] + [InlineData(nameof(GenericTestClass.AnnotatedProperty))] + public async Task IsNullable_WithGenericParameterMembers_WhenDerivedTypeUsesNullableArgument_ShouldReturnTrue( + string memberName) + { + MemberInfo memberInfo = typeof(DerivedFromGenericTestClassWithNullableArgument).GetMember(memberName).Single(); + + bool result = memberInfo is FieldInfo fieldInfo + ? fieldInfo.IsNullable() + : ((PropertyInfo)memberInfo).IsNullable(); + + await That(result).IsTrue(); + } + + [Theory] + [InlineData(nameof(GenericTestClass.UnannotatedField), false)] + [InlineData(nameof(GenericTestClass.AnnotatedField), true)] + [InlineData(nameof(GenericTestClass.UnannotatedProperty), false)] + [InlineData(nameof(GenericTestClass.AnnotatedProperty), true)] + public async Task IsNullable_WithGenericParameterMembers_WhenDerivedTypeUsesNonNullableArgument_ShouldEvaluateAnnotation( + string memberName, bool expectNullable) + { + MemberInfo memberInfo = + typeof(DerivedFromGenericTestClassWithNonNullableArgument).GetMember(memberName).Single(); + + bool result = memberInfo is FieldInfo fieldInfo + ? fieldInfo.IsNullable() + : ((PropertyInfo)memberInfo).IsNullable(); + + await That(result).IsEqualTo(expectNullable); + } + + [Theory] + [InlineData(nameof(GenericTestClass.UnannotatedField))] + [InlineData(nameof(GenericTestClass.UnannotatedProperty))] + public async Task IsNullable_WithGenericParameterMembers_WhenAccessedViaConstructedType_ShouldReturnFalse( + string memberName) + { + // The nullable annotation of a type argument is not part of System.Type, so it can only be + // resolved through the base type metadata of a derived type. + MemberInfo memberInfo = typeof(GenericTestClass).GetMember(memberName).Single(); + + bool result = memberInfo is FieldInfo fieldInfo + ? fieldInfo.IsNullable() + : ((PropertyInfo)memberInfo).IsNullable(); + + await That(result).IsFalse(); + } + + [Theory] + [InlineData(nameof(MostlyNullableTestClass.NonNullableField), false)] + [InlineData(nameof(MostlyNullableTestClass.NonNullableProperty), false)] + [InlineData(nameof(MostlyNullableTestClass.FirstNullableField), true)] + public async Task IsNullable_WhenMemberDiffersFromContextOfDeclaringType_ShouldEvaluateNullability( + string memberName, bool expectNullable) + { + MemberInfo memberInfo = typeof(MostlyNullableTestClass).GetMember(memberName).Single(); + + bool result = memberInfo is FieldInfo fieldInfo + ? fieldInfo.IsNullable() + : ((PropertyInfo)memberInfo).IsNullable(); + + await That(result).IsEqualTo(expectNullable); + } + + [Theory] + [InlineData(nameof(ObliviousTestClass.ObliviousField))] + [InlineData(nameof(ObliviousTestClass.ObliviousProperty))] + public async Task IsNullable_WhenMemberIsOblivious_ShouldReturnFalse(string memberName) + { + MemberInfo memberInfo = typeof(ObliviousTestClass).GetMember(memberName).Single(); + + bool result = memberInfo is FieldInfo fieldInfo + ? fieldInfo.IsNullable() + : ((PropertyInfo)memberInfo).IsNullable(); + + await That(result).IsFalse(); + } + + [Theory] + [InlineData(nameof(ObliviousTestClass.NestedObliviousTestClass.NestedObliviousField))] + [InlineData(nameof(ObliviousTestClass.NestedObliviousTestClass.NestedObliviousProperty))] + public async Task IsNullable_WhenMemberIsObliviousInNestedClass_ShouldReturnFalse(string memberName) + { + MemberInfo memberInfo = typeof(ObliviousTestClass.NestedObliviousTestClass).GetMember(memberName).Single(); + + bool result = memberInfo is FieldInfo fieldInfo + ? fieldInfo.IsNullable() + : ((PropertyInfo)memberInfo).IsNullable(); + + await That(result).IsFalse(); + } + + [Fact] + public async Task GetNullableMembers_ShouldReturnNullableFieldsAndProperties() + { + MemberInfo[] nullableMembers = typeof(NullabilityTestClass).GetNullableMembers(); + + await That(nullableMembers.Select(member => member.Name)).IsEqualTo([ + nameof(NullabilityTestClass.NullableValueField), + nameof(NullabilityTestClass.NullableReferenceField), + nameof(NullabilityTestClass.NullableGenericField), + nameof(NullabilityTestClass.NullableValueProperty), + nameof(NullabilityTestClass.NullableReferenceProperty), + nameof(NullabilityTestClass.NullableGenericProperty), + nameof(NullabilityTestClass.NullableWriteOnlyProperty), + ]).InAnyOrder(); + } + + [Fact] + public async Task GetNotNullableMembers_ShouldReturnNonNullableFieldsAndProperties() + { + MemberInfo[] notNullableMembers = typeof(NullabilityTestClass).GetNotNullableMembers(); + + await That(notNullableMembers.Select(member => member.Name)).IsEqualTo([ + nameof(NullabilityTestClass.NonNullableValueField), + nameof(NullabilityTestClass.NonNullableReferenceField), + nameof(NullabilityTestClass.NonNullableGenericField), + nameof(NullabilityTestClass.NonNullableValueProperty), + nameof(NullabilityTestClass.NonNullableReferenceProperty), + nameof(NullabilityTestClass.NonNullableGenericProperty), + ]).InAnyOrder(); + } + +#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value + public class NullabilityTestClass + { + public int? NullableValueField; + public int NonNullableValueField; + public string? NullableReferenceField; + public string NonNullableReferenceField = ""; + public List? NullableGenericField; + public List NonNullableGenericField = []; + public int? NullableValueProperty { get; set; } + public int NonNullableValueProperty { get; set; } + public string? NullableReferenceProperty { get; set; } + public string NonNullableReferenceProperty { get; set; } = ""; + public List? NullableGenericProperty { get; set; } + public List NonNullableGenericProperty { get; set; } = []; + + public static string? NullableWriteOnlyProperty + { + // ReSharper disable once ValueParameterNotUsed + set { } + } + } + + public class AllNullableTestClass + { + public string? FirstNullableField; + public string? SecondNullableField; + public string? FirstNullableProperty { get; set; } + public string? SecondNullableProperty { get; set; } + } + + public class NullabilityEdgeCaseTestClass + { + public string[]? NullableArrayField; + public string?[] ArrayOfNullableField = []; + public string[]? NullableArrayProperty { get; set; } + public string?[] ArrayOfNullableProperty { get; set; } = []; + + [System.Diagnostics.CodeAnalysis.AllowNull] + public string AllowNullProperty { get; set; } = ""; + + [System.Diagnostics.CodeAnalysis.MaybeNull] + public string MaybeNullProperty { get; set; } = ""; + + public string? this[int index] => null; + } + + // ReSharper disable once UnusedTypeParameter + public class GenericTestClass + { + public T UnannotatedField = default!; + public T? AnnotatedField; + public T UnannotatedProperty { get; set; } = default!; + public T? AnnotatedProperty { get; set; } + } + + public class DerivedFromGenericTestClassWithNullableArgument : GenericTestClass; + + public class DerivedFromGenericTestClassWithNonNullableArgument : GenericTestClass; + + public class MostlyNullableTestClass + { + public string? FirstNullableField; + public string? SecondNullableField; + public string? ThirdNullableField; + public string NonNullableField = ""; + public string? FirstNullableProperty { get; set; } + public string? SecondNullableProperty { get; set; } + public string? ThirdNullableProperty { get; set; } + public string NonNullableProperty { get; set; } = ""; + } + +#nullable disable + public class ObliviousTestClass + { + public string ObliviousField; + public string ObliviousProperty { get; set; } + + public class NestedObliviousTestClass + { + public string NestedObliviousField; + public string NestedObliviousProperty { get; set; } + } + } +#nullable restore +#pragma warning restore CS0649 +} diff --git a/Tests/aweXpect.Reflection.Internal.Tests/aweXpect.Reflection.Internal.Tests.csproj b/Tests/aweXpect.Reflection.Internal.Tests/aweXpect.Reflection.Internal.Tests.csproj index 6530dc29..e3032067 100644 --- a/Tests/aweXpect.Reflection.Internal.Tests/aweXpect.Reflection.Internal.Tests.csproj +++ b/Tests/aweXpect.Reflection.Internal.Tests/aweXpect.Reflection.Internal.Tests.csproj @@ -10,6 +10,9 @@ True + + $(NoWarn);CS0436 diff --git a/Tests/aweXpect.Reflection.Tests/Filters/FieldFilters.WhichAreNotNullable.Tests.cs b/Tests/aweXpect.Reflection.Tests/Filters/FieldFilters.WhichAreNotNullable.Tests.cs new file mode 100644 index 00000000..da821d30 --- /dev/null +++ b/Tests/aweXpect.Reflection.Tests/Filters/FieldFilters.WhichAreNotNullable.Tests.cs @@ -0,0 +1,38 @@ +using aweXpect.Reflection.Collections; +using aweXpect.Reflection.Tests.TestHelpers.Types; + +namespace aweXpect.Reflection.Tests.Filters; + +public sealed partial class FieldFilters +{ + public sealed class WhichAreNotNullable + { + public sealed class Tests + { + [Fact] + public async Task ShouldAllowFilteringForNonNullableFields() + { + Filtered.Fields fields = In.Type() + .Fields().WhichAreNotNullable(); + + await That(fields).AreNotNullable().And.IsNotEmpty(); + await That(fields.GetDescription()) + .IsEqualTo("non-nullable fields in type").AsPrefix(); + } + + [Fact] + public async Task ShouldOnlyKeepNonNullableFields() + { + Filtered.Fields fields = In.Type() + .Fields().WhichAreNotNullable(); + + await That(fields).IsEqualTo([ + typeof(ClassWithMixedNullableMembers) + .GetField(nameof(ClassWithMixedNullableMembers.NonNullableField))!, + typeof(ClassWithMixedNullableMembers) + .GetField(nameof(ClassWithMixedNullableMembers.NonNullableValueField))!, + ]).InAnyOrder(); + } + } + } +} diff --git a/Tests/aweXpect.Reflection.Tests/Filters/FieldFilters.WhichAreNullable.Tests.cs b/Tests/aweXpect.Reflection.Tests/Filters/FieldFilters.WhichAreNullable.Tests.cs new file mode 100644 index 00000000..da5ad2aa --- /dev/null +++ b/Tests/aweXpect.Reflection.Tests/Filters/FieldFilters.WhichAreNullable.Tests.cs @@ -0,0 +1,38 @@ +using aweXpect.Reflection.Collections; +using aweXpect.Reflection.Tests.TestHelpers.Types; + +namespace aweXpect.Reflection.Tests.Filters; + +public sealed partial class FieldFilters +{ + public sealed class WhichAreNullable + { + public sealed class Tests + { + [Fact] + public async Task ShouldAllowFilteringForNullableFields() + { + Filtered.Fields fields = In.Type() + .Fields().WhichAreNullable(); + + await That(fields).AreNullable().And.IsNotEmpty(); + await That(fields.GetDescription()) + .IsEqualTo("nullable fields in type").AsPrefix(); + } + + [Fact] + public async Task ShouldOnlyKeepNullableFields() + { + Filtered.Fields fields = In.Type() + .Fields().WhichAreNullable(); + + await That(fields).IsEqualTo([ + typeof(ClassWithMixedNullableMembers) + .GetField(nameof(ClassWithMixedNullableMembers.NullableField))!, + typeof(ClassWithMixedNullableMembers) + .GetField(nameof(ClassWithMixedNullableMembers.NullableValueField))!, + ]).InAnyOrder(); + } + } + } +} diff --git a/Tests/aweXpect.Reflection.Tests/Filters/PropertyFilters.WhichAreNotNullable.Tests.cs b/Tests/aweXpect.Reflection.Tests/Filters/PropertyFilters.WhichAreNotNullable.Tests.cs new file mode 100644 index 00000000..605e7245 --- /dev/null +++ b/Tests/aweXpect.Reflection.Tests/Filters/PropertyFilters.WhichAreNotNullable.Tests.cs @@ -0,0 +1,38 @@ +using aweXpect.Reflection.Collections; +using aweXpect.Reflection.Tests.TestHelpers.Types; + +namespace aweXpect.Reflection.Tests.Filters; + +public sealed partial class PropertyFilters +{ + public sealed class WhichAreNotNullable + { + public sealed class Tests + { + [Fact] + public async Task ShouldAllowFilteringForNonNullableProperties() + { + Filtered.Properties properties = In.Type() + .Properties().WhichAreNotNullable(); + + await That(properties).AreNotNullable().And.IsNotEmpty(); + await That(properties.GetDescription()) + .IsEqualTo("non-nullable properties in type").AsPrefix(); + } + + [Fact] + public async Task ShouldOnlyKeepNonNullableProperties() + { + Filtered.Properties properties = In.Type() + .Properties().WhichAreNotNullable(); + + await That(properties).IsEqualTo([ + typeof(ClassWithMixedNullableMembers) + .GetProperty(nameof(ClassWithMixedNullableMembers.NonNullableProperty))!, + typeof(ClassWithMixedNullableMembers) + .GetProperty(nameof(ClassWithMixedNullableMembers.NonNullableValueProperty))!, + ]).InAnyOrder(); + } + } + } +} diff --git a/Tests/aweXpect.Reflection.Tests/Filters/PropertyFilters.WhichAreNullable.Tests.cs b/Tests/aweXpect.Reflection.Tests/Filters/PropertyFilters.WhichAreNullable.Tests.cs new file mode 100644 index 00000000..a389c5c4 --- /dev/null +++ b/Tests/aweXpect.Reflection.Tests/Filters/PropertyFilters.WhichAreNullable.Tests.cs @@ -0,0 +1,38 @@ +using aweXpect.Reflection.Collections; +using aweXpect.Reflection.Tests.TestHelpers.Types; + +namespace aweXpect.Reflection.Tests.Filters; + +public sealed partial class PropertyFilters +{ + public sealed class WhichAreNullable + { + public sealed class Tests + { + [Fact] + public async Task ShouldAllowFilteringForNullableProperties() + { + Filtered.Properties properties = In.Type() + .Properties().WhichAreNullable(); + + await That(properties).AreNullable().And.IsNotEmpty(); + await That(properties.GetDescription()) + .IsEqualTo("nullable properties in type").AsPrefix(); + } + + [Fact] + public async Task ShouldOnlyKeepNullableProperties() + { + Filtered.Properties properties = In.Type() + .Properties().WhichAreNullable(); + + await That(properties).IsEqualTo([ + typeof(ClassWithMixedNullableMembers) + .GetProperty(nameof(ClassWithMixedNullableMembers.NullableProperty))!, + typeof(ClassWithMixedNullableMembers) + .GetProperty(nameof(ClassWithMixedNullableMembers.NullableValueProperty))!, + ]).InAnyOrder(); + } + } + } +} diff --git a/Tests/aweXpect.Reflection.Tests/Filters/TypeFilters.WhichOnlyHaveNonNullableMembers.Tests.cs b/Tests/aweXpect.Reflection.Tests/Filters/TypeFilters.WhichOnlyHaveNonNullableMembers.Tests.cs new file mode 100644 index 00000000..3b4964b7 --- /dev/null +++ b/Tests/aweXpect.Reflection.Tests/Filters/TypeFilters.WhichOnlyHaveNonNullableMembers.Tests.cs @@ -0,0 +1,35 @@ +using aweXpect.Reflection.Collections; +using aweXpect.Reflection.Tests.TestHelpers.Types; + +namespace aweXpect.Reflection.Tests.Filters; + +public sealed partial class TypeFilters +{ + public sealed class WhichOnlyHaveNonNullableMembers + { + public sealed class Tests + { + [Fact] + public async Task ShouldAllowFilteringForTypesWhichOnlyHaveNonNullableMembers() + { + Filtered.Types types = In.AssemblyContaining() + .Types().WhichOnlyHaveNonNullableMembers(); + + await That(types).OnlyHaveNonNullableMembers().And.IsNotEmpty(); + await That(types.GetDescription()) + .IsEqualTo("types which only have non-nullable members in assembly").AsPrefix(); + } + + [Fact] + public async Task ShouldOnlyKeepTypesWhichOnlyHaveNonNullableMembers() + { + Filtered.Types types = In + .Types(typeof(ClassWithNullableMembers), typeof(ClassWithMixedNullableMembers), + typeof(ClassWithNonNullableMembers)) + .WhichOnlyHaveNonNullableMembers(); + + await That(types).IsEqualTo([typeof(ClassWithNonNullableMembers),]); + } + } + } +} diff --git a/Tests/aweXpect.Reflection.Tests/Filters/TypeFilters.WhichOnlyHaveNullableMembers.Tests.cs b/Tests/aweXpect.Reflection.Tests/Filters/TypeFilters.WhichOnlyHaveNullableMembers.Tests.cs new file mode 100644 index 00000000..e26792a8 --- /dev/null +++ b/Tests/aweXpect.Reflection.Tests/Filters/TypeFilters.WhichOnlyHaveNullableMembers.Tests.cs @@ -0,0 +1,35 @@ +using aweXpect.Reflection.Collections; +using aweXpect.Reflection.Tests.TestHelpers.Types; + +namespace aweXpect.Reflection.Tests.Filters; + +public sealed partial class TypeFilters +{ + public sealed class WhichOnlyHaveNullableMembers + { + public sealed class Tests + { + [Fact] + public async Task ShouldAllowFilteringForTypesWhichOnlyHaveNullableMembers() + { + Filtered.Types types = In.AssemblyContaining() + .Types().WhichOnlyHaveNullableMembers(); + + await That(types).OnlyHaveNullableMembers().And.IsNotEmpty(); + await That(types.GetDescription()) + .IsEqualTo("types which only have nullable members in assembly").AsPrefix(); + } + + [Fact] + public async Task ShouldOnlyKeepTypesWhichOnlyHaveNullableMembers() + { + Filtered.Types types = In + .Types(typeof(ClassWithNullableMembers), typeof(ClassWithMixedNullableMembers), + typeof(ClassWithNonNullableMembers)) + .WhichOnlyHaveNullableMembers(); + + await That(types).IsEqualTo([typeof(ClassWithNullableMembers),]); + } + } + } +} diff --git a/Tests/aweXpect.Reflection.Tests/TestHelpers/Types/TypesWithNullableMembers.cs b/Tests/aweXpect.Reflection.Tests/TestHelpers/Types/TypesWithNullableMembers.cs new file mode 100644 index 00000000..53197acd --- /dev/null +++ b/Tests/aweXpect.Reflection.Tests/TestHelpers/Types/TypesWithNullableMembers.cs @@ -0,0 +1,81 @@ +using System.Collections.Generic; + +namespace aweXpect.Reflection.Tests.TestHelpers.Types; + +#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value +public class ClassWithNullableMembers +{ + public string? NullableField; + public List? NullableGenericField; + public int? NullableValueField; + public string? NullableProperty { get; set; } + public List? NullableGenericProperty { get; set; } + public int? NullableValueProperty { get; set; } + + public static string? NullableWriteOnlyProperty + { + // ReSharper disable once ValueParameterNotUsed + set { } + } +} + +public class ClassWithNonNullableMembers +{ + public List NonNullableGenericField = []; + public string NonNullableField = ""; + public int NonNullableValueField; + public string NonNullableProperty { get; set; } = ""; + public List NonNullableGenericProperty { get; set; } = []; + public int NonNullableValueProperty { get; set; } +} + +public class ClassWithMixedNullableMembers +{ + public string? NullableField; + public string NonNullableField = ""; + public int? NullableValueField; + public int NonNullableValueField; + public string? NullableProperty { get; set; } + public string NonNullableProperty { get; set; } = ""; + public int? NullableValueProperty { get; set; } + public int NonNullableValueProperty { get; set; } +} + +public class ClassWithMostlyNullableMembers +{ + public string? FirstNullableField; + public string? SecondNullableField; + public string? ThirdNullableField; + public string NonNullableField = ""; + public string? FirstNullableProperty { get; set; } + public string? SecondNullableProperty { get; set; } + public string? ThirdNullableProperty { get; set; } + public string NonNullableProperty { get; set; } = ""; +} + +public class ClassWithSingleNullableProperty +{ + public string? NullableProperty { get; set; } +} + +public class ClassWithSingleNonNullableProperty +{ + public string NonNullableProperty { get; set; } = ""; +} + +public class DerivedClassWithNullableMembers : ClassWithNonNullableMembers +{ + public string? DeclaredNullableField; + public string? DeclaredNullableProperty { get; set; } +} + +public class ClassWithoutMembers; + +#nullable disable +public class ClassWithObliviousMembers +{ + public string ObliviousField; + public string ObliviousProperty { get; set; } +} +#nullable restore +#pragma warning restore CS0649 diff --git a/Tests/aweXpect.Reflection.Tests/ThatField.IsNotNullable.Tests.cs b/Tests/aweXpect.Reflection.Tests/ThatField.IsNotNullable.Tests.cs new file mode 100644 index 00000000..1c8961f5 --- /dev/null +++ b/Tests/aweXpect.Reflection.Tests/ThatField.IsNotNullable.Tests.cs @@ -0,0 +1,147 @@ +using System.Reflection; +using aweXpect.Reflection.Tests.TestHelpers.Types; + +namespace aweXpect.Reflection.Tests; + +public sealed partial class ThatField +{ + public sealed class IsNotNullable + { + public sealed class Tests + { + [Fact] + public async Task WhenFieldIsNonNullableReferenceType_ShouldSucceed() + { + FieldInfo subject = typeof(ClassWithNonNullableMembers) + .GetField(nameof(ClassWithNonNullableMembers.NonNullableField))!; + + async Task Act() + { + await That(subject).IsNotNullable(); + } + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenFieldIsNonNullableValueType_ShouldSucceed() + { + FieldInfo subject = typeof(ClassWithNonNullableMembers) + .GetField(nameof(ClassWithNonNullableMembers.NonNullableValueField))!; + + async Task Act() + { + await That(subject).IsNotNullable(); + } + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenFieldIsOblivious_ShouldSucceed() + { + FieldInfo subject = typeof(ClassWithObliviousMembers) + .GetField(nameof(ClassWithObliviousMembers.ObliviousField))!; + + async Task Act() + { + await That(subject).IsNotNullable(); + } + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenFieldIsNullableReferenceType_ShouldFail() + { + FieldInfo subject = typeof(ClassWithNullableMembers) + .GetField(nameof(ClassWithNullableMembers.NullableField))!; + + 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 WhenFieldIsNullableValueType_ShouldFail() + { + FieldInfo subject = typeof(ClassWithNullableMembers) + .GetField(nameof(ClassWithNullableMembers.NullableValueField))!; + + 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 WhenFieldIsNull_ShouldFail() + { + FieldInfo? subject = null; + + async Task Act() + { + await That(subject).IsNotNullable(); + } + + await That(Act).ThrowsException() + .WithMessage(""" + Expected that subject + is not nullable, + but it was + """); + } + } + + public sealed class NegatedTests + { + [Fact] + public async Task WhenFieldIsNotNullable_ShouldFail() + { + FieldInfo subject = typeof(ClassWithNonNullableMembers) + .GetField(nameof(ClassWithNonNullableMembers.NonNullableField))!; + + 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 WhenFieldIsNullable_ShouldSucceed() + { + FieldInfo subject = typeof(ClassWithNullableMembers) + .GetField(nameof(ClassWithNullableMembers.NullableField))!; + + async Task Act() + { + await That(subject).DoesNotComplyWith(it => it.IsNotNullable()); + } + + await That(Act).DoesNotThrow(); + } + } + } +} diff --git a/Tests/aweXpect.Reflection.Tests/ThatField.IsNullable.Tests.cs b/Tests/aweXpect.Reflection.Tests/ThatField.IsNullable.Tests.cs new file mode 100644 index 00000000..7f40b757 --- /dev/null +++ b/Tests/aweXpect.Reflection.Tests/ThatField.IsNullable.Tests.cs @@ -0,0 +1,218 @@ +using System.Reflection; +using aweXpect.Reflection.Tests.TestHelpers.Types; + +namespace aweXpect.Reflection.Tests; + +public sealed partial class ThatField +{ + public sealed class IsNullable + { + public sealed class Tests + { + [Fact] + public async Task WhenFieldIsNullableReferenceType_ShouldSucceed() + { + FieldInfo subject = typeof(ClassWithNullableMembers) + .GetField(nameof(ClassWithNullableMembers.NullableField))!; + + async Task Act() + { + await That(subject).IsNullable(); + } + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenFieldIsNullableValueType_ShouldSucceed() + { + FieldInfo subject = typeof(ClassWithNullableMembers) + .GetField(nameof(ClassWithNullableMembers.NullableValueField))!; + + async Task Act() + { + await That(subject).IsNullable(); + } + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenFieldIsNullableGenericType_ShouldSucceed() + { + FieldInfo subject = typeof(ClassWithNullableMembers) + .GetField(nameof(ClassWithNullableMembers.NullableGenericField))!; + + async Task Act() + { + await That(subject).IsNullable(); + } + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenFieldIsNullableInMixedClass_ShouldSucceed() + { + FieldInfo subject = typeof(ClassWithMixedNullableMembers) + .GetField(nameof(ClassWithMixedNullableMembers.NullableField))!; + + async Task Act() + { + await That(subject).IsNullable(); + } + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenFieldIsNonNullableReferenceType_ShouldFail() + { + FieldInfo subject = typeof(ClassWithNonNullableMembers) + .GetField(nameof(ClassWithNonNullableMembers.NonNullableField))!; + + 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 WhenFieldIsNonNullableValueType_ShouldFail() + { + FieldInfo subject = typeof(ClassWithNonNullableMembers) + .GetField(nameof(ClassWithNonNullableMembers.NonNullableValueField))!; + + 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 WhenFieldIsNonNullableGenericType_ShouldFail() + { + FieldInfo subject = typeof(ClassWithNonNullableMembers) + .GetField(nameof(ClassWithNonNullableMembers.NonNullableGenericField))!; + + 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 WhenFieldIsNonNullableInMostlyNullableClass_ShouldFail() + { + FieldInfo subject = typeof(ClassWithMostlyNullableMembers) + .GetField(nameof(ClassWithMostlyNullableMembers.NonNullableField))!; + + 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 WhenFieldIsOblivious_ShouldFail() + { + FieldInfo subject = typeof(ClassWithObliviousMembers) + .GetField(nameof(ClassWithObliviousMembers.ObliviousField))!; + + 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 WhenFieldIsNull_ShouldFail() + { + FieldInfo? subject = null; + + async Task Act() + { + await That(subject).IsNullable(); + } + + await That(Act).ThrowsException() + .WithMessage(""" + Expected that subject + is nullable, + but it was + """); + } + } + + public sealed class NegatedTests + { + [Fact] + public async Task WhenFieldIsNotNullable_ShouldSucceed() + { + FieldInfo subject = typeof(ClassWithNonNullableMembers) + .GetField(nameof(ClassWithNonNullableMembers.NonNullableField))!; + + async Task Act() + { + await That(subject).DoesNotComplyWith(it => it.IsNullable()); + } + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenFieldIsNullable_ShouldFail() + { + FieldInfo subject = typeof(ClassWithNullableMembers) + .GetField(nameof(ClassWithNullableMembers.NullableField))!; + + 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/ThatFields.AreNotNullable.Tests.cs b/Tests/aweXpect.Reflection.Tests/ThatFields.AreNotNullable.Tests.cs new file mode 100644 index 00000000..da023e24 --- /dev/null +++ b/Tests/aweXpect.Reflection.Tests/ThatFields.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 ThatFields +{ + public sealed class AreNotNullable + { + public sealed class Tests + { + [Fact] + public async Task WhenAllFieldsAreNotNullable_ShouldSucceed() + { + IEnumerable subject = typeof(ClassWithNonNullableMembers) + .GetFields(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly); + + async Task Act() + { + await That(subject).AreNotNullable(); + } + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenFieldsContainNullableFields_ShouldFail() + { + IEnumerable subject = typeof(ClassWithMixedNullableMembers) + .GetFields(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 fields [ + * + ] + """).AsWildcard(); + } + + [Fact] + public async Task WhenFieldsContainNull_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 fields [ + + ] + """); + } + } + + public sealed class NegatedTests + { + [Fact] + public async Task WhenAllFieldsAreNotNullable_ShouldFail() + { + IEnumerable subject = typeof(ClassWithNonNullableMembers) + .GetFields(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 field, + but it only contained non-nullable fields [ + * + ] + """).AsWildcard(); + } + + [Fact] + public async Task WhenFieldsContainNullableFields_ShouldSucceed() + { + IEnumerable subject = typeof(ClassWithMixedNullableMembers) + .GetFields(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 WhenAllFieldsAreNotNullable_ShouldSucceed() + { + IAsyncEnumerable subject = typeof(ClassWithNonNullableMembers) + .GetFields(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly) + .ToTestAsyncEnumerable(); + + async Task Act() + { + await That(subject).AreNotNullable(); + } + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenFieldsContainNullableFields_ShouldFail() + { + IAsyncEnumerable subject = typeof(ClassWithMixedNullableMembers) + .GetFields(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 fields [ + * + ] + """).AsWildcard(); + } + } +#endif + } +} diff --git a/Tests/aweXpect.Reflection.Tests/ThatFields.AreNullable.Tests.cs b/Tests/aweXpect.Reflection.Tests/ThatFields.AreNullable.Tests.cs new file mode 100644 index 00000000..de14fc48 --- /dev/null +++ b/Tests/aweXpect.Reflection.Tests/ThatFields.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 ThatFields +{ + public sealed class AreNullable + { + public sealed class Tests + { + [Fact] + public async Task WhenAllFieldsAreNullable_ShouldSucceed() + { + IEnumerable subject = typeof(ClassWithNullableMembers) + .GetFields(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly); + + async Task Act() + { + await That(subject).AreNullable(); + } + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenFieldsContainNonNullableFields_ShouldFail() + { + IEnumerable subject = typeof(ClassWithMixedNullableMembers) + .GetFields(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 fields [ + * + ] + """).AsWildcard(); + } + } + + public sealed class NegatedTests + { + [Fact] + public async Task WhenAllFieldsAreNullable_ShouldFail() + { + IEnumerable subject = typeof(ClassWithNullableMembers) + .GetFields(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 fields [ + * + ] + """).AsWildcard(); + } + + [Fact] + public async Task WhenFieldsContainNonNullableFields_ShouldSucceed() + { + IEnumerable subject = typeof(ClassWithMixedNullableMembers) + .GetFields(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 WhenAllFieldsAreNullable_ShouldSucceed() + { + IAsyncEnumerable subject = typeof(ClassWithNullableMembers) + .GetFields(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly) + .ToTestAsyncEnumerable(); + + async Task Act() + { + await That(subject).AreNullable(); + } + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenFieldsContainNonNullableFields_ShouldFail() + { + IAsyncEnumerable subject = typeof(ClassWithMixedNullableMembers) + .GetFields(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 fields [ + * + ] + """).AsWildcard(); + } + } +#endif + } +} diff --git a/Tests/aweXpect.Reflection.Tests/ThatProperties.AreNotNullable.Tests.cs b/Tests/aweXpect.Reflection.Tests/ThatProperties.AreNotNullable.Tests.cs new file mode 100644 index 00000000..c1702ae9 --- /dev/null +++ b/Tests/aweXpect.Reflection.Tests/ThatProperties.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 ThatProperties +{ + public sealed class AreNotNullable + { + public sealed class Tests + { + [Fact] + public async Task WhenAllPropertiesAreNotNullable_ShouldSucceed() + { + IEnumerable subject = typeof(ClassWithNonNullableMembers) + .GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly); + + async Task Act() + { + await That(subject).AreNotNullable(); + } + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenPropertiesContainNullableProperties_ShouldFail() + { + IEnumerable subject = typeof(ClassWithMixedNullableMembers) + .GetProperties(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 properties [ + * + ] + """).AsWildcard(); + } + + [Fact] + public async Task WhenPropertiesContainNull_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 properties [ + + ] + """); + } + } + + public sealed class NegatedTests + { + [Fact] + public async Task WhenAllPropertiesAreNotNullable_ShouldFail() + { + IEnumerable subject = typeof(ClassWithNonNullableMembers) + .GetProperties(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 property, + but it only contained non-nullable properties [ + * + ] + """).AsWildcard(); + } + + [Fact] + public async Task WhenPropertiesContainNullableProperties_ShouldSucceed() + { + IEnumerable subject = typeof(ClassWithMixedNullableMembers) + .GetProperties(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 WhenAllPropertiesAreNotNullable_ShouldSucceed() + { + IAsyncEnumerable subject = typeof(ClassWithNonNullableMembers) + .GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly) + .ToTestAsyncEnumerable(); + + async Task Act() + { + await That(subject).AreNotNullable(); + } + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenPropertiesContainNullableProperties_ShouldFail() + { + IAsyncEnumerable subject = typeof(ClassWithMixedNullableMembers) + .GetProperties(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 properties [ + * + ] + """).AsWildcard(); + } + } +#endif + } +} diff --git a/Tests/aweXpect.Reflection.Tests/ThatProperties.AreNullable.Tests.cs b/Tests/aweXpect.Reflection.Tests/ThatProperties.AreNullable.Tests.cs new file mode 100644 index 00000000..3a116360 --- /dev/null +++ b/Tests/aweXpect.Reflection.Tests/ThatProperties.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 ThatProperties +{ + public sealed class AreNullable + { + public sealed class Tests + { + [Fact] + public async Task WhenAllPropertiesAreNullable_ShouldSucceed() + { + IEnumerable subject = typeof(ClassWithNullableMembers) + .GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly); + + async Task Act() + { + await That(subject).AreNullable(); + } + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenPropertiesContainNonNullableProperties_ShouldFail() + { + IEnumerable subject = typeof(ClassWithMixedNullableMembers) + .GetProperties(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 properties [ + * + ] + """).AsWildcard(); + } + } + + public sealed class NegatedTests + { + [Fact] + public async Task WhenAllPropertiesAreNullable_ShouldFail() + { + IEnumerable subject = typeof(ClassWithNullableMembers) + .GetProperties(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 properties [ + * + ] + """).AsWildcard(); + } + + [Fact] + public async Task WhenPropertiesContainNonNullableProperties_ShouldSucceed() + { + IEnumerable subject = typeof(ClassWithMixedNullableMembers) + .GetProperties(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 WhenAllPropertiesAreNullable_ShouldSucceed() + { + IAsyncEnumerable subject = typeof(ClassWithNullableMembers) + .GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly) + .ToTestAsyncEnumerable(); + + async Task Act() + { + await That(subject).AreNullable(); + } + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenPropertiesContainNonNullableProperties_ShouldFail() + { + IAsyncEnumerable subject = typeof(ClassWithMixedNullableMembers) + .GetProperties(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 properties [ + * + ] + """).AsWildcard(); + } + } +#endif + } +} diff --git a/Tests/aweXpect.Reflection.Tests/ThatProperty.IsNotNullable.Tests.cs b/Tests/aweXpect.Reflection.Tests/ThatProperty.IsNotNullable.Tests.cs new file mode 100644 index 00000000..0d623f77 --- /dev/null +++ b/Tests/aweXpect.Reflection.Tests/ThatProperty.IsNotNullable.Tests.cs @@ -0,0 +1,147 @@ +using System.Reflection; +using aweXpect.Reflection.Tests.TestHelpers.Types; + +namespace aweXpect.Reflection.Tests; + +public sealed partial class ThatProperty +{ + public sealed class IsNotNullable + { + public sealed class Tests + { + [Fact] + public async Task WhenPropertyIsNonNullableReferenceType_ShouldSucceed() + { + PropertyInfo subject = typeof(ClassWithNonNullableMembers) + .GetProperty(nameof(ClassWithNonNullableMembers.NonNullableProperty))!; + + async Task Act() + { + await That(subject).IsNotNullable(); + } + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenPropertyIsNonNullableValueType_ShouldSucceed() + { + PropertyInfo subject = typeof(ClassWithNonNullableMembers) + .GetProperty(nameof(ClassWithNonNullableMembers.NonNullableValueProperty))!; + + async Task Act() + { + await That(subject).IsNotNullable(); + } + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenPropertyIsOblivious_ShouldSucceed() + { + PropertyInfo subject = typeof(ClassWithObliviousMembers) + .GetProperty(nameof(ClassWithObliviousMembers.ObliviousProperty))!; + + async Task Act() + { + await That(subject).IsNotNullable(); + } + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenPropertyIsNullableReferenceType_ShouldFail() + { + PropertyInfo subject = typeof(ClassWithNullableMembers) + .GetProperty(nameof(ClassWithNullableMembers.NullableProperty))!; + + 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 WhenPropertyIsNullableValueType_ShouldFail() + { + PropertyInfo subject = typeof(ClassWithNullableMembers) + .GetProperty(nameof(ClassWithNullableMembers.NullableValueProperty))!; + + 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 WhenPropertyIsNull_ShouldFail() + { + PropertyInfo? subject = null; + + async Task Act() + { + await That(subject).IsNotNullable(); + } + + await That(Act).ThrowsException() + .WithMessage(""" + Expected that subject + is not nullable, + but it was + """); + } + } + + public sealed class NegatedTests + { + [Fact] + public async Task WhenPropertyIsNotNullable_ShouldFail() + { + PropertyInfo subject = typeof(ClassWithNonNullableMembers) + .GetProperty(nameof(ClassWithNonNullableMembers.NonNullableProperty))!; + + 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 WhenPropertyIsNullable_ShouldSucceed() + { + PropertyInfo subject = typeof(ClassWithNullableMembers) + .GetProperty(nameof(ClassWithNullableMembers.NullableProperty))!; + + async Task Act() + { + await That(subject).DoesNotComplyWith(it => it.IsNotNullable()); + } + + await That(Act).DoesNotThrow(); + } + } + } +} diff --git a/Tests/aweXpect.Reflection.Tests/ThatProperty.IsNullable.Tests.cs b/Tests/aweXpect.Reflection.Tests/ThatProperty.IsNullable.Tests.cs new file mode 100644 index 00000000..67b86719 --- /dev/null +++ b/Tests/aweXpect.Reflection.Tests/ThatProperty.IsNullable.Tests.cs @@ -0,0 +1,232 @@ +using System.Reflection; +using aweXpect.Reflection.Tests.TestHelpers.Types; + +namespace aweXpect.Reflection.Tests; + +public sealed partial class ThatProperty +{ + public sealed class IsNullable + { + public sealed class Tests + { + [Fact] + public async Task WhenPropertyIsNullableReferenceType_ShouldSucceed() + { + PropertyInfo subject = typeof(ClassWithNullableMembers) + .GetProperty(nameof(ClassWithNullableMembers.NullableProperty))!; + + async Task Act() + { + await That(subject).IsNullable(); + } + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenPropertyIsNullableValueType_ShouldSucceed() + { + PropertyInfo subject = typeof(ClassWithNullableMembers) + .GetProperty(nameof(ClassWithNullableMembers.NullableValueProperty))!; + + async Task Act() + { + await That(subject).IsNullable(); + } + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenPropertyIsNullableGenericType_ShouldSucceed() + { + PropertyInfo subject = typeof(ClassWithNullableMembers) + .GetProperty(nameof(ClassWithNullableMembers.NullableGenericProperty))!; + + async Task Act() + { + await That(subject).IsNullable(); + } + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenPropertyIsWriteOnlyNullable_ShouldSucceed() + { + PropertyInfo subject = typeof(ClassWithNullableMembers) + .GetProperty(nameof(ClassWithNullableMembers.NullableWriteOnlyProperty))!; + + async Task Act() + { + await That(subject).IsNullable(); + } + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenPropertyIsNullableInMixedClass_ShouldSucceed() + { + PropertyInfo subject = typeof(ClassWithMixedNullableMembers) + .GetProperty(nameof(ClassWithMixedNullableMembers.NullableProperty))!; + + async Task Act() + { + await That(subject).IsNullable(); + } + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenPropertyIsNonNullableReferenceType_ShouldFail() + { + PropertyInfo subject = typeof(ClassWithNonNullableMembers) + .GetProperty(nameof(ClassWithNonNullableMembers.NonNullableProperty))!; + + 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 WhenPropertyIsNonNullableValueType_ShouldFail() + { + PropertyInfo subject = typeof(ClassWithNonNullableMembers) + .GetProperty(nameof(ClassWithNonNullableMembers.NonNullableValueProperty))!; + + 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 WhenPropertyIsNonNullableGenericType_ShouldFail() + { + PropertyInfo subject = typeof(ClassWithNonNullableMembers) + .GetProperty(nameof(ClassWithNonNullableMembers.NonNullableGenericProperty))!; + + 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 WhenPropertyIsNonNullableInMostlyNullableClass_ShouldFail() + { + PropertyInfo subject = typeof(ClassWithMostlyNullableMembers) + .GetProperty(nameof(ClassWithMostlyNullableMembers.NonNullableProperty))!; + + 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 WhenPropertyIsOblivious_ShouldFail() + { + PropertyInfo subject = typeof(ClassWithObliviousMembers) + .GetProperty(nameof(ClassWithObliviousMembers.ObliviousProperty))!; + + 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 WhenPropertyIsNull_ShouldFail() + { + PropertyInfo? subject = null; + + async Task Act() + { + await That(subject).IsNullable(); + } + + await That(Act).ThrowsException() + .WithMessage(""" + Expected that subject + is nullable, + but it was + """); + } + } + + public sealed class NegatedTests + { + [Fact] + public async Task WhenPropertyIsNotNullable_ShouldSucceed() + { + PropertyInfo subject = typeof(ClassWithNonNullableMembers) + .GetProperty(nameof(ClassWithNonNullableMembers.NonNullableProperty))!; + + async Task Act() + { + await That(subject).DoesNotComplyWith(it => it.IsNullable()); + } + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenPropertyIsNullable_ShouldFail() + { + PropertyInfo subject = typeof(ClassWithNullableMembers) + .GetProperty(nameof(ClassWithNullableMembers.NullableProperty))!; + + 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/ThatType.OnlyHasNonNullableMembers.Tests.cs b/Tests/aweXpect.Reflection.Tests/ThatType.OnlyHasNonNullableMembers.Tests.cs new file mode 100644 index 00000000..d7485740 --- /dev/null +++ b/Tests/aweXpect.Reflection.Tests/ThatType.OnlyHasNonNullableMembers.Tests.cs @@ -0,0 +1,137 @@ +using System.Reflection; +using aweXpect.Reflection.Tests.TestHelpers.Types; + +namespace aweXpect.Reflection.Tests; + +public sealed partial class ThatType +{ + public sealed class OnlyHasNonNullableMembers + { + public sealed class Tests + { + [Fact] + public async Task WhenTypeOnlyHasNonNullableMembers_ShouldSucceed() + { + Type subject = typeof(ClassWithNonNullableMembers); + + async Task Act() + { + await That(subject).OnlyHasNonNullableMembers(); + } + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenTypeHasNoMembers_ShouldSucceed() + { + Type subject = typeof(ClassWithoutMembers); + + async Task Act() + { + await That(subject).OnlyHasNonNullableMembers(); + } + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenTypeHasNullableMembers_ShouldFail() + { + Type subject = typeof(ClassWithSingleNullableProperty); + PropertyInfo property = subject + .GetProperty(nameof(ClassWithSingleNullableProperty.NullableProperty))!; + + 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(property)} + ] + """); + } + + [Fact] + public async Task WhenTypeHasMixedMembers_ShouldFail() + { + Type subject = typeof(ClassWithMixedNullableMembers); + + 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 [ + * + ] + """).AsWildcard(); + } + + [Fact] + public async Task WhenTypeIsNull_ShouldFail() + { + Type? subject = null; + + async Task Act() + { + await That(subject).OnlyHasNonNullableMembers(); + } + + await That(Act).ThrowsException() + .WithMessage(""" + Expected that subject + only has non-nullable members, + but it was + """); + } + } + + public sealed class NegatedTests + { + [Fact] + public async Task WhenTypeHasNullableMembers_ShouldSucceed() + { + Type subject = typeof(ClassWithMixedNullableMembers); + + async Task Act() + { + await That(subject).DoesNotComplyWith(it => it.OnlyHasNonNullableMembers()); + } + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenTypeOnlyHasNonNullableMembers_ShouldFail() + { + Type subject = typeof(ClassWithSingleNonNullableProperty); + PropertyInfo property = subject + .GetProperty(nameof(ClassWithSingleNonNullableProperty.NonNullableProperty))!; + + async Task Act() + { + await That(subject).DoesNotComplyWith(it => it.OnlyHasNonNullableMembers()); + } + + await That(Act).ThrowsException() + .WithMessage($""" + Expected that subject + does not only have non-nullable members, + but it only contained non-nullable members [ + {Formatter.Format(property)} + ] + """); + } + } + } +} diff --git a/Tests/aweXpect.Reflection.Tests/ThatType.OnlyHasNullableMembers.Tests.cs b/Tests/aweXpect.Reflection.Tests/ThatType.OnlyHasNullableMembers.Tests.cs new file mode 100644 index 00000000..d5ee3215 --- /dev/null +++ b/Tests/aweXpect.Reflection.Tests/ThatType.OnlyHasNullableMembers.Tests.cs @@ -0,0 +1,171 @@ +using System.Reflection; +using aweXpect.Reflection.Collections; +using aweXpect.Reflection.Tests.TestHelpers.Types; + +namespace aweXpect.Reflection.Tests; + +public sealed partial class ThatType +{ + public sealed class OnlyHasNullableMembers + { + public sealed class Tests + { + [Fact] + public async Task WhenTypeOnlyHasNullableMembers_ShouldSucceed() + { + Type subject = typeof(ClassWithNullableMembers); + + async Task Act() + { + await That(subject).OnlyHasNullableMembers(); + } + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenTypeHasNoMembers_ShouldSucceed() + { + Type subject = typeof(ClassWithoutMembers); + + async Task Act() + { + await That(subject).OnlyHasNullableMembers(); + } + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenTypeHasNonNullableMembers_ShouldFail() + { + Type subject = typeof(ClassWithSingleNonNullableProperty); + PropertyInfo property = subject + .GetProperty(nameof(ClassWithSingleNonNullableProperty.NonNullableProperty))!; + + 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(property)} + ] + """); + } + + [Fact] + public async Task WhenTypeHasMixedMembers_ShouldFail() + { + Type subject = typeof(ClassWithMixedNullableMembers); + + 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 [ + * + ] + """).AsWildcard(); + } + + [Fact] + public async Task WhenBaseTypeHasNonNullableMembers_ShouldSucceed() + { + Type subject = typeof(DerivedClassWithNullableMembers); + + async Task Act() + { + await That(subject).OnlyHasNullableMembers(); + } + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WithIncludingInherited_WhenBaseTypeHasNonNullableMembers_ShouldFail() + { + Type subject = typeof(DerivedClassWithNullableMembers); + + 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 [ + * + ] + """).AsWildcard(); + } + + [Fact] + public async Task WhenTypeIsNull_ShouldFail() + { + Type? subject = null; + + async Task Act() + { + await That(subject).OnlyHasNullableMembers(); + } + + await That(Act).ThrowsException() + .WithMessage(""" + Expected that subject + only has nullable members, + but it was + """); + } + } + + public sealed class NegatedTests + { + [Fact] + public async Task WhenTypeHasNonNullableMembers_ShouldSucceed() + { + Type subject = typeof(ClassWithMixedNullableMembers); + + async Task Act() + { + await That(subject).DoesNotComplyWith(it => it.OnlyHasNullableMembers()); + } + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenTypeOnlyHasNullableMembers_ShouldFail() + { + Type subject = typeof(ClassWithSingleNullableProperty); + PropertyInfo property = subject + .GetProperty(nameof(ClassWithSingleNullableProperty.NullableProperty))!; + + async Task Act() + { + await That(subject).DoesNotComplyWith(it => it.OnlyHasNullableMembers()); + } + + await That(Act).ThrowsException() + .WithMessage($""" + Expected that subject + does not only have nullable members, + but it only contained nullable members [ + {Formatter.Format(property)} + ] + """); + } + } + } +} diff --git a/Tests/aweXpect.Reflection.Tests/ThatTypes.OnlyHaveNonNullableMembers.Tests.cs b/Tests/aweXpect.Reflection.Tests/ThatTypes.OnlyHaveNonNullableMembers.Tests.cs new file mode 100644 index 00000000..012ce921 --- /dev/null +++ b/Tests/aweXpect.Reflection.Tests/ThatTypes.OnlyHaveNonNullableMembers.Tests.cs @@ -0,0 +1,176 @@ +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 ThatTypes +{ + public sealed class OnlyHaveNonNullableMembers + { + public sealed class Tests + { + [Fact] + public async Task WhenAllTypesOnlyHaveNonNullableMembers_ShouldSucceed() + { + IEnumerable subject = + [ + typeof(ClassWithNonNullableMembers), + typeof(ClassWithSingleNonNullableProperty), + typeof(ClassWithoutMembers), + ]; + + async Task Act() + { + await That(subject).OnlyHaveNonNullableMembers(); + } + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenTypesContainTypeWithNullableMembers_ShouldFail() + { + IEnumerable subject = + [ + typeof(ClassWithNonNullableMembers), + typeof(ClassWithSingleNullableProperty), + ]; + PropertyInfo property = typeof(ClassWithSingleNullableProperty) + .GetProperty(nameof(ClassWithSingleNullableProperty.NullableProperty))!; + + async Task Act() + { + await That(subject).OnlyHaveNonNullableMembers(); + } + + await That(Act).ThrowsException() + .WithMessage($""" + Expected that subject + only have non-nullable members, + but it contained types with nullable members [ + ClassWithSingleNullableProperty with nullable members [{Formatter.Format(property)}] + ] + """); + } + + [Fact] + public async Task WhenCollectionContainsNull_ShouldListNullWithoutViolations() + { + IEnumerable subject = + [ + typeof(ClassWithNonNullableMembers), + null, + ]; + + async Task Act() + { + await That(subject).OnlyHaveNonNullableMembers(); + } + + await That(Act).ThrowsException() + .WithMessage(""" + Expected that subject + only have non-nullable members, + but it contained types with nullable members [ + + ] + """); + } + } + + public sealed class NegatedTests + { + [Fact] + public async Task WhenAllTypesOnlyHaveNonNullableMembers_ShouldFail() + { + IEnumerable subject = + [ + typeof(ClassWithSingleNonNullableProperty), + ]; + + async Task Act() + { + await That(subject).DoesNotComplyWith(they => they.OnlyHaveNonNullableMembers()); + } + + await That(Act).ThrowsException() + .WithMessage(""" + Expected that subject + not all only have non-nullable members, + but it only contained types with only non-nullable members [ + ClassWithSingleNonNullableProperty + ] + """); + } + + [Fact] + public async Task WhenTypesContainTypeWithNullableMembers_ShouldSucceed() + { + IEnumerable subject = + [ + typeof(ClassWithNonNullableMembers), + typeof(ClassWithSingleNullableProperty), + ]; + + async Task Act() + { + await That(subject).DoesNotComplyWith(they => they.OnlyHaveNonNullableMembers()); + } + + await That(Act).DoesNotThrow(); + } + } + +#if NET8_0_OR_GREATER + public sealed class AsyncEnumerableTests + { + [Fact] + public async Task WhenAllTypesOnlyHaveNonNullableMembers_ShouldSucceed() + { + IAsyncEnumerable subject = new[] + { + typeof(ClassWithNonNullableMembers), + typeof(ClassWithSingleNonNullableProperty), + } + .ToTestAsyncEnumerable(); + + async Task Act() + { + await That(subject).OnlyHaveNonNullableMembers(); + } + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenTypesContainTypeWithNullableMembers_ShouldFail() + { + IAsyncEnumerable subject = new[] + { + typeof(ClassWithNonNullableMembers), + typeof(ClassWithSingleNullableProperty), + } + .ToTestAsyncEnumerable(); + + async Task Act() + { + await That(subject).OnlyHaveNonNullableMembers(); + } + + await That(Act).ThrowsException() + .WithMessage(""" + Expected that subject + only have non-nullable members, + but it contained types with nullable members [ + * + ] + """).AsWildcard(); + } + } +#endif + } +} diff --git a/Tests/aweXpect.Reflection.Tests/ThatTypes.OnlyHaveNullableMembers.Tests.cs b/Tests/aweXpect.Reflection.Tests/ThatTypes.OnlyHaveNullableMembers.Tests.cs new file mode 100644 index 00000000..4bc524ea --- /dev/null +++ b/Tests/aweXpect.Reflection.Tests/ThatTypes.OnlyHaveNullableMembers.Tests.cs @@ -0,0 +1,203 @@ +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 ThatTypes +{ + public sealed class OnlyHaveNullableMembers + { + public sealed class Tests + { + [Fact] + public async Task WhenAllTypesOnlyHaveNullableMembers_ShouldSucceed() + { + IEnumerable subject = + [ + typeof(ClassWithNullableMembers), + typeof(ClassWithSingleNullableProperty), + typeof(ClassWithoutMembers), + ]; + + async Task Act() + { + await That(subject).OnlyHaveNullableMembers(); + } + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenTypesContainTypeWithNonNullableMembers_ShouldFail() + { + IEnumerable subject = + [ + typeof(ClassWithNullableMembers), + typeof(ClassWithSingleNonNullableProperty), + ]; + PropertyInfo property = typeof(ClassWithSingleNonNullableProperty) + .GetProperty(nameof(ClassWithSingleNonNullableProperty.NonNullableProperty))!; + + async Task Act() + { + await That(subject).OnlyHaveNullableMembers(); + } + + await That(Act).ThrowsException() + .WithMessage($""" + Expected that subject + only have nullable members, + but it contained types with non-nullable members [ + ClassWithSingleNonNullableProperty with non-nullable members [{Formatter.Format(property)}] + ] + """); + } + + [Fact] + public async Task WhenMultipleTypesDoNotComply_ShouldListAllWithComma() + { + IEnumerable subject = + [ + typeof(ClassWithSingleNonNullableProperty), + null, + ]; + PropertyInfo property = typeof(ClassWithSingleNonNullableProperty) + .GetProperty(nameof(ClassWithSingleNonNullableProperty.NonNullableProperty))!; + + async Task Act() + { + await That(subject).OnlyHaveNullableMembers(); + } + + await That(Act).ThrowsException() + .WithMessage($""" + Expected that subject + only have nullable members, + but it contained types with non-nullable members [ + ClassWithSingleNonNullableProperty with non-nullable members [{Formatter.Format(property)}], + + ] + """); + } + + [Fact] + public async Task WhenCollectionContainsNull_ShouldListNullWithoutViolations() + { + IEnumerable subject = + [ + typeof(ClassWithNullableMembers), + null, + ]; + + async Task Act() + { + await That(subject).OnlyHaveNullableMembers(); + } + + await That(Act).ThrowsException() + .WithMessage(""" + Expected that subject + only have nullable members, + but it contained types with non-nullable members [ + + ] + """); + } + } + + public sealed class NegatedTests + { + [Fact] + public async Task WhenAllTypesOnlyHaveNullableMembers_ShouldFail() + { + IEnumerable subject = + [ + typeof(ClassWithSingleNullableProperty), + ]; + + async Task Act() + { + await That(subject).DoesNotComplyWith(they => they.OnlyHaveNullableMembers()); + } + + await That(Act).ThrowsException() + .WithMessage(""" + Expected that subject + not all only have nullable members, + but it only contained types with only nullable members [ + ClassWithSingleNullableProperty + ] + """); + } + + [Fact] + public async Task WhenTypesContainTypeWithNonNullableMembers_ShouldSucceed() + { + IEnumerable subject = + [ + typeof(ClassWithNullableMembers), + typeof(ClassWithSingleNonNullableProperty), + ]; + + async Task Act() + { + await That(subject).DoesNotComplyWith(they => they.OnlyHaveNullableMembers()); + } + + await That(Act).DoesNotThrow(); + } + } + +#if NET8_0_OR_GREATER + public sealed class AsyncEnumerableTests + { + [Fact] + public async Task WhenAllTypesOnlyHaveNullableMembers_ShouldSucceed() + { + IAsyncEnumerable subject = new[] + { + typeof(ClassWithNullableMembers), + typeof(ClassWithSingleNullableProperty), + } + .ToTestAsyncEnumerable(); + + async Task Act() + { + await That(subject).OnlyHaveNullableMembers(); + } + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenTypesContainTypeWithNonNullableMembers_ShouldFail() + { + IAsyncEnumerable subject = new[] + { + typeof(ClassWithNullableMembers), + typeof(ClassWithSingleNonNullableProperty), + } + .ToTestAsyncEnumerable(); + + async Task Act() + { + await That(subject).OnlyHaveNullableMembers(); + } + + await That(Act).ThrowsException() + .WithMessage(""" + Expected that subject + only have nullable members, + but it contained types with non-nullable members [ + * + ] + """).AsWildcard(); + } + } +#endif + } +}