mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[java-inspection] IDEA-251700 ClassCanBeRecord: support converting classes with many constructors
#IDEA-251700 fixed Merge-request: IJ-MR-160901 Merged-by: Bartek Pacia <bartek.pacia@jetbrains.com> GitOrigin-RevId: f0a1224846e203a3dc6638270d665354dcf14fb4
This commit is contained in:
committed by
intellij-monorepo-bot
parent
658ab5d084
commit
f419dbbb08
+1
-1
@@ -135,7 +135,7 @@ public final class ClassCanBeRecordInspection extends BaseInspection implements
|
||||
super.visitClass(aClass);
|
||||
PsiIdentifier classIdentifier = aClass.getNameIdentifier();
|
||||
if (classIdentifier == null) return;
|
||||
RecordCandidate recordCandidate = ConvertToRecordFix.getClassDefinition(aClass, mySuggestAccessorsRenaming, myIgnoredAnnotations);
|
||||
RecordCandidate recordCandidate = ConvertToRecordFix.tryCreateRecordCandidate(aClass, mySuggestAccessorsRenaming, myIgnoredAnnotations);
|
||||
if (recordCandidate == null) return;
|
||||
|
||||
boolean suggestQuickFix = true;
|
||||
|
||||
+234
@@ -0,0 +1,234 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.codeInspection.classCanBeRecord;
|
||||
|
||||
import com.intellij.java.syntax.parser.JavaKeywords;
|
||||
import com.intellij.openapi.util.Ref;
|
||||
import com.intellij.psi.*;
|
||||
import com.intellij.psi.controlFlow.ControlFlowUtil;
|
||||
import com.intellij.psi.util.TypeConversionUtil;
|
||||
import com.intellij.util.JavaPsiConstructorUtil;
|
||||
import com.intellij.util.containers.MultiMap;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.annotations.UnmodifiableView;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import static com.intellij.psi.PsiModifier.STATIC;
|
||||
|
||||
@NotNullByDefault
|
||||
final class ConstructorBodyProcessor {
|
||||
private final PsiMethod constructor;
|
||||
private final Map<PsiParameter, @Nullable PsiField> paramsToFields = new HashMap<>();
|
||||
// TODO(bartekpacia): change type to SequencedMap once we move to Java 21
|
||||
private final LinkedHashMap<String, PsiExpression> fieldNamesToInitializers = new LinkedHashMap<>();
|
||||
private final List<PsiField> instanceFields;
|
||||
|
||||
private boolean canonical = false;
|
||||
private boolean delegating = false;
|
||||
private boolean hasUnresolvedRefs = false;
|
||||
private boolean tooComplex = false;
|
||||
private boolean statementsBeforeAllFieldsAssigned = false;
|
||||
private final List<PsiStatement> otherStatements = new ArrayList<>();
|
||||
private final MultiMap<PsiField, PsiParameter> fieldsToParams = new MultiMap<>();
|
||||
|
||||
ConstructorBodyProcessor(PsiMethod constructor,
|
||||
List<PsiField> instanceFields) {
|
||||
this.constructor = constructor;
|
||||
this.instanceFields = instanceFields;
|
||||
assert constructor.getBody() != null; // The caller asserts this
|
||||
for (PsiStatement statement : constructor.getBody().getStatements()) {
|
||||
execute(statement);
|
||||
}
|
||||
|
||||
postprocess();
|
||||
}
|
||||
|
||||
private void execute(PsiStatement statement) {
|
||||
if (!(statement instanceof PsiExpressionStatement expressionStatement)) {
|
||||
otherStatements.add(statement);
|
||||
return;
|
||||
}
|
||||
final PsiExpression expression = expressionStatement.getExpression();
|
||||
|
||||
// Is it an assignment expression to an instance field?
|
||||
// If not, then all instance variables must already be assigned.
|
||||
if (!expressionIsAssignmentToInstanceField(expression) && !expressionIsDelegatingConstructorCall(expression) && !delegating) {
|
||||
otherStatements.add(statement);
|
||||
|
||||
// If not all instance fields are assigned up to this point,
|
||||
// then this constructor cannot be converted to a non-canonical record constructor.
|
||||
if (fieldNamesToInitializers.size() < instanceFields.size()) {
|
||||
statementsBeforeAllFieldsAssigned = true;
|
||||
// It is OK to have statements before all fields are assigned if this constructor is a canonical constructor.
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (expression instanceof PsiMethodCallExpression methodCallExpr) {
|
||||
if (JavaPsiConstructorUtil.isChainedConstructorCall(methodCallExpr)) {
|
||||
delegating = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!(expression instanceof PsiAssignmentExpression assignExpr)) return;
|
||||
|
||||
if (!(assignExpr.getLExpression() instanceof PsiReferenceExpression leftRefExpr)) return;
|
||||
if (leftRefExpr.resolve() == null) {
|
||||
hasUnresolvedRefs = true;
|
||||
return;
|
||||
}
|
||||
if (!(leftRefExpr.resolve() instanceof PsiField field)) return;
|
||||
|
||||
final PsiExpression rightExpr = assignExpr.getRExpression();
|
||||
if (rightExpr == null) return;
|
||||
final PsiType targetType = leftRefExpr.getType();
|
||||
final PsiType assignedType = rightExpr.getType();
|
||||
if (targetType == null || assignedType == null || !targetType.isAssignableFrom(assignedType)) {
|
||||
hasUnresolvedRefs = true; // "Overload" semantics of this flag
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
fieldNamesToInitializers.put(field.getName(), rightExpr);
|
||||
|
||||
Ref<@Nullable PsiParameter> refParameterForField = new Ref<>();
|
||||
rightExpr.accept(new JavaRecursiveElementWalkingVisitor() {
|
||||
@Override
|
||||
public void visitReferenceExpression(PsiReferenceExpression expression) {
|
||||
super.visitReferenceExpression(expression);
|
||||
final PsiElement resolved = expression.resolve();
|
||||
if (resolved == null) {
|
||||
hasUnresolvedRefs = true;
|
||||
}
|
||||
else if (resolved instanceof PsiParameter parameter) {
|
||||
fieldsToParams.putValue(field, parameter);
|
||||
if (!paramsToFields.containsKey(parameter)) {
|
||||
refParameterForField.set(parameter);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
PsiParameter parameterForField = refParameterForField.get();
|
||||
if (parameterForField != null) {
|
||||
paramsToFields.put(parameterForField, field);
|
||||
}
|
||||
}
|
||||
|
||||
/// Must be called after visiting is done.
|
||||
private void postprocess() {
|
||||
for (PsiParameter parameter : constructor.getParameterList().getParameters()) {
|
||||
if (paramsToFields.get(parameter) == null) {
|
||||
paramsToFields.put(parameter, null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// if "a constructor parameter is referenced from more than a single instance field assignment",
|
||||
// then we cannot convert it because of ambiguities
|
||||
for (PsiParameter parameter : fieldsToParams.values()) {
|
||||
if (fieldsToParams.keySet().stream().filter(field -> fieldsToParams.get(field).contains(parameter)).count() > 1) {
|
||||
tooComplex = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// If the constructor is not delegating, then it must assign all instance fields.
|
||||
// If it doesn't do it, it is not valid.
|
||||
if (!delegating) {
|
||||
assert constructor.getBody() != null; // The caller asserts this
|
||||
PsiParameter[] ctorParams = constructor.getParameterList().getParameters();
|
||||
if (allFieldsAssignedIn(constructor.getBody(), instanceFields)) {
|
||||
if (ctorParams.length == instanceFields.size()) {
|
||||
assert constructor.getContainingClass() != null; // The caller asserts this
|
||||
if (constructor.getContainingClass().getConstructors().length == 1) {
|
||||
// If there is just a single constructor, even if the order and types of its parameters don't exactly match the
|
||||
// order of instance fields, we can still convert it to a canonical constructor using the paramsToFields map.
|
||||
canonical = true;
|
||||
}
|
||||
else {
|
||||
boolean fieldsMatchInOrder = true;
|
||||
for (int i = 0; i < ctorParams.length; i++) {
|
||||
PsiType ctorParamType = ctorParams[i].getType();
|
||||
PsiType instanceFieldType = instanceFields.get(i).getType();
|
||||
if (!TypeConversionUtil.isAssignable(instanceFieldType, ctorParamType)) {
|
||||
fieldsMatchInOrder = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (fieldsMatchInOrder) {
|
||||
canonical = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Not canonical and not delegating: just a custom constructor that assigns all instance fields.
|
||||
}
|
||||
}
|
||||
else {
|
||||
// If the constructor is not delegating, then it must assign all instance fields.
|
||||
// Otherwise it is invalid - and if we are here, we hit this exact case.
|
||||
hasUnresolvedRefs = true; // "Overload" semantics of this flag
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boolean isCanonical() {
|
||||
return canonical;
|
||||
}
|
||||
|
||||
boolean isDelegating() {
|
||||
return delegating;
|
||||
}
|
||||
|
||||
boolean hasUnresolvedRefs() {
|
||||
return hasUnresolvedRefs;
|
||||
}
|
||||
|
||||
boolean isTooComplex() {
|
||||
return tooComplex;
|
||||
}
|
||||
|
||||
boolean hasAnyStatementBeforeAllFieldsAreAssigned() {
|
||||
return statementsBeforeAllFieldsAssigned;
|
||||
}
|
||||
|
||||
Map<PsiParameter, @Nullable PsiField> getParamsToFields() {
|
||||
return paramsToFields;
|
||||
}
|
||||
|
||||
LinkedHashMap<String, PsiExpression> getFieldNamesToInitializers() {
|
||||
return fieldNamesToInitializers;
|
||||
}
|
||||
|
||||
@UnmodifiableView
|
||||
List<PsiStatement> getOtherStatements() {
|
||||
return Collections.unmodifiableList(otherStatements);
|
||||
}
|
||||
|
||||
private static boolean allFieldsAssignedIn(PsiCodeBlock block, List<PsiField> instanceFields) {
|
||||
for (PsiField instanceField : instanceFields) {
|
||||
if (!ControlFlowUtil.variableDefinitelyAssignedIn(instanceField, block)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean expressionIsDelegatingConstructorCall(PsiExpression expr) {
|
||||
if (!(expr instanceof PsiMethodCallExpression methodCallExpr)) return false;
|
||||
return JavaKeywords.THIS.equals(methodCallExpr.getMethodExpression().getReferenceName()) ||
|
||||
JavaKeywords.SUPER.equals(methodCallExpr.getMethodExpression().getReferenceName());
|
||||
}
|
||||
|
||||
private static boolean expressionIsAssignmentToInstanceField(PsiExpression expr) {
|
||||
if (!(expr instanceof PsiAssignmentExpression assignExpr)) return false;
|
||||
PsiExpression leftExpr = assignExpr.getLExpression();
|
||||
if (!(leftExpr instanceof PsiReferenceExpression)) return false;
|
||||
PsiElement resolved = ((PsiReferenceExpression)leftExpr).resolve();
|
||||
return resolved instanceof PsiField && !((PsiField)resolved).hasModifierProperty(STATIC);
|
||||
}
|
||||
}
|
||||
+260
-136
@@ -4,6 +4,7 @@ package com.intellij.codeInspection.classCanBeRecord;
|
||||
import com.intellij.codeInsight.AnnotationTargetUtil;
|
||||
import com.intellij.codeInsight.AnnotationUtil;
|
||||
import com.intellij.codeInsight.ExceptionUtil;
|
||||
import com.intellij.codeInsight.daemon.impl.analysis.HighlightMessageUtil;
|
||||
import com.intellij.codeInsight.intention.preview.IntentionPreviewInfo;
|
||||
import com.intellij.codeInspection.LocalQuickFix;
|
||||
import com.intellij.codeInspection.ProblemDescriptor;
|
||||
@@ -12,10 +13,8 @@ import com.intellij.java.JavaBundle;
|
||||
import com.intellij.java.syntax.parser.JavaKeywords;
|
||||
import com.intellij.lang.jvm.JvmModifier;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.util.Ref;
|
||||
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.*;
|
||||
import com.intellij.usageView.UsageInfo;
|
||||
@@ -27,11 +26,11 @@ import com.siyeh.ig.callMatcher.CallMatcher;
|
||||
import com.siyeh.ig.memory.InnerClassReferenceVisitor;
|
||||
import com.siyeh.ig.psiutils.MethodUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.annotations.UnmodifiableView;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.intellij.psi.CommonClassNames.JAVA_LANG_OBJECT;
|
||||
import static com.intellij.psi.PsiModifier.*;
|
||||
@@ -85,7 +84,7 @@ public final class ConvertToRecordFix implements LocalQuickFix {
|
||||
PsiClass psiClass = ObjectUtils.tryCast(psiElement.getParent(), PsiClass.class);
|
||||
if (psiClass == null) return null;
|
||||
|
||||
RecordCandidate recordCandidate = getClassDefinition(psiClass, mySuggestAccessorsRenaming, myIgnoredAnnotations);
|
||||
RecordCandidate recordCandidate = tryCreateRecordCandidate(psiClass, mySuggestAccessorsRenaming, myIgnoredAnnotations);
|
||||
if (recordCandidate == null) return null;
|
||||
|
||||
return new ConvertToRecordProcessor(recordCandidate, mySuggestAccessorsRenaming);
|
||||
@@ -95,9 +94,9 @@ public final class ConvertToRecordFix implements LocalQuickFix {
|
||||
* 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>.
|
||||
*/
|
||||
static RecordCandidate getClassDefinition(@NotNull PsiClass psiClass,
|
||||
boolean suggestAccessorsRenaming,
|
||||
@NotNull List<String> ignoredAnnotations) {
|
||||
static @Nullable RecordCandidate tryCreateRecordCandidate(@NotNull PsiClass psiClass,
|
||||
boolean suggestAccessorsRenaming,
|
||||
@NotNull List<String> ignoredAnnotations) {
|
||||
boolean isNotAppropriatePsiClass = psiClass.isEnum() ||
|
||||
psiClass.isAnnotationType() ||
|
||||
psiClass instanceof PsiAnonymousClass ||
|
||||
@@ -105,10 +104,8 @@ public final class ConvertToRecordFix implements LocalQuickFix {
|
||||
psiClass.isRecord();
|
||||
if (isNotAppropriatePsiClass) return null;
|
||||
|
||||
PsiModifierList psiClassModifiers = psiClass.getModifierList();
|
||||
if (psiClassModifiers == null || psiClassModifiers.hasModifierProperty(ABSTRACT) || psiClassModifiers.hasModifierProperty(SEALED)) {
|
||||
return null;
|
||||
}
|
||||
PsiModifierList modifierList = psiClass.getModifierList();
|
||||
if (modifierList == null || modifierList.hasModifierProperty(ABSTRACT) || modifierList.hasModifierProperty(SEALED)) return null;
|
||||
if (PsiUtil.isLocalClass(psiClass) && containsOuterNonStaticReferences(psiClass)) return null;
|
||||
if (psiClass.getContainingClass() != null && !psiClass.hasModifierProperty(STATIC)) return null;
|
||||
|
||||
@@ -144,14 +141,14 @@ public final class ConvertToRecordFix implements LocalQuickFix {
|
||||
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 @NotNull PsiClass myClass;
|
||||
private final MultiMap<PsiField, FieldAccessorCandidate> myFieldsToAccessorCandidates = new MultiMap<>(new LinkedHashMap<>());
|
||||
private final Map<PsiMethod, @Nullable RecordConstructorCandidate> myMethodsToConstructorCandidates = new HashMap<>();
|
||||
private final List<PsiMethod> myOrdinaryMethods = new SmartList<>();
|
||||
final List<RecordConstructorCandidate> myConstructorCandidates = new SmartList<>();
|
||||
|
||||
private PsiMethod myEqualsMethod;
|
||||
private PsiMethod myHashCodeMethod;
|
||||
private @Nullable PsiMethod myEqualsMethod;
|
||||
private @Nullable PsiMethod myHashCodeMethod;
|
||||
|
||||
private Map<PsiField, FieldAccessorCandidate> myFieldAccessorsCache;
|
||||
|
||||
@@ -161,11 +158,11 @@ public final class ConvertToRecordFix implements LocalQuickFix {
|
||||
prepare();
|
||||
}
|
||||
|
||||
Project getProject() {
|
||||
@NotNull Project getProject() {
|
||||
return myClass.getProject();
|
||||
}
|
||||
|
||||
PsiClass getPsiClass() {
|
||||
@NotNull PsiClass getPsiClass() {
|
||||
return myClass;
|
||||
}
|
||||
|
||||
@@ -184,8 +181,34 @@ public final class ConvertToRecordFix implements LocalQuickFix {
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Make sure that [#isValid] returns true before calling this method.
|
||||
///
|
||||
/// @return the canonical constructor candidate, or null if there is no such constructor
|
||||
/// @throws IllegalStateException if there is more than 1 possible canonical constructor (usually, the code is red in this case)
|
||||
@Nullable RecordConstructorCandidate getCanonicalConstructorCandidate() {
|
||||
return myConstructorCandidates.size() == 1 ? myConstructorCandidates.get(0) : null;
|
||||
RecordConstructorCandidate result = null;
|
||||
for (RecordConstructorCandidate candidate : myMethodsToConstructorCandidates.values()) {
|
||||
if (candidate == null) continue;
|
||||
// I deem the tiny cost of iterating all of this map's entries worth
|
||||
// the benefit of validating the invariant "only 1 constructor candidate can be canonical".
|
||||
if (candidate.kind() == RecordConstructorCandidate.Kind.CANONICAL) {
|
||||
if (result != null) {
|
||||
String constructor1 = HighlightMessageUtil.getSymbolName(candidate.constructor());
|
||||
String constructor2 = HighlightMessageUtil.getSymbolName(result.constructor());
|
||||
|
||||
// Cannot determine which is canonical.
|
||||
throw new IllegalStateException(
|
||||
"there can be only 1 canonical constructor candidate (found " + constructor1 + " and " + constructor2 + ")");
|
||||
}
|
||||
|
||||
result = candidate;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@NotNull Map<PsiMethod, RecordConstructorCandidate> getMethodsToConstructorCandidates() {
|
||||
return myMethodsToConstructorCandidates;
|
||||
}
|
||||
|
||||
@Nullable PsiMethod getEqualsMethod() {
|
||||
@@ -197,13 +220,26 @@ public final class ConvertToRecordFix implements LocalQuickFix {
|
||||
}
|
||||
|
||||
private boolean isValid() {
|
||||
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.constructorMethod)) return false;
|
||||
int possibleCanonicalConstructorCount = 0;
|
||||
for (var constructorCandidate : myMethodsToConstructorCandidates.values()) {
|
||||
if (constructorCandidate == null) return false; // the constructor was invalid
|
||||
if (!throwsOnlyUncheckedExceptions(constructorCandidate.constructor())) return false;
|
||||
if (containsObjectMethodCalls(constructorCandidate.constructor())) return false;
|
||||
if (constructorCandidate.kind == RecordConstructorCandidate.Kind.CANONICAL) {
|
||||
for (PsiParameter parameter : constructorCandidate.constructor().getParameterList().getParameters()) {
|
||||
if (!constructorCandidate.paramsToFields().containsKey(parameter)) continue;
|
||||
PsiField field = constructorCandidate.paramsToFields.get(parameter);
|
||||
if (field == null) {
|
||||
boolean fieldWithMatchingNameExists = myClass.findFieldByName(parameter.getName(), false) != null;
|
||||
if (!fieldWithMatchingNameExists) return false;
|
||||
}
|
||||
}
|
||||
|
||||
possibleCanonicalConstructorCount++;
|
||||
}
|
||||
}
|
||||
if (!myMethodsToConstructorCandidates.isEmpty() && possibleCanonicalConstructorCount != 1) return false;
|
||||
|
||||
if (myFieldsToAccessorCandidates.size() == 0) return false;
|
||||
for (var entry : myFieldsToAccessorCandidates.entrySet()) {
|
||||
PsiField field = entry.getKey();
|
||||
@@ -227,33 +263,34 @@ public final class ConvertToRecordFix implements LocalQuickFix {
|
||||
}
|
||||
|
||||
private void prepare() {
|
||||
Arrays.stream(myClass.getFields()).filter(field -> !field.hasModifierProperty(STATIC))
|
||||
.forEach(field -> myFieldsToAccessorCandidates.put(field, new ArrayList<>()));
|
||||
for (PsiField field : myClass.getFields()) {
|
||||
if (!field.hasModifierProperty(STATIC)) myFieldsToAccessorCandidates.put(field, new ArrayList<>());
|
||||
}
|
||||
|
||||
for (PsiMethod method : myClass.getMethods()) {
|
||||
if (method.isConstructor()) {
|
||||
Set<PsiField> instanceFields = myFieldsToAccessorCandidates.keySet();
|
||||
myConstructorCandidates.add(new RecordConstructorCandidate(method, instanceFields));
|
||||
continue;
|
||||
// Here, keySet() has a consistent iteration order because this specific MultiMap uses LinkedHashMap under the hood.
|
||||
final var orderedInstanceFields = new ArrayList<>(myFieldsToAccessorCandidates.keySet());
|
||||
RecordConstructorCandidate recordConstructorCandidate = tryCreateRecordConstructorCandidate(method, orderedInstanceFields);
|
||||
myMethodsToConstructorCandidates.put(method, recordConstructorCandidate);
|
||||
}
|
||||
if (MethodUtils.isEquals(method)) {
|
||||
else if (MethodUtils.isEquals(method)) {
|
||||
myEqualsMethod = method;
|
||||
continue;
|
||||
}
|
||||
if (MethodUtils.isHashCode(method)) {
|
||||
else if (MethodUtils.isHashCode(method)) {
|
||||
myHashCodeMethod = method;
|
||||
continue;
|
||||
}
|
||||
if (!throwsOnlyUncheckedExceptions(method)) {
|
||||
myOrdinaryMethods.add(method);
|
||||
continue;
|
||||
}
|
||||
FieldAccessorCandidate fieldAccessorCandidate = tryCreateFieldAccessorCandidate(method);
|
||||
if (fieldAccessorCandidate == null) {
|
||||
else if (!throwsOnlyUncheckedExceptions(method)) {
|
||||
myOrdinaryMethods.add(method);
|
||||
}
|
||||
else {
|
||||
myFieldsToAccessorCandidates.putValue(fieldAccessorCandidate.backingField, fieldAccessorCandidate);
|
||||
FieldAccessorCandidate fieldAccessorCandidate = tryCreateFieldAccessorCandidate(method);
|
||||
if (fieldAccessorCandidate == null) {
|
||||
myOrdinaryMethods.add(method);
|
||||
}
|
||||
else {
|
||||
myFieldsToAccessorCandidates.putValue(fieldAccessorCandidate.backingField, fieldAccessorCandidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -323,103 +360,194 @@ public final class ConvertToRecordFix implements LocalQuickFix {
|
||||
}
|
||||
return backingField == null ? null : new FieldAccessorCandidate(psiMethod, backingField, recordStyleNaming);
|
||||
}
|
||||
|
||||
/// If null is returned, it means that there are problems with the constructor, such as
|
||||
/// - the constructor being generic,
|
||||
/// - unresolved references ("red code"),
|
||||
/// - or simply the constructor being too complex to convert / not implemented yet.
|
||||
///
|
||||
/// In these cases, the class-to-record conversion cannot be performed.
|
||||
private static @Nullable RecordConstructorCandidate tryCreateRecordConstructorCandidate(@NotNull PsiMethod constructorMethod,
|
||||
List<PsiField> instanceFields) {
|
||||
if (constructorMethod.getTypeParameters().length > 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final PsiCodeBlock ctorBody = constructorMethod.getBody();
|
||||
if (ctorBody == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final var bodyProcessor = new ConstructorBodyProcessor(constructorMethod, instanceFields);
|
||||
final boolean canonical = bodyProcessor.isCanonical();
|
||||
final boolean invalid = bodyProcessor.isTooComplex() ||
|
||||
bodyProcessor.hasUnresolvedRefs() ||
|
||||
(!canonical && bodyProcessor.hasAnyStatementBeforeAllFieldsAreAssigned());
|
||||
final boolean delegating = bodyProcessor.isDelegating();
|
||||
|
||||
RecordConstructorCandidate.Kind type;
|
||||
if (invalid) {
|
||||
return null;
|
||||
}
|
||||
else if (canonical) {
|
||||
type = RecordConstructorCandidate.Kind.CANONICAL;
|
||||
}
|
||||
else if (delegating) {
|
||||
type = RecordConstructorCandidate.Kind.DELEGATING;
|
||||
}
|
||||
else {
|
||||
type = RecordConstructorCandidate.Kind.CUSTOM;
|
||||
}
|
||||
|
||||
final Set<PsiStatement> otherStatements = new HashSet<>(bodyProcessor.getOtherStatements());
|
||||
return new RecordConstructorCandidate(
|
||||
type, constructorMethod, bodyProcessor.getParamsToFields(), bodyProcessor.getFieldNamesToInitializers(), otherStatements
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encapsulates information about converting of a single constructor, for example, whether it is canonical or not.
|
||||
*/
|
||||
static class RecordConstructorCandidate {
|
||||
private final @NotNull PsiMethod constructorMethod;
|
||||
/// True if this constructor becomes a Canonical Constructor after conversion to record. See JLS 8.10.4.
|
||||
private final boolean canonical;
|
||||
private final @NotNull Map<@NotNull PsiParameter, @NotNull PsiField> ctorParamsToFields = new HashMap<>();
|
||||
@NotNullByDefault
|
||||
record RecordConstructorCandidate(
|
||||
Kind kind,
|
||||
PsiMethod constructor,
|
||||
Map<PsiParameter, @Nullable PsiField> paramsToFields,
|
||||
LinkedHashMap<String, PsiExpression> fieldNamesToInitializers, // TODO(bartekpacia): change type to SequencedMap once we move to Java 21
|
||||
Set<PsiStatement> otherStatements
|
||||
) {
|
||||
|
||||
private RecordConstructorCandidate(@NotNull PsiMethod constructor, @NotNull Set<PsiField> instanceFields) {
|
||||
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()) {
|
||||
canonical = false;
|
||||
return;
|
||||
}
|
||||
PsiParameter[] ctorParams = constructorMethod.getParameterList().getParameters();
|
||||
if (instanceFields.size() != ctorParams.length) {
|
||||
canonical = false;
|
||||
return;
|
||||
}
|
||||
PsiCodeBlock ctorBody = constructorMethod.getBody();
|
||||
if (ctorBody == null) {
|
||||
canonical = false;
|
||||
return;
|
||||
}
|
||||
/// The "kind" of record constructor that [#constructor] will take after being converted to a record (if at all).
|
||||
enum Kind {
|
||||
/// A constructor that could be converted to a record canonical constructor:
|
||||
/// its signature matches the number and types of fields, and it assigns all instance fields directly.
|
||||
/// See JLS 8.10.4.
|
||||
///
|
||||
/// Becomes a canonical constructor during the class-to-record conversion.
|
||||
CANONICAL,
|
||||
/// A constructor that invokes an alternate constructor with `this()`
|
||||
/// (a technique also known as "constructor redirecting" or "constructor telescoping").
|
||||
/// See JLS 8.8.7.1.
|
||||
///
|
||||
/// Copied as-is during the class-to-record conversion.
|
||||
DELEGATING,
|
||||
|
||||
Ref<Boolean> hasUnresolvedRefs = new Ref<>(false);
|
||||
PsiTreeUtil.processElements(ctorBody, PsiAssignmentExpression.class, (assignExpr) -> {
|
||||
if (!(assignExpr.getLExpression() instanceof PsiReferenceExpression leftRefExpr)) return true;
|
||||
if (!(leftRefExpr.resolve() instanceof PsiField field)) return true;
|
||||
|
||||
final PsiExpression rightExpr = assignExpr.getRExpression();
|
||||
if (rightExpr == null) return true;
|
||||
|
||||
Ref<@Nullable PsiParameter> matchingParameter = new Ref<>();
|
||||
rightExpr.accept(new JavaRecursiveElementWalkingVisitor() {
|
||||
@Override
|
||||
public void visitReferenceExpression(@NotNull PsiReferenceExpression expression) {
|
||||
super.visitReferenceExpression(expression);
|
||||
final PsiElement resolved = expression.resolve();
|
||||
if (resolved == null) {
|
||||
hasUnresolvedRefs.set(true);
|
||||
}
|
||||
else if (resolved instanceof PsiParameter parameter && !ctorParamsToFields.containsKey(parameter)) {
|
||||
matchingParameter.set(parameter);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (matchingParameter.get() == null) {
|
||||
final PsiParameter[] ctorParameters = constructor.getParameterList().getParameters();
|
||||
matchingParameter.set(ContainerUtil.find(ctorParameters, param -> param.getName().equals(leftRefExpr.getReferenceName())));
|
||||
}
|
||||
if (matchingParameter.get() == null) {
|
||||
hasUnresolvedRefs.set(true);
|
||||
return true;
|
||||
}
|
||||
ctorParamsToFields.put(matchingParameter.get(), field);
|
||||
return true;
|
||||
});
|
||||
if (hasUnresolvedRefs.get()) {
|
||||
canonical = false;
|
||||
return;
|
||||
}
|
||||
|
||||
for (PsiField instanceField : instanceFields) {
|
||||
if (!ControlFlowUtil.variableDefinitelyAssignedIn(instanceField, ctorBody)) {
|
||||
canonical = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
canonical = true;
|
||||
}
|
||||
|
||||
public @NotNull @UnmodifiableView Map<PsiParameter, PsiField> getCtorParamsToFields() {
|
||||
return Collections.unmodifiableMap(ctorParamsToFields);
|
||||
}
|
||||
|
||||
@NotNull PsiMethod getConstructorMethod() {
|
||||
return constructorMethod;
|
||||
/// Constructor which is not canonical and not delegating, but assigns all instance fields directly.
|
||||
///
|
||||
/// Becomes a delegating constructor during the class-to-record conversion.
|
||||
CUSTOM,
|
||||
}
|
||||
|
||||
//@formatter:off Temporarily disable formatter because of bug IDEA-371809
|
||||
/// Maps each formal parameter of the constructor to the instance field it is assigned to.
|
||||
///
|
||||
/// Allows for conversion to record when constructor parameter names aren't equal to instance field names.
|
||||
///
|
||||
/// ### Example
|
||||
///
|
||||
/// ```java
|
||||
/// class Point {
|
||||
/// final double x;
|
||||
/// final double y;
|
||||
///
|
||||
/// Point(double x, double second) {
|
||||
/// this.x = x;
|
||||
/// this.y = second;
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
/// results in the following map:
|
||||
/// ```
|
||||
/// PsiParameter:x -> PsiField:x
|
||||
/// PsiParameter:second -> PsiField:y
|
||||
///```
|
||||
//@formatter:on
|
||||
@Override
|
||||
public String toString() {
|
||||
return "RecordConstructorCandidate{" +
|
||||
"constructorMethod=" + constructorMethod +
|
||||
", canonical=" + canonical +
|
||||
", ctorParamToFieldMap=" + ctorParamsToFields +
|
||||
'}';
|
||||
public @UnmodifiableView Map<PsiParameter, @Nullable PsiField> paramsToFields() {
|
||||
return Collections.unmodifiableMap(paramsToFields);
|
||||
}
|
||||
|
||||
//@formatter:off Temporarily disable formatter because of bug IDEA-371809
|
||||
/// Maps the name of each field (that is assigned to in the constructor body) to the [PsiExpression] it is assigned to.
|
||||
///
|
||||
/// Allows for conversion to record when the constructor is not canonical and not delegating,
|
||||
/// i.e., it assigns to all required fields directly.
|
||||
///
|
||||
/// ### Example
|
||||
///
|
||||
/// ```java
|
||||
/// class Point {
|
||||
/// final double x;
|
||||
/// final double y;
|
||||
/// final double z;
|
||||
///
|
||||
/// Point(double x) {
|
||||
/// this.x = x;
|
||||
/// this.y = 0;
|
||||
/// this.z = Integer.parseInt("42");
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
/// results in the following map:
|
||||
/// ```
|
||||
/// "x" -> PsiReferenceExpression:x
|
||||
/// "y" -> PsiLiteralExpression:0
|
||||
/// "z" -> PsiMethodCallExpression:Integer.parseInt("42")
|
||||
/// ```
|
||||
///
|
||||
/// ### How it is used
|
||||
///
|
||||
/// Contents of this map are used by [RecordBuilder] to generate the correct redirecting constructor call.
|
||||
///
|
||||
/// Before:
|
||||
/// ```java
|
||||
/// Point(double x) {
|
||||
/// this.x = x;
|
||||
/// this.y = 0;
|
||||
/// this.z = Integer.parseInt("42");
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// After:
|
||||
/// ```java
|
||||
/// Point(double x) {
|
||||
/// this(x, 0, Integer.parseInt("42");
|
||||
/// }
|
||||
/// ```
|
||||
// @formatter:on
|
||||
@Override
|
||||
public @UnmodifiableView LinkedHashMap<String, PsiExpression> fieldNamesToInitializers() {
|
||||
return fieldNamesToInitializers;
|
||||
}
|
||||
|
||||
//@formatter:off Temporarily disable formatter because of bug IDEA-371809
|
||||
/// Set of statements which are not assignments to instance fields or calls to constructors.
|
||||
///
|
||||
/// ### Example
|
||||
///
|
||||
/// For the single constructor in the following class:
|
||||
///
|
||||
/// ```java
|
||||
/// class Point {
|
||||
/// final double x;
|
||||
/// final double y;
|
||||
///
|
||||
/// Point(double x, double y) {
|
||||
/// System.out.println("ctor: before fields are assigned");
|
||||
/// this.x = x;
|
||||
/// this.y = y;
|
||||
/// System.out.println("ctor: after fields are assigned");
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
/// this set holds the two [PsiExpressionStatement]s that call `System.out.println`.
|
||||
///
|
||||
/// Order is not important.
|
||||
// @formatter:on
|
||||
@Override
|
||||
public @UnmodifiableView Set<PsiStatement> otherStatements() {
|
||||
return Collections.unmodifiableSet(otherStatements);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -443,15 +571,13 @@ public final class ConvertToRecordFix implements LocalQuickFix {
|
||||
/// @param backingField The field being accessed
|
||||
/// @param isDefault Whether this is a default accessor that is redundant after conversion and can be removed
|
||||
/// @param usesRecordStyleNaming Whether the accessor uses record-style naming (method name equals field name)
|
||||
record FieldAccessorCandidate(@NotNull PsiMethod method,
|
||||
@NotNull PsiField backingField,
|
||||
boolean isDefault,
|
||||
boolean usesRecordStyleNaming) {
|
||||
private FieldAccessorCandidate(@NotNull PsiMethod accessor, @NotNull PsiField backingField, boolean recordStyleNaming) {
|
||||
@NotNullByDefault
|
||||
record FieldAccessorCandidate(PsiMethod method, PsiField backingField, boolean isDefault, boolean usesRecordStyleNaming) {
|
||||
private FieldAccessorCandidate(PsiMethod accessor, PsiField backingField, boolean recordStyleNaming) {
|
||||
this(accessor, backingField, calculateDefault(accessor, backingField), recordStyleNaming);
|
||||
}
|
||||
|
||||
private static boolean calculateDefault(@NotNull PsiMethod accessor, @NotNull PsiField backingField) {
|
||||
private static boolean calculateDefault(PsiMethod accessor, PsiField backingField) {
|
||||
if (accessor.getDocComment() != null) {
|
||||
return false;
|
||||
}
|
||||
@@ -471,9 +597,7 @@ public final class ConvertToRecordFix implements LocalQuickFix {
|
||||
* then we have to check whether the method is already marked by this annotation
|
||||
* as a compiler propagates annotations of the record components to appropriate targets automatically.
|
||||
*/
|
||||
private static boolean hasAnnotationConflict(@NotNull PsiModifierListOwner first,
|
||||
@NotNull PsiModifierListOwner second,
|
||||
@NotNull TargetType targetType) {
|
||||
private static boolean hasAnnotationConflict(PsiModifierListOwner first, PsiModifierListOwner second, TargetType targetType) {
|
||||
boolean result = false;
|
||||
for (final PsiAnnotation firstAnn : first.getAnnotations()) {
|
||||
final TargetType firstAnnTarget = AnnotationTargetUtil.findAnnotationTarget(firstAnn, targetType);
|
||||
|
||||
+30
-11
@@ -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.JavaBundle;
|
||||
import com.intellij.java.library.JavaLibraryUtil;
|
||||
import com.intellij.java.refactoring.JavaRefactoringBundle;
|
||||
import com.intellij.openapi.application.ApplicationManager;
|
||||
@@ -20,6 +21,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.JavaPsiRecordUtil;
|
||||
import com.intellij.psi.util.PropertyUtilBase;
|
||||
import com.intellij.psi.util.PsiTreeUtil;
|
||||
import com.intellij.psi.util.PsiUtil;
|
||||
@@ -32,7 +34,10 @@ import com.intellij.refactoring.util.ConflictsUtil;
|
||||
import com.intellij.refactoring.util.RefactoringUIUtil;
|
||||
import com.intellij.usageView.UsageInfo;
|
||||
import com.intellij.usageView.UsageViewDescriptor;
|
||||
import com.intellij.util.*;
|
||||
import com.intellij.util.JavaPsiConstructorUtil;
|
||||
import com.intellij.util.ObjectUtils;
|
||||
import com.intellij.util.SmartList;
|
||||
import com.intellij.util.VisibilityUtil;
|
||||
import com.intellij.util.containers.ContainerUtil;
|
||||
import com.intellij.util.containers.MultiMap;
|
||||
import com.intellij.util.containers.SmartHashSet;
|
||||
@@ -80,7 +85,7 @@ final class ConvertToRecordProcessor extends BaseRefactoringProcessor {
|
||||
|
||||
@Override
|
||||
public String getProcessedElementsHeader() {
|
||||
return JavaRefactoringBundle.message("convert.to.record.title");
|
||||
return JavaBundle.message("class.can.be.record.quick.fix");
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -190,7 +195,7 @@ final class ConvertToRecordProcessor extends BaseRefactoringProcessor {
|
||||
}
|
||||
RecordConstructorCandidate canonicalCtorCandidate = recordCandidate.getCanonicalConstructorCandidate();
|
||||
if (canonicalCtorCandidate != null) {
|
||||
PsiMethod canonicalCtor = canonicalCtorCandidate.getConstructorMethod();
|
||||
PsiMethod canonicalCtor = canonicalCtorCandidate.constructor();
|
||||
if (firstHasWeakerAccess(recordCandidate.getPsiClass(), canonicalCtor)) {
|
||||
result.add(new BrokenEncapsulationUsageInfo(canonicalCtor, JavaRefactoringBundle
|
||||
.message("convert.to.record.ctor.more.accessible",
|
||||
@@ -247,6 +252,7 @@ final class ConvertToRecordProcessor extends BaseRefactoringProcessor {
|
||||
|
||||
final PsiClass psiClass = myRecordCandidate.getPsiClass();
|
||||
final RecordConstructorCandidate canonicalCtorCandidate = myRecordCandidate.getCanonicalConstructorCandidate();
|
||||
final Map<PsiMethod, RecordConstructorCandidate> methodsToConstructorCandidates = myRecordCandidate.getMethodsToConstructorCandidates();
|
||||
final Map<PsiField, FieldAccessorCandidate> fieldToAccessorCandidateMap = myRecordCandidate.getFieldsToAccessorCandidates();
|
||||
RecordBuilder recordBuilder = new RecordBuilder(psiClass);
|
||||
PsiIdentifier classIdentifier = null;
|
||||
@@ -278,8 +284,20 @@ final class ConvertToRecordProcessor extends BaseRefactoringProcessor {
|
||||
recordBuilder.addPsiElement(nextElement);
|
||||
}
|
||||
else if (nextElement instanceof PsiMethod psiMethod) {
|
||||
if (canonicalCtorCandidate != null && psiMethod == canonicalCtorCandidate.getConstructorMethod()) {
|
||||
recordBuilder.addCanonicalCtor(canonicalCtorCandidate.getConstructorMethod());
|
||||
if (methodsToConstructorCandidates.containsKey(psiMethod)) {
|
||||
RecordConstructorCandidate constructorCandidate = methodsToConstructorCandidates.get(psiMethod);
|
||||
switch (constructorCandidate.kind()) {
|
||||
case CANONICAL -> recordBuilder.addCanonicalCtor(psiMethod);
|
||||
case DELEGATING -> recordBuilder.addCtor(psiMethod);
|
||||
case CUSTOM -> {
|
||||
if (canonicalCtorCandidate != null) {
|
||||
recordBuilder.addDelegatingCtor(canonicalCtorCandidate.constructor(),
|
||||
psiMethod,
|
||||
constructorCandidate.fieldNamesToInitializers(),
|
||||
constructorCandidate.otherStatements());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
FieldAccessorCandidate fieldAccessorCandidate = getFieldAccessorCandidate(fieldToAccessorCandidateMap, psiMethod);
|
||||
@@ -345,14 +363,15 @@ final class ConvertToRecordProcessor extends BaseRefactoringProcessor {
|
||||
place, null, null);
|
||||
}
|
||||
|
||||
/// This is needed because canonical constructor parameter names must match record component names.
|
||||
private void renameConstructorParameters() {
|
||||
RecordConstructorCandidate ctorCandidate = myRecordCandidate.getCanonicalConstructorCandidate();
|
||||
if (ctorCandidate == null) return;
|
||||
|
||||
Map<PsiElement, String> ctorParamRenames = new LinkedHashMap<>();
|
||||
List<UsageInfo> usagesToRename = new ArrayList<>();
|
||||
ctorCandidate.getCtorParamsToFields().forEach((ctorParam, field) -> {
|
||||
if (!ctorParam.getName().equals(field.getName())) {
|
||||
ctorCandidate.paramsToFields().forEach((ctorParam, field) -> {
|
||||
if (field != null && !ctorParam.getName().equals(field.getName())) {
|
||||
UsageInfo[] usages = RenameUtil.findUsages(ctorParam, field.getName(), false, false, ctorParamRenames);
|
||||
usagesToRename.addAll(Arrays.asList(usages));
|
||||
ctorParamRenames.put(ctorParam, field.getName());
|
||||
@@ -498,15 +517,14 @@ 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());
|
||||
PsiMethod canonicalCtor = ContainerUtil.find(record.getConstructors(), JavaPsiRecordUtil::isCanonicalConstructor);
|
||||
if (canonicalCtor != null) {
|
||||
PsiCodeBlock ctorBody = canonicalCtor.getBody();
|
||||
if (ctorBody != null) {
|
||||
StreamEx.of(ctorBody.getStatements()).select(PsiExpressionStatement.class)
|
||||
.filter(st -> JavaPsiConstructorUtil.isSuperConstructorCall(st.getExpression()))
|
||||
.findFirst()
|
||||
.ifPresent(st -> st.delete());
|
||||
.ifPresent(PsiElement::delete);
|
||||
}
|
||||
ConstructorSimplifier ctorSimplifier = RedundantRecordConstructorInspection.createCtorSimplifier(canonicalCtor);
|
||||
if (ctorSimplifier != null) {
|
||||
@@ -516,7 +534,8 @@ final class ConvertToRecordProcessor extends BaseRefactoringProcessor {
|
||||
}
|
||||
|
||||
private static void removeRedundantObjectMethods(@NotNull PsiClass record, @NotNull CallMatcher redundantObjectMethods) {
|
||||
ContainerUtil.filter(record.getMethods(), redundantObjectMethods::methodMatches)
|
||||
ContainerUtil
|
||||
.filter(record.getMethods(), redundantObjectMethods::methodMatches)
|
||||
.forEach(PsiMethod::delete);
|
||||
}
|
||||
|
||||
|
||||
+4
-4
@@ -12,13 +12,13 @@ import static com.intellij.openapi.util.NlsContexts.DialogMessage;
|
||||
/**
|
||||
* Marker interface to distinguish custom usage info from possible existing platform ones.
|
||||
*/
|
||||
interface ConvertToRecordUsageInfo {
|
||||
sealed interface ConvertToRecordUsageInfo permits FieldUsageInfo, RenameMethodUsageInfo, BrokenEncapsulationUsageInfo {
|
||||
}
|
||||
|
||||
/**
|
||||
* Encapsulates the field which will become less accessible the record introduces a private final field.
|
||||
*/
|
||||
class FieldUsageInfo extends UsageInfo implements ConvertToRecordUsageInfo {
|
||||
final class FieldUsageInfo extends UsageInfo implements ConvertToRecordUsageInfo {
|
||||
final PsiField myField;
|
||||
|
||||
FieldUsageInfo(@NotNull PsiField psiField, @NotNull PsiReference ref) {
|
||||
@@ -30,7 +30,7 @@ class FieldUsageInfo extends UsageInfo implements ConvertToRecordUsageInfo {
|
||||
/**
|
||||
* Encapsulates the method which will be renamed by its new name as the record specifies accessors naming.
|
||||
*/
|
||||
class RenameMethodUsageInfo extends UsageInfo implements ConvertToRecordUsageInfo {
|
||||
final class RenameMethodUsageInfo extends UsageInfo implements ConvertToRecordUsageInfo {
|
||||
final PsiMethod myMethod;
|
||||
final String myNewName;
|
||||
|
||||
@@ -44,7 +44,7 @@ class RenameMethodUsageInfo extends UsageInfo implements ConvertToRecordUsageInf
|
||||
/**
|
||||
* Encapsulates the method or the field which becomes more accessible.
|
||||
*/
|
||||
class BrokenEncapsulationUsageInfo extends UsageInfo implements ConvertToRecordUsageInfo {
|
||||
final class BrokenEncapsulationUsageInfo extends UsageInfo implements ConvertToRecordUsageInfo {
|
||||
final @DialogMessage String myErrMsg;
|
||||
|
||||
BrokenEncapsulationUsageInfo(@NotNull PsiField psiField, @NotNull @DialogMessage String errMsg) {
|
||||
|
||||
+118
-29
@@ -20,13 +20,11 @@ import com.intellij.psi.javadoc.PsiDocComment;
|
||||
import com.intellij.psi.javadoc.PsiDocTag;
|
||||
import com.intellij.util.VisibilityUtil;
|
||||
import com.intellij.util.containers.ContainerUtil;
|
||||
import com.siyeh.ig.psiutils.CommentTracker;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.StringJoiner;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
final class RecordBuilder {
|
||||
@@ -46,30 +44,123 @@ final class RecordBuilder {
|
||||
myRecordText.append("(");
|
||||
StringJoiner recordComponentsJoiner = new StringJoiner(",");
|
||||
if (canonicalCtorCandidate == null) {
|
||||
fieldToAccessorCandidateMap.forEach((field, fieldAccessor) -> {
|
||||
recordComponentsJoiner.add(generateComponentText(field, field.getType(), fieldAccessor));
|
||||
fieldToAccessorCandidateMap.forEach((field, fieldAccessorCandidate) -> {
|
||||
recordComponentsJoiner.add(generateComponentText(field.getAnnotations(), field.getName(), field.getType(), fieldAccessorCandidate));
|
||||
});
|
||||
}
|
||||
else {
|
||||
PsiMethod canonicalCtor = canonicalCtorCandidate.getConstructorMethod();
|
||||
Arrays.stream(canonicalCtor.getParameterList().getParameters())
|
||||
.map(parameter -> {
|
||||
PsiField field = canonicalCtorCandidate.getCtorParamsToFields().get(parameter);
|
||||
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);
|
||||
PsiParameter[] ctorParams = canonicalCtorCandidate.constructor().getParameterList().getParameters();
|
||||
for (PsiParameter parameter : ctorParams) {
|
||||
if (!canonicalCtorCandidate.paramsToFields().containsKey(parameter)) continue;
|
||||
PsiField field = canonicalCtorCandidate.paramsToFields().get(parameter);
|
||||
if (field == null) {
|
||||
String componentText = generateComponentText(PsiAnnotation.EMPTY_ARRAY, parameter.getName(), parameter.getType(), null);
|
||||
recordComponentsJoiner.add(componentText);
|
||||
continue;
|
||||
}
|
||||
FieldAccessorCandidate fieldAccessorCandidate = fieldToAccessorCandidateMap.get(field);
|
||||
String componentText = generateComponentText(field, parameter, fieldAccessorCandidate);
|
||||
recordComponentsJoiner.add(componentText);
|
||||
}
|
||||
}
|
||||
myRecordText.append(recordComponentsJoiner);
|
||||
myRecordText.append(")");
|
||||
}
|
||||
|
||||
void addCanonicalCtor(@NotNull PsiMethod canonicalCtor) {
|
||||
VisibilityUtil.setVisibility(canonicalCtor.getModifierList(), VisibilityUtil.getVisibilityModifier(myOriginClass.getModifierList()));
|
||||
processUncheckedExceptions(canonicalCtor);
|
||||
myRecordText.append(canonicalCtor.getText());
|
||||
void addCanonicalCtor(@NotNull PsiMethod ctor) {
|
||||
// An explicitly declared canonical constructor must provide at least as much access as the record class. See JLS 8.10.4
|
||||
VisibilityUtil.setVisibility(ctor.getModifierList(), VisibilityUtil.getVisibilityModifier(myOriginClass.getModifierList()));
|
||||
processUncheckedExceptions(ctor);
|
||||
myRecordText.append(ctor.getText());
|
||||
}
|
||||
|
||||
//@formatter:off Temporarily disable formatter because of bug IDEA-371809
|
||||
/// Converts the non-canonical, non-delegating `ctor` (that directly assigns all instance fields) to
|
||||
/// a single call to the canonical constructor.
|
||||
///
|
||||
/// A sample record with an implicit canonical constructor and a delegating constructor:
|
||||
///
|
||||
/// Before:
|
||||
///
|
||||
/// ```java
|
||||
/// class Person {
|
||||
/// final String name;
|
||||
/// final int age;
|
||||
/// final int weight;
|
||||
///
|
||||
/// Person(String name, int age, int weight) {
|
||||
/// this.name = name;
|
||||
/// this.age = age;
|
||||
/// this.weight = weight;
|
||||
/// System.out.println("Created a person!");
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// After:
|
||||
///
|
||||
/// ```java
|
||||
/// record Person(String name, int age, int weight) {
|
||||
/// Person(String name) {
|
||||
/// this(name, 42, 100);
|
||||
/// System.out.println("Created a person!");
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
//@formatter:on
|
||||
void addDelegatingCtor(@NotNull PsiMethod canonicalCtor,
|
||||
@NotNull PsiMethod ctor,
|
||||
@NotNull Map<@NotNull String, @NotNull PsiExpression> fieldNamesToInitializers,
|
||||
@NotNull Set<@NotNull PsiStatement> trailingStatements) {
|
||||
processUncheckedExceptions(ctor);
|
||||
final PsiCodeBlock body = ctor.getBody();
|
||||
assert body != null;
|
||||
|
||||
PsiStatement[] statements = body.getStatements();
|
||||
CommentTracker ct = new CommentTracker();
|
||||
for (int i = 0; i < statements.length; i++) {
|
||||
@NotNull PsiStatement statement = statements[i];
|
||||
@Nullable PsiStatement nextStatement = i < statements.length - 1 ? statements[i + 1] : null;
|
||||
|
||||
if (!trailingStatements.contains(statement)) {
|
||||
final boolean isLastAssignmentStatement = nextStatement == null || trailingStatements.contains(nextStatement);
|
||||
if (isLastAssignmentStatement) {
|
||||
PsiStatement delegatingCtorCall = createDelegatingCtorCall(canonicalCtor, fieldNamesToInitializers);
|
||||
ct.replaceAndRestoreComments(statement, delegatingCtorCall);
|
||||
}
|
||||
else {
|
||||
ct.delete(statement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
processUncheckedExceptions(ctor);
|
||||
myRecordText.append(ctor.getText());
|
||||
}
|
||||
|
||||
private static PsiStatement createDelegatingCtorCall(@NotNull PsiMethod canonicalCtor,
|
||||
@NotNull Map<@NotNull String, @NotNull PsiExpression> fieldNamesToInitializers) {
|
||||
StringBuilder delegatingCtorInvocationText = new StringBuilder();
|
||||
delegatingCtorInvocationText.append("this(");
|
||||
|
||||
List<@NotNull PsiExpression> expressionsInCorrectOrder = new ArrayList<>();
|
||||
for (PsiParameter canonicalCtorParameter : canonicalCtor.getParameterList().getParameters()) {
|
||||
PsiExpression fieldInitializerExpr = fieldNamesToInitializers.get(canonicalCtorParameter.getName());
|
||||
if (fieldInitializerExpr != null) {
|
||||
expressionsInCorrectOrder.add(fieldInitializerExpr);
|
||||
}
|
||||
}
|
||||
|
||||
delegatingCtorInvocationText.append(expressionsInCorrectOrder.stream().map(PsiExpression::getText).collect(Collectors.joining(", ")));
|
||||
delegatingCtorInvocationText.append(");");
|
||||
|
||||
PsiElementFactory factory = PsiElementFactory.getInstance(canonicalCtor.getProject());
|
||||
return factory.createStatementFromText(delegatingCtorInvocationText.toString(), canonicalCtor);
|
||||
}
|
||||
|
||||
void addCtor(@NotNull PsiMethod ctor) {
|
||||
processUncheckedExceptions(ctor);
|
||||
myRecordText.append(ctor.getText());
|
||||
}
|
||||
|
||||
void addFieldAccessor(@NotNull FieldAccessorCandidate fieldAccessorCandidate) {
|
||||
@@ -95,10 +186,8 @@ final class RecordBuilder {
|
||||
myRecordText.append(psiElement.getText());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
PsiClass build() {
|
||||
JavaDummyElement dummyElement = new JavaDummyElement(
|
||||
myRecordText.toString(), (builder, languageLevel) -> {
|
||||
@NotNull PsiClass build() {
|
||||
JavaDummyElement dummyElement = new JavaDummyElement(myRecordText.toString(), (builder, languageLevel) -> {
|
||||
new JavaParser(languageLevel).getDeclarationParser().parse(builder, DeclarationParser.Context.CLASS);
|
||||
}, LanguageLevel.JDK_16);
|
||||
DummyHolder holder = DummyHolderFactory.createHolder(myOriginClass.getManager(), dummyElement, myOriginClass);
|
||||
@@ -114,18 +203,18 @@ final class RecordBuilder {
|
||||
ctorParameter.getName() +
|
||||
"' is assigned");
|
||||
}
|
||||
// Don't use parameter.getType() directly, as type annotations may differ; prefer type annotations on the field
|
||||
// Don't use parameter.getType() directly, as kind annotations may differ; prefer kind annotations on the field
|
||||
PsiType componentType = field.getType();
|
||||
if (ctorParameter.getType() instanceof PsiEllipsisType && componentType instanceof PsiArrayType arrayType) {
|
||||
componentType = new PsiEllipsisType(arrayType.getComponentType(), arrayType.getAnnotationProvider());
|
||||
}
|
||||
return generateComponentText(field, componentType, fieldAccessorCandidate);
|
||||
return generateComponentText(field.getAnnotations(), field.getName(), componentType, fieldAccessorCandidate);
|
||||
}
|
||||
|
||||
private static @NotNull String generateComponentText(@NotNull PsiField field,
|
||||
private static @NotNull String generateComponentText(@NotNull PsiAnnotation @NotNull [] fieldAnnotations,
|
||||
@NotNull String fieldName,
|
||||
@NotNull PsiType componentType,
|
||||
@Nullable FieldAccessorCandidate fieldAccessorCandidate) {
|
||||
PsiAnnotation[] fieldAnnotations = field.getAnnotations();
|
||||
String fieldAnnotationsText = Arrays.stream(fieldAnnotations)
|
||||
.filter(anno -> !AnnotationTargetUtil.isTypeAnnotation(anno))
|
||||
.map(PsiAnnotation::getText).collect(Collectors.joining(" "));
|
||||
@@ -138,7 +227,7 @@ final class RecordBuilder {
|
||||
.map(PsiAnnotation::getText).collect(Collectors.joining(" "));
|
||||
annotationsText = accessorAnnotationsText.isEmpty() ? annotationsText : annotationsText + accessorAnnotationsText + " ";
|
||||
}
|
||||
return annotationsText + componentType.getCanonicalText(true) + " " + field.getName();
|
||||
return annotationsText + componentType.getCanonicalText(true) + " " + fieldName;
|
||||
}
|
||||
|
||||
private void processOverrideAnnotation(@NotNull PsiModifierList accessorModifiers) {
|
||||
|
||||
@@ -21,15 +21,18 @@ package com.intellij.psi;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* a JavaElementVisitor which also visits all children elements
|
||||
* in a tree pre-order, see <a href="https://en.wikipedia.org/wiki/Tree_traversal#Pre-order">Tree traversal:Pre-order</a> for details.
|
||||
* <p>
|
||||
* <b>Note</b>: This visitor handles all containing elements without consuming stack space, so it can be used even for very deep trees.
|
||||
* <b>Note 2</b>: This visitor works for source-based PSI only. Any elements implementing {@link PsiCompiledElement} will be rejected.
|
||||
*/
|
||||
/// A variant of [JavaElementVisitor] which also visits all children elements in the
|
||||
/// [pre-order fashion](https://en.wikipedia.org/wiki/Tree_traversal#Pre-order).
|
||||
///
|
||||
/// It handles all containing elements without consuming stack space, so it can be used
|
||||
/// even for very deep trees without getting a [StackOverflowError].
|
||||
///
|
||||
/// ### Note
|
||||
///
|
||||
/// This visitor works for source-based PSI only.
|
||||
/// Any elements implementing [PsiCompiledElement] are rejected, and an error is logged.
|
||||
public abstract class JavaRecursiveElementWalkingVisitor extends JavaElementVisitor implements PsiRecursiveVisitor {
|
||||
private final PsiWalkingState myWalkingState = new PsiWalkingState(this){
|
||||
private final PsiWalkingState myWalkingState = new PsiWalkingState(this) {
|
||||
@Override
|
||||
public void elementFinished(@NotNull PsiElement element) {
|
||||
JavaRecursiveElementWalkingVisitor.this.elementFinished(element);
|
||||
|
||||
@@ -36,13 +36,14 @@ public final class MethodSignatureUtil {
|
||||
return CollectionFactory.createCustomHashingStrategySet(METHOD_PARAMETERS_ERASURE_STRATEGY);
|
||||
}
|
||||
|
||||
/**
|
||||
* def: (8.4.2 Method Signature) Two method signatures m1 and m2 are override-equivalent iff either m1 is a subsignature of m2 or m2 is a subsignature of m1.
|
||||
*
|
||||
* erasure (erasure) = erasure, so we would check if erasures are equal and then check if the number of type parameters agree:
|
||||
* if signature(m1)=signature(m2), then m1.typeParams=m2.typeParams
|
||||
* if (erasure(signature(m1))=signature(m2), then m2.typeParams.length=0 and vise versa
|
||||
*/
|
||||
/// Two method signatures `m1` and `m2` are override-equivalent iff either `m1` is a subsignature of `m2`
|
||||
/// or `m2` is a subsignature of `m1`.
|
||||
///
|
||||
/// See JLS 8.4.2 Method Signature
|
||||
///
|
||||
/// `erasure (erasure) = erasure`, so we would check if erasures are equal and then check if the number of type parameters agrees:
|
||||
/// - if `signature(m1) = signature(m2)`, then `m1.typeParams=m2.typeParams`
|
||||
/// - if `erasure(signature(m1)) = signature(m2)`, then `m2.typeParams.length=0` and vice versa
|
||||
public static boolean areOverrideEquivalent(@NotNull PsiMethod method1, @NotNull PsiMethod method2) {
|
||||
final int typeParamsLength1 = method1.getTypeParameters().length;
|
||||
final int typeParamsLength2 = method2.getTypeParameters().length;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
|
||||
record R(T a, int b, int c) {
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
|
||||
record R<T extends Number>(T a, int b, int c) {
|
||||
}
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
|
||||
record Point2(double x, double y) {
|
||||
Point2(double x, double y) {
|
||||
this.x = x;
|
||||
this.y = Math.abs(y) + Math.sqrt(x);
|
||||
}
|
||||
}
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
|
||||
record Point2(double x, double y) {
|
||||
Point2(double x, double y) {
|
||||
this.y = Math.abs(x) + Math.sqrt(y);
|
||||
this.x = x;
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
|
||||
record Main(int b, int a) {
|
||||
}
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
|
||||
record Point2(double x, double y) {
|
||||
Point2(double x, double y) {
|
||||
this.x = x;
|
||||
this.y = Math.abs(x) + Math.sqrt(y);
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
record Point(double x, double y) {
|
||||
Point(double x, double y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
System.out.println("Hello I was just created");
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
|
||||
record Main(int a, int b) {
|
||||
|
||||
Main(int a) {
|
||||
this(a, 0);
|
||||
if (a > 0) {
|
||||
System.out.println("A is positive");
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
|
||||
record Main(int a, int b) {
|
||||
|
||||
Main(int a) {
|
||||
this(a, 0);
|
||||
{
|
||||
System.out.println("Some random code block");
|
||||
}
|
||||
for (int i = 0; i < 42; i++) {
|
||||
System.out.println("i is " + i);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
|
||||
/// JLS 8.10.1 says:
|
||||
///
|
||||
/// A record component may be a variable arity record component, indicated by an ellipsis following the type.
|
||||
/// At most one variable arity record component is permitted for a record class.
|
||||
/// It is a compile-time error if a variable arity record component appears anywhere in the list of record components except the last position.
|
||||
record R(int single, int... array) {
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
record Point2(double x, double y) {
|
||||
Point2(double x) {
|
||||
this(x, 0);
|
||||
}
|
||||
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
record Point2(double x, double y) {
|
||||
// Classify: non-canonical, redirect to non-canonical: this(String, String)
|
||||
Point2(String x) {
|
||||
this(x, "0");
|
||||
System.out.println("Point2(String)");
|
||||
}
|
||||
|
||||
// Classify: non-canonical, redirect to canonical: this(double, double)
|
||||
Point2(String x, String y) {
|
||||
this(Double.parseDouble(x), Double.parseDouble(y));
|
||||
System.out.println("Point2(String, String)");
|
||||
}
|
||||
|
||||
// Classify: non-canonical, redirect to canonical: this(double, double)
|
||||
Point2(double x) {
|
||||
this(x, 0);
|
||||
System.out.println("Point2(double)");
|
||||
}
|
||||
|
||||
// Classify: canonical, no redirect
|
||||
Point2 {
|
||||
System.out.println("Point2(double, double)");
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
record Point2(double x, double y) {
|
||||
Point2(double x) {
|
||||
this(x, 0);
|
||||
System.out.println("ctor 1: after fields are assigned");
|
||||
}
|
||||
|
||||
Point2(double x, double y) {
|
||||
System.out.println("ctor 2: before fields are assigned");
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
System.out.println("ctor 2: after fields are assigned");
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
record Point2(double x, double y) {
|
||||
Point2(double x) {
|
||||
this(x, 0);
|
||||
System.out.println("ctor 1: after fields are assigned");
|
||||
}
|
||||
|
||||
// [after-only] Conversion to compact constructor will be suggested by RedundantRecordConstructor inspection
|
||||
Point2(double x, double y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
System.out.println("ctor 2: after fields are assigned");
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
record Point3(double x, double y, double z) {
|
||||
Point3(double x) {
|
||||
this(x, 0);
|
||||
}
|
||||
|
||||
Point3(double x, double y) {
|
||||
this(x, y, 0);
|
||||
}
|
||||
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
record Point2(double x, double y) {
|
||||
Point2(double x) {
|
||||
this(x, 0);
|
||||
}
|
||||
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
// "Convert to record class" "true"
|
||||
record Person(String name, int age) {
|
||||
|
||||
Person(String myName, int myAge, String a) {
|
||||
this(myName, myAge);
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
record Point2(double x, double y) {
|
||||
Point2(double x) {
|
||||
this(x, 0);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
Point2 {
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
record Point2(double x, double y) {
|
||||
Point2(double x) {
|
||||
this(x, 0);
|
||||
}
|
||||
|
||||
Point2(String x) {
|
||||
this(Double.parseDouble(x), 0);
|
||||
}
|
||||
|
||||
Point2(String x, String y) {
|
||||
this(Double.parseDouble(x), Double.parseDouble(y));
|
||||
}
|
||||
|
||||
/// Classify: canonical, no redirect
|
||||
Point2 {
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
record Point2(double x, double y) {
|
||||
// Classify: non-canonical, redirect to canonical: this(double, double)
|
||||
Point2(double x) {
|
||||
this(x, 0);
|
||||
}
|
||||
|
||||
// Classify: non-canonical, redirect to canonical: this(double, double)
|
||||
Point2(String x) {
|
||||
this(Double.parseDouble(x), 0);
|
||||
}
|
||||
|
||||
// Classify: non-canonical, redirect to canonical: this(double, double)
|
||||
Point2(String x, String y) {
|
||||
this(Double.parseDouble(x), Double.parseDouble(y));
|
||||
}
|
||||
|
||||
/// Classify: canonical, no redirect
|
||||
Point2 {
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
record Point2(double x, double y) {
|
||||
Point2(double actuallyY) {
|
||||
this(0, actuallyY);
|
||||
}
|
||||
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
record Person(String name, int age) {
|
||||
Person(int age, String name) {
|
||||
this(name, age);
|
||||
}
|
||||
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
record Point2(double x, double y) {
|
||||
Point2(double x) {
|
||||
this(x, 0);
|
||||
System.out.println("After fields are assigned");
|
||||
}
|
||||
|
||||
Point2(double x, double y) {
|
||||
System.out.println("Before fields are assigned");
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
System.out.println("After fields are assigned");
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
record Point2(double x, double y) {
|
||||
Point2(double x) {
|
||||
this(x, 0);
|
||||
System.out.println("ctor 1: after fields are assigned");
|
||||
}
|
||||
|
||||
Point2 {
|
||||
System.out.println("ctor 2: before fields are assigned");
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
record Point2(double x, double y) {
|
||||
// This is an additional constructor.
|
||||
Point2(double x) {
|
||||
// Before
|
||||
this(x, 0);
|
||||
System.out.println("ctor 1: after fields are assigned");
|
||||
// After
|
||||
}
|
||||
|
||||
/// This is a canonical constructor.
|
||||
Point2 {
|
||||
// Before
|
||||
System.out.println("ctor 2: before fields are assigned");
|
||||
// In the middle
|
||||
// After
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
record Point2(double x, double y) {
|
||||
// This is an additional constructor.
|
||||
Point2(double x) {
|
||||
// Before
|
||||
// After assign X
|
||||
/* crazy */
|
||||
this(x, 0); // After assign Y
|
||||
System.out.println("ctor 1: after fields are assigned");
|
||||
// After
|
||||
}
|
||||
|
||||
/// This is a canonical constructor.
|
||||
Point2 {
|
||||
// Before
|
||||
System.out.println("ctor 2: before fields are assigned");
|
||||
// In the middle
|
||||
// After
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
record Point3(double x, double y, double z) {
|
||||
Point3(double x) {
|
||||
this(x, 0, 0);
|
||||
}
|
||||
|
||||
Point3(double x, double y) {
|
||||
this(x, y, 0);
|
||||
}
|
||||
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
record Person(String name, int age) {
|
||||
|
||||
Person(String name) {
|
||||
this(name, 42);
|
||||
}
|
||||
|
||||
Person(int age) {
|
||||
this("Unknown", age);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Target({ElementType.FIELD})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@interface Field {
|
||||
}
|
||||
|
||||
@Target({ElementType.PARAMETER})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@interface Parameter {
|
||||
}
|
||||
|
||||
@Target({ElementType.METHOD, ElementType.FIELD})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@interface FieldAndMethod {
|
||||
int value();
|
||||
}
|
||||
|
||||
record R(@Field int x, @FieldAndMethod int y) {
|
||||
R(@Parameter int x, int y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
|
||||
record Person(String name, int age) {
|
||||
|
||||
Person(String name) {
|
||||
this(name, 42);
|
||||
}
|
||||
|
||||
Person(int age) {
|
||||
this("Unknown", age);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
|
||||
record Point(double x, double y) {
|
||||
Point(double x) {
|
||||
this(x, 0);
|
||||
}
|
||||
|
||||
// During conversion to record, this constructor will become a canonical constructor, and then be removed because it's redundant.
|
||||
// Removal of a redundant canonical constructor is actually an inspection on its own: RedundantRecordConstructorInspection.
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
|
||||
record Point(double x, double y) {
|
||||
Point(double x) {
|
||||
this(x, 0);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
Point {
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
|
||||
class <caret>R {
|
||||
final T a;
|
||||
final int b;
|
||||
final int c;
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
|
||||
class <caret>R<T extends Number> {
|
||||
final T a;
|
||||
final int b;
|
||||
final int c;
|
||||
|
||||
public T getA(){
|
||||
return a;
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
// "Convert to record class" "false"
|
||||
// Reason: not implemented
|
||||
|
||||
class Point2<caret> {
|
||||
private final double x;
|
||||
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
// "Convert to record class" "false"
|
||||
// Reason: not implemented
|
||||
|
||||
class Point2<caret> {
|
||||
private final double x;
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
// "Convert to record class" "false"
|
||||
// Reason: not implemented
|
||||
|
||||
class Point2<caret> {
|
||||
private final double x;
|
||||
private final double y;
|
||||
|
||||
Point2(double first, double second) {
|
||||
this.y = Math.abs(second) + Math.sqrt(first);
|
||||
this.x = first;
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
// "Convert to record class" "false"
|
||||
// Reason: not implemented
|
||||
|
||||
class Main<caret> {
|
||||
final int a;
|
||||
final int b;
|
||||
|
||||
Main(int a, int b) {
|
||||
this.a = a + b;
|
||||
this.b = b;
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
// "Convert to record class" "false"
|
||||
// Reason: not implemented
|
||||
|
||||
class Main<caret> {
|
||||
final int a;
|
||||
final int b;
|
||||
|
||||
Main(int a, int b) {
|
||||
this.a = a + b;
|
||||
this.b = b + a;
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
// "Convert to record class" "false"
|
||||
// Reason: not implemented
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Target({ElementType.FIELD})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@interface Field1 {
|
||||
}
|
||||
|
||||
@Target({ElementType.FIELD})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@interface Field2 {
|
||||
}
|
||||
|
||||
class Main<caret> {
|
||||
@Field1 @Field2 final int a;
|
||||
final int b;
|
||||
|
||||
Main(int a, int b) {
|
||||
this.a = a + b;
|
||||
this.b = b;
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
// "Convert to record class" "false"
|
||||
// Reason: not implemented
|
||||
|
||||
class Main<caret> {
|
||||
final int a;
|
||||
final int b;
|
||||
|
||||
Main(int first, int second) {
|
||||
this.a = first + second;
|
||||
this.b = second + first;
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
// "Convert to record class" "false"
|
||||
// Reason: not implemented
|
||||
|
||||
class Main<caret> {
|
||||
final int a;
|
||||
final int b;
|
||||
|
||||
Main(int a, int b) {
|
||||
this.a = b + a;
|
||||
this.b = b;
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
|
||||
class Main<caret> {
|
||||
final int a;
|
||||
final int b;
|
||||
|
||||
Main(int a, int b) {
|
||||
this.a = b;
|
||||
this.b = a;
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
// "Convert to record class" "false"
|
||||
|
||||
class Main<caret> {
|
||||
final int a;
|
||||
final int b;
|
||||
|
||||
Main(int a, int c) {
|
||||
this.a = a;
|
||||
this.b = 0;
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
// "Convert to record class" "false"
|
||||
|
||||
class Main<caret> {
|
||||
final int a;
|
||||
final int b;
|
||||
|
||||
Main(int a, int c) {
|
||||
this.a = a;
|
||||
this.b = 0;
|
||||
System.out.println(c);
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
// "Convert to record class" "false"
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
class SomeClass {
|
||||
static class Prob<caret>lem {
|
||||
private final int myLine;
|
||||
private final @NotNull String code;
|
||||
private final int myColumn;
|
||||
|
||||
// Notice 1: order of parameters differs from order of instance fields
|
||||
// Notice 2: second parameter 'line' has incompatible type
|
||||
Problem(int column, Object line, @NotNull String code) {
|
||||
myLine = line;
|
||||
this.code = code;
|
||||
myColumn = column;
|
||||
}
|
||||
|
||||
static Problem make() {
|
||||
int columnArg = 42;
|
||||
int lineArg = 0;
|
||||
return new Problem(columnArg, lineArg, "");
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
// "Convert to record class" "false"
|
||||
// Reason: not implemented
|
||||
|
||||
class Point2<caret> {
|
||||
private final double x;
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
class <caret>R {
|
||||
final String first;
|
||||
|
||||
// red code is here
|
||||
// red code is here: duplicated parameter names
|
||||
R(String first, String first) {
|
||||
this.first = first;
|
||||
}
|
||||
|
||||
+1
-1
@@ -5,4 +5,4 @@ class <caret>R {
|
||||
R(int first, int second) {
|
||||
this.first = first;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
class Poi<caret>nt {
|
||||
final double x;
|
||||
final double y;
|
||||
|
||||
Point(double x, double y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
System.out.println("Hello I was just created");
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
|
||||
class Main<caret> {
|
||||
private final int a;
|
||||
private final int b;
|
||||
|
||||
Main(int a, int b) {
|
||||
this.a = a;
|
||||
this.b = b;
|
||||
}
|
||||
|
||||
Main(int a) {
|
||||
this.a = a;
|
||||
this.b = 0;
|
||||
if (a > 0) {
|
||||
System.out.println("A is positive");
|
||||
}
|
||||
}
|
||||
|
||||
int a() {
|
||||
return a;
|
||||
}
|
||||
|
||||
int b() {
|
||||
return b;
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
|
||||
class Main<caret> {
|
||||
private final int a;
|
||||
private final int b;
|
||||
|
||||
Main(int a, int b) {
|
||||
this.a = a;
|
||||
this.b = b;
|
||||
}
|
||||
|
||||
Main(int a) {
|
||||
this.a = a;
|
||||
this.b = 0;
|
||||
{
|
||||
System.out.println("Some random code block");
|
||||
}
|
||||
for (int i = 0; i < 42; i++) {
|
||||
System.out.println("i is " + i);
|
||||
}
|
||||
}
|
||||
|
||||
int a() {
|
||||
return a;
|
||||
}
|
||||
|
||||
int b() {
|
||||
return b;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
|
||||
/// JLS 8.10.1 says:
|
||||
///
|
||||
/// A record component may be a variable arity record component, indicated by an ellipsis following the type.
|
||||
/// At most one variable arity record component is permitted for a record class.
|
||||
/// It is a compile-time error if a variable arity record component appears anywhere in the list of record components except the last position.
|
||||
final class R<caret> {
|
||||
final int single;
|
||||
final int[] array;
|
||||
|
||||
private R(int single, int... array) {
|
||||
this.single = single;
|
||||
this.array = array;
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
class Point2<caret> {
|
||||
private final double x;
|
||||
private final double y;
|
||||
|
||||
Point2(double x) {
|
||||
this(x, 0);
|
||||
}
|
||||
|
||||
Point2(double x, double y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
class Point2<caret> {
|
||||
private final double x;
|
||||
private final double y;
|
||||
|
||||
// Classify: non-canonical, redirect to non-canonical: this(String, String)
|
||||
Point2(String x) {
|
||||
this(x, "0");
|
||||
System.out.println("Point2(String)");
|
||||
}
|
||||
|
||||
// Classify: non-canonical, redirect to canonical: this(double, double)
|
||||
Point2(String x, String y) {
|
||||
this(Double.parseDouble(x), Double.parseDouble(y));
|
||||
System.out.println("Point2(String, String)");
|
||||
}
|
||||
|
||||
// Classify: non-canonical, redirect to canonical: this(double, double)
|
||||
Point2(double x) {
|
||||
this(x, 0);
|
||||
System.out.println("Point2(double)");
|
||||
}
|
||||
|
||||
// Classify: canonical, no redirect
|
||||
Point2(double x, double y) {
|
||||
System.out.println("Point2(double, double)");
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
class Point2<caret> {
|
||||
private final double x;
|
||||
private final double y;
|
||||
|
||||
Point2(double x) {
|
||||
this(x, 0);
|
||||
System.out.println("ctor 1: after fields are assigned");
|
||||
}
|
||||
|
||||
Point2(double x, double y) {
|
||||
System.out.println("ctor 2: before fields are assigned");
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
System.out.println("ctor 2: after fields are assigned");
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
class Point2<caret> {
|
||||
private final double x;
|
||||
private final double y;
|
||||
|
||||
Point2(double x) {
|
||||
this(x, 0);
|
||||
System.out.println("ctor 1: after fields are assigned");
|
||||
}
|
||||
|
||||
// [after-only] Conversion to compact constructor will be suggested by RedundantRecordConstructor inspection
|
||||
Point2(double x, double y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
System.out.println("ctor 2: after fields are assigned");
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
class Point3<caret> {
|
||||
private final double x;
|
||||
private final double y;
|
||||
private final double z;
|
||||
|
||||
Point3(double x) {
|
||||
this(x, 0);
|
||||
}
|
||||
|
||||
Point3(double x, double y) {
|
||||
this(x, y, 0);
|
||||
}
|
||||
|
||||
Point3(double x, double y, double z) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
// "Convert to record class" "false"
|
||||
class Point3<caret> {
|
||||
private final double x;
|
||||
private final double y;
|
||||
|
||||
Point3(double x) {
|
||||
this(x, 0);
|
||||
}
|
||||
|
||||
Point3(double x, double y) {
|
||||
this(x, y, 0);
|
||||
}
|
||||
|
||||
Point3(double x, double y, double z) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z; // field 'z' doesn't exist
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
class Point2<caret> {
|
||||
private final double x;
|
||||
private final double y;
|
||||
|
||||
Point2(double x) {
|
||||
this.x = x;
|
||||
this.y = 0;
|
||||
}
|
||||
|
||||
Point2(double x, double y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
// "Convert to record class" "true"
|
||||
class Person<caret> {
|
||||
private final String name;
|
||||
private final int age;
|
||||
|
||||
Person(String myName, int myAge) {
|
||||
this.name = myName;
|
||||
this.age = myAge;
|
||||
}
|
||||
|
||||
Person(String myName, int myAge, String a) {
|
||||
this.name = myName;
|
||||
this.age = myAge;
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
class Point2<caret> {
|
||||
private final double x;
|
||||
private final double y;
|
||||
|
||||
Point2(double x) {
|
||||
this.x = x;
|
||||
this.y = 0;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
Point2(double x, double y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
class Point2<caret> {
|
||||
private final double x;
|
||||
private final double y;
|
||||
|
||||
Point2(double x) {
|
||||
this.x = x;
|
||||
this.y = 0;
|
||||
}
|
||||
|
||||
Point2(String x) {
|
||||
this.x = Double.parseDouble(x);
|
||||
this.y = 0;
|
||||
}
|
||||
|
||||
Point2(String x, String y) {
|
||||
this.x = Double.parseDouble(x);
|
||||
this.y = Double.parseDouble(y);
|
||||
}
|
||||
|
||||
/// Classify: canonical, no redirect
|
||||
Point2(double x, double y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
class Point2<caret> {
|
||||
private final double x;
|
||||
private final double y;
|
||||
|
||||
// Classify: non-canonical, redirect to canonical: this(double, double)
|
||||
Point2(double x) {
|
||||
this.x = x;
|
||||
this.y = 0;
|
||||
}
|
||||
|
||||
// Classify: non-canonical, redirect to canonical: this(double, double)
|
||||
Point2(String x) {
|
||||
this.x = Double.parseDouble(x);
|
||||
this.y = 0;
|
||||
}
|
||||
|
||||
// Classify: non-canonical, redirect to canonical: this(double, double)
|
||||
Point2(String x, String y) {
|
||||
this.x = Double.parseDouble(x);
|
||||
this.y = Double.parseDouble(y);
|
||||
}
|
||||
|
||||
/// Classify: canonical, no redirect
|
||||
Point2(double x, double y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
// "Convert to record class" "false"
|
||||
class Point2<caret> {
|
||||
private final double x;
|
||||
private final double y;
|
||||
|
||||
Point2(double x, double y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
Point2(double x, double y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
class Point2<caret> {
|
||||
private final double x;
|
||||
private final double y;
|
||||
|
||||
Point2(double actuallyY) {
|
||||
this.y = actuallyY;
|
||||
this.x = 0;
|
||||
}
|
||||
|
||||
Point2(double x, double y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
class Person<caret> {
|
||||
private final String name;
|
||||
private final int age;
|
||||
|
||||
Person(int age, String name) {
|
||||
this.age = age;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
Person(String name, int age) {
|
||||
this.name = name;
|
||||
this.age = age;
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
// "Convert to record class" "false"
|
||||
|
||||
// Converting this to record would create a new constructor (the record canonical constructor), which
|
||||
// may not be desirable.
|
||||
class Point2<caret> {
|
||||
private final double x;
|
||||
private final double y;
|
||||
|
||||
Point2(double actuallyY) {
|
||||
this.y = actuallyY;
|
||||
this.x = 0;
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
class Point2<caret> {
|
||||
private final double x;
|
||||
private final double y;
|
||||
|
||||
Point2(double x) {
|
||||
this.x = x;
|
||||
this.y = 0;
|
||||
System.out.println("After fields are assigned");
|
||||
}
|
||||
|
||||
Point2(double x, double y) {
|
||||
System.out.println("Before fields are assigned");
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
System.out.println("After fields are assigned");
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
// "Convert to record class" "false"
|
||||
class Point2<caret> {
|
||||
private final double x;
|
||||
private final double y;
|
||||
|
||||
Point2(double x) {
|
||||
// Cannot be converted with semantics preserved because JLS § 8.10.4 says:
|
||||
// The body of every non-canonical constructor in a record declaration must start with an alternate constructor invocation
|
||||
System.out.println("Before fields are assigned");
|
||||
this.x = x;
|
||||
this.y = 0;
|
||||
System.out.println("After fields are assigned");
|
||||
}
|
||||
|
||||
Point2(double x, double y) {
|
||||
System.out.println("Before fields are assigned");
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
System.out.println("After fields are assigned");
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
class Point2<caret> {
|
||||
private final double x;
|
||||
private final double y;
|
||||
|
||||
Point2(double x) {
|
||||
this.x = x;
|
||||
this.y = 0;
|
||||
System.out.println("ctor 1: after fields are assigned");
|
||||
}
|
||||
|
||||
Point2(double x, double y) {
|
||||
System.out.println("ctor 2: before fields are assigned");
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
// "Convert to record class" "false"
|
||||
class Point2<caret> {
|
||||
private final double x;
|
||||
private final double y;
|
||||
|
||||
Point2(double x) {
|
||||
// Cannot be converted because: Call to 'this()' must be first statement in constructor body
|
||||
System.out.println("ctor 1: before fields are assigned");
|
||||
this.x = x;
|
||||
this.y = 0;
|
||||
}
|
||||
|
||||
Point2(double x, double y) {
|
||||
System.out.println("ctor 2: before fields are assigned");
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
// "Convert to record class" "false"
|
||||
class Point2<caret> {
|
||||
private final double x;
|
||||
private final double y;
|
||||
|
||||
Point2(double x) {
|
||||
// Cannot be converted because: Call to 'this()' must be first statement in constructor body
|
||||
this.x = x;
|
||||
System.out.println("ctor 1: before fields are assigned");
|
||||
this.y = 0;
|
||||
}
|
||||
|
||||
Point2(double x, double y) {
|
||||
System.out.println("ctor 2: before fields are assigned");
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
class Point2<caret> {
|
||||
private final double x;
|
||||
private final double y;
|
||||
|
||||
// This is an additional constructor.
|
||||
Point2(double x) {
|
||||
// Before
|
||||
this.x = x;
|
||||
this.y = 0;
|
||||
System.out.println("ctor 1: after fields are assigned");
|
||||
// After
|
||||
}
|
||||
|
||||
/// This is a canonical constructor.
|
||||
Point2(double x, double y) {
|
||||
// Before
|
||||
System.out.println("ctor 2: before fields are assigned");
|
||||
this.x = x;
|
||||
// In the middle
|
||||
this.y = y;
|
||||
// After
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
class Point2<caret> {
|
||||
final double x;
|
||||
final double y;
|
||||
|
||||
// This is an additional constructor.
|
||||
Point2(double x) {
|
||||
// Before
|
||||
this.x = x; // After assign X
|
||||
this.y /* crazy */ = 0; // After assign Y
|
||||
System.out.println("ctor 1: after fields are assigned");
|
||||
// After
|
||||
}
|
||||
|
||||
/// This is a canonical constructor.
|
||||
Point2(double x, double y) {
|
||||
// Before
|
||||
System.out.println("ctor 2: before fields are assigned");
|
||||
this.x = x;
|
||||
// In the middle
|
||||
this.y = y;
|
||||
// After
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
class Point3<caret> {
|
||||
private final double x;
|
||||
private final double y;
|
||||
private final double z;
|
||||
|
||||
Point3(double x) {
|
||||
this.x = x;
|
||||
this.y = 0;
|
||||
this.z = 0;
|
||||
}
|
||||
|
||||
Point3(double x, double y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = 0;
|
||||
}
|
||||
|
||||
Point3(double x, double y, double z) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
class Person<caret> {
|
||||
final String name;
|
||||
final int age;
|
||||
|
||||
Person(String name, int age) {
|
||||
this.name = name;
|
||||
this.age = age;
|
||||
}
|
||||
|
||||
Person(String name) {
|
||||
this.name = name;
|
||||
this.age = 42;
|
||||
}
|
||||
|
||||
Person(int age) {
|
||||
this("Unknown", age);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Target({ElementType.FIELD})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@interface Field {
|
||||
}
|
||||
|
||||
@Target({ElementType.PARAMETER})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@interface Parameter {
|
||||
}
|
||||
|
||||
@Target({ElementType.METHOD, ElementType.FIELD})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@interface FieldAndMethod {
|
||||
int value();
|
||||
}
|
||||
|
||||
class R<caret> {
|
||||
@Field final int x;
|
||||
@FieldAndMethod final int y;
|
||||
|
||||
R(@Parameter int x, int y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
// "Convert to record class" "false"
|
||||
|
||||
class R<caret> {
|
||||
final int x;
|
||||
final int x;
|
||||
final int y;
|
||||
|
||||
R(int x, int y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
|
||||
class Person<caret> {
|
||||
final String name;
|
||||
final int age;
|
||||
|
||||
Person(String name, int age) {
|
||||
this.name = name;
|
||||
this.age = age;
|
||||
}
|
||||
|
||||
Person(String name) {
|
||||
this.name = name;
|
||||
this.age = 42;
|
||||
}
|
||||
|
||||
Person(int age) {
|
||||
this("Unknown", age);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
|
||||
class Poi<caret>nt {
|
||||
final double x;
|
||||
final double y;
|
||||
|
||||
Point(double x) {
|
||||
this.x = x;
|
||||
this.y = 0;
|
||||
}
|
||||
|
||||
// During conversion to record, this constructor will become a canonical constructor, and then be removed because it's redundant.
|
||||
// Removal of a redundant canonical constructor is actually an inspection on its own: RedundantRecordConstructorInspection.
|
||||
Point(double x, double y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
|
||||
class Poi<caret>nt {
|
||||
final double x;
|
||||
final double y;
|
||||
|
||||
Point(double x) {
|
||||
this.x = x;
|
||||
this.y = 0;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
Point(double x, double y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
// "Convert to record class" "GENERIC_ERROR_OR_WARNING"
|
||||
record R(int first) {
|
||||
|
||||
private R(int first, int second) {
|
||||
this(first + second);
|
||||
}
|
||||
}
|
||||
|
||||
class AA{
|
||||
public static void main(String[] args) {
|
||||
R r = new R(1);
|
||||
System.out.println(r.first());
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
// "Convert to record class" "GENERIC_ERROR_OR_WARNING"
|
||||
class <caret>R {
|
||||
final int first;
|
||||
|
||||
private R(int first) {
|
||||
this.first = first;
|
||||
}
|
||||
|
||||
private R(int first, int second) {
|
||||
this.first = first + second;
|
||||
}
|
||||
}
|
||||
|
||||
class AA{
|
||||
public static void main(String[] args) {
|
||||
R r = new R(1);
|
||||
System.out.println(r.first);
|
||||
}
|
||||
}
|
||||
@@ -977,6 +977,7 @@ public class PsiTreeUtil {
|
||||
* @param element root element to process
|
||||
* @param processor processor to consume elements
|
||||
* @return {@code true} if processing was not canceled ({@code Processor.execute()} method returned {@code true} for all elements).
|
||||
* @see PsiElementProcessor#execute
|
||||
*/
|
||||
@Contract("null, _ -> true")
|
||||
public static boolean processElements(@Nullable PsiElement element, @NotNull PsiElementProcessor<? super PsiElement> processor) {
|
||||
|
||||
Reference in New Issue
Block a user