[plugins] IJPL-251952 constraint resolver: protect against potential self-dependency coming from provideCompatibilityDependenciesForRemainingCandidates

(cherry picked from commit 7635566925dac4e62778ea4f17c9f9c214dce36b)

GitOrigin-RevId: f7c7e40470eef79bdcf6d8c6de421008944befe4
This commit is contained in:
Vadim Salavatov
2026-08-19 18:42:06 +00:00
committed by intellij-monorepo-bot
parent b60484177b
commit 1c1ca4ab5e
3 changed files with 48 additions and 5 deletions
@@ -400,7 +400,10 @@ private class PluginSetConstraintsResolver(
}
val compatibilityDependencies = initContext.provideCompatibilityDependenciesForRemainingCandidates(descriptor, remainingCandidatesView)
.mapNotNullTo(ArrayList()) { remainingCandidatesView.resolveReference(it) }
.mapNotNullTo(ArrayList()) { dependencyRef ->
remainingCandidatesView.resolveReference(dependencyRef)
?.takeIf { it !== descriptor }
}
if (compatibilityDependencies.isNotEmpty()) {
contributeDependencies(compatibilityDependencies)
}
@@ -456,8 +459,11 @@ private class PluginSetConstraintsResolver(
val descriptorGraph = DFSTBuilder(DescriptorGraphAdapter(remainingCandidates, resolvedDependents))
if (!descriptorGraph.isAcyclic) {
for (component in descriptorGraph.components) {
if (component.size <= 1) {
continue
if (component.size == 1) {
val selfDependent = component.first() in resolvedDependencies[component.first()].orEmpty()
if (!selfDependent) {
continue
}
}
val component = component.sortedWith(compareBy { it.pluginId }) // makes result stable
val cycleNodesWithDependencies = component.associateWith { ArrayList<IdeaPluginDescriptorImpl>() }
@@ -512,7 +518,7 @@ private class PluginSetConstraintsResolver(
if (!dfstBuilder.isAcyclic) {
for (component in dfstBuilder.components) {
if (component.size <= 1) {
continue
continue // no self-dependency expected: implied by filtering in dependency list construction above
}
val component = component.sortedWith(compareBy { it.representativeModule.pluginId }) // make result stable
val cycleNodesWithDependencies = component.associateWith { ArrayList<RuntimeModuleGroup>() }
@@ -1,6 +1,7 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.ide.plugins
import com.intellij.ide.plugins.PluginDependencyAnalysis.DependencyRef
import com.intellij.ide.plugins.cl.PluginClassLoader
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.extensions.PluginId
@@ -22,6 +23,7 @@ import com.intellij.testFramework.rules.InMemoryFsExtension
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Timeout
import org.junit.jupiter.api.extension.RegisterExtension
import java.nio.file.FileVisitResult
@@ -856,6 +858,21 @@ internal class PluginDependenciesTest {
@Nested
inner class ImplicitDependencyAdditionTests {
@Test
@Timeout(10)
fun `soft compatibility dependency on self is ignored`() {
plugin("foo") {}.installAt(pluginDirPath)
val pluginSet = PluginSetTestBuilder.fromPath(pluginDirPath)
.withCompatibilityDependenciesForRemainingCandidatesProvider { descriptor, remainingCandidates ->
check(remainingCandidates.resolvePluginId(descriptor.pluginId) === descriptor)
sequenceOf(DependencyRef.of(descriptor.pluginId))
}
.build()
val foo = pluginSet.getEnabledPlugin("foo")
assertThat(pluginSet.resolvedPluginSet?.getDirectResolvedDependencies(foo)).doesNotContain(foo)
}
@Test
fun `legacy plugin gets implicit java dependency when all modules marker is present`() {
// marker enables implicit dependencies for legacy plugins
@@ -2,9 +2,12 @@
package com.intellij.platform.pluginSystem.testFramework
import com.intellij.ide.plugins.DiscoveredPluginsList
import com.intellij.ide.plugins.IdeaPluginDescriptorImpl
import com.intellij.ide.plugins.PluginDependencyAnalysis.DependencyRef
import com.intellij.ide.plugins.PluginDescriptorLoadingContext
import com.intellij.ide.plugins.PluginInitContextFactory
import com.intellij.ide.plugins.PluginInitializationContext
import com.intellij.ide.plugins.PluginInitializationContext.RemainingCandidatesView
import com.intellij.ide.plugins.PluginLoadingErrorReportingPolicy
import com.intellij.ide.plugins.PluginMainDescriptor
import com.intellij.ide.plugins.PluginManagerCore
@@ -32,6 +35,8 @@ class PluginSetTestBuilder private constructor(
private var customCoreLoader: UrlClassLoader? = null
private var productMode: ProductMode = ProductMode.MONOLITH
private var explicitPluginSubsetToLoad: Set<PluginId>? = null
private var compatibilityDependenciesForRemainingCandidatesProvider:
((IdeaPluginDescriptorImpl, RemainingCandidatesView) -> Sequence<DependencyRef>)? = null
companion object {
@JvmStatic
@@ -75,6 +80,12 @@ class PluginSetTestBuilder private constructor(
this.explicitPluginSubsetToLoad = pluginsToLoad
}
fun withCompatibilityDependenciesForRemainingCandidatesProvider(
provider: (IdeaPluginDescriptorImpl, RemainingCandidatesView) -> Sequence<DependencyRef>,
): PluginSetTestBuilder = apply {
compatibilityDependenciesForRemainingCandidatesProvider = provider
}
var buildNumber: String
get() = productBuildNumber.toString()
set(value) {
@@ -92,6 +103,15 @@ class PluginSetTestBuilder private constructor(
}
override val explicitPluginSubsetToLoad: Set<PluginId>? = this@PluginSetTestBuilder.explicitPluginSubsetToLoad
override val currentProductModeId: String = productMode.id
override fun provideCompatibilityDependenciesForRemainingCandidates(
descriptor: IdeaPluginDescriptorImpl,
remainingCandidates: RemainingCandidatesView,
): Sequence<DependencyRef> {
compatibilityDependenciesForRemainingCandidatesProvider?.let { provider ->
return provider(descriptor, remainingCandidates)
}
return super.provideCompatibilityDependenciesForRemainingCandidates(descriptor, remainingCandidates)
}
}
}
@@ -120,4 +140,4 @@ class PluginSetTestBuilder private constructor(
}
fun build(): PluginSet = buildState().pluginSet
}
}