From f419dbbb0882285933311a4b90a75a9c7ef109c0 Mon Sep 17 00:00:00 2001 From: Bartek Pacia Date: Tue, 27 May 2025 06:53:38 +0000 Subject: [PATCH] [java-inspection] IDEA-251700 ClassCanBeRecord: support converting classes with many constructors #IDEA-251700 fixed Merge-request: IJ-MR-160901 Merged-by: Bartek Pacia GitOrigin-RevId: f0a1224846e203a3dc6638270d665354dcf14fb4 --- .../ClassCanBeRecordInspection.java | 2 +- .../ConstructorBodyProcessor.java | 234 +++++++++++ .../classCanBeRecord/ConvertToRecordFix.java | 396 ++++++++++++------ .../ConvertToRecordProcessor.java | 41 +- .../ConvertToRecordUsageInfo.java | 8 +- .../classCanBeRecord/RecordBuilder.java | 147 +++++-- .../JavaRecursiveElementWalkingVisitor.java | 19 +- .../psi/util/MethodSignatureUtil.java | 15 +- .../afterCanonicalCtorNotExist_1.java | 4 + .../afterCanonicalCtorNotExist_2.java | 4 + ...=> afterCanonicalCtorNotExist_2_anno.java} | 0 .../afterCtorParamsDifferFromFields_10.java | 8 - .../afterCtorParamsDifferFromFields_11.java | 8 - .../afterCtorParamsDifferFromFields_14.java | 4 + .../afterCtorParamsDifferFromFields_9.java | 8 - .../afterCtorWithAdditionalStatements_1.java | 8 + .../afterCtorWithAdditionalStatements_2.java | 11 + .../afterCtorWithAdditionalStatements_3.java | 14 + .../classCanBeRecord/afterEllipsis.java | 9 + ...fterMultipleConstructors_Delegating_2.java | 7 + ...structors_Delegating_2_differentTypes.java | 25 ++ ...ctors_Delegating_2_sideEffect_complex.java | 14 + ...uctors_Delegating_2_sideEffect_simple.java | 14 + ...fterMultipleConstructors_Delegating_3.java | 11 + ...Constructors_DirectFieldAssignments_2.java | 7 + ...uctors_DirectFieldAssignments_2_and_3.java | 7 + ...rs_DirectFieldAssignments_2_annotated.java | 10 + ...rectFieldAssignments_2_differentTypes.java | 18 + ...Assignments_2_differentTypes_comments.java | 21 + ...s_DirectFieldAssignments_2_misordered.java | 7 + ...ctFieldAssignments_2_misordered_ctors.java | 7 + ...FieldAssignments_2_sideEffect_complex.java | 14 + ...tFieldAssignments_2_sideEffect_simple.java | 11 + ...ignments_2_sideEffect_simple_comments.java | 18 + ...ents_2_sideEffect_simple_moreComments.java | 20 + ...Constructors_DirectFieldAssignments_3.java | 11 + .../afterMultipleConstructors_mixed_1.java | 11 + .../afterSimple_6_annotations.java | 29 ++ .../afterThreeConstructors.java | 12 + .../afterTwoConstructors.java | 10 + .../afterTwoConstructorsAnnotated.java | 11 + .../beforeCanonicalCtorNotExist_1.java | 7 + .../beforeCanonicalCtorNotExist_2.java | 11 + ...> beforeCanonicalCtorNotExist_2_anno.java} | 0 .../beforeCtorParamsDifferFromFields_10.java | 3 +- ...eforeCtorParamsDifferFromFields_11_1.java} | 3 +- ...beforeCtorParamsDifferFromFields_11_2.java | 12 + ...beforeCtorParamsDifferFromFields_12_1.java | 12 + ...beforeCtorParamsDifferFromFields_12_2.java | 12 + ...ParamsDifferFromFields_12_annotations.java | 27 ++ ...CtorParamsDifferFromFields_12_another.java | 12 + .../beforeCtorParamsDifferFromFields_13.java | 12 + .../beforeCtorParamsDifferFromFields_14.java | 11 + ...beforeCtorParamsDifferFromFields_15_1.java | 11 + ...beforeCtorParamsDifferFromFields_15_2.java | 12 + ...mFields_6_large_veryBadOrder_badTypes.java | 24 ++ .../beforeCtorParamsDifferFromFields_9.java | 3 +- .../beforeCtorParamsHaveEqualNames.java | 2 +- .../beforeCtorParamsMoreThanFields.java | 2 +- .../beforeCtorWithAdditionalStatements_1.java | 11 + .../beforeCtorWithAdditionalStatements_2.java | 27 ++ .../beforeCtorWithAdditionalStatements_3.java | 30 ++ .../classCanBeRecord/beforeEllipsis.java | 16 + ...foreMultipleConstructors_Delegating_2.java | 14 + ...structors_Delegating_2_differentTypes.java | 30 ++ ...ctors_Delegating_2_sideEffect_complex.java | 17 + ...uctors_Delegating_2_sideEffect_simple.java | 17 + ...foreMultipleConstructors_Delegating_3.java | 20 + ...ipleConstructors_Delegating_3_redCode.java | 19 + ...Constructors_DirectFieldAssignments_2.java | 15 + ...uctors_DirectFieldAssignments_2_and_3.java | 15 + ...rs_DirectFieldAssignments_2_annotated.java | 16 + ...rectFieldAssignments_2_differentTypes.java | 26 ++ ...Assignments_2_differentTypes_comments.java | 29 ++ ...rectFieldAssignments_2_duplicatedCtor.java | 15 + ...s_DirectFieldAssignments_2_misordered.java | 15 + ...ctFieldAssignments_2_misordered_ctors.java | 15 + ...nments_2_misordered_implicitCanonical.java | 13 + ...FieldAssignments_2_sideEffect_complex.java | 18 + ...ssignments_2_sideEffect_complex_notOK.java | 21 + ...tFieldAssignments_2_sideEffect_simple.java | 17 + ...ssignments_2_sideEffect_simple1_notOK.java | 18 + ...ssignments_2_sideEffect_simple2_notOK.java | 18 + ...ignments_2_sideEffect_simple_comments.java | 24 ++ ...ents_2_sideEffect_simple_moreComments.java | 24 ++ ...Constructors_DirectFieldAssignments_3.java | 24 ++ .../beforeMultipleConstructors_mixed_1.java | 19 + .../beforeSimple_6_annotations.java | 32 ++ .../beforeSimple_6_duplicatedFields.java | 12 + .../beforeThreeConstructors.java | 20 + .../beforeTwoConstructors.java | 18 + .../beforeTwoConstructorsAnnotated.java | 17 + .../afterCtorMultiple.java | 14 + .../beforeCtorMultiple.java | 19 + .../com/intellij/psi/util/PsiTreeUtil.java | 1 + 95 files changed, 1862 insertions(+), 225 deletions(-) create mode 100644 java/java-impl-inspections/src/com/intellij/codeInspection/classCanBeRecord/ConstructorBodyProcessor.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/afterCanonicalCtorNotExist_1.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/afterCanonicalCtorNotExist_2.java rename java/java-tests/testData/inspection/classCanBeRecord/{afterCanonicalCtorNotExist.java => afterCanonicalCtorNotExist_2_anno.java} (100%) delete mode 100644 java/java-tests/testData/inspection/classCanBeRecord/afterCtorParamsDifferFromFields_10.java delete mode 100644 java/java-tests/testData/inspection/classCanBeRecord/afterCtorParamsDifferFromFields_11.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/afterCtorParamsDifferFromFields_14.java delete mode 100644 java/java-tests/testData/inspection/classCanBeRecord/afterCtorParamsDifferFromFields_9.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/afterCtorWithAdditionalStatements_1.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/afterCtorWithAdditionalStatements_2.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/afterCtorWithAdditionalStatements_3.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/afterEllipsis.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_Delegating_2.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_Delegating_2_differentTypes.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_Delegating_2_sideEffect_complex.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_Delegating_2_sideEffect_simple.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_Delegating_3.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_DirectFieldAssignments_2.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_DirectFieldAssignments_2_and_3.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_DirectFieldAssignments_2_annotated.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_DirectFieldAssignments_2_differentTypes.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_DirectFieldAssignments_2_differentTypes_comments.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_DirectFieldAssignments_2_misordered.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_DirectFieldAssignments_2_misordered_ctors.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_DirectFieldAssignments_2_sideEffect_complex.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_DirectFieldAssignments_2_sideEffect_simple.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_DirectFieldAssignments_2_sideEffect_simple_comments.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_DirectFieldAssignments_2_sideEffect_simple_moreComments.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_DirectFieldAssignments_3.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_mixed_1.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/afterSimple_6_annotations.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/afterThreeConstructors.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/afterTwoConstructors.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/afterTwoConstructorsAnnotated.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/beforeCanonicalCtorNotExist_1.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/beforeCanonicalCtorNotExist_2.java rename java/java-tests/testData/inspection/classCanBeRecord/{beforeCanonicalCtorNotExist.java => beforeCanonicalCtorNotExist_2_anno.java} (100%) rename java/java-tests/testData/inspection/classCanBeRecord/{beforeCtorParamsDifferFromFields_11.java => beforeCtorParamsDifferFromFields_11_1.java} (75%) create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsDifferFromFields_11_2.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsDifferFromFields_12_1.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsDifferFromFields_12_2.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsDifferFromFields_12_annotations.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsDifferFromFields_12_another.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsDifferFromFields_13.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsDifferFromFields_14.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsDifferFromFields_15_1.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsDifferFromFields_15_2.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsDifferFromFields_6_large_veryBadOrder_badTypes.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/beforeCtorWithAdditionalStatements_1.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/beforeCtorWithAdditionalStatements_2.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/beforeCtorWithAdditionalStatements_3.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/beforeEllipsis.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_Delegating_2.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_Delegating_2_differentTypes.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_Delegating_2_sideEffect_complex.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_Delegating_2_sideEffect_simple.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_Delegating_3.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_Delegating_3_redCode.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2_and_3.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2_annotated.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2_differentTypes.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2_differentTypes_comments.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2_duplicatedCtor.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2_misordered.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2_misordered_ctors.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2_misordered_implicitCanonical.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2_sideEffect_complex.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2_sideEffect_complex_notOK.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2_sideEffect_simple.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2_sideEffect_simple1_notOK.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2_sideEffect_simple2_notOK.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2_sideEffect_simple_comments.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2_sideEffect_simple_moreComments.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_3.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_mixed_1.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/beforeSimple_6_annotations.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/beforeSimple_6_duplicatedFields.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/beforeThreeConstructors.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/beforeTwoConstructors.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/beforeTwoConstructorsAnnotated.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/weakenAccessibility/afterCtorMultiple.java create mode 100644 java/java-tests/testData/inspection/classCanBeRecord/weakenAccessibility/beforeCtorMultiple.java diff --git a/java/java-impl-inspections/src/com/intellij/codeInspection/classCanBeRecord/ClassCanBeRecordInspection.java b/java/java-impl-inspections/src/com/intellij/codeInspection/classCanBeRecord/ClassCanBeRecordInspection.java index d6084a0a9e8a..3b774bb2442f 100644 --- a/java/java-impl-inspections/src/com/intellij/codeInspection/classCanBeRecord/ClassCanBeRecordInspection.java +++ b/java/java-impl-inspections/src/com/intellij/codeInspection/classCanBeRecord/ClassCanBeRecordInspection.java @@ -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; diff --git a/java/java-impl-inspections/src/com/intellij/codeInspection/classCanBeRecord/ConstructorBodyProcessor.java b/java/java-impl-inspections/src/com/intellij/codeInspection/classCanBeRecord/ConstructorBodyProcessor.java new file mode 100644 index 000000000000..196a8eb166da --- /dev/null +++ b/java/java-impl-inspections/src/com/intellij/codeInspection/classCanBeRecord/ConstructorBodyProcessor.java @@ -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 paramsToFields = new HashMap<>(); + // TODO(bartekpacia): change type to SequencedMap once we move to Java 21 + private final LinkedHashMap fieldNamesToInitializers = new LinkedHashMap<>(); + private final List instanceFields; + + private boolean canonical = false; + private boolean delegating = false; + private boolean hasUnresolvedRefs = false; + private boolean tooComplex = false; + private boolean statementsBeforeAllFieldsAssigned = false; + private final List otherStatements = new ArrayList<>(); + private final MultiMap fieldsToParams = new MultiMap<>(); + + ConstructorBodyProcessor(PsiMethod constructor, + List 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 getParamsToFields() { + return paramsToFields; + } + + LinkedHashMap getFieldNamesToInitializers() { + return fieldNamesToInitializers; + } + + @UnmodifiableView + List getOtherStatements() { + return Collections.unmodifiableList(otherStatements); + } + + private static boolean allFieldsAssignedIn(PsiCodeBlock block, List 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); + } +} diff --git a/java/java-impl-inspections/src/com/intellij/codeInspection/classCanBeRecord/ConvertToRecordFix.java b/java/java-impl-inspections/src/com/intellij/codeInspection/classCanBeRecord/ConvertToRecordFix.java index 6075b92cae97..23e59c729ef4 100644 --- a/java/java-impl-inspections/src/com/intellij/codeInspection/classCanBeRecord/ConvertToRecordFix.java +++ b/java/java-impl-inspections/src/com/intellij/codeInspection/classCanBeRecord/ConvertToRecordFix.java @@ -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: * see the specification. */ - static RecordCandidate getClassDefinition(@NotNull PsiClass psiClass, - boolean suggestAccessorsRenaming, - @NotNull List ignoredAnnotations) { + static @Nullable RecordCandidate tryCreateRecordCandidate(@NotNull PsiClass psiClass, + boolean suggestAccessorsRenaming, + @NotNull List 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 myFieldsToAccessorCandidates = new MultiMap<>(new LinkedHashMap<>()); + private final Map myMethodsToConstructorCandidates = new HashMap<>(); private final List myOrdinaryMethods = new SmartList<>(); - final List myConstructorCandidates = new SmartList<>(); - private PsiMethod myEqualsMethod; - private PsiMethod myHashCodeMethod; + private @Nullable PsiMethod myEqualsMethod; + private @Nullable PsiMethod myHashCodeMethod; private Map 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 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 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 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 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 paramsToFields, + LinkedHashMap fieldNamesToInitializers, // TODO(bartekpacia): change type to SequencedMap once we move to Java 21 + Set otherStatements + ) { - private RecordConstructorCandidate(@NotNull PsiMethod constructor, @NotNull Set instanceFields) { - constructorMethod = constructor; - if (constructorMethod.getTypeParameters().length > 0) { - canonical = false; - return; - } - Set 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 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 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 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 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 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); diff --git a/java/java-impl-inspections/src/com/intellij/codeInspection/classCanBeRecord/ConvertToRecordProcessor.java b/java/java-impl-inspections/src/com/intellij/codeInspection/classCanBeRecord/ConvertToRecordProcessor.java index 580b530d5da3..d9ffa44c9498 100644 --- a/java/java-impl-inspections/src/com/intellij/codeInspection/classCanBeRecord/ConvertToRecordProcessor.java +++ b/java/java-impl-inspections/src/com/intellij/codeInspection/classCanBeRecord/ConvertToRecordProcessor.java @@ -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 methodsToConstructorCandidates = myRecordCandidate.getMethodsToConstructorCandidates(); final Map 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 ctorParamRenames = new LinkedHashMap<>(); List 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); } diff --git a/java/java-impl-inspections/src/com/intellij/codeInspection/classCanBeRecord/ConvertToRecordUsageInfo.java b/java/java-impl-inspections/src/com/intellij/codeInspection/classCanBeRecord/ConvertToRecordUsageInfo.java index c2bc3c1c52f2..f3866fd9dcef 100644 --- a/java/java-impl-inspections/src/com/intellij/codeInspection/classCanBeRecord/ConvertToRecordUsageInfo.java +++ b/java/java-impl-inspections/src/com/intellij/codeInspection/classCanBeRecord/ConvertToRecordUsageInfo.java @@ -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) { diff --git a/java/java-impl-inspections/src/com/intellij/codeInspection/classCanBeRecord/RecordBuilder.java b/java/java-impl-inspections/src/com/intellij/codeInspection/classCanBeRecord/RecordBuilder.java index e771133b1e06..f3cc801d0365 100644 --- a/java/java-impl-inspections/src/com/intellij/codeInspection/classCanBeRecord/RecordBuilder.java +++ b/java/java-impl-inspections/src/com/intellij/codeInspection/classCanBeRecord/RecordBuilder.java @@ -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) { diff --git a/java/java-psi-api/src/com/intellij/psi/JavaRecursiveElementWalkingVisitor.java b/java/java-psi-api/src/com/intellij/psi/JavaRecursiveElementWalkingVisitor.java index 10afb575ebb6..07dbeba51acd 100644 --- a/java/java-psi-api/src/com/intellij/psi/JavaRecursiveElementWalkingVisitor.java +++ b/java/java-psi-api/src/com/intellij/psi/JavaRecursiveElementWalkingVisitor.java @@ -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 Tree traversal:Pre-order for details. - *

