Skip to content

Commit 1f24e4a

Browse files
SONARKT-542 Fix CPD with string templates
1 parent 4dccc46 commit 1f24e4a

File tree

3 files changed

+60
-12
lines changed

3 files changed

+60
-12
lines changed

sonar-kotlin-plugin/src/main/java/org/sonarsource/kotlin/plugin/cpd/CopyPasteDetector.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import org.jetbrains.kotlin.psi.KtFileAnnotationList
2525
import org.jetbrains.kotlin.psi.KtImportDirective
2626
import org.jetbrains.kotlin.psi.KtImportList
2727
import org.jetbrains.kotlin.psi.KtPackageDirective
28-
import org.jetbrains.kotlin.psi.KtStringTemplateEntry
28+
import org.jetbrains.kotlin.psi.KtStringTemplateExpression
2929
import org.jetbrains.kotlin.psi.psiUtil.allChildren
3030
import org.slf4j.LoggerFactory
3131
import org.sonar.api.batch.fs.InputFile
@@ -44,7 +44,7 @@ class CopyPasteDetector : KotlinFileVisitor() {
4444
val cpdTokens = sensorContext.newCpdTokens().onFile(kotlinFileContext.inputFileContext.inputFile)
4545

4646
val tokens = collectCpdRelevantNodes(kotlinFileContext.ktFile).map { node ->
47-
val text = if (node is KtStringTemplateEntry) "LITERAL" else node.text
47+
val text = if (node is KtStringTemplateExpression) "LITERAL" else node.text
4848
val cpdToken = CPDToken(kotlinFileContext.textRange(node), text)
4949
cpdTokens.addToken(cpdToken.range, cpdToken.text)
5050
cpdToken
@@ -60,7 +60,7 @@ class CopyPasteDetector : KotlinFileVisitor() {
6060
acc: MutableList<PsiElement> = mutableListOf()
6161
): List<PsiElement> {
6262
if (!isExcludedFromCpd(node)) {
63-
if ((node is LeafPsiElement && node !is PsiWhiteSpace) || node is KtStringTemplateEntry) {
63+
if ((node is LeafPsiElement && node !is PsiWhiteSpace) || node is KtStringTemplateExpression) {
6464
acc.add(node)
6565
} else {
6666
node.allChildren.forEach { collectCpdRelevantNodes(it, acc) }

sonar-kotlin-plugin/src/test/java/org/sonarsource/kotlin/plugin/KotlinSensorTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ internal class KotlinSensorTest : AbstractSensorTest() {
166166
assertThat(context.measure(inputFile.key(), CoreMetrics.FUNCTIONS).value()).isEqualTo(1)
167167
assertThat(context.measure(inputFile.key(), CoreMetrics.CLASSES).value()).isEqualTo(1)
168168
assertThat(context.cpdTokens(inputFile.key())!![1].value)
169-
.isEqualTo("print(1==1);print(\"LITERAL\");}")
169+
.isEqualTo("print(1==1);print(LITERAL);}")
170170
assertThat(context.measure(inputFile.key(), CoreMetrics.COMPLEXITY).value()).isEqualTo(1)
171171
assertThat(context.measure(inputFile.key(), CoreMetrics.STATEMENTS).value()).isEqualTo(2)
172172

sonar-kotlin-plugin/src/test/java/org/sonarsource/kotlin/plugin/cpd/CopyPasteDetectorTest.kt

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ import org.assertj.core.api.Assertions
2121
import org.assertj.core.api.ObjectAssert
2222
import org.jetbrains.kotlin.config.LanguageVersion
2323
import org.junit.jupiter.api.AfterEach
24+
import org.junit.jupiter.api.DynamicTest
2425
import org.junit.jupiter.api.Test
26+
import org.junit.jupiter.api.TestFactory
2527
import org.junit.jupiter.api.extension.RegisterExtension
2628
import org.junit.jupiter.api.io.TempDir
2729
import org.slf4j.event.Level
@@ -101,25 +103,24 @@ class CopyPasteDetectorTest {
101103
.hasEndUnit(8)
102104

103105
assertThat(cpdTokenLines[2])
104-
.hasValue("""println("LITERAL")""")
106+
.hasValue("""println(LITERAL)""")
105107
.hasStartLine(10)
106108
.hasStartUnit(9)
107-
.hasEndUnit(14)
109+
.hasEndUnit(12)
108110

109111
assertThat(cpdTokenLines[3])
110112
.hasValue("}")
111113
.hasStartLine(11)
112-
.hasStartUnit(15)
113-
.hasEndUnit(15)
114+
.hasStartUnit(13)
115+
.hasEndUnit(13)
114116

115117
assertThat(cpdTokenLines[4])
116118
.hasValue("}")
117119
.hasStartLine(12)
118-
.hasStartUnit(16)
119-
.hasEndUnit(16)
120+
.hasStartUnit(14)
121+
.hasEndUnit(14)
120122
}
121123

122-
123124
@Test
124125
fun `cpd tokens are saved for the next analysis when the cache is enabled`() {
125126
logTester.setLevel(Level.TRACE)
@@ -150,7 +151,7 @@ class CopyPasteDetectorTest {
150151

151152
Assertions.assertThat(logs)
152153
.hasSize(1)
153-
.containsExactly("Caching 16 CPD tokens for next analysis of input file moduleKey:dummy.kt.")
154+
.containsExactly("Caching 14 CPD tokens for next analysis of input file moduleKey:dummy.kt.")
154155
}
155156

156157
@Test
@@ -184,6 +185,53 @@ class CopyPasteDetectorTest {
184185
.hasSize(1)
185186
.containsExactly("No CPD tokens cached for next analysis of input file moduleKey:dummy.kt.")
186187
}
188+
189+
private val d = "$"
190+
private val tq = "\"\"\""
191+
192+
@TestFactory
193+
fun `cpd tokens`() = listOf(
194+
Triple("int literal", """ val x = 42 """, "valx=42"),
195+
Triple("long literal", """ val x = 42L """, "valx=42L"),
196+
Triple("float literal", """ val x = 42.0f """, "valx=42.0f"),
197+
Triple("double literal", """ val x = 42.0 """, "valx=42.0"),
198+
Triple("char literal", """ val x = 'a' """, "valx='a'"),
199+
Triple("null literal", """ val x = null """, "valx=null"),
200+
Triple("double-quote string literal", """ val x = "a" """, "valx=LITERAL"),
201+
Triple("double-quote string literal concatenation", """ val x = "a" + "b" """, "valx=LITERAL+LITERAL"),
202+
Triple("double-quote string template", """ val x = "a $d{1}" """, "valx=LITERAL"),
203+
Triple("triple-quote string literal", """ val x = ${tq}a${tq} """, "valx=LITERAL"),
204+
Triple("triple-quote string literal concatenation", """ val x = ${tq}a${tq} + ${tq}b${tq} """, "valx=LITERAL+LITERAL"),
205+
Triple("triple-quote string template", """ val x = ${tq}a $d{1}${tq} """, "valx=LITERAL"),
206+
Triple("mixed-quote string literal concatenation", """ val x = "a" + ${tq}b${tq} """, "valx=LITERAL+LITERAL"),
207+
Triple(
208+
"triple-quote string template with interpolated vars",
209+
"""
210+
val noInterpolations = "a literal"
211+
val doubleQuoteTwoInterpolations = "$d{x} $d{x}"
212+
val tripleQuoteTwoInterpolations = $tq$d{noInterpolations} $d{doubleQuoteTwoInterpolations}${tq}
213+
var nestedInterpolations = $tq$d{ "$d{1 + 1}" }${tq}
214+
""".trimIndent(),
215+
"""
216+
valnoInterpolations=LITERAL
217+
valdoubleQuoteTwoInterpolations=LITERAL
218+
valtripleQuoteTwoInterpolations=LITERAL
219+
varnestedInterpolations=LITERAL
220+
""".trimIndent()
221+
)
222+
).map { (title, input, expected) ->
223+
DynamicTest.dynamicTest("with $title") {
224+
val sensorContext: SensorContextTester = SensorContextTester.create(tmpFolder!!.root)
225+
val inputFile = TestInputFileBuilder("moduleKey", "test.kt").setModuleBaseDir(Path.of(".")).setContents(input).build()
226+
val root = kotlinTreeOf(input, Environment(disposable, emptyList(), LanguageVersion.LATEST_STABLE), inputFile)
227+
val ctx = InputFileContextImpl(sensorContext, inputFile, false)
228+
CopyPasteDetector().scan(ctx, root)
229+
230+
val cpdTokenLines = sensorContext.cpdTokens(inputFile.key())!!
231+
val tokensStringified = cpdTokenLines.joinToString(separator = "\n", transform = { it.value })
232+
Assertions.assertThat(tokensStringified).isEqualTo(expected)
233+
}
234+
}
187235
}
188236

189237

0 commit comments

Comments
 (0)