blob: e596f53a215939bf576f04f284862d01d398203a [file] [log] [blame]
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.build.lint
import com.android.tools.lint.detector.api.AnnotationUsageType
import com.android.tools.lint.detector.api.Category
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Implementation
import com.android.tools.lint.detector.api.Issue
import com.android.tools.lint.detector.api.JavaContext
import com.android.tools.lint.detector.api.Scope
import com.android.tools.lint.detector.api.Severity
import com.android.tools.lint.detector.api.SourceCodeScanner
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiMethod
import org.jetbrains.uast.UAnnotation
import org.jetbrains.uast.UElement
import org.jetbrains.uast.getContainingUClass
class BanInappropriateExperimentalUsage : Detector(), SourceCodeScanner {
override fun applicableAnnotations(): List<String>? = listOf(
JAVA_EXPERIMENTAL_ANNOTATION,
KOTLIN_OPT_IN_ANNOTATION,
KOTLIN_EXPERIMENTAL_ANNOTATION
)
override fun visitAnnotationUsage(
context: JavaContext,
usage: UElement,
type: AnnotationUsageType,
annotation: UAnnotation,
qualifiedName: String,
method: PsiMethod?,
referenced: PsiElement?,
annotations: List<UAnnotation>,
allMemberAnnotations: List<UAnnotation>,
allClassAnnotations: List<UAnnotation>,
allPackageAnnotations: List<UAnnotation>
) {
when (qualifiedName) {
JAVA_EXPERIMENTAL_ANNOTATION,
JAVA_OPT_IN_ANNOTATION,
KOTLIN_EXPERIMENTAL_ANNOTATION,
KOTLIN_OPT_IN_ANNOTATION -> {
verifyExperimentalOrOptInUsageIsWithinSameGroup(
context, usage, annotation
)
}
}
}
fun verifyExperimentalOrOptInUsageIsWithinSameGroup(
context: JavaContext,
usage: UElement,
annotation: UAnnotation
) {
val declaringGroup = getApproximateAnnotationMavenGroup(annotation)
val usingGroup = getApproximateUsageSiteMavenGroup(usage)
// Don't flag if group is null for some reason (for now at least)
// Also exclude sample for now, since it doesn't work well with our workaround (includes
// class)
if (declaringGroup != null && usingGroup != null && declaringGroup != usingGroup &&
usingGroup != "sample"
) {
context.report(
BanInappropriateExperimentalUsage.ISSUE, usage, context.getNameLocation(usage),
"`Experimental`/`OptIn` APIs should only be used from within the same library " +
"or libraries within the same requireSameVersion group"
)
}
}
fun getApproximateAnnotationMavenGroup(annotation: UAnnotation): String? {
if (annotation.getContainingUClass() == null || annotation.getContainingUClass()!!
.qualifiedName == null
) {
return null
}
return annotation.getContainingUClass()!!.qualifiedName!!.split(".").subList(0, 2)
.joinToString(".")
}
fun getApproximateUsageSiteMavenGroup(usage: UElement): String? {
if (usage.getContainingUClass() == null || usage.getContainingUClass()!!
.qualifiedName == null
) {
return null
}
return usage.getContainingUClass()!!.qualifiedName!!.split(".").subList(0, 2)
.joinToString(".")
}
companion object {
private const val KOTLIN_EXPERIMENTAL_ANNOTATION = "kotlin.Experimental"
private const val JAVA_EXPERIMENTAL_ANNOTATION =
"androidx.annotation.experimental.Experimental"
private const val KOTLIN_OPT_IN_ANNOTATION =
"kotlin.OptIn"
private const val JAVA_OPT_IN_ANNOTATION =
"androidx.annotation.OptIn"
val ISSUE = Issue.create(
"IllegalExperimentalApiUsage",
"Using experimental api from separately versioned library",
"APIs annotated with `@RequiresOptIn` or `@Experimental` are considered alpha." +
"A caller from another library may not use them unless that the two libraries " +
"are part of the same maven group and that group specifies requireSameVersion",
Category.CORRECTNESS, 5, Severity.ERROR,
Implementation(BanInappropriateExperimentalUsage::class.java, Scope.JAVA_FILE_SCOPE)
)
}
}