slack.lint.inclusive.InclusiveNamingChecker.kt Maven / Gradle / Ivy
The newest version!
// Copyright (C) 2021 Slack Technologies, LLC
// SPDX-License-Identifier: Apache-2.0
@file:Suppress("UnstableApiUsage")
package slack.lint.inclusive
import com.android.tools.lint.detector.api.Category
import com.android.tools.lint.detector.api.Context
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.Location
import com.android.tools.lint.detector.api.Position
import com.android.tools.lint.detector.api.Severity
import com.android.tools.lint.detector.api.StringOption
import com.android.tools.lint.detector.api.TextFormat
import com.android.tools.lint.detector.api.XmlContext
import java.util.Locale
import org.jetbrains.uast.UElement
import org.w3c.dom.Node
import slack.lint.util.Priorities
import slack.lint.util.resourcesImplementation
import slack.lint.util.sourceImplementation
sealed class InclusiveNamingChecker {
companion object {
internal val BLOCK_LIST =
StringOption(
"block-list",
"A comma-separated list of words that should not be used in source code.",
null,
"This property should define a comma-separated list of words that should not be used in source code.",
)
private val SOURCE_ISSUE = sourceImplementation().toIssue()
private val RESOURCES_ISSUE =
resourcesImplementation().toIssue()
val ISSUES: List = listOf(SOURCE_ISSUE, RESOURCES_ISSUE)
private fun Implementation.toIssue(): Issue {
return Issue.create(
"InclusiveNaming",
"Use inclusive naming.",
"""
We try to use inclusive naming at Slack. Terms such as blacklist, whitelist, master, slave, etc, while maybe \
widely used today, can be socially charged and make others feel excluded or uncomfortable.
"""
.trimIndent(),
Category.CORRECTNESS,
Priorities.NORMAL,
Severity.ERROR,
this,
)
.setOptions(listOf(BLOCK_LIST))
}
/** Loads a comma-separated list of blocked words from the [BLOCK_LIST] option. */
fun loadBlocklist(context: Context): Set {
return BLOCK_LIST.getValue(context.configuration)
?.splitToSequence(",")
.orEmpty()
.map(String::trim)
.filter(String::isNotBlank)
.toSet()
}
}
abstract val context: C
abstract val blocklist: Set
protected abstract val issue: Issue
abstract fun locationFor(node: N): Location
open fun shouldReport(node: N, location: Location, name: String, isFile: Boolean): Boolean = true
fun check(node: N, name: String?, type: String, isFile: Boolean = false) {
if (name == null) return
val lowerCased = name.lowercase(Locale.US)
blocklist
.find { it in lowerCased }
?.let { matched ->
val location = locationFor(node)
if (!shouldReport(node, location, name, isFile)) return
val description = buildString {
append(issue.getBriefDescription(TextFormat.TEXT))
append(" Matched string is '")
append(matched)
append("' in ")
append(type)
append(" name '")
append(name)
append("'")
}
context.report(issue, location, description, null)
}
}
class SourceCodeChecker(override val context: JavaContext, override val blocklist: Set) :
InclusiveNamingChecker() {
/**
* Some element types will be reported multiple times (such as property parameters). This caches
* reports so we only report once.
*/
private val cachedReports = mutableSetOf()
override val issue: Issue = SOURCE_ISSUE
override fun locationFor(node: UElement): Location {
return context.getLocation(node)
}
override fun shouldReport(
node: UElement,
location: Location,
name: String,
isFile: Boolean,
): Boolean {
return cachedReports.add(CacheKey.fromLocation(location, isFile))
}
private data class CacheKey(val location: String) {
companion object {
fun fromLocation(location: Location, isFile: Boolean): CacheKey {
val fileName = location.file.name
val start: String
val end: String
if (isFile) {
start = ""
end = ""
} else {
start = location.start?.lineString ?: ""
end = location.end?.lineString ?: ""
}
return CacheKey("$fileName-$start-$end")
}
private val Position.lineString: String
get() {
return "$line:$column"
}
}
}
}
class XmlChecker(override val context: XmlContext, override val blocklist: Set) :
InclusiveNamingChecker() {
override val issue: Issue = RESOURCES_ISSUE
override fun locationFor(node: Node): Location {
return context.getLocation(node)
}
}
}