Skip to content

Commit 1dd86bd

Browse files
committed
refactor: harden type-set dependency targets after second review
- DependsOnlyOn/DependOnlyOn/WhichDependOnlyOn with Filtered.Types targets return new only-on results exposing ExcludingOwnSubNamespaces, mirroring the namespace family (the own-sub-namespace exemption was hard-coded) - TypeSetDependencyOptions.Resolve returns a ResolvedTypeSet that performs all matching, so matching against an unresolved target set is unrepresentable; the assertion CancellationToken is forwarded - share the constructed-generic matching rule between MatchesType and the type-set matcher via TypeHelpers.GetGenericTypeDefinitionOfConstruction - parenthesize the target description in the type-set filter suffixes, so the target scope no longer runs into the subject collection scope - drop a provably dead Distinct() in GetDependencyTypeSetViolations - make all WhichDependOn/WhichDoNotDependOn/DependOn/DoNotDependOn overloads adjacent (Sonar S4136) - README: promote type dependencies and architecture rules into their own top-level section and avoid em-dashes
1 parent 754ab14 commit 1dd86bd

18 files changed

Lines changed: 672 additions & 363 deletions

README.md

Lines changed: 227 additions & 218 deletions
Large diffs are not rendered by default.

Source/aweXpect.Reflection/Collections/Filtered.Types.cs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -577,5 +577,56 @@ public TypeSetDependencyFilterResult OrOn(params Filtered.Types[] targets)
577577
return new TypeSetDependencyFilterResult(widened, _build);
578578
}
579579
}
580+
581+
/// <summary>
582+
/// A filtered collection of <see cref="System.Type" /> from a depends-only-on filter whose allowed
583+
/// targets are filtered collections of types, allowing to widen the allowed collections and to opt out
584+
/// of the implicit allowance of the type's own sub-namespaces.
585+
/// </summary>
586+
/// <remarks>
587+
/// Like all filtered collections, this is an immutable value object: <see cref="OrOn" /> and
588+
/// <see cref="ExcludingOwnSubNamespaces" /> do not mutate this instance but rebuild a fresh filter from
589+
/// the original base collection, so deriving multiple views from the same instance cannot corrupt each
590+
/// other.
591+
/// </remarks>
592+
public sealed class TypeSetDependencyOnlyOnFilterResult : Types
593+
{
594+
private readonly Func<TypeSetDependencyOptions, Types> _build;
595+
private readonly TypeSetDependencyOptions _options;
596+
597+
internal TypeSetDependencyOnlyOnFilterResult(
598+
TypeSetDependencyOptions options,
599+
Func<TypeSetDependencyOptions, Types> build)
600+
: base(build(options))
601+
{
602+
_options = options;
603+
_build = build;
604+
}
605+
606+
/// <summary>
607+
/// Widens the filter by the given <paramref name="targets" />.
608+
/// </summary>
609+
public TypeSetDependencyOnlyOnFilterResult OrOn(params Filtered.Types[] targets)
610+
{
611+
TypeSetDependencyOptions widened = _options.Copy();
612+
widened.OrOn(targets);
613+
return new TypeSetDependencyOnlyOnFilterResult(widened, _build);
614+
}
615+
616+
/// <summary>
617+
/// Excludes sub-namespaces of the type's own namespace from being implicitly allowed (so a <c>Foo</c>
618+
/// type referencing <c>Foo.Bar</c> is filtered out unless <c>Foo.Bar</c> types are part of an allowed
619+
/// collection).
620+
/// </summary>
621+
/// <remarks>
622+
/// The type's own namespace itself is always allowed.
623+
/// </remarks>
624+
public TypeSetDependencyOnlyOnFilterResult ExcludingOwnSubNamespaces()
625+
{
626+
TypeSetDependencyOptions refined = _options.Copy();
627+
refined.ExcludingOwnSubNamespaces();
628+
return new TypeSetDependencyOnlyOnFilterResult(refined, _build);
629+
}
630+
}
580631
}
581632
}

Source/aweXpect.Reflection/Filters/TypeFilters.WhichDependOn.cs

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,6 @@ public static Filtered.Types.NamespaceDependencyFilterResult WhichDependOn(
1818
type => options.IsMatchedBy(type),
1919
() => $"which depend on {options.Describe()} ")));
2020

