Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,11 @@ object ContentRootAssertions {
val actualRoots = moduleEntity.contentRoots.map { it.url }
CollectionAssertions.assertEqualsUnordered(expectedRoots, actualRoots)
}

@JvmStatic
fun assertContentRootsOrdered(moduleEntity: ModuleEntity, expectedRoots: List<VirtualFileUrl>) {
val actualRoots = moduleEntity.contentRoots.map { it.url }
CollectionAssertions.assertEqualsOrdered(expectedRoots, actualRoots)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// 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
import com.intellij.platform.workspace.jps.entities.ModuleEntity
import com.intellij.platform.workspace.storage.url.VirtualFileUrl

object ExcludeUrlAssertions {
@JvmStatic
fun assertExcludedUrlsOrdered(moduleEntity: ModuleEntity, expectedExclusions: List<VirtualFileUrl>) {
val actualRoots = moduleEntity.contentRoots.flatMap { it.excludedUrls.map { it.url } }
CollectionAssertions.assertEqualsOrdered(expectedExclusions, actualRoots)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,19 @@ import com.intellij.codeInsight.multiverse.isSharedSourceSupportEnabled
import com.intellij.openapi.externalSystem.service.project.nameGenerator.ModuleNameGenerator
import com.intellij.openapi.externalSystem.util.Order
import com.intellij.openapi.module.impl.UnloadedModulesListStorage
import com.intellij.openapi.project.Project
import com.intellij.platform.workspace.jps.JpsImportedEntitySource
import com.intellij.platform.workspace.jps.entities.ContentRootEntity
import com.intellij.platform.workspace.jps.entities.ExcludeUrlEntity
import com.intellij.platform.workspace.jps.entities.ModuleEntity
import com.intellij.platform.workspace.jps.entities.SourceRootEntity
import com.intellij.platform.workspace.jps.entities.contentRoot
import com.intellij.platform.workspace.jps.entities.exModuleOptions
import com.intellij.platform.workspace.jps.entities.modifyModuleEntity
import com.intellij.platform.workspace.storage.EntityStorage
import com.intellij.platform.workspace.storage.MutableEntityStorage
import com.intellij.platform.workspace.storage.WorkspaceEntity
import com.intellij.platform.workspace.storage.url.VirtualFileUrl
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.plugins.gradle.service.project.ProjectResolverContext
import org.jetbrains.plugins.gradle.service.syncAction.GradleSyncExtension
Expand All @@ -34,7 +39,9 @@ class GradleJpsSyncExtension : GradleSyncExtension {
removeUnloadedModules(context, syncStorage, phase)
removeBridgeModules(context, syncStorage, projectStorage, phase)
renameDuplicatedModules(context, syncStorage, projectStorage, phase)
removeDuplicatedContentRoots(context, syncStorage, projectStorage, phase)
removeDuplicatedUrlEntities<ContentRootEntity>(context, syncStorage, projectStorage, phase) { it.url to it.module }
removeDuplicatedUrlEntities<ExcludeUrlEntity>(context, syncStorage, projectStorage, phase) { it.url to it.contentRoot?.module}
removeDuplicatedUrlEntities<SourceRootEntity>(context, syncStorage, projectStorage, phase) { it.url to it.contentRoot.module }
}

private fun removeModulesWithUsedContentRoots(
Expand Down Expand Up @@ -86,26 +93,29 @@ class GradleJpsSyncExtension : GradleSyncExtension {
syncStorage.removeAllEntities(entitiesToRemove)
}

private fun removeDuplicatedContentRoots(
private inline fun <reified T: WorkspaceEntity> removeDuplicatedUrlEntities(
context: ProjectResolverContext,
syncStorage: MutableEntityStorage,
projectStorage: EntityStorage,
phase: GradleSyncPhase,
key: (T) -> Pair<VirtualFileUrl, ModuleEntity?>
) {
if (isSharedSourceSupportEnabled(context.project)) {
return
}
val contentRootUrls = projectStorage.entitiesToSkip<ContentRootEntity>(context, phase)
.mapTo(HashSet()) { it.url }
val existingProjectEntities = projectStorage.entitiesToSkip<T>(context, phase)
.mapTo(HashSet()) { key(it).sharedSourceSetAware(context.project) }
val entitiesToRemove = ArrayList<WorkspaceEntity>()
for (contentRootEntity in syncStorage.entitiesToReplace<ContentRootEntity>(context, phase)) {
if (!contentRootUrls.add(contentRootEntity.url)) {
entitiesToRemove.add(contentRootEntity)
for (entity in syncStorage.entitiesToReplace<T>(context, phase)) {
if (!existingProjectEntities.add(key(entity).sharedSourceSetAware(context.project))) {
entitiesToRemove.add(entity)
}
}
syncStorage.removeAllEntities(entitiesToRemove)
}

// Duplicate source sets are allowed when shared source support is enabled, but the ones duplicated
// in a single module should still be removed
private fun Pair<VirtualFileUrl, ModuleEntity?>.sharedSourceSetAware(project: Project): Pair<VirtualFileUrl, String?> =
first to second?.name.takeIf { isSharedSourceSupportEnabled(project) }

private fun renameDuplicatedModules(
context: ProjectResolverContext,
syncStorage: MutableEntityStorage,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.plugins.gradle.importing.syncAction

import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.io.toCanonicalPath
import com.intellij.openapi.util.registry.Registry
import com.intellij.openapi.util.use
import com.intellij.platform.backend.workspace.workspaceModel
import com.intellij.platform.testFramework.assertion.moduleAssertion.ContentRootAssertions
import com.intellij.platform.testFramework.assertion.moduleAssertion.ExModuleOptionAssertions
import com.intellij.platform.testFramework.assertion.moduleAssertion.ExcludeUrlAssertions
import com.intellij.platform.testFramework.assertion.moduleAssertion.ModuleAssertions
import com.intellij.platform.workspace.jps.entities.ContentRootEntity
import com.intellij.platform.workspace.jps.entities.ExcludeUrlEntity
import com.intellij.platform.workspace.jps.entities.ExternalSystemModuleOptionsEntity
import com.intellij.platform.workspace.jps.entities.ModuleEntity
import com.intellij.platform.workspace.jps.entities.exModuleOptions
Expand Down Expand Up @@ -138,4 +143,186 @@ class GradleJpsSyncExtensionTest {
}
}
}

@Test
fun `test GradleJpsSyncExtension#removeDuplicatedContentRoots sharedSourceSetsDisabled`(): Unit = runBlocking {
Disposer.newDisposable().use { disposable ->
Registry.get("intellij.platform.shared.source.support").setValue(false, disposable)
val moduleNames = listOf("module1", "module2")
val contentRootPath = virtualFileUrl(projectPath.resolve("contentRoot"))


val phase = GradleSyncPhase.PROJECT_MODEL_PHASE // Any
val entitySource = GradleTestEntitySource(projectPath.toCanonicalPath(), phase)

val projectStorage = MutableEntityStorage.create()
for (moduleName in moduleNames) {
projectStorage addEntity ModuleEntity(moduleName, emptyList(), NonPersistentEntitySource) {
contentRoots += ContentRootEntity(contentRootPath, emptyList(), NonPersistentEntitySource)
}
}

val syncStorage = MutableEntityStorage.create()
for (moduleName in moduleNames) {
syncStorage addEntity ModuleEntity(moduleName, emptyList(), entitySource) {
contentRoots += ContentRootEntity(contentRootPath, emptyList(), entitySource)
}
}

val context = mock<ProjectResolverContext> {
on { project } doReturn project
on { projectPath } doReturn projectPath.toCanonicalPath()
}
GradleJpsSyncExtension().updateProjectModel(context, syncStorage, projectStorage, phase)
GradleBaseSyncExtension().updateProjectModel(context, syncStorage, projectStorage, phase)
Mockito.reset(context)

ModuleAssertions.assertModules(projectStorage, moduleNames)
ModuleAssertions.assertModuleEntity(projectStorage, "module1") { module ->
Assertions.assertEquals(entitySource, module.entitySource)
ContentRootAssertions.assertContentRootsOrdered(module, listOf(contentRootPath))
}
ModuleAssertions.assertModuleEntity(projectStorage, "module2") { module ->
Assertions.assertEquals(entitySource, module.entitySource)
ContentRootAssertions.assertContentRootsOrdered(module, emptyList())
}
}
}

@Test
fun `test GradleJpsSyncExtension#removeDuplicatedContentRoots sharedSourceSetsEnabled`(): Unit = runBlocking {
Disposer.newDisposable().use { disposable ->
Registry.get("intellij.platform.shared.source.support").setValue(true, disposable)
val moduleNames = listOf("module1", "module2")
val contentRootPath = virtualFileUrl(projectPath.resolve("contentRoot"))


val phase = GradleSyncPhase.PROJECT_MODEL_PHASE // Any
val entitySource = GradleTestEntitySource(projectPath.toCanonicalPath(), phase)

val projectStorage = MutableEntityStorage.create()
for (moduleName in moduleNames) {
projectStorage addEntity ModuleEntity(moduleName, emptyList(), NonPersistentEntitySource) {
contentRoots += ContentRootEntity(contentRootPath, emptyList(), NonPersistentEntitySource)
}
}

val syncStorage = MutableEntityStorage.create()
for (moduleName in moduleNames) {
syncStorage addEntity ModuleEntity(moduleName, emptyList(), entitySource) {
contentRoots += ContentRootEntity(contentRootPath, emptyList(), entitySource)
}
}

val context = mock<ProjectResolverContext> {
on { project } doReturn project
on { projectPath } doReturn projectPath.toCanonicalPath()
}
GradleJpsSyncExtension().updateProjectModel(context, syncStorage, projectStorage, phase)
GradleBaseSyncExtension().updateProjectModel(context, syncStorage, projectStorage, phase)
Mockito.reset(context)

ModuleAssertions.assertModules(projectStorage, moduleNames)
ModuleAssertions.assertModuleEntity(projectStorage, "module1") { module ->
Assertions.assertEquals(entitySource, module.entitySource)
ContentRootAssertions.assertContentRootsOrdered(module, listOf(contentRootPath))
}
ModuleAssertions.assertModuleEntity(projectStorage, "module2") { module ->
Assertions.assertEquals(entitySource, module.entitySource)
ContentRootAssertions.assertContentRootsOrdered(module, listOf(contentRootPath))
}
}
}

@Test
fun `test GradleJpsSyncExtension#removeDuplicatedContentRoots sharedSourceSetsEnabled duplicatesWithinSameModule`(): Unit = runBlocking {
Disposer.newDisposable().use { disposable ->
Registry.get("intellij.platform.shared.source.support").setValue(true, disposable)
val moduleName = "module"
val contentRootPath = virtualFileUrl(projectPath.resolve("contentRoot"))


val phase = GradleSyncPhase.PROJECT_MODEL_PHASE // Any
val entitySource = GradleTestEntitySource(projectPath.toCanonicalPath(), phase)

val projectStorage = MutableEntityStorage.create()

projectStorage addEntity ModuleEntity(moduleName, emptyList(), NonPersistentEntitySource) {
contentRoots += ContentRootEntity(contentRootPath, emptyList(), NonPersistentEntitySource)
}


val syncStorage = MutableEntityStorage.create()
syncStorage addEntity ModuleEntity(moduleName, emptyList(), entitySource) {
contentRoots += ContentRootEntity(contentRootPath, emptyList(), entitySource)
contentRoots += ContentRootEntity(contentRootPath, emptyList(), entitySource)
}


val context = mock<ProjectResolverContext> {
on { project } doReturn project
on { projectPath } doReturn projectPath.toCanonicalPath()
}
GradleJpsSyncExtension().updateProjectModel(context, syncStorage, projectStorage, phase)
GradleBaseSyncExtension().updateProjectModel(context, syncStorage, projectStorage, phase)
Mockito.reset(context)

ModuleAssertions.assertModules(projectStorage, moduleName)
ModuleAssertions.assertModuleEntity(projectStorage, moduleName) { module ->
Assertions.assertEquals(entitySource, module.entitySource)
ContentRootAssertions.assertContentRootsOrdered(module, listOf(contentRootPath))
}
}
}

@Test
fun `test GradleJpsSyncExtension#removeDuplicatedExcludeUrls sharedSourceSetsEnabled duplicatesWithinSameModule`(): Unit = runBlocking {
Disposer.newDisposable().use { disposable ->
Registry.get("intellij.platform.shared.source.support").setValue(true, disposable)
val moduleName = "module"
val contentRootPath = virtualFileUrl(projectPath.resolve("contentRoot"))
val excludeUrlPath = virtualFileUrl(projectPath.resolve("contentRoot/excluded"))

val phase = GradleSyncPhase.PROJECT_MODEL_PHASE // Any
val entitySource = GradleTestEntitySource(projectPath.toCanonicalPath(), phase)

val projectStorage = MutableEntityStorage.create()

projectStorage addEntity ModuleEntity(moduleName, emptyList(), NonPersistentEntitySource) {
contentRoots += ContentRootEntity(contentRootPath, emptyList(), NonPersistentEntitySource) {
excludedUrls = listOf(
ExcludeUrlEntity(excludeUrlPath, NonPersistentEntitySource),
)
}
}


val syncStorage = MutableEntityStorage.create()
syncStorage addEntity ModuleEntity(moduleName, emptyList(), entitySource) {
contentRoots += ContentRootEntity(contentRootPath, emptyList(), entitySource) {
excludedUrls = listOf(
ExcludeUrlEntity(excludeUrlPath, entitySource),
ExcludeUrlEntity(excludeUrlPath, entitySource),
)
}
}


val context = mock<ProjectResolverContext> {
on { project } doReturn project
on { projectPath } doReturn projectPath.toCanonicalPath()
}
GradleJpsSyncExtension().updateProjectModel(context, syncStorage, projectStorage, phase)
GradleBaseSyncExtension().updateProjectModel(context, syncStorage, projectStorage, phase)
Mockito.reset(context)

ModuleAssertions.assertModules(projectStorage, moduleName)
ModuleAssertions.assertModuleEntity(projectStorage, moduleName) { module ->
Assertions.assertEquals(entitySource, module.entitySource)
ContentRootAssertions.assertContentRootsOrdered(module, listOf(contentRootPath))
ExcludeUrlAssertions.assertExcludedUrlsOrdered(module, listOf(excludeUrlPath))
}
}
}

}