1 /* 2 * Copyright 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 @file:Suppress("UnstableApiUsage") 18 19 package androidx.work.lint 20 21 import com.android.tools.lint.detector.api.Category 22 import com.android.tools.lint.detector.api.Detector 23 import com.android.tools.lint.detector.api.Implementation 24 import com.android.tools.lint.detector.api.Issue 25 import com.android.tools.lint.detector.api.JavaContext 26 import com.android.tools.lint.detector.api.Scope 27 import com.android.tools.lint.detector.api.Severity 28 import com.android.tools.lint.detector.api.SourceCodeScanner 29 import com.intellij.psi.PsiMethod 30 import java.util.EnumSet 31 import java.util.concurrent.TimeUnit 32 import org.jetbrains.kotlin.name.ClassId 33 import org.jetbrains.kotlin.name.Name 34 import org.jetbrains.uast.UCallExpression 35 import org.jetbrains.uast.UQualifiedReferenceExpression 36 import org.jetbrains.uast.getParameterForArgument 37 import org.jetbrains.uast.skipParenthesizedExprDown 38 39 /** Ensures a valid interval duration for a `PeriodicWorkRequest`. */ 40 class InvalidPeriodicWorkRequestIntervalDetector : Detector(), SourceCodeScanner { 41 companion object { 42 val ISSUE = 43 Issue.create( 44 id = "InvalidPeriodicWorkRequestInterval", 45 briefDescription = "Invalid interval duration", 46 explanation = 47 """ 48 The interval duration for a `PeriodicWorkRequest` must be at least 15 minutes. 49 """, 50 androidSpecific = true, 51 category = Category.CORRECTNESS, 52 severity = Severity.FATAL, 53 implementation = 54 Implementation( 55 InvalidPeriodicWorkRequestIntervalDetector::class.java, 56 EnumSet.of(Scope.JAVA_FILE) 57 ) 58 ) 59 } 60 getApplicableConstructorTypesnull61 override fun getApplicableConstructorTypes() = 62 listOf("androidx.work.PeriodicWorkRequest.Builder") 63 64 @Suppress("UNCHECKED_CAST") 65 override fun visitConstructor( 66 context: JavaContext, 67 node: UCallExpression, 68 constructor: PsiMethod 69 ) { 70 if (node.valueArgumentCount >= 2) { 71 // TestMode.PARENTHESIZED wraps Duration call in parenthesizes 72 val repeatInterval = 73 node.valueArguments 74 .find { node.getParameterForArgument(it)?.name == "repeatInterval" } 75 ?.skipParenthesizedExprDown() 76 77 val timeUnit = 78 node.valueArguments 79 .find { node.getParameterForArgument(it)?.name == "repeatIntervalTimeUnit" } 80 ?.skipParenthesizedExprDown() 81 82 val type = repeatInterval?.getExpressionType()?.canonicalText 83 if ("long" == type) { 84 val value = repeatInterval.evaluate() as? Long 85 // TimeUnit 86 val units = timeUnit?.evaluate() as? Pair<ClassId, Name> 87 if (value != null && units != null) { 88 val (_, timeUnitType) = units 89 val interval: Long? = 90 when (timeUnitType.identifier) { 91 "NANOSECONDS" -> TimeUnit.MINUTES.convert(value, TimeUnit.NANOSECONDS) 92 "MICROSECONDS" -> TimeUnit.MINUTES.convert(value, TimeUnit.MICROSECONDS) 93 "MILLISECONDS" -> TimeUnit.MINUTES.convert(value, TimeUnit.MILLISECONDS) 94 "SECONDS" -> TimeUnit.MINUTES.convert(value, TimeUnit.SECONDS) 95 "MINUTES" -> value 96 "HOURS" -> TimeUnit.MINUTES.convert(value, TimeUnit.HOURS) 97 "DAYS" -> TimeUnit.MINUTES.convert(value, TimeUnit.DAYS) 98 else -> null 99 } 100 if (interval != null && interval < 15) { 101 context.report( 102 ISSUE, 103 context.getLocation(node), 104 """ 105 Interval duration for `PeriodicWorkRequest`s must be at least 15 \ 106 minutes. 107 """ 108 .trimIndent() 109 ) 110 } 111 } 112 } else if ("java.time.Duration" == type) { 113 // Look for the most common Duration specification 114 // Example: Duration.ofMinutes(15) 115 116 val callExpression: UCallExpression? = 117 when (repeatInterval) { 118 // ofMinutes(...) 119 is UCallExpression -> repeatInterval 120 // Duration.ofMinutes(...) 121 is UQualifiedReferenceExpression -> 122 repeatInterval.selector as? UCallExpression 123 else -> null 124 } 125 val unit = callExpression?.methodName 126 val value = callExpression?.valueArguments?.firstOrNull()?.evaluate() as? Long 127 if (value != null) { 128 val interval: Long? = 129 when (unit) { 130 "ofNanos" -> TimeUnit.MINUTES.convert(value, TimeUnit.NANOSECONDS) 131 "ofMillis" -> TimeUnit.MINUTES.convert(value, TimeUnit.MILLISECONDS) 132 "ofSeconds" -> TimeUnit.MINUTES.convert(value, TimeUnit.SECONDS) 133 "ofMinutes" -> value 134 "ofHours" -> TimeUnit.MINUTES.convert(value, TimeUnit.HOURS) 135 "ofDays" -> TimeUnit.MINUTES.convert(value, TimeUnit.DAYS) 136 else -> null 137 } 138 if (interval != null && interval < 15) { 139 context.report( 140 ISSUE, 141 context.getLocation(node), 142 """ 143 Interval duration for `PeriodicWorkRequest`s must be at least 15 \ 144 minutes. 145 """ 146 .trimIndent() 147 ) 148 } 149 } 150 } 151 } 152 } 153 } 154