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