diff --git a/detekt-core/src/main/resources/default-detekt-config.yml b/detekt-core/src/main/resources/default-detekt-config.yml index 5b5c7d3fd1d..f67bed5e645 100644 --- a/detekt-core/src/main/resources/default-detekt-config.yml +++ b/detekt-core/src/main/resources/default-detekt-config.yml @@ -625,6 +625,8 @@ style: active: false UnnecessaryApply: active: true + UnnecessaryBackticks: + active: false UnnecessaryFilter: active: false UnnecessaryInheritance: diff --git a/detekt-rules-style/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/style/StyleGuideProvider.kt b/detekt-rules-style/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/style/StyleGuideProvider.kt index 83c1028690b..bd496f55cfe 100644 --- a/detekt-rules-style/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/style/StyleGuideProvider.kt +++ b/detekt-rules-style/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/style/StyleGuideProvider.kt @@ -95,6 +95,7 @@ class StyleGuideProvider : DefaultRuleSetProvider { UseIsNullOrEmpty(config), UseOrEmpty(config), UseAnyOrNoneInsteadOfFind(config), + UnnecessaryBackticks(config), ) ) } diff --git a/detekt-rules-style/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/style/UnnecessaryBackticks.kt b/detekt-rules-style/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/style/UnnecessaryBackticks.kt new file mode 100644 index 00000000000..8a496aea3f4 --- /dev/null +++ b/detekt-rules-style/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/style/UnnecessaryBackticks.kt @@ -0,0 +1,62 @@ +package io.gitlab.arturbosch.detekt.rules.style + +import io.gitlab.arturbosch.detekt.api.CodeSmell +import io.gitlab.arturbosch.detekt.api.Config +import io.gitlab.arturbosch.detekt.api.Debt +import io.gitlab.arturbosch.detekt.api.Entity +import io.gitlab.arturbosch.detekt.api.Issue +import io.gitlab.arturbosch.detekt.api.Rule +import io.gitlab.arturbosch.detekt.api.Severity +import org.jetbrains.kotlin.com.intellij.psi.PsiElement +import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.psi.KtElement +import org.jetbrains.kotlin.psi.KtSimpleNameStringTemplateEntry +import org.jetbrains.kotlin.psi.psiUtil.allChildren +import org.jetbrains.kotlin.psi.psiUtil.canPlaceAfterSimpleNameEntry +import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType +import org.jetbrains.kotlin.psi.psiUtil.isIdentifier + +/** + * This rule reports unnecessary backticks. + * + * + * class `HelloWorld` + * + * + * + * class HelloWorld + * + */ +class UnnecessaryBackticks(config: Config = Config.empty) : Rule(config) { + override val issue: Issue = Issue( + javaClass.simpleName, + Severity.Style, + "Backticks are unnecessary.", + Debt.FIVE_MINS + ) + + override fun visitKtElement(element: KtElement) { + element.allChildren + .filter { it.node.elementType == KtTokens.IDENTIFIER && it.hasUnnecessaryBackticks() } + .forEach { report(CodeSmell(issue, Entity.from(it), "Backticks are unnecessary.")) } + super.visitKtElement(element) + } + + @Suppress("ReturnCount") + private fun PsiElement.hasUnnecessaryBackticks(): Boolean { + val text = this.text + if (!text.startsWith("`") || !text.endsWith("`")) return false + + val unquoted = text.drop(1).dropLast(1) + if (!unquoted.isIdentifier() || unquoted.isKeyword()) return false + + val stringTemplateEntry = getStrictParentOfType() + return stringTemplateEntry == null || canPlaceAfterSimpleNameEntry(stringTemplateEntry.nextSibling) + } + + private fun String.isKeyword() = this in KEYWORDS || this.all { it == '_' } + + companion object { + private val KEYWORDS = KtTokens.KEYWORDS.types.map { it.toString() } + } +} diff --git a/detekt-rules-style/src/test/kotlin/io/gitlab/arturbosch/detekt/rules/style/UnnecessaryBackticksSpec.kt b/detekt-rules-style/src/test/kotlin/io/gitlab/arturbosch/detekt/rules/style/UnnecessaryBackticksSpec.kt new file mode 100644 index 00000000000..604f19c7e87 --- /dev/null +++ b/detekt-rules-style/src/test/kotlin/io/gitlab/arturbosch/detekt/rules/style/UnnecessaryBackticksSpec.kt @@ -0,0 +1,139 @@ +package io.gitlab.arturbosch.detekt.rules.style + +import io.gitlab.arturbosch.detekt.api.Config +import io.gitlab.arturbosch.detekt.test.assertThat +import io.gitlab.arturbosch.detekt.test.compileAndLint +import io.gitlab.arturbosch.detekt.test.lint +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test + +class UnnecessaryBackticksSpec { + val subject = UnnecessaryBackticks(Config.empty) + + @Nested + inner class `Reports UnnecessaryInnerClass Rule` { + @Test + fun `class`() { + val code = """ + class `Foo` { + val x: `Foo` = `Foo`() + val y = ::`Foo` + } + """.trimIndent() + assertThat(subject.compileAndLint(code)).hasSize(4) + } + + @Test + fun function() { + val code = """ + fun `foo`() = 1 + val x = `foo`() + val y = ::`foo` + """.trimIndent() + assertThat(subject.compileAndLint(code)).hasSize(3) + } + + @Test + fun property() { + val code = """ + val `foo` = "" + val x = `foo` + val y = ::`foo` + val z = `foo`.length + """.trimIndent() + assertThat(subject.compileAndLint(code)).hasSize(4) + } + + @Test + fun import() { + val code = """ + import kotlin.`let` + """.trimIndent() + assertThat(subject.compileAndLint(code)).hasSize(1) + } + + @Test + fun `in string template`() { + val code = """ + val foo = "" + val x = "${'$'}`foo`" + val y = "${'$'}`foo` bar" + val z = "${'$'}{`foo`}bar" + """.trimIndent() + assertThat(subject.compileAndLint(code)).hasSize(3) + } + } + + @Nested + inner class `Does not report UnnecessaryInnerClass Rule` { + @Test + fun `class with spaces`() { + val code = """ + class `Foo Bar` + val x: `Foo Bar` = `Foo Bar`() + val y = ::`Foo Bar` + """.trimIndent() + assertThat(subject.compileAndLint(code)).hasSize(0) + } + + @Test + fun `function with spaces`() { + val code = """ + fun `foo bar`() = 1 + val x = `foo bar`() + val y = ::`foo bar` + """.trimIndent() + assertThat(subject.compileAndLint(code)).hasSize(0) + } + + @Test + fun `property with spaces`() { + val code = """ + val `foo bar` = "" + val x = `foo bar` + val y = ::`foo bar` + val z = `foo bar`.length + """.trimIndent() + assertThat(subject.compileAndLint(code)).hasSize(0) + } + + @Test + fun keyword() { + val code = """ + val `is` = 1 + val `fun` = 2 + """.trimIndent() + assertThat(subject.compileAndLint(code)).isEmpty() + } + + @Test + fun underscore() { + val code = """ + val `_` = 1 + val `__` = 2 + """.trimIndent() + assertThat(subject.compileAndLint(code)).isEmpty() + } + + @Test + fun import() { + val code = """ + package test + import test.`Foo Bar` + class `Foo Bar` + """.trimIndent() + assertThat(subject.lint(code)).hasSize(0) + } + + @Test + fun `in string template`() { + val code = """ + val foo = "" + val x = "${'$'}`foo`bar" + val `bar baz` = "" + val y = "${'$'}`bar baz`" + """.trimIndent() + assertThat(subject.compileAndLint(code)).hasSize(0) + } + } +}