21-
/// <summary>
22-
/// Filter for types which do not depend on (do not reference in their signature) any type in one of the
23-
/// <paramref name="namespaces" /> (including sub-namespaces).
24-
/// </summary>
25-
public static Filtered.Types.NamespaceDependencyFilterResult WhichDoNotDependOn(
26-
this Filtered.Types @this, params IEnumerable<string> namespaces)
27-
=> new(new NamespaceDependencyOptions(namespaces),
28-
options => @this.Which(Filter.Suffix<Type>(
29-
type => !options.IsMatchedBy(type),
30-
() => $"which do not depend on {options.Describe()} ")));
31-
3221
/// <summary>
3322
/// Filter for types which depend on (reference in their signature) at least one type in the filtered
3423
/// collections of types <paramref name="target" /> or <paramref name="additional" />.
@@ -44,10 +33,23 @@ public static Filtered.Types.TypeSetDependencyFilterResult WhichDependOn(
4433
options => @this.Which(Filter.Suffix<Type>(
4534
async type =>
4635
{
47-
await options.Resolve();
48-
return options.IsMatchedBy(type);
36+
ResolvedTypeSet targetSet = await options.Resolve();
37+
return targetSet.IsMatchedBy(type);
4938
},
50-
() => $"which depend on {options.Describe()} ")));
39+
// The parentheses delimit the target description (which ends in the target's source scope,
40+
// e.g. "in all loaded assemblies") from the subject collection's own source suffix.
41+
() => $"which depend on ({options.Describe()}) ")));
42+
43+
/// <summary>
44+
/// Filter for types which do not depend on (do not reference in their signature) any type in one of the
45+
/// <paramref name="namespaces" /> (including sub-namespaces).
46+
/// </summary>
47+
public static Filtered.Types.NamespaceDependencyFilterResult WhichDoNotDependOn(
48+
this Filtered.Types @this, params IEnumerable<string> namespaces)
49+
=> new(new NamespaceDependencyOptions(namespaces),
50+
options => @this.Which(Filter.Suffix<Type>(
51+
type => !options.IsMatchedBy(type),
52+
() => $"which do not depend on {options.Describe()} ")));
5153

5254
/// <summary>
5355
/// Filter for types which do not depend on (do not reference in their signature) any type in the filtered
@@ -64,8 +66,10 @@ public static Filtered.Types.TypeSetDependencyFilterResult WhichDoNotDependOn(
6466
options => @this.Which(Filter.Suffix<Type>(
6567
async type =>
6668
{
67-
await options.Resolve();
68-
return !options.IsMatchedBy(type);
69+
ResolvedTypeSet targetSet = await options.Resolve();
70+
return !targetSet.IsMatchedBy(type);
6971
},
70-
() => $"which do not depend on {options.Describe()} ")));
72+
// The parentheses delimit the target description (which ends in the target's source scope,
73+
// e.g. "in all loaded assemblies") from the subject collection's own source suffix.
74+
() => $"which do not depend on ({options.Describe()}) ")));
7175
}

