[kotlin] Implement 'AddKotlinTestQuickFixProvider' alongside the 'AddKotlinCoroutinesQuickFixProvider'

^KTIJ-23814 Verification Pending
^KTIJ-31125 Verification Pending

GitOrigin-RevId: a79570cebf7c77dc5904177c4e034e589ae0a554
This commit is contained in:
Sebastian Sellmair
2024-09-16 16:11:05 +00:00
committed by intellij-monorepo-bot
parent 5cf2c1c66b
commit 3c9dfaea46
31 changed files with 470 additions and 147 deletions
@@ -1750,6 +1750,7 @@ put.expressions.on.separate.lines=Put expressions on separate lines
put.calls.on.separate.lines=Put calls on separate lines
add.kotlin.coroutines=Add Kotlin coroutines library
add.kotlin.coroutines.description=Add Kotlin Coroutines Library
add.0.library=Add ''{0}'' library
demorgan.law=DeMorgan Law
replace.with.end.of.line.comment=Replace with end of line comment
convert.collection.constructor.to.function=Convert Collection constructor to function
@@ -53,5 +53,6 @@
<orderEntry type="module" module-name="kotlin.base.facet" />
<orderEntry type="module" module-name="intellij.platform.core" />
<orderEntry type="module" module-name="kotlin.base.analysis" />
<orderEntry type="module" module-name="kotlin.base.indices" />
</component>
</module>
@@ -334,6 +334,10 @@
<codeInsight.unresolvedReferenceQuickFixProvider
implementation="org.jetbrains.kotlin.idea.codeInsight.inspections.shared.libraries.AddKotlinCoroutinesQuickFixProvider"/>
<codeInsight.unresolvedReferenceQuickFixProvider
implementation="org.jetbrains.kotlin.idea.codeInsight.inspections.shared.libraries.AddKotlinTestLibraryQuickFixProvider"/>
<localInspection implementationClass="org.jetbrains.kotlin.idea.codeInsight.inspections.shared.InfixCallToOrdinaryInspection"
groupPath="Kotlin"
groupBundle="messages.KotlinBundle" groupKey="group.names.code.migration"
@@ -1,17 +0,0 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.kotlin.idea.codeInsight.inspections.shared.libraries
import com.intellij.openapi.module.Module
import com.intellij.openapi.module.impl.scopes.ModulesScope
import com.intellij.psi.JavaPsiFacade
internal class AddKotlinCoroutinesQuickFixProvider : SimpleAddKotlinLibraryQuickFixProvider(
libraryGroupId = "org.jetbrains.kotlinx",
libraryArtifactId = "kotlinx-coroutines-core",
namesToCheck = setOf("runBlocking", "CoroutineScope")
) {
override fun hasLibrary(module: Module): Boolean {
val scope = ModulesScope.moduleWithDependenciesAndLibrariesScope(module)
return JavaPsiFacade.getInstance(module.project).findClasses("kotlinx.coroutines.CoroutineScope", scope).isNotEmpty()
}
}
@@ -4,6 +4,9 @@ package org.jetbrains.kotlin.idea.codeInsight.inspections.shared.libraries
import com.intellij.codeInsight.intention.HighPriorityAction
import com.intellij.codeInsight.intention.IntentionAction
import com.intellij.codeInspection.util.IntentionName
import com.intellij.openapi.application.Application
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.writeAction
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.project.Project
@@ -21,18 +24,22 @@ internal class AddKotlinLibraryQuickFix(
) : IntentionAction, HighPriorityAction {
override fun getText(): String = quickFixText
override fun getFamilyName(): String = quickFixText
override fun startInWriteAction(): Boolean = true
override fun startInWriteAction(): Boolean = false
override fun isAvailable(project: Project, editor: Editor?, file: PsiFile?): Boolean {
val module = file?.module ?: return false
return dependencyManager.isApplicable(module) && !dependencyManager.isProjectSyncPendingOrInProgress()
}
override fun invoke(project: Project, editor: Editor?, file: PsiFile?) {
if (editor == null || file == null) return
if (editor == null && !ApplicationManager.getApplication().isHeadlessEnvironment) return
if (file == null) return
val psiFile = file.originalFile
val module = psiFile.module ?: return
dependencyManager.addDependency(module, libraryDescriptor)
ApplicationManager.getApplication().runWriteAction {
dependencyManager.addDependency(module, libraryDescriptor)
}
dependencyManager.startProjectSync()
val buildScriptFile = dependencyManager.getBuildScriptFile(module)
@@ -3,57 +3,76 @@ package org.jetbrains.kotlin.idea.codeInsight.inspections.shared.libraries
import com.intellij.codeInsight.daemon.QuickFixActionRegistrar
import com.intellij.codeInsight.quickfix.UnresolvedReferenceQuickFixProvider
import com.intellij.codeInspection.util.IntentionName
import com.intellij.openapi.module.Module
import com.intellij.openapi.module.impl.scopes.ModulesScope
import com.intellij.openapi.roots.DependencyScope
import com.intellij.openapi.roots.ExternalLibraryDescriptor
import com.intellij.openapi.roots.ProjectFileIndex
import com.intellij.psi.JavaPsiFacade
import com.intellij.psi.PsiReference
import org.jetbrains.kotlin.config.toKotlinVersion
import org.jetbrains.kotlin.idea.base.projectStructure.languageVersionSettings
import org.jetbrains.kotlin.idea.base.resources.KotlinBundle
import org.jetbrains.kotlin.idea.base.util.module
import org.jetbrains.kotlin.idea.configuration.KotlinBuildSystemDependencyManager
import org.jetbrains.kotlin.idea.configuration.KotlinLibraryVersionProvider
import org.jetbrains.kotlin.idea.configuration.isProjectSyncPendingOrInProgress
import org.jetbrains.kotlin.idea.configuration.withScope
import org.jetbrains.kotlin.idea.codeInsight.inspections.shared.libraries.AddKotlinLibraryQuickFixProvider.LibraryAvailabilityTester
import org.jetbrains.kotlin.idea.codeInsight.inspections.shared.libraries.AddKotlinLibraryQuickFixProvider.LibraryDescriptorProvider
import org.jetbrains.kotlin.idea.codeInsight.inspections.shared.libraries.AddKotlinLibraryQuickFixProvider.LibraryReferenceTester
import org.jetbrains.kotlin.idea.configuration.*
import org.jetbrains.kotlin.idea.stubindex.KotlinFullClassNameIndex
import org.jetbrains.kotlin.psi.KtQualifiedExpression
import org.jetbrains.kotlin.psi.KtReferenceExpression
/**
* A quick fix provider allowing for Kotlin libraries to be added when an unresolved element was detected.
* [libraryGroupId] and [libraryArtifactId] are the maven coordinates that will be added by this quick-fix.
*/
abstract class AddKotlinLibraryQuickFixProvider(
open class AddKotlinLibraryQuickFixProvider(
private val libraryGroupId: String,
private val libraryArtifactId: String
private val libraryArtifactId: String,
private val libraryDescriptorProvider: LibraryDescriptorProvider,
private val libraryAvailabilityTester: LibraryAvailabilityTester,
private val libraryReferenceTester: LibraryReferenceTester,
@IntentionName
private val quickFixText: String = KotlinBundle.message("add.0.library", "$libraryGroupId:$libraryArtifactId")
) : UnresolvedReferenceQuickFixProvider<PsiReference>() {
/**
* Returns whether the library that will be added by this quick-fix is already present in the module.
*/
protected abstract fun hasLibrary(module: Module): Boolean
/**
* Checks if the reference is a reference to a definition of the library belonging to this quick-fix.
*/
protected abstract fun isLibraryReference(ref: PsiReference): Boolean
fun interface LibraryAvailabilityTester {
/**
* Returns whether the library that will be added by this quick-fix is already present in the module.
*/
fun isAvailable(module: Module): Boolean
private fun PsiReference.getKotlinVersion(): KotlinVersion {
return element.languageVersionSettings.languageVersion.toKotlinVersion()
companion object
}
private fun getLibraryDescriptor(kotlinVersion: KotlinVersion): ExternalLibraryDescriptor? {
val versionProvider = KotlinLibraryVersionProvider.EP_NAME.extensionList.firstOrNull() ?: return null
return versionProvider.getVersion(libraryGroupId, libraryArtifactId, kotlinVersion)
fun interface LibraryReferenceTester {
/**
* Checks if the reference is a reference to a definition of the library belonging to this quick-fix.
*/
fun isLibraryReference(ref: PsiReference): Boolean
companion object
}
fun interface LibraryDescriptorProvider {
fun getLibraryDescriptor(libraryGroupId: String, libraryArtifactId: String, psiReference: PsiReference): ExternalLibraryDescriptor?
companion object
}
override fun registerFixes(ref: PsiReference, registrar: QuickFixActionRegistrar) {
if (!isLibraryReference(ref)) return
if (!libraryReferenceTester.isLibraryReference(ref)) return
val module = ref.element.module ?: return
val extensionList = ref.element.project.extensionArea.getExtensionPoint(KotlinBuildSystemDependencyManager.EP_NAME).extensionList
val dependencyManager = extensionList.firstOrNull { it.isApplicable(module) } ?: return
if (dependencyManager.isProjectSyncPendingOrInProgress()) return
if (hasLibrary(module)) return
val libraryVersionToUse = getLibraryDescriptor(ref.getKotlinVersion()) ?: return
if (libraryAvailabilityTester.isAvailable(module)) return
val libraryDescriptor = libraryDescriptorProvider.getLibraryDescriptor(
libraryGroupId, libraryArtifactId, ref
) ?: return
val scope = if (ProjectFileIndex.getInstance(module.project).isInTestSourceContent(ref.element.containingFile.virtualFile)) {
DependencyScope.TEST
@@ -64,11 +83,44 @@ abstract class AddKotlinLibraryQuickFixProvider(
registrar.register(
AddKotlinLibraryQuickFix(
dependencyManager = dependencyManager,
libraryDescriptor = libraryVersionToUse.withScope(scope),
quickFixText = KotlinBundle.message("add.kotlin.coroutines")
libraryDescriptor = libraryDescriptor.withScope(scope),
quickFixText = quickFixText
)
)
}
override fun getReferenceClass(): Class<PsiReference> = PsiReference::class.java
}
/**
* Will use the [KotlinLibraryVersionProvider] to look up the version
*/
fun LibraryDescriptorProvider.Companion.default() = LibraryDescriptorProvider { libraryGroupId, libraryArtifactId, psiRef ->
val module = psiRef.element.module ?: return@LibraryDescriptorProvider null
val libraryVersion = KotlinLibraryVersionProvider.EP_NAME.extensionList.firstNotNullOfOrNull { provider ->
provider.getVersion(module, libraryGroupId, libraryArtifactId)
}
ExternalLibraryDescriptor(libraryGroupId, libraryArtifactId, libraryVersion, libraryVersion, libraryVersion)
}
/**
* A simple [LibraryReferenceTester] that determines if an unresolved reference belongs to the library if they
* are contained in the [names].
* The names that are checked are references or function calls that are not part of a dot qualified expression.
*/
fun LibraryReferenceTester.Companion.knownNames(vararg names: String) = LibraryReferenceTester { ref ->
val referenceExpression = ref.element as? KtReferenceExpression ?: return@LibraryReferenceTester false
if (referenceExpression.parent is KtQualifiedExpression) return@LibraryReferenceTester false
referenceExpression.text in names
}
/**
* Will check if the provided [fqn] is available as class in the given module
*/
fun LibraryAvailabilityTester.Companion.knownClassFqn(fqn: String) = LibraryAvailabilityTester { module ->
val scope = ModulesScope.moduleWithDependenciesAndLibrariesScope(module)
JavaPsiFacade.getInstance(module.project).findClasses(fqn, scope).isNotEmpty() ||
KotlinFullClassNameIndex[fqn, module.project, scope].isNotEmpty()
}
@@ -1,23 +0,0 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.kotlin.idea.codeInsight.inspections.shared.libraries
import com.intellij.psi.PsiReference
import org.jetbrains.kotlin.psi.KtQualifiedExpression
import org.jetbrains.kotlin.psi.KtReferenceExpression
/**
* A simple [AddKotlinLibraryQuickFixProvider] that determines if an unresolved reference belongs to the library if they
* are contained in the [namesToCheck].
* The names that are checked are references or function calls that are not part of a dot qualified expression.
*/
abstract class SimpleAddKotlinLibraryQuickFixProvider(
libraryGroupId: String,
libraryArtifactId: String,
private val namesToCheck: Set<String>,
) : AddKotlinLibraryQuickFixProvider(libraryGroupId, libraryArtifactId) {
override fun isLibraryReference(ref: PsiReference): Boolean {
val referenceExpression = ref.element as? KtReferenceExpression ?: return false
if (!namesToCheck.contains(referenceExpression.text)) return false
return referenceExpression.parent !is KtQualifiedExpression
}
}
@@ -0,0 +1,25 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.kotlin.idea.codeInsight.inspections.shared.libraries
import org.jetbrains.kotlin.idea.base.resources.KotlinBundle
internal class AddKotlinCoroutinesQuickFixProvider : AddKotlinLibraryQuickFixProvider(
libraryGroupId = "org.jetbrains.kotlinx",
libraryArtifactId = "kotlinx-coroutines-core",
libraryDescriptorProvider = LibraryDescriptorProvider.default(),
libraryAvailabilityTester = LibraryAvailabilityTester.knownClassFqn("kotlinx.coroutines.CoroutineScope"),
libraryReferenceTester = LibraryReferenceTester.knownNames(
"runBlocking", "CoroutineScope", "Dispatchers", "launch", "GlobalScope",
"MainScope", "async", "await", "withContext", "cancel", "isActive", "Job",
),
quickFixText = KotlinBundle.message("add.kotlin.coroutines")
)
internal class AddKotlinTestLibraryQuickFixProvider : AddKotlinLibraryQuickFixProvider(
libraryGroupId = "org.jetbrains.kotlin",
libraryArtifactId = "kotlin-test",
libraryDescriptorProvider = LibraryDescriptorProvider.default(),
libraryAvailabilityTester = LibraryAvailabilityTester.knownClassFqn("kotlin.test.Asserter"),
libraryReferenceTester = LibraryReferenceTester.knownNames("Test", "assertEquals", "assertTrue", "assertFalse", "assertFailsWith"),
)
@@ -153,7 +153,7 @@ abstract class KotlinWithGradleConfigurator : KotlinProjectConfigurator {
}
private fun Project.isGradleSyncPending(module: Module): Boolean {
return KotlinProjectConfigurationService.getInstance(this).isSyncPending(module)
return KotlinProjectConfigurationService.getInstance(this).isSyncDesired(module)
}
private fun Project.isGradleSyncInProgress(): Boolean {
@@ -41,6 +41,10 @@
<kotlinBuildSystemFacade implementation="org.jetbrains.kotlin.idea.gradleJava.KotlinGradleBuildSystemFacade"/>
</extensions>
<extensions defaultExtensionNs="org.jetbrains.kotlin">
<libraryVersionProvider implementation="org.jetbrains.kotlin.idea.gradleJava.KotlinGradlePluginLibraryVersionProvider"/>
</extensions>
<extensions defaultExtensionNs="org.jetbrains.plugins.gradle">
<frameworkSupport implementation="org.jetbrains.kotlin.idea.gradleJava.configuration.GradleKotlinMPPSourceSetsFrameworkSupportProvider"/>
@@ -0,0 +1,21 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.kotlin.idea.gradleJava
import com.intellij.openapi.module.Module
import org.jetbrains.kotlin.idea.configuration.KotlinLibraryVersionProvider
/**
* Returns the modules 'Kotlin Gradle Plugin version' as the version for libraries
* which originate from the Kotlin monorepo. Libraries (such as the kotlin stdlib, kotlin-test, ...) are expected
* to have the same version as the Kotlin Gradle Plugin.
*/
class KotlinGradlePluginLibraryVersionProvider : KotlinLibraryVersionProvider {
companion object {
const val STDLIB_GROUP_ID = "org.jetbrains.kotlin"
}
override fun getVersion(module: Module, groupId: String, artifactId: String): String? {
if (groupId != STDLIB_GROUP_ID) return null
return module.kotlinGradlePluginVersion?.versionString
}
}
@@ -14,6 +14,7 @@ import com.intellij.psi.util.childrenOfType
import org.jetbrains.kotlin.config.LanguageFeature
import org.jetbrains.kotlin.idea.base.codeInsight.CliArgumentStringBuilder.buildArgumentString
import org.jetbrains.kotlin.idea.base.codeInsight.CliArgumentStringBuilder.replaceLanguageFeature
import org.jetbrains.kotlin.idea.base.facet.isMultiPlatformModule
import org.jetbrains.kotlin.idea.base.util.module
import org.jetbrains.kotlin.idea.compiler.configuration.IdeKotlinVersion
import org.jetbrains.kotlin.idea.configuration.*
@@ -219,7 +220,7 @@ class KotlinBuildScriptManipulator(
libraryDescriptor.libraryGroupId,
libraryDescriptor.libraryArtifactId,
libraryDescriptor.preferredVersion ?: libraryDescriptor.maxVersion ?: libraryDescriptor.minVersion,
scope.toGradleCompileScope(scriptFile.module?.buildSystemType == BuildSystemType.AndroidGradle)
scope.toGradleCompileScope(targetModule)
)
if (targetModule != null && usesNewMultiplatform()) {
@@ -2,29 +2,62 @@
package org.jetbrains.kotlin.idea.codeInsight.gradle
import com.intellij.codeInsight.daemon.quickFix.ActionHint
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.application.*
import com.intellij.openapi.externalSystem.autoimport.AutoImportProjectTracker
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.testFramework.IndexingTestUtil
import com.intellij.testFramework.PlatformTestUtil
import com.intellij.testFramework.UsefulTestCase
import com.intellij.testFramework.runInEdtAndWait
import com.intellij.testFramework.*
import com.intellij.testFramework.common.timeoutRunBlocking
import com.intellij.testFramework.utils.vfs.refreshAndGetVirtualDirectory
import kotlinx.coroutines.*
import org.jetbrains.kotlin.idea.configuration.KotlinProjectConfigurationService
import org.jetbrains.kotlin.idea.core.util.toPsiFile
import org.jetbrains.kotlin.idea.test.DirectiveBasedActionUtils
import org.jetbrains.kotlin.psi.KtFile
import java.io.File
import java.nio.file.Files
import kotlin.io.path.isRegularFile
import kotlin.io.path.pathString
import kotlin.io.path.readText
import java.nio.file.Path
import kotlin.io.path.*
import kotlin.streams.asSequence
import kotlin.time.Duration.Companion.minutes
abstract class AbstractGradleMultiFileQuickFixTest : MultiplePluginVersionGradleImportingCodeInsightTestCase() {
override fun testDataDirName() = "fixes"
final override fun testDataDirectory(): File = super.testDataDirectory().resolve("before")
open val afterDirectory get() = testDataDirectory().parentFile.resolve("after")
open val afterTestDataDirectory get() = testDataDirectory().parentFile.resolve("after")
private lateinit var afterDirectory: Path
@OptIn(ExperimentalPathApi::class)
override fun setUp() {
super.setUp()
/* Setup 'after' directory: Ensure that we process it similar to the 'before', by also replacing test properties */
afterDirectory = TemporaryDirectory.generateTemporaryPath("${testDataDirName()}.after")
afterTestDataDirectory.toPath().copyToRecursively(afterDirectory, followLinks = true, copyAction = { source, target ->
if (source.isDirectory()) {
target.createDirectory()
}
if (source.isRegularFile()) {
target.writeText(configureKotlinVersionAndProperties(source.readText()))
}
CopyActionResult.CONTINUE
})
/* Some quick fix (e.g. AddKotlinLibraryQuickFix) will modify build scripts *and* re-sync the project */
AutoImportProjectTracker.enableAutoReloadInTests(testRootDisposable)
}
@OptIn(ExperimentalPathApi::class)
override fun tearDown() {
RunAll.runAll(
{ afterDirectory.deleteRecursively() },
{ super.tearDown() },
)
}
protected fun doMultiFileQuickFixTest(
ignoreChangesInBuildScriptFiles: Boolean = true,
@@ -49,43 +82,48 @@ abstract class AbstractGradleMultiFileQuickFixTest : MultiplePluginVersionGradle
val actionHint = ActionHint.parse(ktFile, mainFileText)
codeInsightTestFixture.configureFromExistingVirtualFile(ktFile.virtualFile)
runInEdtAndWait {
timeoutRunBlocking(3.minutes) {
val actions = codeInsightTestFixture.availableIntentions
val action = actionHint.findAndCheck(actions) { "Test file: ${projectPath.relativize(mainFilePath).pathString}" }
if (action != null) {
action.invoke(myProject, null, ktFile)
codeInsightTestFixture.launchAction(action)
KotlinProjectConfigurationService.getInstance(project).awaitSyncFinished()
IndexingTestUtil.waitUntilIndexesAreReady(myProject)
val expected = afterDirectory.toPath().refreshAndGetVirtualDirectory()
val expected = afterDirectory.refreshAndGetVirtualDirectory()
val projectVFile = projectPath.refreshAndGetVirtualDirectory()
UsefulTestCase.refreshRecursively(expected)
UsefulTestCase.refreshRecursively(projectVFile)
PlatformTestUtil.assertDirectoriesEqual(
expected,
projectVFile,
fun(vFile: VirtualFile): Boolean {
if (vFile.parent == projectVFile) {
when (vFile.name) {
".gradle", "gradle", "build", "gradle.properties", "gradlew", "gradlew.bat", ".kotlin" -> return false
withContext(Dispatchers.EDT) {
PlatformTestUtil.assertDirectoriesEqual(
expected,
projectVFile,
fun(vFile: VirtualFile): Boolean {
if (vFile.parent == projectVFile) {
when (vFile.name) {
".gradle", "gradle", "build", "gradle.properties", "gradlew", "gradlew.bat", ".kotlin" -> return false
}
}
}
if (ignoreChangesInBuildScriptFiles && ".gradle" in vFile.name) return false
if (ignoreChangesInBuildScriptFiles && ".gradle" in vFile.name) return false
return additionalResultFileFilter(vFile)
},
)
return additionalResultFileFilter(vFile)
},
)
}
}
IndexingTestUtil.waitUntilIndexesAreReady(myProject)
codeInsightTestFixture.doHighlighting()
DirectiveBasedActionUtils.checkAvailableActionsAreExpected(ktFile, action?.let { actions - it } ?: actions)
DirectiveBasedActionUtils.checkForUnexpectedErrors(ktFile)
readActionBlocking {
DirectiveBasedActionUtils.checkAvailableActionsAreExpected(ktFile, action?.let { actions - it } ?: actions)
DirectiveBasedActionUtils.checkForUnexpectedErrors(ktFile)
}
}
}
}
@@ -53,4 +53,18 @@ class GradleQuickFixTest : AbstractGradleMultiFileQuickFixTest() {
@Test
@PluginTargetVersions(pluginVersion = "1.5.31+")
fun testCreateActualForGranularSourceSetTarget() = doMultiFileQuickFixTest()
@Test
@PluginTargetVersions(pluginVersion = "2.0+")
fun testAddKotlinTestLibraryJvm() = doMultiFileQuickFixTest(
ignoreChangesInBuildScriptFiles = false,
additionalResultFileFilter = { file -> file.name != "settings.gradle.kts" }
)
@Test
@PluginTargetVersions(pluginVersion = "1.5.31+")
fun testAddKotlinTestLibraryKmp() = doMultiFileQuickFixTest(
ignoreChangesInBuildScriptFiles = false,
additionalResultFileFilter = { file -> file.name != "settings.gradle.kts" }
)
}
@@ -0,0 +1,6 @@
plugins {
kotlin("jvm")
}
dependencies {
testImplementation(kotlin("test"))
}
@@ -0,0 +1,9 @@
// "Add 'org.jetbrains.kotlin:kotlin-test' library" "true"
// ERROR: Unresolved reference: Test
class MyTest {
@Test
fun testFoo() {
}
}
@@ -0,0 +1,3 @@
plugins {
kotlin("jvm")
}
@@ -0,0 +1,9 @@
pluginManagement {
repositories {
{{kts_kotlin_plugin_repositories}}
}
plugins {
kotlin("jvm") version "{{kotlin_plugin_version}}"
}
}
@@ -0,0 +1,9 @@
// "Add 'org.jetbrains.kotlin:kotlin-test' library" "true"
// ERROR: Unresolved reference: Test
class MyTest {
@Test<caret>
fun testFoo() {
}
}
@@ -0,0 +1,19 @@
plugins {
kotlin("multiplatform")
}
kotlin {
jvm()
linuxX64()
sourceSets {
getByName("commonTest") {
dependencies {
implementation(kotlin("test"))
}
}
}
}
repositories {
{{kts_kotlin_plugin_repositories}}
}
@@ -0,0 +1,9 @@
// "Add 'org.jetbrains.kotlin:kotlin-test' library" "true"
import kotlin.test.Test
class MyTest {
@Test
fun testFoo() {
}
}
@@ -0,0 +1,12 @@
plugins {
kotlin("multiplatform")
}
kotlin {
jvm()
linuxX64()
}
repositories {
{{kts_kotlin_plugin_repositories}}
}
@@ -0,0 +1,9 @@
pluginManagement {
repositories {
{{kts_kotlin_plugin_repositories}}
}
plugins {
kotlin("multiplatform") version "{{kotlin_plugin_version}}"
}
}
@@ -0,0 +1,9 @@
// "Add 'org.jetbrains.kotlin:kotlin-test' library" "true"
import kotlin.test.Test
class MyTest {
@Test<caret>
fun testFoo() {
}
}
@@ -37,6 +37,7 @@ import org.jetbrains.kotlin.builtins.StandardNames
import org.jetbrains.kotlin.cli.common.arguments.CommonCompilerArguments
import org.jetbrains.kotlin.config.KotlinFacetSettingsProvider
import org.jetbrains.kotlin.idea.KotlinFileType
import org.jetbrains.kotlin.idea.base.facet.isMultiPlatformModule
import org.jetbrains.kotlin.idea.base.facet.platform.platform
import org.jetbrains.kotlin.idea.base.indices.KotlinPackageIndexUtils
import org.jetbrains.kotlin.idea.base.platforms.*
@@ -103,6 +104,7 @@ const val KOTLIN_GROUP_ID = "org.jetbrains.kotlin"
fun isRepositoryConfigured(repositoriesBlockText: String): Boolean =
repositoriesBlockText.contains(MAVEN_CENTRAL) || repositoriesBlockText.contains(JCENTER)
@Deprecated("Use 'toGradleCompileScope(Module) instead")
fun DependencyScope.toGradleCompileScope(isAndroidModule: Boolean) = when (this) {
DependencyScope.COMPILE -> "implementation"
// TODO: We should add testCompile or androidTestCompile
@@ -112,6 +114,14 @@ fun DependencyScope.toGradleCompileScope(isAndroidModule: Boolean) = when (this)
else -> "implementation"
}
fun DependencyScope.toGradleCompileScope(targetModule: Module? = null) = when (this) {
DependencyScope.COMPILE -> "implementation"
DependencyScope.TEST -> if (targetModule?.isMultiPlatformModule == true) "implementation" else "testImplementation"
DependencyScope.RUNTIME -> "runtime"
DependencyScope.PROVIDED -> "implementation"
else -> "implementation"
}
fun RepositoryDescription.toGroovyRepositorySnippet() = "maven { url '$url' }"
/**
@@ -2,7 +2,7 @@
package org.jetbrains.kotlin.idea.configuration
import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.openapi.roots.ExternalLibraryDescriptor
import com.intellij.openapi.module.Module
import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
@@ -16,5 +16,5 @@ interface KotlinLibraryVersionProvider {
* Returns a version of the library identified by the [groupId] and [artifactId] compatible with the [projectKotlinVersion].
* Returns null if the [KotlinLibraryVersionProvider] does not manage the given library or no compatible version could be found.
*/
fun getVersion(groupId: String, artifactId: String, projectKotlinVersion: KotlinVersion): ExternalLibraryDescriptor?
fun getVersion(module: Module, groupId: String, artifactId: String): String?
}
@@ -5,19 +5,19 @@ import com.intellij.model.SideEffectGuard
import com.intellij.openapi.application.readAction
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.externalSystem.autoimport.ExternalSystemProjectId
import com.intellij.openapi.externalSystem.autoimport.ExternalSystemProjectTracker
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.module.Module
import com.intellij.openapi.project.Project
import com.intellij.platform.ide.progress.withBackgroundProgress
import com.intellij.ui.EditorNotifications
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.*
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.kotlin.idea.projectConfiguration.KotlinProjectConfigurationBundle
import org.jetbrains.kotlin.idea.statistics.KotlinJ2KOnboardingFUSCollector
import org.jetbrains.kotlin.idea.util.isKotlinFileType
import java.util.concurrent.atomic.AtomicReference
@Service(Service.Level.PROJECT)
class KotlinProjectConfigurationService(private val project: Project, private val coroutineScope: CoroutineScope) {
@@ -27,6 +27,35 @@ class KotlinProjectConfigurationService(private val project: Project, private va
}
}
private data class SyncState(
val level: Level,
/**
* Jobs waiting for a sync to finish (aka transition back to 'idle' again)
*/
val onFinishedListeners: List<CompletableJob>
) {
enum class Level {
/**
* No sync is requested, nor currently in-progress running
*/
Idle,
/**
* A sync is enqueued but not yet actually running on the external system, or
* a previous sync is already running, but a new one has been requested already
*/
SyncQueued,
/**
* The sync is currently running by the external system
*/
SyncInProgress,
}
}
private val syncState = AtomicReference(SyncState(SyncState.Level.Idle, emptyList()))
@Volatile
private var checkingAndPerformingAutoConfig: Boolean = false
@@ -36,16 +65,13 @@ class KotlinProjectConfigurationService(private val project: Project, private va
fun shouldShowNotConfiguredDialog(module: Module): Boolean {
if (!findApplicableConfigurator(module).shouldShowNotConfiguredDialog()) return false
if (isSyncPending(module)) return false
if (isSyncDesired(module)) return false
if (checkingAndPerformingAutoConfig) return false
// If notificationCooldownEnd wasn't set, then the autoconfiguration didn't take place
val cooldownEnd = notificationCooldownEnd ?: return true
return System.currentTimeMillis() >= cooldownEnd
}
fun isSyncPending(module: Module): Boolean {
return KotlinBuildSystemDependencyManager.findApplicableConfigurator(module)?.isProjectSyncPending() == true
}
fun refreshEditorNotifications() {
// We want to remove the "Kotlin not configured" notification banner as fast as possible
@@ -60,14 +86,61 @@ class KotlinProjectConfigurationService(private val project: Project, private va
}
}
@Volatile
private var syncInProgress: Boolean = false
/**
* @return true if the build configuration is 'dirty' and the user is shown a button to re-sync.
* @see isSyncing
* @see isSyncInProgress
* @see KotlinBuildSystemDependencyManager.isProjectSyncPending
*/
fun isSyncDesired(module: Module): Boolean {
return KotlinBuildSystemDependencyManager.findApplicableConfigurator(module)?.isProjectSyncPending() == true
}
@Volatile
private var syncQueued: Boolean = false
/**
* @return true if the sync is already enqueued or actually in progress by the underlying external system.
*/
fun isSyncing(): Boolean {
return syncState.get().run {
level == SyncState.Level.SyncQueued || level == SyncState.Level.SyncInProgress
}
}
/**
* @return true if the sync is currently in-progress by the underlying external system.
* Also see [isSyncing] for checking if the sync is queued or in progress.
*/
fun isSyncInProgress(): Boolean {
return syncInProgress
return syncState.get().level == SyncState.Level.SyncInProgress
}
/**
* Will suspend until all queued or 'in progress' syncs are completed.
* @return true if the method suspended and had to wait for a sync
* @return false if no sync was queued or in progress and the method was able to return without suspending.
*/
suspend fun awaitSyncFinished(): Boolean {
val job = Job()
val state = syncState.updateAndGet { state ->
/* Idle: Nothing to do */
if (state.level == SyncState.Level.Idle) return@updateAndGet state
state.copy(onFinishedListeners = state.onFinishedListeners + job)
}
if (state.level != SyncState.Level.Idle) {
job.join()
return true
}
return false
}
/**
* Triggers a sync ([queueSync]) and will suspend until it completes.
*/
suspend fun sync() {
queueSync()
awaitSyncFinished()
}
/**
@@ -77,24 +150,47 @@ class KotlinProjectConfigurationService(private val project: Project, private va
fun queueSync() {
// prevents this side effect from being actually run from quickfix previews (e.g. in Fleet)
SideEffectGuard.checkSideEffectAllowed(SideEffectGuard.EffectType.PROJECT_MODEL)
if (syncInProgress) {
syncQueued = true
} else {
val previousState = syncState.getAndUpdate { state -> state.copy(level = SyncState.Level.SyncQueued) }
if (previousState.level < SyncState.Level.SyncInProgress) {
ExternalSystemProjectTracker.getInstance(project).scheduleProjectRefresh()
}
}
@ApiStatus.Internal
fun onSyncStarted() {
syncInProgress = true
syncState.updateAndGet { state ->
state.copy(level = SyncState.Level.SyncInProgress)
}
}
@ApiStatus.Internal
fun onSyncFinished() {
syncInProgress = false
if (syncQueued) {
syncQueued = false
val previousState = syncState.getAndUpdate { state ->
/*
Only if the previous state was actually 'SyncInProgress', then we can
consider the job to be done and reset the state. If the state is already 'SyncQueued' again, this means
that another queueSync call happened and we should start one more sync.
*/
if (state.level == SyncState.Level.SyncInProgress) {
SyncState(level = SyncState.Level.Idle, emptyList())
} else {
state
}
}
/*
We previously were in 'StateInProgress', which means the job now has finished.
Therefore, its time to complete the waiting coroutines.
*/
if (previousState.level == SyncState.Level.SyncInProgress) {
previousState.onFinishedListeners.forEach { job -> job.complete() }
}
/*
During the current sync, we received another call into 'queueSync', which means
that the currently finished sync is 'dirty' and we should start another one!
*/
if (previousState.level == SyncState.Level.SyncQueued) {
ExternalSystemProjectTracker.getInstance(project).scheduleProjectRefresh()
}
}
@@ -33,5 +33,6 @@
<orderEntry type="module" module-name="intellij.java.psi" />
<orderEntry type="module" module-name="intellij.java.impl" />
<orderEntry type="module" module-name="kotlin.project-configuration" />
<orderEntry type="module" module-name="kotlin.base.project-structure" />
</component>
</module>
@@ -1,15 +1,17 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.kotlin.tools.projectWizard.compatibility
import com.intellij.openapi.roots.ExternalLibraryDescriptor
import com.intellij.openapi.module.Module
import org.jetbrains.kotlin.config.toKotlinVersion
import org.jetbrains.kotlin.idea.base.projectStructure.languageVersionSettings
import org.jetbrains.kotlin.idea.configuration.KotlinLibraryVersionProvider
class DefaultKotlinLibraryVersionProvider : KotlinLibraryVersionProvider {
override fun getVersion(groupId: String, artifactId: String, projectKotlinVersion: KotlinVersion): ExternalLibraryDescriptor? {
override fun getVersion(module: Module, groupId: String, artifactId: String): String? {
val projectKotlinVersion = module.languageVersionSettings.languageVersion.toKotlinVersion()
val versions = KotlinLibrariesCompatibilityStore.getInstance().getVersions(groupId, artifactId).orEmpty()
val kotlinShortVersion = "${projectKotlinVersion.major}.${projectKotlinVersion.minor}"
val versionToUse = versions[kotlinShortVersion] ?: return null
return ExternalLibraryDescriptor(groupId, artifactId, versionToUse, versionToUse, versionToUse)
return versions[kotlinShortVersion]
}
}
@@ -50,5 +50,6 @@
<orderEntry type="module" module-name="intellij.maven.tests" scope="TEST" />
<orderEntry type="module" module-name="kotlin.gradle.gradle-java.tests.shared" scope="TEST" />
<orderEntry type="module" module-name="kotlin.project-configuration" scope="TEST" />
<orderEntry type="module" module-name="kotlin.base.project-structure" scope="TEST" />
</component>
</module>
@@ -1,9 +1,11 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.kotlin.tools.projectWizard.compatibility
import com.intellij.openapi.roots.ExternalLibraryDescriptor
import com.intellij.testFramework.fixtures.BasePlatformTestCase
import org.jetbrains.kotlin.config.LanguageVersion
import org.jetbrains.kotlin.idea.configuration.KotlinLibraryVersionProvider
import org.jetbrains.kotlin.idea.facet.setLanguageAndApiVersionInKotlinFacet
import org.jetbrains.kotlin.idea.test.createFacet
class DefaultKotlinLibraryVersionProviderTest : BasePlatformTestCase() {
private lateinit var provider: DefaultKotlinLibraryVersionProvider
@@ -11,46 +13,35 @@ class DefaultKotlinLibraryVersionProviderTest : BasePlatformTestCase() {
override fun setUp() {
super.setUp()
provider = DefaultKotlinLibraryVersionProvider()
module.createFacet(useProjectSettings = false)
}
private fun getCoroutinesVersion(kotlinVersion: KotlinVersion): ExternalLibraryDescriptor? {
private fun getCoroutinesVersion(version: LanguageVersion): String? {
val groupId = "org.jetbrains.kotlinx"
val artifactId = "kotlinx-coroutines-core"
val descriptor = provider.getVersion(groupId, artifactId, kotlinVersion)
if (descriptor != null) {
assertEquals(groupId, descriptor.libraryGroupId)
assertEquals(artifactId, descriptor.libraryArtifactId)
assertNotNull(descriptor.preferredVersion)
assertEquals(descriptor.preferredVersion, descriptor.minVersion)
assertEquals(descriptor.preferredVersion, descriptor.maxVersion)
}
return descriptor
myFixture.module.setLanguageAndApiVersionInKotlinFacet(version.toString(), version.toString())
return provider.getVersion(myFixture.module, groupId, artifactId)
}
fun testDefaultVersionProviderRegistered() {
assertTrue(KotlinLibraryVersionProvider.Companion.EP_NAME.extensionList.any { it is DefaultKotlinLibraryVersionProvider })
assertTrue(KotlinLibraryVersionProvider.EP_NAME.extensionList.any { it is DefaultKotlinLibraryVersionProvider })
}
fun testKnownVersion() {
val returnedVersion = getCoroutinesVersion(KotlinVersion(1, 9))
val returnedVersion = getCoroutinesVersion(LanguageVersion.KOTLIN_1_9)
assertNotNull(returnedVersion)
}
fun testPatchedKotlinVersion() {
val returnedVersion = getCoroutinesVersion(KotlinVersion(1, 9, 23))
val nonPatchedVersion = getCoroutinesVersion(KotlinVersion(1, 9))
assertNotNull(returnedVersion?.preferredVersion)
assertEquals(returnedVersion?.preferredVersion, nonPatchedVersion?.preferredVersion)
}
fun testUnknownKotlin() {
assertNull(getCoroutinesVersion(KotlinVersion(0, 3)))
assertNull(getCoroutinesVersion(LanguageVersion.KOTLIN_1_0))
}
fun testSpecificCoroutinesVersions() {
// Here we test that versions for old Kotlin versions (which will not receive new updates)
// return the correct exact values
assertEquals("1.6.4", getCoroutinesVersion(KotlinVersion(1, 6))?.preferredVersion)
assertEquals("1.5.2", getCoroutinesVersion(KotlinVersion(1, 5))?.preferredVersion)
assertEquals("1.6.4", getCoroutinesVersion(LanguageVersion.KOTLIN_1_6))
assertEquals("1.5.2", getCoroutinesVersion(LanguageVersion.KOTLIN_1_5))
}
}