Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
package com.babylon.checks.detectors
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.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.android.tools.lint.detector.api.TextFormat
import org.jetbrains.uast.UComment
import org.jetbrains.uast.UElement
import org.jetbrains.uast.UFile
import java.text.SimpleDateFormat
import java.util.Date
import java.util.EnumSet
import java.util.Locale
import java.util.concurrent.TimeUnit
/**
* Lint rule to detekt that all TODOs in the project have the correct format.
*/
class TodoFormatDetector : Detector(), SourceCodeScanner {
private val expiryDateFormatter = SimpleDateFormat("MMM-yyyy", Locale.US)
private val invalidSlackUserName = "Invalid or missing slack user name. " +
"Valid slack user names are $ALLOWED_SLACK_USER_NAMES. Please update the lint rule if your name is missing."
private val invalidTodoFormat = "Please update the TODO as per the TODO template."
private val invalidExpiryDate = "Invalid or missing expiry date. Valid expiry date format is MMM-YYYY"
private val multilineComment = "Todo is not allowed in a multiline comment. " +
"Add TODO using a single line comment and provide additional information as a separate comment."
private val futureExpiryDate = "Expiry date cannot be more than $FUTURE_TODO_THRESHOLD_IN_DAYS days old from today. " +
"Dream big dreams, but never forget that realistic short-term goals are the keys to our success."
companion object {
private const val FUTURE_TODO_THRESHOLD_IN_DAYS = 180
val ALLOWED_SLACK_USER_NAMES = listOf(
"@sakis"
)
val ISSUE: Issue = Issue.create(
id = "TodoFormat",
briefDescription = "Use the correct template for TODOs.",
explanation = "TODO template -> // TODO @slack_user_name MMM-YYYY <comment>.",
category = Category.CORRECTNESS,
priority = 1,
severity = Severity.ERROR,
implementation = Implementation(TodoFormatDetector::class.java, EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES))
)
}
override fun getApplicableUastTypes(): List<Class<out UElement>> = listOf(UFile::class.java)
override fun createUastHandler(context: JavaContext): UElementHandler? = object : UElementHandler() {
@Suppress("MagicNumber")
override fun visitFile(node: UFile) {
val errorList = mutableListOf<String>()
node.allCommentsInFile.forEach {
when {
it.text.startsWith("// TODO") -> {
if (isSlackUserNameInvalid(it, 2)) {
errorList.add(invalidSlackUserName)
}
if (isExpiryDateFormatInvalid(it, 3)) {
errorList.add(invalidExpiryDate)
} else if (!isExpiryDateInAllowedRange(it, 3)) {
errorList.add(futureExpiryDate)
}
}
it.text.startsWith("//TODO") -> {
if (isSlackUserNameInvalid(it, 1)) {
errorList.add(invalidSlackUserName)
}
if (isExpiryDateFormatInvalid(it, 2)) {
errorList.add(invalidExpiryDate)
} else if (!isExpiryDateInAllowedRange(it, 2)) {
errorList.add(futureExpiryDate)
}
}
it.text.contains("TODO") -> {
if (it.text.contains("/*")) {
errorList.add(multilineComment)
} else {
errorList.add(invalidTodoFormat)
}
}
}
if (errorList.isNotEmpty()) {
var errorMessage = "\nMore info:\n"
errorList.forEachIndexed { index, error ->
errorMessage = "$errorMessage ${index + 1}. $error\n"
}
report(context, errorMessage, it)
errorList.clear()
}
}
}
}
@Suppress("TooGenericExceptionCaught")
private fun isExpiryDateInAllowedRange(it: UComment, dateIndex: Int): Boolean {
val expiryDateString = it.text.split(" ").getOrNull(dateIndex)
val currentDateString = expiryDateFormatter.format(Date())
return try {
val expiryDate = expiryDateFormatter.parse(expiryDateString)
val currentDate = expiryDateFormatter.parse(currentDateString)
val differenceInMillis = expiryDate.time - currentDate.time
when {
differenceInMillis > 0 -> {
val differenceInDays = TimeUnit.DAYS.convert(differenceInMillis, TimeUnit.MILLISECONDS)
differenceInDays < FUTURE_TODO_THRESHOLD_IN_DAYS
}
else -> true
}
} catch (e: Exception) {
false
}
}
private fun isExpiryDateFormatInvalid(it: UComment, dateIndex: Int): Boolean {
val expiryDateString = it.text.split(" ").getOrNull(dateIndex)
return try {
expiryDateFormatter.parse(expiryDateString)
false
} catch (e: java.lang.Exception) {
true
}
}
private fun isSlackUserNameInvalid(it: UComment, usernameIndex: Int): Boolean {
val slackUserName = it.text.split(" ").getOrNull(usernameIndex)
return slackUserName?.toLowerCase() !in ALLOWED_SLACK_USER_NAMES
}
private fun report(context: JavaContext?, moreInfo: String = "", node: UComment) {
context?.report(
issue = ISSUE,
scope = node,
location = context.getLocation(node),
message = ISSUE.getExplanation(TextFormat.TEXT) + moreInfo
)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment