-
Notifications
You must be signed in to change notification settings - Fork 1.4k
/
CallNeedsPermissionDetector.java
155 lines (136 loc) · 5.79 KB
/
CallNeedsPermissionDetector.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
package permissions.dispatcher;
import com.android.tools.lint.client.api.UElementHandler;
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 org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.uast.UAnnotation;
import org.jetbrains.uast.UCallExpression;
import org.jetbrains.uast.UClass;
import org.jetbrains.uast.UElement;
import org.jetbrains.uast.UFile;
import org.jetbrains.uast.UMethod;
import org.jetbrains.uast.visitor.AbstractUastVisitor;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public final class CallNeedsPermissionDetector extends Detector implements Detector.UastScanner {
private static final String COLON = ":";
static final Issue ISSUE = Issue.create("CallNeedsPermission",
"Call the corresponding \"WithPermissionCheck\" method of the generated PermissionsDispatcher class instead",
"Directly invoking a method annotated with @NeedsPermission may lead to misleading behaviour on devices running Marshmallow and up. Therefore, it is advised to use the generated PermissionsDispatcher class instead, which provides a \"WithPermissionCheck\" method that safely handles runtime permissions.",
Category.CORRECTNESS,
7,
Severity.ERROR,
new Implementation(CallNeedsPermissionDetector.class, EnumSet.of(Scope.ALL_JAVA_FILES)));
private static Set<String> annotatedMethods = new HashSet<String>();
@Override
public List<Class<? extends UElement>> getApplicableUastTypes() {
return Collections.<Class<? extends UElement>>singletonList(UClass.class);
}
@Override
public UElementHandler createUastHandler(@NotNull final JavaContext context) {
return new UElementHandler() {
@Override
public void visitClass(@NotNull UClass node) {
node.accept(new AnnotationChecker(context));
}
};
}
private static class AnnotationChecker extends AbstractUastVisitor {
private final JavaContext context;
private boolean hasRuntimePermissionAnnotation;
private AnnotationChecker(JavaContext context) {
this.context = context;
}
@Override
public boolean visitAnnotation(@NotNull UAnnotation node) {
if (!"permissions.dispatcher.RuntimePermissions".equals(node.getQualifiedName())) {
return true;
}
hasRuntimePermissionAnnotation = true;
return true;
}
@Override
public boolean visitCallExpression(@NotNull UCallExpression node) {
if (isGeneratedFiles(context) || !hasRuntimePermissionAnnotation) {
return true;
}
if (node.getReceiver() == null && annotatedMethods.contains(methodIdentifier(node))) {
context.report(ISSUE, node, context.getLocation(node), "Trying to access permission-protected method directly");
}
return true;
}
/**
* Generate method identifier from method information.
*
* @param node UCallExpression
* @return className + methodName + parametersType
*/
@Nullable
private static String methodIdentifier(@NotNull UCallExpression node) {
UElement element = node.getUastParent();
while (element != null) {
if (element instanceof UClass) {
break;
}
element = element.getUastParent();
}
UClass uClass = (UClass) element;
if (uClass == null || node.getMethodName() == null) {
return null;
}
return uClass.getName() + COLON + node.getMethodName();
}
@Override
public boolean visitMethod(@NotNull UMethod node) {
if (isGeneratedFiles(context)) {
return super.visitMethod(node);
}
UAnnotation annotation = node.findAnnotation("permissions.dispatcher.NeedsPermission");
if (annotation == null) {
return super.visitMethod(node);
}
String methodIdentifier = methodIdentifier(node);
if (methodIdentifier == null) {
return super.visitMethod(node);
}
annotatedMethods.add(methodIdentifier);
return super.visitMethod(node);
}
/**
* Generate method identifier from method information.
*
* @param node UMethod
* @return className + methodName + parametersType
*/
@Nullable
private static String methodIdentifier(@NotNull UMethod node) {
UElement parent = node.getUastParent();
if (!(parent instanceof UClass)) {
return null;
}
UClass uClass = (UClass) parent;
return uClass.getName() + COLON + node.getName();
}
private static boolean isGeneratedFiles(JavaContext context) {
UFile sourceFile = context.getUastFile();
if (sourceFile == null) {
return false;
}
List<UClass> classes = sourceFile.getClasses();
if (classes.isEmpty()) {
return false;
}
String qualifiedName = classes.get(0).getName();
return qualifiedName != null && qualifiedName.contains("PermissionsDispatcher");
}
}
}