Skip to content

Commit

Permalink
SONARKT-424 Kotlin android metric
Browse files Browse the repository at this point in the history
  • Loading branch information
antonioaversa committed Jan 30, 2025
1 parent 5e7902f commit 090f362
Show file tree
Hide file tree
Showing 8 changed files with 169 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ public class TestsHelper {
.setSonarVersion(System.getProperty(SQ_VERSION_PROPERTY, DEFAULT_SQ_VERSION))
.restoreProfileAtStartup(FileLocation.of("src/test/resources/suppress-warnings-kotlin.xml"))
.restoreProfileAtStartup(FileLocation.of("src/test/resources/norule.xml"))
.setServerProperty("sonar.telemetry.enable", "false")
.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ public static void setUp() {
OrchestratorExtensionBuilder builder = OrchestratorExtension.builderEnv()
.useDefaultAdminCredentialsForBuilds(true)
.setSonarVersion(System.getProperty(SQ_VERSION_PROPERTY, DEFAULT_SQ_VERSION))
.addPlugin(MavenLocation.of("org.sonarsource.sonar-lits-plugin", "sonar-lits-plugin", "0.11.0.2659"));
.addPlugin(MavenLocation.of("org.sonarsource.sonar-lits-plugin", "sonar-lits-plugin", "0.11.0.2659"))
.setServerProperty("sonar.telemetry.enable", "false");

addLanguagePlugins(builder);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiComment
import com.intellij.psi.PsiWhiteSpace
import org.jetbrains.kotlin.kdoc.psi.api.KDoc
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.psi.KtBlockExpression
import org.jetbrains.kotlin.psi.KtClass
import org.jetbrains.kotlin.psi.KtClassOrObject
Expand All @@ -46,6 +47,7 @@ const val NOSONAR_PREFIX = "NOSONAR"
class MetricVisitor(
private val fileLinesContextFactory: FileLinesContextFactory,
private val noSonarFilter: NoSonarFilter,
private val telemetryData: TelemetryData, // Some metrics are stored in telemetry
) : KotlinFileVisitor() {
private lateinit var ktMetricVisitor: KtMetricVisitor

Expand All @@ -67,6 +69,8 @@ class MetricVisitor(
ktMetricVisitor.executableLines.forEach { line -> fileLinesContext.setIntValue(CoreMetrics.EXECUTABLE_LINES_DATA_KEY, line, 1) }
fileLinesContext.save()
noSonarFilter.noSonarInFile(ctx.inputFile, ktMetricVisitor.nosonarLines)

telemetryData.hasAndroidImports = telemetryData.hasAndroidImports || ktMetricVisitor.hasAndroidImports
}

fun commentLines() = ktMetricVisitor.commentLines.toSet()
Expand All @@ -82,9 +86,17 @@ class MetricVisitor(
fun cognitiveComplexity() = ktMetricVisitor.cognitiveComplexity

fun executableLines() = ktMetricVisitor.executableLines

fun hasAndroidImports() = ktMetricVisitor.hasAndroidImports
}

private class KtMetricVisitor : KtTreeVisitorVoid() {
private companion object {
val ANDROID_PACKAGES = setOf(Name.identifier("android"), Name.identifier("androidx"))
}

// User metrics

var linesOfCode: Set<Int> = mutableSetOf()
private set

Expand Down Expand Up @@ -112,6 +124,11 @@ private class KtMetricVisitor : KtTreeVisitorVoid() {
var cognitiveComplexity = 0
private set

// Telemetry metrics

var hasAndroidImports = false
private set

override fun visitKtFile(file: KtFile) {
linesOfCode = file.linesOfCode()

Expand Down Expand Up @@ -153,6 +170,12 @@ private class KtMetricVisitor : KtTreeVisitorVoid() {
super.visitBlockExpression(expression)
}

override fun visitImportDirective(importDirective: KtImportDirective) {
hasAndroidImports = hasAndroidImports ||
ANDROID_PACKAGES.any { importDirective.importPath?.fqName?.startsWith(it) ?: false }
super.visitImportDirective(importDirective)
}

private fun addExecutableLines(elements: List<KtElement>, document: Document) {
elements.asSequence()
.filterNot {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* SonarSource Kotlin
* Copyright (C) 2018-2025 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the Sonar Source-Available License for more details.
*
* You should have received a copy of the Sonar Source-Available License
* along with this program; if not, see https://sonarsource.com/license/ssal/
*/
package org.sonarsource.kotlin.metrics

import org.sonar.api.batch.sensor.SensorContext
import org.sonar.api.utils.Version

private val MIN_SQS_SUPPORTED: Version = Version.create(10, 9)

class TelemetryData {
var hasAndroidImports = false
var hasAndroidImportsReportedAsTrue = false

fun report(sensorContext: SensorContext) {
if (hasAndroidImportsReportedAsTrue || !sensorContext.isTelemetrySupported()) return
sensorContext.addTelemetryProperty("kotlin.android", if (hasAndroidImports) "1" else "0")
hasAndroidImportsReportedAsTrue = hasAndroidImports
}
}

private fun SensorContext.isTelemetrySupported() =
this.runtime().apiVersion.isGreaterThanOrEqual(MIN_SQS_SUPPORTED)

Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ internal class MetricVisitorTest {
)
)
).thenReturn(mockFileLinesContext)
visitor = MetricVisitor(mockFileLinesContextFactory, mockNoSonarFilter)
visitor = MetricVisitor(mockFileLinesContextFactory, mockNoSonarFilter, TelemetryData())
}

@Test
Expand Down Expand Up @@ -341,6 +341,42 @@ internal class MetricVisitorTest {
assertThat(visitor.executableLines()).containsExactly(5, 10)
}

@Test
fun hasAndroidImports() {
fun assert(expected: Boolean, code: String) {
scan(code)
assertThat(visitor.hasAndroidImports()).isEqualTo(expected)
}

assert(true, "import android.content.Context")
assert(true, "import android.content.Context;")
assert(true, "import android.net.Uri")
assert(true, "import androidx.core.view.WindowCompat")
assert(true, "import android.graphics.*")
assert(true, "import android.content.Context as AndroidContext")

assert(false, "import java.io.ByteArrayOutputStream")
assert(false, "import kotlin.properties.ReadWriteProperty")
assert(false, "/* import android.content.Context */")
assert(false, """
// import android.content.Context
""".trimIndent())
assert(false, "import mylibrary.android.MyClass")
assert(false, "import androidy.core.view.WindowCompat")
assert(false, "package android")
assert(false, "class android {}")
assert(false, "fun android() = 42")

assert(true, """
import java.io.*
import android.content.SharedPreferences
""".trimIndent())
assert(true, """
import android.util.Log
import com.facebook.react.PackageList
""".trimIndent())
}

private fun scan(code: String) {
inputFile = TestInputFileBuilder("moduleKey", createTempFile(tempFolder, suffix = ".kt").name)
.setCharset(StandardCharsets.UTF_8)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* SonarSource Kotlin
* Copyright (C) 2018-2025 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the Sonar Source-Available License for more details.
*
* You should have received a copy of the Sonar Source-Available License
* along with this program; if not, see https://sonarsource.com/license/ssal/
*/
package org.sonarsource.kotlin.metrics

import org.junit.jupiter.api.Test

import org.junit.jupiter.api.Assertions.*
import org.sonar.api.batch.sensor.internal.SensorContextTester
import java.nio.file.Path

class TelemetryDataTest {
@Test
fun hasAndroidImportsReportedAsTrue() {
val data = TelemetryData()
assertFalse(data.hasAndroidImports)
assertFalse(data.hasAndroidImportsReportedAsTrue)
data.hasAndroidImports = false
assertFalse(data.hasAndroidImportsReportedAsTrue)
val sensorContext = SensorContextTester.create(Path.of("."))
data.report(sensorContext)
assertFalse(data.hasAndroidImportsReportedAsTrue)
data.hasAndroidImports = true
data.report(sensorContext)
assertTrue(data.hasAndroidImportsReportedAsTrue)
data.hasAndroidImports = false
data.report(sensorContext)
assertTrue(data.hasAndroidImportsReportedAsTrue)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import org.sonarsource.kotlin.metrics.IssueSuppressionVisitor
import org.sonarsource.kotlin.metrics.MetricVisitor
import org.sonarsource.kotlin.metrics.SyntaxHighlighter
import org.sonarsource.kotlin.api.visiting.KtChecksVisitor
import org.sonarsource.kotlin.metrics.TelemetryData

import kotlin.jvm.optionals.getOrElse

Expand All @@ -58,12 +59,23 @@ class KotlinSensor(
): AbstractKotlinSensor(
checkFactory, language, KOTLIN_CHECKS
) {
private val telemetryData = TelemetryData()

override fun describe(descriptor: SensorDescriptor) {
descriptor
.onlyOnLanguage(language.key)
.name(language.name + " Sensor")
}

override fun execute(sensorContext: SensorContext) {
super.execute(sensorContext)
// The MetricsVisitor instantiated by the visitors method keeps a shared reference
// to the TelemetryData of this sensor, and updates it accordingly. The report method
// of TelemetryData takes care of not sending metrics more than once, when execute is
// run multiple times.
telemetryData.report(sensorContext)
}

override fun getExecuteContext(
sensorContext: SensorContext,
filesToAnalyze: Iterable<InputFile>,
Expand Down Expand Up @@ -93,13 +105,13 @@ class KotlinSensor(
if (sensorContext.runtime().product == SonarProduct.SONARLINT) {
listOf(
IssueSuppressionVisitor(),
MetricVisitor(fileLinesContextFactory, noSonarFilter),
MetricVisitor(fileLinesContextFactory, noSonarFilter, telemetryData),
KtChecksVisitor(checks),
)
} else {
listOf(
IssueSuppressionVisitor(),
MetricVisitor(fileLinesContextFactory, noSonarFilter),
MetricVisitor(fileLinesContextFactory, noSonarFilter, telemetryData),
KtChecksVisitor(checks),
CopyPasteDetector(),
SyntaxHighlighter(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -711,6 +711,18 @@ internal class KotlinSensorTest : AbstractSensorTest() {
assertAnalysisIsNotIncremental(files)
}

@Test
fun `execute reports telemetry`() {
val sensor = sensor(checkFactory())
var telemetrySet = false
val context = spyk(context) {
every { addTelemetryProperty(any(), any()) } answers { telemetrySet = true }
}
assertThat(telemetrySet).isFalse()
sensor.execute(context)
assertThat(telemetrySet).isTrue()
}

private fun assertAnalysisIsIncremental(files: Map<InputFile.Status, InputFile>) {
val addedFile = files[InputFile.Status.ADDED]
val changedFile = files[InputFile.Status.CHANGED]
Expand Down

0 comments on commit 090f362

Please sign in to comment.