Skip to content

Instantly share code, notes, and snippets.

@athkalia
Created February 15, 2020 12:02
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save athkalia/33073edd0ed61abfd3333f3338bde048 to your computer and use it in GitHub Desktop.
Save athkalia/33073edd0ed61abfd3333f3338bde048 to your computer and use it in GitHub Desktop.
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