Repository: analysis-dev/diktat (kotlin)

Issue:
Allow more prefixes for boolean methods
Like `equals*`, `equivalent*`, or just make it configurable

Pull request:
## What did you add?
  * Added configuration option
  * More prefixes added

This pull request closes #417 

## Actions checklist
* [ ] Implemented Rule, added Warnings
* [X] Added tests on checks
* [ ] Added tests on fixers
* [X] Updated diktat-analysis.yml
* [ ] Updated available-rules.md

Patch:
diff --git a/diktat-analysis.yml b/diktat-analysis.yml
index 5744f1ca3f..d6f4e18929 100644
--- a/diktat-analysis.yml
+++ b/diktat-analysis.yml
@@ -29,6 +29,8 @@
 # Checks that functions/methods which return boolean have special prefix like "is/should/e.t.c"
 - name: FUNCTION_BOOLEAN_PREFIX
   enabled: true
+  configuration:
+    allowedPrefixes: "" # A list of functions that return boolean and are allowed to use. Input is in a form "foo, bar".
 # Checks that function/method name is in lowerCamelCase
 - name: FUNCTION_NAME_INCORRECT_CASE
   enabled: true
diff --git a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/IdentifierNaming.kt b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/IdentifierNaming.kt
index a11a038431..cb37fd2937 100644
--- a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/IdentifierNaming.kt
+++ b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/IdentifierNaming.kt
@@ -79,8 +79,20 @@ import org.jetbrains.kotlin.psi.psiUtil.parents
  * // FixMe: very important, that current implementation cannot fix identifier naming properly,
  * // FixMe: because it fixes only declaration without the usages
  */
