diff --git a/java/idea-ui/BUILD.bazel b/java/idea-ui/BUILD.bazel
index 0467906557d4..0d30847ad78d 100644
--- a/java/idea-ui/BUILD.bazel
+++ b/java/idea-ui/BUILD.bazel
@@ -52,6 +52,7 @@ jvm_library(
"@lib//:maven-resolver-provider",
"//java/java-syntax:syntax",
"@lib//:slf4j-api",
+ "@lib//:asm",
],
runtime_deps = [":ui_resources"]
)
diff --git a/java/idea-ui/intellij.java.ui.iml b/java/idea-ui/intellij.java.ui.iml
index fcf57a3179b6..bc0f509c8e9c 100644
--- a/java/idea-ui/intellij.java.ui.iml
+++ b/java/idea-ui/intellij.java.ui.iml
@@ -48,5 +48,6 @@
+
\ No newline at end of file
diff --git a/java/idea-ui/resources/messages/JavaUiBundle.properties b/java/idea-ui/resources/messages/JavaUiBundle.properties
index f1642113b251..9617b8ec0121 100644
--- a/java/idea-ui/resources/messages/JavaUiBundle.properties
+++ b/java/idea-ui/resources/messages/JavaUiBundle.properties
@@ -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)
diff --git a/java/idea-ui/src/com/intellij/ide/extractModule/ExtractModuleFileProcessor.kt b/java/idea-ui/src/com/intellij/ide/extractModule/ExtractModuleFileProcessor.kt
new file mode 100644
index 000000000000..991b1372ffd0
--- /dev/null
+++ b/java/idea-ui/src/com/intellij/ide/extractModule/ExtractModuleFileProcessor.kt
@@ -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 = HashSet()
+ private val mutableGatheredClassLinks: MutableMap> = 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
+ get() = mutableReferencedClasses
+
+ val gatheredClassLinks: Map>
+ get() = mutableGatheredClassLinks
+
+ fun processFile(path: Path) = classFileAnalyzer.processFile(path)
+}
\ No newline at end of file
diff --git a/java/idea-ui/src/com/intellij/ide/extractModule/ExtractModuleFromPackageAction.kt b/java/idea-ui/src/com/intellij/ide/extractModule/ExtractModuleFromPackageAction.kt
index 1e34d8f601a6..343e00baeea0 100644
--- a/java/idea-ui/src/com/intellij/ide/extractModule/ExtractModuleFromPackageAction.kt
+++ b/java/idea-ui/src/com/intellij/ide/extractModule/ExtractModuleFromPackageAction.kt
@@ -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()
+ .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()
-
- private fun analyzeDependenciesAndCreateModule(directory: PsiDirectory,
- module: Module,
- moduleName: @NlsSafe String,
- targetSourceRootPath: String?): Promise {
- val promise = AsyncPromise()
- 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()
- val usedLibraries = LinkedHashSet()
- 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()
- .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, usedLibraries: Set, 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()
- 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 {
- return analyzeDependenciesAndCreateModule(directory, module, moduleName, targetSourceRoot)
- }
- }
}
-
diff --git a/java/idea-ui/src/com/intellij/ide/extractModule/ExtractModuleFromPackageDialog.kt b/java/idea-ui/src/com/intellij/ide/extractModule/ExtractModuleFromPackageDialog.kt
index 488878a6b8f1..cca591498743 100644
--- a/java/idea-ui/src/com/intellij/ide/extractModule/ExtractModuleFromPackageDialog.kt
+++ b/java/idea-ui/src/com/intellij/ide/extractModule/ExtractModuleFromPackageDialog.kt
@@ -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
diff --git a/java/idea-ui/src/com/intellij/ide/extractModule/ExtractModuleService.kt b/java/idea-ui/src/com/intellij/ide/extractModule/ExtractModuleService.kt
new file mode 100644
index 000000000000..c672726e7c67
--- /dev/null
+++ b/java/idea-ui/src/com/intellij/ide/extractModule/ExtractModuleService.kt
@@ -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()
+
+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()
+ val usedLibraries = LinkedHashSet()
+ 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().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, packageClasses: Set, moduleClasses: HashSet) =
+ 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 {
+ val moduleGraph = ModuleManager.getInstance(project).moduleGraph()
+ val dependentModules = LinkedHashSet()
+ GraphAlgorithms.getInstance().collectOutsRecursively(moduleGraph, module, dependentModules)
+ return dependentModules
+ }
+
+ @RequiresWriteLock
+ private fun extractModule(
+ directory: PsiDirectory,
+ module: Module,
+ moduleName: @NlsSafe String,
+ usedModules: Set,
+ usedLibraries: Set,
+ targetSourceRootPath: String?,
+ packageDependentModules: List,
+ ) {
+ 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()
+ 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)
+ }
+}
diff --git a/java/idea-ui/src/com/intellij/ide/extractModule/ModuleDependenciesCleaner.kt b/java/idea-ui/src/com/intellij/ide/extractModule/ModuleDependenciesCleaner.kt
index 01fb4f8b6968..16309d150d5a 100644
--- a/java/idea-ui/src/com/intellij/ide/extractModule/ModuleDependenciesCleaner.kt
+++ b/java/idea-ui/src/com/intellij/ide/extractModule/ModuleDependenciesCleaner.kt
@@ -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) {
+class ModuleDependenciesCleaner(
+ private val module: Module,
+ dependenciesToCheck: Collection,
+ private val compilerOutputPath: Path,
+ private val compiledPackagePath: Path,
+) {
private val dependenciesToCheck = dependenciesToCheck.toSet()
private val project = module.project
- fun startInBackground(promise: AsyncPromise) {
- 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()
+ }
+
+ val usedModules: MutableSet = HashSet()
+ val usedLibraries: MutableSet = HashSet()
+
+ readAction {
+ val fileIndex = ProjectFileIndex.getInstance(module.project)
+ 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()
+ .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)
+ }
}
- private fun processRedundantDependencies(directDependencies: Map>, promise: AsyncPromise) {
- val fileIndex = ProjectFileIndex.getInstance(module.project)
- val usedModules = directDependencies.values.asSequence().flatten().mapNotNullTo(HashSet()) {
- it.virtualFile?.let { file -> fileIndex.getModuleForFile(file) }
- }
+ @RequiresReadLock
+ private fun processRedundantDependencies(usedModules: Set, classLinks: Map>): Set? {
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 ->
- val virtualFile = psiFile.virtualFile
- virtualFile != null && fileIndex.getModuleForFile(virtualFile) in dependenciesToCheck
+
+ 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, usedModules: HashSet) {
+ private fun findFile(className: String) = JavaPsiFacade.getInstance(module.project).findClass(className, module.getModuleWithDependenciesAndLibrariesScope(
+ false))?.containingFile
+
+ private fun removeDependencies(dependenciesToRemove: Set, usedModules: Set) {
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),
- JavaUiBundle.message("notification.content.unused.dependencies.were.removed", dependenciesToRemove.first().name, dependenciesToRemove.size - 1, transitiveDependenciesMessage),
- NotificationType.INFORMATION)
+ 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
+ )
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)
}
@@ -135,4 +184,4 @@ class ModuleDependenciesCleaner(private val module: Module, dependenciesToCheck:
DependenciesToolWindow.getInstance(project).addContent(content)
}
}
-}
\ No newline at end of file
+}
diff --git a/java/idea-ui/testSrc/com/intellij/ide/projectView/actions/ExtractModuleFromPackageActionTest.kt b/java/idea-ui/testSrc/com/intellij/ide/projectView/actions/ExtractModuleFromPackageActionTest.kt
index ae75bf0315e7..c7ac364e3acb 100644
--- a/java/idea-ui/testSrc/com/intellij/ide/projectView/actions/ExtractModuleFromPackageActionTest.kt
+++ b/java/idea-ui/testSrc/com/intellij/ide/projectView/actions/ExtractModuleFromPackageActionTest.kt
@@ -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",
- targetSourceRoot)
- promise.blockingGet(10, TimeUnit.SECONDS)
+ 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().extractModuleFromDirectory(directory, main, "main.xxx",
+ targetSourceRoot)
+ }
}
private fun prepareProject(addDirectUsageOfExportedModule: Boolean = false): Pair {
@@ -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; }")
}