slack.lint.text.SpanMarkPointMissingMaskDetector.kt Maven / Gradle / Ivy
The newest version!
// Copyright (C) 2021 Slack Technologies, LLC
// SPDX-License-Identifier: Apache-2.0
package slack.lint.text
import com.android.tools.lint.client.api.UElementHandler
import com.android.tools.lint.detector.api.Category
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Issue
import com.android.tools.lint.detector.api.JavaContext
import com.android.tools.lint.detector.api.LintFix
import com.android.tools.lint.detector.api.Severity
import com.android.tools.lint.detector.api.SourceCodeScanner
import com.android.tools.lint.detector.api.UastLintUtils
import org.jetbrains.uast.UBinaryExpression
import org.jetbrains.uast.UExpression
import org.jetbrains.uast.UReferenceExpression
import org.jetbrains.uast.UastBinaryOperator
import org.jetbrains.uast.tryResolve
import slack.lint.text.SpanMarkPointMissingMaskDetector.Companion.ISSUE
import slack.lint.util.resolveQualifiedNameOrNull
import slack.lint.util.sourceImplementation
/** Checks for SpanMarkPointMissingMask. See [ISSUE]. */
class SpanMarkPointMissingMaskDetector : Detector(), SourceCodeScanner {
companion object {
val ISSUE =
Issue.create(
id = "SpanMarkPointMissingMask",
briefDescription =
"Check that Span flags use the bitwise mask SPAN_POINT_MARK_MASK when being compared to.",
explanation =
"""
Spans flags can have priority or other bits set. \
Ensure that Span flags are checked using \
`currentFlag and Spanned.SPAN_POINT_MARK_MASK == desiredFlag` \
rather than just `currentFlag == desiredFlag`
""",
category = Category.CORRECTNESS,
priority = 4,
severity = Severity.ERROR,
implementation = sourceImplementation(),
)
}
override fun getApplicableUastTypes() = listOf(UBinaryExpression::class.java)
override fun createUastHandler(context: JavaContext): UElementHandler = ReportingHandler(context)
}
private const val SPANNED_CLASS = "android.text.Spanned"
private val MARK_POINT_FIELDS =
setOf(
"$SPANNED_CLASS.SPAN_INCLUSIVE_INCLUSIVE",
"$SPANNED_CLASS.SPAN_INCLUSIVE_EXCLUSIVE",
"$SPANNED_CLASS.SPAN_EXCLUSIVE_INCLUSIVE",
"$SPANNED_CLASS.SPAN_EXCLUSIVE_EXCLUSIVE",
)
private const val MASK_CLASS = "$SPANNED_CLASS.SPAN_POINT_MARK_MASK"
/** Reports violations of SpanMarkPointMissingMask. */
private class ReportingHandler(private val context: JavaContext) : UElementHandler() {
override fun visitBinaryExpression(node: UBinaryExpression) {
if (
node.operator == UastBinaryOperator.EQUALS ||
node.operator == UastBinaryOperator.NOT_EQUALS ||
node.operator == UastBinaryOperator.IDENTITY_EQUALS ||
node.operator == UastBinaryOperator.IDENTITY_NOT_EQUALS
) {
checkExpressions(node, node.leftOperand, node.rightOperand)
checkExpressions(node, node.rightOperand, node.leftOperand)
}
}
private fun checkExpressions(
node: UBinaryExpression,
markPointCheck: UExpression,
maskCheck: UExpression,
) {
if (markPointCheck.isMarkPointFieldName() && !maskCheck.isMaskClass()) {
context.report(
ISSUE,
context.getLocation(node),
"""
Do not check against ${markPointCheck.sourcePsi?.text} directly. \
Instead mask flag with Spanned.SPAN_POINT_MARK_MASK to only check MARK_POINT flags.
"""
.trimIndent(),
LintFix.create()
.replace()
.name("Use bitwise mask")
.text(maskCheck.sourcePsi?.text)
.with("((${maskCheck.sourcePsi?.text}) and $MASK_CLASS)")
.build(),
)
}
}
}
private fun UExpression.isMarkPointFieldName(): Boolean =
this.getQualifiedName() in MARK_POINT_FIELDS
private fun UExpression.getQualifiedName(): String? {
return (this as? UReferenceExpression)
?.referenceNameElement
?.uastParent
?.tryResolve()
?.let(UastLintUtils::getQualifiedName)
}
private fun UExpression.isMaskClass(): Boolean {
return if (this is UBinaryExpression) {
this.leftOperand.resolveQualifiedNameOrNull() == MASK_CLASS ||
this.rightOperand.resolveQualifiedNameOrNull() == MASK_CLASS
} else {
false
}
}