diff --git a/java/java-impl/src/META-INF/JavaPlugin.xml b/java/java-impl/src/META-INF/JavaPlugin.xml index 3c416d95f474..8458eed7be24 100644 --- a/java/java-impl/src/META-INF/JavaPlugin.xml +++ b/java/java-impl/src/META-INF/JavaPlugin.xml @@ -233,11 +233,11 @@ - + - + + + diff --git a/plugins/devkit/resources/org/jetbrains/idea/devkit/DevKitBundle.properties b/plugins/devkit/resources/org/jetbrains/idea/devkit/DevKitBundle.properties index 9f5083dd896e..71a58be911ec 100644 --- a/plugins/devkit/resources/org/jetbrains/idea/devkit/DevKitBundle.properties +++ b/plugins/devkit/resources/org/jetbrains/idea/devkit/DevKitBundle.properties @@ -197,3 +197,7 @@ implemented.at.runtime.jamElement=JAM Element implemented at runtime line.marker.tooltip.extension.declaration={2} declaration in {3} line.marker.tooltip.extension.point.declaration={2} EP declaration in {3} inspections.usage.uelement.as.psi=Usage of UElement as PsiElement is not recommended + +inspections.rename.dialog.title=Rename Inspection Description File and Change the Short Name +inspections.rename.dialog.description=Do you want to also rename the inspection description file and change the short name?\nWarning: it may break existing inspection profiles and suppression comments. +inspections.rename.updating.short.name="Updating inspection short name..." \ No newline at end of file diff --git a/plugins/devkit/src/refactoring/InspectionClassRenameProcessor.java b/plugins/devkit/src/refactoring/InspectionClassRenameProcessor.java new file mode 100644 index 000000000000..76340b7c826e --- /dev/null +++ b/plugins/devkit/src/refactoring/InspectionClassRenameProcessor.java @@ -0,0 +1,147 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + */ +package org.jetbrains.idea.devkit.refactoring; + +import com.intellij.ide.util.PropertiesComponent; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.Result; +import com.intellij.openapi.command.WriteCommandAction; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.module.ModuleUtilCore; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.DialogWrapper; +import com.intellij.openapi.ui.Messages; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.*; +import com.intellij.psi.util.InheritanceUtil; +import com.intellij.refactoring.RefactoringFactory; +import com.intellij.refactoring.RenameRefactoring; +import com.intellij.refactoring.listeners.RefactoringElementListener; +import com.intellij.refactoring.rename.RenameJavaClassProcessor; +import com.intellij.usageView.UsageInfo; +import com.intellij.util.IncorrectOperationException; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.idea.devkit.DevKitBundle; +import org.jetbrains.idea.devkit.inspections.DescriptionType; +import org.jetbrains.idea.devkit.inspections.InspectionDescriptionInfo; +import org.jetbrains.idea.devkit.util.PsiUtil; + +//TODO support not only Java +public class InspectionClassRenameProcessor extends RenameJavaClassProcessor { + private static final String RENAME_DESCRIPTION_AND_SHORT_NAME_DO_NOT_ASK_AGAIN_OPTION = + "inspection.rename.description.and.short.name.do.not.ask.again"; + + @Override + public boolean canProcessElement(@NotNull PsiElement element) { + return isInspectionPsiClass(element); + } + + @Override + public void renameElement(PsiElement element, String newName, UsageInfo[] usages, + @Nullable RefactoringElementListener listener) throws IncorrectOperationException { + super.renameElement(element, newName, usages, listener); + + PsiClass psiClass = (PsiClass)element; + Module module = ModuleUtilCore.findModuleForPsiElement(psiClass); + if (module == null) { + return; + } + + InspectionDescriptionInfo inspectionDescriptionInfo = InspectionDescriptionInfo.create(module, psiClass); + PsiFile descriptionFile = inspectionDescriptionInfo.getDescriptionFile(); + if (descriptionFile == null) { + return; + } + VirtualFile descriptionVirtualFile = descriptionFile.getVirtualFile(); + if (descriptionVirtualFile == null) { + return; + } + + String descriptionFileExtension = StringUtil.notNullize(descriptionVirtualFile.getExtension()); + String newDescriptionFileName = StringUtil.trimEnd(newName, "Inspection"); + if (newDescriptionFileName.equals(descriptionFile.getName())) { + return; + } + String newDescriptionFileNameWithExtension = newDescriptionFileName + "." + descriptionFileExtension; + + ApplicationManager.getApplication().invokeLater(() -> { + if (!PropertiesComponent.getInstance().getBoolean(RENAME_DESCRIPTION_AND_SHORT_NAME_DO_NOT_ASK_AGAIN_OPTION)) { + int dialogExitCode = Messages.showYesNoDialog( + element.getProject(), + DevKitBundle.message("inspections.rename.dialog.description"), + DevKitBundle.message("inspections.rename.dialog.title"), + Messages.getQuestionIcon(), + new InspectionClassRenameDoNotAskOption()); + if (dialogExitCode != Messages.YES) { + return; + } + } + + //TODO it'd be better to do both atomically (and support undo) + updateShortNameMethod(inspectionDescriptionInfo.getShortNameMethod(), newDescriptionFileName); + renameDescriptionFile(descriptionFile, newDescriptionFileNameWithExtension); + }); + } + + + private static boolean isInspectionPsiClass(@NotNull PsiElement element) { + if (!(element instanceof PsiClass)) { + return false; + } + if (!PsiUtil.isPluginProject(element.getProject())) { + return false; + } + return InheritanceUtil.isInheritor((PsiClass)element, DescriptionType.INSPECTION.getClassName()); + } + + private static void updateShortNameMethod(PsiMethod shortNameMethod, String newName) { + if (shortNameMethod == null) { + // getShortName() is not overriden; probably it will be fine after changing the class name + // (otherwise InspectionDescriptionNotFoundInspection will inform about not found description). + return; + } + + PsiCodeBlock shortNameMethodBody = shortNameMethod.getBody(); + if (shortNameMethodBody == null) { + return; + } + + PsiStatement[] statements = shortNameMethodBody.getStatements(); + if (statements.length != 1 || !(statements[0] instanceof PsiReturnStatement)) { + return; + } + + PsiStatement returnStatement = statements[0]; + Project project = shortNameMethod.getProject(); + PsiElementFactory elementFactory = JavaPsiFacade.getInstance(project).getElementFactory(); + new WriteCommandAction(project, + DevKitBundle.message("inspections.rename.updating.short.name"), + shortNameMethod.getContainingFile()) { + @Override + protected void run(@NotNull Result result) { + returnStatement.replace(elementFactory.createStatementFromText("return \"" + newName + "\";", shortNameMethod)); + } + }.execute(); + } + + private static void renameDescriptionFile(PsiFile descriptionFile, String newName) { + RenameRefactoring rename = RefactoringFactory.getInstance(descriptionFile.getProject()).createRename(descriptionFile, newName); + rename.run(); + } + + + private static class InspectionClassRenameDoNotAskOption extends DialogWrapper.DoNotAskOption.Adapter { + @Override + public void rememberChoice(boolean isSelected, int exitCode) { + PropertiesComponent.getInstance().setValue(RENAME_DESCRIPTION_AND_SHORT_NAME_DO_NOT_ASK_AGAIN_OPTION, isSelected); + } + + @Override + public boolean shouldSaveOptionsOnCancel() { + return true; + } + } +}