diff --git a/java/java-analysis-impl/resources/META-INF/JavaAnalysisPlugin.xml b/java/java-analysis-impl/resources/META-INF/JavaAnalysisPlugin.xml
index 9ec4a1693a12..479874bba13c 100644
--- a/java/java-analysis-impl/resources/META-INF/JavaAnalysisPlugin.xml
+++ b/java/java-analysis-impl/resources/META-INF/JavaAnalysisPlugin.xml
@@ -368,5 +368,6 @@
+
diff --git a/java/java-analysis-impl/src/com/intellij/psi/impl/StreamNullabilityPatcher.java b/java/java-analysis-impl/src/com/intellij/psi/impl/StreamNullabilityPatcher.java
new file mode 100644
index 000000000000..ad8873732de3
--- /dev/null
+++ b/java/java-analysis-impl/src/com/intellij/psi/impl/StreamNullabilityPatcher.java
@@ -0,0 +1,34 @@
+// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.intellij.psi.impl;
+
+import com.intellij.codeInsight.JavaExpressionTypeNullabilityPatcher;
+import com.intellij.codeInsight.TypeNullability;
+import com.intellij.psi.*;
+import com.siyeh.ig.callMatcher.CallMatcher;
+import com.siyeh.ig.psiutils.ExpressionUtils;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+final class StreamNullabilityPatcher implements JavaExpressionTypeNullabilityPatcher {
+ private static final CallMatcher STREAM_FILTER_TAKE_WHILE = CallMatcher.instanceCall(
+ CommonClassNames.JAVA_UTIL_STREAM_STREAM, "filter", "takeWhile")
+ .parameterCount(1);
+
+ @Override
+ public @Nullable PsiType tryPatchType(@NotNull PsiExpression expression, @NotNull PsiType type) {
+ if (type instanceof PsiClassType classType &&
+ expression instanceof PsiMethodCallExpression call &&
+ STREAM_FILTER_TAKE_WHILE.test(call) &&
+ ExpressionUtils.isNullFilteringFunction(call.getArgumentList().getExpressions()[0])) {
+ PsiClass psiClass = classType.resolve();
+ if (psiClass != null && CommonClassNames.JAVA_UTIL_STREAM_STREAM.equals(psiClass.getQualifiedName())) {
+ PsiType[] parameters = classType.getParameters();
+ if (parameters.length == 1) {
+ return JavaPsiFacade.getElementFactory(expression.getProject())
+ .createType(psiClass, parameters[0].withNullability(TypeNullability.NOT_NULL_KNOWN));
+ }
+ }
+ }
+ return null;
+ }
+}
diff --git a/java/java-analysis-impl/src/com/siyeh/ig/psiutils/ExpressionUtils.java b/java/java-analysis-impl/src/com/siyeh/ig/psiutils/ExpressionUtils.java
index f72d447295f3..3136e9c14da6 100644
--- a/java/java-analysis-impl/src/com/siyeh/ig/psiutils/ExpressionUtils.java
+++ b/java/java-analysis-impl/src/com/siyeh/ig/psiutils/ExpressionUtils.java
@@ -3,9 +3,7 @@ package com.siyeh.ig.psiutils;
import com.intellij.codeInsight.AnnotationUtil;
import com.intellij.codeInsight.CodeInsightUtilCore;
-import com.intellij.codeInspection.dataFlow.ContractReturnValue;
-import com.intellij.codeInspection.dataFlow.JavaMethodContractUtil;
-import com.intellij.codeInspection.dataFlow.MutationSignature;
+import com.intellij.codeInspection.dataFlow.*;
import com.intellij.java.codeserver.core.JavaPsiReferenceUtil;
import com.intellij.java.codeserver.core.JavaPsiReferenceUtil.ForwardReferenceProblem;
import com.intellij.java.syntax.parser.JavaKeywords;
@@ -30,6 +28,7 @@ import org.jetbrains.annotations.*;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.IntPredicate;
import java.util.function.Predicate;
import java.util.stream.Stream;
@@ -1656,4 +1655,60 @@ public final class ExpressionUtils {
}
return codeBlock.getStatementCount() == 1;
}
+
+ /**
+ * @param expression expression to test
+ * @return true if this expression represents a predicate-like function (accepting one parameter and returning a boolean value),
+ * which returns false for null values (may return false for some non-null values as well). This method is pattern-based,
+ * so it may not recognize specific patterns.
+ */
+ public static boolean isNullFilteringFunction(@NotNull PsiExpression expression) {
+ expression = PsiUtil.skipParenthesizedExprDown(expression);
+ if (expression instanceof PsiLambdaExpression lambda) {
+ PsiParameter[] parameters = lambda.getParameterList().getParameters();
+ if (parameters.length != 1) return false;
+ PsiParameter parameter = parameters[0];
+ PsiExpression expr = PsiUtil.skipParenthesizedExprDown(LambdaUtil.extractSingleExpressionFromBody(lambda.getBody()));
+ while (expr instanceof PsiPolyadicExpression polyadicExpression) {
+ if (polyadicExpression.getOperationTokenType() == JavaTokenType.ANDAND) {
+ expr = polyadicExpression.getOperands()[0];
+ continue;
+ }
+ return getVariableFromNullComparison(expr, false) == parameter;
+ }
+ PsiExpression negated = BoolUtils.getNegated(expr);
+ boolean nullTrue = false;
+ if (negated != null) {
+ expr = negated;
+ nullTrue = true;
+ }
+ if (expr instanceof PsiInstanceOfExpression instanceOf && isReferenceTo(instanceOf.getOperand(), parameter)) {
+ return !nullTrue;
+ }
+ if (expr instanceof PsiMethodCallExpression call) {
+ PsiMethod method = call.resolveMethod();
+ PsiExpression[] args = call.getArgumentList().getExpressions();
+ if (args.length == 0) return false;
+ return isNullFilteringMethod(method, nullTrue, arg -> isReferenceTo(args[arg], parameter));
+ }
+ }
+ if (expression instanceof PsiMethodReferenceExpression methodRef && methodRef.resolve() instanceof PsiMethod method) {
+ return isNullFilteringMethod(method, false, arg -> arg == 0);
+ }
+ return false;
+ }
+
+ private static boolean isNullFilteringMethod(@Nullable PsiMethod method, boolean nullTrue, @NotNull IntPredicate isWantedArg) {
+ if (method == null || !JavaMethodContractUtil.isPure(method)) return false;
+ List extends MethodContract> contracts = JavaMethodContractUtil.getMethodCallContracts(method, null);
+ for (MethodContract contract : contracts) {
+ if (!(ContainerUtil.getOnlyItem(contract.getConditions()) instanceof ContractValue.Condition condition)) return false;
+ int argEqNull = condition.getArgumentComparedTo(ContractValue.nullValue(), true).orElse(-1);
+ if (argEqNull != -1 && isWantedArg.test(argEqNull)) {
+ ContractReturnValue value = contract.getReturnValue();
+ return value.equals(ContractReturnValue.returnBoolean(nullTrue));
+ }
+ }
+ return false;
+ }
}
\ No newline at end of file
diff --git a/java/java-psi-api/src/com/intellij/codeInsight/JavaExpressionTypeNullabilityPatcher.java b/java/java-psi-api/src/com/intellij/codeInsight/JavaExpressionTypeNullabilityPatcher.java
new file mode 100644
index 000000000000..6941f32c6b40
--- /dev/null
+++ b/java/java-psi-api/src/com/intellij/codeInsight/JavaExpressionTypeNullabilityPatcher.java
@@ -0,0 +1,48 @@
+// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.intellij.codeInsight;
+
+import com.intellij.openapi.extensions.ExtensionPointName;
+import com.intellij.psi.PsiExpression;
+import com.intellij.psi.PsiType;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * An extension point which allows to patch the type nullability of expressions.
+ */
+@ApiStatus.Experimental
+public interface JavaExpressionTypeNullabilityPatcher {
+ ExtensionPointName EP_NAME = ExtensionPointName.create("com.intellij.java.expressionTypeNullabilityPatcher");
+
+ /**
+ * Patches the type nullability of the given expression using the extra-linguistic knowledge
+ * (e.g., call to a known library method). May patch not only the top-level nullability but
+ * also nested nullability as well. Implement with care, as this is computed when determining
+ * the expression type, so it's easy to get into infinite recursion if other non-trivial operations
+ * are performed on the expression.
+ *
+ * @param expression expression
+ * @param type computed type with inherent nullability
+ * @return patched type, or null if no patching is supported for a given expression. In this case,
+ * the next patcher will be tried, if available.
+ */
+ @Nullable PsiType tryPatchType(@NotNull PsiExpression expression, @NotNull PsiType type);
+
+ /**
+ * Patches the type nullability of the given expression using all registered patchers.
+ *
+ * @param expression expression whose type should be patched
+ * @param type computed type with inherent nullability
+ * @return the patched type, or the originally computed type if no patcher wants to patch this expression
+ */
+ static @NotNull PsiType patchTypeNullability(@NotNull PsiExpression expression, @NotNull PsiType type) {
+ for (JavaExpressionTypeNullabilityPatcher patcher : EP_NAME.getExtensionList()) {
+ PsiType patchedType = patcher.tryPatchType(expression, type);
+ if (patchedType != null) {
+ return patchedType;
+ }
+ }
+ return type;
+ }
+}
diff --git a/java/java-psi-api/src/com/intellij/codeInsight/NullabilitySource.java b/java/java-psi-api/src/com/intellij/codeInsight/NullabilitySource.java
index 3407e3f9e091..3604486e0e5a 100644
--- a/java/java-psi-api/src/com/intellij/codeInsight/NullabilitySource.java
+++ b/java/java-psi-api/src/com/intellij/codeInsight/NullabilitySource.java
@@ -33,6 +33,12 @@ public /* sealed */ interface NullabilitySource {
* (e.g., primitive type, or disjunction type)
*/
MANDATED,
+ /**
+ * Type nullability is known from a particular code shape.
+ * While it may differ from one defined by the language constructs only,
+ * it's believed to be correct and more helpful to users.
+ */
+ KNOWN,
/**
* Type nullability is depicted explicitly by means of the language.
* Currently, not possible in Java, but may be used in other languages like Kotlin.
diff --git a/java/java-psi-api/src/com/intellij/codeInsight/TypeNullability.java b/java/java-psi-api/src/com/intellij/codeInsight/TypeNullability.java
index e7c0506eddfb..3006462e94da 100644
--- a/java/java-psi-api/src/com/intellij/codeInsight/TypeNullability.java
+++ b/java/java-psi-api/src/com/intellij/codeInsight/TypeNullability.java
@@ -25,6 +25,10 @@ public final class TypeNullability {
* Mandated not-null nullability
*/
public static final TypeNullability NOT_NULL_MANDATED = new TypeNullability(Nullability.NOT_NULL, NullabilitySource.Standard.MANDATED);
+ /**
+ * Known not-null nullability
+ */
+ public static final TypeNullability NOT_NULL_KNOWN = new TypeNullability(Nullability.NOT_NULL, NullabilitySource.Standard.KNOWN);
/**
* Mandated nullable nullability
*/
diff --git a/java/java-psi-impl/resources/intellij.java.psi.impl.xml b/java/java-psi-impl/resources/intellij.java.psi.impl.xml
index 8d33077d9965..a5e12a5296e5 100644
--- a/java/java-psi-impl/resources/intellij.java.psi.impl.xml
+++ b/java/java-psi-impl/resources/intellij.java.psi.impl.xml
@@ -33,6 +33,7 @@
+
diff --git a/java/java-psi-impl/src/com/intellij/psi/impl/source/tree/java/PsiMethodCallExpressionImpl.java b/java/java-psi-impl/src/com/intellij/psi/impl/source/tree/java/PsiMethodCallExpressionImpl.java
index d4ef612d3817..1313a2a0f979 100644
--- a/java/java-psi-impl/src/com/intellij/psi/impl/source/tree/java/PsiMethodCallExpressionImpl.java
+++ b/java/java-psi-impl/src/com/intellij/psi/impl/source/tree/java/PsiMethodCallExpressionImpl.java
@@ -1,6 +1,7 @@
// 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.psi.impl.source.tree.java;
+import com.intellij.codeInsight.JavaExpressionTypeNullabilityPatcher;
import com.intellij.codeInsight.daemon.impl.analysis.JavaGenericsUtil;
import com.intellij.lang.ASTNode;
import com.intellij.openapi.diagnostic.Logger;
@@ -169,7 +170,8 @@ public class PsiMethodCallExpressionImpl extends ExpressionPsiElement implements
}
}
- return PsiClassImplUtil.correctType(theOnly, file.getResolveScope());
+ PsiType correctedType = PsiClassImplUtil.correctType(theOnly, file.getResolveScope());
+ return correctedType == null ? null : JavaExpressionTypeNullabilityPatcher.patchTypeNullability(call, correctedType);
}
private static @Nullable PsiType getResultType(@NotNull PsiMethodCallExpression call,
diff --git a/java/java-tests/testData/inspection/nullableProblems/StreamFilterPatching.java b/java/java-tests/testData/inspection/nullableProblems/StreamFilterPatching.java
new file mode 100644
index 000000000000..6ff75f84a7ea
--- /dev/null
+++ b/java/java-tests/testData/inspection/nullableProblems/StreamFilterPatching.java
@@ -0,0 +1,43 @@
+import org.jspecify.annotations.NullMarked;
+import org.jspecify.annotations.Nullable;
+
+import java.util.*;
+
+@NullMarked
+public class StreamFilterPatching {
+ public static List getIntegersMR(List<@Nullable Integer> integers) {
+ return integers.stream().filter(Objects::nonNull).toList();
+ }
+
+ public static List getIntegersMR2(List<@Nullable Integer> integers) {
+ return integers.stream().filter(Objects::isNull).toList();
+ }
+
+ public static List getIntegersLambda(List<@Nullable Integer> integers) {
+ return integers.stream().filter(obj -> Objects.nonNull(obj)).toList();
+ }
+
+ public static List getIntegersLambda2(List<@Nullable Integer> integers) {
+ return integers.stream().filter(obj -> Objects.isNull(obj)).toList();
+ }
+
+ public static List getIntegersLambda3(List<@Nullable Integer> integers) {
+ return integers.stream().filter(obj -> !Objects.isNull(obj)).toList();
+ }
+
+ public static List getIntegersLambda4(List<@Nullable Integer> integers) {
+ return integers.stream().filter(obj -> obj != null).toList();
+ }
+
+ public static List getIntegersLambda5(List<@Nullable Number> integers) {
+ return integers.stream().filter(obj -> obj instanceof Integer).toList();
+ }
+
+ public static List getIntegersLambda5And(List<@Nullable Number> integers) {
+ return integers.stream().filter(obj -> obj instanceof Integer i && i == 10).toList();
+ }
+
+ public static List getIntegersInstance(List<@Nullable Number> integers) {
+ return integers.stream().filter(Integer.class::isInstance).toList();
+ }
+}
diff --git a/java/java-tests/testSrc/com/intellij/java/codeInspection/NullableStuffInspectionTest.java b/java/java-tests/testSrc/com/intellij/java/codeInspection/NullableStuffInspectionTest.java
index e61e384efa27..05b9e5a4a0d8 100644
--- a/java/java-tests/testSrc/com/intellij/java/codeInspection/NullableStuffInspectionTest.java
+++ b/java/java-tests/testSrc/com/intellij/java/codeInspection/NullableStuffInspectionTest.java
@@ -492,6 +492,12 @@ public class NullableStuffInspectionTest extends LightJavaCodeInsightFixtureTest
doTest();
}
+ public void testStreamFilterPatching() {
+ addJSpecifyNullMarked(myFixture);
+ setupTypeUseAnnotations("org.jspecify.annotations", myFixture);
+ doTest();
+ }
+
public void testNullableExtendsNullable() {
addJSpecifyNullMarked(myFixture);
setupTypeUseAnnotations("org.jspecify.annotations", myFixture);