KT-43941 [Sealed interfaces]: subclass intention

Restriction for sealed inheritors was relaxed. Instead of being nested
class members now they can be the members of the same module and
package.
This commit is contained in:
Andrei Klunnyi
2020-12-22 12:59:27 +01:00
parent 1cac8b0d61
commit 3f287d344e
11 changed files with 116 additions and 17 deletions
@@ -14,12 +14,14 @@ import com.intellij.openapi.project.Project
import com.intellij.openapi.util.TextRange
import com.intellij.psi.JavaDirectoryService
import com.intellij.refactoring.rename.PsiElementRenameHandler
import org.jetbrains.kotlin.config.LanguageFeature
import org.jetbrains.kotlin.descriptors.DescriptorVisibilities
import org.jetbrains.kotlin.descriptors.DescriptorVisibility
import org.jetbrains.kotlin.idea.KotlinBundle
import org.jetbrains.kotlin.idea.core.KotlinNameSuggester
import org.jetbrains.kotlin.idea.core.ShortenReferences
import org.jetbrains.kotlin.idea.core.overrideImplement.ImplementMembersHandler
import org.jetbrains.kotlin.idea.project.languageVersionSettings
import org.jetbrains.kotlin.idea.refactoring.getOrCreateKotlinFile
import org.jetbrains.kotlin.idea.refactoring.ui.CreateKotlinClassDialog
import org.jetbrains.kotlin.idea.util.application.runWriteAction
@@ -73,8 +75,8 @@ class CreateKotlinSubClassIntention : SelfTargetingRangeIntention<KtClass>(
if (editor == null) throw IllegalArgumentException("This intention requires an editor")
val name = element.name ?: throw IllegalStateException("This intention should not be applied to anonymous classes")
if (element.isSealed()) {
createSealedSubclass(element, name, editor)
if (element.isSealed() && !element.languageVersionSettings.supportsFeature(LanguageFeature.SealedInterfaces)) {
createNestedSubclass(element, name, editor)
} else {
createExternalSubclass(element, name, editor)
}
@@ -87,7 +89,7 @@ class CreateKotlinSubClassIntention : SelfTargetingRangeIntention<KtClass>(
private fun targetNameWithoutConflicts(baseName: String, container: KtClassOrObject?) =
KotlinNameSuggester.suggestNameByName(defaultTargetName(baseName)) { container?.hasSameDeclaration(it) != true }
private fun createSealedSubclass(sealedClass: KtClass, sealedName: String, editor: Editor) {
private fun createNestedSubclass(sealedClass: KtClass, sealedName: String, editor: Editor) {
val project = sealedClass.project
val klass = runWriteAction {
val builder = buildClassHeader(targetNameWithoutConflicts(sealedName, sealedClass), sealedClass, sealedName)
@@ -162,7 +164,8 @@ class CreateKotlinSubClassIntention : SelfTargetingRangeIntention<KtClass>(
targetNameWithoutConflicts(baseName, baseClass.containingClassOrObject),
aPackage?.qualifiedName ?: "",
CreateClassKind.CLASS, true,
ModuleUtilCore.findModuleForPsiElement(baseClass)
ModuleUtilCore.findModuleForPsiElement(baseClass),
baseClass.isSealed()
) {
override fun getBaseDir(packageName: String?) = sourceDir
@@ -53,7 +53,8 @@ open class CreateKotlinClassDialog(
targetPackageName: String,
kind: ClassKind,
private val myClassNameEditable: Boolean,
private val myModule: Module?
private val myModule: Module?,
val isSealed: Boolean = false
) : DialogWrapper(myProject, true) {
private val myInformationLabel = JLabel("#")
@@ -63,7 +64,9 @@ open class CreateKotlinClassDialog(
myProject,
RECENTS_KEY,
CodeInsightBundle.message("dialog.create.class.package.chooser.title")
)
).also {
if (isSealed) it.isEnabled = false
}
private val myTfClassName: JTextField = MyTextField()
var targetDirectory: PsiDirectory? = null
private set
@@ -239,6 +242,6 @@ open class CreateKotlinClassDialog(
override fun pass(s: String?) {
setErrorText(s, myDestinationCB)
}
}, myPackageComponent.getChildComponent())
}, myPackageComponent.childComponent, isSealed)
}
}
@@ -28,6 +28,7 @@ import com.intellij.refactoring.move.moveClassesOrPackages.MultipleRootsMoveDest
import com.intellij.ui.*;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.kotlin.idea.KotlinBundle;
import org.jetbrains.kotlin.idea.roots.ProjectRootUtilsKt;
import javax.swing.*;
import java.awt.*;
@@ -72,12 +73,25 @@ public abstract class KotlinDestinationFolderComboBox extends ComboboxWithBrowse
}
public void setData(
Project project,
PsiDirectory initialTargetDirectory,
Pass<String> errorMessageUpdater, EditorComboBox editorComboBox
Project project,
PsiDirectory initialTargetDirectory,
Pass<String> errorMessageUpdater, EditorComboBox editorComboBox
) {
setData(project, initialTargetDirectory, errorMessageUpdater, editorComboBox, false);
}
public void setData(Project project, PsiDirectory initialTargetDirectory, Pass<String> errorMessageUpdater,
EditorComboBox editorComboBox, boolean sourceRootsInTargetDirOnly
) {
myInitialTargetDirectory = initialTargetDirectory;
mySourceRoots = getKotlinAwareDestinationSourceRoots(project);
if (sourceRootsInTargetDirOnly) {
Module module = ModuleUtilCore.findModuleForFile(myInitialTargetDirectory.getVirtualFile(), project);
mySourceRoots = ProjectRootUtilsKt.collectKotlinAwareDestinationSourceRoots(module);
} else {
mySourceRoots = getKotlinAwareDestinationSourceRoots(project);
}
new ComboboxSpeedSearch(getComboBox()) {
@Override
protected String getElementText(Object element) {
@@ -130,7 +144,8 @@ public abstract class KotlinDestinationFolderComboBox extends ComboboxWithBrowse
return;
}
}
setComboboxModel(getComboBox(), root, root, fileIndex, mySourceRoots, project, true, errorMessageUpdater);
setComboboxModel(getComboBox(), root, root, fileIndex, mySourceRoots, project, true, errorMessageUpdater,
sourceRootsInTargetDirOnly);
});
editorComboBox.addDocumentListener(new DocumentListener() {
@@ -140,10 +155,11 @@ public abstract class KotlinDestinationFolderComboBox extends ComboboxWithBrowse
DirectoryChooser.ItemWrapper selectedItem = (DirectoryChooser.ItemWrapper) comboBox.getSelectedItem();
setComboboxModel(comboBox, selectedItem != null && selectedItem != NULL_WRAPPER ? fileIndex
.getSourceRootForFile(selectedItem.getDirectory().getVirtualFile()) : initialSourceRoot, selection[0], fileIndex,
mySourceRoots, project, false, errorMessageUpdater);
mySourceRoots, project, false, errorMessageUpdater, sourceRootsInTargetDirOnly);
}
});
setComboboxModel(getComboBox(), initialSourceRoot, selection[0], fileIndex, mySourceRoots, project, false, errorMessageUpdater);
setComboboxModel(getComboBox(), initialSourceRoot, selection[0], fileIndex, mySourceRoots, project, false, errorMessageUpdater,
sourceRootsInTargetDirOnly);
getComboBox().addActionListener(e -> {
Object selectedItem = getComboBox().getSelectedItem();
updateErrorMessage(errorMessageUpdater, fileIndex, selectedItem);
@@ -199,7 +215,8 @@ public abstract class KotlinDestinationFolderComboBox extends ComboboxWithBrowse
List<VirtualFile> sourceRoots,
Project project,
boolean forceIncludeAll,
Pass<String> updateErrorMessage
Pass<String> updateErrorMessage,
boolean sourceRootsInTargetDirOnly
) {
LinkedHashSet<PsiDirectory> targetDirectories = new LinkedHashSet<>();
HashMap<PsiDirectory, String> pathsToCreate = new HashMap<>();
@@ -224,7 +241,7 @@ public abstract class KotlinDestinationFolderComboBox extends ComboboxWithBrowse
oldOne = itemWrapper;
}
}
if (oldSelection == null || !fileIndex.isInLibrarySource(oldSelection)) {
if (!sourceRootsInTargetDirOnly && (oldSelection == null || !fileIndex.isInLibrarySource(oldSelection))) {
items.add(NULL_WRAPPER);
}
DirectoryChooser.ItemWrapper selection = chooseSelection(initialTargetDirectorySourceRoot, fileIndex, items, initial, oldOne);
@@ -99,7 +99,7 @@ fun getKotlinAwareDestinationSourceRoots(project: Project): List<VirtualFile> {
private val KOTLIN_AWARE_SOURCE_ROOT_TYPES: Set<JpsModuleSourceRootType<JavaSourceRootProperties>> =
JavaModuleSourceRootTypes.SOURCES + ALL_KOTLIN_SOURCE_ROOT_TYPES
private fun Module.collectKotlinAwareDestinationSourceRoots(): List<VirtualFile> {
fun Module.collectKotlinAwareDestinationSourceRoots(): List<VirtualFile> {
return rootManager
.contentEntries
.asSequence()
+8
View File
@@ -0,0 +1,8 @@
// "Implement sealed class" "true"
// WITH_RUNTIME
// SHOULD_BE_AVAILABLE_AFTER_EXECUTION
// COMPILER_ARGUMENTS: -XXLanguage:+SealedInterfaces
sealed class <caret>Base {
abstract fun foo(): Int
}
+14
View File
@@ -0,0 +1,14 @@
// "Implement sealed class" "true"
// WITH_RUNTIME
// SHOULD_BE_AVAILABLE_AFTER_EXECUTION
// COMPILER_ARGUMENTS: -XXLanguage:+SealedInterfaces
sealed class Base {
abstract fun foo(): Int
}
class BaseImpl : Base() {
override fun foo(): Int {
TODO("Not yet implemented")
}
}
@@ -0,0 +1,4 @@
// "Implement sealed class" "true"
// SHOULD_BE_AVAILABLE_AFTER_EXECUTION
// COMPILER_ARGUMENTS: -XXLanguage:+SealedInterfaces
sealed class <caret>Sealed
@@ -0,0 +1,5 @@
// "Implement sealed class" "true"
// SHOULD_BE_AVAILABLE_AFTER_EXECUTION
// COMPILER_ARGUMENTS: -XXLanguage:+SealedInterfaces
sealed class Sealed
<caret>class SealedImpl : Sealed()
@@ -0,0 +1,12 @@
// "Implement sealed class" "true"
// WITH_RUNTIME
// SHOULD_BE_AVAILABLE_AFTER_EXECUTION
// COMPILER_ARGUMENTS: -XXLanguage:+SealedInterfaces
sealed class <caret>Base {
abstract fun foo(): Int
class BaseImpl : Base() {
override fun foo() = throw UnsupportedOperationException()
}
}
@@ -0,0 +1,18 @@
// "Implement sealed class" "true"
// WITH_RUNTIME
// SHOULD_BE_AVAILABLE_AFTER_EXECUTION
// COMPILER_ARGUMENTS: -XXLanguage:+SealedInterfaces
sealed class Base {
abstract fun foo(): Int
class BaseImpl : Base() {
override fun foo() = throw UnsupportedOperationException()
}
}
class BaseImpl : Base() {
override fun foo(): Int {
TODO("Not yet implemented")
}
}
@@ -8204,16 +8204,31 @@ public class QuickFixTestGenerated extends AbstractQuickFixTest {
runTest("idea/testData/quickfix/implement/sealed.kt");
}
@TestMetadata("sealedAfter15.kt")
public void testSealedAfter15() throws Exception {
runTest("idea/testData/quickfix/implement/sealedAfter15.kt");
}
@TestMetadata("sealedEmpty.kt")
public void testSealedEmpty() throws Exception {
runTest("idea/testData/quickfix/implement/sealedEmpty.kt");
}
@TestMetadata("sealedEmptyAfter15.kt")
public void testSealedEmptyAfter15() throws Exception {
runTest("idea/testData/quickfix/implement/sealedEmptyAfter15.kt");
}
@TestMetadata("sealedWithConflict.kt")
public void testSealedWithConflict() throws Exception {
runTest("idea/testData/quickfix/implement/sealedWithConflict.kt");
}
@TestMetadata("sealedWithConflictAfter15.kt")
public void testSealedWithConflictAfter15() throws Exception {
runTest("idea/testData/quickfix/implement/sealedWithConflictAfter15.kt");
}
@TestMetadata("typeParameter.kt")
public void testTypeParameter() throws Exception {
runTest("idea/testData/quickfix/implement/typeParameter.kt");