1 /*
2  * Copyright (C) 2018 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 package com.google.currysrc.processors;
17 
18 import com.google.currysrc.api.process.Context;
19 import com.google.currysrc.api.process.Processor;
20 import com.google.currysrc.api.process.ast.BodyDeclarationLocator;
21 import com.google.currysrc.api.process.ast.BodyDeclarationLocatorStore;
22 import com.google.currysrc.api.process.ast.BodyDeclarationLocatorStore.Mapping;
23 import com.google.currysrc.api.process.ast.BodyDeclarationLocators;
24 import com.google.currysrc.processors.AnnotationInfo.AnnotationClass;
25 import com.google.currysrc.processors.AnnotationInfo.Placeholder;
26 import com.google.gson.Gson;
27 import com.google.gson.GsonBuilder;
28 import com.google.gson.stream.JsonReader;
29 import com.google.gson.stream.JsonToken;
30 import com.google.gson.stream.MalformedJsonException;
31 import java.io.IOException;
32 import java.io.StringReader;
33 import java.nio.charset.StandardCharsets;
34 import java.nio.file.Files;
35 import java.nio.file.Path;
36 import java.util.Collections;
37 import java.util.LinkedHashMap;
38 import java.util.List;
39 import java.util.Map;
40 import java.util.Map.Entry;
41 import java.util.stream.Collectors;
42 import org.eclipse.jdt.core.dom.AST;
43 import org.eclipse.jdt.core.dom.ASTNode;
44 import org.eclipse.jdt.core.dom.ASTVisitor;
45 import org.eclipse.jdt.core.dom.Annotation;
46 import org.eclipse.jdt.core.dom.AnnotationTypeDeclaration;
47 import org.eclipse.jdt.core.dom.AnnotationTypeMemberDeclaration;
48 import org.eclipse.jdt.core.dom.BodyDeclaration;
49 import org.eclipse.jdt.core.dom.CompilationUnit;
50 import org.eclipse.jdt.core.dom.EnumConstantDeclaration;
51 import org.eclipse.jdt.core.dom.EnumDeclaration;
52 import org.eclipse.jdt.core.dom.Expression;
53 import org.eclipse.jdt.core.dom.FieldDeclaration;
54 import org.eclipse.jdt.core.dom.MemberValuePair;
55 import org.eclipse.jdt.core.dom.MethodDeclaration;
56 import org.eclipse.jdt.core.dom.NormalAnnotation;
57 import org.eclipse.jdt.core.dom.NumberLiteral;
58 import org.eclipse.jdt.core.dom.SingleMemberAnnotation;
59 import org.eclipse.jdt.core.dom.StringLiteral;
60 import org.eclipse.jdt.core.dom.TypeDeclaration;
61 import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
62 import org.eclipse.jdt.core.dom.rewrite.ListRewrite;
63 import org.eclipse.text.edits.TextEditGroup;
64 
65 /**
66  * Add annotations to an allowlist of classes and class members.
67  */
68 public class AddAnnotation implements Processor {
69 
70   private final BodyDeclarationLocatorStore<AnnotationInfo> locator2AnnotationInfo;
71   private Listener listener;
72 
73   /**
74    * Create a {@link Processor} that will add annotations of the supplied class to classes and class
75    * members specified in the supplied file.
76    *
77    * <p>The supplied JSON file must consist of an outermost array containing objects with the
78    * following structure:
79    *
80    * <pre>{@code
81    * {
82    *  "@location": "<body declaration location>",
83    *  [<property>[, <property>]*]?
84    * }
85    * }</pre>
86    *
87    * <p>Where:
88    * <ul>
89    * <li>{@code <body declaration location>} is in the format expected by
90    * {@link BodyDeclarationLocators#fromStringForm(String)}. This is the only required field.</li>
91    * <li>{@code <property>} is a property of the annotation and is of the format
92    * {@code "<name>": <value>} where {@code <name>} is the name of the annotations property which
93    * must correspond to the name of a property in the supplied {@link AnnotationClass} and
94    * {@code <value>} is the value that will be supplied for the property. A {@code <value>} must
95    * match the type of the property in the supplied {@link AnnotationClass}.
96    * </ul>
97    *
98    * <p>A {@code <value>} can be one of the following types:
99    * <ul>
100    * <li>{@code <int>} and {@code <long>} which are literal JSON numbers that are inserted into the
101    * source as literal primitive values. The corresponding property type in the supplied
102    * {@link AnnotationClass} must be {@code int.class} or {@code long.class} respectively.</li>
103    * <li>{@code <string>} is a quoted JSON string that is inserted into the source as a literal
104    * string.The corresponding property type in the supplied {@link AnnotationClass} must be
105    * {@code String.class}.</li>
106    * <li>{@code <placeholder>} is a quoted JSON string that is inserted into the source as if it
107    * was a constant expression. It is used to reference constants in annotation values, e.g. {@code
108    * android.compat.annotation.UnsupportedAppUsage.VERSION_CODES.P}. It can be used for any property
109    * type and will be type checked when the generated code is compiled.</li>
110    * </ul>
111    *
112    * <p>See external/icu/tools/srcgen/unsupported-app-usage.json for an example.
113    *
114    * @param annotationClass the type of the annotation to add, includes class name, property names
115    * and types.
116    * @param file the JSON file.
117    */
fromJsonFile(AnnotationClass annotationClass, Path file)118   public static AddAnnotation fromJsonFile(AnnotationClass annotationClass, Path file)
119       throws IOException {
120     Gson gson = new GsonBuilder().create();
121     BodyDeclarationLocatorStore<AnnotationInfo> annotationStore =
122         new BodyDeclarationLocatorStore<>();
123     String jsonStringWithoutComments =
124         Files.lines(file, StandardCharsets.UTF_8)
125             .filter(l -> !l.trim().startsWith("//"))
126             .collect(Collectors.joining("\n"));
127     try (JsonReader reader = gson.newJsonReader(new StringReader(jsonStringWithoutComments))) {
128       try {
129         reader.beginArray();
130 
131         while (reader.hasNext()) {
132           reader.beginObject();
133 
134           BodyDeclarationLocator locator = null;
135 
136           String annotationClassName = annotationClass.getName();
137           Map<String, Object> annotationProperties = new LinkedHashMap<>();
138 
139           while (reader.hasNext()) {
140             String name = reader.nextName();
141             switch (name) {
142               case "@location":
143                 locator = BodyDeclarationLocators.fromStringForm(reader.nextString());
144                 break;
145               default:
146                 Class<?> propertyType = annotationClass.getPropertyType(name);
147                 Object value;
148                 JsonToken token = reader.peek();
149                 if (token == JsonToken.STRING) {
150                   String text = reader.nextString();
151                   if (propertyType != String.class) {
152                     value = new Placeholder(text);
153                   } else {
154                     // Literal string.
155                     value = text;
156                   }
157                 } else {
158                   if (propertyType == boolean.class) {
159                     value = reader.nextBoolean();
160                   } else if (propertyType == int.class) {
161                     value = reader.nextInt();
162                   } else if (propertyType == double.class) {
163                     value = reader.nextDouble();
164                   } else if (propertyType == long.class) {
165                     value = reader.nextLong();
166                   } else {
167                     throw new IllegalStateException(
168                         "Unknown property type: " + propertyType + " for " + annotationClassName);
169                   }
170                 }
171 
172                 annotationProperties.put(name, value);
173             }
174           }
175 
176           if (locator == null) {
177             throw new IllegalStateException("Missing location");
178           }
179           AnnotationInfo annotationInfo = new AnnotationInfo(annotationClass, annotationProperties);
180           annotationStore.add(locator, annotationInfo);
181 
182           reader.endObject();
183         }
184 
185         reader.endArray();
186       } catch (RuntimeException e) {
187         throw new MalformedJsonException("Error parsing JSON at " + reader.getPath(), e);
188       }
189     }
190 
191     return new AddAnnotation(annotationStore);
192   }
193 
194   /**
195    * Create a {@link Processor} that will add annotations of the supplied class to classes and class
196    * members specified in the supplied file.
197    *
198    * <p>Each line in the supplied file can be empty, start with a {@code #} or be in the format
199    * expected by {@link BodyDeclarationLocators#fromStringForm(String)}. Lines that are empty or
200    * start with a {@code #} are ignored.
201    *
202    * @param annotationClassName the fully qualified class name of the annotation to add.
203    * @param file the flat file.
204    */
markerAnnotationFromFlatFile(String annotationClassName, Path file)205   public static AddAnnotation markerAnnotationFromFlatFile(String annotationClassName, Path file) {
206     List<BodyDeclarationLocator> locators =
207         BodyDeclarationLocators.readBodyDeclarationLocators(file);
208     return markerAnnotationFromLocators(annotationClassName, locators);
209   }
210 
211   /**
212    * Create a {@link Processor} that will add annotations of the supplied class to classes and class
213    * members specified in the locators.
214    *
215    * @param annotationClassName the fully qualified class name of the annotation to add.
216    * @param locators list of BodyDeclarationLocator
217    */
markerAnnotationFromLocators(String annotationClassName, List<BodyDeclarationLocator> locators)218   public static AddAnnotation markerAnnotationFromLocators(String annotationClassName,
219       List<BodyDeclarationLocator> locators) {
220     AnnotationClass annotationClass = new AnnotationClass(annotationClassName);
221     AnnotationInfo markerAnnotation = new AnnotationInfo(annotationClass, Collections.emptyMap());
222     BodyDeclarationLocatorStore<AnnotationInfo> locator2AnnotationInfo =
223         new BodyDeclarationLocatorStore<>();
224     locators.forEach(l -> locator2AnnotationInfo.add(l, markerAnnotation));
225     return new AddAnnotation(locator2AnnotationInfo);
226   }
227 
228   /**
229    * Create a {@link Processor} that will add annotations of the supplied class to classes and class
230    * members specified in the supplied file. The annotations will have a single property.
231    *
232    * <p>Each line in the supplied file can be empty, start with a {@code #} or be in the format
233    * expected by {@link BodyDeclarationLocators#fromStringForm(String)}. Lines that are empty or
234    * start with a {@code #} are ignored.
235    *
236    * @param annotationClassName the fully qualified class name of the annotation to add.
237    * @param propertyName the name of the property to add
238    * @param propertyClass the class of the property to add (use {@link Enum} for enums)
239    * @param propertyValue the value of the property to add
240    * @param file the flat file.
241    */
markerAnnotationWithPropertyFromFlatFile( String annotationClassName, String propertyName, Class<?> propertyClass, Object propertyValue, Path file)242   public static AddAnnotation markerAnnotationWithPropertyFromFlatFile(
243       String annotationClassName,
244       String propertyName,
245       Class<?> propertyClass,
246       Object propertyValue,
247       Path file) {
248     List<BodyDeclarationLocator> locators =
249         BodyDeclarationLocators.readBodyDeclarationLocators(file);
250     return markerAnnotationWithPropertyFromLocators(
251         annotationClassName, propertyName, propertyClass, propertyValue, locators);
252   }
253 
254   /**
255    * Create a {@link Processor} that will add annotations of the supplied class to classes and class
256    * members specified in the locators. The annotations will have a single property.
257    *
258    * @param annotationClassName the fully qualified class name of the annotation to add.
259    * @param propertyName the name of the property to add
260    * @param propertyClass the class of the property to add (use {@link Enum} for enums)
261    * @param propertyValue the value of the property to add
262    * @param locators list of BodyDeclarationLocator
263    */
markerAnnotationWithPropertyFromLocators( String annotationClassName, String propertyName, Class<?> propertyClass, Object propertyValue, List<BodyDeclarationLocator> locators)264   public static AddAnnotation markerAnnotationWithPropertyFromLocators(
265       String annotationClassName,
266       String propertyName,
267       Class<?> propertyClass,
268       Object propertyValue,
269       List<BodyDeclarationLocator> locators) {
270     AnnotationClass annotationClass =
271         new AnnotationClass(annotationClassName).addProperty(propertyName, propertyClass);
272     AnnotationInfo markerAnnotation =
273         new AnnotationInfo(annotationClass, Collections.singletonMap(propertyName, propertyValue));
274     BodyDeclarationLocatorStore<AnnotationInfo> locator2AnnotationInfo =
275         new BodyDeclarationLocatorStore<>();
276     locators.forEach(l -> locator2AnnotationInfo.add(l, markerAnnotation));
277     return new AddAnnotation(locator2AnnotationInfo);
278   }
279 
280   public interface Listener {
281 
282     /**
283      * Called when an annotation is added to a class or one of its members.
284      *
285      * @param annotationInfo the information about the annotation that was added.
286      * @param locator the locator of the element to which the annotation was added.
287      * @param bodyDeclaration the modified class or class member.
288      */
onAddAnnotation(AnnotationInfo annotationInfo, BodyDeclarationLocator locator, BodyDeclaration bodyDeclaration)289     void onAddAnnotation(AnnotationInfo annotationInfo, BodyDeclarationLocator locator,
290         BodyDeclaration bodyDeclaration);
291   }
292 
AddAnnotation(BodyDeclarationLocatorStore<AnnotationInfo> locator2AnnotationInfo)293   private AddAnnotation(BodyDeclarationLocatorStore<AnnotationInfo> locator2AnnotationInfo) {
294     this.locator2AnnotationInfo = locator2AnnotationInfo;
295     this.listener = (c, l, b) -> {};
296   }
297 
setListener(Listener listener)298   public void setListener(Listener listener) {
299     this.listener = listener;
300   }
301 
302   @Override
process(Context context, CompilationUnit cu)303   public void process(Context context, CompilationUnit cu) {
304     final ASTRewrite rewrite = context.rewrite();
305     ASTVisitor visitor = new ASTVisitor(false /* visitDocTags */) {
306       @Override
307       public boolean visit(AnnotationTypeDeclaration node) {
308         return handleBodyDeclaration(rewrite, node);
309       }
310 
311       @Override
312       public boolean visit(AnnotationTypeMemberDeclaration node) {
313         return handleBodyDeclaration(rewrite, node);
314       }
315 
316       @Override
317       public boolean visit(EnumConstantDeclaration node) {
318         return handleBodyDeclaration(rewrite, node);
319       }
320 
321       @Override
322       public boolean visit(EnumDeclaration node) {
323         return handleBodyDeclaration(rewrite, node);
324       }
325 
326       @Override
327       public boolean visit(FieldDeclaration node) {
328         return handleBodyDeclaration(rewrite, node);
329       }
330 
331       @Override
332       public boolean visit(MethodDeclaration node) {
333         return handleBodyDeclaration(rewrite, node);
334       }
335 
336       @Override
337       public boolean visit(TypeDeclaration node) {
338         return handleBodyDeclaration(rewrite, node);
339       }
340     };
341     cu.accept(visitor);
342   }
343 
handleBodyDeclaration(ASTRewrite rewrite, BodyDeclaration node)344   private boolean handleBodyDeclaration(ASTRewrite rewrite, BodyDeclaration node) {
345     Mapping<AnnotationInfo> mapping = locator2AnnotationInfo.findMapping(node);
346     if (mapping != null) {
347       AnnotationInfo annotationInfo = mapping.getValue();
348       insertAnnotationBefore(rewrite, node, annotationInfo);
349 
350       // Notify any listeners that an annotation has been added.
351       BodyDeclarationLocator locator = mapping.getLocator();
352       listener.onAddAnnotation(annotationInfo, locator, node);
353     }
354     return true;
355   }
356 
357   /**
358    * Add an annotation to a {@link BodyDeclaration} node.
359    */
insertAnnotationBefore( ASTRewrite rewrite, BodyDeclaration node, AnnotationInfo annotationInfo)360   private static void insertAnnotationBefore(
361       ASTRewrite rewrite, BodyDeclaration node,
362       AnnotationInfo annotationInfo) {
363     final TextEditGroup editGroup = null;
364     AST ast = node.getAST();
365     Map<String, Object> elements = annotationInfo.getProperties();
366     Annotation annotation;
367     if (elements.isEmpty()) {
368       annotation = ast.newMarkerAnnotation();
369     } else if (elements.size() == 1 && elements.containsKey("value")) {
370       SingleMemberAnnotation singleMemberAnnotation = ast.newSingleMemberAnnotation();
371       singleMemberAnnotation.setValue(createAnnotationValue(rewrite, elements.get("value")));
372       annotation = singleMemberAnnotation;
373     } else {
374       NormalAnnotation normalAnnotation = ast.newNormalAnnotation();
375       @SuppressWarnings("unchecked")
376       List<MemberValuePair> values = normalAnnotation.values();
377       for (Entry<String, Object> entry : elements.entrySet()) {
378         MemberValuePair pair = ast.newMemberValuePair();
379         pair.setName(ast.newSimpleName(entry.getKey()));
380         pair.setValue(createAnnotationValue(rewrite, entry.getValue()));
381         values.add(pair);
382       }
383       annotation = normalAnnotation;
384     }
385 
386     annotation.setTypeName(ast.newName(annotationInfo.getQualifiedName()));
387     ListRewrite listRewrite = rewrite.getListRewrite(node, node.getModifiersProperty());
388     listRewrite.insertFirst(annotation, editGroup);
389   }
390 
createAnnotationValue(ASTRewrite rewrite, Object value)391   private static Expression createAnnotationValue(ASTRewrite rewrite, Object value) {
392     if (value instanceof String) {
393       StringLiteral stringLiteral = rewrite.getAST().newStringLiteral();
394       stringLiteral.setLiteralValue((String) value);
395       return stringLiteral;
396     }
397     if ((value instanceof Integer) || (value instanceof Long)) {
398       NumberLiteral numberLiteral = rewrite.getAST().newNumberLiteral();
399       numberLiteral.setToken(value.toString());
400       return numberLiteral;
401     }
402     if (value instanceof Placeholder) {
403       Placeholder placeholder = (Placeholder) value;
404       // The cast is safe because createStringPlaceholder returns an instance of type NumberLiteral
405       // which is an Expression.
406       return (Expression)
407           rewrite.createStringPlaceholder(placeholder.getText(), ASTNode.NUMBER_LITERAL);
408     }
409     throw new IllegalStateException("Unknown value '" + value + "' of class " +
410         (value == null ? "NULL" : value.getClass()));
411   }
412 }
413