[ExternalSystem|Sync] cleanup: extract ModifiableWorkspaceModel#updateSubstitutions

Encapsulate all dependency substitution functionality inside the ModifiableWorkspaceModel class.

### Issues
  * IDEA-134885 Support substitution of library dependency with module dependency when the module is a part of another Maven or Gradle project

GitOrigin-RevId: 6c8817ca2e0eee703137244d747fb07c4cb0775f
This commit is contained in:
Sergei Vorobyov
2025-02-10 16:20:12 +01:00
committed by intellij-monorepo-bot
parent 869a9954be
commit 4be8b382f4
20 changed files with 632 additions and 432 deletions

View File

@@ -46,21 +46,13 @@ c:com.intellij.openapi.externalSystem.importing.ImportSpecBuilder
- com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskNotificationEvent
- <init>(com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskId,com.intellij.build.events.BuildEvent):V
- getBuildEvent():com.intellij.build.events.BuildEvent
*:com.intellij.openapi.externalSystem.service.project.ExternalSystemCoordinateContributor
- *sf:Companion:com.intellij.openapi.externalSystem.service.project.ExternalSystemCoordinateContributor$Companion
- findModuleCoordinate(com.intellij.openapi.module.Module):com.intellij.openapi.externalSystem.model.project.ProjectCoordinate
*f:com.intellij.openapi.externalSystem.service.project.ExternalSystemCoordinateContributor$Companion
com.intellij.openapi.externalSystem.service.project.ExternalSystemProjectResolver
- *:resolveProjectInfo(com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskId,java.lang.String,Z,com.intellij.openapi.externalSystem.model.settings.ExternalSystemExecutionSettings,com.intellij.openapi.externalSystem.importing.ProjectResolverPolicy,com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskNotificationListener):com.intellij.openapi.externalSystem.model.DataNode
com.intellij.openapi.externalSystem.service.project.IdeModifiableModelsProvider
- com.intellij.openapi.externalSystem.service.project.IdeModelsProvider
- com.intellij.openapi.util.UserDataHolder
- *a:findModifiableModel(java.lang.Class):com.intellij.openapi.externalSystem.service.project.ModifiableModel
- *a:findModuleByPublication(com.intellij.openapi.externalSystem.model.project.ProjectCoordinate):java.lang.String
- *a:getModifiableModel(java.lang.Class):com.intellij.openapi.externalSystem.service.project.ModifiableModel
- *a:isSubstituted(java.lang.String):Z
- *a:registerModulePublication(com.intellij.openapi.module.Module,com.intellij.openapi.externalSystem.model.project.ProjectCoordinate):V
- *a:trySubstitute(com.intellij.openapi.module.Module,com.intellij.openapi.roots.LibraryOrderEntry,com.intellij.openapi.externalSystem.model.project.ProjectCoordinate):com.intellij.openapi.roots.ModuleOrderEntry
*:com.intellij.openapi.externalSystem.service.project.ModifiableModel
- a:commit():V
- a:dispose():V

View File

@@ -1012,6 +1012,10 @@ com.intellij.openapi.externalSystem.service.project.ExternalProjectRefreshCallba
- onFailure(java.lang.String,java.lang.String):V
- onSuccess(com.intellij.openapi.externalSystem.model.DataNode):V
- onSuccess(com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskId,com.intellij.openapi.externalSystem.model.DataNode):V
com.intellij.openapi.externalSystem.service.project.ExternalSystemCoordinateContributor
- sf:Companion:com.intellij.openapi.externalSystem.service.project.ExternalSystemCoordinateContributor$Companion
- findModuleCoordinate(com.intellij.openapi.module.Module):com.intellij.openapi.externalSystem.model.project.ProjectCoordinate
f:com.intellij.openapi.externalSystem.service.project.ExternalSystemCoordinateContributor$Companion
com.intellij.openapi.externalSystem.service.project.ExternalSystemProjectResolver
- a:cancelTask(com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskId,com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskNotificationListener):Z
- resolveProjectInfo(com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskId,java.lang.String,Z,com.intellij.openapi.externalSystem.model.settings.ExternalSystemExecutionSettings,com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskNotificationListener):com.intellij.openapi.externalSystem.model.DataNode

View File

