[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
This commit is contained in:
Tagir Valeev
2025-02-21 16:25:37 +00:00
committed by intellij-monorepo-bot
parent 5a96924d32
commit 9559d696ff
9 changed files with 427 additions and 346 deletions
@@ -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<String, Set<String>> exports = CachedValuesManager.getCachedValue(source, () ->
CachedValueProvider.Result.create(exportsMap(source), source.getContainingFile()));
Set<String> targets = exports.get(packageName);
return targets != null && (targets.isEmpty() || target != null && targets.contains(target.getName()));
}
private static @NotNull Map<String, Set<String>> exportsMap(@NotNull PsiJavaModule source) {
Map<String, Set<String>> map = new HashMap<>();
for (PsiPackageAccessibilityStatement statement : source.getExports()) {
String pkg = statement.getPackageName();
List<String> 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
@@ -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<PsiJavaModule, Throwable> {
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
}
@@ -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)
}
}
}
@@ -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<String, Set<String>> exports = CachedValuesManager.getCachedValue(source, () ->
Result.create(exportsMap(source), source.getContainingFile()));
Set<String> 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<String, Set<String>> exportsMap(@NotNull PsiJavaModule source) {
Map<String, Set<String>> map = new HashMap<>();
for (PsiPackageAccessibilityStatement statement : source.getExports()) {
String pkg = statement.getPackageName();
List<String> 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<String, PsiJavaModule> myModules;
private final boolean myIncludeLibraries;
@@ -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) {
@@ -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<IntentionAction> = 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<IntentionAction> {
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<IntentionAction> = 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<PsiJavaModule, Throwable> {
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)
}
}
}
}}
@@ -20,5 +20,6 @@
<orderEntry type="module" module-name="intellij.platform.util.jdom" />
<orderEntry type="module" module-name="intellij.platform.ide.core.impl" />
<orderEntry type="module" module-name="intellij.jvm.analysis.quickFix" />
<orderEntry type="module" module-name="intellij.java.codeserver.core" />
</component>
</module>
@@ -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)
@@ -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