[kotlin] k2: port generate secondary constructor action

^KTIJ-30776 fixed

GitOrigin-RevId: 9491460bfbaf93586145c87537a4a8b81398c8df
This commit is contained in:
Anna Kozlova
2024-07-26 14:34:47 +00:00
committed by intellij-monorepo-bot
parent 2d3ae1afee
commit 18a6a9d5a3
12 changed files with 498 additions and 0 deletions
@@ -244,6 +244,8 @@
</extensions>
<actions>
<group id="KotlinGenerateGroup">
<action id="KotlinGenerateSecondaryConstructor"
class="org.jetbrains.kotlin.idea.k2.codeinsight.generate.KotlinGenerateSecondaryConstructorAction"/>
<action id="KotlinGenerateEqualsAndHashCode"
class="org.jetbrains.kotlin.idea.k2.codeinsight.generate.KotlinGenerateEqualsAndHashcodeAction"/>
<action id="KotlinGenerateToString"
@@ -0,0 +1,246 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.kotlin.idea.k2.codeinsight.generate
import com.intellij.codeInsight.generation.ClassMember
import com.intellij.codeInsight.generation.PsiElementMemberChooserObject
import com.intellij.ide.util.MemberChooser
import com.intellij.java.JavaBundle
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiMethod
import com.intellij.refactoring.util.CommonRefactoringUtil
import org.jetbrains.kotlin.analysis.api.KaExperimentalApi
import org.jetbrains.kotlin.analysis.api.KaSession
import org.jetbrains.kotlin.analysis.api.components.KaDiagnosticCheckerFilter
import org.jetbrains.kotlin.analysis.api.fir.diagnostics.KaFirDiagnostic
import org.jetbrains.kotlin.analysis.api.symbols.KaClassKind
import org.jetbrains.kotlin.analysis.api.symbols.KaClassSymbol
import org.jetbrains.kotlin.analysis.api.types.KaType
import org.jetbrains.kotlin.analysis.api.types.symbol
import org.jetbrains.kotlin.idea.base.analysis.api.utils.analyzeInModalWindow
import org.jetbrains.kotlin.idea.base.codeInsight.KotlinNameSuggester
import org.jetbrains.kotlin.idea.base.codeInsight.KotlinPsiElementMemberChooserObject
import org.jetbrains.kotlin.idea.base.resources.KotlinBundle
import org.jetbrains.kotlin.idea.core.*
import org.jetbrains.kotlin.idea.core.CollectingNameValidator
import org.jetbrains.kotlin.idea.refactoring.addElement
import org.jetbrains.kotlin.idea.util.application.executeWriteCommand
import org.jetbrains.kotlin.idea.util.application.isUnitTestMode
import org.jetbrains.kotlin.name.StandardClassIds
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.quoteIfNeeded
import org.jetbrains.kotlin.psi.psiUtil.siblings
import org.jetbrains.kotlin.types.Variance
import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull
import org.jetbrains.kotlin.utils.addToStdlib.lastIsInstanceOrNull
class KotlinGenerateSecondaryConstructorAction : KotlinGenerateMemberActionBase<KotlinGenerateSecondaryConstructorAction.Info>() {
class Info(
val propertiesToInitialize: List<KotlinPsiElementMemberChooserObject>,
val superConstructors: List<ClassMember>,
val klass: KtClass
)
override fun isValidForClass(targetClass: KtClassOrObject): Boolean {
return targetClass is KtClass
&& targetClass !is KtEnumEntry
&& !targetClass.isInterface()
&& !targetClass.isAnnotation()
&& !targetClass.hasExplicitPrimaryConstructor()
}
private fun shouldPreselect(element: PsiElement) = element is KtProperty && !element.isVar
context(KaSession)
@OptIn(KaExperimentalApi::class)
private fun chooseSuperConstructors(classSymbol: KaClassSymbol): List<ClassMember> {
val superClassSymbol = getSuperClassSymbolNoAny(classSymbol) ?: return emptyList()
val candidates = superClassSymbol.declaredMemberScope.constructors
.filter { it.isVisibleInClass(classSymbol) }
.mapNotNull { constructorSymbol ->
constructorSymbol.psi?.let { KotlinPsiElementMemberChooserObject.getMemberChooserObject(it) } as? ClassMember
}.toList()
return candidates
}
context(KaSession)
private fun getSuperClassSymbolNoAny(classSymbol: KaClassSymbol): KaClassSymbol? =
classSymbol.superTypes.mapNotNull { it.symbol as? KaClassSymbol }.find { superClassSymbol ->
superClassSymbol.classKind == KaClassKind.CLASS && superClassSymbol.classId != StandardClassIds.Any
}
context(KaSession)
@OptIn(KaExperimentalApi::class)
private fun KtProperty.isPropertyNotInitialized(): Boolean {
return containingKtFile.collectDiagnostics(KaDiagnosticCheckerFilter.ONLY_COMMON_CHECKERS)
.filter { it.psi == this@isPropertyNotInitialized }
.any { it is KaFirDiagnostic.MustBeInitializedOrBeAbstract }
}
context(KaSession)
private fun choosePropertiesToInitialize(klass: KtClassOrObject): List<KotlinPsiElementMemberChooserObject> {
return klass.declarations
.asSequence()
.filterIsInstance<KtProperty>()
.filter { it.isVar || it.isPropertyNotInitialized() }
.map { KotlinPsiElementMemberChooserObject.getKotlinMemberChooserObject(it) }
.toList()
}
override fun prepareMembersInfo(klass: KtClassOrObject, project: Project, editor: Editor): Info? {
if (klass !is KtClass) return null
val preInfo = analyzeInModalWindow(klass, KotlinBundle.message("fix.change.signature.prepare")) {
val classSymbol = klass.symbol as? KaClassSymbol ?: return@analyzeInModalWindow null
val superConstructors = chooseSuperConstructors(classSymbol)
val propertiesToInitialize = choosePropertiesToInitialize(klass)
return@analyzeInModalWindow Info(propertiesToInitialize, superConstructors, klass)
} ?: return null
val superConstructors = preInfo.superConstructors
val chosenConstructors = if (isUnitTestMode() || superConstructors.size <= 1) {
superConstructors
}
else {
val chooser = MemberChooser(superConstructors.toTypedArray(), false, true, klass.project)
chooser.title = JavaBundle.message("generate.constructor.super.constructor.chooser.title")
chooser.setCopyJavadocVisible(false)
chooser.show()
chooser.selectedElements
} ?: return null
val properties = preInfo.propertiesToInitialize
val chosenProperties = if (isUnitTestMode() || properties.isEmpty()) {
properties
}
else {
with(MemberChooser(properties.toTypedArray(), true, true, klass.project, false, null)) {
title = KotlinBundle.message("action.generate.secondary.constructor.choose.properties")
setCopyJavadocVisible(false)
selectElements(properties.filter { shouldPreselect(it.element) }.toTypedArray())
show()
selectedElements
}
} ?: return null
return Info(chosenProperties, chosenConstructors, preInfo.klass)
}
override fun generateMembers(project: Project, editor: Editor, info: Info): List<KtDeclaration> {
val targetClass = info.klass
fun Info.findAnchor(): PsiElement? {
targetClass.declarations.lastIsInstanceOrNull<KtSecondaryConstructor>()?.let { return it }
val lastPropertyToInitialize = propertiesToInitialize.lastOrNull()?.psiElement
val declarationsAfter =
lastPropertyToInitialize?.siblings()?.filterIsInstance<KtDeclaration>() ?: targetClass.declarations.asSequence()
val firstNonProperty = declarationsAfter.firstOrNull { it !is KtProperty } ?: return null
return firstNonProperty.siblings(forward = false).firstIsInstanceOrNull<KtProperty>() ?: targetClass.getOrCreateBody().lBrace
}
return with(info) {
val prototypes =
analyzeInModalWindow(klass, KotlinBundle.message("fix.change.signature.prepare")) {
if (superConstructors.isNotEmpty()) {
superConstructors.mapNotNull { generateConstructor(klass, propertiesToInitialize.mapNotNull { it.psiElement as? KtProperty }, it) }
} else {
listOfNotNull(generateConstructor(klass, propertiesToInitialize.mapNotNull { it.psiElement as? KtProperty }, null))
}
}
if (prototypes.isEmpty()) {
val errorText = KotlinBundle.message("action.generate.secondary.constructor.error.already.exists")
CommonRefactoringUtil.showErrorHint(targetClass.project, editor, errorText, commandName, null)
return emptyList()
}
var constructors: List<KtSecondaryConstructor>? = null
project.executeWriteCommand(commandName) {
constructors = insertMembersAfterAndReformat(editor, targetClass, prototypes, findAnchor())
}
return constructors ?: emptyList()
}
}
context(KaSession)
@OptIn(KaExperimentalApi::class)
private fun generateConstructor(
klass: KtClass,
propertiesToInitialize: List<KtProperty>,
superConstructor: ClassMember?
): KtSecondaryConstructor? {
val classSymbol = klass.symbol as? KaClassSymbol ?: return null
val superConstructorPsi = (superConstructor as? PsiElementMemberChooserObject)?.psiElement
val superConstructorSymbol = when (superConstructorPsi) {
is KtConstructor<*> -> superConstructorPsi.symbol
is PsiMethod -> superConstructorPsi.containingClass?.namedClassSymbol?.declaredMemberScope?.constructors?.find { it.psi == superConstructorPsi }
else -> null
}
val constructorParamTypes = propertiesToInitialize.map { it.returnType } +
(superConstructorSymbol?.valueParameters?.map { it.returnType } ?: emptyList())
fun equalTypes(types1: Collection<KaType>, types2: Collection<KaType>): Boolean {
return types1.size == types2.size && types1.zip(types2).all { it.first.semanticallyEquals(it.second) }
}
if (classSymbol.declaredMemberScope.constructors.any { constructorSymbol ->
constructorSymbol.psi is KtConstructor<*> &&
equalTypes(constructorSymbol.valueParameters.map { it.returnType }, constructorParamTypes)
}
) return null
val psiFactory = KtPsiFactory(klass.project)
val validator = CollectingNameValidator()
val constructor = psiFactory.createSecondaryConstructor("constructor()")
val parameterList = constructor.valueParameterList!!
if (superConstructorSymbol != null) {
val superClassSymbol = superConstructorSymbol.containingSymbol as? KaClassSymbol ?: return null
val substitutor = createInheritanceTypeSubstitutor(classSymbol, superClassSymbol) ?: return null
val delegationCallArguments = ArrayList<String>()
for (parameter in superConstructorSymbol.valueParameters) {
val isVararg = parameter.isVararg
val paramName = suggestSafeNameByName(parameter.name.asString(), validator)
val typeToUse = parameter.returnType
val paramType = substitutor.substitute(typeToUse).render(position = Variance.IN_VARIANCE)
val modifiers = if (isVararg) "vararg " else ""
parameterList.addParameter(psiFactory.createParameter("$modifiers$paramName: $paramType"))
delegationCallArguments.add(if (isVararg) "*$paramName" else paramName)
}
val delegationCall =
psiFactory.creareDelegatedSuperTypeEntry(delegationCallArguments.joinToString(prefix = "super(", postfix = ")"))
constructor.replaceImplicitDelegationCallWithExplicit(false).replace(delegationCall)
}
if (propertiesToInitialize.isNotEmpty()) {
val body = psiFactory.createEmptyBody()
for (property in propertiesToInitialize) {
val propertyName = property.name ?: continue
val paramName = suggestSafeNameByName(propertyName, validator)
val paramType = property.returnType.render(position = Variance.IN_VARIANCE)
parameterList.addParameter(psiFactory.createParameter("$paramName: $paramType"))
body.addElement(psiFactory.createExpression("this.${propertyName.quoteIfNeeded()} = $paramName"), true)
}
constructor.add(body)
}
return constructor
}
private fun suggestSafeNameByName(originalName: String, validator: CollectingNameValidator): String =
KotlinNameSuggester.suggestNameByName(originalName, validator).quoteIfNeeded()
}
@@ -0,0 +1,20 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.kotlin.idea.k2.generate
import com.intellij.codeInsight.actions.CodeInsightAction
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.Presentation
import org.jetbrains.kotlin.idea.base.resources.KotlinBundle
import org.jetbrains.kotlin.idea.base.test.InTextDirectivesUtils
import org.jetbrains.kotlin.idea.codeInsight.generate.AbstractCodeInsightActionTest
import org.jetbrains.kotlin.idea.k2.codeinsight.generate.KotlinGenerateSecondaryConstructorAction
import org.jetbrains.kotlin.idea.k2.codeinsight.generate.KotlinGenerateToStringAction
import org.jetbrains.kotlin.idea.k2.codeinsight.generate.KotlinToStringTemplatesManager
import org.jetbrains.kotlin.psi.KtClassOrObject
import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType
abstract class AbstractFirGenerateSecondaryConstructorActionTest : AbstractCodeInsightActionTest() {
override fun createAction(fileText: String): CodeInsightAction {
return KotlinGenerateSecondaryConstructorAction()
}
}
@@ -0,0 +1,107 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.kotlin.idea.k2.generate;
import com.intellij.testFramework.TestDataPath;
import org.jetbrains.kotlin.idea.base.plugin.KotlinPluginMode;
import org.jetbrains.kotlin.idea.base.test.TestRoot;
import org.jetbrains.kotlin.idea.test.JUnit3RunnerWithInners;
import org.jetbrains.kotlin.idea.test.KotlinTestUtils;
import org.jetbrains.kotlin.test.TestMetadata;
import org.junit.runner.RunWith;
/**
* This class is generated by {@link org.jetbrains.kotlin.testGenerator.generator.TestGenerator}.
* DO NOT MODIFY MANUALLY.
*/
@SuppressWarnings("all")
@TestRoot("code-insight/kotlin.code-insight.k2")
@TestDataPath("$CONTENT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
@TestMetadata("../../idea/tests/testData/codeInsight/generate/secondaryConstructors")
public class FirGenerateSecondaryConstructorActionTestGenerated extends AbstractFirGenerateSecondaryConstructorActionTest {
@java.lang.Override
@org.jetbrains.annotations.NotNull
public final KotlinPluginMode getPluginMode() {
return KotlinPluginMode.K2;
}
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, this, testDataFilePath);
}
@TestMetadata("empty.kt")
public void testEmpty() throws Exception {
runTest("../../idea/tests/testData/codeInsight/generate/secondaryConstructors/empty.kt");
}
@TestMetadata("emptyExists.kt")
public void testEmptyExists() throws Exception {
runTest("../../idea/tests/testData/codeInsight/generate/secondaryConstructors/emptyExists.kt");
}
@TestMetadata("javaSupers.kt")
public void testJavaSupers() throws Exception {
runTest("../../idea/tests/testData/codeInsight/generate/secondaryConstructors/javaSupers.kt");
}
@TestMetadata("javaSupersWithGenerics.kt")
public void testJavaSupersWithGenerics() throws Exception {
runTest("../../idea/tests/testData/codeInsight/generate/secondaryConstructors/javaSupersWithGenerics.kt");
}
@TestMetadata("javaSupersWithKeywords.kt")
public void testJavaSupersWithKeywords() throws Exception {
runTest("../../idea/tests/testData/codeInsight/generate/secondaryConstructors/javaSupersWithKeywords.kt");
}
@TestMetadata("primaryExists.kt")
public void testPrimaryExists() throws Exception {
runTest("../../idea/tests/testData/codeInsight/generate/secondaryConstructors/primaryExists.kt");
}
@TestMetadata("properties.kt")
public void testProperties() throws Exception {
runTest("../../idea/tests/testData/codeInsight/generate/secondaryConstructors/properties.kt");
}
@TestMetadata("propertiesWithKeywords.kt")
public void testPropertiesWithKeywords() throws Exception {
runTest("../../idea/tests/testData/codeInsight/generate/secondaryConstructors/propertiesWithKeywords.kt");
}
@TestMetadata("propertiesWithSupers.kt")
public void testPropertiesWithSupers() throws Exception {
runTest("../../idea/tests/testData/codeInsight/generate/secondaryConstructors/propertiesWithSupers.kt");
}
@TestMetadata("superInvisible.kt")
public void testSuperInvisible() throws Exception {
runTest("../../idea/tests/testData/codeInsight/generate/secondaryConstructors/superInvisible.kt");
}
@TestMetadata("supers.kt")
public void testSupers() throws Exception {
runTest("../../idea/tests/testData/codeInsight/generate/secondaryConstructors/supers.kt");
}
@TestMetadata("supersAllExist.kt")
public void testSupersAllExist() throws Exception {
runTest("../../idea/tests/testData/codeInsight/generate/secondaryConstructors/supersAllExist.kt");
}
@TestMetadata("supersSomeExist.kt")
public void testSupersSomeExist() throws Exception {
runTest("../../idea/tests/testData/codeInsight/generate/secondaryConstructors/supersSomeExist.kt");
}
@TestMetadata("supersWithGenerics.kt")
public void testSupersWithGenerics() throws Exception {
runTest("../../idea/tests/testData/codeInsight/generate/secondaryConstructors/supersWithGenerics.kt");
}
@TestMetadata("supersWithVarargs.kt")
public void testSupersWithVarargs() throws Exception {
runTest("../../idea/tests/testData/codeInsight/generate/secondaryConstructors/supersWithVarargs.kt");
}
}
@@ -75,6 +75,11 @@ public class CodeInsightActionTestGenerated extends AbstractCodeInsightActionTes
runTest("testData/codeInsight/generate/secondaryConstructors/propertiesWithSupers.kt");
}
@TestMetadata("superInvisible.kt")
public void testSuperInvisible() throws Exception {
runTest("testData/codeInsight/generate/secondaryConstructors/superInvisible.kt");
}
@TestMetadata("supers.kt")
public void testSupers() throws Exception {
runTest("testData/codeInsight/generate/secondaryConstructors/supers.kt");
@@ -0,0 +1,15 @@
// ACTION_CLASS: org.jetbrains.kotlin.idea.actions.generate.KotlinGenerateSecondaryConstructorAction
class Foo<U> : Base<U, Int> {
val x = 1
<selection><caret></selection>constructor(x: U) : super(x)
constructor(x: U, y: Int) : super(x, y)
fun foo() {
}
fun bar() {
}
}
@@ -0,0 +1,26 @@
// ACTION_CLASS: org.jetbrains.kotlin.idea.actions.generate.KotlinGenerateSecondaryConstructorAction
open class Base(n: Int) {
constructor(a: Int, b: Int): this(a + b)
}
class Foo : Base {
val n: Int
val x = 1
<selection><caret></selection>constructor(n: Int, n1: Int) : super(n) {
this.n = n1
}
constructor(a: Int, b: Int, n: Int) : super(a, b) {
this.n = n
}
fun foo() {
}
fun bar() {
}
}
@@ -0,0 +1,16 @@
// ACTION_CLASS: org.jetbrains.kotlin.idea.actions.generate.KotlinGenerateSecondaryConstructorAction
open class Base(n: Int) {
private constructor(a: Int, b: Int): this(a + b)
}
class Foo : Base {<caret>
val x = 1
fun foo() {
}
fun bar() {
}
}
@@ -0,0 +1,18 @@
// ACTION_CLASS: org.jetbrains.kotlin.idea.actions.generate.KotlinGenerateSecondaryConstructorAction
open class Base(n: Int) {
private constructor(a: Int, b: Int): this(a + b)
}
class Foo : Base {
val x = 1
<selection><caret></selection>constructor(n: Int) : super(n)
fun foo() {
}
fun bar() {
}
}
@@ -0,0 +1,19 @@
// ACTION_CLASS: org.jetbrains.kotlin.idea.actions.generate.KotlinGenerateSecondaryConstructorAction
open class Base(n: Int) {
constructor(a: Int, b: Int): this(a + b)
}
class Foo : Base {
val x = 1
<selection><caret></selection>constructor(n: Int) : super(n)
constructor(a: Int, b: Int) : super(a, b)
fun foo() {
}
fun bar() {
}
}
@@ -0,0 +1,19 @@
// ACTION_CLASS: org.jetbrains.kotlin.idea.actions.generate.KotlinGenerateSecondaryConstructorAction
open class Base<X, Y>(n: X) {
constructor(x: X, y: Y): this(x)
}
class Foo<U> : Base<U, Int> {
val x = 1
<selection><caret></selection>constructor(n: U) : super(n)
constructor(x: U, y: Int) : super(x, y)
fun foo() {
}
fun bar() {
}
}
@@ -8,6 +8,7 @@ import org.jetbrains.kotlin.idea.k2.AbstractKotlinFirJoinLinesTest
import org.jetbrains.kotlin.idea.k2.AbstractKotlinFirPairMatcherTest
import org.jetbrains.kotlin.idea.k2.copyPaste.AbstractK2InsertImportOnPasteTest
import org.jetbrains.kotlin.idea.k2.generate.AbstractFirGenerateHashCodeAndEqualsActionTest
import org.jetbrains.kotlin.idea.k2.generate.AbstractFirGenerateSecondaryConstructorActionTest
import org.jetbrains.kotlin.idea.k2.generate.AbstractFirGenerateToStringActionTest
import org.jetbrains.kotlin.idea.k2.hierarchy.AbstractFirHierarchyTest
import org.jetbrains.kotlin.idea.k2.hierarchy.AbstractFirHierarchyWithLibTest
@@ -156,6 +157,10 @@ internal fun MutableTWorkspace.generateK2CodeInsightTests() {
model("../../../idea/tests/testData/codeInsight/generate/toString")
}
testClass<AbstractFirGenerateSecondaryConstructorActionTest> {
model("../../../idea/tests/testData/codeInsight/generate/secondaryConstructors")
}
testClass<AbstractKotlinFirJoinLinesTest> {
model("../../../idea/tests/testData/joinLines")
}