Skip to content

Commit

Permalink
Add PropertyUsedBeforeDeclaration rule (#6062)
Browse files Browse the repository at this point in the history
  • Loading branch information
t-kameyama committed May 9, 2023
1 parent 11ec4f0 commit a6aeb38
Show file tree
Hide file tree
Showing 4 changed files with 182 additions and 1 deletion.
2 changes: 2 additions & 0 deletions detekt-core/src/main/resources/default-detekt-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,8 @@ potential-bugs:
active: false
NullableToStringCall:
active: false
PropertyUsedBeforeDeclaration:
active: false
UnconditionalJumpStatementInLoop:
active: false
UnnecessaryNotNullCheck:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ class PotentialBugProvider : DefaultRuleSetProvider {
UnreachableCatchBlock(config),
CastToNullableType(config),
CastNullableToNonNullableType(config),
UnusedUnaryOperator(config)
UnusedUnaryOperator(config),
PropertyUsedBeforeDeclaration(config),
)
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package io.gitlab.arturbosch.detekt.rules.bugs

import io.gitlab.arturbosch.detekt.api.CodeSmell
import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.api.Debt
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.internal.RequiresTypeResolution
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor
import org.jetbrains.kotlin.psi.KtClassOrObject
import org.jetbrains.kotlin.psi.KtNameReferenceExpression
import org.jetbrains.kotlin.psi.KtProperty
import org.jetbrains.kotlin.psi.psiUtil.forEachDescendantOfType
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.calls.util.getResolvedCall
import org.jetbrains.kotlin.utils.addIfNotNull

/**
* Reports properties that are used before declaration.
*
* <noncompliant>
* class C {
* private val number
* get() = if (isValid) 1 else 0
*
* val list = listOf(number)
*
* private val isValid = true
* }
*
* fun main() {
* println(C().list) // [0]
* }
* </noncompliant>
*
* <compliant>
* class C {
* private val isValid = true
*
* private val number
* get() = if (isValid) 1 else 0
*
* val list = listOf(number)
* }
*
* fun main() {
* println(C().list) // [1]
* }
* </compliant>
*/
@RequiresTypeResolution
class PropertyUsedBeforeDeclaration(config: Config = Config.empty) : Rule(config) {
override val issue = Issue(
javaClass.simpleName,
Severity.Defect,
"Properties before declaration should not be used.",
Debt.FIVE_MINS
)

override fun visitClassOrObject(classOrObject: KtClassOrObject) {
super.visitClassOrObject(classOrObject)

val classMembers = classOrObject.body?.children ?: return

val allProperties = classMembers.filterIsInstance<KtProperty>().mapNotNull {
val name = it.name ?: return@mapNotNull null
val descriptor = bindingContext[BindingContext.DECLARATION_TO_DESCRIPTOR, it] ?: return@mapNotNull null
name to descriptor
}.toMap()

val declaredProperties = mutableSetOf<DeclarationDescriptor>()

classMembers.forEach { member ->
member.forEachDescendantOfType<KtNameReferenceExpression> {
val property = allProperties[it.text]
if (property != null && property !in declaredProperties && property == it.descriptor()) {
report(CodeSmell(issue, Entity.from(it), "'${it.text}' is before declaration."))
}
}
if (member is KtProperty) {
declaredProperties.addIfNotNull(allProperties[member.name])
}
}
}

private fun KtNameReferenceExpression.descriptor() = getResolvedCall(bindingContext)?.resultingDescriptor
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package io.gitlab.arturbosch.detekt.rules.bugs

import io.gitlab.arturbosch.detekt.rules.KotlinCoreEnvironmentTest
import io.gitlab.arturbosch.detekt.test.assertThat
import io.gitlab.arturbosch.detekt.test.compileAndLintWithContext
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
import org.junit.jupiter.api.Test

@KotlinCoreEnvironmentTest
class PropertyUsedBeforeDeclarationSpec(private val env: KotlinCoreEnvironment) {
private val subject = PropertyUsedBeforeDeclaration()

@Test
fun `used before declaration in getter`() {
val code = """
class C {
private val number get() = if (isValid) 1 else 0
val list = listOf(number)
private val isValid = true
}
fun main() {
println(C().list) // [0]
}
""".trimIndent()
val findings = subject.compileAndLintWithContext(env, code)
assertThat(findings).hasSize(1)
assertThat(findings).hasTextLocations(45 to 52)
assertThat(findings.first()).hasMessage("'isValid' is before declaration.")
}

@Test
fun `used before declaration in init`() {
val code = """
class C {
init {
run {
println(isValid) // false
}
}
private val isValid = true
}
fun main() {
C()
}
""".trimIndent()
val findings = subject.compileAndLintWithContext(env, code)
assertThat(findings).hasSize(1)
}

@Test
fun `used before declaration in function`() {
val code = """
class C {
fun f() = isValid
private val isValid = true
}
""".trimIndent()
val findings = subject.compileAndLintWithContext(env, code)
assertThat(findings).hasSize(1)
}

@Test
fun `used after declaration in getter`() {
val code = """
class C {
private val isValid = true
private val number get() = if (isValid) 1 else 0
val list = listOf(number)
}
""".trimIndent()
val findings = subject.compileAndLintWithContext(env, code)
assertThat(findings).isEmpty()
}

@Test
fun `variable shadowing`() {
val code = """
class C {
fun f(): Boolean {
val isValid = true
return isValid
}
private val isValid = true
}
""".trimIndent()
val findings = subject.compileAndLintWithContext(env, code)
assertThat(findings).isEmpty()
}
}

0 comments on commit a6aeb38

Please sign in to comment.