Patterns and identifiers

Every rule selects code by matching a pattern against an identifier. Two things are worth getting right before you write a glob: what the identifier looks like, and which part of it the verb you chose reads.

Identifiers

A file is identified by its path relative to the project root, always with forward slashes: internal/api/handler.go. A declared type — a class, in the family’s vocabulary — is that path’s package followed by the type’s own name: internal/api.Handler.

Identifiers are normalised, so internal\api\handler.go on Windows and internal/api/handler.go on Linux are the same identifier and one pattern matches both. They are project-relative throughout: no rule ever sees the absolute path of your checkout, and a pattern that starts with one matches nothing.

Globs

Patterns are globs, and globs are sugar: every one of them compiles to an anchored regular expression in one place, so nothing downstream of the fluent API ever sees a glob.

Glob Means
* any run of characters inside one segment, never crossing /
** any run of characters, crossing /; a/** matches a itself as well as a/b/c
? exactly one character, never /
[a-z], [!abc] one character from a class, or not from it
(**), (*) in a slicing only: the part of the identifier to cut a slice name out of

Everything else is literal. Matching is case-sensitive, patterns are anchored at both ends — api/** does not match internal/api/handler.go, and **/api/** does — and there is no escape character, because separators are normalised and the one character you would want to escape is not special anywhere.

What each verb matches against

Which part of the identifier a pattern is read against is the selector’s business, and a violation message names that part rather than the verb, because the part is what you have to compare your glob to.

Verb Matches against For internal/api/handler.go
WithName the filename handler.go
InFolder everything but the filename internal/api
InPath the whole identifier internal/api/handler.go
InFile the whole identifier, taken literally
ForClassesMatching the declared type’s own name Handler

InFile is the one that is not a pattern: the identifier is taken literally, so a file whose name contains *, [ or . needs no defensive spelling. Chaining it twice selects nothing, because scope verbs are combined with AND and no file is two files.

When a glob is not enough

DefinedByRegex and SliceByRegex take Go’s own regexp syntax instead, for the patterns a glob cannot spell — the fluent verb, and the projection behind it a caller can hold. Every other verb, DefinedBy included, reads a glob:

byRegex := archunit.ProjectSlices(nil).DefinedByRegex(`internal/([^/]+)/.*`)
byGlob := archunit.ProjectSlices(nil).DefinedBy("internal/(**)/**") // the glob spelling of the same slicing

Those two describe the same slicing — the second is the first written in the sugar. A regular expression is anchored at both ends as well, and it has to hold exactly one capturing group when it is a slicing, because the capture is where the slice’s name comes from.

The layers family has the general form under a name of its own: DefinedBy matches the whole identifier of a file, so a layer whose members are named rather than placed — internal/**/*_repository.go — is spelled with it, and DefinedByFolder is the folder-shaped case said out loud.

The projections behind the slicing verbs

A slicing is a MapFunction: the thing that relabels one dependency of the graph as a dependency between two slices, or drops it. The fluent verbs build one for you, and four of them are exported for a caller who wants to hold one:

Function Slices a project by
archunit.SliceByPattern the capture in a glob — the projection behind DefinedBy
archunit.SliceByRegex the capture in a regular expression — the projection behind DefinedByRegex
archunit.SliceByFileSuffix the last _-separated word of a filename, so order_handler.go is in the slice handler
archunit.Identity nothing at all: every dependency under the identifiers it already carries

SliceByPattern and SliceByRegex return an error for a pattern that will not compile, or one that does not capture exactly one name. SliceByFileSuffix takes no argument, so it has nothing to get wrong.

A pattern that matches nothing

Zero matches is a violation, not a pass. A selector matching no file is almost always a stale glob or a renamed folder, and a rule about nothing is green forever — so every terminal refuses one. That guard, and the way to ask a selector what it actually resolved to, are on running a rule.