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