diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/CustomMethodHandlers.java b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/CustomMethodHandlers.java index f0f379513e70..69b4d1365164 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/CustomMethodHandlers.java +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/CustomMethodHandlers.java @@ -1,4 +1,4 @@ -// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +// 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.dataFlow; import com.intellij.codeInsight.Nullability; @@ -230,6 +230,15 @@ public final class CustomMethodHandlers { "newConcurrentHashMap", "newTreeMap").parameterCount(0)), toValue((arguments, state, factory, method) -> COLLECTION_SIZE.asDfType(intValue(0)).meet(LOCAL_OBJECT))); + private static final CallMatcher IMMUTABLE_CALLS = anyOf( + staticCall(JAVA_UTIL_COLLECTIONS, "emptyList", "emptySet", "emptyMap").parameterCount(0), + staticCall(JAVA_UTIL_COLLECTIONS, "singleton", "singletonList", "singletonMap"), + staticCall(JAVA_UTIL_LIST, "of", "copyOf"), + staticCall(JAVA_UTIL_SET, "of", "copyOf"), + staticCall(JAVA_UTIL_MAP, "of", "ofEntries", "copyOf"), + staticCall(JAVA_UTIL_ARRAYS, "asList") + ); + public static CustomMethodHandler find(PsiMethod method) { CustomMethodHandler handler = null; if (isConstantCall(method)) { @@ -248,6 +257,15 @@ public final class CustomMethodHandlers { return CONSTANT_CALLS.methodMatches(method); } + /** + * @param method method to check + * @return true if the method returns an immutable result, false otherwise + */ + @Contract("null -> false") + public static boolean isImmutableCall(PsiMethod method) { + return IMMUTABLE_CALLS.methodMatches(method); + } + private static @NotNull DfType handleConstantCall(DfaCallArguments arguments, DfaMemoryState state, PsiMethod method) { PsiType returnType = method.getReturnType(); if (returnType == null) return DfType.TOP; diff --git a/java/java-analysis-impl/src/com/siyeh/ig/memory/StaticCollectionInspection.java b/java/java-analysis-impl/src/com/siyeh/ig/memory/StaticCollectionInspection.java index 044250a85068..1cdba1585b4c 100644 --- a/java/java-analysis-impl/src/com/siyeh/ig/memory/StaticCollectionInspection.java +++ b/java/java-analysis-impl/src/com/siyeh/ig/memory/StaticCollectionInspection.java @@ -15,10 +15,13 @@ */ package com.siyeh.ig.memory; +import com.intellij.codeInspection.dataFlow.CustomMethodHandlers; import com.intellij.codeInspection.options.OptPane; import com.intellij.psi.PsiField; +import com.intellij.psi.PsiMethodCallExpression; import com.intellij.psi.PsiModifier; import com.intellij.psi.PsiType; +import com.intellij.psi.util.PsiUtil; import com.siyeh.InspectionGadgetsBundle; import com.siyeh.ig.BaseInspection; import com.siyeh.ig.BaseInspectionVisitor; @@ -67,6 +70,13 @@ public final class StaticCollectionInspection extends BaseInspection { if (m_ignoreWeakCollections && CollectionUtils.isWeakCollectionClass(type)) { return; } + + // Ignore final fields initialized with immutable, fixed-size collections + if (field.hasModifierProperty(PsiModifier.FINAL) && + PsiUtil.skipParenthesizedExprDown(field.getInitializer()) instanceof PsiMethodCallExpression call && + CustomMethodHandlers.isImmutableCall(call.resolveMethod())){ + return; + } registerFieldError(field); } } diff --git a/java/java-impl/resources/inspectionDescriptions/StaticCollection.html b/java/java-impl/resources/inspectionDescriptions/StaticCollection.html index 498b1d1b354c..4015d897a692 100644 --- a/java/java-impl/resources/inspectionDescriptions/StaticCollection.html +++ b/java/java-impl/resources/inspectionDescriptions/StaticCollection.html @@ -10,6 +10,11 @@ therefore prohibited by some coding standards. } +

+The inspection ignores final fields initialized with immutable, fixed-size collections +(e.g., Map.of(), List.of(), Collections.emptyList(), Collections.singletonMap()). +These collections cannot grow and therefore cannot cause memory leaks. +

