Repository: analysis-dev/diktat (kotlin)

Issue:
Recommendation 6.4.2: Objects should be used for Stateless Interfaces
Kotlin’s object are extremely useful when we need to implement a some interface from an external library that doesn’t have any state.
No need to use class for such structures.

Good example:
```kotlin
interface I {
    fun foo()
}

object O: I {
    override fun foo() {}
}
```

Pull request description:
## Which rule and warnings did you add?
Recommendation 6.4.2: Objects should be used for Stateless Interfaces

This pull request closes #449

## Actions checklist
* [X] Implemented Rule, added Warnings
* [X] Added tests on checks
* [X] Added tests on fixers
* [X] Updated diktat-analysis.yml
* [X] Updated available-rules.md

Patch:
diff --git a/diktat-analysis.yml b/diktat-analysis.yml
index 2010f51806..38a03d1f58 100644
--- a/diktat-analysis.yml
+++ b/diktat-analysis.yml
@@ -311,4 +311,7 @@
   enabled: true
 # Checks if there is class/object that can be replace with extension function
 - name: AVOID_USING_UTILITY_CLASS
+  enabled: true
+# If there is stateless class it is preferred to use object
+- name: OBJECT_IS_PREFERRED
   enabled: true
\ No newline at end of file
diff --git a/diktat-gradle-plugin/gradlew.bat b/diktat-gradle-plugin/gradlew.bat
old mode 100644
new mode 100755
diff --git a/diktat-rules/src/main/kotlin/generated/WarningNames.kt b/diktat-rules/src/main/kotlin/generated/WarningNames.kt
index 52735c2a16..10842cf258 100644
--- a/diktat-rules/src/main/kotlin/generated/WarningNames.kt
+++ b/diktat-rules/src/main/kotlin/generated/WarningNames.kt
@@ -227,4 +227,6 @@ public object WarningNames {
     public const val NO_CORRESPONDING_PROPERTY: String = "NO_CORRESPONDING_PROPERTY"
 
     public const val AVOID_USING_UTILITY_CLASS: String = "AVOID_USING_UTILITY_CLASS"
+
+    public const val OBJECT_IS_PREFERRED: String = "OBJECT_IS_PREFERRED"
 }
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 e0c7f06f0e..156d662dae 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
@@ -146,6 +146,7 @@ enum class Warnings(
     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"),
+    OBJECT_IS_PREFERRED(true, "6.4.2", "it is better to use object for stateless classes"),
     ;
 
     /**
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 b9c8c4a57b..bdc99d4d77 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
@@ -10,6 +10,7 @@ import org.cqfn.diktat.ruleset.rules.classes.CompactInitialization
 import org.cqfn.diktat.ruleset.rules.classes.DataClassesRule
 import org.cqfn.diktat.ruleset.rules.classes.SingleConstructorRule
 import org.cqfn.diktat.ruleset.rules.classes.SingleInitRule
+import org.cqfn.diktat.ruleset.rules.classes.StatelessClassesRule
 import org.cqfn.diktat.ruleset.rules.comments.CommentsRule
 import org.cqfn.diktat.ruleset.rules.comments.HeaderCommentRule
 import org.cqfn.diktat.ruleset.rules.files.BlankLinesRule
@@ -52,7 +53,7 @@ class DiktatRuleSetProvider(private var diktatConfigFile: String = DIKTAT_ANALYS
         val diktatExecutionPath = File(diktatConfigFile)
         if (!diktatExecutionPath.exists()) {
             // for some aggregators of static analyzers we need to provide configuration for cli
-            // in this case diktat would take the configuration from the direcory where jar file is stored
+            // in this case diktat would take the configuration from the directory where jar file is stored
             val ruleSetProviderPath =
                     DiktatRuleSetProvider::class
                         .java
@@ -112,6 +113,7 @@ class DiktatRuleSetProvider(private var diktatConfigFile: String = DIKTAT_ANALYS
             ::CustomGetterSetterRule,
             ::CompactInitialization,
             // other rules
+            ::StatelessClassesRule,
             ::ImplicitBackingPropertyRule,
             ::StringTemplateFormatRule,
             ::DataClassesRule,
diff --git a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/classes/StatelessClassesRule.kt b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/classes/StatelessClassesRule.kt
new file mode 100644
index 0000000000..8a3da3bf1d
--- /dev/null
+++ b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/classes/StatelessClassesRule.kt
@@ -0,0 +1,98 @@
+package org.cqfn.diktat.ruleset.rules.classes
+
+import org.cqfn.diktat.common.config.rules.RulesConfig
+import org.cqfn.diktat.ruleset.constants.EmitType
+import org.cqfn.diktat.ruleset.constants.Warnings
+import org.cqfn.diktat.ruleset.utils.findAllNodesWithSpecificType
+import org.cqfn.diktat.ruleset.utils.getAllChildrenWithType
+import org.cqfn.diktat.ruleset.utils.getFirstChildWithType
+import org.cqfn.diktat.ruleset.utils.hasChildOfType
+
+import com.pinterest.ktlint.core.Rule
+import com.pinterest.ktlint.core.ast.ElementType.CLASS
+import com.pinterest.ktlint.core.ast.ElementType.CLASS_KEYWORD
+import com.pinterest.ktlint.core.ast.ElementType.FILE
+import com.pinterest.ktlint.core.ast.ElementType.FUN
+import com.pinterest.ktlint.core.ast.ElementType.IDENTIFIER
+import com.pinterest.ktlint.core.ast.ElementType.INTERFACE_KEYWORD
+import com.pinterest.ktlint.core.ast.ElementType.OBJECT_DECLARATION
+import com.pinterest.ktlint.core.ast.ElementType.OBJECT_KEYWORD
+import com.pinterest.ktlint.core.ast.ElementType.SUPER_TYPE_ENTRY
+import com.pinterest.ktlint.core.ast.ElementType.SUPER_TYPE_LIST
+import com.pinterest.ktlint.core.ast.children
+import org.jetbrains.kotlin.com.intellij.lang.ASTNode
+import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.CompositeElement
+import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement
+import org.jetbrains.kotlin.psi.KtClass
+
+/**
+ * This rule checks if class is stateless and if so changes it to object.
+ */
+class StatelessClassesRule(private val configRule: List<RulesConfig>) : Rule("stateless-class") {
+    private var isFixMode: Boolean = false
+    private lateinit var emitWarn: EmitType
+
+    override fun visit(node: ASTNode,
+                       autoCorrect: Boolean,
+                       emit: EmitType) {
+        emitWarn = emit
+        isFixMode = autoCorrect
+
+        // Fixme: We should find interfaces in all project and then check them
+        if (node.elementType == FILE) {
+            val interfacesNodes = node
+                .findAllNodesWithSpecificType(CLASS)
+                .filter { it.hasChildOfType(INTERFACE_KEYWORD) }
+            node
+                .findAllNodesWithSpecificType(CLASS)
+                .filterNot { it.hasChildOfType(INTERFACE_KEYWORD) }
+                .forEach { handleClass(it, interfacesNodes) }
+        }
+    }
+
+    @Suppress("UnsafeCallOnNullableType")
+    private fun handleClass(node: ASTNode, interfaces: List<ASTNode>) {
+        if (isClassExtendsValidInterface(node, interfaces) && isStatelessClass(node)) {
+            Warnings.OBJECT_IS_PREFERRED.warnAndFix(configRule, emitWarn, isFixMode,
+                "class ${(node.psi as KtClass).name!!}", node.startOffset, node) {
+                val newObjectNode = CompositeElement(OBJECT_DECLARATION)
+                node.treeParent.addChild(newObjectNode, node)
+                node.children().forEach {
+                    newObjectNode.addChild(it.copyElement(), null)
+                }
+                newObjectNode.addChild(LeafPsiElement(OBJECT_KEYWORD, "object"),
+                    newObjectNode.getFirstChildWithType(CLASS_KEYWORD))
+                newObjectNode.removeChild(newObjectNode.getFirstChildWithType(CLASS_KEYWORD)!!)
+                node.treeParent.removeChild(node)
+            }
+        }
+    }
+
+    private fun isStatelessClass(node: ASTNode): Boolean {
+        val properties = (node.psi as KtClass).getProperties()
+        val functions = node.findAllNodesWithSpecificType(FUN)
+        return properties.isNullOrEmpty() &&
+                functions.isNotEmpty() &&
+                !(node.psi as KtClass).hasExplicitPrimaryConstructor()
+    }
+
+    private fun isClassExtendsValidInterface(node: ASTNode, interfaces: List<ASTNode>): Boolean =
+            node.findChildByType(SUPER_TYPE_LIST)
+                ?.getAllChildrenWithType(SUPER_TYPE_ENTRY)
+                ?.isNotEmpty()
+                ?.and(isClassInheritsStatelessInterface(node, interfaces))
+                ?: false
+
+    @Suppress("UnsafeCallOnNullableType")
+    private fun isClassInheritsStatelessInterface(node: ASTNode, interfaces: List<ASTNode>): Boolean {
+        val classInterfaces = node
+            .findChildByType(SUPER_TYPE_LIST)
+            ?.getAllChildrenWithType(SUPER_TYPE_ENTRY)
+
+        val foundInterfaces = interfaces.filter { inter ->
+            classInterfaces!!.any { it.text == inter.getFirstChildWithType(IDENTIFIER)!!.text }
+        }
+
+        return foundInterfaces.any { (it.psi as KtClass).getProperties().isEmpty() }
+    }
+}
diff --git a/diktat-rules/src/main/resources/diktat-analysis-huawei.yml b/diktat-rules/src/main/resources/diktat-analysis-huawei.yml
index 99342d1c1e..3d8614fc42 100644
--- a/diktat-rules/src/main/resources/diktat-analysis-huawei.yml
+++ b/diktat-rules/src/main/resources/diktat-analysis-huawei.yml
@@ -309,4 +309,7 @@
   enabled: true
 # Checks if there is class/object that can be replace with extension function
 - name: AVOID_USING_UTILITY_CLASS
+  enabled: true
+# If there is stateless class it is preferred to use object
+- name: OBJECT_IS_PREFERRED
   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 05983b1d53..a043d1d4d5 100644
--- a/diktat-rules/src/main/resources/diktat-analysis.yml
+++ b/diktat-rules/src/main/resources/diktat-analysis.yml
@@ -311,4 +311,7 @@
   enabled: true
 # Checks if there is class/object that can be replace with extension function
 - name: AVOID_USING_UTILITY_CLASS
+  enabled: true
+# If there is stateless class it is preferred to use object
+- name: OBJECT_IS_PREFERRED
   enabled: true
\ No newline at end of file
diff --git a/info/available-rules.md b/info/available-rules.md
index 481807504e..55d21e7082 100644
--- a/info/available-rules.md
+++ b/info/available-rules.md
@@ -106,4 +106,5 @@
 |    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 | - | + |
-|    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
+|    6 |     6.4.1 | AVOID_USING_UTILITY_CLASS              | Checks if there is class/object that can be replace with extension function                                                                                                  | no  | - | - |
+|    6 |     6.4.2 | OBJECT_IS_PREFERRED                    | Checks: if class is stateless it is preferred to use `object`                                                                                                                | yes | - | + |
\ No newline at end of file


Test patch:
diff --git a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter6/StatelessClassesRuleFixTest.kt b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter6/StatelessClassesRuleFixTest.kt
new file mode 100644
index 0000000000..f7938845b6
--- /dev/null
+++ b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter6/StatelessClassesRuleFixTest.kt
@@ -0,0 +1,16 @@
+package org.cqfn.diktat.ruleset.chapter6
+
+import org.cqfn.diktat.ruleset.rules.classes.StatelessClassesRule
+import org.cqfn.diktat.util.FixTestBase
+
+import generated.WarningNames.OBJECT_IS_PREFERRED
+import org.junit.jupiter.api.Tag
+import org.junit.jupiter.api.Test
+
+class StatelessClassesRuleFixTest : FixTestBase("test/chapter6/stateless_classes", ::StatelessClassesRule) {
+    @Test
+    @Tag(OBJECT_IS_PREFERRED)
+    fun `fix class to object keyword`() {
+        fixAndCompare("StatelessClassExpected.kt", "StatelessClassTest.kt")
+    }
+}
diff --git a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter6/StatelessClassesRuleWarnTest.kt b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter6/StatelessClassesRuleWarnTest.kt
new file mode 100644
index 0000000000..3717958826
--- /dev/null
+++ b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter6/StatelessClassesRuleWarnTest.kt
@@ -0,0 +1,83 @@
+package org.cqfn.diktat.ruleset.chapter6
+
+import org.cqfn.diktat.ruleset.constants.Warnings
+import org.cqfn.diktat.ruleset.rules.DIKTAT_RULE_SET_ID
+import org.cqfn.diktat.ruleset.rules.classes.StatelessClassesRule
+import org.cqfn.diktat.util.LintTestBase
+
+import com.pinterest.ktlint.core.LintError
+import generated.WarningNames.OBJECT_IS_PREFERRED
+import org.junit.jupiter.api.Tag
+import org.junit.jupiter.api.Test
+
+class StatelessClassesRuleWarnTest : LintTestBase(::StatelessClassesRule) {
+    private val ruleId = "$DIKTAT_RULE_SET_ID:stateless-class"
+
+    @Test
+    @Tag(OBJECT_IS_PREFERRED)
+    fun `should not trigger on class not extending any interface`() {
+        lintMethod(
+            """
+            |class Some : I() {
+            |   override fun some()
+            |}
+            """.trimMargin()
+        )
+    }
+
+    @Test
+    @Tag(OBJECT_IS_PREFERRED)
+    fun `should trigger on class extending interface`() {
+        lintMethod(
+            """
+            |class Some : I {
+            |   override fun some()
+            |}
+            |
+            |interface I {
+            |   fun some()
+            |}
+            """.trimMargin(),
+            LintError(1, 1, ruleId, "${Warnings.OBJECT_IS_PREFERRED.warnText()} class Some", true)
+        )
+    }
+
+    @Test
+    @Tag(OBJECT_IS_PREFERRED)
+    fun `should not trigger on class with constructor`() {
+        lintMethod(
+            """
+            |class Some(b: Int) : I {
+            |   
+            |   override fun some()
+            |}
+            """.trimMargin()
+        )
+    }
+
+    @Test
+    @Tag(OBJECT_IS_PREFERRED)
+    fun `should not trigger on class with no interface in this file`() {
+        lintMethod(
+            """
+            |class Some : I {
+            |   
+            |   override fun some()
+            |}
+            """.trimMargin()
+        )
+    }
+
+    @Test
+    @Tag(OBJECT_IS_PREFERRED)
+    fun `should not trigger on class with state`() {
+        lintMethod(
+            """
+            |class Some : I {
+            |   val a = 5
+            |   override fun some()
+            |}
+            """.trimMargin()
+        )
+    }
+}
diff --git a/diktat-rules/src/test/resources/test/chapter6/stateless_classes/StatelessClassExpected.kt b/diktat-rules/src/test/resources/test/chapter6/stateless_classes/StatelessClassExpected.kt
new file mode 100644
index 0000000000..ed065cfdfd
--- /dev/null
+++ b/diktat-rules/src/test/resources/test/chapter6/stateless_classes/StatelessClassExpected.kt
@@ -0,0 +1,16 @@
+package test.chapter6.stateless_classes
+
+interface I {
+    fun foo()
+}
+
+object O: I {
+    override fun foo() {}
+}
+
+/**
+ * Some KDOC
+ */
+object A: I {
+    override fun foo() {}
+}
diff --git a/diktat-rules/src/test/resources/test/chapter6/stateless_classes/StatelessClassTest.kt b/diktat-rules/src/test/resources/test/chapter6/stateless_classes/StatelessClassTest.kt
new file mode 100644
index 0000000000..dd4fc6614c
--- /dev/null
+++ b/diktat-rules/src/test/resources/test/chapter6/stateless_classes/StatelessClassTest.kt
@@ -0,0 +1,16 @@
+package test.chapter6.stateless_classes
+
+interface I {
+    fun foo()
+}
+
+class O: I {
+    override fun foo() {}
+}
+
+/**
+ * Some KDOC
+ */
+class A: I {
+    override fun foo() {}
+}