Basic test-suite for codeanalysis-annotations

GitOrigin-RevId: a134aead43faf095ac3ccec4fb24af23c8f241a7
This commit is contained in:
Tagir Valeev
2020-02-03 09:39:27 +00:00
committed by intellij-monorepo-bot
parent 1ecf89dcbf
commit 71300a36eb
12 changed files with 232 additions and 51 deletions
@@ -7,11 +7,13 @@ import com.intellij.codeInsight.daemon.GroupNames;
import com.intellij.codeInsight.intention.AddAnnotationPsiFix;
import com.intellij.codeInsight.intention.impl.BaseIntentionAction;
import com.intellij.codeInspection.*;
import com.intellij.codeInspection.dataFlow.DataFlowInstructionVisitor.ConstantResult;
import com.intellij.codeInspection.dataFlow.NullabilityProblemKind.NullabilityProblem;
import com.intellij.codeInspection.dataFlow.fix.*;
import com.intellij.codeInspection.dataFlow.instructions.InstanceofInstruction;
import com.intellij.codeInspection.dataFlow.instructions.Instruction;
import com.intellij.codeInspection.dataFlow.types.DfType;
import com.intellij.codeInspection.dataFlow.types.DfTypes;
import com.intellij.codeInspection.dataFlow.value.DfaValue;
import com.intellij.codeInspection.nullable.NullableStuffInspectionBase;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
@@ -504,15 +506,15 @@ public abstract class DataFlowInspectionBase extends AbstractBaseJavaLocalInspec
return null;
}
private void reportNullabilityProblems(ProblemReporter reporter,
List<NullabilityProblem<?>> problems,
Map<PsiExpression, ConstantResult> expressions) {
protected void reportNullabilityProblems(ProblemReporter reporter,
List<NullabilityProblem<?>> problems,
Map<PsiExpression, ConstantResult> expressions) {
for (NullabilityProblem<?> problem : problems) {
PsiExpression expression = problem.getDereferencedExpression();
if (!REPORT_UNSOUND_WARNINGS) {
if (expression == null) continue;
PsiExpression unwrapped = PsiUtil.skipParenthesizedExprDown(expression);
if (!ExpressionUtils.isNullLiteral(unwrapped) && expressions.get(expression) != DataFlowInstructionVisitor.ConstantResult.NULL) {
if (!ExpressionUtils.isNullLiteral(unwrapped) && expressions.get(expression) != ConstantResult.NULL) {
continue;
}
}
@@ -1136,10 +1138,48 @@ public abstract class DataFlowInspectionBase extends AbstractBaseJavaLocalInspec
return SHORT_NAME;
}
protected enum ConstantResult {
TRUE, FALSE, NULL, UNKNOWN;
@NotNull
@Override
public String toString() {
return StringUtil.toLowerCase(name());
}
public Object value() {
switch (this) {
case TRUE:
return Boolean.TRUE;
case FALSE:
return Boolean.FALSE;
case NULL:
return null;
default:
throw new UnsupportedOperationException();
}
}
@NotNull
static ConstantResult fromDfType(@NotNull DfType dfType) {
if (dfType == DfTypes.NULL) return NULL;
if (dfType == DfTypes.TRUE) return TRUE;
if (dfType == DfTypes.FALSE) return FALSE;
return UNKNOWN;
}
@NotNull
static ConstantResult mergeValue(@Nullable ConstantResult state, @NotNull DfaMemoryState memState, @Nullable DfaValue value) {
if (state == UNKNOWN || value == null) return UNKNOWN;
ConstantResult nextState = fromDfType(memState.getUnboxedDfType(value));
return state == null || state == nextState ? nextState : UNKNOWN;
}
}
/**
* {@link ProblemsHolder} wrapper to avoid reporting two problems on the same anchor
*/
private static class ProblemReporter {
protected static class ProblemReporter {
private final Set<PsiElement> myReportedAnchors = new HashSet<>();
private final ProblemsHolder myHolder;
private final PsiElement myScope;
@@ -1149,7 +1189,7 @@ public abstract class DataFlowInspectionBase extends AbstractBaseJavaLocalInspec
myScope = scope;
}
void registerProblem(PsiElement element, String message, LocalQuickFix... fixes) {
public void registerProblem(PsiElement element, String message, LocalQuickFix... fixes) {
if (register(element)) {
myHolder.registerProblem(element, message, fixes);
}
@@ -1,10 +1,10 @@
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.codeInspection.dataFlow;
import com.intellij.codeInspection.dataFlow.DataFlowInspectionBase.ConstantResult;
import com.intellij.codeInspection.dataFlow.instructions.*;
import com.intellij.codeInspection.dataFlow.types.DfConstantType;
import com.intellij.codeInspection.dataFlow.types.DfType;
import com.intellij.codeInspection.dataFlow.types.DfTypes;
import com.intellij.codeInspection.dataFlow.value.*;
import com.intellij.codeInspection.util.OptionalUtil;
import com.intellij.openapi.application.Application;
@@ -12,7 +12,6 @@ import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.*;
import com.intellij.psi.impl.PsiImplUtil;
import com.intellij.psi.util.PsiTreeUtil;
@@ -387,44 +386,6 @@ final class DataFlowInstructionVisitor extends StandardInstructionVisitor {
}
}
enum ConstantResult {
TRUE, FALSE, NULL, UNKNOWN;
@NotNull
@Override
public String toString() {
return StringUtil.toLowerCase(name());
}
public Object value() {
switch (this) {
case TRUE:
return Boolean.TRUE;
case FALSE:
return Boolean.FALSE;
case NULL:
return null;
default:
throw new UnsupportedOperationException();
}
}
@NotNull
static ConstantResult fromDfType(@NotNull DfType dfType) {
if (dfType == DfTypes.NULL) return NULL;
if (dfType == DfTypes.TRUE) return TRUE;
if (dfType == DfTypes.FALSE) return FALSE;
return UNKNOWN;
}
@NotNull
static ConstantResult mergeValue(@Nullable ConstantResult state, @NotNull DfaMemoryState memState, @Nullable DfaValue value) {
if (state == UNKNOWN || value == null) return UNKNOWN;
ConstantResult nextState = fromDfType(memState.getUnboxedDfType(value));
return state == null || state == nextState ? nextState : UNKNOWN;
}
}
static class ExpressionChunk {
final @NotNull PsiExpression myExpression;
final @Nullable TextRange myRange;
@@ -179,7 +179,7 @@ public class NullabilityProblemKind<T extends PsiElement> {
else if (parent instanceof PsiReturnStatement) {
targetType = PsiTypesUtil.getMethodReturnType(parent);
}
if (targetType != null && !PsiType.VOID.equals(targetType)) {
if (targetType != null && !PsiType.VOID.equals(targetType) && DfaPsiUtil.getTypeNullability(targetType) != Nullability.NULLABLE) {
if (TypeConversionUtil.isPrimitiveAndNotNull(targetType)) {
return createUnboxingProblem(context, expression);
}
@@ -509,13 +509,13 @@ public class NullabilityProblemKind<T extends PsiElement> {
}
@NotNull
public String getMessage(Map<PsiExpression, DataFlowInstructionVisitor.ConstantResult> expressions) {
public 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);
}
PsiExpression expression = PsiUtil.skipParenthesizedExprDown(getDereferencedExpression());
if (expression != null) {
if (ExpressionUtils.isNullLiteral(expression) || expressions.get(expression) == DataFlowInstructionVisitor.ConstantResult.NULL) {
if (ExpressionUtils.isNullLiteral(expression) || expressions.get(expression) == DataFlowInspectionBase.ConstantResult.NULL) {
return myKind.myAlwaysNullMessage;
}
}
@@ -4,6 +4,7 @@ package com.intellij.codeInsight.annoPackages;
import com.intellij.codeInsight.Nullability;
import com.intellij.codeInsight.NullabilityAnnotationInfo;
import com.intellij.codeInsight.NullableNotNullManager;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.psi.PsiAnnotation;
import com.intellij.util.ArrayUtil;
import org.jetbrains.annotations.NotNull;
@@ -48,7 +49,7 @@ public interface AnnotationPackageSupport {
new JetBrainsAnnotationSupport(), new FindBugsAnnotationSupport(), new AndroidAnnotationSupport(),
new Jsr305Support(manager), new CheckerFrameworkSupport()
};
if (CodeAnalysisAnnotationSupport.IS_AVAILABLE) {
if (CodeAnalysisAnnotationSupport.IS_AVAILABLE || ApplicationManager.getApplication().isUnitTestMode()) {
return ArrayUtil.append(extensions, new CodeAnalysisAnnotationSupport());
}
return extensions;
@@ -0,0 +1,11 @@
import codeanalysis.experimental.annotations.*;
class X {
int f;
void test(@Nullable X x) {
m(/*ca-nullable-to-not-null*/x);
}
native void m(@NotNull X x);
}
@@ -0,0 +1,9 @@
import codeanalysis.experimental.annotations.*;
class X {
int f;
void test(@Nullable X x) {
/*ca-nullable-to-not-null*/x.f = 1;
}
}
@@ -0,0 +1,8 @@
import codeanalysis.experimental.annotations.DefaultNotNull;
@DefaultNotNull
class X {
X get() {
return /*ca-nullable-to-not-null*/null;
}
}
@@ -0,0 +1,13 @@
import codeanalysis.experimental.annotations.DefaultNotNull;
import codeanalysis.experimental.annotations.Nullable;
@DefaultNotNull
class NullnessDemo {
@Nullable Object something() {
return null;
}
void foo() {
Object o = something();
}
}
@@ -0,0 +1,10 @@
import codeanalysis.experimental.annotations.DefaultNotNull;
@DefaultNotNull
class X {
void m(X x) {}
void use() {
m(/*ca-nullable-to-not-null*/null);
}
}
@@ -0,0 +1,9 @@
class X {
int f;
void test(X x) {
m(x);
}
native void m(X x);
}
@@ -0,0 +1,9 @@
import codeanalysis.experimental.annotations.*;
class X {
int f;
void test(X x) {
x.f = 1;
}
}
@@ -0,0 +1,110 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.java.codeInspection;
import com.intellij.JavaTestUtil;
import com.intellij.codeInspection.ProblemsHolder;
import com.intellij.codeInspection.dataFlow.DataFlowInspectionBase;
import com.intellij.codeInspection.dataFlow.NullabilityProblemKind;
import com.intellij.codeInspection.ex.InspectionManagerEx;
import com.intellij.openapi.application.ReadAction;
import com.intellij.openapi.vfs.CharsetToolkit;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiElementVisitor;
import com.intellij.psi.PsiExpression;
import com.intellij.psi.PsiFile;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.rt.execution.junit.FileComparisonFailure;
import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase;
import one.util.streamex.EntryStream;
import one.util.streamex.StreamEx;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Collectors;
@RunWith(Parameterized.class)
public class CodeAnalysisAnnotationsTest extends LightJavaCodeInsightFixtureTestCase {
private static final String PACKAGE_NAME = "codeanalysis.experimental.annotations";
private static final Path PATH = Paths.get(JavaTestUtil.getJavaTestDataPath(), "/inspection/dataFlow/codeanalysis/");
@Parameterized.Parameter
public String myFileName;
@Parameterized.Parameters(name = "{0}")
public static List<String> getData() throws IOException {
return Files.walk(PATH).filter(Files::isRegularFile).map(PATH::relativize).map(Path::toString).collect(Collectors.toList());
}
@Before
public void setUp() throws Exception {
super.setUp();
mockAnnotations();
}
@After
public void tearDown() throws Exception {
super.tearDown();
}
private void mockAnnotations() {
String template = "package " + PACKAGE_NAME + ";import java.lang.annotation.*;\n\n@Target(%s)public @interface %s {}";
myFixture.addClass(String.format(Locale.ROOT, template, "ElementType.TYPE_USE", "NotNull"));
myFixture.addClass(String.format(Locale.ROOT, template, "ElementType.TYPE_USE", "Nullable"));
myFixture.addClass(String.format(Locale.ROOT, template, "ElementType.TYPE", "DefaultNotNull"));
}
@Test
public void test() throws IOException {
Path path = PATH.resolve(myFileName);
String fileText = new String(Files.readAllBytes(path), CharsetToolkit.UTF8_CHARSET).replace("\r\n", "\n");
String stripped = fileText.replaceAll("/\\*ca-[a-z\\-]+\\*/", "");
PsiFile file = myFixture.configureByText(path.getFileName().toString(), stripped);
CodeAnalysisDataFlowInspection inspection = new CodeAnalysisDataFlowInspection();
ReadAction.run(() -> {
PsiElementVisitor visitor = inspection.buildVisitor(new ProblemsHolder(new InspectionManagerEx(getProject()), file, false), false);
PsiTreeUtil.processElements(file, e -> {
e.accept(visitor);
return true;
});
String actualText = inspection.getActualText(stripped);
if (!fileText.equals(actualText)) {
throw new FileComparisonFailure("Messages don't match", fileText, actualText, path.toString());
}
});
}
// Reports dataflow problems in code-analysis-conformant way
private static class CodeAnalysisDataFlowInspection extends DataFlowInspectionBase {
private final Map<PsiElement, String> actual = new LinkedHashMap<>();
@Override
protected void reportNullabilityProblems(DataFlowInspectionBase.ProblemReporter reporter,
List<NullabilityProblemKind.NullabilityProblem<?>> problems,
Map<PsiExpression, DataFlowInspectionBase.ConstantResult> expressions) {
for (NullabilityProblemKind.NullabilityProblem<?> problem : problems) {
PsiExpression expression = problem.getDereferencedExpression();
if (expression != null) {
actual.put(expression, "ca-nullable-to-not-null");
}
}
}
String getActualText(String stripped) {
Map<Integer, String> map = EntryStream.of(this.actual)
.mapKeys(e -> e.getTextRange().getStartOffset())
.mapValues(v -> "/*" + v + "*/")
.grouping(Collectors.joining());
return StreamEx.ofKeys(map).prepend(0).append(stripped.length())
.pairMap((prev, next) -> stripped.substring(prev, next) + map.getOrDefault(next, ""))
.joining();
}
}
}