Repository: analysis-dev/diktat (kotlin)

Issue:
Rule 6.4.1: Avoid using utility classes/objects, use extensions instead
As described in [6.2 Extension functions] - extension functions - is a very powerful mechanism.
So instead of using utility classes/objects, use it instead.
This allows you to remove the unnecessary complexity and wrapping class/object and to use top-level functions instead.

Bad example:
```kotlin 
    object StringUtil {
        fun stringInfo(myString: String): Int {
            return myString.count{ "something".contains(it) }
        }
    }
    StringUtil.stringInfo("myStr")
```

Good example:
```kotlin
    fun String.stringInfo(): Int {
        return this.count{ "something".contains(it) }
    }

    "myStr".stringInfo()
```

Pull request:
## Actions checklist
* [x] Implemented Rule, added Warnings
* [x] Added tests on checks
* [ ] Added tests on fixers
* [x] Updated diktat-analysis.yml
* [x] Updated available-rules.md
* [x] closes #448

Patch:
diff --git a/diktat-analysis.yml b/diktat-analysis.yml
index c185824b71..a6a0163678 100644
--- a/diktat-analysis.yml
+++ b/diktat-analysis.yml
@@ -305,4 +305,7 @@
 # there should be explicit backing property with the name of real property
 # Example: val table get() {if (_table == null) ...} -> table should have _table
 - name: NO_CORRESPONDING_PROPERTY
+  enabled: true
+# Checks if there is class/object that can be replace with extension function
+- name: AVOID_USING_UTILITY_CLASS
   enabled: true
\ No newline at end of file
diff --git a/diktat-rules/src/main/kotlin/generated/WarningNames.kt b/diktat-rules/src/main/kotlin/generated/WarningNames.kt
index 9a7f3d4ce0..a73ff5a170 100644
--- a/diktat-rules/src/main/kotlin/generated/WarningNames.kt
+++ b/diktat-rules/src/main/kotlin/generated/WarningNames.kt
@@ -223,4 +223,6 @@ public object WarningNames {
     public const val EMPTY_PRIMARY_CONSTRUCTOR: String = "EMPTY_PRIMARY_CONSTRUCTOR"
 
     public const val NO_CORRESPONDING_PROPERTY: String = "NO_CORRESPONDING_PROPERTY"
+
+    public const val AVOID_USING_UTILITY_CLASS: String = "AVOID_USING_UTILITY_CLASS"
 }
