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