Repository: cqfn/diKTat (kotlin)

Issue:
Allow custom source directory names, like nativeMain
## Describe the bug
Official guidelines for sharing code among platforms suggest using directories like `nativeMain`. Diktat now allows only `common*` and platform-specific names. We should add a new configuration option in cofig.

## Environment information
* diktat version: 0.4.0

Pull request description:
### What's done:
   Added new configuration param
   closes #773

Patch:
diff --git a/diktat-analysis.yml b/diktat-analysis.yml
index 4e4ee6bc7a..af33e6558d 100644
--- a/diktat-analysis.yml
+++ b/diktat-analysis.yml
@@ -8,6 +8,7 @@
     disabledChapters: ""
     testDirs: test
     kotlinVersion: 1.4.30
+    srcDirectories: "main"
 # Checks that the Class/Enum/Interface name does not match Pascal case
 - name: CLASS_NAME_INCORRECT
   enabled: true
diff --git a/diktat-common/src/main/kotlin/org/cqfn/diktat/common/config/rules/RulesConfigReader.kt b/diktat-common/src/main/kotlin/org/cqfn/diktat/common/config/rules/RulesConfigReader.kt
index 771e063597..9cc8ac6f61 100644
--- a/diktat-common/src/main/kotlin/org/cqfn/diktat/common/config/rules/RulesConfigReader.kt
+++ b/diktat-common/src/main/kotlin/org/cqfn/diktat/common/config/rules/RulesConfigReader.kt
@@ -101,7 +101,11 @@ data class CommonConfiguration(private val configuration: Map<String, String>?)
      * List of directory names which will be used to detect test sources
      */
     val testAnchors: List<String> by lazy {
-        (configuration ?: emptyMap()).getOrDefault("testDirs", "test").split(',')
+        val testDirs = (configuration ?: emptyMap()).getOrDefault("testDirs", "test").split(',')
+        if (testDirs.any { !it.toLowerCase().endsWith("test") }) {
+            log.error("test directory names should end with `test`")
+        }
+        testDirs
     }
 
     /**
@@ -130,6 +134,17 @@ data class CommonConfiguration(private val configuration: Map<String, String>?)
         }
     }
 
+    /**
+     * Get source directories from configuration
+     */
+    val srcDirectories: List<String> by lazy {
+        val srcDirs = configuration?.get("srcDirectories")?.split(",")?.map { it.trim() } ?: listOf("main")
+        if (srcDirs.any { !it.toLowerCase().endsWith("main") }) {
+            log.error("source directory names should end with `main`")
+        }
+        srcDirs
+    }
+
     /**
      * False if configuration has been read from config file, true if defaults are used
      */
diff --git a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter1/PackageNaming.kt b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter1/PackageNaming.kt
index cfb90b317f..835060810d 100644
--- a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter1/PackageNaming.kt
+++ b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter1/PackageNaming.kt
@@ -1,5 +1,6 @@
 package org.cqfn.diktat.ruleset.rules.chapter1
 
+import org.cqfn.diktat.common.config.rules.CommonConfiguration
 import org.cqfn.diktat.common.config.rules.RulesConfig
 import org.cqfn.diktat.common.config.rules.getCommonConfiguration
 import org.cqfn.diktat.ruleset.constants.Warnings.INCORRECT_PACKAGE_SEPARATOR
@@ -22,6 +23,7 @@ import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiWhiteSpaceImpl
 import org.jetbrains.kotlin.konan.file.File
 import org.jetbrains.kotlin.lexer.KtTokens.PACKAGE_KEYWORD
 import org.slf4j.LoggerFactory
+
 import java.util.concurrent.atomic.AtomicInteger
 
 /**
@@ -47,7 +49,7 @@ class PackageNaming(configRules: List<RulesConfig>) : DiktatRule(
             if (node.elementType == PACKAGE_DIRECTIVE) {
                 val filePath = node.getRootNode().getFilePath()
                 // calculating package name based on the directory where the file is placed
-                val realPackageName = calculateRealPackageName(filePath)
+                val realPackageName = calculateRealPackageName(filePath, configuration)
 
                 // if node isLeaf - this means that there is no package name declared
                 if (node.isLeaf() && !filePath.isKotlinScript()) {
@@ -92,7 +94,7 @@ class PackageNaming(configRules: List<RulesConfig>) : DiktatRule(
      *
      * @return list with words that are parts of package name like [org, diktat, name]
      */
