diff --git a/Source/Testably.Architecture.Rules/Extensions/TypeExtensions.cs b/Source/Testably.Architecture.Rules/Extensions/TypeExtensions.cs index 3547071..50152e4 100644 --- a/Source/Testably.Architecture.Rules/Extensions/TypeExtensions.cs +++ b/Source/Testably.Architecture.Rules/Extensions/TypeExtensions.cs @@ -68,6 +68,45 @@ public static bool HasMethodWithAttribute( a => predicate(a, method), inherit)); } + /// + /// Determines whether the current implements the . + /// + /// The . + /// The interface . + /// + /// If set to (default value), the + /// can be anywhere in the inheritance tree, otherwise if set to requires the + /// to be directly implemented in the . + /// + public static bool Implements( + this Type type, + Type interfaceType, + bool forceDirect = false) + { + if (!interfaceType.IsInterface) + { + return false; + } + + Type[] interfaces = type.GetInterfaces(); + if (forceDirect && type.BaseType != null) + { + interfaces = interfaces + .Except(type.BaseType.GetInterfaces()) + .ToArray(); + } + + return interfaces + .Any(childInterface => + { + Type currentInterface = childInterface.IsGenericType + ? childInterface.GetGenericTypeDefinition() + : childInterface; + + return currentInterface == interfaceType; + }); + } + /// /// Determines whether the current inherits from the . /// @@ -78,8 +117,7 @@ public static bool HasMethodWithAttribute( /// can be anywhere in the inheritance tree, otherwise if set to requires the /// to be the direct parent. /// - /// - public static bool Inherits( + public static bool InheritsFrom( this Type type, Type parentType, bool forceDirect = false) @@ -111,7 +149,7 @@ public static bool Inherits( return false; } - if (currentType.ImplementsInterface(parentType, forceDirect)) + if (currentType.Implements(parentType, forceDirect)) { return true; } @@ -137,25 +175,5 @@ public static bool Inherits( public static bool IsStatic(this Type type) => type.IsAbstract && type.IsSealed && !type.IsInterface && !type.GetConstructors().Any(m => m.IsPublic); - - internal static bool ImplementsInterface(this Type type, Type interfaceType, bool forceDirect) - { - Type[] interfaces = type.GetInterfaces(); - if (forceDirect && type.BaseType != null) - { - interfaces = interfaces - .Except(type.BaseType.GetInterfaces()) - .ToArray(); - } - - return interfaces - .Any(childInterface => - { - Type currentInterface = childInterface.IsGenericType - ? childInterface.GetGenericTypeDefinition() - : childInterface; - - return currentInterface == interfaceType; - }); - } + } diff --git a/Source/Testably.Architecture.Rules/Filters/FilterOnTypeExtensions.Implement.cs b/Source/Testably.Architecture.Rules/Filters/FilterOnTypeExtensions.Implement.cs new file mode 100644 index 0000000..df8c185 --- /dev/null +++ b/Source/Testably.Architecture.Rules/Filters/FilterOnTypeExtensions.Implement.cs @@ -0,0 +1,44 @@ +using System; + +namespace Testably.Architecture.Rules; + +public static partial class FilterOnTypeExtensions +{ + /// + /// Filter for types that implement the interface . + /// + public static ITypeFilterResult WhichImplement(this ITypeFilter @this, + bool forceDirect = false) + { + return @this.WhichImplement(typeof(TInterface), forceDirect); + } + + /// + /// Filter for types that implement the interface . + /// + public static ITypeFilterResult WhichImplement(this ITypeFilter @this, + Type interfaceType, + bool forceDirect = false) + { + return @this.Which(type => type.Implements(interfaceType, forceDirect)); + } + + /// + /// Filter for types that don't implement the interface . + /// + public static ITypeFilterResult WhichDoNotImplement(this ITypeFilter @this, + bool forceDirect = false) + { + return @this.WhichDoNotImplement(typeof(TInterface), forceDirect); + } + + /// + /// Filter for types that don't implement the interface . + /// + public static ITypeFilterResult WhichDoNotImplement(this ITypeFilter @this, + Type interfaceType, + bool forceDirect = false) + { + return @this.Which(type => !type.Implements(interfaceType, forceDirect)); + } +} diff --git a/Source/Testably.Architecture.Rules/Filters/FilterOnTypeExtensions.InheritFrom.cs b/Source/Testably.Architecture.Rules/Filters/FilterOnTypeExtensions.InheritFrom.cs index 3ff237f..18301cf 100644 --- a/Source/Testably.Architecture.Rules/Filters/FilterOnTypeExtensions.InheritFrom.cs +++ b/Source/Testably.Architecture.Rules/Filters/FilterOnTypeExtensions.InheritFrom.cs @@ -19,7 +19,7 @@ public static ITypeFilterResult WhichInheritFrom(this ITypeFilter @this, public static ITypeFilterResult WhichInheritFrom(this ITypeFilter @this, Type baseType, bool forceDirect = false) { - return @this.Which(type => type.Inherits(baseType, forceDirect)); + return @this.Which(type => type.InheritsFrom(baseType, forceDirect)); } /// @@ -37,6 +37,6 @@ public static ITypeFilterResult WhichDoNotInheritFrom(this ITypeFilter @t public static ITypeFilterResult WhichDoNotInheritFrom(this ITypeFilter @this, Type baseType, bool forceDirect = false) { - return @this.Which(type => !type.Inherits(baseType, forceDirect)); + return @this.Which(type => !type.InheritsFrom(baseType, forceDirect)); } } diff --git a/Source/Testably.Architecture.Rules/Requirements/RequirementOnTypeExtensions.Implement.cs b/Source/Testably.Architecture.Rules/Requirements/RequirementOnTypeExtensions.Implement.cs new file mode 100644 index 0000000..b2a4336 --- /dev/null +++ b/Source/Testably.Architecture.Rules/Requirements/RequirementOnTypeExtensions.Implement.cs @@ -0,0 +1,74 @@ +using System; + +namespace Testably.Architecture.Rules; + +public static partial class RequirementOnTypeExtensions +{ + /// + /// Expect the types to implement the . + /// + /// The type of the interface which should be implemented. + /// The . + /// + /// If set to (default value), the + /// can be anywhere in the inheritance tree, otherwise if set to requires the + /// to be directly implemented. + /// + public static IRequirementResult ShouldImplement( + this IRequirement @this, + bool forceDirect = false) + => @this.ShouldImplement(typeof(TInterface), forceDirect); + + /// + /// Expect the types to implement the . + /// + /// The . + /// The interface which should be implemented. + /// + /// If set to (default value), the + /// can be anywhere in the inheritance tree, otherwise if set to requires the + /// to be directly implemented. + /// + public static IRequirementResult ShouldImplement( + this IRequirement @this, + Type interfaceType, + bool forceDirect = false) + => @this.ShouldSatisfy(Requirement.ForType( + type => type.Implements(interfaceType, forceDirect), + type => new TypeTestError(type, + $"Type '{type.Name}' should{(forceDirect ? " directly" : "")} implement '{interfaceType.Name}'."))); + + /// + /// Expect the types to not implement the . + /// + /// The type of the interface which should be implemented. + /// The . + /// + /// If set to (default value), the + /// can be anywhere in the inheritance tree, otherwise if set to requires the + /// to be directly implemented. + /// + public static IRequirementResult ShouldNotImplement( + this IRequirement @this, + bool forceDirect = false) + => @this.ShouldNotImplement(typeof(TInterface), forceDirect); + + /// + /// Expect the types to not implement the . + /// + /// The . + /// The interface which should be implemented. + /// + /// If set to (default value), the + /// can be anywhere in the inheritance tree, otherwise if set to requires the + /// to be directly implemented. + /// + public static IRequirementResult ShouldNotImplement( + this IRequirement @this, + Type interfaceType, + bool forceDirect = false) + => @this.ShouldSatisfy(Requirement.ForType( + type => !type.Implements(interfaceType, forceDirect), + type => new TypeTestError(type, + $"Type '{type.Name}' should not{(forceDirect ? " directly" : "")} implement '{interfaceType.Name}'."))); +} diff --git a/Source/Testably.Architecture.Rules/Requirements/RequirementOnTypeExtensions.InheritFrom.cs b/Source/Testably.Architecture.Rules/Requirements/RequirementOnTypeExtensions.InheritFrom.cs index 7dcb4ad..1f45d8a 100644 --- a/Source/Testably.Architecture.Rules/Requirements/RequirementOnTypeExtensions.InheritFrom.cs +++ b/Source/Testably.Architecture.Rules/Requirements/RequirementOnTypeExtensions.InheritFrom.cs @@ -33,7 +33,7 @@ public static IRequirementResult ShouldInheritFrom( Type baseType, bool forceDirect = false) => @this.ShouldSatisfy(Requirement.ForType( - type => type.Inherits(baseType, forceDirect), + type => type.InheritsFrom(baseType, forceDirect), type => new TypeTestError(type, $"Type '{type.Name}' should{(forceDirect ? " directly" : "")} inherit from '{baseType.Name}'."))); @@ -66,7 +66,7 @@ public static IRequirementResult ShouldNotInheritFrom( Type baseType, bool forceDirect = false) => @this.ShouldSatisfy(Requirement.ForType( - type => !type.Inherits(baseType, forceDirect), + type => !type.InheritsFrom(baseType, forceDirect), type => new TypeTestError(type, $"Type '{type.Name}' should not{(forceDirect ? " directly" : "")} inherit from '{baseType.Name}'."))); } diff --git a/Tests/Testably.Architecture.Rules.Tests/Extensions/TypeExtensionsTests.cs b/Tests/Testably.Architecture.Rules.Tests/Extensions/TypeExtensionsTests.cs index b373944..4fe5560 100644 --- a/Tests/Testably.Architecture.Rules.Tests/Extensions/TypeExtensionsTests.cs +++ b/Tests/Testably.Architecture.Rules.Tests/Extensions/TypeExtensionsTests.cs @@ -85,11 +85,11 @@ public void HasMethodWithAttribute_WithPredicate_ShouldReturnPredicateResult() [Theory] [InlineData(false)] [InlineData(true)] - public void ImplementsInterface_Object_ShouldReturnFalse(bool forceDirect) + public void Implements_Object_ShouldReturnFalse(bool forceDirect) { Type sut = typeof(object); - bool result = sut.ImplementsInterface(typeof(IFooInterface), forceDirect); + bool result = sut.Implements(typeof(IFooInterface), forceDirect); result.Should().BeFalse(); } diff --git a/Tests/Testably.Architecture.Rules.Tests/Filters/FilterOnTypeExtensionsTests.Implement.cs b/Tests/Testably.Architecture.Rules.Tests/Filters/FilterOnTypeExtensionsTests.Implement.cs new file mode 100644 index 0000000..8b0d704 --- /dev/null +++ b/Tests/Testably.Architecture.Rules.Tests/Filters/FilterOnTypeExtensionsTests.Implement.cs @@ -0,0 +1,123 @@ +using FluentAssertions; +using Testably.Architecture.Rules.Tests.TestHelpers; +using Xunit; + +namespace Testably.Architecture.Rules.Tests.Filters; + +public sealed partial class FilterOnTypeExtensionsTests +{ + public sealed class ImplementTests + { + [Theory] + [InlineData(false)] + [InlineData(true)] + public void WhichImplement_WithClass_ShouldReturnFalse(bool forceDirect) + { + ITestResult result = Expect.That.Types + .WhichImplement(forceDirect) + .ShouldAlwaysFail() + .AllowEmpty() + .Check.InAllLoadedAssemblies(); + + result.ShouldNotBeViolated(); + } + + [Fact] + public void + WhichImplement_WithInterface_WithForceDirect_ShouldReturnTypesDirectlyImplementingTheInterface() + { + ITestResult result = Expect.That.Types + .WhichImplement(true) + .ShouldAlwaysFail() + .Check.InAllLoadedAssemblies(); + + result.Errors.Length.Should().Be(2); + result.Errors.Should() + .Contain(e => e.ToString().Contains(nameof(FooImplementor1))); + result.Errors.Should() + .NotContain(e => e.ToString().Contains(nameof(FooImplementor2))); + result.Errors.Should() + .Contain(e => e.ToString().Contains(typeof(FooGenericImplementor1<>).Name)); + result.Errors.Should() + .NotContain(e => e.ToString().Contains(typeof(FooGenericImplementor2<>).Name)); + } + + [Fact] + public void + WhichImplement_WithInterface_WithoutForceDirect_ShouldReturnAllTypesImplementingInterface() + { + ITestResult result = Expect.That.Types + .WhichImplement() + .ShouldAlwaysFail() + .Check.InAllLoadedAssemblies(); + + result.Errors.Length.Should().Be(4); + result.Errors.Should() + .Contain(e => e.ToString().Contains(nameof(FooImplementor1))); + result.Errors.Should() + .Contain(e => e.ToString().Contains(nameof(FooImplementor2))); + result.Errors.Should() + .Contain(e => e.ToString().Contains(typeof(FooGenericImplementor1<>).Name)); + result.Errors.Should() + .Contain(e => e.ToString().Contains(typeof(FooGenericImplementor2<>).Name)); + } + + [Fact] + public void + WhichImplement_WithGenericInterface_WithForceDirect_ShouldReturnTypesDirectlyImplementingTheInterface() + { + ITestResult result = Expect.That.Types + .WhichImplement(typeof(IGenericFooInterface<>), true) + .ShouldAlwaysFail() + .Check.InAllLoadedAssemblies(); + + result.Errors.Length.Should().Be(1); + result.Errors.Should() + .Contain(e => e.ToString().Contains(typeof(FooGenericImplementor1<>).Name)); + result.Errors.Should() + .NotContain(e => e.ToString().Contains(typeof(FooGenericImplementor2<>).Name)); + } + + [Fact] + public void + WhichImplement_WithGenericInterface_WithoutForceDirect_ShouldReturnAllTypesImplementingInterface() + { + ITestResult result = Expect.That.Types + .WhichImplement(typeof(IGenericFooInterface<>)) + .ShouldAlwaysFail() + .Check.InAllLoadedAssemblies(); + + result.Errors.Length.Should().Be(2); + result.Errors.Should() + .Contain(e => e.ToString().Contains(typeof(FooGenericImplementor1<>).Name)); + result.Errors.Should() + .Contain(e => e.ToString().Contains(typeof(FooGenericImplementor2<>).Name)); + } + + private class FooGenericImplementor1 : IGenericFooInterface, + IFooInterface + { + } + + private class FooGenericImplementor2 : FooGenericImplementor1 + { + } + + private class FooImplementor1 : IFooInterface + { + } + + private class FooImplementor2 : FooImplementor1 + { + } + + private interface IFooInterface + { + } + + // ReSharper disable once UnusedTypeParameter + private interface IGenericFooInterface + { + } + } +} diff --git a/Tests/Testably.Architecture.Rules.Tests/Requirements/RequirementOnTypeExtensionsTests.Implement.cs b/Tests/Testably.Architecture.Rules.Tests/Requirements/RequirementOnTypeExtensionsTests.Implement.cs new file mode 100644 index 0000000..64dc337 --- /dev/null +++ b/Tests/Testably.Architecture.Rules.Tests/Requirements/RequirementOnTypeExtensionsTests.Implement.cs @@ -0,0 +1,220 @@ +using FluentAssertions; +using System; +using Testably.Architecture.Rules.Tests.TestHelpers; +using Xunit; + +namespace Testably.Architecture.Rules.Tests.Requirements; + +public sealed partial class RequirementOnTypeExtensionsTests +{ + public sealed class ImplementTests + { + [Theory] + [InlineData(false)] + [InlineData(true)] + public void + ShouldImplement_WithGenericInterface_ForceDirect_ShouldConsiderParameter( + bool forceDirect) + { + Type type = typeof(GenericFooImplementor2<>); + IRule rule = Expect.That.Types + .WhichAre(type) + .ShouldImplement(typeof(IGenericFooInterface<>), + forceDirect: forceDirect); + + ITestResult result = rule.Check + .InAllLoadedAssemblies(); + + result.ShouldBeViolatedIf(forceDirect); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ShouldImplement_ForceDirect_WithInterface_ShouldConsiderParameter( + bool forceDirect) + { + Type type = typeof(FooImplementor2); + IRule rule = Expect.That.Types + .WhichAre(type) + .ShouldImplement( + forceDirect: forceDirect); + + ITestResult result = rule.Check + .InAllLoadedAssemblies(); + + result.ShouldBeViolatedIf(forceDirect); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ShouldImplement_ToString_ShouldBeCorrect(bool forceDirect) + { + Type type = typeof(FooImplementor2); + IRule rule = Expect.That.Types + .WhichAre(type) + .ShouldImplement( + forceDirect: forceDirect); + + ITestResult result = rule.Check + .InAllLoadedAssemblies(); + + result.ShouldBeViolated(); + result.Errors[0].Should().BeOfType() + .Which.Type.Should().Be(type); + result.Errors[0].ToString().Should().Contain($"Type '{type.Name}'") + .And.Contain( + $"should{(forceDirect ? " directly" : "")} implement '{nameof(IOtherFooInterface)}'."); + } + + [Fact] + public void ShouldImplement_WithGenericClass_ShouldNotBeSatisfied() + { + Type type = typeof(GenericFooImplementor2<>); + IRule rule = Expect.That.Types + .WhichAre(type) + .ShouldImplement(typeof(GenericFooImplementor1<>)); + + ITestResult result = rule.Check + .InAllLoadedAssemblies(); + + result.ShouldBeViolated(); + result.Errors[0].Should().BeOfType() + .Which.Type.Should().Be(type); + } + + [Fact] + public void ShouldImplement_WithClass_ShouldNotBeSatisfied() + { + Type type = typeof(FooImplementor2); + IRule rule = Expect.That.Types + .WhichAre(type) + .ShouldImplement(); + + ITestResult result = rule.Check + .InAllLoadedAssemblies(); + + result.ShouldBeViolated(); + result.Errors[0].Should().BeOfType() + .Which.Type.Should().Be(type); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void + ShouldNotImplement_WithGenericInterface_ForceDirect_ShouldConsiderParameter( + bool forceDirect) + { + Type type = typeof(GenericFooImplementor2<>); + IRule rule = Expect.That.Types + .WhichAre(type) + .ShouldNotImplement(typeof(IGenericFooInterface<>), + forceDirect: forceDirect); + + ITestResult result = rule.Check + .InAllLoadedAssemblies(); + + result.ShouldBeViolatedIf(!forceDirect); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ShouldNotImplement_ForceDirect_WithInterface_ShouldConsiderParameter( + bool forceDirect) + { + Type type = typeof(FooImplementor2); + IRule rule = Expect.That.Types + .WhichAre(type) + .ShouldNotImplement( + forceDirect: forceDirect); + + ITestResult result = rule.Check + .InAllLoadedAssemblies(); + + result.ShouldBeViolatedIf(!forceDirect); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ShouldNotImplement_ToString_ShouldBeCorrect(bool forceDirect) + { + Type type = typeof(FooImplementor2); + IRule rule = Expect.That.Types + .WhichAre(type) + .ShouldNotImplement( + forceDirect: forceDirect); + + ITestResult result = rule.Check + .InAllLoadedAssemblies(); + + result.ShouldNotBeViolated(); + } + + [Fact] + public void ShouldNotImplement_WithGenericClass_ShouldNotBeSatisfied() + { + Type type = typeof(GenericFooImplementor2<>); + IRule rule = Expect.That.Types + .WhichAre(type) + .ShouldNotImplement(typeof(GenericFooImplementor1<>)); + + ITestResult result = rule.Check + .InAllLoadedAssemblies(); + + result.ShouldNotBeViolated(); + } + + [Fact] + public void ShouldNotImplement_WithClass_ShouldNotBeSatisfied() + { + Type type = typeof(FooImplementor2); + IRule rule = Expect.That.Types + .WhichAre(type) + .ShouldNotImplement(); + + ITestResult result = rule.Check + .InAllLoadedAssemblies(); + + result.ShouldNotBeViolated(); + } + + private class FooImplementor1 : IFooInterface + { + } + + private class FooImplementor2 : FooImplementor1 + { + } + + private class GenericFooImplementor1 : IGenericFooInterface, + IFooInterface + { + } + + private class GenericFooImplementor2 : GenericFooImplementor1 + { + } + + private interface IFooInterface + { + } + + // ReSharper disable once UnusedTypeParameter + private interface IGenericFooInterface + { + } + + private interface IOtherFooInterface + { + } + + // ReSharper disable once UnusedTypeParameter + private interface IOtherGenericFooInterface + { + } + } +}