Extract Module From Package Action

[extract module action] fixes

- remove illegal usage of junit5
- extract duplicated code
- specify the return type of getActionUpdateThread
- fix properties' naming
- fix the dialog message

[extract module action] Replace AbstractDependencyVisitor with JvmBytecodeAnalysis; bazel; unignore test

[extract module action] Extract Module From Package Action

- Use classfiles instead of psi
- Tests
- Coroutines


Co-authored-by: Kirill Bochkarev <kirill.bochkarev@jetbrains.com>

Merge-request: IJ-MR-163922
Merged-by: Kirill Bochkarev <kirill.bochkarev@jetbrains.com>

GitOrigin-RevId: 03cf1754a17d5a9e819ea8cfe812ca2e0a1855e0
This commit is contained in:
Nikolay Chashnikov
2025-05-24 00:43:00 +00:00
committed by intellij-monorepo-bot
parent 995eb27843
commit cd627067a5
9 changed files with 436 additions and 186 deletions

View File

@@ -52,6 +52,7 @@ jvm_library(
"@lib//:maven-resolver-provider",
"//java/java-syntax:syntax",
"@lib//:slf4j-api",
"@lib//:asm",
],
runtime_deps = [":ui_resources"]
)

View File

@@ -48,5 +48,6 @@
<orderEntry type="library" name="maven-resolver-provider" level="project" />
<orderEntry type="module" module-name="intellij.java.syntax" />
<orderEntry type="library" name="slf4j-api" level="project" />
<orderEntry type="library" name="ASM" level="project" />
</component>
</module>

View File

@@ -598,6 +598,7 @@ progress.title.extract.module.analyzing.dependencies=Analyzing dependencies of '
button.text.extract.module=Extract
checkbox.move.classes.to.separate.source.root=Move classes to a separate source root:
dialog.title.specify.path.to.new.source.root=Specify Path to New Source Root
dialog.comment.compile.modules=Project will be compiled to find references.
dialog.message.failed.to.extract.module=Failed to extract a module: {0}
select=Select
intellij.idea.module.file.iml=Intellij IDEA module file (*.iml)

View File

@@ -0,0 +1,34 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.ide.extractModule
import com.intellij.java.analysis.bytecode.JvmBytecodeAnalysis
import com.intellij.java.analysis.bytecode.JvmBytecodeReferenceProcessor
import com.intellij.java.analysis.bytecode.JvmClassBytecodeDeclaration
import java.nio.file.Path
internal class ExtractModuleFileProcessor {
private val mutableReferencedClasses: MutableSet<String> = HashSet()
private val mutableGatheredClassLinks: MutableMap<String, MutableSet<String>> = HashMap()
private val referenceProcessor = object : JvmBytecodeReferenceProcessor {
override fun processClassReference(targetClass: JvmClassBytecodeDeclaration, sourceClass: JvmClassBytecodeDeclaration) {
val targetClassName = targetClass.topLevelSourceClassName
if (targetClassName.startsWith("[L")) return // ignore array classes
val sourceClassName = sourceClass.topLevelSourceClassName
if (sourceClassName != targetClassName) {
mutableReferencedClasses.add(targetClassName)
mutableGatheredClassLinks.computeIfAbsent(sourceClassName) { HashSet() }.add(targetClassName)
}
}
}
val classFileAnalyzer = JvmBytecodeAnalysis.getInstance().createReferenceAnalyzer(referenceProcessor)
val referencedClasses: Set<String>
get() = mutableReferencedClasses
val gatheredClassLinks: Map<String, Set<String>>
get() = mutableGatheredClassLinks
fun processFile(path: Path) = classFileAnalyzer.processFile(path)
}

View File

