Files
openide/plugins/gradle/java/src/service/resolve/GradleExtensionsContributor.kt
Nikita Biriukov 7110c90cca [gradle][groovy] IDEA-343916 enable completion for a property with version catalog name
- fixes completion for "libs" `GradleVersionCatalogsCompletionTest#testCompletionForVersionCatalogProperty`
- also enables completion for all other version catalog names of a build

I realized that `GradleExtensionsContributor#processPropertiesFromCatalog` is called not only while resolving but also while completion: when only a part of catalog name was written. In this case, `name` argument is `null` and accessors should be created for all version catalogs of the corresponding build. To achieve that, I added `GradleVersionCatalogHandler#getAccessorsForAllCatalogs`.

I decided to add a separate method for getting all accessors because if I use the existing `getAccessorClass`, it would recreate `ProjectBuildModel` for each version catalog.

Code review: IJ-CR-153925
(cherry picked from commit 1250f5b3bb22b6ed1968683b0b6144418d0ad70f)

GitOrigin-RevId: 3f270387510983fbd51ba630c54a04db7d5a5025
2025-02-14 14:58:29 +00:00

135 lines
6.1 KiB
Kotlin

// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.plugins.gradle.service.resolve
import com.intellij.icons.AllIcons
import com.intellij.lang.properties.IProperty
import com.intellij.openapi.roots.ProjectFileIndex
import com.intellij.psi.*
import com.intellij.psi.scope.PsiScopeProcessor
import com.intellij.psi.util.InheritanceUtil
import org.jetbrains.plugins.gradle.service.resolve.GradleCommonClassNames.GRADLE_API_PROJECT
import org.jetbrains.plugins.gradle.settings.GradleExtensionsSettings
import org.jetbrains.plugins.gradle.settings.GradleExtensionsSettings.GradleExtensionsData
import org.jetbrains.plugins.groovy.lang.psi.impl.synthetic.GrLightField
import org.jetbrains.plugins.groovy.lang.resolve.NonCodeMembersContributor
import org.jetbrains.plugins.groovy.lang.resolve.api.GroovyPropertyBase
import org.jetbrains.plugins.groovy.lang.resolve.getName
import org.jetbrains.plugins.groovy.lang.resolve.processors.inference.type
import org.jetbrains.plugins.groovy.lang.resolve.shouldProcessProperties
class GradleExtensionsContributor : NonCodeMembersContributor() {
override fun getClassNames(): Collection<String> {
return listOf(GradleCommonClassNames.GRADLE_API_EXTRA_PROPERTIES_EXTENSION, GRADLE_API_PROJECT)
}
override fun processDynamicElements(qualifierType: PsiType,
aClass: PsiClass?,
processor: PsiScopeProcessor,
place: PsiElement,
state: ResolveState) {
if (qualifierType !is GradleProjectAwareType && !InheritanceUtil.isInheritor(qualifierType, GradleCommonClassNames.GRADLE_API_EXTRA_PROPERTIES_EXTENSION)) return
if (!processor.shouldProcessProperties()) return
val file = place.containingFile
val data = getExtensionsFor(file) ?: return
val name = processor.getName(state)
val resolvedProperties = processPropertiesFromFile(aClass, processor, place, state)
val versionCatalogProperties = processPropertiesFromCatalog(name, place, processor, state) ?: return
val properties = if (name == null) data.findAllProperties() else listOfNotNull(data.findProperty(name))
for (property in properties) {
if (property.name in resolvedProperties || property.name in versionCatalogProperties) {
continue
}
if (!processor.execute(GradleGroovyProperty(property.name, property.typeFqn, null, file), state)) {
return
}
}
}
private fun processPropertiesFromFile(aClass: PsiClass?,
processor: PsiScopeProcessor,
place: PsiElement,
state: ResolveState) : Set<String> {
if (aClass == null) {
return emptySet()
}
val factory = JavaPsiFacade.getInstance(place.project)
val stringType = factory.findClass(CommonClassNames.JAVA_LANG_STRING, place.resolveScope)?.type() ?: return emptySet()
val properties = gradlePropertiesStream(place)
val name = processor.getName(state)
val processedNames = mutableSetOf<String>()
for (propertyFile in properties) {
if (name == null) {
for (property in propertyFile.properties) {
if (property.name.contains(".")) {
continue
}
processedNames.add(property.name)
val newProperty = createGroovyProperty(aClass, property, stringType)
processor.execute(newProperty, state)
}
}
else {
val property = propertyFile.findPropertyByKey(name) ?: continue
val newProperty = createGroovyProperty(aClass, property, stringType)
processor.execute(newProperty, state)
return setOf(newProperty.name)
}
}
return processedNames
}
companion object {
private fun createGroovyProperty(aClass: PsiClass,
property: IProperty,
stringType: PsiClassType): GrLightField {
val newProperty = GrLightField(aClass, property.name, stringType, property.psiElement)
newProperty.setIcon(AllIcons.FileTypes.Properties)
newProperty.originInfo = PROPERTIES_FILE_ORIGINAL_INFO
return newProperty
}
class StaticVersionCatalogProperty(place: PsiElement, name: String, val clazz: PsiClass) : GroovyPropertyBase(name, place) {
override fun getPropertyType(): PsiType {
return PsiElementFactory.getInstance(project).createType(clazz, PsiSubstitutor.EMPTY)
}
}
fun processPropertiesFromCatalog(name: String?, place: PsiElement, processor: PsiScopeProcessor, state: ResolveState) : Set<String>? {
if (name == null) {
// this case is possible when only a part of a catalog name is written and autocomplete is triggered
return processAllCatalogsOfBuild(place, processor, state)
}
val accessor = getVersionCatalogAccessor(place, name) ?: return emptySet()
val element = StaticVersionCatalogProperty(place, name, accessor)
if (!processor.execute(element, state)) {
return null // to stop processing
}
return setOf(name)
}
fun getExtensionsFor(psiElement: PsiElement): GradleExtensionsData? {
val project = psiElement.project
val virtualFile = psiElement.containingFile?.originalFile?.virtualFile ?: return null
val module = ProjectFileIndex.getInstance(project).getModuleForFile(virtualFile)
return GradleExtensionsSettings.getInstance(project).getExtensionsFor(module)
}
internal const val PROPERTIES_FILE_ORIGINAL_INFO : String = "by gradle.properties"
private fun processAllCatalogsOfBuild(place: PsiElement, processor: PsiScopeProcessor, state: ResolveState): Set<String>? {
val catalogNameToAccessor: Map<String, PsiClass> = getAccessorsForAllCatalogs(place)
catalogNameToAccessor.forEach { (catalogName, accessor) ->
if (!processor.execute(StaticVersionCatalogProperty(place, catalogName, accessor), state)) {
return null // to stop processing
}
}
return catalogNameToAccessor.keys
}
}
}