[java] JSpecify: initial strict mode support

GitOrigin-RevId: a387d37ac8924b8e63847eb54458424f5f690d9a
This commit is contained in:
Tagir Valeev
2021-02-11 05:23:59 +00:00
committed by intellij-monorepo-bot
parent 0b0c0b1bd5
commit ba33ae8bcf
6 changed files with 59 additions and 29 deletions
@@ -85,6 +85,7 @@ dataflow.message.unreachable.switch.label=Switch label <code>#ref</code> #loc is
dataflow.message.constant.expression=Result of <code>#ref</code> #loc is always ''{0}''
dataflow.message.constant.value=Value <code>#ref</code> #loc is always ''{0}''
dataflow.method.fails.with.null.argument=Method will throw an exception when parameter is null
dataflow.message.unknown.nullability=\ (unknown nullability)
dataflow.not.precise={0} is complex: data flow results could be imprecise
dataflow.too.complex={0} is too complex to analyze by data flow algorithm
@@ -153,7 +153,7 @@ final class DataFlowInstructionVisitor extends StandardInstructionVisitor {
}
StreamEx<NullabilityProblemKind.NullabilityProblem<?>> problems() {
return StreamEx.ofKeys(myStateInfos, StateInfo::shouldReport);
return EntryStream.of(myStateInfos).filterValues(StateInfo::shouldReport).mapKeyValue((np, si) -> si.unknown ? np.makeUnknown() : np);
}
public Map<PsiAssignmentExpression, Pair<PsiType, PsiType>> getArrayStoreProblems() {
@@ -117,7 +117,7 @@ public final class NullabilityProblemKind<T extends PsiElement> {
@Contract("null, _ -> null")
@Nullable
public final NullabilityProblem<T> problem(@Nullable T anchor, @Nullable PsiExpression expression) {
return anchor == null || this == noProblem ? null : new NullabilityProblem<>(this, anchor, expression);
return anchor == null || this == noProblem ? null : new NullabilityProblem<>(this, anchor, expression, false);
}
/**
@@ -430,7 +430,12 @@ public final class NullabilityProblemKind<T extends PsiElement> {
if (innerClassNPE == kind || callNPE == kind || arrayAccessNPE == kind || fieldAccessNPE == kind) {
// Qualifier-problems are reported on top-expression level for now as it's rare case to have
// something complex in qualifier and we highlight not the qualifier itself, but something else (e.g. called method name)
unchanged.add(problem.withExpression(findTopExpression(expression)));
boolean unknown = problem.hasUnknownNullability();
problem = problem.withExpression(findTopExpression(expression));
if (unknown) {
problem = problem.makeUnknown();
}
unchanged.add(problem);
continue;
}
// Merge ternary problems reported for both branches into single problem
@@ -450,7 +455,11 @@ public final class NullabilityProblemKind<T extends PsiElement> {
NullabilityProblem<?> otherBranchProblem = expressionToProblem.remove(otherBranch);
if (otherBranchProblem != null) {
expression = ternary;
boolean unknown = problem.hasUnknownNullability() && otherBranchProblem.hasUnknownNullability();
problem = problem.withExpression(ternary);
if (unknown) {
problem = problem.makeUnknown();
}
continue;
}
}
@@ -499,11 +508,16 @@ public final class NullabilityProblemKind<T extends PsiElement> {
private final @NotNull NullabilityProblemKind<T> myKind;
private final @NotNull T myAnchor;
private final @Nullable PsiExpression myDereferencedExpression;
private final boolean myFromUnknown;
NullabilityProblem(@NotNull NullabilityProblemKind<T> kind, @NotNull T anchor, @Nullable PsiExpression dereferencedExpression) {
NullabilityProblem(@NotNull NullabilityProblemKind<T> kind,
@NotNull T anchor,
@Nullable PsiExpression dereferencedExpression,
boolean unknown) {
myKind = kind;
myAnchor = anchor;
myDereferencedExpression = dereferencedExpression;
myFromUnknown = unknown;
}
@NotNull
@@ -528,18 +542,27 @@ public final class NullabilityProblemKind<T extends PsiElement> {
return myDereferencedExpression;
}
/**
* @return true if dereferenced expression has unknown nullability
* (reported in {@link DataFlowInspectionBase#TREAT_UNKNOWN_MEMBERS_AS_NULLABLE} mode).
*/
public boolean hasUnknownNullability() {
return myFromUnknown;
}
@NotNull
public @InspectionMessage String getMessage(Map<PsiExpression, DataFlowInspectionBase.ConstantResult> expressions) {
if (myKind.myAlwaysNullMessage == null || myKind.myNormalMessage == null) {
throw new IllegalStateException("This problem kind has no message associated: " + myKind);
}
String suffix = myFromUnknown ? JavaAnalysisBundle.message("dataflow.message.unknown.nullability") : "";
PsiExpression expression = PsiUtil.skipParenthesizedExprDown(getDereferencedExpression());
if (expression != null) {
if (ExpressionUtils.isNullLiteral(expression) || expressions.get(expression) == DataFlowInspectionBase.ConstantResult.NULL) {
return myKind.myAlwaysNullMessage.get();
return myKind.myAlwaysNullMessage.get() + suffix;
}
}
return myKind.myNormalMessage.get();
return myKind.myNormalMessage.get() + suffix;
}
@NotNull
@@ -567,7 +590,11 @@ public final class NullabilityProblemKind<T extends PsiElement> {
}
public NullabilityProblem<T> withExpression(PsiExpression expression) {
return expression == myDereferencedExpression ? this : new NullabilityProblem<>(myKind, myAnchor, expression);
return expression == myDereferencedExpression ? this : new NullabilityProblem<>(myKind, myAnchor, expression, false);
}
public NullabilityProblem<T> makeUnknown() {
return new NullabilityProblem<>(myKind, myAnchor, myDereferencedExpression, true);
}
}
}
@@ -95,7 +95,7 @@ class Test6 extends BadSuper {
}
public Integer someLength() {
return something.<warning descr="Method invocation 'length' may produce 'NullPointerException'">length</warning>();
return something.<warning descr="Method invocation 'length' may produce 'NullPointerException' (unknown nullability)">length</warning>();
}
protected void overrideableMethod() {
@@ -107,7 +107,7 @@ class Test7 extends BadSuper {
private final String something = new String("something");
protected void overrideableMethod() {
something.<warning descr="Method invocation 'length' may produce 'NullPointerException'">length</warning>();
something.<warning descr="Method invocation 'length' may produce 'NullPointerException' (unknown nullability)">length</warning>();
}
}
@@ -124,6 +124,6 @@ class Test8 {
void other() {
System.out.println(s.hashCode());
System.out.println(s2.<warning descr="Method invocation 'hashCode' may produce 'NullPointerException'">hashCode</warning>());
System.out.println(s2.<warning descr="Method invocation 'hashCode' may produce 'NullPointerException' (unknown nullability)">hashCode</warning>());
}
}
@@ -4,15 +4,15 @@ class Test {
Object o;
void field() {
o.<warning descr="Method invocation 'hashCode' may produce 'NullPointerException'">hashCode</warning>();
o.<warning descr="Method invocation 'hashCode' may produce 'NullPointerException' (unknown nullability)">hashCode</warning>();
}
void parameter(Object o) {
o.<warning descr="Method invocation 'hashCode' may produce 'NullPointerException'">hashCode</warning>();
o.<warning descr="Method invocation 'hashCode' may produce 'NullPointerException' (unknown nullability)">hashCode</warning>();
}
void callUnknownMethod() {
unknownObject().<warning descr="Method invocation 'hashCode' may produce 'NullPointerException'">hashCode</warning>();
unknownObject().<warning descr="Method invocation 'hashCode' may produce 'NullPointerException' (unknown nullability)">hashCode</warning>();
}
void callNotNullMethod() {
@@ -130,6 +130,7 @@ public class JSpecifyAnnotationTest extends LightJavaCodeInsightFixtureTestCase
Map<PsiElement, String> actual = new LinkedHashMap<>();
var dfaInspection = new JSpecifyDataFlowInspection(actual);
dfaInspection.TREAT_UNKNOWN_MEMBERS_AS_NULLABLE = true;
var nullableStuffInspection = new JSpecifyNullableStuffInspection(actual);
var notNullFieldNotInitializedInspection = new JSpecifyNotNullFieldNotInitializedInspection(actual);
List<LocalInspectionTool> inspections = List.of(dfaInspection, nullableStuffInspection, notNullFieldNotInitializedInspection);
@@ -208,24 +209,25 @@ public class JSpecifyAnnotationTest extends LightJavaCodeInsightFixtureTestCase
List<NullabilityProblemKind.NullabilityProblem<?>> problems,
Map<PsiExpression, DataFlowInspectionBase.ConstantResult> expressions) {
for (NullabilityProblemKind.NullabilityProblem<?> problem : problems) {
PsiExpression expression = problem.getDereferencedExpression();
if (expression != null) {
if (problem.getKind() == NullabilityProblemKind.nullableReturn) {
PsiType returnType = PsiTypesUtil.getMethodReturnType(expression);
Nullability nullability = DfaPsiUtil.getTypeNullability(returnType);
if (nullability == Nullability.UNKNOWN) {
warnings.put(expression, "jspecify_nullness_not_enough_information");
}
if (nullability == Nullability.NOT_NULL) {
warnings.put(expression, "jspecify_nullness_mismatch");
}
continue;
}
else if (problem.getKind() == NullabilityProblemKind.passingToNonAnnotatedParameter) continue;
warnings.put(expression, "jspecify_nullness_mismatch");
String warning = getJSpecifyWarning(problem);
if (warning != null) {
warnings.put(problem.getDereferencedExpression(), warning);
}
}
}
private static @Nullable String getJSpecifyWarning(NullabilityProblemKind.NullabilityProblem<?> problem) {
PsiExpression expression = problem.getDereferencedExpression();
if (expression == null) return null;
if (problem.getKind() == NullabilityProblemKind.passingToNonAnnotatedParameter) return null;
if (problem.getKind() == NullabilityProblemKind.nullableReturn) {
PsiType returnType = PsiTypesUtil.getMethodReturnType(expression);
Nullability nullability = DfaPsiUtil.getTypeNullability(returnType);
if (nullability == Nullability.NULLABLE) return null;
if (nullability == Nullability.UNKNOWN) return "jspecify_nullness_not_enough_information";
}
return problem.hasUnknownNullability() ? "jspecify_nullness_not_enough_information" : "jspecify_nullness_mismatch";
}
}
String getActualText(Map<PsiElement, String> actual, String stripped) {
@@ -236,7 +238,7 @@ public class JSpecifyAnnotationTest extends LightJavaCodeInsightFixtureTestCase
.grouping(TreeMap::new, Collectors.toList());
for (String str : stripped.split("\n", -1)) {
int endPos = pos + str.length() + 1;
String warnings = StreamEx.of(map.subMap(pos, endPos).values()).flatMap(List::stream).joining(" & ");
String warnings = StreamEx.of(map.subMap(pos, endPos).values()).flatMap(List::stream).distinct().joining(" & ");
if (!warnings.isEmpty()) {
sb.append("// ").append(warnings);
}