matcher-generator v5.1.0

This is the architecture documentation of hamcrest-matcher-generator. It follows the arc42 template: twelve chapters that go from the goals and the context of the system through its building blocks, its runtime behaviour and its deployment to the decisions, quality requirements and risks that shaped it.

Version 5.1.0, built 2026-11-08.

The structure comes from arc42, the template for the documentation of software and system architecture, created and maintained by Dr. Peter Hruschka and Dr. Gernot Starke and contributors. Thanks to them for making it freely available.

1. Introduction and Goals

hamcrest-matcher-generator is an annotation processor for Java and Kotlin projects. A test developer names the model classes (beans and records) in a @GenerateMatchers annotation, and the processor generates type-safe Hamcrest matchers and AssertJ assertions for their properties while the project compiles. The model classes stay untouched. This chapter states what the system has to do, which qualities matter most and who cares about it.

1.1. Requirements Overview

  • Generate Hamcrest matchers for model/domain classes at compile time

  • No production-code pollution - configuration uses one annotation on a separate configuration class, leaving model classes untouched (the retention rule is in chapter 4, Non-intrusiveness)

  • Compile-safe alternative to Hamcrest’s hasProperty() / HasPropertyWithValue

  • Informative mismatch messages via the generated fluent API

  • Lombok-annotated classes are supported

  • Works with plain Java and Kotlin (via KAPT)

Planned feature set of 6.0.0, the final feature release

Version 6.0.0 adds a second backend for AssertJ next to Hamcrest. Both are driven by the same configuration. Its features are:

  • Completeness check (the lead feature, for both backends): every property of a type is either checked or consciously ignored. Users opt in per assertion and the check runs when the test runs: Hamcrest matchers offer checkingAllProperties(), AssertJ assertions offer the scope satisfiesCheckingAllProperties(…​) and the closing call hasAllPropertiesChecked(), and ignoringFoo() marks a property as consciously ignored. Without the opt-in, matchers behave as in 5.x.

  • Reference-object matcher (Hamcrest): compares a whole object against a reference object and allows single properties to be overridden or reset. The comparison goes deep: a nested value whose type has a generated matcher is compared property by property, a list item by item in order, any other collection in any order, and a map key by key. Everything else is compared with equals. AssertJ users get the same with AssertJ’s built-in recursive comparison, which the documentation shows side by side.

  • Readable mismatches (Hamcrest): only the failing checks are listed, one per line, each with its property path and both values, for example customer.address.city: expected "Berlin" but was "Paris". Paths reach into lists (lines[1].quantity) and maps (bySku["B-2"].quantity); missing and unexpected items or keys get their own line. A generated matcher used inside one of Hamcrest’s own matchers, such as contains(…​), prints on a single line. Values of generated types are shown with their properties instead of their toString().

  • Collection matchers (Hamcrest runtime): itemsInOrder(…​), itemsInAnyOrder(…​) and entries(…​) check every item of a list or collection, or every entry of a map, and produce the paths above. Hamcrest’s own collection matchers keep working but print Hamcrest’s less specific messages.

  • AssertJ messages keep AssertJ’s native Expecting …​ to be …​ but was …​ style and name the property path from the asserted object, for example Expecting Order.customer.address.city to be: "Berlin". Inside a collection or map, the path starts again at the element type, because AssertJ reports the element’s index itself. An optional generated Representation shows generated types with their properties in AssertJ’s own messages.

  • Exclude configuration: packages or classes can be excluded from generation, for every backend.

  • Null-safety annotations (JSpecify) in the generated code and the Hamcrest runtime, which mainly makes the API type-safe for Kotlin.

  • Clear compiler diagnostics: errors for unresolvable names and for generated names that clash (method names with inherited ones, the top level class names the AssertJ backend generates into one package, the scope names of the Kotlin DSL), warnings for configured types without properties and for exclusions that match nothing.

  • Kotlin support: correct detection of Kotlin boolean and is-prefixed properties, and a Kotlin DSL.

  • Java module names: every published jar declares an Automatic-Module-Name.

  • AssertJ assertions with hasFoo, hasFooSatisfying, isFoo/isNotFoo, an InstanceOfAssertFactory per type and soft-assertion support.

After 6.0.0 no further features are planned; the project receives only fixes and automated dependency updates.

1.2. Quality Goals

Priority Quality Motivation Scenarios

1

Correctness

Generated assertions (Hamcrest matchers and AssertJ assertions) must be type-safe, compile without errors and fail exactly when the checked object is wrong: no false green tests - broken generation is worse than no generation

QS-C1, QS-C2, QS-C3, QS-C4

2

Non-intrusiveness

The core design promise: the model classes stay untouched and the generator adds no runtime dependency to production code; the annotation has SOURCE retention, see chapter 4

QS-N1, QS-N2, QS-N3

3

Usability

The generated assertions (Hamcrest matchers and AssertJ assertions) must be discoverable and ergonomic for test writers

QS-U1, QS-U2, QS-U3, QS-U4

4

Build-tool agnosticism

Works via standard APT (javac) and KAPT (kotlinc); no coupling to a specific build plugin

QS-B1, QS-B2

5

Maintainability

The generator codebase must remain understandable and extensible for contributors

QS-M1, QS-M2, QS-M3, QS-M4

1.3. Stakeholders

Role/Name Contact Expectations

Test engineers / Java-Kotlin developers

GitHub Issues

Stable, backwards-compatible releases; correct and ergonomic generated matchers; reliable Maven Central artifacts

Contributors (incl. maintainer marmer)

https://github.com/marmer/hamcrest-matcher-generator

Understandable architecture; clear contribution path; well-tested generator code

2. Architecture Constraints

Constraint Background

Java 17 baseline

The processor, the runtime artifacts and the generated code run on Java 17 and later, so records are always supported (see ADR 0005).

5.x source compatibility

Tests written against 5.x keep compiling after a version bump alone, provided the project already builds on JDK 17 or later (users on Java 11 stay on 5.x): same matcher class names, packages and isX()/withFoo/resetFoo methods. The artifact switch is covered by bridge jars under the old coordinates; anything else goes into the migration guide (see ADR 0002).

Apache-2.0 license

Every published artifact stays under the Apache License 2.0, with a short NOTICE file and an SPDX header in every source file. Generated code belongs to the user and carries no license header, so the generator’s templates must never contain text copied from third-party code. Dependencies that end up on a user’s classpath may only use permissive licenses (Apache-2.0, MIT, BSD-2-Clause, BSD-3-Clause); the build checks this.

Documentation built on JDK 21 or later

The documentation module renders this documentation with Structurizr, whose current libraries need Java 21. A JDK 17 build skips the module (see ADR 0003).

Conventional Commits

The title of every pull request follows Conventional Commits, because the squash-merged title is the commit that release-please reads to decide the next version (see chapter 7).

3. Context and Scope

3.1. Business Context

System context
Figure 2. System context

Test developers configure the types they need assertions for. Maintainers only appear through the release pipeline (see chapter 7).

Neighbour Exchange

Test developer

Annotates a configuration class with @GenerateMatchers, names the types that need assertions, and takes the artifacts from Maven Central. Reads the generated matchers and assertions in the IDE and uses them in tests.

Maintainer

Merges the Release PR. Everything after that, from the build to the publication and the announcement, runs on GitHub Actions (see chapter 7).

Java compiler (javac, KAPT)

Runs the processor in every round and receives the generated sources.

Hamcrest, AssertJ

The assertion libraries that the generated matchers and assertions build on. The generated code needs them on the user’s test class path. The AssertJ backend itself does not use AssertJ while it generates; the Hamcrest backend does use Hamcrest.

Maven Central, GitHub Pages

Where users find the artifacts and the documentation.

3.2. Technical Context

Neighbour Channel Direction

Java compiler (javac, KAPT)

Annotation processing API (javax.annotation.processing), with Filer for Java sources

The compiler invokes the processor; the processor writes sources.

Kotlin (KAPT only)

Directory named by the processor option kapt.kotlin.generated

The processor writes Kotlin sources (see ADR 0004).

Hamcrest

The generated matchers and the runtime library extend TypeSafeMatcher.

Compile and test classpath of the user’s project.

Maven Central

Maven coordinates

Users resolve the artifacts (see chapter 7).

4. Solution Strategy

The solution rests on a few fundamental decisions. The table connects each quality goal of chapter 1 with the approach that serves it and with the place in this documentation where the approach is described.

Goal Approach Details

Correctness

Generated code is checked at three levels. Golden-file tests compare the generated sources character for character, the end-to-end modules apply the processor to real beans and records and use the generated assertions in ordinary tests, and a JaCoCo floor and PIT mutation testing guard the tests themselves. The generated code is plain Java or Kotlin that the compiler checks, so a wrong property name is a compile error, not a test failure.

Chapter 8 (CI gates), chapter 10

Non-intrusiveness

The configuration annotation sits on a separate configuration class, so model classes stay untouched and no runtime dependency reaches production code. Both configuration annotations, @GenerateMatchers and the deprecated 5.x alias @MatcherConfiguration, have SOURCE retention: the processor reads them from the source elements of the running compilation, so a configuration class leaves no trace of them in its class file, even in the production sources.

Chapter 3, chapter 6

Usability

The generated API is fluent and follows one naming scheme: isX(), withFoo(…​), resetFoo(), ignoringFoo() and the opt-in completeness check. Mismatch messages name the property path of every failing check, and compile errors point at the annotation and name the cause.

Chapter 6 (mismatch messages), chapter 8 (diagnostics)

Build-tool agnosticism

The processor uses only the standard annotation processing API of javax.annotation.processing. It runs under javac for Java and under KAPT for Kotlin, in Maven and Gradle builds alike, and does not depend on a build plugin.

