1 /*
2  * Copyright 2019 The Android Open Source Project
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 android.processor.view.inspector;
18 
19 import static javax.tools.Diagnostic.Kind.ERROR;
20 
21 import androidx.annotation.NonNull;
22 
23 import com.squareup.javapoet.ClassName;
24 
25 import java.io.IOException;
26 import java.util.HashMap;
27 import java.util.Map;
28 import java.util.Optional;
29 import java.util.Set;
30 
31 import javax.annotation.processing.AbstractProcessor;
32 import javax.annotation.processing.RoundEnvironment;
33 import javax.annotation.processing.SupportedAnnotationTypes;
34 import javax.lang.model.SourceVersion;
35 import javax.lang.model.element.Element;
36 import javax.lang.model.element.ElementKind;
37 import javax.lang.model.element.Modifier;
38 import javax.lang.model.element.TypeElement;
39 import javax.lang.model.util.ElementFilter;
40 
41 /**
42  * An annotation processor for the platform inspectable annotations.
43  *
44  * It mostly delegates to {@link InspectablePropertyProcessor} and
45  * {@link InspectionCompanionGenerator}. This modular architecture allows the core generation code
46  * to be reused for comparable annotations outside the platform.
47  *
48  * @see android.view.inspector.InspectableProperty
49  */
50 @SupportedAnnotationTypes({PlatformInspectableProcessor.ANNOTATION_QUALIFIED_NAME})
51 public final class PlatformInspectableProcessor extends AbstractProcessor {
52     static final String ANNOTATION_QUALIFIED_NAME =
53             "android.view.inspector.InspectableProperty";
54 
55     @Override
getSupportedSourceVersion()56     public SourceVersion getSupportedSourceVersion() {
57         return SourceVersion.latest();
58     }
59 
60     @Override
process( @onNull Set<? extends TypeElement> annotations, @NonNull RoundEnvironment roundEnv)61     public boolean process(
62             @NonNull Set<? extends TypeElement> annotations,
63             @NonNull RoundEnvironment roundEnv) {
64         final Map<String, InspectableClassModel> modelMap = new HashMap<>();
65 
66         for (TypeElement annotation : annotations) {
67             if (annotation.getQualifiedName().contentEquals(ANNOTATION_QUALIFIED_NAME)) {
68                 processProperties(roundEnv.getElementsAnnotatedWith(annotation), modelMap);
69             } else {
70                 fail("Unexpected annotation type", annotation);
71             }
72         }
73 
74         final InspectionCompanionGenerator generator =
75                 new InspectionCompanionGenerator(processingEnv.getFiler(), getClass());
76 
77         for (InspectableClassModel model : modelMap.values()) {
78             try {
79                 generator.generate(model);
80             } catch (IOException ioException) {
81                 fail(String.format(
82                         "Unable to generate inspection companion for %s due to %s",
83                         model.getClassName().toString(),
84                         ioException.getMessage()));
85             }
86         }
87 
88         return true;
89     }
90 
91     /**
92      * Runs {@link PlatformInspectableProcessor} on a set of annotated elements.
93      *
94      * @param elements A set of annotated elements to process
95      * @param modelMap A map of qualified class names to class models to update
96      */
processProperties( @onNull Set<? extends Element> elements, @NonNull Map<String, InspectableClassModel> modelMap)97     private void processProperties(
98             @NonNull Set<? extends Element> elements,
99             @NonNull Map<String, InspectableClassModel> modelMap) {
100         final InspectablePropertyProcessor processor =
101                 new InspectablePropertyProcessor(ANNOTATION_QUALIFIED_NAME, processingEnv);
102 
103         for (Element element : elements) {
104             final Optional<TypeElement> classElement = enclosingClassElement(element);
105 
106             if (!classElement.isPresent()) {
107                 fail("Element not contained in a class", element);
108                 break;
109             }
110 
111             final Set<Modifier> classModifiers = classElement.get().getModifiers();
112 
113             if (classModifiers.contains(Modifier.PRIVATE)) {
114                 fail("Enclosing class cannot be private", element);
115             }
116 
117             final InspectableClassModel model = modelMap.computeIfAbsent(
118                     classElement.get().getQualifiedName().toString(),
119                     k -> {
120                         if (hasNestedInspectionCompanion(classElement.get())) {
121                             fail(
122                                     String.format(
123                                             "Class %s already has an inspection companion.",
124                                             classElement.get().getQualifiedName().toString()),
125                                     element);
126                         }
127                         return new InspectableClassModel(ClassName.get(classElement.get()));
128                     });
129 
130             processor.process(element, model);
131         }
132     }
133 
134     /**
135      * Determine if a class has a nested class named {@code InspectionCompanion}.
136      *
137      * @param typeElement A type element representing the class to check
138      * @return f the class contains a class named {@code InspectionCompanion}
139      */
hasNestedInspectionCompanion(@onNull TypeElement typeElement)140     private static boolean hasNestedInspectionCompanion(@NonNull TypeElement typeElement) {
141         for (TypeElement nestedClass : ElementFilter.typesIn(typeElement.getEnclosedElements())) {
142             if (nestedClass.getSimpleName().toString().equals("InspectionCompanion")) {
143                 return true;
144             }
145         }
146 
147         return false;
148     }
149 
150     /**
151      * Get the nearest enclosing class if there is one.
152      *
153      * If {@param element} represents a class, it will be returned wrapped in an optional.
154      *
155      * @param element An element to search from
156      * @return A TypeElement of the nearest enclosing class or an empty optional
157      */
158     @NonNull
enclosingClassElement(@onNull Element element)159     private static Optional<TypeElement> enclosingClassElement(@NonNull Element element) {
160         Element cursor = element;
161 
162         while (cursor != null) {
163             if (cursor.getKind() == ElementKind.CLASS) {
164                 return Optional.of((TypeElement) cursor);
165             }
166 
167             cursor = cursor.getEnclosingElement();
168         }
169 
170         return Optional.empty();
171     }
172 
173     /**
174      * Print message and fail the build.
175      *
176      * @param message Message to print
177      */
fail(@onNull String message)178     private void fail(@NonNull String message) {
179         processingEnv.getMessager().printMessage(ERROR, message);
180     }
181 
182     /**
183      * Print message and fail the build.
184      *
185      * @param message Message to print
186      * @param element The element that failed
187      */
fail(@onNull String message, @NonNull Element element)188     private void fail(@NonNull String message, @NonNull Element element) {
189         processingEnv.getMessager().printMessage(ERROR, message, element);
190     }
191 }
192