1 /* 2 * Copyright (c) 2013, 2018, 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) { 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 optionTypeName = getAnnotationValue(annotation, "type", VariableElement.class).getSimpleName().toString(); 228 info.options.add(new OptionInfo(optionName, optionTypeName, help, extraHelp, optionType, declaringClass, field.getSimpleName().toString())); 229 } 230 231 public static void createOptionsDescriptorsFile(ProcessingEnvironment processingEnv, OptionsInfo info) { 232 Element[] originatingElements = info.originatingElements.toArray(new Element[info.originatingElements.size()]); 233 String optionsDescriptorsClassName = info.className + "_" + getSimpleName(OPTION_DESCRIPTORS_CLASS_NAME); 234 235 Filer filer = processingEnv.getFiler(); 236 try (PrintWriter out = createSourceFile(info.packageName, optionsDescriptorsClassName, filer, originatingElements)) { 237 238 out.println("// CheckStyle: stop header check"); 239 out.println("// CheckStyle: stop line length check"); 240 out.println("// GENERATED CONTENT - DO NOT EDIT"); 241 out.println("// Source: " + info.className + ".java"); 242 out.println("package " + info.packageName + ";"); 243 out.println(""); 244 out.println("import java.util.*;"); 245 out.println("import " + getPackageName(OPTION_DESCRIPTORS_CLASS_NAME) + ".*;"); 246 out.println("import " + OPTION_TYPE_CLASS_NAME + ";"); 247 out.println(""); 248 out.println("public class " + optionsDescriptorsClassName + " implements " + getSimpleName(OPTION_DESCRIPTORS_CLASS_NAME) + " {"); 249 250 String desc = getSimpleName(OPTION_DESCRIPTOR_CLASS_NAME); 251 252 Collections.sort(info.options); 253 254 out.println(" @Override"); 255 out.println(" public OptionDescriptor get(String value) {"); 256 out.println(" switch (value) {"); 257 out.println(" // CheckStyle: stop line length check"); 258 for (OptionInfo option : info.options) { 259 String name = option.name; 260 String optionField = option.declaringClass + "." + option.field; 261 out.println(" case \"" + name + "\": {"); 262 String optionType = option.optionType; 263 String type = option.type; 264 String help = option.help; 265 List<String> extraHelp = option.extraHelp; 266 String declaringClass = option.declaringClass; 267 String fieldName = option.field; 268 out.printf(" return " + desc + ".create(\n"); 269 out.printf(" /*name*/ \"%s\",\n", name); 270 out.printf(" /*optionType*/ %s.%s,\n", getSimpleName(OPTION_TYPE_CLASS_NAME), optionType); 271 out.printf(" /*optionValueType*/ %s.class,\n", type); 272 out.printf(" /*help*/ \"%s\",\n", help); 273 if (extraHelp.size() != 0) { 274 out.printf(" /*extraHelp*/ new String[] {\n"); 275 for (String line : extraHelp) { 276 out.printf(" \"%s\",\n", line.replace("\\", "\\\\").replace("\"", "\\\"")); 277 } 278 out.printf(" },\n"); 279 } 280 out.printf(" /*declaringClass*/ %s.class,\n", declaringClass); 281 out.printf(" /*fieldName*/ \"%s\",\n", fieldName); 282 out.printf(" /*option*/ %s);\n", optionField); 283 out.println(" }"); 284 } 285 out.println(" // CheckStyle: resume line length check"); 286 out.println(" }"); 287 out.println(" return null;"); 288 out.println(" }"); 289 out.println(); 290 out.println(" @Override"); 291 out.println(" public Iterator<" + desc + "> iterator() {"); 292 out.println(" return new Iterator<OptionDescriptor>() {"); 293 out.println(" int i = 0;"); 294 out.println(" @Override"); 295 out.println(" public boolean hasNext() {"); 296 out.println(" return i < " + info.options.size() + ";"); 297 out.println(" }"); 298 out.println(" @Override"); 299 out.println(" public OptionDescriptor next() {"); 300 out.println(" switch (i++) {"); 301 for (int i = 0; i < info.options.size(); i++) { 302 OptionInfo option = info.options.get(i); 303 out.println(" case " + i + ": return get(\"" + option.name + "\");"); 304 } 305 out.println(" }"); 306 out.println(" throw new NoSuchElementException();"); 307 out.println(" }"); 308 out.println(" };"); 309 out.println(" }"); 310 out.println("}"); 311 } 312 } 313 314 public static PrintWriter createSourceFile(String pkg, String relativeName, Filer filer, Element... originatingElements) { 315 try { 316 // Ensure Unix line endings to comply with code style guide checked by Checkstyle 317 String className = pkg + "." + relativeName; 318 JavaFileObject sourceFile = filer.createSourceFile(className, originatingElements); 319 return new PrintWriter(sourceFile.openWriter()) { 320 321 @Override 322 public void println() { 323 print("\n"); 324 } 325 }; 326 } catch (IOException e) { 327 throw new RuntimeException(e); 328 } 329 } 330 331 public static class OptionInfo implements Comparable<OptionInfo> { 332 333 public final String name; 334 public final String optionType; 335 public final String help; 336 public final List<String> extraHelp; 337 public final String type; 338 public final String declaringClass; 339 public final String field; 340 341 public OptionInfo(String name, String optionType, String help, List<String> extraHelp, String type, String declaringClass, String field) { 342 this.name = name; 343 this.optionType = optionType; 344 this.help = help; 345 this.extraHelp = extraHelp; 346 this.type = type; 347 this.declaringClass = declaringClass; 348 this.field = field; 349 } 350 351 @Override 352 public int compareTo(OptionInfo other) { 353 return name.compareTo(other.name); 354 } 355 356 @Override 357 public String toString() { 358 return declaringClass + "." + field; 359 } 360 } 361 362 public static class OptionsInfo { 363 364 public final String packageName; 365 public final String className; 366 public final List<OptionInfo> options = new ArrayList<>(); 367 public final Set<Element> originatingElements = new HashSet<>(); 368 369 public OptionsInfo(String packageName, String className) { 370 this.packageName = packageName; 371 this.className = className; 372 } 373 } 374 375 private static Element topDeclaringType(Element element) { 376 Element enclosing = element.getEnclosingElement(); 377 if (enclosing == null || enclosing.getKind() == ElementKind.PACKAGE) { 378 return element; 379 } 380 return topDeclaringType(enclosing); 381 } 382 383 @Override 384 public boolean doProcess(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { 385 if (roundEnv.processingOver()) { 386 return true; 387 } 388 389 TypeElement optionTypeElement = getTypeElement(OPTION_CLASS_NAME); 390 391 optionTypeMirror = optionTypeElement.asType(); 392 optionKeyTypeMirror = getTypeElement(OPTION_KEY_CLASS_NAME).asType(); 393 394 Map<Element, OptionsInfo> map = new HashMap<>(); 395 for (Element element : roundEnv.getElementsAnnotatedWith(optionTypeElement)) { 396 if (!processed.contains(element)) { 397 processed.add(element); 398 Element topDeclaringType = topDeclaringType(element); 399 OptionsInfo options = map.get(topDeclaringType); 400 if (options == null) { 401 String pkg = ((PackageElement) topDeclaringType.getEnclosingElement()).getQualifiedName().toString(); 402 String topDeclaringClass = topDeclaringType.getSimpleName().toString(); 403 options = new OptionsInfo(pkg, topDeclaringClass); 404 map.put(topDeclaringType, options); 405 } 406 if (!element.getEnclosingElement().getSimpleName().toString().endsWith("Options")) { 407 processingEnv.getMessager().printMessage(Kind.ERROR, "Option declaring classes must have a name that ends with 'Options'", element.getEnclosingElement()); 408 } 409 processElement(element, options); 410 } 411 } 412 413 boolean ok = true; 414 Map<String, OptionInfo> uniqueness = new HashMap<>(); 415 for (Map.Entry<Element, OptionsInfo> e : map.entrySet()) { 416 OptionsInfo info = e.getValue(); 417 for (OptionInfo option : info.options) { 418 OptionInfo conflict = uniqueness.put(option.name, option); 419 if (conflict != null) { 420 processingEnv.getMessager().printMessage(Kind.ERROR, "Duplicate option names for " + option + " and " + conflict, e.getKey()); 421 ok = false; 422 } 423 } 424 } 425 426 if (ok) { 427 for (OptionsInfo info : map.values()) { 428 createOptionsDescriptorsFile(processingEnv, info); 429 } 430 } 431 432 return true; 433 } 434 }