Repository: detekt/detekt (kotlin)

Issue:
Sort log output by line then column
All warnings are output in order of detekt provider type. They should be logged in order: by line, then column. This makes it easier to review the list of issues detekt finds.

Pull request:
<!-- A similar PR may already be submitted! -->
<!-- Please search among the [Pull requests](https://github.com/detekt/detekt/pulls) before creating one. -->

<!-- Thanks for submitting a pull request! Please provide enough information so that others can review your pull request. Link to relevant issues if possible. -->

<!-- For more information, see the [CONTRIBUTING guide](https://github.com/detekt/detekt/blob/main/.github/CONTRIBUTING.md). -->

This fixes #5749 by sorting the violations by line and column.

Patch:
diff --git a/detekt-api/api/detekt-api.api b/detekt-api/api/detekt-api.api
index a9f06653b1bb..742bda0ec256 100644
--- a/detekt-api/api/detekt-api.api
+++ b/detekt-api/api/detekt-api.api
@@ -181,13 +181,18 @@ public abstract interface class io/gitlab/arturbosch/detekt/api/Issue$Entity {
 	public abstract fun getSignature ()Ljava/lang/String;
 }
 
-public abstract interface class io/gitlab/arturbosch/detekt/api/Issue$Location {
+public abstract interface class io/gitlab/arturbosch/detekt/api/Issue$Location : java/lang/Comparable {
+	public abstract fun compareTo (Lio/gitlab/arturbosch/detekt/api/Issue$Location;)I
 	public abstract fun getEndSource ()Lio/gitlab/arturbosch/detekt/api/SourceLocation;
 	public abstract fun getPath ()Ljava/nio/file/Path;
 	public abstract fun getSource ()Lio/gitlab/arturbosch/detekt/api/SourceLocation;
 	public abstract fun getText ()Lio/gitlab/arturbosch/detekt/api/TextLocation;
 }
 
+public final class io/gitlab/arturbosch/detekt/api/Issue$Location$DefaultImpls {
+	public static fun compareTo (Lio/gitlab/arturbosch/detekt/api/Issue$Location;Lio/gitlab/arturbosch/detekt/api/Issue$Location;)I
+}
+
 public final class io/gitlab/arturbosch/detekt/api/IssueKt {
 	public static final fun getSuppressed (Lio/gitlab/arturbosch/detekt/api/Issue;)Z
 }
@@ -343,8 +348,10 @@ public final class io/gitlab/arturbosch/detekt/api/Severity : java/lang/Enum {
 	public static fun values ()[Lio/gitlab/arturbosch/detekt/api/Severity;
 }
 
-public final class io/gitlab/arturbosch/detekt/api/SourceLocation {
+public final class io/gitlab/arturbosch/detekt/api/SourceLocation : java/lang/Comparable {
 	public fun <init> (II)V
+	public fun compareTo (Lio/gitlab/arturbosch/detekt/api/SourceLocation;)I
+	public synthetic fun compareTo (Ljava/lang/Object;)I
 	public fun equals (Ljava/lang/Object;)Z
 	public final fun getColumn ()I
 	public final fun getLine ()I
diff --git a/detekt-api/src/main/kotlin/io/gitlab/arturbosch/detekt/api/Issue.kt b/detekt-api/src/main/kotlin/io/gitlab/arturbosch/detekt/api/Issue.kt
index dc1e2b910074..f1a8fdd94a97 100644
--- a/detekt-api/src/main/kotlin/io/gitlab/arturbosch/detekt/api/Issue.kt
+++ b/detekt-api/src/main/kotlin/io/gitlab/arturbosch/detekt/api/Issue.kt
@@ -27,11 +27,13 @@ interface Issue {
         val ktElement: KtElement
     }
 
-    interface Location {
+    interface Location : Comparable<Location> {
         val source: SourceLocation
         val endSource: SourceLocation
         val text: TextLocation
         val path: Path
+
+        override fun compareTo(other: Location): Int = compareValuesBy(this, other, { it.path }, { it.source })
     }
 }
 
diff --git a/detekt-api/src/main/kotlin/io/gitlab/arturbosch/detekt/api/Location.kt b/detekt-api/src/main/kotlin/io/gitlab/arturbosch/detekt/api/Location.kt
index f63e06f9100f..a8d32f74c5c0 100644
--- a/detekt-api/src/main/kotlin/io/gitlab/arturbosch/detekt/api/Location.kt
+++ b/detekt-api/src/main/kotlin/io/gitlab/arturbosch/detekt/api/Location.kt
@@ -73,13 +73,15 @@ class Location(
  * Stores line and column information of a location.
  */
 @Poko
-class SourceLocation(val line: Int, val column: Int) {
+class SourceLocation(val line: Int, val column: Int) : Comparable<SourceLocation> {
     init {
         require(line > 0) { "The source location line must be greater than 0" }
         require(column > 0) { "The source location column must be greater than 0" }
     }
 
     override fun toString(): String = "$line:$column"
+
+    override fun compareTo(other: SourceLocation): Int = compareValuesBy(this, other, { it.line }, { it.column })
 }
 
 /**
diff --git a/detekt-compiler-plugin/src/main/kotlin/io/github/detekt/compiler/plugin/internal/MessageCollectorExtensions.kt b/detekt-compiler-plugin/src/main/kotlin/io/github/detekt/compiler/plugin/internal/MessageCollectorExtensions.kt
index 033525305bb1..0494d7c81c07 100644
--- a/detekt-compiler-plugin/src/main/kotlin/io/github/detekt/compiler/plugin/internal/MessageCollectorExtensions.kt
+++ b/detekt-compiler-plugin/src/main/kotlin/io/github/detekt/compiler/plugin/internal/MessageCollectorExtensions.kt
@@ -24,6 +24,7 @@ fun MessageCollector.error(msg: String) {
 fun MessageCollector.reportIssues(result: Detektion) {
     result.issues
         .filterNot { it.suppressed }
+        .sortedBy { it.location }
         .forEach { issue ->
             val (message, location) = issue.renderAsCompilerWarningMessage()
             warn(message, location)


Test patch:
diff --git a/detekt-compiler-plugin/src/test/kotlin/io/github/detekt/compiler/plugin/CompilerTest.kt b/detekt-compiler-plugin/src/test/kotlin/io/github/detekt/compiler/plugin/CompilerTest.kt
index da828e756a0a..50fc855909f9 100644
--- a/detekt-compiler-plugin/src/test/kotlin/io/github/detekt/compiler/plugin/CompilerTest.kt
+++ b/detekt-compiler-plugin/src/test/kotlin/io/github/detekt/compiler/plugin/CompilerTest.kt
@@ -10,27 +10,46 @@ class CompilerTest {
     fun `with a source file that contains violations`() {
         val result = compile(
             """
-                class KClass {
+                class KClass0 {
                     fun foo() {
-                        val x = 3
+                        var x = 3
                         println(x)
+                        var y = 4
+                        println(y)
                     }
                 }
+            """.trimIndent(),
+            """
+                class KClass1 {
+                   fun foo() {
+                      val x = "test"
+                   }
+                }
             """.trimIndent()
         )
 
         assertThat(result)
             .passCompilation(true)
             .passDetekt(false)
-            .withViolations(2)
-            .withRuleViolation("MagicNumber", "NewLineAtEndOfFile")
+            .withViolations(7)
+            .withRuleViolationInOrder(
+                listOf(
+                    "VarCouldBeVal",
+                    "MagicNumber",
+                    "VarCouldBeVal",
+                    "MagicNumber",
+                    "NewLineAtEndOfFile",
+                    "UnusedVariable",
+                    "NewLineAtEndOfFile"
+                )
+            )
     }
 
     @Test
     fun `with a source file that contains local suppression`() {
         val result = compile(
             """
-                class KClass {
+                class KClass0 {
                     fun foo() {
                         @Suppress("MagicNumber")
                         val x = 3
@@ -52,7 +71,7 @@ class CompilerTest {
     fun `with a source file that does not contain violations`() {
         val result = compile(
             """
-                class KClass {
+                class KClass0 {
                     fun foo() {
                         println("Hello world :)")
                     }
diff --git a/detekt-compiler-plugin/src/test/kotlin/io/github/detekt/compiler/plugin/util/CompilerAssertions.kt b/detekt-compiler-plugin/src/test/kotlin/io/github/detekt/compiler/plugin/util/CompilerAssertions.kt
index ff0477344150..70eef710268a 100644
--- a/detekt-compiler-plugin/src/test/kotlin/io/github/detekt/compiler/plugin/util/CompilerAssertions.kt
+++ b/detekt-compiler-plugin/src/test/kotlin/io/github/detekt/compiler/plugin/util/CompilerAssertions.kt
@@ -10,13 +10,10 @@ fun assertThat(result: JvmCompilationResult) = CompilationAssert(result)
 class CompilationAssert(private val result: JvmCompilationResult) :
     AbstractObjectAssert<CompilationAssert, JvmCompilationResult>(result, CompilationAssert::class.java) {
 
-    private val detektMessages = result.messages.split("\n")
-        .dropWhile { "Running detekt" !in it }
-        .dropLastWhile { "Success?" !in it }
+    private val detektMessages = result.messages.split("\n").dropWhile { !(it.contains(Regex("KClass\\d.kt"))) }
 
-    private val regex = "\\w+\\.kt:\\d+:\\d+: .* \\[(\\w+)\\]".toRegex()
-    private val detektViolations = detektMessages
-        .mapNotNull { line -> regex.find(line)?.groupValues?.get(1) }
+    private val regex = "\\w+\\.kt:\\d+:\\d+ (\\w+): .*".toRegex()
+    private val detektViolations = detektMessages.mapNotNull { line -> regex.find(line)?.groupValues?.get(1) }
 
     fun passCompilation(expectedStatus: Boolean = true) = apply {
         val expectedErrorCode = if (expectedStatus) OK else COMPILATION_ERROR
@@ -62,10 +59,10 @@ class CompilationAssert(private val result: JvmCompilationResult) :
         }
     }
 
-    fun withRuleViolation(vararg expectedRuleName: String) = apply {
-        if (expectedRuleName.any { it !in detektViolations }) {
+    fun withRuleViolationInOrder(expectedRuleNames: List<String>) {
+        if (detektViolations != expectedRuleNames) {
             failWithMessage(
-                "Expected rules ${expectedRuleName.toList()} to raise a violation but not all were found. " +
+                "Expected rules $expectedRuleNames to raise a violation but not all were found. " +
                     "Found violations are instead $detektViolations.\n" +
                     actual.messages,
             )
diff --git a/detekt-compiler-plugin/src/test/kotlin/io/github/detekt/compiler/plugin/util/CompilerTestUtils.kt b/detekt-compiler-plugin/src/test/kotlin/io/github/detekt/compiler/plugin/util/CompilerTestUtils.kt
index 262ad43aa370..5d68cad2935c 100644
--- a/detekt-compiler-plugin/src/test/kotlin/io/github/detekt/compiler/plugin/util/CompilerTestUtils.kt
+++ b/detekt-compiler-plugin/src/test/kotlin/io/github/detekt/compiler/plugin/util/CompilerTestUtils.kt
@@ -15,8 +15,8 @@ object CompilerTestUtils {
         @Language("kotlin") vararg kotlinSources: String,
         useDefaultConfig: Boolean = true,
     ): JvmCompilationResult {
-        val sourceFiles = kotlinSources.map {
-            SourceFile.kotlin("KClass.kt", it, trimIndent = true)
+        val sourceFiles = kotlinSources.mapIndexed { index, sources ->
+            SourceFile.kotlin("KClass$index.kt", sources, trimIndent = true)
         }
         return KotlinCompilation().apply {
             messageOutputStream = object : OutputStream() {