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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ outside the namespace.
| assignable to | `.WhichAreAssignableTo<T>()` | `.IsAssignableTo<T>()` | `.AreAssignableTo<T>()` |
| assignable from | `.WhichAreAssignableFrom<T>()` | `.IsAssignableFrom<T>()` | `.AreAssignableFrom<T>()` |
| instantiable | `.WhichAreInstantiable()` | `.IsInstantiable()` | `.AreInstantiable()` |
| immutable | `.WhichAreImmutable()` | `.IsImmutable()` | `.AreImmutable()` |
| default constructor | `.WhichHaveADefaultConstructor()` | `.HasADefaultConstructor()` | `.HaveADefaultConstructor()` |
| custom predicate | `.Which(t => …)` | `.Satisfies(t => …)` | `.All().Satisfy(t => …)` |

Expand Down Expand Up @@ -323,6 +324,10 @@ an open generic type definition. *Default constructor* checks for an accessible
(value types always have one); this is independent of instantiability (e.g. a type with only a parameterized
constructor is instantiable but has no default constructor).

A type is *immutable* when all instance fields (including inherited ones) are `readonly` and all instance
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.

> **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()`,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System;
using aweXpect.Reflection.Collections;
using aweXpect.Reflection.Helpers;

namespace aweXpect.Reflection;

public static partial class TypeFilters
{
/// <summary>
/// Filters for types that are immutable.
/// </summary>
/// <remarks>
/// A type is considered immutable when all instance fields (including inherited ones) are
/// <see langword="readonly" /> and all instance properties (including inherited ones) have no setter
/// or an init-only setter.
/// </remarks>
public static Filtered.Types WhichAreImmutable(this Filtered.Types @this)
=> @this.Which(Filter.Prefix<Type>(
type => type.IsImmutable(),
"immutable "));

/// <summary>
/// Filters for types that are not immutable.
/// </summary>
/// <remarks>
/// A type is considered immutable when all instance fields (including inherited ones) are
/// <see langword="readonly" /> and all instance properties (including inherited ones) have no setter
/// or an init-only setter.
/// </remarks>
public static Filtered.Types WhichAreNotImmutable(this Filtered.Types @this)
=> @this.Which(Filter.Prefix<Type>(
type => !type.IsImmutable(),
"mutable "));
}
26 changes: 26 additions & 0 deletions Source/aweXpect.Reflection/Helpers/TypeHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1311,6 +1311,32 @@ public static bool IsReallyAbstract(this Type? type)
public static bool IsReallyInstantiable(this Type? type)
=> type is { IsAbstract: false, IsGenericTypeDefinition: false, };

/// <summary>
/// Gets a value indicating whether the <see cref="Type" /> is immutable.
/// </summary>
/// <param name="type">The <see cref="Type" />.</param>
/// <remarks>
/// A type is considered immutable when all instance fields (including inherited ones) are
/// <see langword="readonly" /> and all instance properties (including inherited ones) have no setter
/// or an init-only setter.
/// </remarks>
public static bool IsImmutable(this Type? type)
=> type is not null && type.GetMutableMembers().Length == 0;

/// <summary>
/// Gets the mutable instance members of the <paramref name="type" />: fields (including inherited ones)
/// that are not <see langword="readonly" /> and properties (including inherited ones) with a regular
/// (non-init) setter.
/// </summary>
/// <param name="type">The <see cref="Type" />.</param>
public static MemberInfo[] GetMutableMembers(this Type type)
=> type.GetDeclaredFields(MemberScope.IncludingInherited)
.Where(field => field is { IsStatic: false, IsInitOnly: false, })
.OfType<MemberInfo>()
.Concat(type.GetDeclaredProperties(MemberScope.IncludingInherited)
.Where(property => !property.IsReallyStatic() && property.HasSetter()))
.ToArray();

/// <summary>
/// Gets a value indicating whether the <see cref="Type" /> has an accessible parameterless (default) constructor.
/// </summary>
Expand Down
73 changes: 73 additions & 0 deletions Source/aweXpect.Reflection/ThatType.IsImmutable.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using System;
using System.Text;
using aweXpect.Core;
using aweXpect.Core.Constraints;
using aweXpect.Reflection.Helpers;
using aweXpect.Results;

namespace aweXpect.Reflection;

public static partial class ThatType
{
/// <summary>
/// Verifies that the <see cref="Type" /> is immutable.
/// </summary>
/// <remarks>
/// A type is considered immutable when all instance fields (including inherited ones) are
/// <see langword="readonly" /> and all instance properties (including inherited ones) have no setter
/// or an init-only setter.
/// </remarks>
public static AndOrResult<Type?, IThat<Type?>> IsImmutable(
this IThat<Type?> subject)
=> new(subject.Get().ExpectationBuilder.AddConstraint((it, grammars)
=> new IsImmutableConstraint(it, grammars)),
subject);

/// <summary>
/// Verifies that the <see cref="Type" /> is not immutable.
/// </summary>
/// <remarks>
/// A type is considered immutable when all instance fields (including inherited ones) are
/// <see langword="readonly" /> and all instance properties (including inherited ones) have no setter
/// or an init-only setter.
/// </remarks>
public static AndOrResult<Type?, IThat<Type?>> IsNotImmutable(
this IThat<Type?> subject)
=> new(subject.Get().ExpectationBuilder.AddConstraint((it, grammars)
=> new IsImmutableConstraint(it, grammars).Invert()),
subject);

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

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

protected override void AppendNormalResult(StringBuilder stringBuilder, string? indentation = null)
{
// The mutable members are only needed for this failure message, so they are collected lazily here
// instead of on every (typically succeeding) evaluation.
stringBuilder.Append(It).Append(" was mutable ");
Formatter.Format(stringBuilder, Actual);
stringBuilder.Append(" with mutable members ");
Formatter.Format(stringBuilder, Actual!.GetMutableMembers(), FormattingOptions.Indented(indentation));
}

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

protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null)
{
stringBuilder.Append(It).Append(" was immutable ");
Formatter.Format(stringBuilder, Actual);
}
}
}
148 changes: 148 additions & 0 deletions Source/aweXpect.Reflection/ThatTypes.AreImmutable.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
using System;
using System.Collections.Generic;
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 ThatTypes
{
/// <summary>
/// Verifies that all items in the filtered collection of <see cref="Type" /> are immutable.
/// </summary>
/// <remarks>
/// A type is considered immutable when all instance fields (including inherited ones) are
/// <see langword="readonly" /> and all instance properties (including inherited ones) have no setter
/// or an init-only setter.
/// </remarks>
public static AndOrResult<IEnumerable<Type?>, IThat<IEnumerable<Type?>>> AreImmutable(
this IThat<IEnumerable<Type?>> subject)
=> new(subject.Get().ExpectationBuilder.AddConstraint<IEnumerable<Type?>>((it, grammars)
=> new AreImmutableConstraint(it, grammars)),
subject);

#if NET8_0_OR_GREATER
/// <summary>
/// Verifies that all items in the filtered collection of <see cref="Type" /> are immutable.
/// </summary>
/// <remarks>
/// A type is considered immutable when all instance fields (including inherited ones) are
/// <see langword="readonly" /> and all instance properties (including inherited ones) have no setter
/// or an init-only setter.
/// </remarks>
public static AndOrResult<IAsyncEnumerable<Type?>, IThat<IAsyncEnumerable<Type?>>> AreImmutable(
this IThat<IAsyncEnumerable<Type?>> subject)
=> new(subject.Get().ExpectationBuilder.AddConstraint<IAsyncEnumerable<Type?>>((it, grammars)
=> new AreImmutableConstraint(it, grammars)),
subject);
#endif

/// <summary>
/// Verifies that all items in the filtered collection of <see cref="Type" /> are not immutable.
/// </summary>
/// <remarks>
/// A type is considered immutable when all instance fields (including inherited ones) are
/// <see langword="readonly" /> and all instance properties (including inherited ones) have no setter
/// or an init-only setter.
/// </remarks>
public static AndOrResult<IEnumerable<Type?>, IThat<IEnumerable<Type?>>> AreNotImmutable(
this IThat<IEnumerable<Type?>> subject)
=> new(subject.Get().ExpectationBuilder.AddConstraint<IEnumerable<Type?>>((it, grammars)
=> new AreNotImmutableConstraint(it, grammars)),
subject);

#if NET8_0_OR_GREATER
/// <summary>
/// Verifies that all items in the filtered collection of <see cref="Type" /> are not immutable.
/// </summary>
/// <remarks>
/// A type is considered immutable when all instance fields (including inherited ones) are
/// <see langword="readonly" /> and all instance properties (including inherited ones) have no setter
/// or an init-only setter.
/// </remarks>
public static AndOrResult<IAsyncEnumerable<Type?>, IThat<IAsyncEnumerable<Type?>>> AreNotImmutable(
this IThat<IAsyncEnumerable<Type?>> subject)
=> new(subject.Get().ExpectationBuilder.AddConstraint<IAsyncEnumerable<Type?>>((it, grammars)
=> new AreNotImmutableConstraint(it, grammars)),
subject);
#endif

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

public ConstraintResult IsMetBy(IEnumerable<Type?> actual)
=> SetValue(actual, type => type.IsImmutable());

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

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

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

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

private sealed class AreNotImmutableConstraint(string it, ExpectationGrammars grammars)
: CollectionConstraintResult<Type?>(grammars),
IValueConstraint<IEnumerable<Type?>>
#if NET8_0_OR_GREATER
, IAsyncConstraint<IAsyncEnumerable<Type?>>
#endif
{
#if NET8_0_OR_GREATER
public async Task<ConstraintResult> IsMetBy(IAsyncEnumerable<Type?> actual,
CancellationToken cancellationToken)
=> await SetAsyncValue(actual, type => !type.IsImmutable());
#endif

public ConstraintResult IsMetBy(IEnumerable<Type?> actual)
=> SetValue(actual, type => !type.IsImmutable());

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

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

protected override void AppendNegatedExpectation(StringBuilder stringBuilder, string? indentation = null)
=> stringBuilder.Append("also contain an immutable type");

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