Skip to content

[Lombok] Bug fixes part 2 - #7624

Open
Ivan Kochurkin (KvanTTT) wants to merge 16 commits into
masterfrom
ikochurkin/lombok-bug-fixes-2
Open

[Lombok] Bug fixes part 2#7624
Ivan Kochurkin (KvanTTT) wants to merge 16 commits into
masterfrom
ikochurkin/lombok-bug-fixes-2

Conversation

@KvanTTT

Copy link
Copy Markdown
Contributor

No description provided.

An interface holds no state to generate from and no constructor to
generate, and an annotation class can hold no member at all, so
`checkLombokAnnotations` reports `ANNOTATION_HAS_NO_EFFECT` for both.
Only `@ToString` and `@EqualsAndHashCode` acted on that, though; the
rest generated anyway, and not just uselessly:

 - `@Log` put a companion object holding `log` into either kind, which
   made the warning a lie - the logger was there and callable.
 - `@NoArgsConstructor` put a `constructor()` into an interface, which
   the backend refuses outright ("Interface must not have
   constructors").
 - `@Builder` generated a companion object, a `builder()` factory and a
   builder class whose `build()` had no constructor to call, crashing
   IR generation on an interface with "Sequence is empty.".

Extract the condition `ToStringGenerator`/`EqualsAndHashCodeGenerator`
already duplicated into `isUnsupportedLombokTarget`, and consult it at
the one choke point of each remaining generator: `isCompanionNeeded`,
shared by all three annotations that generate a companion object;
`LoggerGenerator` for the `log` property itself, which a user-declared
companion or `lombok.log.fieldIsStatic=false` reaches without going
through companion generation; `addIfNonClashing` for the constructor and
its static factory; and `extractBuilderWithDeclarations`, the funnel
every builder entry point goes through.

The two generators that take a companion object into account consult the
annotated class rather than the symbol being generated for, since for a
static member the latter is the companion object, which is neither an
interface nor an annotation class.

`@Builder` on a member function of an interface is deliberately left
alone: it builds whatever that function returns and is a legitimate
case, so only the class-level annotation is dropped.

That leaves `@Builder` a silent no-op on an interface and on an
annotation class: nothing is generated any more, but nothing is reported
either, because it allows the broad `KotlinTarget.CLASS` rather than
`CLASS_ONLY`. Narrowing that would also start warning about `@Builder`
on an enum and on an object, so it is postponed to a separate commit and
recorded as a TODO for now.

^KT-87871 Fixed
^KT-88701 Fixed

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tead of `CLASS`

It prevents silent no-ops on unsupported Kotlin declarations
such as annotation classes, objects, enums, and interfaces.
@kotlin-safemerge

kotlin-safemerge Bot commented Aug 20, 2026

Copy link
Copy Markdown

Code Owners

RuleOwnersApproval
/​compiler/​frontend.​common-​psi/​, /​plugins/​lombok/​
kotlin-frontend

bnorm
PR commands for maintainers
CommandDescriptionParameters
/safe-mergeRebase-merges with automatic fixup commit squashing--fixup Autosquash fixup commits (on by default)
/safe-squash-mergeSquash-merges with optional commit title/body override--title Title of the squashed commit
--message Body of the squashed commit
/dry-runRuns the test pipeline with changes rebased on latest master--retry Retry the CI run on failure
/test-publicTriggers the public test suite without rebasing on latest master
/test-privateTriggers the private test suite without rebasing on latest master
/codeownersTriggers code owners check and comment update
/fixupSquashes fixup commits and force pushes the branch
/cancel-coordinatorCancels the merge coordinator currently running for this branch

`java.lang.Enum` declares `equals` and `hashCode` final, so the ones
`@EqualsAndHashCode` generated didn't merely misbehave - the enum failed
verification and never loaded:

    java.lang.VerifyError: class Color overrides final method
    java.lang.Enum.equals(Ljava/lang/Object;)Z

Drop `KotlinTarget.ENUM_CLASS` from the annotation's allowed targets so
`ANNOTATION_HAS_NO_EFFECT` is reported, and stop the generator at the
same class kinds it already skips. This is where Lombok itself draws the
line: `HandleEqualsAndHashCode` checks `isClass`, which excludes an
interface, an enum, an annotation class and a record, and reports
"@EqualsAndHashCode is only supported on a class.". `@ToString` keeps
`ENUM_CLASS`, matching Lombok's `isClassOrEnum` check for it - only
`toString` is non-final in `Enum`.

`@Builder` needed the generator half of the same treatment. Reporting
alone left it a warning that lies, since a builder was still generated
and its `build()` called a constructor signature that doesn't exist - an
enum constructor takes the synthetic name and ordinal parameters - so it
failed with `NoSuchMethodError` at run time. Both annotations now share
`isUnsupportedLombokTargetOrEnumClass`, an enum class on top of
`isUnsupportedLombokTarget`; the predicate stays separate because `@Log`
and `@ToString` support an enum fine.

The box test that reproduced the `VerifyError` no longer needs
`IGNORE_BACKEND_K2`, and asserts instead that the enum keeps the
identity comparison it inherits.

^KT-88507 Fixed
^KT-87871

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An object is a single instance, so the identity comparison it already
inherits is exactly what a generated `equals` amounts to, and a
generated `hashCode` over its properties only weakens it - two calls on
the one instance must agree, which identity gives for free. Lombok draws
the same line: `HandleEqualsAndHashCode` checks `isClass`, and an object
has no Java counterpart that check would accept.

Drop `KotlinTarget.OBJECT` from the annotation's allowed targets, so
both a standalone and a companion object are reported as
`ANNOTATION_HAS_NO_EFFECT`, and generate nothing for either.

With this, `@Builder` and `@EqualsAndHashCode` accept exactly one class
kind between them, so replace `isUnsupportedLombokTargetOrEnumClass` -
which had grown a name that listed its members rather than describing
them - with `isPlainClass`, a single `classKind == ClassKind.CLASS`.
That is precisely Lombok's own `isClass`, and it reads as the
requirement it is at both use sites. A local class stays a plain class:
it is supported by `@EqualsAndHashCode`, and for `@Builder` it is
`isCompanionNeeded` that rules it out, since a local class can't hold
the companion object a `builder()` needs.

For `@Builder` this also drops the nested builder class that an
annotated object still got. Nothing could reach it - its constructor was
private and no `builder()` factory was generated, an object never
getting a companion object - so this removes a declaration rather than
changing what any code could call.

^KT-88614 Fixed

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`lombok.log.fieldIsStatic` alone decides whether the logger is static,
so putting a log annotation on a companion object rather than on the
class holding it buys nothing - and with `fieldIsStatic=false` it did
nothing whatsoever: `LoggerGenerator` dropped the annotation and no
checker said a word about it.

Replace `KotlinTarget.OBJECT` with `KotlinTarget.STANDALONE_OBJECT` in
the allowed targets shared by all eight log annotations. `OBJECT` is
the umbrella target that covers a companion object as well, and
`COMPANION_OBJECT_LIST` and `OBJECT_LIST` differ precisely in their
first entry, so the narrower target excludes a companion object while
keeping a standalone one. `checkLombokAnnotations` consults no config,
so `ANNOTATION_HAS_NO_EFFECT` is reported for either `fieldIsStatic`
value - the same move that already prohibited `@Builder` and
`@EqualsAndHashCode` on the kinds they generate nothing for.

`LoggerGenerator` no longer looks at a log annotation on the companion
object itself, only at the one on the class around it, so nothing is
generated and the warning is not a lie. When both carry `@Log`, the
one on the class keeps working: an inert annotation must not suppress
one that has an effect.

`FirLombokLogChecker` returns early for a companion object, which
would otherwise be reported `LOG_PROPERTY_ALREADY_EXISTS` for a logger
that is never generated. The logger a companion object does hold,
generated from the annotation on its class, is still checked when that
class is visited, `loggerContainer` being its companion object then.

The box test keeps the annotated companion object, without the `log`
usages it can no longer resolve, so codegen is still exercised. The
diagnostics test for `fieldIsStatic=false` gains the companion object
case it lacked, which is the reproducer of the issue.

^KT-88288 Fixed

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… to some Lombok diagnostics

* `ANNOTATION_IS_NOT_SUPPORTED`
* `ANNOTATION_ARGUMENT_IS_NOT_SUPPORTED`
* `ANNOTATION_IS_NOT_SUPPORTED`

^KT-88336 Fixed
^KT-88644 Fixed
… generator

They are irrelevant or soon-to-be-deprecated in Lombok

No new test data added because it's already covered by `diagnostics/kotlin/equalsAndHashCode.kt`

^KT-88620 Fixed
They are irrelevant or soon-to-be-deprecated in Lombok
@KvanTTT
Ivan Kochurkin (KvanTTT) force-pushed the ikochurkin/lombok-bug-fixes-2 branch from 67e60dc to c03a54e Compare August 20, 2026 21:43
to support subtyping correctly

^KT-54072 Fixed

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A value class *is* its underlying value: there is no instance to
initialize field by field, and its constructors compile to static
`constructor-impl` functions that must return that value. The generated
constructor only called the superclass one, so `JvmInlineClassLowering`
left its `INSTANCE_INITIALIZER_CALL` in place and the JVM backend failed
with "Unexpected IR element found during code generation".

`KotlinTarget` sees `@JvmInline value class` as a plain `CLASS`, so the
`CLASS_ONLY` entry matched and the annotation was accepted. A value
class cannot be expressed as a target at all, so
`ImplementedAnnotationsInfo` states it separately with
`isSupportedOnValueClass` and `ANNOTATION_HAS_NO_EFFECT` renders "value
class" as the ineffective target. `AbstractConstructorGeneratorPart`
bails out for one too, keeping the checker and the generator in
agreement; the guard is deliberately not folded into
`isUnsupportedLombokTarget`, which is shared with `@Log` and `@ToString`
- both of which do work on a value class.

Generating `C()` as `C(0)` was considered instead, but it needs a
`this(...)`-delegating constructor rather than the super-call shape the
generator builds, and `force`'s null-for-reference-types rule is unsound
for a non-null Kotlin property. Lombok itself rejects
`@NoArgsConstructor` on records for the same reason.

^KT-88705 Fixed

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant