mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[java] module cycles highlighting
This commit is contained in:
+2
-1
@@ -1628,8 +1628,9 @@ public class HighlightVisitorImpl extends JavaElementVisitor implements Highligh
|
||||
public void visitRequiresStatement(PsiRequiresStatement statement) {
|
||||
super.visitRequiresStatement(statement);
|
||||
if (PsiUtil.isLanguageLevel9OrHigher(myFile)) {
|
||||
PsiJavaModule container = (PsiJavaModule)statement.getParent();
|
||||
PsiJavaModuleReferenceElement ref = statement.getReferenceElement();
|
||||
if (!myHolder.hasErrorResults()) myHolder.add(ModuleHighlightUtil.checkModuleReference(ref));
|
||||
if (!myHolder.hasErrorResults()) myHolder.add(ModuleHighlightUtil.checkModuleReference(ref, container));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* Copyright 2000-2016 JetBrains s.r.o.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.intellij.codeInsight.daemon.impl.analysis;
|
||||
|
||||
import com.intellij.openapi.application.ReadAction;
|
||||
import com.intellij.openapi.module.Module;
|
||||
import com.intellij.openapi.module.ModuleManager;
|
||||
import com.intellij.openapi.module.impl.scopes.ModulesScope;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.vfs.VirtualFile;
|
||||
import com.intellij.psi.*;
|
||||
import com.intellij.psi.search.FilenameIndex;
|
||||
import com.intellij.psi.util.CachedValueProvider.Result;
|
||||
import com.intellij.psi.util.CachedValuesManager;
|
||||
import com.intellij.util.containers.ContainerUtil;
|
||||
import com.intellij.util.containers.MultiMap;
|
||||
import com.intellij.util.graph.DFSTBuilder;
|
||||
import com.intellij.util.graph.Graph;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.intellij.psi.PsiJavaModule.MODULE_INFO_FILE;
|
||||
import static com.intellij.psi.util.PsiModificationTracker.OUT_OF_CODE_BLOCK_MODIFICATION_COUNT;
|
||||
|
||||
public class JavaModuleGraphBuilder {
|
||||
private JavaModuleGraphBuilder() { }
|
||||
|
||||
@Nullable
|
||||
public static Graph<PsiJavaModule> getOrBuild(@NotNull Project project) {
|
||||
return CachedValuesManager.getManager(project).getCachedValue(project, () -> {
|
||||
Graph<PsiJavaModule> graph = ReadAction.compute(() -> build(project));
|
||||
return Result.create(graph, OUT_OF_CODE_BLOCK_MODIFICATION_COUNT);
|
||||
});
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static Collection<PsiJavaModule> findCycle(@NotNull Graph<PsiJavaModule> graph, @NotNull PsiJavaModule module) {
|
||||
return ((JavaModuleGraph)graph).myCycles.stream().filter(set -> set.contains(module)).findFirst().orElse(null);
|
||||
}
|
||||
|
||||
private static Graph<PsiJavaModule> build(Project project) {
|
||||
Set<PsiJavaModule> projectModules = ContainerUtil.newHashSet();
|
||||
for (Module module : ModuleManager.getInstance(project).getModules()) {
|
||||
Collection<VirtualFile> files = FilenameIndex.getVirtualFilesByName(project, MODULE_INFO_FILE, new ModulesScope(module));
|
||||
if (files.size() > 1) {
|
||||
return null;
|
||||
}
|
||||
VirtualFile vFile = ContainerUtil.getFirstItem(files);
|
||||
if (vFile != null) {
|
||||
PsiFile psiFile = PsiManager.getInstance(project).findFile(vFile);
|
||||
if (psiFile instanceof PsiJavaFile) {
|
||||
PsiJavaModule moduleDeclaration = ((PsiJavaFile)psiFile).getModuleDeclaration();
|
||||
if (moduleDeclaration != null) {
|
||||
projectModules.add(moduleDeclaration);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MultiMap<PsiJavaModule, PsiJavaModule> relations = MultiMap.create();
|
||||
for (PsiJavaModule moduleDeclaration : projectModules) {
|
||||
for (PsiElement child = moduleDeclaration.getFirstChild(); child != null; child = child.getNextSibling()) {
|
||||
if (child instanceof PsiRequiresStatement) {
|
||||
PsiJavaModule dependency = resolveDependency((PsiRequiresStatement)child);
|
||||
if (dependency != null && projectModules.contains(dependency)) {
|
||||
relations.putValue(moduleDeclaration, dependency);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return new JavaModuleGraph(relations);
|
||||
}
|
||||
|
||||
private static PsiJavaModule resolveDependency(PsiRequiresStatement statement) {
|
||||
PsiJavaModuleReferenceElement refElement = statement.getReferenceElement();
|
||||
if (refElement != null) {
|
||||
PsiPolyVariantReference ref = refElement.getReference();
|
||||
if (ref != null) {
|
||||
PsiElement target = ref.resolve();
|
||||
if (target instanceof PsiJavaModule) {
|
||||
return (PsiJavaModule)target;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static class JavaModuleGraph implements Graph<PsiJavaModule> {
|
||||
private final MultiMap<PsiJavaModule, PsiJavaModule> myMap;
|
||||
private final List<Set<PsiJavaModule>> myCycles;
|
||||
|
||||
public JavaModuleGraph(MultiMap<PsiJavaModule, PsiJavaModule> map) {
|
||||
myMap = map;
|
||||
|
||||
DFSTBuilder<PsiJavaModule> builder = new DFSTBuilder<>(this);
|
||||
myCycles = builder.getComponents().stream()
|
||||
.map(ContainerUtil::newLinkedHashSet)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<PsiJavaModule> getNodes() {
|
||||
return myMap.keySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<PsiJavaModule> getIn(PsiJavaModule n) {
|
||||
return ContainerUtil.emptyIterator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<PsiJavaModule> getOut(PsiJavaModule n) {
|
||||
return myMap.get(n).iterator();
|
||||
}
|
||||
}
|
||||
}
|
||||
+24
-3
@@ -24,6 +24,7 @@ import com.intellij.codeInsight.daemon.impl.quickfix.GoToSymbolFix;
|
||||
import com.intellij.codeInsight.daemon.impl.quickfix.MoveFileFix;
|
||||
import com.intellij.codeInsight.daemon.impl.quickfix.QuickFixAction;
|
||||
import com.intellij.codeInsight.intention.QuickFixFactory;
|
||||
import com.intellij.openapi.application.ApplicationManager;
|
||||
import com.intellij.openapi.module.Module;
|
||||
import com.intellij.openapi.module.ModuleUtilCore;
|
||||
import com.intellij.openapi.module.impl.scopes.ModulesScope;
|
||||
@@ -34,12 +35,15 @@ import com.intellij.openapi.vfs.VirtualFile;
|
||||
import com.intellij.psi.*;
|
||||
import com.intellij.psi.search.FilenameIndex;
|
||||
import com.intellij.util.containers.ContainerUtil;
|
||||
import com.intellij.util.graph.Graph;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static com.intellij.psi.PsiJavaModule.MODULE_INFO_FILE;
|
||||
|
||||
@@ -118,14 +122,31 @@ public class ModuleHighlightUtil {
|
||||
}
|
||||
|
||||
@Nullable
|
||||
static HighlightInfo checkModuleReference(@Nullable PsiJavaModuleReferenceElement refElement) {
|
||||
static HighlightInfo checkModuleReference(@Nullable PsiJavaModuleReferenceElement refElement, @NotNull PsiJavaModule container) {
|
||||
if (refElement != null) {
|
||||
PsiPolyVariantReference ref = refElement.getReference();
|
||||
assert ref != null : refElement.getParent();
|
||||
if (ref.multiResolve(false).length == 0) {
|
||||
String message = JavaErrorMessages.message("module.ref.unknown", refElement.getReferenceText());
|
||||
PsiElement target = ref.resolve();
|
||||
if (!(target instanceof PsiJavaModule)) {
|
||||
String message = JavaErrorMessages.message("module.not.found", refElement.getReferenceText());
|
||||
return HighlightInfo.newHighlightInfo(HighlightInfoType.WRONG_REF).range(refElement).description(message).create();
|
||||
}
|
||||
else if (target == container) {
|
||||
String message = JavaErrorMessages.message("module.cyclic.dependence", container.getModuleName());
|
||||
return HighlightInfo.newHighlightInfo(HighlightInfoType.ERROR).range(refElement).description(message).create();
|
||||
}
|
||||
else {
|
||||
Graph<PsiJavaModule> graph = JavaModuleGraphBuilder.getOrBuild(target.getProject());
|
||||
if (graph != null) {
|
||||
Collection<PsiJavaModule> cycle = JavaModuleGraphBuilder.findCycle(graph, (PsiJavaModule)target);
|
||||
if (cycle != null && cycle.contains(container)) {
|
||||
Stream<String> stream = cycle.stream().map(PsiJavaModule::getModuleName);
|
||||
if (ApplicationManager.getApplication().isUnitTestMode()) stream = stream.sorted();
|
||||
String message = JavaErrorMessages.message("module.cyclic.dependence", stream.collect(Collectors.joining(", ")));
|
||||
return HighlightInfo.newHighlightInfo(HighlightInfoType.ERROR).range(refElement).description(message).create();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -396,7 +396,8 @@ module.file.duplicate='module-info.java' already exists in the module
|
||||
module.duplicate.requires=Duplicate requires: {0}
|
||||
module.file.wrong.location=Module declaration should be located in a module's source root
|
||||
module.open.duplicate.text=Go to duplicate
|
||||
module.ref.unknown=Cannot resolve module ''{0}''
|
||||
module.not.found=Module not found: {0}
|
||||
module.cyclic.dependence=Cyclic dependence: {0}
|
||||
|
||||
feature.generics=Generics
|
||||
feature.annotations=Annotations
|
||||
|
||||
@@ -24,11 +24,13 @@ import com.intellij.openapi.roots.ModifiableRootModel
|
||||
import com.intellij.openapi.util.io.FileUtil
|
||||
import com.intellij.openapi.vfs.VfsUtil
|
||||
import com.intellij.pom.java.LanguageLevel
|
||||
import com.intellij.psi.PsiJavaModule
|
||||
import com.intellij.testFramework.IdeaTestUtil
|
||||
import com.intellij.testFramework.LightPlatformTestCase
|
||||
import com.intellij.testFramework.LightProjectDescriptor
|
||||
import com.intellij.testFramework.fixtures.DefaultLightProjectDescriptor
|
||||
import com.intellij.testFramework.fixtures.LightCodeInsightFixtureTestCase
|
||||
import com.intellij.testFramework.fixtures.impl.CodeInsightTestFixtureImpl
|
||||
|
||||
class ModuleHighlightingTest : LightCodeInsightFixtureTestCase() {
|
||||
override fun getProjectDescriptor(): LightProjectDescriptor = DESCRIPTOR
|
||||
@@ -49,11 +51,19 @@ class ModuleHighlightingTest : LightCodeInsightFixtureTestCase() {
|
||||
}
|
||||
|
||||
fun testDuplicateRequires() {
|
||||
doTest("""module M { requires M2; <error descr="Duplicate requires: M2">requires M2;</error> }""")
|
||||
doTest("""module M { requires M2; <error descr="Duplicate requires: M2">requires M2;</error> }""", true)
|
||||
}
|
||||
|
||||
fun testUnresolvedModule() {
|
||||
doTest("""module M { requires <error descr="Cannot resolve module 'M.missing'">M.missing</error>; }""")
|
||||
doTest("""module M { requires <error descr="Module not found: M.missing">M.missing</error>; }""")
|
||||
}
|
||||
|
||||
fun testSelfDependence() {
|
||||
doTest("""module M { requires <error descr="Cyclic dependence: M">M</error>; }""")
|
||||
}
|
||||
|
||||
fun testCyclicDependence() {
|
||||
doTest("""module M1 { requires <error descr="Cyclic dependence: M1, M2">M2</error>; }""", true)
|
||||
}
|
||||
|
||||
//<editor-fold desc="Helpers.">
|
||||
@@ -68,7 +78,7 @@ class ModuleHighlightingTest : LightCodeInsightFixtureTestCase() {
|
||||
val src2 = createSourceRoot(m2, "src2")
|
||||
createContentEntry(m2, src2)
|
||||
|
||||
VfsUtil.saveText(src2.createChildData(this, "module-info.java"), "module M2 { }")
|
||||
VfsUtil.saveText(src2.createChildData(this, "module-info.java"), "module M2 { requires M1; }")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,8 +94,11 @@ class ModuleHighlightingTest : LightCodeInsightFixtureTestCase() {
|
||||
file
|
||||
})
|
||||
|
||||
private fun doTest(text: String) {
|
||||
private fun doTest(text: String, filter: Boolean = false) {
|
||||
myFixture.configureByText("module-info.java", text)
|
||||
if (filter) {
|
||||
(myFixture as CodeInsightTestFixtureImpl).setVirtualFileFilter { it.name != PsiJavaModule.MODULE_INFO_FILE }
|
||||
}
|
||||
myFixture.checkHighlighting()
|
||||
}
|
||||
//</editor-fold>
|
||||
|
||||
Reference in New Issue
Block a user