package permissions.dispatcher;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.JavaContext;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.intellij.psi.JavaElementVisitor;
import com.intellij.psi.PsiAnnotation;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiReference;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class NoCorrespondingNeedsPermissionDetector extends Detector
implements Detector.JavaPsiScanner {
public static final Issue ISSUE = Issue.create("NoCorrespondingNeedsPermission",
"The method annotated with @OnShowRationale has no corresponding @NeedsPermission method, and will therefore be ignored by PermissionsDispatcher",
"The @OnShowRationale method with a certain signature is internally connected to another method annotated with @NeedsPermission and the same annotation value. Please ensure that there is a @NeedsPermission method with matching annotation values for this method.",
Category.CORRECTNESS,
4,
Severity.ERROR,
new Implementation(NoCorrespondingNeedsPermissionDetector.class,
EnumSet.of(Scope.JAVA_FILE)));
static final Set<String> NEEDS_PERMISSION_NAME = new HashSet<String>() {{
add("NeedsPermission");
add("permissions.dispatcher.NeedsPermission");
}};
static final Set<String> ON_SHOW_RATIONALE_NAME = new HashSet<String>() {{
add("OnShowRationale");
add("permissions.dispatcher.OnShowRationale");
}};
public NoCorrespondingNeedsPermissionDetector() {
// No-op
}
@Override
public JavaElementVisitor createPsiVisitor(JavaContext context) {
return new AnnotationChecker(context);
}
@Override
public List<Class<? extends PsiElement>> getApplicablePsiTypes() {
return Collections.<Class<? extends PsiElement>>singletonList(PsiAnnotation.class);
}
static class AnnotationChecker extends JavaElementVisitor {
private final JavaContext context;
private final Set<PsiAnnotation> needsPermissionAnnotations;
private final Set<PsiAnnotation> onShowRationaleAnnotations;
private AnnotationChecker(JavaContext context) {
this.context = context;
needsPermissionAnnotations = new HashSet<PsiAnnotation>();
onShowRationaleAnnotations = new HashSet<PsiAnnotation>();
}
@Override
public void visitAnnotation(PsiAnnotation annotation) {
if (!context.isEnabled(ISSUE)) {
super.visitAnnotation(annotation);
return;
}
// Let's store NeedsPermission and OnShowRationale
String type = annotation.getQualifiedName();
if (NEEDS_PERMISSION_NAME.contains(type)) {
needsPermissionAnnotations.add(annotation);
} else if (ON_SHOW_RATIONALE_NAME.contains(type)) {
onShowRationaleAnnotations.add(annotation);
}
if (onShowRationaleAnnotations.size() <= 0) {
super.visitAnnotation(annotation);
return;
}
// If there is OnShowRationale, find corresponding NeedsPermission
boolean found = false;
for (PsiAnnotation onShowRationaleAnnotation : onShowRationaleAnnotations) {
for (PsiAnnotation needsPermissionAnnotation : needsPermissionAnnotations) {
if (hasSameNodes(onShowRationaleAnnotation.getReferences(),
needsPermissionAnnotation.getReferences())) {
found = true;
}
}
if (!found) {
context.report(ISSUE, context.getLocation(onShowRationaleAnnotation),
"Useless @OnShowRationale declaration");
}
}
super.visitAnnotation(annotation);
}
private boolean hasSameNodes(PsiReference[] first, PsiReference[] second) {
if (first.length != second.length) {
return false;
}
for (int i = 0, size = first.length; i < size; i++) {
if (!first[i].toString().equals(second[i].toString())) {
return false;
}
}
return true;
}
}
}