Chapter 3 (technical context), ADR 0004

Maintainability

One library-independent core builds a property model of each type. Every assertion library is a backend behind a small interface (GeneratorBackend) that is found with ServiceLoader, so a new library needs a new module and no change of the core. Maven Enforcer and ArchUnit keep the dependency direction from drifting.

Chapter 5, ADR 0001, ADR 0004

Further decisions that shape the whole system:

  • Generate code instead of reflecting. Assertions are generated while the project compiles, with JavaPoet for Java and KotlinPoet for Kotlin. Nothing is looked up by name at test time, and the IDE can navigate and complete the generated classes.

  • Stay compatible with 5.x. Tests written against 5.x keep compiling after a version bump. Class names and the isX()/withFoo/resetFoo API are kept, the old annotation stays as a deprecated alias and bridge jars cover the old artifact names (ADR 0002).

  • Keep the baseline modern, and the project maintainable without effort. The baseline is Java 17 (ADR 0005), so that dependency updates by Renovate are not blocked by an old Java version. Releases are cut by release-please and published from CI (chapter 7). After 6.0.0 no features are planned, only fixes and updates.

  • Document as code. This documentation and the user guide are built with the project. Code in the guide comes from example modules that compile and run in the same build, and the diagrams are views of one Structurizr model (ADR 0003).

5. Building Block View

5.1. Whitebox Overall System

Containers
Figure 4. Containers
Motivation

The processor is split so that everything that is independent of an assertion library lives once, in the core, while every assertion library is a backend that plugs in from the outside. A user chooses libraries by choosing artifacts (see ADR 0001).

Contained Building Blocks
Building block Responsibility

matcher-generator-annotations

The configuration annotation @GenerateMatchers and its deprecated 5.x alias @MatcherConfiguration.

matcher-generator-core

The processor entry point, the discovery of configured types, the library-free TypeModel and PropertyModel, diagnostics, and the GeneratorBackend interface. It has no assertion library on its classpath.

hamcrest-matcher-generator

The Hamcrest backend: it turns a TypeModel into the source of a <Type>Matcher class with JavaPoet. It also contains the runtime class BeanPropertyMatcher that the generated matchers call at test time and that builds the mismatch messages, and the collection matchers: the factories in CollectionMatchers (itemsInOrder, itemsInAnyOrder, entries) return ItemsInOrder, ItemsInAnyOrder and Entries, which share the package-private base type PathAwareMatcher; BeanPropertyMatcher flattens them into indexed and keyed property paths. The walk along these paths and the rendering of its findings live in the package-private PathWalk and MismatchReport, which both BeanPropertyMatcher and the collection matchers use. ReferenceMatching is what the generated isXLike(reference) methods call: it presets the checks of a reference object (BeanPropertyMatcher.preset), builds one matcher per reference object while a matcher is built, so a cyclic graph yields a cyclic matcher, and BeanPropertyMatcher counts a pair of matcher and item met again as equal. The opt-in completeness check (checkingAllProperties()) is also in BeanPropertyMatcher: a property is addressed when a check or ignoringFoo() names it, resetFoo() removes both, and the properties that are neither are reported after the failing checks. Every generated matcher extends TypeSafeMatcher, implements the public interface GeneratedBeanMatcher (its beanPropertyMatcher() is the seam through which a generated matcher used as a property check of another one is flattened into property paths; it is public because generated code lives in the user’s packages) and passes its property names to the BeanPropertyMatcher constructor.

assertj-assertion-generator

The AssertJ backend: it turns a TypeModel into two classes with JavaPoet, AbstractXAssert<SELF, ACTUAL> on AbstractObjectAssert and a concrete, non-final XAssert with a public constructor and a static assertThat. The abstract class carries hasFoo(value) (deep equality) and hasFooSatisfying(requirements) for every property, and isFoo() and isNotFoo() for boolean ones; all check first that there is an asserted object. Every property also gets ignoringFoo(), and the abstract class has the opt-in completeness check: satisfiesCheckingAllProperties(..) and hasAllPropertiesChecked(). The assertion object keeps the names it has addressed, and the check fails with the properties that are neither addressed nor ignored, after the value checks have passed. The failure reads AssertJ’s wording, Expecting every property of Order to be checked or ignored, but these were neither: tags, and a nested check names its path. The scope form counts only the calls its requirements make, while the closing call counts the whole chain. The property names are a constant of the class. Each concrete class has an InstanceOfAssertFactory constant for as(X) navigation. Each target package gets one SoftAssertionsProvider mixin (written once per package, ADR 0006) with a default assertThat and then per type, so the generated assertions work as soft and BDD assertions. Each target package also gets one PropertyRepresentation, an AssertJ StandardRepresentation that prints configured types by their properties; the user registers it with Assertions.useRepresentation, and it is never registered automatically. Failures use AssertJ’s own Expecting …​ wording and name the property path from the root type. The runtime class AssertionPath carries that path into the assertion of the value a hasFooSatisfying requirement is run for, matched by identity (its limits are in the glossary entry "Assertion path"); inside an array, collection or map the path restarts at the element type. Name clashes in the generated code are reported as compiler errors (see chapter 8, Diagnostics). The backend depends only on the core; the generated code needs AssertJ on the test class path.

hamcrest-matcher-generator-kotlin

The opt-in Kotlin DSL backend (ADR 0004). It writes one <Type>MatcherDsl.kt per configured type next to the Java matcher, with KotlinPoet from Java: an entry function isOrder { } and a scope class OrderSpec per type (nested types included). A nested type whose simple name another type of the package shares gets the names of its enclosing types in front, such as OuterInnerSpec and isOuterInner; a top level type and every type without a clash keep their simple name. The names depend on the set of types of the package only, not on their order, so the backend names them from the models of all types generated in the run. If the names that include the enclosing types still collide (a top level NestedClashCustomer next to NestedClash.Customer), the backend does not pick a new name, since a name that depends on the other types of the build would change when one of them is added. It reports one compiler error per group of colliding types at the @GenerateMatchers annotation, naming all of them, and gives none of them a scope or an entry function; the rest of the DSL is still written. The error is reported by the generation of the type that contains the first colliding type in qualified name order, so it appears once, in whatever order the types are processed in. The types of all @GenerateMatchers configurations of one processing round are compared together; types generated in different rounds are never seen together, so a clash between them is not found. The scope class is marked with @MatcherDsl, a Java annotation in this artifact that carries Kotlin’s @DslMarker, and delegates every call to the generated Java matcher, so mismatch messages are identical. A scope has name(value), name(matcher) and ignoringName() for every property, checkingAllProperties(), and customer { } for a property whose type is a configured type that is compared deeply (neither an enum nor a type without properties). Java types are written as their Kotlin counterparts (String, Int, List<out T>, a star projection for a type variable). A type without properties gets no file. The names of the Hamcrest matcher API come from the public class MatcherNaming of the Hamcrest backend, the one dependency between backends the Enforcer plugin and ArchUnit allow. Without KAPT (GenerationContext.kotlinOutputAvailable is false) the backend generates nothing and reports one compiler note through GenerationContext.reportNote. A property whose name clashes with a member of the scope (checkingAllProperties, delegate, block, matcher) or with an Any member (equals, hashCode, toString) gets no DSL functions and a compiler warning (see chapter 8, Diagnostics); the Java matcher keeps it. Kotlin keywords as property names are escaped with backticks. Collection and map properties take the runtime collection matchers (itemsInOrder, itemsInAnyOrder, entries) whose items are the DSL’s entry functions. The generated Kotlin needs this artifact and kotlin-stdlib on the class path of the Kotlin compilation; it uses only Kotlin 1.9 language features.

hamcrest-matcher-generator-dependencies, hamcrest-matcher-generator-annotationprocessor

Bridge jars without code that depend on hamcrest-matcher-generator, so builds that name a 5.x artifact find a jar and get the Hamcrest backend transitively (see ADR 0002).

-endtoend- modules

Apply the processor to real beans and records, in plain Java, Kotlin and mixed projects, and exercise the generated matchers in ordinary tests.

example-maven-java-hamcrest, example-maven-java-assertj, example-maven-kotlin, example-gradle-java

Documentation examples for the guide and README: Maven Java for each backend, Maven Kotlin with KAPT and the Kotlin DSL, and a Gradle build outside the reactor with its own wrapper. They are never published; the Gradle build resolves the reactor’s artifacts from mavenLocal().

docs

Renders this documentation to HTML and checks its internal links. It is built only on JDK 21 or later and is not published to Maven Central.

Important Interfaces

GeneratorBackend is the only interface between the core and a backend. A backend receives a TypeModel and a GenerationContext (generator name, generation time, whether JSpecify resolves at compile time, the models of all types generated in the run, so that generated code can refer to the code of another type, the names of the methods of a class on the compile class path, inheritedMethodNames, reportError for a compiler error at the annotation, see chapter 8, whether Kotlin output is available, kotlinOutputAvailable, reportNote for a compiler note, reportVerboseNote for a compiler note that is only printed with matchergenerator.verbose, classExistsOutsideOwnOutput for whether a class is on the class path without being in this compilation’s own output, and reportWarning for a compiler warning) and returns its GeneratedSource files, each a Java or Kotlin file (the Hamcrest backend returns one, the AssertJ backend two, the Kotlin DSL backend one Kotlin file, or none). Backends are discovered with java.util.ServiceLoader from the processor path, and every backend found runs for every configured type. Backends depend only on the core, never the other way round, with one exception: the Kotlin DSL backend depends on the Hamcrest backend. The Enforcer plugin and ArchUnit tests guard this.

5.2. Level 2

5.2.1. White Box matcher-generator-core