@@ -1,37 +1,14 @@
// 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.ide.extractModule
import com.intellij.CommonBundle
import com.intellij.analysis.AnalysisScope
import com.intellij.ide.JavaUiBundle
import com.intellij.ide.SaveAndSyncHandler
import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.application.runWriteAction
import com.intellij.openapi.diagnostic.ControlFlowException
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.module.Module
import com.intellij.openapi.module.ModuleManager
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.Task
import com.intellij.openapi.roots.*
import com.intellij.openapi.roots.libraries.Library
import com.intellij.openapi.ui.Messages
import com.intellij.openapi.util.NlsSafe
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.packageDependencies.ForwardDependenciesBuilder
import com.intellij.psi.JavaDirectoryService
import com.intellij.psi.PsiDirectory
import com.intellij.openapi.components.service
import com.intellij.openapi.roots.ModuleRootManager
import com.intellij.openapi.roots.ProjectFileIndex
import com.intellij.psi.PsiManager
import com.intellij.refactoring.move.moveClassesOrPackages.MoveClassesOrPackagesUtil
import com.intellij.workspaceModel.ide.legacyBridge.impl.java.JAVA_MODULE_ENTITY_TYPE_ID_NAME
import org.jetbrains.annotations.TestOnly
import org.jetbrains.concurrency.AsyncPromise
import org.jetbrains.concurrency.Promise
import java.nio.file.Path
class ExtractModuleFromPackageAction : AnAction() {
@@ -46,10 +23,11 @@ class ExtractModuleFromPackageAction : AnAction() {
Path.of(parentContentRoot.path, directory.name, "src").toString())
if (!dialog.showAndGet()) return
analyzeDependenciesAndCreateModule(directory, module, dialog.moduleName, dialog.targetSourceRootPath)
project.service<ExtractModuleService>()
.analyzeDependenciesAndCreateModuleInBackground(directory, module, dialog.moduleName, dialog.targetSourceRootPath)
}
override fun getActionUpdateThread() = ActionUpdateThread.BGT
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT
override fun update(e: AnActionEvent) {
val project = e.project
@@ -57,113 +35,4 @@ class ExtractModuleFromPackageAction : AnAction() {
e.presentation.isEnabledAndVisible = project != null && file != null && file.isDirectory
&& ProjectFileIndex.getInstance(project).isInSourceContent(file)
}
companion object {
private val LOG = logger<ExtractModuleFromPackageAction>()
private fun analyzeDependenciesAndCreateModule(directory: PsiDirectory,
module: Module,
moduleName: @NlsSafe String,
targetSourceRootPath: String?): Promise<Unit> {
val promise = AsyncPromise<Unit>()
val dependenciesBuilder = ForwardDependenciesBuilder(module.project, AnalysisScope(directory))
object : Task.Backgroundable(module.project,
JavaUiBundle.message("progress.title.extract.module.analyzing.dependencies", directory.name)) {
override fun run(indicator: ProgressIndicator) {
indicator.isIndeterminate = false
dependenciesBuilder.analyze()
val usedModules = LinkedHashSet<Module>()
val usedLibraries = LinkedHashSet<Library>()
runReadAction {
val fileIndex = ProjectFileIndex.getInstance(module.project)
dependenciesBuilder.directDependencies.values.asSequence().flatten().forEach { file ->
val virtualFile = file.virtualFile ?: return@forEach
val depModule = fileIndex.getModuleForFile(virtualFile)
if (depModule != null) {
usedModules.add(depModule)
return@forEach
}
val library = fileIndex.getOrderEntriesForFile(virtualFile).asSequence()
.filterIsInstance<LibraryOrderEntry>()
.filter { !it.isModuleLevel }
.mapNotNull { it.library }
.firstOrNull()
if (library != null) {
usedLibraries.add(library)
}
}
}
ApplicationManager.getApplication().invokeLater {
try {
runWriteAction {
extractModule(directory, module, moduleName, usedModules, usedLibraries, targetSourceRootPath)
}
ModuleDependenciesCleaner(module, usedModules).startInBackground(promise)
}
catch (e: Throwable) {
if (e !is ControlFlowException) {
LOG.info(e)
Messages.showErrorDialog(project, JavaUiBundle.message("dialog.message.failed.to.extract.module", e), CommonBundle.getErrorTitle())
}
promise.setError(e)
}
}
}
}.queue()
return promise
}
private fun extractModule(directory: PsiDirectory, module: Module, moduleName: @NlsSafe String,
usedModules: Set<Module>, usedLibraries: Set<Library>, targetSourceRootPath: String?) {
val packagePrefix = JavaDirectoryService.getInstance().getPackage(directory)?.qualifiedName ?: ""
val targetSourceRoot = targetSourceRootPath?.let { VfsUtil.createDirectories(it) }
val (contentRoot, imlFileDirectory) = if (targetSourceRoot != null) {
val parent = targetSourceRoot.parent
if (parent in ModuleRootManager.getInstance(module).contentRoots) targetSourceRoot to module.moduleNioFile.parent
else parent to parent.toNioPath()
}
else {
directory.virtualFile to module.moduleNioFile.parent
}
val newModule = ModuleManager.getInstance(module.project).newModule(imlFileDirectory.resolve("$moduleName.iml"),
JAVA_MODULE_ENTITY_TYPE_ID_NAME)
ModuleRootModificationUtil.updateModel(newModule) { model ->
if (ModuleRootManager.getInstance(module).isSdkInherited) {
model.inheritSdk()
}
else {
model.sdk = ModuleRootManager.getInstance(module).sdk
}
val contentEntry = model.addContentEntry(contentRoot)
if (targetSourceRoot != null) {
contentEntry.addSourceFolder(targetSourceRoot, false)
}
else {
contentEntry.addSourceFolder(directory.virtualFile, false, packagePrefix)
}
val moduleDependencies = JavaProjectDependenciesAnalyzer.removeDuplicatingDependencies(usedModules)
moduleDependencies.forEach { model.addModuleOrderEntry(it) }
val exportedLibraries = HashSet<Library>()
for (moduleDependency in moduleDependencies) {
ModuleRootManager.getInstance(moduleDependency).orderEntries().exportedOnly().recursively().forEachLibrary {
exportedLibraries.add(it)
}
}
(usedLibraries - exportedLibraries).forEach { model.addLibraryEntry(it) }
}
if (targetSourceRoot != null) {
val targetDirectory = VfsUtil.createDirectoryIfMissing(targetSourceRoot, packagePrefix.replace('.', '/'))
MoveClassesOrPackagesUtil.moveDirectoryRecursively(directory, PsiManager.getInstance(module.project).findDirectory(targetDirectory.parent))
}
SaveAndSyncHandler.getInstance().scheduleProjectSave(module.project)
}
@TestOnly
fun extractModuleFromDirectory(directory: PsiDirectory, module: Module, moduleName: @NlsSafe String, targetSourceRoot: String?): Promise<Unit> {
return analyzeDependenciesAndCreateModule(directory, module, moduleName, targetSourceRoot)
}
}
}

