mirror of
https://gitflic.ru/project/openide/openide.git
synced 2025-12-15 02:59:33 +07:00
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:
committed by
intellij-monorepo-bot
parent
995eb27843
commit
cd627067a5
@@ -52,6 +52,7 @@ jvm_library(
|
||||
"@lib//:maven-resolver-provider",
|
||||
"//java/java-syntax:syntax",
|
||||
"@lib//:slf4j-api",
|
||||
"@lib//:asm",
|
||||
],
|
||||
runtime_deps = [":ui_resources"]
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
val usedModules: MutableSet<Module> = HashSet()
|
||||
val usedLibraries: MutableSet<Library> = 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<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)
|
||||
}
|
||||
}
|
||||
|
||||
private fun processRedundantDependencies(directDependencies: Map<PsiFile, Set<PsiFile>>, promise: AsyncPromise<Unit>) {
|
||||
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<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 ->
|
||||
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<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),
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ExtractModuleService>().extractModuleFromDirectory(directory, main, "main.xxx",
|
||||
targetSourceRoot)
|
||||
}
|
||||
}
|
||||
|
||||
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; }")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user