-@Suppress("ForbiddenComment")
+@Suppress("ForbiddenComment", "MISSING_KDOC_CLASS_ELEMENTS")
 class IdentifierNaming(private val configRules: List<RulesConfig>) : Rule("identifier-naming") {
+    private val allMethodPrefixes by lazy {
+        if (configuration.allowedBooleanPrefixes.isNullOrEmpty()) {
+            booleanMethodPrefixes
+        } else {
+            booleanMethodPrefixes + configuration.allowedBooleanPrefixes.filter { it.isNotEmpty() }
+        }
+    }
+    val configuration by lazy {
+        BooleanFunctionsConfiguration(
+            this.configRules.getRuleConfig(FUNCTION_BOOLEAN_PREFIX)?.configuration ?: emptyMap()
+        )
+    }
     private var isFixMode: Boolean = false
     private lateinit var emitWarn: EmitType
 
@@ -358,7 +370,7 @@ class IdentifierNaming(private val configRules: List<RulesConfig>) : Rule("ident
         if (!node.isOverridden()) {
             // if function has Boolean return type in 99% of cases it is much better to name it with isXXX or hasXXX prefix
             if (functionReturnType != null && functionReturnType == PrimitiveType.BOOLEAN.typeName.asString()) {
-                if (booleanMethodPrefixes.none { functionName.text.startsWith(it) }) {
+                if (allMethodPrefixes.none { functionName.text.startsWith(it) }) {
                     FUNCTION_BOOLEAN_PREFIX.warnAndFix(configRules, emitWarn, isFixMode, functionName.text, functionName.startOffset, functionName) {
                         // FixMe: add agressive autofix for this
                     }
@@ -426,13 +438,20 @@ class IdentifierNaming(private val configRules: List<RulesConfig>) : Rule("ident
         } ?: Style.SNAKE_CASE
     }
 
+    class BooleanFunctionsConfiguration(config: Map<String, String>) : RuleConfiguration(config) {
+        /**
+         * A list of functions that return boolean and are allowed to use. Input is in a form "foo, bar".
+         */
+        val allowedBooleanPrefixes = config["allowedPrefixes"]?.split(",")?.map { it.trim() } ?: emptyList()
+    }
+
     companion object {
         const val MAX_IDENTIFIER_LENGTH = 64
         const val MIN_IDENTIFIER_LENGTH = 2
 
         // FixMe: this should be moved to properties
         val oneCharIdentifiers = setOf("i", "j", "k", "x", "y", "z")
-        val booleanMethodPrefixes = setOf("has", "is")
+        val booleanMethodPrefixes = setOf("has", "is", "are", "have", "should")
         val confusingIdentifierNames = setOf("O", "D", "I", "l", "Z", "S", "e", "B", "h", "n", "m", "rn")
     }
 }
diff --git a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/PackageNaming.kt b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/PackageNaming.kt
index bd2499cc52..7be9e7c55f 100644
--- a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/PackageNaming.kt
+++ b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/PackageNaming.kt
@@ -150,7 +150,6 @@ class PackageNaming(private val configRules: List<RulesConfig>) : Rule("package-
     /**
      * only letters, digits and underscore are allowed
      */
-    @Suppress("FUNCTION_BOOLEAN_PREFIX")
     private fun areCorrectSymbolsUsed(word: String): Boolean {
         // underscores are allowed in some cases - see "exceptionForUnderscore"
         val wordFromPackage = word.replace("_", "")
diff --git a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/classes/DataClassesRule.kt b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/classes/DataClassesRule.kt
index 1baefc23d9..38ad64dde7 100644
--- a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/classes/DataClassesRule.kt
+++ b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/classes/DataClassesRule.kt
@@ -94,7 +94,6 @@ class DataClassesRule(private val configRule: List<RulesConfig>) : Rule("data-cl
     /**
      * Checks if any property with accessor contains logic in accessor
      */
-    @Suppress("FUNCTION_BOOLEAN_PREFIX")
     private fun areGoodProps(node: ASTNode): Boolean {
         val propertiesWithAccessors = node.getAllChildrenWithType(PROPERTY).filter { it.hasChildOfType(PROPERTY_ACCESSOR) }
 
@@ -109,7 +108,7 @@ class DataClassesRule(private val configRule: List<RulesConfig>) : Rule("data-cl
         return true
     }
 
-    @Suppress("UnsafeCallOnNullableType", "FUNCTION_BOOLEAN_PREFIX")
+    @Suppress("UnsafeCallOnNullableType")
     private fun areGoodAccessors(accessors: List<ASTNode>): Boolean {
         accessors.forEach {
             if (it.hasChildOfType(BLOCK)) {
diff --git a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/utils/AstNodeUtils.kt b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/utils/AstNodeUtils.kt
index a9fc830025..9acba46866 100644
--- a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/utils/AstNodeUtils.kt
+++ b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/utils/AstNodeUtils.kt
@@ -626,7 +626,6 @@ fun ASTNode.isChildBeforeGroup(child: ASTNode, group: List<ASTNode>): Boolean =
  *
  * @return boolean result
  */
-@Suppress("FUNCTION_BOOLEAN_PREFIX")
 fun ASTNode.areChildrenBeforeChild(children: List<ASTNode>, beforeChild: ASTNode): Boolean =
         areChildrenBeforeGroup(children, listOf(beforeChild))
 
@@ -635,7 +634,7 @@ fun ASTNode.areChildrenBeforeChild(children: List<ASTNode>, beforeChild: ASTNode
  *
  * @return boolean result
  */
-@Suppress("UnsafeCallOnNullableType", "FUNCTION_BOOLEAN_PREFIX")
+@Suppress("UnsafeCallOnNullableType")
 fun ASTNode.areChildrenBeforeGroup(children: List<ASTNode>, group: List<ASTNode>): Boolean {
     require(children.isNotEmpty() && group.isNotEmpty()) { "no sense to operate on empty lists" }
     return children.map { getChildren(null).indexOf(it) }.max()!! < group.map { getChildren(null).indexOf(it) }.min()!!
diff --git a/diktat-rules/src/main/resources/diktat-analysis-huawei.yml b/diktat-rules/src/main/resources/diktat-analysis-huawei.yml
index ab0fd7cc40..799177837d 100644
--- a/diktat-rules/src/main/resources/diktat-analysis-huawei.yml
+++ b/diktat-rules/src/main/resources/diktat-analysis-huawei.yml
@@ -29,6 +29,8 @@
 # Checks that functions/methods which return boolean have special prefix like "is/should/e.t.c"
 - name: FUNCTION_BOOLEAN_PREFIX
   enabled: true
+  configuration:
+    allowedPrefixes: "" # A list of functions that return boolean and are allowed to use. Input is in a form "foo, bar".
 # Checks that function/method name is in lowerCamelCase
 - name: FUNCTION_NAME_INCORRECT_CASE
   enabled: true
diff --git a/diktat-rules/src/main/resources/diktat-analysis.yml b/diktat-rules/src/main/resources/diktat-analysis.yml
index 009a44e353..dd70b9560b 100644
--- a/diktat-rules/src/main/resources/diktat-analysis.yml
+++ b/diktat-rules/src/main/resources/diktat-analysis.yml
@@ -28,6 +28,8 @@
 # Checks that functions/methods which return boolean have special prefix like "is/should/e.t.c"
 - name: FUNCTION_BOOLEAN_PREFIX
   enabled: true
+  configuration:
+    allowedPrefixes: "" # A list of functions that return boolean and are allowed to use. Input is in a form "foo, bar".
 # Checks that function/method name is in lowerCamelCase
 - name: FUNCTION_NAME_INCORRECT_CASE
   enabled: true


Test patch:
diff --git a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter1/IdentifierNamingWarnTest.kt b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter1/IdentifierNamingWarnTest.kt
index d765473f31..3a1b28baac 100644
--- a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter1/IdentifierNamingWarnTest.kt
+++ b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter1/IdentifierNamingWarnTest.kt
@@ -26,6 +26,10 @@ import org.junit.jupiter.api.Test
 
 class IdentifierNamingWarnTest : LintTestBase(::IdentifierNaming) {
     private val ruleId: String = "$DIKTAT_RULE_SET_ID:identifier-naming"
+    private val rulesConfigBooleanFunctions: List<RulesConfig> = listOf(
+        RulesConfig(FUNCTION_BOOLEAN_PREFIX.name, true,
+            mapOf("allowedPrefixes" to "equals, equivalent, foo"))
+    )
 
     // ======== checks for generics ========
     @Test
@@ -392,6 +396,33 @@ class IdentifierNamingWarnTest : LintTestBase(::IdentifierNaming) {
         )
     }
 
+    @Test
+    @Tag(WarningNames.FUNCTION_BOOLEAN_PREFIX)
+    fun `all prefixes for boolean methods`() {
+        lintMethod(
+            """
+                    fun hasEmptyLineAfter(): Boolean { }    
+                    fun haveEmptyLineAfter(): Boolean { }    
+                    fun isEmpty(): Boolean { }    
+                    fun shouldBeEmpty(): Boolean { }
+                    fun areEmpty(): Boolean { }
+                """.trimIndent()
+        )
+    }
+
+    @Test
+    @Tag(WarningNames.FUNCTION_BOOLEAN_PREFIX)
+    fun `test allowed boolean functions in configuration`() {
+        lintMethod(
+            """
+                    fun equalsSome(): Boolean { }
+                    fun fooBar(): Boolean { }
+                    fun equivalentToAnother(): Boolean { }
+                """.trimIndent(),
+            rulesConfigList = rulesConfigBooleanFunctions
+        )
+    }
+
     @Test
     @Tag(WarningNames.IDENTIFIER_LENGTH)
     fun `regression - function argument type`() {