View File

@@ -61,6 +61,9 @@ class ExtractModuleFromPackageDialog(private val project: Project, moduleName: S
.component
}
}
row {
comment(JavaUiBundle.message("dialog.comment.compile.modules"))
}
}
nameField.select(nameField.text.lastIndexOf('.') + 1, nameField.text.length)
return panel

View File

@@ -0,0 +1,248 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.ide.extractModule
import com.intellij.CommonBundle
import com.intellij.ide.JavaUiBundle
import com.intellij.ide.SaveAndSyncHandler
import com.intellij.openapi.application.readAction
import com.intellij.openapi.application.writeAction
import com.intellij.openapi.compiler.CompilerManager
import com.intellij.openapi.components.Service
import com.intellij.openapi.diagnostic.ControlFlowException
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.module.Module
import com.intellij.openapi.module.ModuleManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.*
import com.intellij.openapi.roots.libraries.Library
import com.intellij.openapi.ui.Messages
import com.intellij.openapi.util.NlsSafe
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.platform.ide.progress.withBackgroundProgress
import com.intellij.psi.JavaDirectoryService
import com.intellij.psi.JavaPsiFacade
import com.intellij.psi.PsiDirectory
import com.intellij.psi.PsiManager
import com.intellij.refactoring.move.moveClassesOrPackages.MoveClassesOrPackagesUtil
import com.intellij.util.concurrency.annotations.RequiresEdt
import com.intellij.util.concurrency.annotations.RequiresReadLock
import com.intellij.util.concurrency.annotations.RequiresWriteLock
import com.intellij.util.graph.GraphAlgorithms
import com.intellij.workspaceModel.ide.legacyBridge.impl.java.JAVA_MODULE_ENTITY_TYPE_ID_NAME
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jetbrains.annotations.TestOnly
import java.nio.file.Path
import kotlin.io.path.ExperimentalPathApi
import kotlin.io.path.extension
import kotlin.io.path.walk
private val LOG = logger<ExtractModuleService>()
internal class DependentModule(
val module: Module,
val stillDependsOnOldModule: Boolean,
)
internal suspend fun compilerOutputPath(module: Module): Path? = readAction {
CompilerModuleExtension.getInstance(module)?.compilerOutputPath?.toNioPath()
}
internal suspend fun Path.forEachClassfile(action: suspend (Path) -> Unit) {
walk().filter { it.extension == "class" }.forEach { path ->
action(path)
}
}
@Service(Service.Level.PROJECT)
class ExtractModuleService(
private val project: Project,
private val coroutineScope: CoroutineScope,
) {
@RequiresEdt
fun analyzeDependenciesAndCreateModuleInBackground(
directory: PsiDirectory,
module: Module,
moduleName: @NlsSafe String,
targetSourceRootPath: String?,
) {
CompilerManager.getInstance(project).make { aborted, errors, _, _ ->
if (aborted || errors > 0) {
return@make
}
coroutineScope.launch {
analyzeDependenciesAndCreateModule(directory, module, moduleName, targetSourceRootPath)
}
}
}
@OptIn(ExperimentalPathApi::class)
private suspend fun analyzeDependenciesAndCreateModule(
directory: PsiDirectory,
module: Module,
moduleName: @NlsSafe String,
targetSourceRootPath: String?,
) {
withBackgroundProgress(project, JavaUiBundle.message("progress.title.extract.module.analyzing.dependencies", directory.name)) {
val usedModules = LinkedHashSet<Module>()
val usedLibraries = LinkedHashSet<Library>()
val compilerOutputPath = compilerOutputPath(module) ?: return@withBackgroundProgress
val packageName = readAction {
JavaDirectoryService.getInstance().getPackage(directory)?.qualifiedName
} ?: return@withBackgroundProgress
val compiledPackagePath = packageName.replace('.', '/').let { compilerOutputPath.resolve(it) }
val fileProcessor = ExtractModuleFileProcessor()
compiledPackagePath.forEachClassfile { path ->
fileProcessor.processFile(path)
}
readAction {
val fileIndex = ProjectFileIndex.getInstance(module.project)
fileProcessor.referencedClasses.forEach { className ->
val file = JavaPsiFacade.getInstance(module.project).findClass(className, module.getModuleWithDependenciesAndLibrariesScope(
false))?.containingFile
val virtualFile = file?.virtualFile ?: return@forEach
val depModule = fileIndex.getModuleForFile(virtualFile)
if (depModule != null) {
usedModules.add(depModule)
return@forEach
}
val library = fileIndex.getOrderEntriesForFile(virtualFile).asSequence()
.filterIsInstance<LibraryOrderEntry>().filter { !it.isModuleLevel }.mapNotNull { it.library }.firstOrNull()
if (library != null) {
usedLibraries.add(library)
}
}
}
try {
val allDependentModules = readAction {
collectDependentModules(module)
}
val packageClasses = fileProcessor.gatheredClassLinks.keys.filterTo(HashSet()) { it.startsWith("$packageName.") }
val moduleClasses = ExtractModuleFileProcessor().let { fileProcessor ->
compilerOutputPath.forEachClassfile { path ->
fileProcessor.processFile(path)
}
fileProcessor.gatheredClassLinks.keys.filterTo(HashSet()) { it !in packageClasses }
}
val packageDependentModules = filterDependentModules(allDependentModules, packageClasses, moduleClasses)
writeAction {
extractModule(directory, module, moduleName, usedModules, usedLibraries, targetSourceRootPath, packageDependentModules)
}
ModuleDependenciesCleaner(module, usedModules, compilerOutputPath, compiledPackagePath).startInBackground()
}
catch (e: Throwable) {
if (e !is ControlFlowException) {
LOG.info(e)
Messages.showErrorDialog(project, JavaUiBundle.message("dialog.message.failed.to.extract.module", e),
CommonBundle.getErrorTitle())
}
}
}
}
@OptIn(ExperimentalPathApi::class)
private suspend fun filterDependentModules(dependentModules: Set<Module>, packageClasses: Set<String>, moduleClasses: HashSet<String>) =
dependentModules.mapNotNull {
val compilerOutputPath = compilerOutputPath(it) ?: return@mapNotNull null
val fileProcessor = ExtractModuleFileProcessor()
compilerOutputPath.forEachClassfile { path ->
withContext(Dispatchers.IO) {
fileProcessor.processFile(path)
}
}
val packageDependency = fileProcessor.referencedClasses.any { it in packageClasses }
if (!packageDependency) return@mapNotNull null
val stillDependsOnModule = fileProcessor.referencedClasses.any { it in moduleClasses }
DependentModule(it, stillDependsOnModule)
}
@RequiresReadLock
private fun collectDependentModules(module: Module): Set<Module> {
val moduleGraph = ModuleManager.getInstance(project).moduleGraph()
val dependentModules = LinkedHashSet<Module>()
GraphAlgorithms.getInstance().collectOutsRecursively(moduleGraph, module, dependentModules)
return dependentModules
}
@RequiresWriteLock
private fun extractModule(
directory: PsiDirectory,
module: Module,
moduleName: @NlsSafe String,
usedModules: Set<Module>,
usedLibraries: Set<Library>,
targetSourceRootPath: String?,
packageDependentModules: List<DependentModule>,
) {
val packagePrefix = JavaDirectoryService.getInstance().getPackage(directory)?.qualifiedName ?: ""
val targetSourceRoot = targetSourceRootPath?.let { VfsUtil.createDirectories(it) }
val (contentRoot, imlFileDirectory) = if (targetSourceRoot != null) {
val parent = targetSourceRoot.parent
if (parent in ModuleRootManager.getInstance(module).contentRoots) targetSourceRoot to module.moduleNioFile.parent
else parent to parent.toNioPath()
}
else {
directory.virtualFile to module.moduleNioFile.parent
}
val newModule = ModuleManager.getInstance(module.project).newModule(imlFileDirectory.resolve("$moduleName.iml"),
JAVA_MODULE_ENTITY_TYPE_ID_NAME)
ModuleRootModificationUtil.updateModel(newModule) { model ->
if (ModuleRootManager.getInstance(module).isSdkInherited) {
model.inheritSdk()
}
else {
model.sdk = ModuleRootManager.getInstance(module).sdk
}
val contentEntry = model.addContentEntry(contentRoot)
if (targetSourceRoot != null) {
contentEntry.addSourceFolder(targetSourceRoot, false)
}
else {
contentEntry.addSourceFolder(directory.virtualFile, false, packagePrefix)
}
val moduleDependencies = JavaProjectDependenciesAnalyzer.removeDuplicatingDependencies(usedModules)
moduleDependencies.forEach { model.addModuleOrderEntry(it) }
val exportedLibraries = HashSet<Library>()
for (moduleDependency in moduleDependencies) {
ModuleRootManager.getInstance(moduleDependency).orderEntries().exportedOnly().recursively().forEachLibrary {
exportedLibraries.add(it)
}
}
(usedLibraries - exportedLibraries).forEach { model.addLibraryEntry(it) }
}
packageDependentModules.forEach { dependentModule ->
ModuleRootModificationUtil.updateModel(dependentModule.module) { model ->
model.addModuleOrderEntry(newModule)
if (!dependentModule.stillDependsOnOldModule) {
model.findModuleOrderEntry(module)?.let { orderEntry ->
model.removeOrderEntry(orderEntry)
} ?: LOG.error("Could not find module order entry for $module in ${dependentModule.module}")
}
}
}
if (targetSourceRoot != null) {
val targetDirectory = VfsUtil.createDirectoryIfMissing(targetSourceRoot, packagePrefix.replace('.', '/'))
MoveClassesOrPackagesUtil.moveDirectoryRecursively(directory,
PsiManager.getInstance(module.project).findDirectory(targetDirectory.parent))
}
SaveAndSyncHandler.getInstance().scheduleProjectSave(module.project)
}
@TestOnly
suspend fun extractModuleFromDirectory(directory: PsiDirectory, module: Module, moduleName: @NlsSafe String, targetSourceRoot: String?) {
analyzeDependenciesAndCreateModule(directory, module, moduleName, targetSourceRoot)
}
}

