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