• 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 == '@' && (isLinePrefix ||
152                     doc.startsWith("@param", i, true) ||
153                     doc.startsWith("@return", i, true))
154             ) {
155                 // Found it
156                 end = i
157                 break
158             } else if (c == '\n') {
159                 isLinePrefix = true
160             } else if (c != '*' && !Character.isWhitespace(c)) {
161                 isLinePrefix = false
162             }
163         }
164 
165         return doc.substring(begin, end)
166     }
167 
checkTodosnull168     private fun checkTodos(item: Item) {
169         if (item.documentation.contains("TODO:") || item.documentation.contains("TODO(")) {
170             reporter.report(Issues.TODO, item, "Documentation mentions 'TODO'")
171         }
172     }
173 
checkRequiresPermissionnull174     private fun checkRequiresPermission(method: MethodItem) {
175         val text = method.documentation
176 
177         val annotation = method.modifiers.findAnnotation("android.support.annotation.RequiresPermission")
178         if (annotation != null) {
179             for (attribute in annotation.attributes()) {
180                 var values: List<AnnotationAttributeValue>? = null
181                 when (attribute.name) {
182                     "value", "allOf", "anyOf" -> {
183                         values = attribute.leafValues()
184                     }
185                 }
186                 if (values == null || values.isEmpty()) {
187                     continue
188                 }
189 
190                 for (value in values) {
191                     // var perm = String.valueOf(value.value())
192                     var perm = value.toSource()
193                     if (perm.indexOf('.') >= 0) perm = perm.substring(perm.lastIndexOf('.') + 1)
194                     if (text.contains(perm)) {
195                         reporter.report(
196                             // Why is that a problem? Sometimes you want to describe
197                             // particular use cases.
198                             Issues.REQUIRES_PERMISSION, method, "Method '" + method.name() +
199                                 "' documentation mentions permissions already declared by @RequiresPermission"
200                         )
201                     }
202                 }
203             }
204         } else if (text.contains("android.Manifest.permission") || text.contains("android.permission.")) {
205             reporter.report(
206                 Issues.REQUIRES_PERMISSION, method, "Method '" + method.name() +
207                     "' documentation mentions permissions without declaring @RequiresPermission"
208             )
209         }
210     }
211 
checkIntentActionnull212     private fun checkIntentAction(field: FieldItem) {
213         // Intent rules don't apply to support library
214         if (field.containingClass().qualifiedName().startsWith("android.support.")) {
215             return
216         }
217 
218         val hasBehavior = field.modifiers.findAnnotation("android.annotation.BroadcastBehavior") != null
219         val hasSdkConstant = field.modifiers.findAnnotation("android.annotation.SdkConstant") != null
220 
221         val text = field.documentation
222 
223         if (text.contains("Broadcast Action:") ||
224             text.contains("protected intent") && text.contains("system")
225         ) {
226             if (!hasBehavior) {
227                 reporter.report(
228                     Issues.BROADCAST_BEHAVIOR, field,
229                     "Field '" + field.name() + "' is missing @BroadcastBehavior"
230                 )
231             }
232             if (!hasSdkConstant) {
233                 reporter.report(
234                     Issues.SDK_CONSTANT, field, "Field '" + field.name() +
235                         "' is missing @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)"
236                 )
237             }
238         }
239 
240         if (text.contains("Activity Action:")) {
241             if (!hasSdkConstant) {
242                 reporter.report(
243                     Issues.SDK_CONSTANT, field, "Field '" + field.name() +
244                         "' is missing @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)"
245                 )
246             }
247         }
248     }
249 
checkVariablenull250     private fun checkVariable(
251         item: Item,
252         tag: String?,
253         ident: String,
254         type: TypeItem?
255     ) {
256         type ?: return
257         if (type.toString() == "int" && constantPattern.matcher(getDocumentation(item, tag)).find()) {
258             var foundTypeDef = false
259             for (annotation in item.modifiers.annotations()) {
260                 val cls = annotation.resolve() ?: continue
261                 val modifiers = cls.modifiers
262                 if (modifiers.findAnnotation(SdkConstants.INT_DEF_ANNOTATION.oldName()) != null ||
263                     modifiers.findAnnotation(SdkConstants.INT_DEF_ANNOTATION.newName()) != null
264                 ) {
265                     // TODO: Check that all the constants listed in the documentation are included in the
266                     // annotation?
267                     foundTypeDef = true
268                     break
269                 }
270             }
271 
272             if (!foundTypeDef) {
273                 reporter.report(
274                     Issues.INT_DEF, item,
275                     // TODO: Include source code you can paste right into the code?
276                     "$ident documentation mentions constants without declaring an @IntDef"
277                 )
278             }
279         }
280 
281         if (nullPattern.matcher(getDocumentation(item, tag)).find() &&
282             !item.hasNullnessInfo()
283         ) {
284             reporter.report(
285                 Issues.NULLABLE, item,
286                 "$ident documentation mentions 'null' without declaring @NonNull or @Nullable"
287             )
288         }
289     }
290 
291     companion object {
292         val constantPattern: Pattern = Pattern.compile("[A-Z]{3,}_([A-Z]{3,}|\\*)")
293         @Suppress("SpellCheckingInspection")
294         val nullPattern: Pattern = Pattern.compile("\\bnull\\b")
295     }
296 }
297