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.openapi.util.TextRange
import com.intellij.psi.JavaDirectoryService import com.intellij.psi.JavaDirectoryService
import com.intellij.refactoring.rename.PsiElementRenameHandler import com.intellij.refactoring.rename.PsiElementRenameHandler
import org.jetbrains.kotlin.config.LanguageFeature
import org.jetbrains.kotlin.descriptors.DescriptorVisibilities import org.jetbrains.kotlin.descriptors.DescriptorVisibilities
import org.jetbrains.kotlin.descriptors.DescriptorVisibility import org.jetbrains.kotlin.descriptors.DescriptorVisibility
import org.jetbrains.kotlin.idea.KotlinBundle import org.jetbrains.kotlin.idea.KotlinBundle
import org.jetbrains.kotlin.idea.core.KotlinNameSuggester import org.jetbrains.kotlin.idea.core.KotlinNameSuggester
import org.jetbrains.kotlin.idea.core.ShortenReferences import org.jetbrains.kotlin.idea.core.ShortenReferences
import org.jetbrains.kotlin.idea.core.overrideImplement.ImplementMembersHandler 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.getOrCreateKotlinFile
import org.jetbrains.kotlin.idea.refactoring.ui.CreateKotlinClassDialog import org.jetbrains.kotlin.idea.refactoring.ui.CreateKotlinClassDialog
import org.jetbrains.kotlin.idea.util.application.runWriteAction 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") 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") val name = element.name ?: throw IllegalStateException("This intention should not be applied to anonymous classes")
if (element.isSealed()) { if (element.isSealed() && !element.languageVersionSettings.supportsFeature(LanguageFeature.SealedInterfaces)) {
createSealedSubclass(element, name, editor) createNestedSubclass(element, name, editor)
} else { } else {
createExternalSubclass(element, name, editor) createExternalSubclass(element, name, editor)
} }
@@ -87,7 +89,7 @@ class CreateKotlinSubClassIntention : SelfTargetingRangeIntention<KtClass>(
private fun targetNameWithoutConflicts(baseName: String, container: KtClassOrObject?) = private fun targetNameWithoutConflicts(baseName: String, container: KtClassOrObject?) =
KotlinNameSuggester.suggestNameByName(defaultTargetName(baseName)) { container?.hasSameDeclaration(it) != true } 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 project = sealedClass.project
val klass = runWriteAction { val klass = runWriteAction {
val builder = buildClassHeader(targetNameWithoutConflicts(sealedName, sealedClass), sealedClass, sealedName) val builder = buildClassHeader(targetNameWithoutConflicts(sealedName, sealedClass), sealedClass, sealedName)
@@ -162,7 +164,8 @@ class CreateKotlinSubClassIntention : SelfTargetingRangeIntention<KtClass>(
targetNameWithoutConflicts(baseName, baseClass.containingClassOrObject), targetNameWithoutConflicts(baseName, baseClass.containingClassOrObject),
aPackage?.qualifiedName ?: "", aPackage?.qualifiedName ?: "",
CreateClassKind.CLASS, true, CreateClassKind.CLASS, true,
ModuleUtilCore.findModuleForPsiElement(baseClass) ModuleUtilCore.findModuleForPsiElement(baseClass),
baseClass.isSealed()
) { ) {
override fun getBaseDir(packageName: String?) = sourceDir override fun getBaseDir(packageName: String?) = sourceDir
@@ -53,7 +53,8 @@ open class CreateKotlinClassDialog(
targetPackageName: String, targetPackageName: String,
kind: ClassKind, kind: ClassKind,
private val myClassNameEditable: Boolean, private val myClassNameEditable: Boolean,
private val myModule: Module? private val myModule: Module?,
val isSealed: Boolean = false
) : DialogWrapper(myProject, true) { ) : DialogWrapper(myProject, true) {
private val myInformationLabel = JLabel("#") private val myInformationLabel = JLabel("#")
@@ -63,7 +64,9 @@ open class CreateKotlinClassDialog(
myProject, myProject,
RECENTS_KEY, RECENTS_KEY,
CodeInsightBundle.message("dialog.create.class.package.chooser.title") CodeInsightBundle.message("dialog.create.class.package.chooser.title")
) ).also {
if (isSealed) it.isEnabled = false
}
private val myTfClassName: JTextField = MyTextField() private val myTfClassName: JTextField = MyTextField()
var targetDirectory: PsiDirectory? = null var targetDirectory: PsiDirectory? = null
private set private set
@@ -239,6 +242,6 @@ open class CreateKotlinClassDialog(
override fun pass(s: String?) { override fun pass(s: String?) {
setErrorText(s, myDestinationCB) setErrorText(s, myDestinationCB)
} }
}, myPackageComponent.getChildComponent()) }, myPackageComponent.childComponent, isSealed)
} }
} }
@@ -28,6 +28,7 @@ import com.intellij.refactoring.move.moveClassesOrPackages.MultipleRootsMoveDest
import com.intellij.ui.*; import com.intellij.ui.*;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import org.jetbrains.kotlin.idea.KotlinBundle; import org.jetbrains.kotlin.idea.KotlinBundle;
import org.jetbrains.kotlin.idea.roots.ProjectRootUtilsKt;
import javax.swing.*; import javax.swing.*;
import java.awt.*; import java.awt.*;
@@ -72,12 +73,25 @@ public abstract class KotlinDestinationFolderComboBox extends ComboboxWithBrowse
} }
public void setData( public void setData(
Project project, Project project,
PsiDirectory initialTargetDirectory, PsiDirectory initialTargetDirectory,
Pass<String> errorMessageUpdater, EditorComboBox editorComboBox 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; 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()) { new ComboboxSpeedSearch(getComboBox()) {
@Override @Override
protected String getElementText(Object element) { protected String getElementText(Object element) {
@@ -130,7 +144,8 @@ public abstract class KotlinDestinationFolderComboBox extends ComboboxWithBrowse
return; return;
} }
} }
setComboboxModel(getComboBox(), root, root, fileIndex, mySourceRoots, project, true, errorMessageUpdater); setComboboxModel(getComboBox(), root, root, fileIndex, mySourceRoots, project, true, errorMessageUpdater,
sourceRootsInTargetDirOnly);
}); });
editorComboBox.addDocumentListener(new DocumentListener() { editorComboBox.addDocumentListener(new DocumentListener() {
@@ -140,10 +155,11 @@ public abstract class KotlinDestinationFolderComboBox extends ComboboxWithBrowse
DirectoryChooser.ItemWrapper selectedItem = (DirectoryChooser.ItemWrapper) comboBox.getSelectedItem(); DirectoryChooser.ItemWrapper selectedItem = (DirectoryChooser.ItemWrapper) comboBox.getSelectedItem();
setComboboxModel(comboBox, selectedItem != null && selectedItem != NULL_WRAPPER ? fileIndex setComboboxModel(comboBox, selectedItem != null && selectedItem != NULL_WRAPPER ? fileIndex
.getSourceRootForFile(selectedItem.getDirectory().getVirtualFile()) : initialSourceRoot, selection[0], 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 -> { getComboBox().addActionListener(e -> {
Object selectedItem = getComboBox().getSelectedItem(); Object selectedItem = getComboBox().getSelectedItem();
updateErrorMessage(errorMessageUpdater, fileIndex, selectedItem); updateErrorMessage(errorMessageUpdater, fileIndex, selectedItem);
@@ -199,7 +215,8 @@ public abstract class KotlinDestinationFolderComboBox extends ComboboxWithBrowse
List<VirtualFile> sourceRoots, List<VirtualFile> sourceRoots,
Project project, Project project,
boolean forceIncludeAll, boolean forceIncludeAll,
Pass<String> updateErrorMessage Pass<String> updateErrorMessage,
boolean sourceRootsInTargetDirOnly
) { ) {
LinkedHashSet<PsiDirectory> targetDirectories = new LinkedHashSet<>(); LinkedHashSet<PsiDirectory> targetDirectories = new LinkedHashSet<>();
HashMap<PsiDirectory, String> pathsToCreate = new HashMap<>(); HashMap<PsiDirectory, String> pathsToCreate = new HashMap<>();
@@ -224,7 +241,7 @@ public abstract class KotlinDestinationFolderComboBox extends ComboboxWithBrowse
oldOne = itemWrapper; oldOne = itemWrapper;
} }
} }
if (oldSelection == null || !fileIndex.isInLibrarySource(oldSelection)) { if (!sourceRootsInTargetDirOnly && (oldSelection == null || !fileIndex.isInLibrarySource(oldSelection))) {
items.add(NULL_WRAPPER); items.add(NULL_WRAPPER);
} }
DirectoryChooser.ItemWrapper selection = chooseSelection(initialTargetDirectorySourceRoot, fileIndex, items, initial, oldOne); 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>> = private val KOTLIN_AWARE_SOURCE_ROOT_TYPES: Set<JpsModuleSourceRootType<JavaSourceRootProperties>> =
JavaModuleSourceRootTypes.SOURCES + ALL_KOTLIN_SOURCE_ROOT_TYPES JavaModuleSourceRootTypes.SOURCES + ALL_KOTLIN_SOURCE_ROOT_TYPES
private fun Module.collectKotlinAwareDestinationSourceRoots(): List<VirtualFile> { fun Module.collectKotlinAwareDestinationSourceRoots(): List<VirtualFile> {
return rootManager return rootManager
.contentEntries .contentEntries
.asSequence() .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"); 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") @TestMetadata("sealedEmpty.kt")
public void testSealedEmpty() throws Exception { public void testSealedEmpty() throws Exception {
runTest("idea/testData/quickfix/implement/sealedEmpty.kt"); 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") @TestMetadata("sealedWithConflict.kt")
public void testSealedWithConflict() throws Exception { public void testSealedWithConflict() throws Exception {
runTest("idea/testData/quickfix/implement/sealedWithConflict.kt"); 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") @TestMetadata("typeParameter.kt")
public void testTypeParameter() throws Exception { public void testTypeParameter() throws Exception {
runTest("idea/testData/quickfix/implement/typeParameter.kt"); runTest("idea/testData/quickfix/implement/typeParameter.kt");