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 change: 1 addition & 0 deletions detekt-api/api/detekt-api.api
@@ -1,6 +1,7 @@
public final class io/gitlab/arturbosch/detekt/api/AnnotationExcluder {
public fun <init> (Lorg/jetbrains/kotlin/psi/KtFile;Lio/gitlab/arturbosch/detekt/api/SplitPattern;)V
public fun <init> (Lorg/jetbrains/kotlin/psi/KtFile;Ljava/util/List;)V
public fun <init> (Lorg/jetbrains/kotlin/psi/KtFile;Ljava/util/List;Lorg/jetbrains/kotlin/resolve/BindingContext;)V
public final fun shouldExclude (Ljava/util/List;)Z
}

Expand Down
@@ -1,8 +1,12 @@
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

/**
* Primary use case for an AnnotationExcluder is to decide if a KtElement should be
Expand All @@ -11,48 +15,86 @@ import org.jetbrains.kotlin.psi.KtFile
*/
class AnnotationExcluder(
root: KtFile,
excludes: List<String>,
private val excludes: List<Regex>,
private val context: BindingContext,
) {
private val excludes: List<Regex> = excludes.map {
it.replace(".", "\\.").replace("*", ".*").toRegex()
}

private val fullQualifiedNameGuesser = FullQualifiedNameGuesser(root)

@Deprecated("Use AnnotationExcluder(KtFile, List<String>) instead")
constructor(root: KtFile, excludes: SplitPattern) : this(root, excludes.mapAll { it })
@Deprecated("Use AnnotationExcluder(List<Regex>, KtFile) instead")
constructor(root: KtFile, excludes: SplitPattern) : this(
root,
excludes.mapAll { it }
.map { it.replace(".", "\\.").replace("*", ".*").toRegex() },
BindingContext.EMPTY,
)

@Deprecated("Use AnnotationExcluder(List<Regex>, KtFile) instead")
constructor(
root: KtFile,
excludes: List<String>,
) : this(
root,
excludes.map {
it.replace(".", "\\.").replace("*", ".*").toRegex()
},
BindingContext.EMPTY,
)

/**
* 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)

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)
fun shouldExclude(annotations: List<KtAnnotationEntry>): Boolean {
return annotations.any { annotation -> annotation.typeReference?.let { isExcluded(it, context) } ?: false }
}

private fun isExcluded(annotation: KtTypeReference, context: BindingContext): Boolean {
val fqName = if (context == BindingContext.EMPTY) null else 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 }
} 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. Right now we are using the same heuristic that we use when we don't have type solving
* information. With the type solving information we should know exactly which part is package and which part is
* class name. But right now I don't know how to extract that information. There is a disabled test that should be
* enabled once this is solved.
*/
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) }
}