Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AnnotationSuppressor now resolves Full Qualified Annotation Names without type solving #4570

Merged
merged 12 commits into from Feb 23, 2022
@@ -1,8 +1,11 @@
package io.gitlab.arturbosch.detekt.api

import io.github.detekt.psi.internal.FullQualifiedNameGuesser
import io.gitlab.arturbosch.detekt.rules.fqNameOrNull
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.KtAnnotationEntry
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtTypeReference
import org.jetbrains.kotlin.resolve.BindingContext

/**
Expand Down Expand Up @@ -42,33 +45,52 @@ class AnnotationExcluder(
* Is true if any given annotation name is declared in the SplitPattern
* which basically describes entries to exclude.
*/
fun shouldExclude(annotations: List<KtAnnotationEntry>): Boolean = annotations.any(::isExcluded)
fun shouldExclude(annotations: List<KtAnnotationEntry>): Boolean {
return annotations.any { annotation -> annotation.typeReference?.let { isExcluded(it, context) } ?: false }
}

private fun isExcluded(annotation: KtAnnotationEntry): Boolean {
val annotationText = annotation.typeReference?.text?.ifEmpty { null } ?: return false
/*
We can't know if the annotationText is a full-qualified name or not. We can have these cases:
@Component
@Component.Factory
@dagger.Component.Factory
For that reason we use a heuristic here: If the first character is lower case we assume it's a package name
*/
val possibleNames = if (!annotationText.first().isLowerCase()) {
fullQualifiedNameGuesser.getFullQualifiedName(annotationText)
private fun isExcluded(annotation: KtTypeReference, context: BindingContext): Boolean {
val fqName = annotation.fqNameOrNull(context)
return if (fqName == null) {
fullQualifiedNameGuesser.getFullQualifiedName(annotation.text.toString())
BraisGabin marked this conversation as resolved.
Show resolved Hide resolved
.map { it.getPackage() to it }
BraisGabin marked this conversation as resolved.
Show resolved Hide resolved
} else {
listOf(annotationText)
}.flatMap(::expandFqNames)
return excludes.any { exclude -> possibleNames.any { exclude.matches(it) } }
listOf(fqName.getPackage() to fqName.toString())
}
.flatMap { (pack, fqName) ->
fqName.substringAfter("$pack.", "")
.split(".")
.reversed()
.scan("") { acc, name -> if (acc.isEmpty()) name else "$name.$acc" }
.drop(1) + fqName
}
.any { name -> name in excludes }
}
}

private fun expandFqNames(fqName: String): List<String> {
return fqName
.split(".")
.dropWhile { it.first().isLowerCase() }
.reversed()
.scan("") { acc, name ->
if (acc.isEmpty()) name else "$name.$acc"
}
.drop(1) + fqName
private fun FqName.getPackage(): String {
// This is a shortcut. We should make this properly
return this.toString().getPackage()
cortinico marked this conversation as resolved.
Show resolved Hide resolved
}

private fun String.getPackage(): String {
/* We can't know if the annotationText is a full-qualified name or not. We can have these cases:
* @Component
* @Component.Factory
* @dagger.Component.Factory
* For that reason we use a heuristic here: If the first character is lower case we assume it's a package name
*/
return this
.splitToSequence(".")
.takeWhile { it.first().isLowerCase() }
.joinToString(".")
}

private fun KtTypeReference.fqNameOrNull(bindingContext: BindingContext): FqName? {
return bindingContext[BindingContext.TYPE, this]?.fqNameOrNull()
}

private operator fun Iterable<Regex>.contains(a: String?): Boolean {
if (a == null) return false
return any { it.matches(a) }
}
Expand Up @@ -9,6 +9,7 @@ import org.jetbrains.kotlin.psi.KtAnnotationEntry
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtFunction
import org.jetbrains.kotlin.resolve.BindingContext
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest
Expand Down Expand Up @@ -87,6 +88,69 @@ class AnnotationExcluderSpec(private val env: KotlinCoreEnvironment) {
assertThat(excluder.shouldExclude(listOf(ktAnnotation))).isTrue()
}
}

@Nested
inner class `difference between type solving and no type solving` {

@Nested
inner class `Don't mix annotations with the same name` {
BraisGabin marked this conversation as resolved.
Show resolved Hide resolved

@Test
fun `incorrect without type solving`() {
val (file, ktAnnotation) = createKtFile("@Deprecated")
val excluder = AnnotationExcluder(file, listOf("foo\\.Deprecated".toRegex()), BindingContext.EMPTY)

assertThat(excluder.shouldExclude(listOf(ktAnnotation))).isTrue()
}

@Test
fun `correct without type solving`() {
val (file, ktAnnotation) = createKtFile("@Deprecated")
val binding = env.getContextForPaths(listOf(file, annotationsKtFile))
val excluder = AnnotationExcluder(file, listOf("foo\\.Deprecated".toRegex()), binding)

assertThat(excluder.shouldExclude(listOf(ktAnnotation))).isFalse()
}
}

@Nested
inner class `Know where ends a package` {
val helloWorldAnnotationsKtFile = compileContentForTest(
"""
package com.Hello

annotation class World
""".trimIndent()
)
val file = compileContentForTest(
"""
package foo

import com.Hello.World

@World
fun function() = Unit
""".trimIndent()
)
val ktAnnotation = file.findChildByClass(KtFunction::class.java)!!.annotationEntries.first()!!

@Test
fun `incorrect without type solving`() {
val excluder = AnnotationExcluder(file, listOf("Hello\\.World".toRegex()), BindingContext.EMPTY)

assertThat(excluder.shouldExclude(listOf(ktAnnotation))).isTrue()
}

@Test
@Disabled("This should be doable but it's not imlemented yet")
fun `correct with type solving`() {
val binding = env.getContextForPaths(listOf(file, helloWorldAnnotationsKtFile))
val excluder = AnnotationExcluder(file, listOf("Hello\\.World".toRegex()), binding)

assertThat(excluder.shouldExclude(listOf(ktAnnotation))).isFalse()
}
}
}
}

private fun createKtFile(annotation: String): Pair<KtFile, KtAnnotationEntry> {
Expand Down