Repository: analysis-dev/diktat (kotlin)

Issue:
expect/actual removing abstract modifier 
## Describe the bug

expect/actual removing abstract modifier 

## Expected behavior

actual abstract class CoroutineTest actual constructor() {

    /**
     * Test rule
     */
    @get:Rule
    var coroutineTestRule = CoroutineTestRule()

    /**
     * Run test
     *
     * @param T
     * @param block
     * @receiver a Coroutine Scope
     */
    actual fun <T> runTest(block: suspend CoroutineScope.() -> T) {
        runBlocking {
            block()
        }
    }
}


## Observed behavior

@RunWith(AndroidJUnit4::class)
actual class CoroutineTest actual constructor() {
    /**
     * Test rule
     */
    @get:Rule
    var coroutineTestRule = CoroutineTestRule()

    /**
     * Run test
     *
     * @param T
     * @param block
     * @receiver a Coroutine Scope
     */
    actual fun <T> runTest(block: suspend CoroutineScope.() -> T) {
        runBlocking {
            block()
        }
    }
}

## Steps to Reproduce

## Environment information
* diktat version: 1.0.0-rc.2
* build tool (maven/gradle): gradle
* how is diktat run (CLI, plugin, etc.): plugin
* kotlin version: 1.5.21
* operating system: mac
* link to a project (if your project is public): N/A

Pull request:
### What's done:
* fixed bug in CLASS_SHOULD_NOT_BE_ABSTRACT
* now replace abstract modifier on open modifier
Closes #1001

Patch:
diff --git a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter6/classes/AbstractClassesRule.kt b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter6/classes/AbstractClassesRule.kt
index 66b9fcc8c6..b409636397 100644
--- a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter6/classes/AbstractClassesRule.kt
+++ b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter6/classes/AbstractClassesRule.kt
@@ -13,8 +13,9 @@ import com.pinterest.ktlint.core.ast.ElementType.CLASS_BODY
 import com.pinterest.ktlint.core.ast.ElementType.FUN
 import com.pinterest.ktlint.core.ast.ElementType.IDENTIFIER
 import com.pinterest.ktlint.core.ast.ElementType.MODIFIER_LIST
-import com.pinterest.ktlint.core.ast.isWhiteSpace
+import com.pinterest.ktlint.core.ast.ElementType.OPEN_KEYWORD
 import org.jetbrains.kotlin.com.intellij.lang.ASTNode
