Files
openide/java/java-impl/src/com/intellij/codeInsight/NullableNotNullManagerImpl.java
Tagir Valeev e02fbb2cfc [java-analysis] IDEA-375444 Speedup retrieving package-level nullity annotations
(cherry picked from commit 7be077c9a1a6b7fade1833689567b5947e2c77de)

IJ-CR-168171

GitOrigin-RevId: 01c5b38d7e028585b94d1596c0de8c7c323a2fa3
2025-07-11 10:25:52 +00:00

504 lines
20 KiB
Java

// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInsight;
import com.intellij.codeInsight.annoPackages.AnnotationPackageSupport;
import com.intellij.codeInsight.annoPackages.Jsr305Support;
import com.intellij.codeInsight.options.JavaClassValidator;
import com.intellij.codeInspection.dataFlow.HardcodedContracts;
import com.intellij.codeInspection.options.OptionController;
import com.intellij.codeInspection.options.OptionControllerProvider;
import com.intellij.ide.highlighter.JavaFileType;
import com.intellij.java.JavaBundle;
import com.intellij.java.codeserver.core.JavaPsiModuleUtil;
import com.intellij.java.library.JavaLibraryModificationTracker;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.components.PersistentStateComponent;
import com.intellij.openapi.components.State;
import com.intellij.openapi.extensions.ExtensionPointListener;
import com.intellij.openapi.extensions.PluginDescriptor;
import com.intellij.openapi.fileTypes.FileTypeRegistry;
import com.intellij.openapi.project.DumbService;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ProjectFileIndex;
import com.intellij.openapi.roots.ProjectRootManager;
import com.intellij.openapi.util.*;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.profile.codeInspection.ProjectInspectionProfileManager;
import com.intellij.psi.*;
import com.intellij.psi.impl.java.stubs.index.JavaAnnotationIndex;
import com.intellij.psi.search.DelegatingGlobalSearchScope;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.util.CachedValueProvider.Result;
import com.intellij.psi.util.CachedValuesManager;
import com.intellij.psi.util.PsiModificationTracker;
import com.intellij.psi.util.PsiUtil;
import com.intellij.psi.util.TypeConversionUtil;
import com.intellij.util.containers.ContainerUtil;
import one.util.streamex.StreamEx;
import org.jdom.Element;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Unmodifiable;
import java.util.*;
import java.util.function.Function;
import static com.intellij.codeInsight.AnnotationUtil.NOT_NULL;
import static com.intellij.codeInsight.AnnotationUtil.NULLABLE;
import static com.intellij.codeInspection.options.OptPane.*;
@State(name = "NullableNotNullManager")
public class NullableNotNullManagerImpl extends NullableNotNullManager implements PersistentStateComponent<Element>, ModificationTracker,
Disposable {
private static final String INSTRUMENTED_NOT_NULLS_TAG = "instrumentedNotNulls";
private List<AnnotationPackageSupport> myAnnotationSupports;
private Map<String, AnnotationPackageSupport> myDefaultNullables;
private Map<String, AnnotationPackageSupport> myDefaultNotNulls;
private Map<String, AnnotationPackageSupport> myDefaultUnknowns;
private List<String> myDefaultAll;
public String myDefaultNullable = NULLABLE;
public String myDefaultNotNull = NOT_NULL;
public final JDOMExternalizableStringList myNullables = new JDOMExternalizableStringList();
public final JDOMExternalizableStringList myNotNulls = new JDOMExternalizableStringList();
private List<String> myInstrumentedNotNulls = List.of(NOT_NULL);
private final SimpleModificationTracker myTracker = new SimpleModificationTracker();
public NullableNotNullManagerImpl(Project project) {
super(project);
AnnotationPackageSupport.EP_NAME.addExtensionPointListener(new ExtensionPointListener<>() {
@Override
public void extensionAdded(AnnotationPackageSupport extension, @NotNull PluginDescriptor pluginDescriptor) {
updateDefaults();
}
@Override
public void extensionRemoved(AnnotationPackageSupport extension, @NotNull PluginDescriptor pluginDescriptor) {
updateDefaults();
}
}, this);
updateDefaults();
}
private void updateDefaults() {
myAnnotationSupports = AnnotationPackageSupport.EP_NAME.getExtensionList();
myDefaultNullables = StreamEx.of(myAnnotationSupports)
.cross(s -> s.getNullabilityAnnotations(Nullability.NULLABLE).stream()).invert().toMap();
myDefaultNotNulls = StreamEx.of(myAnnotationSupports)
.cross(s -> s.getNullabilityAnnotations(Nullability.NOT_NULL).stream()).invert().toMap();
myDefaultUnknowns = StreamEx.of(myAnnotationSupports)
.cross(s -> s.getNullabilityAnnotations(Nullability.UNKNOWN).stream()).invert().toMap();
myDefaultAll = StreamEx.of(myDefaultNullables, myDefaultNotNulls, myDefaultUnknowns).toFlatList(Map::keySet);
normalizeDefaults();
}
@Override
public void setNotNulls(String @NotNull ... annotations) {
myNotNulls.clear();
Collections.addAll(myNotNulls, annotations);
normalizeDefaults();
}
@Override
public void setNullables(String @NotNull ... annotations) {
myNullables.clear();
Collections.addAll(myNullables, annotations);
normalizeDefaults();
}
@Override
public @NotNull String getDefaultNullable() {
return myDefaultNullable;
}
@Override
public void setDefaultNullable(@NotNull String defaultNullable) {
LOG.assertTrue(getNullables().contains(defaultNullable));
myDefaultNullable = defaultNullable;
myTracker.incModificationCount();
}
@Override
public @NotNull String getDefaultNotNull() {
return myDefaultNotNull;
}
@Override
public void setDefaultNotNull(@NotNull String defaultNotNull) {
LOG.assertTrue(getNotNulls().contains(defaultNotNull));
myDefaultNotNull = defaultNotNull;
myTracker.incModificationCount();
}
@Override
@NotNull
public List<String> getDefaultNullables() {
return new ArrayList<>(myDefaultNullables.keySet());
}
@Override
@NotNull
public List<String> getDefaultNotNulls() {
return new ArrayList<>(myDefaultNotNulls.keySet());
}
@Override
public @NotNull Optional<Nullability> getAnnotationNullability(String name) {
return Optional.ofNullable(getAllNullabilityAnnotationsWithNickNames().getNullability(name));
}
@Override
public boolean isTypeUseAnnotationLocationRestricted(String name) {
AnnotationPackageSupport support = findAnnotationSupport(name);
return support != null && support.isTypeUseAnnotationLocationRestricted();
}
@Override
public boolean canAnnotateLocals(String name) {
AnnotationPackageSupport support = findAnnotationSupport(name);
return support == null || support.canAnnotateLocals();
}
private @Nullable AnnotationPackageSupport findAnnotationSupport(String name) {
AnnotationPackageSupport support = myDefaultUnknowns.get(name);
if (support == null) {
support = myDefaultNotNulls.get(name);
if (support == null) {
support = myDefaultNullables.get(name);
}
}
return support;
}
@Override
public @NotNull List<String> getNullables() {
return Collections.unmodifiableList(myNullables);
}
@Override
public @NotNull List<String> getNotNulls() {
return Collections.unmodifiableList(myNotNulls);
}
@Override
public @NotNull List<String> getInstrumentedNotNulls() {
return Collections.unmodifiableList(myInstrumentedNotNulls);
}
@Override
public void setInstrumentedNotNulls(@NotNull List<String> names) {
myInstrumentedNotNulls = ContainerUtil.sorted(names);
myTracker.incModificationCount();
}
@Override
protected boolean hasHardcodedContracts(@NotNull PsiElement element) {
return HardcodedContracts.hasHardcodedContracts(element);
}
@Override
public Element getState() {
Element component = new Element("component");
if (!hasDefaultValues()) {
try {
DefaultJDOMExternalizer.writeExternal(this, component);
}
catch (WriteExternalException e) {
LOG.error(e);
}
}
if (myInstrumentedNotNulls.size() != 1 || !NOT_NULL.equals(myInstrumentedNotNulls.get(0))) {
// poor man's @XCollection(style = XCollection.Style.v2)
Element instrumentedNotNulls = new Element(INSTRUMENTED_NOT_NULLS_TAG);
for (String value : myInstrumentedNotNulls) {
instrumentedNotNulls.addContent(new Element("option").setAttribute("value", value));
}
component.addContent(instrumentedNotNulls);
}
return component;
}
private boolean hasDefaultValues() {
return NOT_NULL.equals(myDefaultNotNull) &&
NULLABLE.equals(myDefaultNullable) &&
new HashSet<>(myNullables).equals(myDefaultNullables.keySet()) &&
new HashSet<>(myNotNulls).equals(myDefaultNotNulls.keySet());
}
@Override
public void loadState(@NotNull Element state) {
try {
DefaultJDOMExternalizer.readExternal(this, state);
normalizeDefaults();
}
catch (InvalidDataException e) {
LOG.error(e);
}
Element instrumented = state.getChild(INSTRUMENTED_NOT_NULLS_TAG);
if (instrumented == null) {
myInstrumentedNotNulls = List.of(NOT_NULL);
}
else {
myInstrumentedNotNulls = ContainerUtil.mapNotNull(instrumented.getChildren("option"), o -> o.getAttributeValue("value"));
}
}
private void normalizeDefaults() {
myNotNulls.removeAll(myDefaultNullables.keySet());
myNullables.removeAll(myDefaultNotNulls.keySet());
myNullables.addAll(ContainerUtil.filter(myDefaultNullables.keySet(), s -> !myNullables.contains(s)));
myNotNulls.addAll(ContainerUtil.filter(myDefaultNotNulls.keySet(), s -> !myNotNulls.contains(s)));
myTracker.incModificationCount();
}
private @NotNull List<PsiClass> getAllNullabilityNickNames() {
if (!getNotNulls().contains(Jsr305Support.JAVAX_ANNOTATION_NONNULL)) {
return Collections.emptyList();
}
return CachedValuesManager.getManager(myProject).getCachedValue(myProject, () -> {
Set<PsiClass> result = new HashSet<>(getPossiblyUnresolvedJavaNicknameUsages());
GlobalSearchScope scope = new DelegatingGlobalSearchScope(GlobalSearchScope.allScope(myProject)) {
@Override
public boolean contains(@NotNull VirtualFile file) {
return super.contains(file) && !FileTypeRegistry.getInstance().isFileOfType(file, JavaFileType.INSTANCE);
}
};
PsiClass[] nickDeclarations = JavaPsiFacade.getInstance(myProject).findClasses(Jsr305Support.TYPE_QUALIFIER_NICKNAME, scope);
for (PsiClass tqNick : nickDeclarations) {
result.addAll(ContainerUtil.findAll(MetaAnnotationUtil.getChildren(tqNick, scope), Jsr305Support::isNullabilityNickName));
}
return Result.create(new ArrayList<>(result), PsiModificationTracker.MODIFICATION_COUNT);
});
}
// some frameworks use jsr305 annotations but don't have them in classpath
private @NotNull List<PsiClass> getPossiblyUnresolvedJavaNicknameUsages() {
List<PsiClass> result = new ArrayList<>();
Collection<PsiAnnotation> annotations = JavaAnnotationIndex.getInstance().getAnnotations(StringUtil.getShortName(
Jsr305Support.TYPE_QUALIFIER_NICKNAME), myProject, GlobalSearchScope.allScope(myProject));
for (PsiAnnotation annotation : annotations) {
PsiElement context = annotation.getContext();
if (context instanceof PsiModifierList && context.getContext() instanceof PsiClass ownerClass &&
ownerClass.isAnnotationType() && Jsr305Support.isNullabilityNickName(ownerClass)) {
result.add(ownerClass);
}
}
return result;
}
@Override
protected @NotNull ContextNullabilityInfo findNullityDefaultOnPackage(PsiAnnotation.TargetType @NotNull [] placeTargetTypes,
PsiFile file) {
boolean superPackage = false;
ContextNullabilityInfo info = ContextNullabilityInfo.EMPTY;
ProjectFileIndex index = ProjectRootManager.getInstance(myProject).getFileIndex();
VirtualFile vFile = file.getVirtualFile();
if (vFile == null) return info;
VirtualFile root = index.getSourceRootForFile(vFile);
boolean compiled = false;
if (root == null) {
root = index.getClassRootForFile(vFile);
if (root == null) return info;
compiled = true;
}
// Single-file source root -- no package-info processing for now
if (root.equals(vFile)) return info;
PsiDirectory directory = file.getContainingDirectory();
while (directory != null) {
PsiFile packageFile = directory.findFile(compiled ? PsiPackage.PACKAGE_INFO_CLS_FILE : PsiPackage.PACKAGE_INFO_FILE);
if (packageFile instanceof PsiJavaFile javaFile) {
PsiPackageStatement stmt = javaFile.getPackageStatement();
if (stmt != null) {
PsiModifierList modifierList = stmt.getAnnotationList();
if (modifierList != null) {
for (PsiAnnotation annotation : modifierList.getAnnotations()) {
info = info.orElse(checkNullityDefault(annotation, placeTargetTypes, superPackage));
}
}
}
}
if (root.equals(directory.getVirtualFile())) break;
directory = directory.getParentDirectory();
superPackage = true;
}
return info;
}
@Override
protected @NotNull ContextNullabilityInfo getNullityDefault(@NotNull PsiModifierListOwner container,
PsiAnnotation.TargetType @NotNull [] placeTargetTypes) {
LOG.assertTrue(!(container instanceof PsiPackage)); // Packages are handled separately in findNullityDefaultOnPackage
ContextNullabilityInfo res = ContextNullabilityInfo.EMPTY;
PsiModifierList modifierList = container.getModifierList();
if (modifierList != null) {
for (PsiAnnotation annotation : modifierList.getAnnotations()) {
ContextNullabilityInfo info = checkNullityDefault(annotation, placeTargetTypes, false);
res = res.orElse(info);
}
}
return res;
}
private @NotNull ContextNullabilityInfo checkNullityDefault(@NotNull PsiAnnotation annotation,
PsiAnnotation.TargetType @NotNull [] placeTargetTypes,
boolean superPackage) {
ContextNullabilityInfo info = ContextNullabilityInfo.EMPTY;
for (AnnotationPackageSupport support : myAnnotationSupports) {
info = info.orElse(support.getNullabilityByContainerAnnotation(annotation, placeTargetTypes, superPackage));
}
return info;
}
private @Unmodifiable @NotNull List<String> filterNickNames(@NotNull Nullability nullability) {
return ContainerUtil.mapNotNull(getAllNullabilityNickNames(), c -> Jsr305Support.getNickNamedNullability(c) == nullability ? c.getQualifiedName() : null);
}
@Override
@NotNull
public List<String> getNullablesWithNickNames() {
return CachedValuesManager.getManager(myProject).getCachedValue(myProject, () ->
Result.create(StreamEx.of(getNullables(), filterNickNames(Nullability.NULLABLE)).toFlatList(Function.identity()),
PsiModificationTracker.MODIFICATION_COUNT));
}
@Override
@NotNull
public List<String> getNotNullsWithNickNames() {
return CachedValuesManager.getManager(myProject).getCachedValue(myProject, () ->
Result.create(StreamEx.of(getNotNulls(), filterNickNames(Nullability.NOT_NULL)).toFlatList(Function.identity()),
PsiModificationTracker.MODIFICATION_COUNT));
}
@Override
protected @NotNull NullabilityAnnotationDataHolder getAllNullabilityAnnotationsWithNickNames() {
if (DumbService.isDumb(myProject) || myProject.isDefault()) {
// Searching for nullability nicknames is not available in the dumb mode or for default project
return NullabilityAnnotationDataHolder.fromMap(getNullabilityMap());
}
return CachedValuesManager.getManager(myProject).getCachedValue(myProject, () -> {
Map<String, Nullability> result = getNullabilityMap();
for (PsiClass aClass : getAllNullabilityNickNames()) {
String qName = aClass.getQualifiedName();
if (qName != null) {
result.putIfAbsent(qName, Jsr305Support.getNickNamedNullability(aClass));
}
}
NullabilityAnnotationDataHolder holder = NullabilityAnnotationDataHolder.fromMap(result);
return Result.create(holder, PsiModificationTracker.MODIFICATION_COUNT);
});
}
private @NotNull Map<String, Nullability> getNullabilityMap() {
Map<String, Nullability> result = new HashMap<>();
for (String qName : myDefaultAll) {
result.put(qName, null);
}
for (String qName : getNotNulls()) {
result.put(qName, Nullability.NOT_NULL);
}
for (String qName : getNullables()) {
result.put(qName, Nullability.NULLABLE);
}
for (String qName : myDefaultUnknowns.keySet()) {
result.put(qName, Nullability.UNKNOWN);
}
return result;
}
@Override
protected @NotNull Nullability correctNullability(@NotNull Nullability nullability, @NotNull PsiAnnotation annotation) {
if (nullability == Nullability.NOT_NULL && annotation.hasQualifiedName(Jsr305Support.JAVAX_ANNOTATION_NONNULL)) {
Nullability correctedNullability = Jsr305Support.extractNullityFromWhenValue(annotation);
if (correctedNullability != null) {
return correctedNullability;
}
}
return nullability;
}
@Override
public long getModificationCount() {
return myTracker.getModificationCount();
}
@Override
protected @NotNull ContextNullabilityInfo findNullityDefaultOnModule(PsiAnnotation.@NotNull TargetType @NotNull [] targetTypes,
@NotNull PsiElement element) {
PsiJavaModule module = JavaPsiModuleUtil.findDescriptorByElement(element);
if (module != null) {
return getNullityDefault(module, targetTypes);
}
return ContextNullabilityInfo.EMPTY;
}
public @NotNull OptionController getOptionController() {
return OptionController.fieldsOf(this)
.withRootPane(() -> pane(
tabs(
tab(NullableNotNullDialog.NULLABLE,
string("myDefaultNullable", JavaBundle.message("nullable.notnull.annotation.used.label")),
stringList("myNullables", JavaBundle.message("nullable.notnull.annotations.panel.title", "Nullable"),
new JavaClassValidator().annotationsOnly())),
tab(NullableNotNullDialog.NOT_NULL,
string("myDefaultNotNull", JavaBundle.message("nullable.notnull.annotation.used.label")),
stringList("myNotNulls", JavaBundle.message("nullable.notnull.annotations.panel.title", "NotNull"),
new JavaClassValidator().annotationsOnly())
))));
}
@Override
public final @Nullable NullabilityAnnotationInfo findEffectiveNullabilityInfo(@NotNull PsiModifierListOwner owner) {
PsiType type = PsiUtil.getTypeByPsiElement(owner);
if (type == null || TypeConversionUtil.isPrimitiveAndNotNull(type)) return null;
return CachedValuesManager.getCachedValue(owner, () -> {
NullabilityAnnotationInfo info = doFindEffectiveNullabilityAnnotation(owner);
PsiFile file = owner.getContainingFile();
if (file != null
&& file.getVirtualFile() != null
&& ProjectFileIndex.getInstance(owner.getProject()).isInLibrary(file.getVirtualFile())) {
// there is no need to recompute info on changes in the project code
return Result.create(info, JavaLibraryModificationTracker.getInstance(owner.getProject()));
}
return Result.create(info, PsiModificationTracker.MODIFICATION_COUNT);
});
}
@Override
public void dispose() {
}
/**
* Provides options to setup nullability annotations:
* <ul>
* <li>NullableNotNullManager.myNullables - list of nullable annotation fqns</li>
* <li>NullableNotNullManager.myNotNulls - list of notnull annotation fqns</li>
* <li>NullableNotNullManager.myDefaultNullable - default nullable annotation fqn</li>
* <li>NullableNotNullManager.myDefaultNotNull - default notnull annotation fqn</li>
* </ul>
*/
public static final class Provider implements OptionControllerProvider {
@Override
public @NotNull OptionController forContext(@NotNull PsiElement context) {
Project project = context.getProject();
return ((NullableNotNullManagerImpl)getInstance(project)).getOptionController()
.onValueSet((bindId, value) -> ProjectInspectionProfileManager.getInstance(project).fireProfileChanged());
}
@Override
public @NotNull String name() {
return "NullableNotNullManager";
}
}
}