mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[java-inspections] IDEA-374865 ClassCanBeRecord inspection: support JEP 513 "Flexible Constructor Bodies"
GitOrigin-RevId: a2c2b1a371904fb6d2c9394853b64defcd48156b
This commit is contained in:
committed by
intellij-monorepo-bot
parent
5fddd2c746
commit
a4c688156c
+54
-11
@@ -9,6 +9,7 @@ import com.intellij.psi.util.PsiUtil;
|
||||
import com.intellij.psi.util.TypeConversionUtil;
|
||||
import com.intellij.util.JavaPsiConstructorUtil;
|
||||
import com.intellij.util.containers.MultiMap;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.annotations.UnmodifiableView;
|
||||
@@ -19,6 +20,7 @@ import static com.intellij.psi.PsiModifier.STATIC;
|
||||
|
||||
@NotNullByDefault
|
||||
final class ConstructorBodyProcessor {
|
||||
private final PsiClass containingClass;
|
||||
private final PsiMethod constructor;
|
||||
private final Map<PsiParameter, @Nullable PsiField> paramsToFields = new HashMap<>();
|
||||
// TODO(bartekpacia): change type to SequencedMap once we move to Java 21
|
||||
@@ -29,16 +31,23 @@ final class ConstructorBodyProcessor {
|
||||
private boolean delegating = false;
|
||||
private boolean hasUnresolvedRefs = false;
|
||||
private boolean tooComplex = false;
|
||||
|
||||
/// Even if false, it doesn't necessarily mean that conversion to record isn't possible.
|
||||
///
|
||||
/// It is OK to have statements before all fields are assigned if:
|
||||
/// - this constructor is a canonical constructor, OR
|
||||
/// - (JDK 25+) this statement is in constructor prologue (see JEP 513)
|
||||
private boolean statementsBeforeAllFieldsAssigned = false;
|
||||
private final List<PsiStatement> otherStatements = new ArrayList<>();
|
||||
private final MultiMap<PsiField, PsiParameter> fieldsToParams = new MultiMap<>();
|
||||
|
||||
ConstructorBodyProcessor(PsiMethod constructor,
|
||||
List<PsiField> instanceFields) {
|
||||
this.containingClass = Objects.requireNonNull(constructor.getContainingClass(), "constructor must have containing class");
|
||||
this.constructor = constructor;
|
||||
this.instanceFields = instanceFields;
|
||||
assert constructor.getBody() != null; // The caller asserts this
|
||||
for (PsiStatement statement : constructor.getBody().getStatements()) {
|
||||
final PsiCodeBlock body = Objects.requireNonNull(constructor.getBody(), "constructor must have body");
|
||||
for (PsiStatement statement : body.getStatements()) {
|
||||
execute(statement);
|
||||
}
|
||||
|
||||
@@ -52,22 +61,35 @@ final class ConstructorBodyProcessor {
|
||||
}
|
||||
final PsiExpression expression = expressionStatement.getExpression();
|
||||
|
||||
if (expression instanceof PsiMethodCallExpression methodCallExpr) {
|
||||
if (JavaPsiConstructorUtil.isChainedConstructorCall(methodCallExpr)) {
|
||||
delegating = true;
|
||||
return;
|
||||
if (expression instanceof PsiMethodCallExpression methodCallExpr && JavaPsiConstructorUtil.isChainedConstructorCall(methodCallExpr)) {
|
||||
delegating = true;
|
||||
for (PsiExpression arg : methodCallExpr.getArgumentList().getExpressions()) {
|
||||
if (hasReferenceToContainingClass(containingClass, arg)) {
|
||||
statementsBeforeAllFieldsAssigned = true;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Is it an assignment expression to an instance field?
|
||||
// If not, then all instance variables must already be assigned.
|
||||
if (!expressionIsAssignmentToInstanceField(expression) && !delegating) {
|
||||
// It is NOT an assignment expression to an instance field.
|
||||
// This means that:
|
||||
// - all instance variables must be already initialized, OR
|
||||
// - (JDK 25+) this statement is inside early construction context, more specifically: in constructor prologue (see JEP 513).
|
||||
// This means that it must NOT use use 'this', either implicitly or explicitly, except for simple assignment statements.
|
||||
|
||||
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()) {
|
||||
if (fieldNamesToInitializers.isEmpty() && PsiUtil.isAvailable(JavaFeature.STATEMENTS_BEFORE_SUPER, statement)) {
|
||||
PsiExpression exprToConsider = expression instanceof PsiAssignmentExpression assignExpr ? assignExpr.getRExpression() : expression;
|
||||
if (hasReferenceToContainingClass(containingClass, exprToConsider)) {
|
||||
statementsBeforeAllFieldsAssigned = true;
|
||||
}
|
||||
}
|
||||
else if (fieldNamesToInitializers.size() < instanceFields.size()) {
|
||||
// If not all instance fields are assigned up to this point,
|
||||
// then this constructor cannot be converted to a non-canonical record constructor.
|
||||
statementsBeforeAllFieldsAssigned = true;
|
||||
// It is OK to have statements before all fields are assigned if this constructor is a canonical constructor.
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -225,4 +247,25 @@ final class ConstructorBodyProcessor {
|
||||
PsiElement resolved = referenceExpr.resolve();
|
||||
return resolved instanceof PsiField psiField && !psiField.hasModifierProperty(STATIC);
|
||||
}
|
||||
|
||||
private static boolean hasReferenceToContainingClass(@NotNull PsiClass containingClass, @Nullable PsiExpression expression) {
|
||||
if (expression == null) return false;
|
||||
Ref<Boolean> hasReferenceToClassUnderConstruction = new Ref<>(false);
|
||||
expression.accept(new JavaRecursiveElementWalkingVisitor() {
|
||||
@Override
|
||||
public void visitReferenceExpression(PsiReferenceExpression expression) {
|
||||
super.visitReferenceExpression(expression);
|
||||
PsiElement resolved = expression.resolve();
|
||||
if (resolved instanceof PsiField field && !field.hasModifierProperty(STATIC) && field.getContainingClass() == containingClass) {
|
||||
hasReferenceToClassUnderConstruction.set(true);
|
||||
}
|
||||
else if (resolved instanceof PsiMethod method &&
|
||||
!method.hasModifierProperty(STATIC) &&
|
||||
method.getContainingClass() == containingClass) {
|
||||
hasReferenceToClassUnderConstruction.set(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
return hasReferenceToClassUnderConstruction.get();
|
||||
}
|
||||
}
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
// "Convert to record class" "false"
|
||||
class Person<caret> {
|
||||
final String name;
|
||||
final int age;
|
||||
|
||||
Person(String name, int age) {
|
||||
this.name = name;
|
||||
this.age = age;
|
||||
}
|
||||
|
||||
Person(String name) {
|
||||
this(name, age); // javac error
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
record Point(double x, double y) {
|
||||
Point(double x, double y) {
|
||||
System.out.println("Hello I am going to be created");
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
System.out.println("Hello I was just created");
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
record Person(String name, int age) {
|
||||
|
||||
Person(String name) {
|
||||
System.out.println("age not passed");
|
||||
this(name, 0);
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
record Person(String name, int age) {
|
||||
|
||||
Person(String name) {
|
||||
System.out.println("age not passed" + name);
|
||||
this(name, 0);
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
record Person(String name, int age) {
|
||||
static int staticVar = 42;
|
||||
|
||||
static void staticMethod() {
|
||||
}
|
||||
|
||||
Person(String name) {
|
||||
System.out.println("age not passed" + staticVar);
|
||||
staticMethod();
|
||||
this(name, 0);
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
record Person(String name, int age) {
|
||||
|
||||
Person(String name) {
|
||||
System.out.println("age not passed");
|
||||
this(name, 0);
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
class Poi<caret>nt {
|
||||
final double x;
|
||||
final double y;
|
||||
|
||||
Point(double x, double y) {
|
||||
System.out.println("Hello I am going to be created");
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
System.out.println("Hello I was just created");
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
class Person<caret> {
|
||||
final String name;
|
||||
final int age;
|
||||
|
||||
Person(String name, int age) {
|
||||
this.name = name;
|
||||
this.age = age;
|
||||
}
|
||||
|
||||
Person(String name) {
|
||||
System.out.println("age not passed");
|
||||
this(name, 0);
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
class Person<caret> {
|
||||
final String name;
|
||||
final int age;
|
||||
|
||||
Person(String name, int age) {
|
||||
this.name = name;
|
||||
this.age = age;
|
||||
}
|
||||
|
||||
Person(String name) {
|
||||
System.out.println("age not passed" + name);
|
||||
this(name, 0);
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
// "Convert to record class" "false"
|
||||
class Person<caret> {
|
||||
final String name;
|
||||
final int age;
|
||||
|
||||
Person(String name, int age) {
|
||||
this.name = name;
|
||||
this.age = age;
|
||||
}
|
||||
|
||||
Person(String name) {
|
||||
System.out.println("age not passed" + this.name); // javac error: "cannot reference 'this' before superclass constructor is called"
|
||||
this(name, 0);
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
class Person<caret> {
|
||||
final String name;
|
||||
final int age;
|
||||
static int staticVar = 42;
|
||||
|
||||
static void staticMethod() {
|
||||
}
|
||||
|
||||
Person(String name, int age) {
|
||||
this.name = name;
|
||||
this.age = age;
|
||||
}
|
||||
|
||||
Person(String name) {
|
||||
System.out.println("age not passed" + staticVar);
|
||||
staticMethod();
|
||||
this(name, 0);
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
// "Convert to record class" "false"
|
||||
class Person<caret> {
|
||||
final String name;
|
||||
int age = 0;
|
||||
|
||||
Person(String name, int age) {
|
||||
this.name = name;
|
||||
this.age = age;
|
||||
}
|
||||
|
||||
Person(String name) {
|
||||
this(name, 0 + age); // javac error: "cannot reference age before supertype constructor has been called"
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
// "Convert to record class" "true-preview"
|
||||
class Person<caret> {
|
||||
final String name;
|
||||
final int age;
|
||||
|
||||
Person(String name, int age) {
|
||||
this.name = name;
|
||||
this.age = age;
|
||||
}
|
||||
|
||||
Person(String name) {
|
||||
System.out.println("age not passed");
|
||||
this.name = name;
|
||||
this.age = 0;
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
// "Convert to record class" "false"
|
||||
class Person<caret> {
|
||||
final String name;
|
||||
final int age;
|
||||
|
||||
Person(String name, int age) {
|
||||
this.name = name;
|
||||
this.age = age;
|
||||
}
|
||||
|
||||
Person(String name) {
|
||||
this.name = name;
|
||||
System.out.println("age not passed"); // cannot convert to delegating constructor call while preserving semantics
|
||||
this.age = 0;
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
// 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.java.codeInspection.classCanBeRecord;
|
||||
|
||||
import com.intellij.codeInsight.daemon.quickFix.LightQuickFixParameterizedTestCase;
|
||||
import com.intellij.codeInspection.LocalInspectionTool;
|
||||
import com.intellij.codeInspection.classCanBeRecord.ClassCanBeRecordInspection;
|
||||
import com.intellij.refactoring.BaseRefactoringProcessor;
|
||||
import com.intellij.testFramework.LightProjectDescriptor;
|
||||
import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import static com.intellij.codeInspection.classCanBeRecord.ClassCanBeRecordInspection.ConversionStrategy;
|
||||
|
||||
public class ClassCanBeRecordFlexibleConstructorBodiesTest extends LightQuickFixParameterizedTestCase {
|
||||
@Override
|
||||
protected LocalInspectionTool @NotNull [] configureLocalInspectionTools() {
|
||||
ClassCanBeRecordInspection inspection = new ClassCanBeRecordInspection(ConversionStrategy.DO_NOT_SUGGEST, true);
|
||||
return new LocalInspectionTool[]{inspection};
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NotNull LightProjectDescriptor getProjectDescriptor() {
|
||||
return LightJavaCodeInsightFixtureTestCase.JAVA_LATEST;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getBasePath() {
|
||||
return "/inspection/classCanBeRecord/flexibleConstructorBodies";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void runSingle() throws Throwable {
|
||||
try {
|
||||
super.runSingle();
|
||||
}
|
||||
catch (BaseRefactoringProcessor.ConflictsInTestsException e) {
|
||||
// Verify that no content was changed. See IDEA-371645.
|
||||
checkResultByFile(getTestName(false) + ".java", getBasePath() + "/before" + getTestName(false), false);
|
||||
}
|
||||
|
||||
|
||||
super.runSingle();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user