@@ -10,7 +10,6 @@ import org.jetbrains.annotations.ApiStatus
* The import can use that mapping for binary dependencies substitution on module dependencies
* across unrelated projects based on any build system or another kind of project modules generator.
*/
@ApiStatus.Experimental
interface ExternalSystemCoordinateContributor {
/**

View File

@@ -19,12 +19,9 @@ import com.intellij.facet.ModifiableFacetModel;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.extensions.ExtensionPointName;
import com.intellij.openapi.externalSystem.model.project.ModuleData;
import com.intellij.openapi.externalSystem.model.project.ProjectCoordinate;
import com.intellij.openapi.module.ModifiableModuleModel;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.roots.LibraryOrderEntry;
import com.intellij.openapi.roots.ModifiableRootModel;
import com.intellij.openapi.roots.ModuleOrderEntry;
import com.intellij.openapi.roots.ProjectModelExternalSource;
import com.intellij.openapi.roots.libraries.Library;
import com.intellij.openapi.roots.libraries.LibraryTable;
@@ -51,6 +48,10 @@ public interface IdeModifiableModelsProvider extends IdeModelsProvider, UserData
@NotNull
ModifiableModuleModel getModifiableModuleModel();
@NotNull
@ApiStatus.Internal
ModifiableWorkspaceModel getModifiableWorkspaceModel();
@NotNull
ModifiableRootModel getModifiableRootModel(Module module);
@@ -86,18 +87,4 @@ public interface IdeModifiableModelsProvider extends IdeModelsProvider, UserData
@Nullable
String getProductionModuleName(Module module);
@ApiStatus.Experimental
void registerModulePublication(Module module, ProjectCoordinate modulePublication);
@ApiStatus.Experimental
@Nullable
String findModuleByPublication(ProjectCoordinate publicationId);
@ApiStatus.Experimental
@Nullable
ModuleOrderEntry trySubstitute(Module ownerModule, LibraryOrderEntry libraryOrderEntry, ProjectCoordinate publicationId);
@ApiStatus.Experimental
boolean isSubstituted(String libraryName);
}

View File

@@ -0,0 +1,24 @@
// 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.openapi.externalSystem.service.project
import com.intellij.openapi.roots.libraries.Library
import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
interface ModifiableWorkspaceModel {
fun updateLibrarySubstitutions()
fun isLibrarySubstituted(library: Library): Boolean
fun commit()
companion object {
val NOP = object : ModifiableWorkspaceModel {
override fun updateLibrarySubstitutions() = Unit
override fun isLibrarySubstituted(library: Library): Boolean = false
override fun commit() = Unit
}
}
}

View File

@@ -68,15 +68,6 @@ c:com.intellij.openapi.externalSystem.service.execution.ExternalSystemProcessHan
- a:getPathMapper():com.intellij.util.PathMapper
com.intellij.openapi.externalSystem.service.notification.ExternalSystemNotificationExtension
- *:isInternalError(java.lang.Throwable):Z
*f:com.intellij.openapi.externalSystem.service.project.ModifiableWorkspace
- addSubstitution(java.lang.String,java.lang.String,java.lang.String,com.intellij.openapi.roots.DependencyScope):V
- commit():V
- findModule(com.intellij.openapi.externalSystem.model.project.ProjectCoordinate):java.lang.String
- getSubstitutedLibrary(java.lang.String):java.lang.String
- isSubstituted(java.lang.String):Z
- isSubstitution(java.lang.String,java.lang.String,com.intellij.openapi.roots.DependencyScope):Z
- register(com.intellij.openapi.externalSystem.model.project.ProjectCoordinate,com.intellij.openapi.module.Module):V
- removeSubstitution(java.lang.String,java.lang.String,java.lang.String,com.intellij.openapi.roots.DependencyScope):V
f:com.intellij.openapi.externalSystem.service.ui.ExternalSystemJdkComboBox
- com.intellij.openapi.ui.ComboBoxWithWidePopup
- *:select(java.lang.String):V

View File

@@ -520,7 +520,6 @@ a:com.intellij.openapi.externalSystem.service.project.AbstractIdeModifiableModel
- findIdeLibrary(com.intellij.openapi.externalSystem.model.project.LibraryData):com.intellij.openapi.roots.libraries.Library
- findIdeModule(java.lang.String):com.intellij.openapi.module.Module
- findModifiableModel(java.lang.Class):com.intellij.openapi.externalSystem.service.project.ModifiableModel
- findModuleByPublication(com.intellij.openapi.externalSystem.model.project.ProjectCoordinate):java.lang.String
- getAllDependentModules(com.intellij.openapi.module.Module):java.util.List
- getAllLibraries():com.intellij.openapi.roots.libraries.Library[]
- getContentRoots(com.intellij.openapi.module.Module):com.intellij.openapi.vfs.VirtualFile[]
@@ -539,15 +538,11 @@ a:com.intellij.openapi.externalSystem.service.project.AbstractIdeModifiableModel
- getSourceRoots(com.intellij.openapi.module.Module):com.intellij.openapi.vfs.VirtualFile[]
- getSourceRoots(com.intellij.openapi.module.Module,Z):com.intellij.openapi.vfs.VirtualFile[]
- getUserData(com.intellij.openapi.util.Key):java.lang.Object
- isSubstituted(java.lang.String):Z
- newModule(com.intellij.openapi.externalSystem.model.project.ModuleData):com.intellij.openapi.module.Module
- newModule(java.lang.String,java.lang.String):com.intellij.openapi.module.Module
- putUserData(com.intellij.openapi.util.Key,java.lang.Object):V
- registerModulePublication(com.intellij.openapi.module.Module,com.intellij.openapi.externalSystem.model.project.ProjectCoordinate):V
- removeLibrary(com.intellij.openapi.roots.libraries.Library):V
- setTestModuleProperties(com.intellij.openapi.module.Module,java.lang.String):V
- trySubstitute(com.intellij.openapi.module.Module,com.intellij.openapi.roots.LibraryOrderEntry,com.intellij.openapi.externalSystem.model.project.ProjectCoordinate):com.intellij.openapi.roots.ModuleOrderEntry
- p:updateSubstitutions():V
pc:com.intellij.openapi.externalSystem.service.project.AbstractIdeModifiableModelsProvider$MyUserDataHolderBase
- com.intellij.openapi.util.UserDataHolderBase
- p:<init>():V

View File

@@ -7,17 +7,11 @@ import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.application.ReadAction;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.externalSystem.ExternalSystemManager;
import com.intellij.openapi.externalSystem.model.DataNode;
import com.intellij.openapi.externalSystem.model.ExternalProjectInfo;
import com.intellij.openapi.externalSystem.model.ProjectKeys;
import com.intellij.openapi.externalSystem.model.project.LibraryData;
import com.intellij.openapi.externalSystem.model.project.ModuleData;
import com.intellij.openapi.externalSystem.model.project.ProjectCoordinate;
import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil;
import com.intellij.openapi.module.ModifiableModuleModel;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleManager;
import com.intellij.openapi.module.ModuleWithNameAlreadyExists;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.*;
@@ -32,11 +26,11 @@ import com.intellij.openapi.util.io.FileUtilRt;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.ObjectUtils;
import com.intellij.util.containers.ClassMap;
import com.intellij.util.containers.ContainerUtil;
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;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -47,12 +41,12 @@ public abstract class AbstractIdeModifiableModelsProvider extends IdeModelsProvi
private static final Logger LOG = Logger.getInstance(AbstractIdeModifiableModelsProvider.class);
protected ModifiableModuleModel myModifiableModuleModel;
private ModifiableWorkspaceModel myModifiableWorkspaceModel;
protected final Map<Module, ModifiableRootModel> myModifiableRootModels = new HashMap<>();
protected final Map<Module, ModifiableFacetModel> myModifiableFacetModels = new HashMap<>();
protected final Map<Module, String> myProductionModulesForTestModules = new HashMap<>();
protected final Map<Library, Library.ModifiableModel> myModifiableLibraryModels = new IdentityHashMap<>();
protected final ClassMap<ModifiableModel> myModifiableModels = new ClassMap<>();
private @Nullable ModifiableWorkspace myModifiableWorkspace;
protected final MyUserDataHolderBase myUserData;
private volatile boolean myDisposed;
@@ -173,6 +167,18 @@ public abstract class AbstractIdeModifiableModelsProvider extends IdeModelsProvi
return myModifiableModuleModel;
}
@Override
@ApiStatus.Internal
public @NotNull ModifiableWorkspaceModel getModifiableWorkspaceModel() {
if (myModifiableWorkspaceModel == null) {
myModifiableWorkspaceModel = ReadAction.compute(() -> {
var workspace = ExternalProjectsWorkspace.getInstance(myProject);
return workspace.getModifiableModel(this);
});
}
return myModifiableWorkspaceModel;
}
@Override
public @NotNull ModifiableRootModel getModifiableRootModel(Module module) {
return (ModifiableRootModel)getRootModel(module);
@@ -217,16 +223,6 @@ public abstract class AbstractIdeModifiableModelsProvider extends IdeModelsProvi
return myModifiableLibraryModels.computeIfAbsent(library, k -> doGetModifiableLibraryModel(library));
}
private @Nullable ModifiableWorkspace getModifiableWorkspace() {
if (!ExternalProjectsWorkspaceImpl.isDependencySubstitutionEnabled()) {
return null;
}
if (myModifiableWorkspace == null) {
myModifiableWorkspace = doGetModifiableWorkspace();
}
return myModifiableWorkspace;
}
@Override
public String @NotNull [] getLibraryUrls(@NotNull Library library, @NotNull OrderRootType type) {
final Library.ModifiableModel model = myModifiableLibraryModels.get(library);
@@ -251,11 +247,6 @@ public abstract class AbstractIdeModifiableModelsProvider extends IdeModelsProvi
return list;
}
private ModifiableWorkspace doGetModifiableWorkspace() {
return ReadAction.compute(() -> myProject.getService(ExternalProjectsWorkspaceImpl.class)
.createModifiableWorkspace(() -> Arrays.asList(getModules())));
}
private Graph<Module> getModuleGraph() {
return GraphGenerator.generate(CachingSemiGraph.cache(new InboundSemiGraph<>() {
@Override
@@ -315,44 +306,6 @@ public abstract class AbstractIdeModifiableModelsProvider extends IdeModelsProvi
return myProductionModulesForTestModules.get(module);
}
@Override
public ModuleOrderEntry trySubstitute(Module ownerModule, LibraryOrderEntry libraryOrderEntry, ProjectCoordinate publicationId) {
ModifiableWorkspace workspace = getModifiableWorkspace();
String workspaceModuleCandidate = workspace == null ? null : workspace.findModule(publicationId);
Module workspaceModule = workspaceModuleCandidate == null ? null : findIdeModule(workspaceModuleCandidate);
if (workspaceModule == null) {
return null;
}
ModifiableRootModel modifiableRootModel = getModifiableRootModel(ownerModule);
ModuleOrderEntry moduleOrderEntry = modifiableRootModel.findModuleOrderEntry(workspaceModule);
if (moduleOrderEntry == null) { // if that module exists already (after re-import)
moduleOrderEntry = modifiableRootModel.addModuleOrderEntry(workspaceModule);
}
moduleOrderEntry.setScope(libraryOrderEntry.getScope());
moduleOrderEntry.setExported(libraryOrderEntry.isExported());
workspace.addSubstitution(ownerModule.getName(),
workspaceModule.getName(),
libraryOrderEntry.getLibraryName(),
libraryOrderEntry.getScope());
modifiableRootModel.removeOrderEntry(libraryOrderEntry);
return moduleOrderEntry;
}
@Override
public void registerModulePublication(Module module, ProjectCoordinate modulePublication) {
ModifiableWorkspace workspace = getModifiableWorkspace();
if (workspace != null) {
workspace.register(modulePublication, module);
}
}
@Override
public boolean isSubstituted(String libraryName) {
ModifiableWorkspace workspace = getModifiableWorkspace();
if (workspace == null) return false;
return workspace.isSubstituted(libraryName);
}
@Override
public @Nullable <T> T getUserData(@NotNull Key<T> key) {
return myUserData.getUserData(key);
@@ -362,93 +315,4 @@ public abstract class AbstractIdeModifiableModelsProvider extends IdeModelsProvi
public <T> void putUserData(@NotNull Key<T> key, @Nullable T value) {
myUserData.putUserData(key, value);
}
@Override
public @Nullable String findModuleByPublication(ProjectCoordinate publicationId) {
ModifiableWorkspace workspace = getModifiableWorkspace();
return workspace == null ? null : workspace.findModule(publicationId);
}
protected void updateSubstitutions() {
ModifiableWorkspace workspace = getModifiableWorkspace();
if (workspace == null) return;
final List<String> oldModules = ContainerUtil.map(ModuleManager.getInstance(myProject).getModules(), module -> module.getName());
final List<String> newModules = ContainerUtil.map(myModifiableModuleModel.getModules(), module -> module.getName());
final Collection<String> removedModules = new HashSet<>(oldModules);
removedModules.removeAll(newModules);
Map<String, String> toSubstitute = new HashMap<>();
ProjectDataManager projectDataManager = ProjectDataManager.getInstance();
ExternalSystemManager.EP_NAME.forEachExtensionSafe(manager -> {
Collection<ExternalProjectInfo> projectsData = projectDataManager.getExternalProjectsData(myProject, manager.getSystemId());
for (ExternalProjectInfo projectInfo: projectsData) {
if (projectInfo.getExternalProjectStructure() == null) {
continue;
}
Collection<DataNode<LibraryData>> libraryNodes =
ExternalSystemApiUtil.findAll(projectInfo.getExternalProjectStructure(), ProjectKeys.LIBRARY);
for (DataNode<LibraryData> libraryNode: libraryNodes) {
String substitutionModuleCandidate = workspace.findModule(libraryNode.getData());
if (substitutionModuleCandidate != null) {
toSubstitute.put(libraryNode.getData().getInternalName(), substitutionModuleCandidate);
}
}
}
});
for (Module module: getModules()) {
ModifiableRootModel modifiableRootModel = getModifiableRootModel(module);
boolean changed = false;
OrderEntry[] entries = modifiableRootModel.getOrderEntries();
for (int i = 0, length = entries.length; i < length; i++) {
OrderEntry orderEntry = entries[i];
if (orderEntry instanceof ModuleOrderEntry) {
String workspaceModule = ((ModuleOrderEntry)orderEntry).getModuleName();
if (removedModules.contains(workspaceModule)) {
DependencyScope scope = ((ModuleOrderEntry)orderEntry).getScope();
if (workspace.isSubstitution(module.getName(), workspaceModule, scope)) {
String libraryName = workspace.getSubstitutedLibrary(workspaceModule);
if (libraryName != null) {
Library library = getLibraryByName(libraryName);
if (library != null) {
modifiableRootModel.removeOrderEntry(orderEntry);
entries[i] = modifiableRootModel.addLibraryEntry(library);
changed = true;
workspace.removeSubstitution(module.getName(), workspaceModule, libraryName, scope);
}
}
}
}
}
if (!(orderEntry instanceof LibraryOrderEntry libraryOrderEntry)) continue;
if (!libraryOrderEntry.isModuleLevel() && libraryOrderEntry.getLibraryName() != null) {
String workspaceModule = toSubstitute.get(libraryOrderEntry.getLibraryName());
if (workspaceModule != null) {
Module ideModule = findIdeModule(workspaceModule);
if (ideModule != null) {
ModuleOrderEntry moduleOrderEntry = modifiableRootModel.addModuleOrderEntry(ideModule);
moduleOrderEntry.setScope(libraryOrderEntry.getScope());
modifiableRootModel.removeOrderEntry(orderEntry);
entries[i] = moduleOrderEntry;
changed = true;
workspace.addSubstitution(module.getName(), workspaceModule,
libraryOrderEntry.getLibraryName(),
libraryOrderEntry.getScope());
}
}
}
}
if (changed) {
modifiableRootModel.rearrangeOrderEntries(entries);
}
}
workspace.commit();
}
}

View File

@@ -0,0 +1,44 @@
// 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.openapi.externalSystem.service.project
import com.intellij.openapi.components.*
import com.intellij.openapi.components.StoragePathMacros.CACHE_FILE
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.registry.Registry
import com.intellij.util.concurrency.annotations.RequiresReadLock
import com.intellij.util.xmlb.annotations.MapAnnotation
import com.intellij.util.xmlb.annotations.Property
import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
@Service(Service.Level.PROJECT)
@State(name = "ExternalProjectsWorkspace", storages = [Storage(CACHE_FILE)])
class ExternalProjectsWorkspace(
private val project: Project,
) : SimplePersistentStateComponent<ExternalProjectsWorkspace.State>(State()) {
class State : BaseState() {
@get:Property(surroundWithTag = false)
@get:MapAnnotation(
surroundWithTag = false, surroundKeyWithTag = false, surroundValueWithTag = false,
entryTagName = "module", keyAttributeName = "name", valueAttributeName = "library")
var librarySubstitutions by map<String, String>()
}
@RequiresReadLock
fun getModifiableModel(modelProvider: IdeModifiableModelsProvider): ModifiableWorkspaceModel {
if (!Registry.`is`("external.system.substitute.library.dependencies")) {
return ModifiableWorkspaceModel.NOP
}
return ModifiableWorkspaceModelImpl(project, state, modelProvider)
}
companion object {
@JvmStatic
fun getInstance(project: Project): ExternalProjectsWorkspace {
return project.service<ExternalProjectsWorkspace>()
}
}
}

View File

@@ -1,56 +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 com.intellij.openapi.externalSystem.service.project;
import com.intellij.openapi.components.*;
import com.intellij.openapi.extensions.ExtensionPointName;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.util.xmlb.annotations.MapAnnotation;
import com.intellij.util.xmlb.annotations.Property;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Supplier;
/**
* @author Vladislav.Soroka
*/
@Service(Service.Level.PROJECT)
@ApiStatus.Internal
@State(name = "externalSubstitutions", storages = @Storage(StoragePathMacros.WORKSPACE_FILE))
final class ExternalProjectsWorkspaceImpl implements PersistentStateComponent<ExternalProjectsWorkspaceImpl.State> {
static class State {
@Property(surroundWithTag = false)
@MapAnnotation(surroundWithTag = false, surroundValueWithTag = false, surroundKeyWithTag = false,
keyAttributeName = "name", entryTagName = "module")
public Map<String, Set<String>> substitutions;
@Property(surroundWithTag = false)
@MapAnnotation(surroundWithTag = false, surroundValueWithTag = false, surroundKeyWithTag = false,
keyAttributeName = "module", valueAttributeName = "lib")
public Map<String, String> names;
}
private State myState = new State();
@Override
public State getState() {
return myState;
}
@Override
public void loadState(@NotNull State state) {
myState = state;
}
public static boolean isDependencySubstitutionEnabled() {
return Registry.is("external.system.substitute.library.dependencies");
}
public ModifiableWorkspace createModifiableWorkspace(Supplier<? extends List<Module>> modulesSupplier) {
return new ModifiableWorkspace(myState, modulesSupplier);
}
}

View File

@@ -120,7 +120,9 @@ public class IdeModifiableModelsProviderImpl extends AbstractIdeModifiableModels
private void workspaceModelCommit() {
ProjectRootManagerEx.getInstanceEx(myProject).mergeRootsChangesDuring(() -> {
updateSubstitutions();
var workspaceModel = getModifiableWorkspaceModel();
workspaceModel.updateLibrarySubstitutions();
workspaceModel.commit();
LibraryTable.ModifiableModel projectLibrariesModel = getModifiableProjectLibrariesModel();
for (Map.Entry<Library, Library.ModifiableModel> entry: myModifiableLibraryModels.entrySet()) {
@@ -136,7 +138,7 @@ public class IdeModifiableModelsProviderImpl extends AbstractIdeModifiableModels
else if (fromLibrary.getTable() != null && libraryName != null && projectLibrariesModel.getLibraryByName(libraryName) == null) {
Disposer.dispose(modifiableModel);
}
else if (isSubstituted(fromLibrary.getName())) {
else if (workspaceModel.isLibrarySubstituted(fromLibrary)) {
Disposer.dispose(modifiableModel);
}
else {

View File

@@ -1,163 +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 com.intellij.openapi.externalSystem.service.project;
import com.intellij.openapi.externalSystem.model.project.ProjectCoordinate;
import com.intellij.openapi.externalSystem.model.project.ProjectId;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.roots.DependencyScope;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.containers.CollectionFactory;
import com.intellij.util.containers.HashingStrategy;
import com.intellij.util.containers.MultiMap;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.function.Supplier;
/**
* @author Vladislav.Soroka
*/
@ApiStatus.Experimental
public final class ModifiableWorkspace {
private final Map<ProjectCoordinate, String> myModuleMappingById = CollectionFactory.createCustomHashingStrategyMap(new HashingStrategy<>() {
@Override
public int hashCode(ProjectCoordinate object) {
if (object == null) {
return 0;
}
String groupId = object.getGroupId();
String artifactId = object.getArtifactId();
String version = object.getVersion();
int result1 = (groupId != null ? groupId.hashCode() : 0);
result1 = 31 * result1 + (artifactId != null ? artifactId.hashCode() : 0);
result1 = 31 * result1 + (version != null ? version.hashCode() : 0);
return result1;
}
@Override
public boolean equals(ProjectCoordinate o1, ProjectCoordinate o2) {
if (o1 == o2) {
return true;
}
if (o1 == null || o2 == null) {
return false;
}
if (o1.getGroupId() != null ? !o1.getGroupId().equals(o2.getGroupId()) : o2.getGroupId() != null) return false;
if (o1.getArtifactId() != null ? !o1.getArtifactId().equals(o2.getArtifactId()) : o2.getArtifactId() != null) return false;
if (o1.getVersion() != null ? !o1.getVersion().equals(o2.getVersion()) : o2.getVersion() != null) return false;
return true;
}
});
private final ExternalProjectsWorkspaceImpl.State myState;
private final MultiMap<String/* module owner */, String /* substitution modules */> mySubstitutions = MultiMap.createSet();
private final Map<String /* module name */, String /* library name */> myNamesMap = new HashMap<>();
private final Supplier<? extends List<Module>> myModulesSupplier;
ModifiableWorkspace(ExternalProjectsWorkspaceImpl.State state,
Supplier<? extends List<Module>> modulesSupplier) {
myModulesSupplier = modulesSupplier;
Set<String> existingModules = new HashSet<>();
for (Module module : modulesSupplier.get()) {
register(module);
existingModules.add(module.getName());
}
myState = state;
if (myState.names != null) {
for (Map.Entry<String, String> entry : myState.names.entrySet()) {
if (existingModules.contains(entry.getKey())) {
myNamesMap.put(entry.getKey(), entry.getValue());
}
}
}
if (myState.substitutions != null) {
for (Map.Entry<String, Set<String>> entry : myState.substitutions.entrySet()) {
if (existingModules.contains(entry.getKey())) {
mySubstitutions.put(entry.getKey(), entry.getValue());
}
}
}
}
public void commit() {
Set<String> existingModules = new HashSet<>();
myModulesSupplier.get().stream().map(Module::getName).forEach(existingModules::add);
myState.names = new HashMap<>();
myNamesMap.forEach((module, lib) -> {
if (existingModules.contains(module)) {
myState.names.put(module, lib);
}
});
myState.substitutions = new HashMap<>();
for (Map.Entry<String, Collection<String>> entry : mySubstitutions.entrySet()) {
if (!existingModules.contains(entry.getKey())) continue;
Collection<String> value = entry.getValue();
if (value != null && !value.isEmpty()) {
myState.substitutions.put(entry.getKey(), new TreeSet<>(value));
}
}
}
public void addSubstitution(String ownerModuleName,
String moduleName,
String libraryName,
DependencyScope scope) {
myNamesMap.put(moduleName, libraryName);
mySubstitutions.putValue(ownerModuleName, moduleName + '_' + scope.getDisplayName());
}
public void removeSubstitution(String ownerModuleName,
String moduleName,
String libraryName,
DependencyScope scope) {
mySubstitutions.remove(ownerModuleName, moduleName + '_' + scope.getDisplayName());
Collection<String> substitutions = mySubstitutions.values();
for (DependencyScope dependencyScope : DependencyScope.values()) {
if (substitutions.contains(moduleName + '_' + dependencyScope.getDisplayName())) {
return;
}
}
myNamesMap.remove(moduleName, libraryName);
}
public boolean isSubstitution(String moduleOwner, String substitutionModule, DependencyScope scope) {
return mySubstitutions.get(moduleOwner).contains(substitutionModule + '_' + scope.getDisplayName());
}
public boolean isSubstituted(String libraryName) {
return myNamesMap.containsValue(libraryName);
}
public String getSubstitutedLibrary(String moduleName) {
return myNamesMap.get(moduleName);
}
public @Nullable String findModule(@NotNull ProjectCoordinate id) {
if (StringUtil.isEmpty(id.getArtifactId())) return null;
String result = myModuleMappingById.get(id);
return result == null && id.getVersion() != null
? myModuleMappingById.get(new ProjectId(id.getGroupId(), id.getArtifactId(), null))
: result;
}
public void register(@NotNull ProjectCoordinate id, @NotNull Module module) {
myModuleMappingById.put(id, module.getName());
myModuleMappingById.put(new ProjectId(id.getGroupId(), id.getArtifactId(), null), module.getName());
}
private void register(@NotNull Module module) {
Arrays.stream(ExternalSystemCoordinateContributor.EP_NAME.getExtensions())
.map(contributor -> contributor.findModuleCoordinate(module))
.filter(Objects::nonNull)
.findFirst()
.ifPresent(id -> register(id, module));
}
}

View File

@@ -0,0 +1,199 @@
// 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.openapi.externalSystem.service.project
import com.intellij.openapi.externalSystem.ExternalSystemManager
import com.intellij.openapi.externalSystem.model.ProjectKeys
import com.intellij.openapi.externalSystem.model.project.ProjectCoordinate
import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.LibraryOrderEntry
import com.intellij.openapi.roots.ModifiableRootModel
import com.intellij.openapi.roots.ModuleOrderEntry
import com.intellij.openapi.roots.OrderEntry
import com.intellij.openapi.roots.libraries.Library
import com.intellij.util.concurrency.annotations.RequiresWriteLock
import com.intellij.util.containers.CollectionFactory
import com.intellij.util.containers.HashingStrategy
import org.jetbrains.annotations.ApiStatus
import java.util.*
@ApiStatus.Internal
class ModifiableWorkspaceModelImpl internal constructor(
private val project: Project,
private val state: ExternalProjectsWorkspace.State,
private val modelsProvider: IdeModifiableModelsProvider,
) : ModifiableWorkspaceModel {
private val librarySubstitutions = state.librarySubstitutions.toMutableMap()
@RequiresWriteLock
override fun commit() {
state.librarySubstitutions = librarySubstitutions.toMutableMap()
}
override fun isLibrarySubstituted(library: Library): Boolean {
return library.name != null && librarySubstitutions.containsValue(library.name)
}
override fun updateLibrarySubstitutions() {
val libraryToModuleMap = buildLibraryToModuleMap()
for (module in modelsProvider.modules) {
val modifiableModule = modelsProvider.getModifiableRootModel(module)
val entries = modifiableModule.orderEntries
var changed = false
for (i in entries.indices) {
val orderEntry = entries[i]
val newOrderEntry = updateLibrarySubstitution(modifiableModule, orderEntry, libraryToModuleMap)
if (newOrderEntry !== orderEntry) {
entries[i] = newOrderEntry
changed = true
}
}
if (changed) {
modifiableModule.rearrangeOrderEntries(entries)
}
}
}
private fun buildLibraryToCoordinateMap(): Map<String, ProjectCoordinate> {
val result = HashMap<String, ProjectCoordinate>()
val projectDataManager = ProjectDataManager.getInstance()
ExternalSystemManager.EP_NAME.forEachExtensionSafe { manager ->
val projectsData = projectDataManager.getExternalProjectsData(project, manager.systemId)
for (projectInfo in projectsData) {
val projectStructure = projectInfo.externalProjectStructure ?: continue
val libraryNodes = ExternalSystemApiUtil.findAll(projectStructure, ProjectKeys.LIBRARY)
for (libraryNode in libraryNodes) {
val libraryData = libraryNode.getData()
result.put(libraryData.internalName, libraryData)
}
}
}
return result
}
private fun buildCoordinateToModuleMap(): Map<ProjectCoordinate, String> {
val result = CollectionFactory.createCustomHashingStrategyMap<ProjectCoordinate, String>(ProjectCoordinateHashingStrategy())
ExternalSystemCoordinateContributor.EP_NAME.forEachExtensionSafe { contributor ->
for (module in modelsProvider.modules) {
val moduleCoordinate = contributor.findModuleCoordinate(module) ?: continue
result.put(moduleCoordinate, module.name)
}
}
return result
}
private fun buildLibraryToModuleMap(): Map<String, String> {
val libraryToCoordinateMap = buildLibraryToCoordinateMap()
val coordinateToModuleMap = buildCoordinateToModuleMap()
val result = HashMap<String, String>()
for ((libraryName, libraryCoordinate) in libraryToCoordinateMap) {
val moduleName = coordinateToModuleMap[libraryCoordinate] ?: continue
result[libraryName] = moduleName
}
return result
}
private fun updateLibrarySubstitution(
modifiableModule: ModifiableRootModel,
orderEntry: OrderEntry,
libraryToModuleMap: Map<String, String>,
): OrderEntry {
var newOrderEntry = orderEntry
if (newOrderEntry is ModuleOrderEntry) {
newOrderEntry = replaceModuleWithLibraryOrNull(modifiableModule, newOrderEntry, libraryToModuleMap)
?: newOrderEntry
}
if (newOrderEntry is LibraryOrderEntry) {
newOrderEntry = replaceLibraryWithModuleOrNull(modifiableModule, newOrderEntry, libraryToModuleMap)
?: newOrderEntry
}
return newOrderEntry
}
private fun replaceModuleWithLibraryOrNull(
modifiableModule: ModifiableRootModel,
moduleOrderEntry: ModuleOrderEntry,
libraryToModuleMap: Map<String, String>,
): LibraryOrderEntry? {
val ownerModuleName = modifiableModule.module.name
val moduleName = moduleOrderEntry.moduleName
val moduleScope = moduleOrderEntry.scope
val moduleScopeName = moduleScope.displayName
val isModuleExported = moduleOrderEntry.isExported
val substitutionId = getLibrarySubstitutionId(ownerModuleName, moduleName, moduleScopeName)
val libraryName = librarySubstitutions[substitutionId] ?: return null
if (libraryToModuleMap[libraryName] == moduleName) {
return null
}
val library = modelsProvider.getLibraryByName(libraryName) ?: return null
val libraryOrderEntry = modifiableModule.addLibraryEntry(library)
libraryOrderEntry.setScope(moduleScope)
libraryOrderEntry.setExported(isModuleExported)
modifiableModule.removeOrderEntry(moduleOrderEntry)
librarySubstitutions.remove(substitutionId)
return libraryOrderEntry
}
private fun replaceLibraryWithModuleOrNull(
modifiableModule: ModifiableRootModel,
libraryOrderEntry: LibraryOrderEntry,
libraryToModuleMap: Map<String, String>,
): ModuleOrderEntry? {
if (libraryOrderEntry.isModuleLevel) {
return null
}
val ownerModuleName = modifiableModule.module.name
val libraryName = libraryOrderEntry.libraryName ?: return null
val libraryScope = libraryOrderEntry.scope
val libraryScopeName = libraryScope.displayName
val isLibraryExported = libraryOrderEntry.isExported
val moduleName = libraryToModuleMap[libraryName] ?: return null
val module = modelsProvider.findIdeModule(moduleName) ?: return null
val moduleOrderEntry = modifiableModule.addModuleOrderEntry(module)
moduleOrderEntry.setScope(libraryScope)
moduleOrderEntry.setExported(isLibraryExported)
modifiableModule.removeOrderEntry(libraryOrderEntry)
val substitutionId: String = getLibrarySubstitutionId(ownerModuleName, moduleName, libraryScopeName)
librarySubstitutions.put(substitutionId, libraryName)
return moduleOrderEntry
}
private class ProjectCoordinateHashingStrategy : HashingStrategy<ProjectCoordinate> {
override fun equals(o1: ProjectCoordinate, o2: ProjectCoordinate?): Boolean {
return o2 != null &&
o1.groupId == o2.groupId &&
o1.artifactId == o2.artifactId &&
o1.version == o2.version
}
override fun hashCode(o: ProjectCoordinate): Int {
return Objects.hash(o.groupId, o.artifactId, o.version)
}
}
companion object {
private fun getLibrarySubstitutionId(ownerModuleName: String, moduleName: String, scopeName: String): String {
return ownerModuleName + "_" + moduleName + '_' + scopeName
}
}
}

View File

@@ -84,10 +84,6 @@ public abstract class AbstractModuleDataService<E extends ModuleData> extends Ab
for (DataNode<E> node : toImport) {
Module module = node.getUserData(MODULE_KEY);
if (module != null) {
ProjectCoordinate publication = node.getData().getPublication();
if (publication != null) {
modelsProvider.registerModulePublication(module, publication);
}
String productionModuleId = node.getData().getProductionModuleId();
modelsProvider.setTestModuleProperties(module, productionModuleId);
setModuleOptions(module, node);

View File

@@ -11,6 +11,7 @@ import com.intellij.openapi.externalSystem.model.project.LibraryData;
import com.intellij.openapi.externalSystem.model.project.LibraryPathType;
import com.intellij.openapi.externalSystem.model.project.ProjectData;
import com.intellij.openapi.externalSystem.service.project.ExternalLibraryPathTypeMapper;
import com.intellij.openapi.externalSystem.service.project.ModifiableWorkspaceModel;
import com.intellij.openapi.externalSystem.service.project.IdeModifiableModelsProvider;
import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil;
import com.intellij.openapi.externalSystem.util.ExternalSystemConstants;
@@ -200,6 +201,7 @@ public final class LibraryDataService extends AbstractProjectDataService<Library
final List<Library> orphanIdeLibraries = new SmartList<>();
final LibraryTable.ModifiableModel librariesModel = modelsProvider.getModifiableProjectLibrariesModel();
final ModifiableWorkspaceModel workspaceModel = modelsProvider.getModifiableWorkspaceModel();
final Map<String, Library> namesToLibs = new HashMap<>();
final Set<Library> potentialOrphans = new HashSet<>();
RootPolicy<Void> excludeUsedLibraries = new RootPolicy<>() {
@@ -231,8 +233,8 @@ public final class LibraryDataService extends AbstractProjectDataService<Library
}
}
for (Library lib: potentialOrphans) {
if (!modelsProvider.isSubstituted(lib.getName())) {
for (Library lib : potentialOrphans) {
if (!workspaceModel.isLibrarySubstituted(lib)) {
orphanIdeLibraries.add(lib);
}
}

View File

@@ -75,7 +75,7 @@ public final class LibraryDependencyDataService extends AbstractDependencyDataSe
modelsProvider);
}
else if (entry instanceof LibraryOrderEntry libraryOrderEntry) {
processingResult = importLibraryOrderEntry(libraryOrderEntry, toImport.projectLibraries, modifiableRootModel, modelsProvider,
processingResult = importLibraryOrderEntry(libraryOrderEntry, toImport.projectLibraries, modifiableRootModel,
toImport.hasUnresolvedLibraries);
if (processingResult != null) {
libraryOrderEntry.setExported(processingResult.isExported());
@@ -127,18 +127,12 @@ public final class LibraryDependencyDataService extends AbstractDependencyDataSe
@NotNull LibraryOrderEntry entry,
@NotNull Map<String/* library name + scope */, LibraryDependencyData> projectLibrariesToImport,
@NotNull ModifiableRootModel modifiableRootModel,
@NotNull IdeModifiableModelsProvider modelsProvider,
boolean hasUnresolvedLibraries
) {
String libraryName = entry.getLibraryName();
LibraryDependencyData existing = projectLibrariesToImport.remove(libraryName + entry.getScope().name());
if (existing != null) {
if (modelsProvider.findModuleByPublication(existing.getTarget()) == null) {
return existing;
}
else {
modifiableRootModel.removeOrderEntry(entry);
}
return existing;
}
else if (!hasUnresolvedLibraries) {
// There is a possible case that a project has been successfully imported from external model and after
@@ -205,13 +199,7 @@ public final class LibraryDependencyDataService extends AbstractDependencyDataSe
}
LibraryOrderEntry orderEntry = moduleRootModel.addLibraryEntry(projectLib);
setLibraryScope(orderEntry, projectLib, module, dependencyData);
ModuleOrderEntry substitutionEntry = modelsProvider.trySubstitute(module, orderEntry, libraryData);
if (substitutionEntry != null) {
return substitutionEntry;
}
else {
return orderEntry;
}
return orderEntry;
}
private static void setLibraryScope(@NotNull LibraryOrderEntry orderEntry,

View File

@@ -0,0 +1,216 @@
// 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.openapi.externalSystem.service.project
import com.intellij.platform.backend.workspace.workspaceModel
import com.intellij.platform.testFramework.assertion.moduleAssertion.DependencyAssertions
import com.intellij.platform.testFramework.assertion.moduleAssertion.ModuleAssertions
import com.intellij.platform.workspace.jps.entities.*
import com.intellij.platform.workspace.jps.entities.DependencyScope.*
import com.intellij.testFramework.common.timeoutRunBlocking
import com.intellij.testFramework.junit5.TestApplication
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
@TestApplication
class ExternalProjectsWorkspaceTest : ExternalProjectsWorkspaceTestCase() {
@Test
fun `test add library substitution`(): Unit = timeoutRunBlocking {
coordinates.modules["lib-module"] = "org.example:library:1.0"
coordinates.libraries["library"] = "org.example:library:1.0"
project.workspaceModel.update { storage ->
storage addEntity LibraryEntity("library", LibraryTableId.ProjectLibraryTableId, emptyList(), ENTITY_SOURCE)
storage addEntity ModuleEntity("app-module", emptyList(), ENTITY_SOURCE) {
dependencies += LibraryDependency(LibraryId("library", LibraryTableId.ProjectLibraryTableId), false, COMPILE)
}
storage addEntity ModuleEntity("lib-module", emptyList(), ENTITY_SOURCE)
}
updateLibrarySubstitutions()
ModuleAssertions.assertModuleEntity(project, "app-module") { module ->
Assertions.assertEquals(ENTITY_SOURCE, module.entitySource)
DependencyAssertions.assertDependencies(module, "lib-module")
DependencyAssertions.assertModuleDependency(module, "lib-module") { dependency ->
Assertions.assertEquals(false, dependency.exported)
Assertions.assertEquals(COMPILE, dependency.scope)
}
}
}
@Test
fun `test update library substitution`() = timeoutRunBlocking {
coordinates.modules["lib-module"] = "org.example:library:1.0"
coordinates.libraries["library"] = "org.example:library:1.0"
project.workspaceModel.update { storage ->
storage addEntity LibraryEntity("library", LibraryTableId.ProjectLibraryTableId, emptyList(), ENTITY_SOURCE)
storage addEntity ModuleEntity("app-module", emptyList(), ENTITY_SOURCE) {
dependencies += LibraryDependency(LibraryId("library", LibraryTableId.ProjectLibraryTableId), false, COMPILE)
}
storage addEntity ModuleEntity("lib-module", emptyList(), ENTITY_SOURCE)
}
updateLibrarySubstitutions()
coordinates.modules["library.main"] = coordinates.modules.remove("lib-module")!!
project.workspaceModel.update { storage ->
storage addEntity ModuleEntity("library.main", emptyList(), ENTITY_SOURCE)
}
updateLibrarySubstitutions()
ModuleAssertions.assertModuleEntity(project, "app-module") { module ->
Assertions.assertEquals(ENTITY_SOURCE, module.entitySource)
DependencyAssertions.assertDependencies(module, "library.main")
DependencyAssertions.assertModuleDependency(module, "library.main") { moduleDependency ->
Assertions.assertEquals(false, moduleDependency.exported)
Assertions.assertEquals(COMPILE, moduleDependency.scope)
}
}
}
@Test
fun `test remove library substitution`(): Unit = timeoutRunBlocking {
coordinates.modules["lib-module"] = "org.example:library:1.0"
coordinates.libraries["library"] = "org.example:library:1.0"
project.workspaceModel.update { storage ->
storage addEntity LibraryEntity("library", LibraryTableId.ProjectLibraryTableId, emptyList(), ENTITY_SOURCE)
storage addEntity ModuleEntity("app-module", emptyList(), ENTITY_SOURCE) {
dependencies += LibraryDependency(LibraryId("library", LibraryTableId.ProjectLibraryTableId), false, COMPILE)
}
storage addEntity ModuleEntity("lib-module", emptyList(), ENTITY_SOURCE)
}
updateLibrarySubstitutions()
coordinates.modules.clear()
coordinates.libraries.clear()
updateLibrarySubstitutions()
ModuleAssertions.assertModuleEntity(project, "app-module") { module ->
Assertions.assertEquals(ENTITY_SOURCE, module.entitySource)
DependencyAssertions.assertDependencies(module, "library")
DependencyAssertions.assertLibraryDependency(module, "library") { dependency ->
Assertions.assertEquals(false, dependency.exported)
Assertions.assertEquals(COMPILE, dependency.scope)
}
}
}
@Test
fun `test library substitution scope`(): Unit = timeoutRunBlocking {
coordinates.modules["lib-module-compile"] = "org.example:library-compile:1.0"
coordinates.modules["lib-module-test"] = "org.example:library-test:1.0"
coordinates.modules["lib-module-runtime"] = "org.example:library-runtime:1.0"
coordinates.modules["lib-module-provided"] = "org.example:library-provided:1.0"
coordinates.libraries["library-compile"] = "org.example:library-compile:1.0"
coordinates.libraries["library-test"] = "org.example:library-test:1.0"
coordinates.libraries["library-runtime"] = "org.example:library-runtime:1.0"
coordinates.libraries["library-provided"] = "org.example:library-provided:1.0"
project.workspaceModel.update { storage ->
storage addEntity LibraryEntity("library-compile", LibraryTableId.ProjectLibraryTableId, emptyList(), ENTITY_SOURCE)
storage addEntity LibraryEntity("library-test", LibraryTableId.ProjectLibraryTableId, emptyList(), ENTITY_SOURCE)
storage addEntity LibraryEntity("library-runtime", LibraryTableId.ProjectLibraryTableId, emptyList(), ENTITY_SOURCE)
storage addEntity LibraryEntity("library-provided", LibraryTableId.ProjectLibraryTableId, emptyList(), ENTITY_SOURCE)
storage addEntity ModuleEntity("app-module", emptyList(), ENTITY_SOURCE) {
dependencies += LibraryDependency(LibraryId("library-compile", LibraryTableId.ProjectLibraryTableId), false, COMPILE)
dependencies += LibraryDependency(LibraryId("library-test", LibraryTableId.ProjectLibraryTableId), false, TEST)
dependencies += LibraryDependency(LibraryId("library-runtime", LibraryTableId.ProjectLibraryTableId), false, RUNTIME)
dependencies += LibraryDependency(LibraryId("library-provided", LibraryTableId.ProjectLibraryTableId), false, PROVIDED)
}
storage addEntity ModuleEntity("lib-module-compile", emptyList(), ENTITY_SOURCE)
storage addEntity ModuleEntity("lib-module-test", emptyList(), ENTITY_SOURCE)
storage addEntity ModuleEntity("lib-module-runtime", emptyList(), ENTITY_SOURCE)
storage addEntity ModuleEntity("lib-module-provided", emptyList(), ENTITY_SOURCE)
}
updateLibrarySubstitutions()
ModuleAssertions.assertModuleEntity(project, "app-module") { module ->
Assertions.assertEquals(ENTITY_SOURCE, module.entitySource)
DependencyAssertions.assertDependencies(module, "lib-module-compile", "lib-module-test", "lib-module-runtime", "lib-module-provided")
DependencyAssertions.assertModuleDependency(module, "lib-module-compile") { dependency ->
Assertions.assertEquals(COMPILE, dependency.scope)
}
DependencyAssertions.assertModuleDependency(module, "lib-module-test") { dependency ->
Assertions.assertEquals(TEST, dependency.scope)
}
DependencyAssertions.assertModuleDependency(module, "lib-module-runtime") { dependency ->
Assertions.assertEquals(RUNTIME, dependency.scope)
}
DependencyAssertions.assertModuleDependency(module, "lib-module-provided") { dependency ->
Assertions.assertEquals(PROVIDED, dependency.scope)
}
}
coordinates.modules.clear()
coordinates.libraries.clear()
updateLibrarySubstitutions()
ModuleAssertions.assertModuleEntity(project, "app-module") { module ->
Assertions.assertEquals(ENTITY_SOURCE, module.entitySource)
DependencyAssertions.assertDependencies(module, "library-compile", "library-test", "library-runtime", "library-provided")
DependencyAssertions.assertLibraryDependency(module, "library-compile") { dependency ->
Assertions.assertEquals(COMPILE, dependency.scope)
}
DependencyAssertions.assertLibraryDependency(module, "library-test") { dependency ->
Assertions.assertEquals(TEST, dependency.scope)
}
DependencyAssertions.assertLibraryDependency(module, "library-runtime") { dependency ->
Assertions.assertEquals(RUNTIME, dependency.scope)
}
DependencyAssertions.assertLibraryDependency(module, "library-provided") { dependency ->
Assertions.assertEquals(PROVIDED, dependency.scope)
}
}
}
@Test
fun `test library substitution exported`(): Unit = timeoutRunBlocking {
coordinates.modules["lib-module"] = "org.example:library:1.0"
coordinates.modules["lib-module-exported"] = "org.example:library-exported:1.0"
coordinates.libraries["library"] = "org.example:library:1.0"
coordinates.libraries["library-exported"] = "org.example:library-exported:1.0"
project.workspaceModel.update { storage ->
storage addEntity LibraryEntity("library", LibraryTableId.ProjectLibraryTableId, emptyList(), ENTITY_SOURCE)
storage addEntity LibraryEntity("library-exported", LibraryTableId.ProjectLibraryTableId, emptyList(), ENTITY_SOURCE)
storage addEntity ModuleEntity("app-module", emptyList(), ENTITY_SOURCE) {
dependencies += LibraryDependency(LibraryId("library", LibraryTableId.ProjectLibraryTableId), false, COMPILE)
dependencies += LibraryDependency(LibraryId("library-exported", LibraryTableId.ProjectLibraryTableId), true, COMPILE)
}
storage addEntity ModuleEntity("lib-module", emptyList(), ENTITY_SOURCE)
storage addEntity ModuleEntity("lib-module-exported", emptyList(), ENTITY_SOURCE)
}
updateLibrarySubstitutions()
ModuleAssertions.assertModuleEntity(project, "app-module") { module ->
Assertions.assertEquals(ENTITY_SOURCE, module.entitySource)
DependencyAssertions.assertDependencies(module, "lib-module", "lib-module-exported")
DependencyAssertions.assertModuleDependency(module, "lib-module") { dependency ->
Assertions.assertEquals(false, dependency.exported)
}
DependencyAssertions.assertModuleDependency(module, "lib-module-exported") { dependency ->
Assertions.assertEquals(true, dependency.exported)
}
}
coordinates.modules.clear()
coordinates.libraries.clear()
updateLibrarySubstitutions()
ModuleAssertions.assertModuleEntity(project, "app-module") { module ->
Assertions.assertEquals(ENTITY_SOURCE, module.entitySource)
DependencyAssertions.assertDependencies(module, "library", "library-exported")
DependencyAssertions.assertLibraryDependency(module, "library") { dependency ->
Assertions.assertEquals(false, dependency.exported)
}
DependencyAssertions.assertLibraryDependency(module, "library-exported") { dependency ->
Assertions.assertEquals(true, dependency.exported)
}
}
}
}

View File

@@ -0,0 +1,53 @@
// 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.openapi.externalSystem.service.project
import com.intellij.openapi.application.writeAction
import com.intellij.openapi.externalSystem.model.project.ProjectCoordinate
import com.intellij.openapi.externalSystem.model.project.ProjectId
import com.intellij.openapi.module.Module
import com.intellij.openapi.roots.libraries.Library
import com.intellij.platform.backend.workspace.WorkspaceModel
import com.intellij.platform.workspace.storage.EntitySource
import com.intellij.platform.workspace.storage.MutableEntityStorage
import com.intellij.testFramework.junit5.TestApplication
import com.intellij.testFramework.junit5.fixture.extensionPointFixture
import com.intellij.testFramework.junit5.fixture.projectFixture
@TestApplication
abstract class ExternalProjectsWorkspaceTestCase {
val project by projectFixture()
val coordinates by extensionPointFixture(ExternalSystemCoordinateContributor.EP_NAME, ::TestCoordinateContributor)
suspend fun updateLibrarySubstitutions() {
writeAction {
IdeModifiableModelsProviderImpl(project)
.commit()
}
}
suspend fun WorkspaceModel.update(updater: (MutableEntityStorage) -> Unit) =
update("Test description", updater)
class TestCoordinateContributor : ExternalSystemCoordinateContributor {
val modules = HashMap<String, String>()
val libraries = HashMap<String, String>()
override fun findModuleCoordinate(module: Module): ProjectCoordinate? =
modules[module.name]?.toProjectCoordinate()
override fun findLibraryCoordinate(library: Library): ProjectCoordinate? =
libraries[library.name]?.toProjectCoordinate()
private fun String.toProjectCoordinate(): ProjectCoordinate {
val (groupId, artifactId, version) = split(":")
return ProjectId(groupId, artifactId, version)
}
}
companion object {
val ENTITY_SOURCE = object : EntitySource {}
}
}

View File

@@ -0,0 +1,49 @@
// 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.platform.testFramework.assertion.moduleAssertion
import com.intellij.platform.testFramework.assertion.collectionAssertion.CollectionAssertions.assertEqualsUnordered
import com.intellij.platform.workspace.jps.entities.*
import org.junit.jupiter.api.Assertions
object DependencyAssertions {
val INHERITED_SDK = InheritedSdkDependency::class.simpleName!!
val MODULE_SOURCE = ModuleSourceDependency::class.simpleName!!
fun assertDependencies(module: ModuleEntity, vararg expectedNames: String) {
assertDependencies(module, expectedNames.asList())
}
fun assertDependencies(module: ModuleEntity, expectedNames: List<String>) {
val actualNames = module.dependencies.map { dependency ->
when (dependency) {
InheritedSdkDependency -> INHERITED_SDK
ModuleSourceDependency -> MODULE_SOURCE
is LibraryDependency -> dependency.library.name
is ModuleDependency -> dependency.module.name
is SdkDependency -> dependency.sdk.name
}
}
assertEqualsUnordered(expectedNames, actualNames)
}
fun assertLibraryDependency(module: ModuleEntity, name: String, assertion: (LibraryDependency) -> Unit) {
module.dependencies.filterIsInstance<LibraryDependency>()
.find { it.library.name == name }
.let { dependency ->
Assertions.assertNotNull(dependency, "Cannot find '$name' library dependency in '${module.name}' module")
Assertions.assertEquals(name, dependency!!.library.name)
assertion(dependency)
}
}
fun assertModuleDependency(module: ModuleEntity, name: String, assertion: (ModuleDependency) -> Unit) {
module.dependencies.filterIsInstance<ModuleDependency>()
.find { it.module.name == name }
.let { dependency ->
Assertions.assertNotNull(dependency, "Cannot find '$name' module dependency in '${module.name}' module")
Assertions.assertEquals(name, dependency!!.module.name)
assertion(dependency)
}
}
}

View File

@@ -8,6 +8,7 @@ import com.intellij.platform.testFramework.assertion.collectionAssertion.Collect
import com.intellij.platform.workspace.jps.entities.ModuleEntity
import com.intellij.platform.workspace.storage.EntityStorage
import com.intellij.platform.workspace.storage.entities
import org.junit.jupiter.api.Assertions
object ModuleAssertions {
@@ -54,4 +55,17 @@ object ModuleAssertions {
val actualNames = storage.entities<ModuleEntity>().map { it.name }.toList()
assertContainsUnordered(expectedNames, actualNames)
}
fun assertModuleEntity(project: Project, name: String, assertion: (ModuleEntity) -> Unit) =
assertModuleEntity(project.workspaceModel.currentSnapshot, name, assertion)
fun assertModuleEntity(storage: EntityStorage, name: String, assertion: (ModuleEntity) -> Unit) {
storage.entities<ModuleEntity>()
.find { it.name == name }
.let { module ->
Assertions.assertNotNull(module, "Cannot find '$name' module")
Assertions.assertEquals(name, module!!.name)
assertion(module)
}
}
}