1 /* 2 * Copyright (c) 2013, 2019, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. 8 * 9 * This code is distributed in the hope that it will be useful, but WITHOUT 10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 11 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 12 * version 2 for more details (a copy is included in the LICENSE file that 13 * accompanied this code). 14 * 15 * You should have received a copy of the GNU General Public License version 16 * 2 along with this work; if not, write to the Free Software Foundation, 17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 18 * 19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 20 * or visit www.oracle.com if you need additional information or have any 21 * questions. 22 */ 23 24 25 package org.graalvm.compiler.options.processor; 26 27 import java.io.BufferedReader; 28 import java.io.IOException; 29 import java.io.InputStreamReader; 30 import java.io.PrintWriter; 31 import java.util.ArrayList; 32 import java.util.Collections; 33 import java.util.HashMap; 34 import java.util.HashSet; 35 import java.util.List; 36 import java.util.Map; 37 import java.util.Set; 38 39 import javax.annotation.processing.Filer; 40 import javax.annotation.processing.ProcessingEnvironment; 41 import javax.annotation.processing.RoundEnvironment; 42 import javax.annotation.processing.SupportedAnnotationTypes; 43 import javax.lang.model.SourceVersion; 44 import javax.lang.model.element.AnnotationMirror; 45 import javax.lang.model.element.Element; 46 import javax.lang.model.element.ElementKind; 47 import javax.lang.model.element.Modifier; 48 import javax.lang.model.element.PackageElement; 49 import javax.lang.model.element.TypeElement; 50 import javax.lang.model.element.VariableElement; 51 import javax.lang.model.type.DeclaredType; 52 import javax.lang.model.type.TypeKind; 53 import javax.lang.model.type.TypeMirror; 54 import javax.lang.model.util.Types; 55 import javax.tools.Diagnostic.Kind; 56 import javax.tools.FileObject; 57 import javax.tools.JavaFileObject; 58 import javax.tools.StandardLocation; 59 60 import org.graalvm.compiler.processor.AbstractProcessor; 61 62 /** 63 * Processes static fields annotated with {@code Option}. An {@code OptionDescriptors} 64 * implementation is generated for each top level class containing at least one such field. The name 65 * of the generated class for top level class {@code com.foo.Bar} is 66 * {@code com.foo.Bar_OptionDescriptors}. 67 */ 68 @SupportedAnnotationTypes({"org.graalvm.compiler.options.Option"}) 69 public class OptionProcessor extends AbstractProcessor { 70 71 private static final String OPTION_CLASS_NAME = "org.graalvm.compiler.options.Option"; 72 private static final String OPTION_KEY_CLASS_NAME = "org.graalvm.compiler.options.OptionKey"; 73 private static final String OPTION_TYPE_CLASS_NAME = "org.graalvm.compiler.options.OptionType"; 74 private static final String OPTION_DESCRIPTOR_CLASS_NAME = "org.graalvm.compiler.options.OptionDescriptor"; 75 private static final String OPTION_DESCRIPTORS_CLASS_NAME = "org.graalvm.compiler.options.OptionDescriptors"; 76 77 @Override 78 public SourceVersion getSupportedSourceVersion() { 79 return SourceVersion.latest(); 80 } 81 82 private final Set<Element> processed = new HashSet<>(); 83 84 private TypeMirror optionTypeMirror; 85 private TypeMirror optionKeyTypeMirror; 86 87 private void processElement(Element element, OptionsInfo info) { 88 89 if (!element.getModifiers().contains(Modifier.STATIC)) { 90 processingEnv.getMessager().printMessage(Kind.ERROR, "Option field must be static", element); 91 return; 92 } 93 if (element.getModifiers().contains(Modifier.PRIVATE)) { 94 processingEnv.getMessager().printMessage(Kind.ERROR, "Option field cannot be private", element); 95 return; 96 } 97 98 AnnotationMirror annotation = getAnnotation(element, optionTypeMirror); 99 assert annotation != null; 100 assert element instanceof VariableElement; 101 assert element.getKind() == ElementKind.FIELD; 102 VariableElement field = (VariableElement) element; 103 String fieldName = field.getSimpleName().toString(); 104 105 Types types = processingEnv.getTypeUtils(); 106 107 TypeMirror fieldType = field.asType(); 108 if (fieldType.getKind() != TypeKind.DECLARED) { 109 processingEnv.getMessager().printMessage(Kind.ERROR, "Option field must be of type " + OPTION_KEY_CLASS_NAME, element); 110 return; 111 } 112 DeclaredType declaredFieldType = (DeclaredType) fieldType; 113 114 if (!types.isSubtype(fieldType, types.erasure(optionKeyTypeMirror))) { 115 String msg = String.format("Option field type %s is not a subclass of %s", fieldType, optionKeyTypeMirror); 116 processingEnv.getMessager().printMessage(Kind.ERROR, msg, element); 117 return; 118 } 119 120 if (!field.getModifiers().contains(Modifier.STATIC)) { 121 processingEnv.getMessager().printMessage(Kind.ERROR, "Option field must be static", element); 122 return; 123 } 124 if (field.getModifiers().contains(Modifier.PRIVATE)) { 125 processingEnv.getMessager().printMessage(Kind.ERROR, "Option field cannot be private", element); 126 return; 127 } 128 129 String optionName = getAnnotationValue(annotation, "name", String.class); 130 if (optionName.equals("")) { 131 optionName = fieldName; 132 } 133 134 if (!Character.isUpperCase(optionName.charAt(0))) { 135 processingEnv.getMessager().printMessage(Kind.ERROR, "Option name must start with an upper case letter", element); 136 return; 137 } 138 139 DeclaredType declaredOptionKeyType = declaredFieldType; 140 while (!types.isSameType(types.erasure(declaredOptionKeyType), types.erasure(optionKeyTypeMirror))) { 141 List<? extends TypeMirror> directSupertypes = types.directSupertypes(declaredFieldType); 142 assert !directSupertypes.isEmpty(); 143 declaredOptionKeyType = (DeclaredType) directSupertypes.get(0); 144 } 145 146 assert !declaredOptionKeyType.getTypeArguments().isEmpty(); 147 String optionType = declaredOptionKeyType.getTypeArguments().get(0).toString(); 148 if (optionType.startsWith("java.lang.")) { 149 optionType = optionType.substring("java.lang.".length()); 150 } 151 152 Element enclosing = element.getEnclosingElement(); 153 String declaringClass = ""; 154 String separator = ""; 155 Set<Element> originatingElementsList = info.originatingElements; 156 originatingElementsList.add(field); 157 PackageElement enclosingPackage = null; 158 while (enclosing != null) { 159 if (enclosing.getKind() == ElementKind.CLASS || enclosing.getKind() == ElementKind.INTERFACE || enclosing.getKind() == ElementKind.ENUM) { 160 if (enclosing.getModifiers().contains(Modifier.PRIVATE)) { 161 String msg = String.format("Option field cannot be declared in a private %s %s", enclosing.getKind().name().toLowerCase(), enclosing); 162 processingEnv.getMessager().printMessage(Kind.ERROR, msg, element); 163 return; 164 } 165 originatingElementsList.add(enclosing); 166 declaringClass = enclosing.getSimpleName() + separator + declaringClass; 167 separator = "."; 168 } else if (enclosing.getKind() == ElementKind.PACKAGE) { 169 enclosingPackage = (PackageElement) enclosing; 170 break; 171 } else { 172 processingEnv.getMessager().printMessage(Kind.ERROR, "Unexpected enclosing element kind: " + enclosing.getKind(), element); 173 return; 174 } 175 enclosing = enclosing.getEnclosingElement(); 176 } 177 if (enclosingPackage == null || enclosingPackage.isUnnamed()) { 178 processingEnv.getMessager().printMessage(Kind.ERROR, "Option field cannot be declared in the unnamed package", element); 179 return; 180 } 181 List<String> helpValue = getAnnotationValueList(annotation, "help", String.class); 182 String help = ""; 183 List<String> extraHelp = new ArrayList<>(); 184 185 if (helpValue.size() == 1) { 186 help = helpValue.get(0); 187 if (help.startsWith("file:")) { 188 String path = help.substring("file:".length()); 189 Filer filer = processingEnv.getFiler(); 190 try { 191 FileObject file; 192 try { 193 file = filer.getResource(StandardLocation.SOURCE_PATH, enclosingPackage.getQualifiedName(), path); 194 } catch (IllegalArgumentException | IOException e) { 195 // Handle the case when a compiler doesn't support the SOURCE_PATH location 196 file = filer.getResource(StandardLocation.CLASS_OUTPUT, enclosingPackage.getQualifiedName(), path); 197 } 198 try (BufferedReader br = new BufferedReader(new InputStreamReader(file.openInputStream()))) { 199 help = br.readLine(); 200 if (help == null) { 201 help = ""; 202 } 203 String line = br.readLine(); 204 while (line != null) { 205 extraHelp.add(line); 206 line = br.readLine(); 207 } 208 } 209 } catch (IOException e) { 210 String msg = String.format("Error reading %s containing the help text for option field: %s", path, e); 211 processingEnv.getMessager().printMessage(Kind.ERROR, msg, element); 212 return; 213 } 214 } 215 } else if (helpValue.size() > 1) { 216 help = helpValue.get(0); 217 extraHelp = helpValue.subList(1, helpValue.size()); 218 } 219 if (help.length() != 0) { 220 char firstChar = help.charAt(0); 221 if (!Character.isUpperCase(firstChar)) { 222 processingEnv.getMessager().printMessage(Kind.ERROR, "Option help text must start with an upper case letter", element); 223 return; 224 } 225 } 226 227 String optionStabilityName = getAnnotationValue(annotation, "stability", VariableElement.class).getSimpleName().toString(); 228 if (optionStabilityName.equals("STABLE")) { 229 if (help.length() == 0) { 230 processingEnv.getMessager().printMessage(Kind.ERROR, "A stable option must have non-empty help text", element); 231 return; 232 } 233 } 234 235 String optionTypeName = getAnnotationValue(annotation, "type", VariableElement.class).getSimpleName().toString(); 236 info.options.add(new OptionInfo(optionName, optionTypeName, help, extraHelp, optionType, declaringClass, field.getSimpleName().toString())); 237 } 238 239 public static void createOptionsDescriptorsFile(ProcessingEnvironment processingEnv, OptionsInfo info) { 240 Element[] originatingElements = info.originatingElements.toArray(new Element[info.originatingElements.size()]); 241 String optionsDescriptorsClassName = info.className + "_" + getSimpleName(OPTION_DESCRIPTORS_CLASS_NAME); 242 243 Filer filer = processingEnv.getFiler(); 244 try (PrintWriter out = createSourceFile(info.packageName, optionsDescriptorsClassName, filer, originatingElements)) { 245 246 out.println("// CheckStyle: stop header check"); 247 out.println("// CheckStyle: stop line length check"); 248 out.println("// GENERATED CONTENT - DO NOT EDIT"); 249 out.println("// Source: " + info.className + ".java"); 250 out.println("package " + info.packageName + ";"); 251 out.println(""); 252 out.println("import java.util.*;"); 253 out.println("import " + getPackageName(OPTION_DESCRIPTORS_CLASS_NAME) + ".*;"); 254 out.println("import " + OPTION_TYPE_CLASS_NAME + ";"); 255 out.println(""); 256 out.println("public class " + optionsDescriptorsClassName + " implements " + getSimpleName(OPTION_DESCRIPTORS_CLASS_NAME) + " {"); 257 258 String desc = getSimpleName(OPTION_DESCRIPTOR_CLASS_NAME); 259 260 Collections.sort(info.options); 261 262 out.println(" @Override"); 263 out.println(" public OptionDescriptor get(String value) {"); 264 out.println(" switch (value) {"); 265 out.println(" // CheckStyle: stop line length check"); 266 for (OptionInfo option : info.options) { 267 String name = option.name; 268 String optionField = option.declaringClass + "." + option.field; 269 out.println(" case \"" + name + "\": {"); 270 String optionType = option.optionType; 271 String type = option.type; 272 String help = option.help; 273 List<String> extraHelp = option.extraHelp; 274 String declaringClass = option.declaringClass; 275 String fieldName = option.field; 276 out.printf(" return " + desc + ".create(\n"); 277 out.printf(" /*name*/ \"%s\",\n", name); 278 out.printf(" /*optionType*/ %s.%s,\n", getSimpleName(OPTION_TYPE_CLASS_NAME), optionType); 279 out.printf(" /*optionValueType*/ %s.class,\n", type); 280 out.printf(" /*help*/ \"%s\",\n", help); 281 if (extraHelp.size() != 0) { 282 out.printf(" /*extraHelp*/ new String[] {\n"); 283 for (String line : extraHelp) { 284 out.printf(" \"%s\",\n", line.replace("\\", "\\\\").replace("\"", "\\\"")); 285 } 286 out.printf(" },\n"); 287 } 288 out.printf(" /*declaringClass*/ %s.class,\n", declaringClass); 289 out.printf(" /*fieldName*/ \"%s\",\n", fieldName); 290 out.printf(" /*option*/ %s);\n", optionField); 291 out.println(" }"); 292 } 293 out.println(" // CheckStyle: resume line length check"); 294 out.println(" }"); 295 out.println(" return null;"); 296 out.println(" }"); 297 out.println(); 298 out.println(" @Override"); 299 out.println(" public Iterator<" + desc + "> iterator() {"); 300 out.println(" return new Iterator<OptionDescriptor>() {"); 301 out.println(" int i = 0;"); 302 out.println(" @Override"); 303 out.println(" public boolean hasNext() {"); 304 out.println(" return i < " + info.options.size() + ";"); 305 out.println(" }"); 306 out.println(" @Override"); 307 out.println(" public OptionDescriptor next() {"); 308 out.println(" switch (i++) {"); 309 for (int i = 0; i < info.options.size(); i++) { 310 OptionInfo option = info.options.get(i); 311 out.println(" case " + i + ": return get(\"" + option.name + "\");"); 312 } 313 out.println(" }"); 314 out.println(" throw new NoSuchElementException();"); 315 out.println(" }"); 316 out.println(" };"); 317 out.println(" }"); 318 out.println("}"); 319 } 320 } 321 322 public static PrintWriter createSourceFile(String pkg, String relativeName, Filer filer, Element... originatingElements) { 323 try { 324 // Ensure Unix line endings to comply with code style guide checked by Checkstyle 325 String className = pkg + "." + relativeName; 326 JavaFileObject sourceFile = filer.createSourceFile(className, originatingElements); 327 return new PrintWriter(sourceFile.openWriter()) { 328 329 @Override 330 public void println() { 331 print("\n"); 332 } 333 }; 334 } catch (IOException e) { 335 throw new RuntimeException(e); 336 } 337 } 338 339 public static class OptionInfo implements Comparable<OptionInfo> { 340 341 public final String name; 342 public final String optionType; 343 public final String help; 344 public final List<String> extraHelp; 345 public final String type; 346 public final String declaringClass; 347 public final String field; 348 349 public OptionInfo(String name, String optionType, String help, List<String> extraHelp, String type, String declaringClass, String field) { 350 this.name = name; 351 this.optionType = optionType; 352 this.help = help; 353 this.extraHelp = extraHelp; 354 this.type = type; 355 this.declaringClass = declaringClass; 356 this.field = field; 357 } 358 359 @Override 360 public int compareTo(OptionInfo other) { 361 return name.compareTo(other.name); 362 } 363 364 @Override 365 public String toString() { 366 return declaringClass + "." + field; 367 } 368 } 369 370 public static class OptionsInfo { 371 372 public final String packageName; 373 public final String className; 374 public final List<OptionInfo> options = new ArrayList<>(); 375 public final Set<Element> originatingElements = new HashSet<>(); 376 377 public OptionsInfo(String packageName, String className) { 378 this.packageName = packageName; 379 this.className = className; 380 } 381 } 382 383 private static Element topDeclaringType(Element element) { 384 Element enclosing = element.getEnclosingElement(); 385 if (enclosing == null || enclosing.getKind() == ElementKind.PACKAGE) { 386 return element; 387 } 388 return topDeclaringType(enclosing); 389 } 390 391 @Override 392 public boolean doProcess(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { 393 if (roundEnv.processingOver()) { 394 return true; 395 } 396 397 TypeElement optionTypeElement = getTypeElement(OPTION_CLASS_NAME); 398 399 optionTypeMirror = optionTypeElement.asType(); 400 optionKeyTypeMirror = getTypeElement(OPTION_KEY_CLASS_NAME).asType(); 401 402 Map<Element, OptionsInfo> map = new HashMap<>(); 403 for (Element element : roundEnv.getElementsAnnotatedWith(optionTypeElement)) { 404 if (!processed.contains(element)) { 405 processed.add(element); 406 Element topDeclaringType = topDeclaringType(element); 407 OptionsInfo options = map.get(topDeclaringType); 408 if (options == null) { 409 String pkg = ((PackageElement) topDeclaringType.getEnclosingElement()).getQualifiedName().toString(); 410 String topDeclaringClass = topDeclaringType.getSimpleName().toString(); 411 options = new OptionsInfo(pkg, topDeclaringClass); 412 map.put(topDeclaringType, options); 413 } 414 if (!element.getEnclosingElement().getSimpleName().toString().endsWith("Options")) { 415 processingEnv.getMessager().printMessage(Kind.ERROR, "Option declaring classes must have a name that ends with 'Options'", element.getEnclosingElement()); 416 } 417 processElement(element, options); 418 } 419 } 420 421 boolean ok = true; 422 Map<String, OptionInfo> uniqueness = new HashMap<>(); 423 for (Map.Entry<Element, OptionsInfo> e : map.entrySet()) { 424 OptionsInfo info = e.getValue(); 425 for (OptionInfo option : info.options) { 426 OptionInfo conflict = uniqueness.put(option.name, option); 427 if (conflict != null) { 428 processingEnv.getMessager().printMessage(Kind.ERROR, "Duplicate option names for " + option + " and " + conflict, e.getKey()); 429 ok = false; 430 } 431 } 432 } 433 434 if (ok) { 435 for (OptionsInfo info : map.values()) { 436 createOptionsDescriptorsFile(processingEnv, info); 437 } 438 } 439 440 return true; 441 } 442 }