mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[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:
committed by
intellij-monorepo-bot
parent
5a96924d32
commit
9559d696ff
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user