slack.lint.util.LintUtils.kt Maven / Gradle / Ivy
The newest version!
// Copyright (C) 2021 Slack Technologies, LLC
// SPDX-License-Identifier: Apache-2.0
package slack.lint.util
import com.android.tools.lint.client.api.Configuration
import com.android.tools.lint.client.api.JavaEvaluator
import com.android.tools.lint.client.api.TYPE_BOOLEAN_WRAPPER
import com.android.tools.lint.client.api.TYPE_BYTE_WRAPPER
import com.android.tools.lint.client.api.TYPE_CHARACTER_WRAPPER
import com.android.tools.lint.client.api.TYPE_DOUBLE_WRAPPER
import com.android.tools.lint.client.api.TYPE_FLOAT_WRAPPER
import com.android.tools.lint.client.api.TYPE_INTEGER_WRAPPER
import com.android.tools.lint.client.api.TYPE_LONG_WRAPPER
import com.android.tools.lint.client.api.TYPE_SHORT_WRAPPER
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Implementation
import com.android.tools.lint.detector.api.JavaContext
import com.android.tools.lint.detector.api.LintFix
import com.android.tools.lint.detector.api.ResourceXmlDetector
import com.android.tools.lint.detector.api.Scope
import com.android.tools.lint.detector.api.SourceCodeScanner
import com.android.tools.lint.detector.api.StringOption
import com.android.tools.lint.detector.api.UastLintUtils
import com.android.tools.lint.detector.api.isKotlin
import com.intellij.psi.PsiClass
import com.intellij.psi.PsiClassType
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiJavaFile
import com.intellij.psi.PsiMethod
import com.intellij.psi.PsiModifierListOwner
import com.intellij.psi.PsiType
import com.intellij.psi.PsiTypes
import com.intellij.psi.PsiWildcardType
import java.util.EnumSet
import org.jetbrains.kotlin.idea.KotlinLanguage
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.uast.UClass
import org.jetbrains.uast.UExpression
import org.jetbrains.uast.UMethod
import org.jetbrains.uast.UQualifiedReferenceExpression
import org.jetbrains.uast.UReferenceExpression
import org.jetbrains.uast.USimpleNameReferenceExpression
import org.jetbrains.uast.tryResolve
/**
* @param qualifiedName the qualified name of the desired interface type
* @param nameFilter an optional name filter, used to check when to stop searching up the type
* hierarchy. This is useful if you want to only check direct implementers in certain packages.
* Called with a fully qualified class name; return false if you want to stop searching up the
* type tree, true to continue.
*/
internal fun PsiClass.implements(
qualifiedName: String,
nameFilter: (String) -> Boolean = { true },
): Boolean {
val fqcn = this.qualifiedName ?: return false
if (fqcn == qualifiedName) {
// Found a match
return true
}
if (!nameFilter(fqcn)) {
// Don't proceed further
return false
}
return this.superTypes.filterNotNull().any { classType ->
classType.resolve()?.implements(qualifiedName, nameFilter) ?: false
}
}
/** @return whether [owner] is a Kotlin `value` class. */
internal fun JavaEvaluator.isValueClass(owner: PsiModifierListOwner?): Boolean {
// Check the annotation for JvmInline as a shorter check
return owner?.hasAnnotation("kotlin.jvm.JvmInline") == true ||
hasModifier(owner, KtTokens.VALUE_KEYWORD)
}
internal fun UClass.isInnerClass(evaluator: JavaEvaluator): Boolean {
// If it has no containing class, it's top-level and therefore not inner
containingClass ?: return false
// If it's static (i.e. in Java), it's not an inner class
if (isStatic) return false
// If it's Kotlin and "inner", then it's definitely an inner class
if (isKotlin(language) && evaluator.hasModifier(this, KtTokens.INNER_KEYWORD)) return true
// We could check the containing class's innerClasses to look for a match here, but we've
// logically ruled
// out this possibility above
return false
}
@Suppress("SpreadOperator")
internal inline fun sourceImplementation(
shouldRunOnTestSources: Boolean = true
): Implementation where T : Detector, T : SourceCodeScanner {
// We use the overloaded constructor that takes a varargs of `Scope` as the last param.
// This is to enable on-the-fly IDE checks. We are telling lint to run on both
// JAVA and TEST_SOURCES in the `scope` parameter but by providing the `analysisScopes`
// params, we're indicating that this check can run on either JAVA or TEST_SOURCES and
// doesn't require both of them together.
// From discussion on lint-dev https://groups.google.com/d/msg/lint-dev/ULQMzW1ZlP0/1dG4Vj3-AQAJ
// This was supposed to be fixed in AS 3.4 but still required as recently as 3.6-alpha10.
return if (shouldRunOnTestSources) {
Implementation(
T::class.java,
EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES),
EnumSet.of(Scope.JAVA_FILE),
EnumSet.of(Scope.TEST_SOURCES),
)
} else {
Implementation(T::class.java, EnumSet.of(Scope.JAVA_FILE))
}
}
@Suppress("SpreadOperator")
internal inline fun resourcesImplementation(): Implementation {
return Implementation(T::class.java, Scope.RESOURCE_FILE_SCOPE)
}
/** Removes a given [node] as a fix. */
internal fun LintFix.Builder.removeNode(
context: JavaContext,
node: PsiElement,
name: String? = null,
autoFix: Boolean = true,
text: String = node.text,
): LintFix {
val fixName = name ?: "Remove '$text'"
return replace()
.name(fixName)
.range(context.getLocation(node))
.shortenNames()
.text(text)
.with("")
.apply {
if (autoFix) {
autoFix()
}
}
.build()
}
internal fun String.snakeToCamel(): String {
return buildString {
var capNext = false
var letterSeen = false
for (c in this@snakeToCamel) {
if (c == '_' || c == '-') {
capNext = letterSeen
continue
} else {
letterSeen = true
if (capNext) {
append(c.uppercaseChar())
capNext = false
} else {
append(c)
}
}
}
}
}
@Suppress("ComplexCondition")
internal fun String.toScreamingSnakeCase(): String {
return buildString {
var prevWasLower = false
var hasPendingUnderScore = false
var letterSeen = false
for (c in this@toScreamingSnakeCase) {
if (c == '_') {
if (letterSeen) {
hasPendingUnderScore = true
}
continue
} else if (c == '-' || c == '.' || c == ':' || c == '/') {
// Wild west characters in enum member names
// TODO maybe we should report these
if (letterSeen) {
hasPendingUnderScore = true
}
} else {
letterSeen = true
if (hasPendingUnderScore) {
append('_')
}
hasPendingUnderScore = false
if (c.isUpperCase()) {
if (prevWasLower) {
prevWasLower = false
append('_')
}
append(c)
} else {
prevWasLower = true
append(c.uppercaseChar())
}
}
}
}
}
/** List of platform types that Moshi's reflective adapter refuses. From ClassJsonAdapter. */
private val PLATFORM_PACKAGES =
setOf("android", "androidx", "java", "javax", "kotlin", "kotlinx", "scala")
private val BOXED_PRIMITIVES =
setOf(
TYPE_INTEGER_WRAPPER,
TYPE_BOOLEAN_WRAPPER,
TYPE_BYTE_WRAPPER,
TYPE_SHORT_WRAPPER,
TYPE_LONG_WRAPPER,
TYPE_DOUBLE_WRAPPER,
TYPE_FLOAT_WRAPPER,
TYPE_CHARACTER_WRAPPER,
)
internal fun PsiClass.isBoxedPrimitive(): Boolean {
val fqcn = qualifiedName ?: return false
return fqcn in BOXED_PRIMITIVES
}
internal fun PsiClass.isString(): Boolean {
val fqcn = qualifiedName ?: return false
return fqcn == "java.lang.String"
}
internal fun PsiClass.isObjectOrAny(): Boolean {
val fqcn = qualifiedName ?: return false
return fqcn == "java.lang.Object"
}
internal fun PsiClass.isPlatformType(): Boolean {
val fqcn = qualifiedName ?: return false
val firstPackagePart = fqcn.substringBefore(".")
return firstPackagePart in PLATFORM_PACKAGES
}
/**
* Given reference expressions, try to unwrap the simple name expression (useful if the reference is
* always the same type, like an enum).
*
* `Foo.BAR` -> BAR `BAR` -> BAR
*/
internal fun UExpression.unwrapSimpleNameReferenceExpression(): USimpleNameReferenceExpression {
return when (this) {
is USimpleNameReferenceExpression -> this
is UQualifiedReferenceExpression -> this.selector.unwrapSimpleNameReferenceExpression()
else -> error("Unrecognized reference expression type $javaClass")
}
}
/**
* Returns the fully qualified name of the expression, or null if unknown.
*
* For example, given:
* ```
* import org.x.Clazz.CONSTANT
* ...
* if (aVar == CONSTANT)
* ^^^^^^^^
* ```
*
* The qualified name of the underlined expression will be "org.x.Clazz.CONSTANT".
*/
internal fun UExpression.resolveQualifiedNameOrNull(): String? {
return (this as? UReferenceExpression)?.referenceNameElement?.uastParent?.tryResolve()?.let {
UastLintUtils.getQualifiedName(it)
}
}
/**
* Collects the return type of this [UMethod] in a suspend-safe way.
*
* For coroutines, the suspend methods return context rather than the source-declared return type,
* which is encoded in a continuation parameter at the end of the parameter list.
*
* For example, the following snippet:
* ```
* suspend fun foo(): String
* ```
*
* Will appear like so to lint:
* ```
* Object foo(Continuation super String> continuation)
* ```
*/
internal fun UMethod.safeReturnType(context: JavaContext): PsiType? {
if (language == KotlinLanguage.INSTANCE && context.evaluator.isSuspend(this)) {
val classReference = parameterList.parameters.lastOrNull()?.type as? PsiClassType ?: return null
val wildcard = classReference.parameters.singleOrNull() as? PsiWildcardType ?: return null
return wildcard.bound
} else {
return returnType
}
}
/** Loads a [StringOption] as a [delimiter]-delimited [Set] of strings. */
internal fun StringOption.loadAsSet(
configuration: Configuration,
delimiter: String = ",",
): Set {
return getValue(configuration)
?.splitToSequence(delimiter)
.orEmpty()
.map(String::trim)
.filter(String::isNotBlank)
.toSet()
}
internal inline fun Array.mapArray(transform: (T) -> R): Array =
Array(this.size) { i -> transform(this[i]) }
internal inline fun measureTimeMillisWithResult(block: () -> T): Pair {
val start = System.currentTimeMillis()
val result = block()
return Pair(System.currentTimeMillis() - start, result)
}
private val logVerbosely by lazy {
System.getProperty("slack.lint.logVerbosely", "false").toBoolean()
}
private val logErrorsVerbosely by lazy {
System.getProperty("slack.lint.logErrorsVerbosely", "true").toBoolean()
}
/**
* Logs to std if [logVerbosely] is enabled. Useful for debugging and should not generally be
* enabled.
*/
internal fun slackLintLog(message: String) {
if (logVerbosely) {
println("SlackLint: $message")
}
}
/**
* Logs to std if [logErrorsVerbosely] is enabled. Important for errors that you don't necessarily
* want to fail the build
*/
internal fun slackLintErrorLog(message: String) {
if (logErrorsVerbosely) {
System.err.println("SlackLint: $message")
}
}
/** Returns whether [this] has [packageName] as its package name. */
internal fun PsiMethod.isInPackageName(packageName: PackageName): Boolean {
val actual = (containingFile as? PsiJavaFile)?.packageName
return packageName.javaPackageName == actual
}
/** Whether this [PsiMethod] returns Unit */
internal val PsiMethod.returnsUnit
get() = returnType.isVoidOrUnit
/**
* Whether this [PsiType] is `void` or [Unit]
*
* In Kotlin 1.6 some expressions now explicitly return [Unit] instead of just being [PsiType.VOID],
* so this returns whether this type is either.
*/
internal val PsiType?.isVoidOrUnit
get() = this == PsiTypes.voidType() || this?.canonicalText == "kotlin.Unit"