From e1d8cd47c57b2d6596631f3939206379577faf1e Mon Sep 17 00:00:00 2001 From: Nariman Abdullin Date: Thu, 16 Jun 2022 12:09:46 +0300 Subject: [PATCH] Fix for UNUSED_IMPORT in KDoc ### What's done: - supported KDOC_MARKDOWN_LINK as references --- .../rules/chapter3/files/FileStructureRule.kt | 39 +++++--- .../ruleset/chapter3/FileStructureRuleTest.kt | 89 +++++++++++++++++-- 2 files changed, 110 insertions(+), 18 deletions(-) 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..aec77f17a6 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 @@ -19,6 +19,7 @@ import org.cqfn.diktat.ruleset.utils.handleIncorrectOrder import org.cqfn.diktat.ruleset.utils.ignoreImports import org.cqfn.diktat.ruleset.utils.moveChildBefore import org.cqfn.diktat.ruleset.utils.operatorMap +import org.cqfn.diktat.ruleset.utils.removePrefix import com.pinterest.ktlint.core.ast.ElementType.BLOCK_COMMENT import com.pinterest.ktlint.core.ast.ElementType.EOL_COMMENT @@ -27,6 +28,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 +75,8 @@ class FileStructureRule(configRules: List) : DiktatRule( .mapValues { (_, value) -> value.map { it.split(PACKAGE_SEPARATOR).map(Name::identifier) } } - private val refSet: MutableSet = 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 +227,7 @@ class FileStructureRule(configRules: List) : 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 +265,36 @@ class FileStructureRule(configRules: List) : 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 { + 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/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(), + ) + } }