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