From 2fc7e1f910e97466ecb3b1928ce41482a8ce904c Mon Sep 17 00:00:00 2001 From: Ilya Korennoy Date: Fri, 6 Feb 2026 14:50:12 +0400 Subject: [PATCH] IJPL-930 Implement ModuleExportedDependenciesGraph GitOrigin-RevId: 02a6758a0d6bff6e0021cd1798d9c5f99c1620c2 --- .../openapi/roots/impl/LibraryScopeCache.java | 29 +- .../ModuleExportedDependenciesGraphTest.kt | 344 ++++++++++++++++++ .../module/ModuleExportedDependenciesGraph.kt | 70 ++++ 3 files changed, 426 insertions(+), 17 deletions(-) create mode 100644 platform/platform-tests/testSrc/com/intellij/workspaceModel/ide/legacyBridge/ModuleExportedDependenciesGraphTest.kt create mode 100644 platform/projectModel-impl/src/com/intellij/workspaceModel/ide/impl/legacyBridge/module/ModuleExportedDependenciesGraph.kt diff --git a/platform/analysis-impl/src/com/intellij/openapi/roots/impl/LibraryScopeCache.java b/platform/analysis-impl/src/com/intellij/openapi/roots/impl/LibraryScopeCache.java index 2a846f94b5f2..b36e934ac2a8 100644 --- a/platform/analysis-impl/src/com/intellij/openapi/roots/impl/LibraryScopeCache.java +++ b/platform/analysis-impl/src/com/intellij/openapi/roots/impl/LibraryScopeCache.java @@ -20,7 +20,6 @@ import com.intellij.openapi.util.registry.Registry; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.platform.backend.workspace.VirtualFileUrls; import com.intellij.platform.backend.workspace.WorkspaceModel; -import com.intellij.platform.workspace.jps.entities.ExtensionsKt; import com.intellij.platform.workspace.jps.entities.InheritedSdkDependency; import com.intellij.platform.workspace.jps.entities.LibraryDependency; import com.intellij.platform.workspace.jps.entities.LibraryEntity; @@ -36,6 +35,8 @@ import com.intellij.psi.search.GlobalSearchScope; import com.intellij.util.ConcurrencyUtil; import com.intellij.util.containers.ConcurrentFactoryMap; import com.intellij.util.containers.ContainerUtil; +import com.intellij.util.graph.Graph; +import com.intellij.workspaceModel.ide.impl.legacyBridge.module.ModuleExportedDependenciesGraph; import com.intellij.workspaceModel.ide.impl.legacyBridge.sdk.SdkBridgeImplKt; import com.intellij.workspaceModel.ide.legacyBridge.ModuleBridges; import kotlin.sequences.SequencesKt; @@ -160,9 +161,10 @@ public final class LibraryScopeCache { LibraryEntity lib = null; List modulesLibraryUsedIn = new ArrayList<>(); + var exportedDependentsGraph = ModuleExportedDependenciesGraph.getInstance(myProject).exportedDependentsGraph(); for (var library: libraries) { lib = library; - modulesLibraryUsedIn.addAll(modulesWithLibraryId(library.getSymbolicId(), currentSnapshot)); + modulesLibraryUsedIn.addAll(findModulesWithLibraryId(library.getSymbolicId(), currentSnapshot, exportedDependentsGraph)); } if (lib != null) { @@ -271,8 +273,9 @@ public final class LibraryScopeCache { } Set modulesWithLibrary = new HashSet<>(); + var exportedDependentsGraph = ModuleExportedDependenciesGraph.getInstance(myProject).exportedDependentsGraph(); for (var library : libraries) { - modulesWithLibrary.addAll(modulesWithLibraryId(library.getSymbolicId(), currentSnapshot)); + modulesWithLibrary.addAll(findModulesWithLibraryId(library.getSymbolicId(), currentSnapshot, exportedDependentsGraph)); } modulesWithLibrary.addAll(index.getModulesForFile(virtualFile, false)); @@ -354,25 +357,17 @@ public final class LibraryScopeCache { } } - private static Set modulesWithLibraryId(LibraryId libraryId, ImmutableEntityStorage currentSnapshot) { - Set modulesWithLibrary = new HashSet<>(); + private static Set findModulesWithLibraryId(LibraryId libraryId, ImmutableEntityStorage currentSnapshot, Graph exportedDependentsGraph) { + Set modulesWithLibrary = new HashSet<>(); var ownerModules = SequencesKt.toList(currentSnapshot.referrers(libraryId, ModuleEntity.class)); + for (var module : ownerModules) { - var moduleBridge = ModuleBridges.findModule(module, currentSnapshot); - if (moduleBridge != null) { - modulesWithLibrary.add(moduleBridge); - } + modulesWithLibrary.add(module); if (exportsLibrary(module, libraryId)) { - var transitiveModules = ExtensionsKt.collectTransitivelyDependentModules(module, currentSnapshot); - for (var depModule : transitiveModules) { - var depModuleBridge = ModuleBridges.findModule(depModule, currentSnapshot); - if (depModuleBridge != null) { - modulesWithLibrary.add(depModuleBridge); - } - } + exportedDependentsGraph.getIn(module).forEachRemaining(modulesWithLibrary::add); } } - return modulesWithLibrary; + return ContainerUtil.map2Set(modulesWithLibrary, (moduleEntity) -> ModuleBridges.findModule(moduleEntity, currentSnapshot)); } private static boolean exportsLibrary(ModuleEntity module, LibraryId libraryId) { diff --git a/platform/platform-tests/testSrc/com/intellij/workspaceModel/ide/legacyBridge/ModuleExportedDependenciesGraphTest.kt b/platform/platform-tests/testSrc/com/intellij/workspaceModel/ide/legacyBridge/ModuleExportedDependenciesGraphTest.kt new file mode 100644 index 000000000000..7f1d97bdd65a --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/workspaceModel/ide/legacyBridge/ModuleExportedDependenciesGraphTest.kt @@ -0,0 +1,344 @@ +// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.workspaceModel.ide.legacyBridge + +import com.intellij.openapi.roots.DependencyScope +import com.intellij.openapi.roots.ModuleRootModificationUtil +import com.intellij.testFramework.junit5.TestApplication +import com.intellij.testFramework.rules.ProjectModelExtension +import com.intellij.workspaceModel.ide.impl.legacyBridge.module.ModuleExportedDependenciesGraph +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension + +@TestApplication +class ModuleExportedDependenciesGraphTest { + @JvmField + @RegisterExtension + val projectModel: ProjectModelExtension = ProjectModelExtension() + + private val graph: ModuleExportedDependenciesGraph + get() = ModuleExportedDependenciesGraph.getInstance(projectModel.project) + + @Test + fun `empty graph has no nodes`() { + val exportedGraph = graph.exportedDependentsGraph() + + assertThat(exportedGraph.nodes) + .describedAs("Empty graph should have no nodes") + .isEmpty() + } + + @Test + fun `single module with no dependencies`() { + val moduleA = projectModel.createModule("module-a").findModuleEntity() + + val exportedGraph = graph.exportedDependentsGraph() + + assertThat(exportedGraph.nodes) + .describedAs("Graph should contain single module") + .hasSize(1) + assertThat(exportedGraph.getIn(moduleA).asSequence().toList()) + .describedAs("Module with no dependencies should have no incoming edges") + .isEmpty() + } + + @Test + fun `graph nodes include all modules`() { + projectModel.createModule("module-a") + projectModel.createModule("module-b") + projectModel.createModule("module-c") + + val exportedGraph = graph.exportedDependentsGraph() + + assertThat(exportedGraph.nodes.map { it.name }) + .describedAs("Graph should contain all created modules") + .containsExactlyInAnyOrder("module-a", "module-b", "module-c") + } + + @Test + fun `linear chain with all exported dependencies`() { + // A ← B (exported) ← C (exported) + val moduleA = projectModel.createModule("module-a") + val moduleB = projectModel.createModule("module-b") + val moduleC = projectModel.createModule("module-c") + + ModuleRootModificationUtil.addDependency(moduleB, moduleA, DependencyScope.COMPILE, true) + ModuleRootModificationUtil.addDependency(moduleC, moduleB, DependencyScope.COMPILE, true) + + val exportedGraph = graph.exportedDependentsGraph() + val moduleAEntity = moduleA.findModuleEntity() + val moduleBEntity = moduleB.findModuleEntity() + + assertThat(exportedGraph.getIn(moduleAEntity).asSequence().map { it.name }.toList()) + .describedAs("Module A should have B and C as transitive dependents (both exported)") + .containsExactlyInAnyOrder("module-b", "module-c") + + assertThat(exportedGraph.getIn(moduleBEntity).asSequence().map { it.name }.toList()) + .describedAs("Module B should have C as dependent") + .containsExactlyInAnyOrder("module-c") + } + + @Test + fun `non-exported dependency breaks transitive chain`() { + // A ← B (NOT exported) ← C (exported) + val moduleA = projectModel.createModule("module-a") + val moduleB = projectModel.createModule("module-b") + val moduleC = projectModel.createModule("module-c") + + ModuleRootModificationUtil.addDependency(moduleB, moduleA, DependencyScope.COMPILE, false) + ModuleRootModificationUtil.addDependency(moduleC, moduleB, DependencyScope.COMPILE, true) + + val exportedGraph = graph.exportedDependentsGraph() + val moduleAEntity = moduleA.findModuleEntity() + val moduleBEntity = moduleB.findModuleEntity() + + assertThat(exportedGraph.getIn(moduleAEntity).asSequence().map { it.name }.toList()) + .describedAs("Module A should only have B (C can't reach A transitively because B doesn't export A)") + .containsExactlyInAnyOrder("module-b") + + assertThat(exportedGraph.getIn(moduleBEntity).asSequence().map { it.name }.toList()) + .describedAs("Module B should have C as dependent") + .containsExactlyInAnyOrder("module-c") + } + + @Test + fun `diamond dependency with all exported`() { + // A ← B (exported) ← D (exported) + // A ← C (exported) ← D (exported) + val moduleA = projectModel.createModule("module-a") + val moduleB = projectModel.createModule("module-b") + val moduleC = projectModel.createModule("module-c") + val moduleD = projectModel.createModule("module-d") + + ModuleRootModificationUtil.addDependency(moduleB, moduleA, DependencyScope.COMPILE, true) + ModuleRootModificationUtil.addDependency(moduleC, moduleA, DependencyScope.COMPILE, true) + ModuleRootModificationUtil.addDependency(moduleD, moduleB, DependencyScope.COMPILE, true) + ModuleRootModificationUtil.addDependency(moduleD, moduleC, DependencyScope.COMPILE, true) + + val exportedGraph = graph.exportedDependentsGraph() + val moduleAEntity = moduleA.findModuleEntity() + + assertThat(exportedGraph.getIn(moduleAEntity).asSequence().map { it.name }.toList()) + .describedAs("Module A should have B, C, and D as dependents (all paths exported)") + .containsExactlyInAnyOrder("module-b", "module-c", "module-d") + } + + @Test + fun `diamond dependency with mixed export flags`() { + // A ← B (exported) ← D (exported) + // A ← C (NOT exported) ← D (exported) + val moduleA = projectModel.createModule("module-a") + val moduleB = projectModel.createModule("module-b") + val moduleC = projectModel.createModule("module-c") + val moduleD = projectModel.createModule("module-d") + + ModuleRootModificationUtil.addDependency(moduleB, moduleA, DependencyScope.COMPILE, true) + ModuleRootModificationUtil.addDependency(moduleC, moduleA, DependencyScope.COMPILE, false) + ModuleRootModificationUtil.addDependency(moduleD, moduleB, DependencyScope.COMPILE, true) + ModuleRootModificationUtil.addDependency(moduleD, moduleC, DependencyScope.COMPILE, true) + + val exportedGraph = graph.exportedDependentsGraph() + val moduleAEntity = moduleA.findModuleEntity() + val moduleCEntity = moduleC.findModuleEntity() + + assertThat(exportedGraph.getIn(moduleAEntity).asSequence().map { it.name }.toList()) + .describedAs("Module A should have B, C, and D (D reaches A via B's exported path)") + .containsExactlyInAnyOrder("module-b", "module-c", "module-d") + + assertThat(exportedGraph.getIn(moduleCEntity).asSequence().map { it.name }.toList()) + .describedAs("Module C should only have D (no transitive propagation because C doesn't export A)") + .containsExactlyInAnyOrder("module-d") + } + + @Test + fun `multiple independent dependency chains`() { + // Chain 1: A ← B (exported) + // Chain 2: C ← D (exported) + val moduleA = projectModel.createModule("module-a") + val moduleB = projectModel.createModule("module-b") + val moduleC = projectModel.createModule("module-c") + val moduleD = projectModel.createModule("module-d") + + ModuleRootModificationUtil.addDependency(moduleB, moduleA, DependencyScope.COMPILE, true) + ModuleRootModificationUtil.addDependency(moduleD, moduleC, DependencyScope.COMPILE, true) + + val exportedGraph = graph.exportedDependentsGraph() + val moduleAEntity = moduleA.findModuleEntity() + val moduleCEntity = moduleC.findModuleEntity() + + assertThat(exportedGraph.getIn(moduleAEntity).asSequence().map { it.name }.toList()) + .describedAs("Module A should only have B from its chain") + .containsExactlyInAnyOrder("module-b") + + assertThat(exportedGraph.getIn(moduleCEntity).asSequence().map { it.name }.toList()) + .describedAs("Module C should only have D from its chain") + .containsExactlyInAnyOrder("module-d") + } + + @Test + fun `complex graph with multiple paths`() { + // A ← B (exported) ← D (exported) + // A ← C (exported) ← D (exported) + // B ← E (exported) + // C ← E (exported) + val moduleA = projectModel.createModule("module-a") + val moduleB = projectModel.createModule("module-b") + val moduleC = projectModel.createModule("module-c") + val moduleD = projectModel.createModule("module-d") + val moduleE = projectModel.createModule("module-e") + + ModuleRootModificationUtil.addDependency(moduleB, moduleA, DependencyScope.COMPILE, true) + ModuleRootModificationUtil.addDependency(moduleC, moduleA, DependencyScope.COMPILE, true) + ModuleRootModificationUtil.addDependency(moduleD, moduleB, DependencyScope.COMPILE, true) + ModuleRootModificationUtil.addDependency(moduleD, moduleC, DependencyScope.COMPILE, true) + ModuleRootModificationUtil.addDependency(moduleE, moduleB, DependencyScope.COMPILE, true) + ModuleRootModificationUtil.addDependency(moduleE, moduleC, DependencyScope.COMPILE, true) + + val exportedGraph = graph.exportedDependentsGraph() + val moduleAEntity = moduleA.findModuleEntity() + + assertThat(exportedGraph.getIn(moduleAEntity).asSequence().map { it.name }.toList()) + .describedAs("Module A should have B, C, D, and E (E reaches A via both B and C)") + .containsExactlyInAnyOrder("module-b", "module-c", "module-d", "module-e") + } + + @Test + fun `different dependency scopes with exported flag`() { + // A ← B (exported, TEST scope) ← C (exported, RUNTIME scope) + val moduleA = projectModel.createModule("module-a") + val moduleB = projectModel.createModule("module-b") + val moduleC = projectModel.createModule("module-c") + + ModuleRootModificationUtil.addDependency(moduleB, moduleA, DependencyScope.TEST, true) + ModuleRootModificationUtil.addDependency(moduleC, moduleB, DependencyScope.RUNTIME, true) + + val exportedGraph = graph.exportedDependentsGraph() + val moduleAEntity = moduleA.findModuleEntity() + + assertThat(exportedGraph.getIn(moduleAEntity).asSequence().map { it.name }.toList()) + .describedAs("Exported flag should work regardless of dependency scope") + .containsExactlyInAnyOrder("module-b", "module-c") + } + + @Test + fun `circular dependency with all exported`() { + // A ← B (exported) ← C (exported) ← A (exported) + val moduleA = projectModel.createModule("module-a") + val moduleB = projectModel.createModule("module-b") + val moduleC = projectModel.createModule("module-c") + + ModuleRootModificationUtil.addDependency(moduleB, moduleA, DependencyScope.COMPILE, true) + ModuleRootModificationUtil.addDependency(moduleC, moduleB, DependencyScope.COMPILE, true) + ModuleRootModificationUtil.addDependency(moduleA, moduleC, DependencyScope.COMPILE, true) + + val exportedGraph = graph.exportedDependentsGraph() + val moduleAEntity = moduleA.findModuleEntity() + val moduleBEntity = moduleB.findModuleEntity() + val moduleCEntity = moduleC.findModuleEntity() + + // In a circular dependency with all exported, each module sees all modules in the cycle (including itself) + assertThat(exportedGraph.getIn(moduleAEntity).asSequence().map { it.name }.toList()) + .describedAs("Module A should have B, C, and itself as dependents in circular chain") + .containsExactlyInAnyOrder("module-a", "module-b", "module-c") + + assertThat(exportedGraph.getIn(moduleBEntity).asSequence().map { it.name }.toList()) + .describedAs("Module B should have C, A, and itself as dependents in circular chain") + .containsExactlyInAnyOrder("module-a", "module-b", "module-c") + + assertThat(exportedGraph.getIn(moduleCEntity).asSequence().map { it.name }.toList()) + .describedAs("Module C should have A, B, and itself as dependents in circular chain") + .containsExactlyInAnyOrder("module-a", "module-b", "module-c") + } + + @Test + fun `circular dependency with non-exported break`() { + // A ← B (exported) ← C (NOT exported) ← A (exported) + val moduleA = projectModel.createModule("module-a") + val moduleB = projectModel.createModule("module-b") + val moduleC = projectModel.createModule("module-c") + + ModuleRootModificationUtil.addDependency(moduleB, moduleA, DependencyScope.COMPILE, true) + ModuleRootModificationUtil.addDependency(moduleC, moduleB, DependencyScope.COMPILE, false) + ModuleRootModificationUtil.addDependency(moduleA, moduleC, DependencyScope.COMPILE, true) + + val exportedGraph = graph.exportedDependentsGraph() + val moduleBEntity = moduleB.findModuleEntity() + + assertThat(exportedGraph.getIn(moduleBEntity).asSequence().map { it.name }.toList()) + .describedAs("Non-exported dependency breaks circular chain; C shouldn't propagate to A") + .containsExactlyInAnyOrder("module-c") + } + + @Test + fun `transitive exported dependencies validation`() { + // A ← B (exported) ← C (exported) ← D (NOT exported) + // D is added to A's dependents because it directly depends on C + // But D is not added to the BFS queue because D doesn't export C + val moduleA = projectModel.createModule("module-a") + val moduleB = projectModel.createModule("module-b") + val moduleC = projectModel.createModule("module-c") + val moduleD = projectModel.createModule("module-d") + + ModuleRootModificationUtil.addDependency(moduleB, moduleA, DependencyScope.COMPILE, true) + ModuleRootModificationUtil.addDependency(moduleC, moduleB, DependencyScope.COMPILE, true) + ModuleRootModificationUtil.addDependency(moduleD, moduleC, DependencyScope.COMPILE, false) + + val exportedGraph = graph.exportedDependentsGraph() + val moduleAEntity = moduleA.findModuleEntity() + val moduleCEntity = moduleC.findModuleEntity() + val moduleBEntity = moduleB.findModuleEntity() + + // A's dependents: B (direct, exported), C (transitive via B), D (transitive via C, but D doesn't export C) + assertThat(exportedGraph.getIn(moduleAEntity).asSequence().map { it.name }.toList()) + .describedAs("Module A should have B, C, and D as dependents (D is included because C is in the queue)") + .containsExactlyInAnyOrder("module-b", "module-c", "module-d") + + // B's dependents: C (direct, exported), D (transitive via C) + assertThat(exportedGraph.getIn(moduleBEntity).asSequence().map { it.name }.toList()) + .describedAs("Module B should have C and D as dependents") + .containsExactlyInAnyOrder("module-c", "module-d") + + // C's dependents: only D (direct, not exported) + assertThat(exportedGraph.getIn(moduleCEntity).asSequence().map { it.name }.toList()) + .describedAs("Module C should only have D (no transitive propagation because D doesn't export C)") + .containsExactlyInAnyOrder("module-d") + } + + @Test + fun `getIn returns direct dependents`() { + // A ← B (NOT exported) + // A ← C (NOT exported) + val moduleA = projectModel.createModule("module-a") + val moduleB = projectModel.createModule("module-b") + val moduleC = projectModel.createModule("module-c") + + ModuleRootModificationUtil.addDependency(moduleB, moduleA, DependencyScope.COMPILE, false) + ModuleRootModificationUtil.addDependency(moduleC, moduleA, DependencyScope.COMPILE, false) + + val exportedGraph = graph.exportedDependentsGraph() + val moduleAEntity = moduleA.findModuleEntity() + + assertThat(exportedGraph.getIn(moduleAEntity).asSequence().map { it.name }.toList()) + .describedAs("getIn() should return direct dependents even when not exported") + .containsExactlyInAnyOrder("module-b", "module-c") + } + + @Test + fun `getOut returns module dependencies`() { + // A ← B (exported) + // C ← B (NOT exported) + val moduleA = projectModel.createModule("module-a") + val moduleB = projectModel.createModule("module-b") + val moduleC = projectModel.createModule("module-c") + + ModuleRootModificationUtil.addDependency(moduleB, moduleA, DependencyScope.COMPILE, true) + ModuleRootModificationUtil.addDependency(moduleB, moduleC, DependencyScope.COMPILE, false) + + val exportedGraph = graph.exportedDependentsGraph() + val moduleBEntity = moduleB.findModuleEntity() + + assertThat(exportedGraph.getOut(moduleBEntity).asSequence().map { it.name }.toList()) + .describedAs("getOut() should return all dependencies") + .containsExactlyInAnyOrder("module-a", "module-c") + } +} diff --git a/platform/projectModel-impl/src/com/intellij/workspaceModel/ide/impl/legacyBridge/module/ModuleExportedDependenciesGraph.kt b/platform/projectModel-impl/src/com/intellij/workspaceModel/ide/impl/legacyBridge/module/ModuleExportedDependenciesGraph.kt new file mode 100644 index 000000000000..871175671f7d --- /dev/null +++ b/platform/projectModel-impl/src/com/intellij/workspaceModel/ide/impl/legacyBridge/module/ModuleExportedDependenciesGraph.kt @@ -0,0 +1,70 @@ +// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.workspaceModel.ide.impl.legacyBridge.module + +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import com.intellij.openapi.project.Project +import com.intellij.platform.backend.workspace.WorkspaceModel +import com.intellij.platform.backend.workspace.impl.WorkspaceModelInternal +import com.intellij.platform.workspace.jps.entities.ModuleDependency +import com.intellij.platform.workspace.jps.entities.ModuleEntity +import com.intellij.platform.workspace.storage.CachedValue +import com.intellij.platform.workspace.storage.EntityStorage +import com.intellij.platform.workspace.storage.VersionedEntityStorage +import com.intellij.platform.workspace.storage.referrers +import com.intellij.util.graph.CachingSemiGraph +import com.intellij.util.graph.Graph +import com.intellij.util.graph.GraphGenerator +import com.intellij.util.graph.InboundSemiGraph +import org.jetbrains.annotations.ApiStatus + +/** + * [Graph.getIn] returns [ModuleEntity] which depend on the given module taking into account exported dependencies. + * + * [Graph.getOut] returns [ModuleEntity] on which the given module depends taking into account exported dependencies. + */ +@ApiStatus.Internal +@Service(Service.Level.PROJECT) +class ModuleExportedDependenciesGraph(project: Project) { + + private val entityStore: VersionedEntityStorage = (WorkspaceModel.getInstance(project) as WorkspaceModelInternal).entityStorage + + fun exportedDependentsGraph(): Graph = entityStore.cachedValue(exportedDependentsGraph) + + private val exportedDependentsGraph = CachedValue { storage -> + buildGraph(storage) + } + + private fun buildGraph(storage: EntityStorage): Graph { + return GraphGenerator.generate(CachingSemiGraph.cache(object : InboundSemiGraph { + override fun getNodes(): Collection = + storage.entities(ModuleEntity::class.java).toList() + + override fun getIn(module: ModuleEntity): Iterator { + return collectDependentModules(module, storage).iterator() + } + })) + } + + private fun collectDependentModules(module: ModuleEntity, storage: EntityStorage): Set { + val queue = ArrayDeque() + val result = HashSet() + queue.add(module) + while (queue.isNotEmpty()) { + val currentModule = queue.removeFirst() + for (referrer in storage.referrers(currentModule.symbolicId)) { + val dependency = + referrer.dependencies.filterIsInstance().find { it.module == currentModule.symbolicId } ?: continue + if (result.add(referrer) && dependency.exported) { + queue.add(referrer) + } + } + } + return result + } + + companion object { + @JvmStatic + fun getInstance(project: Project): ModuleExportedDependenciesGraph = project.service() + } +}