diff --git a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/constants/Warnings.kt b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/constants/Warnings.kt
index 492b8a10d8..d1ff316194 100644
--- a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/constants/Warnings.kt
+++ b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/constants/Warnings.kt
@@ -142,6 +142,7 @@ enum class Warnings(
     EXTENSION_FUNCTION_SAME_SIGNATURE(false, "6.2.2", "extension functions should not have same signature if their receiver classes are related"),
     EMPTY_PRIMARY_CONSTRUCTOR(true, "6.1.3", "avoid empty primary constructor"),
     NO_CORRESPONDING_PROPERTY(false, "6.1.7", "backing property should have the same name, but there is no corresponding property"),
+    AVOID_USING_UTILITY_CLASS(false, "6.4.1", "avoid using utility classes/objects, use extensions functions"),
     ;
 
     /**
diff --git a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/AvoidUtilityClass.kt b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/AvoidUtilityClass.kt
new file mode 100644
index 0000000000..7d24a53359
--- /dev/null
+++ b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/AvoidUtilityClass.kt
@@ -0,0 +1,67 @@
+package org.cqfn.diktat.ruleset.rules
+
+import org.cqfn.diktat.common.config.rules.RulesConfig
+import org.cqfn.diktat.common.config.rules.getCommonConfiguration
+import org.cqfn.diktat.ruleset.constants.EmitType
+import org.cqfn.diktat.ruleset.constants.Warnings.AVOID_USING_UTILITY_CLASS
+import org.cqfn.diktat.ruleset.utils.*
+
+import com.pinterest.ktlint.core.Rule
+import com.pinterest.ktlint.core.ast.ElementType.BLOCK_COMMENT
+import com.pinterest.ktlint.core.ast.ElementType.CLASS
+import com.pinterest.ktlint.core.ast.ElementType.CLASS_BODY
+import com.pinterest.ktlint.core.ast.ElementType.EOL_COMMENT
+import com.pinterest.ktlint.core.ast.ElementType.FUN
+import com.pinterest.ktlint.core.ast.ElementType.IDENTIFIER
+import com.pinterest.ktlint.core.ast.ElementType.KDOC
+import com.pinterest.ktlint.core.ast.ElementType.LBRACE
+import com.pinterest.ktlint.core.ast.ElementType.OBJECT_DECLARATION
+import com.pinterest.ktlint.core.ast.ElementType.PRIMARY_CONSTRUCTOR
+import com.pinterest.ktlint.core.ast.ElementType.RBRACE
+import com.pinterest.ktlint.core.ast.ElementType.WHITE_SPACE
+import org.jetbrains.kotlin.com.intellij.lang.ASTNode
+import org.jetbrains.kotlin.psi.psiUtil.children
+
+/**
+ * Rule 6.4.1 checks that class/object, with a word "util" in its name, has only functions.
+ */
+class AvoidUtilityClass(private val configRules: List<RulesConfig>) : Rule("avoid-utility-class") {
+    private var isFixMode: Boolean = false
+    private lateinit var emitWarn: EmitType
+
+    override fun visit(node: ASTNode,
+                       autoCorrect: Boolean,
+                       emit: EmitType) {
+        emitWarn = emit
+        isFixMode = autoCorrect
+        val config by configRules.getCommonConfiguration()
+        val fileName = node.getRootNode().getFileName()
+        if (!(node.hasTestAnnotation() || isLocatedInTest(fileName.splitPathToDirs(), config.testAnchors))) {
+            if (node.elementType == OBJECT_DECLARATION || node.elementType == CLASS) {
+                checkClass(node)
+            }
+        }
+    }
+
+    @Suppress("UnsafeCallOnNullableType", "WRONG_NEWLINES")
+    private fun checkClass(node: ASTNode) {
+        // checks that class/object doesn't contain primary constructor and its identifier doesn't has "utli"
+        if (!node.hasChildOfType(IDENTIFIER) || node.hasChildOfType(PRIMARY_CONSTRUCTOR) ||
+                !node.findChildByType(IDENTIFIER)!!.text.toLowerCase().contains("util")) {
+            return
+        }
+        node.findChildByType(CLASS_BODY)
+            ?.children()
+            ?.toList()
+            ?.takeIf { childList -> childList.all { it.elementType in utilityClassChildren } }
+            ?.filter { it.elementType == FUN }
+            ?.ifEmpty { return }
+            ?: return
+        AVOID_USING_UTILITY_CLASS.warn(configRules, emitWarn, isFixMode, node.findChildByType(IDENTIFIER)?.text ?: node.text, node.startOffset, node)
+    }
+
+    companion object {
+        private val utilityClassChildren = listOf(LBRACE, WHITE_SPACE, FUN, RBRACE, KDOC,
+            EOL_COMMENT, BLOCK_COMMENT, OBJECT_DECLARATION)
+    }
+}
diff --git a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/DiktatRuleSetProvider.kt b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/DiktatRuleSetProvider.kt
index dd865a6e51..b9c8c4a57b 100644
--- a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/DiktatRuleSetProvider.kt
+++ b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/DiktatRuleSetProvider.kt
@@ -117,6 +117,7 @@ class DiktatRuleSetProvider(private var diktatConfigFile: String = DIKTAT_ANALYS
             ::DataClassesRule,
             ::LocalVariablesRule,
             ::SmartCastRule,
+            ::AvoidUtilityClass,
             ::PropertyAccessorFields,
             ::AbstractClassesRule,
             ::SingleInitRule,
diff --git a/diktat-rules/src/main/resources/diktat-analysis-huawei.yml b/diktat-rules/src/main/resources/diktat-analysis-huawei.yml
index abacac11c0..562e152463 100644
--- a/diktat-rules/src/main/resources/diktat-analysis-huawei.yml
+++ b/diktat-rules/src/main/resources/diktat-analysis-huawei.yml
@@ -303,4 +303,7 @@
 # there should be explicit backing property with the name of real property
 # Example: val table get() {if (_table == null) ...} -> table should have _table
 - name: NO_CORRESPONDING_PROPERTY
+  enabled: true
+# Checks if there is class/object that can be replace with extension function
+- name: AVOID_USING_UTILITY_CLASS
   enabled: true
\ No newline at end of file
diff --git a/diktat-rules/src/main/resources/diktat-analysis.yml b/diktat-rules/src/main/resources/diktat-analysis.yml
index 385c80565e..9030e614e5 100644
--- a/diktat-rules/src/main/resources/diktat-analysis.yml
+++ b/diktat-rules/src/main/resources/diktat-analysis.yml
@@ -305,4 +305,7 @@
 # there should be explicit backing property with the name of real property
 # Example: val table get() {if (_table == null) ...} -> table should have _table
 - name: NO_CORRESPONDING_PROPERTY
+  enabled: true
+# Checks if there is class/object that can be replace with extension function
+- name: AVOID_USING_UTILITY_CLASS
   enabled: true
\ No newline at end of file
diff --git a/info/available-rules.md b/info/available-rules.md
index 7f513d8c1d..e67aa65cfb 100644
--- a/info/available-rules.md
+++ b/info/available-rules.md
@@ -104,4 +104,5 @@
 |    6 |    6.1.10 | TRIVIAL_ACCESSORS_ARE_NOT_RECOMMENDED  | Check: if there are any trivial getters or setters <br> Fix: Delete trivial getter or setter                                                                                 | yes | - | - |
 |    6 |    6.1.8  | CUSTOM_GETTERS_SETTERS                 | Check: Inspection that checks that no custom getters and setters are used for properties                                                                                     | no | - | - |
 |    6 |    6.1.11 | COMPACT_OBJECT_INITIALIZATION          | Checks if class instantiation can be wrapped in `apply` for better readability |  |  | |
-|    6 |     6.2.2 | EXTENSION_FUNCTION_SAME_SIGNATURE      | Checks if extension function has the same signature as another extension function and their classes are related                                                              | no | - | + |
\ No newline at end of file
+|    6 |     6.2.2 | EXTENSION_FUNCTION_SAME_SIGNATURE      | Checks if extension function has the same signature as another extension function and their classes are related                                                              | no | - | + |
+|    6 |     6.4.1 | AVOID_USING_UTILITY_CLASS              | Checks if there is class/object that can be replace with extension function                                                                                                  | no  | - | - |
\ No newline at end of file


Test patch:
diff --git a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter6/AvoidUtilityClassWarnTest.kt b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter6/AvoidUtilityClassWarnTest.kt
new file mode 100644
index 0000000000..258a952f48
--- /dev/null
+++ b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter6/AvoidUtilityClassWarnTest.kt
@@ -0,0 +1,136 @@
+package org.cqfn.diktat.ruleset.chapter6
+
+import org.cqfn.diktat.ruleset.constants.Warnings.AVOID_USING_UTILITY_CLASS
+import org.cqfn.diktat.ruleset.rules.AvoidUtilityClass
+import org.cqfn.diktat.ruleset.rules.DIKTAT_RULE_SET_ID
+import org.cqfn.diktat.util.LintTestBase
+
+import com.pinterest.ktlint.core.LintError
+import generated.WarningNames
+import org.junit.jupiter.api.Tag
+import org.junit.jupiter.api.Test
+
+class AvoidUtilityClassWarnTest : LintTestBase(::AvoidUtilityClass) {
+    private val ruleId = "$DIKTAT_RULE_SET_ID:avoid-utility-class"
+
+    @Test
+    @Tag(WarningNames.AVOID_USING_UTILITY_CLASS)
+    fun `simple test`() {
+        lintMethod(
+            """
+                    |object StringUtil {
+                    |   fun stringInfo(myString: String): Int {
+                    |       return myString.count{ "something".contains(it) }
+                    |   }
+                    |}   
+                    |
+                    |class A() {
+                    |   fun foo() { }
+                    |}
+                    |
+                    |class StringUtils {
+                    |   fun goo(tex: String): Int {
+                    |       return myString.count{ "something".contains(it) }
+                    |   }
+                    |}
+                    |
+                    |class StringUtil {
+                    |   val z = "hello"
+                    |   fun goo(tex: String): Int {
+                    |       return myString.count{ "something".contains(it) }
+                    |   }
+                    |}
+                """.trimMargin(),
+            LintError(1, 1, ruleId, "${AVOID_USING_UTILITY_CLASS.warnText()} StringUtil"),
+            LintError(11, 1, ruleId, "${AVOID_USING_UTILITY_CLASS.warnText()} StringUtils")
+        )
+    }
+
+    @Test
+    @Tag(WarningNames.AVOID_USING_UTILITY_CLASS)
+    fun `test with comment anf companion`() {
+        lintMethod(
+            """
+                    |
+                    |class StringUtils {
+                    |   companion object  {
+                    |       private val name = "Hello"
+                    |   }
+                    |   /**
+                    |    * @param tex
+                    |    */
+                    |   fun goo(tex: String): Int {
+                    |       //hehe
+                    |       return myString.count{ "something".contains(it) }
+                    |   }
+                    |}
+                    |
+                    |class StringUtil {
+                    |   /*
+                    |
+                    |    */
+                    |   companion object  {
+                    |   }
+                    |   val z = "hello"
+                    |   fun goo(tex: String): Int {
+                    |       return myString.count{ "something".contains(it) }
+                    |   }
+                    |}
+                """.trimMargin(),
+            LintError(2, 1, ruleId, "${AVOID_USING_UTILITY_CLASS.warnText()} StringUtils")
+        )
+    }
+
+    @Test
+    @Tag(WarningNames.AVOID_USING_UTILITY_CLASS)
+    fun `test with class without identifier`() {
+        lintMethod(
+            """
+                    fun foo() {
+                        window.addMouseListener(object : MouseAdapter() {
+                            override fun mouseClicked(e: MouseEvent) { /*...*/ }
+                        
+                            override fun mouseEntered(e: MouseEvent) { /*...*/ }
+                        })
+                    }
+                """.trimMargin()
+        )
+    }
+
+    @Test
+    @Tag(WarningNames.AVOID_USING_UTILITY_CLASS)
+    fun `check test-class`() {
+        lintMethod(
+            """
+                    |class StringUtils {
+                    |   fun goo(tex: String): Int {
+                    |       return myString.count{ "something".contains(it) }
+                    |   }
+                    |}
+                """.trimMargin(),
+            LintError(1, 1, ruleId, "${AVOID_USING_UTILITY_CLASS.warnText()} StringUtils"),
+            fileName = "src/main/kotlin/org/cqfn/diktat/Example.kt",
+        )
+        lintMethod(
+            """
+                    |@Test
+                    |class StringUtils1 {
+                    |   fun goo(tex: String): Int {
+                    |       return myString.count{ "something".contains(it) }
+                    |   }
+                    |}
+                """.trimMargin(),
+            fileName = "src/test/kotlin/org/cqfn/diktat/Example.kt"
+        )
+        lintMethod(
+            """
+                    |class StringUtils2 {
+                    |   fun goo(tex: String): Int {
+                    |       return myString.count{ "something".contains(it) }
+                    |   }
+                    |}
+                """.trimMargin(),
+            fileName = "src/test/kotlin/org/cqfn/diktat/UtilTest.kt"
+        )
+    }
+}