From 1c1ca4ab5eba6e1b5a1bd4c1c6140bddbe941fa7 Mon Sep 17 00:00:00 2001 From: Vadim Salavatov Date: Wed, 5 Aug 2026 17:27:42 +0200 Subject: [PATCH] [plugins] IJPL-251952 constraint resolver: protect against potential self-dependency coming from `provideCompatibilityDependenciesForRemainingCandidates` (cherry picked from commit 7635566925dac4e62778ea4f17c9f9c214dce36b) GitOrigin-RevId: f7c7e40470eef79bdcf6d8c6de421008944befe4 --- .../pluginInitContextConstraintResolver.kt | 14 ++++++++---- .../ide/plugins/PluginDependenciesTest.kt | 17 ++++++++++++++ .../testFramework/src/PluginSetTestBuilder.kt | 22 ++++++++++++++++++- 3 files changed, 48 insertions(+), 5 deletions(-) diff --git a/platform/core-impl/src/com/intellij/ide/plugins/pluginInitContextConstraintResolver.kt b/platform/core-impl/src/com/intellij/ide/plugins/pluginInitContextConstraintResolver.kt index 729a2991fffb..9293c8286aa6 100644 --- a/platform/core-impl/src/com/intellij/ide/plugins/pluginInitContextConstraintResolver.kt +++ b/platform/core-impl/src/com/intellij/ide/plugins/pluginInitContextConstraintResolver.kt @@ -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() } @@ -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() } diff --git a/platform/platform-tests/testSrc/com/intellij/ide/plugins/PluginDependenciesTest.kt b/platform/platform-tests/testSrc/com/intellij/ide/plugins/PluginDependenciesTest.kt index 2a3b06e6ee50..dc8b5d48d10e 100644 --- a/platform/platform-tests/testSrc/com/intellij/ide/plugins/PluginDependenciesTest.kt +++ b/platform/platform-tests/testSrc/com/intellij/ide/plugins/PluginDependenciesTest.kt @@ -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 diff --git a/platform/pluginSystem/testFramework/src/PluginSetTestBuilder.kt b/platform/pluginSystem/testFramework/src/PluginSetTestBuilder.kt index 55ecd7fd20d8..1fa8df3bfabf 100644 --- a/platform/pluginSystem/testFramework/src/PluginSetTestBuilder.kt +++ b/platform/pluginSystem/testFramework/src/PluginSetTestBuilder.kt @@ -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? = null + private var compatibilityDependenciesForRemainingCandidatesProvider: + ((IdeaPluginDescriptorImpl, RemainingCandidatesView) -> Sequence)? = null companion object { @JvmStatic @@ -75,6 +80,12 @@ class PluginSetTestBuilder private constructor( this.explicitPluginSubsetToLoad = pluginsToLoad } + fun withCompatibilityDependenciesForRemainingCandidatesProvider( + provider: (IdeaPluginDescriptorImpl, RemainingCandidatesView) -> Sequence, + ): PluginSetTestBuilder = apply { + compatibilityDependenciesForRemainingCandidatesProvider = provider + } + var buildNumber: String get() = productBuildNumber.toString() set(value) { @@ -92,6 +103,15 @@ class PluginSetTestBuilder private constructor( } override val explicitPluginSubsetToLoad: Set? = this@PluginSetTestBuilder.explicitPluginSubsetToLoad override val currentProductModeId: String = productMode.id + override fun provideCompatibilityDependenciesForRemainingCandidates( + descriptor: IdeaPluginDescriptorImpl, + remainingCandidates: RemainingCandidatesView, + ): Sequence { + 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 -} \ No newline at end of file +}