/* * Copyright (C) 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.doclava; import java.util.ArrayList; import java.util.List; import java.util.regex.Pattern; public class AndroidLinter implements Linter { @Override public void lintField(FieldInfo field) { if (!shouldLint(field.containingClass())) return; lintCommon(field.position(), field.comment().tags()); for (TagInfo tag : field.comment().tags()) { String text = tag.text(); // Intent rules don't apply to support library if (field.containingClass().qualifiedName().startsWith("android.support.")) continue; if (field.name().contains("ACTION")) { boolean hasBehavior = false; boolean hasSdkConstant = false; for (AnnotationInstanceInfo a : field.annotations()) { hasBehavior |= a.type().qualifiedNameMatches("android", "annotation.BroadcastBehavior"); hasSdkConstant |= a.type().qualifiedNameMatches("android", "annotation.SdkConstant"); } if (text.contains("Broadcast Action:") || (text.contains("protected intent") && text.contains("system"))) { if (!hasBehavior) { Errors.error(Errors.BROADCAST_BEHAVIOR, field, "Field '" + field.name() + "' is missing @BroadcastBehavior"); } if (!hasSdkConstant) { Errors.error(Errors.SDK_CONSTANT, field, "Field '" + field.name() + "' is missing @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)"); } } if (text.contains("Activity Action:")) { if (!hasSdkConstant) { Errors.error(Errors.SDK_CONSTANT, field, "Field '" + field.name() + "' is missing @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)"); } } } } } @Override public void lintMethod(MethodInfo method) { if (!shouldLint(method.containingClass())) return; lintCommon(method.position(), method.comment().tags()); lintCommon(method.position(), method.returnTags().tags()); for (TagInfo tag : method.comment().tags()) { String text = tag.text(); boolean hasAnnotation = false; for (AnnotationInstanceInfo a : method.annotations()) { if (a.type().qualifiedNameMatches("android", "annotation.RequiresPermission")) { hasAnnotation = true; ArrayList values = new ArrayList(); for (AnnotationValueInfo val : a.elementValues()) { switch (val.element().name()) { case "value": values.add(val); break; case "allOf": values = (ArrayList) val.value(); break; case "anyOf": values = (ArrayList) val.value(); break; } } for (AnnotationValueInfo value : values) { String perm = String.valueOf(value.value()); if (perm.indexOf('.') >= 0) perm = perm.substring(perm.lastIndexOf('.') + 1); if (text.contains(perm)) { Errors.error(Errors.REQUIRES_PERMISSION, method, "Method '" + method.name() + "' documentation mentions permissions already declared by @RequiresPermission"); } } } } if (text.contains("android.Manifest.permission") || text.contains("android.permission.")) { if (!hasAnnotation) { Errors.error(Errors.REQUIRES_PERMISSION, method, "Method '" + method.name() + "' documentation mentions permissions without declaring @RequiresPermission"); } } } lintVariable(method.position(), "Return value of '" + method.name() + "'", method.returnType(), method.annotations(), method.returnTags().tags()); } @Override public void lintParameter(MethodInfo method, ParameterInfo param, SourcePositionInfo position, TagInfo tag) { if (!shouldLint(method.containingClass())) return; lintCommon(position, tag); lintVariable(position, "Parameter '" + param.name() + "' of '" + method.name() + "'", param.type(), param.annotations(), tag); } private static void lintVariable(SourcePositionInfo pos, String ident, TypeInfo type, List annotations, TagInfo... tags) { if (type == null) return; for (TagInfo tag : tags) { String text = tag.text(); if (type.simpleTypeName().equals("int") && Pattern.compile("[A-Z]{3,}_([A-Z]{3,}|\\*)").matcher(text).find()) { boolean hasAnnotation = false; for (AnnotationInstanceInfo a : annotations) { for (AnnotationInstanceInfo b : a.type().annotations()) { hasAnnotation |= b.type().qualifiedNameMatches("android", "annotation.IntDef"); } } if (!hasAnnotation) { Errors.error(Errors.INT_DEF, pos, ident + " documentation mentions constants without declaring an @IntDef"); } } if (Pattern.compile("\\bnull\\b").matcher(text).find()) { boolean hasAnnotation = false; for (AnnotationInstanceInfo a : annotations) { hasAnnotation |= a.type().qualifiedNameMatches("android", "annotation.NonNull"); hasAnnotation |= a.type().qualifiedNameMatches("android", "annotation.Nullable"); } if (!hasAnnotation) { Errors.error(Errors.NULLABLE, pos, ident + " documentation mentions 'null' without declaring @NonNull or @Nullable"); } } } } private static void lintCommon(SourcePositionInfo pos, TagInfo... tags) { for (TagInfo tag : tags) { String text = tag.text(); if (text.contains("TODO:") || text.contains("TODO(")) { Errors.error(Errors.TODO, pos, "Documentation mentions 'TODO'"); } } } private static boolean shouldLint(ClassInfo clazz) { return clazz.qualifiedName().startsWith("android.") && !clazz.qualifiedName().startsWith("android.icu."); } }