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
17package com.google.doclava;
18
19import java.util.ArrayList;
20import java.util.List;
21import java.util.regex.Pattern;
22
23public class AndroidLinter implements Linter {
24  @Override
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
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<AnnotationValueInfo>();
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          for (AnnotationValueInfo value : values) {
95            String perm = String.valueOf(value.value());
96            if (perm.indexOf('.') >= 0) perm = perm.substring(perm.lastIndexOf('.') + 1);
97            if (text.contains(perm)) {
98              Errors.error(Errors.REQUIRES_PERMISSION, method, "Method '" + method.name()
99                  + "' documentation mentions permissions already declared by @RequiresPermission");
100            }
101          }
102        }
103      }
104      if (text.contains("android.Manifest.permission") || text.contains("android.permission.")) {
105        if (!hasAnnotation) {
106          Errors.error(Errors.REQUIRES_PERMISSION, method, "Method '" + method.name()
107              + "' documentation mentions permissions without declaring @RequiresPermission");
108        }
109      }
110    }
111
112    lintVariable(method.position(), "Return value of '" + method.name() + "'", method.returnType(),
113        method.annotations(), method.returnTags().tags());
114  }
115
116  @Override
117  public void lintParameter(MethodInfo method, ParameterInfo param, SourcePositionInfo position,
118      TagInfo tag) {
119    if (!shouldLint(method.containingClass())) return;
120    lintCommon(position, tag);
121
122    lintVariable(position, "Parameter '" + param.name() + "' of '" + method.name() + "'",
123        param.type(), param.annotations(), tag);
124  }
125
126  private static void lintVariable(SourcePositionInfo pos, String ident, TypeInfo type,
127      List<AnnotationInstanceInfo> annotations, TagInfo... tags) {
128    if (type == null) return;
129    for (TagInfo tag : tags) {
130      String text = tag.text();
131
132      if (type.simpleTypeName().equals("int")
133          && Pattern.compile("[A-Z]{3,}_([A-Z]{3,}|\\*)").matcher(text).find()) {
134        boolean hasAnnotation = false;
135        for (AnnotationInstanceInfo a : annotations) {
136          for (AnnotationInstanceInfo b : a.type().annotations()) {
137            hasAnnotation |= b.type().qualifiedNameMatches("android", "annotation.IntDef");
138          }
139        }
140        if (!hasAnnotation) {
141          Errors.error(Errors.INT_DEF, pos,
142              ident + " documentation mentions constants without declaring an @IntDef");
143        }
144      }
145
146      if (Pattern.compile("\\bnull\\b").matcher(text).find()) {
147        boolean hasAnnotation = false;
148        for (AnnotationInstanceInfo a : annotations) {
149          hasAnnotation |= a.type().qualifiedNameMatches("android", "annotation.NonNull");
150          hasAnnotation |= a.type().qualifiedNameMatches("android", "annotation.Nullable");
151        }
152        if (!hasAnnotation) {
153          Errors.error(Errors.NULLABLE, pos,
154              ident + " documentation mentions 'null' without declaring @NonNull or @Nullable");
155        }
156      }
157    }
158  }
159
160  private static void lintCommon(SourcePositionInfo pos, TagInfo... tags) {
161    for (TagInfo tag : tags) {
162      String text = tag.text();
163      if (text.contains("TODO:") || text.contains("TODO(")) {
164        Errors.error(Errors.TODO, pos, "Documentation mentions 'TODO'");
165      }
166    }
167  }
168
169  private static boolean shouldLint(ClassInfo clazz) {
170    return clazz.qualifiedName().startsWith("android.")
171        && !clazz.qualifiedName().startsWith("android.icu.");
172  }
173}
174