Repository: analysis-dev/diktat (kotlin)

Issue:
False positive findings for imports that are referenced in KDOC
> [UNUSED_IMPORT] unused imports should be removed: java.io.IOException - unused import

example:
```kotlin
import java.io.IOException

interface BluetoothApi {
    
    /**
     * Send array of bytes to bluetooth output stream.
     * This call is asynchronous.
     *
     * Note that this operation can still throw an [IOException] if the remote device silently
     * closes the connection so the pipe gets broken.
     *
     * @param bytes data to send
     * @return true if success, false if there was an error or device has been disconnected
     */
    fun trySend(bytes: ByteArray): Boolean
}
```

Pull request description:
### What's done:
- supported KDOC_MARKDOWN_LINK as references

This pull request closes #1326

Patch:
diff --git a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter3/files/FileStructureRule.kt b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter3/files/FileStructureRule.kt
index 1e522cc81a..6b0c3fa470 100644
--- a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter3/files/FileStructureRule.kt
+++ b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter3/files/FileStructureRule.kt
@@ -27,6 +27,7 @@ import com.pinterest.ktlint.core.ast.ElementType.FILE_ANNOTATION_LIST
 import com.pinterest.ktlint.core.ast.ElementType.IMPORT_DIRECTIVE
 import com.pinterest.ktlint.core.ast.ElementType.IMPORT_LIST
 import com.pinterest.ktlint.core.ast.ElementType.KDOC
+import com.pinterest.ktlint.core.ast.ElementType.KDOC_MARKDOWN_LINK
 import com.pinterest.ktlint.core.ast.ElementType.OPERATION_REFERENCE
 import com.pinterest.ktlint.core.ast.ElementType.PACKAGE_DIRECTIVE
 import com.pinterest.ktlint.core.ast.ElementType.REFERENCE_EXPRESSION
