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