mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
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:
committed by
intellij-monorepo-bot
parent
2132de9726
commit
a8cc5765a2
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+96
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user