Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
## [13.1.0] - 2025-08-21

- Gradle 9 Support [#937](https://github.com/JLLeitschuh/ktlint-gradle/pull/937)
- Problems API Integration [#927](https://github.com/JLLeitschuh/ktlint-gradle/pull/927)

## [13.0.0] - 2025-07-07

Expand Down
3 changes: 3 additions & 0 deletions plugin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,9 @@ dependencies {
testImplementation(libs.kotlin.reflect)
testImplementation(libs.ktlint.rule.engine)
testImplementation(libs.archunit.junit5)
testImplementation(gradleApi())
// Used to test the problems API
testImplementation(libs.mockito.kotlin)
}

kotlin {
Expand Down
2 changes: 2 additions & 0 deletions plugin/gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ junit5 = "5.5.2"
assertJ = "3.27.4"
commonsIo = "2.17.0"
archUnit = "1.4.0"
mockitoKotlin = "4.1.0"

[libraries]
android-gradle-plugin = { module = "com.android.tools.build:gradle", version.ref = "androidPlugin" }
Expand All @@ -23,5 +24,6 @@ kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", v
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }
kotlin-script-runtime = { module = "org.jetbrains.kotlin:kotlin-script-runtime", version.ref = "kotlin" }
ktlint-rule-engine = { module = "com.pinterest.ktlint:ktlint-rule-engine", version.ref = "ktlint" }
mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version.ref = "mockitoKotlin" }
semver = { module = "net.swiftzer.semver:semver", version.ref = "semver" }
slf4j-nop = { module = "org.slf4j:slf4j-nop", version.ref = "sl4fj" }
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package org.jlleitschuh.gradle.ktlint.reporter

import org.gradle.api.problems.ProblemGroup
import org.gradle.api.problems.ProblemId
import org.gradle.api.problems.ProblemReporter
import org.gradle.api.problems.Problems
import org.gradle.api.problems.Severity
import org.jlleitschuh.gradle.ktlint.worker.SerializableLintError
import javax.inject.Inject

internal class ProblemsApiReporter @Inject constructor(
private val problems: Problems
) {

companion object {
private val PROBLEM_GROUP = ProblemGroup.create("ktlint-gradle", "ktlint-gradle issue")
}

fun reportProblems(lintErrors: Map<String, List<SerializableLintError>>, ignoreFailures: Boolean) {
val severity = if (ignoreFailures) Severity.WARNING else Severity.ERROR
lintErrors.forEach { (filePath, errors) ->
errors.forEach { error ->
reportProblem(error, filePath, severity)
}
}
}

fun reportProblem(error: SerializableLintError, filePath: String, severity: Severity) {
val reporter: ProblemReporter? = problems.reporter
val id = ProblemId.create(error.ruleId, error.detail, PROBLEM_GROUP)
reporter?.report(id) {
lineInFileLocation(filePath, error.line, error.col)
details(error.detail)
severity(severity)
solution("Run ktlintFormat to auto-fix this issue")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,17 @@ import org.gradle.api.provider.Property
import org.gradle.workers.WorkAction
import org.gradle.workers.WorkParameters
import org.jetbrains.kotlin.util.prefixIfNot
import org.jlleitschuh.gradle.ktlint.reporter.ProblemsApiReporter
import java.io.File
import javax.inject.Inject

internal abstract class ConsoleReportWorkAction : WorkAction<ConsoleReportWorkAction.ConsoleReportParameters> {

private val logger = Logging.getLogger("ktlint-console-report-worker")

@Inject
private lateinit var problemsApiReporter: ProblemsApiReporter

override fun execute() {
val errors = KtLintClassesSerializer
.create()
Expand All @@ -42,6 +47,17 @@ internal abstract class ConsoleReportWorkAction : WorkAction<ConsoleReportWorkAc
}

val isLintErrorsFound = lintErrors.values.flatten().isNotEmpty()

// Report problems to Gradle Problems API if available
if (isLintErrorsFound) {
try {
problemsApiReporter.reportProblems(lintErrors, parameters.ignoreFailures.getOrElse(false))
} catch (e: Exception) {
// Problems API might not be available in all Gradle versions
logger.debug("Problems API not available: ${e.message}")
}
}

if (parameters.outputToConsole.getOrElse(false) && isLintErrorsFound) {
val verbose = parameters.verbose.get()
lintErrors.forEach { (filePath, errors) ->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
package org.jlleitschuh.gradle.ktlint.reporter

import org.assertj.core.api.Assertions.assertThat
import org.gradle.api.Action
import org.gradle.api.problems.ProblemReporter
import org.gradle.api.problems.ProblemSpec
import org.gradle.api.problems.Problems
import org.jlleitschuh.gradle.ktlint.worker.SerializableLintError
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.mockito.kotlin.any
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
import org.gradle.api.problems.Severity as GradleSeverity

class ProblemsApiReporterTest {

private lateinit var problemsService: Problems
private lateinit var problemReporter: ProblemReporter
private lateinit var reporter: ProblemsApiReporter

@BeforeEach
fun setUp() {
problemsService = mock()
problemReporter = mock()
reporter = ProblemsApiReporter(problemsService)
whenever(problemsService.reporter).thenReturn(problemReporter)
}

@Test
fun `given a lint error, it correctly reports it to the Gradle Problems API`() {
val error = SerializableLintError(
line = 4,
col = 1,
ruleId = "no-wildcard-imports",
detail = "Wildcard import detected",
canBeAutoCorrected = true
)
val filePath = "src/main/kotlin/TestFile.kt"

reporter.reportProblem(error, filePath, GradleSeverity.ERROR)

val specCaptor = argumentCaptor<Action<ProblemSpec>>()
verify(problemReporter).report(any(), specCaptor.capture())

val spec: ProblemSpec = mock()
specCaptor.firstValue.execute(spec)

verify(spec).details("Wildcard import detected")
verify(spec).severity(GradleSeverity.ERROR)
verify(spec).lineInFileLocation(eq(filePath), eq(4), eq(1))
verify(spec).solution("Run ktlintFormat to auto-fix this issue")
}

@Test
fun `given multiple lint errors, it correctly reports all of them`() {
val errors = mapOf(
"src/main/kotlin/File1.kt" to listOf(
SerializableLintError(
line = 1,
col = 1,
ruleId = "no-wildcard-imports",
detail = "Wildcard import detected",
canBeAutoCorrected = true
)
),
"src/main/kotlin/File2.kt" to listOf(
SerializableLintError(
line = 5,
col = 10,
ruleId = "no-unused-imports",
detail = "Unused import",
canBeAutoCorrected = false
),
SerializableLintError(
line = 10,
col = 5,
ruleId = "no-trailing-whitespace",
detail = "Trailing whitespace",
canBeAutoCorrected = true
)
)
)

reporter.reportProblems(errors, false) // ignoreFailures = false, so ERROR severity

// Should report 3 total errors
verify(problemReporter, times(3)).report(any(), any())
}

@Test
fun `given no errors, it does not report any problems`() {
val emptyErrors = emptyMap<String, List<SerializableLintError>>()

reporter.reportProblems(emptyErrors, false)

verify(problemReporter, never()).report(any(), any())
}

@Test
fun `severity is WARNING when ignoreFailures is true`() {
val errors = mapOf(
"src/main/kotlin/TestFile.kt" to listOf(
SerializableLintError(
line = 1,
col = 1,
ruleId = "test-rule",
detail = "Test error",
canBeAutoCorrected = false
)
)
)

reporter.reportProblems(errors, true) // ignoreFailures = true, so WARNING severity

val specCaptor = argumentCaptor<Action<ProblemSpec>>()
verify(problemReporter).report(any(), specCaptor.capture())

val spec: ProblemSpec = mock()
specCaptor.firstValue.execute(spec)

verify(spec).severity(GradleSeverity.WARNING)
}

@Test
fun `reports correct problem group and id`() {
val error = SerializableLintError(
line = 1,
col = 1,
ruleId = "no-wildcard-imports",
detail = "Wildcard import detected",
canBeAutoCorrected = true
)
val filePath = "src/main/kotlin/TestFile.kt"

reporter.reportProblem(error, filePath, GradleSeverity.ERROR)

val idCaptor = argumentCaptor<org.gradle.api.problems.ProblemId>()
verify(problemReporter).report(idCaptor.capture(), any())

val capturedId = idCaptor.firstValue
assertThat(capturedId.name).isEqualTo("no-wildcard-imports")
assertThat(capturedId.displayName).isEqualTo("Wildcard import detected")
assertThat(capturedId.group.name).isEqualTo("ktlint-gradle")
assertThat(capturedId.group.displayName).isEqualTo("ktlint-gradle issue")
}

@Test
fun `handles different error types correctly`() {
val autoCorrectableError = SerializableLintError(
line = 1,
col = 1,
ruleId = "auto-correctable",
detail = "Can be fixed",
canBeAutoCorrected = true
)
val nonAutoCorrectableError = SerializableLintError(
line = 2,
col = 1,
ruleId = "manual-fix",
detail = "Must fix manually",
canBeAutoCorrected = false
)

reporter.reportProblem(autoCorrectableError, "test.kt", GradleSeverity.ERROR)
reporter.reportProblem(nonAutoCorrectableError, "test.kt", GradleSeverity.ERROR)

// Both should be reported with ERROR severity
verify(problemReporter, times(2)).report(any(), any())
}

@Test
fun `reports errors with correct line and column information`() {
val error = SerializableLintError(
line = 42,
col = 15,
ruleId = "test-rule",
detail = "Test error",
canBeAutoCorrected = false
)
val filePath = "src/main/kotlin/ComplexFile.kt"

reporter.reportProblem(error, filePath, GradleSeverity.ERROR)

val specCaptor = argumentCaptor<Action<ProblemSpec>>()
verify(problemReporter).report(any(), specCaptor.capture())

val spec: ProblemSpec = mock()
specCaptor.firstValue.execute(spec)

verify(spec).lineInFileLocation(eq(filePath), eq(42), eq(15))
}
}
Loading