diff --git a/java/java-impl/src/com/intellij/execution/vmModules/VmModulesExecutor.kt b/java/java-impl/src/com/intellij/execution/vmModules/VmModulesExecutor.kt new file mode 100644 index 000000000000..8f3bbb259e07 --- /dev/null +++ b/java/java-impl/src/com/intellij/execution/vmModules/VmModulesExecutor.kt @@ -0,0 +1,83 @@ +// 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.execution.vmModules + +import com.intellij.execution.configurations.GeneralCommandLine +import com.intellij.execution.process.CapturingProcessRunner +import com.intellij.execution.process.OSProcessHandler +import com.intellij.execution.process.ProcessNotCreatedException +import com.intellij.openapi.module.Module +import com.intellij.openapi.projectRoots.JavaSdk +import com.intellij.openapi.projectRoots.Sdk +import com.intellij.openapi.roots.ModuleRootManager +import com.intellij.util.ConcurrencyUtil +import com.intellij.util.concurrency.AppExecutorUtil +import com.intellij.util.containers.CollectionFactory +import java.util.concurrent.* + +class VmModulesExecutor { + companion object { + private val ourData: ConcurrentMap>> = CollectionFactory.createConcurrentSoftValueMap() + } + + /** + * Retrieves or computes the list of jigsaw modules available for the specified JDK. + * + * @param module the intellij module for which to retrieve or compute the list of available jigsaw modules + * @return the list of jigsaw modules available for the specified intellij module or + * empty if the intellij module does not contain a JDK or an error has occurred. + */ + fun getOrComputeModulesForJdk(module: Module): List { + val sdk = ModuleRootManager.getInstance(module).sdk ?: return listOf() + if (sdk.sdkType !is JavaSdk || sdk.homePath == null) return listOf() + try { + val future = getOrCreateFuture(sdk) + if (future.isDone) { + return future.get(ConcurrencyUtil.DEFAULT_TIMEOUT_MS, TimeUnit.MILLISECONDS) + } + } + catch (_: InterruptedException) { + } + catch (_: TimeoutException) { + } + catch (_: ExecutionException) { + } + return listOf() + } + + private fun getOrCreateFuture(sdk: Sdk): CompletableFuture> { + val sdkHome = sdk.homePath + var future = ourData.computeIfAbsent(sdkHome) { CompletableFuture.supplyAsync({ computeModules(sdk) }, AppExecutorUtil.getAppExecutorService()) } + if (future.isDone) { + // sometimes the timeout may appear, and in order not to block the possibility to get the completion afterwards, it is better to retry + if (future.get(ConcurrencyUtil.DEFAULT_TIMEOUT_MS, TimeUnit.MILLISECONDS) == null) { + future = ourData.computeIfPresent(sdkHome) { _, value -> + if (future != value) return@computeIfPresent value // another thread has already changed the value + return@computeIfPresent CompletableFuture.supplyAsync({ computeModules(sdk) }, AppExecutorUtil.getAppExecutorService()) + } + } + } + return future + } + + // when null is returned, it was a timeout + private fun computeModules(sdk: Sdk): List? { + val vmPath = JavaSdk.getInstance().getVMExecutablePath(sdk) + val generalCommandLine = GeneralCommandLine(vmPath).apply { + addParameters("--list-modules") + } + try { + val handler = OSProcessHandler(generalCommandLine) + val runner = CapturingProcessRunner(handler) + val output = runner.runProcess(1_000) + if (output.isTimeout) { + return null + } + else { + return output.stdout.lineSequence().map { line -> line.substringBefore('@') }.toList() + } + } + catch (e: ProcessNotCreatedException) { + return null + } + } +} \ No newline at end of file diff --git a/java/java-impl/src/com/intellij/psi/impl/JavaPlatformModuleSystem.kt b/java/java-impl/src/com/intellij/psi/impl/JavaPlatformModuleSystem.kt index 7d35ffb32a1d..a71bf1d913a8 100644 --- a/java/java-impl/src/com/intellij/psi/impl/JavaPlatformModuleSystem.kt +++ b/java/java-impl/src/com/intellij/psi/impl/JavaPlatformModuleSystem.kt @@ -9,6 +9,7 @@ import com.intellij.codeInsight.daemon.impl.analysis.JavaModuleGraphUtil import com.intellij.codeInsight.daemon.impl.quickfix.AddExportsDirectiveFix import com.intellij.codeInsight.daemon.impl.quickfix.AddRequiresDirectiveFix import com.intellij.codeInspection.util.IntentionName +import com.intellij.execution.vmModules.VmModulesExecutor import com.intellij.java.JavaBundle import com.intellij.modcommand.ActionContext import com.intellij.modcommand.ModCommand @@ -40,6 +41,8 @@ private val javaVersionPattern: Pattern by lazy { Pattern.compile("java\\d+") } * @see JEP 261: Module System */ internal class JavaPlatformModuleSystem : JavaModuleSystemEx { + private val vmModulesExecutor: VmModulesExecutor = VmModulesExecutor() + override fun getName(): String = JavaBundle.message("java.platform.module.system.name") override fun isAccessible(targetPackageName: String, targetFile: PsiFile?, place: PsiElement): Boolean { @@ -50,8 +53,8 @@ internal class JavaPlatformModuleSystem : JavaModuleSystemEx { return getProblem(targetPackageName, targetFile, place, false) { use, _, target, _, _ -> JavaModuleGraphUtil.reads(use, target) } } - private fun isExported(useModule: PsiJavaModule, packageName: String, targetModule: PsiJavaModule, useModuleName: String, module: Module?) : Boolean { - if(!targetModule.isPhysical || JavaModuleGraphUtil.exports(targetModule, packageName, useModule)) return true + private fun isExported(useModule: PsiJavaModule, packageName: String, targetModule: PsiJavaModule, useModuleName: String, module: Module?): Boolean { + if (!targetModule.isPhysical || JavaModuleGraphUtil.exports(targetModule, packageName, useModule)) return true if (module == null) return false return inAddedExports(module, targetModule.name, packageName, useModuleName) } @@ -118,16 +121,12 @@ internal class JavaPlatformModuleSystem : JavaModuleSystemEx { if (targetName.startsWith("java.") && targetName != PsiJavaModule.JAVA_BASE && !inAddedModules(module, targetName) && - !hasUpgrade(module, targetName, packageName, place)) { - val root = DumbModeAccessType.RELIABLE_DATA_ONLY.ignoreDumbMode { - JavaPsiFacade.getInstance(place.project).findModule("java.se", module.moduleWithLibrariesScope) - } - if (root != null && !isAccessible(root, packageName, targetModule, useName, module)) { - return if (quick) ERR - else ErrorWithFixes( - JavaErrorBundle.message("module.access.not.in.graph", packageName, targetName), - listOf(AddModulesOptionFix(module, targetName).asIntention())) - } + !hasUpgrade(module, targetName, packageName, place) && + !accessibleFromLoadedModules(module, targetName, place, isAccessible, packageName, targetModule, useName)) { + return if (quick) ERR + else ErrorWithFixes( + JavaErrorBundle.message("module.access.not.in.graph", packageName, targetName), + listOf(AddModulesOptionFix(module, targetName).asIntention())) } } @@ -175,6 +174,25 @@ internal class JavaPlatformModuleSystem : JavaModuleSystemEx { return null } + private fun accessibleFromLoadedModules(module: Module, + targetName: String, + place: PsiFileSystemItem, + isAccessible: (useModule: PsiJavaModule, packageName: String, targetModule: PsiJavaModule, useModuleName: String, module: Module?) -> Boolean, + packageName: String, + targetModule: PsiJavaModule, + useName: String): Boolean { + val modules = vmModulesExecutor.getOrComputeModulesForJdk(module) + if (!modules.isEmpty()) { + return modules.contains(targetName) + } + else { + val root = DumbModeAccessType.RELIABLE_DATA_ONLY.ignoreDumbMode { + JavaPsiFacade.getInstance(place.project).findModule("java.se", module.moduleWithLibrariesScope) + } + return root == null || isAccessible(root, packageName, targetModule, useName, module) + } + } + private fun inSameMultiReleaseModule(place: PsiElement, target: PsiElement): Boolean { val placeModule = ModuleUtilCore.findModuleForPsiElement(place) ?: return false val targetModule = ModuleUtilCore.findModuleForPsiElement(target) ?: return false