From 019151b7adfc4c3a92f17cf1fc759fc6adcf108b Mon Sep 17 00:00:00 2001 From: Konstantin Nisht Date: Thu, 24 Nov 2022 22:43:08 +0300 Subject: [PATCH] [gradle] Enable find usages and references in version catalog toml files GitOrigin-RevId: 093c10b45b965b1167a1d766a7f67c2c2ac1e22b --- .../java/intellij.gradle.java.tests.iml | 1 + .../META-INF/gradle-toml-integration.xml | 6 +- .../config/GradleBuildscriptSearchScope.kt | 21 +++ .../java/src/config/GradleUseScopeEnlarger.kt | 9 +- .../GradleVersionCatalogFindUsagesFactory.kt | 20 +++ .../GradleVersionCatalogFindUsagesHandler.kt | 38 ++++ .../VersionCatalogReferenceContributor.kt | 67 +++++++ plugins/gradle/java/src/toml/util.kt | 166 ++++++++++++++++++ .../testSources/dsl/GradleCompletionTest.kt | 2 +- .../GradleVersionCatalogResolveTest.kt | 165 +++++++++++++++++ .../GradleVersionCatalogsFindUsagesTest.kt | 92 ++++++++++ .../GradleCodeInsightTestCase.kt | 11 +- 12 files changed, 581 insertions(+), 17 deletions(-) create mode 100644 plugins/gradle/java/src/config/GradleBuildscriptSearchScope.kt create mode 100644 plugins/gradle/java/src/toml/findUsages/GradleVersionCatalogFindUsagesFactory.kt create mode 100644 plugins/gradle/java/src/toml/findUsages/GradleVersionCatalogFindUsagesHandler.kt create mode 100644 plugins/gradle/java/src/toml/navigation/VersionCatalogReferenceContributor.kt create mode 100644 plugins/gradle/java/src/toml/util.kt create mode 100644 plugins/gradle/java/testSources/dsl/versionCatalogs/GradleVersionCatalogResolveTest.kt create mode 100644 plugins/gradle/java/testSources/dsl/versionCatalogs/GradleVersionCatalogsFindUsagesTest.kt diff --git a/plugins/gradle/java/intellij.gradle.java.tests.iml b/plugins/gradle/java/intellij.gradle.java.tests.iml index f16699052543..c49e0b91e424 100644 --- a/plugins/gradle/java/intellij.gradle.java.tests.iml +++ b/plugins/gradle/java/intellij.gradle.java.tests.iml @@ -30,6 +30,7 @@ + \ No newline at end of file diff --git a/plugins/gradle/java/resources/META-INF/gradle-toml-integration.xml b/plugins/gradle/java/resources/META-INF/gradle-toml-integration.xml index e0423f4c3f04..28959fcd834e 100644 --- a/plugins/gradle/java/resources/META-INF/gradle-toml-integration.xml +++ b/plugins/gradle/java/resources/META-INF/gradle-toml-integration.xml @@ -2,9 +2,9 @@ - - - + + diff --git a/plugins/gradle/java/src/config/GradleBuildscriptSearchScope.kt b/plugins/gradle/java/src/config/GradleBuildscriptSearchScope.kt new file mode 100644 index 000000000000..92c8fe059e1f --- /dev/null +++ b/plugins/gradle/java/src/config/GradleBuildscriptSearchScope.kt @@ -0,0 +1,21 @@ +// 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.config + +import com.intellij.openapi.module.Module +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.search.GlobalSearchScope +import org.jetbrains.plugins.gradle.codeInspection.GradleInspectionBundle +import org.jetbrains.plugins.gradle.util.GradleConstants + +class GradleBuildscriptSearchScope(project: Project) : GlobalSearchScope(project) { + override fun contains(file: VirtualFile): Boolean { + return GradleConstants.EXTENSION == file.extension || file.name.endsWith(GradleConstants.KOTLIN_DSL_SCRIPT_EXTENSION) + } + override fun isSearchInModuleContent(aModule: Module) = true + override fun isSearchInLibraries() = false + + override fun getDisplayName(): String { + return GradleInspectionBundle.message("gradle.buildscript.search.scope") + } +} \ No newline at end of file diff --git a/plugins/gradle/java/src/config/GradleUseScopeEnlarger.kt b/plugins/gradle/java/src/config/GradleUseScopeEnlarger.kt index 4e3f1504e6cb..e054ca0c3f02 100644 --- a/plugins/gradle/java/src/config/GradleUseScopeEnlarger.kt +++ b/plugins/gradle/java/src/config/GradleUseScopeEnlarger.kt @@ -14,7 +14,6 @@ import com.intellij.psi.PsiElement import com.intellij.psi.PsiElementFinder import com.intellij.psi.PsiMember import com.intellij.psi.PsiReference -import com.intellij.psi.search.GlobalSearchScope import com.intellij.psi.search.SearchScope import com.intellij.psi.search.UseScopeEnlarger import com.intellij.psi.search.searches.ReferencesSearch @@ -45,13 +44,7 @@ class GradleUseScopeEnlarger : UseScopeEnlarger() { if (!isInBuildSrc(project, virtualFile) && !isInGradleDistribution(project, virtualFile)) return null - return object : GlobalSearchScope(element.project) { - override fun contains(file: VirtualFile): Boolean { - return GradleConstants.EXTENSION == file.extension || file.name.endsWith(GradleConstants.KOTLIN_DSL_SCRIPT_EXTENSION) - } - override fun isSearchInModuleContent(aModule: Module) = true - override fun isSearchInLibraries() = false - } + return GradleBuildscriptSearchScope(element.project) } private fun isInGradleDistribution(project: Project, file: VirtualFile) : Boolean { diff --git a/plugins/gradle/java/src/toml/findUsages/GradleVersionCatalogFindUsagesFactory.kt b/plugins/gradle/java/src/toml/findUsages/GradleVersionCatalogFindUsagesFactory.kt new file mode 100644 index 000000000000..e3d6b381a55c --- /dev/null +++ b/plugins/gradle/java/src/toml/findUsages/GradleVersionCatalogFindUsagesFactory.kt @@ -0,0 +1,20 @@ +// 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.toml.findUsages + +import com.intellij.find.findUsages.FindUsagesHandler +import com.intellij.find.findUsages.FindUsagesHandlerFactory +import com.intellij.psi.PsiElement +import org.toml.lang.psi.TomlKeySegment + +class GradleVersionCatalogFindUsagesFactory : FindUsagesHandlerFactory() { + override fun canFindUsages(element: PsiElement): Boolean { + return element is TomlKeySegment + } + + override fun createFindUsagesHandler(element: PsiElement, forHighlightUsages: Boolean): FindUsagesHandler { + if (forHighlightUsages) { + return FindUsagesHandler.NULL_HANDLER + } + return GradleVersionCatalogFindUsagesHandler(element as TomlKeySegment) + } +} \ No newline at end of file diff --git a/plugins/gradle/java/src/toml/findUsages/GradleVersionCatalogFindUsagesHandler.kt b/plugins/gradle/java/src/toml/findUsages/GradleVersionCatalogFindUsagesHandler.kt new file mode 100644 index 000000000000..a8813728d16c --- /dev/null +++ b/plugins/gradle/java/src/toml/findUsages/GradleVersionCatalogFindUsagesHandler.kt @@ -0,0 +1,38 @@ +// 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.toml.findUsages + +import com.intellij.find.findUsages.FindUsagesHandler +import com.intellij.find.findUsages.FindUsagesOptions +import com.intellij.openapi.actionSystem.DataContext +import com.intellij.openapi.module.Module +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.PsiElement +import com.intellij.psi.search.GlobalSearchScope +import org.jetbrains.plugins.gradle.codeInspection.GradleInspectionBundle +import org.jetbrains.plugins.gradle.config.GradleBuildscriptSearchScope +import org.toml.lang.psi.TomlKeySegment + +class GradleVersionCatalogFindUsagesHandler(private val tomlElement: TomlKeySegment) : FindUsagesHandler(tomlElement) { + + override fun getFindUsagesOptions(dataContext: DataContext?): FindUsagesOptions { + val superOptions = super.getFindUsagesOptions(dataContext) + superOptions.searchScope = VersionCatalogSearchScope(tomlElement) + return superOptions + } + + private class VersionCatalogSearchScope(context: PsiElement) : GlobalSearchScope(context.project) { + private val buildscriptScope = GradleBuildscriptSearchScope(context.project) + private val tomlScope = fileScope(context.containingFile) + + override fun contains(file: VirtualFile): Boolean = buildscriptScope.contains(file) || tomlScope.contains(file) + + override fun isSearchInModuleContent(aModule: Module): Boolean = buildscriptScope.isSearchInModuleContent(aModule) + + override fun isSearchInLibraries(): Boolean = buildscriptScope.isSearchInLibraries + + override fun getDisplayName(): String { + return GradleInspectionBundle.message("gradle.version.catalog.search.scope") + } + } + +} \ No newline at end of file diff --git a/plugins/gradle/java/src/toml/navigation/VersionCatalogReferenceContributor.kt b/plugins/gradle/java/src/toml/navigation/VersionCatalogReferenceContributor.kt new file mode 100644 index 000000000000..0f082fa5c957 --- /dev/null +++ b/plugins/gradle/java/src/toml/navigation/VersionCatalogReferenceContributor.kt @@ -0,0 +1,67 @@ +// 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.toml.navigation + +import com.intellij.openapi.util.text.StringUtil +import com.intellij.patterns.ElementPattern +import com.intellij.patterns.PatternCondition +import com.intellij.patterns.PlatformPatterns +import com.intellij.patterns.PsiElementPattern +import com.intellij.psi.* +import com.intellij.util.ProcessingContext +import com.intellij.util.asSafely +import org.jetbrains.plugins.gradle.toml.getVersions +import org.toml.lang.psi.TomlInlineTable +import org.toml.lang.psi.TomlKeyValue +import org.toml.lang.psi.TomlLiteral +import org.toml.lang.psi.TomlValue + +class VersionCatalogReferenceContributor : PsiReferenceContributor() { + + override fun registerReferenceProviders(registrar: PsiReferenceRegistrar) { + registrar.registerReferenceProvider(versionRefPattern, VersionCatalogReferenceProvider()) + } +} + +internal val refKeyValuePattern: PsiElementPattern.Capture = PlatformPatterns + .psiElement(TomlKeyValue::class.java) + .with(RefPatternCondition()) +internal val versionRefPattern: ElementPattern = PlatformPatterns + .psiElement(TomlValue::class.java) + .withParent(refKeyValuePattern) + +private class RefPatternCondition : PatternCondition("'ref' key value") { + override fun accepts(t: TomlKeyValue, context: ProcessingContext?): Boolean { + val segments = t.key.segments + if (segments.isEmpty()) { + return false + } + if (segments.last().name != "ref") { + return false + } + if (segments.size == 1) { + return t.parent?.asSafely()?.parent?.asSafely() + ?.takeIf { it.key.segments.lastOrNull()?.name == "version" } != null + } else { + return segments.asReversed()[1].name == "version" + } + } + +} + +private class VersionCatalogReferenceProvider : PsiReferenceProvider() { + override fun getReferencesByElement(element: PsiElement, context: ProcessingContext): Array { + if (element !is TomlLiteral) { + return emptyArray() + } + val text = StringUtil.unquoteString(element.text) + return arrayOf(VersionCatalogVersionReference(element, text)) + } + + private class VersionCatalogVersionReference(literal: TomlLiteral, val text: String) : PsiReferenceBase(literal) { + override fun resolve(): PsiElement? { + val versions = getVersions(element) + return versions.find { it.name == text } + } + } + +} \ No newline at end of file diff --git a/plugins/gradle/java/src/toml/util.kt b/plugins/gradle/java/src/toml/util.kt new file mode 100644 index 000000000000..67b8843555b8 --- /dev/null +++ b/plugins/gradle/java/src/toml/util.kt @@ -0,0 +1,166 @@ +// 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.toml + +import com.intellij.openapi.components.service +import com.intellij.openapi.roots.ProjectFileIndex +import com.intellij.openapi.util.text.StringUtil +import com.intellij.openapi.vfs.VfsUtil +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.* +import com.intellij.psi.util.childrenOfType +import com.intellij.util.asSafely +import com.intellij.util.containers.tail +import org.jetbrains.plugins.gradle.service.resolve.GradleCommonClassNames +import org.jetbrains.plugins.gradle.service.resolve.VersionCatalogsLocator +import org.jetbrains.plugins.gradle.service.resolve.getVersionCatalogFiles +import org.jetbrains.plugins.groovy.lang.psi.util.GroovyPropertyUtils +import org.toml.lang.psi.* +import org.toml.lang.psi.ext.name + +internal fun getVersions(context: PsiElement) : List { + val file = context.containingFile.asSafely() ?: return emptyList() + val targetTable = file.childrenOfType().find { it.header.key?.name == "versions" } ?: return emptyList() + return targetTable.childrenOfType().mapNotNull { it.key.segments.singleOrNull() } +} + + +internal fun String.getVersionCatalogParts() : List = split("_", "-") + +internal fun findTomlFile(context: PsiElement, name: String) : TomlFile? { + val file = getVersionCatalogFiles(context.project)[name] ?: return null + return PsiManager.getInstance(context.project).findFile(file)?.asSafely() +} + +private fun findTomlFileDynamically(context: PsiElement, name: String): VirtualFile? { + val module = context.containingFile?.originalFile?.virtualFile?.let { + ProjectFileIndex.getInstance(context.project).getModuleForFile(it) + } ?: return null + val tomlPath = context.project.service().getVersionCatalogsForModule(module)[name] ?: return null + return VfsUtil.findFile(tomlPath, false) +} + + + +/** + * @param method a method within a synthetic version catalog accessor class. The method must not return an accessor (i.e. it should be a leaf method). + * @param context a context element within any gradle buildscript. + * @return an element within TOML file, that describes [method] + */ +fun findOriginInTomlFile(method: PsiMethod, context: PsiElement): PsiElement? { + val containingClasses = mutableListOf(method.containingClass ?: return null) + while (containingClasses.last().containingClass != null) { + containingClasses.add(containingClasses.last().containingClass!!) + } + containingClasses.reverse() + val name = containingClasses.first().name?.substringAfter(LIBRARIES_FOR_PREFIX) ?: return null + val toml = listOf(StringUtil.decapitalize(name), name).firstNotNullOfOrNull { findTomlFile(context, it) } + ?: return null + val tomlVisitor = TomlVersionCatalogVisitor(containingClasses.tail(), method) + toml.accept(tomlVisitor) + return tomlVisitor.resolveTarget +} + +private class TomlVersionCatalogVisitor(containingClasses: List, val targetMethod: PsiMethod) : TomlRecursiveVisitor() { + private val containingClasses: MutableList = ArrayList(containingClasses) + var resolveTarget: PsiElement? = null + + override fun visitTable(element: TomlTable) { + val headerKind = element.header.key?.segments?.singleOrNull()?.name?.getTomlHeaderKind() ?: return + val firstClass = containingClasses.firstOrNull() + if (firstClass != null) { + val firstClassKind = firstClass.getTomlHeaderKind() ?: return + if (headerKind != firstClassKind) { + return + } + if (targetMethod.returnType?.asSafely()?.resolve()?.qualifiedName != GradleCommonClassNames.GRADLE_API_PROVIDER_PROVIDER) { + return + } + return resolveAsComponent(element.entries) + } + else { + when (targetMethod.name) { + METHOD_GET_PLUGINS -> if (headerKind == TomlHeaderKind.PLUGINS) resolveTarget = element else return + METHOD_GET_BUNDLES -> if (headerKind == TomlHeaderKind.BUNDLES) resolveTarget = element else return + METHOD_GET_VERSIONS -> if (headerKind == TomlHeaderKind.VERSIONS) resolveTarget = element else return + else -> if (headerKind == TomlHeaderKind.LIBRARIES) resolveAsComponent(element.entries) else return + } + } + } + + private fun resolveAsComponent(values: List) { + val camelCasedName = getCapitalizedAccessorName(targetMethod) + for (tomlEntry in values) { + val keyName = + tomlEntry.key.segments.firstOrNull()?.name?.getVersionCatalogParts()?.joinToString("", transform = StringUtil::capitalize) + ?: continue + if (camelCasedName == keyName) { + resolveTarget = tomlEntry + return + } + } + } +} + +internal fun getCapitalizedAccessorName(method: PsiMethod): String? { + val propertyName = GroovyPropertyUtils.getPropertyName(method) ?: return null + val methodFinalPart = GroovyPropertyUtils.capitalize(propertyName) + val methodParts = method.containingClass?.takeUnless { it.name?.startsWith( + LIBRARIES_FOR_PREFIX) == true }?.name?.trimAccessorName() + return (methodParts ?: "") + methodFinalPart +} + + +private fun String.trimAccessorName(): String { + for (suffix in listOf(BUNDLE_ACCESSORS_SUFFIX, + LIBRARY_ACCESSORS_SUFFIX, + PLUGIN_ACCESSORS_SUFFIX, + VERSION_ACCESSORS_SUFFIX)) { + if (endsWith(suffix)) return substringBeforeLast(suffix) + } + return this +} + + +private enum class TomlHeaderKind { + VERSIONS, + BUNDLES, + LIBRARIES, + PLUGINS +} + +private fun PsiClass.getTomlHeaderKind(): TomlHeaderKind? { + val name = name ?: return null + return when { + name.endsWith(VERSION_ACCESSORS_SUFFIX) -> TomlHeaderKind.VERSIONS + name.endsWith(BUNDLE_ACCESSORS_SUFFIX) -> TomlHeaderKind.BUNDLES + name.endsWith(PLUGIN_ACCESSORS_SUFFIX) -> TomlHeaderKind.PLUGINS + name.endsWith(LIBRARY_ACCESSORS_SUFFIX) -> TomlHeaderKind.LIBRARIES + else -> null + } +} + +private fun String.getTomlHeaderKind(): TomlHeaderKind? = + when (this) { + TOML_TABLE_VERSIONS -> TomlHeaderKind.VERSIONS + TOML_TABLE_LIBRARIES -> TomlHeaderKind.LIBRARIES + TOML_TABLE_BUNDLES -> TomlHeaderKind.BUNDLES + TOML_TABLE_PLUGINS -> TomlHeaderKind.PLUGINS + else -> null + } + +private const val TOML_TABLE_VERSIONS = "versions" +private const val TOML_TABLE_LIBRARIES = "libraries" +private const val TOML_TABLE_BUNDLES = "bundles" +private const val TOML_TABLE_PLUGINS = "plugins" + +private const val METHOD_GET_PLUGINS = "getPlugins" +private const val METHOD_GET_VERSIONS = "getVersions" +private const val METHOD_GET_BUNDLES = "getBundles" + + +internal const val BUNDLE_ACCESSORS_SUFFIX = "BundleAccessors" +internal const val LIBRARY_ACCESSORS_SUFFIX = "LibraryAccessors" +internal const val PLUGIN_ACCESSORS_SUFFIX = "PluginAccessors" +internal const val VERSION_ACCESSORS_SUFFIX = "VersionAccessors" + +internal const val LIBRARIES_FOR_PREFIX = "LibrariesFor" diff --git a/plugins/gradle/java/testSources/dsl/GradleCompletionTest.kt b/plugins/gradle/java/testSources/dsl/GradleCompletionTest.kt index d7d6e104572e..44af28881116 100644 --- a/plugins/gradle/java/testSources/dsl/GradleCompletionTest.kt +++ b/plugins/gradle/java/testSources/dsl/GradleCompletionTest.kt @@ -31,7 +31,7 @@ class GradleCompletionTest : GradleCodeInsightTestCase() { @BaseGradleVersionSource fun testGrayOutForeignCompletionElement(gradleVersion: GradleVersion) { testJavaProject(gradleVersion) { - testCompletion("repositories { mavenCentral { go } }") { + testCompletion("build.gradle", "repositories { mavenCentral { go } }") { var hasGoogle = false for (element in it) { if (element.lookupString != "google") { diff --git a/plugins/gradle/java/testSources/dsl/versionCatalogs/GradleVersionCatalogResolveTest.kt b/plugins/gradle/java/testSources/dsl/versionCatalogs/GradleVersionCatalogResolveTest.kt new file mode 100644 index 000000000000..7b9243ddf717 --- /dev/null +++ b/plugins/gradle/java/testSources/dsl/versionCatalogs/GradleVersionCatalogResolveTest.kt @@ -0,0 +1,165 @@ +// 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.dsl.versionCatalogs + +import com.intellij.psi.util.parentOfType +import org.gradle.util.GradleVersion +import org.jetbrains.plugins.gradle.testFramework.GradleCodeInsightTestCase +import org.jetbrains.plugins.gradle.testFramework.GradleTestFixtureBuilder +import org.jetbrains.plugins.gradle.testFramework.annotations.BaseGradleVersionSource +import org.jetbrains.plugins.gradle.testFramework.util.withSettingsFile +import org.jetbrains.plugins.groovy.lang.psi.GroovyPsiElement +import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrMethodCall +import org.junit.jupiter.api.Assertions.assertInstanceOf +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.params.ParameterizedTest +import org.toml.lang.psi.TomlFile +import org.toml.lang.psi.TomlKeyValue +import org.toml.lang.psi.TomlTable + +class GradleVersionCatalogsResolveTest : GradleCodeInsightTestCase() { + + @ParameterizedTest + @BaseGradleVersionSource + fun testNavigationToTomlFile(gradleVersion: GradleVersion) { + test(gradleVersion, BASE_VERSION_CATALOG_FIXTURE) { + testGotoDefinition("libs") { + assertInstanceOf(TomlFile::class.java, it) + assertTrue((it as TomlFile).name == "libs.versions.toml") + } + } + } + + @ParameterizedTest + @BaseGradleVersionSource + fun testNavigationToTomlFile2(gradleVersion: GradleVersion) { + test(gradleVersion, BASE_VERSION_CATALOG_FIXTURE) { + testGotoDefinition("libs2") { + assertInstanceOf(GroovyPsiElement::class.java, it) + assertTrue(it.parentOfType(true)!!.resolveMethod()!!.name == "libs2") + } + } + } + + @ParameterizedTest + @BaseGradleVersionSource + fun testNavigationToCustomCatalog(gradleVersion: GradleVersion) { + test(gradleVersion, BASE_VERSION_CATALOG_FIXTURE) { + testGotoDefinition("libs3") { + assertInstanceOf(GroovyPsiElement::class.java, it) + assertTrue(it.parentOfType(true)!!.resolveMethod()!!.name == "libs3") + } + } + } + + @ParameterizedTest + @BaseGradleVersionSource + fun testNavigationToTomlEntry(gradleVersion: GradleVersion) { + test(gradleVersion, BASE_VERSION_CATALOG_FIXTURE) { + testGotoDefinition("libs.groovy.core") { + assertInstanceOf(TomlKeyValue::class.java, it) + assertTrue((it as TomlKeyValue).key.text == "groovy-core") + } + } + } + + @ParameterizedTest + @BaseGradleVersionSource + fun testNavigationToTomlEntry2(gradleVersion: GradleVersion) { + test(gradleVersion, BASE_VERSION_CATALOG_FIXTURE) { + testGotoDefinition("libs.groovy.core") { + assertInstanceOf(TomlKeyValue::class.java, it) + assertTrue((it as TomlKeyValue).key.text == "groovy-core") + } + } + } + + @ParameterizedTest + @BaseGradleVersionSource + fun testNavigationToTomlTable(gradleVersion: GradleVersion) { + test(gradleVersion, BASE_VERSION_CATALOG_FIXTURE) { + testGotoDefinition("libs.bundles") { + assertInstanceOf(TomlTable::class.java, it) + assertTrue((it as TomlTable).header.text == "[bundles]") + } + } + } + + @ParameterizedTest + @BaseGradleVersionSource + fun testNavigationToLibraryInSettings(gradleVersion: GradleVersion) { + test(gradleVersion, BASE_VERSION_CATALOG_FIXTURE) { + testGotoDefinition("libs3.foo.bar.baz") { + assertInstanceOf(GroovyPsiElement::class.java, it) + assertTrue(it.parentOfType(true)!!.argumentList.expressionArguments[0].text == "\"foo.bar.baz\"") + } + } + } + + @ParameterizedTest + @BaseGradleVersionSource + fun testNavigationToLibraryInSettings2(gradleVersion: GradleVersion) { + test(gradleVersion, BASE_VERSION_CATALOG_FIXTURE) { + testGotoDefinition("libs3.foo.nn.mm") { + assertInstanceOf(GroovyPsiElement::class.java, it) + assertTrue(it.parentOfType(true)!!.argumentList.expressionArguments[0].text == "\"foo-nn-mm\"") + } + } + } + + @ParameterizedTest + @BaseGradleVersionSource + fun testNavigationWithCapitalLetters(gradleVersion: GradleVersion) { + test(gradleVersion, BASE_VERSION_CATALOG_FIXTURE) { + testGotoDefinition("libs2.getCheck().getCapital().getLetter()") { + assertInstanceOf(TomlKeyValue::class.java, it) + assertTrue((it as TomlKeyValue).key.text == "check-Capital-Letter") + } + } + } + + companion object { + private val BASE_VERSION_CATALOG_FIXTURE = GradleTestFixtureBuilder + .create("GradleVersionCatalogs-completion") { + withSettingsFile { + setProjectName("GradleVersionCatalogs-completion") + enableFeaturePreview("VERSION_CATALOGS") + addCode(""" + dependencyResolutionManagement { + versionCatalogs { + libs2 { + from(files("gradle/my.toml")) + } + libs3 { + library("foo.bar.baz", "org.apache.groovy:groovy:4.0.0") + library("foo-nn-mm", "org.apache.groovy:groovy:4.0.0") + } + } + } + """.trimIndent()) + } + withFile("gradle/libs.versions.toml", /* language=TOML */ """ + [versions] + groovy = "3.0.5" + checkstyle = "8.37" + + [libraries] + groovy-core = { module = "org.codehaus.groovy:groovy", version.ref = "groovy" } + groovy-json = { module = "org.codehaus.groovy:groovy-json", version.ref = "groovy" } + groovy-nio = { module = "org.codehaus.groovy:groovy-nio", version.ref = "groovy" } + commons-lang3 = { group = "org.apache.commons", name = "commons-lang3", version = { strictly = "[3.8, 4.0[", prefer="3.9" } } + + [bundles] + groovy = ["groovy-core", "groovy-json", "groovy-nio"] + + [plugins] + jmh = { id = "me.champeau.jmh", version = "0.6.5" } + """.trimIndent()) + withFile("gradle/my.toml", /* language=TOML */ """ + [libraries] + aa-bb-cc = { module = "org.apache.groovy:groovy", version = "4.0.0" } + check-Capital-Letter = { module = "org.apache.groovy:groovy", version = "4.0.0" } + """.trimIndent()) + } + } + +} \ No newline at end of file diff --git a/plugins/gradle/java/testSources/dsl/versionCatalogs/GradleVersionCatalogsFindUsagesTest.kt b/plugins/gradle/java/testSources/dsl/versionCatalogs/GradleVersionCatalogsFindUsagesTest.kt new file mode 100644 index 000000000000..634459b6be07 --- /dev/null +++ b/plugins/gradle/java/testSources/dsl/versionCatalogs/GradleVersionCatalogsFindUsagesTest.kt @@ -0,0 +1,92 @@ +// 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.dsl.versionCatalogs + +import com.intellij.psi.PsiReference +import com.intellij.psi.search.searches.ReferencesSearch +import com.intellij.testFramework.runInEdtAndWait +import org.gradle.util.GradleVersion +import org.jetbrains.plugins.gradle.testFramework.GradleCodeInsightTestCase +import org.jetbrains.plugins.gradle.testFramework.GradleTestFixtureBuilder +import org.jetbrains.plugins.gradle.testFramework.annotations.BaseGradleVersionSource +import org.jetbrains.plugins.gradle.testFramework.util.withSettingsFile +import org.jetbrains.plugins.groovy.lang.psi.GroovyFileBase +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.params.ParameterizedTest +import org.toml.lang.psi.TomlFile + +class GradleVersionCatalogsFindUsagesTest : GradleCodeInsightTestCase() { + + private fun testVersionCatalogFindUsages(version: GradleVersion, versionCatalogText: String, buildGradleText: String, + checker: (Collection) -> Unit) { + checkCaret(versionCatalogText) + test(version, BASE_VERSION_CATALOG_FIXTURE) { + val versionCatalog = findOrCreateFile("gradle/libs.versions.toml", versionCatalogText) + findOrCreateFile("build.gradle", buildGradleText) + runInEdtAndWait { + codeInsightFixture.configureFromExistingVirtualFile(versionCatalog) + val elementAtCaret = codeInsightFixture.elementAtCaret + assertNotNull(elementAtCaret) + val usages = ReferencesSearch.search(elementAtCaret).findAll() + checker(usages) + } + } + + } + + @ParameterizedTest + @BaseGradleVersionSource + fun testHasUsages(gradleVersion: GradleVersion) { + testVersionCatalogFindUsages(gradleVersion, """ + [libraries] + groovy-core = "org.codehaus.groovy:groovy:2.7.3" + """.trimIndent(), """ + libs.groovy.core + """.trimIndent()) { + assert(it.isNotEmpty()) + } + } + + @ParameterizedTest + @BaseGradleVersionSource + fun testHasNoUsages(gradleVersion: GradleVersion) { + testVersionCatalogFindUsages(gradleVersion, """ + [versions] + foo = "4.7.6" + [libraries] + groovy-core = "org.codehaus.groovy:groovy:2.7.3" + aaa-bbb = { group = "org.apache.groovy", name = "groovy", version.ref = "groovy" } + """.trimIndent(), """ + libs.groovy + """.trimIndent()) { + assert(it.isEmpty()) + } + } + + @ParameterizedTest + @BaseGradleVersionSource + fun testFindUsagesOfVersion(gradleVersion: GradleVersion) { + testVersionCatalogFindUsages(gradleVersion, """ + [versions] + foo = "4.7.6" + [libraries] + aaa-bbb = { group = "org.apache.groovy", name = "groovy", version.ref = "foo" } + """.trimIndent(), """ + libs.versions.foo + """.trimIndent()) { refs -> + assertNotNull(refs.find { it.element.containingFile is GroovyFileBase }) + assertNotNull(refs.find { it.element.containingFile is TomlFile }) + } + } + + + companion object { + private val BASE_VERSION_CATALOG_FIXTURE = GradleTestFixtureBuilder + .create("GradleVersionCatalogs-find-usages-bare") { + withSettingsFile { + setProjectName("GradleVersionCatalogs-find-usages-bare") + enableFeaturePreview("VERSION_CATALOGS") + } + } + } + +} \ No newline at end of file diff --git a/plugins/gradle/testSources/org/jetbrains/plugins/gradle/testFramework/GradleCodeInsightTestCase.kt b/plugins/gradle/testSources/org/jetbrains/plugins/gradle/testFramework/GradleCodeInsightTestCase.kt index 60599bb9b965..4daa6a9c455e 100644 --- a/plugins/gradle/testSources/org/jetbrains/plugins/gradle/testFramework/GradleCodeInsightTestCase.kt +++ b/plugins/gradle/testSources/org/jetbrains/plugins/gradle/testFramework/GradleCodeInsightTestCase.kt @@ -30,7 +30,7 @@ abstract class GradleCodeInsightTestCase : GradleCodeInsightBaseTestCase(), Expr } } - private fun checkCaret(expression: String) { + protected fun checkCaret(expression: String) { assertTrue("" in expression, "Please define caret position in build script.") } @@ -59,20 +59,21 @@ abstract class GradleCodeInsightTestCase : GradleCodeInsightBaseTestCase(), Expr } } - fun testCompletion(expression: String, checker: (Array) -> Unit) { + fun testCompletion(fileName: String, expression: String, checker: (Array) -> Unit) { checkCaret(expression) - val file = findOrCreateFile("build.gradle", expression) + val file = findOrCreateFile(fileName, expression) runInEdtAndWait { codeInsightFixture.configureFromExistingVirtualFile(file) checker(codeInsightFixture.completeBasic()) } } - fun testCompletion(expression: String, vararg completionCandidates: String) = testCompletion(expression) { + fun testCompletion(expression: String, vararg completionCandidates: String, orderDependent: Boolean = true) = testCompletion("build.gradle", expression) { val lookup = listOf(*it) var startIndex = 0 for (candidate in completionCandidates) { - val newIndex = lookup.subList(startIndex, lookup.size).indexOfFirst { it.lookupString == candidate } + val fromIndex = if (orderDependent) startIndex else 0 + val newIndex = lookup.subList(fromIndex, lookup.size).indexOfFirst { it.lookupString == candidate } assertTrue(newIndex != -1, "Element '$candidate' must be in the lookup") startIndex = newIndex + 1 }