1 /*
2  * Copyright (C) 2018 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 package com.android.tools.metalava
18 
19 import com.android.tools.metalava.model.ANDROIDX_INT_DEF
20 import com.android.tools.metalava.model.AnnotationAttributeValue
21 import com.android.tools.metalava.model.ClassItem
22 import com.android.tools.metalava.model.Codebase
23 import com.android.tools.metalava.model.FieldItem
24 import com.android.tools.metalava.model.Item
25 import com.android.tools.metalava.model.MethodItem
26 import com.android.tools.metalava.model.ParameterItem
27 import com.android.tools.metalava.model.TypeItem
28 import com.android.tools.metalava.model.visitors.ApiVisitor
29 import com.android.tools.metalava.reporter.Issues
30 import com.android.tools.metalava.reporter.Reporter
31 import java.util.regex.Pattern
32 
33 /** Misc API suggestions */
34 class AndroidApiChecks(val reporter: Reporter) {
checknull35     fun check(codebase: Codebase) {
36         codebase.accept(
37             object :
38                 ApiVisitor(
39                     // Sort by source order such that warnings follow source line number order
40                     methodComparator = MethodItem.sourceOrderComparator,
41                     config = @Suppress("DEPRECATION") options.apiVisitorConfig,
42                 ) {
43                 override fun skip(item: Item): Boolean {
44                     // Limit the checks to the android.* namespace (except for ICU)
45                     if (item is ClassItem) {
46                         val name = item.qualifiedName()
47                         return !(name.startsWith("android.") && !name.startsWith("android.icu."))
48                     }
49                     return super.skip(item)
50                 }
51 
52                 override fun visitItem(item: Item) {
53                     checkTodos(item)
54                 }
55 
56                 override fun visitMethod(method: MethodItem) {
57                     checkRequiresPermission(method)
58                     if (!method.isConstructor()) {
59                         checkVariable(
60                             method,
61                             "@return",
62                             "Return value of '" + method.name() + "'",
63                             method.returnType()
64                         )
65                     }
66                 }
67 
68                 override fun visitField(field: FieldItem) {
69                     if (field.name().contains("ACTION")) {
70                         checkIntentAction(field)
71                     }
72                     checkVariable(field, null, "Field '" + field.name() + "'", field.type())
73                 }
74 
75                 override fun visitParameter(parameter: ParameterItem) {
76                     checkVariable(
77                         parameter,
78                         parameter.name(),
79                         "Parameter '" +
80                             parameter.name() +
81                             "' of '" +
82                             parameter.containingMethod().name() +
83                             "'",
84                         parameter.type()
85                     )
86                 }
87             }
88         )
89     }
90 
91     private var cachedDocumentation: String = ""
92     private var cachedDocumentationItem: Item? = null
93     private var cachedDocumentationTag: String? = null
94 
95     // Cache around findDocumentation
getDocumentationnull96     private fun getDocumentation(item: Item, tag: String?): String {
97         return if (item === cachedDocumentationItem && cachedDocumentationTag == tag) {
98             cachedDocumentation
99         } else {
100             cachedDocumentationItem = item
101             cachedDocumentationTag = tag
102             cachedDocumentation = findDocumentation(item, tag)
103             cachedDocumentation
104         }
105     }
106 
findDocumentationnull107     private fun findDocumentation(item: Item, tag: String?): String {
108         if (item is ParameterItem) {
109             return findDocumentation(item.containingMethod(), item.name())
110         }
111 
112         val doc = item.documentation
113         if (doc.isBlank()) {
114             return ""
115         }
116 
117         if (tag == null) {
118             return doc
119         }
120 
121         var begin: Int
122         if (tag == "@return") {
123             // return tag
124             begin = doc.indexOf("@return")
125         } else {
126             begin = 0
127             while (true) {
128                 begin = doc.indexOf(tag, begin)
129                 if (begin == -1) {
130                     return ""
131                 } else {
132                     // See if it's prefixed by @param
133                     // Scan backwards and allow whitespace and *
134                     var ok = false
135                     for (i in begin - 1 downTo 0) {
136                         val c = doc[i]
137                         if (c != '*' && !Character.isWhitespace(c)) {
138                             if (c == 'm' && doc.startsWith("@param", i - 5, true)) {
139                                 begin = i - 5
140                                 ok = true
141                             }
142                             break
143                         }
144                     }
145                     if (ok) {
146                         // found beginning
147                         break
148                     }
149                 }
150                 begin += tag.length
151             }
152         }
153 
154         if (begin == -1) {
155             return ""
156         }
157 
158         // Find end
159         // This is the first block tag on a new line
160         var isLinePrefix = false
161         var end = doc.length
162         for (i in begin + 1 until doc.length) {
163             val c = doc[i]
164 
165             if (
166                 c == '@' &&
167                     (isLinePrefix ||
168                         doc.startsWith("@param", i, true) ||
169                         doc.startsWith("@return", i, true))
170             ) {
171                 // Found it
172                 end = i
173                 break
174             } else if (c == '\n') {
175                 isLinePrefix = true
176             } else if (c != '*' && !Character.isWhitespace(c)) {
177                 isLinePrefix = false
178             }
179         }
180 
181         return doc.substring(begin, end)
182     }
183 
checkTodosnull184     private fun checkTodos(item: Item) {
185         if (item.documentation.contains("TODO:") || item.documentation.contains("TODO(")) {
186             reporter.report(Issues.TODO, item, "Documentation mentions 'TODO'")
187         }
188     }
189 
checkRequiresPermissionnull190     private fun checkRequiresPermission(method: MethodItem) {
191         val text = method.documentation
192 
193         val annotation = method.modifiers.findAnnotation("androidx.annotation.RequiresPermission")
194         if (annotation != null) {
195             for (attribute in annotation.attributes) {
196                 var values: List<AnnotationAttributeValue>? = null
197                 when (attribute.name) {
198                     "value",
199                     "allOf",
200                     "anyOf" -> {
201                         values = attribute.leafValues()
202                     }
203                 }
204                 if (values == null || values.isEmpty()) {
205                     continue
206                 }
207 
208                 for (value in values) {
209                     // var perm = String.valueOf(value.value())
210                     var perm = value.toSource()
211                     if (perm.indexOf('.') >= 0) perm = perm.substring(perm.lastIndexOf('.') + 1)
212                     if (text.contains(perm)) {
213                         reporter.report(
214                             // Why is that a problem? Sometimes you want to describe
215                             // particular use cases.
216                             Issues.REQUIRES_PERMISSION,
217                             method,
218                             "Method '" +
219                                 method.name() +
220                                 "' documentation mentions permissions already declared by @RequiresPermission"
221                         )
222                     }
223                 }
224             }
225         } else if (
226             text.contains("android.Manifest.permission") || text.contains("android.permission.")
227         ) {
228             reporter.report(
229                 Issues.REQUIRES_PERMISSION,
230                 method,
231                 "Method '" +
232                     method.name() +
233                     "' documentation mentions permissions without declaring @RequiresPermission"
234             )
235         }
236     }
237 
checkIntentActionnull238     private fun checkIntentAction(field: FieldItem) {
239         // Intent rules don't apply to support library
240         if (field.containingClass().qualifiedName().startsWith("android.support.")) {
241             return
242         }
243 
244         val hasBehavior =
245             field.modifiers.findAnnotation("android.annotation.BroadcastBehavior") != null
246         val hasSdkConstant =
247             field.modifiers.findAnnotation("android.annotation.SdkConstant") != null
248 
249         val text = field.documentation
250 
251         if (
252             text.contains("Broadcast Action:") ||
253                 text.contains("protected intent") && text.contains("system")
254         ) {
255             if (!hasBehavior) {
256                 reporter.report(
257                     Issues.BROADCAST_BEHAVIOR,
258                     field,
259                     "Field '" + field.name() + "' is missing @BroadcastBehavior"
260                 )
261             }
262             if (!hasSdkConstant) {
263                 reporter.report(
264                     Issues.SDK_CONSTANT,
265                     field,
266                     "Field '" +
267                         field.name() +
268                         "' is missing @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)"
269                 )
270             }
271         }
272 
273         if (text.contains("Activity Action:")) {
274             if (!hasSdkConstant) {
275                 reporter.report(
276                     Issues.SDK_CONSTANT,
277                     field,
278                     "Field '" +
279                         field.name() +
280                         "' is missing @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)"
281                 )
282             }
283         }
284     }
285 
checkVariablenull286     private fun checkVariable(item: Item, tag: String?, ident: String, type: TypeItem?) {
287         type ?: return
288         if (
289             type.toString() == "int" && constantPattern.matcher(getDocumentation(item, tag)).find()
290         ) {
291             var foundTypeDef = false
292             for (annotation in item.modifiers.annotations()) {
293                 val cls = annotation.resolve() ?: continue
294                 val modifiers = cls.modifiers
295                 if (modifiers.findAnnotation(ANDROIDX_INT_DEF) != null) {
296                     // TODO: Check that all the constants listed in the documentation are included
297                     // in the
298                     // annotation?
299                     foundTypeDef = true
300                     break
301                 }
302             }
303 
304             if (!foundTypeDef) {
305                 reporter.report(
306                     Issues.INT_DEF,
307                     item,
308                     // TODO: Include source code you can paste right into the code?
309                     "$ident documentation mentions constants without declaring an @IntDef"
310                 )
311             }
312         }
313 
314         if (
315             nullPattern.matcher(getDocumentation(item, tag)).find() &&
316                 item.type()?.modifiers?.isPlatformNullability == true
317         ) {
318             reporter.report(
319                 Issues.NULLABLE,
320                 item,
321                 "$ident documentation mentions 'null' without declaring @NonNull or @Nullable"
322             )
323         }
324     }
325 
326     companion object {
327         val constantPattern: Pattern = Pattern.compile("[A-Z]{3,}_([A-Z]{3,}|\\*)")
328         @Suppress("SpellCheckingInspection")
329         val nullPattern: Pattern = Pattern.compile("\\bnull\\b")
330     }
331 }
332