@@ -773,25 +773,34 @@ internal static bool MatchesType(Type dependency, Type target)
773773 {
774774 target = StripElementTypes ( target ) ;
775775
776- if ( dependency == target )
777- {
778- return true ;
779- }
780-
781- return target . IsGenericTypeDefinition &&
782- dependency . IsGenericType &&
783- dependency . GetGenericTypeDefinition ( ) == target ;
776+ return dependency == target ||
777+ dependency . GetGenericTypeDefinitionOfConstruction ( ) == target ;
784778 }
785779
780+ /// <summary>
781+ /// Returns the generic type definition (e.g. <c>List<></c>) when the <paramref name="dependency" />
782+ /// is a constructed generic type (e.g. <c>List<Foo></c>), otherwise <see langword="null" />.
783+ /// </summary>
784+ /// <remarks>
785+ /// The single encoding of the rule that a dependency on any construction also matches its generic type
786+ /// definition; shared between <see cref="MatchesType" /> and <see cref="ResolvedTypeSet.Matches" />, so
787+ /// that the specific-type and the type-set matchers cannot drift apart.
788+ /// </remarks>
789+ internal static Type ? GetGenericTypeDefinitionOfConstruction ( this Type dependency )
790+ => dependency is { IsGenericType : true , IsGenericTypeDefinition : false , }
791+ ? dependency . GetGenericTypeDefinition ( )
792+ : null ;
793+
786794 /// <summary>
787795 /// Strips array/by-ref/pointer wrappers from the <paramref name="type" />, returning the innermost
788796 /// element type.
789797 /// </summary>
790798 /// <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.
799+ /// Shared between dependency unwrapping (<see cref="Unwrap" />), target matching
800+ /// (<see cref="MatchesType" />) and target-set resolution (<see cref="TypeSetDependencyOptions.Resolve" />),
801+ /// so that the sides of the documented symmetry cannot drift apart.
793802 /// </remarks>
794- private static Type StripElementTypes ( Type type )
803+ internal static Type StripElementTypes ( Type type )
795804 {
796805 while ( type . HasElementType && type . GetElementType ( ) is { } elementType )
797806 {
@@ -877,17 +886,12 @@ private static bool IsFrameworkDependency(this Type type, string[] excludedPrefi
877886 internal static IReadOnlyList < string > GetDependencyNamespaceViolations (
878887 this Type type , NamespaceDependencyOptions allowed )
879888 {
880- string ? ownNamespace = type . Namespace ;
881- string [ ] excludedPrefixes = Customize . aweXpect . Reflection ( ) . ExcludedAssemblyPrefixes . Get ( ) ;
882889 List < string > violations = [ ] ;
883890 HashSet < string > seen = new ( StringComparer . Ordinal ) ;
884- foreach ( Type dependency in type . ResolveDependencies ( ) )
891+ foreach ( Type dependency in type . GetDependencyViolations (
892+ ( dependency , ownNamespace , excludedPrefixes )
893+ => IsDependencyViolation ( dependency , ownNamespace , allowed , excludedPrefixes ) ) )
885894 {
886- if ( ! IsDependencyViolation ( dependency , ownNamespace , allowed , excludedPrefixes ) )
887- {
888- continue ;
889- }
890-
891895 string display = dependency . Namespace ?? GlobalNamespaceDisplay ;
892896 if ( seen . Add ( display ) )
893897 {
@@ -908,30 +912,105 @@ internal static IReadOnlyList<string> GetDependencyNamespaceViolations(
908912 /// a verdict and not the violation list.
909913 /// </remarks>
910914 internal static bool HasDependencyNamespaceViolations ( this Type type , NamespaceDependencyOptions allowed )
915+ => type . GetDependencyViolations (
916+ ( dependency , ownNamespace , excludedPrefixes )
917+ => IsDependencyViolation ( dependency , ownNamespace , allowed , excludedPrefixes ) ) . Any ( ) ;
918+
919+ private static bool IsDependencyViolation (
920+ Type dependency , string ? ownNamespace , NamespaceDependencyOptions allowed , string [ ] excludedPrefixes )
921+ => ! IsExemptDependency ( dependency , ownNamespace , allowed . IncludeOwnSubNamespaces , excludedPrefixes ) &&
922+ ! allowed . Matches ( dependency . Namespace ) ;
923+
924+ /// <summary>
925+ /// Checks the exemptions shared by all only-on rules: framework dependencies and dependencies in the
926+ /// type's own namespace are always allowed.
927+ /// </summary>
928+ /// <remarks>
929+ /// Shared between the namespace-based and type-set-based violation predicates, so that a future rule
930+ /// change cannot be applied to one family and missed in the other.
931+ /// </remarks>
932+ private static bool IsExemptDependency (
933+ Type dependency , string ? ownNamespace , bool includeOwnSubNamespaces , string [ ] excludedPrefixes )
934+ => dependency . IsFrameworkDependency ( excludedPrefixes ) ||
935+ IsOwnNamespace ( dependency . Namespace , ownNamespace , includeOwnSubNamespaces ) ;
936+
937+ /// <summary>
938+ /// Enumerates the <paramref name="type" />'s dependencies that the <paramref name="isViolation" />
939+ /// predicate flags as violations, supplying the type's own namespace and the configured excluded
940+ /// assembly prefixes.
941+ /// </summary>
942+ /// <remarks>
943+ /// Shared core of the namespace-based and type-set-based violation helpers, so that the two families
944+ /// cannot drift apart in how the dependencies are walked. Lazy, so verdict-only callers stop at the
945+ /// first violation.
946+ /// </remarks>
947+ private static IEnumerable < Type > GetDependencyViolations (
948+ this Type type , Func < Type , string ? , string [ ] , bool > isViolation )
911949 {
912950 string ? ownNamespace = type . Namespace ;
913951 string [ ] excludedPrefixes = Customize . aweXpect . Reflection ( ) . ExcludedAssemblyPrefixes . Get ( ) ;
914952 return type . ResolveDependencies ( )
915- . Any ( dependency => IsDependencyViolation ( dependency , ownNamespace , allowed , excludedPrefixes ) ) ;
953+ . Where ( dependency => isViolation ( dependency , ownNamespace , excludedPrefixes ) ) ;
916954 }
917955
918- private static bool IsDependencyViolation (
919- Type dependency , string ? ownNamespace , NamespaceDependencyOptions allowed , string [ ] excludedPrefixes )
956+ private static bool IsOwnNamespace ( string ? dependencyNamespace , string ? ownNamespace , bool includeSubNamespaces )
957+ => ownNamespace is null
958+ ? dependencyNamespace is null
959+ : NamespaceMatches ( dependencyNamespace , ownNamespace , includeSubNamespaces ) ;
960+
961+ /// <summary>
962+ /// Collects the <paramref name="type" />'s dependencies that are not allowed by the resolved target set in
963+ /// <paramref name="allowed" />, the type's own namespace, or the framework rule.
964+ /// </summary>
965+ /// <remarks>
966+ /// Same framework and own-namespace rules as <see cref="GetDependencyNamespaceViolations" />, but the allowed
967+ /// set is a concrete set of types, so the violations are reported as formatted type names instead of
968+ /// namespaces. Distinct violators sharing a formatted name (the same simple name in different namespaces)
969+ /// are qualified by their namespace, so they do not collapse into one indistinguishable entry.
970+ /// </remarks>
971+ internal static IReadOnlyList < string > GetDependencyTypeSetViolations (
972+ this Type type , ResolvedTypeSet allowed )
920973 {
921- if ( dependency . IsFrameworkDependency ( excludedPrefixes ) )
974+ List < string > violations = [ ] ;
975+ foreach ( IGrouping < string , Type > sameName in type . GetDependencyViolations (
976+ ( dependency , ownNamespace , excludedPrefixes )
977+ => IsDependencyTypeSetViolation ( dependency , ownNamespace , allowed , excludedPrefixes ) )
978+ . GroupBy ( dependency => Formatter . Format ( dependency ) , StringComparer . Ordinal ) )
922979 {
923- return false ;
980+ Type [ ] violators = sameName . ToArray ( ) ;
981+ if ( violators . Length == 1 )
982+ {
983+ violations . Add ( sameName . Key ) ;
984+ }
985+ else
986+ {
987+ violations . AddRange ( violators . Select ( violator => violator . Namespace is null
988+ ? sameName . Key
989+ : $ "{ violator . Namespace } .{ sameName . Key } ") ) ;
990+ }
924991 }
925992
926- string ? dependencyNamespace = dependency . Namespace ;
927- return ! IsOwnNamespace ( dependencyNamespace , ownNamespace , allowed . IncludeOwnSubNamespaces ) &&
928- ! allowed . Matches ( dependencyNamespace ) ;
993+ violations . Sort ( StringComparer . Ordinal ) ;
994+ return violations ;
929995 }
930996
931- private static bool IsOwnNamespace ( string ? dependencyNamespace , string ? ownNamespace , bool includeSubNamespaces )
932- => ownNamespace is null
933- ? dependencyNamespace is null
934- : NamespaceMatches ( dependencyNamespace , ownNamespace , includeSubNamespaces ) ;
997+ /// <summary>
998+ /// Checks whether the <paramref name="type" /> has at least one dependency outside the resolved target set,
999+ /// stopping at the first one.
1000+ /// </summary>
1001+ /// <remarks>
1002+ /// Same rules as <see cref="GetDependencyTypeSetViolations" />, for callers (like filters) that only need
1003+ /// a verdict and not the violation list.
1004+ /// </remarks>
1005+ internal static bool HasDependencyTypeSetViolations ( this Type type , ResolvedTypeSet allowed )
1006+ => type . GetDependencyViolations (
1007+ ( dependency , ownNamespace , excludedPrefixes )
1008+ => IsDependencyTypeSetViolation ( dependency , ownNamespace , allowed , excludedPrefixes ) ) . Any ( ) ;
1009+
1010+ private static bool IsDependencyTypeSetViolation (
1011+ Type dependency , string ? ownNamespace , ResolvedTypeSet allowed , string [ ] excludedPrefixes )
1012+ => ! IsExemptDependency ( dependency , ownNamespace , allowed . IncludeOwnSubNamespaces , excludedPrefixes ) &&
1013+ ! allowed . Matches ( dependency ) ;
9351014
9361015 /// <summary>
9371016 /// Resolves the dependencies of the <paramref name="type" /> through which all assertions and filters go,
0 commit comments