[Kotlin] Don't check manifest properties in Kotlin JS stdlib detector

Use name matching as it's done for JVM stdlib.
Allows removing org.jetbrains.kotlin.utils.LibraryUtils in kotlin.git.

Changes:
— Remove LibraryUtils usage and supporting code
— Add tests for watershed versions
— Allow requesting KLIBs from MavenDependencyUtil for the new tests
— Workaround: extract VFGist used for KotlinLibraryKind from the service

A comment about the last workaround. StdlibJsDetectorFacilityTest revealed a
Project leakage. The project is captured by a coroutine context and is hard
referenced through ComponentManager by an application-level virtual file gist
cleanup task. The exact reference chain is not clear, but removing the gist
construction from the service prevents the leakage.

KTIJ-27969

GitOrigin-RevId: 7d6dbe93f4aa3125f5d204a81babcf8483874cb4
This commit is contained in:
Pavel Kirpichenkov
2024-08-06 20:45:59 +00:00
committed by intellij-monorepo-bot
parent 934691271b
commit aae5c6cd9f
5 changed files with 157 additions and 55 deletions
@@ -14,6 +14,7 @@ import com.intellij.openapi.roots.libraries.ui.OrderRoot;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.idea.maven.utils.library.RepositoryLibraryProperties;
import org.jetbrains.jps.model.library.JpsMavenRepositoryLibraryDescriptor;
import java.util.Collection;
import java.util.Collections;
@@ -57,7 +58,7 @@ public final class MavenDependencyUtil {
}
/**
* Adds a Maven library to {@code model}
* Adds a Maven library with the default packaging (JAR) to {@code model}
* @param additionalRepositories additional Maven repositories where the artifacts should be searched (in addition to repositories
* configured in intellij project)
*/
@@ -66,9 +67,24 @@ public final class MavenDependencyUtil {
boolean includeTransitiveDependencies,
DependencyScope dependencyScope,
List<RemoteRepositoryDescription> additionalRepositories) {
addFromMaven(model, mavenCoordinates, includeTransitiveDependencies, dependencyScope, additionalRepositories,
JpsMavenRepositoryLibraryDescriptor.DEFAULT_PACKAGING);
}
/**
* Adds a Maven library to {@code model}
* @param packaging artifact packaging matching {@link org.jetbrains.idea.maven.aether.ArtifactKind} of the requested library
*/
public static void addFromMaven(@NotNull ModifiableRootModel model,
String mavenCoordinates,
boolean includeTransitiveDependencies,
DependencyScope dependencyScope,
List<RemoteRepositoryDescription> additionalRepositories,
@NotNull String packaging) {
List<RemoteRepositoryDescription> remoteRepositoryDescriptions = ContainerUtil.concat(getRemoteRepositoryDescriptions(),
additionalRepositories);
RepositoryLibraryProperties libraryProperties = new RepositoryLibraryProperties(mavenCoordinates, includeTransitiveDependencies);
RepositoryLibraryProperties libraryProperties =
new RepositoryLibraryProperties(mavenCoordinates, packaging, includeTransitiveDependencies);
Collection<OrderRoot> roots =
JarRepositoryManager.loadDependenciesModal(model.getProject(), libraryProperties, false, false, null, remoteRepositoryDescriptions);
LibraryTable.ModifiableModel tableModel = model.getModuleLibraryTable().getModifiableModel();
@@ -2,20 +2,16 @@
package org.jetbrains.kotlin.idea.base.platforms
import com.intellij.openapi.util.Key
import com.intellij.openapi.vfs.StandardFileSystems
import com.intellij.openapi.vfs.VfsUtilCore
import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.kotlin.idea.base.plugin.artifacts.KotlinArtifactNames
import org.jetbrains.kotlin.utils.LibraryUtils
import org.jetbrains.kotlin.utils.PathUtil
import java.io.File
import java.util.regex.Pattern
@ApiStatus.Internal
object KotlinJavaScriptStdlibDetectorFacility : StdlibDetectorFacility() {
private val IS_JS_LIBRARY_STD_LIB = Key.create<Boolean>("IS_JS_LIBRARY_STD_LIB")
private val KOTLIN_JS_LIBRARY_KLIB_PATTERN = Pattern.compile("kotlin-stdlib-js.*\\.klib")
override val supportedLibraryKind: KotlinLibraryKind
@@ -23,6 +19,7 @@ object KotlinJavaScriptStdlibDetectorFacility : StdlibDetectorFacility() {
override fun getStdlibJar(roots: List<VirtualFile>): VirtualFile? {
for (root in roots) {
// KLIBs fall under the JAR file system as well
if (root.fileSystem.protocol !== StandardFileSystems.JAR_PROTOCOL) continue
val name = root.url.substringBefore("!/").substringAfterLast('/')
@@ -33,16 +30,7 @@ object KotlinJavaScriptStdlibDetectorFacility : StdlibDetectorFacility() {
|| KOTLIN_JS_LIBRARY_KLIB_PATTERN.matcher(name).matches()
) {
val jar = VfsUtilCore.getVirtualFileForJar(root) ?: continue
var isJSStdLib = jar.getUserData(IS_JS_LIBRARY_STD_LIB)
if (isJSStdLib == null) {
isJSStdLib = KOTLIN_JS_LIBRARY_KLIB_PATTERN.matcher(name).matches()
|| LibraryUtils.isKotlinJavascriptStdLibrary(File(jar.path))
jar.putUserData(IS_JS_LIBRARY_STD_LIB, isJSStdLib)
}
if (isJSStdLib) {
return jar
}
return jar
}
}
@@ -25,6 +25,7 @@ import com.intellij.util.indexing.roots.kind.LibraryOrigin
import com.intellij.util.io.*
import org.jetbrains.kotlin.analysis.decompiler.psi.KotlinBuiltInFileType
import org.jetbrains.kotlin.idea.KotlinFileType
import org.jetbrains.kotlin.idea.base.platforms.LibraryEffectiveKindProvider.LibraryKindScanner
import org.jetbrains.kotlin.platform.idePlatformKind
import org.jetbrains.kotlin.platform.jvm.JvmPlatforms
import org.jetbrains.kotlin.serialization.deserialization.DOT_METADATA_FILE_EXTENSION
@@ -33,42 +34,6 @@ import java.io.DataOutput
@Service(Service.Level.PROJECT)
class LibraryEffectiveKindProvider(private val project: Project) {
private companion object {
val LIBRARY_KIND_KEY: Key<PersistentLibraryKind<*>> = Key.create("LibraryEffectiveKind")
val CLASS_ROOTS_KEY: Key<Array<VirtualFile>> = Key.create("LibraryClassRoots")
val NEEDS_TO_BE_CLARIFIED_KIND: UnknownLibraryKind = UnknownLibraryKind.getOrCreate("Needs to be clarified")
@JvmStatic
private val KOTLIN_LIBRARY_KIND_GIST: VirtualFileGist<PersistentLibraryKind<*>> = GistManager.getInstance().newVirtualFileGist(
"kotlin-library-kind",
1,
object : DataExternalizer<PersistentLibraryKind<*>> {
override fun save(out: DataOutput, value: PersistentLibraryKind<*>) {
val kindId = value.kindId
IOUtil.writeString(kindId, out)
}
override fun read(`in`: DataInput): PersistentLibraryKind<*>? =
when (val kindId = IOUtil.readString(`in`)) {
// as KotlinJvmEffectiveLibraryKind is a fake library kind
KotlinJvmEffectiveLibraryKind.kindId -> KotlinJvmEffectiveLibraryKind
else -> LibraryKindRegistry.getInstance().findKindById(kindId) as? PersistentLibraryKind<*>
}
}
) { _, file ->
val classRoots = file.getUserData(CLASS_ROOTS_KEY) ?: arrayOf(file)
LibraryKindScanner.runScannerOutsideScanningSession(classRoots)
var platformKind: PersistentLibraryKind<*>? = file.getUserData(LIBRARY_KIND_KEY)
if (platformKind == NEEDS_TO_BE_CLARIFIED_KIND) {
val matchingPlatformKind = IdePlatformKindProjectStructure.getLibraryPlatformKind(file)
?: JvmPlatforms.defaultJvmPlatform.idePlatformKind
platformKind = IdePlatformKindProjectStructure.getLibraryKind(matchingPlatformKind)
}
platformKind
}
}
private enum class KnownLibraryKindForIndex {
COMMON, JS, UNKNOWN
}
@@ -97,8 +62,7 @@ class LibraryEffectiveKindProvider(private val project: Project) {
val classRoot = classRoots.firstOrNull() ?: return null
val platformKind: PersistentLibraryKind<*>? =
classRoot.getUserData(LIBRARY_KIND_KEY)?.takeIf { it != NEEDS_TO_BE_CLARIFIED_KIND } ?:
findKind(classRoots)?.let {
classRoot.getUserData(LIBRARY_KIND_KEY)?.takeIf { it != NEEDS_TO_BE_CLARIFIED_KIND } ?: findKind(classRoots)?.let {
classRoot.putUserData(LIBRARY_KIND_KEY, it)
it
}
@@ -122,7 +86,7 @@ class LibraryEffectiveKindProvider(private val project: Project) {
scannerVisitor.result = null
VfsUtil.visitChildrenRecursively(classRoot, object : VirtualFileVisitor<Any?>() {
override fun visitFileEx(file: VirtualFile): Result =
if (visitFile(file)) CONTINUE else skipTo(classRoot)
if (visitFile(file)) CONTINUE else skipTo(classRoot)
override fun visitFile(file: VirtualFile): Boolean {
ProgressManager.checkCanceled()
@@ -210,3 +174,37 @@ class LibraryEffectiveKindProvider(private val project: Project) {
}
}
private val LIBRARY_KIND_KEY: Key<PersistentLibraryKind<*>> = Key.create("LibraryEffectiveKind")
private val CLASS_ROOTS_KEY: Key<Array<VirtualFile>> = Key.create("LibraryClassRoots")
private val NEEDS_TO_BE_CLARIFIED_KIND: UnknownLibraryKind = UnknownLibraryKind.getOrCreate("Needs to be clarified")
private val KOTLIN_LIBRARY_KIND_GIST: VirtualFileGist<PersistentLibraryKind<*>> by lazy {
GistManager.getInstance().newVirtualFileGist(
"kotlin-library-kind",
1,
object : DataExternalizer<PersistentLibraryKind<*>> {
override fun save(out: DataOutput, value: PersistentLibraryKind<*>) {
val kindId = value.kindId
IOUtil.writeString(kindId, out)
}
override fun read(`in`: DataInput): PersistentLibraryKind<*>? =
when (val kindId = IOUtil.readString(`in`)) {
// as KotlinJvmEffectiveLibraryKind is a fake library kind
KotlinJvmEffectiveLibraryKind.kindId -> KotlinJvmEffectiveLibraryKind
else -> LibraryKindRegistry.getInstance().findKindById(kindId) as? PersistentLibraryKind<*>
}
}
) { _, file ->
val classRoots = file.getUserData(CLASS_ROOTS_KEY) ?: arrayOf(file)
LibraryKindScanner.runScannerOutsideScanningSession(classRoots)
var platformKind: PersistentLibraryKind<*>? = file.getUserData(LIBRARY_KIND_KEY)
if (platformKind == NEEDS_TO_BE_CLARIFIED_KIND) {
val matchingPlatformKind = IdePlatformKindProjectStructure.getLibraryPlatformKind(file)
?: JvmPlatforms.defaultJvmPlatform.idePlatformKind
platformKind = IdePlatformKindProjectStructure.getLibraryKind(matchingPlatformKind)
}
platformKind
}
}
@@ -140,5 +140,10 @@
<orderEntry type="module" module-name="intellij.platform.navbar.testFramework" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.execution.dashboard" scope="TEST" />
<orderEntry type="module" module-name="kotlin.code-insight.intentions.shared" scope="TEST" />
<orderEntry type="library" scope="TEST" name="JUnit5" level="project" />
<orderEntry type="library" scope="TEST" name="JUnit5Jupiter" level="project" />
<orderEntry type="library" scope="TEST" name="JUnit5Launcher" level="project" />
<orderEntry type="library" scope="TEST" name="JUnit5Params" level="project" />
<orderEntry type="module" module-name="intellij.platform.testFramework.junit5" />
</component>
</module>
@@ -0,0 +1,95 @@
// 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.configuration
import com.intellij.openapi.module.Module
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.DependencyScope
import com.intellij.openapi.roots.ModuleRootModificationUtil
import com.intellij.openapi.roots.OrderEnumerator
import com.intellij.openapi.roots.OrderRootType
import com.intellij.openapi.roots.libraries.Library
import com.intellij.testFramework.fixtures.MavenDependencyUtil
import com.intellij.testFramework.junit5.TestApplication
import com.intellij.testFramework.junit5.fixture.TestFixture
import com.intellij.testFramework.junit5.fixture.moduleFixture
import com.intellij.testFramework.junit5.fixture.projectFixture
import org.jetbrains.kotlin.idea.base.platforms.KotlinJavaScriptStdlibDetectorFacility
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
@TestApplication
class StdlibJsDetectorFacilityTest {
companion object {
// the project is reused
val project: TestFixture<Project> = projectFixture()
}
// the module is recreated after each test to clean up module libraries
val module: TestFixture<Module> = project.moduleFixture()
@Test
fun testJsStdlib_1_0_7() {
doTest("org.jetbrains.kotlin:kotlin-js-library:1.0.7", isJsStdlib = true)
}
@Test
fun testJsStdlib_1_1_0() {
doTest("org.jetbrains.kotlin:kotlin-stdlib-js:1.1.0", isJsStdlib = true)
}
@Test
fun testJsStdlib_1_3_0() {
doTest("org.jetbrains.kotlin:kotlin-stdlib-js:1.3.0", isJsStdlib = true)
}
@Test
fun testJsStdlib_1_6_20() {
doTest("org.jetbrains.kotlin:kotlin-stdlib-js:1.6.20", isJsStdlib = true)
}
@Test
fun testJsStdlib_1_9_24() {
doTest("org.jetbrains.kotlin:kotlin-stdlib-js:1.9.24", isJsStdlib = true)
}
@Test
fun testJsStdlib_2_0_0() {
doTest("org.jetbrains.kotlin:kotlin-stdlib-js:2.0.0", isJsStdlib = true, packaging = "klib")
}
@Test
fun testNonStdlib() {
doTest("org.jetbrains:annotations:24.0.1", isJsStdlib = false)
}
@Test
fun testNonJsStdlib() {
doTest("org.jetbrains.kotlin:kotlin-stdlib:1.9.24", isJsStdlib = false)
}
private fun doTest(coordinates: String, isJsStdlib: Boolean, packaging: String = "jar") {
ModuleRootModificationUtil.updateModel(module.get()) { modifiableModel ->
MavenDependencyUtil.addFromMaven(
modifiableModel, coordinates, /* includeTransitiveDependencies = */ false,
DependencyScope.COMPILE, /* additionalRepositories = */ emptyList(), packaging,
)
}
val libraries = mutableListOf<Library>()
OrderEnumerator.orderEntries(module.get()).forEachLibrary { library ->
libraries.add(library)
true
}
val theLibrary = libraries.singleOrNull()
Assertions.assertNotNull(theLibrary, "Expected a single library, got: $libraries")
KotlinJavaScriptStdlibDetectorFacility.isStdlib(module.get().project, library = theLibrary!!)
Assertions.assertEquals(
isJsStdlib, KotlinJavaScriptStdlibDetectorFacility.isStdlib(module.get().project, library = theLibrary),
"Expected $theLibrary to be${" not".takeIf { !isJsStdlib }.orEmpty()} detected as stdlib",
)
val stdlibJar = KotlinJavaScriptStdlibDetectorFacility.getStdlibJar(theLibrary.getFiles(OrderRootType.CLASSES).toList())
if (isJsStdlib) Assertions.assertNotNull(stdlibJar) else Assertions.assertNull(stdlibJar)
}
}