Skip to content

Commit

Permalink
Spring Support: Implement Spring-specific references
Browse files Browse the repository at this point in the history
  • Loading branch information
asedunov committed Mar 24, 2016
1 parent 72a17b0 commit 0a71eb7
Show file tree
Hide file tree
Showing 45 changed files with 906 additions and 80 deletions.
1 change: 1 addition & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ New features:
- [KT-9752](https://youtrack.jetbrains.com/issue/KT-9752) More usable file chooser for "Move declaration to another file"
- [KT-9697](https://youtrack.jetbrains.com/issue/KT-9697) Move method to companion object and back
- [KT-11098](https://youtrack.jetbrains.com/issue/KT-11098) Inspection on final classes/functions annotated with Spring @Configuration/@Component/@Bean
- [KT-11405](https://youtrack.jetbrains.com/issue/KT-11405) Navigation and Find Usages for Spring beans referenced in annotation arguments and BeanFactory method calls

General issues fixed:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,9 @@ fun KtStringTemplateExpression.getContentRange(): TextRange {
return TextRange(start, if (lastChild.elementType == KtTokens.CLOSING_QUOTE) length - lastChild.textLength else length)
}

val KtStringTemplateExpression.plainContent: String
get() = getContentRange().substring(text)

fun KtStringTemplateExpression.isSingleQuoted(): Boolean
= node.firstChildNode.textLength == 1

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ 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.references.AbstractSpringReferenceCompletionHandlerTest
import org.jetbrains.kotlin.idea.spring.tests.references.AbstractSpringReferenceCompletionTest
import org.jetbrains.kotlin.idea.spring.tests.references.AbstractSpringReferenceNavigationTest
import org.jetbrains.kotlin.idea.structureView.AbstractKotlinFileStructureTest
import org.jetbrains.kotlin.idea.stubs.AbstractMultiFileHighlightingTest
import org.jetbrains.kotlin.idea.stubs.AbstractResolveByStubTest
Expand Down Expand Up @@ -782,6 +785,18 @@ fun main(args: Array<String>) {
testClass<AbstractQuickFixTest>("UltimateQuickFixTestGenerated") {
model("ultimateQuickFixes", pattern = "^([\\w\\-_]+)\\.kt$", filenameStartsLowerCase = true)
}

testClass<AbstractSpringReferenceCompletionHandlerTest>() {
model("spring/core/references/completion/handler")
}

testClass<AbstractSpringReferenceCompletionTest>() {
model("spring/core/references/completion/variants")
}

testClass<AbstractSpringReferenceNavigationTest>() {
model("spring/core/references/navigation")
}
}

testGroup("idea/tests", "compiler/testData") {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* 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.references

import com.intellij.patterns.ElementPattern
import com.intellij.patterns.PlatformPatterns
import com.intellij.psi.*
import com.intellij.util.ProcessingContext
import org.jetbrains.kotlin.psi.KtElement

abstract class AbstractKotlinReferenceContributor : PsiReferenceContributor() {
protected inline fun <reified E : KtElement> PsiReferenceRegistrar.registerProvider(
priority: Double = PsiReferenceRegistrar.DEFAULT_PRIORITY,
crossinline factory: (E) -> PsiReference?
) {
registerMultiProvider<E>(priority) { factory(it)?.let { arrayOf(it) } ?: PsiReference.EMPTY_ARRAY }
}

protected inline fun <reified E : KtElement> PsiReferenceRegistrar.registerMultiProvider(
priority: Double = PsiReferenceRegistrar.DEFAULT_PRIORITY,
crossinline factory: (E) -> Array<PsiReference>
) {
registerMultiProvider(PlatformPatterns.psiElement(E::class.java), priority, factory)
}

protected inline fun <E : KtElement> PsiReferenceRegistrar.registerMultiProvider(
pattern: ElementPattern<E>,
priority: Double = PsiReferenceRegistrar.DEFAULT_PRIORITY,
crossinline factory: (E) -> Array<PsiReference>
) {
registerReferenceProvider(
pattern,
object : PsiReferenceProvider() {
override fun getReferencesByElement(element: PsiElement, context: ProcessingContext): Array<PsiReference> {
@Suppress("UNCHECKED_CAST")
return factory(element as E)
}
},
priority
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,21 @@

package org.jetbrains.kotlin.idea.references

import com.intellij.patterns.PlatformPatterns
import com.intellij.psi.*
import com.intellij.util.ProcessingContext
import com.intellij.psi.PsiReference
import com.intellij.psi.PsiReferenceRegistrar
import org.jetbrains.kotlin.idea.kdoc.KDocReference
import org.jetbrains.kotlin.kdoc.psi.impl.KDocName
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.*

class KotlinReferenceContributor() : PsiReferenceContributor() {
class KotlinReferenceContributor() : AbstractKotlinReferenceContributor() {
override fun registerReferenceProviders(registrar: PsiReferenceRegistrar) {
with(registrar) {
registerProvider(KtSimpleNameExpression::class.java) {
registerProvider<KtSimpleNameExpression> {
KtSimpleNameReference(it)
}

registerMultiProvider(KtNameReferenceExpression::class.java) {
registerMultiProvider<KtNameReferenceExpression> {
if (it.getReferencedNameElementType() != KtTokens.IDENTIFIER) return@registerMultiProvider emptyArray()

when (it.readWriteAccess(useResolveForReadWrite = false)) {
Expand All @@ -44,46 +43,33 @@ class KotlinReferenceContributor() : PsiReferenceContributor() {
}
}

registerProvider(KtConstructorDelegationReferenceExpression::class.java) {
registerProvider<KtConstructorDelegationReferenceExpression> {
KtConstructorDelegationReference(it)
}

registerProvider(KtCallExpression::class.java) {
registerProvider<KtCallExpression> {
KtInvokeFunctionReference(it)
}

registerProvider(KtArrayAccessExpression::class.java) {
registerProvider<KtArrayAccessExpression> {
KtArrayAccessReference(it)
}

registerProvider(KtForExpression::class.java) {
registerProvider<KtForExpression> {
KtForLoopInReference(it)
}

registerProvider(KtPropertyDelegate::class.java) {
registerProvider<KtPropertyDelegate> {
KtPropertyDelegationMethodsReference(it)
}

registerProvider(KtDestructuringDeclaration::class.java) {
registerProvider<KtDestructuringDeclaration> {
KtDestructuringDeclarationReference(it)
}

registerProvider(KDocName::class.java) {
registerProvider<KDocName> {
KDocReference(it)
}
}
}

private fun <E : KtElement> PsiReferenceRegistrar.registerProvider(elementClass: Class<E>, factory: (E) -> KtReference) {
registerMultiProvider(elementClass, { arrayOf(factory(it)) })
}

private fun <E : KtElement> PsiReferenceRegistrar.registerMultiProvider(elementClass: Class<E>, factory: (E) -> Array<PsiReference>) {
registerReferenceProvider(PlatformPatterns.psiElement(elementClass), object: PsiReferenceProvider() {
override fun getReferencesByElement(element: PsiElement, context: ProcessingContext): Array<PsiReference> {
@Suppress("UNCHECKED_CAST")
return factory(element as E)
}
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,18 @@ abstract class KotlinFixtureCompletionBaseTestCase : KotlinLightCodeInsightFixtu
open fun doTest(testPath: String) {
setUpFixture(testPath)

val fileText = FileUtil.loadFile(File(testPath), true)
try {
val fileText = FileUtil.loadFile(File(testPath), true)

if (ExpectedCompletionUtils.shouldRunHighlightingBeforeCompletion(fileText)) {
myFixture.doHighlighting()
}
if (ExpectedCompletionUtils.shouldRunHighlightingBeforeCompletion(fileText)) {
myFixture.doHighlighting()
}

testCompletion(fileText, getPlatform(), { completionType, count -> complete(completionType, count) }, defaultCompletionType(), defaultInvocationCount())
testCompletion(fileText, getPlatform(), { completionType, count -> complete(completionType, count) }, defaultCompletionType(), defaultInvocationCount())
}
finally {
tearDownFixture()
}
}

protected open fun setUpFixture(testPath: String) {
Expand All @@ -51,4 +56,8 @@ abstract class KotlinFixtureCompletionBaseTestCase : KotlinLightCodeInsightFixtu

myFixture.configureByFile(testPath)
}

protected open fun tearDownFixture() {

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,18 @@ abstract class AbstractCompletionHandlerTest(private val defaultCompletionType:
}
finally {
settingManager.dropTemporarySettings()
tearDownFixture()
}
}

protected open fun setUpFixture(testPath: String) {
fixture.configureByFile(testPath)
}

protected open fun tearDownFixture() {

}

override fun getProjectDescriptor() = KotlinWithJdkAndRuntimeLightProjectDescriptor.INSTANCE
}

Expand Down
2 changes: 2 additions & 0 deletions idea/idea-ultimate/src/META-INF/kotlin-spring.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<idea-plugin>
<extensions defaultExtensionNs="com.intellij">
<psi.referenceContributor language="kotlin" implementation="org.jetbrains.kotlin.idea.spring.references.KotlinSpringReferenceContributor"/>

<localInspection implementationClass="org.jetbrains.kotlin.idea.spring.inspections.KotlinFinalClassOrFunSpringInspection"
displayName="Final Kotlin class or function with Spring annotation"
groupBundle="resources.messages.SpringBundle"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
* 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.references

import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiReference
import com.intellij.psi.PsiReferenceRegistrar
import com.intellij.spring.constants.SpringAnnotationsConstants
import com.intellij.spring.constants.SpringConstants
import com.intellij.spring.model.utils.resources.SpringResourcesBuilder
import com.intellij.spring.model.utils.resources.SpringResourcesUtil
import com.intellij.spring.references.SpringBeanNamesReferenceProvider
import com.intellij.spring.references.SpringBeanReference
import org.jetbrains.kotlin.asJava.toLightClass
import org.jetbrains.kotlin.descriptors.CallableDescriptor
import org.jetbrains.kotlin.descriptors.CallableMemberDescriptor
import org.jetbrains.kotlin.descriptors.ConstructorDescriptor
import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptor
import org.jetbrains.kotlin.idea.imports.importableFqName
import org.jetbrains.kotlin.idea.references.AbstractKotlinReferenceContributor
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.getContentRange
import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType
import org.jetbrains.kotlin.psi.psiUtil.isPlain
import org.jetbrains.kotlin.psi.psiUtil.plainContent
import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall
import org.jetbrains.kotlin.resolve.descriptorUtil.overriddenTreeAsSequence
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
import org.jetbrains.kotlin.resolve.source.getPsi

class KotlinSpringReferenceContributor : AbstractKotlinReferenceContributor() {
override fun registerReferenceProviders(registrar: PsiReferenceRegistrar) {
registrar.registerProvider<KtStringTemplateExpression> {
if (!it.isPlain()) return@registerProvider null

val callExpression = (it.parent as? KtValueArgument)?.getStrictParentOfType<KtCallExpression>() ?: return@registerProvider null
val context = callExpression.analyze(BodyResolveMode.PARTIAL)
val resolvedCall = callExpression.getResolvedCall(context) ?: return@registerProvider null
val callable = resolvedCall.resultingDescriptor as? CallableMemberDescriptor ?: return@registerProvider null
if (!callable.overriddenTreeAsSequence(true).any {
it.containingDeclaration.importableFqName?.asString() == SpringConstants.BEAN_FACTORY_CLASS
}) return@registerProvider null
if (callable.name.asString() !in SpringBeanNamesReferenceProvider.METHODS) return@registerProvider null

SpringBeanReference(it, it.getContentRange())
}

registrar.registerProvider<KtStringTemplateExpression>(PsiReferenceRegistrar.HIGHER_PRIORITY) {
if (!it.isPlain()) return@registerProvider null

val argument = it.parent as? KtValueArgument ?: return@registerProvider null
val argumentName = argument.getArgumentName() ?: return@registerProvider null
if (argumentName.asName.asString() != "name") return@registerProvider null

val entry = argument.getStrictParentOfType<KtAnnotationEntry>() ?: return@registerProvider null
val context = entry.analyze(BodyResolveMode.PARTIAL)
val resolvedCall = entry.getResolvedCall(context) ?: return@registerProvider null
val annotation = (resolvedCall.resultingDescriptor as? ConstructorDescriptor)?.containingDeclaration ?: return@registerProvider null
if (annotation.importableFqName?.asString() != SpringAnnotationsConstants.JAVAX_RESOURCE) return@registerProvider null

val contentRange = it.getContentRange()
var startOffset = contentRange.startOffset
val isFactoryBeanRef: Boolean
if (it.plainContent.startsWith("&")) {
startOffset++
isFactoryBeanRef = true
}
else {
isFactoryBeanRef = false
}

val callable = (it.getStrictParentOfType<KtAnnotationEntry>()?.parent as? KtModifierList)?.parent as? KtCallableDeclaration
val callableType = (callable?.resolveToDescriptor() as? CallableDescriptor)?.returnType
val requiredSuperClass = callableType?.constructor?.declarationDescriptor?.source?.getPsi() as? KtClass

SpringBeanReference(it, TextRange(startOffset, contentRange.endOffset), requiredSuperClass?.toLightClass(), isFactoryBeanRef)
}

registrar.registerProvider<KtStringTemplateExpression>(PsiReferenceRegistrar.HIGHER_PRIORITY) {
if (!it.isPlain()) return@registerProvider null

val argument = it.parent as? KtValueArgument ?: return@registerProvider null
val argumentName = argument.getArgumentName()
if (argumentName != null && argumentName.asName.asString() != "value") return@registerProvider null

val entry = argument.getStrictParentOfType<KtAnnotationEntry>() ?: return@registerProvider null
val context = entry.analyze(BodyResolveMode.PARTIAL)
val resolvedCall = entry.getResolvedCall(context) ?: return@registerProvider null
val annotation = (resolvedCall.resultingDescriptor as? ConstructorDescriptor)?.containingDeclaration ?: return@registerProvider null
if (annotation.importableFqName?.asString() != SpringAnnotationsConstants.SCOPE) return@registerProvider null

KtSpringBeanScopeReference(it)
}

registrar.registerMultiProvider<KtStringTemplateExpression> {
if (!it.isPlain()) return@registerMultiProvider PsiReference.EMPTY_ARRAY

val callExpression = (it.parent as? KtValueArgument)?.getStrictParentOfType<KtCallExpression>()
?: return@registerMultiProvider PsiReference.EMPTY_ARRAY
val context = callExpression.analyze(BodyResolveMode.PARTIAL)
val resolvedCall = callExpression.getResolvedCall(context) ?: return@registerMultiProvider PsiReference.EMPTY_ARRAY
val classDescriptor = (resolvedCall.resultingDescriptor as? ConstructorDescriptor)?.containingDeclaration
?: return@registerMultiProvider PsiReference.EMPTY_ARRAY
val qName = classDescriptor.importableFqName?.asString()
if (qName != SpringConstants.CLASS_PATH_XML_APP_CONTEXT && qName != SpringConstants.CLASS_PATH_RESOURCE) {
return@registerMultiProvider PsiReference.EMPTY_ARRAY
}

val content = it.plainContent
val resourcesBuilder = SpringResourcesBuilder.create(it, content).fromRoot(content.startsWith("/")).soft(false)
SpringResourcesUtil.getInstance().getClassPathReferences(resourcesBuilder)
}
}
}
Loading

0 comments on commit 0a71eb7

Please sign in to comment.