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

slack.lint.ArgInFormattedQuantityStringResDetector.kt Maven / Gradle / Ivy

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

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.PsiLocalVariable
import com.intellij.psi.PsiMethod
import com.intellij.psi.PsiMethodCallExpression
import kotlin.reflect.full.safeCast
import org.jetbrains.uast.UCallExpression
import org.jetbrains.uast.UExpression
import org.jetbrains.uast.java.JavaUCallExpression
import org.jetbrains.uast.java.JavaUCompositeQualifiedExpression
import org.jetbrains.uast.kotlin.KotlinUFunctionCallExpression
import org.jetbrains.uast.kotlin.KotlinUQualifiedReferenceExpression
import org.jetbrains.uast.sourcePsiElement

class ArgInFormattedQuantityStringResDetector : Detector(), SourceCodeScanner {

  companion object {
    val ISSUE_ARG_IN_QUANTITY_STRING_FORMAT: Issue =
      Issue.create(
        "ArgInFormattedQuantityStringRes",
        "Count value in formatted string resource.",
        "Some languages require modifiers to counted values in written text. Consider consulting #plz-localization " +
          "if you are unsure if this formatted string requires a special modifier. If one is required, consider using " +
          "`LocalizationUtils.getFormattedCount()`. If not, suppress this warning.",
        Category.I18N,
        6,
        Severity.WARNING,
        Implementation(ArgInFormattedQuantityStringResDetector::class.java, Scope.JAVA_FILE_SCOPE),
      )

    val issues: List = listOf(ISSUE_ARG_IN_QUANTITY_STRING_FORMAT)
  }

  override fun getApplicableMethodNames(): List = listOf("getQuantityString")

  override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
    // Ignore methods that aren't in a subclass of Android "Resources"
    if (context.evaluator.isMemberInSubClassOf(method, "android.content.res.Resources", false)) {
      node.valueArguments
        .drop(2) // Ignore the first 2 arguments passed to "getQuantityString"
        .forEach { arg ->
          when (arg) {
            is JavaUCompositeQualifiedExpression ->
              checkCall { JavaUCallExpression::class.safeCast(arg.selector) }
            is JavaUCallExpression -> checkCall { arg }
            is KotlinUQualifiedReferenceExpression ->
              checkCall { KotlinUFunctionCallExpression::class.safeCast(arg.selector) }
            is KotlinUFunctionCallExpression -> checkCall { arg }
            else -> checkVariable { arg }
          }.let { countFormatFound ->
            if (!countFormatFound) {
              context.report(
                ISSUE_ARG_IN_QUANTITY_STRING_FORMAT,
                context.getLocation(arg),
                "This may require a localized count modifier. If so, use `LocalizationUtils.getFormattedCount()`. Consult #plz-localization if you are unsure.",
              )
            }
          }
        }
    }

    super.visitMethodCall(context, node, method)
  }

  /**
   * return true if the resolved [UCallExpression] has method name "getFormattedCount", false
   * otherwise
   */
  private fun checkCall(fn: () -> UCallExpression?): Boolean {
    return fn()?.let { call -> "getFormattedCount" == call.methodName } ?: false
  }

  /**
   * return true if the resolved [UExpression] was created from the "getFormattedCount" method,
   * false otherwise
   */
  private fun checkVariable(fn: () -> UExpression?): Boolean {
    return fn()?.let { exp ->
      val variable = exp.sourcePsiElement?.reference?.resolve() as? PsiLocalVariable
      val assignment = variable?.initializer as? PsiMethodCallExpression
      return assignment?.resolveMethod()?.name == "getFormattedCount"
    } ?: false
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy