From d21596a0128eac69ca39846bb5370886e64c7be7 Mon Sep 17 00:00:00 2001 From: Artur Bosch Date: Wed, 24 Aug 2022 09:20:59 +0200 Subject: [PATCH] Introduce baseline tooling api (#5239) --- .../detekt/core/baseline/BaselineFacade.kt | 11 +++--- .../core/baseline/BaselineFilteredResult.kt | 3 +- .../detekt/core/baseline/BaselineFormat.kt | 20 ++++++++--- .../detekt/core/baseline/BaselineHandler.kt | 2 +- .../{Baseline.kt => DefaultBaseline.kt} | 16 ++++----- ...github.detekt.tooling.api.BaselineProvider | 1 + .../baseline/BaselineFilteredResultSpec.kt | 4 +-- .../core/baseline/BaselineFormatSpec.kt | 36 +++++++++++++++++-- .../baseline/BaselineResultMappingSpec.kt | 12 +++---- detekt-tooling/api/detekt-tooling.api | 19 ++++++++++ .../detekt/tooling/api/BaselineProvider.kt | 31 ++++++++++++++++ 11 files changed, 124 insertions(+), 31 deletions(-) rename detekt-core/src/main/kotlin/io/gitlab/arturbosch/detekt/core/baseline/{Baseline.kt => DefaultBaseline.kt} (74%) create mode 100644 detekt-core/src/main/resources/META-INF/services/io.github.detekt.tooling.api.BaselineProvider create mode 100644 detekt-tooling/src/main/kotlin/io/github/detekt/tooling/api/BaselineProvider.kt diff --git a/detekt-core/src/main/kotlin/io/gitlab/arturbosch/detekt/core/baseline/BaselineFacade.kt b/detekt-core/src/main/kotlin/io/gitlab/arturbosch/detekt/core/baseline/BaselineFacade.kt index dead8c6fac9..becc0fbb31e 100644 --- a/detekt-core/src/main/kotlin/io/gitlab/arturbosch/detekt/core/baseline/BaselineFacade.kt +++ b/detekt-core/src/main/kotlin/io/gitlab/arturbosch/detekt/core/baseline/BaselineFacade.kt @@ -11,7 +11,7 @@ class BaselineFacade { fun transformResult(baselineFile: Path, result: Detektion): Detektion { return if (baselineExists(baselineFile)) { - BaselineFilteredResult(result, Baseline.load(baselineFile)) + BaselineFilteredResult(result, DefaultBaseline.load(baselineFile)) } else { result } @@ -20,14 +20,15 @@ class BaselineFacade { fun createOrUpdate(baselineFile: Path, findings: List) { val ids = findings.map { it.baselineId }.toSortedSet() val oldBaseline = if (baselineExists(baselineFile)) { - Baseline.load(baselineFile) + DefaultBaseline.load(baselineFile) } else { - Baseline(emptySet(), emptySet()) + DefaultBaseline(emptySet(), emptySet()) } - val baseline = oldBaseline.copy(currentIssues = ids) + val baselineFormat = BaselineFormat() + val baseline = baselineFormat.of(oldBaseline.manuallySuppressedIssues, ids) if (oldBaseline != baseline) { baselineFile.parent?.let { Files.createDirectories(it) } - BaselineFormat().write(baseline, baselineFile) + baselineFormat.write(baselineFile, baseline) } } diff --git a/detekt-core/src/main/kotlin/io/gitlab/arturbosch/detekt/core/baseline/BaselineFilteredResult.kt b/detekt-core/src/main/kotlin/io/gitlab/arturbosch/detekt/core/baseline/BaselineFilteredResult.kt index 944ec58b8e1..23ce026e006 100644 --- a/detekt-core/src/main/kotlin/io/gitlab/arturbosch/detekt/core/baseline/BaselineFilteredResult.kt +++ b/detekt-core/src/main/kotlin/io/gitlab/arturbosch/detekt/core/baseline/BaselineFilteredResult.kt @@ -1,12 +1,13 @@ package io.gitlab.arturbosch.detekt.core.baseline +import io.github.detekt.tooling.api.Baseline import io.gitlab.arturbosch.detekt.api.Detektion import io.gitlab.arturbosch.detekt.api.Finding import io.gitlab.arturbosch.detekt.api.RuleSetId internal class BaselineFilteredResult( result: Detektion, - private val baseline: Baseline + private val baseline: Baseline, ) : Detektion by result { override val findings: Map> = result.findings diff --git a/detekt-core/src/main/kotlin/io/gitlab/arturbosch/detekt/core/baseline/BaselineFormat.kt b/detekt-core/src/main/kotlin/io/gitlab/arturbosch/detekt/core/baseline/BaselineFormat.kt index ac8c41e7756..888537415f8 100644 --- a/detekt-core/src/main/kotlin/io/gitlab/arturbosch/detekt/core/baseline/BaselineFormat.kt +++ b/detekt-core/src/main/kotlin/io/gitlab/arturbosch/detekt/core/baseline/BaselineFormat.kt @@ -1,5 +1,10 @@ package io.gitlab.arturbosch.detekt.core.baseline +import io.github.detekt.tooling.api.Baseline +import io.github.detekt.tooling.api.BaselineProvider +import io.github.detekt.tooling.api.FindingId +import io.github.detekt.tooling.api.FindingsIdList +import io.gitlab.arturbosch.detekt.api.Finding import org.xml.sax.SAXParseException import java.nio.file.Files import java.nio.file.Path @@ -8,16 +13,21 @@ import javax.xml.parsers.SAXParserFactory import javax.xml.stream.XMLStreamException import javax.xml.stream.XMLStreamWriter -internal class BaselineFormat { +internal class BaselineFormat : BaselineProvider { private val XMLStreamException.positions get() = location.lineNumber to location.columnNumber class InvalidState(msg: String, error: Throwable) : IllegalStateException(msg, error) - fun read(path: Path): Baseline { + override fun id(finding: Finding): FindingId = finding.baselineId + + override fun of(manuallySuppressedIssues: FindingsIdList, currentIssues: FindingsIdList): DefaultBaseline = + DefaultBaseline(manuallySuppressedIssues, currentIssues) + + override fun read(sourcePath: Path): DefaultBaseline { try { - Files.newInputStream(path).use { + Files.newInputStream(sourcePath).use { val reader = SAXParserFactory.newInstance() .apply { setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true) @@ -33,9 +43,9 @@ internal class BaselineFormat { } } - fun write(baseline: Baseline, path: Path) { + override fun write(targetPath: Path, baseline: Baseline) { try { - Files.newBufferedWriter(path).addFinalNewLine().use { + Files.newBufferedWriter(targetPath).addFinalNewLine().use { it.streamXml().prettyPrinter().save(baseline) } } catch (error: XMLStreamException) { diff --git a/detekt-core/src/main/kotlin/io/gitlab/arturbosch/detekt/core/baseline/BaselineHandler.kt b/detekt-core/src/main/kotlin/io/gitlab/arturbosch/detekt/core/baseline/BaselineHandler.kt index cfb11affafa..d6db1278e82 100644 --- a/detekt-core/src/main/kotlin/io/gitlab/arturbosch/detekt/core/baseline/BaselineHandler.kt +++ b/detekt-core/src/main/kotlin/io/gitlab/arturbosch/detekt/core/baseline/BaselineHandler.kt @@ -11,7 +11,7 @@ internal class BaselineHandler : DefaultHandler() { private val currentIssues = mutableSetOf() private val manuallySuppressedIssues = mutableSetOf() - internal fun createBaseline() = Baseline(manuallySuppressedIssues, currentIssues) + internal fun createBaseline() = DefaultBaseline(manuallySuppressedIssues, currentIssues) override fun startElement(uri: String, localName: String, qName: String, attributes: Attributes) { when (qName) { diff --git a/detekt-core/src/main/kotlin/io/gitlab/arturbosch/detekt/core/baseline/Baseline.kt b/detekt-core/src/main/kotlin/io/gitlab/arturbosch/detekt/core/baseline/DefaultBaseline.kt similarity index 74% rename from detekt-core/src/main/kotlin/io/gitlab/arturbosch/detekt/core/baseline/Baseline.kt rename to detekt-core/src/main/kotlin/io/gitlab/arturbosch/detekt/core/baseline/DefaultBaseline.kt index 33ce40a23d4..eba4ddce0ee 100644 --- a/detekt-core/src/main/kotlin/io/gitlab/arturbosch/detekt/core/baseline/Baseline.kt +++ b/detekt-core/src/main/kotlin/io/gitlab/arturbosch/detekt/core/baseline/DefaultBaseline.kt @@ -1,19 +1,19 @@ package io.gitlab.arturbosch.detekt.core.baseline +import io.github.detekt.tooling.api.Baseline +import io.github.detekt.tooling.api.FindingId +import io.github.detekt.tooling.api.FindingsIdList import io.gitlab.arturbosch.detekt.api.Finding import io.gitlab.arturbosch.detekt.core.exists import io.gitlab.arturbosch.detekt.core.isFile import java.nio.file.Path -internal typealias FindingsIdList = Set -internal typealias FindingId = String +internal data class DefaultBaseline( + override val manuallySuppressedIssues: FindingsIdList, + override val currentIssues: FindingsIdList, +) : Baseline { -internal data class Baseline( - val manuallySuppressedIssues: FindingsIdList, - val currentIssues: FindingsIdList -) { - - fun contains(id: FindingId): Boolean = + override fun contains(id: FindingId): Boolean = currentIssues.contains(id) || manuallySuppressedIssues.contains(id) companion object { diff --git a/detekt-core/src/main/resources/META-INF/services/io.github.detekt.tooling.api.BaselineProvider b/detekt-core/src/main/resources/META-INF/services/io.github.detekt.tooling.api.BaselineProvider new file mode 100644 index 00000000000..d09d3865ea5 --- /dev/null +++ b/detekt-core/src/main/resources/META-INF/services/io.github.detekt.tooling.api.BaselineProvider @@ -0,0 +1 @@ +io.gitlab.arturbosch.detekt.core.baseline.BaselineFormat diff --git a/detekt-core/src/test/kotlin/io/gitlab/arturbosch/detekt/core/baseline/BaselineFilteredResultSpec.kt b/detekt-core/src/test/kotlin/io/gitlab/arturbosch/detekt/core/baseline/BaselineFilteredResultSpec.kt index 43e2ef43d76..b88431235ec 100644 --- a/detekt-core/src/test/kotlin/io/gitlab/arturbosch/detekt/core/baseline/BaselineFilteredResultSpec.kt +++ b/detekt-core/src/test/kotlin/io/gitlab/arturbosch/detekt/core/baseline/BaselineFilteredResultSpec.kt @@ -28,13 +28,13 @@ class BaselineFilteredResultSpec { @Test fun `does return the same finding on empty baseline`() { - val actual = BaselineFilteredResult(result, Baseline(emptySet(), emptySet())) + val actual = BaselineFilteredResult(result, DefaultBaseline(emptySet(), emptySet())) assertThat(actual.findings).hasSize(3) } @Test fun `filters with an existing baseline file`() { - val baseline = Baseline.load(baselineFile) + val baseline = DefaultBaseline.load(baselineFile) val actual = BaselineFilteredResult(result, baseline) // Note: Detektion works with Map // but the TestDetektion maps the RuleId as RuleSetId diff --git a/detekt-core/src/test/kotlin/io/gitlab/arturbosch/detekt/core/baseline/BaselineFormatSpec.kt b/detekt-core/src/test/kotlin/io/gitlab/arturbosch/detekt/core/baseline/BaselineFormatSpec.kt index bb00b0bd808..28fc74438f6 100644 --- a/detekt-core/src/test/kotlin/io/gitlab/arturbosch/detekt/core/baseline/BaselineFormatSpec.kt +++ b/detekt-core/src/test/kotlin/io/gitlab/arturbosch/detekt/core/baseline/BaselineFormatSpec.kt @@ -2,6 +2,8 @@ package io.gitlab.arturbosch.detekt.core.baseline import io.github.detekt.test.utils.createTempFileForTest import io.github.detekt.test.utils.resourceAsPath +import io.github.detekt.tooling.api.BaselineProvider +import io.gitlab.arturbosch.detekt.test.createFinding import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatIllegalStateException import org.assertj.core.api.Assertions.assertThatThrownBy @@ -11,6 +13,34 @@ import java.nio.file.Files class BaselineFormatSpec { + @Nested + inner class `implements tooling api` { + + @Test + fun `core implements provider`() { + assertThat(BaselineProvider.load()).isInstanceOf(BaselineFormat::class.java) + } + + @Test + fun `provider allows to query finding baseline ids`() { + assertThat(BaselineProvider.load().id(createFinding())).isEqualTo("TestSmell:TestEntitySignature") + } + + @Test + fun `reads and writes baselines`() { + val provider = BaselineProvider.load() + val path = resourceAsPath("/baseline_feature/valid-baseline.xml") + + val referenceBaseline = provider.read(path) + val tempFile = createTempFileForTest("baseline1", ".xml") + provider.write(tempFile, referenceBaseline) + val actualBaseline = provider.read(path) + + assertThat(actualBaseline.currentIssues).containsAll(referenceBaseline.currentIssues) + assertThat(actualBaseline.manuallySuppressedIssues).containsAll(referenceBaseline.manuallySuppressedIssues) + } + } + @Nested inner class `read a baseline file` { @@ -57,14 +87,14 @@ class BaselineFormatSpec { @Nested inner class `writes a baseline file` { - private val savedBaseline = Baseline(setOf("4", "2", "2"), setOf("1", "2", "3")) + private val savedBaseline = DefaultBaseline(setOf("4", "2", "2"), setOf("1", "2", "3")) @Test fun `has a new line at the end of the written baseline file`() { val tempFile = createTempFileForTest("baseline1", ".xml") val format = BaselineFormat() - format.write(savedBaseline, tempFile) + format.write(tempFile, savedBaseline) val bytes = Files.readAllBytes(tempFile) val content = String(bytes, Charsets.UTF_8) @@ -76,7 +106,7 @@ class BaselineFormatSpec { val tempFile = createTempFileForTest("baseline-saved", ".xml") val format = BaselineFormat() - format.write(savedBaseline, tempFile) + format.write(tempFile, savedBaseline) val loadedBaseline = format.read(tempFile) assertThat(loadedBaseline).isEqualTo(savedBaseline) diff --git a/detekt-core/src/test/kotlin/io/gitlab/arturbosch/detekt/core/baseline/BaselineResultMappingSpec.kt b/detekt-core/src/test/kotlin/io/gitlab/arturbosch/detekt/core/baseline/BaselineResultMappingSpec.kt index 6d5698045d0..f70276934c3 100644 --- a/detekt-core/src/test/kotlin/io/gitlab/arturbosch/detekt/core/baseline/BaselineResultMappingSpec.kt +++ b/detekt-core/src/test/kotlin/io/gitlab/arturbosch/detekt/core/baseline/BaselineResultMappingSpec.kt @@ -55,7 +55,7 @@ class BaselineResultMappingSpec { @Test fun `should not update an existing baseline file if option configured as false`() { - val existing = Baseline.load(existingBaselineFile) + val existing = DefaultBaseline.load(existingBaselineFile) val mapping = resultMapping( baselineFile = existingBaselineFile, createBaseline = false, @@ -63,13 +63,13 @@ class BaselineResultMappingSpec { mapping.transformFindings(findings) - val changed = Baseline.load(existingBaselineFile) + val changed = DefaultBaseline.load(existingBaselineFile) assertThat(existing).isEqualTo(changed) } @Test fun `should not update an existing baseline file if option is not configured`() { - val existing = Baseline.load(existingBaselineFile) + val existing = DefaultBaseline.load(existingBaselineFile) val mapping = resultMapping( baselineFile = existingBaselineFile, createBaseline = null, @@ -77,7 +77,7 @@ class BaselineResultMappingSpec { mapping.transformFindings(findings) - val changed = Baseline.load(existingBaselineFile) + val changed = DefaultBaseline.load(existingBaselineFile) assertThat(existing).isEqualTo(changed) } @@ -108,7 +108,7 @@ class BaselineResultMappingSpec { @Test fun `should update an existing baseline file if a file is configured`() { Files.copy(existingBaselineFile, baselineFile) - val existing = Baseline.load(baselineFile) + val existing = DefaultBaseline.load(baselineFile) val mapping = resultMapping( baselineFile = baselineFile, createBaseline = true, @@ -116,7 +116,7 @@ class BaselineResultMappingSpec { mapping.transformFindings(findings) - val changed = Baseline.load(baselineFile) + val changed = DefaultBaseline.load(baselineFile) assertThat(existing).isNotEqualTo(changed) } } diff --git a/detekt-tooling/api/detekt-tooling.api b/detekt-tooling/api/detekt-tooling.api index fbabfcc9532..4155a325ada 100644 --- a/detekt-tooling/api/detekt-tooling.api +++ b/detekt-tooling/api/detekt-tooling.api @@ -7,6 +7,25 @@ public final class io/github/detekt/tooling/api/AnalysisResultKt { public static final fun exitCode (Lio/github/detekt/tooling/api/AnalysisResult;)I } +public abstract interface class io/github/detekt/tooling/api/Baseline { + public abstract fun contains (Ljava/lang/String;)Z + public abstract fun getCurrentIssues ()Ljava/util/Set; + public abstract fun getManuallySuppressedIssues ()Ljava/util/Set; +} + +public abstract interface class io/github/detekt/tooling/api/BaselineProvider { + public static final field Companion Lio/github/detekt/tooling/api/BaselineProvider$Companion; + public abstract fun id (Lio/gitlab/arturbosch/detekt/api/Finding;)Ljava/lang/String; + public abstract fun of (Ljava/util/Set;Ljava/util/Set;)Lio/github/detekt/tooling/api/Baseline; + public abstract fun read (Ljava/nio/file/Path;)Lio/github/detekt/tooling/api/Baseline; + public abstract fun write (Ljava/nio/file/Path;Lio/github/detekt/tooling/api/Baseline;)V +} + +public final class io/github/detekt/tooling/api/BaselineProvider$Companion { + public final fun load (Ljava/lang/ClassLoader;)Lio/github/detekt/tooling/api/BaselineProvider; + public static synthetic fun load$default (Lio/github/detekt/tooling/api/BaselineProvider$Companion;Ljava/lang/ClassLoader;ILjava/lang/Object;)Lio/github/detekt/tooling/api/BaselineProvider; +} + public abstract interface class io/github/detekt/tooling/api/DefaultConfigurationProvider { public static final field Companion Lio/github/detekt/tooling/api/DefaultConfigurationProvider$Companion; public abstract fun copy (Ljava/nio/file/Path;)V diff --git a/detekt-tooling/src/main/kotlin/io/github/detekt/tooling/api/BaselineProvider.kt b/detekt-tooling/src/main/kotlin/io/github/detekt/tooling/api/BaselineProvider.kt new file mode 100644 index 00000000000..b6b23cce58f --- /dev/null +++ b/detekt-tooling/src/main/kotlin/io/github/detekt/tooling/api/BaselineProvider.kt @@ -0,0 +1,31 @@ +package io.github.detekt.tooling.api + +import io.gitlab.arturbosch.detekt.api.Finding +import java.nio.file.Path +import java.util.ServiceLoader + +interface BaselineProvider { + + fun id(finding: Finding): FindingId + fun of(manuallySuppressedIssues: FindingsIdList, currentIssues: FindingsIdList): Baseline + fun read(sourcePath: Path): Baseline + fun write(targetPath: Path, baseline: Baseline) + + companion object { + + fun load( + classLoader: ClassLoader = BaselineProvider::class.java.classLoader, + ): BaselineProvider = ServiceLoader.load(BaselineProvider::class.java, classLoader).first() + } +} + +typealias FindingsIdList = Set +typealias FindingId = String + +interface Baseline { + + val manuallySuppressedIssues: FindingsIdList + val currentIssues: FindingsIdList + + fun contains(id: FindingId): Boolean +}