forked from robolectric/robolectric
/
ImplementsValidator.java
375 lines (324 loc) · 13.7 KB
/
ImplementsValidator.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
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
package org.robolectric.annotation.processing.validator;
import static org.robolectric.annotation.processing.validator.ImplementationValidator.METHODS_ALLOWED_TO_BE_PUBLIC;
import com.google.auto.common.AnnotationValues;
import com.sun.source.tree.ImportTree;
import com.sun.source.util.Trees;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeSet;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.TypeParameterElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
import javax.lang.model.util.Elements;
import javax.tools.Diagnostic.Kind;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.processing.DocumentedMethod;
import org.robolectric.annotation.processing.Helpers;
import org.robolectric.annotation.processing.RobolectricModel;
/**
* Validator that checks usages of {@link org.robolectric.annotation.Implements}.
*/
public class ImplementsValidator extends Validator {
public static final String IMPLEMENTS_CLASS = "org.robolectric.annotation.Implements";
public static final int MAX_SUPPORTED_ANDROID_SDK = 10000; // Now == Build.VERSION_CODES.O
public static final String STATIC_INITIALIZER_METHOD_NAME = "__staticInitializer__";
public static final String CONSTRUCTOR_METHOD_NAME = "__constructor__";
private final ProcessingEnvironment env;
private final SdkCheckMode sdkCheckMode;
private final SdkStore sdkStore;
/**
* Supported modes for validation of {@link Implementation} methods against SDKs.
*/
public enum SdkCheckMode {
OFF,
WARN,
ERROR
}
public ImplementsValidator(RobolectricModel.Builder modelBuilder, ProcessingEnvironment env,
SdkCheckMode sdkCheckMode, SdkStore sdkStore) {
super(modelBuilder, env, IMPLEMENTS_CLASS);
this.env = env;
this.sdkCheckMode = sdkCheckMode;
this.sdkStore = sdkStore;
}
private TypeElement getClassNameTypeElement(AnnotationValue cv) {
String className = Helpers.getAnnotationStringValue(cv);
return elements.getTypeElement(className.replace('$', '.'));
}
@Override
public Void visitType(TypeElement shadowType, Element parent) {
captureJavadoc(shadowType);
// inner class shadows must be static
if (shadowType.getEnclosingElement().getKind() == ElementKind.CLASS
&& !shadowType.getModifiers().contains(Modifier.STATIC)) {
error("inner shadow classes must be static");
}
// Don't import nested classes because some of them have the same name.
AnnotationMirror am = getCurrentAnnotation();
AnnotationValue av = Helpers.getAnnotationTypeMirrorValue(am, "value");
AnnotationValue cv = Helpers.getAnnotationTypeMirrorValue(am, "className");
AnnotationValue minSdkVal = Helpers.getAnnotationTypeMirrorValue(am, "minSdk");
int minSdk = minSdkVal == null ? -1 : Helpers.getAnnotationIntValue(minSdkVal);
AnnotationValue maxSdkVal = Helpers.getAnnotationTypeMirrorValue(am, "maxSdk");
int maxSdk = maxSdkVal == null ? -1 : Helpers.getAnnotationIntValue(maxSdkVal);
AnnotationValue shadowPickerValue =
Helpers.getAnnotationTypeMirrorValue(am, "shadowPicker");
TypeMirror shadowPickerTypeMirror = shadowPickerValue == null
? null
: Helpers.getAnnotationTypeMirrorValue(shadowPickerValue);
// This shadow doesn't apply to the current SDK. todo: check each SDK.
if (maxSdk != -1 && maxSdk < MAX_SUPPORTED_ANDROID_SDK) {
addShadowNotInSdk(shadowType, av, cv);
return null;
}
TypeElement actualType = null;
if (av == null) {
if (cv == null) {
error("@Implements: must specify <value> or <className>");
return null;
}
actualType = getClassNameTypeElement(cv);
if (actualType == null
&& !suppressWarnings(shadowType, "robolectric.internal.IgnoreMissingClass")) {
error("@Implements: could not resolve class <" + AnnotationValues.toString(cv) + '>', cv);
return null;
}
} else {
TypeMirror value = Helpers.getAnnotationTypeMirrorValue(av);
if (value == null) {
return null;
}
if (cv != null) {
error("@Implements: cannot specify both <value> and <className> attributes");
} else {
actualType = Helpers.getAnnotationTypeMirrorValue(types.asElement(value));
}
}
if (actualType == null) {
addShadowNotInSdk(shadowType, av, cv);
return null;
}
final List<? extends TypeParameterElement> typeTP = actualType.getTypeParameters();
final List<? extends TypeParameterElement> elemTP = shadowType.getTypeParameters();
if (!helpers.isSameParameterList(typeTP, elemTP)) {
StringBuilder message = new StringBuilder();
if (elemTP.isEmpty()) {
message.append("Shadow type is missing type parameters, expected <");
helpers.appendParameterList(message, actualType.getTypeParameters());
message.append('>');
} else if (typeTP.isEmpty()) {
message.append("Shadow type has type parameters but real type does not");
} else {
message.append(
"Shadow type must have same type parameters as its real counterpart: expected <");
helpers.appendParameterList(message, actualType.getTypeParameters());
message.append(">, was <");
helpers.appendParameterList(message, shadowType.getTypeParameters());
message.append('>');
}
messager.printMessage(Kind.ERROR, message, shadowType);
return null;
}
AnnotationValue looseSignaturesAttr =
Helpers.getAnnotationTypeMirrorValue(am, "looseSignatures");
boolean looseSignatures =
looseSignaturesAttr != null && (Boolean) looseSignaturesAttr.getValue();
validateShadowMethods(actualType, shadowType, minSdk, maxSdk, looseSignatures);
modelBuilder.addShadowType(shadowType, actualType,
shadowPickerTypeMirror == null
? null
: (TypeElement) types.asElement(shadowPickerTypeMirror));
return null;
}
private void addShadowNotInSdk(
TypeElement shadowType, AnnotationValue valueAttr, AnnotationValue classNameAttr) {
String sdkClassName;
if (valueAttr == null) {
sdkClassName = Helpers.getAnnotationStringValue(classNameAttr).replace('$', '.');
} else {
sdkClassName = Helpers.getAnnotationTypeMirrorValue(valueAttr).toString();
}
// there's no such type at the current SDK level, so just use strings...
// getQualifiedName() uses Outer.Inner and we want Outer$Inner, so:
String name = getClassFQName(shadowType);
modelBuilder.addExtraShadow(sdkClassName, name);
}
private static boolean suppressWarnings(Element element, String warningName) {
SuppressWarnings[] suppressWarnings = element.getAnnotationsByType(SuppressWarnings.class);
for (SuppressWarnings suppression : suppressWarnings) {
for (String name : suppression.value()) {
if (warningName.equals(name)) {
return true;
}
}
}
return false;
}
static String getClassFQName(TypeElement elem) {
StringBuilder name = new StringBuilder();
while (isClassy(elem.getEnclosingElement().getKind())) {
name.insert(0, "$" + elem.getSimpleName());
elem = (TypeElement) elem.getEnclosingElement();
}
name.insert(0, elem.getQualifiedName());
return name.toString();
}
private static boolean isClassy(ElementKind kind) {
return kind == ElementKind.CLASS || kind == ElementKind.INTERFACE;
}
private void validateShadowMethods(TypeElement sdkClassElem, TypeElement shadowClassElem,
int classMinSdk, int classMaxSdk, boolean looseSignatures) {
for (Element memberElement : ElementFilter.methodsIn(shadowClassElem.getEnclosedElements())) {
ExecutableElement methodElement = (ExecutableElement) memberElement;
// equals, hashCode, and toString are exempt, because of Robolectric's weird special behavior
if (METHODS_ALLOWED_TO_BE_PUBLIC.contains(methodElement.getSimpleName().toString())) {
continue;
}
verifySdkMethod(sdkClassElem, methodElement, classMinSdk, classMaxSdk, looseSignatures);
String methodName = methodElement.getSimpleName().toString();
if (methodName.equals(CONSTRUCTOR_METHOD_NAME)
|| methodName.equals(STATIC_INITIALIZER_METHOD_NAME)) {
Implementation implementation = memberElement.getAnnotation(Implementation.class);
if (implementation == null) {
messager.printMessage(
Kind.ERROR, "Shadow methods must be annotated @Implementation", methodElement);
}
}
}
}
private void verifySdkMethod(TypeElement sdkClassElem, ExecutableElement methodElement,
int classMinSdk, int classMaxSdk, boolean looseSignatures) {
if (sdkCheckMode == SdkCheckMode.OFF) {
return;
}
Implementation implementation = methodElement.getAnnotation(Implementation.class);
if (implementation != null) {
Kind kind = sdkCheckMode == SdkCheckMode.WARN
? Kind.WARNING
: Kind.ERROR;
Problems problems = new Problems(kind);
for (SdkStore.Sdk sdk : sdkStore.sdksMatching(implementation, classMinSdk, classMaxSdk)) {
String problem = sdk.verifyMethod(sdkClassElem, methodElement, looseSignatures);
if (problem != null) {
problems.add(problem, sdk.sdkInt);
}
}
if (problems.any()) {
problems.recount(messager, methodElement);
}
}
}
private void captureJavadoc(TypeElement elem) {
List<String> imports = new ArrayList<>();
try {
List<? extends ImportTree> importLines =
Trees.instance(env).getPath(elem).getCompilationUnit().getImports();
for (ImportTree importLine : importLines) {
imports.add(importLine.getQualifiedIdentifier().toString());
}
} catch (IllegalArgumentException e) {
// Trees relies on javac APIs and is not available in all annotation processing
// implementations
}
List<TypeElement> enclosedTypes = ElementFilter.typesIn(elem.getEnclosedElements());
for (TypeElement enclosedType : enclosedTypes) {
imports.add(enclosedType.getQualifiedName().toString());
}
Elements elementUtils = env.getElementUtils();
modelBuilder.documentType(elem, elementUtils.getDocComment(elem), imports);
for (Element memberElement : ElementFilter.methodsIn(elem.getEnclosedElements())) {
try {
ExecutableElement methodElement = (ExecutableElement) memberElement;
Implementation implementation = memberElement.getAnnotation(Implementation.class);
DocumentedMethod documentedMethod = new DocumentedMethod(memberElement.toString());
for (Modifier modifier : memberElement.getModifiers()) {
documentedMethod.modifiers.add(modifier.toString());
}
documentedMethod.isImplementation = implementation != null;
if (implementation != null) {
documentedMethod.minSdk = sdkOrNull(implementation.minSdk());
documentedMethod.maxSdk = sdkOrNull(implementation.maxSdk());
}
for (VariableElement variableElement : methodElement.getParameters()) {
documentedMethod.params.add(variableElement.toString());
}
documentedMethod.returnType = methodElement.getReturnType().toString();
for (TypeMirror typeMirror : methodElement.getThrownTypes()) {
documentedMethod.exceptions.add(typeMirror.toString());
}
String docMd = elementUtils.getDocComment(methodElement);
if (docMd != null) {
documentedMethod.setDocumentation(docMd);
}
modelBuilder.documentMethod(elem, documentedMethod);
} catch (Exception e) {
throw new RuntimeException(
"failed to capture javadoc for " + elem + "." + memberElement, e);
}
}
}
private Integer sdkOrNull(int sdk) {
return sdk == -1 ? null : sdk;
}
private static class Problems {
private final Kind kind;
private final Map<String, Set<Integer>> problems = new HashMap<>();
public Problems(Kind kind) {
this.kind = kind;
}
void add(String problem, int sdkInt) {
Set<Integer> sdks = problems.get(problem);
if (sdks == null) {
problems.put(problem, sdks = new TreeSet<>());
}
sdks.add(sdkInt);
}
boolean any() {
return !problems.isEmpty();
}
void recount(Messager messager, Element element) {
for (Entry<String, Set<Integer>> e : problems.entrySet()) {
String problem = e.getKey();
Set<Integer> sdks = e.getValue();
StringBuilder buf = new StringBuilder();
buf.append(problem)
.append(" for ")
.append(sdks.size() == 1 ? "SDK " : "SDKs ");
Integer previousSdk = null;
Integer lastSdk = null;
for (Integer sdk : sdks) {
if (previousSdk == null) {
buf.append(sdk);
} else {
if (previousSdk != sdk - 1) {
buf.append("-").append(previousSdk);
buf.append("/").append(sdk);
lastSdk = null;
} else {
lastSdk = sdk;
}
}
previousSdk = sdk;
}
if (lastSdk != null) {
buf.append("-").append(lastSdk);
}
messager.printMessage(kind, buf.toString(), element);
}
}
}
}