@@ -788,10 +788,11 @@ internal static bool MatchesType(Type dependency, Type target)
788788 /// element type.
789789 /// </summary>
790790 /// <remarks>
791- /// Shared between dependency unwrapping (<see cref="Unwrap" />) and target matching
792- /// (<see cref="MatchesType" />), so that the two sides of the documented symmetry cannot drift apart.
791+ /// Shared between dependency unwrapping (<see cref="Unwrap" />), target matching
792+ /// (<see cref="MatchesType" />) and target-set resolution (<see cref="TypeSetDependencyOptions.Resolve" />),
793+ /// so that the sides of the documented symmetry cannot drift apart.
793794 /// </remarks>
794- private static Type StripElementTypes ( Type type )
795+ internal static Type StripElementTypes ( Type type )
795796 {
796797 while ( type . HasElementType && type . GetElementType ( ) is { } elementType )
797798 {
@@ -877,17 +878,12 @@ private static bool IsFrameworkDependency(this Type type, string[] excludedPrefi
877878 internal static IReadOnlyList < string > GetDependencyNamespaceViolations (
878879 this Type type , NamespaceDependencyOptions allowed )
879880 {
880- string ? ownNamespace = type . Namespace ;
881- string [ ] excludedPrefixes = Customize . aweXpect . Reflection ( ) . ExcludedAssemblyPrefixes . Get ( ) ;
882881 List < string > violations = [ ] ;
883882 HashSet < string > seen = new ( StringComparer . Ordinal ) ;
884- foreach ( Type dependency in type . ResolveDependencies ( ) )
883+ foreach ( Type dependency in type . GetDependencyViolations (
884+ ( dependency , ownNamespace , excludedPrefixes )
885+ => IsDependencyViolation ( dependency , ownNamespace , allowed , excludedPrefixes ) ) )
885886 {
886- if ( ! IsDependencyViolation ( dependency , ownNamespace , allowed , excludedPrefixes ) )
887- {
888- continue ;
889- }
890-
891887 string display = dependency . Namespace ?? GlobalNamespaceDisplay ;
892888 if ( seen . Add ( display ) )
893889 {
@@ -908,24 +904,45 @@ internal static IReadOnlyList<string> GetDependencyNamespaceViolations(
908904 /// a verdict and not the violation list.
909905 /// </remarks>
910906 internal static bool HasDependencyNamespaceViolations ( this Type type , NamespaceDependencyOptions allowed )
911- {
912- string ? ownNamespace = type . Namespace ;
913- string [ ] excludedPrefixes = Customize . aweXpect . Reflection ( ) . ExcludedAssemblyPrefixes . Get ( ) ;
914- return type . ResolveDependencies ( )
915- . Any ( dependency => IsDependencyViolation ( dependency , ownNamespace , allowed , excludedPrefixes ) ) ;
916- }
907+ => type . GetDependencyViolations (
908+ ( dependency , ownNamespace , excludedPrefixes )
909+ => IsDependencyViolation ( dependency , ownNamespace , allowed , excludedPrefixes ) ) . Any ( ) ;
917910
918911 private static bool IsDependencyViolation (
919912 Type dependency , string ? ownNamespace , NamespaceDependencyOptions allowed , string [ ] excludedPrefixes )
920- {
921- if ( dependency . IsFrameworkDependency ( excludedPrefixes ) )
922- {
923- return false ;
924- }
913+ => ! IsExemptDependency ( dependency , ownNamespace , allowed . IncludeOwnSubNamespaces , excludedPrefixes ) &&
914+ ! allowed . Matches ( dependency . Namespace ) ;
915+
916+ /// <summary>
917+ /// Checks the exemptions shared by all only-on rules: framework dependencies and dependencies in the
918+ /// type's own namespace are always allowed.
919+ /// </summary>
920+ /// <remarks>
921+ /// Shared between the namespace-based and type-set-based violation predicates, so that a future rule
922+ /// change cannot be applied to one family and missed in the other.
923+ /// </remarks>
924+ private static bool IsExemptDependency (
925+ Type dependency , string ? ownNamespace , bool includeOwnSubNamespaces , string [ ] excludedPrefixes )
926+ => dependency . IsFrameworkDependency ( excludedPrefixes ) ||
927+ IsOwnNamespace ( dependency . Namespace , ownNamespace , includeOwnSubNamespaces ) ;
925928
926- string ? dependencyNamespace = dependency . Namespace ;
927- return ! IsOwnNamespace ( dependencyNamespace , ownNamespace , allowed . IncludeOwnSubNamespaces ) &&
928- ! allowed . Matches ( dependencyNamespace ) ;
929+ /// <summary>
930+ /// Enumerates the <paramref name="type" />'s dependencies that the <paramref name="isViolation" />
931+ /// predicate flags as violations, supplying the type's own namespace and the configured excluded
932+ /// assembly prefixes.
933+ /// </summary>
934+ /// <remarks>
935+ /// Shared core of the namespace-based and type-set-based violation helpers, so that the two families
936+ /// cannot drift apart in how the dependencies are walked. Lazy, so verdict-only callers stop at the
937+ /// first violation.
938+ /// </remarks>
939+ private static IEnumerable < Type > GetDependencyViolations (
940+ this Type type , Func < Type , string ? , string [ ] , bool > isViolation )
941+ {
942+ string ? ownNamespace = type . Namespace ;
943+ string [ ] excludedPrefixes = Customize . aweXpect . Reflection ( ) . ExcludedAssemblyPrefixes . Get ( ) ;
944+ return type . ResolveDependencies ( )
945+ . Where ( dependency => isViolation ( dependency , ownNamespace , excludedPrefixes ) ) ;
929946 }
930947
931948 private static bool IsOwnNamespace ( string ? dependencyNamespace , string ? ownNamespace , bool includeSubNamespaces )
@@ -940,26 +957,30 @@ private static bool IsOwnNamespace(string? dependencyNamespace, string? ownNames
940957 /// <remarks>
941958 /// Same framework and own-namespace rules as <see cref="GetDependencyNamespaceViolations" />, but the allowed
942959 /// set is a concrete set of types, so the violations are reported as formatted type names instead of
943- /// namespaces. Requires <see cref="TypeSetDependencyOptions.Resolve" /> to have been awaited before.
960+ /// namespaces. Distinct violators sharing a formatted name (the same simple name in different namespaces)
961+ /// are qualified by their namespace, so they do not collapse into one indistinguishable entry.
962+ /// Requires <see cref="TypeSetDependencyOptions.Resolve" /> to have been awaited before.
944963 /// </remarks>
945964 internal static IReadOnlyList < string > GetDependencyTypeSetViolations (
946965 this Type type , TypeSetDependencyOptions allowed )
947966 {
948- string ? ownNamespace = type . Namespace ;
949- string [ ] excludedPrefixes = Customize . aweXpect . Reflection ( ) . ExcludedAssemblyPrefixes . Get ( ) ;
950967 List < string > violations = [ ] ;
951- HashSet < string > seen = new ( StringComparer . Ordinal ) ;
952- foreach ( Type dependency in type . ResolveDependencies ( ) )
953- {
954- if ( ! IsDependencyTypeSetViolation ( dependency , ownNamespace , allowed , excludedPrefixes ) )
968+ foreach ( IGrouping < string , Type > sameName in type . GetDependencyViolations (
969+ ( dependency , ownNamespace , excludedPrefixes )
970+ => IsDependencyTypeSetViolation ( dependency , ownNamespace , allowed , excludedPrefixes ) )
971+ . Distinct ( )
972+ . GroupBy ( dependency => Formatter . Format ( dependency ) , StringComparer . Ordinal ) )
973+ {
974+ Type [ ] violators = sameName . ToArray ( ) ;
975+ if ( violators . Length == 1 )
955976 {
956- continue ;
977+ violations . Add ( sameName . Key ) ;
957978 }
958-
959- string display = Formatter . Format ( dependency ) ;
960- if ( seen . Add ( display ) )
979+ else
961980 {
962- violations . Add ( display ) ;
981+ violations . AddRange ( violators . Select ( violator => violator . Namespace is null
982+ ? sameName . Key
983+ : $ "{ violator . Namespace } .{ sameName . Key } ") ) ;
963984 }
964985 }
965986
@@ -976,24 +997,14 @@ internal static IReadOnlyList<string> GetDependencyTypeSetViolations(
976997 /// a verdict and not the violation list.
977998 /// </remarks>
978999 internal static bool HasDependencyTypeSetViolations ( this Type type , TypeSetDependencyOptions allowed )
979- {
980- string ? ownNamespace = type . Namespace ;
981- string [ ] excludedPrefixes = Customize . aweXpect . Reflection ( ) . ExcludedAssemblyPrefixes . Get ( ) ;
982- return type . ResolveDependencies ( )
983- . Any ( dependency => IsDependencyTypeSetViolation ( dependency , ownNamespace , allowed , excludedPrefixes ) ) ;
984- }
1000+ => type . GetDependencyViolations (
1001+ ( dependency , ownNamespace , excludedPrefixes )
1002+ => IsDependencyTypeSetViolation ( dependency , ownNamespace , allowed , excludedPrefixes ) ) . Any ( ) ;
9851003
9861004 private static bool IsDependencyTypeSetViolation (
9871005 Type dependency , string ? ownNamespace , TypeSetDependencyOptions allowed , string [ ] excludedPrefixes )
988- {
989- if ( dependency . IsFrameworkDependency ( excludedPrefixes ) )
990- {
991- return false ;
992- }
993-
994- return ! IsOwnNamespace ( dependency . Namespace , ownNamespace , true ) &&
995- ! allowed . Matches ( dependency ) ;
996- }
1006+ => ! IsExemptDependency ( dependency , ownNamespace , true , excludedPrefixes ) &&
1007+ ! allowed . Matches ( dependency ) ;
9971008
9981009 /// <summary>
9991010 /// Resolves the dependencies of the <paramref name="type" /> through which all assertions and filters go,
0 commit comments