Repository: pmd/pmd (Java)

Issue:
[java] New Rule: Check that Map<K,V> is an EnumMap if K is an enum value
Would be nice to have a rule that enforces this practice - the rationale being that it makes no sense to use `HashMap` or `TreeMap` when `EnumMap` is available whenever the keys are _Enum_(s)


Discussion: @lgoldstein sounds really interesting! With the current status of type resolution, after the great efforts from @WinterGrascph, this should be doable.
Yeah that's a good idea! Would be nice if it covered EnumSets too



Fix (patch):
diff --git a/docs/pages/release_notes.md b/docs/pages/release_notes.md
index b81538f603f..2500b7b2337 100644
--- a/docs/pages/release_notes.md
+++ b/docs/pages/release_notes.md
@@ -14,12 +14,17 @@ This is a {{ site.pmd.release_type }} release.
 
 ### 🚀 New and noteworthy
 
+#### ✨ New Rules
+* The new Java rule {%rule java/bestpractices/UseEnumCollections %} reports usages for `HashSet` and `HashMap`
+  when the keys are of an enum type. The specialized enum collections are more space- and time-efficient.
+
 ### 🐛 Fixed Issues
 * core
   * [#4992](https://github.com/pmd/pmd/pull/4992): \[core] CPD: Include processing errors in XML report
 * apex
   * [#5053](https://github.com/pmd/pmd/issues/5053): \[apex] CPD fails to parse string literals with escaped characters
 * java-bestpractices
+  * [#577](https://github.com/pmd/pmd/issues/577): \[java] New Rule: Check that Map<K,V> is an EnumMap if K is an enum value
   * [#5047](https://github.com/pmd/pmd/issues/5047): \[java] UnusedPrivateMethod FP for Generics & Overloads
 * plsql
   * [#1934](https://github.com/pmd/pmd/issues/1934): \[plsql] ParseException with MERGE statement in anonymous block
diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/rule/bestpractices/UseEnumCollectionsRule.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/rule/bestpractices/UseEnumCollectionsRule.java
new file mode 100644
index 00000000000..a90b9715446
--- /dev/null
+++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/rule/bestpractices/UseEnumCollectionsRule.java
@@ -0,0 +1,50 @@
+/**
+ * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
+ */
+
+package net.sourceforge.pmd.lang.java.rule.bestpractices;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+
+import net.sourceforge.pmd.lang.java.ast.ASTConstructorCall;
+import net.sourceforge.pmd.lang.java.rule.AbstractJavaRulechainRule;
+import net.sourceforge.pmd.lang.java.symbols.JClassSymbol;
+import net.sourceforge.pmd.lang.java.symbols.JTypeDeclSymbol;
+import net.sourceforge.pmd.lang.java.types.JClassType;
+import net.sourceforge.pmd.lang.java.types.JTypeMirror;
+import net.sourceforge.pmd.lang.java.types.TypeTestUtil;
+
+/**
+ * Detect cases where EnumSet and EnumMap can be used.
+ *
+ * @author Clément Fournier
+ */
+public class UseEnumCollectionsRule extends AbstractJavaRulechainRule {
+
+    public UseEnumCollectionsRule() {
+        super(ASTConstructorCall.class);
+    }
+
+
+    @Override
+    public Object visit(ASTConstructorCall call, Object data) {
+        JTypeMirror builtType = call.getTypeMirror();
+
+        if (!builtType.isRaw()) {
+            boolean isMap = TypeTestUtil.isExactlyA(HashMap.class, builtType);
+            if (isMap || TypeTestUtil.isExactlyA(HashSet.class, builtType)) {
+
+                List<JTypeMirror> typeArgs = ((JClassType) builtType).getTypeArgs();
+                JTypeDeclSymbol keySymbol = typeArgs.get(0).getSymbol();
+
+                if (keySymbol instanceof JClassSymbol && ((JClassSymbol) keySymbol).isEnum()) {
+                    String enumCollectionReplacement = isMap ? "EnumMap" : "EnumSet";
+                    asCtx(data).addViolation(call.getTypeNode(), enumCollectionReplacement);
+                }
+            }
+        }
+        return null;
+    }
+}
diff --git a/pmd-java/src/main/resources/category/java/bestpractices.xml b/pmd-java/src/main/resources/category/java/bestpractices.xml
index 8db08d0343e..a8a3169254a 100644
--- a/pmd-java/src/main/resources/category/java/bestpractices.xml
+++ b/pmd-java/src/main/resources/category/java/bestpractices.xml
@@ -1723,6 +1723,40 @@ public class Foo {
         </example>
     </rule>
 
+
+    <rule name="UseEnumCollections"
+          language="java"
+          since="7.3.0"
+          message="This collection could be an {0}"
+          class="net.sourceforge.pmd.lang.java.rule.bestpractices.UseEnumCollectionsRule"
+          externalInfoUrl="${pmd.website.baseurl}/pmd_rules_java_bestpractices.html#useenumcollections">
+        <description>
+            Wherever possible, use `EnumSet` or `EnumMap` instead of `HashSet` and `HashMap` when the keys
+            are of an enum type. The specialized enum collections are more space- and time-efficient.
+            This rule reports constructor expressions for hash sets or maps whose key
+            type is an enum type.
+        </description>
+        <priority>3</priority>
+        <example>
+<![CDATA[
+            import java.util.EnumMap;
+            import java.util.HashSet;
+
+            enum Example {
+                A, B, C;
+
+                public static Set<Example> newSet() {
+                    return new HashSet<>(); // Could be EnumSet.noneOf(Example.class)
+                }
+
+                public static <V> Map<Example, V> newMap() {
+                    return new HashMap<>(); // Could be new EnumMap<>(Example.class)
+                }
+            }
+            ]]>
+        </example>
+    </rule>
+
     <rule name="UseStandardCharsets"
           language="java"
           since="6.34.0"
diff --git a/pmd-java/src/main/resources/rulesets/java/quickstart.xml b/pmd-java/src/main/resources/rulesets/java/quickstart.xml
index da02719225e..4afb964163d 100644
--- a/pmd-java/src/main/resources/rulesets/java/quickstart.xml
+++ b/pmd-java/src/main/resources/rulesets/java/quickstart.xml
@@ -52,6 +52,7 @@
     <rule ref="category/java/bestpractices.xml/UnusedPrivateField"/>
     <rule ref="category/java/bestpractices.xml/UnusedPrivateMethod"/>
     <rule ref="category/java/bestpractices.xml/UseCollectionIsEmpty"/>
+    <!-- <rule ref="category/java/bestpractices.xml/UseEnumCollections"/> -->
     <rule ref="category/java/bestpractices.xml/UseStandardCharsets" />
     <!-- <rule ref="category/java/bestpractices.xml/UseTryWithResources" /> -->
     <!-- <rule ref="category/java/bestpractices.xml/UseVarargs" /> -->