1 /* 2 * Copyright (c) 2013, 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 package org.graalvm.compiler.options.processor; 24 25 import java.io.BufferedReader; 26 import java.io.IOException; 27 import java.io.InputStreamReader; 28 import java.io.PrintWriter; 29 import java.util.ArrayList; 30 import java.util.Arrays; 31 import java.util.Collections; 32 import java.util.HashMap; 33 import java.util.HashSet; 34 import java.util.List; 35 import java.util.Map; 36 import java.util.Set; 37 38 import javax.annotation.processing.AbstractProcessor; 39 import javax.annotation.processing.Filer; 40 import javax.annotation.processing.RoundEnvironment; 41 import javax.annotation.processing.SupportedAnnotationTypes; 42 import javax.lang.model.SourceVersion; 43 import javax.lang.model.element.Element; 44 import javax.lang.model.element.ElementKind; 45 import javax.lang.model.element.Modifier; 46 import javax.lang.model.element.Name; 47 import javax.lang.model.element.PackageElement; 48 import javax.lang.model.element.TypeElement; 49 import javax.lang.model.element.VariableElement; 50 import javax.lang.model.type.DeclaredType; 51 import javax.lang.model.type.TypeKind; 52 import javax.lang.model.type.TypeMirror; 53 import javax.lang.model.util.Elements; 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.options.Option; 61 import org.graalvm.compiler.options.OptionDescriptor; 62 import org.graalvm.compiler.options.OptionDescriptors; 63 import org.graalvm.compiler.options.OptionKey; 64 65 /** 66 * Processes static fields annotated with {@link Option}. An {@link OptionDescriptors} 67 * implementation is generated for each top level class containing at least one such field. The name 68 * of the generated class for top level class {@code com.foo.Bar} is 69 * {@code com.foo.Bar_OptionDescriptors}. 70 */ 71 @SupportedAnnotationTypes({"org.graalvm.compiler.options.Option"}) 72 public class OptionProcessor extends AbstractProcessor { 73 74 @Override 75 public SourceVersion getSupportedSourceVersion() { 76 return SourceVersion.latest(); 77 } 78 79 private final Set<Element> processed = new HashSet<>(); 80 81 private void processElement(Element element, OptionsInfo info) { 82 83 if (!element.getModifiers().contains(Modifier.STATIC)) { 84 processingEnv.getMessager().printMessage(Kind.ERROR, "Option field must be static", element); 85 return; 86 } 87 if (element.getModifiers().contains(Modifier.PRIVATE)) { 88 processingEnv.getMessager().printMessage(Kind.ERROR, "Option field cannot be private", element); 89 return; 90 } 91 92 Option annotation = element.getAnnotation(Option.class); 93 assert annotation != null; 94 assert element instanceof VariableElement; 95 assert element.getKind() == ElementKind.FIELD; 96 VariableElement field = (VariableElement) element; 97 String fieldName = field.getSimpleName().toString(); 98 99 Elements elements = processingEnv.getElementUtils(); 100 Types types = processingEnv.getTypeUtils(); 101 102 TypeMirror fieldType = field.asType(); 103 if (fieldType.getKind() != TypeKind.DECLARED) { 104 processingEnv.getMessager().printMessage(Kind.ERROR, "Option field must be of type " + OptionKey.class.getName(), element); 105 return; 106 } 107 DeclaredType declaredFieldType = (DeclaredType) fieldType; 108 109 TypeMirror optionKeyType = elements.getTypeElement(OptionKey.class.getName()).asType(); 110 if (!types.isSubtype(fieldType, types.erasure(optionKeyType))) { 111 String msg = String.format("Option field type %s is not a subclass of %s", fieldType, optionKeyType); 112 processingEnv.getMessager().printMessage(Kind.ERROR, msg, element); 113 return; 114 } 115 116 if (!field.getModifiers().contains(Modifier.STATIC)) { 117 processingEnv.getMessager().printMessage(Kind.ERROR, "Option field must be static", element); 118 return; 119 } 120 if (field.getModifiers().contains(Modifier.PRIVATE)) { 121 processingEnv.getMessager().printMessage(Kind.ERROR, "Option field cannot be private", element); 122 return; 123 } 124 125 String optionName = annotation.name(); 126 if (optionName.equals("")) { 127 optionName = fieldName; 128 } 129 130 if (!Character.isUpperCase(optionName.charAt(0))) { 131 processingEnv.getMessager().printMessage(Kind.ERROR, "Option name must start with an upper case letter", element); 132 return; 133 } 134 135 DeclaredType declaredOptionKeyType = declaredFieldType; 136 while (!types.isSameType(types.erasure(declaredOptionKeyType), types.erasure(optionKeyType))) { 137 List<? extends TypeMirror> directSupertypes = types.directSupertypes(declaredFieldType); 138 assert !directSupertypes.isEmpty(); 139 declaredOptionKeyType = (DeclaredType) directSupertypes.get(0); 140 } 141 142 assert !declaredOptionKeyType.getTypeArguments().isEmpty(); 143 String optionType = declaredOptionKeyType.getTypeArguments().get(0).toString(); 144 if (optionType.startsWith("java.lang.")) { 145 optionType = optionType.substring("java.lang.".length()); 146 } 147 148 Element enclosing = element.getEnclosingElement(); 149 String declaringClass = ""; 150 String separator = ""; 151 Set<Element> originatingElementsList = info.originatingElements; 152 originatingElementsList.add(field); 153 PackageElement enclosingPackage = null; 154 while (enclosing != null) { 155 if (enclosing.getKind() == ElementKind.CLASS || enclosing.getKind() == ElementKind.INTERFACE) { 156 if (enclosing.getModifiers().contains(Modifier.PRIVATE)) { 157 String msg = String.format("Option field cannot be declared in a private %s %s", enclosing.getKind().name().toLowerCase(), enclosing); 158 processingEnv.getMessager().printMessage(Kind.ERROR, msg, element); 159 return; 160 } 161 originatingElementsList.add(enclosing); 162 declaringClass = enclosing.getSimpleName() + separator + declaringClass; 163 separator = "."; 164 } else if (enclosing.getKind() == ElementKind.PACKAGE) { 165 enclosingPackage = (PackageElement) enclosing; 166 } 167 enclosing = enclosing.getEnclosingElement(); 168 } 169 if (enclosingPackage == null) { 170 processingEnv.getMessager().printMessage(Kind.ERROR, "Option field cannot be declared in the unnamed package", element); 171 return; 172 } 173 String[] helpValue = annotation.help(); 174 String help = ""; 175 String[] extraHelp = {}; 176 177 if (helpValue.length == 1) { 178 help = helpValue[0]; 179 if (help.startsWith("file:")) { 180 String path = help.substring("file:".length()); 181 Filer filer = processingEnv.getFiler(); 182 try { 183 FileObject file; 184 try { 185 file = filer.getResource(StandardLocation.SOURCE_PATH, enclosingPackage.getQualifiedName(), path); 186 } catch (IllegalArgumentException | IOException e) { 187 // Handle the case when a compiler doesn't support the SOURCE_PATH location 188 file = filer.getResource(StandardLocation.CLASS_OUTPUT, enclosingPackage.getQualifiedName(), path); 189 } 190 try (BufferedReader br = new BufferedReader(new InputStreamReader(file.openInputStream()))) { 191 help = br.readLine(); 192 if (help == null) { 193 help = ""; 194 } 195 String line = br.readLine(); 196 List<String> lines = new ArrayList<>(); 197 while (line != null) { 198 lines.add(line); 199 line = br.readLine(); 200 } 201 extraHelp = lines.toArray(new String[lines.size()]); 202 } 203 } catch (IOException e) { 204 String msg = String.format("Error reading %s containing the help text for option field: %s", path, e); 205 processingEnv.getMessager().printMessage(Kind.ERROR, msg, element); 206 return; 207 } 208 } 209 } else if (helpValue.length > 1) { 210 help = helpValue[0]; 211 extraHelp = Arrays.copyOfRange(helpValue, 1, helpValue.length); 212 } 213 if (help.length() != 0) { 214 char firstChar = help.charAt(0); 215 if (!Character.isUpperCase(firstChar)) { 216 processingEnv.getMessager().printMessage(Kind.ERROR, "Option help text must start with an upper case letter", element); 217 return; 218 } 219 } 220 221 info.options.add(new OptionInfo(optionName, help, extraHelp, optionType, declaringClass, field)); 222 } 223 224 private void createFiles(OptionsInfo info) { 225 String pkg = ((PackageElement) info.topDeclaringType.getEnclosingElement()).getQualifiedName().toString(); 226 Name topDeclaringClass = info.topDeclaringType.getSimpleName(); 227 Element[] originatingElements = info.originatingElements.toArray(new Element[info.originatingElements.size()]); 228 229 createOptionsDescriptorsFile(info, pkg, topDeclaringClass, originatingElements); 230 } 231 232 private void createOptionsDescriptorsFile(OptionsInfo info, String pkg, Name topDeclaringClass, Element[] originatingElements) { 233 String optionsClassName = topDeclaringClass + "_" + OptionDescriptors.class.getSimpleName(); 234 235 Filer filer = processingEnv.getFiler(); 236 try (PrintWriter out = createSourceFile(pkg, optionsClassName, 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: " + topDeclaringClass + ".java"); 242 out.println("package " + pkg + ";"); 243 out.println(""); 244 out.println("import java.util.*;"); 245 out.println("import " + OptionDescriptors.class.getPackage().getName() + ".*;"); 246 out.println(""); 247 out.println("public class " + optionsClassName + " implements " + OptionDescriptors.class.getSimpleName() + " {"); 248 249 String desc = OptionDescriptor.class.getSimpleName(); 250 251 Collections.sort(info.options); 252 253 out.println(" @Override"); 254 out.println(" public OptionDescriptor get(String value) {"); 255 out.println(" switch (value) {"); 256 out.println(" // CheckStyle: stop line length check"); 257 for (OptionInfo option : info.options) { 258 String name = option.name; 259 String optionField; 260 if (option.field.getModifiers().contains(Modifier.PRIVATE)) { 261 throw new InternalError(); 262 } else { 263 optionField = option.declaringClass + "." + option.field.getSimpleName(); 264 } 265 out.println(" case \"" + name + "\": {"); 266 String type = option.type; 267 String help = option.help; 268 String[] extraHelp = option.extraHelp; 269 String declaringClass = option.declaringClass; 270 Name fieldName = option.field.getSimpleName(); 271 out.printf(" return " + desc + ".create(\n"); 272 out.printf(" /*name*/ \"%s\",\n", name); 273 out.printf(" /*type*/ %s.class,\n", type); 274 out.printf(" /*help*/ \"%s\",\n", help); 275 if (extraHelp.length != 0) { 276 out.printf(" /*extraHelp*/ new String[] {\n"); 277 for (String line : extraHelp) { 278 out.printf(" \"%s\",\n", line.replace("\\", "\\\\").replace("\"", "\\\"")); 279 } 280 out.printf(" },\n"); 281 } 282 out.printf(" /*declaringClass*/ %s.class,\n", declaringClass); 283 out.printf(" /*fieldName*/ \"%s\",\n", fieldName); 284 out.printf(" /*option*/ %s);\n", optionField); 285 out.println(" }"); 286 } 287 out.println(" // CheckStyle: resume line length check"); 288 out.println(" }"); 289 out.println(" return null;"); 290 out.println(" }"); 291 out.println(); 292 out.println(" @Override"); 293 out.println(" public Iterator<" + desc + "> iterator() {"); 294 out.println(" return new Iterator<OptionDescriptor>() {"); 295 out.println(" int i = 0;"); 296 out.println(" @Override"); 297 out.println(" public boolean hasNext() {"); 298 out.println(" return i < " + info.options.size() + ";"); 299 out.println(" }"); 300 out.println(" @Override"); 301 out.println(" public OptionDescriptor next() {"); 302 out.println(" switch (i++) {"); 303 for (int i = 0; i < info.options.size(); i++) { 304 OptionInfo option = info.options.get(i); 305 out.println(" case " + i + ": return get(\"" + option.name + "\");"); 306 } 307 out.println(" }"); 308 out.println(" throw new NoSuchElementException();"); 309 out.println(" }"); 310 out.println(" };"); 311 out.println(" }"); 312 out.println("}"); 313 } 314 } 315 316 protected PrintWriter createSourceFile(String pkg, String relativeName, Filer filer, Element... originatingElements) { 317 try { 318 // Ensure Unix line endings to comply with code style guide checked by Checkstyle 319 JavaFileObject sourceFile = filer.createSourceFile(pkg + "." + relativeName, originatingElements); 320 return new PrintWriter(sourceFile.openWriter()) { 321 322 @Override 323 public void println() { 324 print("\n"); 325 } 326 }; 327 } catch (IOException e) { 328 throw new RuntimeException(e); 329 } 330 } 331 332 static class OptionInfo implements Comparable<OptionInfo> { 333 334 final String name; 335 final String help; 336 final String[] extraHelp; 337 final String type; 338 final String declaringClass; 339 final VariableElement field; 340 341 OptionInfo(String name, String help, String[] extraHelp, String type, String declaringClass, VariableElement field) { 342 this.name = name; 343 this.help = help; 344 this.extraHelp = extraHelp; 345 this.type = type; 346 this.declaringClass = declaringClass; 347 this.field = field; 348 } 349 350 @Override 351 public int compareTo(OptionInfo other) { 352 return name.compareTo(other.name); 353 } 354 355 @Override 356 public String toString() { 357 return declaringClass + "." + field; 358 } 359 } 360 361 static class OptionsInfo { 362 363 final Element topDeclaringType; 364 final List<OptionInfo> options = new ArrayList<>(); 365 final Set<Element> originatingElements = new HashSet<>(); 366 367 OptionsInfo(Element topDeclaringType) { 368 this.topDeclaringType = topDeclaringType; 369 } 370 } 371 372 private static Element topDeclaringType(Element element) { 373 Element enclosing = element.getEnclosingElement(); 374 if (enclosing == null || enclosing.getKind() == ElementKind.PACKAGE) { 375 assert element.getKind() == ElementKind.CLASS || element.getKind() == ElementKind.INTERFACE; 376 return element; 377 } 378 return topDeclaringType(enclosing); 379 } 380 381 @Override 382 public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { 383 if (roundEnv.processingOver()) { 384 return true; 385 } 386 387 Map<Element, OptionsInfo> map = new HashMap<>(); 388 for (Element element : roundEnv.getElementsAnnotatedWith(Option.class)) { 389 if (!processed.contains(element)) { 390 processed.add(element); 391 Element topDeclaringType = topDeclaringType(element); 392 OptionsInfo options = map.get(topDeclaringType); 393 if (options == null) { 394 options = new OptionsInfo(topDeclaringType); 395 map.put(topDeclaringType, options); 396 } 397 if (!element.getEnclosingElement().getSimpleName().toString().endsWith("Options")) { 398 processingEnv.getMessager().printMessage(Kind.ERROR, "Option declaring classes must have a name that ends with 'Options'", element.getEnclosingElement()); 399 } 400 processElement(element, options); 401 } 402 } 403 404 boolean ok = true; 405 Map<String, OptionInfo> uniqueness = new HashMap<>(); 406 for (OptionsInfo info : map.values()) { 407 for (OptionInfo option : info.options) { 408 OptionInfo conflict = uniqueness.put(option.name, option); 409 if (conflict != null) { 410 processingEnv.getMessager().printMessage(Kind.ERROR, "Duplicate option names for " + option + " and " + conflict, option.field); 411 ok = false; 412 } 413 } 414 } 415 416 if (ok) { 417 for (OptionsInfo info : map.values()) { 418 createFiles(info); 419 } 420 } 421 422 return true; 423 } 424 }