Components of matcher-generator-core
Figure 6. Components of matcher-generator-core
MatcherGenerationProcessor

The annotation processor (package io.github.marmer.matchergenerator.core) registered through META-INF/services. It loads the backends and hands every round to the worker. It declares the processor option kapt.kotlin.generated.

MatcherGenerationProcessorWorker

Resolves the configured class and package names to types, skips types that are already marked as generated, asks every backend for its source and writes it.

TypeModelFactory

Reads a type with the compiler’s element API and creates the TypeModel: getters for beans, components and accessors for records, inherited properties and public member types.

NullabilityResolver

Derives the Nullability of a property for the TypeModel: primitives are non null, JSpecify and JetBrains @Nullable/@NonNull/@NotNull on the accessor decide explicitly, and otherwise the closest @NullMarked or @NullUnmarked scope does. A type variable is null-agnostic and stays unspecified. Kotlin nullness is read from the JetBrains annotations kotlinc emits, not from kotlin.Metadata. The worker also sets the GenerationContext flag that tells backends whether org.jspecify.annotations.NullMarked resolves, which is the condition for emitting JSpecify annotations.

TypeModel, PropertyModel, Nullability

The immutable model that backends work on (see the glossary term "Property model").

5.2.2. White Box hamcrest-matcher-generator

Components of hamcrest-matcher-generator
Figure 8. Components of hamcrest-matcher-generator
HamcrestGeneratorBackend

The Hamcrest implementation of GeneratorBackend, registered through META-INF/services. For every TypeModel it asks MatcherGenerator for the <Type>Matcher source and returns it as one GeneratedSource.

MatcherGenerator

Builds the source of a <Type>Matcher class with JavaPoet from a TypeModel: the isX() entry point, withFoo, resetFoo and ignoringFoo for every property, checkingAllProperties() and the isXLike(reference) method.

BeanPropertyMatcher and its helpers

The runtime library that the generated matchers call at test time. It reads the checked properties of the actual object, collects the failing checks with their property paths and builds the mismatch message (see chapter 6). Its helpers, the collection matchers (itemsInOrder, itemsInAnyOrder, entries), ReferenceMatching, PathWalk and MismatchReport, are described in the building block table above. The library extends Hamcrest’s TypeSafeMatcher.

6. Runtime View

6.1. Generation of matchers

Generation of matchers in one round
Figure 10. Generation of matchers in one round

The compiler runs the processor in every round in which it finds @GenerateMatchers or its 5.x alias @MatcherConfiguration. For every configured type the following happens:

  1. MatcherGenerationProcessor hands the round to MatcherGenerationProcessorWorker, which resolves the configured class and package names to types. Names that match nothing are a compiler error for @GenerateMatchers and a warning for the 5.x alias, which keeps 5.x configurations compiling. The exclude entries then leave out the configured types they name by class or package, and the public member types they name are not modelled as nested types. An exclude entry that matches neither a configured type nor one of their public member types is a warning. Types that carry the processor’s own @Generated marker are skipped with a note.

  2. TypeModelFactory creates the TypeModel of the type, including its public nested types. A configured type without properties produces a compiler warning when the configuration names it; a type that only a package entry picks up gets a verbose note instead, since packages hold types that are no match target. Each skipped method is reported with its reason, but only when the processor option matchergenerator.verbose is given; without it the compiler prints no notes.

  3. The worker calls every GeneratorBackend found through ServiceLoader with that model and a GenerationContext (its content is listed in chapter 5, Important Interfaces). The worker models every configured type of the round first and generates afterwards, so a backend knows the types of other configurations too. There is no selection: the backends on the processor path are the user’s choice.

  4. Each backend returns its GeneratedSource files, one per generated class. The AssertJ backend also returns one SoftAssertionsProvider mixin per target package, from the first type of that package by name (see the glossary entry Soft assertions mixin), and the same type writes the PropertyRepresentation of that package (see the glossary entry Generated representation). The worker writes Java sources through the compiler’s Filer, so they are compiled in the same build.

  5. Kotlin sources are written into the directory named by the processor option kapt.kotlin.generated, which only KAPT provides (see ADR 0004). The GenerationContext tells the backends whether that option is there. The Kotlin DSL backend generates nothing without it and reports one compiler note, once per compilation; a backend that returns Kotlin anyway makes the worker report one compile error that names the backend and the type, because the Kotlin file would otherwise never be compiled. A type without properties of its own gets no DSL file, although its Java matcher exists. The scope classes and entry functions of all types in a package are top level declarations of that package, so the backend names them from the models of all types generated in the run; the naming rule and the error for names that still collide are in chapter 5.

Files written once per package

A backend may write a file that exists once per target package, like the AssertJ SoftAssertionsProvider mixin and PropertyRepresentation. It does so from the first type of the package in the round, so two configuration classes of one package in one round still produce one file with the types of both. The generated API stays the same in every build: the name of the mixin does not depend on the configuration, so a test class that implements it keeps working when a type is added.

What a round cannot see is the file of an earlier round or compilation (decision: ADR 0006). The rule is: a file is written once per compilation, and the first round that writes it decides its content. The worker lives once per compilation and remembers every source it wrote, so the rule below is generic: it applies to every GeneratedSource of every backend, not only to files written once per package.

  • A later round of the same compilation, e.g. because another processor generated a configuration class, that produces a source whose name an earlier round wrote does not write it again (the Filer would refuse). The worker reports one warning at the annotation that configured the type, naming the file and the types of this round in its package that it does not know. Add those types to the configuration of the first round, or generate the configuration class in a round before the matcher generator runs, to have them in the file.

  • A second compilation of the same package (main and test, KAPT) writes its own file, and the compiler prefers it for that compilation. When the AssertJ backend finds a SoftAssertionsProvider of that package already on the class path outside the output directory of the running compilation, it reports a note naming the types the new mixin knows; PropertyRepresentation gets no such note. The class that a previous, non clean build of the same module left in its own output is replaced by this build and therefore no note. The mixin of the second compilation does not contain the types of the first.

  • The Hamcrest isXLike(reference) compares a property deeply only when the matcher for its type is generated in the same round. A property whose type is a class of the user without a generated matcher in the round is compared with equals; with -Amatchergenerator.verbose a note names the property and the type. Library types (java., javax., kotlin.), enums and types generated in the round, also those without properties, do not get a note. Optional<X> is a java. type and is not unwrapped: it is compared with equals without a note.

A round without configuration can still reach the processor: the matchers written in an earlier round come back carrying javax.annotation.processing.Generated and org.jspecify.annotations.NullMarked. The processor claims these two annotation types in that round, and returns without generating anything. Without the claim, javac’s -Xlint:all -Werror builds would fail on annotations nobody claims. The side effect is that javac skips a later processor in that round when that processor supports exactly these annotation types and nothing else.

Notable aspects
  • The model is built once per type and shared by all backends, so detection rules such as record components or getter naming exist only once.

  • A backend problem shows up as a compile error of the user’s build, not as a failure of the user’s tests. A backend reports it through GenerationContext.reportError, once per problem, at the annotation (see chapter 8, Diagnostics).

6.2. Describing a mismatch (Hamcrest)

When an assertion with a generated Hamcrest matcher fails, the generated <Type>Matcher delegates describeTo and describeMismatchSafely to its BeanPropertyMatcher, which builds the message in this order:

  1. It reads every checked property of the actual object and collects the failing checks as path: expected …​ but …​ lines. A check that holds a generated matcher (recognised through GeneratedBeanMatcher) and meets an object of the matched type recurses into it with the extended property path (customer.address.city). If the object is missing (null) or of another type, the whole nested expectation is one failure.

  2. A check that holds any other matcher is a foreign check: the runtime asks that matcher whether it matches and for its description and mismatch. Around these calls a per-thread depth counter is raised.

  3. A generated matcher that Hamcrest’s own matchers (contains, hasItem, hasEntry) call while that counter is above zero renders on one line (OrderLine {sku "X-9", quantity <3>}); otherwise it renders as a block (Order with 4 property checks, one failing check per line).

  4. Values are printed by a Description that shows values of generated types from their properties, never via toString().

  5. With the completeness check on (checkingAllProperties()), the description ends in , every property checked or ignored, and after the failing checks the mismatch adds one line that lists the properties neither checked nor ignored, with their nested paths, as neither checked nor ignored: customer.phone, customer.address. The golden test BeanPropertyMatcherTest pins this line.

    Notable aspects
    • The counter is raised only by generated matchers. A generated matcher that a foreign matcher holds directly, as in assertThat(list, contains(isOrderLine())), keeps the block format, because nothing generated has called the foreign matcher.

    • The properties printed for a type are the ones the first generated matcher of that type reported; properties every object has (class) are left out.

6.3. Describing a mismatch (AssertJ)

The generated AbstractXAssert fails through AssertJ’s own failure, so the message keeps AssertJ’s form, with the path of the failing property from the root type. A value that differs reads Expecting Order.customer.address.city to be:, the expected value on its own line, then but was: and the actual value. A null nested object reads Expecting Order.customer not to be null. The path is carried by AssertionPath into the nested assertion, see the glossary entry "Assertion path".

The completeness check runs after the value checks. When properties are neither addressed nor ignored, the assertion fails with Expecting every property of Order.customer to be checked or ignored, but these were neither: phone, address. The sentence names the path of the asserted object from the root type, so a nested check reads Order.customer, the path of the nested assertion, and not only the nested type. The test CompletenessCheckTest of the AssertJ end-to-end module pins these messages.

6.4. Release

Release
Figure 12. Release

