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

slack.lint.resources.FullyQualifiedResourceDetector.kt Maven / Gradle / Ivy

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

import com.android.tools.lint.client.api.UElementHandler
import com.android.tools.lint.detector.api.Category
import com.android.tools.lint.detector.api.Context
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.isKotlin
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtImportDirective
import org.jetbrains.uast.UElement
import org.jetbrains.uast.UQualifiedReferenceExpression
import slack.lint.resources.ImportAliasesLoader.IMPORT_ALIASES
import slack.lint.util.sourceImplementation

private const val FQN_ANDROID_R = "android.R"
private val WHITESPACE_REGEX = "\\s+".toRegex()

/** Reports an error when an R class is referenced using its fully qualified name. */
class FullyQualifiedResourceDetector : Detector(), SourceCodeScanner {
  private lateinit var importAliases: Map

  override fun beforeCheckRootProject(context: Context) {
    super.beforeCheckRootProject(context)

    importAliases = ImportAliasesLoader.loadImportAliases(context)
  }

  override fun getApplicableUastTypes(): List> =
    listOf(UQualifiedReferenceExpression::class.java)

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

      override fun visitQualifiedReferenceExpression(node: UQualifiedReferenceExpression) {
        // Import alias is a Kotlin feature.
        if (!isKotlin(node.lang)) return

        val qualifier = node.receiver.asSourceString()
        val normalized = qualifier.trim().replace(WHITESPACE_REGEX, "")
        if (normalized.endsWith(".R") && normalized != FQN_ANDROID_R) {
          val alias = importAliases[normalized]

          context.report(
            ISSUE,
            context.getNameLocation(node.receiver),
            if (alias != null) "Use $alias as an import alias instead"
            else "Use an import alias instead",
            quickfixData = createLintFix(alias, node, qualifier),
          )
        }
      }

      private fun createLintFix(
        alias: String?,
        node: UQualifiedReferenceExpression,
        qualifier: String,
      ): LintFix? {
        return if (alias != null) {
          val fixes =
            mutableListOf(
              fix().replace().range(context.getLocation(node.receiver)).with(alias).build()
            )

          // Alternative to ReplaceStringBuilder#imports since that one didn't work here.
          addImportIfMissing(qualifier, alias, fixes)

          fix()
            .name("Replace with import alias")
            .composite(*fixes.reversed().toTypedArray())
            .autoFix()
        } else {
          null
        }
      }

      private fun addImportIfMissing(
        qualifier: String,
        alias: String,
        issues: MutableList,
      ) {
        context.uastFile?.imports?.let { imports ->
          val importIsMissing =
            !imports.any {
              val importDirective = it.sourcePsi as? KtImportDirective

              val importedFqNameString = importDirective?.importedFqName?.asString()?.trim()
              qualifier == importedFqNameString && alias == importDirective.aliasName
            }

          if (importIsMissing) {
            if (imports.isNotEmpty()) {
              val lastImport = imports.last().sourcePsi as? KtImportDirective
              addImportAfterLastImport(lastImport, qualifier, alias, issues)
            } else {
              addImportAfterPackageName(qualifier, alias, issues)
            }
          }
        }
      }

      private fun addImportAfterLastImport(
        lastImport: KtImportDirective?,
        qualifier: String,
        alias: String,
        issues: MutableList,
      ) {
        if (lastImport != null) {
          issues.add(
            fix()
              .replace()
              .range(context.getLocation(lastImport))
              .with(lastImport.text + System.lineSeparator() + "import $qualifier as $alias")
              .build()
          )
        }
      }

      private fun addImportAfterPackageName(
        qualifier: String,
        alias: String,
        issues: MutableList,
      ) {
        (context.psiFile as? KtFile)?.packageDirective?.let { packageDirective ->
          issues.add(
            fix()
              .replace()
              .range(context.getLocation(packageDirective))
              .with(
                packageDirective.text +
                  System.lineSeparator() +
                  System.lineSeparator() +
                  "import $qualifier as $alias"
              )
              .build()
          )
        }
      }
    }
  }

  companion object {
    val ISSUE: Issue =
      Issue.create(
          "FullyQualifiedResource",
          "Resources should use an import alias instead of being fully qualified.",
          "Resources should use an import alias instead of being fully qualified. For example: \n" +
            "import slack.l10n.R as L10nR\n" +
            "...\n" +
            "...getString(L10nR.string.app_name)",
          Category.CORRECTNESS,
          6,
          Severity.ERROR,
          sourceImplementation(),
        )
        .setOptions(listOf(IMPORT_ALIASES))
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy