From 6d178d8680feaf44f13c2fe887c02202304ffe20 Mon Sep 17 00:00:00 2001 From: Mikhail Pyltsin Date: Mon, 9 Feb 2026 16:53:11 +0100 Subject: [PATCH] [java-inspections] IDEA-349198 Catch deconstruction on null value GitOrigin-RevId: 6eac3a29ad4cefa69c3a7fa69a85c46d98187eb6 --- .../com/siyeh/ig/psiutils/SwitchUtils.java | 14 +- .../resources/META-INF/Inspections.xml | 6 + .../MatchException.html | 35 +++ .../EnhancedSwitchMigrationInspection.java | 2 +- .../MatchExceptionInspection.java | 210 ++++++++++++++++++ ...tchStatementsWithoutDefaultInspection.java | 2 +- .../UnnecessaryDefaultInspection.java | 18 +- ...chExceptionDoubleNestedDeconstruction.java | 24 ++ .../MatchExceptionNestedDeconstruction.java | 25 +++ .../MatchExceptionNestedSealedClass.java | 29 +++ .../MatchExceptionSealedClass.java | 18 ++ ...ubleNestedDeconstructionWithDominated.java | 25 +++ ...atchExceptionMostNestedDeconstruction.java | 24 ++ ...eptionNestedDeconstructionWithDefault.java | 26 +++ ...atchExceptionSealedClassWithDominated.java | 22 ++ ...chExceptionSealedClassWithNullDefault.java | 19 ++ .../MatchExceptionInspectionTest.java | 49 ++++ .../UnnecessaryDefaultInspectionTest.java | 46 ++++ .../resources/messages/JavaBundle.properties | 2 + 19 files changed, 576 insertions(+), 20 deletions(-) create mode 100644 java/java-impl/resources/inspectionDescriptions/MatchException.html create mode 100644 java/java-impl/src/com/intellij/codeInspection/MatchExceptionInspection.java create mode 100644 java/java-tests/testData/inspection/matchException/MatchExceptionDoubleNestedDeconstruction.java create mode 100644 java/java-tests/testData/inspection/matchException/MatchExceptionNestedDeconstruction.java create mode 100644 java/java-tests/testData/inspection/matchException/MatchExceptionNestedSealedClass.java create mode 100644 java/java-tests/testData/inspection/matchException/MatchExceptionSealedClass.java create mode 100644 java/java-tests/testData/inspection/matchException/NoMatchExceptionDoubleNestedDeconstructionWithDominated.java create mode 100644 java/java-tests/testData/inspection/matchException/NoMatchExceptionMostNestedDeconstruction.java create mode 100644 java/java-tests/testData/inspection/matchException/NoMatchExceptionNestedDeconstructionWithDefault.java create mode 100644 java/java-tests/testData/inspection/matchException/NoMatchExceptionSealedClassWithDominated.java create mode 100644 java/java-tests/testData/inspection/matchException/NoMatchExceptionSealedClassWithNullDefault.java create mode 100644 java/java-tests/testSrc/com/intellij/java/codeInspection/MatchExceptionInspectionTest.java diff --git a/java/java-analysis-impl/src/com/siyeh/ig/psiutils/SwitchUtils.java b/java/java-analysis-impl/src/com/siyeh/ig/psiutils/SwitchUtils.java index 754019354ba9..8fe1a3d715fc 100644 --- a/java/java-analysis-impl/src/com/siyeh/ig/psiutils/SwitchUtils.java +++ b/java/java-analysis-impl/src/com/siyeh/ig/psiutils/SwitchUtils.java @@ -974,12 +974,9 @@ public final class SwitchUtils { * Evaluates the exhaustiveness state of a switch block. * * @param switchBlock the PsiSwitchBlock to evaluate - * @param considerNestedDeconstructionPatterns flag indicating whether to consider nested deconstruction patterns. It is necessary to take into account, - * because nested deconstruction patterns don't cover null values * @return exhaustiveness state. */ - public static @NotNull SwitchExhaustivenessState evaluateSwitchCompleteness(@NotNull PsiSwitchBlock switchBlock, - boolean considerNestedDeconstructionPatterns) { + public static @NotNull SwitchExhaustivenessState evaluateSwitchCompleteness(@NotNull PsiSwitchBlock switchBlock) { PsiExpression selector = switchBlock.getExpression(); if (selector == null) return SwitchExhaustivenessState.MALFORMED; PsiType selectorType = selector.getType(); @@ -998,15 +995,6 @@ public final class SwitchUtils { return SwitchExhaustivenessState.EXHAUSTIVE_NO_DEFAULT; } if (!needToCheckCompleteness && !isEnumSelector) return SwitchExhaustivenessState.INCOMPLETE; - // It is necessary because deconstruction patterns don't cover cases - // when some of their components are null and deconstructionPattern too - if (!considerNestedDeconstructionPatterns) { - labelElements = ContainerUtil.filter( - labelElements, label -> !(label instanceof PsiDeconstructionPattern deconstructionPattern && - ContainerUtil.or( - deconstructionPattern.getDeconstructionList().getDeconstructionComponents(), - component -> component instanceof PsiDeconstructionPattern))); - } boolean hasError = hasExhaustivenessError(switchBlock, labelElements); // if a switch block is needed to check completeness and switch is incomplete we let highlighting to inform about it as it's a compilation error if (!hasError) { diff --git a/java/java-backend/resources/META-INF/Inspections.xml b/java/java-backend/resources/META-INF/Inspections.xml index b342b0003886..6cfa0f0eae59 100644 --- a/java/java-backend/resources/META-INF/Inspections.xml +++ b/java/java-backend/resources/META-INF/Inspections.xml @@ -486,6 +486,12 @@ groupKey="group.names.nullability.problems" groupBundle="messages.InspectionsBundle" enabledByDefault="true" level="WARNING" implementationClass="com.intellij.codeInspection.nullable.NullableStuffInspection"/> + + +Reports patterns in switch expressions and statements that may throw +MatchException at runtime +due to null values in deconstruction patterns. + +

+ The inspection analyzes record components with @Nullable annotation to detect cases where + deconstruction patterns may not cover null values. +

+ +

Example:

+

+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;
+    };
+}
+
+ +