View File

@@ -1,4 +1,4 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.ide.extractModule
import com.intellij.analysis.AnalysisScope
@@ -7,73 +7,116 @@ import com.intellij.notification.Notification
import com.intellij.notification.NotificationAction
import com.intellij.notification.NotificationType
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.application.runWriteAction
import com.intellij.openapi.application.readAction
import com.intellij.openapi.application.writeAction
import com.intellij.openapi.module.Module
import com.intellij.openapi.module.ModuleManager
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.Task
import com.intellij.openapi.roots.*
import com.intellij.openapi.roots.impl.OrderEntryUtil
import com.intellij.openapi.roots.libraries.Library
import com.intellij.packageDependencies.DependenciesToolWindow
import com.intellij.packageDependencies.ForwardDependenciesBuilder
import com.intellij.packageDependencies.ui.DependenciesPanel
import com.intellij.psi.PsiFile
import com.intellij.platform.ide.progress.withBackgroundProgress
import com.intellij.psi.JavaPsiFacade
import com.intellij.ui.content.ContentFactory
import org.jetbrains.concurrency.AsyncPromise
import com.intellij.util.concurrency.annotations.RequiresReadLock
import java.nio.file.Path
import kotlin.io.path.ExperimentalPathApi
/**
* Finds and removes dependencies which aren't used in the code.
*/
class ModuleDependenciesCleaner(private val module: Module, dependenciesToCheck: Collection<Module>) {
class ModuleDependenciesCleaner(
private val module: Module,
dependenciesToCheck: Collection<Module>,
private val compilerOutputPath: Path,
private val compiledPackagePath: Path,
) {
private val dependenciesToCheck = dependenciesToCheck.toSet()
private val project = module.project
fun startInBackground(promise: AsyncPromise<Unit>) {
val builder = ForwardDependenciesBuilder(project, AnalysisScope(module))
object : Task.Backgroundable(project, JavaUiBundle.message("progress.title.searching.for.redundant.dependencies", module.name)) {
override fun run(indicator: ProgressIndicator) {
indicator.isIndeterminate = false
builder.analyze()
runReadAction { processRedundantDependencies(builder.directDependencies, promise) }
@OptIn(ExperimentalPathApi::class)
suspend fun startInBackground() {
val fileProcessor = ExtractModuleFileProcessor()
compilerOutputPath.forEachClassfile { path ->
if (!path.startsWith(compiledPackagePath)) {
fileProcessor.processFile(path)
}
}.queue()
}
private fun processRedundantDependencies(directDependencies: Map<PsiFile, Set<PsiFile>>, promise: AsyncPromise<Unit>) {
val usedModules: MutableSet<Module> = HashSet()
val usedLibraries: MutableSet<Library> = HashSet()
readAction {
val fileIndex = ProjectFileIndex.getInstance(module.project)
val usedModules = directDependencies.values.asSequence().flatten().mapNotNullTo(HashSet()) {
it.virtualFile?.let { file -> fileIndex.getModuleForFile(file) }
fileProcessor.referencedClasses
.asSequence()
.mapNotNull { className ->
findFile(className)?.virtualFile
}
.forEach { virtualFile ->
val dependencyModule = fileIndex.getModuleForFile(virtualFile)
if (dependencyModule != null) {
usedModules.add(dependencyModule)
}
else {
fileIndex.getOrderEntriesForFile(virtualFile).asSequence()
.filterIsInstance<LibraryOrderEntry>()
.filter { !it.isModuleLevel }
.mapNotNull { it.library }
.firstOrNull()
?.let { usedLibraries.add(it) }
}
}
}
val builder = ForwardDependenciesBuilder(project, AnalysisScope(module))
val dependenciesToRemove =
withBackgroundProgress(project, JavaUiBundle.message("progress.title.searching.for.redundant.dependencies", module.name)) {
builder.analyze()
readAction { processRedundantDependencies(usedModules, fileProcessor.gatheredClassLinks) }
} ?: return
writeAction {
removeDependencies(dependenciesToRemove, usedModules)
}
}
@RequiresReadLock
private fun processRedundantDependencies(usedModules: Set<Module>, classLinks: Map<String, Set<String>>): Set<Module>? {
val dependenciesToRemove = dependenciesToCheck - usedModules
if (dependenciesToRemove.isEmpty()) {
val fileIndex = ProjectFileIndex.getInstance(module.project)
val builder = ForwardDependenciesBuilder(project, AnalysisScope(module))
for ((file, dependencies) in directDependencies) {
if (!file.isValid) continue
val relevantDependencies = dependencies.filterTo(LinkedHashSet()) { psiFile ->
for ((className, dependenciesNames) in classLinks) {
val file = findFile(className)
if (file == null || !file.isValid) continue
val relevantDependencies = dependenciesNames.mapNotNullTo(LinkedHashSet()) {
findFile(it)?.takeIf { psiFile ->
val virtualFile = psiFile.virtualFile
virtualFile != null && fileIndex.getModuleForFile(virtualFile) in dependenciesToCheck
}
}
if (relevantDependencies.isNotEmpty()) {
builder.directDependencies[file] = relevantDependencies
builder.dependencies[file] = relevantDependencies
}
}
showNothingToCleanNotification(builder)
promise.setResult(Unit)
return
return null
}
ApplicationManager.getApplication().invokeLater {
runWriteAction {
removeDependencies(dependenciesToRemove, usedModules)
promise.setResult(Unit)
}
}
return dependenciesToRemove
}
private fun removeDependencies(dependenciesToRemove: Set<Module>, usedModules: HashSet<Module>) {
private fun findFile(className: String) = JavaPsiFacade.getInstance(module.project).findClass(className, module.getModuleWithDependenciesAndLibrariesScope(
false))?.containingFile
private fun removeDependencies(dependenciesToRemove: Set<Module>, usedModules: Set<Module>) {
if (module.isDisposed || dependenciesToRemove.any { it.isDisposed }) return
val model = ModuleRootManager.getInstance(module).modifiableModel
@@ -110,15 +153,21 @@ class ModuleDependenciesCleaner(private val module: Module, dependenciesToCheck:
val transitiveDependenciesMessage =
if (transitiveDependenciesToAdd.isNotEmpty()) JavaUiBundle.message("notification.content.transitive.dependencies.were.added", transitiveDependenciesToAdd.first().name, transitiveDependenciesToAdd.size - 1)
else ""
val notification = Notification("Dependencies", JavaUiBundle.message("notification.title.dependencies.were.cleaned.up", module.name),
val notification = Notification(
"Dependencies",
JavaUiBundle.message("notification.title.dependencies.were.cleaned.up", module.name),
JavaUiBundle.message("notification.content.unused.dependencies.were.removed", dependenciesToRemove.first().name, dependenciesToRemove.size - 1, transitiveDependenciesMessage),
NotificationType.INFORMATION)
NotificationType.INFORMATION
)
notification.notify(project)
}
private fun showNothingToCleanNotification(builder: ForwardDependenciesBuilder) {
val notification = Notification("Dependencies",
JavaUiBundle.message("notification.content.none.module.dependencies.can.be.safely.removed", module.name), NotificationType.INFORMATION)
val notification = Notification(
"Dependencies",
JavaUiBundle.message("notification.content.none.module.dependencies.can.be.safely.removed", module.name),
NotificationType.INFORMATION
)
notification.addAction(ShowDependenciesAction(module, builder))
notification.notify(project)
}

View File

@@ -1,9 +1,12 @@
// 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.ide.projectView.actions
import com.intellij.ide.extractModule.ExtractModuleFromPackageAction
import com.intellij.ide.extractModule.ExtractModuleService
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.compiler.CompilerMessageCategory
import com.intellij.openapi.components.service
import com.intellij.openapi.module.Module
import com.intellij.openapi.module.ModuleManager
import com.intellij.openapi.roots.DependencyScope
import com.intellij.openapi.roots.ModuleRootManager
import com.intellij.openapi.roots.ModuleRootModificationUtil
@@ -11,15 +14,18 @@ import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.psi.PsiDirectory
import com.intellij.psi.PsiManager
import com.intellij.testFramework.ApplicationRule
import com.intellij.testFramework.CompilerTester
import com.intellij.testFramework.DisposableRule
import com.intellij.testFramework.VfsTestUtil
import com.intellij.testFramework.rules.ProjectModelRule
import kotlinx.coroutines.runBlocking
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.SoftAssertions
import org.jetbrains.jps.model.java.JavaSourceRootType
import org.junit.ClassRule
import org.junit.Rule
import org.junit.Test
import java.nio.file.Path
import java.util.concurrent.TimeUnit
import kotlin.io.path.invariantSeparatorsPathString
class ExtractModuleFromPackageActionTest {
@@ -33,6 +39,10 @@ class ExtractModuleFromPackageActionTest {
@JvmField
val projectModel = ProjectModelRule()
@Rule
@JvmField
val disposableRule = DisposableRule()
@Test
fun `extract module in place`() {
val (main, directory) = prepareProject()
@@ -78,10 +88,44 @@ class ExtractModuleFromPackageActionTest {
assertThat(mainRoots.dependencies).containsExactly(dep2, exported)
}
@Test
fun `add dependencies from others modules after extracting`() {
val (main, directory) = prepareProject()
val otherModuleWithoutReference = projectModel.createModule("otherWithoutReference")
val otherModuleWithReference = projectModel.createModule("otherWithReference")
projectModel.addSourceRoot(otherModuleWithReference, "src", JavaSourceRootType.SOURCE).let {
VfsTestUtil.createFile(it, "other/Other.java", "package other;\npublic class Other { main.MyClass myClass; xxx.Main main; }")
}
val otherModuleWithReferenceOnExtractedOnly = projectModel.createModule("otherWithReferenceOnExtracedOnly")
projectModel.addSourceRoot(otherModuleWithReferenceOnExtractedOnly, "src", JavaSourceRootType.SOURCE).let {
VfsTestUtil.createFile(it, "other2/Other2.java", "package other2;\npublic class Other2 { xxx.Main other2; }")
}
ModuleRootModificationUtil.addDependency(otherModuleWithoutReference, main)
ModuleRootModificationUtil.addDependency(otherModuleWithReference, main)
ModuleRootModificationUtil.addDependency(otherModuleWithReferenceOnExtractedOnly, main)
extractModule(directory, main, null)
val extracted = projectModel.moduleManager.findModuleByName("main.xxx")!!
with(SoftAssertions()) {
assertThat(ModuleRootManager.getInstance(otherModuleWithReference).dependencies).containsExactly(main, extracted)
assertThat(ModuleRootManager.getInstance(otherModuleWithoutReference).dependencies).containsExactly(main)
assertThat(ModuleRootManager.getInstance(otherModuleWithReferenceOnExtractedOnly).dependencies).containsExactly(extracted)
assertAll()
}
}
private fun extractModule(directory: PsiDirectory, main: Module, targetSourceRoot: String?) {
val promise = ExtractModuleFromPackageAction.extractModuleFromDirectory(directory, main, "main.xxx",
val compilerTester = CompilerTester(projectModel.project, ModuleManager.getInstance(projectModel.project).modules.toList(), disposableRule.disposable)
val messages = compilerTester.rebuild()
assertThat(messages.filter { it.category == CompilerMessageCategory.ERROR }).isEmpty()
runBlocking {
projectModel.project.service<ExtractModuleService>().extractModuleFromDirectory(directory, main, "main.xxx",
targetSourceRoot)
promise.blockingGet(10, TimeUnit.SECONDS)
}
}
private fun prepareProject(addDirectUsageOfExportedModule: Boolean = false): Pair<Module, PsiDirectory> {
@@ -96,8 +140,8 @@ class ExtractModuleFromPackageActionTest {
val dep1Src = projectModel.addSourceRoot(dep1, "src", JavaSourceRootType.SOURCE)
val dep2Src = projectModel.addSourceRoot(dep2, "src", JavaSourceRootType.SOURCE)
val exportedSrc = projectModel.addSourceRoot(exported, "src", JavaSourceRootType.SOURCE)
val mainClass = VfsTestUtil.createFile(mainSrc, "xxx/Main.java", "package xxx;\nclass Main extends dep1.Dep1 { exported.Util u; }")
VfsTestUtil.createFile(mainSrc, "main/MyClass.java", "package main;\nclass MyClass {}")
val mainClass = VfsTestUtil.createFile(mainSrc, "xxx/Main.java", "package xxx;\npublic class Main extends dep1.Dep1 { exported.Util u; }")
VfsTestUtil.createFile(mainSrc, "main/MyClass.java", "package main;\npublic class MyClass { dep2.Dep2 d; }")
if (addDirectUsageOfExportedModule) {
VfsTestUtil.createFile(mainSrc, "main/ExportedUsage.java", "package main;\nclass ExportedUsage { exported.Util u; }")
}