From ad3c351ee50363079dac01848732e08ac9ca635f Mon Sep 17 00:00:00 2001 From: Anna Kozlova Date: Fri, 1 Oct 2021 20:05:30 +0200 Subject: [PATCH] [java] fix to move a class with unresolved reference on the test library from production to the test root (IDEA-278923) GitOrigin-RevId: d7c792d91997dd2ab6311783ddefe75ef7d4c913 --- .../daemon/impl/quickfix/OrderEntryFix.java | 43 +++++- .../codeInspection/MoveToTestRootFix.java | 145 ++++++++++++++++++ .../resources/messages/JavaBundle.properties | 2 + 3 files changed, 183 insertions(+), 7 deletions(-) create mode 100644 java/java-impl/src/com/intellij/codeInspection/MoveToTestRootFix.java diff --git a/java/java-impl/src/com/intellij/codeInsight/daemon/impl/quickfix/OrderEntryFix.java b/java/java-impl/src/com/intellij/codeInsight/daemon/impl/quickfix/OrderEntryFix.java index 230c73909a31..52b8c3c4349f 100644 --- a/java/java-impl/src/com/intellij/codeInsight/daemon/impl/quickfix/OrderEntryFix.java +++ b/java/java-impl/src/com/intellij/codeInsight/daemon/impl/quickfix/OrderEntryFix.java @@ -6,7 +6,9 @@ import com.intellij.codeInsight.daemon.impl.actions.AddImportAction; import com.intellij.codeInsight.daemon.quickFix.ExternalLibraryResolver; import com.intellij.codeInsight.daemon.quickFix.ExternalLibraryResolver.ExternalClassResolveResult; import com.intellij.codeInsight.intention.IntentionAction; +import com.intellij.codeInsight.intention.PriorityAction; import com.intellij.codeInspection.LocalQuickFix; +import com.intellij.codeInspection.MoveToTestRootFix; import com.intellij.codeInspection.ProblemDescriptor; import com.intellij.lang.java.JavaLanguage; import com.intellij.openapi.editor.Editor; @@ -141,6 +143,7 @@ public abstract class OrderEntryFix implements IntentionAction, LocalQuickFix { Map librariesToAdd = new HashMap<>(); Set jars = new HashSet<>(); Set excluded = new HashSet<>(); + Set withTestScope = new HashSet<>(); ModuleFileIndex moduleFileIndex = ModuleRootManager.getInstance(currentModule).getFileIndex(); for (PsiClass aClass : allowedDependencies) { if (!facade.getResolveHelper().isAccessible(aClass, psiElement, aClass)) continue; @@ -166,11 +169,16 @@ public abstract class OrderEntryFix implements IntentionAction, LocalQuickFix { continue; } OrderEntry entryForFile = moduleFileIndex.getOrderEntryForFile(virtualFile); - if (entryForFile != null && - !(entryForFile instanceof ExportableOrderEntry && - ((ExportableOrderEntry)entryForFile).getScope() == DependencyScope.TEST && - !moduleFileIndex.isInTestSourceContent(refVFile))) { - excluded.add(library); + if (entryForFile != null) { + boolean testScopeLibraryInProduction = entryForFile instanceof ExportableOrderEntry && + ((ExportableOrderEntry)entryForFile).getScope() == DependencyScope.TEST && + !moduleFileIndex.isInTestSourceContent(refVFile); + if (testScopeLibraryInProduction) { + withTestScope.add(library); + } + else { + excluded.add(library); + } } } } @@ -178,10 +186,30 @@ public abstract class OrderEntryFix implements IntentionAction, LocalQuickFix { excluded.forEach(librariesToAdd::remove); if (!librariesToAdd.isEmpty()) { - OrderEntryFix fix = new AddLibraryDependencyFix(reference, currentModule, librariesToAdd, scope, false); + class AddLibraryFix extends AddLibraryDependencyFix implements PriorityAction { + AddLibraryFix(PsiReference reference, + Module currentModule, + Map libraries, DependencyScope scope, boolean exported) { + super(reference, currentModule, libraries, scope, exported); + } + @Override + public @NotNull Priority getPriority() { + return withTestScope.isEmpty() ? Priority.NORMAL : Priority.LOW; + } + } + OrderEntryFix fix = new AddLibraryFix(reference, currentModule, librariesToAdd, scope, false); registrar.register(fix); result.add(fix); } + + if (!withTestScope.isEmpty()) { + MoveToTestRootFix fix = new MoveToTestRootFix(containingFile); + if (fix.isAvailable(containingFile)) { + registrar.register(fix); + result.add(fix); + } + } + } return result; @@ -262,7 +290,8 @@ public abstract class OrderEntryFix implements IntentionAction, LocalQuickFix { facade.findClass(resolveResult.getQualifiedClassName(), currentModule.getModuleWithDependenciesAndLibrariesScope(true)) == null) { fix = new AddExtLibraryDependencyFix(reference, currentModule, resolveResult.getLibrary(), scope, resolveResult.getQualifiedClassName()); } - else if (!fullReferenceText.equals(shortReferenceName)) { + else if (!fullReferenceText.equals(shortReferenceName) && + facade.findClass(fullReferenceText, currentModule.getModuleWithDependenciesAndLibrariesScope(true)) == null) { ExternalLibraryDescriptor descriptor = resolver.resolvePackage(fullReferenceText); if (descriptor != null) { fix = new AddExtLibraryDependencyFix(reference, currentModule, descriptor, scope, null); diff --git a/java/java-impl/src/com/intellij/codeInspection/MoveToTestRootFix.java b/java/java-impl/src/com/intellij/codeInspection/MoveToTestRootFix.java new file mode 100644 index 000000000000..3e4bf34653ec --- /dev/null +++ b/java/java-impl/src/com/intellij/codeInspection/MoveToTestRootFix.java @@ -0,0 +1,145 @@ +// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.intellij.codeInspection; + +import com.intellij.CommonBundle; +import com.intellij.codeInsight.FileModificationService; +import com.intellij.codeInspection.util.IntentionName; +import com.intellij.ide.util.DirectoryChooser; +import com.intellij.java.JavaBundle; +import com.intellij.java.refactoring.JavaRefactoringBundle; +import com.intellij.openapi.application.WriteAction; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.module.ModuleUtilCore; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.roots.ContentFolder; +import com.intellij.openapi.roots.ModuleFileIndex; +import com.intellij.openapi.roots.ModuleRootManager; +import com.intellij.openapi.roots.SourceFolder; +import com.intellij.openapi.ui.Messages; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.*; +import com.intellij.psi.util.PsiUtilCore; +import com.intellij.refactoring.PackageWrapper; +import com.intellij.refactoring.move.moveClassesOrPackages.MoveClassesOrPackagesProcessor; +import com.intellij.refactoring.move.moveClassesOrPackages.SingleSourceRootMoveDestination; +import com.intellij.refactoring.util.RefactoringMessageUtil; +import com.intellij.refactoring.util.RefactoringUtil; +import com.intellij.util.IncorrectOperationException; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Arrays; +import java.util.Objects; +import java.util.stream.Stream; + +public class MoveToTestRootFix extends LocalQuickFixAndIntentionActionOnPsiElement { + private static final Logger LOG = Logger.getInstance(MoveToTestRootFix.class); + + public MoveToTestRootFix(PsiFile psiFile) { + super(psiFile); + } + + @Override + public @IntentionName @NotNull String getText() { + PsiFile containingFile = getStartElement().getContainingFile(); + if (containingFile instanceof PsiClassOwner) { + PsiClass[] classes = ((PsiClassOwner)containingFile).getClasses(); + if (classes.length > 0) { + return JavaBundle.message("intention.name.move.class.to.test.root", classes[0].getName()); + } + } + return getFamilyName(); + } + + @Override + @NotNull + public String getFamilyName() { + return JavaBundle.message("intention.family.name.move.class.to.test.root"); + } + + public boolean isAvailable(PsiFile file) { + if (file == null || !file.isValid()) return false; + VirtualFile virtualFile = PsiUtilCore.getVirtualFile(file); + if (virtualFile == null) return false; + Module module = ModuleUtilCore.findModuleForFile(virtualFile, file.getProject()); + if (module == null) return false; + ModuleRootManager rootManager = ModuleRootManager.getInstance(module); + ModuleFileIndex fileIndex = rootManager.getFileIndex(); + if (!fileIndex.isInSourceContent(virtualFile)) return false; + return getTestRoots(rootManager).findFirst().isPresent(); + } + + @NotNull + private static Stream getTestRoots(ModuleRootManager rootManager) { + return Arrays.stream(rootManager.getContentEntries()) + .flatMap(entry -> Arrays.stream(entry.getSourceFolders())) + .filter(SourceFolder::isTestSource); + } + + @Override + public void invoke(@NotNull Project project, + @NotNull PsiFile file, + @Nullable Editor editor, + @NotNull PsiElement startElement, + @NotNull PsiElement endElement) { + + final PsiFile myFile = startElement.getContainingFile(); + + if (!FileModificationService.getInstance().prepareFileForWrite(myFile)) return; + Module module = ModuleUtilCore.findModuleForFile(myFile); + Stream testRoots = getTestRoots(ModuleRootManager.getInstance(Objects.requireNonNull(module))); + + DirectoryChooser chooser = new DirectoryChooser(project); + chooser.setTitle(JavaRefactoringBundle.message("select.source.root.chooser.title")); + + PsiManager manager = myFile.getManager(); + + PsiDirectory[] directories = testRoots.map(ContentFolder::getFile).filter(Objects::nonNull) + .map(vFile -> manager.findDirectory(vFile)) + .filter(Objects::nonNull).toArray(PsiDirectory[]::new); + chooser.fillList(directories, null, project, ""); + if (!chooser.showAndGet()) { + return; + } + PsiDirectory sourceRoot = chooser.getSelectedDirectory(); + if (sourceRoot == null) return; + + PsiPackage targetPackage = JavaDirectoryService.getInstance().getPackage(myFile.getContainingDirectory()); + + PackageWrapper wrapper = PackageWrapper.create(targetPackage); + + PsiDirectory selectedDirectory = WriteAction.compute(() -> RefactoringUtil.createPackageDirectoryInSourceRoot(wrapper, sourceRoot.getVirtualFile())); + + try { + String error; + try { + error = RefactoringMessageUtil.checkCanCreateFile(selectedDirectory, myFile.getName()); + } + catch (IncorrectOperationException e) { + error = e.getLocalizedMessage(); + } + + if (error != null) { + Messages.showMessageDialog(project, error, CommonBundle.getErrorTitle(), Messages.getErrorIcon()); + return; + } + + new MoveClassesOrPackagesProcessor( + project, + ((PsiJavaFile) myFile).getClasses(), + new SingleSourceRootMoveDestination(wrapper, selectedDirectory), false, + false, + null).run(); + } + catch (IncorrectOperationException e) { + LOG.error(e); + } + } + + @Override + public boolean startInWriteAction() { + return false; + } +} diff --git a/java/openapi/resources/messages/JavaBundle.properties b/java/openapi/resources/messages/JavaBundle.properties index 35d3dbb2e93d..db83ad4faf59 100644 --- a/java/openapi/resources/messages/JavaBundle.properties +++ b/java/openapi/resources/messages/JavaBundle.properties @@ -1687,3 +1687,5 @@ javadoc.documentation.url.checked=The following documentation {0, choice, 1#URL javadoc.edit.api.docs.paths=Edit API docs paths intention.family.name.edit.color.settings=Edit color settings for reassigned variable intention.name.edit.color.settings=Edit ''{0}'' color settings +intention.family.name.move.class.to.test.root=Move class to test root +intention.name.move.class.to.test.root=Move ''{0}'' to test root