From 9559d696ff8242de9e7ee5571c45df1c4ece470c Mon Sep 17 00:00:00 2001 From: Tagir Valeev Date: Fri, 21 Feb 2025 15:19:24 +0100 Subject: [PATCH] [java-highlighting] Refactor JavaPlatformModuleSystem: separate finding problem, reporting it and creating fixes Part of IDEA-365344 Create a new Java error highlighter with minimal dependencies (PSI only) GitOrigin-RevId: 0990deef5a6fc379464fe1e8c9ecb46d73a1c13b --- .../codeserver/core/JavaPsiModuleUtil.java | 23 ++ .../codeserver/core/JpmsModuleAccessInfo.kt | 271 ++++++++++++ .../java/codeserver/core/JpmsModuleInfo.kt | 58 +++ .../impl/analysis/JavaModuleGraphUtil.java | 24 +- ...9RedundantRequiresStatementInspection.java | 3 +- .../psi/impl/JavaPlatformModuleSystem.kt | 388 +++--------------- .../intellij.jvm.analysis.impl.iml | 1 + ...alDependencyOnInternalPackageInspection.kt | 3 +- .../idea/modules/IdeJavaModuleResolver.kt | 2 +- 9 files changed, 427 insertions(+), 346 deletions(-) create mode 100644 java/codeserver/core/src/com/intellij/java/codeserver/core/JpmsModuleAccessInfo.kt create mode 100644 java/codeserver/core/src/com/intellij/java/codeserver/core/JpmsModuleInfo.kt diff --git a/java/codeserver/core/src/com/intellij/java/codeserver/core/JavaPsiModuleUtil.java b/java/codeserver/core/src/com/intellij/java/codeserver/core/JavaPsiModuleUtil.java index 6839a97daaad..bd4246441fe2 100644 --- a/java/codeserver/core/src/com/intellij/java/codeserver/core/JavaPsiModuleUtil.java +++ b/java/codeserver/core/src/com/intellij/java/codeserver/core/JavaPsiModuleUtil.java @@ -321,6 +321,29 @@ public final class JavaPsiModuleUtil { return Collections.emptyList(); } + /** + * @param source source module + * @param packageName package name in source module + * @param target target module + * @return true if a source module exports a specified package to the target module, or to everybody (if the target module is null) + */ + public static boolean exports(@NotNull PsiJavaModule source, @NotNull String packageName, @Nullable PsiJavaModule target) { + Map> exports = CachedValuesManager.getCachedValue(source, () -> + CachedValueProvider.Result.create(exportsMap(source), source.getContainingFile())); + Set targets = exports.get(packageName); + return targets != null && (targets.isEmpty() || target != null && targets.contains(target.getName())); + } + + private static @NotNull Map> exportsMap(@NotNull PsiJavaModule source) { + Map> map = new HashMap<>(); + for (PsiPackageAccessibilityStatement statement : source.getExports()) { + String pkg = statement.getPackageName(); + List targets = statement.getModuleNames(); + map.put(pkg, targets.isEmpty() ? Collections.emptySet() : new HashSet<>(targets)); + } + return map; + } + /** * Represents a dependency conflict when a single package is imported from two modules * @param packageName package name diff --git a/java/codeserver/core/src/com/intellij/java/codeserver/core/JpmsModuleAccessInfo.kt b/java/codeserver/core/src/com/intellij/java/codeserver/core/JpmsModuleAccessInfo.kt new file mode 100644 index 000000000000..ee13a39ca1d3 --- /dev/null +++ b/java/codeserver/core/src/com/intellij/java/codeserver/core/JpmsModuleAccessInfo.kt @@ -0,0 +1,271 @@ +// 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.java.codeserver.core + +import com.intellij.openapi.module.Module +import com.intellij.openapi.module.ModuleUtilCore +import com.intellij.openapi.roots.JdkOrderEntry +import com.intellij.openapi.roots.ModuleRootManager +import com.intellij.openapi.roots.ProjectRootManager +import com.intellij.pom.java.LanguageLevel +import com.intellij.psi.* +import com.intellij.psi.impl.light.LightJavaModule +import com.intellij.psi.search.GlobalSearchScope +import com.intellij.psi.util.JavaMultiReleaseUtil +import com.intellij.psi.util.PsiUtil +import com.intellij.util.indexing.DumbModeAccessType + +/** + * Represents the access details between the current module and the target module. + * + * @property current The current module. + * @property target The target module. + */ +data class JpmsModuleAccessInfo(val current: JpmsModuleInfo.CurrentModuleInfo, val target: JpmsModuleInfo.TargetModuleInfo) { + enum class JpmsModuleAccessProblem { + FROM_NAMED, + FROM_UNNAMED, + TO_UNNAMED, + PACKAGE_BAD_NAME, + BAD_NAME, + PACKAGE_NOT_IN_GRAPH, + NOT_IN_GRAPH, + PACKAGE_DOES_NOT_READ, + DOES_NOT_READ, + JPS_DEPENDENCY_PROBLEM + } + + fun checkAccess( + place: PsiFileSystemItem, + isAccessible: (JpmsModuleAccessInfo) -> Boolean, + ): JpmsModuleAccessProblem? { + val targetModule = target.module + if (targetModule != null) { + if (targetModule == current.module) { + return null + } + + val currentJpsModule = current.jpsModule + if (current.module == null) { + val origin = targetModule.containingFile?.virtualFile + if (origin == null || currentJpsModule == null || + ModuleRootManager.getInstance(currentJpsModule).fileIndex.getOrderEntryForFile(origin) !is JdkOrderEntry + ) { + return null // a target is not on the mandatory module path + } + + if (!accessibleFromJdkModules(place, isAccessible) && + !inAddedModules(currentJpsModule, targetModule.name) && + !hasUpgrade(currentJpsModule, targetModule.name, target.packageName, place)) { + return JpmsModuleAccessProblem.PACKAGE_NOT_IN_GRAPH + } + } + + if (targetModule !is LightJavaModule && + !JavaPsiModuleUtil.exports(targetModule, target.packageName, current.module) && + (currentJpsModule == null || !inAddedExports(currentJpsModule, targetModule.name, target.packageName, current.name)) && + (currentJpsModule == null || !isPatchedModule(targetModule.name, currentJpsModule, place))) { + return if (current.module == null) JpmsModuleAccessProblem.FROM_UNNAMED else JpmsModuleAccessProblem.FROM_NAMED + } + + if (current.module != null && + targetModule.name != PsiJavaModule.JAVA_BASE && + !isAccessible(this) && + !inAddedReads(current.module, targetModule)) { + return when { + PsiNameHelper.isValidModuleName(targetModule.name, current.module) -> JpmsModuleAccessProblem.PACKAGE_DOES_NOT_READ + else -> JpmsModuleAccessProblem.PACKAGE_BAD_NAME + } + } + } + else if (current.module != null) { + val autoModule = JpmsModuleInfo.TargetModuleInfo(detectAutomaticModule(target), target.packageName) + if (autoModule.module == null) { + return JpmsModuleAccessProblem.TO_UNNAMED + } + else if (!isAccessible(JpmsModuleAccessInfo(current, autoModule)) && + !inAddedReads(current.module, null) && + !inSameMultiReleaseModule(current, target)) { + return JpmsModuleAccessProblem.TO_UNNAMED + } + } + + return null + } + + /** + * @param place place where the target module is accessed + * @return access problem, or null if the target module is accessible without any problem + */ + fun checkModuleAccess(place: PsiElement): JpmsModuleAccessProblem? { + val targetModule = target.module + if (targetModule != null) { + if (targetModule == current.module) { + return null + } + + val currentJpsModule = current.jpsModule + if (current.module == null) { + var origin = targetModule.containingFile?.virtualFile + if (origin == null && targetModule is LightJavaModule) origin = targetModule.rootVirtualFile + if (origin == null || currentJpsModule == null) return null + + if (ModuleRootManager.getInstance(currentJpsModule).fileIndex.getOrderEntryForFile(origin) !is JdkOrderEntry) { + val searchScope = GlobalSearchScope.moduleWithDependenciesAndLibrariesScope(currentJpsModule) + if (searchScope.contains(origin)) return null + return JpmsModuleAccessProblem.JPS_DEPENDENCY_PROBLEM + } + + if (!accessibleFromJdkModules(place, { it.isAccessible() }) && + !inAddedModules(currentJpsModule, targetModule.name)) { + return JpmsModuleAccessProblem.NOT_IN_GRAPH + } + } + + if (current.module != null && + targetModule.name != PsiJavaModule.JAVA_BASE && + !isAccessible() && + !inAddedReads(current.module, targetModule)) { + return if (PsiNameHelper.isValidModuleName(targetModule.name, current.module)) JpmsModuleAccessProblem.DOES_NOT_READ + else JpmsModuleAccessProblem.BAD_NAME + } + } + else if (current.module != null) { + val autoModule = JpmsModuleInfo.TargetModuleInfo(detectAutomaticModule(target), target.packageName) + if (autoModule.module != null && + !JpmsModuleAccessInfo(current, autoModule).isAccessible() && + !inAddedReads(current.module, null) && + !inSameMultiReleaseModule(current, target)) { + return JpmsModuleAccessProblem.TO_UNNAMED + } + } + + return null + } + + fun isExported(): Boolean { + val targetModule = target.module ?: return false + if (!targetModule.isPhysical || JavaPsiModuleUtil.exports(targetModule, target.packageName, current.module)) return true + val currentJpsModule = current.jpsModule ?: return false + return inAddedExports(currentJpsModule, targetModule.name, target.packageName, current.name) + } + + fun isAccessible(): Boolean { + val currentModule = current.module ?: return false + val targetModule = target.module ?: return false + return JavaPsiModuleUtil.reads(currentModule, targetModule) + } + + private fun accessibleFromJdkModules( + place: PsiElement, + isAccessible: (JpmsModuleAccessInfo) -> Boolean, + ): Boolean { + val jpsModule = current.jpsModule ?: return false + val targetModule = target.module ?: return false + if (targetModule.name == PsiJavaModule.JAVA_BASE) return true + + if (!isJdkModule(jpsModule, targetModule)) return false + val languageLevel = PsiUtil.getLanguageLevel(place) + // https://bugs.openjdk.org/browse/JDK-8197532 + val jdkModulePred: (PsiJavaModule) -> Boolean = if (languageLevel >= LanguageLevel.JDK_11) { + { module -> module.exports.any { e -> e.moduleNames.isEmpty() } } + } + else { + val javaSE = DumbModeAccessType.RELIABLE_DATA_ONLY.ignoreDumbMode { + JavaPsiFacade.getInstance(place.project).findModule("java.se", jpsModule.moduleWithLibrariesScope) + } + + if (javaSE != null) { + { module -> + (!module.name.startsWith("java.") && module.exports.any { e -> e.moduleNames.isEmpty() }) || + isAccessible(JpmsModuleAccessInfo(JpmsModuleInfo.CurrentModuleInfo(javaSE, current.name) { jpsModule }, target)) + } + } + else { + { _ -> true } + } + } + val noIncubatorPred: (PsiJavaModule) -> Boolean = { module -> !module.doNotResolveByDefault() } + return jdkModulePred(targetModule) && noIncubatorPred(targetModule) + } + + private fun isJdkModule(jpsModule: Module, psiModule: PsiJavaModule): Boolean { + val sdkHomePath = ModuleRootManager.getInstance(jpsModule).getSdk()?.homePath?.replace('\\', '/') + val moduleFilePath = psiModule.containingFile?.virtualFile?.path?.replace('\\', '/') + if (sdkHomePath != null && moduleFilePath != null) { + return moduleFilePath.startsWith("$sdkHomePath!") || + moduleFilePath.startsWith(if (sdkHomePath.last() == '/') sdkHomePath else "$sdkHomePath/") + } + else { + return psiModule.name.startsWith("java.") || + psiModule.name.startsWith("jdk.") + } + } + + private fun inSameMultiReleaseModule(current: JpmsModuleInfo, target: JpmsModuleInfo): Boolean { + val placeModule = current.jpsModule ?: return false + val targetModule = target.jpsModule ?: return false + return JavaMultiReleaseUtil.areMainAndAdditionalMultiReleaseModules(targetModule, placeModule) + } + + private fun detectAutomaticModule(current: JpmsModuleInfo): PsiJavaModule? { + val module = current.jpsModule ?: return null + return JavaPsiFacade.getInstance(module.project) + .findModule(LightJavaModule.moduleName(module.name), + GlobalSearchScope.moduleScope(module)) + } + + private fun hasUpgrade(module: Module, targetName: String, packageName: String, place: PsiFileSystemItem): Boolean { + if (PsiJavaModule.UPGRADEABLE.contains(targetName)) { + val target = JavaPsiFacade.getInstance(module.project).findPackage(packageName) + if (target != null) { + val useVFile = place.virtualFile + if (useVFile != null) { + val index = ModuleRootManager.getInstance(module).fileIndex + val test = index.isInTestSourceContent(useVFile) + val dirs = target.getDirectories(module.getModuleWithDependenciesAndLibrariesScope(test)) + return dirs.any { index.getOrderEntryForFile(it.virtualFile) !is JdkOrderEntry } + } + } + } + + return false + } + + private fun isPatchedModule(targetModuleName: String, module: Module, place: PsiFileSystemItem): Boolean { + val virtualFile = place.virtualFile ?: return false + val rootForFile = ProjectRootManager.getInstance(place.project).fileIndex.getSourceRootForFile(virtualFile) ?: return false + return JavaCompilerConfigurationProxy.isPatchedModuleRoot(targetModuleName, module, rootForFile) + } + + private fun inAddedExports(module: Module, targetName: String, packageName: String, useName: String): Boolean { + val options = JavaCompilerConfigurationProxy.getAdditionalOptions(module.project, module) + if (options.isEmpty()) return false + val prefix = "${targetName}/${packageName}=" + return JavaCompilerConfigurationProxy.optionValues(options, JavaModuleSystem.ADD_EXPORTS_OPTION) + .filter { it.startsWith(prefix) } + .map { it.substring(prefix.length) } + .flatMap { it.splitToSequence(",") } + .any { it == useName } + } + + private fun inAddedModules(module: Module, moduleName: String): Boolean { + val options = JavaCompilerConfigurationProxy.getAdditionalOptions(module.project, module) + return JavaCompilerConfigurationProxy.optionValues(options, JavaModuleSystem.ADD_MODULES_OPTION) + .flatMap { it.splitToSequence(",") } + .any { it == moduleName || it == JavaModuleSystem.ALL_SYSTEM || it == JavaModuleSystem.ALL_MODULE_PATH } + } + + private fun inAddedReads(fromJavaModule: PsiJavaModule, toJavaModule: PsiJavaModule?): Boolean { + val fromModule = ModuleUtilCore.findModuleForPsiElement(fromJavaModule) ?: return false + val options = JavaCompilerConfigurationProxy.getAdditionalOptions(fromModule.project, fromModule) + return JavaCompilerConfigurationProxy.optionValues(options, JavaModuleSystem.ADD_READS_OPTION) + .flatMap { it.splitToSequence(",") } + .any { + val (optFromModuleName, optToModuleName) = it.split("=").apply { it.first() to it.last() } + fromJavaModule.name == optFromModuleName && + (toJavaModule?.name == optToModuleName || (optToModuleName == JavaModuleSystem.ALL_UNNAMED && isUnnamedModule(toJavaModule))) + } + } + + private fun isUnnamedModule(module: PsiJavaModule?) = module == null || module is LightJavaModule +} \ No newline at end of file diff --git a/java/codeserver/core/src/com/intellij/java/codeserver/core/JpmsModuleInfo.kt b/java/codeserver/core/src/com/intellij/java/codeserver/core/JpmsModuleInfo.kt new file mode 100644 index 000000000000..326f684b2f54 --- /dev/null +++ b/java/codeserver/core/src/com/intellij/java/codeserver/core/JpmsModuleInfo.kt @@ -0,0 +1,58 @@ +// 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.java.codeserver.core + +import com.intellij.openapi.module.Module +import com.intellij.openapi.module.ModuleUtilCore +import com.intellij.psi.JavaModuleSystem +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFileSystemItem +import com.intellij.psi.PsiJavaModule +import com.intellij.psi.impl.light.LightJavaModule + +/** + * Represents a JPMS module and the corresponding module in IntelliJ project model + */ +sealed interface JpmsModuleInfo { + val module: PsiJavaModule? + val jpsModule: Module? + + /** + * Represents the details of a current module. + * + * Note: "name" is not always possible to get from "module". + * For example, "module" can be "java.se", but the name is from the original module. + * + * @property module The PsiJavaModule instance representing the module. + * @property name original module name + * @property jpsModule JPS module initialization. + */ + class CurrentModuleInfo(override val module: PsiJavaModule?, val name: String, jps: () -> Module? = { null }) : JpmsModuleInfo { + constructor(use: PsiJavaModule?, element: PsiElement) : this(use, use?.name ?: JavaModuleSystem.ALL_UNNAMED, { + ModuleUtilCore.findModuleForPsiElement(element) + }) + + override val jpsModule: Module? by lazy { jps() } + } + + /** + * Represents the details of a target module + */ + class TargetModuleInfo(element: PsiElement?, val packageName: String) : JpmsModuleInfo { + override val jpsModule: Module? by lazy { + if (element == null) return@lazy null + ModuleUtilCore.findModuleForPsiElement(element) + } + override val module: PsiJavaModule? by lazy { + JavaPsiModuleUtil.findDescriptorByElement(element) + } + + /** + * @return access information when the specified target module is accessed at a given place + */ + fun accessAt(place: PsiFileSystemItem): JpmsModuleAccessInfo { + val useModule = JavaPsiModuleUtil.findDescriptorByElement(place).let { if (it is LightJavaModule) null else it } + val current = CurrentModuleInfo(useModule, place) + return JpmsModuleAccessInfo(current, this) + } + } +} \ No newline at end of file diff --git a/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/JavaModuleGraphUtil.java b/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/JavaModuleGraphUtil.java index 2866433b5e94..51be42e221fa 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/JavaModuleGraphUtil.java +++ b/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/JavaModuleGraphUtil.java @@ -21,8 +21,6 @@ import com.intellij.psi.*; import com.intellij.psi.impl.light.LightJavaModule; import com.intellij.psi.impl.source.resolve.JavaResolveUtil; import com.intellij.psi.search.GlobalSearchScope; -import com.intellij.psi.util.CachedValueProvider.Result; -import com.intellij.psi.util.CachedValuesManager; import com.intellij.psi.util.PsiUtil; import com.intellij.psi.util.PsiUtilCore; import com.intellij.util.ObjectUtils; @@ -32,7 +30,10 @@ import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.*; +import java.util.Collection; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; import static com.intellij.openapi.roots.DependencyScope.PROVIDED; import static com.intellij.psi.PsiJavaModule.JAVA_BASE; @@ -66,13 +67,6 @@ public final class JavaModuleGraphUtil { return javaModule instanceof LightJavaModule ? null : javaModule; } - public static boolean exports(@NotNull PsiJavaModule source, @NotNull String packageName, @Nullable PsiJavaModule target) { - Map> exports = CachedValuesManager.getCachedValue(source, () -> - Result.create(exportsMap(source), source.getContainingFile())); - Set targets = exports.get(packageName); - return targets != null && (targets.isEmpty() || target != null && targets.contains(target.getName())); - } - /** * Determines if a specified module is readable from a given context * @@ -193,16 +187,6 @@ public final class JavaModuleGraphUtil { return scope == PROVIDED; } - private static @NotNull Map> exportsMap(@NotNull PsiJavaModule source) { - Map> map = new HashMap<>(); - for (PsiPackageAccessibilityStatement statement : source.getExports()) { - String pkg = statement.getPackageName(); - List targets = statement.getModuleNames(); - map.put(pkg, targets.isEmpty() ? Collections.emptySet() : new HashSet<>(targets)); - } - return map; - } - public static class JavaModuleScope extends GlobalSearchScope { private final @NotNull MultiMap myModules; private final boolean myIncludeLibraries; diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/java19modules/Java9RedundantRequiresStatementInspection.java b/java/java-analysis-impl/src/com/intellij/codeInspection/java19modules/Java9RedundantRequiresStatementInspection.java index 104dc6d3ce24..7f7b55ee0da1 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInspection/java19modules/Java9RedundantRequiresStatementInspection.java +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/java19modules/Java9RedundantRequiresStatementInspection.java @@ -7,6 +7,7 @@ import com.intellij.codeInspection.*; import com.intellij.codeInspection.reference.*; import com.intellij.concurrency.ConcurrentCollectionFactory; import com.intellij.java.analysis.JavaAnalysisBundle; +import com.intellij.java.codeserver.core.JavaPsiModuleUtil; import com.intellij.modcommand.ModPsiUpdater; import com.intellij.modcommand.PsiUpdateModCommandQuickFix; import com.intellij.openapi.module.LanguageLevelUtil; @@ -173,7 +174,7 @@ public final class Java9RedundantRequiresStatementInspection extends GlobalJavaB .map(PsiPackageAccessibilityStatement::getPackageName) .nonNull() .filter(myImportedPackages::contains) - .anyMatch(packageName -> JavaModuleGraphUtil.exports(transitiveModule, packageName, currentModule)); + .anyMatch(packageName -> JavaPsiModuleUtil.exports(transitiveModule, packageName, currentModule)); } private void addTransitiveDependencies(@NotNull PsiRequiresStatement statementToDelete) { 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 015f802372f2..48262e42a767 100644 --- a/java/java-impl/src/com/intellij/psi/impl/JavaPlatformModuleSystem.kt +++ b/java/java-impl/src/com/intellij/psi/impl/JavaPlatformModuleSystem.kt @@ -4,7 +4,6 @@ package com.intellij.psi.impl import com.intellij.codeInsight.JavaModuleSystemEx import com.intellij.codeInsight.JavaModuleSystemEx.ErrorWithFixes import com.intellij.codeInsight.daemon.JavaErrorBundle -import com.intellij.codeInsight.daemon.impl.analysis.JavaModuleGraphUtil import com.intellij.codeInsight.daemon.impl.quickfix.AddExportsDirectiveFix import com.intellij.codeInsight.daemon.impl.quickfix.AddExportsOptionFix import com.intellij.codeInsight.daemon.impl.quickfix.AddModulesOptionFix @@ -12,23 +11,14 @@ import com.intellij.codeInsight.daemon.impl.quickfix.AddRequiresDirectiveFix import com.intellij.codeInsight.intention.IntentionAction import com.intellij.codeInsight.intention.QuickFixFactory import com.intellij.java.JavaBundle -import com.intellij.java.codeserver.core.JavaPsiModuleUtil -import com.intellij.java.codeserver.core.JavaPsiModuleUtil.findDescriptorByElement -import com.intellij.openapi.module.Module -import com.intellij.openapi.module.ModuleUtilCore -import com.intellij.openapi.roots.JdkOrderEntry -import com.intellij.openapi.roots.ModuleRootManager +import com.intellij.java.codeserver.core.JpmsModuleAccessInfo +import com.intellij.java.codeserver.core.JpmsModuleAccessInfo.JpmsModuleAccessProblem +import com.intellij.java.codeserver.core.JpmsModuleInfo.TargetModuleInfo import com.intellij.openapi.roots.ProjectFileIndex -import com.intellij.openapi.roots.ProjectRootManager import com.intellij.pom.java.JavaFeature -import com.intellij.pom.java.LanguageLevel import com.intellij.psi.* -import com.intellij.psi.JavaModuleSystem.* -import com.intellij.psi.impl.light.LightJavaModule -import com.intellij.psi.search.GlobalSearchScope -import com.intellij.psi.util.JavaMultiReleaseUtil import com.intellij.psi.util.PsiUtil -import com.intellij.util.indexing.DumbModeAccessType +import org.jetbrains.annotations.Nls /** * Checks package accessibility according to JLS 7 "Packages and Modules". @@ -40,46 +30,29 @@ internal class JavaPlatformModuleSystem : JavaModuleSystemEx { override fun getName(): String = JavaBundle.message("java.platform.module.system.name") override fun isAccessible(targetPackageName: String, targetFile: PsiFile?, place: PsiElement): Boolean { - return getProblem(targetPackageName, targetFile, place, true) { (current, target) -> isExported(current, target) } == null + return getProblem(targetPackageName, targetFile, place, true) { it.isExported() } == null } override fun checkAccess(targetPackageName: String, targetFile: PsiFile?, place: PsiElement): ErrorWithFixes? { - return getProblem(targetPackageName, targetFile, place, false) { (current, target) -> - val currentModule = current.module ?: return@getProblem false - val targetModule = target.module ?: return@getProblem false - return@getProblem JavaPsiModuleUtil.reads(currentModule, targetModule) - } + return getProblem(targetPackageName, targetFile, place, false) { it.isAccessible() } } override fun isAccessible(targetModule: PsiJavaModule, place: PsiElement): Boolean { - return getProblem(targetModule, place, true) { (current, target) -> - if (current.module == null || target.module == null) return@getProblem false - return@getProblem JavaPsiModuleUtil.reads(current.module, target.module!!) - } == null + val useFile = place.containingFile?.originalFile ?: return true + return TargetModuleInfo(targetModule, "").accessAt(useFile).checkModuleAccess(place) == null } override fun checkAccess(targetModule: PsiJavaModule, place: PsiElement): ErrorWithFixes? { - return getProblem(targetModule, place, false) { (current, target) -> - if (current.module == null || target.module == null) return@getProblem false - return@getProblem JavaPsiModuleUtil.reads(current.module, target.module!!) - } - } + val useFile = place.containingFile?.originalFile ?: return null + val moduleAccess = TargetModuleInfo(targetModule, "").accessAt(useFile) - private fun isExported(current: CurrentModuleInfo, target: TargetModuleInfo): Boolean { - val targetModule = target.module ?: return false - if (!targetModule.isPhysical || JavaModuleGraphUtil.exports(targetModule, target.packageName, current.module)) return true - val currentJpsModule = current.jpsModule ?: return false - return inAddedExports(currentJpsModule, targetModule.name, target.packageName, current.name) - } - - private fun getProblem(targetModule: PsiJavaModule, place: PsiElement, quick: Boolean, - isAccessible: (ModuleAccessInfo) -> Boolean): ErrorWithFixes? { - val target = TargetModuleInfo(targetModule, "") - return checkModuleAccess(target, place, quick, isAccessible) + val access = moduleAccess.checkModuleAccess(place) + return if (access == null) null + else moduleAccess.toErrorWithFixes(access, place) } private fun getProblem(targetPackageName: String, targetFile: PsiFile?, place: PsiElement, quick: Boolean, - isAccessible: (ModuleAccessInfo) -> Boolean): ErrorWithFixes? { + isAccessible: (JpmsModuleAccessInfo) -> Boolean): ErrorWithFixes? { val originalTargetFile = targetFile?.originalFile val useFile = place.containingFile?.originalFile ?: return null if (!PsiUtil.isAvailable(JavaFeature.MODULES, useFile)) return null @@ -117,300 +90,69 @@ internal class JavaPlatformModuleSystem : JavaModuleSystemEx { private val ERR = ErrorWithFixes("-") - private fun checkModuleAccess( - target: TargetModuleInfo, place: PsiElement, quick: Boolean, - isAccessible: (ModuleAccessInfo) -> Boolean, - ): ErrorWithFixes? { - val useFile = place.containingFile?.originalFile ?: return null - val useModule = findDescriptorByElement(useFile).let { if (it is LightJavaModule) null else it } - val current = CurrentModuleInfo(useModule, place) + private fun checkAccess(target: TargetModuleInfo, place: PsiFileSystemItem, quick: Boolean, + isAccessible: (JpmsModuleAccessInfo) -> Boolean): ErrorWithFixes? { + val moduleAccess = target.accessAt(place) - val targetModule = target.module - if (targetModule != null) { - if (targetModule == current.module) { - return null - } - - val currentJpsModule = current.jpsModule - if (current.module == null) { - var origin = targetModule.containingFile?.virtualFile - if (origin == null && targetModule is LightJavaModule) origin = targetModule.rootVirtualFile - if (origin == null || currentJpsModule == null) return null - - if (ModuleRootManager.getInstance(currentJpsModule).fileIndex.getOrderEntryForFile(origin) !is JdkOrderEntry) { - val searchScope = GlobalSearchScope.moduleWithDependenciesAndLibrariesScope(currentJpsModule) - if (searchScope.contains(origin)) return null - return if (quick) ERR - else if (place is PsiJavaModuleReferenceElement) { - val reference: PsiJavaModuleReference = place.reference ?: return null - val registrar: MutableList = ArrayList() - QuickFixFactory.getInstance().registerOrderEntryFixes(reference, registrar) - ErrorWithFixes("-", registrar) - } - else null - } - - if (!accessibleFromJdkModules(current, target, place, isAccessible) && - !inAddedModules(currentJpsModule, targetModule.name)) { - return if (quick) ERR - else ErrorWithFixes(JavaErrorBundle.message("module.not.in.graph", targetModule.name), - listOf(AddModulesOptionFix(currentJpsModule, targetModule.name).asIntention())) - } - } - - if (current.module != null && - targetModule.name != PsiJavaModule.JAVA_BASE && - !isAccessible(ModuleAccessInfo(current, target)) && - !inAddedReads(current.module, targetModule)) { - return when { - quick -> ERR - PsiNameHelper.isValidModuleName(targetModule.name, current.module) -> ErrorWithFixes(JavaErrorBundle.message("module.does.not.read", targetModule.name, current.name), - listOf(AddRequiresDirectiveFix(current.module, targetModule.name).asIntention())) - else -> ErrorWithFixes(JavaErrorBundle.message("module.bad.name", targetModule.name)) - } - } + val access = moduleAccess.checkAccess(place, isAccessible) + return when { + access == null -> null + quick -> ERR + else -> moduleAccess.toErrorWithFixes(access, place) } - else if (current.module != null) { - val autoModule = TargetModuleInfo(detectAutomaticModule(target), target.packageName) - if (autoModule.module != null && - !isAccessible(ModuleAccessInfo(current, autoModule)) && - !inAddedReads(current.module, null) && - !inSameMultiReleaseModule(current, target)) { - return if (quick) ERR else ErrorWithFixes(JavaErrorBundle.message("module.access.to.unnamed", target.packageName, current.name)) - } - } - - return null } - private fun checkAccess(target: TargetModuleInfo, place: PsiFileSystemItem, quick: Boolean, - isAccessible: (ModuleAccessInfo) -> Boolean): ErrorWithFixes? { - val useModule = findDescriptorByElement(place).let { if (it is LightJavaModule) null else it } - val current = CurrentModuleInfo(useModule, place) + fun JpmsModuleAccessInfo.toErrorWithFixes(problem: JpmsModuleAccessProblem, place: PsiElement): ErrorWithFixes { + return ErrorWithFixes(getMessage(problem), getFixes(problem, place)) + } + private fun JpmsModuleAccessInfo.getMessage(problem: JpmsModuleAccessProblem): @Nls String { + val current = current + val target = target val targetModule = target.module - if (targetModule != null) { - if (targetModule == current.module) { - return null - } + return when (problem) { + JpmsModuleAccessProblem.FROM_NAMED -> + JavaErrorBundle.message("module.access.from.named", target.packageName, targetModule!!.name, current.name) + JpmsModuleAccessProblem.FROM_UNNAMED -> JavaErrorBundle.message("module.access.from.unnamed", target.packageName, targetModule!!.name) + JpmsModuleAccessProblem.TO_UNNAMED -> JavaErrorBundle.message("module.access.to.unnamed", target.packageName, current.name) + JpmsModuleAccessProblem.PACKAGE_BAD_NAME -> JavaErrorBundle.message("module.access.bad.name", target.packageName, targetModule!!.name) + JpmsModuleAccessProblem.BAD_NAME -> JavaErrorBundle.message("module.bad.name", targetModule!!.name) + JpmsModuleAccessProblem.NOT_IN_GRAPH -> JavaErrorBundle.message("module.not.in.graph", targetModule!!.name) + JpmsModuleAccessProblem.PACKAGE_NOT_IN_GRAPH -> JavaErrorBundle.message("module.access.not.in.graph", target.packageName, targetModule!!.name) + JpmsModuleAccessProblem.DOES_NOT_READ -> JavaErrorBundle.message("module.does.not.read", targetModule!!.name, current.name) + JpmsModuleAccessProblem.PACKAGE_DOES_NOT_READ -> JavaErrorBundle.message("module.access.does.not.read", target.packageName, targetModule!!.name, current.name) + JpmsModuleAccessProblem.JPS_DEPENDENCY_PROBLEM -> "-" // TODO: proper name? + } + } - val currentJpsModule = current.jpsModule - if (current.module == null) { - val origin = targetModule.containingFile?.virtualFile - if (origin == null || currentJpsModule == null || - ModuleRootManager.getInstance(currentJpsModule).fileIndex.getOrderEntryForFile(origin) !is JdkOrderEntry) { - return null // a target is not on the mandatory module path - } - - if (!accessibleFromJdkModules(current, target, place, isAccessible) && - !inAddedModules(currentJpsModule, targetModule.name) && - !hasUpgrade(currentJpsModule, targetModule.name, target.packageName, place)) { - return if (quick) ERR - else ErrorWithFixes( - JavaErrorBundle.message("module.access.not.in.graph", target.packageName, targetModule.name), - listOf(AddModulesOptionFix(currentJpsModule, targetModule.name).asIntention())) - } - } - - if (targetModule !is LightJavaModule && - !JavaModuleGraphUtil.exports(targetModule, target.packageName, current.module) && - (currentJpsModule == null || !inAddedExports(currentJpsModule, targetModule.name, target.packageName, current.name)) && - (currentJpsModule == null || !isPatchedModule(targetModule.name, currentJpsModule, place))) { - if (quick) return ERR - val fixes = when { + private fun JpmsModuleAccessInfo.getFixes( + problem: JpmsModuleAccessProblem, + place: PsiElement + ): List { + val currentJpsModule = current.jpsModule + val targetModule = target.module + return when (problem) { + JpmsModuleAccessProblem.FROM_UNNAMED, JpmsModuleAccessProblem.FROM_NAMED -> { + when { target.packageName.isEmpty() -> emptyList() targetModule is PsiCompiledElement && currentJpsModule != null -> - listOf(AddExportsOptionFix(currentJpsModule, targetModule.name, target.packageName, current.name).asIntention()) + listOf(AddExportsOptionFix(currentJpsModule, targetModule.name, target.packageName, + current.name).asIntention()) targetModule !is PsiCompiledElement && current.module != null -> - listOf(AddExportsDirectiveFix(targetModule, target.packageName, current.name).asIntention()) + listOf(AddExportsDirectiveFix(targetModule!!, target.packageName, current.name).asIntention()) else -> emptyList() } - return when (current.module) { - null -> ErrorWithFixes(JavaErrorBundle.message("module.access.from.unnamed", target.packageName, targetModule.name), fixes) - else -> ErrorWithFixes(JavaErrorBundle.message("module.access.from.named", target.packageName, targetModule.name, current.name), fixes) - } } - - if (current.module != null && - targetModule.name != PsiJavaModule.JAVA_BASE && - !isAccessible(ModuleAccessInfo(current, target)) && - !inAddedReads(current.module, targetModule)) { - return when { - quick -> ERR - PsiNameHelper.isValidModuleName(targetModule.name, current.module) -> ErrorWithFixes( - JavaErrorBundle.message("module.access.does.not.read", target.packageName, targetModule.name, current.name), - listOf(AddRequiresDirectiveFix(current.module, targetModule.name).asIntention())) - else -> ErrorWithFixes(JavaErrorBundle.message("module.access.bad.name", target.packageName, targetModule.name)) - } + JpmsModuleAccessProblem.TO_UNNAMED, JpmsModuleAccessProblem.PACKAGE_BAD_NAME, JpmsModuleAccessProblem.BAD_NAME -> listOf() + JpmsModuleAccessProblem.PACKAGE_NOT_IN_GRAPH, JpmsModuleAccessProblem.NOT_IN_GRAPH -> + listOf(AddModulesOptionFix(currentJpsModule!!, targetModule!!.name).asIntention()) + JpmsModuleAccessProblem.PACKAGE_DOES_NOT_READ, JpmsModuleAccessProblem.DOES_NOT_READ -> + listOf(AddRequiresDirectiveFix(current.module!!, targetModule!!.name).asIntention()) + JpmsModuleAccessProblem.JPS_DEPENDENCY_PROBLEM -> { + val reference: PsiJavaModuleReference = (place as? PsiJavaModuleReferenceElement)?.reference ?: return listOf() + val registrar: MutableList = ArrayList() + QuickFixFactory.getInstance().registerOrderEntryFixes(reference, registrar) + registrar } } - else if (current.module != null) { - val autoModule = TargetModuleInfo(detectAutomaticModule(target), target.packageName) - if (autoModule.module == null) { - return if (quick) ERR else ErrorWithFixes(JavaErrorBundle.message("module.access.to.unnamed", target.packageName, current.name)) - } - else if (!isAccessible(ModuleAccessInfo(current, autoModule)) && - !inAddedReads(current.module, null) && - !inSameMultiReleaseModule(current, target)) { - return if (quick) ERR else ErrorWithFixes(JavaErrorBundle.message("module.access.to.unnamed", target.packageName, current.name)) - } - } - - return null - } - - private fun accessibleFromJdkModules( - current: CurrentModuleInfo, - target: TargetModuleInfo, - place: PsiElement, - isAccessible: (ModuleAccessInfo) -> Boolean, - ): Boolean { - val jpsModule = current.jpsModule ?: return false - val targetModule = target.module ?: return false - if(targetModule.name == PsiJavaModule.JAVA_BASE) return true - - if(!isJdkModule(jpsModule, targetModule)) return false - val languageLevel = PsiUtil.getLanguageLevel(place) - // https://bugs.openjdk.org/browse/JDK-8197532 - val jdkModulePred: (PsiJavaModule) -> Boolean = if (languageLevel >= LanguageLevel.JDK_11) { - { module -> module.exports.any { e -> e.moduleNames.isEmpty() } } - } - else { - val javaSE = DumbModeAccessType.RELIABLE_DATA_ONLY.ignoreDumbMode { - JavaPsiFacade.getInstance(place.project).findModule("java.se", jpsModule.moduleWithLibrariesScope) - } - - if(javaSE != null) { - { module -> - (!module.name.startsWith("java.") && module.exports.any { e -> e.moduleNames.isEmpty() }) || - isAccessible(ModuleAccessInfo(CurrentModuleInfo(javaSE, current.name) { jpsModule }, target)) - } - } else { - {_ -> true} - } - } - val noIncubatorPred: (PsiJavaModule) -> Boolean = {module -> !module.doNotResolveByDefault()} - return jdkModulePred(targetModule) && noIncubatorPred(targetModule) - } - - private fun isJdkModule(jpsModule: Module, psiModule: PsiJavaModule): Boolean { - val sdkHomePath = ModuleRootManager.getInstance(jpsModule).getSdk()?.homePath?.replace('\\', '/') - val moduleFilePath = psiModule.containingFile?.virtualFile?.path?.replace('\\', '/') - if (sdkHomePath != null && moduleFilePath != null) { - return moduleFilePath.startsWith("$sdkHomePath!") || - moduleFilePath.startsWith(if(sdkHomePath.last() == '/') sdkHomePath else "$sdkHomePath/") - } else { - return psiModule.name.startsWith("java.") || - psiModule.name.startsWith("jdk.") - } - } - - private fun inSameMultiReleaseModule(current: ModuleInfo, target: ModuleInfo): Boolean { - val placeModule = current.jpsModule ?: return false - val targetModule = target.jpsModule ?: return false - return JavaMultiReleaseUtil.areMainAndAdditionalMultiReleaseModules(targetModule, placeModule) - } - - private fun detectAutomaticModule(current: ModuleInfo): PsiJavaModule? { - val module = current.jpsModule ?: return null - return JavaPsiFacade.getInstance(module.project) - .findModule(LightJavaModule.moduleName(module.name), - GlobalSearchScope.moduleScope(module)) - } - - private fun hasUpgrade(module: Module, targetName: String, packageName: String, place: PsiFileSystemItem): Boolean { - if (PsiJavaModule.UPGRADEABLE.contains(targetName)) { - val target = JavaPsiFacade.getInstance(module.project).findPackage(packageName) - if (target != null) { - val useVFile = place.virtualFile - if (useVFile != null) { - val index = ModuleRootManager.getInstance(module).fileIndex - val test = index.isInTestSourceContent(useVFile) - val dirs = target.getDirectories(module.getModuleWithDependenciesAndLibrariesScope(test)) - return dirs.any { index.getOrderEntryForFile(it.virtualFile) !is JdkOrderEntry } - } - } - } - - return false - } - - private fun isPatchedModule(targetModuleName: String, module: Module, place: PsiFileSystemItem): Boolean { - val virtualFile = place.virtualFile ?: return false - val rootForFile = ProjectRootManager.getInstance(place.project).fileIndex.getSourceRootForFile(virtualFile) ?: return false - return JavaCompilerConfigurationProxy.isPatchedModuleRoot(targetModuleName, module, rootForFile) - } - - private fun inAddedExports(module: Module, targetName: String, packageName: String, useName: String): Boolean { - val options = JavaCompilerConfigurationProxy.getAdditionalOptions(module.project, module) - if (options.isEmpty()) return false - val prefix = "${targetName}/${packageName}=" - return JavaCompilerConfigurationProxy.optionValues(options, ADD_EXPORTS_OPTION) - .filter { it.startsWith(prefix) } - .map { it.substring(prefix.length) } - .flatMap { it.splitToSequence(",") } - .any { it == useName } - } - - private fun inAddedModules(module: Module, moduleName: String): Boolean { - val options = JavaCompilerConfigurationProxy.getAdditionalOptions(module.project, module) - return JavaCompilerConfigurationProxy.optionValues(options, ADD_MODULES_OPTION) - .flatMap { it.splitToSequence(",") } - .any { it == moduleName || it == ALL_SYSTEM || it == ALL_MODULE_PATH } - } - - private fun inAddedReads(fromJavaModule: PsiJavaModule, toJavaModule: PsiJavaModule?): Boolean { - val fromModule = ModuleUtilCore.findModuleForPsiElement(fromJavaModule) ?: return false - val options = JavaCompilerConfigurationProxy.getAdditionalOptions(fromModule.project, fromModule) - return JavaCompilerConfigurationProxy.optionValues(options, ADD_READS_OPTION) - .flatMap { it.splitToSequence(",") } - .any { - val (optFromModuleName, optToModuleName) = it.split("=").apply { it.first() to it.last() } - fromJavaModule.name == optFromModuleName && - (toJavaModule?.name == optToModuleName || (optToModuleName == ALL_UNNAMED && isUnnamedModule(toJavaModule))) - } - } - - private fun isUnnamedModule(module: PsiJavaModule?) = module == null || module is LightJavaModule - - /** - * Represents the access details between the current module and the target module. - * - * @property current The current module. - * @property target The target module. - */ - private data class ModuleAccessInfo(val current: CurrentModuleInfo, val target: TargetModuleInfo) - - private interface ModuleInfo { - val module: PsiJavaModule? - val jpsModule: Module? - } - - /** - * Represents the details of a current module. - * - * Note: "name" is not always possible to get from "module". - * For example, "module" can be "java.se", but the name is from the original module. - * - * @property module The PsiJavaModule instance representing the module. - * @property name original module name - * @property jpsModule JPS module initialization. - */ - private class CurrentModuleInfo(override val module: PsiJavaModule?, val name: String, jps: () -> Module? = { null }) : ModuleInfo { - constructor(use: PsiJavaModule?, element: PsiElement) : this(use, use?.name ?: ALL_UNNAMED, { - ModuleUtilCore.findModuleForPsiElement(element) - }) - - override val jpsModule: Module? by lazy { jps() } - } - - private class TargetModuleInfo(element: PsiElement?, val packageName: String) : ModuleInfo { - override val jpsModule: Module? by lazy { - if (element == null) return@lazy null - ModuleUtilCore.findModuleForPsiElement(element) - } - override val module: PsiJavaModule? by lazy { - findDescriptorByElement(element) - } - } -} \ No newline at end of file + }} \ No newline at end of file diff --git a/jvm/jvm-analysis-impl/intellij.jvm.analysis.impl.iml b/jvm/jvm-analysis-impl/intellij.jvm.analysis.impl.iml index 90f9ee7a0cc7..989b17c24a48 100644 --- a/jvm/jvm-analysis-impl/intellij.jvm.analysis.impl.iml +++ b/jvm/jvm-analysis-impl/intellij.jvm.analysis.impl.iml @@ -20,5 +20,6 @@ + \ No newline at end of file diff --git a/jvm/jvm-analysis-impl/src/com/intellij/codeInspection/IllegalDependencyOnInternalPackageInspection.kt b/jvm/jvm-analysis-impl/src/com/intellij/codeInspection/IllegalDependencyOnInternalPackageInspection.kt index 7250313a5a39..d847bed8a96c 100644 --- a/jvm/jvm-analysis-impl/src/com/intellij/codeInspection/IllegalDependencyOnInternalPackageInspection.kt +++ b/jvm/jvm-analysis-impl/src/com/intellij/codeInspection/IllegalDependencyOnInternalPackageInspection.kt @@ -3,6 +3,7 @@ package com.intellij.codeInspection import com.intellij.analysis.JvmAnalysisBundle import com.intellij.codeInsight.daemon.impl.analysis.JavaModuleGraphUtil +import com.intellij.java.codeserver.core.JavaPsiModuleUtil import com.intellij.packageDependencies.DependenciesBuilder import com.intellij.pom.java.JavaFeature import com.intellij.psi.PsiClass @@ -28,7 +29,7 @@ private class IllegalDependencyOnInternalPackage(private val holder: ProblemsHol val moduleName = javaModule.name if (moduleName.startsWith("java.")) return@analyzeFileDependencies val packageName = dependencyFile.packageName - if (JavaModuleGraphUtil.exports(javaModule, packageName, null)) return@analyzeFileDependencies + if (JavaPsiModuleUtil.exports(javaModule, packageName, null)) return@analyzeFileDependencies holder.registerProblem( place, JvmAnalysisBundle.message("inspection.message.illegal.dependency.module.doesn.t.export", moduleName, packageName) diff --git a/plugins/kotlin/core/src/org/jetbrains/kotlin/idea/modules/IdeJavaModuleResolver.kt b/plugins/kotlin/core/src/org/jetbrains/kotlin/idea/modules/IdeJavaModuleResolver.kt index ea58ebf00ea7..a07c0d32cd21 100644 --- a/plugins/kotlin/core/src/org/jetbrains/kotlin/idea/modules/IdeJavaModuleResolver.kt +++ b/plugins/kotlin/core/src/org/jetbrains/kotlin/idea/modules/IdeJavaModuleResolver.kt @@ -107,7 +107,7 @@ class IdeJavaModuleResolver(private val project: Project) : JavaModuleResolver { // Returns whether or not [source] exports [packageName] to [target] private fun exports(source: PsiJavaModule, packageName: String, target: PsiJavaModule): Boolean = - source is LightJavaModule || JavaModuleGraphUtil.exports(source, packageName, target) + source is LightJavaModule || JavaPsiModuleUtil.exports(source, packageName, target) companion object { private const val MODULE_ANNOTATIONS_CACHE_SIZE = 10000