diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/CommonDataflow.java b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/CommonDataflow.java index 57e2c997e720..c32a7454d49e 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/CommonDataflow.java +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/CommonDataflow.java @@ -1,13 +1,18 @@ // Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.codeInspection.dataFlow; +import com.intellij.codeInspection.dataFlow.interpreter.ReachabilityCountingInterpreter; import com.intellij.codeInspection.dataFlow.interpreter.RunnerResult; +import com.intellij.codeInspection.dataFlow.interpreter.StandardDataFlowInterpreter; import com.intellij.codeInspection.dataFlow.java.JavaDfaListener; import com.intellij.codeInspection.dataFlow.java.anchor.*; import com.intellij.codeInspection.dataFlow.jvm.descriptors.AssertionDisabledDescriptor; import com.intellij.codeInspection.dataFlow.jvm.problems.ContractFailureProblem; import com.intellij.codeInspection.dataFlow.lang.DfaAnchor; +import com.intellij.codeInspection.dataFlow.lang.DfaListener; import com.intellij.codeInspection.dataFlow.lang.UnsatisfiedConditionProblem; +import com.intellij.codeInspection.dataFlow.lang.ir.ControlFlow; +import com.intellij.codeInspection.dataFlow.lang.ir.DataFlowIRProvider; import com.intellij.codeInspection.dataFlow.memory.DfaMemoryState; import com.intellij.codeInspection.dataFlow.rangeSet.LongRangeSet; import com.intellij.codeInspection.dataFlow.types.DfIntegralType; @@ -16,6 +21,7 @@ import com.intellij.codeInspection.dataFlow.types.DfTypes; import com.intellij.codeInspection.dataFlow.value.DfaTypeValue; import com.intellij.codeInspection.dataFlow.value.DfaValue; import com.intellij.codeInspection.dataFlow.value.DfaVariableValue; +import com.intellij.openapi.util.TextRange; import com.intellij.psi.*; import com.intellij.psi.util.*; import com.intellij.util.JavaPsiConstructorUtil; @@ -80,10 +86,11 @@ public final class CommonDataflow { */ public static final class DataflowResult { private final @NotNull Map myData = new HashMap<>(); + private final @NotNull List myUnreachable = new ArrayList<>(); private @NotNull Map myDataAssertionsDisabled = myData; - private final RunnerResult myResult; + private final @NotNull RunnerResult myResult; - public DataflowResult(RunnerResult result) { + public DataflowResult(@NotNull RunnerResult result) { myResult = result; } @@ -91,6 +98,7 @@ public final class CommonDataflow { DataflowResult copy() { DataflowResult copy = new DataflowResult(myResult); myData.forEach((anchor, point) -> copy.myData.put(anchor, new DataflowPoint(point))); + copy.myUnreachable.addAll(myUnreachable); return copy; } @@ -137,23 +145,9 @@ public final class CommonDataflow { } /** - * Returns true if given expression was visited by dataflow. Note that dataflow usually tracks deparenthesized expressions only, - * so you should deparenthesize it in advance if necessary. - * - * @param expression expression to check, not parenthesized - * @return true if given expression was visited by dataflow. - * If false is returned, it's possible that the expression exists in unreachable branch or this expression is not tracked due to - * the dataflow implementation details. + * @param anchor anchor to check + * @return true if a given anchor appeared during the analysis */ - @Contract("null -> false") - public boolean expressionWasAnalyzed(@Nullable PsiExpression expression) { - if (expression == null) return false; - if (expression instanceof PsiParenthesizedExpression) { - throw new IllegalArgumentException("Should not pass parenthesized expression"); - } - return myData.containsKey(new JavaExpressionAnchor(expression)); - } - public boolean anchorWasAnalyzed(@NotNull JavaDfaAnchor anchor) { return myData.containsKey(anchor); } @@ -171,6 +165,10 @@ public final class CommonDataflow { DataflowPoint point = myData.get(new JavaExpressionAnchor(call)); return point != null && !point.myMayFailByContract; } + + public @NotNull Collection getUnreachableRanges() { + return myResult != RunnerResult.OK ? Collections.emptyList() : myUnreachable; + } /** * Returns a set of expression values if known. If non-empty set is returned, then given expression @@ -230,27 +228,43 @@ public final class CommonDataflow { @NotNull private static DataflowResult runDFA(@Nullable PsiElement block) { if (block == null) return new DataflowResult(RunnerResult.NOT_APPLICABLE); - StandardDataFlowRunner runner = new StandardDataFlowRunner(block.getProject(), ThreeState.UNSURE); - var interceptor = new CommonDataflowListener(); - RunnerResult result = runner.analyzeMethodRecursively(block, interceptor); + var listener = new CommonDataflowListener(); + var runner = new StandardDataFlowRunner(block.getProject(), ThreeState.UNSURE) { + @Override + protected @NotNull StandardDataFlowInterpreter createInterpreter(@NotNull DfaListener listener, @NotNull ControlFlow flow) { + return new ReachabilityCountingInterpreter(flow, listener, false, 0); + } + + @Override + protected void afterInterpretation(@NotNull ControlFlow flow, + @NotNull StandardDataFlowInterpreter interpreter, + @NotNull RunnerResult result) { + if (result == RunnerResult.OK) { + Set unreachable = ((ReachabilityCountingInterpreter)interpreter).getUnreachable(); + listener.myResult.myUnreachable.addAll(DataFlowIRProvider.computeUnreachableSegments(block, unreachable)); + } + super.afterInterpretation(flow, interpreter, result); + } + }; + RunnerResult result = runner.analyzeMethodRecursively(block, listener); if (result != RunnerResult.OK) return new DataflowResult(result); - if (!(block instanceof PsiClass)) return interceptor.myResult; - DataflowResult dfr = interceptor.myResult.copy(); - List states = interceptor.myEndOfInitializerStates; - for (PsiMethod method : ((PsiClass)block).getConstructors()) { + if (!(block instanceof PsiClass psiClass)) return listener.myResult; + DataflowResult dfr = listener.myResult.copy(); + List states = listener.myEndOfInitializerStates; + for (PsiMethod method : psiClass.getConstructors()) { List initialStates; PsiCodeBlock body = method.getBody(); if (body == null) continue; PsiMethodCallExpression call = JavaPsiConstructorUtil.findThisOrSuperCallInConstructor(method); - if (JavaPsiConstructorUtil.isChainedConstructorCall(call) || (call == null && hasImplicitImpureSuperCall((PsiClass)block, method))) { + if (JavaPsiConstructorUtil.isChainedConstructorCall(call) || (call == null && hasImplicitImpureSuperCall(psiClass, method))) { initialStates = Collections.singletonList(runner.createMemoryState()); } else { initialStates = ContainerUtil.map(states, DfaMemoryState::createCopy); } - if(runner.analyzeBlockRecursively(body, initialStates, interceptor) == RunnerResult.OK) { - dfr = interceptor.myResult.copy(); + if (runner.analyzeBlockRecursively(body, initialStates, listener) == RunnerResult.OK) { + dfr = listener.myResult.copy(); } else { - interceptor.myResult = dfr; + listener.myResult = dfr; } } return dfr; @@ -262,7 +276,7 @@ public final class CommonDataflow { * @return the dataflow result or null if dataflow cannot be launched for this context (e.g. we are inside too complex method) */ @Nullable - public static DataflowResult getDataflowResult(PsiExpression context) { + public static DataflowResult getDataflowResult(@NotNull PsiElement context) { PsiElement body = DfaUtil.getDataflowContext(context); if (body == null) return null; ConcurrentHashMap fileMap = diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/DfaUtil.java b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/DfaUtil.java index 8dec8347ef39..2857418c1f7b 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/DfaUtil.java +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/DfaUtil.java @@ -245,15 +245,14 @@ public final class DfaUtil { /** * Returns a surrounding PSI element which should be analyzed via DFA * (e.g. passed to {@link StandardDataFlowRunner#analyzeMethodRecursively(PsiElement, DfaListener)}) to cover - * given expression. + * given element. * - * @param expression expression to cover + * @param anchor element to cover * @return a dataflow context; null if no applicable context found. */ - static @Nullable PsiElement getDataflowContext(PsiExpression expression) { - PsiElement element = expression; + static @Nullable PsiElement getDataflowContext(@NotNull PsiElement anchor) { + PsiElement element = anchor; while (true) { - element = element.getParent(); if (element == null || element instanceof PsiAnnotation) return null; if (element instanceof PsiMethod method && !method.isConstructor()) { PsiClass containingClass = method.getContainingClass(); @@ -263,6 +262,7 @@ public final class DfaUtil { } } if (element instanceof PsiClass psiClass && !PsiUtil.isLocalOrAnonymousClass(psiClass)) return psiClass; + element = element.getParent(); } } @@ -274,6 +274,7 @@ public final class DfaUtil { * @return evaluated value or null if cannot be evaluated */ public static @Nullable Boolean evaluateCondition(@Nullable PsiExpression condition) { + if (condition == null) return null; CommonDataflow.DataflowResult result = CommonDataflow.getDataflowResult(condition); if (result == null) return null; return tryCast(ContainerUtil.getOnlyItem(result.getExpressionValues(condition)), Boolean.class); diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/StandardDataFlowRunner.java b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/StandardDataFlowRunner.java index f0a8e6c06fd6..d118d8747345 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/StandardDataFlowRunner.java +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/StandardDataFlowRunner.java @@ -171,7 +171,13 @@ public class StandardDataFlowRunner { @NotNull ControlFlow flow, @NotNull List startingStates) { myInterpreter = createInterpreter(listener, flow); - return myInterpreter.interpret(startingStates); + RunnerResult result = myInterpreter.interpret(startingStates); + afterInterpretation(flow, myInterpreter, result); + return result; + } + + protected void afterInterpretation(@NotNull ControlFlow flow, @NotNull StandardDataFlowInterpreter interpreter, @NotNull RunnerResult result) { + } @NotNull diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/java/ControlFlowAnalyzer.java b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/java/ControlFlowAnalyzer.java index 619f1f6fc85b..dbf0238fbdf0 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/java/ControlFlowAnalyzer.java +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/java/ControlFlowAnalyzer.java @@ -2365,18 +2365,22 @@ public class ControlFlowAnalyzer extends JavaElementVisitor { startElement(castExpression); PsiExpression operand = castExpression.getOperand(); + PsiType operandType = operand == null ? null : operand.getType(); if (operand != null) { operand.accept(this); - generateBoxingUnboxingInstructionFor(castExpression, operand.getType(), castExpression.getType(), true); + generateBoxingUnboxingInstructionFor(castExpression, operandType, castExpression.getType(), true); } else { addInstruction(new PushValueInstruction(DfTypes.typedObject(castExpression.getType(), Nullability.UNKNOWN))); } final PsiTypeElement typeElement = castExpression.getCastType(); - if (typeElement != null && operand != null && operand.getType() != null && !(typeElement.getType() instanceof PsiPrimitiveType)) { + if (typeElement != null && operandType != null && !(typeElement.getType() instanceof PsiPrimitiveType) && + !(operandType instanceof PsiLambdaExpressionType) && !(operandType instanceof PsiMethodReferenceType)) { DfaControlTransferValue transfer = createTransfer("java.lang.ClassCastException"); addInstruction(new TypeCastInstruction(castExpression, operand, typeElement.getType(), transfer)); + } else { + addInstruction(new ResultOfInstruction(new JavaExpressionAnchor(castExpression))); } finishElement(castExpression); } diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/java/JavaDataFlowIRProvider.java b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/java/JavaDataFlowIRProvider.java index 3cf21a2356b1..7d820a58d967 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/java/JavaDataFlowIRProvider.java +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/java/JavaDataFlowIRProvider.java @@ -55,8 +55,13 @@ public final class JavaDataFlowIRProvider implements DataFlowIRProvider { } } if (unreachable instanceof PsiCodeBlock) { - if (unreachable.getParent() instanceof PsiCatchSection && unreachable.getParent().getParent() instanceof PsiTryStatement && - !allUnreachable.contains(unreachable.getParent().getParent())) { + if (unreachable.getParent() instanceof PsiCatchSection catchSection && + catchSection.getParent() instanceof PsiTryStatement tryStatement && + !allUnreachable.contains(tryStatement)) { + return unreachable.getTextRange(); + } + if (unreachable.getParent() instanceof PsiClassInitializer initializer && + !allUnreachable.contains(initializer)) { return unreachable.getTextRange(); } } @@ -66,8 +71,8 @@ public final class JavaDataFlowIRProvider implements DataFlowIRProvider { if (unreachable instanceof PsiSwitchLabelStatement) return null; if (allUnreachable.contains(statementParent)) return null; if (parent instanceof PsiStatement) { - if (parent instanceof PsiIfStatement && ((PsiIfStatement)parent).getElseBranch() == unreachable) { - PsiKeyword elseKeyword = ((PsiIfStatement)parent).getElseElement(); + if (parent instanceof PsiIfStatement ifStatement && ifStatement.getElseBranch() == unreachable) { + PsiKeyword elseKeyword = ifStatement.getElseElement(); if (elseKeyword != null) { return TextRange.create(elseKeyword.getTextRange().getStartOffset(), unreachable.getTextRange().getEndOffset()); } @@ -75,10 +80,10 @@ public final class JavaDataFlowIRProvider implements DataFlowIRProvider { return statement.getTextRange(); } if (parent instanceof PsiCodeBlock) { - if (statement instanceof PsiSwitchLabeledRuleStatement) { - PsiSwitchBlock block = ((PsiSwitchLabeledRuleStatement)statement).getEnclosingSwitchBlock(); + if (statement instanceof PsiSwitchLabeledRuleStatement ruleStatement) { + PsiSwitchBlock block = ruleStatement.getEnclosingSwitchBlock(); if (!allUnreachable.contains(block)) { - PsiStatement body = ((PsiSwitchLabeledRuleStatement)statement).getBody(); + PsiStatement body = ruleStatement.getBody(); if (body != null) { return body.getTextRange(); } @@ -86,8 +91,8 @@ public final class JavaDataFlowIRProvider implements DataFlowIRProvider { return null; } PsiStatement prevStatement = ObjectUtils.tryCast(PsiTreeUtil.skipWhitespacesAndCommentsBackward(statement), PsiStatement.class); - if (prevStatement instanceof PsiSwitchLabelStatement) { - PsiSwitchBlock block = ((PsiSwitchLabelStatement)prevStatement).getEnclosingSwitchBlock(); + if (prevStatement instanceof PsiSwitchLabelStatement labelStatement) { + PsiSwitchBlock block = labelStatement.getEnclosingSwitchBlock(); if (block != null && !allUnreachable.contains(block)) { PsiElement last = ((PsiCodeBlock)statementParent).getRBrace(); PsiSwitchLabelStatement nextLabel = PsiTreeUtil.getNextSiblingOfType(statement, PsiSwitchLabelStatement.class); @@ -102,8 +107,8 @@ public final class JavaDataFlowIRProvider implements DataFlowIRProvider { return null; } if (allUnreachable.contains(prevStatement)) return null; - PsiElement lastStatement = PsiTreeUtil.skipWhitespacesAndCommentsBackward(((PsiCodeBlock)statementParent).getRBrace()); - if (lastStatement != null && prevStatement != null) { + PsiElement lastStatement = getLastStatement(statement); + if (prevStatement != null) { if (prevStatement instanceof PsiLoopStatement && PsiTreeUtil.isAncestor(prevStatement, startAnchor, false)) { return null; } @@ -113,4 +118,14 @@ public final class JavaDataFlowIRProvider implements DataFlowIRProvider { } return null; } + + private static @NotNull PsiElement getLastStatement(@NotNull PsiStatement statement) { + PsiElement lastStatement = statement; + while (true) { + PsiElement nextStatement = PsiTreeUtil.skipWhitespacesAndCommentsForward(lastStatement); + if (!(nextStatement instanceof PsiStatement) || nextStatement instanceof PsiSwitchLabelStatement) break; + lastStatement = nextStatement; + } + return lastStatement; + } } diff --git a/java/java-impl-inspections/src/com/intellij/codeInspection/dataFlow/ConstantValueInspection.java b/java/java-impl-inspections/src/com/intellij/codeInspection/dataFlow/ConstantValueInspection.java index ffa0e76404b0..dc944bcdee20 100644 --- a/java/java-impl-inspections/src/com/intellij/codeInspection/dataFlow/ConstantValueInspection.java +++ b/java/java-impl-inspections/src/com/intellij/codeInspection/dataFlow/ConstantValueInspection.java @@ -402,7 +402,7 @@ public final class ConstantValueInspection extends AbstractBaseJavaLocalInspecti || parent instanceof PsiSwitchStatement || parent instanceof PsiSynchronizedStatement; } - private static boolean isFlagCheck(PsiElement element) { + static boolean isFlagCheck(PsiElement element) { PsiElement scope = PsiTreeUtil.getParentOfType(element, PsiStatement.class, PsiVariable.class); PsiExpression topExpression = scope instanceof PsiIfStatement ifStatement ? ifStatement.getCondition() : scope instanceof PsiVariable variable ? variable.getInitializer() : diff --git a/java/java-impl-inspections/src/com/intellij/codeInspection/dataFlow/UnreachableCodeInspection.java b/java/java-impl-inspections/src/com/intellij/codeInspection/dataFlow/UnreachableCodeInspection.java new file mode 100644 index 000000000000..05513076c236 --- /dev/null +++ b/java/java-impl-inspections/src/com/intellij/codeInspection/dataFlow/UnreachableCodeInspection.java @@ -0,0 +1,196 @@ +// 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.dataFlow; + +import com.intellij.codeInspection.AbstractBaseJavaLocalInspectionTool; +import com.intellij.codeInspection.ProblemHighlightType; +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.codeInspection.SuppressionUtil; +import com.intellij.codeInspection.options.OptPane; +import com.intellij.java.JavaBundle; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.text.HtmlChunk; +import com.intellij.psi.*; +import com.intellij.psi.util.InheritanceUtil; +import com.intellij.psi.util.PsiTreeUtil; +import com.siyeh.ig.psiutils.ControlFlowUtils; +import one.util.streamex.StreamEx; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; + +import static com.intellij.codeInspection.options.OptPane.checkbox; +import static com.intellij.codeInspection.options.OptPane.pane; + +public final class UnreachableCodeInspection extends AbstractBaseJavaLocalInspectionTool { + public boolean ignoreTrivialReturns = true; + public boolean respectConstantValueSuppression = true; + + @Override + public @NotNull OptPane getOptionsPane() { + return pane( + checkbox("ignoreTrivialReturns", JavaBundle.message("inspection.data.flow.unreachable.code.option.ignore.trivial.name")) + .description(HtmlChunk.raw(JavaBundle.message("inspection.data.flow.unreachable.code.option.ignore.trivial.description"))), + checkbox("respectConstantValueSuppression", JavaBundle.message("inspection.data.flow.unreachable.code.option.respect.suppression.name")) + .description(HtmlChunk.raw(JavaBundle.message("inspection.data.flow.unreachable.code.option.respect.suppression.description"))) + ); + } + + @Override + public @NotNull PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { + return new JavaElementVisitor() { + @Override + public void visitClass(@NotNull PsiClass aClass) { + processElement(aClass); + } + + @Override + public void visitMethod(@NotNull PsiMethod method) { + if (!method.isConstructor()) { + processElement(method); + } + } + + private void processElement(@NotNull PsiElement element) { + CommonDataflow.DataflowResult result = CommonDataflow.getDataflowResult(element); + if (result == null) return; + + PsiFile file = holder.getFile(); + for (TextRange range : result.getUnreachableRanges()) { + PsiElement psiElement = file.findElementAt(range.getStartOffset()); + while (psiElement != null && !psiElement.getTextRange().contains(range)) { + psiElement = psiElement.getParent(); + } + if (psiElement != null) { + if (psiElement.getTextRange().equals(range) && shouldSuppress(psiElement)) continue; + if (element != StreamEx.iterate(psiElement, Objects::nonNull, PsiElement::getParent) + .findFirst(e -> e instanceof PsiClass || + e instanceof PsiMethod method && !method.isConstructor()).orElse(null)) { + // Nested method or class: will be reported for the corresponding anchor + continue; + } + holder.registerProblem(psiElement, JavaBundle.message("inspection.data.flow.unreachable.code.display.name"), + ProblemHighlightType.LIKE_UNUSED_SYMBOL, + range.shiftLeft(psiElement.getTextRange().getStartOffset())); + } + } + } + + private boolean shouldSuppress(PsiElement psiElement) { + if (psiElement instanceof PsiExpressionStatement statement && isTrivialExpression(statement.getExpression()) && + psiElement.getParent() instanceof PsiSwitchLabeledRuleStatement rule && rule.isDefaultCase()) { + // Default case in switch might be required by compiler + return true; + } + Condition condition = findCondition(psiElement); + if (condition != null) { + if (ConstantValueInspection.isFlagCheck(condition.expression)) { + return true; + } + if (respectConstantValueSuppression && + (SuppressionUtil.isSuppressed(condition.expression, "ConstantValue") || + SuppressionUtil.isSuppressed(condition.expression, "ConstantConditions"))) { + return true; + } + } + if (psiElement instanceof PsiCodeBlock block) { + if (block.getParent() instanceof PsiCatchSection catchSection) { + PsiType type = catchSection.getCatchType(); + if (mayCover(type, "java.lang.LinkageError") || + mayCover(type, "java.lang.VirtualMachineError")) { + return true; + } + } + else { + if (block.getStatementCount() == 1) { + psiElement = block.getStatements()[0]; + } + } + } + return psiElement instanceof PsiThrowStatement || ignoreTrivialReturns && isTrivialReturn(psiElement); + } + + record Condition(boolean value, @NotNull PsiExpression expression) {} + + private static @Nullable Condition findCondition(PsiElement deadCode) { + if (deadCode instanceof PsiExpression) { + if (deadCode.getParent() instanceof PsiConditionalExpression ternary) { + if (ternary.getElseExpression() == deadCode) { + return new Condition(false, ternary.getCondition()); + } + else if (ternary.getThenExpression() == deadCode) { + return new Condition(true, ternary.getCondition()); + } + } + } + if (deadCode instanceof PsiCodeBlock block && block.getParent() instanceof PsiBlockStatement blockStatement) { + deadCode = blockStatement; + } + if (deadCode.getParent() instanceof PsiIfStatement ifStatement) { + PsiExpression condition = ifStatement.getCondition(); + if (condition != null) { + if (ifStatement.getThenBranch() == deadCode) { + return new Condition(true, condition); + } + if (ifStatement.getElseBranch() == deadCode) { + return new Condition(false, condition); + } + } + } + if (deadCode instanceof PsiStatement statement) { + if (PsiTreeUtil.skipWhitespacesAndCommentsBackward(statement) instanceof PsiIfStatement prevIf) { + if (prevIf.getElseBranch() == null && !ControlFlowUtils.statementMayCompleteNormally(prevIf.getThenBranch())) { + PsiExpression condition = prevIf.getCondition(); + if (condition != null) { + return new Condition(false, condition); + } + } + } + } + return null; + } + + private static boolean mayCover(PsiType type, String exceptionFqn) { + if (type == null) return false; + if (type instanceof PsiDisjunctionType disjunctionType) { + for (PsiType disjunction : disjunctionType.getDisjunctions()) { + if (mayCover(disjunction, exceptionFqn)) return true; + } + return false; + } + if (type instanceof PsiClassType classType) { + PsiClass psiClass = classType.resolve(); + if (psiClass == null) return false; + if (InheritanceUtil.isInheritor(psiClass, exceptionFqn)) return true; + PsiClass exceptionClass = JavaPsiFacade.getInstance(psiClass.getProject()).findClass(exceptionFqn, psiClass.getResolveScope()); + if (exceptionClass != null && exceptionClass.isInheritor(psiClass, true)) return true; + } + return false; + } + + private static boolean isTrivialReturn(@NotNull PsiElement element) { + if (element instanceof PsiBreakStatement || element instanceof PsiContinueStatement) return true; + if (element instanceof PsiYieldStatement yieldStatement) { + return isTrivialExpression(yieldStatement.getExpression()); + } + if (element instanceof PsiReturnStatement returnStatement) { + return isTrivialExpression(returnStatement.getReturnValue()); + } + return false; + } + + private static boolean isTrivialExpression(PsiExpression expression) { + if (expression == null) return true; + if (expression instanceof PsiThisExpression thisExpression && thisExpression.getQualifier() == null) return true; + if (expression instanceof PsiLiteralExpression literal) { + Object value = literal.getValue(); + if (value instanceof Boolean || "".equals(value) || value instanceof Number number && number.doubleValue() == 0.0) { + return true; + } + if (literal.textMatches(PsiKeyword.NULL)) return true; + } + return false; + } + }; + } +} diff --git a/java/java-impl/src/META-INF/JavaPlugin.xml b/java/java-impl/src/META-INF/JavaPlugin.xml index 231fa77f4996..14274f4aa78d 100644 --- a/java/java-impl/src/META-INF/JavaPlugin.xml +++ b/java/java-impl/src/META-INF/JavaPlugin.xml @@ -1873,6 +1873,9 @@ + diff --git a/java/java-impl/src/inspectionDescriptions/UnreachableCode.html b/java/java-impl/src/inspectionDescriptions/UnreachableCode.html new file mode 100644 index 000000000000..0c94b04ae10f --- /dev/null +++ b/java/java-impl/src/inspectionDescriptions/UnreachableCode.html @@ -0,0 +1,17 @@ + + +Reports the code which is never reached according to data flow analysis. +It can be the result of previous always-true or always-false condition, unreachable loop body or +catch section. Usually (though not always) unreachable code is a consequence of a previous warning, +so check inspection warnings form "Nullability and data flow problems", "Constant values", or +"Redundant operation on empty container" to better understand the cause. +

