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,82 @@ 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 = 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) }
}