Configure the inspection: diff --git a/java/java-tests/testData/ig/com/siyeh/igtest/memory/static_collection/StaticCollectionInspection.java b/java/java-tests/testData/ig/com/siyeh/igtest/memory/static_collection/StaticCollectionInspection.java new file mode 100644 index 000000000000..f6b0e252b8d4 --- /dev/null +++ b/java/java-tests/testData/ig/com/siyeh/igtest/memory/static_collection/StaticCollectionInspection.java @@ -0,0 +1,37 @@ +import java.util.*; + +public class StaticCollectionInspection { + // with warning + private static final Map s_map1 = new HashMap(10); + private static final List s_list2 = new ArrayList(10); + private static final Set s_set3 = new HashSet<>(); + private static /* final */ Map s_map4 = Map.of("key", "value"); + private static /* final */ List s_list5 = List.of("a", "b"); + + private static final Map s_map6; + static { + s_map6 = new HashMap<>(); + } + + // without warning + private static final Map s_map7 = Map.of("a", "A", "b", "B", "c", "C"); + private static final List s_list8 = List.of("a", "b", "c"); + private static final Set s_set9 = Set.of(1, 2, 3); + private static final Map s_map10 = Map.copyOf(Collections.emptyMap()); + private static final List s_list11 = List.copyOf(Collections.emptyList()); + private static final Set s_set12 = Set.copyOf(Collections.emptySet()); + private static final List s_list13 = Collections.emptyList(); + private static final Set s_set14 = Collections.emptySet(); + private static final Map s_map15 = Collections.emptyMap(); + private static final List s_list16 = Collections.singletonList("only"); + private static final Set s_set17 = Collections.singleton("only"); + private static final Map s_map18 = Collections.singletonMap("key", "value"); + private static final List s_list19 = Arrays.asList("a", "b", "c"); + private static final Map s_map20 = Map.ofEntries( + Map.entry("key1", "value1"), + Map.entry("key2", "value2") + ); + + private StaticCollectionInspection() { + } +} \ No newline at end of file diff --git a/java/java-tests/testData/ig/com/siyeh/igtest/performance/StaticCollectionInspection.java b/java/java-tests/testData/ig/com/siyeh/igtest/performance/StaticCollectionInspection.java index 939a27711ceb..af90b6f6bd52 100644 --- a/java/java-tests/testData/ig/com/siyeh/igtest/performance/StaticCollectionInspection.java +++ b/java/java-tests/testData/ig/com/siyeh/igtest/performance/StaticCollectionInspection.java @@ -1,17 +1,39 @@ package com.siyeh.igtest.performance; -import java.util.HashMap; -import java.util.Map; -import java.util.ArrayList; -import java.util.List; +import java.util.*; -public class StaticCollectionInspection -{ - private static final Map s_map1 = new HashMap(10); - private static final List s_map2 = new ArrayList(10); +public class StaticCollectionInspection { + // with warning + private static final Map s_map1 = new HashMap(10); + private static final List s_list2 = new ArrayList(10); + private static final Set s_set3 = new HashSet<>(); + private static /* final */ Map s_map4 = Map.of("key", "value"); + private static /* final */ List s_list5 = List.of("a", "b"); - private StaticCollectionInspection() - { - } + private static final Map s_map6; + static { + s_map6 = new HashMap<>(); + } + // without warning + private static final Map s_map7 = Map.of("a", "A", "b", "B", "c", "C"); + private static final List s_list8 = List.of("a", "b", "c"); + private static final Set s_set9 = Set.of(1, 2, 3); + private static final Map s_map10 = Map.copyOf(Collections.emptyMap()); + private static final List s_list11 = List.copyOf(Collections.emptyList()); + private static final Set s_set12 = Set.copyOf(Collections.emptySet()); + private static final List s_list13 = Collections.emptyList(); + private static final Set s_set14 = Collections.emptySet(); + private static final Map s_map15 = Collections.emptyMap(); + private static final List s_list16 = Collections.singletonList("only"); + private static final Set s_set17 = Collections.singleton("only"); + private static final Map s_map18 = Collections.singletonMap("key", "value"); + private static final List s_list19 = Arrays.asList("a", "b", "c"); + private static final Map s_map20 = Map.ofEntries( + Map.entry("key1", "value1"), + Map.entry("key2", "value2") + ); + + private StaticCollectionInspection() { + } } \ No newline at end of file diff --git a/java/java-tests/testSrc/com/siyeh/ig/memory/StaticCollectionInspectionTest.java b/java/java-tests/testSrc/com/siyeh/ig/memory/StaticCollectionInspectionTest.java new file mode 100644 index 000000000000..fae981c9b2a3 --- /dev/null +++ b/java/java-tests/testSrc/com/siyeh/ig/memory/StaticCollectionInspectionTest.java @@ -0,0 +1,25 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.siyeh.ig.memory; + +import com.intellij.codeInspection.InspectionProfileEntry; +import com.intellij.testFramework.LightProjectDescriptor; +import com.siyeh.ig.LightJavaInspectionTestCase; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class StaticCollectionInspectionTest extends LightJavaInspectionTestCase { + public void testStaticCollectionInspection() { + doTest(); + } + + @Override + protected @NotNull LightProjectDescriptor getProjectDescriptor() { + return JAVA_11; + } + + @Nullable + @Override + protected InspectionProfileEntry getInspection() { + return new StaticCollectionInspection(); + } +}