[java, jigsaw, index] prioritize newest Java descriptor in multi-release JARs (IDEA-365082)

GitOrigin-RevId: 10d03d5095264cf4e708b6154b4f4a90ea683155
This commit is contained in:
Aleksey Dobrynin
2025-03-06 09:39:31 +01:00
committed by intellij-monorepo-bot
parent caa1ca7324
commit 24568db463
3 changed files with 122 additions and 35 deletions

View File

@@ -1,22 +1,24 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.psi.impl.java.stubs.index;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ProjectFileIndex;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.pom.java.JavaFeature;
import com.intellij.psi.PsiJavaModule;
import com.intellij.psi.impl.search.JavaSourceFilterScope;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.stubs.StringStubIndexExtension;
import com.intellij.psi.stubs.StubIndex;
import com.intellij.psi.stubs.StubIndexKey;
import com.intellij.util.SmartList;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
public final class JavaModuleNameIndex extends StringStubIndexExtension<PsiJavaModule> {
private static final int MIN_JAVA_VERSION = JavaFeature.MODULES.getMinimumLevel().feature();
private static final JavaModuleNameIndex ourInstance = new JavaModuleNameIndex();
public static JavaModuleNameIndex getInstance() {
@@ -45,29 +47,44 @@ public final class JavaModuleNameIndex extends StringStubIndexExtension<PsiJavaM
public Collection<PsiJavaModule> getModules(@NotNull String name, @NotNull Project project, @NotNull GlobalSearchScope scope) {
Collection<PsiJavaModule> modules = StubIndex.getElements(getKey(), name, project, new JavaSourceFilterScope(scope), PsiJavaModule.class);
if (modules.size() > 1) {
modules = filterVersions(project, modules);
modules = filterHighestVersions(project, modules);
}
return modules;
}
private static Collection<PsiJavaModule> filterVersions(Project project, Collection<PsiJavaModule> modules) {
Set<VirtualFile> filter = new HashSet<>();
/**
* Filters the given collection of Java modules to exclude redundant versions of modules,
* preserving only the highest versions available in the project scope.
*
* @param project the project in which the module filtering is performed
* @param modules a collection of Java modules to be filtered
* @return a collection of Java modules with only the highest versions retained
*/
@NotNull
private static Collection<PsiJavaModule> filterHighestVersions(@NotNull Project project, @NotNull Collection<PsiJavaModule> modules) {
ProjectFileIndex index = ProjectFileIndex.getInstance(project);
for (PsiJavaModule module : modules) {
VirtualFile root = index.getClassRootForFile(module.getContainingFile().getVirtualFile());
if (root != null) {
List<VirtualFile> files = descriptorFiles(root);
VirtualFile main = ContainerUtil.getFirstItem(files);
if (main != null && !(root.equals(main.getParent()) || version(main.getParent()) >= 9)) {
filter.add(main);
}
for (int i = 1; i < files.size(); i++) {
filter.add(files.get(i));
Set<VirtualFile> roots = new HashSet<>();
for (PsiJavaModule javaModule : modules) {
VirtualFile file = index.getClassRootForFile(javaModule.getContainingFile().getVirtualFile());
ContainerUtil.addIfNotNull(roots, file);
}
Set<VirtualFile> filter = new HashSet<>();
for (VirtualFile root : roots) {
Collection<VirtualFile> descriptors = getSortedFileDescriptors(root);
boolean found = false;
// find the highest correct module.
for (VirtualFile descriptor : descriptors) {
if (!found && isCorrectModulePath(root, descriptor)) {
found = true;
} else {
filter.add(descriptor);
}
}
}
// remove the same modules but with a smaller version.
if (!filter.isEmpty()) {
modules = ContainerUtil.filter(modules, m -> !filter.contains(m.getContainingFile().getVirtualFile()));
}
@@ -75,26 +92,50 @@ public final class JavaModuleNameIndex extends StringStubIndexExtension<PsiJavaM
return modules;
}
/**
* Checks if the descriptor is in the root directory or a valid versioned subdirectory.
*
* @param root the root directory.
* @param descriptor the module descriptor to check.
* @return true if the descriptor is correctly located, false otherwise.
*/
private static boolean isCorrectModulePath(@NotNull VirtualFile root, @Nullable VirtualFile descriptor) {
if (descriptor == null) return false;
return root.equals(descriptor.getParent()) || version(descriptor.getParent()) >= MIN_JAVA_VERSION;
}
@Override
public boolean traceKeyHashToVirtualFileMapping() {
return true;
}
private static List<VirtualFile> descriptorFiles(VirtualFile root) {
List<VirtualFile> results = new SmartList<>();
ContainerUtil.addIfNotNull(results, root.findChild(PsiJavaModule.MODULE_INFO_CLS_FILE));
/**
* Collects module descriptor files (e.g., `module-info.class`) from the root and "META-INF/versions",
* sorted by Java version (highest to lowest).
*
* @param root the root virtual file
* @return a sorted collection of module descriptor files
*/
@NotNull
private static Collection<VirtualFile> getSortedFileDescriptors(@NotNull VirtualFile root) {
NavigableMap<Integer, VirtualFile> results = new TreeMap<>((i1,i2) -> Integer.compare(i2, i1));
VirtualFile rootModuleInfo = root.findChild(PsiJavaModule.MODULE_INFO_CLS_FILE);
if (rootModuleInfo != null) {
results.put(MIN_JAVA_VERSION, rootModuleInfo);
}
VirtualFile versionsDir = root.findFileByRelativePath("META-INF/versions");
if (versionsDir != null) {
VirtualFile[] versions = versionsDir.getChildren();
Arrays.sort(versions, JavaModuleNameIndex::compareVersions);
for (VirtualFile version : versions) {
ContainerUtil.addIfNotNull(results, version.findChild(PsiJavaModule.MODULE_INFO_CLS_FILE));
VirtualFile moduleInfo = version.findChild(PsiJavaModule.MODULE_INFO_CLS_FILE);
if (moduleInfo != null) {
results.put(version(version), moduleInfo);
}
}
}
return results;
return results.values();
}
private static int version(VirtualFile dir) {
@@ -105,12 +146,4 @@ public final class JavaModuleNameIndex extends StringStubIndexExtension<PsiJavaM
return Integer.MIN_VALUE;
}
}
private static int compareVersions(VirtualFile dir1, VirtualFile dir2) {
int v1 = version(dir1), v2 = version(dir2);
if (v1 < 9 && v2 < 9) return 0;
if (v1 < 9) return 1;
if (v2 < 9) return -1;
return v1 - v2;
}
}

View File

@@ -1,6 +1,7 @@
// 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.codeInsight.daemon
import com.intellij.JavaTestUtil
import com.intellij.codeInsight.daemon.impl.JavaHighlightInfoTypes
import com.intellij.codeInsight.daemon.impl.analysis.JavaModuleGraphUtil
import com.intellij.codeInsight.intention.IntentionActionDelegate
@@ -19,15 +20,18 @@ import com.intellij.openapi.application.runWriteActionAndWait
import com.intellij.openapi.diagnostic.ReportingClassSubstitutor
import com.intellij.openapi.module.Module
import com.intellij.openapi.module.ModuleManager
import com.intellij.openapi.module.ModuleManager.Companion.getInstance
import com.intellij.openapi.projectRoots.JavaSdk
import com.intellij.openapi.projectRoots.ProjectJdkTable
import com.intellij.openapi.projectRoots.impl.JavaAwareProjectJdkTableImpl
import com.intellij.openapi.roots.ModuleRootManager
import com.intellij.openapi.roots.ModuleRootModificationUtil
import com.intellij.openapi.roots.OrderRootType
import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.openapi.roots.libraries.LibraryTablesRegistrar
import com.intellij.openapi.util.Computable
import com.intellij.openapi.util.TextRange
import com.intellij.openapi.vfs.JarFileSystem
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.openapi.vfs.ex.temp.TempFileSystem
@@ -53,6 +57,7 @@ import junit.framework.TestCase
import org.assertj.core.api.Assertions.assertThat
import org.intellij.lang.annotations.Language
import org.jetbrains.jps.model.java.JavaSourceRootType
import java.io.File
import java.util.jar.JarFile
class ModuleHighlightingTest : LightJava9ModulesCodeInsightFixtureTestCase() {
@@ -1003,6 +1008,55 @@ class ModuleHighlightingTest : LightJava9ModulesCodeInsightFixtureTestCase() {
}
}
fun testMultiReleaseJarWithDifferentJavaVersions() {
val location = JavaTestUtil.getJavaTestDataPath() + "/codeInsight/jigsaw/multi-release.jar"
val libraryFile = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(File(location))!!
val libClasses = JarFileSystem.getInstance().getJarRootForLocalFile(libraryFile)!!
val module = ModuleManager.getInstance(project).findModuleByName(MAIN.moduleName)!!
ApplicationManager.getApplication().runWriteAction {
val library = LibraryTablesRegistrar.getInstance().getLibraryTable(project).createLibrary("MultiReleaseLib")
val model = library.getModifiableModel()
model.addRoot(libClasses, OrderRootType.CLASSES)
model.commit()
ModuleRootModificationUtil.addDependency(module, library)
}
addFile("module-info.java", """
module my.module.name {
requires org.example.multi.release.jar;
}""".trimIndent())
IdeaTestUtil.withLevel(module, LanguageLevel.JDK_1_9) {
highlight("Main.java", """
import <error descr="Package 'org.example.first' is declared in module 'org.example.multi.release.jar', which does not export it to module 'my.module.name'">org.example.first</error>.First;
import org.example.second.Second;
import <error descr="Package 'org.example.third' is declared in module 'org.example.multi.release.jar', which does not export it to module 'my.module.name'">org.example.third</error>.Third;
public class Main {
}""".trimIndent())
}
IdeaTestUtil.withLevel(module, LanguageLevel.JDK_11) {
highlight("Main.java", """
import <error descr="Package 'org.example.first' is declared in module 'org.example.multi.release.jar', which does not export it to module 'my.module.name'">org.example.first</error>.First;
import org.example.second.Second;
import <error descr="Package 'org.example.third' is declared in module 'org.example.multi.release.jar', which does not export it to module 'my.module.name'">org.example.third</error>.Third;
public class Main {
}""".trimIndent())
}
IdeaTestUtil.withLevel(module, LanguageLevel.JDK_17) {
highlight("Main.java", """
import <error descr="Package 'org.example.first' is declared in module 'org.example.multi.release.jar', which does not export it to module 'my.module.name'">org.example.first</error>.First;
import org.example.second.Second;
import <error descr="Package 'org.example.third' is declared in module 'org.example.multi.release.jar', which does not export it to module 'my.module.name'">org.example.third</error>.Third;
public class Main {
}""".trimIndent())
}
}
//<editor-fold desc="Helpers.">
private fun highlight(text: String) = highlight("module-info.java", text)
@@ -1029,7 +1083,7 @@ class ModuleHighlightingTest : LightJava9ModulesCodeInsightFixtureTestCase() {
private fun withInternalJdk(moduleDescriptor: ModuleDescriptor, level: LanguageLevel, block: () -> Unit) {
val name = "INTERNAL_JDK_TEST"
val module = getInstance(project).findModuleByName(moduleDescriptor.moduleName)!!
val module = ModuleManager.getInstance(project).findModuleByName(moduleDescriptor.moduleName)!!
try {
WriteAction.runAndWait<RuntimeException?>(ThrowableRunnable {
@@ -1059,13 +1113,13 @@ class ModuleHighlightingTest : LightJava9ModulesCodeInsightFixtureTestCase() {
val module = ModuleManager.getInstance(project).findModuleByName(moduleName) ?: return null
val dummyRoot = VirtualFileManager.getInstance().findFileByUrl("temp:///") ?: return null
dummyRoot.refresh(false, false)
val srcRoot = dummyRoot.createChildDirectory(this, "${srcPathPrefix}-${this.sourceRootName}");
val srcRoot = dummyRoot.createChildDirectory(this, "${srcPathPrefix}-${this.sourceRootName}")
val tempFs: TempFileSystem = srcRoot.getFileSystem() as TempFileSystem
for (child in srcRoot.getChildren()) {
if (!tempFs.exists(child)) {
tempFs.createChildFile(this, srcRoot, child.getName())
}
child.delete(this);
child.delete(this)
}
ModuleRootModificationUtil.updateModel(module) { model ->
model.addContentEntry(srcRoot).addSourceFolder(srcRoot, JavaSourceRootType.SOURCE)