[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:
Bartek Pacia
2025-05-27 06:53:38 +00:00
committed by intellij-monorepo-bot
parent 658ab5d084
commit f419dbbb08
95 changed files with 1862 additions and 225 deletions
@@ -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;
@@ -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);
}
}
@@ -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);
@@ -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);
}
@@ -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) {
@@ -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) {
}
@@ -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);
}
}
@@ -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;
}
}
@@ -0,0 +1,4 @@
// "Convert to record class" "true-preview"
record Main(int b, int a) {
}
@@ -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);
}
}
@@ -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");
}
}
@@ -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");
}
}
}
@@ -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) {
}
@@ -0,0 +1,7 @@
// "Convert to record class" "true-preview"
record Point2(double x, double y) {
Point2(double x) {
this(x, 0);
}
}
@@ -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)");
}
}
@@ -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");
}
}
@@ -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");
}
}
@@ -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);
}
}
@@ -0,0 +1,7 @@
// "Convert to record class" "true-preview"
record Point2(double x, double y) {
Point2(double x) {
this(x, 0);
}
}
@@ -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);
}
}
@@ -0,0 +1,10 @@
// "Convert to record class" "true-preview"
record Point2(double x, double y) {
Point2(double x) {
this(x, 0);
}
@Deprecated
Point2 {
}
}
@@ -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 {
}
}
@@ -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 {
}
}
@@ -0,0 +1,7 @@
// "Convert to record class" "true-preview"
record Point2(double x, double y) {
Point2(double actuallyY) {
this(0, actuallyY);
}
}
@@ -0,0 +1,7 @@
// "Convert to record class" "true-preview"
record Person(String name, int age) {
Person(int age, String name) {
this(name, age);
}
}
@@ -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");
}
}
@@ -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");
}
}
@@ -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
}
}
@@ -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
}
}
@@ -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);
}
}
@@ -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.
}
@@ -0,0 +1,11 @@
// "Convert to record class" "true-preview"
record Point(double x, double y) {
Point(double x) {
this(x, 0);
}
@Deprecated
Point {
}
}
@@ -0,0 +1,7 @@
// "Convert to record class" "true-preview"
class <caret>R {
final T a;
final int b;
final int c;
}
@@ -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;
}
}
@@ -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,4 +1,5 @@
// "Convert to record class" "true-preview"
// "Convert to record class" "false"
// Reason: not implemented
class Point2<caret> {
private final double x;
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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);
}
}
@@ -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, "");
}
}
}
@@ -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,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;
}
@@ -5,4 +5,4 @@ class <caret>R {
R(int first, int second) {
this.first = first;
}
}
}
@@ -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");
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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");
}
}
@@ -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");
}
}
@@ -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;
}
}
@@ -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
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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");
}
}
@@ -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");
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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
}
}
@@ -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
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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());
}
}
@@ -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) {