Source/aweXpect.Reflection/Filters/TypeFilters.WhichDependOnlyOn.cs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@ public static Filtered.Types.NamespaceDependencyOnlyOnFilterResult WhichDependOn
3737
/// <remarks>
3838
/// The target collections are resolved once per filter; a dependency is allowed when it is a member of the
3939
/// union of the resolved collections (by <see cref="Type" /> identity; a generic type definition in a
40-
/// collection matches any construction of it).
40+
/// collection matches any construction of it). A type's own namespace is always allowed, including its
41+
/// sub-namespaces unless
42+
/// <see cref="Filtered.Types.TypeSetDependencyOnlyOnFilterResult.ExcludingOwnSubNamespaces" /> is used.
4143
/// <para />
4244
/// Dependencies on types whose assembly name matches one of the
4345
/// <see cref="AwexpectCustomization.ReflectionCustomizationValue.ExcludedAssemblyPrefixes" /> at a
@@ -47,14 +49,16 @@ public static Filtered.Types.NamespaceDependencyOnlyOnFilterResult WhichDependOn
4749
/// <c>Microsoft</c>, so e.g. <c>Microsoft.EntityFrameworkCore</c> is also ignored; forbid such a dependency
4850
/// explicitly via <c>WhichDoNotDependOn</c> or customize the prefixes.
4951
/// </remarks>
50-
public static Filtered.Types.TypeSetDependencyFilterResult WhichDependOnlyOn(
52+
public static Filtered.Types.TypeSetDependencyOnlyOnFilterResult WhichDependOnlyOn(
5153
this Filtered.Types @this, Filtered.Types target, params Filtered.Types[] additional)
5254
=> new(new TypeSetDependencyOptions(target, additional),
5355
options => @this.Which(Filter.Suffix<Type>(
5456
async type =>
5557
{
56-
await options.Resolve();
57-
return !type.HasDependencyTypeSetViolations(options);
58+
ResolvedTypeSet allowed = await options.Resolve();
59+
return !type.HasDependencyTypeSetViolations(allowed);
5860
},
59-
() => $"which depend only on {options.Describe()} ")));
61+
// The parentheses delimit the target description (which ends in the target's source scope,
62+
// e.g. "in all loaded assemblies") from the subject collection's own source suffix.
63+
() => $"which depend only on ({options.Describe()}) ")));
6064
}

Source/aweXpect.Reflection/Helpers/TypeHelpers.cs

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -773,16 +773,24 @@ 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&lt;&gt;</c>) when the <paramref name="dependency" />
782+
/// is a constructed generic type (e.g. <c>List&lt;Foo&gt;</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.
@@ -959,16 +967,14 @@ private static bool IsOwnNamespace(string? dependencyNamespace, string? ownNames
959967
/// set is a concrete set of types, so the violations are reported as formatted type names instead of
960968
/// namespaces. Distinct violators sharing a formatted name (the same simple name in different namespaces)
961969
/// 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.
963970
/// </remarks>
964971
internal static IReadOnlyList<string> GetDependencyTypeSetViolations(
965-
this Type type, TypeSetDependencyOptions allowed)
972+
this Type type, ResolvedTypeSet allowed)
966973
{
967974
List<string> violations = [];
968975
foreach (IGrouping<string, Type> sameName in type.GetDependencyViolations(
969976
(dependency, ownNamespace, excludedPrefixes)
970977
=> IsDependencyTypeSetViolation(dependency, ownNamespace, allowed, excludedPrefixes))
971-
.Distinct()
972978
.GroupBy(dependency => Formatter.Format(dependency), StringComparer.Ordinal))
973979
{
974980
Type[] violators = sameName.ToArray();
@@ -996,14 +1002,14 @@ internal static IReadOnlyList<string> GetDependencyTypeSetViolations(
9961002
/// Same rules as <see cref="GetDependencyTypeSetViolations" />, for callers (like filters) that only need
9971003
/// a verdict and not the violation list.
9981004
/// </remarks>
999-
internal static bool HasDependencyTypeSetViolations(this Type type, TypeSetDependencyOptions allowed)
1005+
internal static bool HasDependencyTypeSetViolations(this Type type, ResolvedTypeSet allowed)
10001006
=> type.GetDependencyViolations(
10011007
(dependency, ownNamespace, excludedPrefixes)
10021008
=> IsDependencyTypeSetViolation(dependency, ownNamespace, allowed, excludedPrefixes)).Any();
10031009

10041010
private static bool IsDependencyTypeSetViolation(
1005-
Type dependency, string? ownNamespace, TypeSetDependencyOptions allowed, string[] excludedPrefixes)
1006-
=> !IsExemptDependency(dependency, ownNamespace, true, excludedPrefixes) &&
1011+
Type dependency, string? ownNamespace, ResolvedTypeSet allowed, string[] excludedPrefixes)
1012+
=> !IsExemptDependency(dependency, ownNamespace, allowed.IncludeOwnSubNamespaces, excludedPrefixes) &&
10071013
!allowed.Matches(dependency);
10081014

10091015
/// <summary>

0 commit comments

Comments
 (0)