[java-inspections] IDEA-252259 ClassCanBeRecordInspection: delete redundant Lombok annotations after conversion

Merge-request: IJ-MR-159286
Merged-by: Bartek Pacia <bartek.pacia@jetbrains.com>

GitOrigin-RevId: 7456109ad3a225dcab41229e3f03c2c99a57c094
This commit is contained in:
Bartek Pacia
2025-04-25 15:02:17 +00:00
committed by intellij-monorepo-bot
parent 77f4d62b64
commit 98ee3086d0
38 changed files with 533 additions and 34 deletions
@@ -17,10 +17,11 @@ public abstract class CanBeFinalHandler {
/**
* @param member a member (field or method) to check whether it's allowed to be final
* @return false if a current extension prohibit marking this member as final.
* @return false if this extension prohibits marking this member as final.
* Must return true for every member that is unknown to the current extension.
* Note that if the field is written explicitly, it will be detected by inspection itself,
* so no need to specially handle explicitly written fields here.
* <p>
* If the field is written explicitly, it is detected by the inspection itself,
* so implementations of this method shouldn't care about such a case.
*/
public abstract boolean canBeFinal(@NotNull PsiMember member);
@@ -88,7 +88,7 @@ public final class ConvertToRecordFix implements LocalQuickFix {
RecordCandidate recordCandidate = getClassDefinition(psiClass, mySuggestAccessorsRenaming, myIgnoredAnnotations);
if (recordCandidate == null) return null;
return new ConvertToRecordProcessor(recordCandidate);
return new ConvertToRecordProcessor(recordCandidate, mySuggestAccessorsRenaming);
}
/**
@@ -137,7 +137,7 @@ public final class ConvertToRecordFix implements LocalQuickFix {
}
/**
* Encapsulates necessary information about the converting class e.g its existing fields, accessors...
* Encapsulates necessary information about the class being converted: its fields, accessors, etc.
* It helps to validate whether a class will be a well-formed record and supports performing a refactoring.
*/
static class RecordCandidate {
@@ -207,7 +207,8 @@ public final class ConvertToRecordFix implements LocalQuickFix {
if (myFieldsToAccessorCandidates.size() == 0) return false;
for (var entry : myFieldsToAccessorCandidates.entrySet()) {
PsiField field = entry.getKey();
if (!field.hasModifierProperty(FINAL) || field.hasInitializer()) return false;
if (!field.hasModifierProperty(FINAL)) return false;
if (field.hasInitializer()) return false;
if (JavaPsiRecordUtil.ILLEGAL_RECORD_COMPONENT_NAMES.contains(field.getName())) return false;
if (entry.getValue().size() > 1) return false;
FieldAccessorCandidate firstAccessor = ContainerUtil.getFirstItem(entry.getValue());
@@ -5,6 +5,7 @@ import com.intellij.codeInspection.RedundantRecordConstructorInspection;
import com.intellij.codeInspection.RedundantRecordConstructorInspection.ConstructorSimplifier;
import com.intellij.codeInspection.classCanBeRecord.ConvertToRecordFix.FieldAccessorCandidate;
import com.intellij.codeInspection.classCanBeRecord.ConvertToRecordFix.RecordCandidate;
import com.intellij.java.library.JavaLibraryUtil;
import com.intellij.java.refactoring.JavaRefactoringBundle;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.util.NlsContexts;
@@ -19,6 +20,7 @@ import com.intellij.psi.javadoc.PsiDocTag;
import com.intellij.psi.javadoc.PsiDocTagValue;
import com.intellij.psi.search.searches.ReferencesSearch;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.util.PropertyUtilBase;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtil;
import com.intellij.refactoring.BaseRefactoringProcessor;
@@ -43,6 +45,7 @@ import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Unmodifiable;
import java.util.*;
import java.util.stream.Collectors;
import static com.intellij.codeInspection.classCanBeRecord.ConvertToRecordFix.RecordConstructorCandidate;
@@ -59,12 +62,14 @@ final class ConvertToRecordProcessor extends BaseRefactoringProcessor {
.parameterCount(0);
private final RecordCandidate myRecordCandidate;
private final boolean mySuggestAccessorsRenaming;
private final Map<PsiElement, String> myAllRenames = new LinkedHashMap<>();
ConvertToRecordProcessor(@NotNull RecordCandidate recordCandidate) {
ConvertToRecordProcessor(@NotNull RecordCandidate recordCandidate, boolean suggestAccessorsRenaming) {
super(recordCandidate.getProject());
myRecordCandidate = recordCandidate;
mySuggestAccessorsRenaming = suggestAccessorsRenaming;
}
@Override
@@ -293,14 +298,14 @@ final class ConvertToRecordProcessor extends BaseRefactoringProcessor {
}
recordBuilder.addPsiElement(nextElement);
}
else if (nextElement instanceof PsiMethod) {
if (canonicalCtorCandidate != null && nextElement == canonicalCtorCandidate.getConstructorMethod()) {
else if (nextElement instanceof PsiMethod psiMethod) {
if (canonicalCtorCandidate != null && psiMethod == canonicalCtorCandidate.getConstructorMethod()) {
recordBuilder.addCanonicalCtor(canonicalCtorCandidate.getConstructorMethod());
}
else {
FieldAccessorCandidate fieldAccessorCandidate = getFieldAccessorCandidate(fieldToAccessorCandidateMap, (PsiMethod)nextElement);
FieldAccessorCandidate fieldAccessorCandidate = getFieldAccessorCandidate(fieldToAccessorCandidateMap, psiMethod);
if (fieldAccessorCandidate == null) {
recordBuilder.addPsiElement(nextElement);
recordBuilder.addPsiElement(psiMethod);
}
else {
recordBuilder.addFieldAccessor(fieldAccessorCandidate);
@@ -312,16 +317,34 @@ final class ConvertToRecordProcessor extends BaseRefactoringProcessor {
}
nextElement = nextElement.getNextSibling();
}
Set<PsiMethod> syntheticGetters = Arrays.stream(psiClass.getMethods())
.filter(method -> method instanceof SyntheticElement)
.filter(method -> ContainerUtil.exists(fieldToAccessorCandidateMap.keySet(),
field -> field.getName().equals(PropertyUtilBase.getPropertyNameByGetter(method))))
.collect(Collectors.toSet());
useAccessorsWhenNecessary(usages);
CallMatcher redundantObjectMethods = findRedundantObjectMethods();
CallMatcher redundantObjectMethods = findRedundantObjectMethods(myRecordCandidate);
PsiClass result = (PsiClass)psiClass.replace(recordBuilder.build());
tryToCompactCanonicalCtor(result);
removeRedundantObjectMethods(result, redundantObjectMethods);
generateJavaDocForDocumentedFields(result);
addImplicitLombokGetters(result, syntheticGetters.toArray(PsiMethod.EMPTY_ARRAY));
removeRedundantLombokAnnotations(result);
generateJavaDocForDocumentedFields(result, myRecordCandidate.getFieldsToAccessorCandidates().keySet());
CodeStyleManager.getInstance(myProject).reformat(JavaCodeStyleManager.getInstance(myProject).shortenClassReferences(result));
}
private void useAccessorsWhenNecessary(UsageInfo @NotNull [] usages) {
private void addImplicitLombokGetters(@NotNull PsiClass record, @NotNull PsiMethod @NotNull [] implicitGetters) {
if (!mySuggestAccessorsRenaming) {
PsiElementFactory elementFactory = JavaPsiFacade.getElementFactory(myProject);
for (PsiMethod getter : implicitGetters) {
record.add(elementFactory.createMethodFromText(getter.getText(), record));
}
}
}
private void useAccessorsWhenNecessary(@NotNull UsageInfo @NotNull [] usages) {
for (UsageInfo usage : usages) {
if (usage instanceof FieldUsageInfo fieldUsageInfo) {
PsiField field = fieldUsageInfo.myField;
@@ -354,12 +377,15 @@ final class ConvertToRecordProcessor extends BaseRefactoringProcessor {
}
}
private CallMatcher findRedundantObjectMethods() {
PsiMethod equalsMethod = myRecordCandidate.getEqualsMethod();
PsiMethod hashCodeMethod = myRecordCandidate.getHashCodeMethod();
/**
* Finds methods within {@code recordCandidate} that are redundant implementations of {@link Object#equals} and {@link Object#hashCode}.
*/
private static CallMatcher findRedundantObjectMethods(@NotNull RecordCandidate recordCandidate) {
PsiMethod equalsMethod = recordCandidate.getEqualsMethod();
PsiMethod hashCodeMethod = recordCandidate.getHashCodeMethod();
if (equalsMethod == null && hashCodeMethod == null) return CallMatcher.none();
List<CallMatcher> result = new SmartList<>();
Set<PsiField> fields = myRecordCandidate.getFieldsToAccessorCandidates().keySet();
Set<PsiField> fields = recordCandidate.getFieldsToAccessorCandidates().keySet();
if (EqualsChecker.isStandardEqualsMethod(equalsMethod, fields)) {
result.add(OBJECT_EQUALS);
}
@@ -468,6 +494,8 @@ final class ConvertToRecordProcessor extends BaseRefactoringProcessor {
}
private static void tryToCompactCanonicalCtor(@NotNull PsiClass record) {
if (!record.isRecord()) throw new IllegalArgumentException("Not a record: " + record);
PsiMethod canonicalCtor = ArrayUtil.getFirstElement(record.getConstructors());
if (canonicalCtor != null) {
PsiCodeBlock ctorBody = canonicalCtor.getBody();
@@ -489,9 +517,45 @@ final class ConvertToRecordProcessor extends BaseRefactoringProcessor {
.forEach(PsiMethod::delete);
}
private void generateJavaDocForDocumentedFields(@NotNull PsiClass record) {
/**
* This could be also implemented as a {@link com.intellij.refactoring.RefactoringHelper}.
* {@link BaseRefactoringProcessor} automatically calls refactoring helpers after refactoring is done.
* <p>
* The problem with it is that refactoring helpers don't trivially work in preview, so we would make the bug IDEA-369873 only worse.
*/
private static void removeRedundantLombokAnnotations(@NotNull PsiClass record) {
if (!record.isRecord()) throw new IllegalArgumentException("Not a record: " + record);
if (!JavaLibraryUtil.hasLibraryJar(record.getProject(), "org.projectlombok:lombok")) return;
// Remove annotations from the class
for (final PsiAnnotation annotation : record.getAnnotations()) {
final String qualifiedName = annotation.getQualifiedName();
if (qualifiedName == null) continue;
if (Set.of(
"lombok.ToString",
"lombok.Getter",
"lombok.EqualsAndHashCode",
"lombok.RequiredArgsConstructor",
"lombok.Data",
"lombok.Value").contains(qualifiedName)) {
annotation.delete();
}
}
// Remove annotations from instance fields
for (final PsiField field : record.getFields()) {
for (final PsiAnnotation annotation : field.getAnnotations()) {
final String qualifiedName = annotation.getQualifiedName();
if (qualifiedName != null && qualifiedName.equals("lombok.Getter")) {
annotation.delete();
}
}
}
}
private static void generateJavaDocForDocumentedFields(@NotNull PsiClass record, @NotNull Set<@NotNull PsiField> fields) {
Map<String, String> comments = new LinkedHashMap<>();
for (PsiField field : myRecordCandidate.getFieldsToAccessorCandidates().keySet()) {
for (PsiField field : fields) {
StringBuilder fieldComment = new StringBuilder();
for (PsiComment comment : ObjectUtils.notNull(PsiTreeUtil.getChildrenOfType(field, PsiComment.class), new PsiComment[0])) {
if (comment instanceof PsiDocComment) {
@@ -509,7 +573,7 @@ final class ConvertToRecordProcessor extends BaseRefactoringProcessor {
}
}
if (comments.isEmpty()) return;
PsiJavaParserFacade parserFacade = JavaPsiFacade.getInstance(myProject).getParserFacade();
PsiJavaParserFacade parserFacade = JavaPsiFacade.getInstance(record.getProject()).getParserFacade();
PsiDocComment recordDoc = record.getDocComment();
if (recordDoc == null) {
PsiDocComment emptyDoc = parserFacade.createDocCommentFromText("/** */");
@@ -27,18 +27,20 @@ final class EqualsChecker {
);
/**
* Detects standard equals method implementation with identity check, null/class check, and field-by-field comparison, in the form of:
* <pre>
* public boolean equals(Object obj) {
* if (obj == this) return true;
* if (obj == null || obj.getClass() != this.getClass()) return false;
* var that = (Person)obj;
* return Objects.equals(this.name, that.name) &&
* this.age == that.age;
* }
* </pre>
*/
static boolean isStandardEqualsMethod(@Nullable PsiMethod method, @NotNull Set<PsiField> fields) {
if (method == null) return false;
/*
Finds equals in the form
public boolean equals(Object obj) {
if (obj == this) return true;
if (obj == null || obj.getClass() != this.getClass()) return false;
var that = (Person)obj;
return Objects.equals(this.name, that.name) &&
this.age == that.age;
}
*/
PsiCodeBlock body = method.getBody();
if (body == null) return false;
PsiParameter parameter = method.getParameterList().getParameter(0);
@@ -0,0 +1,3 @@
// "Convert to record class" "true-preview"
record R(int x, int y) {
}
@@ -0,0 +1,18 @@
// "Convert to record class" "true-preview"
class R<caret> {
final int x;
final int y;
R(int x, int y) {
this.x = x;
this.y = y;
}
int getX() {
return x;
}
int getY() {
return y;
}
}
@@ -0,0 +1,18 @@
// "Convert to record class" "true-preview"
record R(int x, int y) {
int getX() {
return x;
}
int getY() {
return y;
}
}
class Foo {
void foo() {
R r = new R(10, 20);
System.out.println("x: " + person.getX() + ", y" + r.getY());
}
}
@@ -0,0 +1,25 @@
// "Convert to record class" "true-preview"
class R<caret> {
final int x;
final int y;
R(int x, int y) {
this.x = x;
this.y = y;
}
int getX() {
return x;
}
int getY() {
return y;
}
}
class Foo {
void foo() {
R r = new R(10, 20);
System.out.println("x: " + person.getX() + ", y" + r.getY());
}
}
@@ -8,7 +8,7 @@ import com.intellij.codeInspection.classCanBeRecord.ClassCanBeRecordInspection.C
import com.intellij.refactoring.BaseRefactoringProcessor;
import org.jetbrains.annotations.NotNull;
public class ClassCanBeRecordInspectionAccessorsRenamingTest extends LightQuickFixParameterizedTestCase {
public class ClassCanBeRecordInspectionNoAccessorsRenamingTest extends LightQuickFixParameterizedTestCase {
@Override
protected LocalInspectionTool @NotNull [] configureLocalInspectionTools() {
@@ -17,7 +17,7 @@ public class ClassCanBeRecordInspectionAccessorsRenamingTest extends LightQuickF
@Override
protected String getBasePath() {
return "/inspection/classCanBeRecord/accessorsRenaming";
return "/inspection/classCanBeRecord/noAccessorsRenaming";
}
@Override
@@ -22,7 +22,9 @@ public final class LombokCanBeFinalHandler extends CanBeFinalHandler {
}
final PsiClass psiClass = PsiTreeUtil.getParentOfType(member, PsiClass.class);
return null == psiClass || !PsiAnnotationSearchUtil.isAnnotatedWith(psiClass, LombokClassNames.SETTER, LombokClassNames.DATA, LombokClassNames.VALUE);
if (psiClass == null) return true;
return !PsiAnnotationSearchUtil.isAnnotatedWith(psiClass, LombokClassNames.SETTER, LombokClassNames.DATA, LombokClassNames.VALUE);
// will return true for our elemnt
}
return true;
}
@@ -29,6 +29,7 @@ import java.util.List;
* @author Plushnikov Michail
*/
public final class GetterFieldProcessor extends AbstractFieldProcessor {
public GetterFieldProcessor() {
super(PsiMethod.class, LombokClassNames.GETTER);
}
@@ -0,0 +1,100 @@
package com.intellij.java.lomboktest;
import com.intellij.codeInsight.intention.IntentionAction;
import com.intellij.codeInspection.classCanBeRecord.ClassCanBeRecordInspection;
import com.intellij.refactoring.BaseRefactoringProcessor;
import com.intellij.testFramework.LightProjectDescriptor;
import de.plushnikov.intellij.plugin.AbstractLombokLightCodeInsightTestCase;
import de.plushnikov.intellij.plugin.LombokTestUtil;
import org.jetbrains.annotations.NotNull;
import static com.intellij.codeInspection.classCanBeRecord.ClassCanBeRecordInspection.ConversionStrategy;
public class AdditionalLombokClassCanBeRecordInspectionTest extends AbstractLombokLightCodeInsightTestCase {
private static final String VALUE_CLASS_SAMPLE = """
@lombok.Value
class Person<caret> {
String name;
int age;
String getInfo() {
return "Person " + name + " is " + age + " years old";
}
}
class Foo {
void foo() {
Person person = new Person("Charlie", 42);
// Renaming getters to accessors requires the Lombok plugin
System.out.println("Person " + person.getName() + " is " + person.getAge() + " years old");
System.out.println(person.getInfo());
}
}
""";
@Override
protected @NotNull LightProjectDescriptor getProjectDescriptor() {
return LombokTestUtil.LOMBOK_NEW_DESCRIPTOR;
}
public void testClassCanBeRecord_renamingAccessors() {
myFixture.enableInspections(new ClassCanBeRecordInspection(ConversionStrategy.DO_NOT_SUGGEST, true));
myFixture.configureByText("Test.java", VALUE_CLASS_SAMPLE);
IntentionAction intention = myFixture.findSingleIntention("Convert to record class");
// myFixture.checkPreviewAndLaunchAction(intention); // preview differs from the actual result because of IDEA-369873
BaseRefactoringProcessor.ConflictsInTestsException.withIgnoredConflicts(() -> myFixture.launchAction(intention));
myFixture.checkResult("""
record Person(String name, int age) {
String getInfo() {
return "Person " + name + " is " + age + " years old";
}
}
class Foo {
void foo() {
Person person = new Person("Charlie", 42);
// Renaming getters to accessors requires the Lombok plugin
System.out.println("Person " + person.name() + " is " + person.age() + " years old");
System.out.println(person.getInfo());
}
}
""");
myFixture.checkHighlighting(true, false, false); // Ensure again that code is well-formed
}
public void testClassCanBeRecord_notRenamingAccessors() {
myFixture.enableInspections(new ClassCanBeRecordInspection(ConversionStrategy.DO_NOT_SUGGEST, false));
myFixture.configureByText("Test.java", VALUE_CLASS_SAMPLE);
IntentionAction intention = myFixture.findSingleIntention("Convert to record class");
// myFixture.checkPreviewAndLaunchAction(intention); // preview differs from the actual result because of IDEA-369873
BaseRefactoringProcessor.ConflictsInTestsException.withIgnoredConflicts(() -> myFixture.launchAction(intention));
myFixture.checkResult("""
record Person(String name, int age) {
String getInfo() {
return "Person " + name + " is " + age + " years old";
}
public String getName() {
return this.name;
}
public int getAge() {
return this.age;
}
}
class Foo {
void foo() {
Person person = new Person("Charlie", 42);
// Renaming getters to accessors requires the Lombok plugin
System.out.println("Person " + person.getName() + " is " + person.getAge() + " years old");
System.out.println(person.getInfo());
}
}
""");
myFixture.checkHighlighting(true, false, false); // Ensure again that code is well-formed
}
}
@@ -0,0 +1,48 @@
// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.java.lomboktest;
import com.intellij.codeInsight.daemon.quickFix.LightQuickFixParameterizedTestCase;
import com.intellij.codeInspection.LocalInspectionTool;
import com.intellij.codeInspection.classCanBeRecord.ClassCanBeRecordInspection;
import com.intellij.codeInspection.classCanBeRecord.ClassCanBeRecordInspection.ConversionStrategy;
import com.intellij.openapi.application.PathManager;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.refactoring.BaseRefactoringProcessor;
import com.intellij.testFramework.LightProjectDescriptor;
import de.plushnikov.intellij.plugin.LombokTestUtil;
import org.jetbrains.annotations.NotNull;
public class LombokClassCanBeRecordInspectionTest extends LightQuickFixParameterizedTestCase {
@Override
protected LocalInspectionTool @NotNull [] configureLocalInspectionTools() {
return new LocalInspectionTool[]{new ClassCanBeRecordInspection(ConversionStrategy.DO_NOT_SUGGEST, true)};
}
@Override
protected @NotNull String getTestDataPath() {
return PathManager.getCommunityHomePath() + "/plugins/lombok/testData";
}
@Override
protected String getBasePath() {
return "/inspection/classCanBeRecord";
}
@Override
protected @NotNull LightProjectDescriptor getProjectDescriptor() {
return LombokTestUtil.LOMBOK_NEW_DESCRIPTOR;
}
@Override
public void runSingle() throws Throwable {
String option = StringUtil.substringAfter(getName(), "__");
if (option != null && option.startsWith("ignoreConflicts")) {
// TODO(bartekpacia): It'd be good to test that specific conflicts appear. See IDEA-370463
BaseRefactoringProcessor.ConflictsInTestsException.withIgnoredConflicts(super::runSingle);
}
else {
super.runSingle();
}
}
}
@@ -0,0 +1,4 @@
// "Convert to record class" "true-preview"
record C(int a, int b) {
}
@@ -0,0 +1,4 @@
// "Convert to record class" "true-preview"
record Person(String name, int age) {
}
@@ -0,0 +1,4 @@
// "Convert to record class" "true-preview"
record Person(String name, int age) {
}
@@ -0,0 +1,15 @@
// "Convert to record class" "true-preview"
record Person(String name, int age) {
}
@Getter // another class, just to verify that the import doesn't get deleted too eagerly
class Person2 {
final String name;
final int age;
Person2(String name, int age) {
this.name = name;
this.age = age;
}
}
@@ -0,0 +1,4 @@
// "Convert to record class" "true-preview"
record Person(String name, int age) {
}
@@ -0,0 +1,4 @@
// "Convert to record class" "true-preview"
record Person(String name, int age) {
}
@@ -0,0 +1,4 @@
// "Convert to record class" "true-preview"
record Person(String name, int age) {
}
@@ -0,0 +1,11 @@
// "Convert to record class" "true-preview"
/// Instance fields of this class have redundant modifiers, because @Value automatically:
/// 1. marks non-static fields as 'final'
/// 2. marks non-static, package-local fields as 'private'
///
/// @param name 'private' is redundant because of (1)
/// @param age 'final' is redundant because of (2)
/// @param city 'private' and 'final' are redundant because of (1) and (2)
record Person(String name, int age, String city) {
}
@@ -0,0 +1,13 @@
// "Convert to record class" "true"
// no "true-preview" above because of IDEA-369873
record Person(String name, int age) {
}
class Foo {
void foo() {
Person person = new Person("Charlie", 42);
// Renaming getters to accessors requires the Lombok plugin
System.out.println("Person " + person.name() + " is " + person.age() + " years old");
}
}
@@ -0,0 +1,4 @@
// "Convert to record class" "true-preview"
record Person(String name, int age) {
}
@@ -0,0 +1,7 @@
// "Convert to record class" "true-preview"
@lombok.Data
class C<caret> {
final int a;
final int b;
}
@@ -0,0 +1,8 @@
// "Convert to record class" "false"
@lombok.Data
class C<caret> {
final int a;
final int b;
int c;
}
@@ -0,0 +1,12 @@
// "Convert to record class" "true-preview"
@lombok.EqualsAndHashCode
class Person<caret> {
final String name;
final int age;
Person(String name, int age) {
this.name = name;
this.age = age;
}
}
@@ -0,0 +1,12 @@
// "Convert to record class" "true-preview"
@lombok.Getter
class Person<caret> {
final String name;
final int age;
Person(String name, int age) {
this.name = name;
this.age = age;
}
}
@@ -0,0 +1,23 @@
// "Convert to record class" "true-preview"
@lombok.Getter
class Person<caret> {
final String name;
final int age;
Person(String name, int age) {
this.name = name;
this.age = age;
}
}
@Getter // another class, just to verify that the import doesn't get deleted too eagerly
class Person2 {
final String name;
final int age;
Person2(String name, int age) {
this.name = name;
this.age = age;
}
}
@@ -0,0 +1,7 @@
// "Convert to record class" "true-preview"
@lombok.RequiredArgsConstructor
class Person<caret> {
final String name;
final int age;
}
@@ -0,0 +1,12 @@
// "Convert to record class" "true-preview"
@lombok.ToString
class Person<caret> {
final String name;
final int age;
Person(String name, int age) {
this.name = name;
this.age = age;
}
}
@@ -0,0 +1,7 @@
// "Convert to record class" "true-preview"
@lombok.Value
class Person<caret> {
String name;
int age;
}
@@ -0,0 +1,11 @@
// "Convert to record class" "true-preview"
/// Instance fields of this class have redundant modifiers, because @Value automatically:
/// 1. marks non-static fields as 'final'
/// 2. marks non-static, package-local fields as 'private'
@lombok.Value
class Person<caret> {
private String name; // 'private' is redundant because of (1)
final int age; // 'final' is redundant because of (2)
private final String city; // 'private' and 'final' are redundant because of (1) and (2)
}
@@ -0,0 +1,16 @@
// "Convert to record class" "true"
// no "true-preview" above because of IDEA-369873
@lombok.Value
class Person<caret> {
String name;
int age;
}
class Foo {
void foo() {
Person person = new Person("Charlie", 42);
// Renaming getters to accessors requires the Lombok plugin
System.out.println("Person " + person.getName() + " is " + person.getAge() + " years old");
}
}
@@ -0,0 +1,11 @@
// "Convert to record class" "true-preview"
class Person<caret> {
@lombok.Getter final String name;
@lombok.Getter final int age;
Person(String name, int age) {
this.name = name;
this.age = age;
}
}
@@ -0,0 +1,11 @@
// "Convert to record class" "false"
class Person<caret> {
@lombok.Getter final String name;
@lombok.Getter int age; // we don't want to suggest converting this class to record, because this field is not final
Person(String name, int age) {
this.name = name;
this.age = age;
}
}
@@ -0,0 +1,12 @@
// "Convert to record class" "false"
class Person<caret> {
// Java records are immutable, so we don't suggest conversion here
@lombok.Setter String name;
@lombok.Setter int age;
Person(String name, int age) {
this.name = name;
this.age = age;
}
}