[java] IDEA-377910 Add AnnotateAsNullMarkedFix

#IDEA-377910 Fixed

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

(cherry picked from commit fef51fbed5f227086d864dfc6483298585a78d8f)

IJ-CR-179837

GitOrigin-RevId: 8c43501fabba014db05c3a4930bcaeb69c865597
This commit is contained in:
Marcin Mikosik
2025-10-27 10:10:00 +00:00
committed by intellij-monorepo-bot
parent e76cf5cd46
commit e14522817c
30 changed files with 846 additions and 8 deletions
@@ -96,6 +96,10 @@ inspection.i18n.quickfix.annotate=Annotate\u2026
inspection.i18n.quickfix.annotate.as=Annotate as ''@{0}''
inspection.i18n.quickfix.annotate.element=Annotate {0} ''{1}''\u2026
inspection.i18n.quickfix.annotate.element.as=Annotate {0} ''{1}'' as ''@{2}''
inspection.i18n.quickfix.annotate.container.as=Annotate container as ''@{0}''
inspection.i18n.quickfix.annotate.choose.container=Choose Container
inspection.i18n.quickfix.remove.annotation=Remove ''@{0}''
inspection.i18n.quickfix.remove.annotation.from.element=Remove ''@{0}'' from {1} ''{2}''
inspection.reference.default.package=<default>
error.message.invalid.java.type=Invalid Java type
move.0.to.the.beginning=Move ''{0}'' to the beginning
@@ -24,6 +24,7 @@ import com.intellij.util.ArrayUtil;
import com.intellij.util.ObjectUtils;
import com.siyeh.ig.psiutils.CommentTracker;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -90,11 +91,7 @@ public class AddAnnotationPsiFix extends LocalQuickFixOnPsiElement implements Lo
final String name = PsiFormatUtil.formatSimple((PsiNamedElement)modifierListOwner);
if (name != null) {
JavaElementKind type = JavaElementKind.fromElement(modifierListOwner).lessDescriptive();
if (shortName == null) {
return JavaAnalysisBundle.message("inspection.i18n.quickfix.annotate.element", type.object(), name);
}
return JavaAnalysisBundle
.message("inspection.i18n.quickfix.annotate.element.as", type.object(), name, shortName);
return calcText(shortName, type, name);
}
}
if (shortName == null) {
@@ -103,6 +100,14 @@ public class AddAnnotationPsiFix extends LocalQuickFixOnPsiElement implements Lo
return JavaAnalysisBundle.message("inspection.i18n.quickfix.annotate.as", shortName);
}
public static @Nls @NotNull String calcText(@Nullable String shortName, @NotNull JavaElementKind type, @NotNull String name) {
if (shortName == null) {
return JavaAnalysisBundle.message("inspection.i18n.quickfix.annotate.element", type.object(), name);
}
return JavaAnalysisBundle
.message("inspection.i18n.quickfix.annotate.element.as", type.object(), name, shortName);
}
public static @Nullable PsiModifierListOwner getContainer(PsiFile file, int offset) {
return getContainer(file, offset, false);
}
@@ -0,0 +1,357 @@
// 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.codeInspection.nullable;
import com.intellij.codeInsight.intention.AddAnnotationPsiFix;
import com.intellij.codeInspection.nullable.MultipleElementsModCommandAction.Change;
import com.intellij.codeInspection.util.IntentionName;
import com.intellij.java.analysis.JavaAnalysisBundle;
import com.intellij.java.library.JavaLibraryUtil;
import com.intellij.modcommand.*;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleUtilCore;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.CodeStyleManager;
import com.intellij.psi.codeStyle.JavaCodeStyleManager;
import com.intellij.psi.util.JavaElementKind;
import com.intellij.psi.util.PsiFormatUtil;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNullByDefault;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import static com.intellij.psi.PsiPackage.PACKAGE_INFO_FILE;
import static com.intellij.psi.util.PsiTreeUtil.getChildOfType;
@NotNullByDefault
public final class AnnotateAsNullMarkedFix implements ModCommandAction {
private static final String NULL_MARKED_FQN = "org.jspecify.annotations.NullMarked";
private static final String NULL_UNMARKED_FQN = "org.jspecify.annotations.NullUnmarked";
private final @Nls String familyName;
private final List<ModCommandAction> actions;
/**
* Creates a quick fix for the PsiTypeElement which is highlighted as expected to be marked with not-null annotation.
* For each container (as specified in JSpecify spec) which encloses passed typeElement
* a quick-fix is generated that either adds @NullMarked annotation or removes @NullUnmarked annotation
* if-and-only-if such modification solves a nullability problem (missing expected not-null annotation).
* If no quick-fix is found, then null is returned.
* If only one quick-fix is found, then it will be returned.
* If multiple quick-fixes are found, then a quick-fix that displays chooser with available quick-fixes is returned.
*
* @param typeElement element with missing not-null annotation
* @param nullables list of nullable annotations that should be removed from typeElement when applying any quick-fix
*/
public static ModCommandAction createAnnotateAsNullMarkedFix(PsiTypeElement typeElement, List<String> nullables) {
return new AnnotateAsNullMarkedFix(provideNullMarkedFixes(typeElement, nullables));
}
private AnnotateAsNullMarkedFix(List<ModCommandAction> actions) {
this.actions = actions;
this.familyName =
JavaAnalysisBundle.message("inspection.i18n.quickfix.annotate.container.as", StringUtil.getShortName(NULL_MARKED_FQN));
}
@Override
public @Nullable Presentation getPresentation(ActionContext context) {
return switch (actions.size()) {
case 0 -> null;
case 1 -> actions.getFirst().getPresentation(context);
default -> Presentation.of(familyName);
};
}
@Override
public String getFamilyName() {
return familyName;
}
@Override
public ModCommand perform(ActionContext context) {
return ModCommand.chooseAction(JavaAnalysisBundle.message("inspection.i18n.quickfix.annotate.choose.container"), actions);
}
private static List<ModCommandAction> provideNullMarkedFixes(PsiTypeElement typeElement, List<String> nullables) {
if (!isJSpecifyLibraryAvailable(typeElement.getContainingFile())) {
return List.of();
}
List<Change<? extends PsiElement>> childrenChanges = new ArrayList<>();
addChangesThatRemoveNullableAnnotations(typeElement, nullables, childrenChanges);
List<? extends Container<? extends PsiElement>> containers = getContainersThatCanBeAnnotatedWithNullMarked(typeElement);
var modCommandActions = new ArrayList<ModCommandAction>();
for (int i = 0; i < containers.size(); i++) {
Container<? extends PsiElement> container = containers.get(i);
if (container.hasNullMarkedAnnotation()) {
if (container.hasNullUnmarkedAnnotation()) {
// jspecify spec treats case when both @NullMarked and @NullUnmarked annotations are present
// as the case when none is present
var text = container.textForRemoveAnnotationAction(NULL_UNMARKED_FQN);
Change<?> removeUnmarked = container.newChangeThatRemovesAnnotation(NULL_UNMARKED_FQN);
modCommandActions.add(new MultipleElementsModCommandAction(text, ContainerUtil.append(childrenChanges, removeUnmarked)));
}
else {
break;
}
}
else {
if (container.hasNullUnmarkedAnnotation()) {
Change<?> removeUnmarked = container.newChangeThatRemovesAnnotation(NULL_UNMARKED_FQN);
String enclosing = annotationFromNearestAnnotatedParentContainer(containers, i);
if (enclosing != null && enclosing.equals(NULL_MARKED_FQN)) {
var text = container.textForRemoveAnnotationAction(NULL_UNMARKED_FQN);
modCommandActions.add(new MultipleElementsModCommandAction(text, ContainerUtil.append(childrenChanges, removeUnmarked)));
break;
}
else {
var text = container.textForAddAnnotationAction(NULL_MARKED_FQN);
Change<?> addMarked = container.newChangeThatAddsAnnotation(NULL_MARKED_FQN);
var changes = ContainerUtil.append(childrenChanges, removeUnmarked, addMarked);
modCommandActions.add(new MultipleElementsModCommandAction(text, changes));
}
childrenChanges = ContainerUtil.append(childrenChanges, removeUnmarked);
}
else {
var text = container.textForAddAnnotationAction(NULL_MARKED_FQN);
Change<?> addMarked = container.newChangeThatAddsAnnotation(NULL_MARKED_FQN);
modCommandActions.add(new MultipleElementsModCommandAction(text, ContainerUtil.append(childrenChanges, addMarked)));
}
}
}
return modCommandActions;
}
private static boolean isJSpecifyLibraryAvailable(PsiFile file) {
Module module = ModuleUtilCore.findModuleForFile(file);
if (module == null) return false;
return JavaLibraryUtil.hasLibraryClass(module, NULL_MARKED_FQN) && JavaLibraryUtil.hasLibraryClass(module, NULL_UNMARKED_FQN);
}
/**
* @return effective annotation on the nearest enclosing container that affects container at index i.
*/
private static @Nullable String annotationFromNearestAnnotatedParentContainer(List<? extends Container<? extends PsiElement>> containers,
int i) {
for (int j = i + 1; j < containers.size(); j++) {
Container<? extends PsiElement> container = containers.get(j);
String annotation = container.getEffectiveAnnotation();
if (annotation != null) {
return annotation;
}
}
return null;
}
private static List<? extends Container<? extends PsiElement>> getContainersThatCanBeAnnotatedWithNullMarked(PsiElement element) {
List<Container<? extends PsiElement>> result = new ArrayList<>();
PsiModifierListOwner parent = PsiTreeUtil.getParentOfType(element, PsiModifierListOwner.class, true);
while (parent != null) {
if (parent.getModifierList() != null && (parent instanceof PsiClass || parent instanceof PsiMethod)) {
result.add(new ModifierListOwnerContainer(parent, parent.getModifierList()));
}
parent = PsiTreeUtil.getParentOfType(parent, PsiModifierListOwner.class, true);
}
var containingFile = element.getContainingFile();
if (containingFile == null) return result;
var directory = containingFile.getContainingDirectory();
if (directory == null) return result;
var packageStatement = getChildOfType(containingFile, PsiPackageStatement.class);
if (packageStatement == null) return result;
var packageFqn = packageStatement.getPackageName();
result.add(new PackageContainer(directory, packageFqn));
return result;
}
private static void addChangesThatRemoveNullableAnnotations(PsiTypeElement typeElement, List<String> nullables,
List<Change<? extends PsiElement>> changes) {
var annotationFqn = ContainerUtil.filter(nullables, annotation -> typeElement.hasAnnotation(annotation));
changes.addAll(ContainerUtil.map(annotationFqn, a -> new Change<>(typeElement, (updater, element) -> removeAnnotation(element, a))));
}
/**
* Container (PsiElement) on which JSpecify container annotation (NullMarked/NullUnmarked) can be added or removed.
*/
private static sealed abstract class Container<T extends PsiElement> permits PackageContainer, ModifierListOwnerContainer {
private final boolean hasNullMarkedAnnotation;
private final boolean hasNullUnmarkedAnnotation;
private Container(@Nullable PsiModifierList modifierList) {
this.hasNullMarkedAnnotation = hasAnnotation(modifierList, NULL_MARKED_FQN);
this.hasNullUnmarkedAnnotation = hasAnnotation(modifierList, NULL_UNMARKED_FQN);
}
private static boolean hasAnnotation(@Nullable PsiModifierList modifierList, String annotationFqn) {
return modifierList != null && modifierList.hasAnnotation(annotationFqn);
}
private boolean hasNullMarkedAnnotation() {
return hasNullMarkedAnnotation;
}
private boolean hasNullUnmarkedAnnotation() {
return hasNullUnmarkedAnnotation;
}
public @Nullable String getEffectiveAnnotation() {
// jspecify spec treats the case when both annotations are present as the case when none is present
if (hasNullMarkedAnnotation && hasNullUnmarkedAnnotation) return null;
if (hasNullMarkedAnnotation) return NULL_MARKED_FQN;
if (hasNullUnmarkedAnnotation) return NULL_UNMARKED_FQN;
return null;
}
public abstract Change<T> newChangeThatAddsAnnotation(String annotationFqn);
public abstract Change<T> newChangeThatRemovesAnnotation(String annotationFqn);
@IntentionName
public abstract String textForAddAnnotationAction(String annotationFqn);
@IntentionName
public abstract String textForRemoveAnnotationAction(String annotationFqn);
}
/**
* Provides Container operations for PsiModifierListOwner.
* Add/Remove annotation operations are applied to modifier list.
*/
private static final class ModifierListOwnerContainer extends Container<PsiModifierList> {
private final PsiModifierListOwner modifierListOwner;
private final PsiModifierList modifierList;
private ModifierListOwnerContainer(PsiModifierListOwner modifierListOwner, PsiModifierList modifierList) {
super(modifierList);
this.modifierListOwner = modifierListOwner;
this.modifierList = modifierList;
}
@Override
public Change<PsiModifierList> newChangeThatAddsAnnotation(String annotationFqn) {
return new Change<>(modifierList, (updater, m) -> updater.highlight(
JavaCodeStyleManager.getInstance(m.getProject()).shortenClassReferences(m.addAnnotation(annotationFqn))));
}
@Override
public Change<PsiModifierList> newChangeThatRemovesAnnotation(String annotationFqn) {
return new Change<>(modifierList, (updater, modifierList) -> removeAnnotation(modifierList, annotationFqn));
}
@Override
public String textForAddAnnotationAction(String annotationFqn) {
return AddAnnotationPsiFix.calcText(modifierListOwner, annotationFqn);
}
@Override
public String textForRemoveAnnotationAction(String annotationFqn) {
if (modifierListOwner instanceof PsiNamedElement) {
String name = PsiFormatUtil.formatSimple((PsiNamedElement)modifierListOwner);
if (name != null) {
var javaElementKind = JavaElementKind.fromElement(modifierListOwner).lessDescriptive();
return createTextForRemoveAnnotationAction(annotationFqn, name, javaElementKind);
}
}
return JavaAnalysisBundle.message("inspection.i18n.quickfix.remove.annotation", StringUtil.getShortName(annotationFqn));
}
}
/**
* Provides container operations for a package.
* Remove/Add operations are applied to package statement in package-info.java file
* which is created on the fly if it doesn't exist.
*/
private static final class PackageContainer extends Container<PsiDirectory> {
private final PsiDirectory directory;
private final String packageFqn;
private PackageContainer(PsiDirectory directory, String packageFqn) {
super(getPackageInfoModifierList(directory));
this.directory = directory;
this.packageFqn = packageFqn;
}
private static @Nullable PsiModifierList getPackageInfoModifierList(PsiDirectory directory) {
directory.findFile(PACKAGE_INFO_FILE);
var packageInfoFile = directory.findFile(PACKAGE_INFO_FILE);
if (packageInfoFile == null) return null;
if (!(packageInfoFile instanceof PsiJavaFile packageInfoJavaFile)) return null;
var packageStatement = packageInfoJavaFile.getPackageStatement();
if (packageStatement == null) return null;
return packageStatement.getAnnotationList();
}
@Override
public Change<PsiDirectory> newChangeThatAddsAnnotation(String annotationFqn) {
return new Change<>(directory, (updater, dir) -> {
var packageInfoFile = updater.getWritable(getPackageInfoFile(updater, dir));
var packageStatement = getPackageStatement(packageInfoFile, dir.getProject());
var elementFactory = JavaPsiFacade.getElementFactory(dir.getProject());
var annotation = elementFactory.createAnnotationFromText("@" + annotationFqn, packageStatement);
var addedAnnotation = packageInfoFile.addBefore(annotation, packageStatement);
JavaCodeStyleManager.getInstance(dir.getProject()).shortenClassReferences(addedAnnotation);
CodeStyleManager.getInstance(directory.getProject()).reformat(packageInfoFile);
updater.moveCaretTo(packageStatement);
updater.highlight(addedAnnotation);
});
}
private PsiPackageStatement getPackageStatement(PsiFile packageInfoFile, Project project) {
if (packageInfoFile instanceof PsiJavaFile packageInfoJavaFile) {
var packageStatement = packageInfoJavaFile.getPackageStatement();
if (packageStatement != null) return packageStatement;
}
var elementFactory = JavaPsiFacade.getElementFactory(project);
var packageStatement = elementFactory.createPackageStatement(packageFqn);
return (PsiPackageStatement)packageInfoFile.addBefore(packageStatement, packageInfoFile.getFirstChild());
}
private static PsiFile getPackageInfoFile(ModPsiUpdater updater, PsiDirectory directory) {
var packageInfoFile = directory.findFile(PACKAGE_INFO_FILE);
if (packageInfoFile == null) {
packageInfoFile = updater.getWritable(directory).createFile(PACKAGE_INFO_FILE);
}
return packageInfoFile;
}
@Override
public Change<PsiDirectory> newChangeThatRemovesAnnotation(String annotationFqn) {
return new Change<>(directory, (updater, dir) -> {
var packageInfoFile = updater.getWritable(getPackageInfoFile(updater, dir));
var packageStatement = getPackageStatement(packageInfoFile, dir.getProject());
var modifierList = packageStatement.getAnnotationList();
if (modifierList == null) return;
removeAnnotation(modifierList, annotationFqn);
});
}
@Override
public String textForAddAnnotationAction(String annotationFqn) {
return AddAnnotationPsiFix.calcText(StringUtil.getShortName(annotationFqn), JavaElementKind.PACKAGE, packageFqn);
}
@Override
public String textForRemoveAnnotationAction(String annotationFqn) {
return createTextForRemoveAnnotationAction(annotationFqn, packageFqn, JavaElementKind.PACKAGE);
}
}
private static void removeAnnotation(PsiAnnotationOwner annotationOwner, String annotationFqn) {
var annotation = annotationOwner.findAnnotation(annotationFqn);
if (annotation != null) {
annotation.delete();
}
}
private static @Nls String createTextForRemoveAnnotationAction(String annotationFqn, String name, JavaElementKind elementKind) {
String shortName = StringUtil.getShortName(annotationFqn);
return JavaAnalysisBundle.message("inspection.i18n.quickfix.remove.annotation.from.element", shortName, elementKind.object(), name);
}
}
@@ -0,0 +1,105 @@
// 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.codeInspection.nullable;
import com.intellij.codeInspection.util.IntentionName;
import com.intellij.modcommand.*;
import com.intellij.psi.PsiElement;
import com.intellij.psi.SmartPointerManager;
import com.intellij.psi.SmartPsiElementPointer;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNullByDefault;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiConsumer;
/**
* A ModCommand action that performs multiple PsiElement changes.
* Each change is passed a a {@link Change} object
* that contains PsiElement to be modified (target) and the operation to performed.
* Internally this action uses {@link SmartPsiElementPointer}s to track the target elements.
*/
@NotNullByDefault
class MultipleElementsModCommandAction implements ModCommandAction {
@IntentionName
private final String familyName;
private final List<SmartChange<? extends PsiElement>> changes;
/**
* Creates a new action that performs multiple PsiElement changes.
*
* @param familyName the name of the action family to be displayed in the UI
* @param changes list of changes to be performed on PsiElements
*/
MultipleElementsModCommandAction(@IntentionName String familyName, List<Change<? extends PsiElement>> changes) {
this.changes = ContainerUtil.map(changes, change -> change.toSmartChange());
this.familyName = familyName;
}
@Override
public String getFamilyName() {
return familyName;
}
@Override
public Presentation getPresentation(ActionContext context) {
return Presentation.of(familyName);
}
@Override
public ModCommand perform(ActionContext context) {
return ModCommand.psiUpdate(context, updater -> applyOperations(changes, updater));
}
private static <T extends PsiElement> void applyOperations(List<SmartChange<? extends T>> smartChanges, ModPsiUpdater updater) {
List<? extends Change<? extends T>> changes = convertSmartChangesToChanges(updater, smartChanges);
if (changes == null) return;
for (Change<? extends T> change : changes) {
change.invokeOperation(updater);
}
}
private static <T extends PsiElement> @Nullable List<? extends Change<? extends T>> convertSmartChangesToChanges(ModPsiUpdater updater,
List<SmartChange<? extends T>> smartChanges) {
List<Change<? extends T>> changes = new ArrayList<>();
for (SmartChange<? extends T> smartChange : smartChanges) {
Change<? extends T> change = smartChange.toChange(updater);
if (change == null) return null;
changes.add(change);
}
return changes;
}
/**
* Represents a change operation to be performed on a PsiElement.
*
* @param target the PsiElement to be modified
* @param operation the operation to be performed on the target element
* @param <T> the type of PsiElement
*/
public record Change<T extends PsiElement>(T target, BiConsumer<ModPsiUpdater, T> operation) {
SmartChange<T> toSmartChange() {
return new SmartChange<>(SmartPointerManager.createPointer(target), operation);
}
public void invokeOperation(ModPsiUpdater updater) {
operation().accept(updater, target());
}
}
/**
* Represents a change operation to be performed on a PsiElement tracked by SmartPsiElementPointer.
*
* @param target smart pointer to the PsiElement to be modified
* @param operation the operation to be performed on the target element
* @param <T> the type of PsiElement
*/
private record SmartChange<T extends PsiElement>(SmartPsiElementPointer<T> target, BiConsumer<ModPsiUpdater, T> operation) {
private @Nullable Change<T> toChange(ModPsiUpdater updater) {
T writable = updater.getWritable(target.getElement());
if (writable == null) return null;
return new Change<>(writable, operation);
}
}
}
@@ -48,6 +48,7 @@ import java.util.*;
import java.util.function.Consumer;
import static com.intellij.codeInsight.AnnotationUtil.*;
import static com.intellij.codeInspection.nullable.AnnotateAsNullMarkedFix.createAnnotateAsNullMarkedFix;
import static com.intellij.patterns.PsiJavaPatterns.psiElement;
import static com.intellij.patterns.PsiJavaPatterns.psiMethod;
import static com.intellij.util.ObjectUtils.tryCast;
@@ -442,12 +443,13 @@ public class NullableStuffInspectionBase extends AbstractBaseJavaLocalInspection
!(typeNullability == Nullability.UNKNOWN && type instanceof PsiWildcardType wildcardType && !wildcardType.isExtends())) {
String annotationToAdd = manager.getDefaultAnnotation(Nullability.NOT_NULL, reference);
PsiClass annotationClass = JavaPsiFacade.getInstance(project).findClass(annotationToAdd, element.getResolveScope());
AddTypeAnnotationFix fix = null;
List<LocalQuickFix> fixes = new ArrayList<>();
if (annotationClass != null &&
AnnotationTargetUtil.findAnnotationTarget(annotationClass, PsiAnnotation.TargetType.TYPE_USE) != null) {
fix = new AddTypeAnnotationFix(typeArgument, annotationToAdd, manager.getNullables());
fixes.add(LocalQuickFix.from(new AddTypeAnnotationFix(typeArgument, annotationToAdd, manager.getNullables())));
}
reportProblem(holder, typeArgument, fix, "non.null.type.argument.is.expected");
fixes.add(LocalQuickFix.from(createAnnotateAsNullMarkedFix(typeArgument, manager.getNullables()), false));
reportProblem(holder, typeArgument, fixes.toArray(LocalQuickFix.EMPTY_ARRAY), "non.null.type.argument.is.expected");
}
}
}
@@ -0,0 +1,11 @@
// "Annotate container as '@NullMarked'|->Annotate class 'Enclosing' as '@NullMarked'" "true"
import org.jspecify.annotations.NullMarked;
class NonNullHolder<T extends @org.jspecify.annotations.NonNull Object> {}
@NullMarked
class Enclosing {
public void foo(NonNullHolder<<caret>String> holder) {
}
}
@@ -0,0 +1,11 @@
// "Annotate container as '@NullMarked'|->Annotate class 'Enclosing' as '@NullMarked'" "true"
import org.jspecify.annotations.NullMarked;
class NonNullHolder<T extends @org.jspecify.annotations.NonNull Object> {}
@NullMarked
class Enclosing {
public void foo(NonNullHolder<<caret>String> holder) {
}
}
@@ -0,0 +1,13 @@
// "Annotate container as '@NullMarked'|->Annotate class 'Enclosing' as '@NullMarked'" "true"
import org.jspecify.annotations.NullMarked;
class NonNullHolder<T extends @org.jspecify.annotations.NonNull Object> {}
@NullMarked
class Enclosing {
class InnerEnclosing {
public void foo(NonNullHolder<<caret>String> holder) {
}
}
}
@@ -0,0 +1,11 @@
// "Annotate container as '@NullMarked'|->Annotate method 'foo()' as '@NullMarked'" "true"
import org.jspecify.annotations.NullMarked;
class NonNullHolder<T extends @org.jspecify.annotations.NonNull Object> {}
class Enclosing {
@NullMarked
public void foo(NonNullHolder<<caret>String> holder) {
}
}
@@ -0,0 +1,13 @@
// "Annotate container as '@NullMarked'|->Annotate class 'Enclosing' as '@NullMarked'" "true"
import org.jspecify.annotations.NullMarked;
class NonNullHolder<T extends @org.jspecify.annotations.NonNull Object> {}
@NullMarked
class Enclosing {
class Inner {
public void foo(NonNullHolder<<caret>String> holder) {
}
}
}
@@ -0,0 +1,11 @@
// "Annotate container as '@NullMarked'|->Annotate method 'foo()' as '@NullMarked'" "true"
import org.jspecify.annotations.NullMarked;
class NonNullHolder<T extends @org.jspecify.annotations.NonNull Object> {}
class Enclosing {
@NullMarked
public void foo(NonNullHolder<<caret>String> holder) {
}
}
@@ -0,0 +1,11 @@
// "Annotate container as '@NullMarked'|->Annotate class 'Enclosing' as '@NullMarked'" "true"
import org.jspecify.annotations.NullMarked;
class NonNullHolder<T extends @org.jspecify.annotations.NonNull Object> {}
@NullMarked
class Enclosing {
public void foo(NonNullHolder<<caret>String> holder) {
}
}
@@ -0,0 +1,14 @@
// "Annotate container as '@NullMarked'|->Remove '@NullUnmarked' from class 'Enclosing'" "true"
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.NullUnmarked;
class NonNullHolder<T extends @org.jspecify.annotations.NonNull Object> {}
@NullMarked
class Outer {
class Enclosing {
public void foo(NonNullHolder<<caret>String> holder) {
}
}
}
@@ -0,0 +1,12 @@
// "Annotate container as '@NullMarked'|->Remove '@NullUnmarked' from class 'Enclosing'" "true"
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.NullUnmarked;
class NonNullHolder<T extends @org.jspecify.annotations.NonNull Object> {}
@NullMarked
class Enclosing {
public void foo(NonNullHolder<<caret>String> holder) {
}
}
@@ -0,0 +1,8 @@
// "Annotate container as '@NullMarked'|->Annotate class 'Enclosing' as '@NullMarked'" "true"
class NonNullHolder<T extends @org.jspecify.annotations.NonNull Object> {}
class Enclosing {
public void foo(NonNullHolder<<caret>String> holder) {
}
}
@@ -0,0 +1,8 @@
// "Annotate container as '@NullMarked'|->Annotate class 'Enclosing' as '@NullMarked'" "true"
class NonNullHolder<T extends @org.jspecify.annotations.NonNull Object> {}
class Enclosing {
public void foo(NonNullHolder<@org.jetbrains.annotations.Nullable <caret>String> holder) {
}
}
@@ -0,0 +1,10 @@
// "Annotate container as '@NullMarked'|->Annotate class 'Enclosing' as '@NullMarked'" "true"
class NonNullHolder<T extends @org.jspecify.annotations.NonNull Object> {}
class Enclosing {
class InnerEnclosing {
public void foo(NonNullHolder<<caret>String> holder) {
}
}
}
@@ -0,0 +1,8 @@
// "Annotate container as '@NullMarked'|->Annotate method 'foo()' as '@NullMarked'" "true"
class NonNullHolder<T extends @org.jspecify.annotations.NonNull Object> {}
class Enclosing {
public void foo(NonNullHolder<<caret>String> holder) {
}
}
@@ -0,0 +1,12 @@
// "Annotate container as '@NullMarked'|->Annotate class 'Enclosing' as '@NullMarked'" "true"
class NonNullHolder<T extends @org.jspecify.annotations.NonNull Object> {}
class Enclosing {
@org.jspecify.annotations.NullUnmarked
class Inner {
@org.jspecify.annotations.NullUnmarked
public void foo(NonNullHolder<<caret>String> holder) {
}
}
}
@@ -0,0 +1,9 @@
// "Annotate container as '@NullMarked'|->Annotate method 'foo()' as '@NullMarked'" "true"
class NonNullHolder<T extends @org.jspecify.annotations.NonNull Object> {}
class Enclosing {
@org.jspecify.annotations.NullUnmarked
public void foo(NonNullHolder<<caret>String> holder) {
}
}
@@ -0,0 +1,9 @@
// "Annotate container as '@NullMarked'|->Annotate class 'Enclosing' as '@NullMarked'" "true"
class NonNullHolder<T extends @org.jspecify.annotations.NonNull Object> {}
class Enclosing {
@org.jspecify.annotations.NullUnmarked
public void foo(NonNullHolder<<caret>String> holder) {
}
}
@@ -0,0 +1,15 @@
// "Annotate container as '@NullMarked'|->Remove '@NullUnmarked' from class 'Enclosing'" "true"
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.NullUnmarked;
class NonNullHolder<T extends @org.jspecify.annotations.NonNull Object> {}
@NullMarked
class Outer {
@NullUnmarked
class Enclosing {
public void foo(NonNullHolder<<caret>String> holder) {
}
}
}
@@ -0,0 +1,13 @@
// "Annotate container as '@NullMarked'|->Remove '@NullUnmarked' from class 'Enclosing'" "true"
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.NullUnmarked;
class NonNullHolder<T extends @org.jspecify.annotations.NonNull Object> {}
@NullUnmarked
@NullMarked
class Enclosing {
public void foo(NonNullHolder<<caret>String> holder) {
}
}
@@ -0,0 +1,104 @@
// 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.codeInsight.daemon.impl.quickfix;
import com.intellij.codeInspection.nullable.NullableStuffInspection;
import com.intellij.openapi.application.impl.NonBlockingReadActionImpl;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.roots.ContentEntry;
import com.intellij.openapi.roots.ModifiableRootModel;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.testFramework.LightProjectDescriptor;
import com.intellij.testFramework.fixtures.DefaultLightProjectDescriptor;
import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase;
import com.intellij.testFramework.fixtures.MavenDependencyUtil;
import com.intellij.ui.ChooserInterceptor;
import com.intellij.ui.UiInterceptors;
import org.intellij.lang.annotations.Language;
import org.jetbrains.annotations.NotNull;
import java.util.regex.Pattern;
public class AnnotateAsNullMarkedFixPackageTest extends LightJavaCodeInsightFixtureTestCase {
@Language("JAVA")
private static final String ENCLOSING_CLASS_FILE_TEXT = """
package com.example;
class NonNullHolder<T extends @org.jspecify.annotations.NonNull Object> {}
public class Enclosing {
public void foo(NonNullHolder< <caret>String> holder) {
}
}
""";
@Override
protected @NotNull LightProjectDescriptor getProjectDescriptor() {
return new DefaultLightProjectDescriptor() {
@Override
public void configureModule(@NotNull Module module, @NotNull ModifiableRootModel model, @NotNull ContentEntry contentEntry) {
MavenDependencyUtil.addFromMaven(model, "org.jspecify:jspecify:1.0.0");
}
};
}
public void testAnnotatePackageAsNullMarkedWhenPackageInfoIsMissing() {
configureJavaFileFromPathAndText("com/example/Enclosing.java", ENCLOSING_CLASS_FILE_TEXT);
runAnnotatePackageAsNullMarkedAction();
assertJavaFileContent("com/example/package-info.java", """
@NullMarked
package com.example;
import org.jspecify.annotations.NullMarked;""");
}
public void testAnnotatePackageAsNullMarkedWhenPackageInfoIsPresent() {
configureJavaFileFromPathAndText("com/example/Enclosing.java", ENCLOSING_CLASS_FILE_TEXT);
myFixture.addFileToProject("com/example/package-info.java", "package com.example;");
runAnnotatePackageAsNullMarkedAction();
assertJavaFileContent("com/example/package-info.java", """
@NullMarked
package com.example;
import org.jspecify.annotations.NullMarked;""");
}
public void testAnnotatePackageAsNullMarkedWhenPackageInfoIsPresentAndContainsNullUnmarkedAnnotation() {
configureJavaFileFromPathAndText("com/example/Enclosing.java", ENCLOSING_CLASS_FILE_TEXT);
myFixture.addFileToProject("com/example/package-info.java", """
@NullUnmarked
package com.example;
import org.jspecify.annotations.NullUnmarked;""");
runAnnotatePackageAsNullMarkedAction();
assertJavaFileContent("com/example/package-info.java", """
@NullMarked
package com.example;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.NullUnmarked;""");
}
private void runAnnotatePackageAsNullMarkedAction() {
myFixture.enableInspections(new NullableStuffInspection());
var intentionAction = myFixture.findSingleIntention("Annotate container as '@NullMarked'");
UiInterceptors.register(new ChooserInterceptor(null, Pattern.quote("Annotate package 'com.example' as '@NullMarked'")));
myFixture.launchAction(intentionAction);
NonBlockingReadActionImpl.waitForAsyncTaskCompletion();
}
private void configureJavaFileFromPathAndText(String path, @Language("JAVA") String text) {
VirtualFile file = myFixture.addFileToProject(path, text).getVirtualFile();
myFixture.configureFromExistingVirtualFile(file);
}
private void assertJavaFileContent(String path, @Language("JAVA") String expected) {
myFixture.checkResult(path, expected, false);
}
}
@@ -0,0 +1,36 @@
// 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.codeInsight.daemon.impl.quickfix;
import com.intellij.codeInsight.daemon.quickFix.LightQuickFixParameterizedTestCase;
import com.intellij.codeInspection.LocalInspectionTool;
import com.intellij.codeInspection.nullable.NullableStuffInspection;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.roots.ContentEntry;
import com.intellij.openapi.roots.ModifiableRootModel;
import com.intellij.testFramework.LightProjectDescriptor;
import com.intellij.testFramework.fixtures.DefaultLightProjectDescriptor;
import com.intellij.testFramework.fixtures.MavenDependencyUtil;
import org.jetbrains.annotations.NotNull;
public class AnnotateAsNullMarkedFixTest extends LightQuickFixParameterizedTestCase {
@Override
protected String getBasePath() {
return "/codeInsight/daemonCodeAnalyzer/quickFix/annotateAsNullMarkedFix";
}
@Override
protected @NotNull LightProjectDescriptor getProjectDescriptor() {
return new SimpleLightProjectDescriptor(getModuleTypeId(), getProjectJDK()) {
@Override
protected void configureModule(@NotNull Module module, @NotNull ModifiableRootModel model, @NotNull ContentEntry contentEntry) {
DefaultLightProjectDescriptor.addJetBrainsAnnotationsWithTypeUse(model);
MavenDependencyUtil.addFromMaven(model, "org.jspecify:jspecify:1.0.0");
}
};
}
@Override
protected LocalInspectionTool @NotNull [] configureLocalInspectionTools() {
return new LocalInspectionTool[]{new NullableStuffInspection()};
}
}
+2
View File
@@ -299,6 +299,7 @@ com.intellij.codeInspection.LocalQuickFix
- sf:EMPTY_ARRAY:com.intellij.codeInspection.LocalQuickFix[]
- availableInBatchMode():Z
- s:from(com.intellij.modcommand.ModCommandAction):com.intellij.codeInspection.LocalQuickFix
- s:from(com.intellij.modcommand.ModCommandAction,Z):com.intellij.codeInspection.LocalQuickFix
- generatePreview(com.intellij.openapi.project.Project,com.intellij.codeInspection.ProblemDescriptor):com.intellij.codeInsight.intention.preview.IntentionPreviewInfo
- getRangesToHighlight(com.intellij.openapi.project.Project,com.intellij.codeInspection.ProblemDescriptor):java.util.List
- s:notNullElements(com.intellij.codeInspection.LocalQuickFix):com.intellij.codeInspection.LocalQuickFix[]
@@ -637,6 +638,7 @@ com.intellij.modcommand.ModCommandService
- a:wrap(com.intellij.modcommand.ModCommandAction):com.intellij.codeInsight.intention.IntentionAction
- a:wrapToLocalQuickFixAndIntentionActionOnPsiElement(com.intellij.modcommand.ModCommandAction,com.intellij.psi.PsiElement):com.intellij.codeInspection.LocalQuickFixAndIntentionActionOnPsiElement
- a:wrapToQuickFix(com.intellij.modcommand.ModCommandAction):com.intellij.codeInspection.LocalQuickFix
- a:wrapToQuickFix(com.intellij.modcommand.ModCommandAction,Z):com.intellij.codeInspection.LocalQuickFix
com.intellij.modcommand.ModPsiUpdater
- com.intellij.modcommand.ModPsiNavigator
- a:cancel(java.lang.String):V
@@ -124,4 +124,9 @@ public interface LocalQuickFix extends QuickFix<ProblemDescriptor>, FileModifier
static @Nullable LocalQuickFix from(@Nullable ModCommandAction action) {
return action == null ? null : ModCommandService.getInstance().wrapToQuickFix(action);
}
@Contract("null, _ -> null; !null, _ -> !null")
static @Nullable LocalQuickFix from(@Nullable ModCommandAction action, boolean availableInBatchMode) {
return action == null ? null : ModCommandService.getInstance().wrapToQuickFix(action, availableInBatchMode);
}
}
@@ -39,6 +39,8 @@ public interface ModCommandService {
*/
@NotNull LocalQuickFix wrapToQuickFix(@NotNull ModCommandAction action);
@NotNull LocalQuickFix wrapToQuickFix(@NotNull ModCommandAction action, boolean availableInBatchMode);
/**
* @param fix {@link LocalQuickFix}
* @return a {@link ModCommandAction} which is wrapped inside the supplied quick-fix; null if the supplied quick-fix
@@ -13,9 +13,20 @@ import org.jetbrains.annotations.NotNull;
final class ModCommandActionQuickFixWrapper extends ModCommandQuickFix implements ReportingClassSubstitutor {
private final ModCommandAction myAction;
private boolean myAvailableInBatchMode;
ModCommandActionQuickFixWrapper(@NotNull ModCommandAction action) {
this(action, true);
}
ModCommandActionQuickFixWrapper(@NotNull ModCommandAction action, boolean availableInBatchMode) {
myAction = action;
myAvailableInBatchMode = availableInBatchMode;
}
@Override
public boolean availableInBatchMode() {
return myAvailableInBatchMode;
}
ModCommandAction getAction() {
@@ -24,6 +24,7 @@ import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
@ApiStatus.Internal
@@ -44,6 +45,11 @@ public final class ModCommandServiceImpl implements ModCommandService {
return new ModCommandActionQuickFixWrapper(action);
}
@Override
public @NotNull LocalQuickFix wrapToQuickFix(@NotNull ModCommandAction action, boolean availableInBatchMode) {
return new ModCommandActionQuickFixWrapper(action, availableInBatchMode);
}
@Override
public @Nullable ModCommandAction unwrap(@NotNull LocalQuickFix fix) {
if (fix instanceof ModCommandActionQuickFixWrapper wrapper) {