slack.lint.resources.MissingResourceImportAliasDetector.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 org.jetbrains.kotlin.psi.KtImportDirective
import org.jetbrains.uast.UElement
import org.jetbrains.uast.UImportStatement
import org.jetbrains.uast.USimpleNameReferenceExpression
import org.jetbrains.uast.getContainingUFile
import org.jetbrains.uast.getQualifiedParentOrThis
import slack.lint.resources.ImportAliasesLoader.IMPORT_ALIASES
import slack.lint.resources.model.RootIssueData
import slack.lint.util.sourceImplementation
/**
* Reports an error when an R class, other than the local module's, is imported without an import
* alias.
*/
class MissingResourceImportAliasDetector : Detector(), SourceCodeScanner {
private val fixes = mutableListOf()
private var rootIssueData: RootIssueData? = null
private lateinit var importAliases: Map
override fun beforeCheckRootProject(context: Context) {
super.beforeCheckRootProject(context)
importAliases = ImportAliasesLoader.loadImportAliases(context)
}
override fun afterCheckFile(context: Context) {
// Collect all the fixes and apply them to one issue on the import to avoid adding the import
// alias with a fix
// and leaving the R references still referencing the non-aliased R or vice versa.
rootIssueData?.let {
context.report(
ISSUE,
it.nameLocation,
"Use an import alias for R classes from other modules",
quickfixData =
fix()
.name("Add import alias")
// Apply the fixes in reverse so that the ranges/locations don't change.
.composite(*fixes.reversed().toTypedArray())
.autoFix(),
)
rootIssueData = null
fixes.clear()
}
}
override fun getApplicableUastTypes(): List> =
listOf(UImportStatement::class.java, USimpleNameReferenceExpression::class.java)
override fun createUastHandler(context: JavaContext): UElementHandler {
return object : UElementHandler() {
override fun visitImportStatement(node: UImportStatement) {
// Import alias is a Kotlin feature.
val importDirective = node.sourcePsi as? KtImportDirective ?: return
if (importDirective.importedName?.identifier == "R" && importDirective.aliasName == null) {
val packageName = context.project.`package`
val filePackageName = node.getContainingUFile()?.packageName
val importedFqName = importDirective.importedFqName
val parentImportedFqName = importedFqName?.parent()?.asString()
val isLocalResourceImport =
parentImportedFqName?.let {
if (packageName != null) it == packageName else filePackageName?.startsWith(it)
} ?: true
if (!isLocalResourceImport) {
val importedFqNameString = requireNotNull(importedFqName).asString()
val alias = importAliases[importedFqNameString]
if (alias != null) {
rootIssueData =
RootIssueData(
alias = alias,
nameLocation = context.getNameLocation(importDirective),
)
fixes.add(createImportLintFix(importDirective, importedFqNameString, alias))
} else {
context.report(
ISSUE,
context.getNameLocation(importDirective),
"Use an import alias for R classes from other modules",
)
}
}
}
}
private fun createImportLintFix(
importDirective: KtImportDirective,
importedFqNameString: String,
alias: String?,
): LintFix {
return fix()
.replace()
.range(context.getLocation(importDirective))
.text(importedFqNameString)
.with("$importedFqNameString as $alias")
.build()
}
override fun visitSimpleNameReferenceExpression(node: USimpleNameReferenceExpression) {
rootIssueData?.alias?.let {
if (
node.asSourceString() == "R" &&
// Make sure node is its own parent to safeguard against cases like
// Build.VERSION_CODES.R.
node.getQualifiedParentOrThis() == node
) {
fixes.add(createReferenceLintFix(node, it))
}
}
}
private fun createReferenceLintFix(
node: USimpleNameReferenceExpression,
alias: String,
): LintFix {
return fix().replace().range(context.getLocation(node)).with(alias).build()
}
}
}
companion object {
val ISSUE: Issue =
Issue.create(
"MissingResourceImportAlias",
"Missing import alias for R class.",
"""
Only the local module's R class is allowed to be imported without an alias. \
Add an import alias for this. For example, import slack.l10n.R as L10nR
""",
Category.CORRECTNESS,
6,
Severity.ERROR,
sourceImplementation(),
)
.setOptions(listOf(IMPORT_ALIASES))
}
}