[AA] Test project structure: Fix duplicate binary library modules in main module dependencies

- `TestModuleStructureFactory.createProjectStructureByTestStructure`
  created `KtModule`s for each test module and then also created new
  `KtModule`s for binary dependencies. This split worked as long as no
  binary dependency was also a test module and thus part of the main
  modules. But if not, `addLibraryDependencies` created a duplicate
  `KtModule`, instead of referring to the existing binary module.
- The fix registers a main binary library module in the library cache
  before it can become the dependency of another main module. Because
  main modules can only depend on main modules preceding them in the
  list, a separate pass over all main modules is not needed.

^KT-64647 fixed
This commit is contained in:
Marco Pennekamp
2024-01-09 00:52:56 +01:00
committed by Space Team
parent 62facee96c
commit ff0224d28d
2 changed files with 46 additions and 19 deletions
@@ -13,6 +13,7 @@ import org.jetbrains.kotlin.analysis.api.standalone.base.project.structure.KtMod
import org.jetbrains.kotlin.analysis.api.standalone.base.project.structure.KtModuleWithFiles
import org.jetbrains.kotlin.analysis.api.standalone.base.project.structure.StandaloneProjectFactory
import org.jetbrains.kotlin.analysis.project.structure.KtBinaryModule
import org.jetbrains.kotlin.analysis.project.structure.KtModule
import org.jetbrains.kotlin.analysis.project.structure.KtNotUnderContentRootModule
import org.jetbrains.kotlin.analysis.test.framework.services.environmentManager
import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys
@@ -34,6 +35,10 @@ import kotlin.io.path.exists
import kotlin.io.path.extension
import kotlin.io.path.nameWithoutExtension
private typealias LibraryCache = MutableMap<Set<Path>, KtBinaryModule>
private typealias ModulesByName = Map<String, KtModuleWithFiles>
object TestModuleStructureFactory {
fun createProjectStructureByTestStructure(
moduleStructure: TestModuleStructure,
@@ -44,34 +49,58 @@ object TestModuleStructureFactory {
testServices.getKtModuleFactoryForTestModule(testModule).createModule(testModule, testServices, project)
}
val moduleEntriesByName = moduleEntries.associateByName()
val modulesByName = moduleEntries.associateByName()
val libraryCache = mutableMapOf<Set<Path>, KtBinaryModule>()
val libraryCache: LibraryCache = mutableMapOf()
for (testModule in moduleStructure.modules) {
val moduleWithFiles = moduleEntriesByName[testModule.name] ?: moduleEntriesByName.getValue(testModule.files.single().name)
when (val ktModule = moduleWithFiles.ktModule) {
is KtNotUnderContentRootModule -> {
// Not-under-content-root modules have no external dependencies on purpose
}
is KtModuleWithModifiableDependencies -> {
addModuleDependencies(testModule, moduleEntriesByName, ktModule)
addLibraryDependencies(testModule, testServices, project, ktModule, libraryCache::getOrPut)
}
else -> error("Unexpected module type: " + ktModule.javaClass.name)
}
val ktModule = modulesByName.getByTestModule(testModule).ktModule
ktModule.addToLibraryCacheIfNeeded(libraryCache)
ktModule.addDependencies(testModule, testServices, modulesByName, libraryCache)
}
return KtModuleProjectStructure(moduleEntries, libraryCache.values)
}
private fun ModulesByName.getByTestModule(testModule: TestModule): KtModuleWithFiles =
this[testModule.name] ?: this.getValue(testModule.files.single().name)
/**
* A main module may be a binary library module, which may be a dependency of subsequent main modules. We need to add such a module to
* the library cache before it is processed as a dependency. Otherwise, when another module's binary dependency is processed,
* [addLibraryDependencies] will create a *duplicate* binary library module with the same roots and name as the already existing binary
* library module.
*/
private fun KtModule.addToLibraryCacheIfNeeded(libraryCache: LibraryCache) {
if (this is KtBinaryModule) {
libraryCache.put(getBinaryRoots().toSet(), this)
}
}
private fun KtModule.addDependencies(
testModule: TestModule,
testServices: TestServices,
modulesByName: ModulesByName,
libraryCache: LibraryCache,
) = when (this) {
is KtNotUnderContentRootModule -> {
// Not-under-content-root modules have no external dependencies on purpose
}
is KtModuleWithModifiableDependencies -> {
addModuleDependencies(testModule, modulesByName, this)
addLibraryDependencies(testModule, testServices, project, this, libraryCache::getOrPut)
}
else -> error("Unexpected module type: " + javaClass.name)
}
private fun addModuleDependencies(
testModule: TestModule,
moduleByName: Map<String, KtModuleWithFiles>,
ktModule: KtModuleWithModifiableDependencies
modulesByName: ModulesByName,
ktModule: KtModuleWithModifiableDependencies,
) {
testModule.allDependencies.forEach { dependency ->
val dependencyKtModule = moduleByName.getValue(dependency.moduleName).ktModule
val dependencyKtModule = modulesByName.getValue(dependency.moduleName).ktModule
when (dependency.relation) {
DependencyRelation.RegularDependency -> ktModule.directRegularDependencies.add(dependencyKtModule)
DependencyRelation.FriendDependency -> ktModule.directFriendDependencies.add(dependencyKtModule)
@@ -42,9 +42,7 @@ class SealedClassesInheritorsCaclulatorPreAnalysisHandler(
}
val ktFilesByModule = moduleStructure.modules.associateWith { testModule ->
testServices.ktModuleProvider.getModuleFiles(testModule)
.filterIsInstance<KtFile>()
.filterNot(KtFile::isCompiled)
testServices.ktModuleProvider.getModuleFiles(testModule).filterIsInstance<KtFile>()
}
for ((testModule, ktFiles) in ktFilesByModule) {