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 a white list 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    * dalvik.annotation.compat.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 {
165                     throw new IllegalStateException(
166                         "Unknown property type: " + propertyType + " for " + annotationClassName);
167                   }
168                 }
169 
170                 annotationProperties.put(name, value);
171             }
172           }
173 
174           if (locator == null) {
175             throw new IllegalStateException("Missing location");
176           }
177           AnnotationInfo annotationInfo = new AnnotationInfo(annotationClass, annotationProperties);
178           annotationStore.add(locator, annotationInfo);
179 
180           reader.endObject();
181         }
182 
183         reader.endArray();
184       } catch (RuntimeException e) {
185         throw new MalformedJsonException("Error parsing JSON at " + reader.getPath(), e);
186       }
187     }
188 
189     return new AddAnnotation(annotationStore);
190   }
191 
192   /**
193    * Create a {@link Processor} that will add annotations of the supplied class to classes and class
194    * members specified in the supplied file.
195    *
196    * <p>Each line in the supplied file can be empty, start with a {@code #} or be in the format
197    * expected by {@link BodyDeclarationLocators#fromStringForm(String)}. Lines that are empty or
198    * start with a {@code #} are ignored.
199    *
200    * @param annotationClassName the fully qualified class name of the annotation to add.
201    * @param file the flat file.
202    */
markerAnnotationFromFlatFile(String annotationClassName, Path file)203   public static AddAnnotation markerAnnotationFromFlatFile(String annotationClassName, Path file) {
204     List<BodyDeclarationLocator> locators =
205         BodyDeclarationLocators.readBodyDeclarationLocators(file);
206     return markerAnnotationFromLocators(annotationClassName, locators);
207   }
208 
209   /**
210    * Create a {@link Processor} that will add annotations of the supplied class to classes and class
211    * members specified in the locators.
212    *
213    * @param annotationClassName the fully qualified class name of the annotation to add.
214    * @param locators list of BodyDeclarationLocator
215    */
markerAnnotationFromLocators(String annotationClassName, List<BodyDeclarationLocator> locators)216   public static AddAnnotation markerAnnotationFromLocators(String annotationClassName,
217       List<BodyDeclarationLocator> locators) {
218     AnnotationClass annotationClass = new AnnotationClass(annotationClassName);
219     AnnotationInfo markerAnnotation = new AnnotationInfo(annotationClass, Collections.emptyMap());
220     BodyDeclarationLocatorStore<AnnotationInfo> locator2AnnotationInfo =
221         new BodyDeclarationLocatorStore<>();
222     locators.forEach(l -> locator2AnnotationInfo.add(l, markerAnnotation));
223     return new AddAnnotation(locator2AnnotationInfo);
224   }
225 
226   public interface Listener {
227 
228     /**
229      * Called when an annotation is added to a class or one of its members.
230      *
231      * @param annotationInfo the information about the annotation that was added.
232      * @param locator the locator of the element to which the annotation was added.
233      * @param bodyDeclaration the modified class or class member.
234      */
onAddAnnotation(AnnotationInfo annotationInfo, BodyDeclarationLocator locator, BodyDeclaration bodyDeclaration)235     void onAddAnnotation(AnnotationInfo annotationInfo, BodyDeclarationLocator locator,
236         BodyDeclaration bodyDeclaration);
237   }
238 
AddAnnotation(BodyDeclarationLocatorStore<AnnotationInfo> locator2AnnotationInfo)239   private AddAnnotation(BodyDeclarationLocatorStore<AnnotationInfo> locator2AnnotationInfo) {
240     this.locator2AnnotationInfo = locator2AnnotationInfo;
241     this.listener = (c, l, b) -> {};
242   }
243 
setListener(Listener listener)244   public void setListener(Listener listener) {
245     this.listener = listener;
246   }
247 
248   @Override
process(Context context, CompilationUnit cu)249   public void process(Context context, CompilationUnit cu) {
250     final ASTRewrite rewrite = context.rewrite();
251     ASTVisitor visitor = new ASTVisitor(false /* visitDocTags */) {
252       @Override
253       public boolean visit(AnnotationTypeDeclaration node) {
254         return handleBodyDeclaration(rewrite, node);
255       }
256 
257       @Override
258       public boolean visit(AnnotationTypeMemberDeclaration node) {
259         return handleBodyDeclaration(rewrite, node);
260       }
261 
262       @Override
263       public boolean visit(EnumConstantDeclaration node) {
264         return handleBodyDeclaration(rewrite, node);
265       }
266 
267       @Override
268       public boolean visit(EnumDeclaration node) {
269         return handleBodyDeclaration(rewrite, node);
270       }
271 
272       @Override
273       public boolean visit(FieldDeclaration node) {
274         return handleBodyDeclaration(rewrite, node);
275       }
276 
277       @Override
278       public boolean visit(MethodDeclaration node) {
279         return handleBodyDeclaration(rewrite, node);
280       }
281 
282       @Override
283       public boolean visit(TypeDeclaration node) {
284         return handleBodyDeclaration(rewrite, node);
285       }
286     };
287     cu.accept(visitor);
288   }
289 
handleBodyDeclaration(ASTRewrite rewrite, BodyDeclaration node)290   private boolean handleBodyDeclaration(ASTRewrite rewrite, BodyDeclaration node) {
291     Mapping<AnnotationInfo> mapping = locator2AnnotationInfo.findMapping(node);
292     if (mapping != null) {
293       AnnotationInfo annotationInfo = mapping.getValue();
294       insertAnnotationBefore(rewrite, node, annotationInfo);
295 
296       // Notify any listeners that an annotation has been added.
297       BodyDeclarationLocator locator = mapping.getLocator();
298       listener.onAddAnnotation(annotationInfo, locator, node);
299     }
300     return true;
301   }
302 
303   /**
304    * Add an annotation to a {@link BodyDeclaration} node.
305    */
insertAnnotationBefore( ASTRewrite rewrite, BodyDeclaration node, AnnotationInfo annotationInfo)306   private static void insertAnnotationBefore(
307       ASTRewrite rewrite, BodyDeclaration node,
308       AnnotationInfo annotationInfo) {
309     final TextEditGroup editGroup = null;
310     AST ast = node.getAST();
311     Map<String, Object> elements = annotationInfo.getProperties();
312     Annotation annotation;
313     if (elements.isEmpty()) {
314       annotation = ast.newMarkerAnnotation();
315     } else if (elements.size() == 1 && elements.containsKey("value")) {
316       SingleMemberAnnotation singleMemberAnnotation = ast.newSingleMemberAnnotation();
317       singleMemberAnnotation.setValue(createAnnotationValue(rewrite, elements.get("value")));
318       annotation = singleMemberAnnotation;
319     } else {
320       NormalAnnotation normalAnnotation = ast.newNormalAnnotation();
321       @SuppressWarnings("unchecked")
322       List<MemberValuePair> values = normalAnnotation.values();
323       for (Entry<String, Object> entry : elements.entrySet()) {
324         MemberValuePair pair = ast.newMemberValuePair();
325         pair.setName(ast.newSimpleName(entry.getKey()));
326         pair.setValue(createAnnotationValue(rewrite, entry.getValue()));
327         values.add(pair);
328       }
329       annotation = normalAnnotation;
330     }
331 
332     annotation.setTypeName(ast.newName(annotationInfo.getQualifiedName()));
333     ListRewrite listRewrite = rewrite.getListRewrite(node, node.getModifiersProperty());
334     listRewrite.insertFirst(annotation, editGroup);
335   }
336 
createAnnotationValue(ASTRewrite rewrite, Object value)337   private static Expression createAnnotationValue(ASTRewrite rewrite, Object value) {
338     if (value instanceof String) {
339       StringLiteral stringLiteral = rewrite.getAST().newStringLiteral();
340       stringLiteral.setLiteralValue((String) value);
341       return stringLiteral;
342     }
343     if (value instanceof Integer) {
344       NumberLiteral numberLiteral = rewrite.getAST().newNumberLiteral();
345       numberLiteral.setToken(value.toString());
346       return numberLiteral;
347     }
348     if (value instanceof Placeholder) {
349       Placeholder placeholder = (Placeholder) value;
350       // The cast is safe because createStringPlaceholder returns an instance of type NumberLiteral
351       // which is an Expression.
352       return (Expression)
353           rewrite.createStringPlaceholder(placeholder.getText(), ASTNode.NUMBER_LITERAL);
354     }
355     throw new IllegalStateException("Unknown value '" + value + "' of class " +
356         (value == null ? "NULL" : value.getClass()));
357   }
358 }
359