diff --git a/README.md b/README.md index 2575ff91..4d4d14b1 100644 --- a/README.md +++ b/README.md @@ -413,147 +413,6 @@ In.AllLoadedAssemblies().Methods() .WhichAreGeneric().WithArgument("TKey").AtIndex(0) ``` -### Type dependencies - -Layering and architecture rules over the types a type references **in its signature**: - -| | Filter | Assert (single) | Assert (many) | -|--------------------------|------------------------------|-------------------------|------------------------| -| depends on namespace | `.WhichDependOn("x", …)` | `.DependsOn("x", …)` | `.DependOn("x", …)` | -| does not depend on | `.WhichDoNotDependOn("x", …)`| `.DoesNotDependOn("x", …)` | `.DoNotDependOn("x", …)` | -| depends only on set | `.WhichDependOnlyOn("x", …)` | `.DependsOnlyOn("x", …)`| `.DependOnlyOn("x", …)`| - -```csharp -// Presentation must not reference the data layer -await Expect.That(Types.InNamespace("MyApp.Presentation")) - .DoNotDependOn("MyApp.Data"); - -// The API layer may only reference the application and domain layers -await Expect.That(Types.InNamespace("MyApp.Api")) - .DependOnlyOn("MyApp.Application", "MyApp.Domain"); - -// Filter for the types that depend on a namespace -In.AllLoadedAssemblies().Types().WhichDependOn("System.Data") -``` - -A type *depends on* every type referenced in its **declared signature**: the base type and directly -implemented interfaces, generic arguments and parameter constraints, field/property/event types, indexer -parameters, method return/parameter/generic-argument types, constructor parameters and the types of attributes -applied to the type, its members, their parameters and return values (including `typeof(…)` and enum attribute -arguments). Element types of -arrays/pointers/by-ref and generic type arguments are unwrapped (`List` depends on `List`, -which also matches a `List<>` target, and on `Infra.Foo`; a closed-generic target like `List` -only matches that exact construction). Purely synthetic references that you never wrote are ignored: -compiler-generated members, the implicit `object`/`ValueType`/`Enum` base type, interfaces inherited from the -base type, records' synthesized `IEquatable`, delegates' runtime infrastructure (only the `Invoke` -signature counts), enums' underlying-value plumbing and the attributes the compiler emits onto authored code -(nullability metadata, required members, async/iterator state machines, …), so the compiler's own plumbing -never counts. Should a future compiler version emit a marker attribute this library does not know about yet, -exclude it yourself via `Customize.aweXpect.Reflection().ExcludedAttributeTypes()` (full attribute type names; -extends the built-in set). Types you write in authored signatures always do count, including primitives and -`void` return types (namespace `System`); in practice, almost every type with members *does* depend on -`System`. - -> **Signature-level only:** dependencies are computed from reflection metadata, so body-level references such -> as `new Infra.Foo()`, static calls and local variables are **not** detected. Function-pointer signatures -> (`delegate*<…>`) are not decomposed either; the types inside them are invisible to dependency assertions. -> Nested types are separate types with their own dependency surface: asserting on `typeof(Outer)` does not -> include what `Outer.Inner` references. The collection-based assertions (e.g. over `Types.InNamespace(…)`) -> enumerate nested types as their own items and therefore cover them. For IL/body-level accuracy, plug in -> your own resolver via `Customize.aweXpect.Reflection().DependencyResolver()` (see -> [Configuration](#dependency-resolver)). - -Namespace matching is ordinal and case-sensitive and, like `WithinNamespace`, includes sub-namespaces by -default (so `Foo.Bar` matches `Foo.Bar.Baz` but not `Foo.BarBaz`). A dependency in the **global namespace** -can be targeted or allowed with an empty string (`""`). Each result is chainable: - -```csharp -// Widen the set with .OrOn(…) -await Expect.That(Types.InNamespace("MyApp.Api")) - .DependOnlyOn("MyApp.Application").OrOn("MyApp.Domain"); - -// Opt out of sub-namespace matching for the whole expression -await Expect.That(types).DoNotDependOn("MyApp.Data").ExcludingSubNamespaces(); -``` - -For `DependsOnlyOn` a type's own namespace is always allowed, and by default so are its sub-namespaces. Use -`.ExcludingOwnSubNamespaces()` (only available on the *only-on* family) to also forbid references into a -type's own sub-namespaces: - -```csharp -await Expect.That(Types.InNamespace("MyApp.Domain")) - .DependOnlyOn("MyApp.Domain").ExcludingSubNamespaces().ExcludingOwnSubNamespaces(); -``` - -`DependsOn` and `DoesNotDependOn` (single types only) also accept a **specific type** via `()` or -`(Type)`, with `.OrOn()` / `.OrOn(Type)` to widen: - -```csharp -await Expect.That(typeof(MyDomainType)).DoesNotDependOn().OrOn(); -``` - -> **Framework dependencies are ignored unless you name one explicitly.** `DependOnlyOn` ignores dependencies -> whose assembly name matches one of the -> [`ExcludedAssemblyPrefixes`](#assembly-exclusions) at a name-segment boundary: `System` covers `System` -> and `System.Text.Json`, but not an assembly named `SystemsBiology` (so you never have to whitelist -> `System.*` and unrelated assemblies are never swallowed by a prefix), while a -> type's **own namespace** is always allowed. `DependsOn` / `DoesNotDependOn` / `WhichDependOn` still match a -> framework namespace when you name it explicitly (e.g. `DoesNotDependOn("System.Data")`). -> -> ⚠️ The default prefixes include `Microsoft`, so `DependOnlyOn` also ignores dependencies on e.g. -> `Microsoft.EntityFrameworkCore`, `Microsoft.AspNetCore` and `Microsoft.Extensions.*`; a domain entity -> inheriting `DbContext` does **not** fail `DependOnlyOn("MyApp.Domain")`. To forbid such dependencies, name -> them explicitly (`DoesNotDependOn()` or `DoNotDependOn("Microsoft.EntityFrameworkCore")`) or -> customize the [`ExcludedAssemblyPrefixes`](#assembly-exclusions). Note that the customization also affects -> assembly scanning and assembly-level dependency assertions. - -#### Dependency cycles - -The "slices should be free of cycles" architecture rule: assert that the namespaces of a set of types do not -(transitively) depend on each other. - -```csharp -// No dependency cycles among the namespaces under MyApp -await Expect.That(Types.InNamespace("MyApp")) - .HaveNoDependencyCycles(); -``` - -A namespace `A` *depends on* a namespace `B` when some type in `A` references a type in `B` (in its -[signature](#type-dependencies), read through the same resolver as the other dependency assertions). The -namespaces of the analyzed types form the nodes of a directed graph, and each -[strongly-connected component](https://en.wikipedia.org/wiki/Strongly_connected_component) with more than one -node is reported as a cycle, e.g. `MyApp.Orders -> MyApp.Billing -> MyApp.Orders`. Only namespaces present in -the analyzed set form nodes, so dependencies on framework or otherwise out-of-set namespaces never create an -edge, and a namespace referencing itself is not a cycle. - -By default a namespace and its sub-namespaces collapse into a single node (a family), consistent with how the -other dependency assertions treat a type's own sub-namespaces. So a reference between a namespace and its -ancestor/descendant (e.g. `MyApp.Orders` ↔ `MyApp.Orders.Domain`) never creates an edge and cannot by itself form -a cycle. But because the family is one node (not just a suppressed pair of edges), a cycle that leaves the family -and returns through a *different* member of it (e.g. `MyApp.Orders -> MyApp.Billing -> MyApp.Orders.Domain`) is -still detected. Use `ExcludingSubNamespaces()` to treat every namespace as its own node, so that such a -parent/child reference becomes an edge (and can form a cycle): - -```csharp -// Treat every namespace as its own node (MyApp.Orders ↔ MyApp.Orders.Domain can now form a cycle) -await Expect.That(Types.InNamespace("MyApp")) - .HaveNoDependencyCycles().ExcludingSubNamespaces(); -``` - -Pass a **slice root** to group all namespaces below it into one slice each (by the namespace segment immediately -following the root), so that, for example, `MyApp.Orders`, `MyApp.Orders.Domain` and `MyApp.Orders.Api` collapse -into the single slice `MyApp.Orders`: - -```csharp -// Group MyApp.Orders.* / MyApp.Billing.* / … into one slice each before looking for cycles -await Expect.That(Types.InNamespace("MyApp")) - .HaveNoDependencyCycles("MyApp"); -``` - -Because the edges come from the same dependency resolution as the other dependency assertions, configuring a -[custom dependency resolver](#dependency-resolver) (e.g. an IL-level one) also sharpens cycle -detection: body-level references it surfaces can complete a cycle that the signature-level default cannot see. - ### Methods In addition to [access modifiers](#access-modifiers), @@ -886,6 +745,232 @@ var managers = publicClasses.Where(type => type!.GetInterfaces().Length > 2); await Expect.That(managers).HaveName("Manager").AsSuffix(); ``` +## Architecture rules + +Layering and architecture rules are expressed over the types a type references **in its signature**: +[Type dependencies](#type-dependencies) covers the dependency filters and assertions (including +[dependency cycles](#dependency-cycles)), and [Layers as type selections](#layers-as-type-selections) shows +how to combine them with reusable type selections into a full architecture test suite. + +### Type dependencies + +The dependency filters and assertions follow the familiar filter/assert pairing: + +| | Filter | Assert (single) | Assert (many) | +|--------------------------|------------------------------|-------------------------|------------------------| +| depends on namespace | `.WhichDependOn("x", …)` | `.DependsOn("x", …)` | `.DependOn("x", …)` | +| does not depend on | `.WhichDoNotDependOn("x", …)`| `.DoesNotDependOn("x", …)` | `.DoNotDependOn("x", …)` | +| depends only on set | `.WhichDependOnlyOn("x", …)` | `.DependsOnlyOn("x", …)`| `.DependOnlyOn("x", …)`| + +```csharp +// Presentation must not reference the data layer +await Expect.That(Types.InNamespace("MyApp.Presentation")) + .DoNotDependOn("MyApp.Data"); + +// The API layer may only reference the application and domain layers +await Expect.That(Types.InNamespace("MyApp.Api")) + .DependOnlyOn("MyApp.Application", "MyApp.Domain"); + +// Filter for the types that depend on a namespace +In.AllLoadedAssemblies().Types().WhichDependOn("System.Data") +``` + +A type *depends on* every type referenced in its **declared signature**: the base type and directly +implemented interfaces, generic arguments and parameter constraints, field/property/event types, indexer +parameters, method return/parameter/generic-argument types, constructor parameters and the types of attributes +applied to the type, its members, their parameters and return values (including `typeof(…)` and enum attribute +arguments). Element types of +arrays/pointers/by-ref and generic type arguments are unwrapped (`List` depends on `List`, +which also matches a `List<>` target, and on `Infra.Foo`; a closed-generic target like `List` +only matches that exact construction). Purely synthetic references that you never wrote are ignored: +compiler-generated members, the implicit `object`/`ValueType`/`Enum` base type, interfaces inherited from the +base type, records' synthesized `IEquatable`, delegates' runtime infrastructure (only the `Invoke` +signature counts), enums' underlying-value plumbing and the attributes the compiler emits onto authored code +(nullability metadata, required members, async/iterator state machines, …), so the compiler's own plumbing +never counts. Should a future compiler version emit a marker attribute this library does not know about yet, +exclude it yourself via `Customize.aweXpect.Reflection().ExcludedAttributeTypes()` (full attribute type names; +extends the built-in set). Types you write in authored signatures always do count, including primitives and +`void` return types (namespace `System`); in practice, almost every type with members *does* depend on +`System`. + +> **Signature-level only:** dependencies are computed from reflection metadata, so body-level references such +> as `new Infra.Foo()`, static calls and local variables are **not** detected. Function-pointer signatures +> (`delegate*<…>`) are not decomposed either; the types inside them are invisible to dependency assertions. +> Nested types are separate types with their own dependency surface: asserting on `typeof(Outer)` does not +> include what `Outer.Inner` references. The collection-based assertions (e.g. over `Types.InNamespace(…)`) +> enumerate nested types as their own items and therefore cover them. For IL/body-level accuracy, plug in +> your own resolver via `Customize.aweXpect.Reflection().DependencyResolver()` (see +> [Configuration](#dependency-resolver)). + +Namespace matching is ordinal and case-sensitive and, like `WithinNamespace`, includes sub-namespaces by +default (so `Foo.Bar` matches `Foo.Bar.Baz` but not `Foo.BarBaz`). A dependency in the **global namespace** +can be targeted or allowed with an empty string (`""`). Each result is chainable: + +```csharp +// Widen the set with .OrOn(…) +await Expect.That(Types.InNamespace("MyApp.Api")) + .DependOnlyOn("MyApp.Application").OrOn("MyApp.Domain"); + +// Opt out of sub-namespace matching for the whole expression +await Expect.That(types).DoNotDependOn("MyApp.Data").ExcludingSubNamespaces(); +``` + +For `DependsOnlyOn` a type's own namespace is always allowed, and by default so are its sub-namespaces. Use +`.ExcludingOwnSubNamespaces()` (only available on the *only-on* family) to also forbid references into a +type's own sub-namespaces: + +```csharp +await Expect.That(Types.InNamespace("MyApp.Domain")) + .DependOnlyOn("MyApp.Domain").ExcludingSubNamespaces().ExcludingOwnSubNamespaces(); +``` + +`DependsOn` and `DoesNotDependOn` (single types only) also accept a **specific type** via `()` or +`(Type)`, with `.OrOn()` / `.OrOn(Type)` to widen: + +```csharp +await Expect.That(typeof(MyDomainType)).DoesNotDependOn().OrOn(); +``` + +All three dependency families additionally accept a reusable `Filtered.Types` selection as target; see +[Layers as type selections](#layers-as-type-selections). + +> **Framework dependencies are ignored unless you name one explicitly.** `DependOnlyOn` ignores dependencies +> whose assembly name matches one of the +> [`ExcludedAssemblyPrefixes`](#assembly-exclusions) at a name-segment boundary: `System` covers `System` +> and `System.Text.Json`, but not an assembly named `SystemsBiology` (so you never have to whitelist +> `System.*` and unrelated assemblies are never swallowed by a prefix), while a +> type's **own namespace** is always allowed. `DependsOn` / `DoesNotDependOn` / `WhichDependOn` still match a +> framework namespace when you name it explicitly (e.g. `DoesNotDependOn("System.Data")`). +> +> ⚠️ The default prefixes include `Microsoft`, so `DependOnlyOn` also ignores dependencies on e.g. +> `Microsoft.EntityFrameworkCore`, `Microsoft.AspNetCore` and `Microsoft.Extensions.*`; a domain entity +> inheriting `DbContext` does **not** fail `DependOnlyOn("MyApp.Domain")`. To forbid such dependencies, name +> them explicitly (`DoesNotDependOn()` or `DoNotDependOn("Microsoft.EntityFrameworkCore")`) or +> customize the [`ExcludedAssemblyPrefixes`](#assembly-exclusions). Note that the customization also affects +> assembly scanning and assembly-level dependency assertions. + +#### Dependency cycles + +The "slices should be free of cycles" architecture rule: assert that the namespaces of a set of types do not +(transitively) depend on each other. + +```csharp +// No dependency cycles among the namespaces under MyApp +await Expect.That(Types.InNamespace("MyApp")) + .HaveNoDependencyCycles(); +``` + +A namespace `A` *depends on* a namespace `B` when some type in `A` references a type in `B` (in its +[signature](#type-dependencies), read through the same resolver as the other dependency assertions). The +namespaces of the analyzed types form the nodes of a directed graph, and each +[strongly-connected component](https://en.wikipedia.org/wiki/Strongly_connected_component) with more than one +node is reported as a cycle, e.g. `MyApp.Orders -> MyApp.Billing -> MyApp.Orders`. Only namespaces present in +the analyzed set form nodes, so dependencies on framework or otherwise out-of-set namespaces never create an +edge, and a namespace referencing itself is not a cycle. + +By default a namespace and its sub-namespaces collapse into a single node (a family), consistent with how the +other dependency assertions treat a type's own sub-namespaces. So a reference between a namespace and its +ancestor/descendant (e.g. `MyApp.Orders` ↔ `MyApp.Orders.Domain`) never creates an edge and cannot by itself form +a cycle. But because the family is one node (not just a suppressed pair of edges), a cycle that leaves the family +and returns through a *different* member of it (e.g. `MyApp.Orders -> MyApp.Billing -> MyApp.Orders.Domain`) is +still detected. Use `ExcludingSubNamespaces()` to treat every namespace as its own node, so that such a +parent/child reference becomes an edge (and can form a cycle): + +```csharp +// Treat every namespace as its own node (MyApp.Orders ↔ MyApp.Orders.Domain can now form a cycle) +await Expect.That(Types.InNamespace("MyApp")) + .HaveNoDependencyCycles().ExcludingSubNamespaces(); +``` + +Pass a **slice root** to group all namespaces below it into one slice each (by the namespace segment immediately +following the root), so that, for example, `MyApp.Orders`, `MyApp.Orders.Domain` and `MyApp.Orders.Api` collapse +into the single slice `MyApp.Orders`: + +```csharp +// Group MyApp.Orders.* / MyApp.Billing.* / … into one slice each before looking for cycles +await Expect.That(Types.InNamespace("MyApp")) + .HaveNoDependencyCycles("MyApp"); +``` + +Because the edges come from the same dependency resolution as the other dependency assertions, configuring a +[custom dependency resolver](#dependency-resolver) (e.g. an IL-level one) also sharpens cycle +detection: body-level references it surfaces can complete a cycle that the signature-level default cannot see. + +### Layers as type selections + +There is no separate rule engine: a "layer" is just a reusable `Filtered.Types` selection (with the full +filter vocabulary at your disposal), and an architecture rule is just an expectation on it. + +```csharp +Filtered.Types domain = Types.InNamespace("MyApp.Domain"); +Filtered.Types infrastructure = Types.InNamespace("MyApp.Infrastructure"); +Filtered.Types repositories = Types.InNamespace("MyApp.Data").WithName("Repository").AsSuffix(); +``` + +The dependency assertions and filters accept such a selection as a **target**, alongside the namespace and +specific-type forms: `DependsOn` / `DoesNotDependOn` / `DependsOnlyOn` (and the plural `DependOn` / +`DoNotDependOn` / `DependOnlyOn` and the `WhichDependOn` / `WhichDoNotDependOn` / `WhichDependOnlyOn` +filters) take one or more `Filtered.Types` arguments. Each target selection is resolved once per assertion; +a dependency matches when it is a member of the union of the resolved selections. Matching is by type +identity, where a generic type definition in the selection (e.g. a scanned `Repository<>`) matches any of +its constructions. +Multiple targets and `.OrOn(…)` mean *any of*; for the *only-on* family the union is the allowed set, while +the own-namespace and framework rules apply unchanged, including the `.ExcludingOwnSubNamespaces()` opt-out +(an empty selection thus allows only the own namespace +and framework dependencies). A selection is an explicit target, so framework types contained in it are +matched normally by `DependsOn` / `DoesNotDependOn`. + +```csharp +// Outgoing rule with a selection as target: +await Expect.That(domain).DoNotDependOn(infrastructure); + +// Incoming rules are written explicitly from the other side: +await Expect.That(infrastructure).DoNotDependOn(domain); + +// Allowed set as union of selections (own namespace + framework stay allowed): +await Expect.That(domain).DependOnlyOn(repositories).OrOn(infrastructure); +``` + +Combine several rules into a single verification with aweXpect's `Expect.ThatAll(…)` (see +[multiple expectations](https://docs.testably.org/aweXpect/advanced/multiple-expectations)): every rule is +evaluated and all failures are reported together. Any assertion works on a selection, not just the +dependency ones, so naming conventions or sealing rules live in the same check: + +```csharp +await Expect.ThatAll( + Expect.That(domain).DoNotDependOn(infrastructure), + Expect.That(domain).DependOnlyOn(repositories).OrOn(infrastructure), + Expect.That(domain).AreSealed()); +``` + +A failing rule reports all violations, numbered per expectation: + +``` +Expected all of the following to succeed: + [01] Expected that domain all do not depend on types within namespace "MyApp.Infrastructure" in all loaded assemblies + [02] Expected that domain are all sealed +but + [01] it contained types with the dependency [ + OrderService +] + [02] it contained non-sealed types [ + Order, + Invoice +] +``` + +Exemptions to a rule use the [`Except` filter](#filters-and-the-matching-assertions) on the subject +selection: + +```csharp +await Expect.That(domain.Except()).DoNotDependOn(infrastructure); +await Expect.That(domain.Except(type => type.Name.StartsWith("Generated"))).AreSealed(); +``` + +A layer spanning several namespaces is built by widening a dependency *target* with additional selections +(or `.OrOn(…)`); for a *subject* spanning several namespaces, assert each namespace selection as its own +rule inside the same `Expect.ThatAll(…)`. + ## Configuration ### Assembly exclusions diff --git a/Source/aweXpect.Reflection/Collections/Filtered.Types.cs b/Source/aweXpect.Reflection/Collections/Filtered.Types.cs index 3330e6ee..0b3c5a89 100644 --- a/Source/aweXpect.Reflection/Collections/Filtered.Types.cs +++ b/Source/aweXpect.Reflection/Collections/Filtered.Types.cs @@ -543,5 +543,90 @@ public Types InEntryAssembly() public Types InExecutingAssembly() => In.ExecutingAssembly().Types().WithinNamespace(_namespace); } + + /// + /// A filtered collection of from a dependency filter whose targets are filtered + /// collections of types, allowing to widen the targeted/allowed collections. + /// + /// + /// Like all filtered collections, this is an immutable value object: does not mutate + /// this instance but rebuilds a fresh filter from the original base collection, so deriving multiple views + /// from the same instance cannot corrupt each other. + /// + public sealed class TypeSetDependencyFilterResult : Types + { + private readonly Func _build; + private readonly TypeSetDependencyOptions _options; + + internal TypeSetDependencyFilterResult( + TypeSetDependencyOptions options, + Func build) + : base(build(options)) + { + _options = options; + _build = build; + } + + /// + /// Widens the filter by the given . + /// + public TypeSetDependencyFilterResult OrOn(params Filtered.Types[] targets) + { + TypeSetDependencyOptions widened = _options.Copy(); + widened.OrOn(targets); + return new TypeSetDependencyFilterResult(widened, _build); + } + } + + /// + /// A filtered collection of from a depends-only-on filter whose allowed + /// targets are filtered collections of types, allowing to widen the allowed collections and to opt out + /// of the implicit allowance of the type's own sub-namespaces. + /// + /// + /// Like all filtered collections, this is an immutable value object: and + /// do not mutate this instance but rebuild a fresh filter from + /// the original base collection, so deriving multiple views from the same instance cannot corrupt each + /// other. + /// + public sealed class TypeSetDependencyOnlyOnFilterResult : Types + { + private readonly Func _build; + private readonly TypeSetDependencyOptions _options; + + internal TypeSetDependencyOnlyOnFilterResult( + TypeSetDependencyOptions options, + Func build) + : base(build(options)) + { + _options = options; + _build = build; + } + + /// + /// Widens the filter by the given . + /// + public TypeSetDependencyOnlyOnFilterResult OrOn(params Filtered.Types[] targets) + { + TypeSetDependencyOptions widened = _options.Copy(); + widened.OrOn(targets); + return new TypeSetDependencyOnlyOnFilterResult(widened, _build); + } + + /// + /// Excludes sub-namespaces of the type's own namespace from being implicitly allowed (so a Foo + /// type referencing Foo.Bar is filtered out unless Foo.Bar types are part of an allowed + /// collection). + /// + /// + /// The type's own namespace itself is always allowed. + /// + public TypeSetDependencyOnlyOnFilterResult ExcludingOwnSubNamespaces() + { + TypeSetDependencyOptions refined = _options.Copy(); + refined.ExcludingOwnSubNamespaces(); + return new TypeSetDependencyOnlyOnFilterResult(refined, _build); + } + } } } diff --git a/Source/aweXpect.Reflection/Filters/TypeFilters.WhichDependOn.cs b/Source/aweXpect.Reflection/Filters/TypeFilters.WhichDependOn.cs index a91b6ea2..2558634d 100644 --- a/Source/aweXpect.Reflection/Filters/TypeFilters.WhichDependOn.cs +++ b/Source/aweXpect.Reflection/Filters/TypeFilters.WhichDependOn.cs @@ -18,6 +18,28 @@ public static Filtered.Types.NamespaceDependencyFilterResult WhichDependOn( type => options.IsMatchedBy(type), () => $"which depend on {options.Describe()} "))); + /// + /// Filter for types which depend on (reference in their signature) at least one type in the filtered + /// collections of types or . + /// + /// + /// The target collections are resolved once per filter; a dependency matches when it is a member of the + /// union of the resolved collections (by identity; a generic type definition in a + /// collection matches any construction of it). + /// + public static Filtered.Types.TypeSetDependencyFilterResult WhichDependOn( + this Filtered.Types @this, Filtered.Types target, params Filtered.Types[] additional) + => new(new TypeSetDependencyOptions(target, additional), + options => @this.Which(Filter.Suffix( + async type => + { + ResolvedTypeSet targetSet = await options.Resolve(); + return targetSet.IsMatchedBy(type); + }, + // The parentheses delimit the target description (which ends in the target's source scope, + // e.g. "in all loaded assemblies") from the subject collection's own source suffix. + () => $"which depend on ({options.Describe()}) "))); + /// /// Filter for types which do not depend on (do not reference in their signature) any type in one of the /// (including sub-namespaces). @@ -28,4 +50,26 @@ public static Filtered.Types.NamespaceDependencyFilterResult WhichDoNotDependOn( options => @this.Which(Filter.Suffix( type => !options.IsMatchedBy(type), () => $"which do not depend on {options.Describe()} "))); + + /// + /// Filter for types which do not depend on (do not reference in their signature) any type in the filtered + /// collections of types or . + /// + /// + /// The target collections are resolved once per filter; a dependency matches when it is a member of the + /// union of the resolved collections (by identity; a generic type definition in a + /// collection matches any construction of it). + /// + public static Filtered.Types.TypeSetDependencyFilterResult WhichDoNotDependOn( + this Filtered.Types @this, Filtered.Types target, params Filtered.Types[] additional) + => new(new TypeSetDependencyOptions(target, additional), + options => @this.Which(Filter.Suffix( + async type => + { + ResolvedTypeSet targetSet = await options.Resolve(); + return !targetSet.IsMatchedBy(type); + }, + // The parentheses delimit the target description (which ends in the target's source scope, + // e.g. "in all loaded assemblies") from the subject collection's own source suffix. + () => $"which do not depend on ({options.Describe()}) "))); } diff --git a/Source/aweXpect.Reflection/Filters/TypeFilters.WhichDependOnlyOn.cs b/Source/aweXpect.Reflection/Filters/TypeFilters.WhichDependOnlyOn.cs index 794d5236..4e176696 100644 --- a/Source/aweXpect.Reflection/Filters/TypeFilters.WhichDependOnlyOn.cs +++ b/Source/aweXpect.Reflection/Filters/TypeFilters.WhichDependOnlyOn.cs @@ -28,4 +28,37 @@ public static Filtered.Types.NamespaceDependencyOnlyOnFilterResult WhichDependOn options => @this.Which(Filter.Suffix( type => !type.HasDependencyNamespaceViolations(options), () => $"which depend only on {options.Describe()} "))); + + /// + /// Filter for types which depend on (reference in their signature) only types in the filtered collections + /// of types or , their own namespace or framework + /// assemblies. + /// + /// + /// The target collections are resolved once per filter; a dependency is allowed when it is a member of the + /// union of the resolved collections (by identity; a generic type definition in a + /// collection matches any construction of it). A type's own namespace is always allowed, including its + /// sub-namespaces unless + /// is used. + /// + /// Dependencies on types whose assembly name matches one of the + /// at a + /// name-segment boundary (System covers System.Text.Json, but not + /// SystemsBiology.Core) are ignored, so + /// that framework types do not have to be included explicitly. The default prefixes include + /// Microsoft, so e.g. Microsoft.EntityFrameworkCore is also ignored; forbid such a dependency + /// explicitly via WhichDoNotDependOn or customize the prefixes. + /// + public static Filtered.Types.TypeSetDependencyOnlyOnFilterResult WhichDependOnlyOn( + this Filtered.Types @this, Filtered.Types target, params Filtered.Types[] additional) + => new(new TypeSetDependencyOptions(target, additional), + options => @this.Which(Filter.Suffix( + async type => + { + ResolvedTypeSet allowed = await options.Resolve(); + return !type.HasDependencyTypeSetViolations(allowed); + }, + // The parentheses delimit the target description (which ends in the target's source scope, + // e.g. "in all loaded assemblies") from the subject collection's own source suffix. + () => $"which depend only on ({options.Describe()}) "))); } diff --git a/Source/aweXpect.Reflection/Helpers/TypeHelpers.cs b/Source/aweXpect.Reflection/Helpers/TypeHelpers.cs index 95e7a6ef..dd74f308 100644 --- a/Source/aweXpect.Reflection/Helpers/TypeHelpers.cs +++ b/Source/aweXpect.Reflection/Helpers/TypeHelpers.cs @@ -773,25 +773,34 @@ internal static bool MatchesType(Type dependency, Type target) { target = StripElementTypes(target); - if (dependency == target) - { - return true; - } - - return target.IsGenericTypeDefinition && - dependency.IsGenericType && - dependency.GetGenericTypeDefinition() == target; + return dependency == target || + dependency.GetGenericTypeDefinitionOfConstruction() == target; } + /// + /// Returns the generic type definition (e.g. List<>) when the + /// is a constructed generic type (e.g. List<Foo>), otherwise . + /// + /// + /// The single encoding of the rule that a dependency on any construction also matches its generic type + /// definition; shared between and , so + /// that the specific-type and the type-set matchers cannot drift apart. + /// + internal static Type? GetGenericTypeDefinitionOfConstruction(this Type dependency) + => dependency is { IsGenericType: true, IsGenericTypeDefinition: false, } + ? dependency.GetGenericTypeDefinition() + : null; + /// /// Strips array/by-ref/pointer wrappers from the , returning the innermost /// element type. /// /// - /// Shared between dependency unwrapping () and target matching - /// (), so that the two sides of the documented symmetry cannot drift apart. + /// Shared between dependency unwrapping (), target matching + /// () and target-set resolution (), + /// so that the sides of the documented symmetry cannot drift apart. /// - private static Type StripElementTypes(Type type) + internal static Type StripElementTypes(Type type) { while (type.HasElementType && type.GetElementType() is { } elementType) { @@ -877,17 +886,12 @@ private static bool IsFrameworkDependency(this Type type, string[] excludedPrefi internal static IReadOnlyList GetDependencyNamespaceViolations( this Type type, NamespaceDependencyOptions allowed) { - string? ownNamespace = type.Namespace; - string[] excludedPrefixes = Customize.aweXpect.Reflection().ExcludedAssemblyPrefixes.Get(); List violations = []; HashSet seen = new(StringComparer.Ordinal); - foreach (Type dependency in type.ResolveDependencies()) + foreach (Type dependency in type.GetDependencyViolations( + (dependency, ownNamespace, excludedPrefixes) + => IsDependencyViolation(dependency, ownNamespace, allowed, excludedPrefixes))) { - if (!IsDependencyViolation(dependency, ownNamespace, allowed, excludedPrefixes)) - { - continue; - } - string display = dependency.Namespace ?? GlobalNamespaceDisplay; if (seen.Add(display)) { @@ -908,30 +912,105 @@ internal static IReadOnlyList GetDependencyNamespaceViolations( /// a verdict and not the violation list. /// internal static bool HasDependencyNamespaceViolations(this Type type, NamespaceDependencyOptions allowed) + => type.GetDependencyViolations( + (dependency, ownNamespace, excludedPrefixes) + => IsDependencyViolation(dependency, ownNamespace, allowed, excludedPrefixes)).Any(); + + private static bool IsDependencyViolation( + Type dependency, string? ownNamespace, NamespaceDependencyOptions allowed, string[] excludedPrefixes) + => !IsExemptDependency(dependency, ownNamespace, allowed.IncludeOwnSubNamespaces, excludedPrefixes) && + !allowed.Matches(dependency.Namespace); + + /// + /// Checks the exemptions shared by all only-on rules: framework dependencies and dependencies in the + /// type's own namespace are always allowed. + /// + /// + /// Shared between the namespace-based and type-set-based violation predicates, so that a future rule + /// change cannot be applied to one family and missed in the other. + /// + private static bool IsExemptDependency( + Type dependency, string? ownNamespace, bool includeOwnSubNamespaces, string[] excludedPrefixes) + => dependency.IsFrameworkDependency(excludedPrefixes) || + IsOwnNamespace(dependency.Namespace, ownNamespace, includeOwnSubNamespaces); + + /// + /// Enumerates the 's dependencies that the + /// predicate flags as violations, supplying the type's own namespace and the configured excluded + /// assembly prefixes. + /// + /// + /// Shared core of the namespace-based and type-set-based violation helpers, so that the two families + /// cannot drift apart in how the dependencies are walked. Lazy, so verdict-only callers stop at the + /// first violation. + /// + private static IEnumerable GetDependencyViolations( + this Type type, Func isViolation) { string? ownNamespace = type.Namespace; string[] excludedPrefixes = Customize.aweXpect.Reflection().ExcludedAssemblyPrefixes.Get(); return type.ResolveDependencies() - .Any(dependency => IsDependencyViolation(dependency, ownNamespace, allowed, excludedPrefixes)); + .Where(dependency => isViolation(dependency, ownNamespace, excludedPrefixes)); } - private static bool IsDependencyViolation( - Type dependency, string? ownNamespace, NamespaceDependencyOptions allowed, string[] excludedPrefixes) + private static bool IsOwnNamespace(string? dependencyNamespace, string? ownNamespace, bool includeSubNamespaces) + => ownNamespace is null + ? dependencyNamespace is null + : NamespaceMatches(dependencyNamespace, ownNamespace, includeSubNamespaces); + + /// + /// Collects the 's dependencies that are not allowed by the resolved target set in + /// , the type's own namespace, or the framework rule. + /// + /// + /// Same framework and own-namespace rules as , but the allowed + /// set is a concrete set of types, so the violations are reported as formatted type names instead of + /// namespaces. Distinct violators sharing a formatted name (the same simple name in different namespaces) + /// are qualified by their namespace, so they do not collapse into one indistinguishable entry. + /// + internal static IReadOnlyList GetDependencyTypeSetViolations( + this Type type, ResolvedTypeSet allowed) { - if (dependency.IsFrameworkDependency(excludedPrefixes)) + List violations = []; + foreach (IGrouping sameName in type.GetDependencyViolations( + (dependency, ownNamespace, excludedPrefixes) + => IsDependencyTypeSetViolation(dependency, ownNamespace, allowed, excludedPrefixes)) + .GroupBy(dependency => Formatter.Format(dependency), StringComparer.Ordinal)) { - return false; + Type[] violators = sameName.ToArray(); + if (violators.Length == 1) + { + violations.Add(sameName.Key); + } + else + { + violations.AddRange(violators.Select(violator => violator.Namespace is null + ? sameName.Key + : $"{violator.Namespace}.{sameName.Key}")); + } } - string? dependencyNamespace = dependency.Namespace; - return !IsOwnNamespace(dependencyNamespace, ownNamespace, allowed.IncludeOwnSubNamespaces) && - !allowed.Matches(dependencyNamespace); + violations.Sort(StringComparer.Ordinal); + return violations; } - private static bool IsOwnNamespace(string? dependencyNamespace, string? ownNamespace, bool includeSubNamespaces) - => ownNamespace is null - ? dependencyNamespace is null - : NamespaceMatches(dependencyNamespace, ownNamespace, includeSubNamespaces); + /// + /// Checks whether the has at least one dependency outside the resolved target set, + /// stopping at the first one. + /// + /// + /// Same rules as , for callers (like filters) that only need + /// a verdict and not the violation list. + /// + internal static bool HasDependencyTypeSetViolations(this Type type, ResolvedTypeSet allowed) + => type.GetDependencyViolations( + (dependency, ownNamespace, excludedPrefixes) + => IsDependencyTypeSetViolation(dependency, ownNamespace, allowed, excludedPrefixes)).Any(); + + private static bool IsDependencyTypeSetViolation( + Type dependency, string? ownNamespace, ResolvedTypeSet allowed, string[] excludedPrefixes) + => !IsExemptDependency(dependency, ownNamespace, allowed.IncludeOwnSubNamespaces, excludedPrefixes) && + !allowed.Matches(dependency); /// /// Resolves the dependencies of the through which all assertions and filters go, diff --git a/Source/aweXpect.Reflection/Options/TypeSetDependencyOptions.cs b/Source/aweXpect.Reflection/Options/TypeSetDependencyOptions.cs new file mode 100644 index 00000000..88108149 --- /dev/null +++ b/Source/aweXpect.Reflection/Options/TypeSetDependencyOptions.cs @@ -0,0 +1,211 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using aweXpect.Reflection.Collections; +using aweXpect.Reflection.Helpers; + +namespace aweXpect.Reflection.Options; + +/// +/// Options for a dependency assertion or filter whose targets are filtered collections of types +/// (). +/// +/// +/// The instance is shared between the chainable result and the underlying constraint/filter, so that +/// OrOn(…) and ExcludingOwnSubNamespaces(…) widen or refine the (lazily evaluated) expression. +/// +/// The target collections are resolved once per assertion via (the union of all +/// collections); matching happens on the returned , so it cannot be used +/// before the resolution completed. +/// +internal sealed class TypeSetDependencyOptions +{ + private readonly List _targets = []; + private bool _excludeOwnSubNamespaces; + private ResolvedTypeSet? _resolved; + + public TypeSetDependencyOptions(Filtered.Types target, Filtered.Types[] additional) + { + _targets.Add(target ?? throw new ArgumentNullException(nameof(target), + "The target collection of types must not be null.")); + if (additional is null) + { + throw new ArgumentNullException(nameof(additional), + "The additional target collections of types must not be null."); + } + + Add(additional, nameof(additional)); + } + + private TypeSetDependencyOptions(IEnumerable targets, bool excludeOwnSubNamespaces) + { + _targets.AddRange(targets); + _excludeOwnSubNamespaces = excludeOwnSubNamespaces; + } + + /// + /// Indicates whether sub-namespaces of the type's own namespace are still allowed (for depends-only-on). + /// + /// + /// The type's own namespace is always allowed; its sub-namespaces stay allowed unless the caller opted into + /// . + /// + public bool IncludeOwnSubNamespaces => !_excludeOwnSubNamespaces; + + /// + /// Creates an independent copy, so that refining a (reusable) filter does not mutate the shared instance. + /// + public TypeSetDependencyOptions Copy() + => new(_targets, _excludeOwnSubNamespaces); + + /// + /// Widens the set of targeted/allowed types by the given . + /// + public void OrOn(Filtered.Types[] targets) + { + if (targets is null) + { + throw new ArgumentNullException(nameof(targets), "The target collections of types must not be null."); + } + + if (targets.Length == 0) + { + throw new ArgumentException("At least one collection of types must be specified."); + } + + Add(targets, nameof(targets)); + } + + /// + /// Excludes sub-namespaces of the type's own namespace from being implicitly allowed (for depends-only-on). + /// + /// + /// Only the exemption rule changes, not the resolved target set, so a previously resolved set stays valid. + /// + public void ExcludingOwnSubNamespaces() + => _excludeOwnSubNamespaces = true; + + private void Add(Filtered.Types[] targets, string paramName) + { + // Fully validate before mutating, so that a failed widening leaves the shared instance untouched. + if (targets.Contains(null!)) + { + throw new ArgumentNullException(paramName, "The target collections of types must not contain null."); + } + + _targets.AddRange(targets); + // Widening invalidates a previously resolved set. + _resolved = null; + } + + /// + /// Resolves the target collections once into their union set and returns it as a + /// , which performs all matching. + /// + /// + /// Each member is normalized via : dependencies are stored + /// element-stripped at collection time, so array/by-ref/pointer targets must be unwrapped symmetrically + /// (mirroring in the specific-type overloads). + /// +#if NET8_0_OR_GREATER + public async ValueTask Resolve(CancellationToken cancellationToken = default) + { + if (_resolved is not null) + { + return _resolved; + } + + HashSet resolved = []; + foreach (Filtered.Types target in _targets) + { + await foreach (Type type in target.WithCancellation(cancellationToken)) + { + resolved.Add(TypeHelpers.StripElementTypes(type)); + } + } + + _resolved = new ResolvedTypeSet(resolved, this); + return _resolved; + } +#else + public Task Resolve(CancellationToken cancellationToken = default) + { + if (_resolved is null) + { + HashSet resolved = []; + foreach (Filtered.Types target in _targets) + { + foreach (Type type in target) + { + cancellationToken.ThrowIfCancellationRequested(); + resolved.Add(TypeHelpers.StripElementTypes(type)); + } + } + + _resolved = new ResolvedTypeSet(resolved, this); + } + + return Task.FromResult(_resolved); + } +#endif + + /// + /// Describes the configured target collections for an expectation message. + /// + /// + /// The constructor guarantees at least one target, so an empty set cannot occur here. + /// + public string Describe() + => string.Join(" or ", _targets.Select(target => target.GetDescription().TrimEnd())); +} + +/// +/// The resolved union set of the target collections of a , returned +/// by . +/// +/// +/// All matching goes through this type, so matching against an unresolved target set is unrepresentable. +/// +internal sealed class ResolvedTypeSet +{ + private readonly TypeSetDependencyOptions _options; + private readonly HashSet _resolved; + + internal ResolvedTypeSet(HashSet resolved, TypeSetDependencyOptions options) + { + _resolved = resolved; + _options = options; + } + + /// + /// Indicates whether sub-namespaces of the type's own namespace are still allowed (for depends-only-on). + /// + /// + /// Delegates to the owning options, so that a later refinement is honored even when the set was + /// already resolved (the exemption rule is independent of the resolved set). + /// + public bool IncludeOwnSubNamespaces => _options.IncludeOwnSubNamespaces; + + /// + /// Checks whether the is a member of the resolved target set. + /// + /// + /// Membership is by identity. Because dependencies keep constructed generic types as + /// written (e.g. List<Foo>) while a scanned target collection contains the generic type + /// definition (List<>), a dependency on any construction additionally matches when its + /// definition is a member of the set (the same rule as in the specific-type overloads, shared via + /// ). + /// + public bool Matches(Type dependency) + => _resolved.Contains(dependency) || + (dependency.GetGenericTypeDefinitionOfConstruction() is { } definition && + _resolved.Contains(definition)); + + /// + /// Checks whether the has at least one dependency in the resolved target set. + /// + public bool IsMatchedBy(Type type) + => type.ResolveDependencies().Any(Matches); +} diff --git a/Source/aweXpect.Reflection/Results/TypeSetDependencyOnlyOnResult.cs b/Source/aweXpect.Reflection/Results/TypeSetDependencyOnlyOnResult.cs new file mode 100644 index 00000000..a260919c --- /dev/null +++ b/Source/aweXpect.Reflection/Results/TypeSetDependencyOnlyOnResult.cs @@ -0,0 +1,47 @@ +using aweXpect.Core; +using aweXpect.Reflection.Collections; +using aweXpect.Reflection.Options; +using aweXpect.Results; + +namespace aweXpect.Reflection.Results; + +/// +/// The result of a depends-only-on assertion whose allowed targets are filtered collections of types, +/// allowing to widen the allowed collections and to opt out of the implicit allowance of the type's own +/// sub-namespaces. +/// +public sealed class TypeSetDependencyOnlyOnResult + : AndOrResult> +{ + private readonly TypeSetDependencyOptions _options; + + internal TypeSetDependencyOnlyOnResult( + ExpectationBuilder expectationBuilder, + IThat subject, + TypeSetDependencyOptions options) + : base(expectationBuilder, subject) + => _options = options; + + /// + /// Widens the expression by the given . + /// + public TypeSetDependencyOnlyOnResult OrOn(params Filtered.Types[] targets) + { + _options.OrOn(targets); + return this; + } + + /// + /// Excludes sub-namespaces of the type's own namespace from being implicitly allowed (so a Foo type + /// referencing Foo.Bar becomes a violation unless Foo.Bar types are part of an allowed + /// collection). + /// + /// + /// The type's own namespace itself is always allowed. + /// + public TypeSetDependencyOnlyOnResult ExcludingOwnSubNamespaces() + { + _options.ExcludingOwnSubNamespaces(); + return this; + } +} diff --git a/Source/aweXpect.Reflection/Results/TypeSetDependencyResult.cs b/Source/aweXpect.Reflection/Results/TypeSetDependencyResult.cs new file mode 100644 index 00000000..38d702f1 --- /dev/null +++ b/Source/aweXpect.Reflection/Results/TypeSetDependencyResult.cs @@ -0,0 +1,32 @@ +using aweXpect.Core; +using aweXpect.Reflection.Collections; +using aweXpect.Reflection.Options; +using aweXpect.Results; + +namespace aweXpect.Reflection.Results; + +/// +/// The result of a dependency assertion whose targets are filtered collections of types, allowing to widen +/// the targeted/allowed collections. +/// +public sealed class TypeSetDependencyResult + : AndOrResult> +{ + private readonly TypeSetDependencyOptions _options; + + internal TypeSetDependencyResult( + ExpectationBuilder expectationBuilder, + IThat subject, + TypeSetDependencyOptions options) + : base(expectationBuilder, subject) + => _options = options; + + /// + /// Widens the expression by the given . + /// + public TypeSetDependencyResult OrOn(params Filtered.Types[] targets) + { + _options.OrOn(targets); + return this; + } +} diff --git a/Source/aweXpect.Reflection/ThatType.DependsOn.cs b/Source/aweXpect.Reflection/ThatType.DependsOn.cs index 10c09d53..43680c17 100644 --- a/Source/aweXpect.Reflection/ThatType.DependsOn.cs +++ b/Source/aweXpect.Reflection/ThatType.DependsOn.cs @@ -2,8 +2,11 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using System.Threading; +using System.Threading.Tasks; using aweXpect.Core; using aweXpect.Core.Constraints; +using aweXpect.Reflection.Collections; using aweXpect.Reflection.Helpers; using aweXpect.Reflection.Options; using aweXpect.Reflection.Results; @@ -48,6 +51,26 @@ public static partial class ThatType options); } + /// + /// Verifies that the depends on (references in its signature) at least one type in the + /// filtered collections of types or . + /// + /// + /// The target collections are resolved once per assertion; a dependency matches when it is a member of the + /// union of the resolved collections (by identity; a generic type definition in a + /// collection matches any construction of it). + /// + public static TypeSetDependencyResult DependsOn( + this IThat subject, Filtered.Types target, params Filtered.Types[] additional) + { + TypeSetDependencyOptions options = new(target, additional); + return new TypeSetDependencyResult(subject.Get().ExpectationBuilder + .AddConstraint((it, grammars) + => new DependsOnTypeSetConstraint(it, grammars, options)), + subject, + options); + } + /// /// Verifies that the does not depend on (does not reference in its signature) any type in /// one of the (including sub-namespaces). @@ -86,6 +109,26 @@ public static partial class ThatType options); } + /// + /// Verifies that the does not depend on (does not reference in its signature) any type in + /// the filtered collections of types or . + /// + /// + /// The target collections are resolved once per assertion; a dependency matches when it is a member of the + /// union of the resolved collections (by identity; a generic type definition in a + /// collection matches any construction of it). + /// + public static TypeSetDependencyResult DoesNotDependOn( + this IThat subject, Filtered.Types target, params Filtered.Types[] additional) + { + TypeSetDependencyOptions options = new(target, additional); + return new TypeSetDependencyResult(subject.Get().ExpectationBuilder + .AddConstraint((it, grammars) + => new DependsOnTypeSetConstraint(it, grammars, options).Invert()), + subject, + options); + } + private sealed class DependsOnNamespaceConstraint( string it, ExpectationGrammars grammars, @@ -183,4 +226,52 @@ protected override void AppendNegatedResult(StringBuilder stringBuilder, string? .ToArray()); } } + + private sealed class DependsOnTypeSetConstraint( + string it, + ExpectationGrammars grammars, + TypeSetDependencyOptions options) + : ConstraintResult.WithNotNullValue(it, grammars), + IAsyncConstraint + { + private Type[] _dependencies = []; + private ResolvedTypeSet? _targetSet; + + public async Task IsMetBy(Type? actual, CancellationToken cancellationToken) + { + Actual = actual; + if (actual is null) + { + Outcome = Outcome.Failure; + return this; + } + + ResolvedTypeSet targetSet = await options.Resolve(cancellationToken); + _targetSet = targetSet; + _dependencies = actual.ResolveDependencies(); + Outcome = _dependencies.Any(targetSet.Matches) ? Outcome.Success : Outcome.Failure; + return this; + } + + protected override void AppendNormalExpectation(StringBuilder stringBuilder, string? indentation = null) + => stringBuilder.Append("depends on ").Append(options.Describe()); + + protected override void AppendNormalResult(StringBuilder stringBuilder, string? indentation = null) + => stringBuilder.Append(It).Append(" did not"); + + protected override void AppendNegatedExpectation(StringBuilder stringBuilder, string? indentation = null) + => stringBuilder.Append("does not depend on ").Append(options.Describe()); + + protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null) + { + // The sorted matching types are only needed for this failure message, so they are built lazily. + // _targetSet can only be null when the subject was null, in which case _dependencies is empty. + stringBuilder.Append(It).Append(" depended on "); + Formatter.Format(stringBuilder, _dependencies + .Where(dependency => _targetSet?.Matches(dependency) == true) + .Distinct() + .OrderBy(type => type.FullName ?? type.Name, StringComparer.Ordinal) + .ToArray()); + } + } } diff --git a/Source/aweXpect.Reflection/ThatType.DependsOnlyOn.cs b/Source/aweXpect.Reflection/ThatType.DependsOnlyOn.cs index 5c30fb3b..566b72df 100644 --- a/Source/aweXpect.Reflection/ThatType.DependsOnlyOn.cs +++ b/Source/aweXpect.Reflection/ThatType.DependsOnlyOn.cs @@ -1,9 +1,12 @@ using System; using System.Collections.Generic; using System.Text; +using System.Threading; +using System.Threading.Tasks; using aweXpect.Core; using aweXpect.Core.Constraints; using aweXpect.Customization; +using aweXpect.Reflection.Collections; using aweXpect.Reflection.Helpers; using aweXpect.Reflection.Options; using aweXpect.Reflection.Results; @@ -36,6 +39,37 @@ public static partial class ThatType options); } + /// + /// Verifies that the depends on (references in its signature) only types in the filtered + /// collections of types or , its own namespace or + /// framework assemblies. + /// + /// + /// The target collections are resolved once per assertion; a dependency is allowed when it is a member of the + /// union of the resolved collections (by identity; a generic type definition in a + /// collection matches any construction of it). The type's own namespace is always allowed, including its + /// sub-namespaces unless + /// is used. + /// + /// Dependencies on types whose assembly name matches one of the + /// at a + /// name-segment boundary (System covers System.Text.Json, but not + /// SystemsBiology.Core) are ignored, so + /// that framework types do not have to be included explicitly. The default prefixes include + /// Microsoft, so e.g. Microsoft.EntityFrameworkCore is also ignored; forbid such a dependency + /// explicitly via DoesNotDependOn or customize the prefixes. + /// + public static TypeSetDependencyOnlyOnResult DependsOnlyOn( + this IThat subject, Filtered.Types target, params Filtered.Types[] additional) + { + TypeSetDependencyOptions options = new(target, additional); + return new TypeSetDependencyOnlyOnResult(subject.Get().ExpectationBuilder + .AddConstraint((it, grammars) + => new DependsOnlyOnTypeSetConstraint(it, grammars, options)), + subject, + options); + } + private sealed class DependsOnlyOnConstraint( string it, ExpectationGrammars grammars, @@ -74,4 +108,44 @@ protected override void AppendNegatedExpectation(StringBuilder stringBuilder, st protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null) => stringBuilder.Append(It).Append(" only depended on the allowed namespaces"); } + + private sealed class DependsOnlyOnTypeSetConstraint( + string it, + ExpectationGrammars grammars, + TypeSetDependencyOptions options) + : ConstraintResult.WithNotNullValue(it, grammars), + IAsyncConstraint + { + private IReadOnlyList _violations = []; + + public async Task IsMetBy(Type? actual, CancellationToken cancellationToken) + { + Actual = actual; + if (actual is null) + { + Outcome = Outcome.Failure; + return this; + } + + ResolvedTypeSet allowed = await options.Resolve(cancellationToken); + _violations = actual.GetDependencyTypeSetViolations(allowed); + Outcome = _violations.Count == 0 ? Outcome.Success : Outcome.Failure; + return this; + } + + protected override void AppendNormalExpectation(StringBuilder stringBuilder, string? indentation = null) + => stringBuilder.Append("depends only on ").Append(options.Describe()); + + protected override void AppendNormalResult(StringBuilder stringBuilder, string? indentation = null) + { + stringBuilder.Append(It).Append(" also depended on "); + Formatter.Format(stringBuilder, _violations); + } + + protected override void AppendNegatedExpectation(StringBuilder stringBuilder, string? indentation = null) + => stringBuilder.Append("does not depend only on ").Append(options.Describe()); + + protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null) + => stringBuilder.Append(It).Append(" only depended on the allowed types"); + } } diff --git a/Source/aweXpect.Reflection/ThatTypes.DependOn.cs b/Source/aweXpect.Reflection/ThatTypes.DependOn.cs index fac15e68..b244251f 100644 --- a/Source/aweXpect.Reflection/ThatTypes.DependOn.cs +++ b/Source/aweXpect.Reflection/ThatTypes.DependOn.cs @@ -1,15 +1,14 @@ using System; using System.Collections.Generic; using System.Text; +using System.Threading; +using System.Threading.Tasks; using aweXpect.Core; using aweXpect.Core.Constraints; +using aweXpect.Reflection.Collections; using aweXpect.Reflection.Helpers; using aweXpect.Reflection.Options; using aweXpect.Reflection.Results; -#if NET8_0_OR_GREATER -using System.Threading; -using System.Threading.Tasks; -#endif // ReSharper disable PossibleMultipleEnumeration @@ -49,6 +48,50 @@ public static partial class ThatTypes } #endif + /// + /// Verifies that all items in the filtered collection of depend on (reference in their + /// signature) at least one type in the filtered collections of types or + /// . + /// + /// + /// The target collections are resolved once per assertion; a dependency matches when it is a member of the + /// union of the resolved collections (by identity; a generic type definition in a + /// collection matches any construction of it). + /// + public static TypeSetDependencyResult> DependOn( + this IThat> subject, Filtered.Types target, params Filtered.Types[] additional) + { + TypeSetDependencyOptions options = new(target, additional); + return new TypeSetDependencyResult>(subject.Get().ExpectationBuilder + .AddConstraint>((it, grammars) + => new DependOnTypeSetConstraint(it, grammars, options)), + subject, + options); + } + +#if NET8_0_OR_GREATER + /// + /// Verifies that all items in the filtered collection of depend on (reference in their + /// signature) at least one type in the filtered collections of types or + /// . + /// + /// + /// The target collections are resolved once per assertion; a dependency matches when it is a member of the + /// union of the resolved collections (by identity; a generic type definition in a + /// collection matches any construction of it). + /// + public static TypeSetDependencyResult> DependOn( + this IThat> subject, Filtered.Types target, params Filtered.Types[] additional) + { + TypeSetDependencyOptions options = new(target, additional); + return new TypeSetDependencyResult>(subject.Get().ExpectationBuilder + .AddConstraint>((it, grammars) + => new DependOnTypeSetConstraint(it, grammars, options)), + subject, + options); + } +#endif + /// /// Verifies that all items in the filtered collection of do not depend on (do not reference /// in their signature) any type in one of the (including sub-namespaces). @@ -81,6 +124,50 @@ public static partial class ThatTypes } #endif + /// + /// Verifies that all items in the filtered collection of do not depend on (do not + /// reference in their signature) any type in the filtered collections of types or + /// . + /// + /// + /// The target collections are resolved once per assertion; a dependency matches when it is a member of the + /// union of the resolved collections (by identity; a generic type definition in a + /// collection matches any construction of it). + /// + public static TypeSetDependencyResult> DoNotDependOn( + this IThat> subject, Filtered.Types target, params Filtered.Types[] additional) + { + TypeSetDependencyOptions options = new(target, additional); + return new TypeSetDependencyResult>(subject.Get().ExpectationBuilder + .AddConstraint>((it, grammars) + => new DoNotDependOnTypeSetConstraint(it, grammars, options)), + subject, + options); + } + +#if NET8_0_OR_GREATER + /// + /// Verifies that all items in the filtered collection of do not depend on (do not + /// reference in their signature) any type in the filtered collections of types or + /// . + /// + /// + /// The target collections are resolved once per assertion; a dependency matches when it is a member of the + /// union of the resolved collections (by identity; a generic type definition in a + /// collection matches any construction of it). + /// + public static TypeSetDependencyResult> DoNotDependOn( + this IThat> subject, Filtered.Types target, params Filtered.Types[] additional) + { + TypeSetDependencyOptions options = new(target, additional); + return new TypeSetDependencyResult>(subject.Get().ExpectationBuilder + .AddConstraint>((it, grammars) + => new DoNotDependOnTypeSetConstraint(it, grammars, options)), + subject, + options); + } +#endif + private static bool DependsOnNamespace(Type? type, NamespaceDependencyOptions options) => type is not null && options.IsMatchedBy(type); @@ -89,6 +176,14 @@ private static bool DependsOnNamespace(Type? type, NamespaceDependencyOptions op private static bool DoesNotDependOnNamespace(Type? type, NamespaceDependencyOptions options) => type is not null && !options.IsMatchedBy(type); + private static bool DependsOnTypeSet(Type? type, ResolvedTypeSet targetSet) + => type is not null && targetSet.IsMatchedBy(type); + + // A null item's dependencies cannot be verified, so it fails the negative assertion just like the + // positive one (and like DependOnlyOn), instead of slipping through as "does not depend". + private static bool DoesNotDependOnTypeSet(Type? type, ResolvedTypeSet targetSet) + => type is not null && !targetSet.IsMatchedBy(type); + private sealed class DependOnConstraint( string it, ExpectationGrammars grammars, @@ -162,4 +257,90 @@ protected override void AppendNegatedResult(StringBuilder stringBuilder, string? Formatter.Format(stringBuilder, Matching, FormattingOptions.Indented(indentation)); } } + + private sealed class DependOnTypeSetConstraint( + string it, + ExpectationGrammars grammars, + TypeSetDependencyOptions options) + : CollectionConstraintResult(grammars), + IAsyncConstraint> +#if NET8_0_OR_GREATER + , IAsyncConstraint> +#endif + { +#if NET8_0_OR_GREATER + public async Task IsMetBy(IAsyncEnumerable actual, CancellationToken cancellationToken) + { + ResolvedTypeSet targetSet = await options.Resolve(cancellationToken); + return await SetAsyncValue(actual, type => DependsOnTypeSet(type, targetSet)); + } +#endif + + public async Task IsMetBy(IEnumerable actual, CancellationToken cancellationToken) + { + ResolvedTypeSet targetSet = await options.Resolve(cancellationToken); + return SetValue(actual, type => DependsOnTypeSet(type, targetSet)); + } + + protected override void AppendNormalExpectation(StringBuilder stringBuilder, string? indentation = null) + => stringBuilder.Append("all depend on ").Append(options.Describe()); + + protected override void AppendNormalResult(StringBuilder stringBuilder, string? indentation = null) + { + stringBuilder.Append(it).Append(" contained types without the dependency "); + Formatter.Format(stringBuilder, NotMatching, FormattingOptions.Indented(indentation)); + } + + protected override void AppendNegatedExpectation(StringBuilder stringBuilder, string? indentation = null) + => stringBuilder.Append("not all depend on ").Append(options.Describe()); + + protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null) + { + stringBuilder.Append(it).Append(" only contained types with the dependency "); + Formatter.Format(stringBuilder, Matching, FormattingOptions.Indented(indentation)); + } + } + + private sealed class DoNotDependOnTypeSetConstraint( + string it, + ExpectationGrammars grammars, + TypeSetDependencyOptions options) + : CollectionConstraintResult(grammars), + IAsyncConstraint> +#if NET8_0_OR_GREATER + , IAsyncConstraint> +#endif + { +#if NET8_0_OR_GREATER + public async Task IsMetBy(IAsyncEnumerable actual, CancellationToken cancellationToken) + { + ResolvedTypeSet targetSet = await options.Resolve(cancellationToken); + return await SetAsyncValue(actual, type => DoesNotDependOnTypeSet(type, targetSet)); + } +#endif + + public async Task IsMetBy(IEnumerable actual, CancellationToken cancellationToken) + { + ResolvedTypeSet targetSet = await options.Resolve(cancellationToken); + return SetValue(actual, type => DoesNotDependOnTypeSet(type, targetSet)); + } + + protected override void AppendNormalExpectation(StringBuilder stringBuilder, string? indentation = null) + => stringBuilder.Append("all do not depend on ").Append(options.Describe()); + + protected override void AppendNormalResult(StringBuilder stringBuilder, string? indentation = null) + { + stringBuilder.Append(it).Append(" contained types with the dependency "); + Formatter.Format(stringBuilder, NotMatching, FormattingOptions.Indented(indentation)); + } + + protected override void AppendNegatedExpectation(StringBuilder stringBuilder, string? indentation = null) + => stringBuilder.Append("not all do not depend on ").Append(options.Describe()); + + protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null) + { + stringBuilder.Append(it).Append(" only contained types without the dependency "); + Formatter.Format(stringBuilder, Matching, FormattingOptions.Indented(indentation)); + } + } } diff --git a/Source/aweXpect.Reflection/ThatTypes.DependOnlyOn.cs b/Source/aweXpect.Reflection/ThatTypes.DependOnlyOn.cs index 292d3282..88f36a5c 100644 --- a/Source/aweXpect.Reflection/ThatTypes.DependOnlyOn.cs +++ b/Source/aweXpect.Reflection/ThatTypes.DependOnlyOn.cs @@ -1,16 +1,15 @@ using System; using System.Collections.Generic; using System.Text; +using System.Threading; +using System.Threading.Tasks; using aweXpect.Core; using aweXpect.Core.Constraints; using aweXpect.Customization; +using aweXpect.Reflection.Collections; using aweXpect.Reflection.Helpers; using aweXpect.Reflection.Options; using aweXpect.Reflection.Results; -#if NET8_0_OR_GREATER -using System.Threading; -using System.Threading.Tasks; -#endif // ReSharper disable PossibleMultipleEnumeration @@ -70,6 +69,70 @@ public static partial class ThatTypes } #endif + /// + /// Verifies that all items in the filtered collection of depend on (reference in their + /// signature) only types in the filtered collections of types or + /// , their own namespace or framework assemblies. + /// + /// + /// The target collections are resolved once per assertion; a dependency is allowed when it is a member of the + /// union of the resolved collections (by identity; a generic type definition in a + /// collection matches any construction of it). A type's own namespace is always allowed, including its + /// sub-namespaces unless + /// is used. + /// + /// Dependencies on types whose assembly name matches one of the + /// at a + /// name-segment boundary (System covers System.Text.Json, but not + /// SystemsBiology.Core) are ignored, so + /// that framework types do not have to be included explicitly. The default prefixes include + /// Microsoft, so e.g. Microsoft.EntityFrameworkCore is also ignored; forbid such a dependency + /// explicitly via DoNotDependOn or customize the prefixes. + /// + public static TypeSetDependencyOnlyOnResult> DependOnlyOn( + this IThat> subject, Filtered.Types target, params Filtered.Types[] additional) + { + TypeSetDependencyOptions options = new(target, additional); + return new TypeSetDependencyOnlyOnResult>(subject.Get().ExpectationBuilder + .AddConstraint>((it, grammars) + => new DependOnlyOnTypeSetConstraint(it, grammars, options)), + subject, + options); + } + +#if NET8_0_OR_GREATER + /// + /// Verifies that all items in the filtered collection of depend on (reference in their + /// signature) only types in the filtered collections of types or + /// , their own namespace or framework assemblies. + /// + /// + /// The target collections are resolved once per assertion; a dependency is allowed when it is a member of the + /// union of the resolved collections (by identity; a generic type definition in a + /// collection matches any construction of it). A type's own namespace is always allowed, including its + /// sub-namespaces unless + /// is used. + /// + /// Dependencies on types whose assembly name matches one of the + /// at a + /// name-segment boundary (System covers System.Text.Json, but not + /// SystemsBiology.Core) are ignored, so + /// that framework types do not have to be included explicitly. The default prefixes include + /// Microsoft, so e.g. Microsoft.EntityFrameworkCore is also ignored; forbid such a dependency + /// explicitly via DoNotDependOn or customize the prefixes. + /// + public static TypeSetDependencyOnlyOnResult> DependOnlyOn( + this IThat> subject, Filtered.Types target, params Filtered.Types[] additional) + { + TypeSetDependencyOptions options = new(target, additional); + return new TypeSetDependencyOnlyOnResult>(subject.Get().ExpectationBuilder + .AddConstraint>((it, grammars) + => new DependOnlyOnTypeSetConstraint(it, grammars, options)), + subject, + options); + } +#endif + private sealed class DependOnlyOnConstraint( string it, ExpectationGrammars grammars, @@ -122,4 +185,63 @@ protected override void AppendNegatedResult(StringBuilder stringBuilder, string? Formatter.Format(stringBuilder, Matching, FormattingOptions.Indented(indentation)); } } + + private sealed class DependOnlyOnTypeSetConstraint( + string it, + ExpectationGrammars grammars, + TypeSetDependencyOptions options) + : CollectionConstraintResult(grammars), + IAsyncConstraint> +#if NET8_0_OR_GREATER + , IAsyncConstraint> +#endif + { + private readonly Dictionary> _violations = new(); + + private bool DependsOnlyOnAllowed(Type? type, ResolvedTypeSet allowed) + { + if (type is null) + { + return false; + } + + IReadOnlyList violations = type.GetDependencyTypeSetViolations(allowed); + if (violations.Count > 0) + { + _violations[type] = violations; + } + + return violations.Count == 0; + } + +#if NET8_0_OR_GREATER + public async Task IsMetBy(IAsyncEnumerable actual, CancellationToken cancellationToken) + { + ResolvedTypeSet allowed = await options.Resolve(cancellationToken); + return await SetAsyncValue(actual, type => DependsOnlyOnAllowed(type, allowed)); + } +#endif + + public async Task IsMetBy(IEnumerable actual, CancellationToken cancellationToken) + { + ResolvedTypeSet allowed = await options.Resolve(cancellationToken); + return SetValue(actual, type => DependsOnlyOnAllowed(type, allowed)); + } + + protected override void AppendNormalExpectation(StringBuilder stringBuilder, string? indentation = null) + => stringBuilder.Append("all depend only on ").Append(options.Describe()); + + protected override void AppendNormalResult(StringBuilder stringBuilder, string? indentation = null) + => DependencyViolationRenderer.AppendItemsWithDisallowedDependencies(stringBuilder, it, + " contained types with disallowed dependencies ", NotMatching, _violations, indentation); + + protected override void AppendNegatedExpectation(StringBuilder stringBuilder, string? indentation = null) + => stringBuilder.Append("not all depend only on ").Append(options.Describe()); + + protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null) + { + stringBuilder.Append(it).Append(" only contained types depending only on the allowed types "); + Formatter.Format(stringBuilder, Matching, FormattingOptions.Indented(indentation)); + } + } } diff --git a/Tests/aweXpect.Reflection.Api.Tests/Expected/aweXpect.Reflection_net10.0.txt b/Tests/aweXpect.Reflection.Api.Tests/Expected/aweXpect.Reflection_net10.0.txt index 7a9a8965..99151c76 100644 --- a/Tests/aweXpect.Reflection.Api.Tests/Expected/aweXpect.Reflection_net10.0.txt +++ b/Tests/aweXpect.Reflection.Api.Tests/Expected/aweXpect.Reflection_net10.0.txt @@ -1619,10 +1619,13 @@ namespace aweXpect.Reflection public static aweXpect.Reflection.Results.TypeContainingMembersResult ContainsProperties(this aweXpect.Core.IThat subject, System.Func filter, aweXpect.Reflection.Collections.MemberScope memberScope = 0) { } public static aweXpect.Reflection.Results.NamespaceDependencyResult DependsOn(this aweXpect.Core.IThat subject, System.Collections.Generic.IEnumerable namespaces) { } public static aweXpect.Reflection.Results.TypeDependencyResult DependsOn(this aweXpect.Core.IThat subject, System.Type type) { } + public static aweXpect.Reflection.Results.TypeSetDependencyResult DependsOn(this aweXpect.Core.IThat subject, aweXpect.Reflection.Collections.Filtered.Types target, params aweXpect.Reflection.Collections.Filtered.Types[] additional) { } public static aweXpect.Reflection.Results.TypeDependencyResult DependsOn(this aweXpect.Core.IThat subject) { } public static aweXpect.Reflection.Results.NamespaceDependencyOnlyOnResult DependsOnlyOn(this aweXpect.Core.IThat subject, System.Collections.Generic.IEnumerable namespaces) { } + public static aweXpect.Reflection.Results.TypeSetDependencyOnlyOnResult DependsOnlyOn(this aweXpect.Core.IThat subject, aweXpect.Reflection.Collections.Filtered.Types target, params aweXpect.Reflection.Collections.Filtered.Types[] additional) { } public static aweXpect.Reflection.Results.NamespaceDependencyResult DoesNotDependOn(this aweXpect.Core.IThat subject, System.Collections.Generic.IEnumerable namespaces) { } public static aweXpect.Reflection.Results.TypeDependencyResult DoesNotDependOn(this aweXpect.Core.IThat subject, System.Type type) { } + public static aweXpect.Reflection.Results.TypeSetDependencyResult DoesNotDependOn(this aweXpect.Core.IThat subject, aweXpect.Reflection.Collections.Filtered.Types target, params aweXpect.Reflection.Collections.Filtered.Types[] additional) { } public static aweXpect.Reflection.Results.TypeDependencyResult DoesNotDependOn(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> DoesNotHave(this aweXpect.Core.IThat subject, bool inherit = true) where TAttribute : System.Attribute { } @@ -1802,10 +1805,16 @@ namespace aweXpect.Reflection public static aweXpect.Reflection.Results.TypeContainingMembersResult> ContainProperties(this aweXpect.Core.IThat> subject, System.Func filter, aweXpect.Reflection.Collections.MemberScope memberScope = 0) { } public static aweXpect.Reflection.Results.NamespaceDependencyResult> DependOn(this aweXpect.Core.IThat> subject, System.Collections.Generic.IEnumerable namespaces) { } public static aweXpect.Reflection.Results.NamespaceDependencyResult> DependOn(this aweXpect.Core.IThat> subject, System.Collections.Generic.IEnumerable namespaces) { } + public static aweXpect.Reflection.Results.TypeSetDependencyResult> DependOn(this aweXpect.Core.IThat> subject, aweXpect.Reflection.Collections.Filtered.Types target, params aweXpect.Reflection.Collections.Filtered.Types[] additional) { } + public static aweXpect.Reflection.Results.TypeSetDependencyResult> DependOn(this aweXpect.Core.IThat> subject, aweXpect.Reflection.Collections.Filtered.Types target, params aweXpect.Reflection.Collections.Filtered.Types[] additional) { } public static aweXpect.Reflection.Results.NamespaceDependencyOnlyOnResult> DependOnlyOn(this aweXpect.Core.IThat> subject, System.Collections.Generic.IEnumerable namespaces) { } public static aweXpect.Reflection.Results.NamespaceDependencyOnlyOnResult> DependOnlyOn(this aweXpect.Core.IThat> subject, System.Collections.Generic.IEnumerable namespaces) { } + public static aweXpect.Reflection.Results.TypeSetDependencyOnlyOnResult> DependOnlyOn(this aweXpect.Core.IThat> subject, aweXpect.Reflection.Collections.Filtered.Types target, params aweXpect.Reflection.Collections.Filtered.Types[] additional) { } + public static aweXpect.Reflection.Results.TypeSetDependencyOnlyOnResult> DependOnlyOn(this aweXpect.Core.IThat> subject, aweXpect.Reflection.Collections.Filtered.Types target, params aweXpect.Reflection.Collections.Filtered.Types[] additional) { } public static aweXpect.Reflection.Results.NamespaceDependencyResult> DoNotDependOn(this aweXpect.Core.IThat> subject, System.Collections.Generic.IEnumerable namespaces) { } public static aweXpect.Reflection.Results.NamespaceDependencyResult> DoNotDependOn(this aweXpect.Core.IThat> subject, System.Collections.Generic.IEnumerable namespaces) { } + public static aweXpect.Reflection.Results.TypeSetDependencyResult> DoNotDependOn(this aweXpect.Core.IThat> subject, aweXpect.Reflection.Collections.Filtered.Types target, params aweXpect.Reflection.Collections.Filtered.Types[] additional) { } + public static aweXpect.Reflection.Results.TypeSetDependencyResult> DoNotDependOn(this aweXpect.Core.IThat> subject, aweXpect.Reflection.Collections.Filtered.Types target, params aweXpect.Reflection.Collections.Filtered.Types[] additional) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> DoNotHave(this aweXpect.Core.IThat> subject, bool inherit = true) where TAttribute : System.Attribute { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> DoNotHave(this aweXpect.Core.IThat> subject, bool inherit = true) @@ -1943,8 +1952,11 @@ namespace aweXpect.Reflection public static aweXpect.Reflection.TypeFilters.TypesContainingMembers WhichContainMethods(this aweXpect.Reflection.Collections.Filtered.Types @this, System.Func filter, aweXpect.Reflection.Collections.MemberScope memberScope = 0) { } public static aweXpect.Reflection.TypeFilters.TypesContainingMembers WhichContainProperties(this aweXpect.Reflection.Collections.Filtered.Types @this, System.Func filter, aweXpect.Reflection.Collections.MemberScope memberScope = 0) { } public static aweXpect.Reflection.Collections.Filtered.Types.NamespaceDependencyFilterResult WhichDependOn(this aweXpect.Reflection.Collections.Filtered.Types @this, System.Collections.Generic.IEnumerable namespaces) { } + public static aweXpect.Reflection.Collections.Filtered.Types.TypeSetDependencyFilterResult WhichDependOn(this aweXpect.Reflection.Collections.Filtered.Types @this, aweXpect.Reflection.Collections.Filtered.Types target, params aweXpect.Reflection.Collections.Filtered.Types[] additional) { } public static aweXpect.Reflection.Collections.Filtered.Types.NamespaceDependencyOnlyOnFilterResult WhichDependOnlyOn(this aweXpect.Reflection.Collections.Filtered.Types @this, System.Collections.Generic.IEnumerable namespaces) { } + public static aweXpect.Reflection.Collections.Filtered.Types.TypeSetDependencyOnlyOnFilterResult WhichDependOnlyOn(this aweXpect.Reflection.Collections.Filtered.Types @this, aweXpect.Reflection.Collections.Filtered.Types target, params aweXpect.Reflection.Collections.Filtered.Types[] additional) { } public static aweXpect.Reflection.Collections.Filtered.Types.NamespaceDependencyFilterResult WhichDoNotDependOn(this aweXpect.Reflection.Collections.Filtered.Types @this, System.Collections.Generic.IEnumerable namespaces) { } + public static aweXpect.Reflection.Collections.Filtered.Types.TypeSetDependencyFilterResult WhichDoNotDependOn(this aweXpect.Reflection.Collections.Filtered.Types @this, aweXpect.Reflection.Collections.Filtered.Types target, params aweXpect.Reflection.Collections.Filtered.Types[] additional) { } public static aweXpect.Reflection.Collections.Filtered.Types WhichDoNotHaveADefaultConstructor(this aweXpect.Reflection.Collections.Filtered.Types @this) { } public static aweXpect.Reflection.Collections.Filtered.Types WhichDoNotImplement(this aweXpect.Reflection.Collections.Filtered.Types @this, System.Type interfaceType, bool forceDirect = false) { } public static aweXpect.Reflection.Collections.Filtered.Types WhichDoNotImplement(this aweXpect.Reflection.Collections.Filtered.Types @this, bool forceDirect = false) { } @@ -2230,6 +2242,15 @@ namespace aweXpect.Reflection.Collections public aweXpect.Reflection.Collections.Filtered.Types.StringEqualityResult AsWildcard() { } public aweXpect.Reflection.Collections.Filtered.Types.StringEqualityResult Exactly() { } } + public sealed class TypeSetDependencyFilterResult : aweXpect.Reflection.Collections.Filtered.Types + { + public aweXpect.Reflection.Collections.Filtered.Types.TypeSetDependencyFilterResult OrOn(params aweXpect.Reflection.Collections.Filtered.Types[] targets) { } + } + public sealed class TypeSetDependencyOnlyOnFilterResult : aweXpect.Reflection.Collections.Filtered.Types + { + public aweXpect.Reflection.Collections.Filtered.Types.TypeSetDependencyOnlyOnFilterResult ExcludingOwnSubNamespaces() { } + public aweXpect.Reflection.Collections.Filtered.Types.TypeSetDependencyOnlyOnFilterResult OrOn(params aweXpect.Reflection.Collections.Filtered.Types[] targets) { } + } } } public abstract class Filtered : System.Collections.Generic.IAsyncEnumerable @@ -2510,4 +2531,13 @@ namespace aweXpect.Reflection.Results public aweXpect.Reflection.Results.TypeDependencyResult OrOn(System.Type type) { } public aweXpect.Reflection.Results.TypeDependencyResult OrOn() { } } + public sealed class TypeSetDependencyOnlyOnResult : aweXpect.Results.AndOrResult> + { + public aweXpect.Reflection.Results.TypeSetDependencyOnlyOnResult ExcludingOwnSubNamespaces() { } + public aweXpect.Reflection.Results.TypeSetDependencyOnlyOnResult OrOn(params aweXpect.Reflection.Collections.Filtered.Types[] targets) { } + } + public sealed class TypeSetDependencyResult : aweXpect.Results.AndOrResult> + { + public aweXpect.Reflection.Results.TypeSetDependencyResult OrOn(params aweXpect.Reflection.Collections.Filtered.Types[] targets) { } + } } \ No newline at end of file diff --git a/Tests/aweXpect.Reflection.Api.Tests/Expected/aweXpect.Reflection_net8.0.txt b/Tests/aweXpect.Reflection.Api.Tests/Expected/aweXpect.Reflection_net8.0.txt index 6ef55401..28c2442d 100644 --- a/Tests/aweXpect.Reflection.Api.Tests/Expected/aweXpect.Reflection_net8.0.txt +++ b/Tests/aweXpect.Reflection.Api.Tests/Expected/aweXpect.Reflection_net8.0.txt @@ -1619,10 +1619,13 @@ namespace aweXpect.Reflection public static aweXpect.Reflection.Results.TypeContainingMembersResult ContainsProperties(this aweXpect.Core.IThat subject, System.Func filter, aweXpect.Reflection.Collections.MemberScope memberScope = 0) { } public static aweXpect.Reflection.Results.NamespaceDependencyResult DependsOn(this aweXpect.Core.IThat subject, System.Collections.Generic.IEnumerable namespaces) { } public static aweXpect.Reflection.Results.TypeDependencyResult DependsOn(this aweXpect.Core.IThat subject, System.Type type) { } + public static aweXpect.Reflection.Results.TypeSetDependencyResult DependsOn(this aweXpect.Core.IThat subject, aweXpect.Reflection.Collections.Filtered.Types target, params aweXpect.Reflection.Collections.Filtered.Types[] additional) { } public static aweXpect.Reflection.Results.TypeDependencyResult DependsOn(this aweXpect.Core.IThat subject) { } public static aweXpect.Reflection.Results.NamespaceDependencyOnlyOnResult DependsOnlyOn(this aweXpect.Core.IThat subject, System.Collections.Generic.IEnumerable namespaces) { } + public static aweXpect.Reflection.Results.TypeSetDependencyOnlyOnResult DependsOnlyOn(this aweXpect.Core.IThat subject, aweXpect.Reflection.Collections.Filtered.Types target, params aweXpect.Reflection.Collections.Filtered.Types[] additional) { } public static aweXpect.Reflection.Results.NamespaceDependencyResult DoesNotDependOn(this aweXpect.Core.IThat subject, System.Collections.Generic.IEnumerable namespaces) { } public static aweXpect.Reflection.Results.TypeDependencyResult DoesNotDependOn(this aweXpect.Core.IThat subject, System.Type type) { } + public static aweXpect.Reflection.Results.TypeSetDependencyResult DoesNotDependOn(this aweXpect.Core.IThat subject, aweXpect.Reflection.Collections.Filtered.Types target, params aweXpect.Reflection.Collections.Filtered.Types[] additional) { } public static aweXpect.Reflection.Results.TypeDependencyResult DoesNotDependOn(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> DoesNotHave(this aweXpect.Core.IThat subject, bool inherit = true) where TAttribute : System.Attribute { } @@ -1802,10 +1805,16 @@ namespace aweXpect.Reflection public static aweXpect.Reflection.Results.TypeContainingMembersResult> ContainProperties(this aweXpect.Core.IThat> subject, System.Func filter, aweXpect.Reflection.Collections.MemberScope memberScope = 0) { } public static aweXpect.Reflection.Results.NamespaceDependencyResult> DependOn(this aweXpect.Core.IThat> subject, System.Collections.Generic.IEnumerable namespaces) { } public static aweXpect.Reflection.Results.NamespaceDependencyResult> DependOn(this aweXpect.Core.IThat> subject, System.Collections.Generic.IEnumerable namespaces) { } + public static aweXpect.Reflection.Results.TypeSetDependencyResult> DependOn(this aweXpect.Core.IThat> subject, aweXpect.Reflection.Collections.Filtered.Types target, params aweXpect.Reflection.Collections.Filtered.Types[] additional) { } + public static aweXpect.Reflection.Results.TypeSetDependencyResult> DependOn(this aweXpect.Core.IThat> subject, aweXpect.Reflection.Collections.Filtered.Types target, params aweXpect.Reflection.Collections.Filtered.Types[] additional) { } public static aweXpect.Reflection.Results.NamespaceDependencyOnlyOnResult> DependOnlyOn(this aweXpect.Core.IThat> subject, System.Collections.Generic.IEnumerable namespaces) { } public static aweXpect.Reflection.Results.NamespaceDependencyOnlyOnResult> DependOnlyOn(this aweXpect.Core.IThat> subject, System.Collections.Generic.IEnumerable namespaces) { } + public static aweXpect.Reflection.Results.TypeSetDependencyOnlyOnResult> DependOnlyOn(this aweXpect.Core.IThat> subject, aweXpect.Reflection.Collections.Filtered.Types target, params aweXpect.Reflection.Collections.Filtered.Types[] additional) { } + public static aweXpect.Reflection.Results.TypeSetDependencyOnlyOnResult> DependOnlyOn(this aweXpect.Core.IThat> subject, aweXpect.Reflection.Collections.Filtered.Types target, params aweXpect.Reflection.Collections.Filtered.Types[] additional) { } public static aweXpect.Reflection.Results.NamespaceDependencyResult> DoNotDependOn(this aweXpect.Core.IThat> subject, System.Collections.Generic.IEnumerable namespaces) { } public static aweXpect.Reflection.Results.NamespaceDependencyResult> DoNotDependOn(this aweXpect.Core.IThat> subject, System.Collections.Generic.IEnumerable namespaces) { } + public static aweXpect.Reflection.Results.TypeSetDependencyResult> DoNotDependOn(this aweXpect.Core.IThat> subject, aweXpect.Reflection.Collections.Filtered.Types target, params aweXpect.Reflection.Collections.Filtered.Types[] additional) { } + public static aweXpect.Reflection.Results.TypeSetDependencyResult> DoNotDependOn(this aweXpect.Core.IThat> subject, aweXpect.Reflection.Collections.Filtered.Types target, params aweXpect.Reflection.Collections.Filtered.Types[] additional) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> DoNotHave(this aweXpect.Core.IThat> subject, bool inherit = true) where TAttribute : System.Attribute { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> DoNotHave(this aweXpect.Core.IThat> subject, bool inherit = true) @@ -1943,8 +1952,11 @@ namespace aweXpect.Reflection public static aweXpect.Reflection.TypeFilters.TypesContainingMembers WhichContainMethods(this aweXpect.Reflection.Collections.Filtered.Types @this, System.Func filter, aweXpect.Reflection.Collections.MemberScope memberScope = 0) { } public static aweXpect.Reflection.TypeFilters.TypesContainingMembers WhichContainProperties(this aweXpect.Reflection.Collections.Filtered.Types @this, System.Func filter, aweXpect.Reflection.Collections.MemberScope memberScope = 0) { } public static aweXpect.Reflection.Collections.Filtered.Types.NamespaceDependencyFilterResult WhichDependOn(this aweXpect.Reflection.Collections.Filtered.Types @this, System.Collections.Generic.IEnumerable namespaces) { } + public static aweXpect.Reflection.Collections.Filtered.Types.TypeSetDependencyFilterResult WhichDependOn(this aweXpect.Reflection.Collections.Filtered.Types @this, aweXpect.Reflection.Collections.Filtered.Types target, params aweXpect.Reflection.Collections.Filtered.Types[] additional) { } public static aweXpect.Reflection.Collections.Filtered.Types.NamespaceDependencyOnlyOnFilterResult WhichDependOnlyOn(this aweXpect.Reflection.Collections.Filtered.Types @this, System.Collections.Generic.IEnumerable namespaces) { } + public static aweXpect.Reflection.Collections.Filtered.Types.TypeSetDependencyOnlyOnFilterResult WhichDependOnlyOn(this aweXpect.Reflection.Collections.Filtered.Types @this, aweXpect.Reflection.Collections.Filtered.Types target, params aweXpect.Reflection.Collections.Filtered.Types[] additional) { } public static aweXpect.Reflection.Collections.Filtered.Types.NamespaceDependencyFilterResult WhichDoNotDependOn(this aweXpect.Reflection.Collections.Filtered.Types @this, System.Collections.Generic.IEnumerable namespaces) { } + public static aweXpect.Reflection.Collections.Filtered.Types.TypeSetDependencyFilterResult WhichDoNotDependOn(this aweXpect.Reflection.Collections.Filtered.Types @this, aweXpect.Reflection.Collections.Filtered.Types target, params aweXpect.Reflection.Collections.Filtered.Types[] additional) { } public static aweXpect.Reflection.Collections.Filtered.Types WhichDoNotHaveADefaultConstructor(this aweXpect.Reflection.Collections.Filtered.Types @this) { } public static aweXpect.Reflection.Collections.Filtered.Types WhichDoNotImplement(this aweXpect.Reflection.Collections.Filtered.Types @this, System.Type interfaceType, bool forceDirect = false) { } public static aweXpect.Reflection.Collections.Filtered.Types WhichDoNotImplement(this aweXpect.Reflection.Collections.Filtered.Types @this, bool forceDirect = false) { } @@ -2230,6 +2242,15 @@ namespace aweXpect.Reflection.Collections public aweXpect.Reflection.Collections.Filtered.Types.StringEqualityResult AsWildcard() { } public aweXpect.Reflection.Collections.Filtered.Types.StringEqualityResult Exactly() { } } + public sealed class TypeSetDependencyFilterResult : aweXpect.Reflection.Collections.Filtered.Types + { + public aweXpect.Reflection.Collections.Filtered.Types.TypeSetDependencyFilterResult OrOn(params aweXpect.Reflection.Collections.Filtered.Types[] targets) { } + } + public sealed class TypeSetDependencyOnlyOnFilterResult : aweXpect.Reflection.Collections.Filtered.Types + { + public aweXpect.Reflection.Collections.Filtered.Types.TypeSetDependencyOnlyOnFilterResult ExcludingOwnSubNamespaces() { } + public aweXpect.Reflection.Collections.Filtered.Types.TypeSetDependencyOnlyOnFilterResult OrOn(params aweXpect.Reflection.Collections.Filtered.Types[] targets) { } + } } } public abstract class Filtered : System.Collections.Generic.IAsyncEnumerable @@ -2510,4 +2531,13 @@ namespace aweXpect.Reflection.Results public aweXpect.Reflection.Results.TypeDependencyResult OrOn(System.Type type) { } public aweXpect.Reflection.Results.TypeDependencyResult OrOn() { } } + public sealed class TypeSetDependencyOnlyOnResult : aweXpect.Results.AndOrResult> + { + public aweXpect.Reflection.Results.TypeSetDependencyOnlyOnResult ExcludingOwnSubNamespaces() { } + public aweXpect.Reflection.Results.TypeSetDependencyOnlyOnResult OrOn(params aweXpect.Reflection.Collections.Filtered.Types[] targets) { } + } + public sealed class TypeSetDependencyResult : aweXpect.Results.AndOrResult> + { + public aweXpect.Reflection.Results.TypeSetDependencyResult OrOn(params aweXpect.Reflection.Collections.Filtered.Types[] targets) { } + } } \ No newline at end of file diff --git a/Tests/aweXpect.Reflection.Api.Tests/Expected/aweXpect.Reflection_netstandard2.0.txt b/Tests/aweXpect.Reflection.Api.Tests/Expected/aweXpect.Reflection_netstandard2.0.txt index 279732d5..0126d718 100644 --- a/Tests/aweXpect.Reflection.Api.Tests/Expected/aweXpect.Reflection_netstandard2.0.txt +++ b/Tests/aweXpect.Reflection.Api.Tests/Expected/aweXpect.Reflection_netstandard2.0.txt @@ -1343,10 +1343,13 @@ namespace aweXpect.Reflection public static aweXpect.Reflection.Results.TypeContainingMembersResult ContainsProperties(this aweXpect.Core.IThat subject, System.Func filter, aweXpect.Reflection.Collections.MemberScope memberScope = 0) { } public static aweXpect.Reflection.Results.NamespaceDependencyResult DependsOn(this aweXpect.Core.IThat subject, System.Collections.Generic.IEnumerable namespaces) { } public static aweXpect.Reflection.Results.TypeDependencyResult DependsOn(this aweXpect.Core.IThat subject, System.Type type) { } + public static aweXpect.Reflection.Results.TypeSetDependencyResult DependsOn(this aweXpect.Core.IThat subject, aweXpect.Reflection.Collections.Filtered.Types target, params aweXpect.Reflection.Collections.Filtered.Types[] additional) { } public static aweXpect.Reflection.Results.TypeDependencyResult DependsOn(this aweXpect.Core.IThat subject) { } public static aweXpect.Reflection.Results.NamespaceDependencyOnlyOnResult DependsOnlyOn(this aweXpect.Core.IThat subject, System.Collections.Generic.IEnumerable namespaces) { } + public static aweXpect.Reflection.Results.TypeSetDependencyOnlyOnResult DependsOnlyOn(this aweXpect.Core.IThat subject, aweXpect.Reflection.Collections.Filtered.Types target, params aweXpect.Reflection.Collections.Filtered.Types[] additional) { } public static aweXpect.Reflection.Results.NamespaceDependencyResult DoesNotDependOn(this aweXpect.Core.IThat subject, System.Collections.Generic.IEnumerable namespaces) { } public static aweXpect.Reflection.Results.TypeDependencyResult DoesNotDependOn(this aweXpect.Core.IThat subject, System.Type type) { } + public static aweXpect.Reflection.Results.TypeSetDependencyResult DoesNotDependOn(this aweXpect.Core.IThat subject, aweXpect.Reflection.Collections.Filtered.Types target, params aweXpect.Reflection.Collections.Filtered.Types[] additional) { } public static aweXpect.Reflection.Results.TypeDependencyResult DoesNotDependOn(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> DoesNotHave(this aweXpect.Core.IThat subject, bool inherit = true) where TAttribute : System.Attribute { } @@ -1476,8 +1479,11 @@ namespace aweXpect.Reflection public static aweXpect.Reflection.Results.TypeContainingMembersResult> ContainMethods(this aweXpect.Core.IThat> subject, System.Func filter, aweXpect.Reflection.Collections.MemberScope memberScope = 0) { } public static aweXpect.Reflection.Results.TypeContainingMembersResult> ContainProperties(this aweXpect.Core.IThat> subject, System.Func filter, aweXpect.Reflection.Collections.MemberScope memberScope = 0) { } public static aweXpect.Reflection.Results.NamespaceDependencyResult> DependOn(this aweXpect.Core.IThat> subject, System.Collections.Generic.IEnumerable namespaces) { } + public static aweXpect.Reflection.Results.TypeSetDependencyResult> DependOn(this aweXpect.Core.IThat> subject, aweXpect.Reflection.Collections.Filtered.Types target, params aweXpect.Reflection.Collections.Filtered.Types[] additional) { } public static aweXpect.Reflection.Results.NamespaceDependencyOnlyOnResult> DependOnlyOn(this aweXpect.Core.IThat> subject, System.Collections.Generic.IEnumerable namespaces) { } + public static aweXpect.Reflection.Results.TypeSetDependencyOnlyOnResult> DependOnlyOn(this aweXpect.Core.IThat> subject, aweXpect.Reflection.Collections.Filtered.Types target, params aweXpect.Reflection.Collections.Filtered.Types[] additional) { } public static aweXpect.Reflection.Results.NamespaceDependencyResult> DoNotDependOn(this aweXpect.Core.IThat> subject, System.Collections.Generic.IEnumerable namespaces) { } + public static aweXpect.Reflection.Results.TypeSetDependencyResult> DoNotDependOn(this aweXpect.Core.IThat> subject, aweXpect.Reflection.Collections.Filtered.Types target, params aweXpect.Reflection.Collections.Filtered.Types[] additional) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> DoNotHave(this aweXpect.Core.IThat> subject, bool inherit = true) where TAttribute : System.Attribute { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> DoNotHaveADefaultConstructor(this aweXpect.Core.IThat> subject) { } @@ -1582,8 +1588,11 @@ namespace aweXpect.Reflection public static aweXpect.Reflection.TypeFilters.TypesContainingMembers WhichContainMethods(this aweXpect.Reflection.Collections.Filtered.Types @this, System.Func filter, aweXpect.Reflection.Collections.MemberScope memberScope = 0) { } public static aweXpect.Reflection.TypeFilters.TypesContainingMembers WhichContainProperties(this aweXpect.Reflection.Collections.Filtered.Types @this, System.Func filter, aweXpect.Reflection.Collections.MemberScope memberScope = 0) { } public static aweXpect.Reflection.Collections.Filtered.Types.NamespaceDependencyFilterResult WhichDependOn(this aweXpect.Reflection.Collections.Filtered.Types @this, System.Collections.Generic.IEnumerable namespaces) { } + public static aweXpect.Reflection.Collections.Filtered.Types.TypeSetDependencyFilterResult WhichDependOn(this aweXpect.Reflection.Collections.Filtered.Types @this, aweXpect.Reflection.Collections.Filtered.Types target, params aweXpect.Reflection.Collections.Filtered.Types[] additional) { } public static aweXpect.Reflection.Collections.Filtered.Types.NamespaceDependencyOnlyOnFilterResult WhichDependOnlyOn(this aweXpect.Reflection.Collections.Filtered.Types @this, System.Collections.Generic.IEnumerable namespaces) { } + public static aweXpect.Reflection.Collections.Filtered.Types.TypeSetDependencyOnlyOnFilterResult WhichDependOnlyOn(this aweXpect.Reflection.Collections.Filtered.Types @this, aweXpect.Reflection.Collections.Filtered.Types target, params aweXpect.Reflection.Collections.Filtered.Types[] additional) { } public static aweXpect.Reflection.Collections.Filtered.Types.NamespaceDependencyFilterResult WhichDoNotDependOn(this aweXpect.Reflection.Collections.Filtered.Types @this, System.Collections.Generic.IEnumerable namespaces) { } + public static aweXpect.Reflection.Collections.Filtered.Types.TypeSetDependencyFilterResult WhichDoNotDependOn(this aweXpect.Reflection.Collections.Filtered.Types @this, aweXpect.Reflection.Collections.Filtered.Types target, params aweXpect.Reflection.Collections.Filtered.Types[] additional) { } public static aweXpect.Reflection.Collections.Filtered.Types WhichDoNotHaveADefaultConstructor(this aweXpect.Reflection.Collections.Filtered.Types @this) { } public static aweXpect.Reflection.Collections.Filtered.Types WhichDoNotImplement(this aweXpect.Reflection.Collections.Filtered.Types @this, System.Type interfaceType, bool forceDirect = false) { } public static aweXpect.Reflection.Collections.Filtered.Types WhichDoNotImplement(this aweXpect.Reflection.Collections.Filtered.Types @this, bool forceDirect = false) { } @@ -1869,6 +1878,15 @@ namespace aweXpect.Reflection.Collections public aweXpect.Reflection.Collections.Filtered.Types.StringEqualityResult AsWildcard() { } public aweXpect.Reflection.Collections.Filtered.Types.StringEqualityResult Exactly() { } } + public sealed class TypeSetDependencyFilterResult : aweXpect.Reflection.Collections.Filtered.Types + { + public aweXpect.Reflection.Collections.Filtered.Types.TypeSetDependencyFilterResult OrOn(params aweXpect.Reflection.Collections.Filtered.Types[] targets) { } + } + public sealed class TypeSetDependencyOnlyOnFilterResult : aweXpect.Reflection.Collections.Filtered.Types + { + public aweXpect.Reflection.Collections.Filtered.Types.TypeSetDependencyOnlyOnFilterResult ExcludingOwnSubNamespaces() { } + public aweXpect.Reflection.Collections.Filtered.Types.TypeSetDependencyOnlyOnFilterResult OrOn(params aweXpect.Reflection.Collections.Filtered.Types[] targets) { } + } } } public abstract class Filtered : System.Collections.Generic.IEnumerable, System.Collections.IEnumerable @@ -2148,4 +2166,13 @@ namespace aweXpect.Reflection.Results public aweXpect.Reflection.Results.TypeDependencyResult OrOn(System.Type type) { } public aweXpect.Reflection.Results.TypeDependencyResult OrOn() { } } + public sealed class TypeSetDependencyOnlyOnResult : aweXpect.Results.AndOrResult> + { + public aweXpect.Reflection.Results.TypeSetDependencyOnlyOnResult ExcludingOwnSubNamespaces() { } + public aweXpect.Reflection.Results.TypeSetDependencyOnlyOnResult OrOn(params aweXpect.Reflection.Collections.Filtered.Types[] targets) { } + } + public sealed class TypeSetDependencyResult : aweXpect.Results.AndOrResult> + { + public aweXpect.Reflection.Results.TypeSetDependencyResult OrOn(params aweXpect.Reflection.Collections.Filtered.Types[] targets) { } + } } \ No newline at end of file diff --git a/Tests/aweXpect.Reflection.Tests/ArchitectureRules.Tests.cs b/Tests/aweXpect.Reflection.Tests/ArchitectureRules.Tests.cs new file mode 100644 index 00000000..206d89bd --- /dev/null +++ b/Tests/aweXpect.Reflection.Tests/ArchitectureRules.Tests.cs @@ -0,0 +1,68 @@ +using aweXpect.Reflection.Collections; +using aweXpect.Reflection.Tests.TestHelpers.Dependencies.Consumers; + +namespace aweXpect.Reflection.Tests; + +/// +/// End-to-end tests for the architecture-rules pattern: layers are reusable +/// selections, combined into one verification with Expect.ThatAll(…). +/// +public sealed class ArchitectureRulesTests +{ + private const string ConsumersNamespace = "aweXpect.Reflection.Tests.TestHelpers.Dependencies.Consumers"; + private const string Layer1Namespace = "aweXpect.Reflection.Tests.TestHelpers.Dependencies.Layer1"; + private const string Layer2Namespace = "aweXpect.Reflection.Tests.TestHelpers.Dependencies.Layer2"; + + [Fact] + public async Task ShouldVerifyMultipleRulesWithThatAll() + { + // A "layer" is just a reusable Filtered.Types selection. + Filtered.Types layer1 = Types.InNamespace(Layer1Namespace); + Filtered.Types layer2 = Types.InNamespace(Layer2Namespace); + + async Task Act() + => await ThatAll( + // Filtered.Types as dependency target: + That(layer2).DoNotDependOn(layer1), + // Namespace as dependency target on the same selection: + That(layer2).DoNotDependOn(Layer1Namespace), + // Any other assertion works on the same selection: + That(layer2).AreWithinNamespace(Layer2Namespace)); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenARuleIsViolated_ShouldReportAggregatedFailure() + { + Filtered.Types layer1 = Types.InNamespace(Layer1Namespace); + Filtered.Types layer2 = Types.InNamespace(Layer2Namespace); + + async Task Act() + => await ThatAll( + // Fails: Layer1's TargetSeverityAttribute references Layer2's TargetSeverity. + That(layer1).DoNotDependOn(layer2), + // Succeeds: + That(layer2).AreWithinNamespace(Layer2Namespace)); + + await That(Act).ThrowsException() + .WithMessage("*TargetSeverityAttribute*").AsWildcard(); + } + + [Fact] + public async Task ShouldSupportExemptionsViaExcept() + { + Filtered.Types layer2 = Types.InNamespace(Layer2Namespace); + + async Task Act() + => await ThatAll( + // The consumers that intentionally reference Layer2 are exempted from the rule: + That(Types.InNamespace(ConsumersNamespace) + .Except() + .Except() + .Except()) + .DoNotDependOn(layer2)); + + await That(Act).DoesNotThrow(); + } +} diff --git a/Tests/aweXpect.Reflection.Tests/Filters/TypeFilters.WhichDependOn.Tests.cs b/Tests/aweXpect.Reflection.Tests/Filters/TypeFilters.WhichDependOn.Tests.cs index 6d0ca50a..e6d2bf4a 100644 --- a/Tests/aweXpect.Reflection.Tests/Filters/TypeFilters.WhichDependOn.Tests.cs +++ b/Tests/aweXpect.Reflection.Tests/Filters/TypeFilters.WhichDependOn.Tests.cs @@ -50,5 +50,53 @@ await That(Act).Throws() .WithMessage("At least one namespace must be specified."); } } + + public sealed class FilteredTypesTargetTests + { + [Fact] + public async Task ShouldFilterForTypesDependingOnTargetCollection() + { + Filtered.Types types = Types.InNamespace(ConsumersNamespace) + .WhichDependOn(Types.InNamespace(Layer1Namespace)); + + await That(types).Contains(typeof(ViaField)); + await That(types).Contains(typeof(ViaSubNamespace)); + await That(types).DoesNotContain(typeof(FrameworkConsumer)); + } + + [Fact] + public async Task WhenWidenedWithOrOn_ShouldMatchEither() + { + Filtered.Types types = Types.InNamespace(ConsumersNamespace) + .WhichDependOn(Types.InNamespace("Non.Existent.Namespace")) + .OrOn(Types.InNamespace(Layer1Namespace)); + + await That(types).Contains(typeof(ViaField)); + } + + [Fact] + public async Task OrOn_ShouldNotAffectOriginalFilter() + { + Filtered.Types.TypeSetDependencyFilterResult original = Types.InNamespace(ConsumersNamespace) + .WhichDependOn(Types.InNamespace("Non.Existent.Namespace")); + _ = original.OrOn(Types.InNamespace(Layer1Namespace)); + + await That(original).DoesNotContain(typeof(ViaField)); + } + + [Fact] + public async Task ShouldDelimitTargetDescriptionFromSourceSuffix() + { + // The parentheses keep the target's trailing source scope apart from the subject collection's + // own source suffix, so the two "in all loaded assemblies" do not run into each other. + Filtered.Types types = Types.InNamespace(ConsumersNamespace) + .WhichDependOn(Types.InNamespace(Layer1Namespace)); + + await That(types.GetDescription()).IsEqualTo( + $"types within namespace \"{ConsumersNamespace}\" which depend on " + + $"(types within namespace \"{Layer1Namespace}\" in all loaded assemblies) " + + "in all loaded assemblies"); + } + } } } diff --git a/Tests/aweXpect.Reflection.Tests/Filters/TypeFilters.WhichDependOnlyOn.Tests.cs b/Tests/aweXpect.Reflection.Tests/Filters/TypeFilters.WhichDependOnlyOn.Tests.cs index 46dfdccf..8cb47748 100644 --- a/Tests/aweXpect.Reflection.Tests/Filters/TypeFilters.WhichDependOnlyOn.Tests.cs +++ b/Tests/aweXpect.Reflection.Tests/Filters/TypeFilters.WhichDependOnlyOn.Tests.cs @@ -22,5 +22,52 @@ public async Task ShouldFilterForTypesDependingOnlyOnAllowedNamespaces() await That(types).DoesNotContain(typeof(Layer1AndLayer2)); } } + + public sealed class FilteredTypesTargetTests + { + [Fact] + public async Task ShouldFilterForTypesDependingOnlyOnTargetCollection() + { + Filtered.Types types = Types.InNamespace(ConsumersNamespace) + .WhichDependOnlyOn(Types.InNamespace(Layer1Namespace)); + + await That(types).Contains(typeof(OnlyLayer1)); + await That(types).Contains(typeof(FrameworkConsumer)); + await That(types).Contains(typeof(ReferencesOwnNamespace)); + await That(types).DoesNotContain(typeof(Layer1AndLayer2)); + } + + [Fact] + public async Task WhenWidenedWithOrOn_ShouldAllowEither() + { + const string layer2Namespace = "aweXpect.Reflection.Tests.TestHelpers.Dependencies.Layer2"; + Filtered.Types types = Types.InNamespace(ConsumersNamespace) + .WhichDependOnlyOn(Types.InNamespace(Layer1Namespace)) + .OrOn(Types.InNamespace(layer2Namespace)); + + await That(types).Contains(typeof(Layer1AndLayer2)); + } + + [Fact] + public async Task WhenExcludingOwnSubNamespaces_ShouldFilterOutTypesReferencingOwnSubNamespace() + { + Filtered.Types types = Types.InNamespace(ConsumersNamespace) + .WhichDependOnlyOn(Types.InNamespace(Layer1Namespace)) + .ExcludingOwnSubNamespaces(); + + await That(types).Contains(typeof(OnlyLayer1)); + await That(types).DoesNotContain(typeof(ReferencesOwnSubNamespace)); + } + + [Fact] + public async Task ExcludingOwnSubNamespaces_ShouldNotAffectOriginalFilter() + { + Filtered.Types.TypeSetDependencyOnlyOnFilterResult original = Types.InNamespace(ConsumersNamespace) + .WhichDependOnlyOn(Types.InNamespace(Layer1Namespace)); + _ = original.ExcludingOwnSubNamespaces(); + + await That(original).Contains(typeof(ReferencesOwnSubNamespace)); + } + } } } diff --git a/Tests/aweXpect.Reflection.Tests/Filters/TypeFilters.WhichDoNotDependOn.Tests.cs b/Tests/aweXpect.Reflection.Tests/Filters/TypeFilters.WhichDoNotDependOn.Tests.cs index f49728ba..ae6fbefd 100644 --- a/Tests/aweXpect.Reflection.Tests/Filters/TypeFilters.WhichDoNotDependOn.Tests.cs +++ b/Tests/aweXpect.Reflection.Tests/Filters/TypeFilters.WhichDoNotDependOn.Tests.cs @@ -21,5 +21,28 @@ public async Task ShouldFilterForTypesNotDependingOnNamespace() await That(types).DoesNotContain(typeof(ViaField)); } } + + public sealed class FilteredTypesTargetTests + { + [Fact] + public async Task ShouldFilterForTypesNotDependingOnTargetCollection() + { + Filtered.Types types = Types.InNamespace(ConsumersNamespace) + .WhichDoNotDependOn(Types.InNamespace(Layer1Namespace)); + + await That(types).Contains(typeof(FrameworkConsumer)); + await That(types).DoesNotContain(typeof(ViaField)); + } + + [Fact] + public async Task WhenTargetCollectionIsEmpty_ShouldKeepAllTypes() + { + Filtered.Types types = Types.InNamespace(ConsumersNamespace) + .WhichDoNotDependOn(Types.InNamespace("Non.Existent.Namespace")); + + await That(types).Contains(typeof(ViaField)); + await That(types).Contains(typeof(FrameworkConsumer)); + } + } } } diff --git a/Tests/aweXpect.Reflection.Tests/TestHelpers/Dependencies/DependencyFixtures.cs b/Tests/aweXpect.Reflection.Tests/TestHelpers/Dependencies/DependencyFixtures.cs index f529b1c6..c59494a2 100644 --- a/Tests/aweXpect.Reflection.Tests/TestHelpers/Dependencies/DependencyFixtures.cs +++ b/Tests/aweXpect.Reflection.Tests/TestHelpers/Dependencies/DependencyFixtures.cs @@ -224,6 +224,21 @@ private sealed class Nested [TargetSeverity(TargetSeverity.High)] public class ViaEnumAttributeArgument; + // Layer1's TargetGeneric is referenced ONLY as the constructed TargetGeneric; a scanned + // target collection contains the open definition, which must match the construction. + public class ViaLayer1GenericConstruction + { + private TargetGeneric _target; + } + + // Two distinct disallowed dependencies share the simple name "AmbiguousTarget"; the violation list + // must keep them apart by qualifying each with its namespace. + public class WithSameNamedDependencies + { + private AmbiguousA.AmbiguousTarget _a; + private AmbiguousB.AmbiguousTarget _b; + } + public class WithAsyncMethod { public static async void MethodAsync() => await Task.CompletedTask; @@ -322,3 +337,15 @@ public class CovariantReturnDerived : CovariantReturnBase } #endif } + +// Two namespaces deliberately declaring the same simple type name, so that violation messages must +// disambiguate (see WithSameNamedDependencies above). +namespace aweXpect.Reflection.Tests.TestHelpers.Dependencies.AmbiguousA +{ + public class AmbiguousTarget; +} + +namespace aweXpect.Reflection.Tests.TestHelpers.Dependencies.AmbiguousB +{ + public class AmbiguousTarget; +} diff --git a/Tests/aweXpect.Reflection.Tests/ThatType.DependsOn.Tests.cs b/Tests/aweXpect.Reflection.Tests/ThatType.DependsOn.Tests.cs index 7ccfd0dc..58c7cfa3 100644 --- a/Tests/aweXpect.Reflection.Tests/ThatType.DependsOn.Tests.cs +++ b/Tests/aweXpect.Reflection.Tests/ThatType.DependsOn.Tests.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using aweXpect.Reflection.Collections; using aweXpect.Reflection.Tests.TestHelpers.Dependencies.Consumers; using aweXpect.Reflection.Tests.TestHelpers.Dependencies.Layer1; using aweXpect.Reflection.Tests.TestHelpers.Dependencies.Layer2; @@ -469,6 +470,191 @@ await That(Act).Throws() } } + public sealed class FilteredTypesTargetTests + { + [Fact] + public async Task WhenTypeDependsOnTypeInTargetCollection_ShouldSucceed() + { + Type subject = typeof(ViaField); + + async Task Act() + => await That(subject).DependsOn(Types.InNamespace(Layer1Namespace)); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenTypeDoesNotDependOnAnyTypeInTargetCollection_ShouldFail() + { + Type subject = typeof(OnlyLayer1); + + async Task Act() + => await That(subject).DependsOn(Types.InNamespace(Layer2Namespace)); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + depends on types within namespace "{Layer2Namespace}" in all loaded assemblies, + but it did not + """); + } + + [Fact] + public async Task WhenAnyTargetCollectionMatches_ShouldSucceed() + { + Type subject = typeof(OnlyLayer1); + + async Task Act() + => await That(subject).DependsOn(Types.InNamespace(Layer2Namespace), Types.InNamespace(Layer1Namespace)); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenWidenedWithOrOn_ShouldSucceed() + { + Type subject = typeof(OnlyLayer1); + + async Task Act() + => await That(subject).DependsOn(Types.InNamespace(Layer2Namespace)).OrOn(Types.InNamespace(Layer1Namespace)); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenGenericTypeDefinitionIsInTargetCollection_ShouldMatchConstruction() + { + // ViaLayer1GenericConstruction references the constructed TargetGeneric; the scanned + // target collection contains the open definition TargetGeneric<>. + Type subject = typeof(ViaLayer1GenericConstruction); + + async Task Act() + => await That(subject).DependsOn(In.Type(typeof(TargetGeneric<>))); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenTargetCollectionContainsArrayType_ShouldMatchElementTypeDependency() + { + // Dependencies are stored element-stripped, so an array target matches like its element + // type, mirroring the specific-type overload DependsOn(typeof(TargetA[])). + Type subject = typeof(ViaField); + + async Task Act() + => await That(subject).DependsOn(In.Type(typeof(TargetA[]))); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenTargetCollectionContainsFrameworkTypes_ShouldMatchThem() + { + // A filtered collection is an explicit target, so framework types in it are checked normally. + Type subject = typeof(FrameworkConsumer); + + async Task Act() + => await That(subject).DependsOn(In.Type()); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenTargetCollectionIsEmpty_ShouldFail() + { + Type subject = typeof(ViaField); + + async Task Act() + => await That(subject).DependsOn(Types.InNamespace("Non.Existent.Namespace")); + + await That(Act).Throws(); + } + + [Fact] + public async Task WhenTypeIsNull_ShouldFail() + { + Type? subject = null; + + async Task Act() + => await That(subject).DependsOn(Types.InNamespace(Layer1Namespace)); + + await That(Act).ThrowsException() + .WithMessage($""" + Expected that subject + depends on types within namespace "{Layer1Namespace}" in all loaded assemblies, + but it was + """); + } + + [Fact] + public async Task WhenTargetIsNull_ShouldThrowArgumentNullException() + { + Type subject = typeof(OnlyLayer1); + + async Task Act() + => await That(subject).DependsOn((Filtered.Types)null!); + + await That(Act).Throws() + .WithMessage("The target collection of types must not be null.*").AsWildcard(); + } + + [Fact] + public async Task WhenAdditionalTargetsArrayIsNull_ShouldThrowArgumentNullException() + { + Type subject = typeof(OnlyLayer1); + + async Task Act() + => await That(subject).DependsOn(Types.InNamespace(Layer1Namespace), null!); + + await That(Act).Throws() + .WithMessage("The additional target collections of types must not be null.*").AsWildcard(); + } + + [Fact] + public async Task WhenAdditionalTargetIsNull_ShouldThrowArgumentNullException() + { + Type subject = typeof(OnlyLayer1); + + async Task Act() + => await That(subject) + .DependsOn(Types.InNamespace(Layer1Namespace), (Filtered.Types)null!, (Filtered.Types)null!); + + // The localized paramName suffix differs between frameworks ("(Parameter 'additional')" vs + // "Parametername: additional"), so only the shared part is matched. + await That(Act).Throws() + .WithMessage("The target collections of types must not contain null.*additional*") + .AsWildcard(); + } + + [Fact] + public async Task WhenWidenedWithoutTargets_ShouldThrowArgumentException() + { + Type subject = typeof(OnlyLayer1); + + async Task Act() + => await That(subject).DependsOn(Types.InNamespace(Layer1Namespace)).OrOn(); + + await That(Act).Throws() + .WithMessage("At least one collection of types must be specified."); + } + + [Fact] + public async Task WhenNegated_ShouldListMatchingDependencies() + { + Type subject = typeof(ViaField); + + async Task Act() + => await That(subject).DoesNotComplyWith(it => it.DependsOn(Types.InNamespace(Layer1Namespace))); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + does not depend on types within namespace "{Layer1Namespace}" in all loaded assemblies, + but it depended on [TargetA] + """); + } + } + public sealed class NegatedTests { [Fact] diff --git a/Tests/aweXpect.Reflection.Tests/ThatType.DependsOnlyOn.Tests.cs b/Tests/aweXpect.Reflection.Tests/ThatType.DependsOnlyOn.Tests.cs index d4ef2b14..afe1f6f1 100644 --- a/Tests/aweXpect.Reflection.Tests/ThatType.DependsOnlyOn.Tests.cs +++ b/Tests/aweXpect.Reflection.Tests/ThatType.DependsOnlyOn.Tests.cs @@ -1,4 +1,5 @@ using aweXpect.Reflection.Tests.TestHelpers.Dependencies.Consumers; +using aweXpect.Reflection.Tests.TestHelpers.Dependencies.Synthetic; using Xunit.Sdk; namespace aweXpect.Reflection.Tests; @@ -183,6 +184,149 @@ async Task Act() } } + public sealed class FilteredTypesTargetTests + { + [Fact] + public async Task WhenAllDependenciesAreInTargetCollection_ShouldSucceed() + { + Type subject = typeof(OnlyLayer1); + + async Task Act() + => await That(subject).DependsOnlyOn(Types.InNamespace(Layer1Namespace)); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenDependingOnTypeOutsideTargetCollections_ShouldFail() + { + Type subject = typeof(Layer1AndLayer2); + + async Task Act() + => await That(subject).DependsOnlyOn(Types.InNamespace(Layer1Namespace)); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + depends only on types within namespace "{Layer1Namespace}" in all loaded assemblies, + but it also depended on ["TargetB"] + """); + } + + [Fact] + public async Task WhenWidenedWithOrOn_ShouldSucceed() + { + Type subject = typeof(Layer1AndLayer2); + + async Task Act() + => await That(subject).DependsOnlyOn(Types.InNamespace(Layer1Namespace)) + .OrOn(Types.InNamespace(Layer2Namespace)); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenDistinctViolatorsShareTheSimpleName_ShouldQualifyThemByNamespace() + { + // Both AmbiguousTarget dependencies are disallowed; they must stay apart in the message + // instead of collapsing into one indistinguishable "AmbiguousTarget" entry. + Type subject = typeof(WithSameNamedDependencies); + + async Task Act() + => await That(subject).DependsOnlyOn(Types.InNamespace(Layer1Namespace)); + + await That(Act).Throws() + .WithMessage("*AmbiguousA.AmbiguousTarget*AmbiguousB.AmbiguousTarget*").AsWildcard(); + } + + [Fact] + public async Task WhenTargetCollectionIsEmpty_ShouldStillAllowFrameworkDependencies() + { + Type subject = typeof(FrameworkConsumer); + + async Task Act() + => await That(subject).DependsOnlyOn(Types.InNamespace("Non.Existent.Namespace")); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenTargetCollectionIsEmpty_ShouldStillAllowOwnNamespace() + { + Type subject = typeof(ReferencesOwnNamespace); + + async Task Act() + => await That(subject).DependsOnlyOn(Types.InNamespace("Non.Existent.Namespace")); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenTargetCollectionIsEmpty_DisallowedDependencyShouldFail() + { + Type subject = typeof(OnlyLayer1); + + async Task Act() + => await That(subject).DependsOnlyOn(Types.InNamespace("Non.Existent.Namespace")); + + await That(Act).Throws(); + } + + [Fact] + public async Task WhenNegated_ShouldSucceedForDisallowedDependency() + { + Type subject = typeof(Layer1AndLayer2); + + async Task Act() + => await That(subject) + .DoesNotComplyWith(it => it.DependsOnlyOn(Types.InNamespace(Layer1Namespace))); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenReferencingOwnSubNamespace_ShouldSucceedByDefault() + { + Type subject = typeof(ReferencesOwnSubNamespace); + + async Task Act() + => await That(subject).DependsOnlyOn(Types.InNamespace(Layer1Namespace)); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenExcludingOwnSubNamespaces_OwnSubNamespaceBecomesViolation() + { + Type subject = typeof(ReferencesOwnSubNamespace); + + async Task Act() + => await That(subject).DependsOnlyOn(Types.InNamespace(Layer1Namespace)) + .ExcludingOwnSubNamespaces(); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + depends only on types within namespace "{Layer1Namespace}" in all loaded assemblies, + but it also depended on ["OwnSubTarget"] + """); + } + + [Fact] + public async Task WhenExcludingOwnSubNamespaces_TargetCollectionCoveringTheSubNamespaceStillAllowsIt() + { + // The own sub-namespace is no longer implicitly allowed, but the target collection contains the + // referenced OwnSub type, which covers it explicitly. + Type subject = typeof(ReferencesOwnSubNamespace); + + async Task Act() + => await That(subject).DependsOnlyOn(Types.InNamespace(ConsumersNamespace)) + .ExcludingOwnSubNamespaces(); + + await That(Act).DoesNotThrow(); + } + } + public sealed class NegatedTests { [Fact] diff --git a/Tests/aweXpect.Reflection.Tests/ThatType.DoesNotDependOn.Tests.cs b/Tests/aweXpect.Reflection.Tests/ThatType.DoesNotDependOn.Tests.cs index ae358c7d..02505c1b 100644 --- a/Tests/aweXpect.Reflection.Tests/ThatType.DoesNotDependOn.Tests.cs +++ b/Tests/aweXpect.Reflection.Tests/ThatType.DoesNotDependOn.Tests.cs @@ -341,6 +341,64 @@ Expected that subject } } + public sealed class FilteredTypesTargetTests + { + [Fact] + public async Task WhenTypeDoesNotDependOnAnyTypeInTargetCollection_ShouldSucceed() + { + Type subject = typeof(OnlyLayer1); + + async Task Act() + => await That(subject).DoesNotDependOn(Types.InNamespace(Layer2Namespace)); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenTypeDependsOnTypeInTargetCollection_ShouldFail() + { + Type subject = typeof(ViaField); + + async Task Act() + => await That(subject).DoesNotDependOn(Types.InNamespace(Layer1Namespace)); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + does not depend on types within namespace "{Layer1Namespace}" in all loaded assemblies, + but it depended on [TargetA] + """); + } + + [Fact] + public async Task WhenTargetCollectionIsEmpty_ShouldSucceedTrivially() + { + Type subject = typeof(ViaField); + + async Task Act() + => await That(subject).DoesNotDependOn(Types.InNamespace("Non.Existent.Namespace")); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenWidenedWithOrOn_ShouldFailIfAnyMatches() + { + Type subject = typeof(Layer1AndLayer2); + + async Task Act() + => await That(subject).DoesNotDependOn(Types.InNamespace(Layer2Namespace)) + .OrOn(Types.InNamespace(Layer1Namespace)); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + does not depend on types within namespace "{Layer2Namespace}" in all loaded assemblies or types within namespace "{Layer1Namespace}" in all loaded assemblies, + but it depended on [TargetA, TargetB] + """); + } + } + public sealed class NegatedTests { [Fact] diff --git a/Tests/aweXpect.Reflection.Tests/ThatTypes.DependOn.Tests.cs b/Tests/aweXpect.Reflection.Tests/ThatTypes.DependOn.Tests.cs index 60316249..313b2cae 100644 --- a/Tests/aweXpect.Reflection.Tests/ThatTypes.DependOn.Tests.cs +++ b/Tests/aweXpect.Reflection.Tests/ThatTypes.DependOn.Tests.cs @@ -77,5 +77,62 @@ async Task Act() await That(Act).DoesNotThrow(); } } + + public sealed class FilteredTypesTargetTests + { + [Fact] + public async Task WhenAllTypesDependOnTargetCollection_ShouldSucceed() + { + IEnumerable subject = + [ + typeof(ViaField), + typeof(ViaProperty), + typeof(OnlyLayer1), + ]; + + async Task Act() + => await That(subject).DependOn(Types.InNamespace(Layer1Namespace)); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenSomeTypeDoesNotDependOnTargetCollection_ShouldFail() + { + IEnumerable subject = + [ + typeof(ViaField), + typeof(FrameworkConsumer), + ]; + + async Task Act() + => await That(subject).DependOn(Types.InNamespace(Layer1Namespace)); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + all depend on types within namespace "{Layer1Namespace}" in all loaded assemblies, + but it contained types without the dependency [ + FrameworkConsumer + ] + """); + } + + [Fact] + public async Task WhenWidenedWithOrOn_ShouldSucceed() + { + IEnumerable subject = + [ + typeof(OnlyLayer1), + typeof(Layer1AndLayer2), + ]; + + async Task Act() + => await That(subject).DependOn(Types.InNamespace("Non.Existent.Namespace")) + .OrOn(Types.InNamespace(Layer1Namespace)); + + await That(Act).DoesNotThrow(); + } + } } } diff --git a/Tests/aweXpect.Reflection.Tests/ThatTypes.DependOnlyOn.Tests.cs b/Tests/aweXpect.Reflection.Tests/ThatTypes.DependOnlyOn.Tests.cs index 94a3f4be..7c176340 100644 --- a/Tests/aweXpect.Reflection.Tests/ThatTypes.DependOnlyOn.Tests.cs +++ b/Tests/aweXpect.Reflection.Tests/ThatTypes.DependOnlyOn.Tests.cs @@ -88,5 +88,100 @@ async Task Act() await That(Act).DoesNotThrow(); } } + + public sealed class FilteredTypesTargetTests + { + [Fact] + public async Task WhenAllDependenciesAreInTargetCollection_ShouldSucceed() + { + IEnumerable subject = + [ + typeof(OnlyLayer1), + typeof(FrameworkConsumer), + typeof(ReferencesOwnNamespace), + ]; + + async Task Act() + => await That(subject).DependOnlyOn(Types.InNamespace(Layer1Namespace)); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenSomeTypeDependsOnTypeOutsideTargetCollections_ShouldFail() + { + IEnumerable subject = + [ + typeof(OnlyLayer1), + typeof(Layer1AndLayer2), + ]; + + async Task Act() + => await That(subject).DependOnlyOn(Types.InNamespace(Layer1Namespace)); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + all depend only on types within namespace "{Layer1Namespace}" in all loaded assemblies, + but it contained types with disallowed dependencies [ + Layer1AndLayer2 depends on ["TargetB"] + ] + """); + } + + [Fact] + public async Task WhenTargetCollectionIsEmpty_ShouldStillAllowOwnNamespaceAndFramework() + { + IEnumerable subject = + [ + typeof(FrameworkConsumer), + typeof(ReferencesOwnNamespace), + ]; + + async Task Act() + => await That(subject).DependOnlyOn(Types.InNamespace("Non.Existent.Namespace")); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenWidenedWithOrOn_ShouldSucceed() + { + IEnumerable subject = + [ + typeof(OnlyLayer1), + typeof(Layer1AndLayer2), + ]; + + async Task Act() + => await That(subject).DependOnlyOn(Types.InNamespace(Layer1Namespace)) + .OrOn(Types.InNamespace(Layer2Namespace)); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenExcludingOwnSubNamespaces_OwnSubNamespaceBecomesViolation() + { + IEnumerable subject = + [ + typeof(OnlyLayer1), + typeof(ReferencesOwnSubNamespace), + ]; + + async Task Act() + => await That(subject).DependOnlyOn(Types.InNamespace(Layer1Namespace)) + .ExcludingOwnSubNamespaces(); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + all depend only on types within namespace "{Layer1Namespace}" in all loaded assemblies, + but it contained types with disallowed dependencies [ + ReferencesOwnSubNamespace depends on ["OwnSubTarget"] + ] + """); + } + } } } diff --git a/Tests/aweXpect.Reflection.Tests/ThatTypes.DoNotDependOn.Tests.cs b/Tests/aweXpect.Reflection.Tests/ThatTypes.DoNotDependOn.Tests.cs index 06b06cdb..b0fb33f7 100644 --- a/Tests/aweXpect.Reflection.Tests/ThatTypes.DoNotDependOn.Tests.cs +++ b/Tests/aweXpect.Reflection.Tests/ThatTypes.DoNotDependOn.Tests.cs @@ -74,5 +74,76 @@ but it contained types with the dependency [ """); } } + + public sealed class FilteredTypesTargetTests + { + [Fact] + public async Task WhenNoTypeDependsOnTargetCollection_ShouldSucceed() + { + IEnumerable subject = + [ + typeof(ViaField), + typeof(OnlyLayer1), + ]; + + async Task Act() + => await That(subject).DoNotDependOn(Types.InNamespace(Layer2Namespace)); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenSomeTypeDependsOnTargetCollection_ShouldFail() + { + IEnumerable subject = + [ + typeof(OnlyLayer1), + typeof(Layer1AndLayer2), + ]; + + async Task Act() + => await That(subject).DoNotDependOn(Types.InNamespace(Layer2Namespace)); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + all do not depend on types within namespace "{Layer2Namespace}" in all loaded assemblies, + but it contained types with the dependency [ + Layer1AndLayer2 + ] + """); + } + + [Fact] + public async Task WhenTargetCollectionIsEmpty_ShouldSucceedTrivially() + { + IEnumerable subject = + [ + typeof(ViaField), + typeof(Layer1AndLayer2), + typeof(FrameworkConsumer), + ]; + + async Task Act() + => await That(subject).DoNotDependOn(Types.InNamespace("Non.Existent.Namespace")); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenWidenedWithOrOn_ShouldFailIfAnyMatches() + { + IEnumerable subject = + [ + typeof(OnlyLayer1), + ]; + + async Task Act() + => await That(subject).DoNotDependOn(Types.InNamespace(Layer2Namespace)) + .OrOn(Types.InNamespace(Layer1Namespace)); + + await That(Act).Throws(); + } + } } }