Three Java annotations with matching IntelliJ IDEA tooling - covering static resource-path validation, an extended @Contract grammar, and a full-featured builder generator with runtime validation.
Important
@ClassBuilder uses javac AST mutation and requires javac (ecj is not supported). The processor opens jdk.compiler internals automatically at load time via sun.misc.Unsafe + MethodHandles.Lookup.IMPL_LOOKUP (same technique Lombok uses), so no --add-exports flags are needed in consumer builds. @ResourcePath and @XContract have no compiler dependency and work on any build.
@ResourcePath- validates that string expressions at annotated sites resolve to files that exist in the project's source or resource roots. Supports an optionalbasedirectory prefix and a caller-side inspection that catchesbasemismatches across method boundaries.@XContract- a superset of JetBrains@Contractwith relational comparisons,&&/||grouping, named-parameter references,instanceofchecks, typedthrowsreturns, and chained comparisons. A synthetic@Contractis inferred so IntelliJ's data-flow analysis works from a single annotation.@ClassBuilder- generates apublic static class Buildervia javac AST mutation, covering classes, records, and interfaces. Full Lombok@Builderparity plus richer setter shapes:- Boolean zero-arg + typed pair with
@Negateinverse Optional<T>dual setters (raw nullable + wrapped)@Collectorvarargs/iterable bulk overloads with opt-in single-element add/put, clear, and lazy put-if-absent@Formattable@PrintFormatstring overload@BuildRule(retainInit = true)carries field initializers (UUID.randomUUID(),List.of(...), etc.) into the builder as defaults evaluated fresh perbuild()@BuildRule(flag = @BuildFlag(...))runtime validator enforcingnonNull/notEmpty/group/pattern/limitin the generatedbuild()
- Boolean zero-arg + typed pair with
Gradle (Kotlin DSL)
dependencies {
implementation("dev.sbs:simplified-annotations:1.2.0")
annotationProcessor("dev.sbs:simplified-annotations:1.2.0")
}Gradle (Groovy DSL)
dependencies {
implementation 'dev.sbs:simplified-annotations:1.2.0'
annotationProcessor 'dev.sbs:simplified-annotations:1.2.0'
}Maven
<dependency>
<groupId>dev.sbs</groupId>
<artifactId>simplified-annotations</artifactId>
<version>1.2.0</version>
</dependency>For annotation-processor registration on Maven, add the same coordinate under <annotationProcessorPaths> in the maven-compiler-plugin configuration.
Note
Published to Maven Central as dev.sbs:simplified-annotations and to JetBrains Marketplace as plugin ID dev.sbs.simplified-annotations.
Settings > Plugins > Marketplace > search Simplified Annotations.
The plugin hosts every inspection, quick-fix, gutter marker, and the editor-side Builder synthesis. It is optional at build time but strongly recommended while developing - autocompletion, goto-symbol, and type resolution for generated builder methods all work before the first javac round.
| JDK | Status |
|---|---|
| 17, 21, 25 | Tested on every commit |
| 18-20, 22-24 | Inherit the JDK 17 shim; expected to work but not in the CI matrix |
import dev.sbs.annotation.ResourcePath;
public class Assets {
@ResourcePath
static final String LOGO = "images/logo.png"; // checked at edit time
@ResourcePath(base = "shaders")
static final String VERTEX = "sprite.vert"; // resolves to shaders/sprite.vert
}The inspection reports an error if the resolved path does not exist in any source or resource root. A caller-side inspection additionally flags arguments passed into resource-loading sinks (Class.getResourceAsStream, etc.) when the callee parameter carries @ResourcePath with a mismatched base.
import dev.sbs.annotation.XContract;
@XContract("index >= 0 && index < size -> !null; _ -> fail")
public Node get(int index) { ... }
@XContract(value = "paramName != null -> this", mutates = "this")
public Builder name(String paramName) { ... }The grammar supports relational comparisons (<, <=, ==, !=, >=, >), logical && / || with grouping, chained comparisons (0 < index < size), named-parameter references, instanceof, and typed throws returns. IntelliJ's data-flow analysis sees the translatable subset via a synthetic @Contract; the richer clauses are enforced by this plugin's own inspections.
import dev.sbs.annotation.*;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@ClassBuilder
public class Pizza {
@BuildRule(retainInit = true) UUID id = UUID.randomUUID();
@BuildRule(flag = @BuildFlag(nonNull = true)) String name;
@Collector(singular = true, clearable = true) List<String> toppings;
@Formattable Optional<String> description;
@Negate("vegetarian") boolean containsMeat;
}Generates a Pizza.Builder with:
id(UUID)- defaults to a freshUUID.randomUUID()evaluated on everybuild()(carried forward from the field initializer)name(String)- chained@BuildFlagenforcement atbuild()timetoppings(String...),toppings(Iterable<String>),addTopping(String),clearToppings()description(String),description(Optional<String>),description(String fmt, Object... args)with null-safeString.formatisContainsMeat(),isContainsMeat(boolean),isVegetarian(),isVegetarian(boolean)(booleans always useisprefix)
Plus bootstrap methods on Pizza itself: static Pizza.Builder builder(), static Pizza.Builder from(Pizza), and Pizza.Builder mutate().
Note
@BuildRule(retainInit = true) evaluates the field initializer fresh per builder instance - UUID.randomUUID() produces a new UUID each time, new ArrayList<>() produces a fresh list. Any expression valid in the target class's scope is supported (constructor calls, factory methods, static method invocations, ternaries, etc.).
For abstract classes, @ClassBuilder produces a self-typed Builder<T, B> that concrete subclasses inherit with class Builder extends Super.Builder<Sub, Sub.Builder>; self() and build() are abstract on the root and overridden per subclass. This mirrors Lombok's @SuperBuilder with no runtime dependency.
Records, interfaces, and plain classes are all supported. For interfaces, the processor writes a sibling <Name>Impl.java in addition to <Name>Builder.java since there is no in-source mutation surface on an interface body.
| Attribute | Type | Default | Description |
|---|---|---|---|
builderName |
String |
"Builder" |
Simple name of the generated builder class |
builderMethodName |
String |
"builder" |
Static factory method returning a fresh builder |
buildMethodName |
String |
"build" |
Terminal method on the builder |
fromMethodName |
String |
"from" |
Static copy-factory seeding a builder from an existing instance |
toBuilderMethodName |
String |
"mutate" |
Instance method returning a pre-seeded builder |
methodPrefix |
String |
"" |
Setter method prefix (booleans always use is) |
access |
AccessLevel |
PUBLIC |
Access level of generated bootstrap methods and builder class |
validate |
boolean |
true |
Whether build() calls BuildFlagValidator.validate(target) |
emitContracts |
boolean |
true |
Whether to emit @XContract annotations on generated methods |
generateBuilder |
boolean |
true |
Whether to emit the static builder() factory |
generateFrom |
boolean |
true |
Whether to emit the static copy factory |
generateMutate |
boolean |
true |
Whether to emit the instance mutate() method |
generateImpl |
boolean |
true |
Interface targets only: whether to generate <Name>Impl |
factoryMethod |
String |
"" |
Static factory method build() delegates to instead of new |
exclude |
String[] |
{} |
Field names to exclude from the builder |
| Attribute | Type | Default | Description |
|---|---|---|---|
retainInit |
boolean |
false |
Carry the field's declared initializer into the builder as a per-build default |
ignore |
boolean |
false |
Exclude this field from builder synthesis entirely |
flag |
@BuildFlag |
@BuildFlag |
Runtime validation constraints (see below) |
obtainVia |
@ObtainVia |
@ObtainVia |
Override how from(T) / mutate() reads this field |
| Attribute | Type | Default | Description |
|---|---|---|---|
nonNull |
boolean |
false |
Field must not be null at build() time |
notEmpty |
boolean |
false |
String/Collection/Map/Optional/array must not be empty |
pattern |
String |
"" |
Regex the field value must match (CharSequence / Optional<String>) |
limit |
int |
-1 |
Maximum length/size (String/Collection/Map/array/Optional) |
group |
String[] |
{} |
At-least-one-of group: all members null/empty throws |
| Attribute | Type | Default | Description |
|---|---|---|---|
method |
String |
"" |
Instance method to call instead of the standard getter |
field |
String |
"" |
Alternate field name to read |
isStatic |
boolean |
false |
Whether method is a static Type.method(instance) helper |
| Annotation | Target | Description |
|---|---|---|
@Collector |
Collection, List, Set, Map |
Emits varargs + Iterable bulk setters; opt-in singular, clearable, compute (maps: putIfAbsent(K, Supplier<V>)) |
@Negate("inverse") |
boolean |
Emits an inverse setter pair (isInverse() / isInverse(boolean)) alongside the direct pair |
@Formattable |
String, Optional<String> |
Emits a @PrintFormat overload (withField(String fmt, Object... args)) with null-safe String.format |
Full attribute reference lives on the annotation Javadocs in library/src/main/java/dev/sbs/annotation/. Architectural notes for contributors are in CLAUDE.md.
This project is licensed under the Apache License 2.0 - see LICENSE for the full text.