Repository: detekt/detekt (kotlin)

Issue:
`VarCouldBeVal` reported on `private lateinit var`
<!-- The bug you're experiencing might have already be reported! -->
<!-- Please search in the [issues](https://github.com/detekt/detekt/issues) before creating one. -->

## Steps to Reproduce
```kt
class A {
    private lateinit var label: JLabel
}
```

## Context
Per https://github.com/detekt/detekt/pull/4664#issuecomment-1086835405, I'm not sure our use-case is mainstream enough to warrant a change in detekt, but I'd like to bring it up as a datapoint anyways in case there there is additional discussion in the future.

As part of my work I develop plugins for JetBrains IDEs.
We have a number of older Kotlin source files backing visual forms designed using the JetBrains [GUI Designer](https://www.jetbrains.com/help/idea/gui-designer.html). I'm fuzzy on all the details, but how this generally works is that there's instrumentation at compile time that injects constructors and all the necessary method calls needed to setup the Swing components correctly.

For example, the code snippet above would result in decompiled bytecode that might look like the following:
```kt
class A {
    private lateinit var label: JLabel

    private fun `$$$setupUI$$$`() {
        label = JLabel("some label text")
        label.setIcon(Icon("and maybe we have a /path/to/icon declared here too"))
    }

    init {
        `$$$setupUI$$$`() 
    }
}
```

Obviously since detekt acts on source code and not bytecode, it can't know that the fields actually end up initialized.

The `VarCouldBeVal` rule is still very useful for us, so as a workaround to this regression we'd likely either mark the ~60 or so instances in our codebase with an annotation or throw it in a baseline file and call it a day.

## Your Environment
* Version of detekt used: 1.20.0
* Link to your project (if it's a public repository): https://github.com/aws/aws-toolkit-jetbrains

Pull request:
I'm not entirely convinced that it makes sense to disable this across the entire codebase, but maybe someone else has a use-case.

Fixes #4731

Patch:
diff --git a/detekt-core/src/main/resources/default-detekt-config.yml b/detekt-core/src/main/resources/default-detekt-config.yml
index 9879910ba866..5b5c7d3fd1d0 100644
--- a/detekt-core/src/main/resources/default-detekt-config.yml
+++ b/detekt-core/src/main/resources/default-detekt-config.yml
@@ -675,6 +675,7 @@ style:
     active: true
   VarCouldBeVal:
     active: true
+    ignoreLateinitVar: false
   WildcardImport:
     active: true
     excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
diff --git a/detekt-rules-style/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/style/VarCouldBeVal.kt b/detekt-rules-style/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/style/VarCouldBeVal.kt
index e602d19640fe..6a98d79a3c5d 100644
--- a/detekt-rules-style/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/style/VarCouldBeVal.kt
+++ b/detekt-rules-style/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/style/VarCouldBeVal.kt
@@ -8,8 +8,11 @@ import io.gitlab.arturbosch.detekt.api.Entity
 import io.gitlab.arturbosch.detekt.api.Issue
 import io.gitlab.arturbosch.detekt.api.Rule
 import io.gitlab.arturbosch.detekt.api.Severity
+import io.gitlab.arturbosch.detekt.api.config
 import io.gitlab.arturbosch.detekt.api.internal.ActiveByDefault
+import io.gitlab.arturbosch.detekt.api.internal.Configuration
 import io.gitlab.arturbosch.detekt.api.internal.RequiresTypeResolution
+import io.gitlab.arturbosch.detekt.rules.isLateinit
 import io.gitlab.arturbosch.detekt.rules.isOverride
 import org.jetbrains.kotlin.descriptors.DeclarationDescriptor
 import org.jetbrains.kotlin.lexer.KtTokens
@@ -68,10 +71,13 @@ class VarCouldBeVal(config: Config = Config.empty) : Rule(config) {
         Debt.FIVE_MINS
     )
 
+    @Configuration("Whether to ignore uninitialized lateinit vars")
+    private val ignoreLateinitVar: Boolean by config(defaultValue = false)
+
     override fun visitKtFile(file: KtFile) {
         super.visitKtFile(file)
         if (bindingContext == BindingContext.EMPTY) return
-        val assignmentVisitor = AssignmentVisitor(bindingContext)
+        val assignmentVisitor = AssignmentVisitor(bindingContext, ignoreLateinitVar)
         file.accept(assignmentVisitor)
 
         assignmentVisitor.getNonReAssignedDeclarations().forEach {
@@ -80,7 +86,10 @@ class VarCouldBeVal(config: Config = Config.empty) : Rule(config) {
     }
 
     @Suppress("TooManyFunctions")
-    private class AssignmentVisitor(private val bindingContext: BindingContext) : DetektVisitor() {
+    private class AssignmentVisitor(
+        private val bindingContext: BindingContext,
+        private val ignoreLateinitVar: Boolean
+    ) : DetektVisitor() {
 
         private val declarationCandidates = mutableSetOf<KtNamedDeclaration>()
         private val assignments = mutableMapOf<String, MutableSet<KtExpression>>()
@@ -197,7 +206,7 @@ class VarCouldBeVal(config: Config = Config.empty) : Rule(config) {
 
         private fun KtProperty.isDeclarationCandidate(): Boolean {
             return when {
-                !isVar || isOverride() -> false
+                !isVar || isOverride() || (ignoreLateinitVar && isLateinit()) -> false
                 isLocal || isPrivate() -> true
                 else -> {
                     // Check for whether property belongs to an anonymous object


Test patch:
diff --git a/detekt-rules-style/src/test/kotlin/io/gitlab/arturbosch/detekt/rules/style/VarCouldBeValSpec.kt b/detekt-rules-style/src/test/kotlin/io/gitlab/arturbosch/detekt/rules/style/VarCouldBeValSpec.kt
index 8982aab7f031..17e89eb46805 100644
--- a/detekt-rules-style/src/test/kotlin/io/gitlab/arturbosch/detekt/rules/style/VarCouldBeValSpec.kt
+++ b/detekt-rules-style/src/test/kotlin/io/gitlab/arturbosch/detekt/rules/style/VarCouldBeValSpec.kt
@@ -1,6 +1,7 @@
 package io.gitlab.arturbosch.detekt.rules.style
 
 import io.gitlab.arturbosch.detekt.rules.KotlinCoreEnvironmentTest
+import io.gitlab.arturbosch.detekt.test.TestConfig
 import io.gitlab.arturbosch.detekt.test.compileAndLint
 import io.gitlab.arturbosch.detekt.test.compileAndLintWithContext
 import org.assertj.core.api.Assertions.assertThat
@@ -491,4 +492,24 @@ class VarCouldBeValSpec(val env: KotlinCoreEnvironment) {
             }
         }
     }
+
+    @Nested
+    inner class `lateinit vars - #4731` {
+        val code = """
+            public class A {
+                private lateinit var test: String
+            }
+        """.trimIndent()
+
+        @Test
+        fun `reports uninitialized lateinit vars by default`() {
+            assertThat(subject.compileAndLintWithContext(env, code)).hasSize(1)
+        }
+
+        @Test
+        fun `does not report uninitialized lateinit vars if disabled in config`() {
+            val subject = VarCouldBeVal(TestConfig("ignoreLateinitVar" to true))
+            assertThat(subject.compileAndLintWithContext(env, code)).isEmpty()
+        }
+    }
 }