[java, quickfix, jigsaw] IDEA-380666 add circular dependency handling for Jigsaw module completion

(cherry picked from commit 0beb868ac32c4cabae9392a9c34cb66086935a02)


(cherry picked from commit 5a7924c57cb9afd068e60aed6eea4d3b151cf8de)

IJ-MR-182986

GitOrigin-RevId: 201885016bf87fce8b6a98413c63d47de5023539
This commit is contained in:
Aleksey Dobrynin
2025-11-21 11:43:10 +00:00
committed by intellij-monorepo-bot
parent 470e4f879a
commit 547ccf8e3e
15 changed files with 129 additions and 6 deletions
@@ -431,7 +431,7 @@ public final class JavaPsiModuleUtil {
PsiJavaModuleReference ref = statement.getModuleReference();
if (ref != null) {
if (JAVA_BASE.equals(ref.getCanonicalText())) explicitJavaBase = true;
for (ResolveResult result : ref.multiResolve(false)) {
for (ResolveResult result : ref.multiResolve(true)) {
PsiJavaModule dependency = (PsiJavaModule)result.getElement();
assert dependency != null : result;
relations.putValue(module, dependency);
@@ -491,6 +491,8 @@ public final class JavaPsiModuleUtil {
}
public boolean reads(PsiJavaModule source, PsiJavaModule destination) {
source = getPhysicalModule(source);
destination = getPhysicalModule(destination);
Collection<PsiJavaModule> nodes = myGraph.getNodes();
if (nodes.contains(destination) && nodes.contains(source)) {
Iterator<PsiJavaModule> directReaders = myGraph.getOut(destination);
@@ -505,6 +507,7 @@ public final class JavaPsiModuleUtil {
}
private @Nullable ModulePackageConflict findConflict(@NotNull PsiJavaModule source) {
source = getPhysicalModule(source);
Map<String, PsiJavaModule> exports = new HashMap<>();
return processExports(source, (pkg, m) -> {
PsiJavaModule found = exports.put(pkg, m);
@@ -516,10 +519,11 @@ public final class JavaPsiModuleUtil {
}
private @Nullable PsiJavaModule findOrigin(@NotNull PsiJavaModule module, @NotNull String packageName) {
return processExports(module, (pkg, m) -> packageName.equals(pkg) ? m : null);
return processExports(getPhysicalModule(module), (pkg, m) -> packageName.equals(pkg) ? m : null);
}
private <T> @Nullable T processExports(@NotNull PsiJavaModule start, @NotNull BiFunction<? super String, ? super PsiJavaModule, ? extends T> processor) {
start = getPhysicalModule(start);
return myGraph.getNodes().contains(start) ? processExports(start.getName(), start, true, new HashSet<>(), processor) : null;
}
@@ -528,6 +532,7 @@ public final class JavaPsiModuleUtil {
boolean direct,
@NotNull Set<? super PsiJavaModule> visited,
@NotNull BiFunction<? super String, ? super PsiJavaModule, ? extends T> processor) {
module = getPhysicalModule(module);
if (visited.add(module)) {
if (!direct) {
for (PsiPackageAccessibilityStatement statement : module.getExports()) {
@@ -556,11 +561,12 @@ public final class JavaPsiModuleUtil {
public @NotNull Set<PsiJavaModule> getAllDependencies(@NotNull PsiJavaModule module, boolean transitive) {
Set<PsiJavaModule> requires = new HashSet<>();
collectDependencies(module, requires, transitive);
collectDependencies(getPhysicalModule(module), requires, transitive);
return requires;
}
private void collectDependencies(@NotNull PsiJavaModule module, @NotNull Set<PsiJavaModule> dependencies, boolean transitive) {
module = getPhysicalModule(module);
for (Iterator<PsiJavaModule> iterator = myGraph.getIn(module); iterator.hasNext();) {
PsiJavaModule dependency = iterator.next();
if (!dependencies.contains(dependency) && (!transitive || myTransitiveEdges.contains(key(dependency, module)))) {
@@ -569,6 +575,14 @@ public final class JavaPsiModuleUtil {
}
}
}
private static @NotNull PsiJavaModule getPhysicalModule(@NotNull PsiJavaModule from) {
if (from.isPhysical()) return from;
if (!(from.getContainingFile() instanceof PsiJavaFile file)) return from;
if (!(file.getOriginalFile() instanceof PsiJavaFile origin)) return from;
if (origin.getModuleDeclaration() instanceof PsiJavaModule result) return result;
return from;
}
}
/**
@@ -104,6 +104,9 @@ public final class JavaModuleGraphUtil {
if (to.equals(from.getName())) return false;
if (!PsiNameHelper.isValidModuleName(to, from)) return false;
if (alreadyContainsRequires(from, to)) return false;
PsiJavaModule toModule = JavaPsiFacade.getInstance(from.getProject()).findModule(to, from.getResolveScope());
if (toModule != null && JavaPsiModuleUtil.reads(toModule, from)) return false; // check for circular dependencies
PsiUtil.addModuleStatement(from, JavaKeywords.REQUIRES + " " +
(isStaticModule(to, scope) ? JavaKeywords.STATIC + " " : "") +
(isExported ? JavaKeywords.TRANSITIVE + " " : "") +
@@ -133,6 +136,7 @@ public final class JavaModuleGraphUtil {
if (!PsiNameHelper.isValidModuleName(to.getName(), to)) return false;
if (contains(from.getRequires(), to.getName())) return false;
if (JavaPsiModuleUtil.reads(from, to)) return false;
if (JavaPsiModuleUtil.reads(to, from)) return false; // check for circular dependencies
PsiUtil.addModuleStatement(from, JavaKeywords.REQUIRES + " " +
(isStaticModule(to.getName(), scope) ? JavaKeywords.STATIC + " " : "") +
(isExported(from, to) ? JavaKeywords.TRANSITIVE + " " : "") +
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="11" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/main.iml" filepath="$PROJECT_DIR$/main.iml" />
<module fileurl="file://$PROJECT_DIR$/A/A.iml" filepath="$PROJECT_DIR$/A/A.iml" />
<module fileurl="file://$PROJECT_DIR$/B/B.iml" filepath="$PROJECT_DIR$/B/B.iml" />
</modules>
</component>
</project>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
@@ -0,0 +1,4 @@
module module.a {
requires module.main;
exports org.jetbrains.a;
}
@@ -0,0 +1,4 @@
package org.jetbrains.a;
public class MyAClass {
}
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
@@ -0,0 +1,3 @@
module module.b {
exports org.jetbrains.b;
}
@@ -0,0 +1,4 @@
package org.jetbrains.b;
public class MyBClass {
}
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="module" module-name="A" />
<orderEntry type="module" module-name="B" />
</component>
</module>
@@ -0,0 +1,5 @@
public class Main {
private void foo() {
new MyACla
}
}
@@ -0,0 +1,3 @@
module module.main {
requires module.b;
}
@@ -1,7 +1,8 @@
// 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.codeInsight.daemon.impl.quickfix
import com.intellij.java.testFramework.fixtures.LightJava9ModulesCodeInsightFixtureTestCase
import com.intellij.java.testFramework.fixtures.MultiModuleJava9ProjectDescriptor
import com.intellij.modcommand.ModCommandAction
import com.intellij.openapi.command.CommandProcessor
import com.intellij.psi.PsiJavaFile
@@ -43,6 +44,11 @@ class AddModuleDirectiveTest : LightJava9ModulesCodeInsightFixtureTestCase() {
"module M { requires M2; }",
"module M { requires M2; }")
fun testNoCircularDependency() {
addFile("module-info.java", "module M2 { requires M; }", MultiModuleJava9ProjectDescriptor.ModuleDescriptor.M2)
doRequiresTest("module M { }", "module M { }")
}
fun testRequiresInIncompleteModule(): Unit = doRequiresTest(
"module M {",
"module M {\n" +
@@ -1,4 +1,4 @@
// 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.java.codeInsight.completion;
import com.intellij.JavaTestUtil;
@@ -152,9 +152,34 @@ public class JigsawCodeCompletionTest extends LightFixtureCompletionTestCase {
}""");
}
public void testCircularDependencyCompletion() {
completeBasic("Main.java", """
public class Main {
private void foo() {
new MyACla<caret>
}
}
""")
.variants(new Variant("MyAClass", " org.jetbrains.a", Color.RED))
.choose(new Variant("MyAClass", " org.jetbrains.a", Color.RED))
.check("Main.java", """
import org.jetbrains.a.MyAClass;
public class Main {
private void foo() {
new MyAClass()
}
}
""")
.check("module-info.java", """
module module.main {
requires module.b;
}""");
}
private JigsawCodeCompletionTest variants(Variant @NotNull ... variants) {
final LookupElement[] elements = myFixture.getLookupElements();
assertEquals(Arrays.toString(elements), elements.length, variants.length);
assertEquals(Arrays.toString(elements), variants.length, elements.length);
for (int i = 0; i < elements.length; i++) {
final LookupElementPresentation element = NormalCompletionTestCase.renderElement(elements[i]);
assertEquals(variants[i].text(), element.getItemText());