Repository: detekt/detekt (kotlin)

Issue:
Add ending line and column to `Location.kt`
External tooling, such SARIF or IDE may need ending lines and columns, in addition to the `SourceLocation`. They should be provided in `:detekt-api`.

_Originally posted by @BraisGabin in https://github.com/detekt/detekt/pull/5011#discussion_r911449557_

Pull request description:
Close #5025 

Added `endSource` as the last parameter in the `Location` constructor.

I need some guidelines:
 - Is it necessary to change the name of parameters, for example `startSource` and `endSource`?
 - Should the order of the parameters be changed? 
 - Should `compact()`  string representation be updated?
 - Should there be a test for such trivia?

This could trigger changes in many files.

Patch:
diff --git a/detekt-api/api/detekt-api.api b/detekt-api/api/detekt-api.api
index 8405d7309808..665e3f51d909 100644
--- a/detekt-api/api/detekt-api.api
+++ b/detekt-api/api/detekt-api.api
@@ -353,6 +353,7 @@ public final class io/gitlab/arturbosch/detekt/api/LazyRegex : kotlin/properties
 
 public final class io/gitlab/arturbosch/detekt/api/Location : io/gitlab/arturbosch/detekt/api/Compactable {
 	public static final field Companion Lio/gitlab/arturbosch/detekt/api/Location$Companion;
+	public fun <init> (Lio/gitlab/arturbosch/detekt/api/SourceLocation;Lio/gitlab/arturbosch/detekt/api/SourceLocation;Lio/gitlab/arturbosch/detekt/api/TextLocation;Lio/github/detekt/psi/FilePath;)V
 	public fun <init> (Lio/gitlab/arturbosch/detekt/api/SourceLocation;Lio/gitlab/arturbosch/detekt/api/TextLocation;Lio/github/detekt/psi/FilePath;)V
 	public fun <init> (Lio/gitlab/arturbosch/detekt/api/SourceLocation;Lio/gitlab/arturbosch/detekt/api/TextLocation;Ljava/lang/String;)V
 	public fun <init> (Lio/gitlab/arturbosch/detekt/api/SourceLocation;Lio/gitlab/arturbosch/detekt/api/TextLocation;Ljava/lang/String;Lio/github/detekt/psi/FilePath;)V
@@ -367,6 +368,7 @@ public final class io/gitlab/arturbosch/detekt/api/Location : io/gitlab/arturbos
 	public final fun copy (Lio/gitlab/arturbosch/detekt/api/SourceLocation;Lio/gitlab/arturbosch/detekt/api/TextLocation;Ljava/lang/String;Lio/github/detekt/psi/FilePath;)Lio/gitlab/arturbosch/detekt/api/Location;
 	public static synthetic fun copy$default (Lio/gitlab/arturbosch/detekt/api/Location;Lio/gitlab/arturbosch/detekt/api/SourceLocation;Lio/gitlab/arturbosch/detekt/api/TextLocation;Ljava/lang/String;Lio/github/detekt/psi/FilePath;ILjava/lang/Object;)Lio/gitlab/arturbosch/detekt/api/Location;
 	public fun equals (Ljava/lang/Object;)Z
+	public final fun getEndSource ()Lio/gitlab/arturbosch/detekt/api/SourceLocation;
 	public final fun getFile ()Ljava/lang/String;
 	public final fun getFilePath ()Lio/github/detekt/psi/FilePath;
 	public final fun getSource ()Lio/gitlab/arturbosch/detekt/api/SourceLocation;
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 5f7fccd61866..a95648addb83 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
@@ -26,6 +26,8 @@ data class Location @Deprecated("Consider relative path by passing a [FilePath]"
     val file: String,
     val filePath: FilePath = FilePath.fromAbsolute(Paths.get(file))
 ) : Compactable {
+    var endSource: SourceLocation = source
+        private set
 
     @Suppress("DEPRECATION")
     constructor(
@@ -34,6 +36,16 @@ data class Location @Deprecated("Consider relative path by passing a [FilePath]"
         filePath: FilePath
     ) : this(source, text, filePath.absolutePath.toString(), filePath)
 
+    @Suppress("DEPRECATION")
+    constructor(
+        source: SourceLocation,
+        endSource: SourceLocation,
+        text: TextLocation,
+        filePath: FilePath,
+    ) : this(source, text, filePath.absolutePath.toString(), filePath) {
+        this.endSource = endSource
+    }
+
     @Suppress("DEPRECATION")
     @Deprecated(
         """
@@ -62,20 +74,33 @@ data class Location @Deprecated("Consider relative path by passing a [FilePath]"
         fun from(element: PsiElement, offset: Int = 0): Location {
             val start = startLineAndColumn(element, offset)
             val sourceLocation = SourceLocation(start.line, start.column)
+            val end = endLineAndColumn(element, offset)
+            val endSourceLocation = SourceLocation(end.line, end.column)
             val textLocation = TextLocation(element.startOffset + offset, element.endOffset + offset)
-            return Location(sourceLocation, textLocation, element.containingFile.toFilePath())
+            return Location(sourceLocation, endSourceLocation, textLocation, element.containingFile.toFilePath())
         }
 
         /**
-         * Determines the line and column of a [PsiElement] in the source file.
+         * Determines the start line and column of a [PsiElement] in the source file.
          */
-        fun startLineAndColumn(element: PsiElement, offset: Int = 0): PsiDiagnosticUtils.LineAndColumn {
+        fun startLineAndColumn(element: PsiElement, offset: Int = 0): PsiDiagnosticUtils.LineAndColumn =
+            lineAndColumn(
+                element,
+                TextRange(element.textRange.startOffset + offset, element.textRange.endOffset + offset)
+            )
+
+        /**
+         * Determines the end line and column of a [PsiElement] in the source file.
+         */
+        private fun endLineAndColumn(element: PsiElement, offset: Int = 0): PsiDiagnosticUtils.LineAndColumn =
+            lineAndColumn(
+                element,
+                TextRange(element.textRange.endOffset + offset, element.textRange.endOffset + offset)
+            )
+
+        private fun lineAndColumn(element: PsiElement, range: TextRange): PsiDiagnosticUtils.LineAndColumn {
             return try {
-                val range = element.textRange
-                DiagnosticUtils.getLineAndColumnInPsiFile(
-                    element.containingFile,
-                    TextRange(range.startOffset + offset, range.endOffset + offset)
-                )
+                DiagnosticUtils.getLineAndColumnInPsiFile(element.containingFile, range)
             } catch (@Suppress("SwallowedException", "TooGenericExceptionCaught") e: IndexOutOfBoundsException) {
                 // #3317 If any rule mutates the PsiElement, searching the original PsiElement may throw exception.
                 PsiDiagnosticUtils.LineAndColumn(-1, -1, null)


Test patch:
diff --git a/detekt-api/src/test/kotlin/io/gitlab/arturbosch/detekt/api/LocationSpec.kt b/detekt-api/src/test/kotlin/io/gitlab/arturbosch/detekt/api/LocationSpec.kt
new file mode 100644
index 000000000000..b55dda3392bf
--- /dev/null
+++ b/detekt-api/src/test/kotlin/io/gitlab/arturbosch/detekt/api/LocationSpec.kt
@@ -0,0 +1,35 @@
+package io.gitlab.arturbosch.detekt.api
+
+import io.github.detekt.test.utils.compileContentForTest
+import org.assertj.core.api.Assertions.assertThat
+import org.jetbrains.kotlin.psi.KtNamedFunction
+import org.junit.jupiter.api.Test
+
+class LocationSpec {
+
+    @Test
+    fun `start and end positions of block`() {
+        val code = """
+            fun data(): Int {
+                return 0
+            }
+        """.trimIndent()
+        val psiElement = compileContentForTest(code).findChildByClass(KtNamedFunction::class.java)!!
+        val location = Location.from(psiElement)
+
+        assertThat("${location.source} - ${location.endSource}").isEqualTo("1:1 - 3:2")
+    }
+
+    @Test
+    fun `start and end positions of fun keyword`() {
+        val code = """
+            fun data(): Int {
+                return 0
+            }
+        """.trimIndent()
+        val psiElement = compileContentForTest(code).findChildByClass(KtNamedFunction::class.java)!!
+        val location = Location.from(psiElement.funKeyword!!)
+
+        assertThat("${location.source} - ${location.endSource}").isEqualTo("1:1 - 1:4")
+    }
+}