1 /* 2 * Copyright (C) 2017 Google Inc. 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.google.doclava; 18 19 import java.util.ArrayList; 20 import java.util.List; 21 import java.util.regex.Pattern; 22 23 public class AndroidLinter implements Linter { 24 @Override lintField(FieldInfo field)25 public void lintField(FieldInfo field) { 26 if (!shouldLint(field.containingClass())) return; 27 lintCommon(field.position(), field.comment().tags()); 28 29 for (TagInfo tag : field.comment().tags()) { 30 String text = tag.text(); 31 32 // Intent rules don't apply to support library 33 if (field.containingClass().qualifiedName().startsWith("android.support.")) continue; 34 35 if (field.name().contains("ACTION")) { 36 boolean hasBehavior = false; 37 boolean hasSdkConstant = false; 38 for (AnnotationInstanceInfo a : field.annotations()) { 39 hasBehavior |= a.type().qualifiedNameMatches("android", 40 "annotation.BroadcastBehavior"); 41 hasSdkConstant |= a.type().qualifiedNameMatches("android", 42 "annotation.SdkConstant"); 43 } 44 45 if (text.contains("Broadcast Action:") 46 || (text.contains("protected intent") && text.contains("system"))) { 47 if (!hasBehavior) { 48 Errors.error(Errors.BROADCAST_BEHAVIOR, field, 49 "Field '" + field.name() + "' is missing @BroadcastBehavior"); 50 } 51 if (!hasSdkConstant) { 52 Errors.error(Errors.SDK_CONSTANT, field, "Field '" + field.name() 53 + "' is missing @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)"); 54 } 55 } 56 57 if (text.contains("Activity Action:")) { 58 if (!hasSdkConstant) { 59 Errors.error(Errors.SDK_CONSTANT, field, "Field '" + field.name() 60 + "' is missing @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)"); 61 } 62 } 63 } 64 } 65 } 66 67 @Override lintMethod(MethodInfo method)68 public void lintMethod(MethodInfo method) { 69 if (!shouldLint(method.containingClass())) return; 70 lintCommon(method.position(), method.comment().tags()); 71 lintCommon(method.position(), method.returnTags().tags()); 72 73 for (TagInfo tag : method.comment().tags()) { 74 String text = tag.text(); 75 76 boolean hasAnnotation = false; 77 for (AnnotationInstanceInfo a : method.annotations()) { 78 if (a.type().qualifiedNameMatches("android", "annotation.RequiresPermission")) { 79 hasAnnotation = true; 80 ArrayList<AnnotationValueInfo> values = new ArrayList<>(); 81 for (AnnotationValueInfo val : a.elementValues()) { 82 switch (val.element().name()) { 83 case "value": 84 values.add(val); 85 break; 86 case "allOf": 87 values = (ArrayList<AnnotationValueInfo>) val.value(); 88 break; 89 case "anyOf": 90 values = (ArrayList<AnnotationValueInfo>) val.value(); 91 break; 92 } 93 } 94 if (values.isEmpty()) continue; 95 96 for (AnnotationValueInfo value : values) { 97 String perm = String.valueOf(value.value()); 98 if (perm.indexOf('.') >= 0) perm = perm.substring(perm.lastIndexOf('.') + 1); 99 if (text.contains(perm)) { 100 Errors.error(Errors.REQUIRES_PERMISSION, method, "Method '" + method.name() 101 + "' documentation mentions permissions already declared by @RequiresPermission"); 102 } 103 } 104 } 105 } 106 if (text.contains("android.Manifest.permission") || text.contains("android.permission.")) { 107 if (!hasAnnotation) { 108 Errors.error(Errors.REQUIRES_PERMISSION, method, "Method '" + method.name() 109 + "' documentation mentions permissions without declaring @RequiresPermission"); 110 } 111 } 112 } 113 114 lintVariable(method.position(), "Return value of '" + method.name() + "'", method.returnType(), 115 method.annotations(), method.returnTags().tags()); 116 } 117 118 @Override lintParameter(MethodInfo method, ParameterInfo param, SourcePositionInfo position, TagInfo tag)119 public void lintParameter(MethodInfo method, ParameterInfo param, SourcePositionInfo position, 120 TagInfo tag) { 121 if (!shouldLint(method.containingClass())) return; 122 lintCommon(position, tag); 123 124 lintVariable(position, "Parameter '" + param.name() + "' of '" + method.name() + "'", 125 param.type(), param.annotations(), tag); 126 } 127 lintVariable(SourcePositionInfo pos, String ident, TypeInfo type, List<AnnotationInstanceInfo> annotations, TagInfo... tags)128 private static void lintVariable(SourcePositionInfo pos, String ident, TypeInfo type, 129 List<AnnotationInstanceInfo> annotations, TagInfo... tags) { 130 if (type == null) return; 131 for (TagInfo tag : tags) { 132 String text = tag.text(); 133 134 if (type.simpleTypeName().equals("int") 135 && Pattern.compile("[A-Z]{3,}_([A-Z]{3,}|\\*)").matcher(text).find()) { 136 boolean hasAnnotation = false; 137 for (AnnotationInstanceInfo a : annotations) { 138 for (AnnotationInstanceInfo b : a.type().annotations()) { 139 hasAnnotation |= b.type().qualifiedNameMatches("android", "annotation.IntDef"); 140 } 141 } 142 if (!hasAnnotation) { 143 Errors.error(Errors.INT_DEF, pos, 144 ident + " documentation mentions constants without declaring an @IntDef"); 145 } 146 } 147 148 if (Pattern.compile("\\bnull\\b").matcher(text).find()) { 149 boolean hasAnnotation = false; 150 for (AnnotationInstanceInfo a : annotations) { 151 hasAnnotation |= a.type().qualifiedNameMatches("android", "annotation.NonNull"); 152 hasAnnotation |= a.type().qualifiedNameMatches("android", "annotation.Nullable"); 153 } 154 if (!hasAnnotation) { 155 Errors.error(Errors.NULLABLE, pos, 156 ident + " documentation mentions 'null' without declaring @NonNull or @Nullable"); 157 } 158 } 159 } 160 } 161 lintCommon(SourcePositionInfo pos, TagInfo... tags)162 private static void lintCommon(SourcePositionInfo pos, TagInfo... tags) { 163 for (TagInfo tag : tags) { 164 String text = tag.text(); 165 if (text.contains("TODO:") || text.contains("TODO(")) { 166 Errors.error(Errors.TODO, pos, "Documentation mentions 'TODO'"); 167 } 168 } 169 } 170 shouldLint(ClassInfo clazz)171 private static boolean shouldLint(ClassInfo clazz) { 172 return clazz.qualifiedName().startsWith("android.") 173 && !clazz.qualifiedName().startsWith("android.icu."); 174 } 175 } 176