From d7fcc693158be2273185f1a795688033a8ef1497 Mon Sep 17 00:00:00 2001 From: Nicolay Mitropolsky Date: Tue, 29 Aug 2017 15:25:48 +0300 Subject: [PATCH] `KotlinLanguageInjector#injectInAnnotationCall` optimization: using `PsiClassNamePatternCondition` to avoid calling `getResolvedCall` --- .../jetbrains/kotlin/util/TypeIndexUtil.kt | 2 +- .../idea/injection/KotlinLanguageInjector.kt | 138 ++++++++----- .../kotlin/idea/patterns/KotlinPatterns.kt | 11 +- .../kotlin/psi/KotlinInjectionTest.kt | 193 ++++++++---------- 4 files changed, 186 insertions(+), 158 deletions(-) diff --git a/idea/ide-common/src/org/jetbrains/kotlin/util/TypeIndexUtil.kt b/idea/ide-common/src/org/jetbrains/kotlin/util/TypeIndexUtil.kt index 1b32a22de5d..8017355596e 100644 --- a/idea/ide-common/src/org/jetbrains/kotlin/util/TypeIndexUtil.kt +++ b/idea/ide-common/src/org/jetbrains/kotlin/util/TypeIndexUtil.kt @@ -31,7 +31,7 @@ fun KtUserType.aliasImportMap(): Multimap { return (file as KtFile).aliasImportMap() } -private fun KtFile.aliasImportMap(): Multimap { +fun KtFile.aliasImportMap(): Multimap { val cached = getUserData(ALIAS_IMPORT_DATA_KEY) val modificationStamp = modificationStamp if (cached != null && modificationStamp == cached.fileModificationStamp) { diff --git a/idea/idea-jvm/src/org/jetbrains/kotlin/idea/injection/KotlinLanguageInjector.kt b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/injection/KotlinLanguageInjector.kt index f66f727ae17..45f609d97dc 100644 --- a/idea/idea-jvm/src/org/jetbrains/kotlin/idea/injection/KotlinLanguageInjector.kt +++ b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/injection/KotlinLanguageInjector.kt @@ -23,21 +23,29 @@ import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.progress.ProgressManager import com.intellij.openapi.project.Project import com.intellij.openapi.util.Key +import com.intellij.openapi.util.text.StringUtilRt +import com.intellij.patterns.PatternCondition +import com.intellij.patterns.PatternConditionPlus +import com.intellij.patterns.PsiClassNamePatternCondition +import com.intellij.patterns.ValuePatternCondition import com.intellij.psi.* import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil import com.intellij.psi.search.LocalSearchScope import com.intellij.psi.search.searches.ReferencesSearch +import com.intellij.psi.util.CachedValueProvider +import com.intellij.psi.util.CachedValuesManager 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.LanguageInjectionSupport import org.intellij.plugins.intelliLang.inject.TemporaryPlacesRegistry import org.intellij.plugins.intelliLang.inject.config.BaseInjection +import org.intellij.plugins.intelliLang.inject.config.InjectionPlace import org.intellij.plugins.intelliLang.inject.java.JavaLanguageInjectionSupport import org.intellij.plugins.intelliLang.util.AnnotationUtilEx -import org.jetbrains.kotlin.descriptors.DeclarationDescriptorWithSource import org.jetbrains.kotlin.descriptors.FunctionDescriptor import org.jetbrains.kotlin.idea.caches.resolve.analyze +import org.jetbrains.kotlin.idea.patterns.KotlinFunctionPattern import org.jetbrains.kotlin.idea.references.KtReference import org.jetbrains.kotlin.idea.references.mainReference import org.jetbrains.kotlin.idea.runInReadActionWithWriteActionPriority @@ -45,27 +53,27 @@ import org.jetbrains.kotlin.idea.util.ProjectRootsUtil import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.psi.* import org.jetbrains.kotlin.resolve.annotations.argumentValue -import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode -import org.jetbrains.kotlin.resolve.source.getPsi -import java.util.* -import kotlin.collections.ArrayList +import org.jetbrains.kotlin.util.aliasImportMap +import org.jetbrains.kotlin.utils.SmartList +import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull class KotlinLanguageInjector( - val configuration: Configuration, - val project: Project, - val temporaryPlacesRegistry: TemporaryPlacesRegistry + private val configuration: Configuration, + private val project: Project, + private val temporaryPlacesRegistry: TemporaryPlacesRegistry ) : MultiHostInjector { companion object { private val STRING_LITERALS_REGEXP = "\"([^\"]*)\"".toRegex() private val ABSENT_KOTLIN_INJECTION = BaseInjection("ABSENT_KOTLIN_BASE_INJECTION") } - val kotlinSupport: KotlinLanguageInjectionSupport? by lazy { + private val kotlinSupport: KotlinLanguageInjectionSupport? by lazy { ArrayList(InjectorUtils.getActiveInjectionSupports()).filterIsInstance(KotlinLanguageInjectionSupport::class.java).firstOrNull() } private data class KotlinCachedInjection(val modificationCount: Long, val baseInjection: BaseInjection) + private var KtStringTemplateExpression.cachedInjectionWithModification: KotlinCachedInjection? by UserDataProperty( Key.create("CACHED_INJECTION_WITH_MODIFICATION")) @@ -159,7 +167,6 @@ class KotlinLanguageInjector( ?: injectInAnnotationCall(place) ?: injectWithReceiver(place) ?: injectWithVariableUsage(place, originalHost) - ?: injectWithAnnotationEntry(place) } private fun injectWithExplicitCodeInstruction(host: KtElement): InjectionInfo? { @@ -178,9 +185,9 @@ class KotlinLanguageInjector( val callExpression = qualifiedExpression.selectorExpression as? KtCallExpression ?: return null val callee = callExpression.calleeExpression ?: return null - if (isAnalyzeOff(qualifiedExpression.project)) return null + if (isAnalyzeOff()) return null - val kotlinInjections = Configuration.getInstance().getInjections(KOTLIN_SUPPORT_ID) + val kotlinInjections = configuration.getInjections(KOTLIN_SUPPORT_ID) val calleeName = callee.text val possibleNames = collectPossibleNames(kotlinInjections) @@ -223,11 +230,10 @@ class KotlinLanguageInjector( // Given place is not original host of the injection so we stop to prevent stepping through indirect references if (!originalHost) return null - val ktHost: KtElement = host val ktProperty = host.parent as? KtProperty ?: return null if (ktProperty.initializer != host) return null - if (isAnalyzeOff(ktHost.project)) return null + if (isAnalyzeOff()) return null val searchScope = LocalSearchScope(arrayOf(ktProperty.containingFile), "", true) return ReferencesSearch.search(ktProperty, searchScope).asSequence().mapNotNull { psiReference -> @@ -243,7 +249,7 @@ class KotlinLanguageInjector( val callExpression = PsiTreeUtil.getParentOfType(ktHost, KtCallExpression::class.java) ?: return null val callee = callExpression.calleeExpression ?: return null - if (isAnalyzeOff(ktHost.project)) return null + if (isAnalyzeOff()) return null for (reference in callee.references) { ProgressManager.checkCanceled() @@ -269,25 +275,34 @@ class KotlinLanguageInjector( private fun injectInAnnotationCall(host: KtElement): InjectionInfo? { val argument = host.parent as? KtValueArgument ?: return null val annotationEntry = argument.parent.parent as? KtAnnotationEntry ?: return null - val callDescriptor = annotationEntry.getResolvedCall(annotationEntry.analyze())?.candidateDescriptor - val psiClass = (callDescriptor as? DeclarationDescriptorWithSource)?.source?.getPsi() as? PsiClass ?: return null - val argumentName = argument.getArgumentName()?.asName?.identifier ?: "value" - val method = psiClass.findMethodsByName(argumentName, false).singleOrNull() ?: return null - return findInjection(method, Configuration.getInstance().getInjections(JavaLanguageInjectionSupport.JAVA_SUPPORT_ID)) + if (!fastCheckInjectionsExists(annotationEntry)) return null + val calleeReference = annotationEntry.calleeExpression?.constructorReferenceExpression?.mainReference + val callee = calleeReference?.resolve() + when (callee) { + is KtFunction -> return injectionForKotlinCall(argument, callee, calleeReference) + is PsiClass -> { + val psiClass = callee as? PsiClass ?: return null + val argumentName = argument.getArgumentName()?.asName?.identifier ?: "value" + val method = psiClass.findMethodsByName(argumentName, false).singleOrNull() ?: return null + return findInjection(method, configuration.getInjections(JavaLanguageInjectionSupport.JAVA_SUPPORT_ID)) + } + else -> 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 injectionInfo = findInjection(psiParameter, Configuration.getInstance().getInjections(JavaLanguageInjectionSupport.JAVA_SUPPORT_ID)) + val injectionInfo = findInjection(psiParameter, configuration.getInjections(JavaLanguageInjectionSupport.JAVA_SUPPORT_ID)) if (injectionInfo != null) { return injectionInfo } val annotations = AnnotationUtilEx.getAnnotationFrom( psiParameter, - Configuration.getProjectInstance(psiParameter.project).advancedConfiguration.languageAnnotationPair, + configuration.advancedConfiguration.languageAnnotationPair, true) if (annotations.isNotEmpty()) { @@ -301,7 +316,7 @@ class KotlinLanguageInjector( val argumentIndex = (argument.parent as KtValueArgumentList).arguments.indexOf(argument) val ktParameter = ktFunction.valueParameters.getOrNull(argumentIndex) ?: return null - val patternInjection = findInjection(ktParameter, Configuration.getInstance().getInjections(KOTLIN_SUPPORT_ID)) + val patternInjection = findInjection(ktParameter, configuration.getInjections(KOTLIN_SUPPORT_ID)) if (patternInjection != null) { return patternInjection } @@ -319,29 +334,6 @@ class KotlinLanguageInjector( return InjectionInfo(languageId, null, null) } - private fun injectWithAnnotationEntry(host: KtElement): InjectionInfo? { - val argument = host.parent as? KtValueArgument ?: return null - val annotationEntry = PsiTreeUtil.getParentOfType(host, KtAnnotationEntry::class.java) ?: return null - if (isAnalyzeOff(host.project)) return null - - val calleeReference = annotationEntry.calleeExpression?.constructorReferenceExpression?.mainReference - val callee = calleeReference?.resolve() - return when (callee) { - is KtFunction -> injectionForKotlinCall(argument, callee, calleeReference) - is PsiClass -> { - // Look for java injections for the PsiAnnotationMethod. - (argument.reference ?: argument.getArgumentName()?.referenceExpression?.mainReference) - ?.let { it.resolve() as? PsiMethod } - ?.let { findInjection(it, Configuration.getInstance().getInjections(JavaLanguageInjectionSupport.JAVA_SUPPORT_ID)) } - ?.takeIf { injectionInfo -> - // Temporary forbid injection for SpEL because of inspections that gives warnings when host language is not Java. - injectionInfo.languageId != "SpEL" - } - } - else -> null - } - } - private fun findInjection(element: PsiElement?, injections: List): InjectionInfo? { for (injection in injections) { if (injection.acceptsPsiElement(element)) { @@ -352,8 +344,8 @@ class KotlinLanguageInjector( return null } - private fun isAnalyzeOff(project: Project): Boolean { - return Configuration.getProjectInstance(project).advancedConfiguration.dfaOption == Configuration.DfaOption.OFF + private fun isAnalyzeOff(): Boolean { + return configuration.advancedConfiguration.dfaOption == Configuration.DfaOption.OFF } private class InjectionInfo(val languageId: String?, val prefix: String?, val suffix: String?) { @@ -382,4 +374,52 @@ class KotlinLanguageInjector( return InjectionInfo(id, prefix, suffix) } -} + + private val injectableTargetClassShortNames = CachedValuesManager.getManager(project) + .createCachedValue({ + CachedValueProvider.Result.create(HashSet().apply { + for (injection in configuration.getInjections(JavaLanguageInjectionSupport.JAVA_SUPPORT_ID)) { + for (injectionPlace in injection.injectionPlaces) { + for (targetClassFQN in retrieveJavaPlaceTargetClassesFQNs(injectionPlace)) { + add(StringUtilRt.getShortName(targetClassFQN)) + } + } + } + for (injection in configuration.getInjections(org.jetbrains.kotlin.idea.injection.KOTLIN_SUPPORT_ID)) { + for (injectionPlace in injection.injectionPlaces) { + for (targetClassFQN in retrieveKotlinPlaceTargetClassesFQNs(injectionPlace)) { + add(StringUtilRt.getShortName(targetClassFQN)) + } + } + } + }, configuration) + }, false) + + private fun fastCheckInjectionsExists(annotationEntry: KtAnnotationEntry): Boolean { + val referencedName = (annotationEntry.typeReference?.typeElement as? KtUserType)?.referencedName ?: return false + val annotationShortName = annotationEntry.containingKtFile.aliasImportMap()[referencedName].singleOrNull() ?: referencedName + return annotationShortName in injectableTargetClassShortNames.value + } + + private fun retrieveJavaPlaceTargetClassesFQNs(place: InjectionPlace): Collection { + val classCondition = place.elementPattern.condition.conditions.firstOrNull { it.debugMethodName == "definedInClass" } + as? PatternConditionPlus<*, *> ?: return emptyList() + val psiClassNamePatternCondition = classCondition.valuePattern.condition.conditions. + firstIsInstanceOrNull() ?: return emptyList() + val valuePatternCondition = psiClassNamePatternCondition.namePattern.condition.conditions.firstIsInstanceOrNull>() ?: return emptyList() + return valuePatternCondition.values + } + + private fun retrieveKotlinPlaceTargetClassesFQNs(place: InjectionPlace): Collection { + val classNames = SmartList() + fun collect(condition: PatternCondition<*>) { + when (condition) { + is PatternConditionPlus<*, *> -> condition.valuePattern.condition.conditions.forEach { collect(it) } + is KotlinFunctionPattern.DefinedInClassCondition -> classNames.add(condition.fqName) + } + } + place.elementPattern.condition.conditions.forEach { collect(it) } + return classNames + } + +} \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/patterns/KotlinPatterns.kt b/idea/src/org/jetbrains/kotlin/idea/patterns/KotlinPatterns.kt index 72a60dd4b7c..002118de064 100644 --- a/idea/src/org/jetbrains/kotlin/idea/patterns/KotlinPatterns.kt +++ b/idea/src/org/jetbrains/kotlin/idea/patterns/KotlinPatterns.kt @@ -74,14 +74,15 @@ open class KotlinFunctionPattern : PsiElementPattern - if (function.parent is KtFile) return@withPatternCondition false - - function.containingClassOrObject?.fqName?.asString() == fqName + class DefinedInClassCondition(val fqName: String) : PatternCondition("kotlinFunctionPattern-definedInClass") { + override fun accepts(element: KtFunction, context: ProcessingContext?): Boolean { + if (element.parent is KtFile) return false + return element.containingClassOrObject?.fqName?.asString() == fqName } } + fun definedInClass(fqName: String): KotlinFunctionPattern = with(DefinedInClassCondition(fqName)) + fun definedInPackage(packageFqName: String): KotlinFunctionPattern { return withPatternCondition("kotlinFunctionPattern-definedInPackage") { function, _ -> if (function.parent !is KtFile) return@withPatternCondition false diff --git a/idea/tests/org/jetbrains/kotlin/psi/KotlinInjectionTest.kt b/idea/tests/org/jetbrains/kotlin/psi/KotlinInjectionTest.kt index 2d642440a25..940c067ab69 100644 --- a/idea/tests/org/jetbrains/kotlin/psi/KotlinInjectionTest.kt +++ b/idea/tests/org/jetbrains/kotlin/psi/KotlinInjectionTest.kt @@ -19,6 +19,7 @@ package org.jetbrains.kotlin.psi import com.intellij.codeInsight.completion.CompletionType import com.intellij.lang.html.HTMLLanguage import com.intellij.openapi.fileTypes.PlainTextLanguage +import org.intellij.lang.annotations.Language import org.intellij.lang.regexp.RegExpLanguage import org.intellij.plugins.intelliLang.Configuration import org.intellij.plugins.intelliLang.inject.config.BaseInjection @@ -266,7 +267,7 @@ class KotlinInjectionTest : AbstractInjectionTest() { fun other() { foo(v) } """, languageId = HTMLLanguage.INSTANCE.id, unInjectShouldBePresent = false) - + fun testInjectionOfCustomParameterWithAnnotation() = doInjectionPresentTest( """ import org.intellij.lang.annotations.Language @@ -413,140 +414,126 @@ class KotlinInjectionTest : AbstractInjectionTest() { ) fun testJavaAnnotationsPattern() { - val customInjection = BaseInjection("java") - customInjection.injectedLanguageId = RegExpLanguage.INSTANCE.id - val elementPattern = customInjection.compiler.createElementPattern( - """psiMethod().withName("value").withParameters().definedInClass("Matches")""", - "temp rule") - customInjection.setInjectionPlaces(InjectionPlace(elementPattern, true)) + myFixture.addClass(""" + @interface Matches { String value(); } + """) - try { - Configuration.getInstance().replaceInjections(listOf(customInjection), listOf(), true) - - doInjectionPresentTest( - javaText = """ - @interface Matches { String value(); } - """, - text = """ - @Matches("[A-Z][a-z]+") - val name = "John" - """, - languageId = RegExpLanguage.INSTANCE.id, - unInjectShouldBePresent = false - ) - - doInjectionPresentTest( - javaText = """ - @interface Matches { String value(); } - """, - text = """ - @Matches(value = "[A-Z][a-z]+") - val name = "John" - """, - languageId = RegExpLanguage.INSTANCE.id, - unInjectShouldBePresent = false - ) - } - finally { - Configuration.getInstance().replaceInjections(listOf(), listOf(customInjection), true) - } + doAnnotationInjectionTest( + injectedLanguage = RegExpLanguage.INSTANCE.id, + pattern = """psiMethod().withName("value").withParameters().definedInClass("Matches")""", + kotlinCode = """ + @Matches("[A-Z][a-z]+") + val name = "John" + """ + ) } fun testKotlinAnnotationsPattern() { - val customInjection = BaseInjection("kotlin") - customInjection.injectedLanguageId = RegExpLanguage.INSTANCE.id - val elementPattern = customInjection.compiler.createElementPattern( - """kotlinParameter().ofFunction(0, kotlinFunction().withName("Matches").definedInClass("Matches"))""".trimIndent(), - "temp rule") - customInjection.setInjectionPlaces(InjectionPlace(elementPattern, true)) - - try { - Configuration.getInstance().replaceInjections(listOf(customInjection), listOf(), true) - - doInjectionPresentTest( - text = """ + doAnnotationInjectionTest( + patternLanguage = "kotlin", + injectedLanguage = RegExpLanguage.INSTANCE.id, + pattern = """kotlinParameter().ofFunction(0, kotlinFunction().withName("Matches").definedInClass("Matches"))""", + kotlinCode = """ annotation class Matches(val pattern: String) @Matches("[A-Z][a-z]+") val name = "John" - """, - languageId = RegExpLanguage.INSTANCE.id, - unInjectShouldBePresent = false - ) + """ + ) + } - doInjectionPresentTest( - text = """ + fun testKotlinAnnotationsPatternNamed() { + doAnnotationInjectionTest( + patternLanguage = "kotlin", + injectedLanguage = RegExpLanguage.INSTANCE.id, + pattern = """kotlinParameter().ofFunction(0, kotlinFunction().withName("Matches").definedInClass("Matches"))""", + kotlinCode = """ annotation class Matches(val pattern: String) @Matches(pattern = "[A-Z][a-z]+") val name = "John" - """, - languageId = RegExpLanguage.INSTANCE.id, - unInjectShouldBePresent = false + """ ) } - finally { - Configuration.getInstance().replaceInjections(listOf(), listOf(customInjection), true) - } - } + fun testInjectionInJavaAnnotation() { - val customInjection = BaseInjection("java") - customInjection.injectedLanguageId = HTMLLanguage.INSTANCE.id - val elementPattern = customInjection.compiler.createElementPattern( - """psiMethod().withName("value").withParameters().definedInClass("InHtml")""", - "SuppressWarnings temp rule") - customInjection.setInjectionPlaces(InjectionPlace(elementPattern, true)) - try { - Configuration.getInstance().replaceInjections(listOf(customInjection), listOf(), true) + myFixture.addClass(""" + @interface InHtml { + String value(); + } + """) - doInjectionPresentTest( - """ - @InHtml("l>") - fun foo() { - } - """, """ - @interface InHtml { - String value(); - } - """.trimIndent(), - HTMLLanguage.INSTANCE.id, - unInjectShouldBePresent = false - ) + doAnnotationInjectionTest( + injectedLanguage = HTMLLanguage.INSTANCE.id, + pattern = """psiMethod().withName("value").withParameters().definedInClass("InHtml")""", + kotlinCode = """ + @InHtml("l>") + fun foo() { + } + """, + additionalAsserts = { assertSameElements(myFixture.complete(CompletionType.BASIC).flatMap { it.allLookupStrings }, "html") } + ) - assertSameElements(myFixture.complete(CompletionType.BASIC).flatMap { it.allLookupStrings }, - "html") - } - finally { - Configuration.getInstance().replaceInjections(listOf(), listOf(customInjection), true) - } } fun testInjectionInJavaAnnotationWithNamedParam() { - val customInjection = BaseInjection("java") - customInjection.injectedLanguageId = HTMLLanguage.INSTANCE.id + myFixture.addClass(""" + package myinjection; + + @interface InHtml { + String html(); + } + """) + doAnnotationInjectionTest( + injectedLanguage = HTMLLanguage.INSTANCE.id, + pattern = """psiMethod().withName("html").withParameters().definedInClass("myinjection.InHtml")""", + kotlinCode = """ + import myinjection.InHtml + + @InHtml(html = "l>") + fun foo() { + } + """) + } + + fun testInjectionInAliasedJavaAnnotation() { + myFixture.addClass(""" + @interface InHtml { + String html(); + } + """) + doAnnotationInjectionTest( + injectedLanguage = HTMLLanguage.INSTANCE.id, + pattern = """psiMethod().withName("html").withParameters().definedInClass("InHtml")""", + kotlinCode = """ + import InHtml as InHtmlAliased + + @InHtmlAliased(html = "l>") + fun foo() { + } + """ + ) + } + + private fun doAnnotationInjectionTest(patternLanguage: String = "java", injectedLanguage: String, pattern: String, @Language("kotlin") kotlinCode: String, additionalAsserts: () -> Unit = {}) { + val customInjection = BaseInjection(patternLanguage) + customInjection.injectedLanguageId = injectedLanguage val elementPattern = customInjection.compiler.createElementPattern( - """psiMethod().withName("html").withParameters().definedInClass("InHtml")""", - "SuppressWarnings temp rule") + pattern, + "temp rule") customInjection.setInjectionPlaces(InjectionPlace(elementPattern, true)) try { Configuration.getInstance().replaceInjections(listOf(customInjection), listOf(), true) doInjectionPresentTest( - """ - @InHtml(html = "l>") - fun foo() { - } - """, """ - @interface InHtml { - String html(); - } - """.trimIndent(), - HTMLLanguage.INSTANCE.id, + kotlinCode, null, + injectedLanguage, unInjectShouldBePresent = false ) + additionalAsserts() } finally { Configuration.getInstance().replaceInjections(listOf(), listOf(customInjection), true)