Repository: checkstyle/checkstyle

Problem statement:
HideUtilityClassConstructor - Add option to skip validation based on list of annotations
I have read check documentation: https://checkstyle.org/checks/design/hideutilityclassconstructor.html
I have downloaded the latest cli from: https://checkstyle.org/cmdline.html#Download_and_Run
I have executed the cli and showed it below, as cli describes the problem better than 1,000 words

**How it works Now:**
As discussed in #5434, the `HideUtilityClassConstructorCheck` can benefit from an option that allows a list of annotations to skip/ignore the validation. This is especially useful when using frameworks with context containers and dependency injection, like Spring and OSGi implementations.

An example of how it's working now is a simple Spring Boot application class. Because the class only contains the static `main(..)` method, the rule expects the class to be `final` and a private/protected constructor to hide the default one. It's not possible to make the class final or the constructor public on the Spring Boot application class because Spring needs to create an instance of it to start the application.

**Is your feature request related to a problem? Please describe.**
Classes with specific annotations should not be final or hide the default constructor because they need to be instantiated later by some framework like Spring or OSGi implementations. Allowing to easily suppress the `HideUtilityClassConstructor` check when these annotations are present in the class can solve this problem.

**Describe the solution you'd like**
Add an option to the `HideUtilityClassConstructor` check that allows the define a list of annotations that skips/ignore validation.


new property should be named `ignoreAnnotatedBy` , as we did at https://github.com/checkstyle/checkstyle/pull/14553

```
<?xml version="1.0"?>
<!DOCTYPE module PUBLIC "-//Puppy Crawl//DTD Check Configuration 1.3//EN" 
     "http://checkstyle.sourceforge.net/dtds/configuration_1_3.dtd">
<module name="Checker">
    <module name="TreeWalker">
        <module name="HideUtilityClassConstructor" >
            <property name="ignoreAnnotatedBy" value="SpringBootApplication,MySomeOther">
         </module>
    </module>
</module>
```

```
package biz.gabrys.agabrys.sonarqube.falsepositives.d20180106;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * @see https://github.com/checkstyle/checkstyle/issues/5434
 */
@SpringBootApplication
public class HideUtilityClassConstructorCheck {

    public static void main(String[] args) {
        SpringApplication.run(HideUtilityClassConstructorCheck.class, args);
    }
}
```

Expected: no violation.


**Additional context**
So far, one workaround is to add an XPath suppression. However, the query is a bit hard to implement and understand, and adding extra annotations makes the query bigger and not so easy to maintain. Here's an example of the XPath suppression that works for the `@SpringBootApplication` annotation:

```xml
<module name="SuppressionXpathSingleFilter">
  <property name="files" value="\.java$" />
  <property name="checks" value="HideUtilityClassConstructor" />
  <property name="query" 
    value="//CLASS_DEF[./MODIFIERS[./ANNOTATION[./IDENT[@text='SpringBootApplication']]]]" />
</module>
```

**Final solution:**
new property `ignoreAnnotatedBy`



Hints:
@nrmancuso , please review and approve if you agree with design.
We can also start with simple implementation here (exact match), as we did in https://github.com/checkstyle/checkstyle/pull/14553, and extend check to handle imports as needed later.


Hi, I would like to work on this

Patch:
diff --git a/src/main/java/com/puppycrawl/tools/checkstyle/checks/design/HideUtilityClassConstructorCheck.java b/src/main/java/com/puppycrawl/tools/checkstyle/checks/design/HideUtilityClassConstructorCheck.java
--- a/src/main/java/com/puppycrawl/tools/checkstyle/checks/design/HideUtilityClassConstructorCheck.java
+++ b/src/main/java/com/puppycrawl/tools/checkstyle/checks/design/HideUtilityClassConstructorCheck.java
@@ -19,10 +19,14 @@
 
 package com.puppycrawl.tools.checkstyle.checks.design;
 
+import java.util.Collections;
+import java.util.Set;
+
 import com.puppycrawl.tools.checkstyle.StatelessCheck;
 import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
 import com.puppycrawl.tools.checkstyle.api.DetailAST;
 import com.puppycrawl.tools.checkstyle.api.TokenTypes;
