Skip to content

Add basic convert class to record and record to class hints - #9800

Open
mbien wants to merge 3 commits into
apache:masterfrom
mbien:convert-record
Open

mbien wants to merge 3 commits into
apache:masterfrom
mbien:convert-record

Conversation

@mbien

@mbien mbien commented Nov 12, 2026 •

Copy link
Copy Markdown
Member

first commit as preparation:

  • Add TreeMaker.Record and record support to pretty printer and diff engine
  • this might unlock more fixes now, e.g. thinking on the code generator for class files when src is not available

second commit:

  • Add Class To Record and Record To Class hints
  • both hints are kept minimal. They don't check field usage or things like that
  • class -> record is a global hint by default, record -> class is current-line only

Assisted-by: qwen3.8-27b

fixes #7762

@mbien mbien added this to the NB33 milestone Nov 12, 2026
@mbien
mbien requested a review from lahodaj November 12, 2026 17:00
@mbien mbien added Java [ci] enable extra Java tests (java.completion, java.source.base, java.hints, refactoring.java, form) hints ci:dev-build [ci] produce a dev-build zip artifact (7 days expiration, see link on workflow summary page) labels Nov 12, 2026
@mbien mbien changed the title Convert record Add basic convert class to record and record to class hints Nov 12, 2026
import org.openide.util.NbBundle.Messages;

/**
* Suggests converting a record declaration into a class.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • I am glad to see this "backwards conversion"
  • often I start with record and then need to "open up"
  • this kind of conversion will be useful

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is a chance that java will have compact constructors for regular classes too one day. Transition would be more fluent then.

* The record components become {@code private final} fields, the record's
* implicit finality is preserved by a {@code final} class modifier, and an
* explicit canonical constructor is generated when the record does not
* declare one.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Record is more, I guess:

  • there is a special java.io.Serializable support (usually I don't care)
    • who needs Java serialization these days? Maybe Enso ;-?
    • moreover it is probably quite complicated to implement
  • there is special handling of equals and hashCode (I guess I do care often)
  • there is default toString() implementation (useful I

Shall the conversion add compatible equals, hashCode and toString implementations?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It certainly is part of the convenience of a record to have good automated impls for hash/identity/tostring. I thought about removing those in the class -> record direction at the very least. But this might be overreach if the class has well thought out methods of that kind already. The other direction could call the pre-existing generator actions.. but dev can do that too in a second pass so nothing is really lost.

This is already 1.4k lines for the minimal impl, smarter solutions often get complex quick. Maybe it is better to use this a release or two first before doing a followup. (when something is too-smart, it can also lead to bad UX since it becomes unpredictable. e.g "why did it not remove the toString here when it removed it over there").

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i actually thought about a second entry point for the action. "force record conversion". That one would lead to error badges in many cases. E.g remove default constructors, boilerplate methods and things like that.

@lahodaj

lahodaj commented Nov 13, 2026

Copy link
Copy Markdown
Contributor

First, thank you for looking into this!

Regarding the class -> record conversion, unless I misread the code, it will convert classes with mutable fields, but it won't convert classes that have any explicit constructor. To me, personally, it feels like a class that is conceptually very far from a record: it basically relies on being constructed "empty" and then the fields are set later. Which cannot be done with records. E.g., I didn't try, but I think the code will convert this:

class Mutable {
     private int i;
     public int getI() { return i; }
     public void setI() { this.i = i; }
}

to me, this class is very unsuitable to become a record.

OTOH, for classes that would be very suitable to become records, like:

class RecordLike {
    private final int i;
    public RecordLike(int i) {
        this.i = i;
    }
    public int i() {
        return i;
    }
    //possibly equals, hashCode, toString
}

will not be converted, as they have the constructor and accessor (despite neither the constructor, nor the accessor do anything funny).

Do I miss something?

For the record->class conversion, I think I would second to Jaroslav's sentiment around equals/hashCode/toString - those methods are part of the backbone of records. I think doing the conversion and ignore those is not ideal.

(I think I am mostly fine with ignoring the serialization aspect - in the worst case, denying the conversion or warning the user serialization may change for serializable records. I hope truly (binary) serializable records are/will be rare, and we can't detect other kinds of "serialization" anyway.)

@mbien

mbien commented Nov 13, 2026

Copy link
Copy Markdown
Member Author

i can do this but this, in fact i already did start with it but stopped since this would end up being a lot more code. Probably 2x for the hint. But I agree it should be able to convert the snippet above.

Accept record-shaped classes: a canonical constructor that only assigns
the parameters to the fields and record-style accessors are dropped in
favor of the implicit ones, everything else (including
equals/hashCode/toString) remains in the record body. Classes with an
explicit super(...) call, local and anonymous classes are rejected.

Add an option to also drop equals(), hashCode() and toString() methods
in favor of the ones provided by java.lang.Record; the value is read
when the fix is applied, so the preference can be changed after the
hint was shown.

Build TreePaths by direct linking instead of rescanning with
Trees.getPath().

Assisted-by: qwen3.8-27b
@mbien

mbien commented Nov 13, 2026

Copy link
Copy Markdown
Member Author

(rebased, changes are in tmp third commit - since i want to test this all in one with git and osgi changes)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci:dev-build [ci] produce a dev-build zip artifact (7 days expiration, see link on workflow summary page) hints Java [ci] enable extra Java tests (java.completion, java.source.base, java.hints, refactoring.java, form) Need Squashing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refactor record to class and vice versa

3 participants