com.pinterest.ktlint.cli.reporter.baseline.Baseline.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of ktlint-cli-reporter-baseline Show documentation
Show all versions of ktlint-cli-reporter-baseline Show documentation
An anti-bikeshedding Kotlin linter with built-in formatter.
The newest version!
package com.pinterest.ktlint.cli.reporter.baseline
import com.pinterest.ktlint.cli.reporter.baseline.Baseline.Status.INVALID
import com.pinterest.ktlint.cli.reporter.baseline.Baseline.Status.NOT_FOUND
import com.pinterest.ktlint.cli.reporter.baseline.Baseline.Status.VALID
import com.pinterest.ktlint.cli.reporter.core.api.KtlintCliError
import com.pinterest.ktlint.cli.reporter.core.api.KtlintCliError.Status.BASELINE_IGNORED
import com.pinterest.ktlint.logger.api.initKtLintKLogger
import com.pinterest.ktlint.rule.engine.core.api.RuleId
import io.github.oshai.kotlinlogging.KotlinLogging
import org.w3c.dom.Element
import org.xml.sax.SAXException
import java.io.IOException
import java.io.InputStream
import java.nio.file.Paths
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.parsers.ParserConfigurationException
private val LOGGER = KotlinLogging.logger {}.initKtLintKLogger()
/**
* Baseline of lint errors to be ignored in subsequent calls to ktlint.
*/
public class Baseline(
/**
* Path to the baseline file.
*/
public val path: String? = null,
/**
* Status of the baseline file.
*/
public val status: Status,
/**
* Lint errors grouped by (relative) file path.
*/
public val lintErrorsPerFile: Map> = emptyMap(),
) {
public enum class Status {
/**
* Consumer did not request the Baseline file to be loaded.
*/
DISABLED,
/**
* Baseline file is successfully parsed.
*/
VALID,
/**
* Baseline file does not exist. File needs to be generated by the consumer first.
*/
NOT_FOUND,
/**
* Baseline file is not successfully parsed. File needs to be regenerated by the consumer.
*/
INVALID,
}
}
public enum class BaselineErrorHandling {
/**
* Log an error message. Does not throw an exception.
*/
LOG,
/**
* Throws an exception on error. Does not log the error.
*/
EXCEPTION,
}
/**
* Loads the [Baseline] from the file located on [path]. Exceptions are swallowed and log message is written. On error, the baseline file is
* deleted.
*/
@Deprecated(
message = "Marked for removal in Ktlint 2.0",
replaceWith = ReplaceWith("loadBaseline(path, BaselineErrorHandling.LOG)"),
)
public fun loadBaseline(path: String): Baseline = loadBaseline(path, BaselineErrorHandling.LOG)
/**
* Loads the [Baseline] from the file located on [path]. In case the baseline file can not be loaded successfully, it will be deleted.
*/
public fun loadBaseline(
path: String,
errorHandling: BaselineErrorHandling = BaselineErrorHandling.EXCEPTION,
): Baseline =
with(BaselineLoader(path)) {
try {
load()
} catch (e: Exception) {
// Delete baseline as it contains an error
try {
delete()
} catch (e: Exception) {
if (errorHandling == BaselineErrorHandling.LOG) {
LOGGER.error { e.message }
} else {
// Swallow as original exception from loading is to be returned only
}
}
// Handle original exception
if (errorHandling == BaselineErrorHandling.EXCEPTION) {
throw e
} else {
LOGGER.error { e.message }
Baseline(path = path, status = INVALID)
}
}
}
private class BaselineLoader(
private val path: String,
) {
private val baselinePath =
Paths
.get(path)
.toFile()
.takeIf { it.exists() }
var ruleReferenceWithoutRuleSetIdPrefix = 0
fun load(): Baseline {
require(path.isNotBlank()) { "Path for loading baseline may not be blank or empty" }
baselinePath
?.let { baselineFile ->
try {
return Baseline(
path = path,
lintErrorsPerFile = baselineFile.inputStream().parseBaseline(),
status = VALID,
).also {
if (ruleReferenceWithoutRuleSetIdPrefix > 0) {
LOGGER.warn {
"Baseline file '$path' contains $ruleReferenceWithoutRuleSetIdPrefix reference(s) to rule ids without " +
"a rule set id. For those references the rule set id 'standard' is assumed. It is advised to " +
"regenerate this baseline file."
}
}
}
} catch (e: IOException) {
throw BaselineLoaderException("Unable to parse baseline file: $path", e)
} catch (e: ParserConfigurationException) {
throw BaselineLoaderException("Unable to parse baseline file: $path", e)
} catch (e: SAXException) {
throw BaselineLoaderException("Unable to parse baseline file: $path", e)
}
}
return Baseline(path = path, status = NOT_FOUND)
}
/**
* Parses the [InputStream] of a baseline file and return the lint errors grouped by the relative file names.
*/
private fun InputStream.parseBaseline(): Map> {
val lintErrorsPerFile = HashMap>()
with(parseDocument().getElementsByTagName("file")) {
for (i in 0 until length) {
with(item(i) as Element) {
val fileName = getAttribute("name")
lintErrorsPerFile[fileName] = parseBaselineFileElement()
}
}
}
return lintErrorsPerFile
}
private fun InputStream.parseDocument() =
DocumentBuilderFactory
.newInstance()
.newDocumentBuilder()
.parse(this)
/**
* Parses a "file" [Element] in the baseline file.
*/
private fun Element.parseBaselineFileElement(): List {
val ktlintCliErrorsInFileElement = mutableListOf()
with(getElementsByTagName("error")) {
for (i in 0 until length) {
ktlintCliErrorsInFileElement.add(
with(item(i) as Element) {
parseBaselineErrorElement()
},
)
}
}
return ktlintCliErrorsInFileElement
}
/**
* Parses an "error" [Element] in the baseline file.
*/
private fun Element.parseBaselineErrorElement() =
KtlintCliError(
line = getAttribute("line").toInt(),
col = getAttribute("column").toInt(),
ruleId =
getAttribute("source")
.let { ruleId ->
// Ensure backwards compatibility with baseline files in which the rule set id for standard rules is not saved
RuleId
.prefixWithStandardRuleSetIdWhenMissing(ruleId)
.also { prefixedRuleId ->
if (prefixedRuleId != ruleId) {
ruleReferenceWithoutRuleSetIdPrefix++
}
}
},
// Detail is not available in the baseline
detail = "",
status = BASELINE_IGNORED,
)
fun delete() {
try {
baselinePath?.delete()
} catch (e: IOException) {
throw BaselineLoaderException("Unable to delete baseline file: $path", e)
}
}
}
public class BaselineLoaderException(
message: String,
throwable: Throwable,
) : RuntimeException(message, throwable)
/**
* Checks if the list contains the given [KtlintCliError]. The [List.contains] function can not be used as [KtlintCliError.detail] is not
* available in the baseline file and a normal equality check on the [KtlintCliError] fails.
*/
public fun List.containsLintError(ktlintCliError: KtlintCliError): Boolean = any { it.isSameAs(ktlintCliError) }
private fun KtlintCliError.isSameAs(lintError: KtlintCliError) =
col == lintError.col &&
line == lintError.line &&
RuleId.prefixWithStandardRuleSetIdWhenMissing(ruleId) == RuleId.prefixWithStandardRuleSetIdWhenMissing(lintError.ruleId)
/**
* Checks if the list does not contain the given [KtlintCliError]. The [List.contains] function can not be used as [KtlintCliError.detail]
* is not available in the baseline file and a normal equality check on the [KtlintCliError] fails.
*/
public fun List.doesNotContain(ktlintCliError: KtlintCliError): Boolean = none { it.isSameAs(ktlintCliError) }
© 2015 - 2024 Weber Informatics LLC | Privacy Policy