[java] add conflicts detection when java files are moved together with non-classes (IDEA-277808)

GitOrigin-RevId: f28d9f997fff4671662f65f49e190470b14914cb
This commit is contained in:
Anna Kozlova
2021-09-14 06:40:39 +00:00
committed by intellij-monorepo-bot
parent 69f5144576
commit bd40d80a9a
12 changed files with 135 additions and 55 deletions
@@ -1,4 +1,4 @@
// Copyright 2000-2021 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.
// 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.refactoring.move.moveClassesOrPackages;
import com.intellij.codeInsight.daemon.impl.analysis.JavaModuleGraphUtil;
@@ -176,7 +176,7 @@ public class MoveClassesOrPackagesProcessor extends BaseRefactoringProcessor {
protected UsageInfo @NotNull [] findUsages() {
final List<UsageInfo> allUsages = new ArrayList<>();
final List<UsageInfo> usagesToSkip = new ArrayList<>();
myConflicts = new MultiMap<>();
for (PsiElement element : myElementsToMove) {
String newName = getNewQName(element);
UsageInfo[] usages = MoveClassesOrPackagesUtil.findUsages(
@@ -195,17 +195,25 @@ public class MoveClassesOrPackagesProcessor extends BaseRefactoringProcessor {
}
}
}
myMoveDestination.analyzeModuleConflicts(Arrays.asList(myElementsToMove), myConflicts,
allUsages.toArray(UsageInfo.EMPTY_ARRAY));
final UsageInfo[] usageInfos = allUsages.toArray(UsageInfo.EMPTY_ARRAY);
detectPackageLocalsMoved(usageInfos, myConflicts);
detectPackageLocalsUsed(myConflicts, myElementsToMove, myTargetPackage);
myConflicts = new MultiMap<>();
detectConflicts(usageInfos, myConflicts, myElementsToMove, myTargetPackage, myMoveDestination);
myModuleInfoUsageDetector.detectModuleStatementsUsed(allUsages, myConflicts);
detectMoveToDefaultPackage(usageInfos, myConflicts, myTargetPackage);
allUsages.removeAll(usagesToSkip);
return UsageViewUtil.removeDuplicatedUsages(allUsages.toArray(UsageInfo.EMPTY_ARRAY));
}
public static void detectConflicts(UsageInfo[] usageInfos,
MultiMap<PsiElement, @Nls String> conflicts,
PsiElement @NotNull[] elementsToMove,
@NotNull PackageWrapper targetPackage,
@NotNull MoveDestination moveDestination) {
moveDestination.analyzeModuleConflicts(Arrays.asList(elementsToMove), conflicts, usageInfos);
detectPackageLocalsMoved(usageInfos, conflicts, targetPackage, elementsToMove);
detectPackageLocalsUsed(conflicts, elementsToMove, targetPackage);
detectMoveToDefaultPackage(usageInfos, conflicts, targetPackage);
}
private static void detectMoveToDefaultPackage(UsageInfo[] infos,
MultiMap<PsiElement, @Nls String> conflicts,
PackageWrapper aPackage) {
@@ -266,8 +274,8 @@ public class MoveClassesOrPackagesProcessor extends BaseRefactoringProcessor {
return showConflicts(myConflicts, usages);
}
private boolean isInsideMoved(PsiElement place) {
for (PsiElement element : myElementsToMove) {
private static boolean isInsideMoved(PsiElement place, PsiElement[] elementsToMove) {
for (PsiElement element : elementsToMove) {
if (element instanceof PsiClass) {
if (PsiTreeUtil.isAncestor(element, place, false)) return true;
}
@@ -287,7 +295,10 @@ public class MoveClassesOrPackagesProcessor extends BaseRefactoringProcessor {
}
}
private void detectPackageLocalsMoved(final UsageInfo[] usages, final MultiMap<PsiElement, String> conflicts) {
private static void detectPackageLocalsMoved(final UsageInfo[] usages,
final MultiMap<PsiElement, String> conflicts,
@NotNull PackageWrapper targetPackage,
PsiElement[] elementsToMove) {
Set<PsiClass> movedClasses = new HashSet<>();
Map<PsiClass,Set<PsiElement>> reportedClassToContainers = new HashMap<>();
for (UsageInfo usage : usages) {
@@ -305,11 +316,11 @@ public class MoveClassesOrPackagesProcessor extends BaseRefactoringProcessor {
if (!reported.contains(container)) {
reported.add(container);
PsiFile containingFile = element.getContainingFile();
if (containingFile != null && !isInsideMoved(element)) {
if (containingFile != null && !isInsideMoved(element, elementsToMove)) {
PsiDirectory directory = containingFile.getContainingDirectory();
if (directory != null) {
PsiPackage usagePackage = JavaDirectoryService.getInstance().getPackage(directory);
if (usagePackage != null && !myTargetPackage.equalToPackage(usagePackage)) {
if (usagePackage != null && !targetPackage.equalToPackage(usagePackage)) {
final String message = JavaRefactoringBundle.message("a.package.local.class.0.will.no.longer.be.accessible.from.1",
CommonRefactoringUtil.htmlEmphasize(aClass.getName()),
RefactoringUIUtil.getDescription(
@@ -323,7 +334,7 @@ public class MoveClassesOrPackagesProcessor extends BaseRefactoringProcessor {
}
}
final MyClassInstanceReferenceVisitor instanceReferenceVisitor = new MyClassInstanceReferenceVisitor(conflicts);
final MyClassInstanceReferenceVisitor instanceReferenceVisitor = new MyClassInstanceReferenceVisitor(targetPackage, elementsToMove, conflicts);
for (final PsiClass aClass : movedClasses) {
String visibility = VisibilityUtil.getVisibilityModifier(aClass.getModifierList());
if (PsiModifier.PACKAGE_LOCAL.equals(visibility)) {
@@ -828,13 +839,19 @@ public class MoveClassesOrPackagesProcessor extends BaseRefactoringProcessor {
return JavaRefactoringBundle.message("move.classes.command", elements, target);
}
private class MyClassInstanceReferenceVisitor implements ClassInstanceScanner.ClassInstanceReferenceVisitor {
private static class MyClassInstanceReferenceVisitor implements ClassInstanceScanner.ClassInstanceReferenceVisitor {
private final MultiMap<PsiElement, String> myConflicts;
private final Map<PsiModifierListOwner,HashSet<PsiElement>> myReportedElementToContainer = new HashMap<>();
private final Map<PsiClass, RefactoringUtil.IsDescendantOf> myIsDescendantOfCache = new HashMap<>();
private final @NotNull PackageWrapper myTargetPackage;
private final PsiElement[] myElementsToMove;
MyClassInstanceReferenceVisitor(MultiMap<PsiElement, String> conflicts) {
MyClassInstanceReferenceVisitor(@NotNull PackageWrapper targetPackage,
PsiElement[] elementsToMove,
MultiMap<PsiElement, String> conflicts) {
myConflicts = conflicts;
myTargetPackage = targetPackage;
myElementsToMove = elementsToMove;
}
@Override
@@ -857,14 +874,14 @@ public class MoveClassesOrPackagesProcessor extends BaseRefactoringProcessor {
private synchronized void visitMemberReference(final PsiModifierListOwner member, PsiReferenceExpression qualified, final RefactoringUtil.IsDescendantOf descendantOf) {
if (member.hasModifierProperty(PsiModifier.PACKAGE_LOCAL)) {
visitPackageLocalMemberReference(qualified, member);
visitPackageLocalMemberReference(qualified, member, myElementsToMove);
} else if (member.hasModifierProperty(PsiModifier.PROTECTED)) {
final PsiExpression qualifier = qualified.getQualifierExpression();
if (qualifier != null && !(qualifier instanceof PsiThisExpression) && !(qualifier instanceof PsiSuperExpression)) {
visitPackageLocalMemberReference(qualified, member);
visitPackageLocalMemberReference(qualified, member, myElementsToMove);
} else {
if (!isInInheritor(qualified, descendantOf)) {
visitPackageLocalMemberReference(qualified, member);
visitPackageLocalMemberReference(qualified, member, myElementsToMove);
}
}
}
@@ -879,13 +896,14 @@ public class MoveClassesOrPackagesProcessor extends BaseRefactoringProcessor {
return false;
}
private void visitPackageLocalMemberReference(PsiJavaCodeReferenceElement qualified, PsiModifierListOwner member) {
private void visitPackageLocalMemberReference(PsiJavaCodeReferenceElement qualified,
PsiModifierListOwner member, PsiElement[] elementsToMove) {
PsiElement container = ConflictsUtil.getContainer(qualified);
Set<PsiElement> reportedContainers = myReportedElementToContainer.computeIfAbsent(member, __ -> new HashSet<>());
if (!reportedContainers.contains(container)) {
reportedContainers.add(container);
if (!isInsideMoved(container)) {
if (!isInsideMoved(container, elementsToMove)) {
PsiFile containingFile = container.getContainingFile();
if (containingFile != null) {
PsiDirectory directory = containingFile.getContainingDirectory();
@@ -1,18 +1,4 @@
/*
* Copyright 2000-2011 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-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.refactoring.move.moveClassesOrPackages;
@@ -22,10 +8,12 @@ import com.intellij.openapi.roots.JavaProjectRootsUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.*;
import com.intellij.psi.util.FileTypeUtils;
import com.intellij.refactoring.PackageWrapper;
import com.intellij.refactoring.move.moveFilesOrDirectories.MoveFileHandler;
import com.intellij.refactoring.util.MoveRenameUsageInfo;
import com.intellij.usageView.UsageInfo;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.containers.MultiMap;
import java.util.ArrayList;
import java.util.Collections;
@@ -65,6 +53,19 @@ public class MoveJavaFileHandler extends MoveFileHandler {
return result.isEmpty() ? null : result;
}
@Override
public void detectConflicts(MultiMap<PsiElement, String> conflicts,
PsiElement[] elementsToMove,
UsageInfo[] usages,
PsiDirectory targetDirectory) {
PsiPackage aPackage = JavaDirectoryService.getInstance().getPackage(targetDirectory);
if (aPackage != null) {
PackageWrapper packageWrapper = new PackageWrapper(aPackage);
MoveClassesOrPackagesProcessor.detectConflicts(usages, conflicts, elementsToMove, packageWrapper, new SingleSourceRootMoveDestination(packageWrapper,
targetDirectory));
}
}
@Override
public void retargetUsages(List<UsageInfo> usageInfos, Map<PsiElement, PsiElement> oldToNewMap) {
for (UsageInfo usage : usageInfos) {
@@ -0,0 +1,4 @@
package p1;
public class A {
B b;
}
@@ -0,0 +1 @@
class B {}
@@ -0,0 +1,4 @@
package p1;
public class A {
B b;
}
@@ -0,0 +1,2 @@
package p1;
class B {}
@@ -3,6 +3,7 @@ package com.intellij.refactoring;
import com.intellij.JavaTestUtil;
import com.intellij.openapi.util.registry.Registry;
import org.junit.Assert;
/**
* @author ven
@@ -14,6 +15,15 @@ public class MoveJavaFileTest extends MoveFileTestCase {
}
public void testPackageInfo() { doTest("pack2", "pack1/package-info.java"); }
public void testConflict() {
try {
doTest("p2", "p1/B.java");
fail("Conflict not detected!");
}
catch (BaseRefactoringProcessor.ConflictsInTestsException e) {
Assert.assertEquals("Package-local class <b><code>B</code></b> will no longer be accessible from field <b><code>A.b</code></b>", e.getMessage());
}
}
public static class BranchTest extends MoveJavaFileTest {
@Override
@@ -1,18 +1,4 @@
/*
* Copyright 2000-2009 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-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.refactoring;
import com.intellij.openapi.project.Project;
@@ -65,7 +51,20 @@ public interface MoveDestination {
@Nullable @NlsContexts.DialogMessage
String verify(PsiPackage source);
void analyzeModuleConflicts(@NotNull Collection<? extends PsiElement> elements, @NotNull MultiMap<PsiElement,String> conflicts, final UsageInfo[] usages);
/**
* Searches for conflicts which arise when elements are moved from one module to another
*
* E.g. when target module has no dependency on required library,
* or when usage in the third module has no dependency on the target module
*
* Impl note: do nothing if elements are kept in the same module
*/
void analyzeModuleConflicts(@NotNull Collection<? extends PsiElement> elements,
@NotNull MultiMap<PsiElement,String> conflicts,
final UsageInfo[] usages);
/**
* @return true if runtime scope of {@code place}'s module contains target destination
*/
boolean isTargetAccessible(@NotNull Project project, @NotNull VirtualFile place);
}
@@ -1,4 +1,4 @@
// Copyright 2000-2018 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.
// 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.refactoring.move.moveFilesOrDirectories;
@@ -8,6 +8,7 @@ import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.usageView.UsageInfo;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.containers.MultiMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -53,6 +54,21 @@ public abstract class MoveFileHandler {
@Nullable
public abstract List<UsageInfo> findUsages(PsiFile psiFile, PsiDirectory newParent, boolean searchInComments, boolean searchInNonJavaFiles);
/**
* Finds conflicts which may arise when file is moved,
* e.g. missing module dependency or visibility which makes moved element not accessible
*
* {@link #detectConflicts(PsiElement[], UsageInfo[], PsiDirectory, MultiMap)} passes all elements which were moved
* as well as all found usages for each handler. It's the responsibility of handler to ignore inappropriate elements/usages.
*
* @param elementsToMove all elements which were moved during refactoring, including those which won't be normally processed by the handler
* @param usages all usages found during refactoring
*/
public void detectConflicts(MultiMap<PsiElement, String> conflicts,
PsiElement[] elementsToMove,
UsageInfo[] usages,
PsiDirectory targetDirectory) {}
/**
* After a file has been moved, updates the references to the file so that they point to the new location of the file.
*
@@ -69,6 +85,18 @@ public abstract class MoveFileHandler {
*/
public abstract void updateMovedFile(PsiFile file) throws IncorrectOperationException;
/**
* Fills {@code conflicts} based on elements which were moved, target directory and already calculated usages.
*/
public static void detectConflicts(PsiElement[] elementsToMove,
UsageInfo[] usageInfos,
PsiDirectory targetDirectory,
MultiMap<PsiElement, String> conflicts) {
for (MoveFileHandler handler : EP_NAME.getExtensionList()) {
handler.detectConflicts(conflicts, elementsToMove, usageInfos, targetDirectory);
}
}
@NotNull
public static MoveFileHandler forElement(PsiFile element) {
for(MoveFileHandler processor: EP_NAME.getExtensionList()) {
@@ -106,6 +134,4 @@ public abstract class MoveFileHandler {
}
};
}
@@ -1,4 +1,4 @@
// Copyright 2000-2020 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.
// 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.refactoring.move.moveFilesOrDirectories;
import com.intellij.ide.util.EditorHelper;
@@ -6,11 +6,14 @@ import com.intellij.lang.FileASTNode;
import com.intellij.model.BranchableUsageInfo;
import com.intellij.model.ModelBranch;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ReadAction;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.paths.PsiDynaReference;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.project.DumbService;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.Ref;
import com.intellij.psi.*;
import com.intellij.psi.impl.source.resolve.reference.impl.providers.FileReference;
import com.intellij.psi.search.GlobalSearchScope;
@@ -28,6 +31,7 @@ import com.intellij.usageView.UsageInfo;
import com.intellij.usageView.UsageViewDescriptor;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.MultiMap;
import one.util.streamex.EntryStream;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -273,6 +277,17 @@ public class MoveFilesOrDirectoriesProcessor extends BaseRefactoringProcessor {
}
}
@Override
protected boolean preprocessUsages(@NotNull Ref<UsageInfo[]> refUsages) {
MultiMap<PsiElement, String> conflicts = new MultiMap<>();
UsageInfo[] usages = refUsages.get();
if (!ProgressManager.getInstance().runProcessWithProgressSynchronously(() -> ReadAction.run(() -> MoveFileHandler.detectConflicts(myElementsToMove, usages, myNewParent, conflicts)),
RefactoringBundle.message("detecting.possible.conflicts"), true, myProject)) {
return false;
}
return showConflicts(conflicts, usages);
}
private void afterMove(@Nullable ModelBranch branch, Set<SmartPsiElementPointer<PsiFile>> movedFiles, List<Runnable> notifyListeners) {
notifyListeners.forEach(Runnable::run);
if (myMoveCallback != null) {