+import com.puppycrawl.tools.checkstyle.utils.AnnotationUtil;
 
 /**
  * <div>
@@ -53,6 +57,19 @@
  *   }
  * }
  * </pre>
+ * <ul>
+ * <li>
+ * Property {@code ignoreAnnotatedBy} - Ignore classes annotated
+ * with the specified annotation(s). Annotation names provided in this property
+ * must exactly match the annotation names on the classes. If the target class has annotations
+ * specified with their fully qualified names (including package), the annotations in this
+ * property should also be specified with their fully qualified names. Similarly, if the target
+ * class has annotations specified with their simple names, this property should contain the
+ * annotations with the same simple names.
+ * Type is {@code java.lang.String[]}.
+ * Default value is {@code ""}.
+ * </li>
+ * </ul>
  *
  * <p>
  * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
@@ -78,6 +95,33 @@ public class HideUtilityClassConstructorCheck extends AbstractCheck {
      */
     public static final String MSG_KEY = "hide.utility.class";
 
+    /**
+     * Ignore classes annotated with the specified annotation(s). Annotation names
+     * provided in this property must exactly match the annotation names on the classes.
+     * If the target class has annotations specified with their fully qualified names
+     * (including package), the annotations in this property should also be specified with
+     * their fully qualified names. Similarly, if the target class has annotations specified
+     * with their simple names, this property should contain the annotations with the same
+     * simple names.
+     */
+    private Set<String> ignoreAnnotatedBy = Collections.emptySet();
+
+    /**
+     * Setter to ignore classes annotated with the specified annotation(s). Annotation names
+     * provided in this property must exactly match the annotation names on the classes.
+     * If the target class has annotations specified with their fully qualified names
+     * (including package), the annotations in this property should also be specified with
+     * their fully qualified names. Similarly, if the target class has annotations specified
+     * with their simple names, this property should contain the annotations with the same
+     * simple names.
+     *
+     * @param annotationNames specified annotation(s)
+     * @since 10.20.0
+     */
+    public void setIgnoreAnnotatedBy(String... annotationNames) {
+        ignoreAnnotatedBy = Set.of(annotationNames);
+    }
+
     @Override
     public int[] getDefaultTokens() {
         return getRequiredTokens();
@@ -96,7 +140,7 @@ public int[] getRequiredTokens() {
     @Override
     public void visitToken(DetailAST ast) {
         // abstract class could not have private constructor
-        if (!isAbstract(ast)) {
+        if (!isAbstract(ast) && !shouldIgnoreClass(ast)) {
             final boolean hasStaticModifier = isStatic(ast);
 
             final Details details = new Details(ast);
@@ -146,6 +190,16 @@ private static boolean isStatic(DetailAST ast) {
             .findFirstToken(TokenTypes.LITERAL_STATIC) != null;
     }
 
+    /**
+     * Checks if class is annotated by specific annotation(s) to skip.
+     *
+     * @param ast class to check
+     * @return true if annotated by ignored annotations
+     */
+    private boolean shouldIgnoreClass(DetailAST ast) {
+        return AnnotationUtil.containsAnnotation(ast, ignoreAnnotatedBy);
+    }
+
     /**
      * Details of class that are required for validation.
      */
diff --git a/src/xdocs-examples/java/com/puppycrawl/tools/checkstyle/checks/design/HideUtilityClassConstructorCheckExamplesTest.java b/src/xdocs-examples/java/com/puppycrawl/tools/checkstyle/checks/design/HideUtilityClassConstructorCheckExamplesTest.java
--- a/src/xdocs-examples/java/com/puppycrawl/tools/checkstyle/checks/design/HideUtilityClassConstructorCheckExamplesTest.java
+++ b/src/xdocs-examples/java/com/puppycrawl/tools/checkstyle/checks/design/HideUtilityClassConstructorCheckExamplesTest.java
@@ -35,10 +35,20 @@ protected String getPackageLocation() {
     @Test
     public void testExample1() throws Exception {
         final String[] expected = {
-            "12:1: " + getCheckMessage(MSG_KEY),
-            "37:1: " + getCheckMessage(MSG_KEY),
+            "13:1: " + getCheckMessage(MSG_KEY),
+            "39:1: " + getCheckMessage(MSG_KEY),
+            "45:1: " + getCheckMessage(MSG_KEY),
         };
 
         verifyWithInlineConfigParser(getPath("Example1.java"), expected);
     }
+
+    @Test
+    public void testExample2() throws Exception {
+        final String[] expected = {
+            "42:1: " + getCheckMessage(MSG_KEY),
+        };
+
+        verifyWithInlineConfigParser(getPath("Example2.java"), expected);
+    }
 }
diff --git a/src/xdocs-examples/resources/com/puppycrawl/tools/checkstyle/checks/design/hideutilityclassconstructor/Example1.java b/src/xdocs-examples/resources/com/puppycrawl/tools/checkstyle/checks/design/hideutilityclassconstructor/Example1.java
--- a/src/xdocs-examples/resources/com/puppycrawl/tools/checkstyle/checks/design/hideutilityclassconstructor/Example1.java
+++ b/src/xdocs-examples/resources/com/puppycrawl/tools/checkstyle/checks/design/hideutilityclassconstructor/Example1.java
@@ -9,7 +9,9 @@
 package com.puppycrawl.tools.checkstyle.checks.design.hideutilityclassconstructor;
 
 // xdoc section -- start
-class Example1 { // violation
+// violation below, 'should not have a public or default constructor'
+@java.lang.Deprecated
+class Example1 {
 
   public Example1() {
   }
@@ -18,7 +20,7 @@ public static void fun() {
   }
 }
 
-class Foo { // OK
+class Foo {
 
   private Foo() {
   }
@@ -26,7 +28,7 @@ private Foo() {
   static int n;
 }
 
-class Bar { // OK
+class Bar {
 
   protected Bar() {
     // prevents calls from subclass
@@ -34,8 +36,17 @@ protected Bar() {
   }
 }
 
-class UtilityClass { // violation
+@Deprecated // violation, 'should not have a public or default constructor'
+class UtilityClass {
 
   static float f;
 }
+// violation below, 'should not have a public or default constructor'
+@SpringBootApplication
+class Application1 {
+
+  public static void main(String[] args) {
+  }
+}
 // xdoc section -- end
+@interface SpringBootApplication {}
diff --git a/src/xdocs-examples/resources/com/puppycrawl/tools/checkstyle/checks/design/hideutilityclassconstructor/Example2.java b/src/xdocs-examples/resources/com/puppycrawl/tools/checkstyle/checks/design/hideutilityclassconstructor/Example2.java
new file mode 100644
--- /dev/null
+++ b/src/xdocs-examples/resources/com/puppycrawl/tools/checkstyle/checks/design/hideutilityclassconstructor/Example2.java
@@ -0,0 +1,54 @@
+/*xml
+<module name="Checker">
+  <module name="TreeWalker">
+    <module name="HideUtilityClassConstructor">
+      <property name="ignoreAnnotatedBy"
+        value="SpringBootApplication, java.lang.Deprecated" />
+    </module>
+   </module>
+</module>
+*/
+
+package com.puppycrawl.tools.checkstyle.checks.design.hideutilityclassconstructor;
+
+// xdoc section -- start
+// ok below, skipped by annotation
+@java.lang.Deprecated
+class Example2 {
+
+  public Example2() {
+  }
+
+  public static void fun() {
+  }
+}
+
+class Foo2 {
+
+  private Foo2() {
+  }
+
+  static int n;
+}
+
+class Bar2 {
+
+  protected Bar2() {
+    // prevents calls from subclass
+    throw new UnsupportedOperationException();
+  }
+}
+
+@Deprecated // violation, 'should not have a public or default constructor'
+class UtilityClass2 {
+
+  static float f;
+}
+// ok below, skipped by annotation
+@SpringBootApplication
+class Application2 {
+
+  public static void main(String[] args) {
+  }
+}
+// xdoc section -- end


Test patch:
diff --git a/src/test/java/com/puppycrawl/tools/checkstyle/checks/design/HideUtilityClassConstructorCheckTest.java b/src/test/java/com/puppycrawl/tools/checkstyle/checks/design/HideUtilityClassConstructorCheckTest.java
--- a/src/test/java/com/puppycrawl/tools/checkstyle/checks/design/HideUtilityClassConstructorCheckTest.java
+++ b/src/test/java/com/puppycrawl/tools/checkstyle/checks/design/HideUtilityClassConstructorCheckTest.java
@@ -146,4 +146,26 @@ public void testGetAcceptableTokens() {
             .isEqualTo(expected);
     }
 
+    @Test
+    public void testIgnoreAnnotatedBy() throws Exception {
+        final String[] expected = {
+            "30:1: " + getCheckMessage(MSG_KEY),
+        };
+        verifyWithInlineConfigParser(
+                getPath("InputHideUtilityClassConstructorIgnoreAnnotationBy.java"),
+                expected
+        );
+    }
+
+    @Test
+    public void testIgnoreAnnotatedByFullQualifier() throws Exception {
+        final String[] expected = {
+            "9:1: " + getCheckMessage(MSG_KEY),
+        };
+        verifyWithInlineConfigParser(
+                getPath("InputHideUtilityClassConstructor"
+                        + "IgnoreAnnotationByFullyQualifiedName.java"),
+                expected
+        );
+    }
 }
diff --git a/src/test/resources/com/puppycrawl/tools/checkstyle/checks/design/hideutilityclassconstructor/InputHideUtilityClassConstructorIgnoreAnnotationBy.java b/src/test/resources/com/puppycrawl/tools/checkstyle/checks/design/hideutilityclassconstructor/InputHideUtilityClassConstructorIgnoreAnnotationBy.java
new file mode 100644
--- /dev/null
+++ b/src/test/resources/com/puppycrawl/tools/checkstyle/checks/design/hideutilityclassconstructor/InputHideUtilityClassConstructorIgnoreAnnotationBy.java
@@ -0,0 +1,46 @@
+/*
+HideUtilityClassConstructor
+ignoreAnnotatedBy = Skip, SkipWithParam, SkipWithAnnotationAsParam
+
+*/
+
+package com.puppycrawl.tools.checkstyle.checks.design.hideutilityclassconstructor;
+
+@Skip
+public class InputHideUtilityClassConstructorIgnoreAnnotationBy {
+  public static void func() {}
+}
+
+@SkipWithParam(name = "tool1")
+class ToolClass1 {
+  public static void func() {}
+}
+
+@SkipWithAnnotationAsParam(skip = @Skip)
+class ToolClass2 {
+  public static void func() {}
+}
+
+@CommonAnnot
+@Skip
+class ToolClass3 {
+  public static void func() {}
+}
+
+@CommonAnnot // violation, should not have a public or default constructor
+class ToolClass4 {
+  public static void func() {}
+}
+
+
+@interface Skip {}
+
+@interface SkipWithParam {
+  String name();
+}
+
+@interface SkipWithAnnotationAsParam {
+  Skip skip();
+}
+
+@interface CommonAnnot {}
diff --git a/src/test/resources/com/puppycrawl/tools/checkstyle/checks/design/hideutilityclassconstructor/InputHideUtilityClassConstructorIgnoreAnnotationByFullyQualifiedName.java b/src/test/resources/com/puppycrawl/tools/checkstyle/checks/design/hideutilityclassconstructor/InputHideUtilityClassConstructorIgnoreAnnotationByFullyQualifiedName.java
new file mode 100644
--- /dev/null
+++ b/src/test/resources/com/puppycrawl/tools/checkstyle/checks/design/hideutilityclassconstructor/InputHideUtilityClassConstructorIgnoreAnnotationByFullyQualifiedName.java
@@ -0,0 +1,19 @@
+/*
+HideUtilityClassConstructor
+ignoreAnnotatedBy = java.lang.Deprecated
+
+*/
+
+package com.puppycrawl.tools.checkstyle.checks.design.hideutilityclassconstructor;
+
+@Deprecated // violation, should not have a public or default constructor
+public class InputHideUtilityClassConstructorIgnoreAnnotationByFullyQualifiedName {
+  public static void func() {}
+}
+
+@java.lang.Deprecated
+class DeprecatedClass {
+  public static void func() {}
+}
+
+@interface Deprecated {}