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, @JBossLog or @Flogger that never references the generated log field. Any reference counts as usage, including fluent calls (log.atDebug()), log.isDebugEnabled(), passing log as an argument, and use from lambdas or nested classes. Lombok generates log as a private field, so only the declaring file is checked.

  • Unused private logger - a hand-declared private Logger field that is never referenced in its file.

  • Unused builder - a class or record annotated with @Builder or @SuperBuilder whose 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, @With and 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.

  • @ToString and @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 custom builderMethodName, belong to an abstract @SuperBuilder class or a @SuperBuilder parent 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

Lombok logger that is never used
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
    }
}
Private logger that is never used
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();
    }
}
Builder that is never used
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

Lombok logger in use
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();
    }
}
Private logger in use
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();
    }
}
Builder in use
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();

Configuration

This rule has no configuration.