Lombok Annotation Validation Rule (SL026)
Reports Lombok annotations and private loggers whose generated members are never used.
Description
Scans the service’s own Java sources (every module, excluding generated code under target/) and reports annotations whose generated code is demonstrably unused. The rule is advisory: every finding is INFO and never fails the build.
What is flagged
-
Unused Lombok logger - a class annotated with
@Slf4j,@XSlf4j,@CommonsLog,@Log,@Log4j,@Log4j2,@JBossLogor@Floggerthat never references the generatedlogfield. Any reference counts as usage, including fluent calls (log.atDebug()),log.isDebugEnabled(), passinglogas an argument, and use from lambdas or nested classes. Lombok generateslogas a private field, so only the declaring file is checked. -
Unused private logger - a hand-declared
privateLoggerfield that is never referenced in its file. -
Unused builder - a class or record annotated with
@Builderor@SuperBuilderwhose builder is never used. Declarations come from main sources; usages are searched across main and test sources of all modules, because a builder used only by tests is still needed to compile them.
What is deliberately not flagged
The rule has no parser, so it prefers missing a smell over reporting a false one. These are skipped:
-
@Getter,@Setter,@Data,@Value,@Withand the constructor annotations - their members are routinely used reflectively (Jackson, Spring binding, JPA, MapStruct) or through method references, so a missing call site does not prove they are unused. -
@ToStringand@EqualsAndHashCode- invoked implicitly by string concatenation, collections and logging. -
Non-private logger fields, which may be used by subclasses or other classes.
-
Builders that are consumed reflectively (
@Jacksonized,@JsonDeserialize), use a custombuilderMethodName, belong to an abstract@SuperBuilderclass or a@SuperBuilderparent that has subclasses, or are declared on a constructor or method.
How to fix
Remove the annotation (or the unused logger field), or start using what it generates. To silence the rule, disable it by name (LombokAnnotationValidationRule) in the service-lint config, with disabledRules in the Maven plugin or --disable-rules on the CLI, or exempt paths with its ignored-module globs. There are no per-detection switches and no inline suppression marker.
Rationale
Annotations whose generated code is never used are misleading boilerplate: a @Slf4j class that never logs, or a builder nothing calls, suggests behaviour the class does not have.
Findings
-
INFO
-
A class has a Lombok logging annotation but never references the generated 'log' field.
-
A class declares a private Logger field that is never referenced.
-
A class has @Builder or @SuperBuilder but its builder is not used in any main or test source of the service.
-
Noncompliant
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class OrderService {
public void process(final Order order) {
order.markProcessed(); // 'log' is never used, so @Slf4j is dead boilerplate
}
}
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class OrderService {
// declared but never referenced
private static final Logger LOG = LoggerFactory.getLogger(OrderService.class);
public void process(final Order order) {
order.markProcessed();
}
}
import lombok.Builder;
// No main or test source in the service ever calls OrderSummary.builder()
@Builder
public class OrderSummary {
private final String orderId;
private final int itemCount;
}
Compliant
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class OrderService {
public void process(final Order order) {
log.atDebug().log(() -> "Processing order %s".formatted(order.getId()));
order.markProcessed();
}
}
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class OrderService {
private static final Logger LOG = LoggerFactory.getLogger(OrderService.class);
public void process(final Order order) {
LOG.debug("Processing order {}", order.getId());
order.markProcessed();
}
}
import lombok.Builder;
@Builder
public class OrderSummary {
private final String orderId;
private final int itemCount;
}
// elsewhere in main or test sources
OrderSummary summary = OrderSummary.builder()
.orderId("123")
.itemCount(2)
.build();