Java: disallow making builder class from the constructor class itself (IDEA-254858)

GitOrigin-RevId: 071a0fd3c45f2b28b73b811e0d844a0f1b911372
This commit is contained in:
Bas Leijdekkers
2024-09-05 15:02:58 +00:00
committed by intellij-monorepo-bot
parent cfc65a9890
commit 2870f49e0a
4 changed files with 68 additions and 71 deletions
@@ -16,6 +16,7 @@ import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.InputValidatorEx;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.ui.Splitter;
import com.intellij.openapi.util.NlsContexts;
import com.intellij.openapi.util.NlsSafe;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.wm.IdeFocusManager;
@@ -25,6 +26,7 @@ import com.intellij.psi.codeStyle.VariableKind;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.util.PropertyUtilBase;
import com.intellij.refactoring.HelpID;
import com.intellij.refactoring.MoveDestination;
import com.intellij.refactoring.PackageWrapper;
import com.intellij.refactoring.RefactoringBundle;
import com.intellij.refactoring.move.moveClassesOrPackages.DestinationFolderComboBox;
@@ -71,7 +73,6 @@ public class ReplaceConstructorWithBuilderDialog extends RefactoringDialog {
private static final String RECENT_KEYS = "ReplaceConstructorWithBuilder.RECENT_KEYS";
private static final String SETTER_PREFIX_KEY = "ConstructorWithBuilder.SetterPrefix";
protected ReplaceConstructorWithBuilderDialog(@NotNull Project project, PsiMethod[] constructors) {
super(project, false);
myConstructors = constructors;
@@ -95,21 +96,46 @@ public class ReplaceConstructorWithBuilderDialog extends RefactoringDialog {
final String className;
final String packageName;
if (myCreateBuilderClassRadioButton.isSelected()) {
boolean createNewBuilder = myCreateBuilderClassRadioButton.isSelected();
if (createNewBuilder) {
className = myNewClassName.getText().trim();
packageName = myPackageTextField.getText().trim();
} else {
final String fqName = myExistentClassTF.getText().trim();
className = StringUtil.getShortName(fqName);
packageName = StringUtil.getPackageName(fqName);
final PsiClass builderClass = JavaPsiFacade.getInstance(myProject).findClass(StringUtil.getQualifiedName(packageName, className), GlobalSearchScope.projectScope(myProject));
if (builderClass != null && !CommonRefactoringUtil.checkReadOnlyStatus(myProject, builderClass)) return;
}
final PsiClass builderClass = JavaPsiFacade.getInstance(myProject)
.findClass(StringUtil.getQualifiedName(packageName, className), GlobalSearchScope.projectScope(myProject));
if (!createNewBuilder) {
if (builderClass == null) {
String message = JavaRefactoringBundle.message("replace.constructor.builder.error.selected.class.was.not.found",
CommonRefactoringUtil.htmlEmphasize(className));
showErrorDialog(message);
return;
}
if (builderClass == myConstructors[0].getContainingClass()) {
showErrorDialog(JavaRefactoringBundle.message("replace.constructor.builder.error.builder.class.cannot.be.the.same",
CommonRefactoringUtil.htmlEmphasize(className)));
return;
}
}
else if (builderClass != null) {
showErrorDialog(JavaRefactoringBundle.message("replace.constructor.builder.error.class.with.chosen.name.already.exist",
CommonRefactoringUtil.htmlEmphasize(className),
CommonRefactoringUtil.htmlEmphasize(packageName)));
return;
}
if (!createNewBuilder && !CommonRefactoringUtil.checkReadOnlyStatus(myProject, builderClass)) return;
MoveDestination destination =
((DestinationFolderComboBox)myDestinationCb).selectDirectory(new PackageWrapper(myConstructors[0].getManager(), packageName), false);
invokeRefactoring(new ReplaceConstructorWithBuilderProcessor(getProject(), myConstructors, myParametersMap, className, packageName,
((DestinationFolderComboBox)myDestinationCb).selectDirectory(new PackageWrapper(myConstructors[0].getManager(), packageName), false),
myCreateBuilderClassRadioButton.isSelected()));
destination, createNewBuilder));
}
private void showErrorDialog(@NlsContexts.DialogMessage String message) {
Messages.showErrorDialog(getRootPane(), message, JavaRefactoringBundle.message("replace.constructor.with.builder"));
}
@Nullable
@Override
@@ -133,10 +159,9 @@ public class ReplaceConstructorWithBuilderDialog extends RefactoringDialog {
}
private void applyNewSetterPrefix() {
final String setterPrefix = Messages.showInputDialog(myTable, JavaRefactoringBundle
.message("constructor.with.builder.new.setter.prefix.dialog.message"), JavaRefactoringBundle
.message("constructor.with.builder.rename.setters.prefix.action.name"), null,
mySetterPrefix, new MySetterPrefixInputValidator());
String message = JavaRefactoringBundle.message("constructor.with.builder.new.setter.prefix.dialog.message");
String title = JavaRefactoringBundle.message("constructor.with.builder.rename.setters.prefix.action.name");
final String setterPrefix = Messages.showInputDialog(myTable, message, title, null, mySetterPrefix, new MySetterPrefixInputValidator());
if (setterPrefix != null) {
mySetterPrefix = setterPrefix;
PropertiesComponent.getInstance(myProject).setValue(SETTER_PREFIX_KEY, setterPrefix);
@@ -207,14 +232,16 @@ public class ReplaceConstructorWithBuilderDialog extends RefactoringDialog {
}
if (myCreateBuilderClassRadioButton.isSelected()) {
final String className = myNewClassName.getText().trim();
if (className.length() == 0 || !nameHelper.isQualifiedName(className)) throw new ConfigurationException(
if (className.isEmpty()) throw new ConfigurationException(null);
if (!nameHelper.isQualifiedName(className)) throw new ConfigurationException(
JavaRefactoringBundle.message("replace.constructor.builder.error.invalid.builder.class.name", className));
final String packageName = myPackageTextField.getText().trim();
if (packageName.length() > 0 && !nameHelper.isQualifiedName(packageName)) throw new ConfigurationException(
if (!packageName.isEmpty() && !nameHelper.isQualifiedName(packageName)) throw new ConfigurationException(
JavaRefactoringBundle.message("replace.constructor.builder.error.invalid.builder.package.name", packageName));
} else {
final String qualifiedName = myExistentClassTF.getText().trim();
if (qualifiedName.length() == 0 || !nameHelper.isQualifiedName(qualifiedName)) throw new ConfigurationException(
if (qualifiedName.isEmpty()) throw new ConfigurationException(null);
if (!nameHelper.isQualifiedName(qualifiedName)) throw new ConfigurationException(
JavaRefactoringBundle.message("replace.constructor.builder.error.invalid.builder.qualified.class.name", qualifiedName));
}
}
@@ -1,4 +1,4 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.refactoring.replaceConstructorWithBuilder;
import com.intellij.ide.highlighter.JavaFileType;
@@ -8,6 +8,7 @@ import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleUtilCore;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.NlsContexts;
import com.intellij.openapi.util.NlsSafe;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.text.StringUtil;
@@ -25,6 +26,7 @@ import com.intellij.refactoring.MoveDestination;
import com.intellij.refactoring.replaceConstructorWithBuilder.usageInfo.ReplaceConstructorWithSettersChainInfo;
import com.intellij.refactoring.util.FixableUsageInfo;
import com.intellij.refactoring.util.FixableUsagesRefactoringProcessor;
import com.intellij.refactoring.util.RefactoringUIUtil;
import com.intellij.usageView.UsageInfo;
import com.intellij.usageView.UsageViewDescriptor;
import com.intellij.util.CommonJavaRefactoringUtil;
@@ -45,8 +47,8 @@ public class ReplaceConstructorWithBuilderProcessor extends FixableUsagesRefacto
private final PsiMethod[] myConstructors;
private final Map<String, ParameterData> myParametersMap;
@NotNull
private final String myClassName;
private final String myPackageName;
private final @NlsSafe String myClassName;
private final @NlsSafe String myPackageName;
private final boolean myCreateNewBuilderClass;
private final PsiElementFactory myElementFactory;
private final MoveDestination myMoveDestination;
@@ -113,7 +115,6 @@ public class ReplaceConstructorWithBuilderProcessor extends FixableUsagesRefacto
}
if (directory != null) {
final CodeStyleManager codeStyleManager = CodeStyleManager.getInstance(PsiManager.getInstance(myProject).getProject());
final PsiJavaFile reformattedFile = (PsiJavaFile)codeStyleManager.reformat(JavaCodeStyleManager.getInstance(newFile.getProject()).shortenClassReferences(newFile));
@@ -125,7 +126,6 @@ public class ReplaceConstructorWithBuilderProcessor extends FixableUsagesRefacto
@Override
protected void performRefactoring(UsageInfo @NotNull [] usageInfos) {
final JavaPsiFacade psiFacade = JavaPsiFacade.getInstance(myProject);
final PsiClass builderClass = myCreateNewBuilderClass
? createBuilderClass()
@@ -222,19 +222,12 @@ public class ReplaceConstructorWithBuilderProcessor extends FixableUsagesRefacto
final PsiMethod constructor = getWorkingConstructor();
for (PsiParameter parameter : constructor.getParameterList().getParameters()) {
final String pureParamName = styleManager.variableNameToPropertyName(parameter.getName(), VariableKind.PARAMETER);
if (buf.length() > 0) buf.append(", ");
if (!buf.isEmpty()) buf.append(", ");
buf.append(myParametersMap.get(pureParamName).getFieldName());
}
return myElementFactory.createMethodFromText("public " +
constructor.getName() +
" " +
createMethodName +
"(){\n return new " +
constructor.getName() +
"(" +
buf +
")" +
";\n}", constructor);
return myElementFactory.createMethodFromText("public " + constructor.getName() + " " + createMethodName + "(){" +
"\n return new " + constructor.getName() + "(" + buf + ");" +
"\n}", constructor);
}
private PsiMethod getWorkingConstructor() {
@@ -284,30 +277,19 @@ public class ReplaceConstructorWithBuilderProcessor extends FixableUsagesRefacto
return "create" + StringUtil.capitalize(myConstructors[0].getName());
}
@Override
protected boolean preprocessUsages(@NotNull Ref<UsageInfo[]> refUsages) {
final MultiMap<PsiElement, String> conflicts = new MultiMap<>();
final JavaPsiFacade psiFacade = JavaPsiFacade.getInstance(myProject);
PsiClass anchor = myConstructors[0].getContainingClass();
final PsiClass builderClass =
psiFacade.findClass(StringUtil.getQualifiedName(myPackageName, myClassName), GlobalSearchScope.projectScope(myProject));
if (builderClass == null) {
if (!myCreateNewBuilderClass) {
conflicts.putValue(anchor, JavaRefactoringBundle.message("replace.constructor.builder.error.selected.class.was.not.found", myClassName));
}
} else if (myCreateNewBuilderClass){
conflicts.putValue(builderClass,
JavaRefactoringBundle.message("replace.constructor.builder.error.class.with.chosen.name.already.exist"));
}
final MultiMap<PsiElement, @NlsContexts.DialogMessage String> conflicts = new MultiMap<>();
if (myMoveDestination != null && myCreateNewBuilderClass) {
myMoveDestination.analyzeModuleConflicts(Collections.emptyList(), conflicts, refUsages.get());
}
final PsiMethod commonConstructor = getMostCommonConstructor();
if (commonConstructor == null) {
conflicts.putValue(anchor, JavaRefactoringBundle.message("replace.constructor.builder.error.no.constructor.chain"));
PsiClass containingClass = myConstructors[0].getContainingClass();
assert containingClass != null;
conflicts.putValue(containingClass, JavaRefactoringBundle.message("replace.constructor.builder.error.no.constructor.chain",
RefactoringUIUtil.getDescription(containingClass, false)));
}
return showConflicts(conflicts, refUsages.get());
@@ -1,19 +1,4 @@
/*
* Copyright 2000-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.java.refactoring;
import com.intellij.JavaTestUtil;
@@ -68,7 +53,7 @@ public class ReplaceConstructorWithBuilderTest extends LightMultiFileTestCase {
}
public void testConstructorTree() {
doTest(true, null, "Found constructors are not reducible to simple chain");
doTest(true, null, "Constructors of class <b><code>Test</code></b> do not form a simple chain.");
}
public void testGenerics() {
@@ -117,13 +102,15 @@ public class ReplaceConstructorWithBuilderTest extends LightMultiFileTestCase {
try {
new ReplaceConstructorWithBuilderProcessor(getProject(), constructors, map, "Builder", packageName, null, createNewBuilderClass).run();
if (conflicts != null) {
fail("Conflicts were not detected:" + conflicts);
fail("Conflicts were not detected: " + conflicts);
}
}
catch (BaseRefactoringProcessor.ConflictsInTestsException e) {
if (conflicts == null) {
fail("Conflict detected:" + e.getMessage());
fail("Conflict detected: " + e.getMessage());
}
else if (!conflicts.equals(e.getMessage())) {
fail("Conflicts do not match. Expected:\n" + conflicts + "\nActual:\n" + e.getMessage());
}
}
});
@@ -765,16 +765,17 @@ replace.constructor.builder.setter.name.table.title=Setter Name
replace.constructor.builder.field.name.table.title=Field Name
replace.constructor.builder.parameter.table.title=Parameter
replace.constructor.builder.select.builder.class.chooser.title=Select Builder Class
replace.constructor.builder.error.invalid.builder.qualified.class.name=''{0}'' is invalid builder qualified class name
replace.constructor.builder.error.invalid.builder.package.name=''{0}'' is invalid builder package name
replace.constructor.builder.error.invalid.builder.class.name=''{0}'' is invalid builder class name
replace.constructor.builder.error.invalid.builder.qualified.class.name=''{0}'' is not a valid fully qualified class name
replace.constructor.builder.error.invalid.builder.package.name=''{0}'' is not a valid package name
replace.constructor.builder.error.invalid.builder.class.name=''{0}'' is not a valid class name
replace.constructor.builder.error.builder.class.cannot.be.the.same=Class {0} cannot be its own builder class.
replace.constructor.builder.error.invalid.setter.name=''{0}'' is not a valid setter name
replace.constructor.builder.error.invalid.field.name=''{0}'' is not a valid field name
replace.constructor.builder.error.no.constructors=Current class doesn't have constructors to replace with builder.
replace.constructor.builder.error.caret.position=The caret should be positioned inside a class whose constructors are to be replaced with builder.
replace.constructor.builder.error.no.constructor.chain=Found constructors are not reducible to a simple chain
replace.constructor.builder.error.class.with.chosen.name.already.exist=A class with the selected name already exist.
replace.constructor.builder.error.selected.class.was.not.found=Selected builder class ''{0}'' was not found.
replace.constructor.builder.error.no.constructor.chain=Constructors of {0} do not form a simple chain.
replace.constructor.builder.error.class.with.chosen.name.already.exist=Class {0} already exists in package {1}.
replace.constructor.builder.error.selected.class.was.not.found=Existing builder class {0} not found.
replace.constructor.factory.error.invalid.factory.method.name=''{0}'' not a valid factory method name
replace.constructor.factory.error.factory.method.already.exists=Factory method {0} already exists and will be used instead of the newly created.
java.safe.delete.empty.callee.text=Callee text would be shown here