mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[java-highlighting] move module-related warnings to JavaModuleDefinitionInspection
Part of IDEA-365344 Create a new Java error highlighter with minimal dependencies (PSI only) GitOrigin-RevId: 66179a41da5e02b759f07f9d6638b87711181348
This commit is contained in:
committed by
intellij-monorepo-bot
parent
b58f63db9e
commit
a0f777069b
@@ -4,6 +4,7 @@ package com.intellij.java.codeserver.core;
|
||||
import com.intellij.ide.highlighter.ArchiveFileType;
|
||||
import com.intellij.openapi.module.Module;
|
||||
import com.intellij.openapi.module.ModuleManager;
|
||||
import com.intellij.openapi.module.ModuleUtilCore;
|
||||
import com.intellij.openapi.project.DumbService;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.roots.ModuleRootManager;
|
||||
@@ -20,10 +21,8 @@ import com.intellij.psi.search.FilenameIndex;
|
||||
import com.intellij.psi.search.GlobalSearchScope;
|
||||
import com.intellij.psi.search.ProjectScope;
|
||||
import com.intellij.psi.search.searches.JavaModuleSearch;
|
||||
import com.intellij.psi.util.CachedValueProvider;
|
||||
import com.intellij.psi.util.CachedValuesManager;
|
||||
import com.intellij.psi.util.JavaMultiReleaseUtil;
|
||||
import com.intellij.psi.util.PsiUtilCore;
|
||||
import com.intellij.psi.util.*;
|
||||
import com.intellij.util.ArrayUtil;
|
||||
import com.intellij.util.containers.ContainerUtil;
|
||||
import com.intellij.util.containers.MultiMap;
|
||||
import com.intellij.util.graph.DFSTBuilder;
|
||||
@@ -522,6 +521,67 @@ public final class JavaPsiModuleUtil {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* State of package reference in module-info file
|
||||
*/
|
||||
public enum PackageReferenceState {
|
||||
/**
|
||||
* Valid reference to a non-empty package
|
||||
*/
|
||||
VALID,
|
||||
/**
|
||||
* No package is found
|
||||
*/
|
||||
PACKAGE_NOT_FOUND,
|
||||
/**
|
||||
* Package exists but contains no classes (for exports) or no files (for opens)
|
||||
*/
|
||||
PACKAGE_EMPTY
|
||||
}
|
||||
|
||||
/**
|
||||
* @param statement statement to check ('opens' or 'exports' statement)
|
||||
* @return state of the reference
|
||||
*/
|
||||
public static @NotNull PackageReferenceState checkPackageReference(@NotNull PsiPackageAccessibilityStatement statement) {
|
||||
PsiJavaCodeReferenceElement refElement = statement.getPackageReference();
|
||||
if (refElement != null) {
|
||||
PsiFile file = statement.getContainingFile();
|
||||
Module module = ModuleUtilCore.findModuleForFile(file);
|
||||
if (module != null) {
|
||||
PsiElement target = refElement.resolve();
|
||||
PsiDirectory[] directories = PsiDirectory.EMPTY_ARRAY;
|
||||
if (target instanceof PsiPackage psiPackage) {
|
||||
boolean inTests = ModuleRootManager.getInstance(module).getFileIndex().isInTestSourceContent(file.getVirtualFile());
|
||||
directories = psiPackage.getDirectories(module.getModuleScope(inTests));
|
||||
Module mainMultiReleaseModule = JavaMultiReleaseUtil.getMainMultiReleaseModule(module);
|
||||
if (mainMultiReleaseModule != null) {
|
||||
directories = ArrayUtil.mergeArrays(directories, psiPackage.getDirectories(mainMultiReleaseModule.getModuleScope(inTests)));
|
||||
}
|
||||
}
|
||||
String packageName = statement.getPackageName();
|
||||
if (directories.length == 0) {
|
||||
return PackageReferenceState.PACKAGE_NOT_FOUND;
|
||||
}
|
||||
boolean opens = statement.getRole() == PsiPackageAccessibilityStatement.Role.OPENS;
|
||||
if (packageName != null && isPackageEmpty(directories, packageName, opens)) {
|
||||
return PackageReferenceState.PACKAGE_EMPTY;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return PackageReferenceState.VALID;
|
||||
}
|
||||
|
||||
private static boolean isPackageEmpty(PsiDirectory @NotNull [] directories, @NotNull String packageName, boolean anyFile) {
|
||||
if (anyFile) {
|
||||
return !ContainerUtil.exists(directories, dir -> dir.getFiles().length > 0);
|
||||
}
|
||||
else {
|
||||
return PsiUtil.isPackageEmpty(directories, packageName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper service to support resolve in java-psi-impl
|
||||
*/
|
||||
|
||||
@@ -514,3 +514,5 @@ module.cyclic.dependence=Cyclic dependency: {0}
|
||||
module.duplicate.exports.target=Duplicate ''exports'' target: {0}
|
||||
module.duplicate.opens.target=Duplicate ''opens'' target: {0}
|
||||
module.duplicate.implementation=Duplicate implementation: {0}
|
||||
module.reference.package.not.found=Package not found: {0}
|
||||
module.reference.package.empty=Package is empty: {0}
|
||||
|
||||
+1
@@ -664,6 +664,7 @@ final class JavaErrorVisitor extends JavaElementVisitor {
|
||||
if (isApplicable(JavaFeature.MODULES)) {
|
||||
if (!hasErrorResults()) myModuleChecker.checkHostModuleStrength(statement);
|
||||
if (!hasErrorResults()) myModuleChecker.checkDuplicateModuleReferences(statement);
|
||||
if (!hasErrorResults()) myModuleChecker.checkPackageReference(statement);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+12
@@ -282,4 +282,16 @@ final class ModuleChecker {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void checkPackageReference(@NotNull PsiPackageAccessibilityStatement statement) {
|
||||
if (statement.getRole() == PsiPackageAccessibilityStatement.Role.OPENS) return;
|
||||
PsiJavaCodeReferenceElement refElement = statement.getPackageReference();
|
||||
if (refElement == null) return;
|
||||
JavaPsiModuleUtil.PackageReferenceState state = JavaPsiModuleUtil.checkPackageReference(statement);
|
||||
if (state == JavaPsiModuleUtil.PackageReferenceState.VALID) return;
|
||||
var kind = state == JavaPsiModuleUtil.PackageReferenceState.PACKAGE_NOT_FOUND
|
||||
? JavaErrorKinds.MODULE_REFERENCE_PACKAGE_NOT_FOUND
|
||||
: JavaErrorKinds.MODULE_REFERENCE_PACKAGE_EMPTY;
|
||||
myVisitor.report(kind.create(statement));
|
||||
}
|
||||
}
|
||||
|
||||
+8
@@ -1457,6 +1457,14 @@ public final class JavaErrorKinds {
|
||||
public static final Simple<PsiJavaModuleReferenceElement> MODULE_DUPLICATE_OPENS_TARGET =
|
||||
error(PsiJavaModuleReferenceElement.class, "module.duplicate.opens.target")
|
||||
.withRawDescription(ref -> message("module.duplicate.opens.target", ref.getReferenceText()));
|
||||
public static final Simple<PsiPackageAccessibilityStatement> MODULE_REFERENCE_PACKAGE_NOT_FOUND =
|
||||
error(PsiPackageAccessibilityStatement.class, "module.reference.package.not.found")
|
||||
.withAnchor(st -> st.getPackageReference())
|
||||
.withRawDescription(st -> message("module.reference.package.not.found", st.getPackageName()));
|
||||
public static final Simple<PsiPackageAccessibilityStatement> MODULE_REFERENCE_PACKAGE_EMPTY =
|
||||
error(PsiPackageAccessibilityStatement.class, "module.reference.package.empty")
|
||||
.withAnchor(st -> st.getPackageReference())
|
||||
.withRawDescription(st -> message("module.reference.package.empty", st.getPackageName()));
|
||||
|
||||
private static @NotNull <Psi extends PsiElement> Simple<Psi> error(
|
||||
@NotNull @PropertyKey(resourceBundle = JavaCompilationErrorBundle.BUNDLE) String key) {
|
||||
|
||||
@@ -218,6 +218,7 @@ inspection.implicit.subclass.make.class.extendable=Make class ''{0}'' {1,choice,
|
||||
inspection.infinite.loop.option=Ignore when placed in Thread.run
|
||||
inspection.java.module.naming.terminal.digits=Module name component ''{0}'' should avoid terminal digits
|
||||
inspection.java.module.naming=Java module name contradicts the convention
|
||||
inspection.java.module.definition=Java module definition problems
|
||||
inspection.local.can.be.final.display.name=Local variable or parameter can be 'final'
|
||||
inspection.local.can.be.final.option1=Report method parameters
|
||||
inspection.local.can.be.final.option2=Report catch parameters
|
||||
@@ -611,3 +612,5 @@ safe.varargs.not.suppress.potentially.unsafe.operations=@SafeVarargs do not supp
|
||||
safe.varargs.on.reifiable.type=@SafeVarargs is not applicable to reifiable types
|
||||
inspection.unreachable.catch.name=Unreachable catch section
|
||||
inspection.unreachable.catch.message=Unreachable section: {1, choice, 0#exception|2#exceptions} ''{0}'' {1, choice, 0#has|2#have} already been caught
|
||||
module.service.unused=Service interface provided but not exported or used
|
||||
module.ambiguous=Ambiguous module reference: {0}
|
||||
|
||||
@@ -89,6 +89,10 @@
|
||||
groupPath="Java" groupBundle="messages.InspectionsBundle" groupKey="group.names.naming.conventions"
|
||||
bundle="messages.JavaAnalysisBundle" key="inspection.java.module.naming"
|
||||
implementationClass="com.intellij.codeInspection.java19modules.JavaModuleNamingInspection"/>
|
||||
<localInspection language="JAVA" shortName="JavaModuleDefinition" enabledByDefault="true" level="WARNING"
|
||||
groupPath="Java" groupBundle="messages.InspectionsBundle" groupKey="group.names.declaration.redundancy"
|
||||
bundle="messages.JavaAnalysisBundle" key="inspection.java.module.definition"
|
||||
implementationClass="com.intellij.codeInspection.java19modules.JavaModuleDefinitionInspection"/>
|
||||
<localInspection language="JAVA" shortName="JavaRequiresAutoModule" enabledByDefault="true" level="WARNING"
|
||||
groupPathKey="group.path.names.java.language.level.specific.issues.and.migration.aids"
|
||||
groupBundle="messages.InspectionsBundle" groupKey="group.names.language.level.specific.issues.and.migration.aids9"
|
||||
|
||||
-22
@@ -321,28 +321,6 @@ public class HighlightVisitorImpl extends JavaElementVisitor implements Highligh
|
||||
if (!hasErrorResults()) model.checkSwitchLabelValues(myErrorSink);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitModule(@NotNull PsiJavaModule module) {
|
||||
super.visitModule(module);
|
||||
if (!hasErrorResults()) ModuleHighlightUtil.checkUnusedServices(module, myFile, myErrorSink);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitRequiresStatement(@NotNull PsiRequiresStatement statement) {
|
||||
super.visitRequiresStatement(statement);
|
||||
if (JavaFeature.MODULES.isSufficient(myLanguageLevel)) {
|
||||
if (!hasErrorResults()) add(ModuleHighlightUtil.checkModuleReference(statement));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitPackageAccessibilityStatement(@NotNull PsiPackageAccessibilityStatement statement) {
|
||||
super.visitPackageAccessibilityStatement(statement);
|
||||
if (JavaFeature.MODULES.isSufficient(myLanguageLevel)) {
|
||||
if (!hasErrorResults()) add(ModuleHighlightUtil.checkPackageReference(statement, myFile));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitModuleReferenceElement(@NotNull PsiJavaModuleReferenceElement refElement) {
|
||||
super.visitModuleReferenceElement(refElement);
|
||||
|
||||
+9
@@ -23,6 +23,8 @@ import com.intellij.lang.jvm.actions.JvmElementActionFactories;
|
||||
import com.intellij.lang.jvm.actions.MemberRequestsKt;
|
||||
import com.intellij.openapi.application.ApplicationManager;
|
||||
import com.intellij.openapi.components.Service;
|
||||
import com.intellij.openapi.module.Module;
|
||||
import com.intellij.openapi.module.ModuleUtilCore;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.pom.java.JavaFeature;
|
||||
import com.intellij.pom.java.LanguageLevel;
|
||||
@@ -168,6 +170,13 @@ final class JavaErrorFixProvider {
|
||||
PsiClassType type = JavaPsiFacade.getElementFactory(error.project()).createType(error.context().superClass());
|
||||
return myFactory.createExtendsListFix(error.context().subClass(), type, true);
|
||||
});
|
||||
JavaFixProvider<PsiPackageAccessibilityStatement, Void> createClassInPackage = error -> {
|
||||
String packageName = error.psi().getPackageName();
|
||||
Module module = ModuleUtilCore.findModuleForFile(error.psi().getContainingFile());
|
||||
return module == null ? null : myFactory.createCreateClassInPackageInModuleFix(module, packageName);
|
||||
};
|
||||
fix(MODULE_REFERENCE_PACKAGE_NOT_FOUND, createClassInPackage);
|
||||
fix(MODULE_REFERENCE_PACKAGE_EMPTY, createClassInPackage);
|
||||
}
|
||||
|
||||
private void createStatementFixes() {
|
||||
|
||||
-129
@@ -5,74 +5,19 @@ import com.intellij.codeInsight.JavaModuleSystemEx;
|
||||
import com.intellij.codeInsight.daemon.JavaErrorBundle;
|
||||
import com.intellij.codeInsight.daemon.impl.HighlightInfo;
|
||||
import com.intellij.codeInsight.daemon.impl.HighlightInfoType;
|
||||
import com.intellij.codeInsight.daemon.impl.quickfix.AddExportsDirectiveFix;
|
||||
import com.intellij.codeInsight.daemon.impl.quickfix.AddUsesDirectiveFix;
|
||||
import com.intellij.codeInsight.daemon.impl.quickfix.QuickFixAction;
|
||||
import com.intellij.codeInsight.intention.IntentionAction;
|
||||
import com.intellij.codeInsight.intention.QuickFixFactory;
|
||||
import com.intellij.modcommand.ModCommandAction;
|
||||
import com.intellij.openapi.module.Module;
|
||||
import com.intellij.openapi.module.ModuleUtilCore;
|
||||
import com.intellij.openapi.roots.ModuleRootManager;
|
||||
import com.intellij.openapi.util.text.StringUtil;
|
||||
import com.intellij.psi.*;
|
||||
import com.intellij.psi.PsiPackageAccessibilityStatement.Role;
|
||||
import com.intellij.psi.impl.IncompleteModelUtil;
|
||||
import com.intellij.psi.util.JavaMultiReleaseUtil;
|
||||
import com.intellij.psi.util.PsiUtil;
|
||||
import com.intellij.util.ArrayUtil;
|
||||
import com.intellij.util.containers.ContainerUtil;
|
||||
import com.intellij.util.containers.JBIterable;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static java.util.Objects.requireNonNullElse;
|
||||
|
||||
// generates HighlightInfoType.ERROR-like HighlightInfos for modularity-related (Jigsaw) problems
|
||||
final class ModuleHighlightUtil {
|
||||
|
||||
static void checkUnusedServices(@NotNull PsiJavaModule module, @NotNull PsiFile file, @NotNull Consumer<? super HighlightInfo.Builder> errorSink) {
|
||||
Module host = ModuleUtilCore.findModuleForFile(file);
|
||||
if (host == null) {
|
||||
return;
|
||||
}
|
||||
List<PsiProvidesStatement> provides = JBIterable.from(module.getProvides()).toList();
|
||||
if (!provides.isEmpty()) {
|
||||
Set<String> exports = JBIterable.from(module.getExports()).map(PsiPackageAccessibilityStatement::getPackageName).filter(Objects::nonNull).toSet();
|
||||
Set<String> uses = JBIterable.from(module.getUses()).map(st -> qName(st.getClassReference())).filter(Objects::nonNull).toSet();
|
||||
for (PsiProvidesStatement statement : provides) {
|
||||
PsiJavaCodeReferenceElement ref = statement.getInterfaceReference();
|
||||
if (ref != null) {
|
||||
PsiElement target = ref.resolve();
|
||||
if (target instanceof PsiClass && ModuleUtilCore.findModuleForFile(target.getContainingFile()) == host) {
|
||||
String className = qName(ref);
|
||||
String packageName = StringUtil.getPackageName(className);
|
||||
if (!exports.contains(packageName) && !uses.contains(className)) {
|
||||
String message = JavaErrorBundle.message("module.service.unused");
|
||||
HighlightInfo.Builder info =
|
||||
HighlightInfo.newHighlightInfo(HighlightInfoType.WARNING).range(
|
||||
requireNonNullElse(ref.getReferenceNameElement(), ref)).descriptionAndTooltip(message);
|
||||
ModCommandAction action1 = new AddExportsDirectiveFix(module, packageName, "");
|
||||
info.registerFix(action1, null, null, null, null);
|
||||
ModCommandAction action = new AddUsesDirectiveFix(module, className);
|
||||
info.registerFix(action, null, null, null, null);
|
||||
errorSink.accept(info);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String qName(PsiJavaCodeReferenceElement ref) {
|
||||
return ref != null ? ref.getQualifiedName() : null;
|
||||
}
|
||||
|
||||
static HighlightInfo.Builder checkModuleReference(@NotNull PsiImportModuleStatement statement) {
|
||||
PsiJavaModuleReferenceElement refElement = statement.getModuleReference();
|
||||
if (refElement == null) return null;
|
||||
@@ -93,29 +38,6 @@ final class ModuleHighlightUtil {
|
||||
return null;
|
||||
}
|
||||
|
||||
static HighlightInfo.Builder checkModuleReference(@NotNull PsiRequiresStatement statement) {
|
||||
PsiJavaModuleReferenceElement refElement = statement.getReferenceElement();
|
||||
if (refElement != null) {
|
||||
PsiJavaModuleReference ref = refElement.getReference();
|
||||
assert ref != null : refElement.getParent();
|
||||
PsiJavaModule target = ref.resolve();
|
||||
if (target == null) {
|
||||
PsiJavaModuleReference ref1 = refElement.getReference();
|
||||
assert ref1 != null : refElement.getParent();
|
||||
|
||||
ResolveResult[] results = ref1.multiResolve(true);
|
||||
if (results.length > 1) {
|
||||
// TODO: make as error or extract to inspection
|
||||
return HighlightInfo.newHighlightInfo(HighlightInfoType.WARNING)
|
||||
.range(refElement)
|
||||
.descriptionAndTooltip(JavaErrorBundle.message("module.ambiguous", refElement.getReferenceText()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static @NotNull HighlightInfo.Builder getUnresolvedJavaModuleReason(@NotNull PsiElement parent, @NotNull PsiJavaModuleReferenceElement refElement) {
|
||||
PsiJavaModuleReference ref = refElement.getReference();
|
||||
assert ref != null : refElement.getParent();
|
||||
@@ -145,55 +67,4 @@ final class ModuleHighlightUtil {
|
||||
.descriptionAndTooltip(JavaErrorBundle.message("module.ambiguous", refElement.getReferenceText()));
|
||||
}
|
||||
}
|
||||
|
||||
static HighlightInfo.Builder checkPackageReference(@NotNull PsiPackageAccessibilityStatement statement, @NotNull PsiFile file) {
|
||||
PsiJavaCodeReferenceElement refElement = statement.getPackageReference();
|
||||
if (refElement != null) {
|
||||
Module module = ModuleUtilCore.findModuleForFile(file);
|
||||
if (module != null) {
|
||||
PsiElement target = refElement.resolve();
|
||||
PsiDirectory[] directories = PsiDirectory.EMPTY_ARRAY;
|
||||
if (target instanceof PsiPackage psiPackage) {
|
||||
boolean inTests = ModuleRootManager.getInstance(module).getFileIndex().isInTestSourceContent(file.getVirtualFile());
|
||||
directories = psiPackage.getDirectories(module.getModuleScope(inTests));
|
||||
Module mainMultiReleaseModule = JavaMultiReleaseUtil.getMainMultiReleaseModule(module);
|
||||
if (mainMultiReleaseModule != null) {
|
||||
directories = ArrayUtil.mergeArrays(directories, psiPackage.getDirectories(mainMultiReleaseModule.getModuleScope(inTests)));
|
||||
}
|
||||
}
|
||||
String packageName = statement.getPackageName();
|
||||
boolean opens = statement.getRole() == Role.OPENS;
|
||||
HighlightInfoType type = opens ? HighlightInfoType.WARNING : HighlightInfoType.ERROR;
|
||||
if (directories.length == 0) {
|
||||
String message = JavaErrorBundle.message("package.not.found", packageName);
|
||||
HighlightInfo.Builder info = HighlightInfo.newHighlightInfo(type).range(refElement).descriptionAndTooltip(message);
|
||||
IntentionAction action = QuickFixFactory.getInstance().createCreateClassInPackageInModuleFix(module, packageName);
|
||||
if (action != null) {
|
||||
info.registerFix(action, null, null, null, null);
|
||||
}
|
||||
return info;
|
||||
}
|
||||
if (packageName != null && isPackageEmpty(directories, packageName, opens)) {
|
||||
String message = JavaErrorBundle.message("package.is.empty", packageName);
|
||||
HighlightInfo.Builder info = HighlightInfo.newHighlightInfo(type).range(refElement).descriptionAndTooltip(message);
|
||||
IntentionAction action = QuickFixFactory.getInstance().createCreateClassInPackageInModuleFix(module, packageName);
|
||||
if (action != null) {
|
||||
info.registerFix(action, null, null, null, null);
|
||||
}
|
||||
return info;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean isPackageEmpty(PsiDirectory @NotNull [] directories, @NotNull String packageName, boolean anyFile) {
|
||||
if (anyFile) {
|
||||
return !ContainerUtil.exists(directories, dir -> dir.getFiles().length > 0);
|
||||
}
|
||||
else {
|
||||
return PsiUtil.isPackageEmpty(directories, packageName);
|
||||
}
|
||||
}
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.codeInspection.java19modules;
|
||||
|
||||
import com.intellij.codeInsight.daemon.impl.quickfix.AddExportsDirectiveFix;
|
||||
import com.intellij.codeInsight.daemon.impl.quickfix.AddUsesDirectiveFix;
|
||||
import com.intellij.codeInsight.intention.IntentionAction;
|
||||
import com.intellij.codeInsight.intention.QuickFixFactory;
|
||||
import com.intellij.codeInspection.AbstractBaseJavaLocalInspectionTool;
|
||||
import com.intellij.codeInspection.LocalQuickFixBackedByIntentionAction;
|
||||
import com.intellij.codeInspection.ProblemsHolder;
|
||||
import com.intellij.java.analysis.JavaAnalysisBundle;
|
||||
import com.intellij.java.codeserver.core.JavaPsiModuleUtil;
|
||||
import com.intellij.java.codeserver.highlighting.errors.JavaErrorKinds;
|
||||
import com.intellij.openapi.module.Module;
|
||||
import com.intellij.openapi.module.ModuleUtilCore;
|
||||
import com.intellij.openapi.util.text.StringUtil;
|
||||
import com.intellij.psi.*;
|
||||
import com.intellij.psi.util.PsiUtil;
|
||||
import com.intellij.util.containers.JBIterable;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import static java.util.Objects.requireNonNullElse;
|
||||
|
||||
public final class JavaModuleDefinitionInspection extends AbstractBaseJavaLocalInspectionTool {
|
||||
@Override
|
||||
public @NotNull PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) {
|
||||
return !PsiUtil.isModuleFile(holder.getFile()) ? PsiElementVisitor.EMPTY_VISITOR : new JavaElementVisitor() {
|
||||
@Override
|
||||
public void visitModule(@NotNull PsiJavaModule module) {
|
||||
checkUnusedServices(module);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitRequiresStatement(@NotNull PsiRequiresStatement statement) {
|
||||
checkModuleReference(statement);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitPackageAccessibilityStatement(@NotNull PsiPackageAccessibilityStatement statement) {
|
||||
if (statement.getRole() != PsiPackageAccessibilityStatement.Role.OPENS) return;
|
||||
PsiJavaCodeReferenceElement refElement = statement.getPackageReference();
|
||||
if (refElement == null) return;
|
||||
JavaPsiModuleUtil.PackageReferenceState state = JavaPsiModuleUtil.checkPackageReference(statement);
|
||||
if (state == JavaPsiModuleUtil.PackageReferenceState.VALID) return;
|
||||
|
||||
var kind = state == JavaPsiModuleUtil.PackageReferenceState.PACKAGE_NOT_FOUND
|
||||
? JavaErrorKinds.MODULE_REFERENCE_PACKAGE_NOT_FOUND
|
||||
: JavaErrorKinds.MODULE_REFERENCE_PACKAGE_EMPTY;
|
||||
String message = kind.description(statement, null).toString();
|
||||
String packageName = statement.getPackageName();
|
||||
Module module = ModuleUtilCore.findModuleForFile(holder.getFile());
|
||||
IntentionAction action =
|
||||
module == null ? null : QuickFixFactory.getInstance().createCreateClassInPackageInModuleFix(module, packageName);
|
||||
holder.problem(refElement, message)
|
||||
.maybeFix(action == null ? null : new LocalQuickFixBackedByIntentionAction(action))
|
||||
.register();
|
||||
}
|
||||
|
||||
void checkModuleReference(@NotNull PsiRequiresStatement statement) {
|
||||
PsiJavaModuleReferenceElement refElement = statement.getReferenceElement();
|
||||
if (refElement != null) {
|
||||
PsiJavaModuleReference ref = refElement.getReference();
|
||||
if (ref != null) {
|
||||
ResolveResult[] results = ref.multiResolve(true);
|
||||
if (results.length > 1 && ref.resolve() == null) {
|
||||
holder.registerProblem(refElement, JavaAnalysisBundle.message("module.ambiguous", refElement.getReferenceText()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void checkUnusedServices(@NotNull PsiJavaModule module) {
|
||||
Module host = ModuleUtilCore.findModuleForFile(holder.getFile());
|
||||
if (host == null) {
|
||||
return;
|
||||
}
|
||||
List<PsiProvidesStatement> provides = JBIterable.from(module.getProvides()).toList();
|
||||
if (!provides.isEmpty()) {
|
||||
Set<String>
|
||||
exports =
|
||||
JBIterable.from(module.getExports()).map(PsiPackageAccessibilityStatement::getPackageName).filter(Objects::nonNull).toSet();
|
||||
Set<String> uses = JBIterable.from(module.getUses()).map(st -> qName(st.getClassReference())).filter(Objects::nonNull).toSet();
|
||||
for (PsiProvidesStatement statement : provides) {
|
||||
|
||||
PsiJavaCodeReferenceElement ref = statement.getInterfaceReference();
|
||||
if (ref != null) {
|
||||
PsiElement target = ref.resolve();
|
||||
if (target instanceof PsiClass && ModuleUtilCore.findModuleForFile(target.getContainingFile()) == host) {
|
||||
String className = qName(ref);
|
||||
String packageName = StringUtil.getPackageName(className);
|
||||
if (!exports.contains(packageName) && !uses.contains(className)) {
|
||||
holder.problem(requireNonNullElse(ref.getReferenceNameElement(), ref),
|
||||
JavaAnalysisBundle.message("module.service.unused"))
|
||||
.fix(new AddExportsDirectiveFix(module, packageName, ""))
|
||||
.fix(new AddUsesDirectiveFix(module, className))
|
||||
.register();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static String qName(PsiJavaCodeReferenceElement ref) {
|
||||
return ref != null ? ref.getQualifiedName() : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<html>
|
||||
<body>
|
||||
Reports miscellaneous problems with the module-info.java file. For example, it reports a service which is provided but not exported or used.
|
||||
<p><b>Example:</b></p>
|
||||
<pre><code>
|
||||
module myModule {
|
||||
// Service is provided but its containing package is not exported
|
||||
provides com.example.MyService with com.example.MyServiceImpl;
|
||||
}
|
||||
</code></pre>
|
||||
<!-- tooltip end -->
|
||||
</body>
|
||||
</html>
|
||||
@@ -193,7 +193,6 @@ module.ambiguous=Ambiguous module reference: {0}
|
||||
module.not.on.path=Module is not in dependencies: {0}
|
||||
package.not.found=Package not found: {0}
|
||||
package.is.empty=Package is empty: {0}
|
||||
module.service.unused=Service interface provided but not exported or used
|
||||
module.access.to.unnamed=Package ''{0}'' is declared in the unnamed module, but module ''{1}'' does not read it
|
||||
module.access.from.named=Package ''{0}'' is declared in module ''{1}'', which does not export it to module ''{2}''
|
||||
module.access.from.unnamed=Package ''{0}'' is declared in module ''{1}'', which does not export it to the unnamed module
|
||||
|
||||
+6
@@ -3,11 +3,17 @@ package com.intellij.codeInsight.daemon.impl.quickfix
|
||||
|
||||
import com.intellij.codeInsight.daemon.impl.analysis.JavaModuleGraphUtil
|
||||
import com.intellij.codeInsight.intention.impl.CreateClassInPackageInModuleFix
|
||||
import com.intellij.codeInspection.java19modules.JavaModuleDefinitionInspection
|
||||
import com.intellij.psi.PsiJavaFile
|
||||
import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase
|
||||
|
||||
class CreateClassInPackageInModuleTest : LightJavaCodeInsightFixtureTestCase() {
|
||||
|
||||
override fun setUp() {
|
||||
super.setUp()
|
||||
myFixture.enableInspections(JavaModuleDefinitionInspection())
|
||||
}
|
||||
|
||||
fun testExportsMissingDir(): Unit = doTestMissingDir("exports")
|
||||
fun testOpensMissingDir(): Unit = doTestMissingDir("opens")
|
||||
|
||||
|
||||
+8
-2
@@ -7,6 +7,7 @@ import com.intellij.codeInsight.intention.IntentionActionDelegate
|
||||
import com.intellij.codeInspection.IllegalDependencyOnInternalPackageInspection
|
||||
import com.intellij.codeInspection.deprecation.DeprecationInspection
|
||||
import com.intellij.codeInspection.deprecation.MarkedForRemovalInspection
|
||||
import com.intellij.codeInspection.java19modules.JavaModuleDefinitionInspection
|
||||
import com.intellij.java.testFramework.fixtures.LightJava9ModulesCodeInsightFixtureTestCase
|
||||
import com.intellij.java.testFramework.fixtures.MultiModuleJava9ProjectDescriptor.ModuleDescriptor
|
||||
import com.intellij.java.testFramework.fixtures.MultiModuleJava9ProjectDescriptor.ModuleDescriptor.*
|
||||
@@ -14,6 +15,7 @@ import com.intellij.java.workspace.entities.JavaModuleSettingsEntity
|
||||
import com.intellij.java.workspace.entities.javaSettings
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.application.runWriteActionAndWait
|
||||
import com.intellij.openapi.diagnostic.ReportingClassSubstitutor
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.module.ModuleManager
|
||||
import com.intellij.openapi.roots.ModuleRootManager
|
||||
@@ -50,6 +52,8 @@ import java.util.jar.JarFile
|
||||
class ModuleHighlightingTest : LightJava9ModulesCodeInsightFixtureTestCase() {
|
||||
override fun setUp() {
|
||||
super.setUp()
|
||||
|
||||
myFixture.enableInspections(JavaModuleDefinitionInspection())
|
||||
|
||||
addFile("module-info.java", "module M2 { }", M2)
|
||||
addFile("module-info.java", "module M3 { }", M3)
|
||||
@@ -289,7 +293,7 @@ class ModuleHighlightingTest : LightJava9ModulesCodeInsightFixtureTestCase() {
|
||||
}
|
||||
|
||||
fun testWeakModule() {
|
||||
highlight("""open module M { <error descr="'opens' is not allowed in an open module">opens pkg.missing;</error> }""")
|
||||
highlight("""open module M { <error descr="'opens' is not allowed in an open module">opens <warning descr="Package not found: pkg.missing">pkg.missing</warning>;</error> }""")
|
||||
}
|
||||
|
||||
fun testUses() {
|
||||
@@ -327,6 +331,7 @@ class ModuleHighlightingTest : LightJava9ModulesCodeInsightFixtureTestCase() {
|
||||
import pkg.main.Impl6;
|
||||
module M {
|
||||
requires M2;
|
||||
exports pkg.main;
|
||||
provides pkg.main.C with pkg.main.<error descr="Cannot resolve symbol 'NoImpl'">NoImpl</error>;
|
||||
provides pkg.main.C with pkg.main.<error descr="'pkg.main.Impl1' is not public in 'pkg.main'. Cannot be accessed from outside package">Impl1</error>;
|
||||
provides pkg.main.C with pkg.main.<error descr="The service implementation type must be a subtype of the service interface type, or have a public static no-args 'provider' method">Impl2</error>;
|
||||
@@ -973,9 +978,10 @@ class ModuleHighlightingTest : LightJava9ModulesCodeInsightFixtureTestCase() {
|
||||
myFixture.configureFromExistingVirtualFile(addFile(path, text))
|
||||
val availableIntentions = myFixture.availableIntentions
|
||||
val available = availableIntentions
|
||||
.map { (it.asModCommandAction() ?: IntentionActionDelegate.unwrap(it))::class.java }
|
||||
.map { ReportingClassSubstitutor.getClassToReport(it) }
|
||||
.filter { it.name.startsWith("com.intellij.codeInsight.") &&
|
||||
!(it.name.startsWith("com.intellij.codeInsight.intention.impl.") && it.name.endsWith("Action"))
|
||||
&& !it.name.endsWith("DisableHighlightingIntentionAction")
|
||||
&& !it.name.endsWith("DeclarativeHintsTogglingIntention")}
|
||||
.map { it.simpleName }
|
||||
assertThat(available).describedAs(availableIntentions.toString()).containsExactlyInAnyOrder(*fixes)
|
||||
|
||||
Reference in New Issue
Block a user