+import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement
 
 /**
  * Checks if abstract class has any abstract method. If not, warns that class should not be abstract
@@ -45,25 +46,9 @@ class AbstractClassesRule(configRules: List<RulesConfig>) : DiktatRule(
         if (functions.isNotEmpty() && functions.none { hasAbstractModifier(it) }) {
             CLASS_SHOULD_NOT_BE_ABSTRACT.warnAndFix(configRules, emitWarn, isFixMode, identifier, node.startOffset, node) {
                 val modList = classNode.getFirstChildWithType(MODIFIER_LIST)!!
-                if (modList.getChildren(null).size > 1) {
-                    val abstractKeyword = modList.getFirstChildWithType(ABSTRACT_KEYWORD)!!
-
-                    // we are deleting one keyword, so we need to delete extra space
-                    val spaceInModifiers = if (abstractKeyword == modList.firstChildNode) {
-                        abstractKeyword.treeNext
-                    } else {
-                        abstractKeyword.treePrev
-                    }
-                    modList.removeChild(abstractKeyword)
-                    if (spaceInModifiers != null && spaceInModifiers.isWhiteSpace()) {
-                        modList.removeChild(spaceInModifiers)
-                    }
-                } else {
-                    if (modList.treeNext.isWhiteSpace()) {
-                        classNode.removeChild(modList.treeNext)
-                    }
-                    classNode.removeChild(modList)
-                }
+                val abstractKeyword = modList.getFirstChildWithType(ABSTRACT_KEYWORD)!!
+                val newOpenKeyword = LeafPsiElement(OPEN_KEYWORD, "open")
+                modList.replaceChild(abstractKeyword, newOpenKeyword)
             }
         }
     }
diff --git a/info/guide/guide-chapter-6.md b/info/guide/guide-chapter-6.md
index ff6c9bb6c9..7f9a1bd939 100644
--- a/info/guide/guide-chapter-6.md
+++ b/info/guide/guide-chapter-6.md
@@ -184,7 +184,7 @@ class Square() : Rectangle() {
 
 #### <a name="r6.1.6"></a> 6.1.6 Abstract class should have at least one abstract method
 Abstract classes are used to force a developer to implement some of its parts in their inheritors.
-When the abstract class has no abstract methods, it was set `abstract` incorrectly and can be converted to a regular class.
+When the abstract class has no abstract methods, it was set `abstract` incorrectly and can be converted to open class.
 
 **Invalid example**:
 ```kotlin
@@ -204,11 +204,18 @@ abstract class NotAbstract {
 }
 
 // OR
-class NotAbstract {
+open class NotAbstract {
     fun foo() {}
     
     fun test() {}
 }
+
+// OR
+class NotAbstract {
+    fun foo() {}
+
+    fun test() {}
+}
 ```
 
 


Test patch:
diff --git a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter6/AbstractClassesFixTest.kt b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter6/AbstractClassesFixTest.kt
index b5a222d4fe..ccbf59a0af 100644
--- a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter6/AbstractClassesFixTest.kt
+++ b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter6/AbstractClassesFixTest.kt
@@ -11,6 +11,6 @@ class AbstractClassesFixTest : FixTestBase("test/chapter6/abstract_classes", ::A
     @Test
     @Tag(CLASS_SHOULD_NOT_BE_ABSTRACT)
     fun `fix abstract class`() {
-        fixAndCompare("ShouldRemoveAbstractKeywordExpected.kt", "ShouldRemoveAbstractKeywordTest.kt")
+        fixAndCompare("ShouldReplaceAbstractKeywordExpected.kt", "ShouldReplaceAbstractKeywordTest.kt")
     }
 }
diff --git a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter6/AbstractClassesWarnTest.kt b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter6/AbstractClassesWarnTest.kt
index 4212c29cb4..76d2fc9511 100644
--- a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter6/AbstractClassesWarnTest.kt
+++ b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter6/AbstractClassesWarnTest.kt
@@ -15,7 +15,7 @@ class AbstractClassesWarnTest : LintTestBase(::AbstractClassesRule) {
 
     @Test
     @Tag(CLASS_SHOULD_NOT_BE_ABSTRACT)
-    fun `should not remove abstract`() {
+    fun `should not replace abstract with open`() {
         lintMethod(
             """
                 |abstract class Some(val a: Int = 5) {
@@ -29,7 +29,7 @@ class AbstractClassesWarnTest : LintTestBase(::AbstractClassesRule) {
 
     @Test
     @Tag(CLASS_SHOULD_NOT_BE_ABSTRACT)
-    fun `should remove abstract`() {
+    fun `should replace abstract on open`() {
         lintMethod(
             """
                 |abstract class Some(val a: Int = 5) {
@@ -42,7 +42,7 @@ class AbstractClassesWarnTest : LintTestBase(::AbstractClassesRule) {
 
     @Test
     @Tag(CLASS_SHOULD_NOT_BE_ABSTRACT)
-    fun `should remove abstract with inner`() {
+    fun `should replace abstract on open with inner`() {
         lintMethod(
             """
                 |class Some(val a: Int = 5) {
@@ -56,4 +56,34 @@ class AbstractClassesWarnTest : LintTestBase(::AbstractClassesRule) {
             LintError(4, 32, ruleId, "${Warnings.CLASS_SHOULD_NOT_BE_ABSTRACT.warnText()} Inner", true)
         )
     }
+
+    @Test
+    @Tag(CLASS_SHOULD_NOT_BE_ABSTRACT)
+    fun `should replace abstract on open in actual or expect classes`() {
+        lintMethod(
+            """
+                |actual abstract class CoroutineTest actual constructor() {
+                |    /**
+                |     * Test rule
+                |     */
+                |    @get:Rule
+                |    var coroutineTestRule = CoroutineTestRule()
+                |    
+                |    /**
+                |     * Run test
+                |     *
+                |     * @param T
+                |     * @param block
+                |     * @receiver a Coroutine Scope
+                |     */
+                |    actual fun <T> runTest(block: suspend CoroutineScope.() -> T) {
+                |       runBlocking {
+                |       block()
+                |       }
+                |    }
+                |}
+            """.trimMargin(),
+            LintError(1, 58, ruleId, "${Warnings.CLASS_SHOULD_NOT_BE_ABSTRACT.warnText()} CoroutineTest", true)
+        )
+    }
 }
diff --git a/diktat-rules/src/test/resources/test/chapter6/abstract_classes/ShouldRemoveAbstractKeywordExpected.kt b/diktat-rules/src/test/resources/test/chapter6/abstract_classes/ShouldRemoveAbstractKeywordExpected.kt
deleted file mode 100644
index e1b6d5167b..0000000000
--- a/diktat-rules/src/test/resources/test/chapter6/abstract_classes/ShouldRemoveAbstractKeywordExpected.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-package test.paragraph6.abstract_classes
-
-class Some() {
-    fun some(){}
-
-    fun another(){}
-
-    @SomeAnnotation @Another inner class Any {
-        fun func(){}
-    }
-
-    inner class Second {
-        fun someFunc(){}
-    }
-}
-
-abstract class Another {
-    abstract fun absFunc()
-
-    fun someFunc(){}
-}
diff --git a/diktat-rules/src/test/resources/test/chapter6/abstract_classes/ShouldReplaceAbstractKeywordExpected.kt b/diktat-rules/src/test/resources/test/chapter6/abstract_classes/ShouldReplaceAbstractKeywordExpected.kt
new file mode 100644
index 0000000000..ba222e0ef9
--- /dev/null
+++ b/diktat-rules/src/test/resources/test/chapter6/abstract_classes/ShouldReplaceAbstractKeywordExpected.kt
@@ -0,0 +1,29 @@
+package test.paragraph6.abstract_classes
+
+actual open class CoroutineTest actual constructor() {
+    actual fun <T> runTest(block: suspend CoroutineScope.() -> T) {
+        runBlocking {
+            block()
+        }
+    }
+}
+
+open class Some() {
+    fun some(){}
+
+    fun another(){}
+
+    @SomeAnnotation @Another open inner class Any {
+        fun func(){}
+    }
+
+    inner open class Second {
+        fun someFunc(){}
+    }
+}
+
+abstract class Another {
+    abstract fun absFunc()
+
+    fun someFunc(){}
+}
diff --git a/diktat-rules/src/test/resources/test/chapter6/abstract_classes/ShouldRemoveAbstractKeywordTest.kt b/diktat-rules/src/test/resources/test/chapter6/abstract_classes/ShouldReplaceAbstractKeywordTest.kt
similarity index 63%
rename from diktat-rules/src/test/resources/test/chapter6/abstract_classes/ShouldRemoveAbstractKeywordTest.kt
rename to diktat-rules/src/test/resources/test/chapter6/abstract_classes/ShouldReplaceAbstractKeywordTest.kt
index 6784219efb..0e26c4a8d5 100644
--- a/diktat-rules/src/test/resources/test/chapter6/abstract_classes/ShouldRemoveAbstractKeywordTest.kt
+++ b/diktat-rules/src/test/resources/test/chapter6/abstract_classes/ShouldReplaceAbstractKeywordTest.kt
@@ -1,5 +1,13 @@
 package test.paragraph6.abstract_classes
 
+actual abstract class CoroutineTest actual constructor() {
+    actual fun <T> runTest(block: suspend CoroutineScope.() -> T) {
+        runBlocking {
+            block()
+        }
+    }
+}
+
 abstract class Some() {
     fun some(){}