diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/DataFlowInspectionBase.java b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/DataFlowInspectionBase.java index c8b7236ad4f2..f11a24cecd50 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/DataFlowInspectionBase.java +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/DataFlowInspectionBase.java @@ -28,6 +28,7 @@ import com.intellij.codeInsight.AnnotationUtil; import com.intellij.codeInsight.NullableNotNullManager; import com.intellij.codeInsight.daemon.GroupNames; import com.intellij.codeInsight.daemon.impl.quickfix.SimplifyBooleanExpressionFix; +import com.intellij.codeInsight.intention.impl.AddNotNullAnnotationFix; import com.intellij.codeInsight.intention.impl.AddNullableAnnotationFix; import com.intellij.codeInspection.*; import com.intellij.codeInspection.dataFlow.instructions.*; @@ -71,6 +72,7 @@ public class DataFlowInspectionBase extends BaseJavaBatchLocalInspectionTool { public boolean IGNORE_ASSERT_STATEMENTS; public boolean REPORT_CONSTANT_REFERENCE_VALUES = true; public boolean REPORT_NULLS_PASSED_TO_NOT_NULL_PARAMETER = true; + public boolean REPORT_NULLABLE_METHODS_RETURNING_NOT_NULL = true; @Override public JComponent createOptionsPanel() { @@ -93,6 +95,9 @@ public class DataFlowInspectionBase extends BaseJavaBatchLocalInspectionTool { if (!REPORT_NULLS_PASSED_TO_NOT_NULL_PARAMETER) { node.addContent(new Element("option").setAttribute("name", "REPORT_NULLS_PASSED_TO_NOT_NULL_PARAMETER").setAttribute("value", "false")); } + if (!REPORT_NULLABLE_METHODS_RETURNING_NOT_NULL) { + node.addContent(new Element("option").setAttribute("name", "REPORT_NULLABLE_METHODS_RETURNING_NOT_NULL").setAttribute("value", "false")); + } } @Override @@ -337,6 +342,26 @@ public class DataFlowInspectionBase extends BaseJavaBatchLocalInspectionTool { if (REPORT_CONSTANT_REFERENCE_VALUES) { reportConstantReferenceValues(holder, visitor, reportedAnchors); } + + if (REPORT_NULLABLE_METHODS_RETURNING_NOT_NULL && visitor.isAlwaysReturnsNotNull()) { + reportAlwaysReturnsNotNull(holder, scope); + } + } + + private static void reportAlwaysReturnsNotNull(ProblemsHolder holder, PsiElement scope) { + if (!(scope.getParent() instanceof PsiMethod)) return; + + PsiMethod method = (PsiMethod)scope.getParent(); + if (PsiUtil.canBeOverriden(method)) return; + + PsiAnnotation nullableAnno = NullableNotNullManager.getInstance(scope.getProject()).getNullableAnnotation(method, false); + if (nullableAnno == null || !nullableAnno.isPhysical()) return; + + PsiJavaCodeReferenceElement annoName = nullableAnno.getNameReferenceElement(); + assert annoName != null; + String msg = "@" + NullableStuffInspectionBase.getPresentableAnnoName(nullableAnno) + + " method '" + method.getName() + "' always return a non-null value"; + holder.registerProblem(annoName, msg, new AddNotNullAnnotationFix(method)); } private static void reportAlwaysFailingCalls(ProblemsHolder holder, @@ -870,6 +895,7 @@ public class DataFlowInspectionBase extends BaseJavaBatchLocalInspectionTool { private final Map, StateInfo> myStateInfos = ContainerUtil.newHashMap(); private final Set myCCEInstructions = ContainerUtil.newHashSet(); private final Map myFailingCalls = new HashMap<>(); + private boolean myAlwaysReturnsNotNull = true; @Override protected void onInstructionProducesCCE(TypeCastInstruction instruction) { @@ -890,6 +916,10 @@ public class DataFlowInspectionBase extends BaseJavaBatchLocalInspectionTool { return StreamEx.ofKeys(myFailingCalls, v -> v).map(MethodCallInstruction::getCallExpression).toList(); } + boolean isAlwaysReturnsNotNull() { + return myAlwaysReturnsNotNull; + } + @Override public DfaInstructionState[] visitMethodCall(MethodCallInstruction instruction, DataFlowRunner runner, @@ -914,6 +944,10 @@ public class DataFlowInspectionBase extends BaseJavaBatchLocalInspectionTool { @Override protected boolean checkNotNullable(DfaMemoryState state, DfaValue value, NullabilityProblem problem, PsiElement anchor) { + if (problem == NullabilityProblem.nullableReturn && !state.isNotNull(value)) { + myAlwaysReturnsNotNull = false; + } + boolean ok = super.checkNotNullable(state, value, problem, anchor); if (!ok && anchor != null) { myProblems.putValue(problem, anchor); diff --git a/java/java-impl/src/com/intellij/codeInspection/dataFlow/DataFlowInspection.java b/java/java-impl/src/com/intellij/codeInspection/dataFlow/DataFlowInspection.java index d9047fcf366a..901d43106b8a 100644 --- a/java/java-impl/src/com/intellij/codeInspection/dataFlow/DataFlowInspection.java +++ b/java/java-impl/src/com/intellij/codeInspection/dataFlow/DataFlowInspection.java @@ -75,6 +75,7 @@ public class DataFlowInspection extends DataFlowInspectionBase { private final JCheckBox myDontReportTrueAsserts; private final JCheckBox myTreatUnknownMembersAsNullable; private final JCheckBox myReportNullArguments; + private final JCheckBox myReportNullableMethodsReturningNotNull; private OptionsPanel() { super(new GridBagLayout()); @@ -141,6 +142,15 @@ public class DataFlowInspection extends DataFlowInspectionBase { } }); + myReportNullableMethodsReturningNotNull = new JCheckBox("Report nullable methods that always return a non-null value"); + myReportNullableMethodsReturningNotNull.setSelected(REPORT_NULLABLE_METHODS_RETURNING_NOT_NULL); + myReportNullableMethodsReturningNotNull.getModel().addChangeListener(new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + REPORT_NULLABLE_METHODS_RETURNING_NOT_NULL = myReportNullableMethodsReturningNotNull.isSelected(); + } + }); + gc.insets = JBUI.emptyInsets(); gc.gridy = 0; add(mySuggestNullables, gc); @@ -169,6 +179,9 @@ public class DataFlowInspection extends DataFlowInspectionBase { gc.gridy++; add(myReportNullArguments, gc); + + gc.gridy++; + add(myReportNullableMethodsReturningNotNull, gc); } } diff --git a/java/java-tests/testData/inspection/dataFlow/fixture/NullableMethodReturningNotNull.java b/java/java-tests/testData/inspection/dataFlow/fixture/NullableMethodReturningNotNull.java new file mode 100644 index 000000000000..70a7444bf308 --- /dev/null +++ b/java/java-tests/testData/inspection/dataFlow/fixture/NullableMethodReturningNotNull.java @@ -0,0 +1,15 @@ +import org.jetbrains.annotations.*; + +final class Foo { + @Nullable Object foo(int param) { + return param == 1 ? new Object() { + Object unrelated() { + return null; + } + } : bar(); + } + + @NotNull Foo bar() { + return this; + } +} diff --git a/java/java-tests/testSrc/com/intellij/codeInspection/DataFlowInspectionTest.java b/java/java-tests/testSrc/com/intellij/codeInspection/DataFlowInspectionTest.java index a513e17845f0..240aadc52776 100644 --- a/java/java-tests/testSrc/com/intellij/codeInspection/DataFlowInspectionTest.java +++ b/java/java-tests/testSrc/com/intellij/codeInspection/DataFlowInspectionTest.java @@ -412,4 +412,7 @@ public class DataFlowInspectionTest extends DataFlowInspectionTestCase { } public void testCapturedWildcardNotNull() { doTest(); } + + public void testNullableMethodReturningNotNull() { doTest(); } + }