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