From 233f2b468d41036b47c8ce837967b9e65aedeec6 Mon Sep 17 00:00:00 2001 From: Alexey Sedunov Date: Fri, 4 Mar 2016 13:13:07 +0300 Subject: [PATCH] Spring Support: "Generate Dependency..." actions #KT-11407 Fixed --- ChangeLog.md | 1 + .../kotlin/generators/tests/GenerateTests.kt | 5 + .../caches/resolve/JavaResolveExtension.kt | 7 + .../kotlin/idea/core/NameValidators.kt | 23 +- .../src/META-INF/kotlin-spring.xml | 15 + ...enerateKotlinSpringBeanDependencyAction.kt | 73 +++ .../generate/generateDependenciesUtils.kt | 497 ++++++++++++++++++ ...tractGenerateSpringDependencyActionTest.kt | 68 +++ ...teSpringDependencyActionTestGenerated.java | 67 +++ .../kotlin/idea/editor/BatchTemplateRunner.kt | 74 +++ .../idea/formatter/kotlinSpacingRules.kt | 1 + ...KotlinNestedClassesToUpperLevelDialog.java | 4 +- .../factoryBean-config.xml | 9 + .../factoryBean-config.xml.after | 11 + .../factoryBean.kt.after | 13 + .../factoryBean.kt_disabled | 13 + .../factoryMethod-config.xml | 8 + .../factoryMethod-config.xml.after | 10 + .../factoryMethod.kt.after | 14 + .../factoryMethod.kt_disabled | 9 + .../firstConstructor-config.xml | 8 + .../firstConstructor-config.xml.after | 10 + .../beanDependenciesByXml/firstConstructor.kt | 9 + .../firstConstructor.kt.after | 9 + .../primaryConstructorAddParam-config.xml | 11 + ...rimaryConstructorAddParam-config.xml.after | 12 + .../primaryConstructorAddParam.kt | 11 + .../primaryConstructorAddParam.kt.after | 11 + .../beanDependenciesByXml/property-config.xml | 8 + .../property-config.xml.after | 10 + .../beanDependenciesByXml/property.kt | 9 + .../beanDependenciesByXml/property.kt.after | 10 + .../secondaryConstructor-config.xml | 8 + .../secondaryConstructor-config.xml.after | 8 + .../secondaryConstructor.kt.after | 10 + .../secondaryConstructor.kt_disabled | 9 + .../secondaryConstructorAddParam-config.xml | 11 + ...ondaryConstructorAddParam-config.xml.after | 12 + .../secondaryConstructorAddParam.kt | 12 + .../secondaryConstructorAddParam.kt.after | 12 + .../beanDependenciesByXml/setter-config.xml | 8 + .../setter-config.xml.after | 10 + .../generate/beanDependenciesByXml/setter.kt | 9 + .../beanDependenciesByXml/setter.kt.after | 11 + .../generate/AbstractCodeInsightActionTest.kt | 11 + 45 files changed, 1163 insertions(+), 8 deletions(-) create mode 100644 idea/idea-ultimate/src/org/jetbrains/kotlin/idea/spring/generate/GenerateKotlinSpringBeanDependencyAction.kt create mode 100644 idea/idea-ultimate/src/org/jetbrains/kotlin/idea/spring/generate/generateDependenciesUtils.kt create mode 100644 idea/idea-ultimate/tests/org/jetbrains/kotlin/idea/spring/tests/generate/AbstractGenerateSpringDependencyActionTest.kt create mode 100644 idea/idea-ultimate/tests/org/jetbrains/kotlin/idea/spring/tests/generate/GenerateSpringDependencyActionTestGenerated.java create mode 100644 idea/src/org/jetbrains/kotlin/idea/editor/BatchTemplateRunner.kt create mode 100644 idea/testData/spring/core/generate/beanDependenciesByXml/factoryBean-config.xml create mode 100644 idea/testData/spring/core/generate/beanDependenciesByXml/factoryBean-config.xml.after create mode 100644 idea/testData/spring/core/generate/beanDependenciesByXml/factoryBean.kt.after create mode 100644 idea/testData/spring/core/generate/beanDependenciesByXml/factoryBean.kt_disabled create mode 100644 idea/testData/spring/core/generate/beanDependenciesByXml/factoryMethod-config.xml create mode 100644 idea/testData/spring/core/generate/beanDependenciesByXml/factoryMethod-config.xml.after create mode 100644 idea/testData/spring/core/generate/beanDependenciesByXml/factoryMethod.kt.after create mode 100644 idea/testData/spring/core/generate/beanDependenciesByXml/factoryMethod.kt_disabled create mode 100644 idea/testData/spring/core/generate/beanDependenciesByXml/firstConstructor-config.xml create mode 100644 idea/testData/spring/core/generate/beanDependenciesByXml/firstConstructor-config.xml.after create mode 100644 idea/testData/spring/core/generate/beanDependenciesByXml/firstConstructor.kt create mode 100644 idea/testData/spring/core/generate/beanDependenciesByXml/firstConstructor.kt.after create mode 100644 idea/testData/spring/core/generate/beanDependenciesByXml/primaryConstructorAddParam-config.xml create mode 100644 idea/testData/spring/core/generate/beanDependenciesByXml/primaryConstructorAddParam-config.xml.after create mode 100644 idea/testData/spring/core/generate/beanDependenciesByXml/primaryConstructorAddParam.kt create mode 100644 idea/testData/spring/core/generate/beanDependenciesByXml/primaryConstructorAddParam.kt.after create mode 100644 idea/testData/spring/core/generate/beanDependenciesByXml/property-config.xml create mode 100644 idea/testData/spring/core/generate/beanDependenciesByXml/property-config.xml.after create mode 100644 idea/testData/spring/core/generate/beanDependenciesByXml/property.kt create mode 100644 idea/testData/spring/core/generate/beanDependenciesByXml/property.kt.after create mode 100644 idea/testData/spring/core/generate/beanDependenciesByXml/secondaryConstructor-config.xml create mode 100644 idea/testData/spring/core/generate/beanDependenciesByXml/secondaryConstructor-config.xml.after create mode 100644 idea/testData/spring/core/generate/beanDependenciesByXml/secondaryConstructor.kt.after create mode 100644 idea/testData/spring/core/generate/beanDependenciesByXml/secondaryConstructor.kt_disabled create mode 100644 idea/testData/spring/core/generate/beanDependenciesByXml/secondaryConstructorAddParam-config.xml create mode 100644 idea/testData/spring/core/generate/beanDependenciesByXml/secondaryConstructorAddParam-config.xml.after create mode 100644 idea/testData/spring/core/generate/beanDependenciesByXml/secondaryConstructorAddParam.kt create mode 100644 idea/testData/spring/core/generate/beanDependenciesByXml/secondaryConstructorAddParam.kt.after create mode 100644 idea/testData/spring/core/generate/beanDependenciesByXml/setter-config.xml create mode 100644 idea/testData/spring/core/generate/beanDependenciesByXml/setter-config.xml.after create mode 100644 idea/testData/spring/core/generate/beanDependenciesByXml/setter.kt create mode 100644 idea/testData/spring/core/generate/beanDependenciesByXml/setter.kt.after diff --git a/ChangeLog.md b/ChangeLog.md index d46505e1eba..7073a247745 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -129,6 +129,7 @@ New features: - [KT-3741](https://youtrack.jetbrains.com/issue/KT-3741) Show Spring-specific line markers on Kotlin classes - [KT-11406](https://youtrack.jetbrains.com/issue/KT-11406) Support Spring EL injections inside of Kotlin string literals - [KT-11604](https://youtrack.jetbrains.com/issue/KT-11604) Support "Configure Spring facet" inspection on Kotlin classes +- [KT-11407](https://youtrack.jetbrains.com/issue/KT-11407) Implemented "Generate Spring Dependency..." actions Issues fixed: diff --git a/generators/src/org/jetbrains/kotlin/generators/tests/GenerateTests.kt b/generators/src/org/jetbrains/kotlin/generators/tests/GenerateTests.kt index e5eb33d8f49..e37da052342 100644 --- a/generators/src/org/jetbrains/kotlin/generators/tests/GenerateTests.kt +++ b/generators/src/org/jetbrains/kotlin/generators/tests/GenerateTests.kt @@ -99,6 +99,7 @@ import org.jetbrains.kotlin.idea.refactoring.pushDown.AbstractPushDownTest import org.jetbrains.kotlin.idea.refactoring.rename.AbstractRenameTest import org.jetbrains.kotlin.idea.refactoring.safeDelete.AbstractSafeDeleteTest import org.jetbrains.kotlin.idea.resolve.* +import org.jetbrains.kotlin.idea.spring.tests.generate.AbstractGenerateSpringDependencyActionTest import org.jetbrains.kotlin.idea.spring.tests.references.AbstractSpringReferenceCompletionHandlerTest import org.jetbrains.kotlin.idea.spring.tests.references.AbstractSpringReferenceCompletionTest import org.jetbrains.kotlin.idea.spring.tests.references.AbstractSpringReferenceNavigationTest @@ -814,6 +815,10 @@ fun main(args: Array) { testClass() { model("spring/core/references/navigation") } + + testClass() { + model("spring/core/generate", pattern = "^([\\w]+)\\.kt$", singleClass = true) + } } testGroup("idea/tests", "compiler/testData") { diff --git a/idea/idea-analysis/src/org/jetbrains/kotlin/idea/caches/resolve/JavaResolveExtension.kt b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/caches/resolve/JavaResolveExtension.kt index a78c36bdc38..7b79ab206e6 100644 --- a/idea/idea-analysis/src/org/jetbrains/kotlin/idea/caches/resolve/JavaResolveExtension.kt +++ b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/caches/resolve/JavaResolveExtension.kt @@ -32,6 +32,7 @@ import org.jetbrains.kotlin.load.java.structure.impl.* import org.jetbrains.kotlin.name.Name import org.jetbrains.kotlin.psi.KtClassOrObject import org.jetbrains.kotlin.psi.KtDeclaration +import org.jetbrains.kotlin.psi.psiUtil.parameterIndex import org.jetbrains.kotlin.resolve.jvm.JavaDescriptorResolver import org.jetbrains.kotlin.resolve.jvm.platform.JvmPlatform import org.jetbrains.kotlin.resolve.scopes.MemberScope @@ -82,6 +83,12 @@ fun PsiMember.getJavaOrKotlinMemberDescriptor(resolutionFacade: ResolutionFacade } } +fun PsiParameter.getParameterDescriptor(resolutionFacade: ResolutionFacade? = null): ValueParameterDescriptor? { + val method = declarationScope as? PsiMethod ?: return null + val methodDescriptor = method.getJavaMethodDescriptor(resolutionFacade) ?: return null + return methodDescriptor.valueParameters[parameterIndex()] +} + fun PsiClass.resolveToDescriptor( resolutionFacade: ResolutionFacade, declarationTranslator: (KtClassOrObject) -> KtClassOrObject? = { it } diff --git a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/NameValidators.kt b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/NameValidators.kt index 27a7b1ec630..fc350f788ca 100644 --- a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/NameValidators.kt +++ b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/NameValidators.kt @@ -18,6 +18,7 @@ package org.jetbrains.kotlin.idea.core import com.intellij.psi.PsiElement import org.jetbrains.kotlin.descriptors.DeclarationDescriptor +import org.jetbrains.kotlin.descriptors.DeclarationDescriptorWithSource import org.jetbrains.kotlin.descriptors.DeclarationDescriptorWithVisibility import org.jetbrains.kotlin.idea.caches.resolve.analyze import org.jetbrains.kotlin.idea.caches.resolve.getResolutionFacade @@ -35,6 +36,7 @@ import org.jetbrains.kotlin.resolve.descriptorUtil.isExtension import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode import org.jetbrains.kotlin.resolve.scopes.LexicalScope import org.jetbrains.kotlin.resolve.scopes.utils.findClassifier +import org.jetbrains.kotlin.resolve.source.getPsi import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull import java.util.* @@ -60,14 +62,18 @@ class CollectingNameValidator @JvmOverloads constructor( class NewDeclarationNameValidator( private val visibleDeclarationsContext: KtElement?, private val checkDeclarationsIn: Sequence, - private val target: NewDeclarationNameValidator.Target + private val target: NewDeclarationNameValidator.Target, + private val excludedDeclarations: List = emptyList() ) : (String) -> Boolean { - - constructor(container: PsiElement, anchor: PsiElement?, target: NewDeclarationNameValidator.Target) + constructor(container: PsiElement, + anchor: PsiElement?, + target: NewDeclarationNameValidator.Target, + excludedDeclarations: List = emptyList()) : this( (anchor ?: container).parentsWithSelf.firstIsInstanceOrNull(), anchor?.siblings() ?: container.allChildren, - target) + target, + excludedDeclarations) enum class Target { VARIABLES, @@ -88,6 +94,8 @@ class NewDeclarationNameValidator( } } + private fun notExcluded(it: DeclarationDescriptorWithSource) = it.source.getPsi() !in excludedDeclarations + private fun LexicalScope.hasConflict(name: Name): Boolean { fun DeclarationDescriptor.isVisible(): Boolean { return when (this) { @@ -98,14 +106,15 @@ class NewDeclarationNameValidator( return when(target) { Target.VARIABLES -> - getAllAccessibleVariables(name).any { !it.isExtension && it.isVisible() } + getAllAccessibleVariables(name).any { !it.isExtension && it.isVisible() && notExcluded(it) } Target.FUNCTIONS_AND_CLASSES -> - getAllAccessibleFunctions(name).any { !it.isExtension && it.isVisible() } || - findClassifier(name, NoLookupLocation.FROM_IDE)?.let { it.isVisible() } ?: false + getAllAccessibleFunctions(name).any { !it.isExtension && it.isVisible() && notExcluded(it) } || + findClassifier(name, NoLookupLocation.FROM_IDE)?.let { it.isVisible() && notExcluded(it) } ?: false } } private fun KtNamedDeclaration.isConflicting(name: Name): Boolean { + if (this in excludedDeclarations) return false if (nameAsName != name) return false if (this is KtCallableDeclaration && receiverTypeReference != null) return false return when(target) { diff --git a/idea/idea-ultimate/src/META-INF/kotlin-spring.xml b/idea/idea-ultimate/src/META-INF/kotlin-spring.xml index 19d611f2172..299bf1b55c0 100644 --- a/idea/idea-ultimate/src/META-INF/kotlin-spring.xml +++ b/idea/idea-ultimate/src/META-INF/kotlin-spring.xml @@ -22,4 +22,19 @@ level="WARNING" implementationClass="org.jetbrains.kotlin.idea.spring.inspections.KotlinSpringFacetCodeInspection"/> + + + + + + + + + + + + \ No newline at end of file diff --git a/idea/idea-ultimate/src/org/jetbrains/kotlin/idea/spring/generate/GenerateKotlinSpringBeanDependencyAction.kt b/idea/idea-ultimate/src/org/jetbrains/kotlin/idea/spring/generate/GenerateKotlinSpringBeanDependencyAction.kt new file mode 100644 index 00000000000..992a7836d7d --- /dev/null +++ b/idea/idea-ultimate/src/org/jetbrains/kotlin/idea/spring/generate/GenerateKotlinSpringBeanDependencyAction.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2010-2016 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. + */ + +package org.jetbrains.kotlin.idea.spring.generate + +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiFile +import com.intellij.psi.xml.XmlFile +import com.intellij.spring.SpringBundle +import com.intellij.spring.SpringManager +import com.intellij.spring.model.actions.generate.GenerateSpringBeanDependenciesUtil +import com.intellij.spring.model.utils.SpringModelUtils +import icons.SpringApiIcons +import org.jetbrains.kotlin.asJava.toLightClass +import org.jetbrains.kotlin.idea.actions.generate.KotlinGenerateActionBase +import org.jetbrains.kotlin.idea.editor.BatchTemplateRunner +import org.jetbrains.kotlin.idea.util.application.executeWriteCommand +import org.jetbrains.kotlin.psi.KtClass +import org.jetbrains.kotlin.psi.KtClassOrObject + +abstract class GenerateKotlinSpringBeanDependencyAction( + text: String, + private val injectionKind: SpringDependencyInjectionKind +) : KotlinGenerateActionBase() { + class Constructor: GenerateKotlinSpringBeanDependencyAction( + SpringBundle.message("action.Spring.Beans.Generate.Constructor.Dependency.Action.text"), + SpringDependencyInjectionKind.CONSTRUCTOR + ) + class Setter: GenerateKotlinSpringBeanDependencyAction( + SpringBundle.message("action.Spring.Beans.Generate.Setter.Dependency.Action.text"), + SpringDependencyInjectionKind.SETTER + ) + class LateinitProperty: GenerateKotlinSpringBeanDependencyAction( + "Spring 'lateinit' Dependency...", + SpringDependencyInjectionKind.LATEINIT_PROPERTY + ) + + init { + templatePresentation.text = text + templatePresentation.icon = SpringApiIcons.Spring + } + + override fun isValidForClass(targetClass: KtClassOrObject): Boolean { + if (targetClass !is KtClass || targetClass.isInterface() || targetClass.isEnum() || targetClass.fqName == null) return false + val lightClass = targetClass.toLightClass() ?: return false + val module = GenerateSpringBeanDependenciesUtil.getSpringModule(lightClass) ?: return false + return SpringManager.getInstance(module.project).getCombinedModel(module).configFiles.any { it is XmlFile } + } + + override fun invoke(project: Project, editor: Editor, file: PsiFile) { + val klass = getTargetClass(editor, file) as? KtClass ?: return + val lightClass = klass.toLightClass() ?: return + val springModel = SpringModelUtils.getInstance().getPsiClassSpringModel(lightClass) + val templates = project.executeWriteCommand>("") { + generateDependenciesFor(springModel, lightClass, injectionKind) + } + templates.forEach { it.runTemplates() } + } +} \ No newline at end of file diff --git a/idea/idea-ultimate/src/org/jetbrains/kotlin/idea/spring/generate/generateDependenciesUtils.kt b/idea/idea-ultimate/src/org/jetbrains/kotlin/idea/spring/generate/generateDependenciesUtils.kt new file mode 100644 index 00000000000..1b55a04992f --- /dev/null +++ b/idea/idea-ultimate/src/org/jetbrains/kotlin/idea/spring/generate/generateDependenciesUtils.kt @@ -0,0 +1,497 @@ +/* + * Copyright 2010-2016 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. + */ + +package org.jetbrains.kotlin.idea.spring.generate + +import com.intellij.codeInsight.lookup.LookupElement +import com.intellij.codeInsight.lookup.LookupElementBuilder +import com.intellij.codeInsight.template.Expression +import com.intellij.codeInsight.template.ExpressionContext +import com.intellij.codeInsight.template.TemplateBuilderImpl +import com.intellij.codeInsight.template.TextResult +import com.intellij.codeInspection.SmartHashMap +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Key +import com.intellij.psi.* +import com.intellij.psi.codeStyle.CodeStyleManager +import com.intellij.psi.util.PropertyUtil +import com.intellij.psi.util.PsiTypesUtil +import com.intellij.spring.CommonSpringModel +import com.intellij.spring.model.CommonSpringBean +import com.intellij.spring.model.SpringBeanPointer +import com.intellij.spring.model.SpringModelSearchParameters +import com.intellij.spring.model.actions.generate.GenerateSpringBeanDependenciesUtil +import com.intellij.spring.model.actions.generate.GenerateSpringBeanDependenciesUtil.* +import com.intellij.spring.model.actions.generate.SpringBeanClassMember +import com.intellij.spring.model.highlighting.SpringConstructorArgResolveUtil +import com.intellij.spring.model.utils.SpringBeanCoreUtils +import com.intellij.spring.model.utils.SpringBeanUtils +import com.intellij.spring.model.utils.SpringModelSearchers +import com.intellij.spring.model.utils.SpringModelUtils +import com.intellij.spring.model.xml.DomSpringBean +import com.intellij.spring.model.xml.beans.Beans +import com.intellij.spring.model.xml.beans.SpringBean +import com.intellij.util.IncorrectOperationException +import com.intellij.util.xml.DomElement +import com.intellij.util.xml.DomUtil +import org.jetbrains.annotations.NotNull +import org.jetbrains.annotations.TestOnly +import org.jetbrains.kotlin.asJava.KtLightClass +import org.jetbrains.kotlin.asJava.KtLightMethod +import org.jetbrains.kotlin.descriptors.CallableDescriptor +import org.jetbrains.kotlin.descriptors.ClassDescriptor +import org.jetbrains.kotlin.idea.caches.resolve.* +import org.jetbrains.kotlin.idea.completion.BasicLookupElementFactory +import org.jetbrains.kotlin.idea.completion.InsertHandlerProvider +import org.jetbrains.kotlin.idea.core.KotlinNameSuggester +import org.jetbrains.kotlin.idea.core.NewDeclarationNameValidator +import org.jetbrains.kotlin.idea.core.getOrCreateCompanionObject +import org.jetbrains.kotlin.idea.editor.BatchTemplateRunner +import org.jetbrains.kotlin.idea.util.CallType +import org.jetbrains.kotlin.idea.util.IdeDescriptorRenderers +import org.jetbrains.kotlin.idea.util.ShortenReferences +import org.jetbrains.kotlin.load.java.propertyNameBySetMethodName +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.types.KotlinType +import org.jetbrains.kotlin.types.expressions.TypeReconstructionUtil +import org.jetbrains.kotlin.utils.SmartList +import org.jetbrains.kotlin.utils.ifEmpty +import java.util.* + +// TODO: GenerateSpringBeanDependenciesUtil.ensureFileWritable() is not accessible here +private fun DomElement.ensureFileWritable() = ensureFileWritable(DomUtil.getFile(this).virtualFile, manager.project) + +// TODO: GenerateSpringBeanDependenciesUtil.getReferenceName() is not accessible here +private fun getReferencedName(currentBean: SpringBean, bean: SpringBeanPointer): String? { + val model = SpringModelUtils.getInstance().getSpringModel(currentBean) + return SpringBeanCoreUtils.getReferencedName(bean, model.allCommonBeans) +} + +// TODO: GenerateSpringBeanDependenciesUtil.getExistedSetter() is not accessible here +private fun getExistedSetter(currentBeanClass: PsiClass, setterPsiClass: PsiClass): PsiMethod? { + val psiClassType = JavaPsiFacade.getInstance(setterPsiClass.project).elementFactory.createType(setterPsiClass) + return currentBeanClass.allMethods.firstOrNull { + PropertyUtil.isSimplePropertySetter(it) && it.parameterList.parameters.first().type.isAssignableFrom(psiClassType) + } +} + +// TODO: GenerateSpringBeanDependenciesUtil.findConstructor() is not accessible here +private fun findConstructor(constructors: Array, psiParameterTypes: List): PsiMethod? { + return constructors.firstOrNull { + val parameters = it.parameterList.parameters + if (it.parameterList.parametersCount != psiParameterTypes.size) return@firstOrNull false + (psiParameterTypes zip parameters).all { it.first.isAssignableFrom(it.second.type) } + } +} + +// TODO: GenerateSpringBeanDependenciesUtil.findExistedConstructor() is not accessible here +private fun findExistedConstructor( + currentBean: SpringBean, + currentBeanClass: PsiClass, + candidateParameterClasses: Array +): PsiMethod? { + val constructors = SpringConstructorArgResolveUtil.findMatchingMethods(currentBean) + for (candidateBeanClass in candidateParameterClasses) { + for (constructor in constructors) { + val psiParameterTypes = SmartList().apply { + constructor.parameterList.parameters.mapTo(this) { it.type } + add(PsiTypesUtil.getClassType(candidateBeanClass)) + } + findConstructor(currentBeanClass.constructors, psiParameterTypes)?.let { return it } + } + } + + return null +} + +// TODO: GenerateSpringBeanDependenciesUtil.createSpringBean() is not accessible here +private fun createSpringBean(parentBeans: Beans, psiClass: PsiClass): SpringBean? { + if (!((parentBeans as DomElement)).ensureFileWritable()) return null + + return parentBeans.addBean().apply { + clazz.stringValue = psiClass.qualifiedName + id.stringValue = SpringBeanCoreUtils.suggestBeanNames(this).firstOrNull() ?: "" + } +} + +private fun SpringBean.getFactoryFunctionName(factoryBeanClass: KtClass): String { + factoryMethod.stringValue?.let { if (!it.isBlank()) return it } + + val existingNames = factoryBeanClass.declarations.mapNotNull { if (it is KtFunction || it is KtClassOrObject) it.name else null } + return KotlinNameSuggester.suggestNameByName("create${beanClass!!.name}") { it !in existingNames } +} + +private val PsiClass.defaultTypeText: String + get() { + val qName = qualifiedName ?: return "Any" + val typeParameters = typeParameters + if (typeParameters.isEmpty()) return qName + return TypeReconstructionUtil.getTypeNameAndStarProjectionsString(qName, typeParameters.size) + } + +private fun createSettable( + candidateBean: SpringBeanPointer, + currentBeanClass: KtLightClass, + candidateBeanClasses: Array, + injectionKind: SpringDependencyInjectionKind +): KtNamedDeclaration { + val project = currentBeanClass.project + val psiFactory = KtPsiFactory(project) + val beanName = candidateBean.name + try { + val candidateClass = candidateBeanClasses.first() + val propertyName = if (beanName != null && KotlinNameSuggester.isIdentifier(beanName)) beanName else candidateClass.name!! + + val prototype: KtNamedDeclaration = when (injectionKind) { + SpringDependencyInjectionKind.SETTER -> { + psiFactory.createFunction("fun set${propertyName.capitalize()}(${propertyName.decapitalize()}: ${candidateClass.defaultTypeText}) { }") + } + + SpringDependencyInjectionKind.LATEINIT_PROPERTY -> { + psiFactory.createProperty("lateinit var ${propertyName.decapitalize()}: ${candidateClass.defaultTypeText}") + } + + else -> error("Unexpected injection kind: $injectionKind") + } + return currentBeanClass.kotlinOrigin!!.addDeclaration(prototype).apply { ShortenReferences.DEFAULT.process(this) } + } + catch (e: IncorrectOperationException) { + throw RuntimeException(e) + } +} + +private fun getSuggestedNames( + beanPointer: SpringBeanPointer, + declaration: KtCallableDeclaration, + getType: CallableDescriptor.() -> KotlinType? +): Collection { + val names = LinkedHashSet() + + val validator = NewDeclarationNameValidator(declaration.parent, null, NewDeclarationNameValidator.Target.VARIABLES, listOf(declaration)) + + SpringBeanUtils.getInstance() + .findBeanNames(beanPointer.springBean) + .asSequence() + .filter { KotlinNameSuggester.isIdentifier(it) } + .mapTo(names) { KotlinNameSuggester.suggestNameByName(it, validator) } + + (declaration.resolveToDescriptor() as CallableDescriptor).getType()?.let { + names += KotlinNameSuggester.suggestNamesByType(it, validator) + } + + return names +} + +private fun TemplateBuilderImpl.appendVariableTemplate( + variable: KtCallableDeclaration, + candidateBeanClasses: Array, + computeSuggestions: (() -> Collection)? +) { + if (computeSuggestions != null) { + val suggestedNames = computeSuggestions() + val defaultName = variable.name ?: "" + val nameExpression = object : Expression() { + override fun calculateResult(context: ExpressionContext) = TextResult(defaultName) + + override fun calculateQuickResult(context: ExpressionContext) = calculateResult(context) + + override fun calculateLookupItems(context: ExpressionContext): Array? { + PsiDocumentManager.getInstance(context.project).commitAllDocuments() + return suggestedNames.map { LookupElementBuilder.create(it) }.toTypedArray() + + } + } + replaceElement(variable.nameIdentifier, "names", nameExpression, true) + } + + val superTypeVariants = getSuperTypeVariants(candidateBeanClasses) + if (superTypeVariants.size > 1) { + val lookupFactory = BasicLookupElementFactory(variable.project, InsertHandlerProvider(CallType.TYPE) { emptyList() }) + val typeReferenceExpression = object : Expression() { + override fun calculateResult(context: ExpressionContext) = TextResult(candidateBeanClasses.first().qualifiedName ?: "") + + override fun calculateQuickResult(context: ExpressionContext) = calculateResult(context) + + override fun calculateLookupItems(context: ExpressionContext): Array? { + return superTypeVariants + .mapTo(LinkedHashSet()) { lookupFactory.createLookupElementForJavaClass(it, qualifyNestedClasses = true) } + .toTypedArray() + } + } + replaceElement(variable.typeReference, "type", typeReferenceExpression, true) + } +} + +private fun BatchTemplateRunner.addCreateFunctionTemplate( + function: KtFunction, + candidateBeanClasses: Map>, + dependency: SpringBeanPointer +) { + val parameterList = function.valueParameterList!! + val builder = TemplateBuilderImpl(parameterList) + addTemplateFactory(parameterList) { + for ((paramIndex, candidateBeanClassesForParam) in candidateBeanClasses) { + builder.appendVariableTemplate(function.valueParameters[paramIndex], candidateBeanClassesForParam) { + getSuggestedNames(dependency, function) { valueParameters[paramIndex].type } + } + } + + builder.buildInlineTemplate() + } +} + +private fun BatchTemplateRunner.addCreatePropertyTemplate(property: KtProperty, candidateBeanClasses: Array) { + val builder = TemplateBuilderImpl(property) + addTemplateFactory(property) { + builder.appendVariableTemplate(property, candidateBeanClasses, null) + builder.buildInlineTemplate() + } +} + +private fun BatchTemplateRunner.addCreateFunctionTemplate( + function: KtFunction, + paramIndex: Int, + candidateBeanClasses: Array, + dependency: SpringBeanPointer +) { + addCreateFunctionTemplate(function, mapOf(paramIndex to candidateBeanClasses), dependency) +} + +private fun getOrCreateSetter( + candidateBean: SpringBeanPointer, + currentBeanClass: KtLightClass, + candidateBeanClasses: Array, + templatesHolder: BatchTemplateRunner, + injectionKind: SpringDependencyInjectionKind +): PsiNamedElement? { + for (candidateBeanClass in candidateBeanClasses) { + getExistedSetter(currentBeanClass, candidateBeanClass)?.let { return it } + } + + if (!ensureFileWritable(currentBeanClass)) return null + val settable = createSettable(candidateBean, currentBeanClass, candidateBeanClasses, injectionKind) + when (settable) { + is KtNamedFunction -> templatesHolder.addCreateFunctionTemplate(settable, 0, candidateBeanClasses, candidateBean) + is KtProperty -> templatesHolder.addCreatePropertyTemplate(settable, candidateBeanClasses) + } + + return settable +} + +private fun createSetterDependency( + currentBean: SpringBean, + dependency: SpringBeanPointer, + injectionKind: SpringDependencyInjectionKind +): BatchTemplateRunner? { + val templatesHolder = BatchTemplateRunner(currentBean.manager.project) + val currentBeanClass = currentBean.beanClass as? KtLightClass ?: return null + val candidateBeanClasses = dependency.effectiveBeanType.ifEmpty { return null } + val setter = getOrCreateSetter(dependency, currentBeanClass, candidateBeanClasses, templatesHolder, injectionKind) ?: return null + currentBean.addProperty().apply { + name.ensureXmlElementExists() + name.stringValue = when (setter) { + is PsiMethod, is KtFunction -> propertyNameBySetMethodName(Name.identifier(setter.name!!), false)!!.asString() + else -> setter.name + } + refAttr.setStringValue(getReferencedName(currentBean, dependency)) + } + return templatesHolder +} + +private fun addConstructorParameter( + currentBeanClass: KtLightClass, + candidateBeanClass: PsiClass, + constructor: KtFunction +) { + val psiFactory = KtPsiFactory(currentBeanClass) + + val validator = NewDeclarationNameValidator(constructor, null, NewDeclarationNameValidator.Target.VARIABLES) + val resolutionFacade = currentBeanClass.kotlinOrigin!!.getResolutionFacade() + val defaultType = (candidateBeanClass.getJavaOrKotlinMemberDescriptor(resolutionFacade) as ClassDescriptor).defaultType + val name = KotlinNameSuggester.suggestNamesByType(defaultType, validator).first() + + val parameter = psiFactory.createParameter("$name: ${candidateBeanClass.defaultTypeText}") + constructor.getValueParameterList()!!.addParameter(parameter).apply { ShortenReferences.DEFAULT.process(this) } +} + +private fun findProperConstructorAndAddParameter( + currentBean: SpringBean, + dependency: SpringBeanPointer, + currentBeanClass: KtLightClass, + candidateBeanClass: PsiClass, + holder: BatchTemplateRunner +): PsiMethod? { + val lightConstructor = currentBean.resolvedConstructorArgs.resolvedMethod as? KtLightMethod ?: return null + val constructorOrigin = lightConstructor.kotlinOrigin + val properConstructor = when (constructorOrigin) { + is KtFunction -> constructorOrigin + is KtClass -> constructorOrigin.createPrimaryConstructorIfAbsent() + else -> return null + } + addConstructorParameter(currentBeanClass, candidateBeanClass, properConstructor) + holder.addCreateFunctionTemplate(properConstructor, properConstructor.getValueParameters().lastIndex, arrayOf(candidateBeanClass), dependency) + return lightConstructor +} + +private fun createConstructorWithTemplate( + currentBean: SpringBean, + dependency: SpringBeanPointer, + templatesHolder: BatchTemplateRunner +) { + val beanClass = currentBean.beanClass as? KtLightClass ?: return + val ktBeanClass = beanClass.kotlinOrigin as? KtClass ?: return + + try { + val psiFactory = KtPsiFactory(beanClass) + val factoryFunction = when { + currentBean.factoryBean.exists() -> { + // TODO: Support non-Kotlin factory beans + val factoryClass = currentBean.factoryBean.value?.beanClass as? KtLightClass ?: return + val ktFactoryClass = factoryClass.kotlinOrigin as? KtClass ?: return + val funName = currentBean.getFactoryFunctionName(ktFactoryClass) + val funText = "fun $funName(): ${beanClass.defaultTypeText} {\n return null \n}" + ktFactoryClass.addDeclaration(psiFactory.createFunction(funText)) + } + currentBean.factoryMethod.exists() -> { + val funName = currentBean.getFactoryFunctionName(ktBeanClass) + val funText = "@JvmStatic fun $funName(): ${beanClass.defaultTypeText} {\n return null \n}" + ktBeanClass.getOrCreateCompanionObject().addDeclaration(psiFactory.createFunction(funText)) + } + else -> { + if (ktBeanClass.hasPrimaryConstructor() || ktBeanClass.getSecondaryConstructors().isNotEmpty()) { + ktBeanClass.addDeclaration(psiFactory.createSecondaryConstructor("constructor()")) + } + else { + ktBeanClass.createPrimaryConstructorIfAbsent() + } + } + } as KtFunction + + ShortenReferences.DEFAULT.process(factoryFunction) + + val dummyMethod = JavaPsiFacade.getElementFactory(beanClass.project).createMethod("foo", PsiType.VOID) + dummyMethod.containingFile.moduleInfo = ktBeanClass.getModuleInfo() + SpringConstructorArgResolveUtil.suggestParamsForConstructorArgs(currentBean).forEach { dummyMethod.parameterList.add(it) } + + val parameterList = factoryFunction.valueParameterList!! + val indexOffset = parameterList.parameters.size + val candidateClassMap = SmartHashMap>() + val resolutionFacade = ktBeanClass.getResolutionFacade() + for ((i, psiParam) in dummyMethod.parameterList.parameters.withIndex()) { + val descriptor = psiParam.getParameterDescriptor(resolutionFacade)!! + val ktParameter = psiFactory.createParameter( + "${psiParam.name}: ${IdeDescriptorRenderers.SOURCE_CODE_NOT_NULL_TYPE_APPROXIMATION.renderType(descriptor.type)}" + ) + parameterList.addParameter(ktParameter) + (psiParam.type as? PsiClassType)?.resolve()?.let { candidateClassMap[i + indexOffset] = arrayOf(it) } + } + + templatesHolder.addCreateFunctionTemplate(factoryFunction, candidateClassMap, dependency) + } + catch (e: IncorrectOperationException) { + throw RuntimeException(e) + } +} + +private fun createConstructorDependency( + currentBean: SpringBean, + dependency: SpringBeanPointer +): BatchTemplateRunner? { + val templatesHolder = BatchTemplateRunner(currentBean.manager.project) + val currentBeanClass = currentBean.beanClass as? KtLightClass ?: return null + val candidateBeanClasses = dependency.effectiveBeanType.ifEmpty { return null } + var existedConstructor = findExistedConstructor(currentBean, currentBeanClass, candidateBeanClasses) + if (existedConstructor == null) { + if (!ensureFileWritable(currentBeanClass)) return null + existedConstructor = findProperConstructorAndAddParameter(currentBean, dependency, currentBeanClass, candidateBeanClasses.first(), templatesHolder) + } + + val newConstructorArg = currentBean.addConstructorArg() + newConstructorArg.refAttr.stringValue = getReferencedName(currentBean, dependency) + if (existedConstructor == null && SpringConstructorArgResolveUtil.findMatchingMethods(currentBean).isEmpty()) { + createConstructorWithTemplate(currentBean, dependency, templatesHolder) + } + + return templatesHolder +} + +private fun generateDependency( + springBean: SpringBean, + dependency: SpringBeanPointer, + injectionKind: SpringDependencyInjectionKind +): BatchTemplateRunner? { + return if (injectionKind.isSetter) { + createSetterDependency(springBean, dependency, injectionKind) + } + else { + createConstructorDependency(springBean, dependency) + } +} + +enum class SpringDependencyInjectionKind(val isSetter: Boolean) { + CONSTRUCTOR(false), + SETTER(true), + LATEINIT_PROPERTY(true) +} + +@set:TestOnly +var Project.beanFilter: (SpringBeanPointer) -> Boolean + by NotNullableUserDataProperty(Key.create("BEAN_CHOOSER")) { true } + +@NotNull +private fun chooseDependentBeans( + candidates: Set, + project: Project, + isSetterDependency: Boolean +): List> { + return if (ApplicationManager.getApplication().isUnitTestMode) { + candidates.map { it.springBean }.filter(project.beanFilter) + } + else { + GenerateSpringBeanDependenciesUtil.chooseDependentBeans(candidates, project, isSetterDependency) + } +} + +fun generateDependenciesFor( + springModel: CommonSpringModel, + klass: KtLightClass, + injectionKind: SpringDependencyInjectionKind +): List { + val project = klass.project + val isSetter = injectionKind.isSetter + + SpringModelSearchers + .findBeans(springModel, SpringModelSearchParameters.byClass(klass)) + .asSequence() + .map { it.springBean } + .filterIsInstance() + .firstOrNull { acceptBean(it, false) } + ?.let { springBean -> + if (!springBean.ensureFileWritable()) return emptyList() + if (springBean.beanClass == null) return emptyList() + val dependencies = chooseDependentBeans(getCandidates(springBean, isSetter), project, isSetter) + return dependencies.mapNotNull { generateDependency(springBean, it, injectionKind) } + } + + val dependencies = chooseDependentBeans(getCandidates(springModel, klass, isSetter), project, isSetter) + val beanXml = dependencies.singleOrNull()?.springBean as? DomSpringBean ?: return emptyList() + val beansXml = beanXml.getParentOfType(Beans::class.java, false) ?: return emptyList() + val newBean = createSpringBean(beansXml, klass) ?: return emptyList() + + return dependencies.mapNotNull { generateDependency(newBean, it, injectionKind) } +} diff --git a/idea/idea-ultimate/tests/org/jetbrains/kotlin/idea/spring/tests/generate/AbstractGenerateSpringDependencyActionTest.kt b/idea/idea-ultimate/tests/org/jetbrains/kotlin/idea/spring/tests/generate/AbstractGenerateSpringDependencyActionTest.kt new file mode 100644 index 00000000000..acd686afd3e --- /dev/null +++ b/idea/idea-ultimate/tests/org/jetbrains/kotlin/idea/spring/tests/generate/AbstractGenerateSpringDependencyActionTest.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2010-2016 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. + */ + +package org.jetbrains.kotlin.idea.spring.tests.generate + +import com.intellij.openapi.util.io.FileUtil +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.util.PathUtil +import junit.framework.TestCase +import org.jetbrains.kotlin.idea.codeInsight.generate.AbstractCodeInsightActionTest +import org.jetbrains.kotlin.idea.spring.generate.beanFilter +import org.jetbrains.kotlin.idea.spring.tests.SpringTestFixtureExtension +import org.jetbrains.kotlin.idea.test.TestFixtureExtension +import org.jetbrains.kotlin.test.InTextDirectivesUtils +import java.io.File + +abstract class AbstractGenerateSpringDependencyActionTest : AbstractCodeInsightActionTest() { + private var configFile: VirtualFile? = null + private var configFilePath: String? = null + + override fun setUp() { + super.setUp() + TestFixtureExtension.loadFixture(myModule) + } + + override fun configureExtra(mainFilePath: String, mainFileText: String) { + configFilePath = InTextDirectivesUtils.findStringWithPrefixes(mainFileText, "// CONFIG_FILE: ")?.let { + "${PathUtil.getParentPath(mainFilePath)}/$it" + } ?: mainFilePath + val springFileSet = TestFixtureExtension + .getFixture()!! + .configureFileSet(myFixture, listOf(configFilePath!!)) + if (configFilePath != mainFilePath) { + configFile = springFileSet.files.single().file!! + } + + val beansToChoose = InTextDirectivesUtils.findListWithPrefixes(mainFileText, "// CHOOSE_BEAN: ") + project.beanFilter = { it.name in beansToChoose } + } + + override fun checkExtra() { + configFile?.let { + myFixture.openFileInEditor(it) + val afterFile = File("$configFilePath.after") + TestCase.assertTrue(afterFile.exists()) + myFixture.checkResult(FileUtil.loadFile(afterFile, true)) + } + } + + override fun tearDown() { + configFile = null + TestFixtureExtension.unloadFixture() + super.tearDown() + } +} \ No newline at end of file diff --git a/idea/idea-ultimate/tests/org/jetbrains/kotlin/idea/spring/tests/generate/GenerateSpringDependencyActionTestGenerated.java b/idea/idea-ultimate/tests/org/jetbrains/kotlin/idea/spring/tests/generate/GenerateSpringDependencyActionTestGenerated.java new file mode 100644 index 00000000000..6772298d562 --- /dev/null +++ b/idea/idea-ultimate/tests/org/jetbrains/kotlin/idea/spring/tests/generate/GenerateSpringDependencyActionTestGenerated.java @@ -0,0 +1,67 @@ +/* + * Copyright 2010-2016 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. + */ + +package org.jetbrains.kotlin.idea.spring.tests.generate; + +import com.intellij.testFramework.TestDataPath; +import org.jetbrains.kotlin.test.JUnit3RunnerWithInners; +import org.jetbrains.kotlin.test.KotlinTestUtils; +import org.jetbrains.kotlin.test.TestMetadata; +import org.junit.runner.RunWith; + +import java.io.File; +import java.util.regex.Pattern; + +/** This class is generated by {@link org.jetbrains.kotlin.generators.tests.TestsPackage}. DO NOT MODIFY MANUALLY */ +@SuppressWarnings("all") +@TestMetadata("idea/testData/spring/core/generate") +@TestDataPath("$PROJECT_ROOT") +@RunWith(JUnit3RunnerWithInners.class) +public class GenerateSpringDependencyActionTestGenerated extends AbstractGenerateSpringDependencyActionTest { + public void testAllFilesPresentInGenerate() throws Exception { + KotlinTestUtils.assertAllTestsPresentInSingleGeneratedClass(this.getClass(), new File("idea/testData/spring/core/generate"), Pattern.compile("^([\\w]+)\\.kt$")); + } + + @TestMetadata("beanDependenciesByXml/firstConstructor.kt") + public void testBeanDependenciesByXml_FirstConstructor() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/spring/core/generate/beanDependenciesByXml/firstConstructor.kt"); + doTest(fileName); + } + + @TestMetadata("beanDependenciesByXml/primaryConstructorAddParam.kt") + public void testBeanDependenciesByXml_PrimaryConstructorAddParam() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/spring/core/generate/beanDependenciesByXml/primaryConstructorAddParam.kt"); + doTest(fileName); + } + + @TestMetadata("beanDependenciesByXml/property.kt") + public void testBeanDependenciesByXml_Property() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/spring/core/generate/beanDependenciesByXml/property.kt"); + doTest(fileName); + } + + @TestMetadata("beanDependenciesByXml/secondaryConstructorAddParam.kt") + public void testBeanDependenciesByXml_SecondaryConstructorAddParam() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/spring/core/generate/beanDependenciesByXml/secondaryConstructorAddParam.kt"); + doTest(fileName); + } + + @TestMetadata("beanDependenciesByXml/setter.kt") + public void testBeanDependenciesByXml_Setter() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/spring/core/generate/beanDependenciesByXml/setter.kt"); + doTest(fileName); + } +} diff --git a/idea/src/org/jetbrains/kotlin/idea/editor/BatchTemplateRunner.kt b/idea/src/org/jetbrains/kotlin/idea/editor/BatchTemplateRunner.kt new file mode 100644 index 00000000000..a4d91da65c1 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/editor/BatchTemplateRunner.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2010-2016 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. + */ + +package org.jetbrains.kotlin.idea.editor + +import com.intellij.codeInsight.template.Template +import com.intellij.codeInsight.template.TemplateEditingAdapter +import com.intellij.codeInsight.template.TemplateManager +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.fileEditor.OpenFileDescriptor +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiElement +import com.intellij.psi.SmartPsiElementPointer +import org.jetbrains.kotlin.idea.util.application.executeWriteCommand +import org.jetbrains.kotlin.psi.psiUtil.createSmartPointer +import java.util.* + +class BatchTemplateRunner(private val project: Project) { + private val elementsAndFactories = ArrayList, () -> Template?>>() + + fun addTemplateFactory(element: PsiElement, factory: () -> Template?) { + elementsAndFactories.add(element.createSmartPointer() to factory) + } + + fun runTemplates() { + runTemplates(elementsAndFactories.iterator()) + } + + private fun getEditor(pointer: SmartPsiElementPointer<*>): Editor? { + val element = pointer.element ?: return null + val virtualFile = element.containingFile?.virtualFile ?: return null + val descriptor = OpenFileDescriptor(project, virtualFile, element.textRange.startOffset) + return FileEditorManager.getInstance(project).openTextEditor(descriptor, true) + } + + private fun runTemplates(iterator: Iterator, () -> Template?>>) { + if (!iterator.hasNext()) return + + val manager = TemplateManager.getInstance(project) + project.executeWriteCommand("") { + val (pointer, factory) = iterator.next() + + val editor = getEditor(pointer) ?: return@executeWriteCommand + PsiDocumentManager.getInstance(project).doPostponedOperationsAndUnblockDocument(editor.document) + val template = factory() ?: return@executeWriteCommand + manager.startTemplate( + editor, + template, + object : TemplateEditingAdapter() { + override fun templateFinished(template: Template?, brokenOff: Boolean) { + if (brokenOff) return + ApplicationManager.getApplication().invokeLater { runTemplates(iterator) } + } + } + ) + } + } +} \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/formatter/kotlinSpacingRules.kt b/idea/src/org/jetbrains/kotlin/idea/formatter/kotlinSpacingRules.kt index 44e66922325..57a035bc6cc 100644 --- a/idea/src/org/jetbrains/kotlin/idea/formatter/kotlinSpacingRules.kt +++ b/idea/src/org/jetbrains/kotlin/idea/formatter/kotlinSpacingRules.kt @@ -118,6 +118,7 @@ fun createSpacingBuilder(settings: CodeStyleSettings): KotlinSpacingBuilder { between(ENUM_ENTRY, SEMICOLON).spaces(0) beforeInside(FUN, TokenSet.create(BODY, CLASS_BODY)).lineBreakInCode() + beforeInside(SECONDARY_CONSTRUCTOR, TokenSet.create(BODY, CLASS_BODY)).lineBreakInCode() beforeInside(CLASS, TokenSet.create(BODY, CLASS_BODY)).lineBreakInCode() beforeInside(OBJECT_DECLARATION, TokenSet.create(BODY, CLASS_BODY)).lineBreakInCode() before(PROPERTY).lineBreakInCode() diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/MoveKotlinNestedClassesToUpperLevelDialog.java b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/MoveKotlinNestedClassesToUpperLevelDialog.java index e79f7ccb28c..9eae5fa6507 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/MoveKotlinNestedClassesToUpperLevelDialog.java +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/MoveKotlinNestedClassesToUpperLevelDialog.java @@ -64,6 +64,7 @@ import javax.swing.*; import java.awt.*; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; +import java.util.Collections; import java.util.List; public class MoveKotlinNestedClassesToUpperLevelDialog extends MoveDialogBase { @@ -202,7 +203,8 @@ public class MoveKotlinNestedClassesToUpperLevelDialog extends MoveDialogBase { Function1 validator = innerClassBody != null ? new NewDeclarationNameValidator(innerClassBody, (PsiElement) null, - NewDeclarationNameValidator.Target.VARIABLES) + NewDeclarationNameValidator.Target.VARIABLES, + Collections.emptyList()) : new CollectingNameValidator(); List suggestions = KotlinNameSuggester.INSTANCE.suggestNamesByType(getOuterInstanceType(), validator, "outer"); parameterField.setSuggestions(ArrayUtil.toStringArray(suggestions)); diff --git a/idea/testData/spring/core/generate/beanDependenciesByXml/factoryBean-config.xml b/idea/testData/spring/core/generate/beanDependenciesByXml/factoryBean-config.xml new file mode 100644 index 00000000000..41d844ac510 --- /dev/null +++ b/idea/testData/spring/core/generate/beanDependenciesByXml/factoryBean-config.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/idea/testData/spring/core/generate/beanDependenciesByXml/factoryBean-config.xml.after b/idea/testData/spring/core/generate/beanDependenciesByXml/factoryBean-config.xml.after new file mode 100644 index 00000000000..a9588081399 --- /dev/null +++ b/idea/testData/spring/core/generate/beanDependenciesByXml/factoryBean-config.xml.after @@ -0,0 +1,11 @@ + + + + + + + + \ No newline at end of file diff --git a/idea/testData/spring/core/generate/beanDependenciesByXml/factoryBean.kt.after b/idea/testData/spring/core/generate/beanDependenciesByXml/factoryBean.kt.after new file mode 100644 index 00000000000..e9ad046b134 --- /dev/null +++ b/idea/testData/spring/core/generate/beanDependenciesByXml/factoryBean.kt.after @@ -0,0 +1,13 @@ +// ACTION_CLASS: org.jetbrains.kotlin.idea.spring.generate.GenerateKotlinSpringBeanDependencyAction$Constructor +// CONFIG_FILE: factoryMethod-config.xml +// CHOOSE_BEAN: barBean +package a + +open class FooBean { +} + +open class FooFactoryBean { + open fun create(barBean: BarBean) = FooBean() +} + +open class BarBean \ No newline at end of file diff --git a/idea/testData/spring/core/generate/beanDependenciesByXml/factoryBean.kt_disabled b/idea/testData/spring/core/generate/beanDependenciesByXml/factoryBean.kt_disabled new file mode 100644 index 00000000000..dcf374bc3fd --- /dev/null +++ b/idea/testData/spring/core/generate/beanDependenciesByXml/factoryBean.kt_disabled @@ -0,0 +1,13 @@ +// ACTION_CLASS: org.jetbrains.kotlin.idea.spring.generate.GenerateKotlinSpringBeanDependencyAction$Constructor +// CONFIG_FILE: factoryMethod-config.xml +// CHOOSE_BEAN: barBean +package a + +open class FooBean { +} + +open class FooFactoryBean { + open fun create() = FooBean() +} + +open class BarBean \ No newline at end of file diff --git a/idea/testData/spring/core/generate/beanDependenciesByXml/factoryMethod-config.xml b/idea/testData/spring/core/generate/beanDependenciesByXml/factoryMethod-config.xml new file mode 100644 index 00000000000..653e6d0790f --- /dev/null +++ b/idea/testData/spring/core/generate/beanDependenciesByXml/factoryMethod-config.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/idea/testData/spring/core/generate/beanDependenciesByXml/factoryMethod-config.xml.after b/idea/testData/spring/core/generate/beanDependenciesByXml/factoryMethod-config.xml.after new file mode 100644 index 00000000000..f78da8da8dc --- /dev/null +++ b/idea/testData/spring/core/generate/beanDependenciesByXml/factoryMethod-config.xml.after @@ -0,0 +1,10 @@ + + + + + + + \ No newline at end of file diff --git a/idea/testData/spring/core/generate/beanDependenciesByXml/factoryMethod.kt.after b/idea/testData/spring/core/generate/beanDependenciesByXml/factoryMethod.kt.after new file mode 100644 index 00000000000..51a40fdd467 --- /dev/null +++ b/idea/testData/spring/core/generate/beanDependenciesByXml/factoryMethod.kt.after @@ -0,0 +1,14 @@ +// ACTION_CLASS: org.jetbrains.kotlin.idea.spring.generate.GenerateKotlinSpringBeanDependencyAction$Constructor +// CONFIG_FILE: factoryMethod-config.xml +// CHOOSE_BEAN: barBean +package a + +open class FooBean { + companion object { + @JvmStatic fun create(barBean: BarBean): FooBean { + return null + } + } +} + +open class BarBean \ No newline at end of file diff --git a/idea/testData/spring/core/generate/beanDependenciesByXml/factoryMethod.kt_disabled b/idea/testData/spring/core/generate/beanDependenciesByXml/factoryMethod.kt_disabled new file mode 100644 index 00000000000..16fa45bf3dc --- /dev/null +++ b/idea/testData/spring/core/generate/beanDependenciesByXml/factoryMethod.kt_disabled @@ -0,0 +1,9 @@ +// ACTION_CLASS: org.jetbrains.kotlin.idea.spring.generate.GenerateKotlinSpringBeanDependencyAction$Constructor +// CONFIG_FILE: factoryMethod-config.xml +// CHOOSE_BEAN: barBean +package a + +open class FooBean { +} + +open class BarBean \ No newline at end of file diff --git a/idea/testData/spring/core/generate/beanDependenciesByXml/firstConstructor-config.xml b/idea/testData/spring/core/generate/beanDependenciesByXml/firstConstructor-config.xml new file mode 100644 index 00000000000..e5ee5310463 --- /dev/null +++ b/idea/testData/spring/core/generate/beanDependenciesByXml/firstConstructor-config.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/idea/testData/spring/core/generate/beanDependenciesByXml/firstConstructor-config.xml.after b/idea/testData/spring/core/generate/beanDependenciesByXml/firstConstructor-config.xml.after new file mode 100644 index 00000000000..81dc2809199 --- /dev/null +++ b/idea/testData/spring/core/generate/beanDependenciesByXml/firstConstructor-config.xml.after @@ -0,0 +1,10 @@ + + + + + + + \ No newline at end of file diff --git a/idea/testData/spring/core/generate/beanDependenciesByXml/firstConstructor.kt b/idea/testData/spring/core/generate/beanDependenciesByXml/firstConstructor.kt new file mode 100644 index 00000000000..b213095167f --- /dev/null +++ b/idea/testData/spring/core/generate/beanDependenciesByXml/firstConstructor.kt @@ -0,0 +1,9 @@ +// ACTION_CLASS: org.jetbrains.kotlin.idea.spring.generate.GenerateKotlinSpringBeanDependencyAction$Constructor +// CONFIG_FILE: firstConstructor-config.xml +// CHOOSE_BEAN: barBean +package a + +open class FooBean { +} + +open class BarBean \ No newline at end of file diff --git a/idea/testData/spring/core/generate/beanDependenciesByXml/firstConstructor.kt.after b/idea/testData/spring/core/generate/beanDependenciesByXml/firstConstructor.kt.after new file mode 100644 index 00000000000..1163a05776c --- /dev/null +++ b/idea/testData/spring/core/generate/beanDependenciesByXml/firstConstructor.kt.after @@ -0,0 +1,9 @@ +// ACTION_CLASS: org.jetbrains.kotlin.idea.spring.generate.GenerateKotlinSpringBeanDependencyAction$Constructor +// CONFIG_FILE: firstConstructor-config.xml +// CHOOSE_BEAN: barBean +package a + +open class FooBean(barBean: BarBean) { +} + +open class BarBean \ No newline at end of file diff --git a/idea/testData/spring/core/generate/beanDependenciesByXml/primaryConstructorAddParam-config.xml b/idea/testData/spring/core/generate/beanDependenciesByXml/primaryConstructorAddParam-config.xml new file mode 100644 index 00000000000..0724386c606 --- /dev/null +++ b/idea/testData/spring/core/generate/beanDependenciesByXml/primaryConstructorAddParam-config.xml @@ -0,0 +1,11 @@ + + + + + + + + \ No newline at end of file diff --git a/idea/testData/spring/core/generate/beanDependenciesByXml/primaryConstructorAddParam-config.xml.after b/idea/testData/spring/core/generate/beanDependenciesByXml/primaryConstructorAddParam-config.xml.after new file mode 100644 index 00000000000..113837a2c84 --- /dev/null +++ b/idea/testData/spring/core/generate/beanDependenciesByXml/primaryConstructorAddParam-config.xml.after @@ -0,0 +1,12 @@ + + + + + + + + + \ No newline at end of file diff --git a/idea/testData/spring/core/generate/beanDependenciesByXml/primaryConstructorAddParam.kt b/idea/testData/spring/core/generate/beanDependenciesByXml/primaryConstructorAddParam.kt new file mode 100644 index 00000000000..95fa3405af8 --- /dev/null +++ b/idea/testData/spring/core/generate/beanDependenciesByXml/primaryConstructorAddParam.kt @@ -0,0 +1,11 @@ +// ACTION_CLASS: org.jetbrains.kotlin.idea.spring.generate.GenerateKotlinSpringBeanDependencyAction$Constructor +// CONFIG_FILE: primaryConstructorAddParam-config.xml +// CHOOSE_BEAN: bazBean +package a + +open class FooBean(barBean: BarBean) { +} + +open class BarBean + +open class BazBean \ No newline at end of file diff --git a/idea/testData/spring/core/generate/beanDependenciesByXml/primaryConstructorAddParam.kt.after b/idea/testData/spring/core/generate/beanDependenciesByXml/primaryConstructorAddParam.kt.after new file mode 100644 index 00000000000..d88722a7fab --- /dev/null +++ b/idea/testData/spring/core/generate/beanDependenciesByXml/primaryConstructorAddParam.kt.after @@ -0,0 +1,11 @@ +// ACTION_CLASS: org.jetbrains.kotlin.idea.spring.generate.GenerateKotlinSpringBeanDependencyAction$Constructor +// CONFIG_FILE: primaryConstructorAddParam-config.xml +// CHOOSE_BEAN: bazBean +package a + +open class FooBean(barBean: BarBean, bazBean: BazBean) { +} + +open class BarBean + +open class BazBean \ No newline at end of file diff --git a/idea/testData/spring/core/generate/beanDependenciesByXml/property-config.xml b/idea/testData/spring/core/generate/beanDependenciesByXml/property-config.xml new file mode 100644 index 00000000000..e5ee5310463 --- /dev/null +++ b/idea/testData/spring/core/generate/beanDependenciesByXml/property-config.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/idea/testData/spring/core/generate/beanDependenciesByXml/property-config.xml.after b/idea/testData/spring/core/generate/beanDependenciesByXml/property-config.xml.after new file mode 100644 index 00000000000..947f453b6ab --- /dev/null +++ b/idea/testData/spring/core/generate/beanDependenciesByXml/property-config.xml.after @@ -0,0 +1,10 @@ + + + + + + + \ No newline at end of file diff --git a/idea/testData/spring/core/generate/beanDependenciesByXml/property.kt b/idea/testData/spring/core/generate/beanDependenciesByXml/property.kt new file mode 100644 index 00000000000..d09a4e26ac0 --- /dev/null +++ b/idea/testData/spring/core/generate/beanDependenciesByXml/property.kt @@ -0,0 +1,9 @@ +// ACTION_CLASS: org.jetbrains.kotlin.idea.spring.generate.GenerateKotlinSpringBeanDependencyAction$LateinitProperty +// CONFIG_FILE: property-config.xml +// CHOOSE_BEAN: barBean +package a + +open class FooBean { +} + +open class BarBean \ No newline at end of file diff --git a/idea/testData/spring/core/generate/beanDependenciesByXml/property.kt.after b/idea/testData/spring/core/generate/beanDependenciesByXml/property.kt.after new file mode 100644 index 00000000000..948f319f03c --- /dev/null +++ b/idea/testData/spring/core/generate/beanDependenciesByXml/property.kt.after @@ -0,0 +1,10 @@ +// ACTION_CLASS: org.jetbrains.kotlin.idea.spring.generate.GenerateKotlinSpringBeanDependencyAction$LateinitProperty +// CONFIG_FILE: property-config.xml +// CHOOSE_BEAN: barBean +package a + +open class FooBean { + lateinit var barBean: BarBean +} + +open class BarBean \ No newline at end of file diff --git a/idea/testData/spring/core/generate/beanDependenciesByXml/secondaryConstructor-config.xml b/idea/testData/spring/core/generate/beanDependenciesByXml/secondaryConstructor-config.xml new file mode 100644 index 00000000000..e5ee5310463 --- /dev/null +++ b/idea/testData/spring/core/generate/beanDependenciesByXml/secondaryConstructor-config.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/idea/testData/spring/core/generate/beanDependenciesByXml/secondaryConstructor-config.xml.after b/idea/testData/spring/core/generate/beanDependenciesByXml/secondaryConstructor-config.xml.after new file mode 100644 index 00000000000..e5ee5310463 --- /dev/null +++ b/idea/testData/spring/core/generate/beanDependenciesByXml/secondaryConstructor-config.xml.after @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/idea/testData/spring/core/generate/beanDependenciesByXml/secondaryConstructor.kt.after b/idea/testData/spring/core/generate/beanDependenciesByXml/secondaryConstructor.kt.after new file mode 100644 index 00000000000..5be5ad1ab11 --- /dev/null +++ b/idea/testData/spring/core/generate/beanDependenciesByXml/secondaryConstructor.kt.after @@ -0,0 +1,10 @@ +// ACTION_CLASS: org.jetbrains.kotlin.idea.spring.generate.GenerateKotlinSpringBeanDependencyAction$Constructor +// CONFIG_FILE: secondaryConstructor-config.xml +// CHOOSE_BEAN: barBean +package a + +open class FooBean(n: Int) { + +} + +open class BarBean \ No newline at end of file diff --git a/idea/testData/spring/core/generate/beanDependenciesByXml/secondaryConstructor.kt_disabled b/idea/testData/spring/core/generate/beanDependenciesByXml/secondaryConstructor.kt_disabled new file mode 100644 index 00000000000..f996220b413 --- /dev/null +++ b/idea/testData/spring/core/generate/beanDependenciesByXml/secondaryConstructor.kt_disabled @@ -0,0 +1,9 @@ +// ACTION_CLASS: org.jetbrains.kotlin.idea.spring.generate.GenerateKotlinSpringBeanDependencyAction$Constructor +// CONFIG_FILE: secondaryConstructor-config.xml +// CHOOSE_BEAN: barBean +package a + +open class FooBean(n: Int) { +} + +open class BarBean \ No newline at end of file diff --git a/idea/testData/spring/core/generate/beanDependenciesByXml/secondaryConstructorAddParam-config.xml b/idea/testData/spring/core/generate/beanDependenciesByXml/secondaryConstructorAddParam-config.xml new file mode 100644 index 00000000000..0724386c606 --- /dev/null +++ b/idea/testData/spring/core/generate/beanDependenciesByXml/secondaryConstructorAddParam-config.xml @@ -0,0 +1,11 @@ + + + + + + + + \ No newline at end of file diff --git a/idea/testData/spring/core/generate/beanDependenciesByXml/secondaryConstructorAddParam-config.xml.after b/idea/testData/spring/core/generate/beanDependenciesByXml/secondaryConstructorAddParam-config.xml.after new file mode 100644 index 00000000000..113837a2c84 --- /dev/null +++ b/idea/testData/spring/core/generate/beanDependenciesByXml/secondaryConstructorAddParam-config.xml.after @@ -0,0 +1,12 @@ + + + + + + + + + \ No newline at end of file diff --git a/idea/testData/spring/core/generate/beanDependenciesByXml/secondaryConstructorAddParam.kt b/idea/testData/spring/core/generate/beanDependenciesByXml/secondaryConstructorAddParam.kt new file mode 100644 index 00000000000..577d8b9ed69 --- /dev/null +++ b/idea/testData/spring/core/generate/beanDependenciesByXml/secondaryConstructorAddParam.kt @@ -0,0 +1,12 @@ +// ACTION_CLASS: org.jetbrains.kotlin.idea.spring.generate.GenerateKotlinSpringBeanDependencyAction$Constructor +// CONFIG_FILE: secondaryConstructorAddParam-config.xml +// CHOOSE_BEAN: bazBean +package a + +open class FooBean(n: Int) { + constructor(barBean: BarBean): this(1) +} + +open class BarBean + +open class BazBean \ No newline at end of file diff --git a/idea/testData/spring/core/generate/beanDependenciesByXml/secondaryConstructorAddParam.kt.after b/idea/testData/spring/core/generate/beanDependenciesByXml/secondaryConstructorAddParam.kt.after new file mode 100644 index 00000000000..dd41823b604 --- /dev/null +++ b/idea/testData/spring/core/generate/beanDependenciesByXml/secondaryConstructorAddParam.kt.after @@ -0,0 +1,12 @@ +// ACTION_CLASS: org.jetbrains.kotlin.idea.spring.generate.GenerateKotlinSpringBeanDependencyAction$Constructor +// CONFIG_FILE: secondaryConstructorAddParam-config.xml +// CHOOSE_BEAN: bazBean +package a + +open class FooBean(n: Int) { + constructor(barBean: BarBean, bazBean: BazBean): this(1) +} + +open class BarBean + +open class BazBean \ No newline at end of file diff --git a/idea/testData/spring/core/generate/beanDependenciesByXml/setter-config.xml b/idea/testData/spring/core/generate/beanDependenciesByXml/setter-config.xml new file mode 100644 index 00000000000..e5ee5310463 --- /dev/null +++ b/idea/testData/spring/core/generate/beanDependenciesByXml/setter-config.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/idea/testData/spring/core/generate/beanDependenciesByXml/setter-config.xml.after b/idea/testData/spring/core/generate/beanDependenciesByXml/setter-config.xml.after new file mode 100644 index 00000000000..947f453b6ab --- /dev/null +++ b/idea/testData/spring/core/generate/beanDependenciesByXml/setter-config.xml.after @@ -0,0 +1,10 @@ + + + + + + + \ No newline at end of file diff --git a/idea/testData/spring/core/generate/beanDependenciesByXml/setter.kt b/idea/testData/spring/core/generate/beanDependenciesByXml/setter.kt new file mode 100644 index 00000000000..08f5fa2dbd7 --- /dev/null +++ b/idea/testData/spring/core/generate/beanDependenciesByXml/setter.kt @@ -0,0 +1,9 @@ +// ACTION_CLASS: org.jetbrains.kotlin.idea.spring.generate.GenerateKotlinSpringBeanDependencyAction$Setter +// CONFIG_FILE: setter-config.xml +// CHOOSE_BEAN: barBean +package a + +open class FooBean { +} + +open class BarBean \ No newline at end of file diff --git a/idea/testData/spring/core/generate/beanDependenciesByXml/setter.kt.after b/idea/testData/spring/core/generate/beanDependenciesByXml/setter.kt.after new file mode 100644 index 00000000000..1aec3e2e81e --- /dev/null +++ b/idea/testData/spring/core/generate/beanDependenciesByXml/setter.kt.after @@ -0,0 +1,11 @@ +// ACTION_CLASS: org.jetbrains.kotlin.idea.spring.generate.GenerateKotlinSpringBeanDependencyAction$Setter +// CONFIG_FILE: setter-config.xml +// CHOOSE_BEAN: barBean +package a + +open class FooBean { + fun setBarBean(barBean: BarBean) { + } +} + +open class BarBean \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/idea/codeInsight/generate/AbstractCodeInsightActionTest.kt b/idea/tests/org/jetbrains/kotlin/idea/codeInsight/generate/AbstractCodeInsightActionTest.kt index 1c7461bb881..70e20d658c5 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/codeInsight/generate/AbstractCodeInsightActionTest.kt +++ b/idea/tests/org/jetbrains/kotlin/idea/codeInsight/generate/AbstractCodeInsightActionTest.kt @@ -37,6 +37,14 @@ abstract class AbstractCodeInsightActionTest : KotlinLightCodeInsightFixtureTest return Class.forName(actionClassName).newInstance() as CodeInsightAction } + protected open fun configureExtra(mainFilePath: String, mainFileText: String) { + + } + + protected open fun checkExtra() { + + } + protected open fun testAction(action: AnAction, forced: Boolean): Presentation { val e = TestActionEvent(action) action.beforeActionPerformedUpdate(e) @@ -65,6 +73,7 @@ abstract class AbstractCodeInsightActionTest : KotlinLightCodeInsightFixtureTest .forEach { myFixture.configureByFile(File(rootDir, it).path.replace(File.separator, "/")) } + configureExtra(path, fileText) myFixture.configureByFile(path) val action = createAction(fileText) @@ -83,6 +92,8 @@ abstract class AbstractCodeInsightActionTest : KotlinLightCodeInsightFixtureTest val afterFile = File("$path.after") TestCase.assertTrue(afterFile.exists()) myFixture.checkResult(FileUtil.loadFile(afterFile, true)) + + checkExtra() } } catch (e: CommonRefactoringUtil.RefactoringErrorHintException) {