@@ -73,9 +74,8 @@ class FileStructureRule(configRules: List<RulesConfig>) : DiktatRule(
         .mapValues { (_, value) ->
             value.map { it.split(PACKAGE_SEPARATOR).map(Name::identifier) }
         }
-    private val refSet: MutableSet<String> = mutableSetOf()
     private var packageName = ""
-    
+
     /**
      * There are groups of methods, which should be excluded from usage check without type resolution.
      * `componentN` is a method for N-th component in destructuring declarations.
@@ -226,7 +226,7 @@ class FileStructureRule(configRules: List<RulesConfig>) : DiktatRule(
     private fun checkUnusedImport(
         node: ASTNode
     ) {
-        findAllReferences(node)
+        val refSet = findAllReferences(node)
         packageName = (node.findChildByType(PACKAGE_DIRECTIVE)?.psi as KtPackageDirective).qualifiedName
         node.findChildByType(IMPORT_LIST)
             ?.getChildren(TokenSet.create(IMPORT_DIRECTIVE))
@@ -264,24 +264,36 @@ class FileStructureRule(configRules: List<RulesConfig>) : DiktatRule(
         ) { ktImportDirective.delete() }
     }
 
-    private fun findAllReferences(node: ASTNode) {
-        node.findAllDescendantsWithSpecificType(OPERATION_REFERENCE).forEach { ref ->
-            if (!ref.isPartOf(IMPORT_DIRECTIVE)) {
+    private fun findAllReferences(node: ASTNode): Set<String> {
+        val referencesFromOperations = node.findAllDescendantsWithSpecificType(OPERATION_REFERENCE)
+            .filterNot { it.isPartOf(IMPORT_DIRECTIVE) }
+            .flatMap { ref ->
                 val references = operatorMap.filterValues { ref.text in it }
                 if (references.isNotEmpty()) {
-                    references.keys.forEach { key -> refSet.add(key) }
+                    references.keys
                 } else {
                     // this is needed to check infix functions that relate to operation reference
-                    refSet.add(ref.text)
+                    setOf(ref.text)
                 }
             }
-        }
-        node.findAllDescendantsWithSpecificType(REFERENCE_EXPRESSION).forEach {
-            if (!it.isPartOf(IMPORT_DIRECTIVE)) {
+        val referencesFromExpressions = node.findAllDescendantsWithSpecificType(REFERENCE_EXPRESSION)
+            .filterNot { it.isPartOf(IMPORT_DIRECTIVE) }
+            .map {
                 // the importedName method removes the quotes, but the node.text method does not
-                refSet.add(it.text.replace("`", ""))
+                it.text.replace("`", "")
             }
-        }
+        val referencesFromKdocs = node.findAllDescendantsWithSpecificType(KDOC)
+            .flatMap { it.findAllDescendantsWithSpecificType(KDOC_MARKDOWN_LINK) }
+            .map { it.text.removePrefix("[").removeSuffix("]") }
+            .flatMap {
+                if (it.contains(".")) {
+                    // support cases with reference to method
+                    listOf(it, it.substringBeforeLast("."))
+                } else {
+                    listOf(it)
+                }
+            }
+        return (referencesFromOperations + referencesFromExpressions + referencesFromKdocs).toSet()
     }
 
     private fun rearrangeImports(
diff --git a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter3/files/IndentationRule.kt b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter3/files/IndentationRule.kt
index 0ba1b38fe9..30bab7fc64 100644
--- a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter3/files/IndentationRule.kt
+++ b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter3/files/IndentationRule.kt
@@ -33,7 +33,6 @@ import com.pinterest.ktlint.core.ast.ElementType.LONG_STRING_TEMPLATE_ENTRY
 import com.pinterest.ktlint.core.ast.ElementType.LONG_TEMPLATE_ENTRY_END
 import com.pinterest.ktlint.core.ast.ElementType.LONG_TEMPLATE_ENTRY_START
 import com.pinterest.ktlint.core.ast.ElementType.LPAR
-import com.pinterest.ktlint.core.ast.ElementType.OPEN_QUOTE
 import com.pinterest.ktlint.core.ast.ElementType.RBRACE
 import com.pinterest.ktlint.core.ast.ElementType.RBRACKET
 import com.pinterest.ktlint.core.ast.ElementType.RPAR


Test patch:
diff --git a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/FileStructureRuleTest.kt b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/FileStructureRuleTest.kt
index b5abea6c91..14e934eff7 100644
--- a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/FileStructureRuleTest.kt
+++ b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/FileStructureRuleTest.kt
@@ -79,7 +79,7 @@ class FileStructureRuleTest : LintTestBase(::FileStructureRule) {
                 |import org.junit.Test
                 |import org.cqfn.diktat.Foo
                 |
-                |class Example { 
+                |class Example {
                 |val x: Test = null
                 |val y: Foo = null
                 |}
@@ -273,7 +273,7 @@ class FileStructureRuleTest : LintTestBase(::FileStructureRule) {
                 |
                 |import org.cqfn.diktat.example.Foo
                 |
-                |class Example { 
+                |class Example {
                 |}
             """.trimMargin(),
             LintError(1, 1, ruleId, "${Warnings.UNUSED_IMPORT.warnText()} org.cqfn.diktat.example.Foo - unused import", true)
@@ -289,7 +289,7 @@ class FileStructureRuleTest : LintTestBase(::FileStructureRule) {
                 |
                 |import org.cqfn.diktat.Foo
                 |
-                |class Example { 
+                |class Example {
                 |}
             """.trimMargin(),
             LintError(1, 1, ruleId, "${Warnings.UNUSED_IMPORT.warnText()} org.cqfn.diktat.Foo - unused import", true)
@@ -305,7 +305,7 @@ class FileStructureRuleTest : LintTestBase(::FileStructureRule) {
                 |
                 |import org.cqfn.diktat.Foo
                 |
-                |class Example { 
+                |class Example {
                 |val x: Foo = null
                 |}
             """.trimMargin(),
@@ -321,7 +321,7 @@ class FileStructureRuleTest : LintTestBase(::FileStructureRule) {
                 |
                 |import kotlin.io.path.div
                 |
-                |class Example { 
+                |class Example {
                 |val pom = kotlin.io.path.createTempFile().toFile()
                 |val x = listOf(pom.parentFile.toPath() / "src/main/kotlin/exclusion")
                 |}
@@ -482,4 +482,83 @@ class FileStructureRuleTest : LintTestBase(::FileStructureRule) {
             LintError(1, 1, ruleId, "${Warnings.UNUSED_IMPORT.warnText()} tasks.getValue - unused import", true)
         )
     }
+
+    @Test
+    @Tag(WarningNames.UNUSED_IMPORT)
+    fun `import in KDoc #1`() {
+        lintMethod(
+            """
+                |import java.io.IOException
+                |
+                |interface BluetoothApi {
+                |
+                |    /**
+                |     * Send array of bytes to bluetooth output stream.
+                |     * This call is asynchronous.
+                |     *
+                |     * Note that this operation can still throw an [IOException] if the remote device silently
+                |     * closes the connection so the pipe gets broken.
+                |     *
+                |     * @param bytes data to send
+                |     * @return true if success, false if there was an error or device has been disconnected
+                |     */
+                |    fun trySend(bytes: ByteArray): Boolean
+                |}
+            """.trimMargin(),
+        )
+    }
+
+    @Test
+    @Tag(WarningNames.UNUSED_IMPORT)
+    fun `import in KDoc #2`() {
+        lintMethod(
+            """
+                |import java.io.IOException
+                |import java.io.IOException as IOE
+                |import java.io.UncheckedIOException
+                |import java.io.UncheckedIOException as UIOE
+                |
+                |interface BluetoothApi {
+                |    /**
+                |     * @see IOException
+                |     * @see [UncheckedIOException]
+                |     * @see IOE
+                |     * @see [UIOE]
+                |     */
+                |    fun trySend(bytes: ByteArray): Boolean
+                |}
+            """.trimMargin(),
+        )
+    }
+
+    @Test
+    @Tag(WarningNames.UNUSED_IMPORT)
+    fun `import in KDoc #3`() {
+        lintMethod(
+            """
+                |package com.example
+                |
+                |import com.example.Library1 as Lib1
+                |import com.example.Library1.doSmth as doSmthElse1
+                |import com.example.Library2 as Lib2
+                |import com.example.Library2.doSmth as doSmthElse2
+                |
+                |object Library1 {
+                |    fun doSmth(): Unit = TODO()
+                |}
+                |
+                |object Library2 {
+                |    fun doSmth(): Unit = TODO()
+                |}
+                |
+                |/**
+                | * @see Lib1.doSmth
+                | * @see doSmthElse1
+                | * @see [Lib2.doSmth]
+                | * @see [doSmthElse2]
+                | */
+                |class Client
+            """.trimMargin(),
+        )
+    }
 }
diff --git a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter5/AvoidNestedFunctionsWarnTest.kt b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter5/AvoidNestedFunctionsWarnTest.kt
index 4635395199..4cf0b0ca81 100644
--- a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter5/AvoidNestedFunctionsWarnTest.kt
+++ b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter5/AvoidNestedFunctionsWarnTest.kt
@@ -6,7 +6,6 @@ import org.cqfn.diktat.ruleset.rules.chapter5.AvoidNestedFunctionsRule
 import org.cqfn.diktat.util.LintTestBase
 
 import com.pinterest.ktlint.core.LintError
-import generated.WarningNames
 import generated.WarningNames.AVOID_NESTED_FUNCTIONS
 import org.junit.jupiter.api.Tag
 import org.junit.jupiter.api.Test
diff --git a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter5/ParameterNameInOuterLambdaRuleWarnTest.kt b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter5/ParameterNameInOuterLambdaRuleWarnTest.kt
index 5bd632d61e..c71028e8e1 100644
--- a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter5/ParameterNameInOuterLambdaRuleWarnTest.kt
+++ b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter5/ParameterNameInOuterLambdaRuleWarnTest.kt
@@ -4,7 +4,6 @@ import org.cqfn.diktat.common.config.rules.RulesConfig
 import org.cqfn.diktat.ruleset.constants.Warnings
 import org.cqfn.diktat.ruleset.rules.DIKTAT_RULE_SET_ID
 import org.cqfn.diktat.ruleset.rules.chapter5.ParameterNameInOuterLambdaRule
-import org.cqfn.diktat.ruleset.rules.chapter6.classes.AbstractClassesRule
 import org.cqfn.diktat.util.LintTestBase
 
 import com.pinterest.ktlint.core.LintError
diff --git a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/utils/AstNodeUtilsTest.kt b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/utils/AstNodeUtilsTest.kt
index f8897e7bd9..5a89060d28 100644
--- a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/utils/AstNodeUtilsTest.kt
+++ b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/utils/AstNodeUtilsTest.kt
@@ -14,7 +14,6 @@ import com.pinterest.ktlint.core.Rule
 import com.pinterest.ktlint.core.RuleSet
 import com.pinterest.ktlint.core.api.FeatureInAlphaState
 import com.pinterest.ktlint.core.ast.ElementType
-import com.pinterest.ktlint.core.ast.ElementType.BLOCK
 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
@@ -23,7 +22,6 @@ 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.INTEGER_CONSTANT
-import com.pinterest.ktlint.core.ast.ElementType.LAMBDA_EXPRESSION
 import com.pinterest.ktlint.core.ast.ElementType.MODIFIER_LIST
 import com.pinterest.ktlint.core.ast.ElementType.PROPERTY
 import com.pinterest.ktlint.core.ast.ElementType.TYPE_REFERENCE
@@ -129,7 +127,7 @@ class AstNodeUtilsTest {
     fun `test getTypeParameterList`() {
         val code = """
             class Array<T>(val size: Int) {
-                
+
             }
         """.trimIndent()
         applyToCode(code, 1) { node, counter ->
@@ -408,7 +406,7 @@ class AstNodeUtilsTest {
     fun `test isNodeFromFileLevel - node isn't from file level`() {
         val code = """
             val x = 2
-            
+
         """.trimIndent()
         applyToCode(code, 8) { node, counter ->
             if (node.elementType != FILE) {