From c6e48d8e593b5a32d382242a10bfb3e622f7f3b6 Mon Sep 17 00:00:00 2001 From: Nikolay Krasko Date: Tue, 19 Apr 2016 20:01:46 +0300 Subject: [PATCH] First version of Kotlin patterns in injection #KT-8088 In Progress --- idea/resources/injection/kotlinInjections.xml | 9 ++ idea/src/META-INF/injection.xml | 1 + .../KotlinLanguageInjectionSupport.kt | 6 +- .../idea/injection/KotlinLanguageInjector.kt | 58 ++++++--- .../kotlin/idea/patterns/KotlinPatterns.kt | 121 ++++++++++++++++++ .../kotlin/psi/KotlinInjectionTest.kt | 18 ++- 6 files changed, 192 insertions(+), 21 deletions(-) create mode 100644 idea/resources/injection/kotlinInjections.xml create mode 100644 idea/src/org/jetbrains/kotlin/idea/patterns/KotlinPatterns.kt diff --git a/idea/resources/injection/kotlinInjections.xml b/idea/resources/injection/kotlinInjections.xml new file mode 100644 index 00000000000..60a1f265af6 --- /dev/null +++ b/idea/resources/injection/kotlinInjections.xml @@ -0,0 +1,9 @@ + + + + Kotlin + + + diff --git a/idea/src/META-INF/injection.xml b/idea/src/META-INF/injection.xml index a313ac1337b..71ca3dc2b0f 100644 --- a/idea/src/META-INF/injection.xml +++ b/idea/src/META-INF/injection.xml @@ -5,5 +5,6 @@ + diff --git a/idea/src/org/jetbrains/kotlin/idea/injection/KotlinLanguageInjectionSupport.kt b/idea/src/org/jetbrains/kotlin/idea/injection/KotlinLanguageInjectionSupport.kt index b06e9418409..3342a72cf3d 100644 --- a/idea/src/org/jetbrains/kotlin/idea/injection/KotlinLanguageInjectionSupport.kt +++ b/idea/src/org/jetbrains/kotlin/idea/injection/KotlinLanguageInjectionSupport.kt @@ -24,7 +24,6 @@ import com.intellij.psi.* import com.intellij.psi.search.GlobalSearchScope import com.intellij.psi.util.PsiTreeUtil import com.intellij.psi.util.PsiTreeUtil.getDeepestLast -import com.intellij.util.ArrayUtil import com.intellij.util.Processor import org.intellij.plugins.intelliLang.Configuration import org.intellij.plugins.intelliLang.inject.AbstractLanguageInjectionSupport @@ -32,6 +31,7 @@ import org.intellij.plugins.intelliLang.inject.InjectLanguageAction import org.intellij.plugins.intelliLang.inject.InjectedLanguage import org.intellij.plugins.intelliLang.inject.TemporaryPlacesRegistry import org.jetbrains.annotations.NonNls +import org.jetbrains.kotlin.idea.patterns.KotlinPatterns import org.jetbrains.kotlin.idea.util.addAnnotation import org.jetbrains.kotlin.idea.util.application.executeWriteCommand import org.jetbrains.kotlin.idea.util.findAnnotation @@ -48,10 +48,12 @@ val LINE_LANGUAGE_REGEXP_COMMENT = Regex("//\\s*language=[\\w-]+") class KotlinLanguageInjectionSupport : AbstractLanguageInjectionSupport() { override fun getId(): String = KOTLIN_SUPPORT_ID - override fun getPatternClasses() = ArrayUtil.EMPTY_CLASS_ARRAY + override fun getPatternClasses() = arrayOf(KotlinPatterns::class.java) override fun isApplicableTo(host: PsiLanguageInjectionHost?) = host is KtElement + override fun useDefaultInjector(host: PsiLanguageInjectionHost?): Boolean = false + override fun addInjectionInPlace(language: Language?, host: PsiLanguageInjectionHost?): Boolean { if (language == null || host == null) return false diff --git a/idea/src/org/jetbrains/kotlin/idea/injection/KotlinLanguageInjector.kt b/idea/src/org/jetbrains/kotlin/idea/injection/KotlinLanguageInjector.kt index 84b056122b9..fc09b491195 100644 --- a/idea/src/org/jetbrains/kotlin/idea/injection/KotlinLanguageInjector.kt +++ b/idea/src/org/jetbrains/kotlin/idea/injection/KotlinLanguageInjector.kt @@ -26,10 +26,7 @@ import com.intellij.psi.util.PsiTreeUtil import org.intellij.plugins.intelliLang.Configuration import org.intellij.plugins.intelliLang.inject.InjectorUtils import org.intellij.plugins.intelliLang.inject.java.JavaLanguageInjectionSupport -import org.jetbrains.kotlin.psi.KtCallExpression -import org.jetbrains.kotlin.psi.KtElement -import org.jetbrains.kotlin.psi.KtValueArgument -import org.jetbrains.kotlin.psi.KtValueArgumentList +import org.jetbrains.kotlin.psi.* import java.util.* class KotlinLanguageInjector : LanguageInjector { @@ -66,19 +63,50 @@ class KotlinLanguageInjector : LanguageInjector { for (reference in callee.references) { ProgressManager.checkCanceled() - val javaMethod = reference.resolve() - if (javaMethod !is PsiMethod) continue - - val argumentIndex = (argument.parent as KtValueArgumentList).arguments.indexOf(argument) - val psiParameter = javaMethod.parameterList.parameters.getOrNull(argumentIndex) ?: continue - - val injections = Configuration.getInstance().getInjections(JavaLanguageInjectionSupport.JAVA_SUPPORT_ID) - - for (injection in injections) { - if (injection.acceptsPsiElement(psiParameter)) { - return InjectionInfo(injection.injectedLanguageId, injection.prefix, injection.suffix) + val resolvedTo = reference.resolve() + if (resolvedTo is PsiMethod) { + val injectionForJavaMethod = injectionForJavaMethod(argument, resolvedTo) + if (injectionForJavaMethod != null) { + return injectionForJavaMethod } } + else if (resolvedTo is KtFunction) { + val injectionForJavaMethod = injectionForKotlinCall(argument, resolvedTo) + if (injectionForJavaMethod != null) { + return injectionForJavaMethod + } + } + } + + return null + } + + private fun injectionForJavaMethod(argument: KtValueArgument, javaMethod: PsiMethod): InjectionInfo? { + val argumentIndex = (argument.parent as KtValueArgumentList).arguments.indexOf(argument) + val psiParameter = javaMethod.parameterList.parameters.getOrNull(argumentIndex) ?: return null + + val injections = Configuration.getInstance().getInjections(JavaLanguageInjectionSupport.JAVA_SUPPORT_ID) + + for (injection in injections) { + if (injection.acceptsPsiElement(psiParameter)) { + // ?? if (!processXmlInjections(injection, owner, method, paramIndex)) { + return InjectionInfo(injection.injectedLanguageId, injection.prefix, injection.suffix) + } + } + + return null + } + + private fun injectionForKotlinCall(argument: KtValueArgument, ktFunction: KtFunction): InjectionInfo? { + val argumentIndex = (argument.parent as KtValueArgumentList).arguments.indexOf(argument) + val ktParameter = ktFunction.valueParameters.getOrNull(argumentIndex) ?: return null + + val injections = Configuration.getInstance().getInjections(KOTLIN_SUPPORT_ID) + + for (injection in injections) { + if (injection.acceptsPsiElement(ktParameter)) { + return InjectionInfo(injection.injectedLanguageId, injection.prefix, injection.suffix) + } } return null diff --git a/idea/src/org/jetbrains/kotlin/idea/patterns/KotlinPatterns.kt b/idea/src/org/jetbrains/kotlin/idea/patterns/KotlinPatterns.kt new file mode 100644 index 00000000000..7066ab97616 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/patterns/KotlinPatterns.kt @@ -0,0 +1,121 @@ +/* + * 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.patterns + +import com.intellij.patterns.* +import com.intellij.util.PairProcessor +import com.intellij.util.ProcessingContext +import org.jetbrains.kotlin.descriptors.FunctionDescriptor +import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptorIfAny +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.psi.KtFunction +import org.jetbrains.kotlin.psi.KtParameter +import org.jetbrains.kotlin.psi.KtTypeReference +import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject +import org.jetbrains.kotlin.renderer.DescriptorRenderer + +// Methods in this class are used through reflection +@Suppress("unused") +object KotlinPatterns: StandardPatterns() { + @JvmStatic fun psiParameter() = KtParameterPattern() + @JvmStatic fun psiMethod() = KotlinFunctionPattern() +} + +// Methods in this class are used through reflection during pattern construction +@Suppress("unused") +open class KotlinFunctionPattern : PsiElementPattern(KtFunction::class.java) { + fun withParameters(vararg parameterTypes: String): KotlinFunctionPattern { + return withPatternCondition("kotlinFunctionPattern-withParameters") { function, context -> + if (function.valueParameters.size != parameterTypes.size) return@withPatternCondition false + + val descriptor = function.resolveToDescriptorIfAny() as? FunctionDescriptor ?: return@withPatternCondition false + val valueParameters = descriptor.valueParameters + + if (valueParameters.size != parameterTypes.size) return@withPatternCondition false + for (i in 0..valueParameters.size - 1) { + val expectedTypeString = parameterTypes[i] + val actualParameterDescriptor = valueParameters[i] + + if (DescriptorRenderer.FQ_NAMES_IN_TYPES.renderType(actualParameterDescriptor.type) != expectedTypeString) { + return@withPatternCondition false + } + } + + true + } + } + + fun withReceiver(receiverFqName: String): KotlinFunctionPattern { + return withPatternCondition("kotlinFunctionPattern-withReceiver") { function, context -> + if (function.receiverTypeReference == null) return@withPatternCondition false + if (receiverFqName == "?") return@withPatternCondition true + + val descriptor = function.resolveToDescriptorIfAny() as? FunctionDescriptor ?: return@withPatternCondition false + val receiver = descriptor.extensionReceiverParameter ?: return@withPatternCondition false + + DescriptorRenderer.FQ_NAMES_IN_TYPES.renderType(receiver.type) == receiverFqName + } + } + + fun definedInClass(fqName: String): KotlinFunctionPattern { + return withPatternCondition("kotlinFunctionPattern-definedInClass") { function, context -> + if (function.parent is KtFile) return@withPatternCondition false + + function.containingClassOrObject?.fqName?.asString() == fqName + } + } + + fun definedInPackage(packageFqName: String): KotlinFunctionPattern { + return withPatternCondition("kotlinFunctionPattern-definedInPackage") { function, context -> + if (function.parent !is KtFile) return@withPatternCondition false + + function.getContainingKtFile().packageFqName.asString() == packageFqName + } + } + + private fun withPatternCondition(debugName: String, condition: (KtFunction, ProcessingContext?) -> Boolean): KotlinFunctionPattern { + return with(object: PatternCondition(debugName) { + override fun accepts(function: KtFunction, context: ProcessingContext?): Boolean { + return condition(function, context) + } + }) + } +} + +// Methods in this class are used through reflection during pattern construction +@Suppress("unused") +class KtParameterPattern : PsiElementPattern(KtParameter::class.java) { + fun ofMethod(index: Int, pattern: ElementPattern): KtParameterPattern { + return with(object : PatternConditionPlus("KtParameterPattern-ofMethod", pattern) { + override fun processValues(ktParameter: KtParameter, + context: ProcessingContext, + processor: PairProcessor): Boolean { + return processor.process(ktParameter.ownerFunction, context) + } + + override fun accepts(ktParameter: KtParameter, context: ProcessingContext): Boolean { + val ktFunction = ktParameter.ownerFunction ?: return false + + val parameters = ktFunction.valueParameters + if (index < 0 || index >= parameters.size || ktParameter != parameters[index]) return false + + return super.accepts(ktParameter, context) + } + }) + } +} + diff --git a/idea/tests/org/jetbrains/kotlin/psi/KotlinInjectionTest.kt b/idea/tests/org/jetbrains/kotlin/psi/KotlinInjectionTest.kt index a54d256f545..a288cc8e178 100644 --- a/idea/tests/org/jetbrains/kotlin/psi/KotlinInjectionTest.kt +++ b/idea/tests/org/jetbrains/kotlin/psi/KotlinInjectionTest.kt @@ -31,6 +31,7 @@ import org.intellij.plugins.intelliLang.inject.config.InjectionPlace import org.intellij.plugins.intelliLang.references.FileReferenceInjector import org.jetbrains.kotlin.idea.test.KotlinLightCodeInsightFixtureTestCase import org.jetbrains.kotlin.idea.test.KotlinLightProjectDescriptor +import org.jetbrains.kotlin.idea.test.KotlinWithJdkAndRuntimeLightProjectDescriptor class KotlinInjectionTest : KotlinLightCodeInsightFixtureTestCase() { fun testInjectUnInjectOnSimpleString() { @@ -57,6 +58,14 @@ class KotlinInjectionTest : KotlinLightCodeInsightFixtureTestCase() { unInjectShouldBePresent = false ) + fun testInjectionOnKotlinPredefinedMethodWithRuntime() = testInjectionPresent( + """ + |val test1 = kotlin.text.Regex("some") + """, + RegExpLanguage.INSTANCE.id, + unInjectShouldBePresent = false + ) + fun testInjectionOnJavaCustomInjectionWithAnnotation() { val customInjection = BaseInjection("java") customInjection.injectedLanguageId = HTMLLanguage.INSTANCE.id @@ -285,11 +294,12 @@ class KotlinInjectionTest : KotlinLightCodeInsightFixtureTestCase() { ) override fun getProjectDescriptor(): LightProjectDescriptor { - if (getTestName(true).endsWith("WithAnnotation")) { - return KotlinLightProjectDescriptor.INSTANCE + val testName = getTestName(true) + return when { + testName.endsWith("WithAnnotation") -> KotlinLightProjectDescriptor.INSTANCE + testName.endsWith("WithRuntime") -> KotlinWithJdkAndRuntimeLightProjectDescriptor.INSTANCE + else -> JAVA_LATEST } - - return JAVA_LATEST } private fun testInjectionPresent(text: String, languageId: String? = null, unInjectShouldBePresent: Boolean = true) {