From e8f8233c105e3f6f44f6acf0576ad91b915fa909 Mon Sep 17 00:00:00 2001 From: Alexander Linne Date: Fri, 23 Oct 2026 18:23:52 +0200 Subject: [PATCH] feat: nest slices in packages via GenerationOptions, deprecate MatchingWithPackages MatchingWithPackages existed because a slice carried nothing but its name: the exporter worked out the nesting from that string, so it had to be fully qualified, while rules needed short names. The choice between the two had to be made when the slices were created, and the qualified names ended up in failure messages and frozen-rule stores as well. Both methods group types the same way: the namespace before the capture group is literal pattern text, so prefixing every name with it changes no grouping. Keep that diagram data on the slice instead of in its name. Each slice of a pattern carries, internally and outside its identity, the namespace the pattern matched before the capture group and the path it captured; SliceAssignment.Apply gives each slice that namespace if all of its types matched the same one. GenerationOptions.NestSlicesInPackages (default false) makes the exporter nest slices along that namespace and path, which it builds into the qualified names it lays the diagram out with. Slices without diagram data, or whose types matched different namespaces before the group, are drawn on their own under their names. Two slices that would be drawn under the same name are rejected with an ArgumentException instead of being merged into one node. Focus mode takes the options too. MatchingWithPackages is obsolete. It creates the same slices as Matching, so its rules report the same names and violations. It only still nests its slices in diagrams by default, so existing diagram code keeps drawing the same diagrams: every former MatchingWithPackages snapshot is unchanged with Matching and NestSlicesInPackages. The root package now comes from the match rather than from the pattern text. With a leading "..", MatchingWithPackages used the pattern text with one of its dots stripped, so "..DuplicatePrefix.Sub.(**)" was nested under a package ".DuplicatePrefix.Sub." instead of the namespace the types are in. The guide shows Matching with NestSlicesInPackages in its diagram examples and gains a migration section. Breaking changes: - MatchingWithPackages is obsolete, and creates Matching's slices, so its failure messages and frozen violations no longer use names qualified with the namespace before the capture group. - Slice.NameSpace, Slice.ContainsNamespace() and SliceIdentifier.NameSpace are removed, and SliceIdentifier.Of loses its nameSpace parameter. Signed-off-by: Alexander Linne --- .../PlantUml/Export/GenerationOptions.cs | 9 + .../PlantUml/Export/PlantUmlFileBuilder.cs | 195 +++++++++++++----- ArchUnitNET/Domain/Slice.cs | 24 ++- ArchUnitNET/Domain/SliceIdentifier.cs | 48 ++++- ...antUmlFluentComponentDiagramInitializer.cs | 16 +- ArchUnitNET/Fluent/Slices/SliceAssignment.cs | 46 ++++- .../Fluent/Slices/SliceRuleInitializer.cs | 63 +++--- .../PlantUml/PlantUmlSliceDiagramTests.cs | 158 +++++++++----- ...ildUmlBySlicesNested_C4Style.verified.txt} | 0 ...esNested_CaptureGroupAtStart.verified.txt} | 0 ...lBySlicesNested_DirectCircle.verified.txt} | 0 ...ySlicesNested_DoubleAsterisk.verified.txt} | 0 ...Nested_DuplicatePrefixSegment.verified.txt | 13 ++ ...icesNested_LimitDependencies.verified.txt} | 0 ...Nested_MultipleCaptureGroups.verified.txt} | 0 ...d_NonContiguousCaptureGroups.verified.txt} | 0 ...icesNested_SameNamedPackages.verified.txt} | 0 ...ed_SameNamedPackages_C4Style.verified.txt} | 0 ...ySlicesNested_SingleAsterisk.verified.txt} | 0 .../PlantUmlFluentComponentDiagramTests.cs | 11 +- .../Slices/MatchingWithPackagesTests.cs | 192 +++++++++++++++++ .../Fluent/Slices/SliceDomainTests.cs | 11 + ArchUnitNETTests/Fluent/Slices/SlicesTests.cs | 55 +---- ...ithPackages_ReportsViolations.verified.txt | 19 -- documentation/docs/guide.md | 34 ++- 25 files changed, 660 insertions(+), 234 deletions(-) rename ArchUnitNETTests/Domain/PlantUml/Snapshots/{PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_C4Style.verified.txt => PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_C4Style.verified.txt} (100%) rename ArchUnitNETTests/Domain/PlantUml/Snapshots/{PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_CaptureGroupAtStart.verified.txt => PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_CaptureGroupAtStart.verified.txt} (100%) rename ArchUnitNETTests/Domain/PlantUml/Snapshots/{PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_DirectCircle.verified.txt => PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_DirectCircle.verified.txt} (100%) rename ArchUnitNETTests/Domain/PlantUml/Snapshots/{PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_DoubleAsterisk.verified.txt => PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_DoubleAsterisk.verified.txt} (100%) create mode 100644 ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_DuplicatePrefixSegment.verified.txt rename ArchUnitNETTests/Domain/PlantUml/Snapshots/{PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_LimitDependencies.verified.txt => PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_LimitDependencies.verified.txt} (100%) rename ArchUnitNETTests/Domain/PlantUml/Snapshots/{PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_MultipleCaptureGroups.verified.txt => PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_MultipleCaptureGroups.verified.txt} (100%) rename ArchUnitNETTests/Domain/PlantUml/Snapshots/{PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_NonContiguousCaptureGroups.verified.txt => PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_NonContiguousCaptureGroups.verified.txt} (100%) rename ArchUnitNETTests/Domain/PlantUml/Snapshots/{PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_SameNamedPackages.verified.txt => PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_SameNamedPackages.verified.txt} (100%) rename ArchUnitNETTests/Domain/PlantUml/Snapshots/{PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_SameNamedPackages_C4Style.verified.txt => PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_SameNamedPackages_C4Style.verified.txt} (100%) rename ArchUnitNETTests/Domain/PlantUml/Snapshots/{PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_SingleAsterisk.verified.txt => PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_SingleAsterisk.verified.txt} (100%) create mode 100644 ArchUnitNETTests/Fluent/Slices/MatchingWithPackagesTests.cs delete mode 100644 ArchUnitNETTests/Fluent/Slices/Snapshots/SlicesTests.NotDependOnEachOther_MatchingWithPackages_ReportsViolations.verified.txt diff --git a/ArchUnitNET/Domain/PlantUml/Export/GenerationOptions.cs b/ArchUnitNET/Domain/PlantUml/Export/GenerationOptions.cs index 44348f502..1b995aa7b 100644 --- a/ArchUnitNET/Domain/PlantUml/Export/GenerationOptions.cs +++ b/ArchUnitNET/Domain/PlantUml/Export/GenerationOptions.cs @@ -10,5 +10,14 @@ public class GenerationOptions public bool IncludeNodesWithoutDependencies { get; set; } = true; public bool LimitDependencies { get; set; } = false; public bool C4Style { get; set; } = false; + + /// + /// Whether slice diagrams nest each slice in package blocks along the namespace its + /// types matched, instead of drawing every slice on its own under its name. A slice whose + /// types matched different namespaces before the first capture group, or that does not + /// come from a slice pattern, is drawn on its own either way. Slices of the obsolete + /// MatchingWithPackages are always nested. + /// + public bool NestSlicesInPackages { get; set; } = false; } } diff --git a/ArchUnitNET/Domain/PlantUml/Export/PlantUmlFileBuilder.cs b/ArchUnitNET/Domain/PlantUml/Export/PlantUmlFileBuilder.cs index 7432a2354..fc758aa6f 100644 --- a/ArchUnitNET/Domain/PlantUml/Export/PlantUmlFileBuilder.cs +++ b/ArchUnitNET/Domain/PlantUml/Export/PlantUmlFileBuilder.cs @@ -15,6 +15,12 @@ public class PlantUmlFileBuilder private List _sliceList = new List(); + /// + /// The name each slice is drawn under, and the root package it is nested in, if any. + /// + private Dictionary _diagramNames = + new Dictionary(); + public string AsString(RenderOptions renderOptions = null) { if (renderOptions == null) @@ -122,13 +128,21 @@ public PlantUmlFileBuilder WithDependenciesFrom( return this; } + /// + /// Draws the given slices as components, with an arrow from one slice to another wherever + /// one of its types depends on one of the other's. + /// + /// The slices to draw, e.g. from GivenSlices.GetObjects. + /// Filters and layout options; the defaults if null. + /// With , slices from a slice pattern + /// are nested in package blocks along the namespace they matched. + /// Two slices would be drawn under the same name. public PlantUmlFileBuilder WithDependenciesFrom( IEnumerable slices, GenerationOptions generationOptions = null ) { _sliceList = slices.Distinct().ToList(); - RemovePatternInappropriateSlices(); var nodes = new Dictionary(); if (generationOptions == null) @@ -136,20 +150,23 @@ public PlantUmlFileBuilder WithDependenciesFrom( generationOptions = new GenerationOptions(); } + AssignDiagramNames(generationOptions.NestSlicesInPackages); + RemovePatternInappropriateSlices(); + foreach (var slice in _sliceList) { var sliceIsPackage = IsPackage(slice); var dependencyTargets = SelectDependencies( slice, _sliceList, - target => target.Description, + NameOf, target => target.Types, generationOptions ); if (!sliceIsPackage) { - var slc = new PlantUmlSlice(slice.Description, slice.NameSpace); + var slc = new PlantUmlSlice(NameOf(slice), NameSpaceOf(slice)); if (generationOptions.C4Style) { slc.UseS4Style(); @@ -163,8 +180,8 @@ public PlantUmlFileBuilder WithDependenciesFrom( { _dependencies.AddRange( dependencyTargets.Select(target => new PlantUmlDependency( - slice.Description, - target.Description, + NameOf(slice), + NameOf(target), DependencyType.PackageToOne )) ); @@ -173,21 +190,21 @@ public PlantUmlFileBuilder WithDependenciesFrom( { _dependencies.AddRange( dependencyTargets.Select(target => new PlantUmlDependency( - slice.Description, - target.Description, + NameOf(slice), + NameOf(target), DependencyType.OneToOne )) ); } } - else if (slice.ContainsNamespace()) + else if (NameSpaceOf(slice) != null) { if (sliceIsPackage) { _dependencies.AddRange( dependencyTargets.Select(target => new PlantUmlDependency( - slice.Description, - target.Description, + NameOf(slice), + NameOf(target), DependencyType.PackageToPackageIfSameParentNamespace )) ); @@ -196,15 +213,15 @@ public PlantUmlFileBuilder WithDependenciesFrom( { var dt = dependencyTargets .Select(target => new PlantUmlDependency( - slice.Description, - target.Description, + NameOf(slice), + NameOf(target), DependencyType.OneToOneIfSameParentNamespace )) .Where(dependency => dependency.OriginCountOfDots() < dependency.TargetCountOfDots() || _sliceList.All(slc => - slc.Description == dependency.Target - || !slc.Description.Contains(dependency.Target) + NameOf(slc) == dependency.Target + || !NameOf(slc).Contains(dependency.Target) ) ); _dependencies.AddRange(dt); @@ -214,8 +231,8 @@ public PlantUmlFileBuilder WithDependenciesFrom( { _dependencies.AddRange( dependencyTargets.Select(target => new PlantUmlDependency( - slice.Description, - target.Description, + NameOf(slice), + NameOf(target), DependencyType.OneToOneCompact )) ); @@ -224,7 +241,7 @@ public PlantUmlFileBuilder WithDependenciesFrom( if (!generationOptions.IncludeNodesWithoutDependencies) { - RemoveNodesNotContainedInDependencies(nodes, slice => slice.Description); + RemoveNodesNotContainedInDependencies(nodes, NameOf); } if (!generationOptions.LimitDependencies) @@ -328,12 +345,25 @@ var parent in parentNamespaces.Where(parentName => return this; } + /// + /// Draws only the dependencies from and to the slices whose diagram name contains + /// , and colors those slices. + /// + /// The slices to draw, e.g. from GivenSlices.GetObjects. + /// The text to focus on. When slices are nested in packages, their + /// diagram names are fully qualified, so this has to be as well. + /// Only + /// is read; the defaults if null. + /// The package is empty, a single dot or not contained + /// in any slice's diagram name, or two slices would be drawn under the same name. public PlantUmlFileBuilder WithDependenciesFromFocusOn( IEnumerable slices, - string package + string package, + GenerationOptions generationOptions = null ) { _sliceList = slices.Distinct().ToList(); + AssignDiagramNames(generationOptions?.NestSlicesInPackages ?? false); switch (package) { @@ -351,7 +381,7 @@ string package RemovePatternInappropriateSlices(package); var existPackage = _sliceList.Any(slice => - (slice.NameSpace + slice.Description).Contains(package) + (NameSpaceOf(slice) + NameOf(slice)).Contains(package) ); if (!existPackage) { @@ -361,7 +391,7 @@ string package } var nodes = new Dictionary(); - var generationOptions = new GenerationOptions(); + var defaultOptions = new GenerationOptions(); foreach (var slice in _sliceList) { @@ -369,17 +399,17 @@ string package var dependencyTargets = SelectDependencies( slice, _sliceList, - target => target.Description, + NameOf, target => target.Types, - generationOptions + defaultOptions ); if (sliceIsPackage) { _dependencies.AddRange( dependencyTargets.Select(target => new PlantUmlDependency( - slice.Description, - target.Description, + NameOf(slice), + NameOf(target), DependencyType.PackageToOne )) ); @@ -388,8 +418,8 @@ string package { _dependencies.AddRange( dependencyTargets.Select(target => new PlantUmlDependency( - slice.Description, - target.Description, + NameOf(slice), + NameOf(target), DependencyType.OneToOne )) ); @@ -399,20 +429,20 @@ string package { nodes.Add( slice, - slice.Description.Contains(package) - ? new PlantUmlSlice(slice.Description, slice.NameSpace, "99ffd1") - : new PlantUmlSlice(slice.Description, slice.NameSpace) + NameOf(slice).Contains(package) + ? new PlantUmlSlice(NameOf(slice), NameSpaceOf(slice), "99ffd1") + : new PlantUmlSlice(NameOf(slice), NameSpaceOf(slice)) ); } - else if (!slice.Description.Contains(package)) + else if (!NameOf(slice).Contains(package)) { - nodes.Add(slice, new PlantUmlSlice(slice.Description + ".", slice.NameSpace)); + nodes.Add(slice, new PlantUmlSlice(NameOf(slice) + ".", NameSpaceOf(slice))); } } - if (!generationOptions.IncludeNodesWithoutDependencies) + if (!defaultOptions.IncludeNodesWithoutDependencies) { - RemoveNodesNotContainedInDependencies(nodes, slice => slice.Description); + RemoveNodesNotContainedInDependencies(nodes, NameOf); } RemoveDuplicateDependenciesWhenShowingPackages(); @@ -433,8 +463,8 @@ string package for (var i = nodes.Count - 1; i >= 0; i--) { var nodeExist = _dependencies.Any(dep => - nodes.ElementAt(i).Key.Description == dep.Origin - || nodes.ElementAt(i).Key.Description == dep.Target + NameOf(nodes.ElementAt(i).Key) == dep.Origin + || NameOf(nodes.ElementAt(i).Key) == dep.Target ); if (!nodeExist) { @@ -445,10 +475,10 @@ string package _diagram.AddElements(nodes.Values); var tempList = _sliceList - .Where(slice => IsPackage(slice) && slice.Description.Contains(package)) + .Where(slice => IsPackage(slice) && NameOf(slice).Contains(package)) .Select(slice => new PlantUmlSlice( - slice.Description + ".", - slice.NameSpace, + NameOf(slice) + ".", + NameSpaceOf(slice), "99ffd1" )) .ToList(); @@ -517,7 +547,7 @@ private void RemovePatternInappropriateSlices(string thatContainsThisString = nu if ( thatContainsThisString != null && !_sliceList.Any(slice => - (slice.NameSpace + slice.Description).Contains(thatContainsThisString) + (NameSpaceOf(slice) + NameOf(slice)).Contains(thatContainsThisString) ) ) { @@ -530,14 +560,15 @@ private void RemovePatternInappropriateSlices(string thatContainsThisString = nu if ( thatContainsThisString != null - && !(_sliceList[i].Description).Contains(thatContainsThisString) + && !NameOf(_sliceList[i]).Contains(thatContainsThisString) ) { continue; } + // The description is never qualified, so its dots are those below the namespace + // the pattern matched before its capture group. dots += _sliceList[i].Description.Count(c => c == '.'); - dots -= (_sliceList[i].NameSpace ?? string.Empty).Count(c => c == '.'); if (dots >= _sliceList[i].CountOfAsteriskInPattern) { @@ -565,7 +596,7 @@ private void RemoveDuplicatedArrowsIfExist() private void RemoveDuplicateDependenciesWhenShowingPackages() { - if (_sliceList.Any(slice => !slice.ContainsNamespace())) + if (_sliceList.Any(slice => NameSpaceOf(slice) == null)) { return; } @@ -574,8 +605,8 @@ private void RemoveDuplicateDependenciesWhenShowingPackages() { if ( _sliceList.Any(slice => - slice.Description.Contains(_dependencies[j].Target) - && slice.Description != _dependencies[j].Target + NameOf(slice).Contains(_dependencies[j].Target) + && NameOf(slice) != _dependencies[j].Target ) ) { @@ -686,9 +717,79 @@ private void ReplaceCirclesWithAppropriateDependencyType() } } + /// + /// Decides for every slice the name it is drawn under and the root package it is nested + /// in, if any. + /// + /// + /// + /// The slice exporter works out the layout from these names alone: which slice is the + /// package of another, which arrows to merge, how deep LimitDependencies goes. A + /// nested slice is therefore drawn under its qualified name, the namespace its types + /// matched before the first capture group followed by its + /// , e.g. "App.Orders.Http", with that namespace + /// as its root package. Every other slice is drawn under its description, without a + /// package. + /// + /// + /// Qualified names of one pattern are distinct, but a description can coincide with a + /// qualified name. Drawing two slices as one node would be wrong, so that is rejected. + /// + /// + /// + /// Whether the options ask for nesting; slices of MatchingWithPackages are nested + /// regardless. + /// + private void AssignDiagramNames(bool nestSlicesInPackages) + { + var nest = nestSlicesInPackages || _sliceList.Any(slice => slice.NestByDefault); + _diagramNames = new Dictionary(); + var slicesByName = new Dictionary(); + foreach (var slice in _sliceList) + { + var nameSpace = slice.DiagramNamespace; + var diagramPath = slice.Identifier.DiagramPath; + (string Name, string NameSpace) diagramName = + nest && nameSpace != null && diagramPath != null + ? (nameSpace + diagramPath, nameSpace == "" ? null : nameSpace) + : (slice.Description, null); + + if (slicesByName.TryGetValue(diagramName.Name, out var other)) + { + throw new ArgumentException( + "The slices \"" + + other.Identifier.Identifier + + "\" and \"" + + slice.Identifier.Identifier + + "\" would both be drawn as \"" + + diagramName.Name + + "\"." + ); + } + + slicesByName.Add(diagramName.Name, slice); + _diagramNames.Add(slice, diagramName); + } + } + + /// The name the diagram draws a slice under; see . + private string NameOf(Slice slice) + { + return _diagramNames[slice].Name; + } + + /// + /// The root package a slice is nested in, with a trailing ".", or null if it is + /// drawn on its own. + /// + private string NameSpaceOf(Slice slice) + { + return _diagramNames[slice].NameSpace; + } + private bool IsPackage(Slice slice) { - if (!slice.ContainsNamespace()) + if (NameSpaceOf(slice) == null) { return false; } @@ -696,8 +797,8 @@ private bool IsPackage(Slice slice) for (var i = _sliceList.Count - 1; i >= 0; i--) { if ( - _sliceList[i].Description != slice.Description - & _sliceList[i].Description.StartsWith(slice.Description) + NameOf(_sliceList[i]) != NameOf(slice) + & NameOf(_sliceList[i]).StartsWith(NameOf(slice)) ) { return true; diff --git a/ArchUnitNET/Domain/Slice.cs b/ArchUnitNET/Domain/Slice.cs index 658a06596..d2261b35f 100644 --- a/ArchUnitNET/Domain/Slice.cs +++ b/ArchUnitNET/Domain/Slice.cs @@ -27,10 +27,25 @@ public Slice(SliceIdentifier identifier, IEnumerable types) public string Description => Identifier.Description; - [CanBeNull] - public string NameSpace => Identifier.NameSpace; public int? CountOfAsteriskInPattern => Identifier.CountOfAsteriskInPattern; + /// + /// The namespace a diagram nests this slice under when it nests slices in packages: the + /// part of the namespace that the pattern matched before the first capture group, if + /// it is the same for every type in the slice, which may be empty. null if it + /// differs between the types, or if the slice does not come from a slice pattern; a + /// diagram then draws the slice on its own. It takes no part in the slice's identity. + /// + [CanBeNull] + internal string DiagramNamespace { get; set; } + + /// + /// Whether diagrams nest this slice in packages even if + /// is not set, which + /// is the case for slices of the obsolete MatchingWithPackages. + /// + internal bool NestByDefault { get; set; } + protected bool Equals(Slice other) { return Equals(Identifier, other.Identifier); @@ -65,10 +80,5 @@ public override string ToString() { return Description; } - - public bool ContainsNamespace() - { - return NameSpace != null; - } } } diff --git a/ArchUnitNET/Domain/SliceIdentifier.cs b/ArchUnitNET/Domain/SliceIdentifier.cs index e84a4a539..8dc528989 100644 --- a/ArchUnitNET/Domain/SliceIdentifier.cs +++ b/ArchUnitNET/Domain/SliceIdentifier.cs @@ -11,28 +11,62 @@ private SliceIdentifier( string identifier, bool ignored, int? countOfAsteriskInPattern = null, - string nameSpace = null + string matchedPrefix = null, + string diagramPath = null ) : base(identifier) { Ignored = ignored; CountOfAsteriskInPattern = countOfAsteriskInPattern; - NameSpace = nameSpace; + MatchedPrefix = matchedPrefix; + DiagramPath = diagramPath; } public string Description => Identifier; + /// + /// The part of the type's namespace the pattern matched before the first capture group, + /// e.g. "App." for "App.(*)" over "App.Orders", or an empty string if the pattern starts + /// with a group; null if the identifier does not come from a slice pattern. Only + /// used to nest the slice in diagrams; it takes no part in the slice's identity. + /// [CanBeNull] - public readonly string NameSpace; + internal readonly string MatchedPrefix; + + /// + /// The path a diagram nests the slice along below : the part of + /// the namespace the pattern captured, e.g. "Orders.Web" for "App.(**)" over + /// "App.Orders.Web". null if the identifier does not come from a slice pattern. It + /// takes no part in the slice's identity. + /// + [CanBeNull] + internal readonly string DiagramPath; + public readonly int? CountOfAsteriskInPattern; - public static SliceIdentifier Of( + public static SliceIdentifier Of(string identifier, int? countOfAsteriskInPattern = null) + { + return new SliceIdentifier(identifier, false, countOfAsteriskInPattern); + } + + /// + /// Creates the identifier a slice pattern assigns to a type, together with where the + /// pattern matched, which diagrams use to nest the slice. + /// + internal static SliceIdentifier OfMatch( string identifier, - int? countOfAsteriskInPattern = null, - string nameSpace = null + int? countOfAsteriskInPattern, + string matchedPrefix, + string diagramPath ) { - return new SliceIdentifier(identifier, false, countOfAsteriskInPattern, nameSpace); + return new SliceIdentifier( + identifier, + false, + countOfAsteriskInPattern, + matchedPrefix, + diagramPath + ); } public static SliceIdentifier Ignore() diff --git a/ArchUnitNET/Fluent/PlantUml/PlantUmlFluentComponentDiagramInitializer.cs b/ArchUnitNET/Fluent/PlantUml/PlantUmlFluentComponentDiagramInitializer.cs index 2bc3f92bc..93d65edc3 100644 --- a/ArchUnitNET/Fluent/PlantUml/PlantUmlFluentComponentDiagramInitializer.cs +++ b/ArchUnitNET/Fluent/PlantUml/PlantUmlFluentComponentDiagramInitializer.cs @@ -65,14 +65,26 @@ public GivenPlantUmlFluentComponentDiagram WithDependenciesFromSlices( return new GivenPlantUmlFluentComponentDiagram(_fluentComponentDiagramCreator); } + /// + /// Draws only the dependencies from and to the slices whose diagram name contains + /// ; see + /// . + /// + /// The slices to draw. + /// The text to focus on, fully qualified when slices are + /// nested in packages. + /// Only + /// is read. public GivenPlantUmlFluentComponentDiagram WithDependenciesFromSlices( IEnumerable slices, - string focusOnPackage + string focusOnPackage, + GenerationOptions generationOptions = null ) { _fluentComponentDiagramCreator.Builder.WithDependenciesFromFocusOn( slices, - focusOnPackage + focusOnPackage, + generationOptions ); _fluentComponentDiagramCreator.AddToDescription("with dependencies from slices"); return new GivenPlantUmlFluentComponentDiagram(_fluentComponentDiagramCreator); diff --git a/ArchUnitNET/Fluent/Slices/SliceAssignment.cs b/ArchUnitNET/Fluent/Slices/SliceAssignment.cs index db5bdf1c4..9fdfe8aa7 100644 --- a/ArchUnitNET/Fluent/Slices/SliceAssignment.cs +++ b/ArchUnitNET/Fluent/Slices/SliceAssignment.cs @@ -12,21 +12,57 @@ public class SliceAssignment : IHasDescription public SliceAssignment( Func assignIdentifierFunc, string description + ) + : this(assignIdentifierFunc, description, false) { } + + internal SliceAssignment( + Func assignIdentifierFunc, + string description, + bool nestByDefault ) { _assignIdentifierFunc = assignIdentifierFunc; Description = description; + _nestByDefault = nestByDefault; } + /// + /// Whether diagrams nest the slices in packages even if + /// is not set; + /// only for the obsolete MatchingWithPackages. + /// + private readonly bool _nestByDefault; + public string Description { get; } public IEnumerable Apply(IEnumerable types) { - return types.GroupBy( - _assignIdentifierFunc, - (identifier, enumerable) => new Slice(identifier, enumerable), - SliceIdentifier.Comparer - ); + return types + .Select(type => (Type: type, Identifier: _assignIdentifierFunc(type))) + .GroupBy( + assigned => assigned.Identifier, + (identifier, assigned) => CreateSlice(identifier, assigned.ToList()), + SliceIdentifier.Comparer + ); + } + + private Slice CreateSlice( + SliceIdentifier identifier, + List<(IType Type, SliceIdentifier Identifier)> assigned + ) + { + // The types of one slice can have matched different prefixes, e.g. with a leading + // ".."; such a slice has no single namespace to be nested under. + var prefixes = assigned + .Select(typeAndIdentifier => typeAndIdentifier.Identifier.MatchedPrefix) + .Distinct() + .ToList(); + var types = assigned.Select(typeAndIdentifier => typeAndIdentifier.Type).ToList(); + return new Slice(identifier, types) + { + DiagramNamespace = prefixes.Count == 1 ? prefixes[0] : null, + NestByDefault = _nestByDefault, + }; } } } diff --git a/ArchUnitNET/Fluent/Slices/SliceRuleInitializer.cs b/ArchUnitNET/Fluent/Slices/SliceRuleInitializer.cs index 228de6820..1482a6602 100644 --- a/ArchUnitNET/Fluent/Slices/SliceRuleInitializer.cs +++ b/ArchUnitNET/Fluent/Slices/SliceRuleInitializer.cs @@ -21,29 +21,36 @@ public SliceRuleInitializer(SliceRuleCreator ruleCreator) /// public GivenSlices Matching(string pattern) { - _ruleCreator.SetSliceAssignment( - new SliceAssignment( - t => - { - var parseResult = Parse(pattern); - return AssignFunc(t, parseResult.pattern, parseResult.asterisk); - }, - "matching \"" + pattern + "\"" - ) - ); - return new GivenSlices(_ruleCreator); + return Matching(pattern, false); } + /// + /// Creates the same slices as , which PlantUML diagrams nest in + /// package blocks even without + /// . + /// + /// The namespace pattern, as for . + [Obsolete( + "Use Matching(pattern) and draw the slices with " + + "new GenerationOptions { NestSlicesInPackages = true }. MatchingWithPackages " + + "creates the same slices as Matching and will be removed in a future release." + )] public GivenSlices MatchingWithPackages(string pattern) + { + return Matching(pattern, true); + } + + private GivenSlices Matching(string pattern, bool nestByDefault) { _ruleCreator.SetSliceAssignment( new SliceAssignment( t => { var parseResult = Parse(pattern); - return AssignFunc(t, parseResult.pattern, parseResult.asterisk, true); + return AssignFunc(t, parseResult.pattern, parseResult.asterisk); }, - "matching \"" + pattern + "\"" + "matching \"" + pattern + "\"", + nestByDefault ) ); return new GivenSlices(_ruleCreator); @@ -87,8 +94,7 @@ private static (string pattern, int? asterisk) Parse(string pattern) private static SliceIdentifier AssignFunc( IType type, string pattern, - int? countOfSingleAsterisk, - bool fullName = false + int? countOfSingleAsterisk ) { var indexOfAsteriskInPattern = pattern.IndexOf("(*", StringComparison.Ordinal); @@ -133,14 +139,14 @@ private static SliceIdentifier AssignFunc( } var sliceString = namespc; + var matchedPrefix = ""; if (slicePrefix != "") { - sliceString = namespc - .Substring( - namespc.IndexOf(slicePrefix, StringComparison.Ordinal) + slicePrefix.Length - ) - .TrimStart('.'); + var prefixEnd = + namespc.IndexOf(slicePrefix, StringComparison.Ordinal) + slicePrefix.Length; + matchedPrefix = namespc.Substring(0, prefixEnd); + sliceString = namespc.Substring(prefixEnd).TrimStart('.'); } if (!sliceString.Contains(slicePostfix)) @@ -161,15 +167,14 @@ private static SliceIdentifier AssignFunc( ); } - // An empty prefix means the pattern starts with its capture group; there is no - // namespace to nest the slice under, so report none rather than an empty one. - return fullName - ? SliceIdentifier.Of( - slicePrefix + sliceString, - countOfSingleAsterisk, - slicePrefix == "" ? null : slicePrefix - ) - : SliceIdentifier.Of(sliceString, countOfSingleAsterisk); + // What the pattern captured is both what identifies the slice and the path a + // diagram nests it along. + return SliceIdentifier.OfMatch( + sliceString, + countOfSingleAsterisk, + matchedPrefix, + sliceString + ); } } } diff --git a/ArchUnitNETTests/Domain/PlantUml/PlantUmlSliceDiagramTests.cs b/ArchUnitNETTests/Domain/PlantUml/PlantUmlSliceDiagramTests.cs index 824bece2b..d3f13c11b 100644 --- a/ArchUnitNETTests/Domain/PlantUml/PlantUmlSliceDiagramTests.cs +++ b/ArchUnitNETTests/Domain/PlantUml/PlantUmlSliceDiagramTests.cs @@ -22,12 +22,15 @@ public class PlantUmlSliceDiagramTests private const string Root = "SlicesTestAssembly.MultipleSubnamespaces."; + private static GenerationOptions Nested => + new GenerationOptions { NestSlicesInPackages = true }; + // --- The asterisk count decides which slices the exporter renders --------------- // // Every pattern below matches the same nine slices -- SlicesTests.MatchingTest and // Matching_SingleAsterisk_CapturesEveryDepth assert that. What differs is how many // of them survive PlantUmlFileBuilder.RemovePatternInappropriateSlices, which drops - // a slice once its depth (dots in Description, less dots in NameSpace) reaches + // a slice once its depth (dots in its Description) reaches // Slice.CountOfAsteriskInPattern: // // (*) count 1 -> 3 of 9 slices rendered @@ -54,7 +57,7 @@ public Task BuildUmlBySlices_DoubleAsterisk() // The matcher discards everything after the first "(*", so "(*).(*)" and "(*)..(*)" // are indistinguishable to it and the next two snapshots are byte-identical -- as are - // their MatchingWithPackages counterparts below. That identity is the point: it is + // their nested counterparts below. That identity is the point: it is // what a reimplementation giving the second capture group any meaning would have to // change. SlicesTests.DotDot_* cover the shapes that would tell the two patterns // apart. @@ -76,64 +79,60 @@ public Task BuildUmlBySlices_NonContiguousCaptureGroups() } [Fact] - public Task BuildUmlBySlicesMatchingWithPackages_SingleAsterisk() + public Task BuildUmlBySlicesNested_SingleAsterisk() { - return VerifySlices(SliceRuleDefinition.Slices().MatchingWithPackages(Root + "(*)")); + return VerifySlices(SliceRuleDefinition.Slices().Matching(Root + "(*)"), Nested); } [Fact] - public Task BuildUmlBySlicesMatchingWithPackages_DoubleAsterisk() + public Task BuildUmlBySlicesNested_DoubleAsterisk() { - return VerifySlices(SliceRuleDefinition.Slices().MatchingWithPackages(Root + "(**)")); + return VerifySlices(SliceRuleDefinition.Slices().Matching(Root + "(**)"), Nested); } [Fact] - public Task BuildUmlBySlicesMatchingWithPackages_MultipleCaptureGroups() + public Task BuildUmlBySlicesNested_MultipleCaptureGroups() { - return VerifySlices( - SliceRuleDefinition.Slices().MatchingWithPackages(Root + "(*).(*)") - ); + return VerifySlices(SliceRuleDefinition.Slices().Matching(Root + "(*).(*)"), Nested); } [Fact] - public Task BuildUmlBySlicesMatchingWithPackages_NonContiguousCaptureGroups() + public Task BuildUmlBySlicesNested_NonContiguousCaptureGroups() { - return VerifySlices( - SliceRuleDefinition.Slices().MatchingWithPackages(Root + "(*)..(*)") - ); + return VerifySlices(SliceRuleDefinition.Slices().Matching(Root + "(*)..(*)"), Nested); } [Fact] - public Task BuildUmlBySlicesMatchingWithPackages_CaptureGroupAtStart() + public Task BuildUmlBySlicesNested_CaptureGroupAtStart() { // Nothing precedes the capture group, so there is no namespace to nest the slices // under and they are drawn flat, like those of Matching. - return VerifySlices(SliceRuleDefinition.Slices().MatchingWithPackages("(**).Service")); + return VerifySlices(SliceRuleDefinition.Slices().Matching("(**).Service"), Nested); } [Fact] - public Task BuildUmlBySlicesMatchingWithPackages_LimitDependencies() + public Task BuildUmlBySlicesNested_LimitDependencies() { return VerifySlices( - SliceRuleDefinition.Slices().MatchingWithPackages(Root + "(**)"), - new GenerationOptions { LimitDependencies = true } + SliceRuleDefinition.Slices().Matching(Root + "(**)"), + new GenerationOptions { NestSlicesInPackages = true, LimitDependencies = true } ); } [Fact] - public Task BuildUmlBySlicesMatchingWithPackages_C4Style() + public Task BuildUmlBySlicesNested_C4Style() { return VerifySlices( - SliceRuleDefinition.Slices().MatchingWithPackages(Root + "(**)"), - new GenerationOptions { C4Style = true } + SliceRuleDefinition.Slices().Matching(Root + "(**)"), + new GenerationOptions { NestSlicesInPackages = true, C4Style = true } ); } [Fact] public Task BuildUmlBySlices_C4Style() { - // Slices produced by Matching (as opposed to MatchingWithPackages) have no namespace - // prefix, so this exercises PlantUmlSlice.BuildStringC4Style's no-namespace branch. + // Slices that are not nested have no namespace prefix, so this exercises + // PlantUmlSlice.BuildStringC4Style's no-namespace branch. return VerifySlices( SliceRuleDefinition.Slices().Matching(Root + "(**)"), new GenerationOptions { C4Style = true } @@ -143,11 +142,9 @@ public Task BuildUmlBySlices_C4Style() [Fact] public Task BuildUmlBySlicesFocusOn() { - var slices = SortedSlices( - SliceRuleDefinition.Slices().MatchingWithPackages(Root + "(**)") - ); + var slices = SortedSlices(SliceRuleDefinition.Slices().Matching(Root + "(**)")); var uml = new PlantUmlFileBuilder() - .WithDependenciesFromFocusOn(slices, Root + "Slice1") + .WithDependenciesFromFocusOn(slices, Root + "Slice1", Nested) .AsString(); return VerifyUml(uml); } @@ -155,10 +152,11 @@ public Task BuildUmlBySlicesFocusOn() [Fact] public Task BuildUmlBySlicesFocusOn_Matching_ColorsFocusedSlices() { - // Slices produced by Matching have no namespace, so the focused ones take + // Slices that are not nested have no namespace, so the focused ones take // PlantUmlSlice.BuildString's namespace-less branch with a colour set -- the only // path on which the exporter renders "[Slice1] #99ffd1" rather than a package. - // Their Description carries no Root prefix either, hence the bare focus string. + // They are drawn under their description, which carries no Root prefix, hence the + // bare focus string. var slices = SortedSlices(SliceRuleDefinition.Slices().Matching(Root + "(**)")); var uml = new PlantUmlFileBuilder() .WithDependenciesFromFocusOn(slices, "Slice1") @@ -179,14 +177,12 @@ public Task BuildUmlBySlices_DirectCircle_RendersCircleDependency() } [Fact] - public Task BuildUmlBySlicesMatchingWithPackages_DirectCircle() + public Task BuildUmlBySlicesNested_DirectCircle() { var slices = SortedSlices( - SliceRuleDefinition - .Slices() - .MatchingWithPackages("SlicesTestAssembly.DirectCircle.(*)") + SliceRuleDefinition.Slices().Matching("SlicesTestAssembly.DirectCircle.(*)") ); - var uml = new PlantUmlFileBuilder().WithDependenciesFrom(slices).AsString(); + var uml = new PlantUmlFileBuilder().WithDependenciesFrom(slices, Nested).AsString(); return VerifyUml(uml); } @@ -199,19 +195,20 @@ public Task BuildUmlBySlicesMatchingWithPackages_DirectCircle() private const string SameNamedRoot = "SlicesTestAssembly.SameNamedSubnamespaces."; [Fact] - public Task BuildUmlBySlicesMatchingWithPackages_SameNamedPackages() + public Task BuildUmlBySlicesNested_SameNamedPackages() { return VerifySlices( - SliceRuleDefinition.Slices().MatchingWithPackages(SameNamedRoot + "(**)") + SliceRuleDefinition.Slices().Matching(SameNamedRoot + "(**)"), + Nested ); } [Fact] - public Task BuildUmlBySlicesMatchingWithPackages_SameNamedPackages_C4Style() + public Task BuildUmlBySlicesNested_SameNamedPackages_C4Style() { return VerifySlices( - SliceRuleDefinition.Slices().MatchingWithPackages(SameNamedRoot + "(**)"), - new GenerationOptions { C4Style = true } + SliceRuleDefinition.Slices().Matching(SameNamedRoot + "(**)"), + new GenerationOptions { NestSlicesInPackages = true, C4Style = true } ); } @@ -272,10 +269,10 @@ public void BuildUmlBySlicesFocusOn_EmptyPackage_Throws() { var slices = SliceRuleDefinition .Slices() - .MatchingWithPackages(Root + "(**)") + .Matching(Root + "(**)") .GetObjects(Architecture); var ex = Assert.Throws(() => - new PlantUmlFileBuilder().WithDependenciesFromFocusOn(slices, "") + new PlantUmlFileBuilder().WithDependenciesFromFocusOn(slices, "", Nested) ); Assert.Equal("Package can't be empty", ex.Message); } @@ -285,10 +282,10 @@ public void BuildUmlBySlicesFocusOn_DotOnlyPackage_Throws() { var slices = SliceRuleDefinition .Slices() - .MatchingWithPackages(Root + "(**)") + .Matching(Root + "(**)") .GetObjects(Architecture); var ex = Assert.Throws(() => - new PlantUmlFileBuilder().WithDependenciesFromFocusOn(slices, ".") + new PlantUmlFileBuilder().WithDependenciesFromFocusOn(slices, ".", Nested) ); Assert.Equal("Package can't contain a single dot only", ex.Message); } @@ -301,10 +298,10 @@ public void BuildUmlBySlicesFocusOn_UnknownPackage_Throws() // focus string that no slice contains, on the way to this exception. var slices = SliceRuleDefinition .Slices() - .MatchingWithPackages(Root + "(*)") + .Matching(Root + "(*)") .GetObjects(Architecture); var ex = Assert.Throws(() => - new PlantUmlFileBuilder().WithDependenciesFromFocusOn(slices, "NotAPackage") + new PlantUmlFileBuilder().WithDependenciesFromFocusOn(slices, "NotAPackage", Nested) ); Assert.Contains("is not contained in this slice", ex.Message); } @@ -314,18 +311,18 @@ public void BuildUmlBySlicesFocusOn_TrailingDotPackage_IsTrimmed() { var slicesWithTrailingDot = SliceRuleDefinition .Slices() - .MatchingWithPackages(Root + "(**)") + .Matching(Root + "(**)") .GetObjects(Architecture); var umlWithTrailingDot = new PlantUmlFileBuilder() - .WithDependenciesFromFocusOn(slicesWithTrailingDot, Root + "Slice1.") + .WithDependenciesFromFocusOn(slicesWithTrailingDot, Root + "Slice1.", Nested) .AsString(); var slicesWithoutTrailingDot = SliceRuleDefinition .Slices() - .MatchingWithPackages(Root + "(**)") + .Matching(Root + "(**)") .GetObjects(Architecture); var umlWithoutTrailingDot = new PlantUmlFileBuilder() - .WithDependenciesFromFocusOn(slicesWithoutTrailingDot, Root + "Slice1") + .WithDependenciesFromFocusOn(slicesWithoutTrailingDot, Root + "Slice1", Nested) .AsString(); Assert.Equal(umlWithoutTrailingDot, umlWithTrailingDot); @@ -340,15 +337,68 @@ public Task BuildUmlBySlicesFocusOn_SingleAsteriskPattern_SkipsSlicesWithoutFocu // doesn't contain it (e.g. "Slice1") skip the pattern-appropriateness check via // RemovePatternInappropriateSlices' per-slice "continue", while "Slice3" itself is // shallow enough to survive that check and keep the package reachable. - var slices = SortedSlices( - SliceRuleDefinition.Slices().MatchingWithPackages(Root + "(*)") - ); + var slices = SortedSlices(SliceRuleDefinition.Slices().Matching(Root + "(*)")); var uml = new PlantUmlFileBuilder() - .WithDependenciesFromFocusOn(slices, Root + "Slice3") + .WithDependenciesFromFocusOn(slices, Root + "Slice3", Nested) .AsString(); return VerifyUml(uml); } + // --- Where nested slices are rooted ---------------------------------------------- + // + // A nested slice's root package is the namespace its types matched before the first + // capture group, so it has to come from the match, not from the pattern text. + + [Fact] + public Task BuildUmlBySlicesNested_DuplicatePrefixSegment() + { + // The leading ".." matches "SlicesTestAssembly.DuplicatePrefix.Sub.", so that whole + // namespace is the root package, although "DuplicatePrefix.Sub" occurs in it twice. + return VerifySlices( + SliceRuleDefinition.Slices().Matching("..DuplicatePrefix.Sub.(**)"), + Nested + ); + } + + // --- One set of slices, drawn flat and nested ------------------------------------ + + [Fact] + public void BuildUmlBySlices_SameSlicesDrawnFlatAndNested() + { + var slices = SortedSlices(SliceRuleDefinition.Slices().Matching(Root + "(**)")); + + var flat = new PlantUmlFileBuilder().WithDependenciesFrom(slices).AsString(); + var nested = new PlantUmlFileBuilder().WithDependenciesFrom(slices, Nested).AsString(); + + Assert.DoesNotContain("package", flat); + Assert.Contains("[Slice1.Service]", flat); + Assert.Contains("package \"SlicesTestAssembly.MultipleSubnamespaces\"", nested); + Assert.DoesNotContain("[Slice1.Service]", nested); + } + + // --- Two slices may not share a node ---------------------------------------------- + + [Fact] + public void BuildUmlBySlices_SameDiagramName_Throws() + { + // A slice that is not nested is drawn under its description, which here is the + // qualified name a nested slice is drawn under. + var slices = SortedSlices(SliceRuleDefinition.Slices().Matching(Root + "(**)")) + .Concat( + new[] { new Slice(SliceIdentifier.Of(Root + "Slice1"), Array.Empty()) } + ) + .ToList(); + + var ex = Assert.Throws(() => + new PlantUmlFileBuilder().WithDependenciesFrom(slices, Nested) + ); + Assert.Equal( + "The slices \"Slice1\" and \"SlicesTestAssembly.MultipleSubnamespaces.Slice1\" " + + "would both be drawn as \"SlicesTestAssembly.MultipleSubnamespaces.Slice1\".", + ex.Message + ); + } + /// /// GetObjects returns slices in Mono.Cecil's type-table order, which is deterministic /// per build but not contractually stable across Cecil or runtime upgrades. Sorting by diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_C4Style.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_C4Style.verified.txt similarity index 100% rename from ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_C4Style.verified.txt rename to ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_C4Style.verified.txt diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_CaptureGroupAtStart.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_CaptureGroupAtStart.verified.txt similarity index 100% rename from ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_CaptureGroupAtStart.verified.txt rename to ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_CaptureGroupAtStart.verified.txt diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_DirectCircle.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_DirectCircle.verified.txt similarity index 100% rename from ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_DirectCircle.verified.txt rename to ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_DirectCircle.verified.txt diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_DoubleAsterisk.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_DoubleAsterisk.verified.txt similarity index 100% rename from ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_DoubleAsterisk.verified.txt rename to ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_DoubleAsterisk.verified.txt diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_DuplicatePrefixSegment.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_DuplicatePrefixSegment.verified.txt new file mode 100644 index 000000000..3e8672334 --- /dev/null +++ b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_DuplicatePrefixSegment.verified.txt @@ -0,0 +1,13 @@ +@startuml + +!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml + +HIDE_STEREOTYPE() + +package "SlicesTestAssembly.DuplicatePrefix.Sub" as SlicesTestAssembly.DuplicatePrefix.Sub { + package "DuplicatePrefix" as SlicesTestAssembly.DuplicatePrefix.Sub.DuplicatePrefix { + [Sub] as SlicesTestAssembly.DuplicatePrefix.Sub.DuplicatePrefix.Sub + } +} + +@enduml diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_LimitDependencies.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_LimitDependencies.verified.txt similarity index 100% rename from ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_LimitDependencies.verified.txt rename to ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_LimitDependencies.verified.txt diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_MultipleCaptureGroups.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_MultipleCaptureGroups.verified.txt similarity index 100% rename from ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_MultipleCaptureGroups.verified.txt rename to ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_MultipleCaptureGroups.verified.txt diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_NonContiguousCaptureGroups.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_NonContiguousCaptureGroups.verified.txt similarity index 100% rename from ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_NonContiguousCaptureGroups.verified.txt rename to ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_NonContiguousCaptureGroups.verified.txt diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_SameNamedPackages.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_SameNamedPackages.verified.txt similarity index 100% rename from ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_SameNamedPackages.verified.txt rename to ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_SameNamedPackages.verified.txt diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_SameNamedPackages_C4Style.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_SameNamedPackages_C4Style.verified.txt similarity index 100% rename from ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_SameNamedPackages_C4Style.verified.txt rename to ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_SameNamedPackages_C4Style.verified.txt diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_SingleAsterisk.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_SingleAsterisk.verified.txt similarity index 100% rename from ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_SingleAsterisk.verified.txt rename to ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_SingleAsterisk.verified.txt diff --git a/ArchUnitNETTests/Fluent/PlantUml/PlantUmlFluentComponentDiagramTests.cs b/ArchUnitNETTests/Fluent/PlantUml/PlantUmlFluentComponentDiagramTests.cs index 033fd5683..a18ebfeb7 100644 --- a/ArchUnitNETTests/Fluent/PlantUml/PlantUmlFluentComponentDiagramTests.cs +++ b/ArchUnitNETTests/Fluent/PlantUml/PlantUmlFluentComponentDiagramTests.cs @@ -45,11 +45,14 @@ public void ComponentDiagramFromSlicesTest() Assert.NotEmpty(uml2); var arch1 = new ArchLoader().LoadAssembly(typeof(Architecture).Assembly).Build(); - var sliceRule1 = SliceRuleDefinition - .Slices() - .MatchingWithPackages("ArchUnitNET.(*).(*).(*)"); + var sliceRule1 = SliceRuleDefinition.Slices().Matching("ArchUnitNET.(*).(*).(*)"); const string path = "../../../Fluent/PlantUml/Test.puml"; - var g = new GenerationOptions() { C4Style = true, LimitDependencies = true }; + var g = new GenerationOptions() + { + C4Style = true, + LimitDependencies = true, + NestSlicesInPackages = true, + }; ComponentDiagram() .WithDependenciesFromSlices(sliceRule1.GetObjects(arch1), g) .WriteToFile(path); diff --git a/ArchUnitNETTests/Fluent/Slices/MatchingWithPackagesTests.cs b/ArchUnitNETTests/Fluent/Slices/MatchingWithPackagesTests.cs new file mode 100644 index 000000000..2b38579ed --- /dev/null +++ b/ArchUnitNETTests/Fluent/Slices/MatchingWithPackagesTests.cs @@ -0,0 +1,192 @@ +using System; +using System.Linq; +using ArchUnitNET.Domain; +using ArchUnitNET.Domain.PlantUml.Export; +using ArchUnitNET.Fluent; +using ArchUnitNET.Fluent.Slices; +using Xunit; + +// These tests cover the obsolete method itself. +#pragma warning disable CS0618 + +namespace ArchUnitNETTests.Fluent.Slices +{ + /// + /// is obsolete. It creates exactly the + /// slices creates, so its rules behave the same, + /// and the only thing it still does differently is nest its slices in diagrams by default. + /// + public class MatchingWithPackagesTests + { + private static Architecture Architecture => StaticTestArchitectures.SlicesTestArchitecture; + + private const string Root = "SlicesTestAssembly.MultipleSubnamespaces."; + + public static TheoryData Patterns => + new TheoryData + { + Root + "(*)", + Root + "(**)", + Root + "(**)..", + Root + "(*).(*)", + Root + "(*)..(*)", + "..DuplicatePrefix.Sub.(**)", + "(**).Service", + "SlicesTestAssembly.SubnamespaceCircle.(**)", + }; + + [Theory] + [MemberData(nameof(Patterns))] + public void Slices_AreThoseOfMatching(string pattern) + { + var withPackages = SliceRuleDefinition + .Slices() + .MatchingWithPackages(pattern) + .GetObjects(Architecture) + .ToList(); + var matching = SliceRuleDefinition.Slices().Matching(pattern).GetObjects(Architecture); + + Assert.Equal(matching.Select(Describe), withPackages.Select(Describe)); + } + + [Theory] + [MemberData(nameof(Patterns))] + public void Rules_EvaluateLikeThoseOfMatching(string pattern) + { + AssertSameResults( + SliceRuleDefinition.Slices().Matching(pattern).Should().BeFreeOfCycles(), + SliceRuleDefinition.Slices().MatchingWithPackages(pattern).Should().BeFreeOfCycles() + ); + AssertSameResults( + SliceRuleDefinition.Slices().Matching(pattern).Should().NotDependOnEachOther(), + SliceRuleDefinition + .Slices() + .MatchingWithPackages(pattern) + .Should() + .NotDependOnEachOther() + ); + } + + [Fact] + public void BeFreeOfCycles_DetectsCycle() + { + Assert.False( + SliceRuleDefinition + .Slices() + .MatchingWithPackages(Root + "(**)") + .Should() + .BeFreeOfCycles() + .HasNoViolations(Architecture) + ); + } + + // SubnamespaceCircle has real edges (Slice1 -> Slice2, Slice2.Inner -> Slice1) but no + // cycle, so an empty graph is not what makes this pass. + [Fact] + public void BeFreeOfCycles_WhenAcyclic_Passes() + { + Assert.True( + SliceRuleDefinition + .Slices() + .MatchingWithPackages("SlicesTestAssembly.SubnamespaceCircle.(**)") + .Should() + .BeFreeOfCycles() + .HasNoViolations(Architecture) + ); + } + + [Theory] + [MemberData(nameof(Patterns))] + public void Diagram_IsThatOfMatchingNestedInPackages(string pattern) + { + foreach ( + var options in new[] + { + new GenerationOptions(), + new GenerationOptions { C4Style = true }, + new GenerationOptions { LimitDependencies = true }, + } + ) + { + var expected = new PlantUmlFileBuilder() + .WithDependenciesFrom( + SortedSlices(SliceRuleDefinition.Slices().Matching(pattern)), + new GenerationOptions + { + NestSlicesInPackages = true, + C4Style = options.C4Style, + LimitDependencies = options.LimitDependencies, + } + ) + .AsString(); + var actual = new PlantUmlFileBuilder() + .WithDependenciesFrom( + SortedSlices(SliceRuleDefinition.Slices().MatchingWithPackages(pattern)), + options + ) + .AsString(); + + Assert.Equal(expected, actual); + } + } + + [Fact] + public void Diagram_FocusOnQualifiedPackage_IsThatOfMatchingNestedInPackages() + { + var expected = new PlantUmlFileBuilder() + .WithDependenciesFromFocusOn( + SortedSlices(SliceRuleDefinition.Slices().Matching(Root + "(**)")), + Root + "Slice1", + new GenerationOptions { NestSlicesInPackages = true } + ) + .AsString(); + var actual = new PlantUmlFileBuilder() + .WithDependenciesFromFocusOn( + SortedSlices(SliceRuleDefinition.Slices().MatchingWithPackages(Root + "(**)")), + Root + "Slice1" + ) + .AsString(); + + Assert.Equal(expected, actual); + Assert.Contains("package \"SlicesTestAssembly.MultipleSubnamespaces\"", actual); + } + + private static string Describe(Slice slice) + { + return slice.Description + + ": " + + string.Join( + ", ", + slice + .Types.Select(type => type.FullName) + .OrderBy(n => n, StringComparer.Ordinal) + ); + } + + private static void AssertSameResults(IArchRule matching, IArchRule withPackages) + { + Assert.Equal(matching.Description, withPackages.Description); + Assert.Equal( + matching.Evaluate(Architecture).Select(Summarize), + withPackages.Evaluate(Architecture).Select(Summarize) + ); + } + + private static string Summarize(EvaluationResult result) + { + return result.Passed + + " " + + result.EvaluatedObjectIdentifier.Identifier + + " " + + result.Description; + } + + private static Slice[] SortedSlices(GivenSlices slices) + { + return slices + .GetObjects(Architecture) + .OrderBy(slice => slice.Description, StringComparer.Ordinal) + .ToArray(); + } + } +} diff --git a/ArchUnitNETTests/Fluent/Slices/SliceDomainTests.cs b/ArchUnitNETTests/Fluent/Slices/SliceDomainTests.cs index bf2866ab8..0c2b92e02 100644 --- a/ArchUnitNETTests/Fluent/Slices/SliceDomainTests.cs +++ b/ArchUnitNETTests/Fluent/Slices/SliceDomainTests.cs @@ -116,6 +116,17 @@ public void SliceIdentifier_Equals_Branches() Assert.False(identifier.Equals(SliceIdentifier.Of("B"))); } + [Fact] + public void SliceIdentifier_Equals_IgnoresDiagramData() + { + // Where a diagram draws the slice is no part of its identity. + var nested = SliceIdentifier.OfMatch("A.B", null, "X.", "A.B"); + + Assert.Equal(SliceIdentifier.Of("A.B"), nested); + Assert.Equal(SliceIdentifier.Of("A.B").GetHashCode(), nested.GetHashCode()); + Assert.Equal("A.B", nested.Description); + } + [Fact] public void SliceIdentifierComparer_Equals_NullFirstArgument_ReturnsFalse() { diff --git a/ArchUnitNETTests/Fluent/Slices/SlicesTests.cs b/ArchUnitNETTests/Fluent/Slices/SlicesTests.cs index 143d03af5..232cb4a2b 100644 --- a/ArchUnitNETTests/Fluent/Slices/SlicesTests.cs +++ b/ArchUnitNETTests/Fluent/Slices/SlicesTests.cs @@ -27,48 +27,6 @@ public void CycleDetectionTest() .AssertNoViolations(helper); } - // --- Rule evaluation driven by MatchingWithPackages (not just Matching) ------- - - [Fact] - public void BeFreeOfCycles_MatchingWithPackages_DetectsCycle() - { - var helper = new SlicesAssemblyTestHelper().WithoutSnapshot(); - SliceRuleDefinition - .Slices() - .MatchingWithPackages("SlicesTestAssembly.MultipleSubnamespaces.(**)") - .Should() - .BeFreeOfCycles() - .AssertOnlyViolations(helper); - } - - // SubnamespaceCircle has real edges (Slice1 -> Slice2, Slice2.Inner -> Slice1) but no - // cycle, unlike DotDotSemantics whose fixtures have no members and thus no edges at all -- - // an empty graph would pass here even if BeFreeOfCycles always returned "no cycles". - [Fact] - public void BeFreeOfCycles_MatchingWithPackages_WhenAcyclic_Passes() - { - var helper = new SlicesAssemblyTestHelper().WithoutSnapshot(); - SliceRuleDefinition - .Slices() - .MatchingWithPackages("SlicesTestAssembly.SubnamespaceCircle.(**)") - .Should() - .BeFreeOfCycles() - .AssertNoViolations(helper); - } - - [Fact] - public Task NotDependOnEachOther_MatchingWithPackages_ReportsViolations() - { - var helper = new SlicesAssemblyTestHelper(); - var rule = SliceRuleDefinition - .Slices() - .MatchingWithPackages("SlicesTestAssembly.MultipleSubnamespaces.(**)..") - .Should() - .NotDependOnEachOther(); - rule.AssertAnyViolations(helper); - return helper.AssertSnapshotMatches(); - } - [Fact] public void MatchingTest() { @@ -305,21 +263,20 @@ public void Matching_LeadingDotDot_CapturesEveryDepth() // (SlicesTestAssembly.DuplicatePrefix.Sub.DuplicatePrefix.Sub), so a leading ".." pattern // for that prefix has two candidate starting positions to strip from. This pins that // AssignFunc's IndexOf (leftmost match) picks the first occurrence, leaving the second - // "DuplicatePrefix.Sub" -- and the one leading dot "..DuplicatePrefix.Sub." keeps after - // stripping only one of its two literal dots -- in the description. With an absolute - // (non-"..") prefix, the prefix can only ever be found at index 0, so that variant of this - // test would pass even if IndexOf were replaced by a hardcoded 0. + // "DuplicatePrefix.Sub" in the description. With an absolute (non-"..") prefix, the + // prefix can only ever be found at index 0, so that variant of this test would pass even + // if IndexOf were replaced by a hardcoded 0. [Fact] - public void MatchingWithPackages_DuplicatePrefixSegment_UsesFirstOccurrenceAsPrefix() + public void DuplicatePrefixSegment_CapturesAfterTheFirstOccurrenceOfThePrefix() { var slices = SliceRuleDefinition .Slices() - .MatchingWithPackages("..DuplicatePrefix.Sub.(*)") + .Matching("..DuplicatePrefix.Sub.(**)") .GetObjects(StaticTestArchitectures.SlicesTestArchitecture) .ToList(); Assert.Single(slices); - Assert.Equal(".DuplicatePrefix.Sub.DuplicatePrefix.Sub", slices[0].Description); + Assert.Equal("DuplicatePrefix.Sub", slices[0].Description); } // --- ".." semantics ------------------------------------------------------------- diff --git a/ArchUnitNETTests/Fluent/Slices/Snapshots/SlicesTests.NotDependOnEachOther_MatchingWithPackages_ReportsViolations.verified.txt b/ArchUnitNETTests/Fluent/Slices/Snapshots/SlicesTests.NotDependOnEachOther_MatchingWithPackages_ReportsViolations.verified.txt deleted file mode 100644 index ae856ef18..000000000 --- a/ArchUnitNETTests/Fluent/Slices/Snapshots/SlicesTests.NotDependOnEachOther_MatchingWithPackages_ReportsViolations.verified.txt +++ /dev/null @@ -1,19 +0,0 @@ -Query: Slices matching "SlicesTestAssembly.MultipleSubnamespaces.(**).." should not depend on each other -Result: False -Description: SlicesTestAssembly.MultipleSubnamespaces.Slice1 does depend on other slices: -SlicesTestAssembly.MultipleSubnamespaces.Slice1 -> SlicesTestAssembly.MultipleSubnamespaces.Slice2 - SlicesTestAssembly.MultipleSubnamespaces.Slice1.Service.Service1Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service.Service2Class - -Result: True -Description: SlicesTestAssembly.MultipleSubnamespaces.Slice2 does not depend on another slice. -Result: True -Description: SlicesTestAssembly.MultipleSubnamespaces.Slice3 does not depend on another slice. -Message: -"Slices matching "SlicesTestAssembly.MultipleSubnamespaces.(**).." should not depend on each other" failed: - SlicesTestAssembly.MultipleSubnamespaces.Slice1 does depend on other slices: -SlicesTestAssembly.MultipleSubnamespaces.Slice1 -> SlicesTestAssembly.MultipleSubnamespaces.Slice2 - SlicesTestAssembly.MultipleSubnamespaces.Slice1.Service.Service1Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service.Service2Class - - - - diff --git a/documentation/docs/guide.md b/documentation/docs/guide.md index 022bf60d9..2c522a8c1 100644 --- a/documentation/docs/guide.md +++ b/documentation/docs/guide.md @@ -239,17 +239,18 @@ PlantUmlDefinition.ComponentDiagram().WithDependenciesFromSlices(sliceRule.GetOb ``` ### 5.2 Structured diagram dependencies -On the previous case there are too many dependencies, this option is suitable mainly for small projects / architectures / slices. With an increase in the number of objects, it makes sense to introduce another type of display - a display with packages. It differs from the previous case only with the modified creation of SliceRule. +On the previous case there are too many dependencies, this option is suitable mainly for small projects / architectures / slices. With an increase in the number of objects, it makes sense to introduce another type of display - a display with packages. It differs from the previous case only in the `NestSlicesInPackages` option, which nests the slices of a pattern in `package` blocks along the namespace they matched. A slice that does not come from a pattern, or whose types matched different namespaces before the capture group, is drawn on its own under its name. ![Diagram](diagrams/archUnitNet_all_withPackages.svg) ```cs string pattern = "ArchUnitNET.(**)"; -GivenSlices sliceRule = SliceRuleDefinition.Slices().MatchingWithPackages(pattern); +GivenSlices sliceRule = SliceRuleDefinition.Slices().Matching(pattern); Architecture arch = new ArchLoader().LoadAssembly(typeof(ArchUnitNET.Domain.Architecture).Assembly).Build(); +GenerationOptions g = new GenerationOptions(){NestSlicesInPackages = true}; string path = "diagram.puml"; -PlantUmlDefinition.ComponentDiagram().WithDependenciesFromSlices(sliceRule.GetObjects(arch)).WriteToFile(path); +PlantUmlDefinition.ComponentDiagram().WithDependenciesFromSlices(sliceRule.GetObjects(arch), g).WriteToFile(path); ``` ### 5.3 LimitDependencies-mod @@ -258,9 +259,9 @@ The previous case still shows a large number of connections. For maximum simplif ![Diagram](diagrams/archUnitNet_all_compact.svg) ```cs string pattern = "ArchUnitNET.(**)"; -GivenSlices sliceRule = SliceRuleDefinition.Slices().MatchingWithPackages(pattern); +GivenSlices sliceRule = SliceRuleDefinition.Slices().Matching(pattern); Architecture arch = new ArchLoader().LoadAssembly(typeof(ArchUnitNET.Domain.Architecture).Assembly).Build(); -GenerationOptions g = new GenerationOptions(){LimitDependencies = true}; +GenerationOptions g = new GenerationOptions(){NestSlicesInPackages = true, LimitDependencies = true}; string path = "diagram.puml"; @@ -298,9 +299,9 @@ To enable an C4plantUML-style view of diagrams, set in the GenerationOptions fla ![Diagram](diagrams/archUnitNet_three_alternative.svg) ```cs string pattern = "ArchUnitNET.(*).(*).(*)"; -GivenSlices sliceRule = SliceRuleDefinition.Slices().MatchingWithPackages(pattern); +GivenSlices sliceRule = SliceRuleDefinition.Slices().Matching(pattern); Architecture arch = new ArchLoader().LoadAssembly(typeof(ArchUnitNET.Domain.Architecture).Assembly).Build(); -GenerationOptions g = new GenerationOptions(){C4Style = true}; +GenerationOptions g = new GenerationOptions(){NestSlicesInPackages = true, C4Style = true}; string path = "diagram.puml"; @@ -308,20 +309,31 @@ PlantUmlDefinition.ComponentDiagram().WithDependenciesFromSlices(sliceRule.GetOb ``` ### 5.6 Focus-mod -Focus mod allows you to show all dependencies on the selected package or out of the package. +Focus mod allows you to show all dependencies on the selected package or out of the package. With `NestSlicesInPackages`, slices are known by their fully qualified path, so the package to focus on is fully qualified as well. ![Diagram](diagrams/archUnitNet_focusOn.svg) ```cs string pattern = "ArchUnitNET.(**)"; -string focusOnThisPackage = "ArchUnitNET.Fluent.Syntax.Elements" -GivenSlices sliceRule = SliceRuleDefinition.Slices().MatchingWithPackages(pattern); +string focusOnThisPackage = "ArchUnitNET.Fluent.Syntax.Elements"; +GivenSlices sliceRule = SliceRuleDefinition.Slices().Matching(pattern); Architecture arch = new ArchLoader().LoadAssembly(typeof(ArchUnitNET.Domain.Architecture).Assembly).Build(); +GenerationOptions g = new GenerationOptions(){NestSlicesInPackages = true}; string path = "diagram.puml"; -PlantUmlDefinition.ComponentDiagram().WithDependenciesFromSlices(sliceRule.GetObjects(arch), focusOnThisPackage).WriteToFile(path); +PlantUmlDefinition.ComponentDiagram().WithDependenciesFromSlices(sliceRule.GetObjects(arch), focusOnThisPackage, g).WriteToFile(path); ``` +### 5.7 Migrating from MatchingWithPackages +`MatchingWithPackages` is obsolete and will be removed in a future release. It creates the same slices +as `Matching`, with the same names and violations, and only nests them in diagrams. Replace +`MatchingWithPackages(pattern)` with `Matching(pattern)` and draw the slices with +`new GenerationOptions { NestSlicesInPackages = true }`. + +Failure messages of former `MatchingWithPackages` rules no longer start a slice's name with the +namespace before the first capture group, and their frozen violations, which were stored under those +longer names, have to be frozen again once. + ## 6. Further Reading and Examples A complete overview of all available methods can be found [here](additional.md).