[java-inspections] IDEA-14669 fixed: suggest adding specific non-null assertions from test frameworks

(cherry picked from commit 0d22d640ea4ee77a6bdfb5b4af504332e7456b68)

IJ-MR-150371

GitOrigin-RevId: 04a5fe47cf874ae78c5ad73282b55123f55e84cb
This commit is contained in:
Bartek Pacia
2024-11-27 17:59:39 +01:00
committed by intellij-monorepo-bot
parent 47a2170f17
commit 5569a2965d
14 changed files with 376 additions and 6 deletions

View File

@@ -1,10 +1,11 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// 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.codeInspection.dataFlow;
import com.intellij.codeInsight.daemon.impl.quickfix.UnwrapSwitchLabelFix;
import com.intellij.codeInsight.options.JavaInspectionButtons;
import com.intellij.codeInsight.options.JavaInspectionControls;
import com.intellij.codeInspection.*;
import com.intellij.codeInspection.AddAssertNonNullFromTestFrameworksFix.Variant;
import com.intellij.codeInspection.dataFlow.fix.*;
import com.intellij.codeInspection.nullable.NullableStuffInspection;
import com.intellij.codeInspection.options.OptPane;
@@ -140,9 +141,14 @@ public final class DataFlowInspection extends DataFlowInspectionBase {
if (isVolatileFieldReference(qualifier)) {
ContainerUtil.addIfNotNull(fixes, createIntroduceVariableFix());
}
else if (!alwaysNull && !SideEffectChecker.mayHaveSideEffects(qualifier)) {
else if (!alwaysNull && !SideEffectChecker.mayHaveSideEffects(qualifier)) {
String suffix = " != null";
if (PsiUtil.isAvailable(JavaFeature.ASSERTIONS, qualifier) && CodeBlockSurrounder.canSurround(expression)) {
Variant testFrameworkFixVariant = AddAssertNonNullFromTestFrameworksFix.isAvailable(expression);
if (testFrameworkFixVariant != null) {
fixes.add(new AddAssertNonNullFromTestFrameworksFix(qualifier, testFrameworkFixVariant));
}
else if (PsiUtil.isAvailable(JavaFeature.ASSERTIONS, qualifier) && CodeBlockSurrounder.canSurround(expression)) {
String replacement = ParenthesesUtils.getText(qualifier, ParenthesesUtils.EQUALITY_PRECEDENCE) + suffix;
fixes.add(new AddAssertStatementFix(replacement));
}
@@ -176,7 +182,9 @@ public final class DataFlowInspection extends DataFlowInspectionBase {
}
@Override
protected @NotNull List<@NotNull LocalQuickFix> createUnboxingNullableFixes(@NotNull PsiExpression qualifier, PsiElement anchor, boolean onTheFly) {
protected @NotNull List<@NotNull LocalQuickFix> createUnboxingNullableFixes(@NotNull PsiExpression qualifier,
PsiElement anchor,
boolean onTheFly) {
List<LocalQuickFix> result = new SmartList<>();
if (TypeConversionUtil.isBooleanType(qualifier.getType())) {
result.add(new ReplaceWithBooleanEqualsFix(qualifier));
@@ -218,4 +226,4 @@ public final class DataFlowInspection extends DataFlowInspectionBase {
JavaInspectionButtons.ButtonKind.NULLABILITY_ANNOTATIONS)
);
}
}
}

View File

@@ -0,0 +1,103 @@
// 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.codeInspection;
import com.intellij.codeInsight.TestFrameworks;
import com.intellij.codeInsight.intention.HighPriorityAction;
import com.intellij.java.JavaBundle;
import com.intellij.modcommand.ModPsiUpdater;
import com.intellij.modcommand.PsiUpdateModCommandQuickFix;
import com.intellij.openapi.project.Project;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.JavaCodeStyleManager;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.testIntegration.TestFramework;
import com.siyeh.ig.psiutils.CodeBlockSurrounder;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class AddAssertNonNullFromTestFrameworksFix extends PsiUpdateModCommandQuickFix implements HighPriorityAction {
private final String myText;
private final SmartPsiElementPointer<PsiExpression> myQualifierPointer;
private final Variant myVariant;
public AddAssertNonNullFromTestFrameworksFix(@NotNull PsiExpression qualifier, Variant variant) {
myText = qualifier.getText();
myQualifierPointer = SmartPointerManager.getInstance(qualifier.getProject()).createSmartPsiElementPointer(qualifier);
myVariant = variant;
}
public enum Variant {
JUNIT_3("JUnit 3", "assertNotNull"),
JUNIT_4("JUnit 4", "Assert.assertNotNull"),
JUNIT_5("JUnit 5", "Assertions.assertNotNull"),
TESTNG("TestNG", "Assert.assertNotNull");
/// Used only for presentation purposes.
public final String name;
/// Used only for presentation purposes.
public final String replacement;
Variant(String name, String replacement) {
this.name = name;
this.replacement = replacement;
}
}
@Override
public @NotNull String getName() {
return JavaBundle.message("inspection.testframework.assert.quickfix", myVariant.name, myVariant.replacement + "(" + myText + ")");
}
@Override
public @NotNull String getFamilyName() {
return JavaBundle.message("inspection.quickfix.assert.family");
}
@Override
protected void applyFix(@NotNull Project project, @NotNull PsiElement element, @NotNull ModPsiUpdater updater) {
PsiExpression qualifier = updater.getWritable(myQualifierPointer.getElement());
PsiExpression expr = PsiTreeUtil.getNonStrictParentOfType(qualifier, PsiExpression.class);
if (expr == null) return;
CodeBlockSurrounder surrounder = CodeBlockSurrounder.forExpression(expr);
if (surrounder == null) return;
CodeBlockSurrounder.SurroundResult result = surrounder.surround();
expr = result.getExpression();
PsiElement anchorElement = result.getAnchor();
// If the element before our qualifier is an inspection suppression comment, then we want to
// add assertion before this suppression comment so it's not accidentally disabled.
PsiElement prev = PsiTreeUtil.skipWhitespacesBackward(anchorElement);
if (prev instanceof PsiComment && JavaSuppressionUtil.getSuppressedInspectionIdsIn(prev) != null) {
anchorElement = prev;
}
String text = switch (myVariant) {
case JUNIT_3 -> "assertNotNull(" + myText + ")";
case JUNIT_4 -> "org.junit.Assert.assertNotNull(" + myText + ")";
case JUNIT_5 -> "org.junit.jupiter.api.Assertions.assertNotNull(" + myText + ")";
case TESTNG -> "org.testng.Assert.assertNotNull(" + myText + ")";
} + ";";
PsiStatement assertStatement = JavaPsiFacade.getElementFactory(project).createStatementFromText(text, expr);
PsiElement added = anchorElement.getParent().addBefore(assertStatement, anchorElement);
JavaCodeStyleManager.getInstance(project).shortenClassReferences(added);
}
public static @Nullable Variant isAvailable(@NotNull PsiExpression expression) {
PsiMethod containingMethod = PsiTreeUtil.getParentOfType(expression, PsiMethod.class);
if (containingMethod == null) return null;
PsiClass containingClass = containingMethod.getContainingClass();
if (containingClass == null) return null;
TestFramework detectedTestFramework = TestFrameworks.detectFramework(containingClass);
if (detectedTestFramework == null) return null;
return switch (detectedTestFramework.getName()) {
case "JUnit3" -> Variant.JUNIT_3;
case "JUnit4" -> Variant.JUNIT_4;
case "JUnit5" -> Variant.JUNIT_5;
case "TestNG" -> Variant.TESTNG;
default -> null;
};
}
}

View File

@@ -0,0 +1,20 @@
// "Assert with JUnit 3 'assertNotNull(s)'" "true-preview"
import junit.framework.TestCase;
import org.jetbrains.annotations.Nullable;
public class SomeJUnit3Test extends TestCase {
@Nullable
String getNullableString() {
double random = Math.random();
if (random > 0.75) return null;
if (random > 0.50) return "";
else return "bruh";
}
public void test() {
String s = getNullableString();
assertNotNull(s);
assertTrue(s.isEmpty());
}
}

View File

@@ -0,0 +1,19 @@
// "Assert with JUnit 4 'Assert.assertNotNull(s)'" "true-preview"
import org.jetbrains.annotations.Nullable;
import org.junit.Assert;
import org.junit.Test;
import static org.junit.Assert.assertTrue;
public final class SomeJUnit4Test {
@Nullable
native String getNullableString();
@Test
public void test() {
String s = getNullableString();
Assert.assertNotNull(s);
assertTrue(s.isEmpty());
}
}

View File

@@ -0,0 +1,20 @@
// "Assert with JUnit 4 'Assert.assertNotNull(s)'" "true-preview"
import org.jetbrains.annotations.Nullable;
import org.junit.Assert;
import org.junit.Test;
import static org.junit.Assert.assertTrue;
public final class SomeJUnit4Test {
@Nullable
native String getNullableString();
@Test
public void test() {
String s = getNullableString();
Assert.assertNotNull(s);
//noinspection SimplifiableConditionalExpression
assertTrue(s.isEmpty() ? true : false);
}
}

View File

@@ -0,0 +1,24 @@
// "Assert with JUnit 5 'Assertions.assertNotNull(s)'" "true-preview"
import org.jetbrains.annotations.Nullable;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class SomeJUnit5Test {
@Nullable
String getNullableString() {
double random = Math.random();
if (random > 0.75) return null;
if (random > 0.50) return "";
else return "bruh";
}
@Test
public void test() {
String s = getNullableString();
Assertions.assertNotNull(s);
assertTrue(s.isEmpty());
}
}

View File

@@ -0,0 +1,24 @@
// "Assert with TestNG 'Assert.assertNotNull(s)'" "true-preview"
import org.jetbrains.annotations.Nullable;
import org.testng.Assert;
import org.testng.annotations.Test;
import static org.testng.Assert.assertTrue;
public class SomeTestNGTest {
@Nullable
String getNullableString() {
double random = Math.random();
if (random > 0.75) return null;
if (random > 0.50) return "";
else return "bruh";
}
@Test
public void test() {
String s = getNullableString();
Assert.assertNotNull(s);
assertTrue(s.isEmpty());
}
}

View File

@@ -0,0 +1,19 @@
// "Assert with JUnit 3 'assertNotNull(s)'" "true-preview"
import junit.framework.TestCase;
import org.jetbrains.annotations.Nullable;
public class SomeJUnit3Test extends TestCase {
@Nullable
String getNullableString() {
double random = Math.random();
if (random > 0.75) return null;
if (random > 0.50) return "";
else return "bruh";
}
public void test() {
String s = getNullableString();
assertTrue(s.isEm<caret>pty());
}
}

View File

@@ -0,0 +1,17 @@
// "Assert with JUnit 4 'Assert.assertNotNull(s)'" "true-preview"
import org.jetbrains.annotations.Nullable;
import org.junit.Test;
import static org.junit.Assert.assertTrue;
public final class SomeJUnit4Test {
@Nullable
native String getNullableString();
@Test
public void test() {
String s = getNullableString();
assertTrue(s.isEm<caret>pty());
}
}

View File

@@ -0,0 +1,18 @@
// "Assert with JUnit 4 'Assert.assertNotNull(s)'" "true-preview"
import org.jetbrains.annotations.Nullable;
import org.junit.Test;
import static org.junit.Assert.assertTrue;
public final class SomeJUnit4Test {
@Nullable
native String getNullableString();
@Test
public void test() {
String s = getNullableString();
//noinspection SimplifiableConditionalExpression
assertTrue(s.isEm<caret>pty() ? true : false);
}
}

View File

@@ -0,0 +1,22 @@
// "Assert with JUnit 5 'Assertions.assertNotNull(s)'" "true-preview"
import org.jetbrains.annotations.Nullable;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class SomeJUnit5Test {
@Nullable
String getNullableString() {
double random = Math.random();
if (random > 0.75) return null;
if (random > 0.50) return "";
else return "bruh";
}
@Test
public void test() {
String s = getNullableString();
assertTrue(s.isEm<caret>pty());
}
}

View File

@@ -0,0 +1,22 @@
// "Assert with TestNG 'Assert.assertNotNull(s)'" "true-preview"
import org.jetbrains.annotations.Nullable;
import org.testng.annotations.Test;
import static org.testng.Assert.assertTrue;
public class SomeTestNGTest {
@Nullable
String getNullableString() {
double random = Math.random();
if (random > 0.75) return null;
if (random > 0.50) return "";
else return "bruh";
}
@Test
public void test() {
String s = getNullableString();
assertTrue(s.isEm<caret>pty());
}
}

View File

@@ -0,0 +1,53 @@
// 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.codeInsight.daemon.impl.quickfix;
import com.intellij.codeInsight.daemon.quickFix.LightQuickFixParameterizedTestCase;
import com.intellij.codeInspection.LocalInspectionTool;
import com.intellij.codeInspection.dataFlow.DataFlowInspection;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.roots.ContentEntry;
import com.intellij.openapi.roots.ModifiableRootModel;
import com.intellij.project.IntelliJProjectConfiguration;
import com.intellij.testFramework.LightProjectDescriptor;
import com.intellij.testFramework.PsiTestUtil;
import com.intellij.testFramework.fixtures.DefaultLightProjectDescriptor;
import com.intellij.util.ArrayUtil;
import org.jetbrains.annotations.NotNull;
public class AddAssertNonNullFromTestFrameworksFixTest extends LightQuickFixParameterizedTestCase {
private static final LightProjectDescriptor ourProjectDescriptor = new DefaultLightProjectDescriptor() {
@Override
public void configureModule(@NotNull Module module, @NotNull ModifiableRootModel model, @NotNull ContentEntry contentEntry) {
super.configureModule(module, model, contentEntry);
IntelliJProjectConfiguration.LibraryRoots junit3Library = IntelliJProjectConfiguration.getProjectLibrary("JUnit3");
PsiTestUtil.addLibrary(model, "JUnit3", "", ArrayUtil.toStringArray(junit3Library.getClassesPaths()));
IntelliJProjectConfiguration.LibraryRoots junit4Library = IntelliJProjectConfiguration.getProjectLibrary("JUnit4");
PsiTestUtil.addLibrary(model, "JUnit4", "", ArrayUtil.toStringArray(junit4Library.getClassesPaths()));
IntelliJProjectConfiguration.LibraryRoots junit5Library = IntelliJProjectConfiguration.getProjectLibrary("JUnit5");
PsiTestUtil.addLibrary(model, "JUnit5", "", ArrayUtil.toStringArray(junit5Library.getClassesPaths()));
IntelliJProjectConfiguration.LibraryRoots testNGLibrary = IntelliJProjectConfiguration.getProjectLibrary("TestNG");
PsiTestUtil.addLibrary(model, "TestNG", "", ArrayUtil.toStringArray(testNGLibrary.getClassesPaths()));
DefaultLightProjectDescriptor.addJetBrainsAnnotations(model);
}
};
@Override
public @NotNull LightProjectDescriptor getProjectDescriptor() {
return ourProjectDescriptor;
}
@Override
protected LocalInspectionTool @NotNull [] configureLocalInspectionTools() {
return new LocalInspectionTool[]{new DataFlowInspection()};
}
@Override
protected @NotNull String getBasePath() {
return "/codeInsight/daemonCodeAnalyzer/quickFix/addAssertNonNullFromTestFrameworks";
}
}

View File

@@ -371,6 +371,7 @@ include.accessors=&Include Accessors
infer.nullity.progress=Post-processing results\u2026
insert.override.annotation=Insert @&Override annotation
inspection.assert.quickfix=Assert ''{0}''
inspection.testframework.assert.quickfix=Assert with {0} ''{1}''
inspection.capturing.cleaner=Runnable passed to Cleaner.register() captures ''{0}'' reference
inspection.capturing.cleaner.description=Cleaner captures object reference
inspection.cast.can.be.removed.narrowing.variable.type.fix.family.name=Change variable type and remove cast
@@ -1984,4 +1985,4 @@ command.completion.inline.text=Inline
command.completion.project.tool.text=Open Project tool window
command.completion.recent.files.text=Open recent files popup
advanced.setting.java.show.irrelevant.templates.in.source.roots=Show irrelevant New File templates in Java source roots
advanced.setting.java.show.irrelevant.templates.in.source.roots=Show irrelevant New File templates in Java source roots