Example:

+

+  void finishApplication() {
+    System.exit(0);
+    System.out.println("Application is terminated"); // Unreachable code
+  }
+
+

New in 2024.1

+ + diff --git a/java/java-tests/testData/inspection/dataFlow/unreachableCode/CatchLinkageError.java b/java/java-tests/testData/inspection/dataFlow/unreachableCode/CatchLinkageError.java new file mode 100644 index 000000000000..716b3e928d06 --- /dev/null +++ b/java/java-tests/testData/inspection/dataFlow/unreachableCode/CatchLinkageError.java @@ -0,0 +1,68 @@ +class Test { + static class Cls2 { + static int value; + } + + void test() { + try { + int x = Cls2.value; + } + catch (NoClassDefFoundError ex) { + System.out.println("No class"); + } + } + + void test2() { + try { + int x = Cls2.value; + } + catch (StackOverflowError ex) { + System.out.println("SO"); + } + } + + void test3() { + try { + int x = Cls2.value; + } + catch (AssertionError ex) { + System.out.println("AE"); + } + } + + void test4() { + try { + int x = Cls2.value; + } + catch (LinkageError ex) { + System.out.println("No class"); + } + } + + void test5() { + try { + int x = Cls2.value; + } + catch (Error ex) { + System.out.println("No class"); + } + } + + void test6() { + try { + int x = Cls2.value; + } + catch (Throwable ex) { + System.out.println("No class"); + } + } + + void test7() { + try { + int x = Cls2.value; + } + catch (Exception ex) { + System.out.println("No class"); + } + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/dataFlow/unreachableCode/Constructor.java b/java/java-tests/testData/inspection/dataFlow/unreachableCode/Constructor.java new file mode 100644 index 000000000000..3c50769c86df --- /dev/null +++ b/java/java-tests/testData/inspection/dataFlow/unreachableCode/Constructor.java @@ -0,0 +1,8 @@ +class Test { + Test() { + if (true) { + throw new AssertionError(); + } + System.out.println("Hello"); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/dataFlow/unreachableCode/Initializer.java b/java/java-tests/testData/inspection/dataFlow/unreachableCode/Initializer.java new file mode 100644 index 000000000000..39617bb9fabd --- /dev/null +++ b/java/java-tests/testData/inspection/dataFlow/unreachableCode/Initializer.java @@ -0,0 +1,12 @@ +class Test { + static { + if (true) { + throw new AssertionError(); + } + System.out.println("Unreachable"); + } + + static { + System.out.println("You cannot see me"); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/dataFlow/unreachableCode/LambdaCast.java b/java/java-tests/testData/inspection/dataFlow/unreachableCode/LambdaCast.java new file mode 100644 index 000000000000..723b200132b8 --- /dev/null +++ b/java/java-tests/testData/inspection/dataFlow/unreachableCode/LambdaCast.java @@ -0,0 +1,18 @@ + +class Test { + public interface Visitor { + boolean visit(Type component); + } + + public static native void iterate(final Visitor visitor); + + public static void analyze() { + try { + iterate((Visitor)(String component) -> true); + System.out.println("hello"); + } + catch (Exception e) { + System.out.println(e); + } + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/dataFlow/unreachableCode/SimpleCatch.java b/java/java-tests/testData/inspection/dataFlow/unreachableCode/SimpleCatch.java new file mode 100644 index 000000000000..fd3a7397da08 --- /dev/null +++ b/java/java-tests/testData/inspection/dataFlow/unreachableCode/SimpleCatch.java @@ -0,0 +1,11 @@ +class Test { + void test(int x) { + int y; + try { + y = x; + } + catch (NumberFormatException nfe) { + throw new IllegalArgumentException(); + } + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/dataFlow/unreachableCode/SwitchDefault.java b/java/java-tests/testData/inspection/dataFlow/unreachableCode/SwitchDefault.java new file mode 100644 index 000000000000..53417387b281 --- /dev/null +++ b/java/java-tests/testData/inspection/dataFlow/unreachableCode/SwitchDefault.java @@ -0,0 +1,11 @@ +class Test { + void test(int x) { + if (x < 0 || x > 2) return; + int result = switch(x) { + case 0 -> 1; + case 1 -> 2; + case 2 -> 3; + default -> 0; // unreachable but required by compiler + }; + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/dataFlow/unreachableCode/SwitchFail.java b/java/java-tests/testData/inspection/dataFlow/unreachableCode/SwitchFail.java new file mode 100644 index 000000000000..6e9ed6732ce5 --- /dev/null +++ b/java/java-tests/testData/inspection/dataFlow/unreachableCode/SwitchFail.java @@ -0,0 +1,18 @@ +class Test { + void test(int x) { + switch(x) { + default: + System.exit(0); + System.out.println("oops"); + return; + case 0: + System.out.println(0); + case 1: + System.out.println(1); + case 2: + System.out.println(2); + case 3: + System.out.println(3); + } + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/dataFlow/unreachableCode/SwitchFallthrough.java b/java/java-tests/testData/inspection/dataFlow/unreachableCode/SwitchFallthrough.java new file mode 100644 index 000000000000..fe44ff89fead --- /dev/null +++ b/java/java-tests/testData/inspection/dataFlow/unreachableCode/SwitchFallthrough.java @@ -0,0 +1,17 @@ +class Test { + void test(int x) { + if (x < 0 || x > 2) return; + switch(x) { + default: + System.out.println("Impossible"); + case 0: + System.out.println(0); + case 1: + System.out.println(1); + case 2: + System.out.println(2); + case 3: + System.out.println(3); + } + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/dataFlow/unreachableCode/SystemExit.java b/java/java-tests/testData/inspection/dataFlow/unreachableCode/SystemExit.java new file mode 100644 index 000000000000..0ea3c93734eb --- /dev/null +++ b/java/java-tests/testData/inspection/dataFlow/unreachableCode/SystemExit.java @@ -0,0 +1,6 @@ +class Test { + void test() { + System.exit(0); + System.out.println("unreachable"); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/dataFlow/unreachableCode/Throw.java b/java/java-tests/testData/inspection/dataFlow/unreachableCode/Throw.java new file mode 100644 index 000000000000..62f89c556042 --- /dev/null +++ b/java/java-tests/testData/inspection/dataFlow/unreachableCode/Throw.java @@ -0,0 +1,10 @@ +class Test { + int test(int x) { + if (x < 0) { + System.exit(0); + throw new AssertionError("unreachable"); + } + System.out.println(x); + return 5; + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/dataFlow/unreachableCode/TrivialReturn.java b/java/java-tests/testData/inspection/dataFlow/unreachableCode/TrivialReturn.java new file mode 100644 index 000000000000..abdf0e39a911 --- /dev/null +++ b/java/java-tests/testData/inspection/dataFlow/unreachableCode/TrivialReturn.java @@ -0,0 +1,14 @@ +class Test { + int test(int x) { + if (x < 0) { + return 2; + } + if (x > 0) { + return 1; + } + if (x == 0) { + return 3; + } + return 0; + } +} \ No newline at end of file diff --git a/java/java-tests/testSrc/com/intellij/java/codeInspection/dataFlow/UnreachableCodeInspectionTest.java b/java/java-tests/testSrc/com/intellij/java/codeInspection/dataFlow/UnreachableCodeInspectionTest.java new file mode 100644 index 000000000000..15390325f7ff --- /dev/null +++ b/java/java-tests/testSrc/com/intellij/java/codeInspection/dataFlow/UnreachableCodeInspectionTest.java @@ -0,0 +1,48 @@ +// 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.java.codeInspection.dataFlow; + +import com.intellij.JavaTestUtil; +import com.intellij.codeInspection.dataFlow.UnreachableCodeInspection; +import com.intellij.testFramework.LightProjectDescriptor; +import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase; +import org.jetbrains.annotations.NotNull; + +public class UnreachableCodeInspectionTest extends LightJavaCodeInsightFixtureTestCase { + @Override + protected String getTestDataPath() { + return JavaTestUtil.getJavaTestDataPath() + "/inspection/dataFlow/unreachableCode/"; + } + + @Override + protected @NotNull LightProjectDescriptor getProjectDescriptor() { + return JAVA_17_ANNOTATED; + } + + public void testSystemExit() { doTest(); } + + public void testThrow() { doTest(); } + + public void testTrivialReturn() { doTest(); } + + public void testSimpleCatch() { doTest(); } + + public void testSwitchFallthrough() { doTest(); } + + public void testSwitchFail() { doTest(); } + + public void testSwitchDefault() { doTest(); } + + public void testInitializer() { doTest(); } + + public void testConstructor() { doTest(); } + + public void testCatchLinkageError() { doTest(); } + + public void testLambdaCast() { doTest(); } + + private void doTest() { + myFixture.configureByFile(getTestName(false) + ".java"); + myFixture.enableInspections(new UnreachableCodeInspection()); + myFixture.checkHighlighting(); + } +} diff --git a/java/openapi/resources/messages/JavaBundle.properties b/java/openapi/resources/messages/JavaBundle.properties index c8f09e593959..fc5eba12003a 100644 --- a/java/openapi/resources/messages/JavaBundle.properties +++ b/java/openapi/resources/messages/JavaBundle.properties @@ -389,6 +389,11 @@ inspection.convert.to.local.quickfix=Convert to local inspection.data.flow.display.name=Nullability and data flow problems inspection.data.flow.optional.of.nullable.misuse.display.name=Use of Optional.ofNullable with null or not-null argument inspection.data.flow.constant.values.display.name=Constant values +inspection.data.flow.unreachable.code.display.name=Unreachable code +inspection.data.flow.unreachable.code.option.ignore.trivial.name=Ignore trivial exit statements +inspection.data.flow.unreachable.code.option.ignore.trivial.description=Do not report unreachable statements like return 0, break, etc., as they could be necessary to satisfy Java control flow rules. +inspection.data.flow.unreachable.code.option.respect.suppression.name=Respect 'Constant value' inspection suppressions +inspection.data.flow.unreachable.code.option.respect.suppression.description=Try to avoid reporting unreachable code if it caused by a constant condition, where a corresponding warning is suppressed. inspection.data.flow.filter.notnull.quickfix=Insert 'filter(Objects::nonNull)' step inspection.data.flow.nullable.quickfix.option=Suggest @Nullable annotation for methods/fields/parameters where nullable values are used inspection.data.flow.true.asserts.option=Don't report assertions with condition statically proven to be always true diff --git a/platform/analysis-impl/src/com/intellij/codeInspection/dataFlow/interpreter/ReachabilityCountingInterpreter.java b/platform/analysis-impl/src/com/intellij/codeInspection/dataFlow/interpreter/ReachabilityCountingInterpreter.java index 83c55d398249..ab16ef733d43 100644 --- a/platform/analysis-impl/src/com/intellij/codeInspection/dataFlow/interpreter/ReachabilityCountingInterpreter.java +++ b/platform/analysis-impl/src/com/intellij/codeInspection/dataFlow/interpreter/ReachabilityCountingInterpreter.java @@ -17,6 +17,12 @@ import java.util.Set; public class ReachabilityCountingInterpreter extends StandardDataFlowInterpreter { protected final @NotNull BitSet myReached = new BitSet(); + /** + * @param flow control flow to interpret + * @param listener listener to use + * @param stopOnNull whether to stop interpretation on inevitable NullPointerException + * @param startingIndex starting instruction index (usually, 0) + */ public ReachabilityCountingInterpreter(@NotNull ControlFlow flow, @NotNull DfaListener listener, boolean stopOnNull, int startingIndex) { super(flow, listener, stopOnNull); myReached.set(0, startingIndex);