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