IDEA-376483 [java]: Quickfixes for warnings from NullAway with warning suppression

This merge request implements console filter that locates lines with NullAway checker (https://github.com/uber/NullAway) warnings
and adds add @SuppressWarning inlay button at the end of those lines.
Each button action adds {@linkplain SuppressWarnings} annotation to the source code to silence relevant warning.

Merge-request: IJ-MR-185390
Merged-by: Marcin Mikosik <marcin.mikosik@jetbrains.com>

GitOrigin-RevId: 9b73c253538f7de6ac9209fa5f80f179514c2804
This commit is contained in:
Marcin Mikosik
2026-01-09 10:46:50 +00:00
committed by intellij-monorepo-bot
parent 2132de9726
commit a8cc5765a2
8 changed files with 846 additions and 0 deletions
@@ -1641,6 +1641,7 @@
<trigramIndexFilterExcludeExtension implementation="com.intellij.java.JavaLibrarySourcesExcludingIndexFilterExtension"/>
<virtualFileCustomDataProvider implementation="com.intellij.ide.JavaCodeStyleSettingsCustomDataSynchronizer"/>
<consoleFilterProvider implementation="com.intellij.java.impl.nullaway.NullAwayFilterProvider"/>
</extensions>
<extensions defaultExtensionNs="org.jetbrains">
@@ -41,3 +41,6 @@ referenced.element.out.of.project={0} is out of project
unable.to.migrate.statement.to.enum=Unable to migrate statement to enum constant.{0}
codestyle.settings.extractor.command.name=CodeStyleSettings Extractor
nullaway.suppress.command.name=Suppress NullAway Problem
nullaway.suppress.inlay.text=Add @SuppressWarnings
nullaway.suppress.error.no.target=Cannot find element to suppress
@@ -0,0 +1,32 @@
// 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.java.impl.nullaway;
import com.intellij.execution.filters.Filter;
import com.intellij.util.concurrency.annotations.RequiresReadLock;
import org.jetbrains.annotations.NotNullByDefault;
import org.jetbrains.annotations.Nullable;
import java.util.List;
/// Console filter that locates lines with [NullAway checker](https://github.com/uber/NullAway) warnings
/// and adds `add @SuppressWarning` inlay button at the end of those lines.
/// Each button action adds {@linkplain SuppressWarnings} annotation to the source code to silence relevant warning.
/// The exact location where annotation is added is calculated based on information from the console line: file-path, line number,
/// type of NullAway warning.
@NotNullByDefault
class NullAwayFilter implements Filter {
@Override
@RequiresReadLock
public @Nullable Result applyFilter(String line, int entireLength) {
var nullAwayProblem = NullAwayProblem.fromLogLine(line);
if (nullAwayProblem == null) return null;
var result = new Result(List.of(new NullAwayInlayProvider(entireLength - 1, entireLength - 1, nullAwayProblem)));
result.setNextAction(NextAction.CONTINUE_FILTERING);
return result;
}
@Override
public boolean isDumbAware() {
return true;
}
}
@@ -0,0 +1,51 @@
// 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.java.impl.nullaway;
import com.intellij.compiler.CompilerConfiguration;
import com.intellij.execution.filters.ConsoleFilterProvider;
import com.intellij.execution.filters.Filter;
import com.intellij.java.library.JavaLibraryUtil;
import com.intellij.openapi.application.ReadAction;
import com.intellij.openapi.module.ModuleManager;
import com.intellij.openapi.project.Project;
import org.jetbrains.annotations.NotNullByDefault;
/// Provides [NullAwayFilter] when [NullAway checker](https://github.com/uber/NullAway) is configured in the project.
@NotNullByDefault
class NullAwayFilterProvider implements ConsoleFilterProvider {
@Override
public Filter[] getDefaultFilters(Project project) {
if (hasNullAwayPlugin(project)) {
return new Filter[]{new NullAwayFilter()};
}
else {
return Filter.EMPTY_ARRAY;
}
}
/// Returns true when NullAway plugin is configured.
/// Checks whether NullAway library is added to the project or the annotation processor path contains NullAway library.
/// Note:
/// - In maven projects NullAway library is not added to libraries even when null-away is configured as a plugin
/// for error-prone annotation processor in a maven build.
/// - In bazel projects NullAway library is not added to the annotation processor path even when NullAway is configured as a plugin
/// for error-prone annotation processor in a bazel build.
///
/// @return true when NullAway plugin is configured.
private static boolean hasNullAwayPlugin(Project project) {
var hasNullAwayLib = ReadAction.compute(() -> JavaLibraryUtil.hasLibraryJar(project, "com.uber.nullaway:nullaway"));
if (hasNullAwayLib) return true;
var compilerConfiguration = CompilerConfiguration.getInstance(project);
if (!compilerConfiguration.isAnnotationProcessorsEnabled()) return false;
for (var module : ModuleManager.getInstance(project).getModules()) {
var config = compilerConfiguration.getAnnotationProcessingConfiguration(module);
if (config.isEnabled()) {
if (config.getProcessorPath().contains("nullaway")) {
return true;
}
}
}
return false;
}
}
@@ -0,0 +1,113 @@
// 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.java.impl.nullaway;
import com.intellij.codeInsight.hint.HintManager;
import com.intellij.codeInsight.hints.presentation.InlayButtonPresentationFactory;
import com.intellij.codeInsight.hints.presentation.PresentationFactory;
import com.intellij.codeInsight.hints.presentation.PresentationRenderer;
import com.intellij.codeInspection.JavaSuppressionUtil;
import com.intellij.execution.filters.Filter;
import com.intellij.execution.impl.InlayProvider;
import com.intellij.java.JavaPluginDisposable;
import com.intellij.modcommand.ActionContext;
import com.intellij.modcommand.ModCommand;
import com.intellij.modcommand.ModCommandExecutor;
import com.intellij.modcommand.ModNavigate;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.application.ReadAction;
import com.intellij.openapi.editor.DefaultLanguageHighlighterColors;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.EditorCustomElementRenderer;
import com.intellij.openapi.project.DumbService;
import com.intellij.openapi.project.Project;
import com.intellij.pom.java.JavaFeature;
import com.intellij.psi.PsiAnnotation;
import com.intellij.psi.PsiModifierList;
import com.intellij.psi.PsiModifierListOwner;
import com.intellij.psi.util.PsiUtil;
import com.intellij.refactoring.RefactorJBundle;
import com.intellij.util.concurrency.AppExecutorUtil;
import com.intellij.util.concurrency.annotations.RequiresReadLock;
import org.jetbrains.annotations.NotNullByDefault;
import org.jetbrains.annotations.Nullable;
import static com.intellij.codeInspection.JavaSuppressionUtil.SUPPRESS_INSPECTIONS_ANNOTATION_NAME;
@NotNullByDefault
class NullAwayInlayProvider extends Filter.ResultItem implements InlayProvider {
private final NullAwayProblem problem;
NullAwayInlayProvider(int startOffset, int endOffset, NullAwayProblem problem) {
super(startOffset, endOffset, null);
this.problem = problem;
}
@Override
@RequiresReadLock
public EditorCustomElementRenderer createInlayRenderer(Editor editor) {
var factory = new InlayButtonPresentationFactory(
editor,
new PresentationFactory(editor),
DefaultLanguageHighlighterColors.INLAY_BUTTON_DEFAULT,
DefaultLanguageHighlighterColors.INLAY_BUTTON_HOVERED,
DefaultLanguageHighlighterColors.INLAY_BUTTON_FOCUSED);
var inlayPresentation = factory.smallText(RefactorJBundle.message("nullaway.suppress.inlay.text"))
.onClick((event, point) -> scheduleFixAction(editor, problem))
.build();
return new PresentationRenderer(inlayPresentation);
}
private static void scheduleFixAction(Editor editor, NullAwayProblem problem) {
Project project = editor.getProject();
if (project == null) return;
ReadAction.nonBlocking(() -> findSuppressionTarget(project, problem))
.expireWith(JavaPluginDisposable.getInstance(project))
.finishOnUiThread(ModalityState.current(), target -> applyFix(editor, problem, target))
.submit(AppExecutorUtil.getAppExecutorService());
}
private static @Nullable PsiModifierListOwner findSuppressionTarget(Project project, NullAwayProblem problem) {
var target = problem.findSuppressionTarget(project);
if (target == null) return null;
if (!PsiUtil.isAvailable(JavaFeature.ANNOTATIONS, target)) return null;
return target;
}
private static void applyFix(Editor editor, NullAwayProblem problem, @Nullable PsiModifierListOwner target) {
if (target == null) {
if (!editor.isDisposed()) {
HintManager.getInstance().showErrorHint(editor, RefactorJBundle.message("nullaway.suppress.error.no.target"));
}
}
else {
addSuppression(target, problem.kind().nameToSuppress());
}
}
private static void addSuppression(PsiModifierListOwner modifierListOwner, String namedToSuppress) {
ModCommandExecutor.executeInteractively(
ActionContext.from(null, modifierListOwner.getContainingFile()),
RefactorJBundle.message("nullaway.suppress.command.name"),
null,
() -> createNavigateCommand(modifierListOwner).andThen(createSuppressAndHighlightCommand(modifierListOwner, namedToSuppress)));
}
private static ModNavigate createNavigateCommand(PsiModifierListOwner modifierListOwner) {
return new ModNavigate(modifierListOwner.getContainingFile().getVirtualFile(), -1, -1,
modifierListOwner.getTextRange().getStartOffset());
}
private static ModCommand createSuppressAndHighlightCommand(PsiModifierListOwner modifierListOwner, String namedToSuppress) {
return ModCommand.psiUpdate(modifierListOwner, (e, updater) -> {
Project project = e.getProject();
DumbService.getInstance(project).withAlternativeResolveEnabled(() -> {
JavaSuppressionUtil.addSuppressAnnotation(project, e, e, namedToSuppress);
PsiModifierList modifierList = e.getModifierList();
if (modifierList == null) return;
PsiAnnotation annotation = modifierList.findAnnotation(SUPPRESS_INSPECTIONS_ANNOTATION_NAME);
if (annotation == null) return;
updater.highlight(annotation);
});
});
}
}
@@ -0,0 +1,379 @@
// 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.java.impl.nullaway;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.psi.*;
import com.intellij.psi.util.PsiTreeUtil;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNullByDefault;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.VisibleForTesting;
import java.nio.file.Path;
import java.util.EnumSet;
import java.util.List;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static com.intellij.java.impl.nullaway.NullAwayProblem.Kind.TargetKind.FIELD;
import static com.intellij.java.impl.nullaway.NullAwayProblem.Kind.TargetKind.METHOD;
/// NullAway problem reported in the console.
///
/// @param filePath path to the file containing the problem
/// @param lineNumber zero-based line number containing the problem
/// @param kind type of the problem
@NotNullByDefault
@VisibleForTesting
public record NullAwayProblem(Path filePath, int lineNumber, Kind kind) {
private static final Pattern NULLAWAY_LOG_LINE_PATTERN =
Pattern.compile(
"^(\\[(?:ERROR|WARNING|INFO)] )?(?<path>.+?):[(\\[]?(?<line>\\d+)([:,](?<column>\\d++))?[)\\]]?(?:: | )(?:(?:error|warning): )?\\[NullAway] (?<message>.+)");
@VisibleForTesting
public static @Nullable NullAwayProblem fromLogLine(String line) {
if (!line.contains("[NullAway]")) return null;
Matcher matcher = NULLAWAY_LOG_LINE_PATTERN.matcher(line);
if (!matcher.find()) return null;
String path = matcher.group("path");
Kind kind = Kind.fromMessage(matcher.group("message"));
if (kind == null) return null;
try {
int oneBasedLineNumber = Integer.parseInt(matcher.group("line"));
return new NullAwayProblem(Path.of(path), oneBasedLineNumber - 1, kind);
}
catch (NumberFormatException e) {
return null;
}
}
@VisibleForTesting
public @Nullable PsiModifierListOwner findSuppressionTarget(Project project) {
var psiJavaFile = findFileWithProblem(project);
if (psiJavaFile == null) return null;
return findFaultyElement(psiJavaFile);
}
private @Nullable PsiJavaFile findFileWithProblem(Project project) {
Path path = resolvedPath(project);
if (path == null) return null;
var virtualFile = LocalFileSystem.getInstance().findFileByNioFile(path);
if (virtualFile == null) return null;
var psiFile = PsiManager.getInstance(project).findFile(virtualFile);
if (!(psiFile instanceof PsiJavaFile psiJavaFile) || psiFile instanceof PsiCompiledElement) return null;
return psiJavaFile;
}
private @Nullable Path resolvedPath(Project project) {
if (filePath.isAbsolute()) return filePath;
String basePath = project.getBasePath();
if (basePath == null) return null;
return Path.of(basePath).resolve(filePath);
}
@Nullable
private PsiModifierListOwner findFaultyElement(PsiJavaFile file) {
Project project = file.getProject();
Document document = PsiDocumentManager.getInstance(project).getDocument(file);
if (document == null) return null;
int lineIndex = Math.max(0, lineNumber);
if (lineIndex >= document.getLineCount()) return null;
int lineStartOffset = document.getLineStartOffset(lineIndex);
int lineEndOffset = document.getLineEndOffset(lineIndex);
var lineRange = TextRange.create(lineStartOffset, lineEndOffset);
PsiElement atLineStart = file.findElementAt(lineStartOffset);
PsiClass psiClass = PsiTreeUtil.getParentOfType(atLineStart, PsiClass.class, false);
if (psiClass == null) return null;
if (kind.targetKinds().contains(FIELD)) {
PsiField[] fields = psiClass.getFields();
var field = findElementInLine(fields, lineRange);
if (field != null) return field;
}
if (kind.targetKinds().contains(METHOD)) {
var method = findElementInLine(psiClass.getMethods(), lineRange);
if (method != null) return method;
method = findElementInLine(psiClass.getConstructors(), lineRange);
if (method != null) return method;
PsiClassInitializer classInitializer = PsiTreeUtil.getParentOfType(atLineStart, PsiClassInitializer.class, false);
if (classInitializer != null && PsiTreeUtil.getParentOfType(classInitializer, PsiClass.class, false) == psiClass) {
return psiClass;
}
}
return null;
}
private static <T extends PsiElement> @Nullable T findElementInLine(T[] psiElements,
TextRange lineRange) {
for (T psiElement : psiElements) {
if (lineRange.intersects(psiElement.getTextRange())) {
return psiElement;
}
}
return null;
}
@ApiStatus.Internal
public record Kind(Predicate<String> matchingPredicate, String nameToSuppress, EnumSet<TargetKind> targetKinds) {
Kind(String matchingRegex, String nameToSuppress, TargetKind elementType, TargetKind... rest) {
this(Pattern.compile(matchingRegex).asMatchPredicate(), nameToSuppress, EnumSet.of(elementType, rest));
}
public enum TargetKind {
METHOD, FIELD
}
public static final Kind NON_NULL_FIELD_NOT_INITIALIZED = new Kind(
"^@NonNull field (.+) not initialized.*",
"NullAway.Init",
FIELD
);
public static final Kind INITIALIZER_DOES_NOT_GUARANTEE_INITIALIZATION = new Kind(
"^initializer method does not guarantee @NonNull field (.+) \\(line \\d+\\) is initialized along all control-flow paths.*",
"NullAway.Init",
METHOD
);
public static final Kind DEREFERENCED_EXPRESSION_IS_NULLABLE = new Kind(
"^dereferenced expression .* is @Nullable.*",
"NullAway",
METHOD,
FIELD
);
public static final Kind RETURNING_NULLABLE_FROM_NONNULL_METHOD = new Kind(
"^returning @Nullable expression from method with @NonNull return type.*",
"NullAway",
METHOD
);
public static final Kind PASSING_NULLABLE_PARAMETER_WHERE_NONNULL_REQUIRED = new Kind(
"^passing @Nullable parameter '.+' where @NonNull is required.*",
"NullAway",
METHOD,
FIELD
);
public static final Kind METHOD_RETURNS_NULLABLE_BUT_SUPERCLASS_METHOD_NONNULL = new Kind(
"^method returns @Nullable, but superclass method .* returns @NonNull.*",
"NullAway",
METHOD
);
public static final Kind ASSIGNING_NULLABLE_TO_NONNULL_FIELD = new Kind(
"^assigning @Nullable expression to @NonNull field.*",
"NullAway",
METHOD,
FIELD
);
public static final Kind REFERENCED_METHOD_RETURNS_NULLABLE = new Kind(
"^referenced method returns @Nullable, but functional interface method .* returns @NonNull.*",
"NullAway",
METHOD
);
public static final Kind UNBOUND_INSTANCE_METHOD_REFERENCE_FIRST_PARAMETER_NULLABLE = new Kind(
"^unbound instance method reference cannot be used, as first parameter .* is @Nullable.*",
"NullAway",
METHOD
);
public static final Kind PARAMETER_IS_NONNULL_BUT_PARAMETER_IN_SUPERCLASS_NULLABLE = new Kind(
"^parameter .* is @NonNull, but parameter in .* is @Nullable.*",
"NullAway",
METHOD
);
public static final Kind UNBOXING_OF_NULLABLE_VALUE = new Kind(
"^unboxing of a @Nullable value.*",
"NullAway",
METHOD
);
public static final Kind READ_OF_NONNULL_FIELD_BEFORE_INIT = new Kind(
"^read of @NonNull field .* before initialization.*",
"NullAway",
METHOD
);
public static final Kind ENHANCED_FOR_EXPRESSION_NULLABLE = new Kind(
"^enhanced-for expression .* is @Nullable.*",
"NullAway",
METHOD
);
public static final Kind SYNCHRONIZED_BLOCK_EXPRESSION_NULLABLE = new Kind(
"^synchronized block expression \".+\" is @Nullable.*",
"NullAway",
METHOD
);
public static final Kind NONNULL_STATIC_FIELD_NOT_INITIALIZED = new Kind(
"^@NonNull static field .* not initialized.*",
"NullAway.Init",
FIELD
);
public static final Kind PASSING_NONNULL_TO_CAST_TO_NONNULL = new Kind(
"^passing known @NonNull parameter '.+' to CastToNonNullMethod \\(.*\\) .*",
"NullAway",
METHOD
);
public static final Kind INVOKING_GET_ON_EMPTY_OPTIONAL = new Kind(
"^Invoking get\\(\\) on possibly empty Optional .*",
"NullAway",
METHOD
);
public static final Kind SWITCH_EXPRESSION_NULLABLE = new Kind(
"^switch expression .* is @Nullable.*",
"NullAway",
METHOD
);
public static final Kind METHOD_ANNOTATED_WITH_ENSURES_NONNULL_BUT_FAILS = new Kind(
"^Method is annotated with @EnsuresNonNull but fails to ensure .*",
"NullAway",
METHOD
);
public static final Kind METHOD_ANNOTATED_WITH_ENSURES_NONNULL_IF_BUT_DOES_NOT_ENSURE = new Kind(
"^Method is annotated with @EnsuresNonNullIf but does not ensure fields .*",
"NullAway",
METHOD
);
public static final Kind EXPECTED_STATIC_FIELD_NONNULL_DUE_TO_REQUIRES_NONNULL = new Kind(
"^Expected static field .* to be non-null at call site due to @RequiresNonNull annotation on invoked method.*",
"NullAway",
METHOD
);
public static final Kind EXPECTED_FIELD_NONNULL_DUE_TO_REQUIRES_NONNULL = new Kind(
"^Expected field .* to be non-null at call site due to @RequiresNonNull annotation on invoked method.*",
"NullAway",
METHOD
);
public static final Kind POSTCONDITION_INHERITANCE_VIOLATED = new Kind(
"^postcondition inheritance is violated, this method must guarantee that all fields .* are @NonNull .*",
"NullAway",
METHOD
);
public static final Kind PRECONDITION_INHERITANCE_VIOLATED = new Kind(
"^precondition inheritance is violated, method in child class cannot have a stricter precondition .*",
"NullAway",
METHOD
);
public static final Kind TYPE_ARGUMENT_CANNOT_BE_NULLABLE = new Kind(
"^Type argument cannot be @Nullable, as method .*'s type variable .* is not @Nullable.*",
"NullAway",
METHOD
);
public static final Kind GENERIC_TYPE_PARAMETER_CANNOT_BE_NULLABLE = new Kind(
"^Generic type parameter cannot be @Nullable, as type variable .* of type .* does not have a @Nullable upper bound.*",
"NullAway",
METHOD
);
public static final Kind INCOMPATIBLE_TYPES_GENERIC = new Kind(
"^incompatible types: .* cannot be converted to .*",
"NullAway",
METHOD
);
public static final Kind CONDITIONAL_EXPRESSION_TYPE_MISMATCH = new Kind(
"^Conditional expression must have type .* but the sub-expression has type .*",
"NullAway",
METHOD
);
public static final Kind METHOD_RETURNS_GENERIC_WITH_MISMATCHED_NULLABILITY = new Kind(
"^Method returns .*, but overridden method returns .*, which has mismatched type parameter nullability.*",
"NullAway",
METHOD
);
public static final Kind PARAMETER_TYPE_GENERIC_WITH_MISMATCHED_NULLABILITY = new Kind(
"^Parameter has type .*, but overridden method has parameter type .*, which has mismatched type parameter nullability.*",
"NullAway",
METHOD
);
public static final Kind WRITING_NULLABLE_INTO_NONNULL_ARRAY = new Kind(
"^Writing @Nullable expression into array with @NonNull contents.*",
"NullAway",
METHOD
);
public static final Kind FAILED_TO_INFER_TYPE_ARGUMENT_NULLABILITY = new Kind(
"^Failed to infer type argument nullability for call .*",
"NullAway",
METHOD
);
public static final Kind TYPE_USE_NULLABILITY_ON_WRONG_NESTED_CLASS_LEVEL = new Kind(
"^Type-use nullability annotations should be applied on inner class.*",
"NullAway",
METHOD
);
private static final List<Kind> ALL_KINDS =
List.of(NON_NULL_FIELD_NOT_INITIALIZED,
INITIALIZER_DOES_NOT_GUARANTEE_INITIALIZATION,
DEREFERENCED_EXPRESSION_IS_NULLABLE,
RETURNING_NULLABLE_FROM_NONNULL_METHOD,
PASSING_NULLABLE_PARAMETER_WHERE_NONNULL_REQUIRED,
METHOD_RETURNS_NULLABLE_BUT_SUPERCLASS_METHOD_NONNULL,
ASSIGNING_NULLABLE_TO_NONNULL_FIELD,
REFERENCED_METHOD_RETURNS_NULLABLE,
UNBOUND_INSTANCE_METHOD_REFERENCE_FIRST_PARAMETER_NULLABLE,
PARAMETER_IS_NONNULL_BUT_PARAMETER_IN_SUPERCLASS_NULLABLE,
UNBOXING_OF_NULLABLE_VALUE,
READ_OF_NONNULL_FIELD_BEFORE_INIT,
ENHANCED_FOR_EXPRESSION_NULLABLE,
SYNCHRONIZED_BLOCK_EXPRESSION_NULLABLE,
NONNULL_STATIC_FIELD_NOT_INITIALIZED,
PASSING_NONNULL_TO_CAST_TO_NONNULL,
INVOKING_GET_ON_EMPTY_OPTIONAL,
SWITCH_EXPRESSION_NULLABLE,
METHOD_ANNOTATED_WITH_ENSURES_NONNULL_BUT_FAILS,
METHOD_ANNOTATED_WITH_ENSURES_NONNULL_IF_BUT_DOES_NOT_ENSURE,
EXPECTED_STATIC_FIELD_NONNULL_DUE_TO_REQUIRES_NONNULL,
EXPECTED_FIELD_NONNULL_DUE_TO_REQUIRES_NONNULL,
POSTCONDITION_INHERITANCE_VIOLATED,
PRECONDITION_INHERITANCE_VIOLATED,
TYPE_ARGUMENT_CANNOT_BE_NULLABLE,
GENERIC_TYPE_PARAMETER_CANNOT_BE_NULLABLE,
INCOMPATIBLE_TYPES_GENERIC,
CONDITIONAL_EXPRESSION_TYPE_MISMATCH,
METHOD_RETURNS_GENERIC_WITH_MISMATCHED_NULLABILITY,
PARAMETER_TYPE_GENERIC_WITH_MISMATCHED_NULLABILITY,
WRITING_NULLABLE_INTO_NONNULL_ARRAY,
FAILED_TO_INFER_TYPE_ARGUMENT_NULLABILITY,
TYPE_USE_NULLABILITY_ON_WRONG_NESTED_CLASS_LEVEL);
private static @Nullable Kind fromMessage(String message) {
for (Kind kind : ALL_KINDS) {
if (kind.matchingPredicate.test(message)) {
return kind;
}
}
return null;
}
}
}
@@ -0,0 +1,96 @@
// 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.java.impl.nullaway;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiField;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiMethod;
import com.intellij.testFramework.fixtures.JavaCodeInsightFixtureTestCase;
import org.intellij.lang.annotations.Language;
import org.jetbrains.annotations.Nullable;
import org.junit.jupiter.api.Assertions;
import java.nio.file.Path;
import static com.intellij.java.impl.nullaway.NullAwayProblem.Kind.ASSIGNING_NULLABLE_TO_NONNULL_FIELD;
public class NullAwayProblemFindTargetTest extends JavaCodeInsightFixtureTestCase {
public void testFindFieldTarget() {
Path filePath = configureFile(
"""
public class MyClass {
private String targetField;
}
""");
var nullAwayProblem = new NullAwayProblem(filePath, 1, ASSIGNING_NULLABLE_TO_NONNULL_FIELD);
assertFieldTargetFound(nullAwayProblem, "targetField");
}
public void testFindMethodTarget() {
Path filePath = configureFile(
"""
public class MyClass {
public String someMethod(String s) {
return s;
}
}
""");
var nullAwayProblem = new NullAwayProblem(filePath, 2, ASSIGNING_NULLABLE_TO_NONNULL_FIELD);
assertMethodTargetFound(nullAwayProblem, "someMethod");
}
public void testFindClassTargetForStaticInitializer() {
Path filePath = configureFile(
"""
public class MyClass {
String nullable = null;
@NonNull nonNull;
static {
nonNull = nullable;
}
}
""");
var nullAwayProblem = new NullAwayProblem(filePath, 4, ASSIGNING_NULLABLE_TO_NONNULL_FIELD);
assertClassTargetFound(nullAwayProblem, "MyClass");
}
private @Nullable Path configureFile(@Language("JAVA") String text) {
PsiFile file = myFixture.configureByText("MyClass.java", text);
String canonicalPath = file.getVirtualFile().getCanonicalPath();
return canonicalPath != null ? Path.of(canonicalPath) : null;
}
private void assertFieldTargetFound(NullAwayProblem nullAwayProblem, String fieldName) {
var target = nullAwayProblem.findSuppressionTarget(getProject());
if (target instanceof PsiField psiField) {
assertEquals(fieldName, psiField.getName());
}
else {
Assertions.fail("Expected target to be of PsiField type but got " + (target == null ? "null" : target.getClass().getName()));
}
}
private void assertMethodTargetFound(NullAwayProblem nullAwayProblem, String methodName) {
var target = nullAwayProblem.findSuppressionTarget(getProject());
if (target instanceof PsiMethod psiMethod) {
assertEquals(methodName, psiMethod.getName());
}
else {
Assertions.fail("Expected target to be of PsiMethod type but got " + (target == null ? "null" : target.getClass().getName()));
}
}
private void assertClassTargetFound(NullAwayProblem nullAwayProblem, String className) {
var target = nullAwayProblem.findSuppressionTarget(getProject());
if (target instanceof PsiClass psiClass) {
assertEquals(className, psiClass.getName());
}
else {
Assertions.fail("Expected target to be of PsiClass type but got " + (target == null ? "null" : target.getClass().getName()));
}
}
}
@@ -0,0 +1,171 @@
// 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.java.impl.nullaway;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.nio.file.Path;
import java.util.List;
import static com.intellij.java.impl.nullaway.NullAwayProblem.Kind.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class NullAwayProblemTest {
@ParameterizedTest
@MethodSource
void filePathParsedCorrectly(String logLine, String expectedFilePath) {
NullAwayProblem problem = NullAwayProblem.fromLogLine(logLine);
assertEquals(Path.of(expectedFilePath), problem.filePath());
}
public static List<Arguments> filePathParsedCorrectly() {
return List.of(
// Gradle format
Arguments.of(
"/full/path/MyClass.java:10: error: [NullAway] @NonNull field myValue not initialized",
"/full/path/MyClass.java"),
Arguments.of(
"/full/path/AClass.java:11: warning: [NullAway] @NonNull field someValue not initialized",
"/full/path/AClass.java"),
// Maven format
Arguments.of(
"/full/path/MyClass.java:[100,11] [NullAway] @NonNull field value3 not initialized",
"/full/path/MyClass.java"),
Arguments.of(
"[WARNING] /full/path/AClass.java:[102,11] [NullAway] @NonNull field valueX not initialized",
"/full/path/AClass.java")
);
}
@ParameterizedTest
@MethodSource
void lineNumberParsedCorrectly(String logLine, int expectedLineNumber) {
NullAwayProblem problem = NullAwayProblem.fromLogLine(logLine);
assertEquals(expectedLineNumber, problem.lineNumber());
}
public static List<Arguments> lineNumberParsedCorrectly() {
return List.of(
// gradle
Arguments.of("/full/path/MyClass.java:10: error: [NullAway] @NonNull field myValue not initialized", 9),
Arguments.of("/full/path/MyClass.java:10:12: error: [NullAway] @NonNull field myValue not initialized", 9),
// maven
Arguments.of("/full/path/MyClass.java:[100,11] [NullAway] @NonNull field value3 not initialized", 99),
Arguments.of("/full/path/MyClass.java:[101] [NullAway] @NonNull field value3 not initialized", 100),
Arguments.of("[WARNING] /full/path/AClass.java:[102,11] [NullAway] @NonNull field valueX not initialized", 101)
);
}
@ParameterizedTest
@MethodSource
void kindMatchedCorrectly(String logLine, NullAwayProblem.Kind expectedKind) {
NullAwayProblem problem = NullAwayProblem.fromLogLine(logLine);
assertEquals(expectedKind, problem.kind());
}
public static List<Arguments> kindMatchedCorrectly() {
return List.of(
Arguments.of(
"/full/path/MyClass.java:10: error: [NullAway] @NonNull field myValue not initialized",
NON_NULL_FIELD_NOT_INITIALIZED),
Arguments.of(
"/full/path/MyClass.java:20: error: [NullAway] initializer method does not guarantee @NonNull field fieldA (line 33) is initialized along all control-flow paths (remember to check for exceptions or early returns).",
INITIALIZER_DOES_NOT_GUARANTEE_INITIALIZATION),
Arguments.of(
"/full/path/MyClass.java:30: error: [NullAway] dereferenced expression data is @Nullable",
DEREFERENCED_EXPRESSION_IS_NULLABLE),
Arguments.of(
"/full/path/MyClass.java:40: error: [NullAway] returning @Nullable expression from method with @NonNull return type",
RETURNING_NULLABLE_FROM_NONNULL_METHOD),
Arguments.of(
"/full/path/MyClass.java:50: error: [NullAway] passing @Nullable parameter 'foo' where @NonNull is required",
PASSING_NULLABLE_PARAMETER_WHERE_NONNULL_REQUIRED),
Arguments.of(
"/full/path/MyClass.java:60: error: [NullAway] method returns @Nullable, but superclass method org.testcases.SuperClass.getstring() returns @NonNull",
METHOD_RETURNS_NULLABLE_BUT_SUPERCLASS_METHOD_NONNULL),
Arguments.of(
"/full/path/MyClass.java:70: error: [NullAway] assigning @Nullable expression to @NonNull field",
ASSIGNING_NULLABLE_TO_NONNULL_FIELD),
Arguments.of(
"/full/path/MyClass.java:80: error: [NullAway] referenced method returns @Nullable, but functional interface method someMethod() returns @NonNull",
REFERENCED_METHOD_RETURNS_NULLABLE),
Arguments.of(
"/full/path/MyClass.java:90: error: [NullAway] unbound instance method reference cannot be used, as first parameter param1 is @Nullable",
UNBOUND_INSTANCE_METHOD_REFERENCE_FIRST_PARAMETER_NULLABLE),
Arguments.of(
"/full/path/MyClass.java:100: error: [NullAway] parameter param1 is @NonNull, but parameter in superMethod is @Nullable",
PARAMETER_IS_NONNULL_BUT_PARAMETER_IN_SUPERCLASS_NULLABLE),
Arguments.of(
"/full/path/MyClass.java:110: error: [NullAway] unboxing of a @Nullable value",
UNBOXING_OF_NULLABLE_VALUE),
Arguments.of(
"/full/path/MyClass.java:120: error: [NullAway] read of @NonNull field myField before initialization",
READ_OF_NONNULL_FIELD_BEFORE_INIT),
Arguments.of(
"/full/path/MyClass.java:130: error: [NullAway] enhanced-for expression items is @Nullable",
ENHANCED_FOR_EXPRESSION_NULLABLE),
Arguments.of(
"/full/path/MyClass.java:140: error: [NullAway] synchronized block expression \"lock\" is @Nullable",
SYNCHRONIZED_BLOCK_EXPRESSION_NULLABLE),
Arguments.of(
"/full/path/MyClass.java:150: error: [NullAway] @NonNull static field myStaticField not initialized",
NONNULL_STATIC_FIELD_NOT_INITIALIZED),
Arguments.of(
"/full/path/MyClass.java:160: error: [NullAway] passing known @NonNull parameter 'value' to CastToNonNullMethod (com.example.Util.cast) is unnecessary",
PASSING_NONNULL_TO_CAST_TO_NONNULL),
Arguments.of(
"/full/path/MyClass.java:170: error: [NullAway] Invoking get() on possibly empty Optional myOptional",
INVOKING_GET_ON_EMPTY_OPTIONAL),
Arguments.of(
"/full/path/MyClass.java:180: error: [NullAway] switch expression result is @Nullable",
SWITCH_EXPRESSION_NULLABLE),
Arguments.of(
"/full/path/MyClass.java:190: error: [NullAway] Method is annotated with @EnsuresNonNull but fails to ensure field myField is non-null",
METHOD_ANNOTATED_WITH_ENSURES_NONNULL_BUT_FAILS),
Arguments.of(
"/full/path/MyClass.java:200: error: [NullAway] Method is annotated with @EnsuresNonNullIf but does not ensure fields myField are non-null",
METHOD_ANNOTATED_WITH_ENSURES_NONNULL_IF_BUT_DOES_NOT_ENSURE),
Arguments.of(
"/full/path/MyClass.java:210: error: [NullAway] Expected static field myStaticField to be non-null at call site due to @RequiresNonNull annotation on invoked method",
EXPECTED_STATIC_FIELD_NONNULL_DUE_TO_REQUIRES_NONNULL),
Arguments.of(
"/full/path/MyClass.java:220: error: [NullAway] Expected field myField to be non-null at call site due to @RequiresNonNull annotation on invoked method",
EXPECTED_FIELD_NONNULL_DUE_TO_REQUIRES_NONNULL),
Arguments.of(
"/full/path/MyClass.java:230: error: [NullAway] postcondition inheritance is violated, this method must guarantee that all fields myField are @NonNull after invocation",
POSTCONDITION_INHERITANCE_VIOLATED),
Arguments.of(
"/full/path/MyClass.java:240: error: [NullAway] precondition inheritance is violated, method in child class cannot have a stricter precondition than parent",
PRECONDITION_INHERITANCE_VIOLATED),
Arguments.of(
"/full/path/MyClass.java:250: error: [NullAway] Type argument cannot be @Nullable, as method someMethod's type variable T is not @Nullable",
TYPE_ARGUMENT_CANNOT_BE_NULLABLE),
Arguments.of(
"/full/path/MyClass.java:260: error: [NullAway] Generic type parameter cannot be @Nullable, as type variable T of type MyClass does not have a @Nullable upper bound",
GENERIC_TYPE_PARAMETER_CANNOT_BE_NULLABLE),
Arguments.of(
"/full/path/MyClass.java:270: error: [NullAway] incompatible types: List<@Nullable String> cannot be converted to List<String>",
INCOMPATIBLE_TYPES_GENERIC),
Arguments.of(
"/full/path/MyClass.java:280: error: [NullAway] Conditional expression must have type List<String> but the sub-expression has type List<@Nullable String>",
CONDITIONAL_EXPRESSION_TYPE_MISMATCH),
Arguments.of(
"/full/path/MyClass.java:290: error: [NullAway] Method returns List<@Nullable String>, but overridden method returns List<String>, which has mismatched type parameter nullability",
METHOD_RETURNS_GENERIC_WITH_MISMATCHED_NULLABILITY),
Arguments.of(
"/full/path/MyClass.java:300: error: [NullAway] Parameter has type List<String>, but overridden method has parameter type List<@Nullable String>, which has mismatched type parameter nullability",
PARAMETER_TYPE_GENERIC_WITH_MISMATCHED_NULLABILITY),
Arguments.of(
"/full/path/MyClass.java:310: error: [NullAway] Writing @Nullable expression into array with @NonNull contents.",
WRITING_NULLABLE_INTO_NONNULL_ARRAY),
Arguments.of(
"/full/path/MyClass.java:320: error: [NullAway] Failed to infer type argument nullability for call someMethod()",
FAILED_TO_INFER_TYPE_ARGUMENT_NULLABILITY),
Arguments.of(
"/full/path/MyClass.java:330: error: [NullAway] Type-use nullability annotations should be applied on inner class",
TYPE_USE_NULLABILITY_ON_WRONG_NESTED_CLASS_LEVEL)
);
}
}