-    private fun calculateRealPackageName(fileName: String): List<String> {
+    private fun calculateRealPackageName(fileName: String, configuration: CommonConfiguration): List<String> {
         val filePathParts = fileName.splitPathToDirs()
 
         return if (!filePathParts.contains(PACKAGE_PATH_ANCHOR)) {
@@ -104,8 +106,9 @@ class PackageNaming(configRules: List<RulesConfig>) : DiktatRule(
             // 1) getting a path after the base project directory (after "src" directory)
             // 2) removing src/main/kotlin/java/e.t.c dirs and removing file name
             // 3) adding company's domain name at the beginning
+            val allDirs = languageDirNames + configuration.srcDirectories + configuration.testAnchors
             val fileSubDir = filePathParts.subList(filePathParts.lastIndexOf(PACKAGE_PATH_ANCHOR), filePathParts.size - 1)
-                .dropWhile { languageDirNames.contains(it) }
+                .dropWhile { allDirs.contains(it) }
             // no need to add DOMAIN_NAME to the package name if it is already in path
             val domainPrefix = if (!fileSubDir.joinToString(PACKAGE_SEPARATOR).startsWith(domainName)) domainName.split(PACKAGE_SEPARATOR) else emptyList()
             domainPrefix + fileSubDir
@@ -259,6 +262,6 @@ class PackageNaming(configRules: List<RulesConfig>) : DiktatRule(
          * Directories that are supposed to be first in sources file paths, relative to [PACKAGE_PATH_ANCHOR].
          * For kotlin multiplatform projects directories for targets from [kmmTargets] are supported.
          */
-        val languageDirNames = listOf("src", "main", "test", "java", "kotlin") + kmmTargets.flatMap { listOf("${it}Main", "${it}Test") }
+        val languageDirNames = listOf("src", "java", "kotlin") + kmmTargets.flatMap { listOf("${it}Main", "${it}Test") }
     }
 }
diff --git a/diktat-rules/src/main/resources/diktat-analysis-huawei.yml b/diktat-rules/src/main/resources/diktat-analysis-huawei.yml
index 4a05681ddb..5c89bffb56 100644
--- a/diktat-rules/src/main/resources/diktat-analysis-huawei.yml
+++ b/diktat-rules/src/main/resources/diktat-analysis-huawei.yml
@@ -8,6 +8,7 @@
     disabledChapters: ""
     testDirs: test
     kotlinVersion: 1.4.30
+    srcDirectories: "main"
 # Checks that the Class/Enum/Interface name does not match Pascal case
 - name: CLASS_NAME_INCORRECT
   enabled: true
diff --git a/diktat-rules/src/main/resources/diktat-analysis.yml b/diktat-rules/src/main/resources/diktat-analysis.yml
index 14e8dae481..ad1ed749f5 100644
--- a/diktat-rules/src/main/resources/diktat-analysis.yml
+++ b/diktat-rules/src/main/resources/diktat-analysis.yml
@@ -6,6 +6,7 @@
     testDirs: test
     disabledChapters: ""
     kotlinVersion: 1.4
+    srcDirectories: "main"
 # Checks that the Class/Enum/Interface name does not match Pascal case
 - name: CLASS_NAME_INCORRECT
   enabled: true


Test patch:
diff --git a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter1/PackageNamingWarnTest.kt b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter1/PackageNamingWarnTest.kt
index 4e2b3043b1..1f700f71ce 100644
--- a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter1/PackageNamingWarnTest.kt
+++ b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter1/PackageNamingWarnTest.kt
@@ -25,6 +25,13 @@ class PackageNamingWarnTest : LintTestBase(::PackageNaming) {
     private val rulesConfigListEmptyDomainName: List<RulesConfig> = listOf(
         RulesConfig("DIKTAT_COMMON", true, mapOf("domainName" to ""))
     )
+    private val rulesConfigSourceDirectories: List<RulesConfig> = listOf(
+        RulesConfig("DIKTAT_COMMON", true, mapOf(
+            "domainName" to "org.cqfn.diktat",
+            "srcDirectories" to "nativeMain, mobileMain",
+            "testDirs" to "nativeTest"
+        ))
+    )
 
     @Test
     @Tag(WarningNames.PACKAGE_NAME_MISSING)
@@ -148,6 +155,84 @@ class PackageNamingWarnTest : LintTestBase(::PackageNaming) {
         )
     }
 
+    @Test
+    @Tag(WarningNames.PACKAGE_NAME_INCORRECT_SYMBOLS)
+    fun `test source config`() {
+        lintMethod(
+
+            """
+                package org.cqfn.diktat.domain
+
+                import org.cqfn.diktat.a.b.c
+
+                /**
+                 * testComment
+                 */
+                class TestPackageName {  }
+
+            """.trimIndent(),
+            fileName = "~/diktat/diktat-rules/src/nativeMain/kotlin/org/cqfn/diktat/domain/BlaBla.kt",
+            rulesConfigList = rulesConfigSourceDirectories
+        )
+
+        lintMethod(
+
+            """
+                package org.cqfn.diktat.domain
+
+                import org.cqfn.diktat.a.b.c
+
+                /**
+                 * testComment
+                 */
+                class TestPackageName {  }
+
+            """.trimIndent(),
+            LintError(1, 9, ruleId, "${PACKAGE_NAME_INCORRECT_PATH.warnText()} org.cqfn.diktat.main.kotlin.org.cqfn.diktat.domain", true),
+            fileName = "~/diktat/diktat-rules/src/main/kotlin/org/cqfn/diktat/domain/BlaBla.kt",
+            rulesConfigList = rulesConfigSourceDirectories
+        )
+    }
+
+    @Test
+    @Tag(WarningNames.PACKAGE_NAME_INCORRECT_SYMBOLS)
+    fun `test directories for test config`() {
+        lintMethod(
+
+            """
+                package org.cqfn.diktat.domain
+
+                import org.cqfn.diktat.a.b.c
+
+                /**
+                 * testComment
+                 */
+                class TestPackageName {  }
+
+            """.trimIndent(),
+            fileName = "~/diktat/diktat-rules/src/nativeTest/kotlin/org/cqfn/diktat/domain/BlaBla.kt",
+            rulesConfigList = rulesConfigSourceDirectories
+        )
+
+        lintMethod(
+
+            """
+                package org.cqfn.diktat.domain
+
+                import org.cqfn.diktat.a.b.c
+
+                /**
+                 * testComment
+                 */
+                class TestPackageName {  }
+
+            """.trimIndent(),
+            LintError(1, 9, ruleId, "${PACKAGE_NAME_INCORRECT_PATH.warnText()} org.cqfn.diktat.test.kotlin.org.cqfn.diktat.domain", true),
+            fileName = "~/diktat/diktat-rules/src/test/kotlin/org/cqfn/diktat/domain/BlaBla.kt",
+            rulesConfigList = rulesConfigSourceDirectories
+        )
+    }
+
     @Test
     @Tag(WarningNames.PACKAGE_NAME_INCORRECT_PATH)
     fun `regression - incorrect warning on file under test directory`() {