• 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.tools.metalava.model.ANDROIDX_INT_DEF
20 import com.android.tools.metalava.model.CallableItem
21 import com.android.tools.metalava.model.Codebase
22 import com.android.tools.metalava.model.FieldItem
23 import com.android.tools.metalava.model.Item
24 import com.android.tools.metalava.model.MethodItem
25 import com.android.tools.metalava.model.PackageItem
26 import com.android.tools.metalava.model.ParameterItem
27 import com.android.tools.metalava.model.SelectableItem
28 import com.android.tools.metalava.model.TypeItem
29 import com.android.tools.metalava.model.visitors.ApiVisitor
30 import com.android.tools.metalava.reporter.Issues
31 import com.android.tools.metalava.reporter.Reporter
32 import java.util.regex.Pattern
33 
34 /** Misc API suggestions */
35 class AndroidApiChecks(val reporter: Reporter) {
checknull36     fun check(codebase: Codebase) {
37         for (packageItem in codebase.getPackages().packages) {
38             // Get the package name with a trailing `.` to simplify prefix checking below. Without
39             // it the checks would have to check for `android` and `android.` separately.
40             val name = packageItem.qualifiedName() + "."
41 
42             // Limit the checks to the android.* namespace (except for ICU)
43             if (!name.startsWith("android.") || name.startsWith("android.icu.")) continue
44 
45             checkPackage(packageItem)
46         }
47     }
48 
checkPackagenull49     private fun checkPackage(packageItem: PackageItem) {
50         packageItem.accept(
51             object :
52                 ApiVisitor(
53                     apiPredicateConfig = @Suppress("DEPRECATION") options.apiPredicateConfig,
54                 ) {
55 
56                 override fun visitSelectableItem(item: SelectableItem) {
57                     // TODOs are only checked on [Item]s with documentation and [ParameterItem]s
58                     // do not have any. Documentation for parameters is stored within the containing
59                     // callable in @param sections.
60                     checkTodos(item)
61                 }
62 
63                 override fun visitCallable(callable: CallableItem) {
64                     checkRequiresPermission(callable)
65                 }
66 
67                 override fun visitMethod(method: MethodItem) {
68                     checkVariable(
69                         method,
70                         "@return",
71                         "Return value of '" + method.name() + "'",
72                         method.returnType()
73                     )
74                 }
75 
76                 override fun visitField(field: FieldItem) {
77                     if (field.name().contains("ACTION")) {
78                         checkIntentAction(field)
79                     }
80                     checkVariable(field, null, "Field '" + field.name() + "'", field.type())
81                 }
82 
83                 override fun visitParameter(parameter: ParameterItem) {
84                     checkVariable(
85                         parameter,
86                         parameter.name(),
87                         "Parameter '" +
88                             parameter.name() +
89                             "' of '" +
90                             parameter.containingCallable().name() +
91                             "'",
92                         parameter.type()
93                     )
94                 }
95             }
96         )
97     }
98 
99     private var cachedDocumentation: String = ""
100     private var cachedDocumentationItem: Item? = null
101     private var cachedDocumentationTag: String? = null
102 
103     // Cache around findDocumentation
getDocumentationnull104     private fun getDocumentation(item: Item, tag: String?): String {
105         return if (item === cachedDocumentationItem && cachedDocumentationTag == tag) {
106             cachedDocumentation
107         } else {
108             cachedDocumentationItem = item
109             cachedDocumentationTag = tag
110             cachedDocumentation = findDocumentation(item, tag)
111             cachedDocumentation
112         }
113     }
114 
findDocumentationnull115     private fun findDocumentation(item: Item, tag: String?): String {
116         if (item is ParameterItem) {
117             return findDocumentation(item.containingCallable(), item.name())
118         }
119 
120         val doc = item.documentation.text
121         if (doc.isBlank()) {
122             return ""
123         }
124 
125         if (tag == null) {
126             return doc
127         }
128 
129         var begin: Int
130         if (tag == "@return") {
131             // return tag
132             begin = doc.indexOf("@return")
133         } else {
134             begin = 0
135             while (true) {
136                 begin = doc.indexOf(tag, begin)
137                 if (begin == -1) {
138                     return ""
139                 } else {
140                     // See if it's prefixed by @param
141                     // Scan backwards and allow whitespace and *
142                     var ok = false
143                     for (i in begin - 1 downTo 0) {
144                         val c = doc[i]
145                         if (c != '*' && !Character.isWhitespace(c)) {
146                             if (c == 'm' && doc.startsWith("@param", i - 5, true)) {
147                                 begin = i - 5
148                                 ok = true
149                             }
150                             break
151                         }
152                     }
153                     if (ok) {
154                         // found beginning
155                         break
156                     }
157                 }
158                 begin += tag.length
159             }
160         }
161 
162         if (begin == -1) {
163             return ""
164         }
165 
166         // Find end
167         // This is the first block tag on a new line
168         var isLinePrefix = false
169         var end = doc.length
170         for (i in begin + 1 until doc.length) {
171             val c = doc[i]
172 
173             if (
174                 c == '@' &&
175                     (isLinePrefix ||
176                         doc.startsWith("@param", i, true) ||
177                         doc.startsWith("@return", i, true))
178             ) {
179                 // Found it
180                 end = i
181                 break
182             } else if (c == '\n') {
183                 isLinePrefix = true
184             } else if (c != '*' && !Character.isWhitespace(c)) {
185                 isLinePrefix = false
186             }
187         }
188 
189         return doc.substring(begin, end)
190     }
191 
checkTodosnull192     private fun checkTodos(item: Item) {
193         if (item.documentation.contains("TODO:") || item.documentation.contains("TODO(")) {
194             reporter.report(Issues.TODO, item, "Documentation mentions 'TODO'")
195         }
196     }
197 
checkRequiresPermissionnull198     private fun checkRequiresPermission(callable: CallableItem) {
199         val text = callable.documentation
200 
201         val annotation = callable.modifiers.findAnnotation("androidx.annotation.RequiresPermission")
202         if (annotation != null) {
203             var conditional = false
204             val permissions = mutableListOf<String>()
205             for (attribute in annotation.attributes) {
206                 when (attribute.name) {
207                     "value",
208                     "allOf",
209                     "anyOf" -> {
210                         attribute.leafValues().mapTo(permissions) { it.toSource() }
211                     }
212                     "conditional" -> {
213                         conditional = attribute.legacyValue.value() == true
214                     }
215                 }
216             }
217             for (item in permissions) {
218                 val perm = item.substringAfterLast('.')
219                 // Search for the permission name as a whole word.
220                 val regex = Regex("""\b\Q$perm\E\b""")
221                 val mentioned = text.contains(regex)
222                 if (mentioned && !conditional) {
223                     reporter.report(
224                         Issues.REQUIRES_PERMISSION,
225                         callable,
226                         "Method '${callable.name()}' documentation duplicates auto-generated documentation by @RequiresPermission. If the permissions are only required under certain circumstances use conditional=true to suppress the auto-documentation"
227                     )
228                 } else if (!mentioned && conditional) {
229                     reporter.report(
230                         Issues.CONDITIONAL_REQUIRES_PERMISSION_NOT_EXPLAINED,
231                         callable,
232                         "Method '${callable.name()}' documentation does not explain when the conditional permission '$perm' is required."
233                     )
234                 }
235             }
236         } else if (
237             text.contains("android.Manifest.permission") || text.contains("android.permission.")
238         ) {
239             reporter.report(
240                 Issues.REQUIRES_PERMISSION,
241                 callable,
242                 "Method '" +
243                     callable.name() +
244                     "' documentation mentions permissions without declaring @RequiresPermission"
245             )
246         }
247     }
248 
checkIntentActionnull249     private fun checkIntentAction(field: FieldItem) {
250         // Intent rules don't apply to support library
251         if (field.containingClass().qualifiedName().startsWith("android.support.")) {
252             return
253         }
254 
255         val hasBehavior =
256             field.modifiers.findAnnotation("android.annotation.BroadcastBehavior") != null
257         val hasSdkConstant =
258             field.modifiers.findAnnotation("android.annotation.SdkConstant") != null
259 
260         val text = field.documentation
261 
262         if (
263             text.contains("Broadcast Action:") ||
264                 text.contains("protected intent") && text.contains("system")
265         ) {
266             if (!hasBehavior) {
267                 reporter.report(
268                     Issues.BROADCAST_BEHAVIOR,
269                     field,
270                     "Field '" + field.name() + "' is missing @BroadcastBehavior"
271                 )
272             }
273             if (!hasSdkConstant) {
274                 reporter.report(
275                     Issues.SDK_CONSTANT,
276                     field,
277                     "Field '" +
278                         field.name() +
279                         "' is missing @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)"
280                 )
281             }
282         }
283 
284         if (text.contains("Activity Action:")) {
285             if (!hasSdkConstant) {
286                 reporter.report(
287                     Issues.SDK_CONSTANT,
288                     field,
289                     "Field '" +
290                         field.name() +
291                         "' is missing @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)"
292                 )
293             }
294         }
295     }
296 
checkVariablenull297     private fun checkVariable(item: Item, tag: String?, ident: String, type: TypeItem?) {
298         type ?: return
299         if (
300             type.toString() == "int" && constantPattern.matcher(getDocumentation(item, tag)).find()
301         ) {
302             var foundTypeDef = false
303             for (annotation in item.modifiers.annotations()) {
304                 val cls = annotation.resolve() ?: continue
305                 val modifiers = cls.modifiers
306                 if (modifiers.findAnnotation(ANDROIDX_INT_DEF) != null) {
307                     // TODO: Check that all the constants listed in the documentation are included
308                     // in the
309                     // annotation?
310                     foundTypeDef = true
311                     break
312                 }
313             }
314 
315             if (!foundTypeDef) {
316                 reporter.report(
317                     Issues.INT_DEF,
318                     item,
319                     // TODO: Include source code you can paste right into the code?
320                     "$ident documentation mentions constants without declaring an @IntDef"
321                 )
322             }
323         }
324 
325         if (
326             nullPattern.matcher(getDocumentation(item, tag)).find() &&
327                 item.type()?.modifiers?.isPlatformNullability == true
328         ) {
329             reporter.report(
330                 Issues.NULLABLE,
331                 item,
332                 "$ident documentation mentions 'null' without declaring @NonNull or @Nullable"
333             )
334         }
335     }
336 
337     companion object {
338         val constantPattern: Pattern = Pattern.compile("[A-Z]{3,}_([A-Z]{3,}|\\*)")
339         @Suppress("SpellCheckingInspection")
340         val nullPattern: Pattern = Pattern.compile("\\bnull\\b")
341     }
342 }
343