Skip to content

Commit

Permalink
Merge pull request #30 from abema/add-no-expiry-date-param
Browse files Browse the repository at this point in the history
Add no expiry date param
  • Loading branch information
momomomo111 authored Jun 26, 2023
2 parents 3e522a4 + d0044ae commit 46561be
Show file tree
Hide file tree
Showing 11 changed files with 440 additions and 91 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -399,15 +399,18 @@ fun awesomeFeatureEnabled(): Boolean
Then, in the operation stage, it can be implemented using `@FlagType.Ops` and OpsFlagSource as well.
If you implement `ExperimentFlagSource` and `OpsFlagSource`, you can use one flag management tool either.

Since `@FlagType.Ops` and `@FlagType.Permission` may be operated indefinitely, there is no need to set `expiryDate`.
`@FlagType.Ops` and `@FlagType.Permission` may sometimes have no expiry date, so please set them to `NO_EXPIRY_DATE` instead.

```kotlin
import tv.abema.flagfit.FlagfitExpansionParams.NO_EXPIRY_DATE

@BooleanFlag(
key = "awesome-feature",
defaultValue = false
)
@FlagType.Ops(
owner = "{GitHub UserId}",
expiryDate = NO_EXPIRY_DATE
)
fun awesomeFeatureEnabled(): Boolean
```
Expand Down
11 changes: 9 additions & 2 deletions flagfit-flagtype/src/main/java/tv/abema/flagfit/FlagType.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class FlagType {
*/
annotation class Ops(
val owner: String,
val expiryDate: String = "",
val expiryDate: String,
)

/**
Expand All @@ -40,7 +40,7 @@ class FlagType {
*/
annotation class Permission(
val owner: String,
val expiryDate: String = "",
val expiryDate: String,
)

class WorkInProgressAnnotationAdapter : AnnotationAdapter<WorkInProgress> {
Expand Down Expand Up @@ -112,6 +112,13 @@ class FlagType {
}

companion object {
const val EXPIRY_DATE_INFINITE = "EXPIRY_DATE_INFINITE"

@Deprecated("FlagType should have the actual owner set")
const val OWNER_NOT_DEFINED = "OWNER_NOT_DEFINED"

@Deprecated("FlagType should have the actual expiry date set")
const val EXPIRY_DATE_NOT_DEFINED = "EXPIRY_DATE_NOT_DEFINED"
fun annotationAdapters() = listOf(
WorkInProgressAnnotationAdapter(),
ExperimentAnnotationAdapter(),
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package tv.abema.flagfit

object AnnotationPackagePath {
const val PACKAGE_PATH_WIP = "tv.abema.flagfit.FlagType.WorkInProgress"
const val PACKAGE_PATH_EXPERIMENT = "tv.abema.flagfit.FlagType.Experiment"
const val PACKAGE_PATH_OPS = "tv.abema.flagfit.FlagType.Ops"
const val PACKAGE_PATH_PERMISSION = "tv.abema.flagfit.FlagType.Permission"
const val PACKAGE_PATH_BOOLEAN_FLAG = "tv.abema.flagfit.annotation.BooleanFlag"
const val PACKAGE_PATH_VARIATION_FLAG = "tv.abema.flagfit.annotation.VariationFlag"
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,27 +14,37 @@ import com.android.tools.lint.detector.api.SourceCodeScanner
import com.android.tools.lint.detector.api.StringOption
import org.jetbrains.uast.UElement
import org.jetbrains.uast.kotlin.KotlinUMethod
import tv.abema.flagfit.AnnotationPackagePath.PACKAGE_PATH_BOOLEAN_FLAG
import tv.abema.flagfit.AnnotationPackagePath.PACKAGE_PATH_EXPERIMENT
import tv.abema.flagfit.AnnotationPackagePath.PACKAGE_PATH_OPS
import tv.abema.flagfit.AnnotationPackagePath.PACKAGE_PATH_PERMISSION
import tv.abema.flagfit.AnnotationPackagePath.PACKAGE_PATH_VARIATION_FLAG
import tv.abema.flagfit.AnnotationPackagePath.PACKAGE_PATH_WIP
import tv.abema.flagfit.FlagType.Companion.EXPIRY_DATE_INFINITE
import tv.abema.flagfit.FlagType.Companion.EXPIRY_DATE_NOT_DEFINED
import tv.abema.flagfit.FlagType.Companion.OWNER_NOT_DEFINED
import java.time.LocalDate
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.time.format.DateTimeParseException
import java.util.EnumSet

class DeadlineExpiredDetector : Detector(), SourceCodeScanner {

override fun applicableAnnotations(): List<String> {
return listOf(
"tv.abema.flagfit.FlagType.WorkInProgress",
"tv.abema.flagfit.FlagType.Experiment",
"tv.abema.flagfit.FlagType.Ops",
"tv.abema.flagfit.FlagType.Permission",
PACKAGE_PATH_WIP,
PACKAGE_PATH_EXPERIMENT,
PACKAGE_PATH_OPS,
PACKAGE_PATH_PERMISSION,
)
}

override fun isApplicableAnnotationUsage(type: AnnotationUsageType): Boolean {
return type == AnnotationUsageType.DEFINITION || super.isApplicableAnnotationUsage(type)
}

@Suppress("NewApi", "UnstableApiUsage")
@Suppress("NewApi", "UnstableApiUsage", "DEPRECATION")
override fun visitAnnotationUsage(
context: JavaContext,
element: UElement,
Expand All @@ -50,33 +60,35 @@ class DeadlineExpiredDetector : Detector(), SourceCodeScanner {
withZone(ZoneId.of(timeZoneId))
}
}
val qualifiedName = annotationInfo.qualifiedName
val annotationAttributes = annotationInfo.annotation.attributeValues
val owner = (annotationAttributes.firstOrNull { it.name == "owner" }?.evaluate() as String?)
?: ""
val expiryDate = (annotationAttributes.firstOrNull { it.name == "expiryDate" }
?.evaluate() as String?) ?: ""
if (owner == "OWNER_NOT_DEFINED" || expiryDate == "EXPIRY_DATE_NOT_DEFINED") return
if (qualifiedName == "tv.abema.flagfit.FlagType.Ops" && expiryDate.isEmpty()
|| qualifiedName == "tv.abema.flagfit.FlagType.Permission" && expiryDate.isEmpty()) return
val location = context.getLocation(element)
if (expiryDate == EXPIRY_DATE_INFINITE) return
if (owner == OWNER_NOT_DEFINED || expiryDate == EXPIRY_DATE_NOT_DEFINED) return
val currentLocalDate = if (currentTime.isNullOrEmpty()) {
LocalDate.now()
} else {
LocalDate.parse(currentTime, dateTimeFormatter)
}
val expiryLocalDate = LocalDate.parse(expiryDate, dateTimeFormatter)
val expiryLocalDate = try {
LocalDate.parse(expiryDate, dateTimeFormatter)
} catch (ex: DateTimeParseException) {
return
}
val soonExpiryLocalDate = expiryLocalDate.minusDays(7)
val uastParent = (element.uastParent as? KotlinUMethod) ?: return
val methodName = uastParent.name
val key = uastParent.annotations
.first {
it.qualifiedName == "tv.abema.flagfit.annotation.BooleanFlag" ||
it.qualifiedName == "tv.abema.flagfit.annotation.VariationFlag"
it.qualifiedName == PACKAGE_PATH_BOOLEAN_FLAG ||
it.qualifiedName == PACKAGE_PATH_VARIATION_FLAG
}
.parameterList.attributes.first { it.name == "key" }.value?.text
if (currentLocalDate.isAfter(soonExpiryLocalDate)) {
val name = annotationInfo.qualifiedName.substringAfterLast('.')
val location = context.getLocation(element)
if (currentLocalDate.isAfter(expiryLocalDate)) {
val message = "The @FlagType.$name created by `owner: $owner` has expired!\n" +
"Please consider deleting `@FlagType.$name` as the expiration date has passed on $expiryDate.\n" +
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package tv.abema.flagfit

import com.android.tools.lint.detector.api.AnnotationInfo
import com.android.tools.lint.detector.api.AnnotationUsageInfo
import com.android.tools.lint.detector.api.AnnotationUsageType
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.Location
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 org.jetbrains.uast.UElement
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.time.format.DateTimeParseException
import java.util.EnumSet

class FlagTypeExpiryDateIllegalParamDetector : Detector(), SourceCodeScanner {

override fun applicableAnnotations(): List<String> {
return listOf(
AnnotationPackagePath.PACKAGE_PATH_WIP,
AnnotationPackagePath.PACKAGE_PATH_EXPERIMENT,
AnnotationPackagePath.PACKAGE_PATH_OPS,
AnnotationPackagePath.PACKAGE_PATH_PERMISSION,
)
}

override fun isApplicableAnnotationUsage(type: AnnotationUsageType): Boolean {
return type == AnnotationUsageType.DEFINITION || super.isApplicableAnnotationUsage(type)
}

@Suppress("NewApi")
override fun visitAnnotationUsage(
context: JavaContext,
element: UElement,
annotationInfo: AnnotationInfo,
usageInfo: AnnotationUsageInfo,
) {
val qualifiedName = annotationInfo.qualifiedName
val location = context.getLocation(element)
val annotationAttributes = annotationInfo.annotation.attributeValues
val expiryDate = (annotationAttributes.firstOrNull { it.name == "expiryDate" }
?.evaluate() as String?) ?: ""
if (expiryDate == FlagType.EXPIRY_DATE_INFINITE) {
reportInfiniteExpiryDateErrorIfNeeded(qualifiedName, context, element, location)
return
}
if (!isDateFormatValid(expiryDate)) {
val message = "The value of expireDate is not in the correct date format.\n" +
"Please set the expiration date in the following format: \"yyyy-mm-dd\""
context.report(ISSUE_ILLEGAL_DATE, element, location, message)
}
}

private fun reportInfiniteExpiryDateErrorIfNeeded(
qualifiedName: String,
context: JavaContext,
element: UElement,
location: Location,
) {
when (qualifiedName) {
AnnotationPackagePath.PACKAGE_PATH_WIP, AnnotationPackagePath.PACKAGE_PATH_EXPERIMENT -> {
val message = "`NO_EXPIRE_DATE` cannot be set for the expireDate of `@FlagType.WorkInProgress` and `@FlagType.Experiment`.\n" +
"Please set the expiration date in the following format: \"yyyy-mm-dd\""
context.report(ISSUE_ILLEGAL_NO_EXPIRE_PARAM, element, location, message)
}

AnnotationPackagePath.PACKAGE_PATH_OPS, AnnotationPackagePath.PACKAGE_PATH_PERMISSION -> {
// do nothing
}
}
}

private fun isDateFormatValid(dateString: String): Boolean {
val formatter = DateTimeFormatter.ISO_LOCAL_DATE
return try {
LocalDate.parse(dateString, formatter)
true
} catch (ex: DateTimeParseException) {
false
}
}

companion object {
val ISSUE_ILLEGAL_NO_EXPIRE_PARAM = Issue.create(
id = "FlagfitIllegalNoExpireParam",
briefDescription = "The argument of expireDate is illigal.",
explanation = "Do not set NO_EXPIRE_DATE for @FlagType.WorkInProgress and @FlagType.Experiment...",
category = Category.PRODUCTIVITY,
priority = 4,
severity = Severity.ERROR,
implementation = Implementation(
FlagTypeExpiryDateIllegalParamDetector::class.java,
EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES)
)
)
val ISSUE_ILLEGAL_DATE = Issue.create(
id = "FlagfitIllegalDate",
briefDescription = "The argument of expireDate is illigal.",
explanation = "The value of expireDate should be in the format \"yyyy-MM-dd\"",
category = Category.PRODUCTIVITY,
priority = 6,
severity = Severity.ERROR,
implementation = Implementation(
FlagTypeExpiryDateIllegalParamDetector::class.java,
EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES)
)
)
}
}
4 changes: 3 additions & 1 deletion flagfit-lint/src/main/java/tv/abema/flagfit/IssueRegistry.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ internal class IssueRegistry : IssueRegistry() {
override val issues: List<Issue>
get() = listOf(
DeadlineExpiredDetector.ISSUE_DEADLINE_EXPIRED,
DeadlineExpiredDetector.ISSUE_DEADLINE_SOON
DeadlineExpiredDetector.ISSUE_DEADLINE_SOON,
FlagTypeExpiryDateIllegalParamDetector.ISSUE_ILLEGAL_NO_EXPIRE_PARAM,
FlagTypeExpiryDateIllegalParamDetector.ISSUE_ILLEGAL_DATE
)
}
Loading

0 comments on commit 46561be

Please sign in to comment.