diff --git a/java/java-impl/resources/META-INF/JavaPlugin.xml b/java/java-impl/resources/META-INF/JavaPlugin.xml index 383d8dd82193..d3cb2ff0c4f8 100644 --- a/java/java-impl/resources/META-INF/JavaPlugin.xml +++ b/java/java-impl/resources/META-INF/JavaPlugin.xml @@ -1641,6 +1641,7 @@ + diff --git a/java/java-impl/resources/messages/RefactorJBundle.properties b/java/java-impl/resources/messages/RefactorJBundle.properties index c698ab501730..adc64a1e9db0 100644 --- a/java/java-impl/resources/messages/RefactorJBundle.properties +++ b/java/java-impl/resources/messages/RefactorJBundle.properties @@ -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 diff --git a/java/java-impl/src/com/intellij/java/impl/nullaway/NullAwayFilter.java b/java/java-impl/src/com/intellij/java/impl/nullaway/NullAwayFilter.java new file mode 100644 index 000000000000..1cfff638d333 --- /dev/null +++ b/java/java-impl/src/com/intellij/java/impl/nullaway/NullAwayFilter.java @@ -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; + } +} diff --git a/java/java-impl/src/com/intellij/java/impl/nullaway/NullAwayFilterProvider.java b/java/java-impl/src/com/intellij/java/impl/nullaway/NullAwayFilterProvider.java new file mode 100644 index 000000000000..a5d4cb50c72c --- /dev/null +++ b/java/java-impl/src/com/intellij/java/impl/nullaway/NullAwayFilterProvider.java @@ -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; + } +} diff --git a/java/java-impl/src/com/intellij/java/impl/nullaway/NullAwayInlayProvider.java b/java/java-impl/src/com/intellij/java/impl/nullaway/NullAwayInlayProvider.java new file mode 100644 index 000000000000..b61467732855 --- /dev/null +++ b/java/java-impl/src/com/intellij/java/impl/nullaway/NullAwayInlayProvider.java @@ -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); + }); + }); + } +} diff --git a/java/java-impl/src/com/intellij/java/impl/nullaway/NullAwayProblem.java b/java/java-impl/src/com/intellij/java/impl/nullaway/NullAwayProblem.java new file mode 100644 index 000000000000..141dd8c931b9 --- /dev/null +++ b/java/java-impl/src/com/intellij/java/impl/nullaway/NullAwayProblem.java @@ -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)] )?(?.+?):[(\\[]?(?\\d+)([:,](?\\d++))?[)\\]]?(?:: | )(?:(?:error|warning): )?\\[NullAway] (?.+)"); + + @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 @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 matchingPredicate, String nameToSuppress, EnumSet 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 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; + } + } +} diff --git a/java/java-tests/testSrc/com/intellij/java/impl/nullaway/NullAwayProblemFindTargetTest.java b/java/java-tests/testSrc/com/intellij/java/impl/nullaway/NullAwayProblemFindTargetTest.java new file mode 100644 index 000000000000..17a048b37c29 --- /dev/null +++ b/java/java-tests/testSrc/com/intellij/java/impl/nullaway/NullAwayProblemFindTargetTest.java @@ -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())); + } + } +} diff --git a/java/java-tests/testSrc/com/intellij/java/impl/nullaway/NullAwayProblemTest.java b/java/java-tests/testSrc/com/intellij/java/impl/nullaway/NullAwayProblemTest.java new file mode 100644 index 000000000000..73ddbe473313 --- /dev/null +++ b/java/java-tests/testSrc/com/intellij/java/impl/nullaway/NullAwayProblemTest.java @@ -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 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 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 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", + INCOMPATIBLE_TYPES_GENERIC), + Arguments.of( + "/full/path/MyClass.java:280: error: [NullAway] Conditional expression must have type List 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, 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, 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) + ); + } +}