[java] Jigsaw package accessibility highlighting (source dependencies)

This commit is contained in:
Roman Shevchenko
2016-09-29 14:11:09 +02:00
parent b5347c6a1d
commit 4f22f2e176
7 changed files with 267 additions and 28 deletions
@@ -65,6 +65,7 @@ public class HighlightVisitorImpl extends JavaElementVisitor implements Highligh
private JavaSdkVersion myJavaSdkVersion;
@SuppressWarnings("StatefulEp") private PsiFile myFile;
@SuppressWarnings("StatefulEp") private PsiJavaModule myJavaModule;
// map codeBlock->List of PsiReferenceExpression of uninitialized final variables
private final Map<PsiElement, Collection<PsiReferenceExpression>> myUninitializedVarProblems = new THashMap<>();
@@ -155,13 +156,9 @@ public class HighlightVisitorImpl extends JavaElementVisitor implements Highligh
@Override
public boolean analyze(@NotNull PsiFile file, boolean updateWholeFile, @NotNull HighlightInfoHolder holder, @NotNull Runnable highlight) {
myFile = file;
myHolder = Holder.CHECK_ELEMENT_LEVEL ? new CheckLevelHighlightInfoHolder(file, holder) : holder;
boolean success = true;
try {
myLanguageLevel = PsiUtil.getLanguageLevel(file);
myJavaSdkVersion = notNull(JavaVersionService.getInstance().getJavaSdkVersion(file), JavaSdkVersion.fromLanguageLevel(myLanguageLevel));
prepare(Holder.CHECK_ELEMENT_LEVEL ? new CheckLevelHighlightInfoHolder(file, holder) : holder, file);
if (updateWholeFile) {
ProgressIndicator progress = ProgressManager.getInstance().getProgressIndicator();
if (progress == null) throw new IllegalStateException("Must be run under progress");
@@ -193,6 +190,7 @@ public class HighlightVisitorImpl extends JavaElementVisitor implements Highligh
myReassignedParameters.clear();
myRefCountHolder = null;
myJavaModule = null;
myFile = null;
myHolder = null;
myDuplicateMethods.clear();
@@ -202,6 +200,18 @@ public class HighlightVisitorImpl extends JavaElementVisitor implements Highligh
return success;
}
protected void prepareToRunAsInspection(@NotNull HighlightInfoHolder holder) {
prepare(holder, holder.getContextFile());
}
private void prepare(HighlightInfoHolder holder, PsiFile file) {
myHolder = holder;
myFile = file;
myLanguageLevel = PsiUtil.getLanguageLevel(file);
myJavaSdkVersion = notNull(JavaVersionService.getInstance().getJavaSdkVersion(file), JavaSdkVersion.fromLanguageLevel(myLanguageLevel));
myJavaModule = myLanguageLevel.isAtLeast(LanguageLevel.JDK_1_9) ? ModuleHighlightUtil.getModuleDescriptor(file) : null;
}
@Override
public void visitElement(PsiElement element) {
if (myRefCountHolder != null && myFile instanceof ServerPageFile) {
@@ -1030,8 +1040,14 @@ public class HighlightVisitorImpl extends JavaElementVisitor implements Highligh
@Override
public void visitReferenceElement(PsiJavaCodeReferenceElement ref) {
JavaResolveResult resolveResult = doVisitReferenceElement(ref);
if (resolveResult != null && !myHolder.hasErrorResults()) myHolder.add(GenericsHighlightUtil.checkRawOnParameterizedType(ref, resolveResult.getElement()));
JavaResolveResult result = doVisitReferenceElement(ref);
if (result != null) {
PsiElement resolved = result.getElement();
if (!myHolder.hasErrorResults()) myHolder.add(GenericsHighlightUtil.checkRawOnParameterizedType(ref, resolved));
if (!myHolder.hasErrorResults() && resolved != null && myJavaModule != null) {
myHolder.add(ModuleHighlightUtil.checkPackageAccessibility(ref, resolved, myJavaModule));
}
}
}
private JavaResolveResult doVisitReferenceElement(@NotNull PsiJavaCodeReferenceElement ref) {
@@ -1688,14 +1704,4 @@ public class HighlightVisitorImpl extends JavaElementVisitor implements Highligh
private HighlightInfo checkFeature(@NotNull PsiElement element, @NotNull Feature feature) {
return HighlightUtil.checkFeature(element, feature, myLanguageLevel, myFile);
}
protected void prepareToRunAsInspection(@NotNull HighlightInfoHolder holder) {
PsiFile file = holder.getContextFile();
JavaSdkVersion sdkVersion = JavaVersionService.getInstance().getJavaSdkVersion(file);
myHolder = holder;
myFile = file;
myLanguageLevel = PsiUtil.getLanguageLevel(file);
myJavaSdkVersion = sdkVersion != null ? sdkVersion : JavaSdkVersion.fromLanguageLevel(myLanguageLevel);
}
}
@@ -24,10 +24,12 @@ 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.psi.util.PsiUtil;
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 com.intellij.util.graph.GraphGenerator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -49,6 +51,13 @@ public class JavaModuleGraphUtil {
return ContainerUtil.find(cycles, set -> set.contains(module));
}
public static boolean reads(@NotNull PsiJavaModule source, @NotNull PsiJavaModule destination) {
Project project = source.getProject();
RequiresGraph graph = CachedValuesManager.getManager(project).getCachedValue(project, () ->
Result.create(ReadAction.compute(() -> buildRequiresGraph(project)), OUT_OF_CODE_BLOCK_MODIFICATION_COUNT));
return graph.reads(source, destination);
}
// Looks for cycles between Java modules in the project sources.
// Library/JDK modules are excluded - in assumption there can't be any lib -> src dependencies.
// Module references are resolved "globally" (i.e., without taking project dependencies into account).
@@ -65,15 +74,15 @@ public class JavaModuleGraphUtil {
if (!projectModules.isEmpty()) {
MultiMap<PsiJavaModule, PsiJavaModule> relations = MultiMap.create();
for (PsiJavaModule moduleDeclaration : projectModules) {
for (PsiRequiresStatement statement : psiTraverser().children(moduleDeclaration).filter(PsiRequiresStatement.class)) {
for (PsiJavaModule module : projectModules) {
for (PsiRequiresStatement statement : psiTraverser().children(module).filter(PsiRequiresStatement.class)) {
Optional.ofNullable(statement.getReferenceElement())
.map(PsiJavaModuleReferenceElement::getReference)
.map(ref -> ref.multiResolve(true))
.map(a -> a.length == 1 ? a[0].getElement() : null)
.map(e -> e instanceof PsiJavaModule ? (PsiJavaModule)e : null)
.filter(projectModules::contains)
.ifPresent(dependency -> relations.putValue(moduleDeclaration, dependency));
.ifPresent(dependency -> relations.putValue(module, dependency));
}
}
@@ -90,6 +99,74 @@ public class JavaModuleGraphUtil {
return Collections.emptyList();
}
// Starting from source modules, collects all module dependencies in the project.
// The resulting graph is used for tracing readability.
private static RequiresGraph buildRequiresGraph(Project project) {
MultiMap<PsiJavaModule, PsiJavaModule> relations = MultiMap.create();
Set<String> publicEdges = ContainerUtil.newTroveSet();
for (Module module : ModuleManager.getInstance(project).getModules()) {
Collection<VirtualFile> files = FilenameIndex.getVirtualFilesByName(project, MODULE_INFO_FILE, module.getModuleScope(false));
Optional.ofNullable(ContainerUtil.getFirstItem(files))
.map(PsiManager.getInstance(project)::findFile)
.map(f -> f instanceof PsiJavaFile ? ((PsiJavaFile)f).getModuleDeclaration() : null)
.ifPresent(m -> visit(m, relations, publicEdges));
}
GraphGenerator<PsiJavaModule> graph = GraphGenerator.create(new RequiresSemiGraph(relations));
return new RequiresGraph(graph, publicEdges);
}
private static void visit(PsiJavaModule module, MultiMap<PsiJavaModule, PsiJavaModule> relations, Set<String> publicEdges) {
if (!relations.containsKey(module)) {
relations.putValues(module, Collections.emptyList());
for (PsiRequiresStatement statement : psiTraverser().children(module).filter(PsiRequiresStatement.class)) {
Optional.ofNullable(statement.getReferenceElement())
.map(PsiJavaModuleReferenceElement::getReference)
.map(PsiReference::resolve)
.map(e -> e instanceof PsiJavaModule ? (PsiJavaModule)e : null)
.ifPresent(dependency -> {
relations.putValue(module, dependency);
if (isPublic(statement)) publicEdges.add(RequiresGraph.key(dependency, module));
visit(dependency, relations, publicEdges);
});
}
}
}
private static boolean isPublic(PsiRequiresStatement statement) {
for (PsiElement child = statement.getFirstChild(); child != null; child = child.getNextSibling()) {
if (PsiUtil.isJavaToken(child, JavaTokenType.PUBLIC_KEYWORD)) return true;
if (child instanceof PsiJavaModuleReferenceElement) break;
}
return false;
}
private static class RequiresGraph {
private final Graph<PsiJavaModule> myGraph;
private final Set<String> myPublicEdges;
public RequiresGraph(Graph<PsiJavaModule> graph, Set<String> publicEdges) {
myGraph = graph;
myPublicEdges = publicEdges;
}
public boolean reads(PsiJavaModule source, PsiJavaModule destination) {
Iterator<PsiJavaModule> directReaders = myGraph.getOut(destination);
while (directReaders.hasNext()) {
PsiJavaModule next = directReaders.next();
if (source.equals(next) || myPublicEdges.contains(key(destination, next)) && reads(source, next)) {
return true;
}
}
return false;
}
public static String key(PsiJavaModule module, PsiJavaModule exporter) {
return module.getModuleName() + '/' + exporter.getModuleName();
}
}
//<editor-fold desc="Helpers.">
private static class SourceSemiGraph implements Graph<PsiJavaModule> {
private final MultiMap<PsiJavaModule, PsiJavaModule> myMap;
@@ -112,4 +189,23 @@ public class JavaModuleGraphUtil {
return myMap.get(n).iterator();
}
}
private static class RequiresSemiGraph implements GraphGenerator.SemiGraph<PsiJavaModule> {
private final MultiMap<PsiJavaModule, PsiJavaModule> myMap;
public RequiresSemiGraph(MultiMap<PsiJavaModule, PsiJavaModule> map) {
myMap = map;
}
@Override
public Collection<PsiJavaModule> getNodes() {
return myMap.keySet();
}
@Override
public Iterator<PsiJavaModule> getIn(PsiJavaModule n) {
return myMap.get(n).iterator();
}
}
//</editor-fold>
}
@@ -54,6 +54,34 @@ import static com.intellij.psi.PsiJavaModule.MODULE_INFO_FILE;
import static com.intellij.psi.SyntaxTraverser.psiTraverser;
public class ModuleHighlightUtil {
@Nullable
static PsiJavaModule getModuleDescriptor(@NotNull PsiElement element) {
VirtualFile file = Optional.of(element)
.map(e -> e instanceof PsiFileSystemItem ? (PsiFileSystemItem)e : e.getContainingFile())
.map(PsiFileSystemItem::getVirtualFile)
.orElse(null);
if (file == null) return null;
Project project = element.getProject();
ProjectFileIndex index = ProjectFileIndex.SERVICE.getInstance(project);
if (element instanceof PsiCompiledElement) {
return Optional.ofNullable(index.getClassRootForFile(file))
.map(r -> r.findChild(PsiJavaModule.MODULE_INFO_CLS_FILE))
.map(PsiManager.getInstance(project)::findFile)
.map(f -> f instanceof PsiJavaFile ? ((PsiJavaFile)f).getModuleDeclaration() : null)
.orElse(null);
}
else {
Module module = index.getModuleForFile(file);
return Optional.ofNullable(module)
.map(m -> FilenameIndex.getVirtualFilesByName(project, MODULE_INFO_FILE, m.getModuleScope(false)))
.map(c -> c.size () == 1 ? c.iterator().next() : null)
.map(PsiManager.getInstance(project)::findFile)
.map(f -> f instanceof PsiJavaFile ? ((PsiJavaFile)f).getModuleDeclaration() : null)
.orElse(null);
}
}
@Nullable
static HighlightInfo checkFileName(@NotNull PsiJavaModule element, @NotNull PsiFile file) {
if (!MODULE_INFO_FILE.equals(file.getName())) {
@@ -307,6 +335,70 @@ public class ModuleHighlightUtil {
return null;
}
@Nullable
static HighlightInfo checkPackageAccessibility(@NotNull PsiJavaCodeReferenceElement ref,
@NotNull PsiElement target,
@NotNull PsiJavaModule refModule) {
Module module = ModuleUtilCore.findModuleForPsiElement(ref);
if (module != null) {
if (target instanceof PsiClass && !(target instanceof PsiCompiledElement) && module != ModuleUtilCore.findModuleForPsiElement(target)) {
PsiElement targetFile = target.getParent();
if (targetFile instanceof PsiClassOwner) {
PsiJavaModule targetModule = getModuleDescriptor(target);
String packageName = ((PsiClassOwner)targetFile).getPackageName();
return checkPackageAccessibility(ref, refModule, targetModule, packageName);
}
}
else if (target instanceof PsiPackage) {
PsiElement refImport = ref.getParent();
if (refImport instanceof PsiImportStatementBase && ((PsiImportStatementBase)refImport).isOnDemand()) {
PsiDirectory[] dirs = ((PsiPackage)target).getDirectories(module.getModuleWithDependenciesAndLibrariesScope(false));
if (dirs.length == 1 && ModuleUtilCore.findModuleForPsiElement(dirs[0]) != module) {
PsiJavaModule targetModule = getModuleDescriptor(dirs[0]);
String packageName = ((PsiPackage)target).getQualifiedName();
return checkPackageAccessibility(ref, refModule, targetModule, packageName);
}
}
}
}
return null;
}
private static HighlightInfo checkPackageAccessibility(PsiJavaCodeReferenceElement ref,
PsiJavaModule refModule,
PsiJavaModule targetModule,
String packageName) {
if (targetModule == null) {
String message = JavaErrorMessages.message("module.package.on.classpath");
return HighlightInfo.newHighlightInfo(HighlightInfoType.WRONG_REF).range(ref).description(message).create();
}
String refModuleName = refModule.getModuleName();
String requiredName = targetModule.getModuleName();
if (!(targetModule instanceof PsiCompiledElement) && !isExported(targetModule, packageName)) {
String message = JavaErrorMessages.message("module.package.not.exported", requiredName, packageName, refModuleName);
return HighlightInfo.newHighlightInfo(HighlightInfoType.WRONG_REF).range(ref).description(message).create();
}
if (!(PsiJavaModule.JAVA_BASE.equals(requiredName) || JavaModuleGraphUtil.reads(refModule, targetModule))) {
String message = JavaErrorMessages.message("module.not.in.requirements", refModuleName, requiredName);
return HighlightInfo.newHighlightInfo(HighlightInfoType.WRONG_REF).range(ref).description(message).create();
}
return null;
}
private static boolean isExported(PsiJavaModule module, String packageName) {
for (PsiExportsStatement statement : psiTraverser().children(module).filter(PsiExportsStatement.class)) {
String exportedName = Optional.ofNullable(statement.getPackageReference()).map(ModuleHighlightUtil::refText).orElse("");
if (packageName.equals(exportedName)) {
return true;
}
}
return false;
}
private static HighlightInfo moduleResolveError(PsiJavaModuleReferenceElement refElement, PsiPolyVariantReference ref) {
boolean missing = ref.multiResolve(true).length == 0;
String message = JavaErrorMessages.message(missing ? "module.not.found" : "module.not.on.path", refElement.getReferenceText());
@@ -26,6 +26,7 @@ public interface PsiJavaModule extends NavigatablePsiElement, PsiNamedElement, P
String MODULE_INFO_CLASS = "module-info";
String MODULE_INFO_FILE = MODULE_INFO_CLASS + ".java";
String MODULE_INFO_CLS_FILE = MODULE_INFO_CLASS + ".class";
String JAVA_BASE = "java.base";
@NotNull PsiJavaModuleReferenceElement getNameElement();
@NotNull String getModuleName();
@@ -411,6 +411,9 @@ module.service.abstract=The service implementation is an abstract class: {0}
module.service.no.ctor=The service implementation does not have a default constructor: {0}
module.service.hidden.ctor=The default constructor of the service implementation is not public: {0}
module.service.unused=Service interface provided but not exported or used
module.package.not.exported=The module ''{0}'' does not export the package ''{1}'' to the module ''{2}''
module.package.on.classpath=A named module cannot access packages of an unnamed one
module.not.in.requirements=The module ''{0}'' does not have the module ''{1}'' in requirements
feature.generics=Generics
feature.annotations=Annotations
@@ -17,8 +17,7 @@ package com.intellij.codeInsight.daemon
import com.intellij.psi.PsiJavaModule
import com.intellij.testFramework.fixtures.LightJava9ModulesCodeInsightFixtureTestCase
import com.intellij.testFramework.fixtures.MultiModuleJava9ProjectDescriptor.ModuleDescriptor.M2
import com.intellij.testFramework.fixtures.MultiModuleJava9ProjectDescriptor.ModuleDescriptor.M3
import com.intellij.testFramework.fixtures.MultiModuleJava9ProjectDescriptor.ModuleDescriptor.*
import com.intellij.testFramework.fixtures.impl.CodeInsightTestFixtureImpl
import org.assertj.core.api.Assertions.assertThat
@@ -136,6 +135,33 @@ class ModuleHighlightingTest : LightJava9ModulesCodeInsightFixtureTestCase() {
fixes("module M { uses <caret>pkg.m3.C3; }", "AddModuleDependencyFix")
}
fun testPackageAccessibility() {
addFile("module-info.java", "module M { requires M2; requires M6; }")
addFile("module-info.java", "module M2 { exports pkg.m2; }", M2)
addFile("pkg/m2/C2.java", "package pkg.m2;\npublic class C2 { }", M2)
addFile("pkg/m2/impl/C2Impl.java", "package pkg.m2.impl;\nimport pkg.m2.C2;\npublic class C2Impl { public static C2 make() {} }", M2)
addFile("pkg/m4/C4.java", "package pkg.m4;\npublic class C4 { }", M4)
addFile("module-info.java", "module M5 { exports pkg.m5; }", M5)
addFile("pkg/m5/C5.java", "package pkg.m5;\npublic class C5 { }", M5)
addFile("module-info.java", "module M6 { requires public M7; }", M6)
addFile("module-info.java", "module M7 { exports pkg.m7; }", M7)
addFile("pkg/m7/C7.java", "package pkg.m7;\npublic class C7 { }", M7)
highlight("test.java", """
import pkg.m2.C2;
import pkg.m2.*;
import <error descr="The module 'M2' does not export the package 'pkg.m2.impl' to the module 'M'">pkg.m2.impl.C2Impl</error>;
import <error descr="The module 'M2' does not export the package 'pkg.m2.impl' to the module 'M'">pkg.m2.impl</error>.*;
import <error descr="A named module cannot access packages of an unnamed one">pkg.m4.C4</error>;
import <error descr="The module 'M' does not have the module 'M5' in requirements">pkg.m5.C5</error>;
import pkg.m7.C7;
import static <error descr="The module 'M2' does not export the package 'pkg.m2.impl' to the module 'M'">pkg.m2.impl.C2Impl</error>.make;
class C { }
""".trimIndent(), true)
}
//<editor-fold desc="Helpers.">
private fun highlight(text: String, filter: Boolean = false) = highlight("module-info.java", text, filter)
@@ -20,10 +20,7 @@ import com.intellij.openapi.module.Module
import com.intellij.openapi.module.ModuleManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.roots.ContentEntry
import com.intellij.openapi.roots.LanguageLevelModuleExtension
import com.intellij.openapi.roots.ModifiableRootModel
import com.intellij.openapi.roots.ModuleRootModificationUtil
import com.intellij.openapi.roots.*
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.ex.temp.TempFileSystem
@@ -32,13 +29,17 @@ import com.intellij.testFramework.IdeaTestUtil
import com.intellij.testFramework.LightPlatformTestCase
/**
* Dependencies: 'main' -> 'm2', 'm3'
* Dependencies: 'main' -> 'm2', 'main' -> 'm4', 'main' -> 'm5', 'main' -> 'm6' => 'm7'
*/
object MultiModuleJava9ProjectDescriptor : DefaultLightProjectDescriptor() {
enum class ModuleDescriptor(internal val moduleName: String, internal val rootName: String) {
MAIN(TEST_MODULE_NAME, "/not_used/"),
M2("${TEST_MODULE_NAME}_m2", "src_m2"),
M3("${TEST_MODULE_NAME}_m3", "src_m3");
M3("${TEST_MODULE_NAME}_m3", "src_m3"),
M4("${TEST_MODULE_NAME}_m4", "src_m4"),
M5("${TEST_MODULE_NAME}_m5", "src_m5"),
M6("${TEST_MODULE_NAME}_m6", "src_m6"),
M7("${TEST_MODULE_NAME}_m7", "src_m7");
fun root(): VirtualFile =
if (this == MAIN) LightPlatformTestCase.getSourceRoot() else TempFileSystem.getInstance().findFileByPath("/$rootName")!!
@@ -50,9 +51,23 @@ object MultiModuleJava9ProjectDescriptor : DefaultLightProjectDescriptor() {
super.setUpProject(project, handler)
runWriteAction {
val main = ModuleManager.getInstance(project).findModuleByName(TEST_MODULE_NAME)!!
val m2 = makeModule(project, ModuleDescriptor.M2)
ModuleRootModificationUtil.addDependency(main, m2)
makeModule(project, ModuleDescriptor.M3)
val m4 = makeModule(project, ModuleDescriptor.M4)
ModuleRootModificationUtil.addDependency(main, m4)
val m5 = makeModule(project, ModuleDescriptor.M5)
ModuleRootModificationUtil.addDependency(main, m5)
val m6 = makeModule(project, ModuleDescriptor.M6)
ModuleRootModificationUtil.addDependency(main, m6)
val m7 = makeModule(project, ModuleDescriptor.M7)
ModuleRootModificationUtil.addDependency(m6, m7, DependencyScope.COMPILE, true)
}
}