The maintainer merges the Release PR that release-please keeps up to date. The merge creates the tag and the release page, and the same workflow then builds the tag on GitHub Actions. Only a green build of the tag publishes the signed bundle to Maven Central, and the documentation is deployed to GitHub Pages from the same tag. Finally the workflow announces the release to the maintainer. The workflows, jobs and credentials are described in chapter 7.

7. Deployment View

7.1. Infrastructure Level 1

Deployment of the build artifacts and the documentation
Figure 14. Deployment of the build artifacts and the documentation

The project has no runtime infrastructure of its own: the processor runs inside the compiler of the user’s build. What is deployed are the build artifacts and this documentation.

Motivation

Users take the artifacts from Maven Central, and readers take the documentation from GitHub Pages. Both are produced by automated builds, so no manual upload step exists.

Quality and/or Performance Features

Every published jar is signed and comes with a sources jar and a Javadoc jar, which Maven Central requires. A CycloneDX software bill of materials is attached to every published module, so users can see which dependencies it brings along.

Mapping of Building Blocks to Infrastructure
Building block Deployed to

matcher-generator-annotations, matcher-generator-core, hamcrest-matcher-generator and the two bridge jars

Maven Central through the Central Portal.

-endtoend- modules, docs

Not published. The release bundle excludes them (excludeArtifacts).

Documentation

GitHub Pages, deployed by the Docs GitHub Actions workflow after the docs build and its link checks passed.

7.2. Infrastructure Level 2

7.2.1. Release to Maven Central

The release Maven profile signs the jars with GPG, attaches the sources jar, the Javadoc jar and the CycloneDX SBOM, and lets the central-publishing-maven-plugin upload one bundle to the Central Portal. The plugin publishes the bundle automatically and waits until it is published. The former OSSRH is not used any more.

7.2.2. Documentation on GitHub Pages

The Docs workflow builds the docs module on JDK 21, uploads the rendered HTML as a Pages artifact and deploys it with the official Pages actions. It runs for version tags and on manual request.

The site is published on its custom domain, the one base URL of the docs build (docs.base.url in docs/pom.xml). Every page carries a canonical link to its address there. The site root also holds sitemap.xml (every rendered page), robots.txt (every crawler allowed, names the sitemap) and llms.txt. The github.io address of the project only redirects to the base URL.

7.3. CI pipeline

Pull requests and pushes to main run these workflows on GitHub-hosted runners:

  • .github/workflows/ci-build.yml (CI-Build) runs the jobs Build (mvnw -B clean install -DskipTests), Verify JDK 17, Verify JDK 21, Verify JDK 25 and Verify JDK latest (the matrix; latest follows the newest GA JDK), Release dry run (the release profile with verify, then scripts/ci/check-release-artifacts.sh), Mutation testing (PIT, with a history cache keyed on the PIT version) and Sonar (validates SONAR_TOKEN with scripts/ci/check-sonar-token.py before the build, then runs the scanner with the quality gate), and Example Gradle (validates the wrapper jar with the SHA-pinned gradle/actions/wrapper-validation, runs mvnw -B -DskipEndtoend -DskipTests install, then gradlew build in example-gradle-java on JDK 21 and again on JDK 17, because users copy the example on both; the example resolves the reactor’s artifacts from mavenLocal()). Example Gradle has no Gradle cache (the build is small and a cache is state that can differ between runs) and a 30 minute time limit; the wrapper distribution is pinned by distributionSha256Sum.

  • .github/workflows/domain-docs-sync.yml (Domain docs in sync) runs the domain docs check, the frozen 5.x suite check, scripts/ci/check-project-version.py (every module pom’s own and parent version equals the manifest version, is an extra-files entry, and project dependencies have no literal version), scripts/ci/check-workflow-hardening.py, the unit tests of scripts/ci and scripts/agents, and the check of the ruleset’s job names. It also runs scripts/ci/sync-readme-snippets.py --check, which fails when a README snippet no longer matches its tagged example region; the README’s x-release-please-version lines are raised by release-please through the README.md entry of extra-files.

  • .github/workflows/dependency-review.yml (Dependency review) runs on pull requests, .github/workflows/scorecard.yml (Scorecard) weekly and on pushes to main, and .github/workflows/scanoss.yml (SCANOSS) on pull requests and weekly. SCANOSS is non-blocking.

  • .github/workflows/credential-reminder.yml (Credential reminder) runs weekly and on manual request, in a job of the release environment (so it reads SONAR_TOKEN, CENTRAL_USERNAME, CENTRAL_PASSWORD and MAVEN_GPG_KEY there). It validates the Sonar and Central Portal tokens (scripts/ci/check-sonar-token.py, scripts/ci/check-central-token.py), then scripts/ci/check-credential-expiry.py reads the expiry of the GPG key (the Sonar token has no expiry date, so it raises no Sonar reminder) and the expiry dates (YYYY-MM-DD, set by the maintainer after each renewal) in the repository variables CENTRAL_TOKEN_EXPIRES_ON and RELEASE_APP_KEY_EXPIRES_ON, and opens or updates one renewal issue for a credential that expires within 30 days.

The main ruleset requires Build, Verify JDK 17, Verify JDK 21, Verify JDK 25, Verify JDK latest, Domain docs in sync, Release dry run, Sonar, Mutation testing and Example Gradle. scripts/ci/check-ruleset-job-names.py checks that every required name is a job name, so renaming the Example Gradle job without changing the ruleset fails it. Dependency review, Scorecard and SCANOSS are not required. Scorecard reports only; its score does not gate merges; accepted findings are listed in chapter 11.

7.4. Release pipeline

Releases run on CI and need no machine of the owner.

  • .github/workflows/pr-title.yml (required check PR title) enforces Conventional Commits on pull request titles, because the squash-merged title is the commit that release-please reads.

  • .github/workflows/release.yml runs on every push to main. release-please (type simple, default bump rules, configured in release-please-config.json and .release-please-manifest.json) keeps a Release PR up to date, using an App token so that the PR runs its checks and the new tag triggers the docs deployment. Tags have no v prefix (6.0.0), matching the [0-9]* trigger of docs.yml. The project version lives in the module poms (own and parent <version>), which release-please raises through one xml entry per pom in extra-files, and in the Gradle example (marker x-release-please-version). The simple type is used instead of maven because the Maven updater rewrites the @project.version@ of the frozen 5.x suite and misses the parent versions. The simple type’s optional version.txt is not created: its updater has createIfMissing: false and the release-please 17.11.2 simulation of the 6.0.0 release changed only CHANGELOG.md, the Gradle example and the 20 poms. Dependencies on project artifacts use ${project.version}, the 5.x suite and its runner’s consumer poms are filtered by the build (@project.version@). scripts/ci/check-project-version.py guards all of it.

  • Merging the Release PR creates the tag and the release page. In the same workflow the job Release build runs mvnw verify on the tag, and only if it is green the job Publish to Maven Central runs mvnw -P release deploy (GPG signatures, SBOM, central-publishing-maven-plugin waiting for published), attaches a build-provenance attestation (actions/attest) and makes sure the release page exists.

  • After a confirmed publish, the job Announce release comments on the merged Release PR with the App token, mentioning the owner and linking Maven Central, the release page and the CHANGELOG section.

  • A failed publish is re-run with workflow_dispatch and the existing tag as input; tags are never moved.

  • 6.0.0 is forced with release-as and merged by the owner. Auto-merge of Release PRs stays off until after 6.0.0.

The credentials live in the repository environment release (CENTRAL_USERNAME, CENTRAL_PASSWORD, MAVEN_GPG_KEY, MAVEN_GPG_PASSPHRASE) and in the App settings (RELEASE_APP_ID, RELEASE_APP_PRIVATE_KEY).

8. Cross-cutting Concepts

8.1. Continuous integration gates

Every push runs a small set of automated gates. Each one exists because a defect once reached the main branch or blocked it. The checks the main branch ruleset requires before GitHub merges a pull request are listed in chapter 7.

Gate What it guarantees

Build and Verify matrix (Build, Verify JDK 17, Verify JDK 21, Verify JDK 25, Verify JDK latest)

The reactor installs without tests, and ./mvnw clean verify is green on every supported JDK, including the newest generally available one.

JaCoCo floor (part of Build and Verify matrix, checked in verify)

