[java-inspections] ClassCanBeRecord: support case when field names and constructor param names differ

#IDEA-265154 fixed

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

GitOrigin-RevId: 7a04d4830e1f76ee3ad965390f28168834dca9e9
This commit is contained in:
Bartek Pacia
2025-04-03 16:45:06 +00:00
committed by intellij-monorepo-bot
parent 84ed145470
commit f619cad1d0
86 changed files with 511 additions and 227 deletions

View File

@@ -15,10 +15,7 @@ import com.intellij.psi.*;
import com.intellij.psi.PsiAnnotation.TargetType;
import com.intellij.psi.controlFlow.ControlFlowUtil;
import com.intellij.psi.search.searches.ClassInheritorsSearch;
import com.intellij.psi.util.JavaPsiRecordUtil;
import com.intellij.psi.util.PropertyUtil;
import com.intellij.psi.util.PropertyUtilBase;
import com.intellij.psi.util.PsiUtil;
import com.intellij.psi.util.*;
import com.intellij.usageView.UsageInfo;
import com.intellij.util.ObjectUtils;
import com.intellij.util.SmartList;
@@ -28,9 +25,9 @@ import com.siyeh.ig.InspectionGadgetsFix;
import com.siyeh.ig.callMatcher.CallMatcher;
import com.siyeh.ig.memory.InnerClassReferenceVisitor;
import com.siyeh.ig.psiutils.MethodUtils;
import com.siyeh.ig.psiutils.TypeUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.UnmodifiableView;
import java.util.*;
import java.util.stream.Collectors;
@@ -61,11 +58,26 @@ public class ConvertToRecordFix extends InspectionGadgetsFix {
protected void doFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
final ConvertToRecordProcessor processor = getRecordProcessor(descriptor);
if (processor == null) return;
// Without the next line, the conflicts view is not shown
processor.setPrepareSuccessfulSwingThreadCallback(() -> {
});
processor.run();
}
@Override
public @NotNull IntentionPreviewInfo generatePreview(@NotNull Project project, @NotNull ProblemDescriptor previewDescriptor) {
final ConvertToRecordProcessor processor = getRecordProcessor(previewDescriptor);
if (processor == null) return IntentionPreviewInfo.EMPTY;
// We can't use the below here, because BaseRefactoringProcessor#doRun calls PsiDocumentManager#commitAllDocumentsUnderProgress,
// and its Javadoc says "must be called on UI thread".
// processor.run();
processor.performRefactoring(UsageInfo.EMPTY_ARRAY);
return IntentionPreviewInfo.DIFF;
}
private @Nullable ConvertToRecordProcessor getRecordProcessor(ProblemDescriptor descriptor) {
PsiElement psiElement = descriptor.getPsiElement();
if (psiElement == null) return null;
@@ -78,14 +90,6 @@ public class ConvertToRecordFix extends InspectionGadgetsFix {
return new ConvertToRecordProcessor(recordCandidate);
}
@Override
public @NotNull IntentionPreviewInfo generatePreview(@NotNull Project project, @NotNull ProblemDescriptor previewDescriptor) {
final ConvertToRecordProcessor processor = getRecordProcessor(previewDescriptor);
if (processor == null) return IntentionPreviewInfo.EMPTY;
processor.performRefactoring(UsageInfo.EMPTY_ARRAY);
return IntentionPreviewInfo.DIFF;
}
/**
* There are some restrictions for records:
* <a href="https://docs.oracle.com/javase/specs/jls/se15/preview/specs/records-jls.html">see the specification</a>.
@@ -93,8 +97,11 @@ public class ConvertToRecordFix extends InspectionGadgetsFix {
static RecordCandidate getClassDefinition(@NotNull PsiClass psiClass,
boolean suggestAccessorsRenaming,
@NotNull List<String> ignoredAnnotations) {
boolean isNotAppropriatePsiClass = psiClass.isEnum() || psiClass.isAnnotationType() || psiClass instanceof PsiAnonymousClass ||
psiClass.isInterface() || psiClass.isRecord();
boolean isNotAppropriatePsiClass = psiClass.isEnum() ||
psiClass.isAnnotationType() ||
psiClass instanceof PsiAnonymousClass ||
psiClass.isInterface() ||
psiClass.isRecord();
if (isNotAppropriatePsiClass) return null;
PsiModifierList psiClassModifiers = psiClass.getModifierList();
@@ -133,15 +140,14 @@ public class ConvertToRecordFix extends InspectionGadgetsFix {
* It helps to validate whether a class will be a well-formed record and supports performing a refactoring.
*/
static class RecordCandidate {
private static final CallMatcher OBJECT_METHOD_CALLS = CallMatcher.anyOf(
CallMatcher.exactInstanceCall(JAVA_LANG_OBJECT, "equals").parameterCount(1),
CallMatcher.exactInstanceCall(JAVA_LANG_OBJECT, "hashCode", "toString").parameterCount(0)
);
private static final CallMatcher OBJECT_METHOD_CALLS =
CallMatcher.anyOf(CallMatcher.exactInstanceCall(JAVA_LANG_OBJECT, "equals").parameterCount(1),
CallMatcher.exactInstanceCall(JAVA_LANG_OBJECT, "hashCode", "toString").parameterCount(0));
private final PsiClass myClass;
private final boolean mySuggestAccessorsRenaming;
private final MultiMap<PsiField, FieldAccessorCandidate> myFieldAccessors = new MultiMap<>(new LinkedHashMap<>());
private final MultiMap<PsiField, FieldAccessorCandidate> myFieldsToAccessorCandidates = new MultiMap<>(new LinkedHashMap<>());
private final List<PsiMethod> myOrdinaryMethods = new SmartList<>();
private final List<RecordConstructorCandidate> myConstructors = new SmartList<>();
final List<RecordConstructorCandidate> myConstructorCandidates = new SmartList<>();
private PsiMethod myEqualsMethod;
private PsiMethod myHashCodeMethod;
@@ -162,46 +168,43 @@ public class ConvertToRecordFix extends InspectionGadgetsFix {
return myClass;
}
@NotNull
Map<PsiField, @Nullable FieldAccessorCandidate> getFieldAccessors() {
@UnmodifiableView
@NotNull Map<PsiField, @Nullable FieldAccessorCandidate> getFieldsToAccessorCandidates() {
if (myFieldAccessorsCache != null) return myFieldAccessorsCache;
Map<PsiField, FieldAccessorCandidate> result = new LinkedHashMap<>();
for (var entry : myFieldAccessors.entrySet()) {
for (var entry : myFieldsToAccessorCandidates.entrySet()) {
PsiField newKey = entry.getKey();
Collection<FieldAccessorCandidate> oldValue = entry.getValue();
FieldAccessorCandidate newValue = ContainerUtil.getOnlyItem(oldValue);
result.put(newKey, newValue);
}
myFieldAccessorsCache = result;
myFieldAccessorsCache = Collections.unmodifiableMap(result);
return result;
}
@Nullable
PsiMethod getCanonicalConstructor() {
return myConstructors.size() == 1 ? myConstructors.get(0).myConstructor : null;
@Nullable RecordConstructorCandidate getCanonicalConstructorCandidate() {
return myConstructorCandidates.size() == 1 ? myConstructorCandidates.get(0) : null;
}
@Nullable
PsiMethod getEqualsMethod() {
@Nullable PsiMethod getEqualsMethod() {
return myEqualsMethod;
}
@Nullable
PsiMethod getHashCodeMethod() {
@Nullable PsiMethod getHashCodeMethod() {
return myHashCodeMethod;
}
private boolean isValid() {
if (myConstructors.size() > 1) return false;
if (myConstructors.size() == 1) {
RecordConstructorCandidate ctorCandidate = myConstructors.get(0);
boolean isCanonical = ctorCandidate.myCanonical && throwsOnlyUncheckedExceptions(ctorCandidate.myConstructor);
if (myConstructorCandidates.size() > 1) return false;
if (myConstructorCandidates.size() == 1) {
RecordConstructorCandidate ctorCandidate = myConstructorCandidates.get(0);
boolean isCanonical = ctorCandidate.canonical && throwsOnlyUncheckedExceptions(ctorCandidate.constructorMethod);
if (!isCanonical) return false;
if (containsObjectMethodCalls(ctorCandidate.myConstructor)) return false;
if (containsObjectMethodCalls(ctorCandidate.constructorMethod)) return false;
}
if (myFieldAccessors.size() == 0) return false;
for (var entry : myFieldAccessors.entrySet()) {
if (myFieldsToAccessorCandidates.size() == 0) return false;
for (var entry : myFieldsToAccessorCandidates.entrySet()) {
PsiField field = entry.getKey();
if (!field.hasModifierProperty(FINAL) || field.hasInitializer()) return false;
if (JavaPsiRecordUtil.ILLEGAL_RECORD_COMPONENT_NAMES.contains(field.getName())) return false;
@@ -213,7 +216,7 @@ public class ConvertToRecordFix extends InspectionGadgetsFix {
for (PsiMethod ordinaryMethod : myOrdinaryMethods) {
if (ordinaryMethod.hasModifierProperty(NATIVE)) return false;
boolean conflictsWithPotentialAccessor = ordinaryMethod.getParameterList().isEmpty() &&
ContainerUtil.exists(myFieldAccessors.keySet(),
ContainerUtil.exists(myFieldsToAccessorCandidates.keySet(),
field -> field.getName().equals(ordinaryMethod.getName()));
if (conflictsWithPotentialAccessor) return false;
if (containsObjectMethodCalls(ordinaryMethod)) return false;
@@ -223,10 +226,12 @@ public class ConvertToRecordFix extends InspectionGadgetsFix {
private void prepare() {
Arrays.stream(myClass.getFields()).filter(field -> !field.hasModifierProperty(STATIC))
.forEach(field -> myFieldAccessors.put(field, new ArrayList<>()));
.forEach(field -> myFieldsToAccessorCandidates.put(field, new ArrayList<>()));
for (PsiMethod method : myClass.getMethods()) {
if (method.isConstructor()) {
myConstructors.add(new RecordConstructorCandidate(method, myFieldAccessors.keySet()));
Set<PsiField> instanceFields = myFieldsToAccessorCandidates.keySet();
myConstructorCandidates.add(new RecordConstructorCandidate(method, instanceFields));
continue;
}
if (MethodUtils.isEquals(method)) {
@@ -241,12 +246,12 @@ public class ConvertToRecordFix extends InspectionGadgetsFix {
myOrdinaryMethods.add(method);
continue;
}
FieldAccessorCandidate fieldAccessorCandidate = createFieldAccessor(method);
FieldAccessorCandidate fieldAccessorCandidate = tryCreateFieldAccessorCandidate(method);
if (fieldAccessorCandidate == null) {
myOrdinaryMethods.add(method);
}
else {
myFieldAccessors.putValue(fieldAccessorCandidate.myBackingField, fieldAccessorCandidate);
myFieldsToAccessorCandidates.putValue(fieldAccessorCandidate.myBackingField, fieldAccessorCandidate);
}
}
}
@@ -292,13 +297,13 @@ public class ConvertToRecordFix extends InspectionGadgetsFix {
return visitor.existsSuperMethodCalls;
}
private @Nullable FieldAccessorCandidate createFieldAccessor(@NotNull PsiMethod psiMethod) {
private @Nullable FieldAccessorCandidate tryCreateFieldAccessorCandidate(@NotNull PsiMethod psiMethod) {
if (psiMethod.hasModifier(JvmModifier.STATIC)) return null;
if (!psiMethod.getParameterList().isEmpty()) return null;
String methodName = psiMethod.getName();
PsiField backingField = null;
boolean recordStyleNaming = false;
for (PsiField field : myFieldAccessors.keySet()) {
for (PsiField field : myFieldsToAccessorCandidates.keySet()) {
if (!field.getType().equals(psiMethod.getReturnType())) continue;
String fieldName = field.getName();
if (fieldName.equals(methodName)) {
@@ -306,7 +311,8 @@ public class ConvertToRecordFix extends InspectionGadgetsFix {
recordStyleNaming = true;
break;
}
if (mySuggestAccessorsRenaming && fieldName.equals(PropertyUtilBase.getPropertyNameByGetter(psiMethod)) &&
if (mySuggestAccessorsRenaming &&
fieldName.equals(PropertyUtilBase.getPropertyNameByGetter(psiMethod)) &&
!ContainerUtil.exists(psiMethod.findDeepestSuperMethods(),
superMethod -> superMethod instanceof PsiCompiledElement || superMethod instanceof SyntheticElement)) {
backingField = field;
@@ -318,57 +324,84 @@ public class ConvertToRecordFix extends InspectionGadgetsFix {
}
/**
* Encapsulates information about the converting constructor e.g whether its canonical or not.
* Encapsulates information about converting of a single constructor, for example, whether it is canonical or not.
*/
private static class RecordConstructorCandidate {
private final PsiMethod myConstructor;
private final boolean myCanonical;
static class RecordConstructorCandidate {
private final @NotNull PsiMethod constructorMethod;
private final boolean canonical;
private final @NotNull Map<PsiParameter, PsiField> ctorParamsToFields = new HashMap<>();
private RecordConstructorCandidate(@NotNull PsiMethod constructor, @NotNull Set<PsiField> instanceFields) {
myConstructor = constructor;
if (myConstructor.getTypeParameters().length > 0) {
myCanonical = false;
constructorMethod = constructor;
if (constructorMethod.getTypeParameters().length > 0) {
canonical = false;
return;
}
Set<String> instanceFieldNames = instanceFields.stream().map(PsiField::getName).collect(Collectors.toSet());
if (instanceFieldNames.size() != instanceFields.size()) {
myCanonical = false;
canonical = false;
return;
}
PsiParameter[] ctorParams = myConstructor.getParameterList().getParameters();
PsiParameter[] ctorParams = constructorMethod.getParameterList().getParameters();
if (instanceFields.size() != ctorParams.length) {
myCanonical = false;
canonical = false;
return;
}
PsiCodeBlock ctorBody = myConstructor.getBody();
PsiCodeBlock ctorBody = constructorMethod.getBody();
if (ctorBody == null) {
myCanonical = false;
canonical = false;
return;
}
Map<String, PsiType> ctorParamsWithType = Arrays.stream(ctorParams)
.collect(Collectors.toMap(param -> param.getName(), param -> param.getType(), (first, second) -> first));
final boolean allProcessed = PsiTreeUtil.processElements(ctorBody, PsiAssignmentExpression.class, (assignExpr) -> {
if (!(assignExpr.getLExpression() instanceof PsiReferenceExpression leftRefExpr)) return true;
if (!(leftRefExpr.resolve() instanceof PsiField field)) return true;
if (!(assignExpr.getRExpression() instanceof PsiReferenceExpression rightRefExpr)) return true;
final PsiElement assignmentValue = rightRefExpr.resolve();
if (assignmentValue == null) return false; // using 'false' as a sentinel value
if (!(assignmentValue instanceof PsiParameter parameter)) return true;
ctorParamsToFields.put(parameter, field);
return true;
});
if (!allProcessed) {
canonical = false;
return;
}
for (PsiField instanceField : instanceFields) {
PsiType ctorParamType = ctorParamsWithType.get(instanceField.getName());
if (ctorParamType instanceof PsiEllipsisType) {
ctorParamType = ((PsiEllipsisType)ctorParamType).toArrayType();
}
if (ctorParamType == null || !TypeUtils.typeEquals(ctorParamType.getCanonicalText(), instanceField.getType())) {
myCanonical = false;
return;
}
if (!ControlFlowUtil.variableDefinitelyAssignedIn(instanceField, ctorBody)) {
myCanonical = false;
canonical = false;
return;
}
}
myCanonical = true;
canonical = true;
}
public @NotNull @UnmodifiableView Map<PsiParameter, PsiField> getCtorParamsToFields() {
return Collections.unmodifiableMap(ctorParamsToFields);
}
@NotNull PsiMethod getConstructorMethod() {
return constructorMethod;
}
@Override
public String toString() {
return "RecordConstructorCandidate{" +
"constructorMethod=" + constructorMethod +
", canonical=" + canonical +
", ctorParamToFieldMap=" + ctorParamsToFields +
'}';
}
}
/**
* Encapsulates information about the converting of field accessors.
* For instance an existing default accessor may be removed during further record creation.
* Encapsulates information about converting of a single field accessor.
* <p>
* For instance, an existing default accessor may be removed during further record creation.
*/
static class FieldAccessorCandidate {
private final PsiMethod myFieldAccessor;
@@ -395,13 +428,11 @@ public class ConvertToRecordFix extends InspectionGadgetsFix {
!hasAnnotationConflict(backingField, accessor, TargetType.METHOD);
}
@NotNull
PsiMethod getAccessor() {
@NotNull PsiMethod getAccessor() {
return myFieldAccessor;
}
@NotNull
PsiField getBackingField() {
@NotNull PsiField getBackingField() {
return myBackingField;
}
@@ -412,6 +443,14 @@ public class ConvertToRecordFix extends InspectionGadgetsFix {
boolean isRecordStyleNaming() {
return myRecordStyleNaming;
}
@Override
public String toString() {
return "FieldAccessorCandidate{" +
"myFieldAccessor=" + myFieldAccessor +
", myBackingField=" + myBackingField +
'}';
}
}
/**

View File

@@ -22,6 +22,8 @@ import com.intellij.psi.tree.IElementType;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtil;
import com.intellij.refactoring.BaseRefactoringProcessor;
import com.intellij.refactoring.RefactoringFactory;
import com.intellij.refactoring.RenameRefactoring;
import com.intellij.refactoring.rename.RenameProcessor;
import com.intellij.refactoring.rename.RenamePsiElementProcessor;
import com.intellij.refactoring.rename.RenameUtil;
@@ -38,14 +40,24 @@ import com.siyeh.ig.callMatcher.CallMatcher;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Unmodifiable;
import java.util.*;
import static com.intellij.codeInspection.classCanBeRecord.ConvertToRecordFix.RecordConstructorCandidate;
/**
* Responsible for converting a single {@link RecordCandidate} that is {@link RecordCandidate#isValid valid}.
*/
final class ConvertToRecordProcessor extends BaseRefactoringProcessor {
private static final CallMatcher OBJECT_EQUALS = CallMatcher.instanceCall(CommonClassNames.JAVA_LANG_OBJECT, "equals")
private static final CallMatcher OBJECT_EQUALS = CallMatcher
.instanceCall(CommonClassNames.JAVA_LANG_OBJECT, "equals")
.parameterTypes(CommonClassNames.JAVA_LANG_OBJECT);
private static final CallMatcher OBJECT_HASHCODE =
CallMatcher.instanceCall(CommonClassNames.JAVA_LANG_OBJECT, "hashCode").parameterCount(0);
private static final CallMatcher OBJECT_HASHCODE = CallMatcher
.instanceCall(CommonClassNames.JAVA_LANG_OBJECT, "hashCode")
.parameterCount(0);
private final RecordCandidate myRecordCandidate;
private final Map<PsiElement, String> myAllRenames = new LinkedHashMap<>();
@@ -73,10 +85,28 @@ final class ConvertToRecordProcessor extends BaseRefactoringProcessor {
@Override
protected void doRun() {
prepareRenameOfAccessors();
prepareRenameOfConstructorParameters();
super.doRun();
}
private void prepareRenameOfConstructorParameters() {
RecordConstructorCandidate ctorCandidate = myRecordCandidate.getCanonicalConstructorCandidate();
if (ctorCandidate == null) return;
ctorCandidate.getCtorParamsToFields().forEach((ctorParam, field) -> {
if (!ctorParam.getName().equals(field.getName())) {
RenameRefactoring renameRefactoring = RefactoringFactory.getInstance(myProject).createRename(ctorParam, field.getName());
renameRefactoring.setPreviewUsages(false);
renameRefactoring.setSearchInComments(false);
// The below line is required to not show conflicts midway and break the refactoring flow.
renameRefactoring.setSearchInNonJavaFiles(false);
renameRefactoring.setInteractive(null);
renameRefactoring.run();
}
});
}
private void prepareRenameOfAccessors() {
List<FieldAccessorCandidate> accessorsToRename = getAccessorsToRename();
@@ -96,9 +126,9 @@ final class ConvertToRecordProcessor extends BaseRefactoringProcessor {
@Override
protected UsageInfo @NotNull [] findUsages() {
List<UsageInfo> usages = new SmartList<>();
for (var psiField : myRecordCandidate.getFieldAccessors().keySet()) {
for (var psiField : myRecordCandidate.getFieldsToAccessorCandidates().keySet()) {
if (!psiField.hasModifierProperty(PsiModifier.PRIVATE)) {
for (PsiReference reference : ReferencesSearch.search(psiField).asIterable()) {
for (PsiReference reference : ReferencesSearch.search(psiField).findAll()) {
usages.add(new FieldUsageInfo(psiField, reference));
}
}
@@ -126,11 +156,11 @@ final class ConvertToRecordProcessor extends BaseRefactoringProcessor {
}
/**
* @return list of accessors which have not record-compatible names and need to be renamed separately.
* @return list of accessors whose names aren't record-compatible and need to be renamed separately.
*/
private @NotNull List<@NotNull FieldAccessorCandidate> getAccessorsToRename() {
private @NotNull @Unmodifiable List<@NotNull FieldAccessorCandidate> getAccessorsToRename() {
List<FieldAccessorCandidate> list = ContainerUtil.filter(
myRecordCandidate.getFieldAccessors().values(),
myRecordCandidate.getFieldsToAccessorCandidates().values(),
fieldAccessorCandidate -> fieldAccessorCandidate != null && !fieldAccessorCandidate.isRecordStyleNaming()
);
return list;
@@ -140,7 +170,7 @@ final class ConvertToRecordProcessor extends BaseRefactoringProcessor {
* @param accessor a declaration to find supers methods for
* @return a list of direct super methods, or the declaration itself if no super methods are found
*/
private static @NotNull List<@NotNull PsiMethod> substituteWithSuperMethodsIfPossible(@NotNull PsiMethod accessor) {
private static @NotNull @Unmodifiable List<@NotNull PsiMethod> substituteWithSuperMethodsIfPossible(@NotNull PsiMethod accessor) {
PsiMethod[] superMethods = accessor.findSuperMethods();
if (superMethods.length == 0) {
return List.of(accessor);
@@ -152,7 +182,7 @@ final class ConvertToRecordProcessor extends BaseRefactoringProcessor {
static @NotNull List<UsageInfo> findConflicts(@NotNull RecordCandidate recordCandidate) {
List<UsageInfo> result = new SmartList<>();
for (var entry : recordCandidate.getFieldAccessors().entrySet()) {
for (var entry : recordCandidate.getFieldsToAccessorCandidates().entrySet()) {
PsiField psiField = entry.getKey();
FieldAccessorCandidate fieldAccessorCandidate = entry.getValue();
if (fieldAccessorCandidate == null) {
@@ -175,13 +205,16 @@ final class ConvertToRecordProcessor extends BaseRefactoringProcessor {
}
}
}
PsiMethod canonicalCtor = recordCandidate.getCanonicalConstructor();
if (canonicalCtor != null && firstHasWeakerAccess(recordCandidate.getPsiClass(), canonicalCtor)) {
result.add(new BrokenEncapsulationUsageInfo(canonicalCtor, JavaRefactoringBundle
.message("convert.to.record.ctor.more.accessible",
StringUtil.capitalize(RefactoringUIUtil.getDescription(canonicalCtor, false)),
VisibilityUtil.getVisibilityStringToDisplay(canonicalCtor),
VisibilityUtil.getVisibilityStringToDisplay(recordCandidate.getPsiClass()))));
RecordConstructorCandidate canonicalCtorCandidate = recordCandidate.getCanonicalConstructorCandidate();
if (canonicalCtorCandidate != null) {
PsiMethod canonicalCtor = canonicalCtorCandidate.getConstructorMethod();
if (firstHasWeakerAccess(recordCandidate.getPsiClass(), canonicalCtor)) {
result.add(new BrokenEncapsulationUsageInfo(canonicalCtor, JavaRefactoringBundle
.message("convert.to.record.ctor.more.accessible",
StringUtil.capitalize(RefactoringUIUtil.getDescription(canonicalCtor, false)),
VisibilityUtil.getVisibilityStringToDisplay(canonicalCtor),
VisibilityUtil.getVisibilityStringToDisplay(recordCandidate.getPsiClass()))));
}
}
return result;
}
@@ -226,11 +259,12 @@ final class ConvertToRecordProcessor extends BaseRefactoringProcessor {
@Override
protected void performRefactoring(UsageInfo @NotNull [] usages) {
prepareRenameOfConstructorParameters();
renameMembers(usages);
PsiClass psiClass = myRecordCandidate.getPsiClass();
PsiMethod canonicalCtor = myRecordCandidate.getCanonicalConstructor();
Map<PsiField, FieldAccessorCandidate> fieldAccessors = myRecordCandidate.getFieldAccessors();
final PsiClass psiClass = myRecordCandidate.getPsiClass();
final RecordConstructorCandidate canonicalCtorCandidate = myRecordCandidate.getCanonicalConstructorCandidate();
final Map<PsiField, FieldAccessorCandidate> fieldToAccessorCandidateMap = myRecordCandidate.getFieldsToAccessorCandidates();
RecordBuilder recordBuilder = new RecordBuilder(psiClass);
PsiIdentifier classIdentifier = null;
PsiElement nextElement = psiClass.getFirstChild();
@@ -245,7 +279,7 @@ final class ConvertToRecordProcessor extends BaseRefactoringProcessor {
else if (nextElement instanceof PsiTypeParameterList) {
recordBuilder.addPsiElement(nextElement);
if (PsiTreeUtil.skipWhitespacesAndCommentsBackward(nextElement) == classIdentifier) {
recordBuilder.addRecordHeader(canonicalCtor, fieldAccessors);
recordBuilder.addRecordHeader(canonicalCtorCandidate, fieldToAccessorCandidateMap);
classIdentifier = null;
}
}
@@ -254,18 +288,18 @@ final class ConvertToRecordProcessor extends BaseRefactoringProcessor {
}
else if (nextElement instanceof PsiField psiField) {
psiField.normalizeDeclaration();
if (fieldAccessors.containsKey(psiField)) {
if (fieldToAccessorCandidateMap.containsKey(psiField)) {
nextElement = PsiTreeUtil.skipWhitespacesForward(nextElement);
continue;
}
recordBuilder.addPsiElement(nextElement);
}
else if (nextElement instanceof PsiMethod) {
if (nextElement == canonicalCtor) {
recordBuilder.addCanonicalCtor(canonicalCtor);
if (canonicalCtorCandidate != null && nextElement == canonicalCtorCandidate.getConstructorMethod()) {
recordBuilder.addCanonicalCtor(canonicalCtorCandidate.getConstructorMethod());
}
else {
FieldAccessorCandidate fieldAccessorCandidate = getFieldAccessorCandidate(fieldAccessors, (PsiMethod)nextElement);
FieldAccessorCandidate fieldAccessorCandidate = getFieldAccessorCandidate(fieldToAccessorCandidateMap, (PsiMethod)nextElement);
if (fieldAccessorCandidate == null) {
recordBuilder.addPsiElement(nextElement);
}
@@ -326,7 +360,7 @@ final class ConvertToRecordProcessor extends BaseRefactoringProcessor {
PsiMethod hashCodeMethod = myRecordCandidate.getHashCodeMethod();
if (equalsMethod == null && hashCodeMethod == null) return CallMatcher.none();
List<CallMatcher> result = new SmartList<>();
Set<PsiField> fields = myRecordCandidate.getFieldAccessors().keySet();
Set<PsiField> fields = myRecordCandidate.getFieldsToAccessorCandidates().keySet();
if (EqualsChecker.isStandardEqualsMethod(equalsMethod, fields)) {
result.add(OBJECT_EQUALS);
}
@@ -451,14 +485,14 @@ final class ConvertToRecordProcessor extends BaseRefactoringProcessor {
}
}
private static void removeRedundantObjectMethods(@NotNull PsiClass record, CallMatcher redundantObjectMethods) {
private static void removeRedundantObjectMethods(@NotNull PsiClass record, @NotNull CallMatcher redundantObjectMethods) {
ContainerUtil.filter(record.getMethods(), redundantObjectMethods::methodMatches)
.forEach(PsiMethod::delete);
.forEach(PsiMethod::delete);
}
private void generateJavaDocForDocumentedFields(@NotNull PsiClass record) {
Map<String, String> comments = new LinkedHashMap<>();
for (PsiField field : myRecordCandidate.getFieldAccessors().keySet()) {
for (PsiField field : myRecordCandidate.getFieldsToAccessorCandidates().keySet()) {
StringBuilder fieldComment = new StringBuilder();
for (PsiComment comment : ObjectUtils.notNull(PsiTreeUtil.getChildrenOfType(field, PsiComment.class), new PsiComment[0])) {
if (comment instanceof PsiDocComment) {

View File

@@ -16,7 +16,7 @@ interface ConvertToRecordUsageInfo {
}
/**
* Encapsulates the field which will be narrowed its visibility as the record introduces a private final field.
* Encapsulates the field which will become less accessible the record introduces a private final field.
*/
class FieldUsageInfo extends UsageInfo implements ConvertToRecordUsageInfo {
final PsiField myField;
@@ -56,4 +56,4 @@ class BrokenEncapsulationUsageInfo extends UsageInfo implements ConvertToRecordU
super(psiMethod);
myErrMsg = errMsg;
}
}
}

View File

@@ -6,6 +6,7 @@ import com.intellij.codeInsight.AnnotationUtil;
import com.intellij.codeInsight.intention.AddAnnotationPsiFix;
import com.intellij.codeInsight.javadoc.JavaDocUtil;
import com.intellij.codeInspection.classCanBeRecord.ConvertToRecordFix.FieldAccessorCandidate;
import com.intellij.codeInspection.classCanBeRecord.ConvertToRecordFix.RecordConstructorCandidate;
import com.intellij.java.syntax.parser.DeclarationParser;
import com.intellij.java.syntax.parser.JavaParser;
import com.intellij.pom.java.LanguageLevel;
@@ -40,16 +41,29 @@ class RecordBuilder {
myRecordText.append("record");
}
void addRecordHeader(@Nullable PsiMethod canonicalCtor, @NotNull Map<PsiField, @Nullable FieldAccessorCandidate> fieldAccessors) {
void addRecordHeader(@Nullable RecordConstructorCandidate canonicalCtorCandidate,
@NotNull Map<PsiField, @Nullable FieldAccessorCandidate> fieldToAccessorCandidateMap) {
myRecordText.append("(");
StringJoiner recordComponentsJoiner = new StringJoiner(",");
if (canonicalCtor == null) {
fieldAccessors.forEach(
(field, fieldAccessor) -> recordComponentsJoiner.add(generateComponentText(field, field.getType(), fieldAccessor)));
if (canonicalCtorCandidate == null) {
fieldToAccessorCandidateMap.forEach((field, fieldAccessor) -> {
recordComponentsJoiner.add(generateComponentText(field, field.getType(), fieldAccessor));
});
}
else {
PsiMethod canonicalCtor = canonicalCtorCandidate.getConstructorMethod();
Arrays.stream(canonicalCtor.getParameterList().getParameters())
.map(parameter -> generateComponentText(parameter, fieldAccessors))
.map(parameter -> {
PsiField field = canonicalCtorCandidate.getCtorParamsToFields().get(parameter);
if (field == null) {
field = ContainerUtil.find(myOriginClass.getFields(), f -> f.getName().equals(parameter.getName()));
if (field == null) {
throw new IllegalStateException("no field found corresponding to constructor parameter '" + parameter.getName() + "'");
}
}
return generateComponentText(field, parameter, fieldToAccessorCandidateMap.get(field));
})
.forEach(recordComponentsJoiner::add);
}
myRecordText.append(recordComponentsJoiner);
@@ -95,27 +109,25 @@ class RecordBuilder {
return (PsiClass)Objects.requireNonNull(SourceTreeToPsiMap.treeElementToPsi(holder.getTreeElement().getFirstChildNode()));
}
private static @NotNull String generateComponentText(@NotNull PsiParameter parameter,
@NotNull Map<PsiField, @Nullable FieldAccessorCandidate> fieldAccessors) {
PsiField field = null;
FieldAccessorCandidate fieldAccessorCandidate = null;
for (var entry : fieldAccessors.entrySet()) {
if (entry.getKey().getName().equals(parameter.getName())) {
field = entry.getKey();
fieldAccessorCandidate = entry.getValue();
break;
}
private static @NotNull String generateComponentText(@NotNull PsiField field,
@NotNull PsiParameter ctorParameter,
@Nullable FieldAccessorCandidate fieldAccessorCandidate) {
if (field == null) {
throw new IllegalStateException("no field found to which the constructor parameter '" +
ctorParameter.getType().toString() +
ctorParameter.getName() +
"' is assigned");
}
assert field != null;
// Do not use parameter.getType() directly, as type annotations may differ; prefer type annotations on the field
// Don't use parameter.getType() directly, as type annotations may differ; prefer type annotations on the field
PsiType componentType = field.getType();
if (parameter.getType() instanceof PsiEllipsisType && componentType instanceof PsiArrayType arrayType) {
if (ctorParameter.getType() instanceof PsiEllipsisType && componentType instanceof PsiArrayType arrayType) {
componentType = new PsiEllipsisType(arrayType.getComponentType(), arrayType.getAnnotationProvider());
}
return generateComponentText(field, componentType, fieldAccessorCandidate);
}
private static @NotNull String generateComponentText(@NotNull PsiField field, @NotNull PsiType componentType,
private static @NotNull String generateComponentText(@NotNull PsiField field,
@NotNull PsiType componentType,
@Nullable FieldAccessorCandidate fieldAccessorCandidate) {
PsiAnnotation[] fieldAnnotations = field.getAnnotations();
String fieldAnnotationsText = Arrays.stream(fieldAnnotations)