diff --git a/java/java-indexing-impl/src/com/intellij/psi/impl/java/stubs/index/JavaModuleNameIndex.java b/java/java-indexing-impl/src/com/intellij/psi/impl/java/stubs/index/JavaModuleNameIndex.java index f58f110d1a22..7226c18b7fb6 100644 --- a/java/java-indexing-impl/src/com/intellij/psi/impl/java/stubs/index/JavaModuleNameIndex.java +++ b/java/java-indexing-impl/src/com/intellij/psi/impl/java/stubs/index/JavaModuleNameIndex.java @@ -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 { + 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 getModules(@NotNull String name, @NotNull Project project, @NotNull GlobalSearchScope scope) { Collection 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 filterVersions(Project project, Collection modules) { - Set 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 filterHighestVersions(@NotNull Project project, @NotNull Collection modules) { ProjectFileIndex index = ProjectFileIndex.getInstance(project); - for (PsiJavaModule module : modules) { - VirtualFile root = index.getClassRootForFile(module.getContainingFile().getVirtualFile()); - if (root != null) { - List 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 roots = new HashSet<>(); + for (PsiJavaModule javaModule : modules) { + VirtualFile file = index.getClassRootForFile(javaModule.getContainingFile().getVirtualFile()); + ContainerUtil.addIfNotNull(roots, file); + } + + Set filter = new HashSet<>(); + for (VirtualFile root : roots) { + Collection 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= MIN_JAVA_VERSION; + } + @Override public boolean traceKeyHashToVirtualFileMapping() { return true; } - private static List descriptorFiles(VirtualFile root) { - List 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 getSortedFileDescriptors(@NotNull VirtualFile root) { + NavigableMap 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 StringStubIndexExtensionorg.example.first.First; + import org.example.second.Second; + import org.example.third.Third; + + public class Main { + }""".trimIndent()) + } + + IdeaTestUtil.withLevel(module, LanguageLevel.JDK_11) { + highlight("Main.java", """ + import org.example.first.First; + import org.example.second.Second; + import org.example.third.Third; + + public class Main { + }""".trimIndent()) + } + + IdeaTestUtil.withLevel(module, LanguageLevel.JDK_17) { + highlight("Main.java", """ + import org.example.first.First; + import org.example.second.Second; + import org.example.third.Third; + + public class Main { + }""".trimIndent()) + } + } + // 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(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)