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