+ Use the provided quick-fix to add a default branch to handle unmatched cases safely. +

+ +

New in 2026.1 + + \ No newline at end of file diff --git a/java/java-impl/src/com/intellij/codeInspection/EnhancedSwitchMigrationInspection.java b/java/java-impl/src/com/intellij/codeInspection/EnhancedSwitchMigrationInspection.java index ee889f884a98..b07701db8133 100644 --- a/java/java-impl/src/com/intellij/codeInspection/EnhancedSwitchMigrationInspection.java +++ b/java/java-impl/src/com/intellij/codeInspection/EnhancedSwitchMigrationInspection.java @@ -340,7 +340,7 @@ public final class EnhancedSwitchMigrationInspection extends AbstractBaseJavaLoc if (branch.isDefault()) return true; if (existsDefaultLabelElement(branch.myLabelStatement)) return true; } - SwitchUtils.SwitchExhaustivenessState completenessResult = evaluateSwitchCompleteness(switchStatement, true); + SwitchUtils.SwitchExhaustivenessState completenessResult = evaluateSwitchCompleteness(switchStatement); return completenessResult == SwitchUtils.SwitchExhaustivenessState.EXHAUSTIVE_CAN_ADD_DEFAULT || completenessResult == SwitchUtils.SwitchExhaustivenessState.EXHAUSTIVE_NO_DEFAULT; } diff --git a/java/java-impl/src/com/intellij/codeInspection/MatchExceptionInspection.java b/java/java-impl/src/com/intellij/codeInspection/MatchExceptionInspection.java new file mode 100644 index 000000000000..42672566dd48 --- /dev/null +++ b/java/java-impl/src/com/intellij/codeInspection/MatchExceptionInspection.java @@ -0,0 +1,210 @@ +// 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 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 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; + } +} diff --git a/java/java-impl/src/com/siyeh/ig/controlflow/SwitchStatementsWithoutDefaultInspection.java b/java/java-impl/src/com/siyeh/ig/controlflow/SwitchStatementsWithoutDefaultInspection.java index d1f04b6de569..ed4eecd1acd7 100644 --- a/java/java-impl/src/com/siyeh/ig/controlflow/SwitchStatementsWithoutDefaultInspection.java +++ b/java/java-impl/src/com/siyeh/ig/controlflow/SwitchStatementsWithoutDefaultInspection.java @@ -73,7 +73,7 @@ public final class SwitchStatementsWithoutDefaultInspection extends AbstractBase infoMode = true; } else { - SwitchExhaustivenessState completenessResult = evaluateSwitchCompleteness(statement, true); + SwitchExhaustivenessState completenessResult = evaluateSwitchCompleteness(statement); if (completenessResult == SwitchExhaustivenessState.UNNECESSARY || completenessResult == SwitchExhaustivenessState.MALFORMED || completenessResult == SwitchExhaustivenessState.EMPTY || diff --git a/java/java-impl/src/com/siyeh/ig/controlflow/UnnecessaryDefaultInspection.java b/java/java-impl/src/com/siyeh/ig/controlflow/UnnecessaryDefaultInspection.java index 1f7a272b649b..901ee54e20ea 100644 --- a/java/java-impl/src/com/siyeh/ig/controlflow/UnnecessaryDefaultInspection.java +++ b/java/java-impl/src/com/siyeh/ig/controlflow/UnnecessaryDefaultInspection.java @@ -16,6 +16,7 @@ package com.siyeh.ig.controlflow; 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; @@ -283,12 +284,19 @@ public final class UnnecessaryDefaultInspection extends BaseInspection { if (!(type instanceof PsiClassType)) { return null; } - final PsiElement result = JavaPsiSwitchUtil.findDefaultElement(switchBlock); - if (result == null) { + final PsiElement defaultElement = JavaPsiSwitchUtil.findDefaultElement(switchBlock); + if (defaultElement == null) { return null; } - final SwitchExhaustivenessState completenessResult = evaluateSwitchCompleteness(switchBlock, false); - return completenessResult == SwitchExhaustivenessState.EXHAUSTIVE_CAN_ADD_DEFAULT - ? result : null; + final SwitchExhaustivenessState completenessResult = evaluateSwitchCompleteness(switchBlock); + + if (completenessResult != SwitchExhaustivenessState.EXHAUSTIVE_CAN_ADD_DEFAULT) { + return null; + } + + if (MatchExceptionInspection.findPatternCanProduceMatchException(switchBlock, Set.of(defaultElement), false) != null) { + return null; + } + return defaultElement; } } \ No newline at end of file diff --git a/java/java-tests/testData/inspection/matchException/MatchExceptionDoubleNestedDeconstruction.java b/java/java-tests/testData/inspection/matchException/MatchExceptionDoubleNestedDeconstruction.java new file mode 100644 index 000000000000..dfd7aa35923b --- /dev/null +++ b/java/java-tests/testData/inspection/matchException/MatchExceptionDoubleNestedDeconstruction.java @@ -0,0 +1,24 @@ +import org.jetbrains.annotations.Nullable; + +class Test2 { + private static void testSwitchPattern() { + final var nullLevel3 = new Level1(new Level2(null)); + System.out.println(getLevel3(nullLevel3)); + } + + private static String getLevel3(@Nullable final Level1 level1) { + return switch (level1) { + case Level1(Level2(Level3(var something))) -> something; + case null -> null; + }; + } + + record Level3(String text) { + } + + record Level2(@Nullable Level3 something) { + } + + record Level1(Level2 level2) { + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/matchException/MatchExceptionNestedDeconstruction.java b/java/java-tests/testData/inspection/matchException/MatchExceptionNestedDeconstruction.java new file mode 100644 index 000000000000..70f021e5f662 --- /dev/null +++ b/java/java-tests/testData/inspection/matchException/MatchExceptionNestedDeconstruction.java @@ -0,0 +1,25 @@ +import org.jetbrains.annotations.Nullable; + +class Test { + private static void testSwitchPattern() { + final var nullLevel2 = new Level1(null); + System.out.println(getLevel3(null)); + System.out.println(getLevel3(nullLevel2)); + } + + private static Level3 getLevel3(@Nullable final Level1 level1) { + return switch (level1) { + case Level1(Level2(var something)) -> something; + case null -> null; + }; + } + + static class Level3 { + } + + record Level2(Level3 something) { + } + + record Level1(@Nullable Level2 level2) { + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/matchException/MatchExceptionNestedSealedClass.java b/java/java-tests/testData/inspection/matchException/MatchExceptionNestedSealedClass.java new file mode 100644 index 000000000000..ba4caf728b01 --- /dev/null +++ b/java/java-tests/testData/inspection/matchException/MatchExceptionNestedSealedClass.java @@ -0,0 +1,29 @@ +import org.jetbrains.annotations.Nullable; + +class TestExample2 { + public static void main(String[] args) { + getII(new RI(new RI2(null))); + } + + sealed interface II { + record AI() implements II { + } + + record BI() implements II { + } + } + + record RI(RI2 value) { + } + + record RI2(@Nullable II value) { + } + + private static II getII(RI ri) { + return switch (ri) { + case RI(RI2(II.BI bi)) -> bi; + case RI(RI2(II.AI ai)) -> ai; + }; + } +} + diff --git a/java/java-tests/testData/inspection/matchException/MatchExceptionSealedClass.java b/java/java-tests/testData/inspection/matchException/MatchExceptionSealedClass.java new file mode 100644 index 000000000000..4da4efc60d8c --- /dev/null +++ b/java/java-tests/testData/inspection/matchException/MatchExceptionSealedClass.java @@ -0,0 +1,18 @@ +import org.jetbrains.annotations.Nullable; + +class TestExample { + 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; + case RI(II.AI ai) -> ai; + }; + } +} diff --git a/java/java-tests/testData/inspection/matchException/NoMatchExceptionDoubleNestedDeconstructionWithDominated.java b/java/java-tests/testData/inspection/matchException/NoMatchExceptionDoubleNestedDeconstructionWithDominated.java new file mode 100644 index 000000000000..0b97a8068e37 --- /dev/null +++ b/java/java-tests/testData/inspection/matchException/NoMatchExceptionDoubleNestedDeconstructionWithDominated.java @@ -0,0 +1,25 @@ +import org.jetbrains.annotations.Nullable; + +class Test2 { + private static void testSwitchPattern() { + final var nullLevel3 = new Level1(new Level2(null)); + System.out.println(getLevel3(nullLevel3)); + } + + private static String getLevel3(@Nullable final Level1 level1) { + return switch (level1) { + case Level1(Level2(Level3(var something))) -> something; + case Level1(Level2 t) -> "some"; + case null -> null; + }; + } + + record Level3(String text) { + } + + record Level2(@Nullable Level3 something) { + } + + record Level1(Level2 level2) { + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/matchException/NoMatchExceptionMostNestedDeconstruction.java b/java/java-tests/testData/inspection/matchException/NoMatchExceptionMostNestedDeconstruction.java new file mode 100644 index 000000000000..9e61dd0ee013 --- /dev/null +++ b/java/java-tests/testData/inspection/matchException/NoMatchExceptionMostNestedDeconstruction.java @@ -0,0 +1,24 @@ +import org.jetbrains.annotations.Nullable; + +class Test2 { + private static void testSwitchPattern() { + final var full = new Level1(new Level2(new Level3(null))); + System.out.println(getLevel3(full)); + } + + private static String getLevel3(@Nullable final Level1 level1) { + return switch (level1) { + case Level1(Level2(Level3(var something))) -> something; + case null -> null; + }; + } + + record Level3(@Nullable String text) { + } + + record Level2(Level3 something) { + } + + record Level1(Level2 level2) { + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/matchException/NoMatchExceptionNestedDeconstructionWithDefault.java b/java/java-tests/testData/inspection/matchException/NoMatchExceptionNestedDeconstructionWithDefault.java new file mode 100644 index 000000000000..c972b07cb693 --- /dev/null +++ b/java/java-tests/testData/inspection/matchException/NoMatchExceptionNestedDeconstructionWithDefault.java @@ -0,0 +1,26 @@ +import org.jetbrains.annotations.Nullable; + +class Test { + private static void testSwitchPattern() { + final var nullLevel2 = new Level1(null); + System.out.println(getLevel3(null)); + System.out.println(getLevel3(nullLevel2)); + } + + private static Level3 getLevel3(@Nullable final Level1 level1) { + return switch (level1) { + case Level1(Level2(var something)) -> something; + case null -> null; + default -> null; + }; + } + + static class Level3 { + } + + record Level2(Level3 something) { + } + + record Level1(@Nullable Level2 level2) { + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/matchException/NoMatchExceptionSealedClassWithDominated.java b/java/java-tests/testData/inspection/matchException/NoMatchExceptionSealedClassWithDominated.java new file mode 100644 index 000000000000..60f1e89603be --- /dev/null +++ b/java/java-tests/testData/inspection/matchException/NoMatchExceptionSealedClassWithDominated.java @@ -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) { + } + + private static II getII(RI ri) { + return switch (ri) { + case RI(II.BI bi) -> bi; + case RI(II.AI ai) -> ai; + case RI(II a) -> a; + }; + } +} diff --git a/java/java-tests/testData/inspection/matchException/NoMatchExceptionSealedClassWithNullDefault.java b/java/java-tests/testData/inspection/matchException/NoMatchExceptionSealedClassWithNullDefault.java new file mode 100644 index 000000000000..9a9ef5dd90b0 --- /dev/null +++ b/java/java-tests/testData/inspection/matchException/NoMatchExceptionSealedClassWithNullDefault.java @@ -0,0 +1,19 @@ +import org.jetbrains.annotations.Nullable; + +class TestExample { + 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; + case RI(II.AI ai) -> ai; + case null, default -> null; + }; + } +} diff --git a/java/java-tests/testSrc/com/intellij/java/codeInspection/MatchExceptionInspectionTest.java b/java/java-tests/testSrc/com/intellij/java/codeInspection/MatchExceptionInspectionTest.java new file mode 100644 index 000000000000..efb8d7e8e5e7 --- /dev/null +++ b/java/java-tests/testSrc/com/intellij/java/codeInspection/MatchExceptionInspectionTest.java @@ -0,0 +1,49 @@ +// 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(); } +} diff --git a/java/java-tests/testSrc/com/siyeh/ig/controlflow/UnnecessaryDefaultInspectionTest.java b/java/java-tests/testSrc/com/siyeh/ig/controlflow/UnnecessaryDefaultInspectionTest.java index f18d7249c898..d7cd3561d4f3 100644 --- a/java/java-tests/testSrc/com/siyeh/ig/controlflow/UnnecessaryDefaultInspectionTest.java +++ b/java/java-tests/testSrc/com/siyeh/ig/controlflow/UnnecessaryDefaultInspectionTest.java @@ -179,6 +179,52 @@ public class UnnecessaryDefaultInspectionTest extends LightJavaInspectionTestCas "}"); } + public void testDefaultWithDominance() { + doTest(""" + import org.jetbrains.annotations.Nullable; + class TestExample { + 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; + case RI(II.AI ai) -> ai; + default -> null; + }; + } + } + """); + } + + public void testNullDefaultWithDominance() { + doTest(""" + import org.jetbrains.annotations.Nullable; + class TestExample { + 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; + case RI(II.AI ai) -> ai; + case null, default -> null; + }; + } + } + """); + } + @Override protected String[] getEnvironmentClasses() { return new String[] { diff --git a/java/openapi/resources/messages/JavaBundle.properties b/java/openapi/resources/messages/JavaBundle.properties index 732597025048..c2b36b800dff 100644 --- a/java/openapi/resources/messages/JavaBundle.properties +++ b/java/openapi/resources/messages/JavaBundle.properties @@ -568,6 +568,8 @@ 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={0}.#ref() without ''isPresent()'' check inspection.optional.get.without.is.present.method.reference.message=#ref 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