Repository navigation
Match slice patterns with regex like ArchUnit - #516
Open
alexanderlinne wants to merge 1 commit into
Open
alexanderlinne wants to merge 1 commit into
alexanderlinne wants to merge 1 commit into
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## identify-slices-by-captures #516 +/- ##
============================================================
Coverage 86.48% 86.48%
============================================================
Files 261 261
Lines 12676 12679 +3
Branches 1232 1231 -1
============================================================
+ Hits 10963 10966 +3
Misses 1366 1366
Partials 347 347 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
alexanderlinne
force-pushed
the
match-slice-patterns-with-regex
branch
from
October 30, 2026 19:44
4c67d23 to
756f9a3
Compare
alexanderlinne
marked this pull request as ready for review
October 30, 2026 19:51
Slice patterns were matched by string surgery: the pattern was split at
the first "(*", and everything after it was discarded and replaced with
"(**).". Every pattern therefore behaved as if it ended in a greedy
capture, so "(*)", "(*)..", "(*).(*)" and "(*)..(*)" all produced the
same nine slices, a trailing literal such as "(**).Service.." matched
nothing, and alternation and wildcards were ignored outright.
Convert the pattern to a regular expression instead, porting
PackageMatcher.convertToRegex from ArchUnit. "(*)" now captures exactly
one namespace segment, "(**)" one or more, "*" matches a segment without
capturing it, ".." skips segments, and "[A|B]" alternates. "(A|B)" is a
capture group, as it is in ArchUnit: the regex captures it like any
other group, so the "has to contain a capture group" check accepts it as
well.
".." deviates from upstream. ArchUnit's TWO_DOTS_REGEX,
(?:(?:^\w*)?\.(?:\w+\.)*(?:\w*$)?)?
is optional as a whole and brings its own dots, so it also matches the
empty string in the middle of a segment ("App.(*)..(*)" on App.Single
would capture ["Singl", "e"]). Expand ".." depending on where it sits in
the pattern instead, so the separating dot is always consumed:
leading (?:\w+\.)* skip whole leading segments
trailing (?:\.\w+)* skip whole trailing segments
inner \.(?:\w+\.)* the separating dot, then whole segments
The expansion stays greedy; only the mid-segment matches are gone. The
deviation is recorded on the method.
Every capture group now captures a part of the slice, so a pattern with
several groups gives slices with several parts, named like ArchUnit:
App.(*).(*) App.Orders.Http Slice Orders - Http
App.(*).Service.(*) App.Orders.Service.Http Slice Orders - Http
App.(*)..(*) App.Orders.Web.Http Slice Orders - Http
Nested diagrams need nothing from the pattern; slices with several
parts nest at their types' common namespace like any other, and focus
mode and LimitDependencies, which use that namespace too, need no
change.
Pattern validation moves from the per-type assignment callback to
Matching/MatchingWithPackages, so a malformed pattern is rejected where
it is written rather than on the first type that happens to be checked.
The "has to contain a capture group" check is deliberately last: a
pattern that is malformed *and* has no capture group should report the
specific problem.
The old matcher could produce slice names containing dots even for a
pattern made only of "(*)" groups, and PlantUmlFileBuilder compensated
with RemovePatternInappropriateSlices, which dropped every slice with at
least as many dots as the pattern had "(*)" groups. Now that "(*)"
expands to \w+, a captured part can never contain a dot, so that filter
can no longer remove anything. Remove it along with the
CountOfAsteriskInPattern it consumed.
Document the pattern syntax and the multi-capture naming on
SliceRuleInitializer and SliceIdentifier, and add the pattern token
table and the multi-capture naming rules to the guide's "Slice Rules"
section.
Visible in the tests:
- "SubnamespaceCircle.(*).." now folds Slice2.Inner into Slice2 and so
reports the cycle Slice1 -> Slice2 -> Slice1. It used to
give Inner a slice of its own and see no cycle.
- "MultipleSubnamespaces.(**).." no longer drops the top-level classes,
so it now sees the Slice3 -> Slice1 edge that closes the loop.
- The DotDotSemantics fixture shows ".." no longer splitting
"AlphaService" or merging it with "Alpha.Service".
BREAKING CHANGE: slice membership follows the pattern now. "(*)" matches
exactly one namespace segment, so Matching("App.(*)") leaves out every
type below App.<X>.<Y> instead of giving it a slice of its own -- use
"App.(*).." to fold those types into their top-level slice, or "App.(**)"
to keep one slice per namespace. "(**).." now also includes the types
directly in the captured namespace. Rules can therefore report different
violations, and slice names in failure messages and frozen-rule stores
change along with the slices. Rules with several capture groups have to
be re-frozen once.
BREAKING CHANGE: Slice.CountOfAsteriskInPattern,
SliceIdentifier.CountOfAsteriskInPattern and the countOfAsteriskInPattern
parameter of SliceIdentifier.Of are gone.
Signed-off-by: Alexander Linne <alexander.linne@tngtech.com>
alexanderlinne
force-pushed
the
match-slice-patterns-with-regex
branch
from
October 30, 2026 20:08
756f9a3 to
f702e73
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #468
Fixes #208
Why
Slice patterns were matched by string surgery: everything after the first
(*was replaced with(**)., so every pattern behaved as if it ended in a greedy capture.(*)and(**)selected the same slices.(*),(*)..,(*).(*)and(*)..(*)all produced the same nine slices.(**).Service..matched nothing.The exporter compensated with
RemovePatternInappropriateSlices, which dropped every slice with at least as many dots as the pattern had(*)groups, so a diagram showed fewer slices than the rule checked.What changes
Matching. The pattern is converted to a regular expression, ported from ArchUnit's
PackageMatcher:(*)(**)(A|B)*..[A|B]..deviates from ArchUnit on purpose. ArchUnit's expression for it can match the empty string in the middle of a segment (App.(*)..(*)onApp.Singlewould captureSinglande), so it is expanded depending on where it sits and always consumes the separating dot. The method documents why.Malformed patterns are rejected by
Matching/MatchingWithPackageswhen the rule is defined, instead of on the first type that happens to be checked.This is what fixes both issues:
Slices don't differentiate between (*) and (**) making top-level cycle checks impossible #468:
(*)no longer behaves like(**). With…MultipleSubnamespaces.(*),Slice2.Serviceused to be a slice of its own in rules, which the exporter then hid from diagrams withRemovePatternInappropriateSlices. Now there are exactly three slices, one per top-level namespace, each holding only the types directly in it, so a diagram shows what the rule checks. The diagram symptoms, such asSlice1drawn as an empty package in focus mode, are already gone with Nest slices in packages via GenerationOptions, deprecate MatchingWithPackages #513.Slice matching does not group classes of subnamespaces into the correct slice #208:
Matching("Module.(*)..")now foldsModule.Two.InnerNamespaceinto theTwoslice, so the cycle is found.Matching("Module.(*)"), as written in the issue, now leavesInnerNamespaceout entirely, as ArchUnit does, instead of giving it a slice of its own. A new test on theSubnamespaceCirclefixture pins this.Several capture groups. Every capture group now captures a part of the slice, so a pattern with several groups gives slices with several parts. #515 already defined how those are identified, named and frozen:
App.(*).(*)App.Orders.HttpSlice Orders - HttpApp.(*).Service.(*)App.Orders.Service.HttpSlice Orders - HttpApp.(*)..(*)App.Orders.Web.HttpSlice Orders - HttpApp.(*).*.(*)App.Orders.Web.HttpSlice Orders - HttpThe pattern text between the groups only selects types, so all four are the same slice.
Nested diagrams need nothing from the pattern: slices with several parts nest at their types' common namespace like any other (#513). Focus mode and
LimitDependencies, which use that namespace too, need no change either. They apply to slices with several parts as they are.(*).(*)and(*)..(*)no longer draw the same diagram:(*).(*)needs exactly two segments, so the top-levelSliceNclasses belong to no slice any more, and the package arrows that came from them are gone.(*)..(*)also picks up theInnernamespaces. The sliceSlice3 - Innerholds the types ofGroup1.InnerandGroup2.Inner, so it sits inSlice3.Only the types decide where a slice sits:
Slice Slice3 - Innersits atSlice3.Group1.Innerfor(*).Group1.(*), and atSlice3for(*).*.(*), whoseInnerslice spans both groups. Before, both patterns drew the same diagram as(*).(*)above:Where names no longer say where a slice lives, focus mode and
LimitDependenciesstill work, as they go by namespaces. With…MultipleSubnamespaces..(*), the slicesGroup1,Group2,InnerandSlice3all lie inSlice3, so focusing on…Slice3focuses all four.LimitDependenciesdropsGroup1 → Slice3andService → Slice3, because those slices lie at depths 4, 2 and 3, although none of the names has a dot. New snapshots pin both.Removed. A
(*)capture can no longer contain a dot, soRemovePatternInappropriateSlicescan't remove anything any more. It is removed, along with theCountOfAsteriskInPatternit consumed.Docs. The XML documentation of
SliceRuleInitializerandSliceIdentifierdescribes the pattern syntax and the multi-capture naming. The guide's "Slice Rules" section gains:Section 5.4 describes repeated
(*)groups.Breaking changes
(*)matches exactly one namespace segment, soMatching("App.(*)")leaves out every type belowApp.<X>.<Y>instead of giving it a slice of its own. UseApp.(*)..to fold those types into their top-level slice, orApp.(**)to keep one slice per namespace.(**)..now also includes the types directly in the captured namespace.Slice.CountOfAsteriskInPattern,SliceIdentifier.CountOfAsteriskInPatternand thecountOfAsteriskInPatternparameter ofSliceIdentifier.Ofare removed.How the images were made
Both images render the
.verified.txtsnapshots at #515 and at this PR, on theMultipleSubnamespacesfixture; the "before" side of image 2 is the same diagram as that of image 1. They were rendered with PlantUML 1.2025.4 and Graphviz, with the remote!includeof C4-PlantUML replaced by PlantUML's bundled copy (!include <C4/C4_Container>).