Each production bundle (matcher-generator-core, matcher-generator-annotations, hamcrest-matcher-generator) has at least 99 % line and 99 % branch coverage. Unit and integration coverage are merged before the check, and no class is excluded, so a drop below the floor fails verify on every JDK in the matrix. The bridge jars are not measured because they hold no code (deliberate deviation from #126).

Release dry run

The release profile still builds through the deploy lifecycle with signing and publishing switched off. A script checks that every published module has its jar, sources jar, Javadoc jar and CycloneDX bill of materials, and that CycloneDX skipped no module. Nothing is uploaded.

Mutation testing (Mutation testing)

PIT mutates the production code of the published modules, and each module must detect at least 90 % of its mutants (mutationThreshold 90, checked by scripts/ci/pit-scores.py). This tests the tests: a suite that stays green when the code is broken fails the gate.

Sonar quality gate (Sonar)

SonarCloud analyses the build and its quality gate fails the job when new code adds issues, duplicated or uncovered code. Old code is not judged again when a rule changes.

Example Gradle (Example Gradle)

The Gradle example outside the reactor builds against the artifacts of the reactor, so the guide’s Gradle setup keeps working on JDK 21 and on JDK 17 (see chapter 7).

Dependency review (Dependency review, not a required check)

A pull request that adds a dependency with a known vulnerability is blocked.

License gate (part of the Build and verify matrix)

Every project dependency that a published jar needs uses a permissive license (Apache-2.0, MIT, BSD-2-Clause or BSD-3-Clause); test-scope dependencies may also use EPL-1.0 or EPL-2.0. The license-maven-plugin fails the build on an unknown or missing license, so a new dependency with an unseen license name must be added to the merge list on purpose. Every published jar carries META-INF/LICENSE, META-INF/NOTICE and THIRD-PARTY.txt, which a per-module test checks. Build plugins and the docs module’s tooling are not checked.

Static analysis (JDK 21+ profile static-analysis, part of Verify JDK 21, Verify JDK 25 and Verify JDK latest)

Spotless checks google-java-format (AOSP style, 4-space indentation) and the SPDX header in every Java and Kotlin source of the modules, including tests and end-to-end sources, except the frozen 5.x suite, which stays frozen and so carries no SPDX headers (deliberate deviation from #128). Error Prone runs its default checks on every module at ERROR with no baseline. NullAway runs at ERROR in JSpecify mode on the io.github.marmer packages only: main, test and end-to-end sources in that package tree. The end-to-end POJOs in foo.bar are not NullAway-checked. Generated code is not checked and carries no license header. The JDK 17 leg skips the profile.

Frozen 5.x suite (part of Domain docs in sync)

The endtoend-5x-compatibility module is the one the 5.x promise was made with, frozen except for the owner-approved deviations (version references filtered by the consumer build, RelocationPomTest deleted with #159), with the tree hash as the guard; its parent and own version (@project.version@) follow the project version, as a 5.x user’s version bump does. It is not a reactor module: endtoend-5x-compatibility-runner builds it as a consumer build with the invoker plugin against an isolated local repository that holds the reactor artifacts, so Maven resolves the bridge jars like it does for a 5.x user, with the project’s own compiler plugin version. The suite runs every test it has; its RelocationPomTest, which asserted the former relocation POMs, was deleted with the owner’s approval (#159), and each bridge module tests its own shape (BridgeJarIT). Its tree hash must equal the stored one. Only the owner may update that hash.

Ruleset against job names (part of Domain docs in sync)

Every status check that the main ruleset requires is the name of an existing CI job, so a removed job cannot block all merges.

Docs sync (Domain docs in sync)

The agent-facing glossary and ADRs are mirrored in these chapters, the required checks named in the agent docs are CI job names, and the file names and packages that CLAUDE.md mentions exist.

8.2. Diagnostics

The core reports a misconfiguration as a compiler diagnostic at the annotation that configured the type, once per problem and naming the offending value (the unresolvable names of #113). A backend that finds a problem in a configured type reports it the same way through GenerationContext.reportError(message): the core prints the message as a compiler error at the @GenerateMatchers annotation. The backend leaves the offending member out of its generated code, so the error is the only one the user sees. The AssertJ backend does this for a generated method whose name an inherited AssertJ method already has: one error per property, naming the property and the clashing methods. It also reports two properties that generate the same method name (such as a boolean foo and notFoo, both isNotFoo()) and two nested types whose factory constants share a name (such as FooBar and Foo_Bar, both FOO_BAR), one error per name. It also reports a top level class name that two types of one package need (Foo and AbstractFoo both generate AbstractFooAssert, or a type named like the package’s mixin SoftAssertionsProvider) and then writes nothing for the package, while FooBar, Foo_Bar and FOOBAR are different classes and no clash; like the other per-package files it sees only the types of one processing round (ADR 0006).

A backend that leaves something out of its generated code without failing the build reports it through GenerationContext.reportWarning(message): the core prints the message as a compiler warning at the same annotation. The Kotlin DSL backend uses it for a property whose name clashes with a member of its DSL scope; that property is left out of the DSL and the Java matcher keeps it.

9. Architecture Decisions

9.1. ADR 0001: Shared property model with separate library backends

Status: accepted (6.0.0 planning)

Version 6.0.0 generates assertions for both Hamcrest and AssertJ from a single configuration. To keep the two libraries cleanly apart, a library-independent core module builds a property model of each configured type, and each assertion library has its own backend module that turns this model into code. Backends depend only on the core and are discovered via Java’s ServiceLoader, so users pick a library simply by adding its artifact to the annotation processor path. The dependency rules are enforced by Maven Enforcer and ArchUnit. The existing @MatcherConfiguration annotation keeps working unchanged as a deprecated alias, and the migration is carried out behind the existing automated tests.

Details: ADR 0001

9.2. ADR 0002: Coordinates, packages and 5.x compatibility

Status: accepted (6.0.0 planning)

The project keeps its name and the group ID io.github.marmer.testutils. Core and annotations get neutral matcher-generator- artifacts, the Hamcrest artifact keeps its name, and AssertJ gets assertj-assertion-generator. New code lives in io.github.marmer.matchergenerator., and the new annotation is @GenerateMatchers. Only the deprecated @MatcherConfiguration stays in the old package. The two 5.x artifacts stay as code-free bridge jars that depend on the Hamcrest artifact, because older compiler plugins do not follow a relocation in the processor path (amendment of 2026-10-27). 5.x user tests should compile after a version bump alone. The Central namespace io.github.marmer is verified in the Central Portal, so these coordinates stay usable.

Details: ADR 0002

9.3. ADR 0003: Docs as code with a Structurizr C4 model and embedded examples

Status: accepted (6.0.0 planning)

Outdated snippets, broken links and missing images are common in project documentation. To rule them out, the whole documentation is built by the normal Maven build. A dedicated docs module renders the user guide and this architecture documentation with Asciidoctor. Code shown in the guide is included directly from example Maven modules, which are compiled and tested in the same build. The diagrams of the context, building block, runtime and deployment views are C4 views generated from a single Structurizr model, so the levels stay consistent and need little explanatory text. The build fails on Asciidoctor warnings, and tests check links, anchors, images and that every modelled view appears in the documentation. Because the current Structurizr libraries require Java 21, the documentation is only built on JDK 21 or newer.

Details: ADR 0003

9.4. ADR 0004: Kotlin DSL as a Kotlin-emitting backend on top of the Hamcrest backend

Status: accepted (6.0.0 planning)

Kotlin users get an optional, type-safe DSL such as isOrder { customer { name("Ada") } }, which builds on the generated Hamcrest matchers. Because it needs one wrapper per configured type, it is generated as well, by a separate opt-in artifact hamcrest-matcher-generator-kotlin. This artifact is another backend behind the same plug-in interface as Hamcrest and AssertJ; that interface therefore returns Java or Kotlin source files, one or more per type. It is the only backend allowed to depend on another backend, the Hamcrest one, whose naming it reuses, and the build enforces that exception explicitly. The generator itself is written in Java and produces Kotlin with the KotlinPoet library, so the project’s production code stays plain Java. It runs only under KAPT, the Kotlin compiler’s way of running annotation processors; under plain javac it skips the DSL and prints one note.

Details: ADR 0004

9.5. ADR 0005: Java 17 baseline for 6.0.0

Status: accepted (6.0.0 planning)

Version 6.0.0 needs Java 17 or newer, for the annotation processor, the runtime artifacts and the generated code. Earlier planning assumed Java 11, but the libraries and tools this project updates automatically have moved on: JUnit 6 and AssertJ 4 need Java 17, Maven 4 needs it to run, and some tools already need Java 21. With Java 11, every such update would stop and wait for the maintainer, which contradicts the goal of a project that maintains itself. A major release is the right moment for this break. Users who stay on Java 11 keep using 5.x; everyone on Java 17 or newer upgrades by changing the version number.

Details: ADR 0005

9.6. ADR 0006: Files written once per package, and the backend SPI channels that report it

Status: accepted (2026-10-29)

Some generated files exist once per target package, such as the AssertJ soft assertions mixin, and they only know the types of the processing round that wrote them. Instead of failing with a Filer exception or leaving types out silently, the file is written once per package, the first round decides its content, and a later round or a second compilation (main and test, KAPT) gets a diagnostic. Naming the mixin per configuration was rejected: a test class that implements it would break when the configuration layout changes, and the types of earlier rounds would still be missing. To report this, the backend plug-in interface gained a verbose-only note and a question whether a class exists outside the compilation’s own output; the rule that a source is not written again applies to every backend. Consequences are that builds with warnings as errors fail on the late-round warning and that the class path note cannot tell every case apart. A diagnostics interface instead of the growing list of channels is recorded as an open idea.

Details: ADR 0006

10. Quality Requirements

The quality requirements refine the five quality goals from chapter 1.2 into concrete, measurable scenarios. They are classified by the quality model of ISO/IEC 25010:2023. Characteristics that are not a quality goal of their own were checked for gaps; where they matter for a build-time test library, they got scenarios as well.

10.1. Quality Requirements Overview

The table maps every quality goal and every further relevant ISO/IEC 25010:2023 characteristic to its scenarios in chapter 10.2.

Quality goal ISO/IEC 25010:2023 characteristic / sub-characteristic Scenarios

1 Correctness

Functional suitability / Functional correctness; Reliability / Faultlessness

QS-C1, QS-C2, QS-C3, QS-C4

2 Non-intrusiveness

Compatibility / Co-existence

QS-N1, QS-N2, QS-N3

3 Usability

Interaction capability / User error protection, Self-descriptiveness, Learnability, Operability

QS-U1, QS-U2, QS-U3, QS-U4

4 Build-tool agnosticism

Compatibility / Interoperability; Flexibility / Adaptability

QS-B1, QS-B2

5 Maintainability

Maintainability / Modifiability, Modularity, Analysability

QS-M1, QS-M2, QS-M3, QS-M4

No quality goal

Compatibility / Co-existence; Flexibility / Replaceability; Security / Integrity, Authenticity; Reliability / Fault tolerance; Performance efficiency / Time behaviour

QS-X1, QS-X2, QS-X3, QS-X4, QS-X5

The remaining characteristics have no scenarios, for these reasons:

  • Safety: the library has no physical effect on people or the environment.

  • Reliability / Availability and Recoverability: the library is not a service; it only runs while a project compiles and tests.

  • Security / Confidentiality, Non-repudiation and Accountability: the library processes no user data and has no users of its own; integrity and authenticity of the released artifacts are covered by QS-X3.

  • Interaction capability / Inclusivity and User engagement: the library has no user interface; the project site’s accessibility is not a quality of the architecture.

  • Performance efficiency / Resource utilization and Capacity, Flexibility / Scalability: the only load is the number of configured types at compile time, which QS-X5 covers.

  • Flexibility / Installability: adding the library is a plain build dependency; QS-U3 and QS-B1 cover it.

10.2. Quality Scenarios

Each scenario is one sentence that names all elements of the SEI scenario form (source, stimulus, environment, artifact, response and response measure), following this pattern:

When source stimulus while environment, artifact response; response measure.

Scenarios describe either a use of the library at runtime (a test developer compiles or runs tests) or a change to the system or its environment (a contributor, a bot or a new JDK). The column "Protected by" names the check that decides whether the scenario is met, as decided for 6.0.0. "None yet (planned for 6.0.0)" marks a scenario without such a check; 6.0.0 adds that check.

ID Characteristic Scenario Protected by

QS-C1

Functional suitability / Functional correctness

Wrong property is caught. When a test developer asserts an object that differs from the expected one in exactly one property while running a JUnit test on any supported JDK, the generated Hamcrest matcher or AssertJ assertion fails that test; this holds in both backends for every property kind of the sample set (primitive, wrapper, String, nested generated type, list, set, map, record component, Kotlin Boolean property), and the same assertion passes for a correct object.

Annotation processor integration tests; end-to-end modules on every JDK

QS-C2

Functional suitability / Functional correctness

A new property is not silently ignored. When a developer adds a property to a model class without touching a test that opted into the completeness check (checkingAllProperties() or hasAllPropertiesChecked()), while that test runs in the next build, the generated assertion fails the test and names the unchecked property; no test with the completeness check stays green while a property is unchecked.

End-to-end modules

QS-C3

Reliability / Faultlessness

A faulty change is stopped. When a contributor opens a pull request that lets a generated assertion pass for a wrong object while CI runs the required checks, the build of the affected processor, runtime or backend module turns red and the pull request cannot be merged; a mutation score of at least 90 % per module and at least 99 % line and branch coverage of production code keep such a change from slipping through.

Build; Mutation testing (PIT); JaCoCo coverage floor

QS-C4

Functional suitability / Functional correctness

A cyclic object graph ends the comparison. When a test developer compares an object whose graph contains a cycle (such as parent.children[0].parent) while using the Hamcrest reference-object matcher, the runtime treats an already visited pair of objects as equal and finishes the comparison; the test ends with a pass or a regular mismatch, never with a StackOverflowError.

Runtime unit test ReferenceMatchingTest; end-to-end ReferenceObjectTest (java-records module)

QS-N1

Compatibility / Co-existence

Production code stays untouched. When a project adds the generator and @GenerateMatchers in test scope only while building with Maven, Gradle or KAPT, the production artifact keeps its model classes and runtime dependencies unchanged: the generator adds no runtime-scope dependency, and no production .class file carries the annotation, because it sits on a configuration class in the test sources. The annotation’s SOURCE retention (chapter 4) extends this to a configuration class in the production sources.

ProductionClassFootprintTest (end-to-end plain-java-minimal: the compiled production class PackageConfigurationProduction contains neither GenerateMatchers nor RuntimeInvisibleAnnotations); CoreProcessorIT (both annotations leave no trace, with a foreign RUNTIME annotation as control). The dependency half of the scenario has no automated check yet

QS-N2

Compatibility / Co-existence

A small footprint on Java 17. When a test developer runs tests on Java 17 while the generated code and its runtime are on the test classpath, they run with no dependencies beyond Hamcrest or AssertJ, JSpecify and the project’s own runtime artifact; bytecode level 17 and this dependency list are verified on every build.

Maven Enforcer; Verify JDK 17

QS-N3

Compatibility / Co-existence

Generated code compiles without warnings. When a test developer compiles a project’s tests with -Xlint:all -Werror on any JDK from 17 to the latest, the generated sources compile; they produce no warning at all, including rawtypes, unchecked and deprecation warnings.

End-to-end modules compiled with -Xlint:all -Werror (-Xlint:all,-processing -Werror in the two modules that run the processor on test sources, see CLAUDE.md)

QS-U1

Interaction capability / User error protection

A misconfiguration is reported where it happens. When a test developer configures a name that cannot be resolved, or a type whose generated names clash (method names with inherited ones, AssertJ top level class names in one package, Kotlin DSL scope names), while compiling with javac or KAPT, the annotation processor reports one compiler error per problem at the annotation element and names the offending value; there is no stack trace, and a backend writes nothing for a clashing name, so no follow-up error appears in generated code.

Annotation processor integration tests

QS-U2

Interaction capability / Self-descriptiveness

A failure explains itself. When a test developer’s assertion with a generated Hamcrest matcher fails while the tests run in the IDE or on CI, the matcher’s mismatch message lists every failing check on its own line with property path, expected value and actual value (for example customer.address.city: expected "Berlin" but was "Paris"); the developer finds the wrong property without a debugger, and the messages match the golden messages of the tests.

Annotation processor integration tests and runtime tests with golden messages

QS-U3

Interaction capability / Learnability

The first green assertion comes from the README. When a developer new to the library copies the README snippet into a Maven or Gradle project on JDK 17 or later, the library generates an assertion that turns the developer’s first test green; this takes at most 10 minutes, and the snippet always compiles because CI keeps it in sync with an example module.

Example modules; README snippet sync check

QS-U4

Interaction capability / Operability

Kotlin properties feel native. When a Kotlin developer configures a class with properties such as val enabled: Boolean, val isX: Boolean? or other nullable properties while the processor runs under KAPT, the generated matchers offer withEnabled and withIsX with matching nullability; every such property gets its methods, and the Kotlin compiler reports no platform-type warning for them. Because JSpecify mismatches are errors in Kotlin by default, -Xjspecify-annotations=warn is the documented escape hatch.

Kotlin end-to-end modules

QS-B1

Compatibility / Interoperability

The same API with every build tool. When a test developer applies the same configuration with Maven (classpath discovery or annotationProcessorPaths), Gradle (annotationProcessor) or KAPT on any supported JDK, the annotation processor generates the same assertion API; all four example modules and the end-to-end modules build green on every JDK in the CI matrix.

Build; Verify JDK 17/21/25/latest

QS-B2

Flexibility / Adaptability

A new JDK shows up early. When a new JDK version reaches general availability while the project runs on autopilot, Renovate moves the CI matrix’s latest JDK to it without the owner, and the generator is built and tested on it; a break becomes visible on that CI run within 7 days of the release, before users report it.

Verify JDK latest (Renovate-managed)

QS-M1

Maintainability / Modifiability

A green major update needs no owner. When Renovate opens a major update of a dependency (such as Hamcrest, AssertJ, JUnit or a Maven plugin) while all required checks pass, the pull request is merged and released automatically; the owner spends no time on it.

Required checks; auto-merge; release-please

QS-M2

Maintainability / Modifiability

A new property kind stays local. When a contributor adds support for a new property kind (such as Optional) on the main branch, the change touches only the core property model, each backend’s rendering and their tests; no backend depends on another (apart from the documented Kotlin DSL exception), which every build checks.

Maven Enforcer; ArchUnit tests

QS-M3

Maintainability / Modularity

A new backend plugs in. When a contributor adds a third backend (such as one for Google Truth) to the main branch, it is a new module behind the backend SPI; no line changes in the core or in the existing backends.

ArchUnit tests; ADR 0001

QS-M4

Maintainability / Analysability

Code quality does not erode. When a contributor’s pull request introduces new issues, duplicated code or uncovered code while CI analyses it, the Sonar quality gate fails and blocks the merge; new code adds no issue.

Sonar

QS-X1

Compatibility / Co-existence

The generator works alongside other processors. When a test developer runs the generator in the same compilation as Lombok and other annotation processors, the annotation processor sees the accessors Lombok generates and leaves the other processors' output unchanged; the generated matchers cover every Lombok-generated getter, and the build reports no processor error.

hamcrest-matcher-generator-endtoend-lombok module, whose LombokGettersTest asserts the generated matchers against Lombok-generated getters

QS-X2

Flexibility / Replaceability

Upgrading from 5.x is a version bump. When a 5.x user raises the version to 6.0.0 in an existing project that builds on JDK 17 or later, the user’s tests compile and pass against the newly generated matchers; no line of test code changes (the bridge jars under the old coordinates cover the changed artifact, also for compiler plugins older than 3.12, which do not follow a relocation in the processor path).

The frozen 5.x suite, built as a consumer build against the reactor’s bridge jars (see chapter 8, Frozen 5.x suite); BridgeJarIT checks the bridge jars' shape

QS-X3

Security / Integrity, Authenticity

Releases are verifiable. When a user or their supply-chain tooling downloads a release from Maven Central, the artifacts' GPG signatures, the GitHub build-provenance attestation (gh attestation verify) and the CycloneDX SBOM all verify; and while a pull request adds a dependency with a known vulnerability, dependency review blocks its merge.

Release workflow (release.yml); Dependency review

QS-X4

Reliability / Fault tolerance

Unusual input never crashes the compiler. When a test developer configures types with generics, inner classes, no properties or cyclic references between types while compiling with javac or KAPT, the annotation processor generates matchers and warns about each configured type without properties; the compilation never ends with an uncaught exception from the processor.

Annotation processor integration tests

QS-X5

Performance efficiency / Time behaviour

Generation stays fast. When a test developer configures 200 types while compiling on a GitHub-hosted CI runner, the annotation processor adds at most 2 seconds to the compilation.

None: accepted risk, see chapter 11

11. Risks and Technical Debts

After 6.0.0 the project is meant to run on autopilot: bots update dependencies, merge green pull requests and publish releases, and the maintainer only steps in when something turns red. The risks below are the ways this can stall or go wrong; the technical debts are deliberate compromises the architecture carries.

Each entry is rated on two scales:

  • Probability that the risk occurs, or that the debt causes harm: low (1), medium (2) or high (3).

  • Impact if it does: tolerable (1, a nuisance that does not stop development or users), disabling (2, merges, releases or a feature stop until someone acts; recoverable) or catastrophic (3, harm that cannot be undone, such as a malicious release signed with the project’s key).

The list is ordered by priority, the product of both values, highest first; entries with the same product are ordered by urgency.

Risk or technical debt Probability Impact Description Measures

Risk: credentials expire

medium

disabling

The autopilot depends on four credentials: the SonarCloud token (a personal token with no fixed validity period, which SonarCloud removes after 60 days without use), the GPG signing key (three years), the Maven Central Portal token (from the legacy username/password account) and the private key of the GitHub App that creates release pull requests. When one expires or is revoked, either every merge stops (Sonar is a required check) or releases fail.

A weekly reminder workflow (credential-reminder.yml) validates the Sonar token and the Central token and reads the expiry of the GPG key and the Sonar token’s expirationDate (the earliest among the user’s tokens, because the API does not tell which one is SONAR_TOKEN); the Sonar token has no expiry date, so it raises no Sonar reminder, and the 60-day limit is covered by the CI runs and the weekly check that keep it in use; the Central token and the App key have their expiry dates in repository variables. 30 days before an expiry it opens or updates an issue that mentions the maintainer; the issue itself carries the renewal steps. The validation also catches a removed or revoked Sonar token, and CI runs keep it in use. Invalid credentials fail with an explicit message instead of an unclear red build. The maintainer runbook (MAINTAINING.md) lists every credential, where it is stored and how to renew it.

Risk: free external services change or fail

medium

disabling

The project relies on services that are free for open-source projects: GitHub Actions and Pages, SonarCloud, the Mend Renovate app, the Maven Central Portal and release-please. Any of them may change its terms, rate-limit (the free Renovate app already runs only every few hours) or be unavailable. Because Sonar is a required check, a SonarCloud outage blocks every merge.

Accepted. All required checks are the project’s own workflow jobs, not checks posted by the vendors, so a failing service shows up as a clearly named red job. During an outage the maintainer may merge past the ruleset. Fallbacks are known: Renovate can run self-hosted in GitHub Actions, and the Sonar job can be made non-blocking.

Risk: JDK internals break the newest JDK leg

medium

disabling

The annotation processor’s integration tests compile code in memory with Google Compile Testing, which needs --add-opens access to the JDK’s internal compiler packages. A future JDK may close that access or change the language model (javax.lang.model), for example for value classes. The Verify JDK latest check is required, so such a break blocks every merge, not only the JDK update itself.

Renovate moves the latest leg to each new JDK in its own pull request, so a break is visible within days of the release and attributable to the JDK (QS-B2). The fallback is to compile the test sources with the plain javax.tools compiler API instead of Compile Testing.

Risk: a single maintainer

medium

disabling

Every red pull request, credential renewal and baseline decision lands on one person. If the maintainer is unavailable, updates stop and users receive no fixes. Nothing broken gets merged, because red pull requests wait.

Accepted. The autopilot keeps routine work away from the maintainer. A maintainer runbook (MAINTAINING.md) gives the playbooks for these failures, so that anyone with admin rights, or an AI agent session, can act.

Risk: the Java baseline erodes again

medium

disabling

6.0.0 requires Java 17 (ADR 0005). Libraries and tools keep raising their own minimum; Structurizr and Error Prone already need Java 21. When a dependency on the users' classpath or a build tool needed on the JDK 17 leg requires a newer Java version, its update cannot be merged automatically.

The case shows up as one red Renovate pull request (Maven Enforcer’s bytecode check or the JDK 17 leg). The maintainer either caps that dependency in the Renovate configuration with a comment naming the reason, or raises the baseline in a new major version. Tools that are only needed for analysis or documentation run in the JDK 21+ profile and do not affect the baseline.

Risk: a compromised dependency is released automatically

low

catastrophic

Green pull requests are merged without a human, and releases are published without one. A malicious version of a dependency or a GitHub Action that passes every check would be released to Maven Central, signed with the project’s GPG key, and reach every user who updates.

Renovate waits until a version is at least three days old (minimumReleaseAge), so most withdrawn or compromised releases never get proposed. Actions are pinned by commit SHA, workflow permissions are minimal, dependency review blocks known vulnerabilities, and OpenSSF Scorecard reports the repository’s posture weekly. Every release carries a build-provenance attestation and an SBOM, so users can trace what was built from which commit, and every release notifies the maintainer.

Technical debt: the 5.x compatibility layer stays forever

high

tolerable

To let 5.x users upgrade by a version bump, 6.0.0 keeps the deprecated @MatcherConfiguration annotation as an alias, bridge jars for the changed artifacts and the 5.x method names such as resetFoo (ADR 0002). As 6.0.0 is the final feature release, there is no later major version that would remove them.

Accepted. The frozen 5.x suite (consumer build endtoend-5x-compatibility-runner, tree hash guarded) keeps the layer working (QS-X2); the deprecated alias is the only code in the old package.

Technical debt: the Kotlin DSL backend depends on the Hamcrest backend

high

tolerable

The architecture keeps backends independent of each other: each one depends only on the core. The Kotlin DSL backend is a deliberate exception. It builds on the Hamcrest backend and reuses its naming of the generated matchers (XMatcher, isX, withFoo, ignoringFoo). A change to that naming therefore breaks the generated Kotlin code, and the two modules cannot evolve independently (ADR 0004).

The exception is allowed for exactly this one dependency, and Maven Enforcer and an ArchUnit test reject any other dependency between backends. Golden-file tests of the generated Kotlin code and the Kotlin end-to-end modules fail as soon as the Hamcrest naming changes without the DSL following it.

Risk: a release builds with a wrong module version

low

disabling

The release type simple with one explicit extra-files entry per module pom (see chapter 7) was only verified by a simulation with the updaters of release-please 17.11.2, never by a real release-please run, because the development sandbox cannot call the GitHub API. The first real Release PR after this change is the first real test. A new module pom without an extra-files entry is caught by scripts/ci/check-project-version.py in the required job Domain docs in sync. It is assumed, not verified, that the checks also run on the Release PR, because the GitHub App token creates it.

scripts/ci/check-project-version.py runs on the Release PR and turns a wrong one red before the release. The maintainer looks at the first Release PR after the change before merging it: own and parent version 6.0.0 in every module pom, and the pom of the frozen 5.x suite still @project.version@. The playbook "Red Release pull request" in MAINTAINING.md applies.

Risk: the Kotlin DSL needs KAPT

low

disabling

The Kotlin DSL is only generated when the annotation processor runs under KAPT, the Kotlin compiler’s way of running Java annotation processors. JetBrains puts its new development into KSP, which is out of scope for this project. If a future Kotlin release dropped KAPT, the DSL could no longer be generated.

The DSL is optional: the generated Java matchers work from Kotlin without it, and they do not depend on KAPT. Renovate keeps the tested Kotlin version current, so a removal of KAPT shows up as a failing update instead of a surprise for users.

Risk: strict quality gates stop the autopilot

medium

tolerable

Merges require at least 99 % line and branch coverage. The CI jobs Mutation testing (a mutation score of at least 90 % per module) and Sonar (the quality gate) are required merge checks of the main ruleset, so a red one blocks the merge. An update of PIT or JaCoCo (new mutators, different branch counting) or a change of SonarCloud’s rules can turn a pull request or the main branch red although no project code changed.

PIT, JaCoCo and the Sonar scanner are updated in their own Renovate pull requests, so the cause of a red build is unambiguous. The Sonar quality gate judges new code only, so new rules do not fail old code. Thresholds are never lowered silently; a change goes to the maintainer.

Risk: a major update of a runtime dependency ships as a patch release

low

tolerable

Renovate merges major updates of the libraries on the users' classpath (Hamcrest, AssertJ, JSpecify) automatically, as fix(deps) commits, which release-please publishes as a patch release. Users therefore receive a new major version of such a library transitively with a patch release of this project, and an API change in it may break their own tests.

Accepted. The end-to-end modules exercise the generated code against the new version before it is released, Maven Enforcer rejects a dependency that needs a newer Java version, and the release notes generated by release-please name the dependency update. Users can pin the library version in their own build.

Risk: the generator’s run time is not measured

low

tolerable

Scenario QS-X5 asks that generating assertions for 200 configured types adds at most 2 seconds to a compilation. No check measures this, so a change that slows down the annotation processor would go unnoticed until users complain about slow builds.

The risk is accepted: projects configure far fewer types than that, and a run-time check on shared CI runners would produce unreliable results. Should users report slow builds, a benchmark module can be added later.

11.1. Accepted OpenSSF Scorecard findings

The OpenSSF Scorecard (.github/workflows/scorecard.yml) reports the repository’s posture; its score does not gate merges. The table below lists every Scorecard check that was accepted instead of fixed, with the reason and the owner’s decision. It is separate from the table above, because a Scorecard check measures a practice and not a risk that occurs, so the probability and impact scales do not apply. The entries rest on the Scorecard API result of 2026-10-31 (analysed commit 95d1727, Scorecard v5.6.0, total score 7.7) and on the ruleset as the owner left it. The date is the owner’s decision where one is recorded, otherwise the date of that result.

Fuzzing (score 0 on 2026-10-31) has no entry: the project has a Jazzer fuzz test (ItemsInAnyOrderFuzzTest), and an entry is written only if a weekly Scorecard run after it still reports 0.

CII-Best-Practices (score 0) has no entry yet: the badge registration (issue #284) is still open. It gets an entry only after the owner has decided the unmet criteria of the passing level.

Check Score Reason Owner decision Date

Code-Review

0

Scorecard counts 0 of 29 changesets as approved by a reviewer. The project has one maintainer, and pull requests the maintainer merges are never counted as reviewed under any setup. An automatic bot approval was rejected, because it would show a review practice that does not exist.

Accepted (#285). The main ruleset keeps 0 required approvals, see Branch-Protection.

2026-11-01

Branch-Protection

3

Scorecard warns that main requires no approvers and no code owner review, dismisses no stale reviews, and does not apply its rules to administrators, among others. The ruleset Development (Trunk) requires 0 approvals and lets administrators bypass it, so auto-merge can merge a green pull request without a human review. Requiring one approval would make auto-merge wait for a review the single maintainer cannot give, for an expected gain of 3 to 4 points. The owner judged that not worth the auto-merge risk; the experiments E1 and E2 were not run.

Accepted (#285). The ruleset is unchanged.

2026-11-01

Binary-Artifacts

9

The Gradle wrapper jar of the Gradle example, example-gradle-java/gradle/wrapper/gradle-wrapper.jar. The CI job Example Gradle (#270) validates it with the SHA-pinned wrapper validation action, and the wrapper distribution is pinned by its checksum.

Accepted (spec #104 plan).

2026-10-31 (Scorecard result)

Token-Permissions

10

Warning: contents: write in the job Publish to Maven Central (.github/workflows/release.yml, line 96). The job creates the GitHub Release if release-please did not, so it needs write access to the repository contents. No other job asks for write access to the contents. The score stays 10.

Accepted (spec #104 plan).

2026-10-31 (Scorecard result)

Contributors

6

Scorecard counts the contributing organizations: 2 companies or organizations. The project cannot influence this.

Accepted (spec #104 plan).

2026-10-31 (Scorecard result)

12. Glossary

Term Definition

Property model

The description of a configured type and its properties that does not depend on any assertion library. All backends share it. In code it consists of TypeModel (a whole type with its nested types), PropertyModel (one property) and TypeModel.Kind (whether the type is a plain POJO or a RECORD).

Nullability

Whether a property may be null: NULLABLE, NON_NULL or UNSPECIFIED. The core derives it from the accessor: primitives are non null, JSpecify and JetBrains (Kotlin) annotations decide explicitly, and otherwise the closest @NullMarked or @NullUnmarked scope does. A type variable stays UNSPECIFIED. Generated Hamcrest code is @NullMarked with @Nullable on equality values and nullable property types, only when JSpecify resolves at compile time. Kotlin reports mismatches as errors; -Xjspecify-annotations=warn lowers them to warnings.

Property

A readable value of a bean (via its getter) or a record (via its component accessor) that generated assertions can check. Every generated matcher also has class (from getClass()) as a property, so withClass(…​) and ignoringClass() are generated, as in 5.x, but the completeness check does not count class. Enums also get declaringClass; whether the completeness check should count it is still undecided.

Backend

A generator that turns the property model into assertion code for one specific assertion library, such as Hamcrest or AssertJ.

Assertion class

The pair of classes the AssertJ backend generates per configured type: AbstractXAssert<SELF, ACTUAL> on AbstractObjectAssert, which holds the generated assertions, and the concrete, non-final XAssert with a public XAssert(X) constructor and a static assertThat(X). Neither is named Assertions or SoftAssertions, and none extends org.assertj.core.api.Assertions, so assertThat(String) stays unambiguous.

Soft assertions mixin

The interface the AssertJ backend generates per target package, named SoftAssertionsProvider, with a default assertThat(X) and a default then(X) for every generated type of the package. Mixed into a SoftAssertions or BDDSoftAssertions subclass, it makes the generated assertions soft and BDD assertions. The mixin is written once per package and covers only the types generated in the same processing round, whichever configuration asked for them; a type from another round or compilation is not in it, which the processor reports (see arc42 chapter 6, "Files written once per package", and ADR 0006).

Generated representation

The class the AssertJ backend generates per target package, named PropertyRepresentation: an AssertJ StandardRepresentation that prints an instance of each configured type by its properties, such as Line[sku="A-1", quantity=2], nested types included, and formats every other value as AssertJ does. The user registers it with Assertions.useRepresentation; it is never registered automatically. Like the mixin, it is written once per package and covers only types generated in the same processing round; a later round gets the processor’s "not written again" warning, but a second compilation gets no class-path note for it (only the mixin does).

Bridge jar

The code-free jar that a 5.x artifact (hamcrest-matcher-generator-dependencies, hamcrest-matcher-generator-annotationprocessor) is published as in 6.0.0. It depends on hamcrest-matcher-generator, so old coordinates still resolve to a jar and the Hamcrest backend arrives transitively. A relocation POM is not used because older compiler plugins do not follow it in a processor path.

Configuration

The single annotation-based declaration of the types for which assertions are generated. It is the same for every backend.

Reference object

An instance of a configured type whose property values are used as the expected values for all of its properties, so that a whole object can be compared in one step with isXLike(reference). The comparison is deep for properties whose type has a generated matcher (also in lists, other collections, arrays and maps) and uses equals for the rest, including enums and types without properties. It turns the completeness check on, withFoo, resetFoo and ignoringFoo override single properties, and a cyclic object graph ends because a pair met again counts as equal. Deep comparison applies only to types whose matcher is generated in the same processing round; a type from another round or compilation is compared by equals, and -Amatchergenerator.verbose prints a note per property that names the type (see arc42 chapter 6, "Files written once per package"). A back-reference to the root reference object shares the root’s matcher, so an override on the root applies there too.

Property path

The route from the asserted object to the value that was checked, written with dots between property names, [index] for list items and [key] for map entries. Hamcrest mismatch messages use it to say exactly which value differs, for example customer.address.city or lines[1].quantity.

Assertion path

The AssertJ counterpart of the property path: the path of an AssertJ failure, rooted at the asserted type (Order.customer.address.city). The generated assertion of the value a hasFooSatisfying requirement is run for continues the path of the enclosing property, carried by the runtime class AssertionPath. An assertion for any other object created inside the requirement (OrderAssert.assertThat(other)) starts at its own type, because the path is bound to the value by identity, not to everything created in the lambda. Known limits of the identity match: an unrelated assertion on null inside a requirement whose value is null inherits the path, and the same instance behind two properties reports the path of the property whose requirement is running. Inside an array, collection or map the path restarts at the element type (OrderLine.sku). Navigation with extracting(.., as(X)) or first(as(X)) starts a new path at the navigated type (Customer.name), while hasFooSatisfying keeps the root path (Order.customer.name). A missing asserted object reads Expecting Order.customer not to be null.

Generated bean matcher

A generated Hamcrest <Type>Matcher, identified at runtime by the interface GeneratedBeanMatcher. When one is used as the check of a property of another, the runtime flattens it into property paths instead of treating it like a foreign matcher.

Collection matcher

A runtime matcher for a collection or map property, created by itemsInOrder(…​), itemsInAnyOrder(…​) or entries(…​) in CollectionMatchers. Mismatches name the size, the path of every failing item or entry, and missing, closest and unexpected items or keys. Arrays count as lists. With the completeness check, itemsInAnyOrder(…​) assigns items by their value checks and reports the properties neither checked nor ignored of an assigned item with its position, as in contacts[1].phone.

One-line rendering

The form in which a generated matcher describes itself and its mismatch, OrderLine {sku "X-9", quantity <3>}, while it is held by one of Hamcrest’s own matchers such as contains(…​). Outside such matchers it renders as a block with one failing check per line.

Completeness check

The check that every property of a type is either checked or consciously ignored. It makes sure that a property added later cannot go unchecked without anyone noticing. Opt-in per assertion. A property counts as addressed by a check (withFoo in Hamcrest, hasFoo, hasFooSatisfying, isFoo, isNotFoo in AssertJ) or by ignoringFoo(), which both backends generate. Hamcrest runs the check with checkingAllProperties(); AssertJ runs it with the scope satisfiesCheckingAllProperties(..) or the closing call hasAllPropertiesChecked(). The scope form counts only the calls its requirements make, while the closing call counts the whole chain. Value mismatches fail first, and the check then names the properties that are neither checked nor ignored. Navigation with extracting(.., as(X)) or first(as(X)) yields a new assertion for a new subject, which has its own set of addressed properties: checks made before the navigation do not count for it, and a hasAllPropertiesChecked() after the navigation covers only the navigated type.

DSL scope

The Kotlin receiver block for one configured type (isOrder { … }), in which only that type’s property checks can be written; a property of another configured type opens a nested DSL scope (customer { … }). Its class and entry function are named by the Kotlin DSL naming rule in arc42 chapter 5 (building block view), which also says what happens when two names still collide.

Generated code

The source files that the processor writes into the user’s project at compile time: matchers, assertions and Kotlin DSL files. They belong to the user and are not covered by the project’s license.

Runtime library

The published artifact that generated code calls while the user’s tests run, such as the Hamcrest BeanPropertyMatcher. Like every other artifact of the project, it is under the project’s license (Apache-2.0).