All Downloads are FREE. Search and download functionalities are using the official Maven repository.

slack.lint.ui.ItemDecorationViewBindingDetector.kt Maven / Gradle / Ivy

The newest version!
// Copyright (C) 2024 Slack Technologies, LLC
// SPDX-License-Identifier: Apache-2.0
package slack.lint.ui

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.Severity
import com.android.tools.lint.detector.api.SourceCodeScanner
import com.intellij.psi.PsiClass
import com.intellij.psi.util.InheritanceUtil.isInheritor
import kotlin.jvm.java
import org.jetbrains.uast.UCallExpression
import org.jetbrains.uast.getContainingUClass
import slack.lint.util.implements
import slack.lint.util.sourceImplementation

/**
 * Lint detector that ensures `ItemDecoration` does not inflate a view. If the view contains
 * `TextView`, this textual information cannot be announced to screen readers by TalkBack.
 */
class ItemDecorationViewBindingDetector : Detector(), SourceCodeScanner {
  override fun getApplicableUastTypes() = listOf(UCallExpression::class.java)

  override fun createUastHandler(context: JavaContext): UElementHandler =
    object : UElementHandler() {

      override fun visitCallExpression(node: UCallExpression) {
        val containingClass = node.getContainingUClass() ?: return
        if (!containingClass.implements(ITEM_DECORATION_CLASS_NAME)) return

        val method = node.resolve() ?: return
        val methodClass = method.containingClass

        if (
          LAYOUT_INFLATER_METHOD_NAME == method.name && methodClass?.isViewBindingClass() == true
        ) {
          context.report(ISSUE, node, context.getNameLocation(node), ISSUE_DESCRIPTION)
        }
      }
    }

  private fun PsiClass.isViewBindingClass(): Boolean {
    return LAYOUT_INFLATER_PACKAGE_NAME == this.qualifiedName ||
      isInheritor(this, VIEW_BINDING_PACKAGE_NAME)
  }

  companion object {
    private const val ISSUE_ID = "InflationInItemDecoration"
    private const val ITEM_DECORATION_CLASS_NAME =
      "androidx.recyclerview.widget.RecyclerView.ItemDecoration"
    private const val LAYOUT_INFLATER_METHOD_NAME = "inflate"
    private const val LAYOUT_INFLATER_PACKAGE_NAME = "android.view.LayoutInflater"
    private const val VIEW_BINDING_PACKAGE_NAME = "androidx.viewbinding.ViewBinding"

    private const val ISSUE_BRIEF_DESCRIPTION = "Avoid inflating a view to display text"
    private const val ISSUE_DESCRIPTION =
      """
        ViewBinding should not be used in `ItemDecoration`. If an inflated view contains \
        meaningful textual information, it will not be visible to TalkBack. This means \
        that screen reader users will not be able to know what is on the screen.
      """

    val ISSUE =
      Issue.create(
        id = ISSUE_ID,
        briefDescription = ISSUE_BRIEF_DESCRIPTION,
        explanation = ISSUE_DESCRIPTION,
        category = Category.A11Y,
        priority = 10,
        severity = Severity.WARNING,
        implementation = sourceImplementation(),
      )
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy