Repository: detekt/detekt (kotlin)

Issue:
New rule: `UnnecessaryBackticks`
## Expected Behavior of the rule
Flag things like this:

```kotlin
fun `thisFunctionDoesntNeedBacktickts`()

class `HelloWorld`

val `fixMePleaseIdontLikeThoseBackticks`: Int
```

Because there is no need for those backticks (`` ` ``)

## Context
After we move from spek to JUnit we have a lot of unnecessary backticks in function and class names.

Pull request description:
Fixes #4739

Patch:
diff --git a/detekt-core/src/main/resources/default-detekt-config.yml b/detekt-core/src/main/resources/default-detekt-config.yml
index 5b5c7d3fd1d0..f67bed5e645d 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 83c1028690ba..bd496f55cfec 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 000000000000..8a496aea3f45
--- /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.
+ *
+ * <noncompliant>
+ * class `HelloWorld`
+ * </noncompliant>
+ *
+ * <compliant>
+ * class HelloWorld
+ * </compliant>
+ */
+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<KtSimpleNameStringTemplateEntry>()
+        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() }
+    }
+}


Test patch:
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 000000000000..604f19c7e871
--- /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)
+        }
+    }
+}