[java-inspections] IDEA-349198 IJ-CR-190983 Catch deconstruction on null value

- use dfa

GitOrigin-RevId: 19cdc3be30263c4b3dbbfff173c557c62c376d67
This commit is contained in:
Mikhail Pyltsin
2026-02-11 14:51:17 +00:00
committed by intellij-monorepo-bot
parent 4983585e6a
commit c6ca035622
24 changed files with 271 additions and 307 deletions
@@ -4,6 +4,8 @@ package com.intellij.java.codeserver.core;
import com.intellij.codeInsight.ExpressionUtil;
import com.intellij.java.syntax.parser.JavaKeywords;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.pom.java.JavaFeature;
import com.intellij.psi.CommonClassNames;
import com.intellij.psi.JavaPsiFacade;
@@ -13,14 +15,19 @@ import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiClassType;
import com.intellij.psi.PsiCodeBlock;
import com.intellij.psi.PsiConstantEvaluationHelper;
import com.intellij.psi.PsiDeconstructionList;
import com.intellij.psi.PsiDeconstructionPattern;
import com.intellij.psi.PsiDefaultCaseLabelElement;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiElementFactory;
import com.intellij.psi.PsiEnumConstant;
import com.intellij.psi.PsiExpression;
import com.intellij.psi.PsiKeyword;
import com.intellij.psi.PsiLiteralExpression;
import com.intellij.psi.PsiModifier;
import com.intellij.psi.PsiPattern;
import com.intellij.psi.PsiPrimitiveType;
import com.intellij.psi.PsiRecordComponent;
import com.intellij.psi.PsiReferenceExpression;
import com.intellij.psi.PsiStatement;
import com.intellij.psi.PsiSwitchBlock;
@@ -32,6 +39,7 @@ import com.intellij.psi.util.JavaPsiPatternUtil;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtil;
import com.intellij.psi.util.TypeConversionUtil;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.ObjectUtils;
import com.intellij.util.SmartList;
import com.intellij.util.containers.ContainerUtil;
@@ -45,6 +53,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static java.util.Objects.requireNonNull;
@@ -206,7 +215,8 @@ public final class JavaPsiSwitchUtil {
if (!isOverWhomUnconditionalForSelector &&
((!(overWhom instanceof PsiExpression expression) || ExpressionUtil.isNullLiteral(expression)) &&
who instanceof PsiKeyword &&
JavaKeywords.DEFAULT.equals(who.getText()) || isInCaseNullDefaultLabel(who))) {
JavaKeywords.DEFAULT.equals(who.getText()) || isInCaseNullDefaultLabel(who) ||
who instanceof PsiSwitchLabelStatementBase switchLabelStatementBase && switchLabelStatementBase.isDefaultCase())) {
// JEP 440-441
// A 'default' label dominates a case label with a case pattern,
// and it also dominates a case label with a null case constant.
@@ -450,6 +460,106 @@ public final class JavaPsiSwitchUtil {
return elementsToCheckCompleteness;
}
/**
* Determines whether a MatchException may occur during the deconstruction process.
*
* @param deconstructionPattern the deconstruction pattern being analyzed
* @param recordComponent the record component being checked
* @param deconstructionComponent the deconstruction component corresponding to the record component being checked,
* @param skipDominatingElements a set of elements to skip during dominance check
* @return {@code true} if a MatchException may occur during deconstruction,
* {@code false} otherwise
*/
public static boolean mayCauseMatchExceptionDuringDeconstruction(@Nullable PsiDeconstructionPattern deconstructionPattern,
@Nullable PsiRecordComponent recordComponent,
@Nullable PsiPattern deconstructionComponent,
@NotNull Set<@NotNull PsiElement> skipDominatingElements) {
if (deconstructionPattern == null || deconstructionComponent == null || recordComponent == null) return false;
PsiDeconstructionPattern topLevelDeconstruction = deconstructionPattern;
while (topLevelDeconstruction.getParent() instanceof PsiDeconstructionList deconstructionList &&
deconstructionList.getParent() instanceof PsiDeconstructionPattern parent) {
topLevelDeconstruction = parent;
}
PsiElement deconstructionParent = topLevelDeconstruction.getParent();
if (!(deconstructionParent instanceof PsiCaseLabelElementList caseLabelElementList)) return false;
if (!(caseLabelElementList.getParent() instanceof PsiSwitchLabelStatementBase switchLabelStatement)) return false;
if (!(switchLabelStatement.getParent() instanceof PsiCodeBlock codeBlock &&
codeBlock.getParent() instanceof PsiSwitchBlock switchBlock)) {
return false;
}
PsiType recordComponentType = recordComponent.getType();
PsiClass recordComponentClass = PsiUtil.resolveClassInClassTypeOnly(recordComponentType);
if (recordComponentClass == null) return false;
if (deconstructionComponent instanceof PsiDeconstructionPattern ||
recordComponentClass.hasModifierProperty(PsiModifier.SEALED)) {
if (!hasDominated(switchBlock,
topLevelDeconstruction,
deconstructionComponent,
recordComponentClass,
skipDominatingElements)) {
return true;
}
}
return false;
}
private static boolean hasDominated(@NotNull PsiSwitchBlock block,
@NotNull PsiDeconstructionPattern pattern,
@NotNull PsiPattern deconstructionComponent,
@NotNull PsiClass componentClass,
@NotNull Set<@NotNull PsiElement> skipDominatingElements) {
String text = pattern.getText();
TextRange textRange = pattern.getTextRange();
TextRange componentTextRange = deconstructionComponent.getTextRange();
if (!textRange.contains(componentTextRange)) return true;
TextRange toChange = componentTextRange.shiftLeft(textRange.getStartOffset());
String newPatternTe = StringUtil.replaceSubstring(text, toChange, componentClass.getQualifiedName() + " someVariable");
PsiPattern newPattern = createPatternFromText(newPatternTe, block);
if (newPattern == null) return true;
List<PsiElement> branches = getSwitchBranches(block);
PsiExpression expression = block.getExpression();
if (expression == null) return true;
PsiType selectorType = expression.getType();
if (selectorType == null) return true;
for (PsiElement branch : branches) {
if (skipDominatingElements.contains(branch) ||
//case null, default
(isNullOrDefault(branch) &&
ContainerUtil.exists(skipDominatingElements, e -> isInCaseNullDefaultLabel(e)))) {
continue;
}
boolean dominated = isDominated(newPattern, branch, selectorType);
if (dominated) return true;
}
return false;
}
private static boolean isNullOrDefault(@NotNull PsiElement branch) {
return ExpressionUtil.isNullLiteral(branch) ||
(branch instanceof PsiKeyword && JavaKeywords.DEFAULT.equals(branch.getText()));
}
private static @Nullable PsiPattern createPatternFromText(@NotNull String patternText, @NotNull PsiElement context) {
PsiElementFactory factory = PsiElementFactory.getInstance(context.getProject());
String labelText = "case " + patternText + "->{}";
PsiStatement statement;
try {
statement = factory.createStatementFromText(labelText, context);
}
catch (IncorrectOperationException e) {
return null;
}
PsiSwitchLabelStatementBase label = ObjectUtils.tryCast(statement, PsiSwitchLabelStatementBase.class);
if (label == null) return null;
PsiCaseLabelElementList list = label.getCaseLabelElementList();
if (list == null) return null;
PsiCaseLabelElement element = list.getElements()[0];
if (!(element instanceof PsiPattern pattern)) return null;
return pattern;
}
/**
* Kinds of switch selector
* @see #getSwitchSelectorKind(PsiType)
@@ -37,6 +37,8 @@ dataflow.message.npe.methodref.invocation=Method reference invocation <code>#ref
dataflow.message.only.switch.label=Switch label <code>#ref</code> is the only reachable in the whole switch
dataflow.message.passing.null.argument.nonannotated=Passing <code>null</code> argument to non-annotated parameter
dataflow.message.passing.null.argument=Passing <code>null</code> argument to parameter annotated as @NotNull
dataflow.message.deconstruction.match.exception=Pattern matching will throw <code>MatchException</code>
dataflow.message.may.deconstruction.match.exception=Pattern matching may throw <code>MatchException</code>
dataflow.message.passing.nullable.argument.methodref.nonannotated=Method reference argument might be null but passed to non-annotated parameter
dataflow.message.passing.nullable.argument.methodref=Method reference argument might be null
dataflow.message.passing.nullable.argument.nonannotated=Argument <code>#ref</code> might be null but passed to non-annotated parameter
@@ -8,6 +8,7 @@ import com.intellij.codeInsight.Nullability;
import com.intellij.codeInsight.NullabilityAnnotationInfo;
import com.intellij.codeInsight.NullableNotNullManager;
import com.intellij.codeInsight.intention.AddAnnotationModCommandAction;
import com.intellij.codeInsight.intention.QuickFixFactory;
import com.intellij.codeInsight.intention.impl.BaseIntentionAction;
import com.intellij.codeInspection.AbstractBaseJavaLocalInspectionTool;
import com.intellij.codeInspection.InspectionsBundle;
@@ -136,6 +137,7 @@ public abstract class DataFlowInspectionBase extends AbstractBaseJavaLocalInspec
public boolean REPORT_NULLS_PASSED_TO_NOT_NULL_PARAMETER = true;
public boolean REPORT_NULLABLE_METHODS_RETURNING_NOT_NULL = true;
public boolean REPORT_UNSOUND_WARNINGS = true;
public boolean REPORT_MATCHED_EXCEPTION = true;
@Override
public void writeSettings(@NotNull Element node) throws WriteExternalException {
@@ -154,6 +156,9 @@ public abstract class DataFlowInspectionBase extends AbstractBaseJavaLocalInspec
if (!REPORT_NULLABLE_METHODS_RETURNING_NOT_NULL) {
node.addContent(new Element("option").setAttribute("name", "REPORT_NULLABLE_METHODS_RETURNING_NOT_NULL").setAttribute("value", "false"));
}
if (!REPORT_MATCHED_EXCEPTION) {
node.addContent(new Element("option").setAttribute("name", "CHECK_MATCHED_EXCEPTION").setAttribute("value", "false"));
}
if (!REPORT_UNSOUND_WARNINGS) {
node.addContent(new Element("option").setAttribute("name", "REPORT_UNSOUND_WARNINGS").setAttribute("value", "false"));
}
@@ -607,6 +612,11 @@ public abstract class DataFlowInspectionBase extends AbstractBaseJavaLocalInspec
Consumer<PsiExpression> reportNullability = expr -> reportNullabilityProblem(reporter, problem, expression);
NullabilityProblemKind.assigningToNotNull.ifMyProblem(problem, reportNullability);
NullabilityProblemKind.storingToNotNullArray.ifMyProblem(problem, reportNullability);
if (REPORT_MATCHED_EXCEPTION) {
NullabilityProblemKind.deconstructionMatchException.ifMyProblem(problem, pattern -> {
reportDeconstructionMatchExceptionProblem(reporter, problem, pattern);
});
}
if (SUGGEST_NULLABLE_ANNOTATIONS) {
NullabilityProblemKind.passingToNonAnnotatedMethodRefParameter.ifMyProblem(
problem, methodRef -> reportNullableArgumentPassedToNonAnnotatedMethodRef(reporter, problem, methodRef));
@@ -621,6 +631,26 @@ public abstract class DataFlowInspectionBase extends AbstractBaseJavaLocalInspec
}
}
private void reportDeconstructionMatchExceptionProblem(@NotNull ProblemReporter reporter,
@NotNull NullabilityProblem<?> problem,
@NotNull PsiPattern pattern) {
ModCommandAction modCommandAction = createSwitchAddDefault(pattern);
if (modCommandAction == null) {
reporter.registerProblem(pattern, problem.getMessage(IGNORE_ASSERT_STATEMENTS));
return;
}
reporter.registerProblem(pattern, problem.getMessage(IGNORE_ASSERT_STATEMENTS), LocalQuickFix.from(modCommandAction));
}
private static @Nullable ModCommandAction createSwitchAddDefault(@NotNull PsiPattern pattern) {
QuickFixFactory quickFixFactory = QuickFixFactory.getInstance();
PsiSwitchBlock switchBlock = PsiTreeUtil.getParentOfType(pattern, PsiSwitchBlock.class);
if (switchBlock == null) return null;
ModCommandAction modCommandAction = quickFixFactory.createAddSwitchDefaultFix(switchBlock, null).asModCommandAction();
if (modCommandAction == null) return null;
return modCommandAction;
}
private void reportNullabilityProblem(ProblemReporter reporter,
NullabilityProblem<?> problem,
PsiExpression expr) {
@@ -41,6 +41,7 @@ import com.intellij.psi.PsiNewExpression;
import com.intellij.psi.PsiParameter;
import com.intellij.psi.PsiParameterList;
import com.intellij.psi.PsiParenthesizedExpression;
import com.intellij.psi.PsiPattern;
import com.intellij.psi.PsiPolyadicExpression;
import com.intellij.psi.PsiPrimitiveType;
import com.intellij.psi.PsiReferenceExpression;
@@ -100,6 +101,7 @@ import static com.intellij.util.ObjectUtils.tryCast;
public final class NullabilityProblemKind<T extends PsiElement> {
private static final String NPE = JAVA_LANG_NULL_POINTER_EXCEPTION;
private static final String RE = JAVA_LANG_RUNTIME_EXCEPTION;
private static final String MATCH_EXCEPTION = "java.lang.MatchException";
private final String myName;
private final Supplier<@Nls String> myAlwaysNullMessage;
@@ -169,6 +171,11 @@ public final class NullabilityProblemKind<T extends PsiElement> {
"dataflow.message.passing.nullable.argument.methodref.nonannotated");
// assumeNotNull problem is not reported, just used to force the argument to be not null
public static final NullabilityProblemKind<PsiExpression> assumeNotNull = new NullabilityProblemKind<>(RE, "assumeNotNull");
public static final NullabilityProblemKind<PsiPattern> deconstructionMatchException =
new NullabilityProblemKind<>(MATCH_EXCEPTION, "deconstructionMatchException",
"dataflow.message.deconstruction.match.exception",
"dataflow.message.may.deconstruction.match.exception");
/**
* noProblem is not reported and used to override another problem
* @see ControlFlowAnalyzer#addCustomNullabilityProblem(PsiExpression, NullabilityProblemKind)
@@ -117,6 +117,7 @@ import com.intellij.codeInspection.dataFlow.value.DfaValueFactory;
import com.intellij.codeInspection.dataFlow.value.DfaVariableValue;
import com.intellij.codeInspection.dataFlow.value.RelationType;
import com.intellij.codeInspection.dataFlow.value.VariableDescriptor;
import com.intellij.java.codeserver.core.JavaPsiSwitchUtil;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.pom.java.JavaFeature;
import com.intellij.psi.JavaCodeFragment;
@@ -264,6 +265,7 @@ import java.util.Map;
import java.util.Objects;
import java.util.Set;
import static com.intellij.codeInspection.dataFlow.NullabilityProblemKind.deconstructionMatchException;
import static com.intellij.psi.CommonClassNames.JAVA_LANG_ASSERTION_ERROR;
import static com.intellij.psi.CommonClassNames.JAVA_LANG_ERROR;
import static com.intellij.psi.CommonClassNames.JAVA_LANG_RUNTIME_EXCEPTION;
@@ -1362,6 +1364,9 @@ public class ControlFlowAnalyzer extends JavaElementVisitor {
VariableDescriptor descriptor = field == null ? new GetterDescriptor(accessor) : new PlainDescriptor(field);
DfaVariableValue accessorDfaVar = getFactory().getVarFactory().createVariableValue(descriptor, patternDfaVar);
addInstruction(new PushInstruction(accessorDfaVar, null));
if (JavaPsiSwitchUtil.mayCauseMatchExceptionDuringDeconstruction(deconstructionPattern, recordComponent, patternComponent, Set.of())) {
addNullCheck(deconstructionMatchException.problem(patternComponent, null));
}
processPattern(sourcePattern, patternComponent, substitutor.substitute(recordComponent.getType()), null, endPatternOffset);
}
}
@@ -486,12 +486,6 @@
groupKey="group.names.nullability.problems" groupBundle="messages.InspectionsBundle" enabledByDefault="true"
level="WARNING"
implementationClass="com.intellij.codeInspection.nullable.NullableStuffInspection"/>
<localInspection groupPathKey="group.path.names.probable.bugs" language="JAVA" bundle="messages.JavaBundle"
key="inspection.match.exception.problems.display.name"
hasStaticDescription="true"
groupKey="group.names.probable.bugs" groupBundle="messages.InspectionsBundle" enabledByDefault="true"
level="WARNING"
implementationClass="com.intellij.codeInspection.MatchExceptionInspection"/>
<localInspection groupPath="Java" language="JAVA" shortName="UnsatisfiedRange" bundle="messages.JavaAnalysisBundle"
key="inspection.unsatisfied.range.display.name"
groupKey="group.names.probable.bugs" groupBundle="messages.InspectionsBundle" enabledByDefault="true" level="WARNING"
@@ -245,6 +245,8 @@ public final class DataFlowInspection extends DataFlowInspectionBase {
message("inspection.data.flow.report.nullable.methods.that.always.return.a.non.null.value")),
checkbox("IGNORE_ASSERT_STATEMENTS",
message("inspection.data.flow.ignore.assert.statements")),
checkbox("REPORT_MATCHED_EXCEPTION",
message("inspection.data.flow.report.match.exception.problem")),
checkbox("REPORT_UNSOUND_WARNINGS",
message("inspection.data.flow.report.problems.that.happen.only.on.some.code.paths")),
JavaInspectionControls.button(
@@ -37,6 +37,8 @@ Integer square(@Nullable Integer input) {
<li>Use the <b>Ignore assert statements</b> option to control how the inspection treats <code>assert</code> statements. By default, the option
is disabled, which means that the assertions are assumed to be executed (-ea mode). If the option is enabled, the assertions will be completely ignored
(-da mode).</li>
<li>Use the <b>Reports patterns that may throw MatchException</b> option to report patterns that may throw
<code>MatchException</code> at runtime due to null values in deconstruction patterns.</li>
<li>Use the <b>Report problems that happen only on some code paths</b> option to control whether to report problems that may happen only
on some code path. If this option is disabled, warnings like <i>exception is possible</i> will not be reported. The inspection will report
only warnings like <i>exception will definitely occur</i>. This mode may greatly reduce the number of false-positives, especially if the code
@@ -1,35 +0,0 @@
<html>
<body>
Reports patterns in switch expressions and statements that may throw
<code>MatchException</code> at runtime
due to <code>null</code> values in deconstruction patterns.
<p>
The inspection analyzes record components with <code>@Nullable</code> annotation to detect cases where
deconstruction patterns may not cover <code>null</code> values.
</p>
<p><b>Example:</b></p>
<pre><code>
sealed interface II {
record AI() implements II {}
record BI() implements II {}
}
record RI(@Nullable II value) {}
private static II getII(RI ri) {
return switch (ri) {
case RI(II.BI bi) -> bi; // MatchException if ri.value is null
case RI(II.AI ai) -> ai;
};
}
</code></pre>
<p>
Use the provided quick-fix to add a <code>default</code> branch to handle unmatched cases safely.
</p>
<!-- tooltip end -->
<p><small>New in 2026.1</small>
</body>
</html>
@@ -1,210 +0,0 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInspection;
import com.intellij.codeInsight.ExpressionUtil;
import com.intellij.codeInsight.Nullability;
import com.intellij.codeInsight.TypeNullability;
import com.intellij.codeInsight.intention.QuickFixFactory;
import com.intellij.java.JavaBundle;
import com.intellij.java.codeserver.core.JavaPsiSwitchUtil;
import com.intellij.java.syntax.parser.JavaKeywords;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.JavaElementVisitor;
import com.intellij.psi.PsiCaseLabelElement;
import com.intellij.psi.PsiCaseLabelElementList;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiDeconstructionPattern;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiElementFactory;
import com.intellij.psi.PsiElementVisitor;
import com.intellij.psi.PsiExpression;
import com.intellij.psi.PsiKeyword;
import com.intellij.psi.PsiModifier;
import com.intellij.psi.PsiPattern;
import com.intellij.psi.PsiRecordComponent;
import com.intellij.psi.PsiStatement;
import com.intellij.psi.PsiSwitchBlock;
import com.intellij.psi.PsiSwitchExpression;
import com.intellij.psi.PsiSwitchLabelStatementBase;
import com.intellij.psi.PsiSwitchStatement;
import com.intellij.psi.PsiType;
import com.intellij.psi.PsiTypeElement;
import com.intellij.psi.util.PsiUtil;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.ObjectUtils;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Objects;
import java.util.Set;
public final class MatchExceptionInspection extends AbstractBaseJavaLocalInspectionTool {
@Override
public @NotNull PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) {
return new JavaElementVisitor() {
@Override
public void visitSwitchExpression(@NotNull PsiSwitchExpression expression) {
super.visitSwitchExpression(expression);
checkSwitchBlock(expression);
}
@Override
public void visitSwitchStatement(@NotNull PsiSwitchStatement statement) {
super.visitSwitchStatement(statement);
checkSwitchBlock(statement);
}
private void checkSwitchBlock(@NotNull PsiSwitchBlock switchBlock) {
//fast exit
if (JavaPsiSwitchUtil.findDefaultElement(switchBlock) != null) {
return;
}
PsiPattern pattern = findPatternCanProduceMatchException(switchBlock, Set.of(), true);
if (pattern == null) {
return;
}
QuickFixFactory quickFixFactory = QuickFixFactory.getInstance();
holder.problem(pattern, JavaBundle.message("inspection.match.exception.problems.message"))
.fix(Objects.requireNonNull(quickFixFactory.createAddSwitchDefaultFix(switchBlock, null).asModCommandAction()))
.register();
}
};
}
/**
* Identifies a pattern in the branches of a given {@code PsiSwitchBlock} that could potentially
* produce a match exception during execution.
*
* @param switchBlock the {@link PsiSwitchBlock} to analyze, representing a switch statement or
* switch expression in Java code. It must not be null.
* @param skipDominatingElements a set of elements that should be skipped when searching for a pattern.
* @param necessaryNullable flag indicating whether the pattern must be certainly nullable.
* @return a {@link PsiPattern} that could potentially cause a match exception, or {@code null}
* if no such pattern is found.
*/
public static @Nullable PsiPattern findPatternCanProduceMatchException(@NotNull PsiSwitchBlock switchBlock,
@NotNull Set<@NotNull PsiElement> skipDominatingElements,
boolean necessaryNullable) {
List<PsiElement> branches = JavaPsiSwitchUtil.getSwitchBranches(switchBlock);
for (PsiElement branch : branches) {
if (!(branch instanceof PsiDeconstructionPattern psiDeconstructionPattern)) continue;
PsiPattern deconstructionComponent = findDeconstructionComponentCanProduceMatchException(switchBlock,
psiDeconstructionPattern,
psiDeconstructionPattern,
skipDominatingElements,
necessaryNullable);
if (deconstructionComponent != null) return deconstructionComponent;
}
return null;
}
private static @Nullable PsiPattern findDeconstructionComponentCanProduceMatchException(
@NotNull PsiSwitchBlock switchBlock,
@NotNull PsiDeconstructionPattern psiDeconstructionPattern,
@NotNull PsiDeconstructionPattern topLevelDeconstructionPattern,
@NotNull Set<@NotNull PsiElement> skipDominatingElements,
boolean necessaryNullable) {
PsiTypeElement typeElement = psiDeconstructionPattern.getTypeElement();
PsiType recordType = typeElement.getType();
PsiClass recordClass = PsiUtil.resolveClassInClassTypeOnly(recordType);
if (recordClass == null || !recordClass.isRecord()) {
return null;
}
PsiRecordComponent[] recordComponents = recordClass.getRecordComponents();
@NotNull PsiPattern @NotNull [] deconstructionComponents =
psiDeconstructionPattern.getDeconstructionList().getDeconstructionComponents();
if (deconstructionComponents.length != recordComponents.length) {
return null;
}
for (int i = 0; i < recordComponents.length; i++) {
PsiPattern deconstructionComponent = deconstructionComponents[i];
if (deconstructionComponent instanceof PsiDeconstructionPattern nestedDeconstructionPattern) {
PsiPattern canProduceMatchException =
findDeconstructionComponentCanProduceMatchException(switchBlock,
nestedDeconstructionPattern,
topLevelDeconstructionPattern,
skipDominatingElements, necessaryNullable);
if (canProduceMatchException != null) return canProduceMatchException;
}
PsiRecordComponent component = recordComponents[i];
PsiType componentType = component.getType();
TypeNullability nullability = componentType.getNullability();
PsiExpression expression = switchBlock.getExpression();
if (expression == null) return null;
if (necessaryNullable && nullability.nullability() != Nullability.NULLABLE) continue;
if (!necessaryNullable && nullability.nullability() == Nullability.NOT_NULL) continue;
PsiClass componentClass = PsiUtil.resolveClassInClassTypeOnly(componentType);
if (componentClass == null) continue;
if (deconstructionComponent instanceof PsiDeconstructionPattern ||
componentClass.hasModifierProperty(PsiModifier.SEALED)) {
if (!hasDominated(switchBlock, topLevelDeconstructionPattern, deconstructionComponent, componentClass, skipDominatingElements)) {
return deconstructionComponent;
}
}
}
return null;
}
private static boolean hasDominated(@NotNull PsiSwitchBlock block,
@NotNull PsiDeconstructionPattern pattern,
@NotNull PsiPattern deconstructionComponent,
@NotNull PsiClass sealedClass,
@NotNull Set<@NotNull PsiElement> skipDominatingElements) {
String text = pattern.getText();
TextRange textRange = pattern.getTextRange();
TextRange componentTextRange = deconstructionComponent.getTextRange();
TextRange toChange = componentTextRange.shiftLeft(textRange.getStartOffset());
String newPatternTe = StringUtil.replaceSubstring(text, toChange, sealedClass.getQualifiedName() + " someVariable");
PsiPattern newPattern = createPatternFromText(newPatternTe, block);
if (newPattern == null) return true;
List<PsiElement> branches = JavaPsiSwitchUtil.getSwitchBranches(block);
PsiExpression expression = block.getExpression();
if (expression == null) return true;
PsiType selectorType = expression.getType();
if (selectorType == null) return true;
for (PsiElement branch : branches) {
if (skipDominatingElements.contains(branch) ||
//case null, default
(isNullOrDefault(branch) &&
ContainerUtil.exists(skipDominatingElements, e -> JavaPsiSwitchUtil.isInCaseNullDefaultLabel(e)))) {
continue;
}
boolean dominated = JavaPsiSwitchUtil.isDominated(newPattern, branch, selectorType);
if (dominated) return true;
}
return false;
}
private static boolean isNullOrDefault(@NotNull PsiElement branch) {
return ExpressionUtil.isNullLiteral(branch) ||
(branch instanceof PsiKeyword && JavaKeywords.DEFAULT.equals(branch.getText()));
}
private static @Nullable PsiPattern createPatternFromText(@NotNull String patternText, @NotNull PsiElement context) {
PsiElementFactory factory = PsiElementFactory.getInstance(context.getProject());
String labelText = "case " + patternText + "->{}";
PsiStatement statement;
try {
statement = factory.createStatementFromText(labelText, context);
}
catch (IncorrectOperationException e) {
return null;
}
PsiSwitchLabelStatementBase label = ObjectUtils.tryCast(statement, PsiSwitchLabelStatementBase.class);
if (label == null) return null;
PsiCaseLabelElementList list = label.getCaseLabelElementList();
if (list == null) return null;
PsiCaseLabelElement element = list.getElements()[0];
if (!(element instanceof PsiPattern pattern)) return null;
return pattern;
}
}
@@ -15,8 +15,9 @@
*/
package com.siyeh.ig.controlflow;
import com.intellij.codeInsight.Nullability;
import com.intellij.codeInsight.TypeNullability;
import com.intellij.codeInspection.LocalQuickFix;
import com.intellij.codeInspection.MatchExceptionInspection;
import com.intellij.codeInspection.ProblemHighlightType;
import com.intellij.codeInspection.dataFlow.fix.DeleteSwitchLabelFix;
import com.intellij.codeInspection.options.OptPane;
@@ -31,6 +32,7 @@ import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiClassInitializer;
import com.intellij.psi.PsiClassType;
import com.intellij.psi.PsiCodeBlock;
import com.intellij.psi.PsiDeconstructionPattern;
import com.intellij.psi.PsiDefaultCaseLabelElement;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiExpression;
@@ -43,6 +45,8 @@ import com.intellij.psi.PsiMember;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiModifier;
import com.intellij.psi.PsiParenthesizedExpression;
import com.intellij.psi.PsiPattern;
import com.intellij.psi.PsiRecordComponent;
import com.intellij.psi.PsiReferenceExpression;
import com.intellij.psi.PsiStatement;
import com.intellij.psi.PsiSwitchBlock;
@@ -52,6 +56,7 @@ import com.intellij.psi.PsiSwitchLabeledRuleStatement;
import com.intellij.psi.PsiSwitchStatement;
import com.intellij.psi.PsiThrowStatement;
import com.intellij.psi.PsiType;
import com.intellij.psi.PsiTypeElement;
import com.intellij.psi.PsiTypes;
import com.intellij.psi.PsiVariable;
import com.intellij.psi.controlFlow.AnalysisCanceledException;
@@ -72,6 +77,7 @@ import org.jetbrains.annotations.Nullable;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import static com.intellij.codeInspection.ProblemHighlightType.GENERIC_ERROR_OR_WARNING;
@@ -294,9 +300,61 @@ public final class UnnecessaryDefaultInspection extends BaseInspection {
return null;
}
if (MatchExceptionInspection.findPatternCanProduceMatchException(switchBlock, Set.of(defaultElement), false) != null) {
if (findPatternCanProduceMatchException(switchBlock, Set.of(defaultElement)) != null) {
return null;
}
return defaultElement;
}
private static @Nullable PsiPattern findPatternCanProduceMatchException(@NotNull PsiSwitchBlock switchBlock,
@NotNull Set<@NotNull PsiElement> skipDominatingElements) {
List<PsiElement> branches = JavaPsiSwitchUtil.getSwitchBranches(switchBlock);
for (PsiElement branch : branches) {
if (!(branch instanceof PsiDeconstructionPattern psiDeconstructionPattern)) continue;
PsiPattern deconstructionComponent = findDeconstructionComponentCanProduceMatchException(switchBlock,
psiDeconstructionPattern,
skipDominatingElements);
if (deconstructionComponent != null) return deconstructionComponent;
}
return null;
}
private static @Nullable PsiPattern findDeconstructionComponentCanProduceMatchException(
@NotNull PsiSwitchBlock switchBlock,
@NotNull PsiDeconstructionPattern psiDeconstructionPattern,
@NotNull Set<@NotNull PsiElement> skipDominatingElements) {
PsiTypeElement typeElement = psiDeconstructionPattern.getTypeElement();
PsiType recordType = typeElement.getType();
PsiClass recordClass = PsiUtil.resolveClassInClassTypeOnly(recordType);
if (recordClass == null || !recordClass.isRecord()) {
return null;
}
PsiRecordComponent[] recordComponents = recordClass.getRecordComponents();
@NotNull PsiPattern @NotNull [] deconstructionComponents =
psiDeconstructionPattern.getDeconstructionList().getDeconstructionComponents();
if (deconstructionComponents.length != recordComponents.length) {
return null;
}
for (int i = 0; i < recordComponents.length; i++) {
PsiPattern deconstructionComponent = deconstructionComponents[i];
if (deconstructionComponent instanceof PsiDeconstructionPattern nestedDeconstructionPattern) {
PsiPattern canProduceMatchException =
findDeconstructionComponentCanProduceMatchException(switchBlock,
nestedDeconstructionPattern,
skipDominatingElements);
if (canProduceMatchException != null) return canProduceMatchException;
}
PsiRecordComponent component = recordComponents[i];
PsiType componentType = component.getType();
TypeNullability nullability = componentType.getNullability();
PsiExpression expression = switchBlock.getExpression();
if (expression == null) return null;
if (nullability.nullability() == Nullability.NOT_NULL) continue;
if (JavaPsiSwitchUtil.mayCauseMatchExceptionDuringDeconstruction(psiDeconstructionPattern, component, deconstructionComponent, skipDominatingElements)) {
return deconstructionComponent;
}
}
return null;
}
}
@@ -6,6 +6,7 @@ class Test2 {
System.out.println(getLevel3(nullLevel3));
}
@Nullable
private static String getLevel3(@Nullable final Level1 level1) {
return switch (level1) {
case Level1(Level2(<warning descr="Pattern matching may throw 'MatchException'">Level3(var something)</warning>)) -> something;
@@ -7,6 +7,7 @@ class Test {
System.out.println(getLevel3(nullLevel2));
}
@Nullable
private static Level3 getLevel3(@Nullable final Level1 level1) {
return switch (level1) {
case Level1(<warning descr="Pattern matching may throw 'MatchException'">Level2(var something)</warning>) -> something;
@@ -19,7 +19,7 @@ class TestExample2 {
record RI2(@Nullable II value) {
}
private static II getII(RI ri) {
public static II getII(RI ri) {
return switch (ri) {
case RI(RI2(<warning descr="Pattern matching may throw 'MatchException'">II.BI bi</warning>)) -> bi;
case RI(RI2(II.AI ai)) -> ai;
@@ -6,6 +6,7 @@ class Test2 {
System.out.println(getLevel3(nullLevel3));
}
@Nullable
private static String getLevel3(@Nullable final Level1 level1) {
return switch (level1) {
case Level1(Level2(Level3(var something))) -> something;
@@ -6,6 +6,7 @@ class Test2 {
System.out.println(getLevel3(full));
}
@Nullable
private static String getLevel3(@Nullable final Level1 level1) {
return switch (level1) {
case Level1(Level2(Level3(var something))) -> something;
@@ -7,6 +7,7 @@ class Test {
System.out.println(getLevel3(nullLevel2));
}
@Nullable
private static Level3 getLevel3(@Nullable final Level1 level1) {
return switch (level1) {
case Level1(Level2(var something)) -> something;
@@ -0,0 +1,22 @@
import org.jetbrains.annotations.Nullable;
class TestExample {
sealed interface II {
record AI() implements II {}
record BI() implements II {}
}
record RI(@Nullable II value) {}
@Nullable
private static II getII(RI ri) {
if (ri.value == null) {
return null;
}
return switch (ri) {
case RI(II.BI bi) -> bi;
case RI(II.AI ai) -> ai;
};
}
}
@@ -12,6 +12,7 @@ class TestExample {
record RI(@Nullable II value) {
}
@Nullable
private static II getII(RI ri) {
return switch (ri) {
case RI(II.BI bi) -> bi;
@@ -9,6 +9,7 @@ class TestExample {
record RI(@Nullable II value) {}
@Nullable
private static II getII(RI ri) {
return switch (ri) {
case RI(II.BI bi) -> bi;
@@ -254,7 +254,27 @@ public class DataFlowInspection21Test extends DataFlowInspectionTestCase {
setupTypeUseAnnotations("org.jspecify.annotations", myFixture);
doTest();
}
public void testMatchExceptionNestedDeconstruction() { doTest(); }
public void testMatchExceptionSealedClass() { doTest(); }
public void testNoMatchExceptionSealedClassDataFlow() { doTest(); }
public void testMatchExceptionDoubleNestedDeconstruction() { doTest(); }
public void testNoMatchExceptionMostNestedDeconstruction() { doTest(); }
public void testMatchExceptionNestedSealedClass() { doTest(); }
public void testNoMatchExceptionNestedDeconstructionWithDefault() { doTest(); }
public void testNoMatchExceptionSealedClassWithNullDefault() { doTest(); }
public void testNoMatchExceptionDoubleNestedDeconstructionWithDominated() { doTest(); }
public void testNoMatchExceptionSealedClassWithDominated() { doTest(); }
public void testOptionalInference() {
doTestWith((dfi, cvi) -> dfi.SUGGEST_NULLABLE_ANNOTATIONS = false);
}
@@ -1,49 +0,0 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.java.codeInspection;
import com.intellij.JavaTestUtil;
import com.intellij.codeInspection.MatchExceptionInspection;
import com.intellij.testFramework.LightProjectDescriptor;
import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase;
import org.jetbrains.annotations.NotNull;
public final class MatchExceptionInspectionTest extends LightJavaCodeInsightFixtureTestCase {
@Override
protected String getBasePath() {
return JavaTestUtil.getRelativeJavaTestDataPath() + "/inspection/matchException";
}
@NotNull
@Override
protected LightProjectDescriptor getProjectDescriptor() {
return JAVA_21;
}
@Override
protected void setUp() throws Exception {
super.setUp();
myFixture.enableInspections(new MatchExceptionInspection());
}
private void doTest() {
myFixture.testHighlighting(getTestName(false) + ".java");
}
public void testMatchExceptionNestedDeconstruction() { doTest(); }
public void testMatchExceptionSealedClass() { doTest(); }
public void testMatchExceptionDoubleNestedDeconstruction() { doTest(); }
public void testNoMatchExceptionMostNestedDeconstruction() { doTest(); }
public void testMatchExceptionNestedSealedClass() { doTest(); }
public void testNoMatchExceptionNestedDeconstructionWithDefault() { doTest(); }
public void testNoMatchExceptionSealedClassWithNullDefault() { doTest(); }
public void testNoMatchExceptionDoubleNestedDeconstructionWithDominated() { doTest(); }
public void testNoMatchExceptionSealedClassWithDominated() { doTest(); }
}
@@ -374,6 +374,7 @@ inspection.data.flow.ignore.assert.statements=Ignore assert statements
inspection.data.flow.treat.non.annotated.members.and.parameters.as.nullable=Treat non-annotated members and parameters as @Nullable
inspection.data.flow.report.not.null.required.parameter.with.null.literal.argument.usages=Report non-null required parameter with null-literal argument usages
inspection.data.flow.report.nullable.methods.that.always.return.a.non.null.value=Report nullable methods that always return a non-null value
inspection.data.flow.report.match.exception.problem=Reports patterns that may throw MatchException
inspection.data.flow.report.problems.that.happen.only.on.some.code.paths=Report problems that happen only on some code paths
inspection.data.flow.use.computeifpresent.quickfix=Replace 'compute' with 'computeIfPresent'
inspection.dead.code.option.applet=Applets
@@ -568,8 +569,6 @@ inspection.nullable.problems.notnull.to.nullable.assignment.conflicts=Report ass
inspection.nullable.problems.redundant.nullability.inside.container=Report redundant nullability annotation in the scope of annotated container
inspection.optional.get.without.is.present.message=<code>{0}.#ref()</code> without ''isPresent()'' check
inspection.optional.get.without.is.present.method.reference.message=<code>#ref</code> without 'isPresent()' check
inspection.match.exception.problems.display.name=Possible MatchException in switch pattern matching
inspection.match.exception.problems.message=Pattern matching may throw 'MatchException'
inspection.overflowing.loop.index.inspection.description=Loop executes zero or billions of times
inspection.overflowing.loop.index.inspection.name=Loop executes zero or billions of times
inspection.overwritten.key.map.message=Duplicate Map key