Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added StringLiteralDuplication rule #300

Merged
merged 5 commits into from
Aug 19, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions detekt-cli/src/main/resources/default-detekt-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ complexity:
threshold: 3
LabeledExpression:
active: false
StringLiteralDuplication:
active: false
threshold: 2
ignoreAnnotation: true
excludeStringsWithLessThan5Characters: true
ignoreStringsRegex: '$^'

code-smell:
active: true
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package io.gitlab.arturbosch.detekt.rules.complexity

import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.api.Debt
import io.gitlab.arturbosch.detekt.api.DetektVisitor
import io.gitlab.arturbosch.detekt.api.Entity
import io.gitlab.arturbosch.detekt.api.Issue
import io.gitlab.arturbosch.detekt.api.Metric
import io.gitlab.arturbosch.detekt.api.Severity
import io.gitlab.arturbosch.detekt.api.ThresholdRule
import io.gitlab.arturbosch.detekt.api.ThresholdedCodeSmell
import io.gitlab.arturbosch.detekt.api.isPartOf
import org.jetbrains.kotlin.psi.KtAnnotationEntry
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtLiteralStringTemplateEntry

class StringLiteralDuplication(config: Config = Config.empty,
threshold: Int = StringLiteralDuplication.DEFAULT_DUPLICATION) : ThresholdRule(config, threshold) {

override val issue = Issue(javaClass.simpleName, Severity.Maintainability,
"Multiple occurrences of the same string literal within a single kt file detected.",
Debt.FIVE_MINS)

private val ignoreAnnotation = valueOrDefault(IGNORE_ANNOTATION, true)
private val excludeStringsWithLessThan5Characters = valueOrDefault(EXCLUDE_SHORT_STRING, true)
private val ignoreStringsRegex = Regex(valueOrDefault(IGNORE_STRINGS_REGEX, "$^"))

override fun visitKtFile(file: KtFile) {
val visitor = StringLiteralVisitor()
file.accept(visitor)
val type = "SIZE: "
visitor.getLiteralsOverThreshold().forEach {
report(ThresholdedCodeSmell(issue, Entity.from(file), Metric(type + it.key, it.value, threshold)))
}
}

internal inner class StringLiteralVisitor : DetektVisitor() {

private var literals = HashMap<String, Int>()
private val pass: Unit = Unit

fun getLiteralsOverThreshold(): Map<String, Int> {
return literals.filterValues { it > threshold }
}

override fun visitLiteralStringTemplateEntry(entry: KtLiteralStringTemplateEntry) {
val text = entry.text
when {
ignoreAnnotation && entry.isPartOf(KtAnnotationEntry::class) -> pass
excludeStringsWithLessThan5Characters && text.length < STRING_EXCLUSION_LENGTH -> pass
text.matches(ignoreStringsRegex) -> pass
else -> add(text)
}
}

private fun add(text: String) {
literals.put(text, literals.getOrDefault(text, 0) + 1)
}
}

companion object {
const val DEFAULT_DUPLICATION = 2
const val STRING_EXCLUSION_LENGTH = 5
const val IGNORE_ANNOTATION = "ignoreAnnotation"
const val EXCLUDE_SHORT_STRING = "excludeStringsWithLessThan5Characters"
const val IGNORE_STRINGS_REGEX = "ignoreStringsRegex"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import io.gitlab.arturbosch.detekt.rules.complexity.NestedBlockDepth
import io.gitlab.arturbosch.detekt.rules.complexity.TooManyFunctions
import io.gitlab.arturbosch.detekt.rules.complexity.ComplexCondition
import io.gitlab.arturbosch.detekt.rules.complexity.LabeledExpression
import io.gitlab.arturbosch.detekt.rules.complexity.StringLiteralDuplication

/**
* @author Artur Bosch
Expand All @@ -25,6 +26,7 @@ class ComplexityProvider : RuleSetProvider {
LongMethod(config),
LargeClass(config),
ComplexMethod(config),
StringLiteralDuplication(config),
NestedBlockDepth(config),
TooManyFunctions(config),
ComplexCondition(config),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package io.gitlab.arturbosch.detekt.rules.complexity

import io.gitlab.arturbosch.detekt.test.TestConfig
import io.gitlab.arturbosch.detekt.test.lint
import org.assertj.core.api.Java6Assertions.assertThat
import org.jetbrains.spek.api.dsl.given
import org.jetbrains.spek.api.dsl.it
import org.jetbrains.spek.subject.SubjectSpek
import org.jetbrains.spek.subject.dsl.SubjectProviderDsl

class StringLiteralDuplicationSpec : SubjectSpek<StringLiteralDuplication>({
subject { StringLiteralDuplication() }

given("many hardcoded strings") {

it("reports 3 equal hardcoded strings") {
val code = """
class Duplication {
var s1 = "lorem"
fun f(s: String = "lorem") {
s1.equals("lorem")
}
}"""
assertCodeFindings(code, 1)
}

it("does not report 2 equal hardcoded strings") {
val code = """val str = "lorem" + "lorem" + "ipsum""""
assertCodeFindings(code, 0)
}
}

given("strings in annotations") {

val code = """
@Suppress("unused")
class A
@Suppress("unused")
class B
@Suppress("unused")
class C
""""

it("does not report strings in annotations") {
assertCodeFindings(code, 0)
}

it("reports strings in annotations according to config") {
val config = TestConfig(mapOf(StringLiteralDuplication.IGNORE_ANNOTATION to "false"))
assertFindingWithConfig(code, config, 1)
}
}

given("strings with less than 5 characters") {

val code = """val str = "amet" + "amet" + "amet""""

it("does not report strings with 4 characters") {
assertCodeFindings(code, 0)
}

it("reports string with 4 characters") {
val config = TestConfig(mapOf(StringLiteralDuplication.EXCLUDE_SHORT_STRING to "false"))
assertFindingWithConfig(code, config, 1)
}
}

given("strings with values to match for the regex") {

it("does not report lorem or ipsum according to config in regex") {
val code = """
val str1 = "lorem" + "lorem" + "lorem"
val str2 = "ipsum" + "ipsum" + "ipsum"
"""
val config = TestConfig(mapOf(StringLiteralDuplication.IGNORE_STRINGS_REGEX to "(lorem|ipsum)"))
assertFindingWithConfig(code, config, 0)
}
}
})

private fun SubjectProviderDsl<StringLiteralDuplication>.assertCodeFindings(code: String, expected: Int) {
assertThat(subject.lint(code)).hasSize(expected)
}

private fun assertFindingWithConfig(code: String, config: TestConfig, expected: Int) {
val findings = StringLiteralDuplication(config).lint(code)
assertThat(findings).hasSize(expected)
}