[java, complete, import-module] Check access module names for Module Import Declarations DEA-356710

GitOrigin-RevId: ef96cf46f062068539cc417a3e130172fd4b6132
This commit is contained in:
Aleksey Dobrynin
2024-07-30 13:24:51 +02:00
committed by intellij-monorepo-bot
parent fe56e465d6
commit 85c104a858
3 changed files with 232 additions and 13 deletions

View File

@@ -230,6 +230,10 @@ public final class JavaModuleGraphUtil {
return getRequiresGraph(source).reads(source, destination);
}
public static boolean reads(@NotNull PsiJavaModule source, @NotNull String destination) {
return getRequiresGraph(source).reads(source, source, destination);
}
public static @NotNull Set<PsiJavaModule> getAllDependencies(PsiJavaModule source) {
return getRequiresGraph(source).getAllDependencies(source);
}
@@ -505,6 +509,26 @@ public final class JavaModuleGraphUtil {
myTransitiveEdges = transitiveEdges;
}
public boolean reads(@NotNull PsiJavaModule source, @NotNull String destination) {
return reads(source, source, destination);
}
private boolean reads(@NotNull PsiJavaModule top, @NotNull PsiJavaModule source, @NotNull String destination) {
Collection<PsiJavaModule> nodes = myGraph.getNodes();
if (ContainerUtil.exists(nodes, m -> m.getName().equals(destination)) && nodes.contains(source)) {
Iterator<PsiJavaModule> directReaders = myGraph.getIn(source);
while (directReaders.hasNext()) {
PsiJavaModule next = directReaders.next();
if (top.equals(source)) {
if (next.getName().equals(destination) || reads(top, next, destination)) return true;
} else if(myTransitiveEdges.contains(key(next, source))) {
if (next.getName().equals(destination) || reads(top, next, destination)) return true;
}
}
}
return false;
}
public boolean reads(PsiJavaModule source, PsiJavaModule destination) {
Collection<PsiJavaModule> nodes = myGraph.getNodes();
if (nodes.contains(destination) && nodes.contains(source)) {

View File

@@ -66,6 +66,7 @@ import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtil;
import com.intellij.psi.util.PsiUtilCore;
import com.intellij.psi.util.TypeConversionUtil;
import com.intellij.ui.JBColor;
import com.intellij.util.*;
import com.intellij.util.containers.ContainerUtil;
import com.siyeh.ig.psiutils.JavaDeprecationUtils;
@@ -1387,8 +1388,9 @@ public final class JavaCompletionContributor extends CompletionContributor imple
private static void addModuleReferences(PsiElement moduleRef, PsiElement position, PsiFile originalFile, CompletionResultSet result) {
PsiElement statement = moduleRef.getParent();
boolean withAutoModules;
if ((withAutoModules = statement instanceof PsiRequiresStatement || statement instanceof PsiImportModuleStatement) || statement instanceof PsiPackageAccessibilityStatement) {
boolean withAutoModules, checkAccess;
if ((withAutoModules = (checkAccess = statement instanceof PsiImportModuleStatement) || statement instanceof PsiRequiresStatement) ||
statement instanceof PsiPackageAccessibilityStatement) {
PsiElement parent = statement.getParent();
if (parent != null) {
Project project = moduleRef.getProject();
@@ -1415,9 +1417,12 @@ public final class JavaCompletionContributor extends CompletionContributor imple
JavaModuleNameIndex index = JavaModuleNameIndex.getInstance();
GlobalSearchScope scope = ProjectScope.getAllScope(project);
PsiJavaModule currentModule = JavaModuleGraphUtil.findDescriptorByElement(originalFile);
for (String name : index.getAllKeys(project)) {
if (!index.getModules(name, project, scope).isEmpty() && filter.add(name)) {
Collection<PsiJavaModule> modules = index.getModules(name, project, scope);
if (!modules.isEmpty() && filter.add(name)) {
LookupElement lookup = LookupElementBuilder.create(name).withIcon(AllIcons.Nodes.JavaModule);
if (checkAccess && !isModuleReadable(parent, currentModule, modules)) lookup = highlightLookup(lookup);
if (withAutoModules) lookup = TailTypeDecorator.withTail(lookup, TailTypes.semicolonType());
result.addElement(lookup);
}
@@ -1438,7 +1443,15 @@ public final class JavaCompletionContributor extends CompletionContributor imple
shadowedNames.add(LightJavaModule.moduleName(jarRoot.getNameWithoutExtension()));
}
}
addAutoModuleReference(name, parent, filter, result);
LookupElement lookup = getAutoModuleReference(name, parent, filter, lookupElement -> {
if (!checkAccess) return PrioritizedLookupElement.withPriority(lookupElement, -1);
if (currentModule != null && !isModuleReadable(currentModule, name)) return highlightLookup(lookupElement);
if (currentModule == null && !isModuleReadable(parent, manifests)) return highlightLookup(lookupElement);
return lookupElement;
});
if (lookup != null) {
result.addElement(lookup);
}
}
}
VirtualFile[] roots = ModuleRootManager.getInstance(module).orderEntries().withoutSdk().librariesOnly().getClassesRoots();
@@ -1447,8 +1460,17 @@ public final class JavaCompletionContributor extends CompletionContributor imple
if (shadowedNames.contains(name)) {
continue;
}
if (!JavaAutoModuleNameIndex.getFilesByKey(name, scope).isEmpty()) {
addAutoModuleReference(name, parent, filter, result);
Collection<VirtualFile> files = JavaAutoModuleNameIndex.getFilesByKey(name, scope);
if (!files.isEmpty()) {
LookupElement lookup = getAutoModuleReference(name, parent, filter, lookupElement -> {
if (!checkAccess) return PrioritizedLookupElement.withPriority(lookupElement, -1);
if (currentModule != null && !isModuleReadable(currentModule, name)) return highlightLookup(lookupElement);
if (currentModule == null && !isModuleReadable(parent, files)) return highlightLookup(lookupElement);
return lookupElement;
});
if (lookup != null) {
result.addElement(lookup);
}
}
}
}
@@ -1457,6 +1479,81 @@ public final class JavaCompletionContributor extends CompletionContributor imple
}
}
/**
* Highlights the given {@link LookupElement} by modifying its appearance.
* The text of the element is displayed in red color.
*
* @param lookup the {@link LookupElement} to be highlighted
* @return a new {@link LookupElement} with adjusted proximity and custom rendering
*/
@NotNull
private static LookupElement highlightLookup(@NotNull LookupElement lookup) {
return PrioritizedLookupElement.withExplicitProximity(LookupElementDecorator.withRenderer(lookup, new LookupElementRenderer<>() {
@Override
public void renderElement(LookupElementDecorator<LookupElement> element, LookupElementPresentation presentation) {
element.getDelegate().renderElement(presentation);
presentation.setItemTextForeground(JBColor.RED);
}
}), -1);
}
/**
* Determines if a specified module is readable from a given context
*
* @param place current module/position
* @param targetModuleFiles files from the target module
* @return {@code true} if the target module is readable from the place; {@code false} otherwise.
*/
private static boolean isModuleReadable(@NotNull PsiElement place,
@NotNull Collection<VirtualFile> targetModuleFiles) {
Module currentModule = ModuleUtilCore.findModuleForPsiElement(place);
if (currentModule == null) return false;
GlobalSearchScope scope = GlobalSearchScope.moduleWithDependenciesAndLibrariesScope(currentModule);
for (VirtualFile targetModuleFile : targetModuleFiles) {
if (scope.contains(targetModuleFile)) return true;
}
return false;
}
/**
* Determines if the specified modules are readable from a given context.
*
* @param place the current position or element from where readability is being checked
* @param currentModule the module from which readability is being checked
* @param targetModules the collection of target modules to check readability against
* @return {@code true} if any of the target modules are readable from the current context; {@code false} otherwise
*/
private static boolean isModuleReadable(@NotNull PsiElement place,
@Nullable PsiJavaModule currentModule,
@NotNull Collection<PsiJavaModule> targetModules) {
if (currentModule != null) {
for (PsiJavaModule targetModule : targetModules) {
if (JavaModuleGraphUtil.reads(currentModule, targetModule)) return true;
}
} else {
for (PsiJavaModule targetModule : targetModules) {
PsiFile file = targetModule.getContainingFile();
if (file == null) continue;
VirtualFile virtualFile = file.getVirtualFile();
if (virtualFile == null) continue;
if (isModuleReadable(place, List.of(virtualFile))) return true;
}
}
return false;
}
/**
* Checks if the specified target module is readable from the current module.
*
* @param currentModule the module from which readability is being checked
* @param targetModuleName the name of the target module to check readability against
* @return true if the target module is readable from the current module; false otherwise
*/
private static boolean isModuleReadable(@NotNull PsiJavaModule currentModule,
@NotNull String targetModuleName) {
return JavaModuleGraphUtil.reads(currentModule, targetModuleName);
}
/**
* Searching for a module name in a broken PsiFile when import module declaration typing before the module description.
@@ -1494,13 +1591,14 @@ public final class JavaCompletionContributor extends CompletionContributor imple
return result;
}
private static void addAutoModuleReference(String name, PsiElement parent, Set<? super String> filter, CompletionResultSet result) {
@Nullable
private static LookupElement getAutoModuleReference(@NotNull String name, @NotNull PsiElement parent,
@NotNull Set<? super String> filter, @NotNull Function<LookupElement, LookupElement> wrapper) {
if (PsiNameHelper.isValidModuleName(name, parent) && filter.add(name)) {
LookupElement lookup = LookupElementBuilder.create(name).withIcon(AllIcons.FileTypes.Archive);
lookup = TailTypeDecorator.withTail(lookup, TailTypes.semicolonType());
lookup = PrioritizedLookupElement.withPriority(lookup, -1);
result.addElement(lookup);
return wrapper.apply(TailTypeDecorator.withTail(lookup, TailTypes.semicolonType()));
}
return null;
}
static class IndentingDecorator extends LookupElementDecorator<LookupElement> {

View File

@@ -3,10 +3,13 @@ package com.intellij.java.codeInsight.completion
import com.intellij.codeInsight.completion.CompletionType
import com.intellij.java.testFramework.fixtures.LightJava9ModulesCodeInsightFixtureTestCase
import com.intellij.java.testFramework.fixtures.MultiModuleJava9ProjectDescriptor.ModuleDescriptor.M2
import com.intellij.java.testFramework.fixtures.MultiModuleJava9ProjectDescriptor.ModuleDescriptor.M4
import com.intellij.java.testFramework.fixtures.MultiModuleJava9ProjectDescriptor.ModuleDescriptor.*
import com.intellij.openapi.module.ModuleManager
import com.intellij.openapi.roots.ModuleRootModificationUtil
import com.intellij.testFramework.NeedsIndex
import com.intellij.ui.JBColor
import org.assertj.core.api.Assertions.assertThat
import java.awt.Color
import java.util.jar.JarFile
class ModuleCompletionTest : LightJava9ModulesCodeInsightFixtureTestCase() {
@@ -249,6 +252,74 @@ class ModuleCompletionTest : LightJava9ModulesCodeInsightFixtureTestCase() {
myFixture.assertPreferredCompletionItems(0, "MyClassC", "MyClassB", "MyClassA")
}
fun testReadableCompletion1() {
addFile("module-info.java", "module current.module.name { requires first.module.name; }")
addFile("module-info.java", "module first.module.name { }", M2)
addFile("module-info.java", "module second.module.name { }", M4)
fileComplete("MyClass.java", """
import module module<caret>
public class MyClass { }
""".trimIndent(), mapOf("current.module.name" to JBColor.foreground(),
"first.module.name" to JBColor.foreground(),
"second.module.name" to JBColor.RED))
}
fun testReadableCompletion2() {
addFile("module-info.java", "module current.module.name { requires first.module.name; }")
addFile(JarFile.MANIFEST_NAME, "Manifest-Version: 1.0\nAutomatic-Module-Name: first.module.name\n", M2)
addFile(JarFile.MANIFEST_NAME, "Manifest-Version: 1.0\nAutomatic-Module-Name: second.module.name\n", M4)
fileComplete("MyClass.java", """
import module module<caret>
public class MyClass { }
""".trimIndent(), mapOf("current.module.name" to JBColor.foreground(),
"first.module.name" to JBColor.foreground(),
"second.module.name" to JBColor.RED))
}
fun testReadableCompletion3() {
addFile("module-info.java", "module first.module.name { }", M2)
addFile("module-info.java", "module second.module.name { }", M3)
fileComplete("MyClass.java", """
import module module<caret>
public class MyClass { }
""".trimIndent(), mapOf("first.module.name" to JBColor.foreground(),
"second.module.name" to JBColor.RED))
}
fun testReadableCompletion4() {
addFile(JarFile.MANIFEST_NAME, "Manifest-Version: 1.0\nAutomatic-Module-Name: first.module.name\n", M2)
addFile(JarFile.MANIFEST_NAME, "Manifest-Version: 1.0\nAutomatic-Module-Name: second.module.name\n", M3)
fileComplete("MyClass.java", """
import module module<caret>
public class MyClass { }
""".trimIndent(), mapOf("first.module.name" to JBColor.foreground(),
"second.module.name" to JBColor.RED))
}
fun testReadableCompletionTransitive() {
addFile("module-info.java", "module current.module.name { requires first.module.name; }")
addFile("module-info.java", "module first.module.name { requires transitive second.module.name; }", M2)
addFile("module-info.java", "module second.module.name { requires transitive third.module.name; }", M4)
addFile(JarFile.MANIFEST_NAME, "Manifest-Version: 1.0\nAutomatic-Module-Name: third.module.name\n", M5)
ModuleRootModificationUtil.addDependency(ModuleManager.getInstance(project).findModuleByName(M2.moduleName)!!,
ModuleManager.getInstance(project).findModuleByName(M4.moduleName)!!)
ModuleRootModificationUtil.addDependency(ModuleManager.getInstance(project).findModuleByName(M4.moduleName)!!,
ModuleManager.getInstance(project).findModuleByName(M5.moduleName)!!)
fileComplete("MyClass.java", """
import module module<caret>
public class MyClass { }
""".trimIndent(), mapOf("current.module.name" to JBColor.foreground(),
"first.module.name" to JBColor.foreground(),
"second.module.name" to JBColor.foreground(),
"third.module.name" to JBColor.foreground()))
}
//<editor-fold desc="Helpers.">
private fun complete(text: String, expected: String) = fileComplete("module-info.java", text, expected)
private fun fileComplete(fileName: String, text: String, expected: String) {
@@ -257,12 +328,38 @@ class ModuleCompletionTest : LightJava9ModulesCodeInsightFixtureTestCase() {
myFixture.checkResult(expected)
}
private fun fileComplete(fileName: String, text: String, expected: Map<String, Color>) {
myFixture.configureByText(fileName, text)
myFixture.completeBasic()
myFixture.lookup
val items = myFixture.lookupElements?.map { lookup ->
NormalCompletionTestCase.renderElement(lookup).let { element ->
element.itemText to element.itemTextForeground
}
}?.toMap() ?: emptyMap()
val error = StringBuilder()
expected.forEach { (name, color) ->
if (color != items[name]) error.append("""
${name}:
expected: ${color}
actual: ${items[name]}
""".trimIndent())
}
if (error.isNotBlank()) {
fail(error.toString())
}
}
private fun variants(text: String, vararg variants: String) = fileVariants("module-info.java", text, *variants)
private fun fileVariants(fileName: String, text: String, vararg variants: String) {
myFixture.configureByText(fileName, text)
myFixture.completeBasic()
assertThat(myFixture.lookupElementStrings).containsExactlyInAnyOrder(*variants)
}
//</editor-fold>
}