Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions docs/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
FROM jekyll/jekyll:stable
FROM ruby:3.3-slim

MAINTAINER Peter Gafert <peter.gafert@archunit.org>
LABEL maintainer="Peter Gafert <peter.gafert@archunit.org>"

RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
git \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /srv/jekyll

COPY Gemfile .
COPY Gemfile.lock .
COPY Gemfile Gemfile.lock ./

RUN bundle install
RUN gem install bundler -v 2.3.25 \
&& bundle install
17 changes: 15 additions & 2 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,22 @@ Install [Docker](https://docs.docker.com/engine/installation/) and [Docker Compo
then execute

```
docker-compose up
docker compose up
```
The site will be served on [localhost:4000](http://localhost:4000)

The site will be served on [localhost:4000](http://localhost:4000).

### Rebuilding the User Guide

Jekyll does not render the AsciiDoc user guide (`userguide/*.adoc`) — Gradle
does. After editing any `.adoc` file, run from the repository root:

```
./gradlew :docs:renderUserGuide
```

This regenerates `userguide/html/` and `_pages/use-cases.md`, which the
running Jekyll container will pick up on the next browser refresh.


## Credits
Expand Down
6 changes: 2 additions & 4 deletions docs/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
version: "3.0"

services:
site:
build: .
image: archunit/jekyll:latest
command: bundle exec jekyll serve --verbose --trace --host 0.0.0.0 --watch --incremental
command: bundle exec jekyll serve --verbose --trace --host 0.0.0.0 --watch --force_polling
environment:
- JEKYLL_ENV=development
volumes:
- ./:/srv/jekyll
- ./_config-dev.yml:/srv/jekyll/_config.yml
ports:
- 4000:4000
- "4000:4000"
5 changes: 3 additions & 2 deletions docs/userguide/003_Getting_Started.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,9 @@ ArchRule myRule = classes()
.should().onlyBeAccessed().byAnyPackage("..controller..", "..service..");
----

The two dots represent any number of packages (compare AspectJ Pointcuts). The returned
object of type `ArchRule` can now be evaluated against a set of imported classes:
The two dots represent any number of packages (compare AspectJ Pointcuts); for details on
the supported package pattern syntax see <<Package Identifiers>>. The returned object of
Comment thread
StefanGraeber marked this conversation as resolved.
type `ArchRule` can now be evaluated against a set of imported classes:

[source,java,options="nowrap"]
----
Expand Down
117 changes: 117 additions & 0 deletions docs/userguide/006_The_Core_API.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -396,3 +396,120 @@ ArchUnit's own rule APIs (compare <<The Lang API>>) never rely on the
classpath though. Thus the evaluation of default rules and syntax combinations, described in the
next section, does not depend on whether the classes were imported from the classpath or
some JAR / folder.

=== Package Identifiers

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel that Package Identifiers should be inside Domain, not at the same level as Import and Domain.

Suggested change
=== Package Identifiers
==== Package Identifiers


Several ArchUnit methods accept a `String` package identifier to describe a set of packages,
for example `resideInAPackage(..)` and `resideInAnyPackage(..)` (and their negated variants)
in the Lang API, `slices().matching(..)` and `modules().definedByPackages(..)` in the Library
API, or the component stereotypes of a PlantUML component diagram. All of them delegate to
the same underlying https://javadoc.io/doc/com.tngtech.archunit/archunit/latest/com/tngtech/archunit/core/domain/PackageMatcher.html[`PackageMatcher`], whose syntax is inspired by AspectJ type patterns and
extended with capturing groups.

==== Wildcards

The syntax is built from the following elements:

[cols="1,4"]
|===
| Pattern | Meaning

| `*`
| Matches any sequence of characters **not containing** the dot `.`,
i.e. exactly one package segment.

| `..`
| Matches any sequence of characters that **may contain** the dot `.`,
i.e. any number of package segments (including zero).

| `(*)`
| Like `*`, but additionally _captures_ the matched segment,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find no explanation of what it means that it is captured.
Can you access it later froim somewhere in the test? can you have a back reference in the match string?
apart from

`com.myapp.controller` (group 1 = `controller`) and
`com.myapp.service` (group 1 = `service`).
The deeper packages do not match because `(*)` allows only a single segment.

I find no information about that part.
What is the use of group1?
Is there any beyond allowing

com.myapp.(*)..

which seems to be the only way to exclude the parent package com.myapp. Or is that even excluded because there is no class in it? (I don't think so)
The section in file 8 adds some usage of it. Maybe link to that section as usage hint from here?

@hankem hankem Jul 24, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See SlicesRuleDefinition, which can even make use of multiple capturing groups, see Slice.as.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hankem the file 8 talks about Slices and tests of them, so I think we mean the same thing.
Just from documentation here, I have no idea what these groups are used for

@hankem hankem Jul 24, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The immediate effect is that captured substrings can be obtained from PackageMatcher.match's Result's getGroup(int) (similar to java.util.regex.MatchResult#group(int)).

Slices according to package identifiers are just a prominent use case.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First of all: thanks Stefan for raising this question. To me the use-cases for capturing and groups here are also not that clear based on the JavaDoc in PackageMatcher..

Based on this discussion I think we should mention that they can be obtained programmatically, as outlined by Manfred. Furthermore, I like the idea of linking to file 8 where one exemplary use-case of this feature is documented.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's absolutely fair to document that capturing groups are relevant for the result of {@link #match}.

Slices are a good example; the architecture rules wouldn't allow a reference from PackageMatcher (in core) to Slices (in library), but the JavaDoc might not be covered... 😅

so it can be referenced later (e.g. as a slice identifier, see <<Slices>>).

| `(**)`
| Like `..`, but additionally _captures_ the matched segments.

| `[a\|b]`
| Alternation: matches either `a` or `b`. Alternations are only allowed inside brackets
`[...]` or inside capturing groups `(...)`.
|===

Note that `(..)` is not a valid capturing group — use `(**)` instead. `()` and `[]` cannot
be nested inside each other.

The segments matched by capturing groups can be retrieved from the result of
https://javadoc.io/doc/com.tngtech.archunit/archunit/latest/com/tngtech/archunit/core/domain/PackageMatcher.html#match(java.lang.String)[`PackageMatcher#match(String)`]
via
https://javadoc.io/doc/com.tngtech.archunit/archunit/latest/com/tngtech/archunit/core/domain/PackageMatcher.Result.html#getGroup(int)[`PackageMatcher.Result#getGroup(int)`]
(groups are 1-based, in the order the capturing groups appear in the pattern).

==== Worked Example

Consider the following five classes and the packages they reside in:

[cols="2,2"]
|===
| Class | Package

| `com.myapp.controller.SomeController` | `com.myapp.controller`
| `com.myapp.service.SomeService` | `com.myapp.service`
| `com.myapp.service.impl.SomeServiceImpl` | `com.myapp.service.impl`
| `com.myapp.persistence.dao.SomeDao` | `com.myapp.persistence.dao`
| `com.myapp.persistence.dao.jpa.SomeJpa` | `com.myapp.persistence.dao.jpa`
|===

The following table shows a range of package identifiers and which of the five packages
above each of them matches:

[cols="2,3"]
|===
| Package Identifier | Matches

| `com.myapp.service`
| `com.myapp.service` only (exact match).

| `com.myapp.*`
| `com.myapp.controller` and `com.myapp.service`
(exactly one more segment after `com.myapp`).

| `com.myapp..`
| All five packages — `com.myapp..` matches `com.myapp` itself and every subpackage of it.

| `..service`
| `com.myapp.service` only (segment `service` at the end).

| `..service..`
| `com.myapp.service` and `com.myapp.service.impl`
(any package containing a segment `service`).

| `..dao..`
| `com.myapp.persistence.dao` and `com.myapp.persistence.dao.jpa`.

| `..impl`
| `com.myapp.service.impl` only.

| `com.myapp.(*)`
| `com.myapp.controller` (group 1 = `controller`) and
`com.myapp.service` (group 1 = `service`).
The deeper packages do not match because `(*)` allows only a single segment.

| `com.myapp.(*)..`
| All five packages; group 1 captures the first sub-package below `com.myapp`
(`controller`, `service`, `service`, `persistence`, `persistence` respectively).
This is the typical pattern used by `slices().matching(..)`.

| `..[service\|controller]..`
| `com.myapp.controller`, `com.myapp.service` and `com.myapp.service.impl`.
|===

==== What is Matched Against the Pattern

An important detail is that the package identifier is matched against the
**package name of a class**, not against the fully qualified class name.
So for `com.myapp.service.SomeService` the string that is checked against
the pattern is `com.myapp.service`, not `com.myapp.service.SomeService`.

This means that a pattern like `..SomeService` will _not_ match a class named `SomeService`,
because `SomeService` is the simple class name, not a package segment. To match by class
name use a name-based predicate such as
`haveSimpleName("SomeService")` or `haveNameMatching(".*SomeService")` instead.
5 changes: 4 additions & 1 deletion docs/userguide/008_The_Library_API.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,10 @@ com.tngtech.archunit.library.dependencies.SlicesRuleDefinition
----

The API is based on the idea to sort classes into slices according to one or several package
infixes, and then write assertions against those slices. At the moment this is for example:
infixes, and then write assertions against those slices. The `matching(..)` argument follows
the package pattern syntax described in <<Package Identifiers>>, where the parentheses
`+++(*)+++` / `+++(**)+++` mark the captured segment(s) that are used as slice identifiers.
At the moment this is for example:

[source,java,options="nowrap"]
----
Expand Down
Loading