- * Note: This visitor handles all containing elements without consuming stack space, so it can be used even for very deep trees. - * Note 2: 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); diff --git a/java/java-psi-api/src/com/intellij/psi/util/MethodSignatureUtil.java b/java/java-psi-api/src/com/intellij/psi/util/MethodSignatureUtil.java index d3c23ae5cd91..26363c35fe4f 100644 --- a/java/java-psi-api/src/com/intellij/psi/util/MethodSignatureUtil.java +++ b/java/java-psi-api/src/com/intellij/psi/util/MethodSignatureUtil.java @@ -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; diff --git a/java/java-tests/testData/inspection/classCanBeRecord/afterCanonicalCtorNotExist_1.java b/java/java-tests/testData/inspection/classCanBeRecord/afterCanonicalCtorNotExist_1.java new file mode 100644 index 000000000000..f5ad17fa81c8 --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/afterCanonicalCtorNotExist_1.java @@ -0,0 +1,4 @@ +// "Convert to record class" "true-preview" + +record R(T a, int b, int c) { +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/afterCanonicalCtorNotExist_2.java b/java/java-tests/testData/inspection/classCanBeRecord/afterCanonicalCtorNotExist_2.java new file mode 100644 index 000000000000..3debeb30ffeb --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/afterCanonicalCtorNotExist_2.java @@ -0,0 +1,4 @@ +// "Convert to record class" "true-preview" + +record R(T a, int b, int c) { +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/afterCanonicalCtorNotExist.java b/java/java-tests/testData/inspection/classCanBeRecord/afterCanonicalCtorNotExist_2_anno.java similarity index 100% rename from java/java-tests/testData/inspection/classCanBeRecord/afterCanonicalCtorNotExist.java rename to java/java-tests/testData/inspection/classCanBeRecord/afterCanonicalCtorNotExist_2_anno.java diff --git a/java/java-tests/testData/inspection/classCanBeRecord/afterCtorParamsDifferFromFields_10.java b/java/java-tests/testData/inspection/classCanBeRecord/afterCtorParamsDifferFromFields_10.java deleted file mode 100644 index d1f7e282c8a0..000000000000 --- a/java/java-tests/testData/inspection/classCanBeRecord/afterCtorParamsDifferFromFields_10.java +++ /dev/null @@ -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); - } -} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/afterCtorParamsDifferFromFields_11.java b/java/java-tests/testData/inspection/classCanBeRecord/afterCtorParamsDifferFromFields_11.java deleted file mode 100644 index bfe253b908cb..000000000000 --- a/java/java-tests/testData/inspection/classCanBeRecord/afterCtorParamsDifferFromFields_11.java +++ /dev/null @@ -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; - } -} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/afterCtorParamsDifferFromFields_14.java b/java/java-tests/testData/inspection/classCanBeRecord/afterCtorParamsDifferFromFields_14.java new file mode 100644 index 000000000000..7455f3a71586 --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/afterCtorParamsDifferFromFields_14.java @@ -0,0 +1,4 @@ +// "Convert to record class" "true-preview" + +record Main(int b, int a) { +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/afterCtorParamsDifferFromFields_9.java b/java/java-tests/testData/inspection/classCanBeRecord/afterCtorParamsDifferFromFields_9.java deleted file mode 100644 index fe12ce63684f..000000000000 --- a/java/java-tests/testData/inspection/classCanBeRecord/afterCtorParamsDifferFromFields_9.java +++ /dev/null @@ -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); - } -} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/afterCtorWithAdditionalStatements_1.java b/java/java-tests/testData/inspection/classCanBeRecord/afterCtorWithAdditionalStatements_1.java new file mode 100644 index 000000000000..1e828ac3749f --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/afterCtorWithAdditionalStatements_1.java @@ -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"); + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/afterCtorWithAdditionalStatements_2.java b/java/java-tests/testData/inspection/classCanBeRecord/afterCtorWithAdditionalStatements_2.java new file mode 100644 index 000000000000..2dbbed615e11 --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/afterCtorWithAdditionalStatements_2.java @@ -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"); + } + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/afterCtorWithAdditionalStatements_3.java b/java/java-tests/testData/inspection/classCanBeRecord/afterCtorWithAdditionalStatements_3.java new file mode 100644 index 000000000000..1ed61dfa1d7d --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/afterCtorWithAdditionalStatements_3.java @@ -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); + } + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/afterEllipsis.java b/java/java-tests/testData/inspection/classCanBeRecord/afterEllipsis.java new file mode 100644 index 000000000000..f40548cf45a7 --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/afterEllipsis.java @@ -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) { +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_Delegating_2.java b/java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_Delegating_2.java new file mode 100644 index 000000000000..aa315fae727d --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_Delegating_2.java @@ -0,0 +1,7 @@ +// "Convert to record class" "true-preview" +record Point2(double x, double y) { + Point2(double x) { + this(x, 0); + } + +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_Delegating_2_differentTypes.java b/java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_Delegating_2_differentTypes.java new file mode 100644 index 000000000000..895ad5b84272 --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_Delegating_2_differentTypes.java @@ -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)"); + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_Delegating_2_sideEffect_complex.java b/java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_Delegating_2_sideEffect_complex.java new file mode 100644 index 000000000000..d7035c11a303 --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_Delegating_2_sideEffect_complex.java @@ -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"); + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_Delegating_2_sideEffect_simple.java b/java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_Delegating_2_sideEffect_simple.java new file mode 100644 index 000000000000..58d90c141b93 --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_Delegating_2_sideEffect_simple.java @@ -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"); + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_Delegating_3.java b/java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_Delegating_3.java new file mode 100644 index 000000000000..cf6bcd503ef8 --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_Delegating_3.java @@ -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); + } + +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_DirectFieldAssignments_2.java b/java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_DirectFieldAssignments_2.java new file mode 100644 index 000000000000..aa315fae727d --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_DirectFieldAssignments_2.java @@ -0,0 +1,7 @@ +// "Convert to record class" "true-preview" +record Point2(double x, double y) { + Point2(double x) { + this(x, 0); + } + +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_DirectFieldAssignments_2_and_3.java b/java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_DirectFieldAssignments_2_and_3.java new file mode 100644 index 000000000000..f0e19b4aea22 --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_DirectFieldAssignments_2_and_3.java @@ -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); + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_DirectFieldAssignments_2_annotated.java b/java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_DirectFieldAssignments_2_annotated.java new file mode 100644 index 000000000000..67f80c53ab04 --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_DirectFieldAssignments_2_annotated.java @@ -0,0 +1,10 @@ +// "Convert to record class" "true-preview" +record Point2(double x, double y) { + Point2(double x) { + this(x, 0); + } + + @Deprecated + Point2 { + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_DirectFieldAssignments_2_differentTypes.java b/java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_DirectFieldAssignments_2_differentTypes.java new file mode 100644 index 000000000000..9d4fd2b56954 --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_DirectFieldAssignments_2_differentTypes.java @@ -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 { + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_DirectFieldAssignments_2_differentTypes_comments.java b/java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_DirectFieldAssignments_2_differentTypes_comments.java new file mode 100644 index 000000000000..4125d15b2ef8 --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_DirectFieldAssignments_2_differentTypes_comments.java @@ -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 { + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_DirectFieldAssignments_2_misordered.java b/java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_DirectFieldAssignments_2_misordered.java new file mode 100644 index 000000000000..72f17313c662 --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_DirectFieldAssignments_2_misordered.java @@ -0,0 +1,7 @@ +// "Convert to record class" "true-preview" +record Point2(double x, double y) { + Point2(double actuallyY) { + this(0, actuallyY); + } + +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_DirectFieldAssignments_2_misordered_ctors.java b/java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_DirectFieldAssignments_2_misordered_ctors.java new file mode 100644 index 000000000000..aa7a79d1b3bc --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_DirectFieldAssignments_2_misordered_ctors.java @@ -0,0 +1,7 @@ +// "Convert to record class" "true-preview" +record Person(String name, int age) { + Person(int age, String name) { + this(name, age); + } + +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_DirectFieldAssignments_2_sideEffect_complex.java b/java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_DirectFieldAssignments_2_sideEffect_complex.java new file mode 100644 index 000000000000..9b53bbf7e7eb --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_DirectFieldAssignments_2_sideEffect_complex.java @@ -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"); + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_DirectFieldAssignments_2_sideEffect_simple.java b/java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_DirectFieldAssignments_2_sideEffect_simple.java new file mode 100644 index 000000000000..d84521249384 --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_DirectFieldAssignments_2_sideEffect_simple.java @@ -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"); + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_DirectFieldAssignments_2_sideEffect_simple_comments.java b/java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_DirectFieldAssignments_2_sideEffect_simple_comments.java new file mode 100644 index 000000000000..d2474da667f0 --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_DirectFieldAssignments_2_sideEffect_simple_comments.java @@ -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 + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_DirectFieldAssignments_2_sideEffect_simple_moreComments.java b/java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_DirectFieldAssignments_2_sideEffect_simple_moreComments.java new file mode 100644 index 000000000000..87d7a9e76d40 --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_DirectFieldAssignments_2_sideEffect_simple_moreComments.java @@ -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 + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_DirectFieldAssignments_3.java b/java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_DirectFieldAssignments_3.java new file mode 100644 index 000000000000..7293a719a705 --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_DirectFieldAssignments_3.java @@ -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); + } + +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_mixed_1.java b/java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_mixed_1.java new file mode 100644 index 000000000000..3101715b9d4f --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/afterMultipleConstructors_mixed_1.java @@ -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); + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/afterSimple_6_annotations.java b/java/java-tests/testData/inspection/classCanBeRecord/afterSimple_6_annotations.java new file mode 100644 index 000000000000..83809eaa80df --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/afterSimple_6_annotations.java @@ -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; + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/afterThreeConstructors.java b/java/java-tests/testData/inspection/classCanBeRecord/afterThreeConstructors.java new file mode 100644 index 000000000000..53cb20b498c9 --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/afterThreeConstructors.java @@ -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); + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/afterTwoConstructors.java b/java/java-tests/testData/inspection/classCanBeRecord/afterTwoConstructors.java new file mode 100644 index 000000000000..2277e7977223 --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/afterTwoConstructors.java @@ -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. +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/afterTwoConstructorsAnnotated.java b/java/java-tests/testData/inspection/classCanBeRecord/afterTwoConstructorsAnnotated.java new file mode 100644 index 000000000000..6b56235248ee --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/afterTwoConstructorsAnnotated.java @@ -0,0 +1,11 @@ +// "Convert to record class" "true-preview" + +record Point(double x, double y) { + Point(double x) { + this(x, 0); + } + + @Deprecated + Point { + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/beforeCanonicalCtorNotExist_1.java b/java/java-tests/testData/inspection/classCanBeRecord/beforeCanonicalCtorNotExist_1.java new file mode 100644 index 000000000000..920a35ed45ca --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/beforeCanonicalCtorNotExist_1.java @@ -0,0 +1,7 @@ +// "Convert to record class" "true-preview" + +class R { + final T a; + final int b; + final int c; +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/beforeCanonicalCtorNotExist_2.java b/java/java-tests/testData/inspection/classCanBeRecord/beforeCanonicalCtorNotExist_2.java new file mode 100644 index 000000000000..770a1cd6a763 --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/beforeCanonicalCtorNotExist_2.java @@ -0,0 +1,11 @@ +// "Convert to record class" "true-preview" + +class R { + final T a; + final int b; + final int c; + + public T getA(){ + return a; + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/beforeCanonicalCtorNotExist.java b/java/java-tests/testData/inspection/classCanBeRecord/beforeCanonicalCtorNotExist_2_anno.java similarity index 100% rename from java/java-tests/testData/inspection/classCanBeRecord/beforeCanonicalCtorNotExist.java rename to java/java-tests/testData/inspection/classCanBeRecord/beforeCanonicalCtorNotExist_2_anno.java diff --git a/java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsDifferFromFields_10.java b/java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsDifferFromFields_10.java index 1e07d0a0611a..6c4d89dad54b 100644 --- a/java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsDifferFromFields_10.java +++ b/java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsDifferFromFields_10.java @@ -1,4 +1,5 @@ -// "Convert to record class" "true-preview" +// "Convert to record class" "false" +// Reason: not implemented class Point2 { private final double x; diff --git a/java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsDifferFromFields_11.java b/java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsDifferFromFields_11_1.java similarity index 75% rename from java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsDifferFromFields_11.java rename to java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsDifferFromFields_11_1.java index bb811a246834..cf58111a4c61 100644 --- a/java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsDifferFromFields_11.java +++ b/java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsDifferFromFields_11_1.java @@ -1,4 +1,5 @@ -// "Convert to record class" "true-preview" +// "Convert to record class" "false" +// Reason: not implemented class Point2 { private final double x; diff --git a/java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsDifferFromFields_11_2.java b/java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsDifferFromFields_11_2.java new file mode 100644 index 000000000000..5ab4c88885f5 --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsDifferFromFields_11_2.java @@ -0,0 +1,12 @@ +// "Convert to record class" "false" +// Reason: not implemented + +class Point2 { + private final double x; + private final double y; + + Point2(double first, double second) { + this.y = Math.abs(second) + Math.sqrt(first); + this.x = first; + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsDifferFromFields_12_1.java b/java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsDifferFromFields_12_1.java new file mode 100644 index 000000000000..dd2203151a5a --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsDifferFromFields_12_1.java @@ -0,0 +1,12 @@ +// "Convert to record class" "false" +// Reason: not implemented + +class Main { + final int a; + final int b; + + Main(int a, int b) { + this.a = a + b; + this.b = b; + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsDifferFromFields_12_2.java b/java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsDifferFromFields_12_2.java new file mode 100644 index 000000000000..023a4d36ce74 --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsDifferFromFields_12_2.java @@ -0,0 +1,12 @@ +// "Convert to record class" "false" +// Reason: not implemented + +class Main { + final int a; + final int b; + + Main(int a, int b) { + this.a = a + b; + this.b = b + a; + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsDifferFromFields_12_annotations.java b/java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsDifferFromFields_12_annotations.java new file mode 100644 index 000000000000..c2f049693b8f --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsDifferFromFields_12_annotations.java @@ -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 { + @Field1 @Field2 final int a; + final int b; + + Main(int a, int b) { + this.a = a + b; + this.b = b; + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsDifferFromFields_12_another.java b/java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsDifferFromFields_12_another.java new file mode 100644 index 000000000000..95a562a48cdc --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsDifferFromFields_12_another.java @@ -0,0 +1,12 @@ +// "Convert to record class" "false" +// Reason: not implemented + +class Main { + final int a; + final int b; + + Main(int first, int second) { + this.a = first + second; + this.b = second + first; + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsDifferFromFields_13.java b/java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsDifferFromFields_13.java new file mode 100644 index 000000000000..33de11899908 --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsDifferFromFields_13.java @@ -0,0 +1,12 @@ +// "Convert to record class" "false" +// Reason: not implemented + +class Main { + final int a; + final int b; + + Main(int a, int b) { + this.a = b + a; + this.b = b; + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsDifferFromFields_14.java b/java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsDifferFromFields_14.java new file mode 100644 index 000000000000..272441072601 --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsDifferFromFields_14.java @@ -0,0 +1,11 @@ +// "Convert to record class" "true-preview" + +class Main { + final int a; + final int b; + + Main(int a, int b) { + this.a = b; + this.b = a; + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsDifferFromFields_15_1.java b/java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsDifferFromFields_15_1.java new file mode 100644 index 000000000000..2dd08bee0cac --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsDifferFromFields_15_1.java @@ -0,0 +1,11 @@ +// "Convert to record class" "false" + +class Main { + final int a; + final int b; + + Main(int a, int c) { + this.a = a; + this.b = 0; + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsDifferFromFields_15_2.java b/java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsDifferFromFields_15_2.java new file mode 100644 index 000000000000..612d9f0363b0 --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsDifferFromFields_15_2.java @@ -0,0 +1,12 @@ +// "Convert to record class" "false" + +class Main { + final int a; + final int b; + + Main(int a, int c) { + this.a = a; + this.b = 0; + System.out.println(c); + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsDifferFromFields_6_large_veryBadOrder_badTypes.java b/java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsDifferFromFields_6_large_veryBadOrder_badTypes.java new file mode 100644 index 000000000000..8f63bd5367f3 --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsDifferFromFields_6_large_veryBadOrder_badTypes.java @@ -0,0 +1,24 @@ +// "Convert to record class" "false" +import org.jetbrains.annotations.NotNull; + +class SomeClass { + static class Problem { + 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, ""); + } + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsDifferFromFields_9.java b/java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsDifferFromFields_9.java index 3a1460b5cd2a..7b790cf42312 100644 --- a/java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsDifferFromFields_9.java +++ b/java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsDifferFromFields_9.java @@ -1,4 +1,5 @@ -// "Convert to record class" "true-preview" +// "Convert to record class" "false" +// Reason: not implemented class Point2 { private final double x; diff --git a/java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsHaveEqualNames.java b/java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsHaveEqualNames.java index e2bc156a2e17..fd6b2fefcc00 100644 --- a/java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsHaveEqualNames.java +++ b/java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsHaveEqualNames.java @@ -2,7 +2,7 @@ class R { final String first; - // red code is here + // red code is here: duplicated parameter names R(String first, String first) { this.first = first; } diff --git a/java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsMoreThanFields.java b/java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsMoreThanFields.java index 5c4d8cfd3730..67b1f4dcf2e6 100644 --- a/java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsMoreThanFields.java +++ b/java/java-tests/testData/inspection/classCanBeRecord/beforeCtorParamsMoreThanFields.java @@ -5,4 +5,4 @@ class R { R(int first, int second) { this.first = first; } -} \ No newline at end of file +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/beforeCtorWithAdditionalStatements_1.java b/java/java-tests/testData/inspection/classCanBeRecord/beforeCtorWithAdditionalStatements_1.java new file mode 100644 index 000000000000..b51dcae41b47 --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/beforeCtorWithAdditionalStatements_1.java @@ -0,0 +1,11 @@ +// "Convert to record class" "true-preview" +class Point { + 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"); + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/beforeCtorWithAdditionalStatements_2.java b/java/java-tests/testData/inspection/classCanBeRecord/beforeCtorWithAdditionalStatements_2.java new file mode 100644 index 000000000000..3ac55da494cf --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/beforeCtorWithAdditionalStatements_2.java @@ -0,0 +1,27 @@ +// "Convert to record class" "true-preview" + +class Main { + 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; + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/beforeCtorWithAdditionalStatements_3.java b/java/java-tests/testData/inspection/classCanBeRecord/beforeCtorWithAdditionalStatements_3.java new file mode 100644 index 000000000000..299f75fc850e --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/beforeCtorWithAdditionalStatements_3.java @@ -0,0 +1,30 @@ +// "Convert to record class" "true-preview" + +class Main { + 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; + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/beforeEllipsis.java b/java/java-tests/testData/inspection/classCanBeRecord/beforeEllipsis.java new file mode 100644 index 000000000000..fd8f29aaaf45 --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/beforeEllipsis.java @@ -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 { + final int single; + final int[] array; + + private R(int single, int... array) { + this.single = single; + this.array = array; + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_Delegating_2.java b/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_Delegating_2.java new file mode 100644 index 000000000000..9f9b5022edfe --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_Delegating_2.java @@ -0,0 +1,14 @@ +// "Convert to record class" "true-preview" +class Point2 { + 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; + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_Delegating_2_differentTypes.java b/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_Delegating_2_differentTypes.java new file mode 100644 index 000000000000..7ce95627e732 --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_Delegating_2_differentTypes.java @@ -0,0 +1,30 @@ +// "Convert to record class" "true-preview" +class Point2 { + 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; + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_Delegating_2_sideEffect_complex.java b/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_Delegating_2_sideEffect_complex.java new file mode 100644 index 000000000000..2822afbd03aa --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_Delegating_2_sideEffect_complex.java @@ -0,0 +1,17 @@ +// "Convert to record class" "true-preview" +class Point2 { + 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"); + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_Delegating_2_sideEffect_simple.java b/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_Delegating_2_sideEffect_simple.java new file mode 100644 index 000000000000..f457651d6c44 --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_Delegating_2_sideEffect_simple.java @@ -0,0 +1,17 @@ +// "Convert to record class" "true-preview" +class Point2 { + 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"); + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_Delegating_3.java b/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_Delegating_3.java new file mode 100644 index 000000000000..6509440b25f0 --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_Delegating_3.java @@ -0,0 +1,20 @@ +// "Convert to record class" "true-preview" +class Point3 { + 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; + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_Delegating_3_redCode.java b/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_Delegating_3_redCode.java new file mode 100644 index 000000000000..4a578f26bdea --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_Delegating_3_redCode.java @@ -0,0 +1,19 @@ +// "Convert to record class" "false" +class Point3 { + 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 + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2.java b/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2.java new file mode 100644 index 000000000000..28a236bbb5a8 --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2.java @@ -0,0 +1,15 @@ +// "Convert to record class" "true-preview" +class Point2 { + 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; + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2_and_3.java b/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2_and_3.java new file mode 100644 index 000000000000..db6fc4cdf404 --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2_and_3.java @@ -0,0 +1,15 @@ +// "Convert to record class" "true" +class Person { + 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; + } + } diff --git a/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2_annotated.java b/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2_annotated.java new file mode 100644 index 000000000000..9c28a547c935 --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2_annotated.java @@ -0,0 +1,16 @@ +// "Convert to record class" "true-preview" +class Point2 { + 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; + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2_differentTypes.java b/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2_differentTypes.java new file mode 100644 index 000000000000..15fbe37e0d87 --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2_differentTypes.java @@ -0,0 +1,26 @@ +// "Convert to record class" "true-preview" +class Point2 { + 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; + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2_differentTypes_comments.java b/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2_differentTypes_comments.java new file mode 100644 index 000000000000..aa5382f3fec7 --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2_differentTypes_comments.java @@ -0,0 +1,29 @@ +// "Convert to record class" "true-preview" +class Point2 { + 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; + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2_duplicatedCtor.java b/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2_duplicatedCtor.java new file mode 100644 index 000000000000..21669ec81d57 --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2_duplicatedCtor.java @@ -0,0 +1,15 @@ +// "Convert to record class" "false" +class Point2 { + 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; + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2_misordered.java b/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2_misordered.java new file mode 100644 index 000000000000..a05d1c001452 --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2_misordered.java @@ -0,0 +1,15 @@ +// "Convert to record class" "true-preview" +class Point2 { + 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; + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2_misordered_ctors.java b/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2_misordered_ctors.java new file mode 100644 index 000000000000..e5d3d04ee46c --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2_misordered_ctors.java @@ -0,0 +1,15 @@ +// "Convert to record class" "true-preview" +class Person { + 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; + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2_misordered_implicitCanonical.java b/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2_misordered_implicitCanonical.java new file mode 100644 index 000000000000..289fe64caf23 --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2_misordered_implicitCanonical.java @@ -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 { + private final double x; + private final double y; + + Point2(double actuallyY) { + this.y = actuallyY; + this.x = 0; + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2_sideEffect_complex.java b/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2_sideEffect_complex.java new file mode 100644 index 000000000000..1cc99b9bf0e4 --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2_sideEffect_complex.java @@ -0,0 +1,18 @@ +// "Convert to record class" "true-preview" +class Point2 { + 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"); + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2_sideEffect_complex_notOK.java b/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2_sideEffect_complex_notOK.java new file mode 100644 index 000000000000..c3d7a8151754 --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2_sideEffect_complex_notOK.java @@ -0,0 +1,21 @@ +// "Convert to record class" "false" +class Point2 { + 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"); + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2_sideEffect_simple.java b/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2_sideEffect_simple.java new file mode 100644 index 000000000000..5e299eb0cb99 --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2_sideEffect_simple.java @@ -0,0 +1,17 @@ +// "Convert to record class" "true-preview" +class Point2 { + 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; + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2_sideEffect_simple1_notOK.java b/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2_sideEffect_simple1_notOK.java new file mode 100644 index 000000000000..7fd33dbd49e7 --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2_sideEffect_simple1_notOK.java @@ -0,0 +1,18 @@ +// "Convert to record class" "false" +class Point2 { + 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; + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2_sideEffect_simple2_notOK.java b/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2_sideEffect_simple2_notOK.java new file mode 100644 index 000000000000..e6a1562f468d --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2_sideEffect_simple2_notOK.java @@ -0,0 +1,18 @@ +// "Convert to record class" "false" +class Point2 { + 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; + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2_sideEffect_simple_comments.java b/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2_sideEffect_simple_comments.java new file mode 100644 index 000000000000..94b82b1330f5 --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2_sideEffect_simple_comments.java @@ -0,0 +1,24 @@ +// "Convert to record class" "true-preview" +class Point2 { + 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 + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2_sideEffect_simple_moreComments.java b/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2_sideEffect_simple_moreComments.java new file mode 100644 index 000000000000..2323d26116c3 --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_2_sideEffect_simple_moreComments.java @@ -0,0 +1,24 @@ +// "Convert to record class" "true-preview" +class Point2 { + 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 + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_3.java b/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_3.java new file mode 100644 index 000000000000..08683afd36b7 --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_DirectFieldAssignments_3.java @@ -0,0 +1,24 @@ +// "Convert to record class" "true-preview" +class Point3 { + 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; + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_mixed_1.java b/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_mixed_1.java new file mode 100644 index 000000000000..1496384dd948 --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/beforeMultipleConstructors_mixed_1.java @@ -0,0 +1,19 @@ +// "Convert to record class" "true-preview" +class Person { + 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); + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/beforeSimple_6_annotations.java b/java/java-tests/testData/inspection/classCanBeRecord/beforeSimple_6_annotations.java new file mode 100644 index 000000000000..e7f6a287ce58 --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/beforeSimple_6_annotations.java @@ -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 { + @Field final int x; + @FieldAndMethod final int y; + + R(@Parameter int x, int y) { + this.x = x; + this.y = y; + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/beforeSimple_6_duplicatedFields.java b/java/java-tests/testData/inspection/classCanBeRecord/beforeSimple_6_duplicatedFields.java new file mode 100644 index 000000000000..3d9912fb6ae1 --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/beforeSimple_6_duplicatedFields.java @@ -0,0 +1,12 @@ +// "Convert to record class" "false" + +class R { + final int x; + final int x; + final int y; + + R(int x, int y) { + this.x = x; + this.y = y; + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/beforeThreeConstructors.java b/java/java-tests/testData/inspection/classCanBeRecord/beforeThreeConstructors.java new file mode 100644 index 000000000000..425bbd426902 --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/beforeThreeConstructors.java @@ -0,0 +1,20 @@ +// "Convert to record class" "true-preview" + +class Person { + 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); + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/beforeTwoConstructors.java b/java/java-tests/testData/inspection/classCanBeRecord/beforeTwoConstructors.java new file mode 100644 index 000000000000..e4a49c6350de --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/beforeTwoConstructors.java @@ -0,0 +1,18 @@ +// "Convert to record class" "true-preview" + +class Point { + 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; + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/beforeTwoConstructorsAnnotated.java b/java/java-tests/testData/inspection/classCanBeRecord/beforeTwoConstructorsAnnotated.java new file mode 100644 index 000000000000..840fe728d8bb --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/beforeTwoConstructorsAnnotated.java @@ -0,0 +1,17 @@ +// "Convert to record class" "true-preview" + +class Point { + 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; + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/weakenAccessibility/afterCtorMultiple.java b/java/java-tests/testData/inspection/classCanBeRecord/weakenAccessibility/afterCtorMultiple.java new file mode 100644 index 000000000000..59e9b60691fa --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/weakenAccessibility/afterCtorMultiple.java @@ -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()); + } +} diff --git a/java/java-tests/testData/inspection/classCanBeRecord/weakenAccessibility/beforeCtorMultiple.java b/java/java-tests/testData/inspection/classCanBeRecord/weakenAccessibility/beforeCtorMultiple.java new file mode 100644 index 000000000000..a09ea64dd2ae --- /dev/null +++ b/java/java-tests/testData/inspection/classCanBeRecord/weakenAccessibility/beforeCtorMultiple.java @@ -0,0 +1,19 @@ +// "Convert to record class" "GENERIC_ERROR_OR_WARNING" +class 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); + } +} diff --git a/platform/core-api/src/com/intellij/psi/util/PsiTreeUtil.java b/platform/core-api/src/com/intellij/psi/util/PsiTreeUtil.java index 83d265b3da59..65fcf791a519 100644 --- a/platform/core-api/src/com/intellij/psi/util/PsiTreeUtil.java +++ b/platform/core-api/src/com/intellij/psi/util/PsiTreeUtil.java @@ -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 processor) {