[java-inspections] UnreachableCatchInspection extracted from highlighter

Part of IDEA-365344 Create a new Java error highlighter with minimal dependencies (PSI only)

GitOrigin-RevId: b2e389aa89d75c63969a3884a495b3771d2712b2
This commit is contained in:
Tagir Valeev
2025-01-27 16:27:01 +01:00
committed by intellij-monorepo-bot
parent 4b94cd67e6
commit 9dbf2ace73
11 changed files with 168 additions and 114 deletions

View File

@@ -114,7 +114,7 @@ final class JavaErrorVisitor extends JavaElementVisitor {
public void visitTryStatement(@NotNull PsiTryStatement statement) { public void visitTryStatement(@NotNull PsiTryStatement statement) {
super.visitTryStatement(statement); super.visitTryStatement(statement);
if (!hasErrorResults()) { if (!hasErrorResults()) {
UnhandledExceptions thrownTypes = collectUnhandledExceptions(statement); UnhandledExceptions thrownTypes = UnhandledExceptions.fromTryStatement(statement);
if (thrownTypes.hasUnresolvedCalls()) return; if (thrownTypes.hasUnresolvedCalls()) return;
for (PsiParameter parameter : statement.getCatchBlockParameters()) { for (PsiParameter parameter : statement.getCatchBlockParameters()) {
myStatementChecker.checkExceptionAlreadyCaught(parameter); myStatementChecker.checkExceptionAlreadyCaught(parameter);
@@ -841,22 +841,6 @@ final class JavaErrorVisitor extends JavaElementVisitor {
} }
} }
private static @NotNull UnhandledExceptions collectUnhandledExceptions(@NotNull PsiTryStatement statement) {
UnhandledExceptions thrownTypes = UnhandledExceptions.EMPTY;
PsiCodeBlock tryBlock = statement.getTryBlock();
if (tryBlock != null) {
thrownTypes = thrownTypes.merge(UnhandledExceptions.collect(tryBlock));
}
PsiResourceList resources = statement.getResourceList();
if (resources != null) {
thrownTypes = thrownTypes.merge(UnhandledExceptions.collect(resources));
}
return thrownTypes;
}
void checkFeature(@NotNull PsiElement element, @NotNull JavaFeature feature) { void checkFeature(@NotNull PsiElement element, @NotNull JavaFeature feature) {
if (!feature.isSufficient(myLanguageLevel)) { if (!feature.isSufficient(myLanguageLevel)) {
report(JavaErrorKinds.UNSUPPORTED_FEATURE.create(element, feature)); report(JavaErrorKinds.UNSUPPORTED_FEATURE.create(element, feature));

View File

@@ -613,4 +613,6 @@ intention.name.do.not.report.conditions.with.possible.side.effect=Do not report
dfa.find.cause.or.another=or {0} dfa.find.cause.or.another=or {0}
dfa.find.cause.and.another=and {0} dfa.find.cause.and.another=and {0}
safe.varargs.not.suppress.potentially.unsafe.operations=@SafeVarargs do not suppress potentially unsafe operations safe.varargs.not.suppress.potentially.unsafe.operations=@SafeVarargs do not suppress potentially unsafe operations
safe.varargs.on.reifiable.type=@SafeVarargs is not applicable for reifiable types safe.varargs.on.reifiable.type=@SafeVarargs is not applicable for 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

View File

@@ -2,10 +2,8 @@
package com.intellij.codeInsight.daemon.impl.analysis; package com.intellij.codeInsight.daemon.impl.analysis;
import com.intellij.codeInsight.ContainerProvider; import com.intellij.codeInsight.ContainerProvider;
import com.intellij.codeInsight.ExceptionUtil;
import com.intellij.codeInsight.JavaModuleSystemEx; import com.intellij.codeInsight.JavaModuleSystemEx;
import com.intellij.codeInsight.JavaModuleSystemEx.ErrorWithFixes; import com.intellij.codeInsight.JavaModuleSystemEx.ErrorWithFixes;
import com.intellij.codeInsight.UnhandledExceptions;
import com.intellij.codeInsight.daemon.HighlightDisplayKey; import com.intellij.codeInsight.daemon.HighlightDisplayKey;
import com.intellij.codeInsight.daemon.JavaErrorBundle; import com.intellij.codeInsight.daemon.JavaErrorBundle;
import com.intellij.codeInsight.daemon.impl.HighlightInfo; import com.intellij.codeInsight.daemon.impl.HighlightInfo;
@@ -49,7 +47,6 @@ import com.intellij.psi.tree.IElementType;
import com.intellij.psi.util.*; import com.intellij.psi.util.*;
import com.intellij.ui.ColorUtil; import com.intellij.ui.ColorUtil;
import com.intellij.ui.NewUI; import com.intellij.ui.NewUI;
import com.intellij.util.ArrayUtilRt;
import com.intellij.util.JavaPsiConstructorUtil; import com.intellij.util.JavaPsiConstructorUtil;
import com.intellij.util.ObjectUtils; import com.intellij.util.ObjectUtils;
import com.intellij.util.containers.ContainerUtil; import com.intellij.util.containers.ContainerUtil;
@@ -637,81 +634,6 @@ public final class HighlightUtil {
return null; return null;
} }
static @NotNull UnhandledExceptions collectUnhandledExceptions(@NotNull PsiTryStatement statement) {
UnhandledExceptions thrownTypes = UnhandledExceptions.EMPTY;
PsiCodeBlock tryBlock = statement.getTryBlock();
if (tryBlock != null) {
thrownTypes = thrownTypes.merge(UnhandledExceptions.collect(tryBlock));
}
PsiResourceList resources = statement.getResourceList();
if (resources != null) {
thrownTypes = thrownTypes.merge(UnhandledExceptions.collect(resources));
}
return thrownTypes;
}
static void checkWithImprovedCatchAnalysis(@NotNull PsiParameter parameter,
@NotNull Collection<? extends PsiClassType> thrownInTryStatement,
@NotNull PsiFile containingFile, @NotNull Consumer<? super HighlightInfo.Builder> errorSink) {
PsiElement scope = parameter.getDeclarationScope();
if (!(scope instanceof PsiCatchSection catchSection)) return;
PsiCatchSection[] allCatchSections = catchSection.getTryStatement().getCatchSections();
int idx = ArrayUtilRt.find(allCatchSections, catchSection);
if (idx <= 0) return;
Collection<PsiClassType> thrownTypes = new HashSet<>(thrownInTryStatement);
PsiManager manager = containingFile.getManager();
GlobalSearchScope parameterResolveScope = parameter.getResolveScope();
thrownTypes.add(PsiType.getJavaLangError(manager, parameterResolveScope));
thrownTypes.add(PsiType.getJavaLangRuntimeException(manager, parameterResolveScope));
List<PsiTypeElement> parameterTypeElements = PsiUtil.getParameterTypeElements(parameter);
boolean isMultiCatch = parameterTypeElements.size() > 1;
for (PsiTypeElement catchTypeElement : parameterTypeElements) {
PsiType catchType = catchTypeElement.getType();
if (ExceptionUtil.isGeneralExceptionType(catchType)) continue;
// collect exceptions caught by this type
List<PsiClassType> caught = new ArrayList<>();
for (PsiClassType t : thrownTypes) {
if (catchType.isAssignableFrom(t) || t.isAssignableFrom(catchType)) {
caught.add(t);
}
}
if (caught.isEmpty()) continue;
Collection<PsiClassType> caughtCopy = new HashSet<>(caught);
// exclude all caught by previous catch sections
for (int i = 0; i < idx; i++) {
PsiParameter prevCatchParameter = allCatchSections[i].getParameter();
if (prevCatchParameter == null) continue;
for (PsiTypeElement prevCatchTypeElement : PsiUtil.getParameterTypeElements(prevCatchParameter)) {
PsiType prevCatchType = prevCatchTypeElement.getType();
caught.removeIf(prevCatchType::isAssignableFrom);
if (caught.isEmpty()) break;
}
}
// check & warn
if (caught.isEmpty()) {
String message = JavaErrorBundle.message("exception.already.caught.warn", formatTypes(caughtCopy), caughtCopy.size());
HighlightInfo.Builder builder =
HighlightInfo.newHighlightInfo(HighlightInfoType.WARNING).range(catchSection).descriptionAndTooltip(message);
IntentionAction action = isMultiCatch ?
getFixFactory().createDeleteMultiCatchFix(catchTypeElement) :
getFixFactory().createDeleteCatchFix(parameter);
builder.registerFix(action, null, null, null, null);
errorSink.accept(builder);
}
}
}
static HighlightInfo.Builder checkNotAStatement(@NotNull PsiStatement statement) { static HighlightInfo.Builder checkNotAStatement(@NotNull PsiStatement statement) {
if (PsiUtil.isStatement(statement)) { if (PsiUtil.isStatement(statement)) {
return null; return null;

View File

@@ -1,7 +1,6 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. // 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.codeInsight.daemon.impl.analysis; package com.intellij.codeInsight.daemon.impl.analysis;
import com.intellij.codeInsight.UnhandledExceptions;
import com.intellij.codeInsight.daemon.JavaErrorBundle; import com.intellij.codeInsight.daemon.JavaErrorBundle;
import com.intellij.codeInsight.daemon.impl.DefaultHighlightUtil; import com.intellij.codeInsight.daemon.impl.DefaultHighlightUtil;
import com.intellij.codeInsight.daemon.impl.HighlightInfo; import com.intellij.codeInsight.daemon.impl.HighlightInfo;
@@ -1170,20 +1169,6 @@ public class HighlightVisitorImpl extends JavaElementVisitor implements Highligh
} }
} }
@Override
public void visitTryStatement(@NotNull PsiTryStatement statement) {
super.visitTryStatement(statement);
if (!hasErrorResults()) {
UnhandledExceptions thrownTypes = HighlightUtil.collectUnhandledExceptions(statement);
if (thrownTypes.hasUnresolvedCalls()) return;
for (PsiParameter parameter : statement.getCatchBlockParameters()) {
if (!hasErrorResults()) {
HighlightUtil.checkWithImprovedCatchAnalysis(parameter, thrownTypes.exceptions(), myFile, myErrorSink);
}
}
}
}
@Override @Override
public void visitResourceExpression(@NotNull PsiResourceExpression resource) { public void visitResourceExpression(@NotNull PsiResourceExpression resource) {
super.visitResourceExpression(resource); super.visitResourceExpression(resource);

View File

@@ -1972,6 +1972,11 @@
level="INFORMATION" level="INFORMATION"
implementationClass="com.intellij.codeInspection.streamToLoop.StreamToLoopInspection" implementationClass="com.intellij.codeInspection.streamToLoop.StreamToLoopInspection"
key="inspection.stream.to.loop.display.name" bundle="messages.JavaBundle"/> key="inspection.stream.to.loop.display.name" bundle="messages.JavaBundle"/>
<localInspection groupPath="Java" language="JAVA" shortName="UnreachableCatch"
groupKey="group.names.verbose.or.redundant.code.constructs" groupBundle="messages.InspectionsBundle"
enabledByDefault="true" level="WARNING"
implementationClass="com.intellij.codeInspection.deadCode.UnreachableCatchInspection"
key="inspection.unreachable.catch.name" bundle="messages.JavaAnalysisBundle"/>
<localInspection groupPath="Java" language="JAVA" shortName="OptionalToIf" <localInspection groupPath="Java" language="JAVA" shortName="OptionalToIf"
groupKey="group.names.code.style.issues" groupBundle="messages.InspectionsBundle" enabledByDefault="true" groupKey="group.names.code.style.issues" groupBundle="messages.InspectionsBundle" enabledByDefault="true"
level="INFORMATION" level="INFORMATION"

View File

@@ -0,0 +1,95 @@
// 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.codeInspection.deadCode;
import com.intellij.codeInsight.ExceptionUtil;
import com.intellij.codeInsight.UnhandledExceptions;
import com.intellij.codeInsight.daemon.impl.analysis.JavaHighlightUtil;
import com.intellij.codeInsight.daemon.impl.quickfix.DeleteCatchFix;
import com.intellij.codeInsight.daemon.impl.quickfix.DeleteMultiCatchFix;
import com.intellij.codeInspection.AbstractBaseJavaLocalInspectionTool;
import com.intellij.codeInspection.ProblemsHolder;
import com.intellij.ide.nls.NlsMessages;
import com.intellij.java.analysis.JavaAnalysisBundle;
import com.intellij.java.codeserver.highlighting.JavaErrorCollector;
import com.intellij.modcommand.ModCommandAction;
import com.intellij.psi.*;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.util.PsiUtil;
import com.intellij.util.ArrayUtilRt;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
public final class UnreachableCatchInspection extends AbstractBaseJavaLocalInspectionTool {
@Override
public @NotNull PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) {
return new JavaElementVisitor() {
@Override
public void visitTryStatement(@NotNull PsiTryStatement statement) {
UnhandledExceptions thrownTypes = UnhandledExceptions.fromTryStatement(statement);
if (thrownTypes.hasUnresolvedCalls()) return;
for (PsiParameter parameter : statement.getCatchBlockParameters()) {
checkWithImprovedCatchAnalysis(statement, parameter, thrownTypes.exceptions());
}
}
void checkWithImprovedCatchAnalysis(@NotNull PsiTryStatement statement, @NotNull PsiParameter parameter,
@NotNull Collection<? extends PsiClassType> thrownInTryStatement) {
PsiElement scope = parameter.getDeclarationScope();
if (!(scope instanceof PsiCatchSection catchSection)) return;
PsiCatchSection[] allCatchSections = catchSection.getTryStatement().getCatchSections();
int idx = ArrayUtilRt.find(allCatchSections, catchSection);
if (idx <= 0) return;
Collection<PsiClassType> thrownTypes = new HashSet<>(thrownInTryStatement);
PsiManager manager = holder.getFile().getManager();
GlobalSearchScope parameterResolveScope = parameter.getResolveScope();
thrownTypes.add(PsiType.getJavaLangError(manager, parameterResolveScope));
thrownTypes.add(PsiType.getJavaLangRuntimeException(manager, parameterResolveScope));
List<PsiTypeElement> parameterTypeElements = PsiUtil.getParameterTypeElements(parameter);
boolean isMultiCatch = parameterTypeElements.size() > 1;
for (PsiTypeElement catchTypeElement : parameterTypeElements) {
PsiType catchType = catchTypeElement.getType();
if (ExceptionUtil.isGeneralExceptionType(catchType)) continue;
// collect exceptions caught by this type
List<PsiClassType> caught = new ArrayList<>();
for (PsiClassType t : thrownTypes) {
if (catchType.isAssignableFrom(t) || t.isAssignableFrom(catchType)) {
caught.add(t);
}
}
if (caught.isEmpty()) continue;
Collection<PsiClassType> caughtCopy = new HashSet<>(caught);
// exclude all caught by previous catch sections
for (int i = 0; i < idx; i++) {
PsiParameter prevCatchParameter = allCatchSections[i].getParameter();
if (prevCatchParameter == null) continue;
for (PsiTypeElement prevCatchTypeElement : PsiUtil.getParameterTypeElements(prevCatchParameter)) {
PsiType prevCatchType = prevCatchTypeElement.getType();
caught.removeIf(prevCatchType::isAssignableFrom);
if (caught.isEmpty()) break;
}
}
// check & warn
if (caught.isEmpty()) {
if (JavaErrorCollector.findSingleError(statement) != null) return;
String types = caughtCopy.stream().map(JavaHighlightUtil::formatType).collect(NlsMessages.joiningAnd());
String message = JavaAnalysisBundle.message("inspection.unreachable.catch.message", types, caughtCopy.size());
ModCommandAction action = isMultiCatch ?
new DeleteMultiCatchFix(catchTypeElement) :
new DeleteCatchFix(parameter);
holder.problem(catchSection.getFirstChild(), message).fix(action).register();
}
}
}
};
}
}

View File

@@ -0,0 +1,39 @@
<html>
<body>
Reports catch sections which are never executed, despite allowed by Java language specification.
<p>
While normally, unreachable catch sections are disallowed by Java compiler and reported as compilation errors,
in some rare cases the analysis mandated by Java language specification is not complete.
This inspection provides enhanced analysis and reports some unreachable catch sections which are not reported by the compiler.
Such sections are redundant and could be safely removed.
</p>
<p><b>Example:</b></p>
<pre><code>
void method() {
try {
throw new FileNotFoundException();
}
catch (FileNotFoundException e) {
}
catch (IOException e) {
// this catch is allowed by specification
// but never executed
}
}
</code></pre>
<p>The quick-fix is provided, which removes the redundant catch section:</p>
<pre><code>
void method() {
try {
throw new FileNotFoundException();
}
catch (FileNotFoundException e) {
}
}
</code></pre>
<!-- tooltip end -->
<p><small>New in 2025.1</small></p>
</body>
</html>

View File

@@ -146,7 +146,6 @@ repeated.modifier=Repeated modifier ''{0}''
modifier.not.allowed=Modifier ''{0}'' not allowed here modifier.not.allowed=Modifier ''{0}'' not allowed here
modifier.not.allowed.on.local.classes=Modifier ''{0}'' not allowed on local classes modifier.not.allowed.on.local.classes=Modifier ''{0}'' not allowed on local classes
modifier.not.allowed.on.classes.without.sealed.super=Modifier 'non-sealed' not allowed on classes that do not have a sealed superclass modifier.not.allowed.on.classes.without.sealed.super=Modifier 'non-sealed' not allowed on classes that do not have a sealed superclass
exception.already.caught.warn=Unreachable section: {1, choice, 0#exception|2#exceptions} ''{0}'' {1, choice, 0#has|2#have} already been caught
not.a.statement=Not a statement not.a.statement=Not a statement
invalid.statement=Invalid statement invalid.statement=Invalid statement
incompatible.types=Incompatible types. Found: ''{1}'', required: ''{0}'' incompatible.types=Incompatible types. Found: ''{1}'', required: ''{0}''

View File

@@ -215,4 +215,25 @@ public class UnhandledExceptions {
}; };
return collect(element, topElement, callFilter); return collect(element, topElement, callFilter);
} }
/**
* @param statement try-statement to collect unhandled exceptions from its body
* @return exceptions unhandled in try statement body and resource declarations; they are still could be handled by declared
* catch sections on the same try statement. The primary purpose of this method is to check whether catch sections are correct.
*/
public static @NotNull UnhandledExceptions fromTryStatement(@NotNull PsiTryStatement statement) {
UnhandledExceptions thrownTypes = EMPTY;
PsiCodeBlock tryBlock = statement.getTryBlock();
if (tryBlock != null) {
thrownTypes = thrownTypes.merge(collect(tryBlock));
}
PsiResourceList resources = statement.getResourceList();
if (resources != null) {
thrownTypes = thrownTypes.merge(collect(resources));
}
return thrownTypes;
}
} }

View File

@@ -26,7 +26,7 @@ class C {
void m0() { void m0() {
try { throw new FileNotFoundException(); } try { throw new FileNotFoundException(); }
catch (FileNotFoundException e) { } catch (FileNotFoundException e) { }
<warning descr="Unreachable section: exception 'java.io.FileNotFoundException' has already been caught">catch (IOException e) { }</warning> <warning descr="Unreachable section: exception 'java.io.FileNotFoundException' has already been caught">catch</warning> (IOException e) { }
} }
void m1() { void m1() {

View File

@@ -7,6 +7,7 @@ import com.intellij.codeInspection.InspectionProfileEntry;
import com.intellij.codeInspection.SafeVarargsHasNoEffectInspection; import com.intellij.codeInspection.SafeVarargsHasNoEffectInspection;
import com.intellij.codeInspection.SafeVarargsOnNonReifiableTypeInspection; import com.intellij.codeInspection.SafeVarargsOnNonReifiableTypeInspection;
import com.intellij.codeInspection.compiler.JavacQuirksInspection; import com.intellij.codeInspection.compiler.JavacQuirksInspection;
import com.intellij.codeInspection.deadCode.UnreachableCatchInspection;
import com.intellij.codeInspection.deadCode.UnusedDeclarationInspection; import com.intellij.codeInspection.deadCode.UnusedDeclarationInspection;
import com.intellij.codeInspection.deadCode.UnusedDeclarationInspectionBase; import com.intellij.codeInspection.deadCode.UnusedDeclarationInspectionBase;
import com.intellij.codeInspection.defUse.DefUseInspection; import com.intellij.codeInspection.defUse.DefUseInspection;
@@ -38,7 +39,8 @@ public class LightAdvHighlightingJdk7Test extends LightDaemonAnalyzerTestCase {
protected void setUp() throws Exception { protected void setUp() throws Exception {
super.setUp(); super.setUp();
enableInspectionTools(new UnusedDeclarationInspection(), new UncheckedWarningLocalInspection(), new JavacQuirksInspection(), new RedundantCastInspection(), enableInspectionTools(new UnusedDeclarationInspection(), new UncheckedWarningLocalInspection(), new JavacQuirksInspection(), new RedundantCastInspection(),
new SafeVarargsHasNoEffectInspection(), new SafeVarargsOnNonReifiableTypeInspection()); new SafeVarargsHasNoEffectInspection(), new SafeVarargsOnNonReifiableTypeInspection(),
new UnreachableCatchInspection());
setLanguageLevel(LanguageLevel.JDK_1_7); setLanguageLevel(LanguageLevel.JDK_1_7);
IdeaTestUtil.setTestVersion(JavaSdkVersion.JDK_1_7, getModule(), getTestRootDisposable()); IdeaTestUtil.setTestVersion(JavaSdkVersion.JDK_1_7, getModule(), getTestRootDisposable());
} }