diff --git a/idea/src/org/jetbrains/kotlin/idea/quickfix/MoveToSealedMatchingPackageFix.kt b/idea/src/org/jetbrains/kotlin/idea/quickfix/MoveToSealedMatchingPackageFix.kt index 0f88b4a7042..51e0b690149 100644 --- a/idea/src/org/jetbrains/kotlin/idea/quickfix/MoveToSealedMatchingPackageFix.kt +++ b/idea/src/org/jetbrains/kotlin/idea/quickfix/MoveToSealedMatchingPackageFix.kt @@ -7,25 +7,46 @@ package org.jetbrains.kotlin.idea.quickfix import com.intellij.openapi.actionSystem.LangDataKeys import com.intellij.openapi.actionSystem.impl.SimpleDataContext +import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.editor.Editor import com.intellij.openapi.project.Project import com.intellij.openapi.roots.ProjectFileIndex +import com.intellij.openapi.vfs.VirtualFile import com.intellij.psi.PsiDirectory +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFileSystemItem import com.intellij.psi.search.GlobalSearchScope import com.intellij.psi.util.parentOfType +import com.intellij.refactoring.PackageWrapper +import com.intellij.refactoring.move.MoveCallback +import com.intellij.refactoring.move.MoveHandler import org.jetbrains.kotlin.descriptors.containingPackage import org.jetbrains.kotlin.diagnostics.Diagnostic import org.jetbrains.kotlin.idea.KotlinBundle +import org.jetbrains.kotlin.idea.actions.internal.refactoringTesting.cases.* +import org.jetbrains.kotlin.idea.refactoring.move.getTargetPackageFqName +import org.jetbrains.kotlin.idea.refactoring.move.guessNewFileName import org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.MoveKotlinDeclarationsHandler +import org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.MoveKotlinDeclarationsHandlerActions +import org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.ui.KotlinAwareMoveFilesOrDirectoriesModel +import org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.ui.MoveKotlinNestedClassesToUpperLevelModel +import org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.ui.MoveKotlinTopLevelDeclarationsModel import org.jetbrains.kotlin.idea.references.resolveMainReferenceToDescriptors +import org.jetbrains.kotlin.idea.util.application.executeCommand import org.jetbrains.kotlin.idea.util.projectStructure.module import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject import org.jetbrains.kotlin.resolve.DescriptorToSourceUtils import org.jetbrains.kotlin.resolve.jvm.KotlinJavaPsiFacade class MoveToSealedMatchingPackageFix(element: KtTypeReference) : KotlinQuickFixAction(element) { - private val moveHandler = MoveKotlinDeclarationsHandler(false) + private val moveHandler = + if (ApplicationManager.getApplication().isUnitTestMode) { + MoveKotlinDeclarationsHandler(MoveKotlinDeclarationsHandlerTestActions()) + } else { + MoveKotlinDeclarationsHandler(false) + } override fun invoke(project: Project, editor: Editor?, file: KtFile) { val typeReference = element ?: return @@ -78,4 +99,115 @@ class MoveToSealedMatchingPackageFix(element: KtTypeReference) : KotlinQuickFixA return MoveToSealedMatchingPackageFix(annotationEntry) } } +} + +private class MoveKotlinDeclarationsHandlerTestActions : MoveKotlinDeclarationsHandlerActions { + + override fun invokeMoveKotlinTopLevelDeclarationsRefactoring( + project: Project, + elementsToMove: Set, + targetPackageName: String, + targetDirectory: PsiDirectory?, + targetFile: KtFile?, + freezeTargets: Boolean, + moveToPackage: Boolean, + moveCallback: MoveCallback? + ) { + val sourceFiles = getSourceFiles(elementsToMove) + val targetFilePath = + targetFile?.virtualFile?.path ?: sourceFiles[0].virtualFile.parent.path + "/" + guessNewFileName(elementsToMove) + + val model = MoveKotlinTopLevelDeclarationsModel( + project = project, + elementsToMove = elementsToMove.toList(), + targetPackage = targetPackageName, + selectedPsiDirectory = targetDirectory, + fileNameInPackage = "Derived.kt", + targetFilePath = targetFilePath, + isMoveToPackage = true, + isSearchReferences = false, + isSearchInComments = false, + isSearchInNonJavaFiles = false, + isDeleteEmptyFiles = false, + applyMPPDeclarations = false, + moveCallback = null + ) + + model.computeModelResult(throwOnConflicts = true).processor.run() + } + + private fun getSourceFiles(elementsToMove: Collection): List { + return elementsToMove.map { obj: KtPureElement -> obj.containingKtFile } + .distinct() + } + + override fun invokeKotlinSelectNestedClassChooser(nestedClass: KtClassOrObject, targetContainer: PsiElement?) = + doWithMoveKotlinNestedClassesToUpperLevelModel(nestedClass, targetContainer) + + private fun doWithMoveKotlinNestedClassesToUpperLevelModel(nestedClass: KtClassOrObject, targetContainer: PsiElement?) { + + val outerClass = nestedClass.containingClassOrObject ?: throw FailedToRunCaseException() + val newTarget = targetContainer + ?: outerClass.containingClassOrObject + ?: outerClass.containingFile.let { it.containingDirectory ?: it } + + val packageName = getTargetPackageFqName(newTarget)?.asString() ?: "" + + val model = object : MoveKotlinNestedClassesToUpperLevelModel( + project = nestedClass.project, + innerClass = nestedClass, + target = newTarget, + parameter = "", + className = nestedClass.name ?: "", + passOuterClass = false, + searchInComments = false, + isSearchInNonJavaFiles = false, + packageName = packageName, + isOpenInEditor = false + ) { + override fun chooseSourceRoot( + newPackage: PackageWrapper, + contentSourceRoots: List, + initialDir: PsiDirectory? + ) = contentSourceRoots.firstOrNull() + } + + model.computeModelResult(throwOnConflicts = true).processor.run() + } + + override fun invokeKotlinAwareMoveFilesOrDirectoriesRefactoring( + project: Project, + initialDirectory: PsiDirectory?, + elements: List, + moveCallback: MoveCallback? + ) { + val targetPath = + initialDirectory?.virtualFile?.path + ?: elements.firstOrNull()?.containingFile?.virtualFile?.path + ?: throw NotImplementedError() + + val model = KotlinAwareMoveFilesOrDirectoriesModel( + project = project, + elementsToMove = elements, + targetDirectoryName = randomDirectoryPathMutator(targetPath), + updatePackageDirective = randomBoolean(), + searchReferences = randomBoolean(), + moveCallback = null + ) + + project.executeCommand(MoveHandler.getRefactoringName()) { + model.computeModelResult().processor.run() + } + } + + override fun showErrorHint(project: Project, editor: Editor?, message: String, title: String, helpId: String?) = + throw NotImplementedError() + + override fun invokeMoveKotlinNestedClassesRefactoring( + project: Project, + elementsToMove: List, + originalClass: KtClassOrObject, + targetClass: KtClassOrObject, + moveCallback: MoveCallback? + ) = throw NotImplementedError() } \ No newline at end of file diff --git a/idea/testData/quickfix/moveToSealedParent/nestedDeclarationToSealed.test b/idea/testData/quickfix/moveToSealedParent/nestedDeclarationToSealed.test new file mode 100644 index 00000000000..d41a244591f --- /dev/null +++ b/idea/testData/quickfix/moveToSealedParent/nestedDeclarationToSealed.test @@ -0,0 +1,27 @@ +// FILE: DerivedOutsideOfPackage.before.kt +// "Move Derived to the package/module of SomeSealedClass" "true" +// ERROR: Inheritor of sealed class or interface declared in package but it must be in package foo where base class is declared +// COMPILER_ARGUMENTS: -XXLanguage:+SealedInterfaces -XXLanguage:+AllowSealedInheritorsInDifferentFilesOfSamePackage + +import foo.SomeSealedClass + +class ArbitraryClass { + class Derived: SomeSealedClass() +} + + +// FILE: foo/SealedDeclarations.kt +package foo + +sealed class SomeSealedClass + + +// FILE: DerivedOutsideOfPackage.after.kt +// "Move Derived to the package/module of SomeSealedClass" "true" +// ERROR: Inheritor of sealed class or interface declared in package but it must be in package foo where base class is declared +// COMPILER_ARGUMENTS: -XXLanguage:+SealedInterfaces -XXLanguage:+AllowSealedInheritorsInDifferentFilesOfSamePackage + +class ArbitraryClass { +} + + diff --git a/idea/testData/quickfix/moveToSealedParent/topLevelDeclarationToSealed.test b/idea/testData/quickfix/moveToSealedParent/topLevelDeclarationToSealed.test new file mode 100644 index 00000000000..49e392b3d63 --- /dev/null +++ b/idea/testData/quickfix/moveToSealedParent/topLevelDeclarationToSealed.test @@ -0,0 +1,27 @@ +// FILE: DerivedOutsideOfPackage.before.kt +// "Move Derived to the package/module of SomeSealedClass" "true" +// ERROR: Inheritor of sealed class or interface declared in package but it must be in package foo where base class is declared +// COMPILER_ARGUMENTS: -XXLanguage:+SealedInterfaces -XXLanguage:+AllowSealedInheritorsInDifferentFilesOfSamePackage + +import foo.SomeSealedClass + +class Derived: SomeSealedClass() +class ArbitraryClass + + +// FILE: foo/SealedDeclarations.kt +package foo + +sealed class SomeSealedClass + + +// FILE: DerivedOutsideOfPackage.after.kt +// "Move Derived to the package/module of SomeSealedClass" "true" +// ERROR: Inheritor of sealed class or interface declared in package but it must be in package foo where base class is declared +// COMPILER_ARGUMENTS: -XXLanguage:+SealedInterfaces -XXLanguage:+AllowSealedInheritorsInDifferentFilesOfSamePackage + +import foo.SomeSealedClass + +class ArbitraryClass + + diff --git a/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixMultiFileTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixMultiFileTestGenerated.java index 413c71cdcbd..f4ec48d922e 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixMultiFileTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixMultiFileTestGenerated.java @@ -3493,6 +3493,29 @@ public class QuickFixMultiFileTestGenerated extends AbstractQuickFixMultiFileTes } } + @TestMetadata("idea/testData/quickfix/moveToSealedParent") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class MoveToSealedParent extends AbstractQuickFixMultiFileTest { + private void runTest(String testDataFilePath) throws Exception { + KotlinTestUtils.runTest(this::doTestWithExtraFile, this, testDataFilePath); + } + + public void testAllFilesPresentInMoveToSealedParent() throws Exception { + KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("idea/testData/quickfix/moveToSealedParent"), Pattern.compile("^(\\w+)\\.((before\\.Main\\.\\w+)|(test))$"), null, true); + } + + @TestMetadata("nestedDeclarationToSealed.test") + public void testNestedDeclarationToSealed() throws Exception { + runTest("idea/testData/quickfix/moveToSealedParent/nestedDeclarationToSealed.test"); + } + + @TestMetadata("topLevelDeclarationToSealed.test") + public void testTopLevelDeclarationToSealed() throws Exception { + runTest("idea/testData/quickfix/moveToSealedParent/topLevelDeclarationToSealed.test"); + } + } + @TestMetadata("idea/testData/quickfix/moveTypeAliasToTopLevel") @TestDataPath("$PROJECT_ROOT") @RunWith(JUnit3RunnerWithInners.class) diff --git a/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixTestGenerated.java index 2d431daa1df..524ed4f1a3a 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixTestGenerated.java @@ -9808,6 +9808,19 @@ public class QuickFixTestGenerated extends AbstractQuickFixTest { } } + @TestMetadata("idea/testData/quickfix/moveToSealedParent") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class MoveToSealedParent extends AbstractQuickFixTest { + private void runTest(String testDataFilePath) throws Exception { + KotlinTestUtils.runTest(this::doTest, this, testDataFilePath); + } + + public void testAllFilesPresentInMoveToSealedParent() throws Exception { + KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("idea/testData/quickfix/moveToSealedParent"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), null, true); + } + } + @TestMetadata("idea/testData/quickfix/moveTypeAliasToTopLevel") @TestDataPath("$PROJECT_ROOT") @RunWith(JUnit3RunnerWithInners.class)