mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-01-07 22:09:38 +07:00
[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:
committed by
intellij-monorepo-bot
parent
84ed145470
commit
f619cad1d0
@@ -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 +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user