• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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