Home | History | Annotate | Download | only in doclava
      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
     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<>();
     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
    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 
    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 
    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 
    171   private static boolean shouldLint(ClassInfo clazz) {
    172     return clazz.qualifiedName().startsWith("android.")
    173         && !clazz.qualifiedName().startsWith("android.icu.");
    174   }
    175 }
    176