blob: 0ad4bbd01cab6aabb98e330052af837517263cb6 [file] [log] [blame]
/*
* Copyright (c) 2013, 2018, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.graalvm.compiler.options.processor;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.processing.Filer;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.Name;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic.Kind;
import javax.tools.FileObject;
import javax.tools.JavaFileObject;
import javax.tools.StandardLocation;
import org.graalvm.compiler.processor.AbstractProcessor;
/**
* Processes static fields annotated with {@code Option}. An {@code OptionDescriptors}
* implementation is generated for each top level class containing at least one such field. The name
* of the generated class for top level class {@code com.foo.Bar} is
* {@code com.foo.Bar_OptionDescriptors}.
*/
@SupportedAnnotationTypes({"org.graalvm.compiler.options.Option"})
public class OptionProcessor extends AbstractProcessor {
private static final String OPTION_CLASS_NAME = "org.graalvm.compiler.options.Option";
private static final String OPTION_KEY_CLASS_NAME = "org.graalvm.compiler.options.OptionKey";
private static final String OPTION_TYPE_CLASS_NAME = "org.graalvm.compiler.options.OptionType";
private static final String OPTION_DESCRIPTOR_CLASS_NAME = "org.graalvm.compiler.options.OptionDescriptor";
private static final String OPTION_DESCRIPTORS_CLASS_NAME = "org.graalvm.compiler.options.OptionDescriptors";
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latest();
}
private final Set<Element> processed = new HashSet<>();
private TypeMirror optionTypeMirror;
private TypeMirror optionKeyTypeMirror;
private void processElement(Element element, OptionsInfo info) {
if (!element.getModifiers().contains(Modifier.STATIC)) {
processingEnv.getMessager().printMessage(Kind.ERROR, "Option field must be static", element);
return;
}
if (element.getModifiers().contains(Modifier.PRIVATE)) {
processingEnv.getMessager().printMessage(Kind.ERROR, "Option field cannot be private", element);
return;
}
AnnotationMirror annotation = getAnnotation(element, optionTypeMirror);
assert annotation != null;
assert element instanceof VariableElement;
assert element.getKind() == ElementKind.FIELD;
VariableElement field = (VariableElement) element;
String fieldName = field.getSimpleName().toString();
Types types = processingEnv.getTypeUtils();
TypeMirror fieldType = field.asType();
if (fieldType.getKind() != TypeKind.DECLARED) {
processingEnv.getMessager().printMessage(Kind.ERROR, "Option field must be of type " + OPTION_KEY_CLASS_NAME, element);
return;
}
DeclaredType declaredFieldType = (DeclaredType) fieldType;
if (!types.isSubtype(fieldType, types.erasure(optionKeyTypeMirror))) {
String msg = String.format("Option field type %s is not a subclass of %s", fieldType, optionKeyTypeMirror);
processingEnv.getMessager().printMessage(Kind.ERROR, msg, element);
return;
}
if (!field.getModifiers().contains(Modifier.STATIC)) {
processingEnv.getMessager().printMessage(Kind.ERROR, "Option field must be static", element);
return;
}
if (field.getModifiers().contains(Modifier.PRIVATE)) {
processingEnv.getMessager().printMessage(Kind.ERROR, "Option field cannot be private", element);
return;
}
String optionName = getAnnotationValue(annotation, "name", String.class);
if (optionName.equals("")) {
optionName = fieldName;
}
if (!Character.isUpperCase(optionName.charAt(0))) {
processingEnv.getMessager().printMessage(Kind.ERROR, "Option name must start with an upper case letter", element);
return;
}
DeclaredType declaredOptionKeyType = declaredFieldType;
while (!types.isSameType(types.erasure(declaredOptionKeyType), types.erasure(optionKeyTypeMirror))) {
List<? extends TypeMirror> directSupertypes = types.directSupertypes(declaredFieldType);
assert !directSupertypes.isEmpty();
declaredOptionKeyType = (DeclaredType) directSupertypes.get(0);
}
assert !declaredOptionKeyType.getTypeArguments().isEmpty();
String optionType = declaredOptionKeyType.getTypeArguments().get(0).toString();
if (optionType.startsWith("java.lang.")) {
optionType = optionType.substring("java.lang.".length());
}
Element enclosing = element.getEnclosingElement();
String declaringClass = "";
String separator = "";
Set<Element> originatingElementsList = info.originatingElements;
originatingElementsList.add(field);
PackageElement enclosingPackage = null;
while (enclosing != null) {
if (enclosing.getKind() == ElementKind.CLASS || enclosing.getKind() == ElementKind.INTERFACE) {
if (enclosing.getModifiers().contains(Modifier.PRIVATE)) {
String msg = String.format("Option field cannot be declared in a private %s %s", enclosing.getKind().name().toLowerCase(), enclosing);
processingEnv.getMessager().printMessage(Kind.ERROR, msg, element);
return;
}
originatingElementsList.add(enclosing);
declaringClass = enclosing.getSimpleName() + separator + declaringClass;
separator = ".";
} else if (enclosing.getKind() == ElementKind.PACKAGE) {
enclosingPackage = (PackageElement) enclosing;
}
enclosing = enclosing.getEnclosingElement();
}
if (enclosingPackage == null) {
processingEnv.getMessager().printMessage(Kind.ERROR, "Option field cannot be declared in the unnamed package", element);
return;
}
List<String> helpValue = getAnnotationValueList(annotation, "help", String.class);
String help = "";
List<String> extraHelp = new ArrayList<>();
if (helpValue.size() == 1) {
help = helpValue.get(0);
if (help.startsWith("file:")) {
String path = help.substring("file:".length());
Filer filer = processingEnv.getFiler();
try {
FileObject file;
try {
file = filer.getResource(StandardLocation.SOURCE_PATH, enclosingPackage.getQualifiedName(), path);
} catch (IllegalArgumentException | IOException e) {
// Handle the case when a compiler doesn't support the SOURCE_PATH location
file = filer.getResource(StandardLocation.CLASS_OUTPUT, enclosingPackage.getQualifiedName(), path);
}
try (BufferedReader br = new BufferedReader(new InputStreamReader(file.openInputStream()))) {
help = br.readLine();
if (help == null) {
help = "";
}
String line = br.readLine();
while (line != null) {
extraHelp.add(line);
line = br.readLine();
}
}
} catch (IOException e) {
String msg = String.format("Error reading %s containing the help text for option field: %s", path, e);
processingEnv.getMessager().printMessage(Kind.ERROR, msg, element);
return;
}
}
} else if (helpValue.size() > 1) {
help = helpValue.get(0);
extraHelp = helpValue.subList(1, helpValue.size());
}
if (help.length() != 0) {
char firstChar = help.charAt(0);
if (!Character.isUpperCase(firstChar)) {
processingEnv.getMessager().printMessage(Kind.ERROR, "Option help text must start with an upper case letter", element);
return;
}
}
String optionTypeName = getAnnotationValue(annotation, "type", VariableElement.class).getSimpleName().toString();
info.options.add(new OptionInfo(optionName, optionTypeName, help, extraHelp, optionType, declaringClass, field));
}
private void createFiles(OptionsInfo info) {
String pkg = ((PackageElement) info.topDeclaringType.getEnclosingElement()).getQualifiedName().toString();
Name topDeclaringClass = info.topDeclaringType.getSimpleName();
Element[] originatingElements = info.originatingElements.toArray(new Element[info.originatingElements.size()]);
createOptionsDescriptorsFile(info, pkg, topDeclaringClass, originatingElements);
}
private void createOptionsDescriptorsFile(OptionsInfo info, String pkg, Name topDeclaringClass, Element[] originatingElements) {
String optionsClassName = topDeclaringClass + "_" + getSimpleName(OPTION_DESCRIPTORS_CLASS_NAME);
Filer filer = processingEnv.getFiler();
try (PrintWriter out = createSourceFile(pkg, optionsClassName, filer, originatingElements)) {
out.println("// CheckStyle: stop header check");
out.println("// CheckStyle: stop line length check");
out.println("// GENERATED CONTENT - DO NOT EDIT");
out.println("// Source: " + topDeclaringClass + ".java");
out.println("package " + pkg + ";");
out.println("");
out.println("import java.util.*;");
out.println("import " + getPackageName(OPTION_DESCRIPTORS_CLASS_NAME) + ".*;");
out.println("import " + OPTION_TYPE_CLASS_NAME + ";");
out.println("");
out.println("public class " + optionsClassName + " implements " + getSimpleName(OPTION_DESCRIPTORS_CLASS_NAME) + " {");
String desc = getSimpleName(OPTION_DESCRIPTOR_CLASS_NAME);
Collections.sort(info.options);
out.println(" @Override");
out.println(" public OptionDescriptor get(String value) {");
out.println(" switch (value) {");
out.println(" // CheckStyle: stop line length check");
for (OptionInfo option : info.options) {
String name = option.name;
String optionField;
if (option.field.getModifiers().contains(Modifier.PRIVATE)) {
throw new InternalError();
} else {
optionField = option.declaringClass + "." + option.field.getSimpleName();
}
out.println(" case \"" + name + "\": {");
String optionType = option.optionType;
String type = option.type;
String help = option.help;
List<String> extraHelp = option.extraHelp;
String declaringClass = option.declaringClass;
Name fieldName = option.field.getSimpleName();
out.printf(" return " + desc + ".create(\n");
out.printf(" /*name*/ \"%s\",\n", name);
out.printf(" /*optionType*/ %s.%s,\n", getSimpleName(OPTION_TYPE_CLASS_NAME), optionType);
out.printf(" /*optionValueType*/ %s.class,\n", type);
out.printf(" /*help*/ \"%s\",\n", help);
if (extraHelp.size() != 0) {
out.printf(" /*extraHelp*/ new String[] {\n");
for (String line : extraHelp) {
out.printf(" \"%s\",\n", line.replace("\\", "\\\\").replace("\"", "\\\""));
}
out.printf(" },\n");
}
out.printf(" /*declaringClass*/ %s.class,\n", declaringClass);
out.printf(" /*fieldName*/ \"%s\",\n", fieldName);
out.printf(" /*option*/ %s);\n", optionField);
out.println(" }");
}
out.println(" // CheckStyle: resume line length check");
out.println(" }");
out.println(" return null;");
out.println(" }");
out.println();
out.println(" @Override");
out.println(" public Iterator<" + desc + "> iterator() {");
out.println(" return new Iterator<OptionDescriptor>() {");
out.println(" int i = 0;");
out.println(" @Override");
out.println(" public boolean hasNext() {");
out.println(" return i < " + info.options.size() + ";");
out.println(" }");
out.println(" @Override");
out.println(" public OptionDescriptor next() {");
out.println(" switch (i++) {");
for (int i = 0; i < info.options.size(); i++) {
OptionInfo option = info.options.get(i);
out.println(" case " + i + ": return get(\"" + option.name + "\");");
}
out.println(" }");
out.println(" throw new NoSuchElementException();");
out.println(" }");
out.println(" };");
out.println(" }");
out.println("}");
}
}
protected PrintWriter createSourceFile(String pkg, String relativeName, Filer filer, Element... originatingElements) {
try {
// Ensure Unix line endings to comply with code style guide checked by Checkstyle
JavaFileObject sourceFile = filer.createSourceFile(pkg + "." + relativeName, originatingElements);
return new PrintWriter(sourceFile.openWriter()) {
@Override
public void println() {
print("\n");
}
};
} catch (IOException e) {
throw new RuntimeException(e);
}
}
static class OptionInfo implements Comparable<OptionInfo> {
final String name;
final String optionType;
final String help;
final List<String> extraHelp;
final String type;
final String declaringClass;
final VariableElement field;
OptionInfo(String name, String optionType, String help, List<String> extraHelp, String type, String declaringClass, VariableElement field) {
this.name = name;
this.optionType = optionType;
this.help = help;
this.extraHelp = extraHelp;
this.type = type;
this.declaringClass = declaringClass;
this.field = field;
}
@Override
public int compareTo(OptionInfo other) {
return name.compareTo(other.name);
}
@Override
public String toString() {
return declaringClass + "." + field;
}
}
static class OptionsInfo {
final Element topDeclaringType;
final List<OptionInfo> options = new ArrayList<>();
final Set<Element> originatingElements = new HashSet<>();
OptionsInfo(Element topDeclaringType) {
this.topDeclaringType = topDeclaringType;
}
}
private static Element topDeclaringType(Element element) {
Element enclosing = element.getEnclosingElement();
if (enclosing == null || enclosing.getKind() == ElementKind.PACKAGE) {
assert element.getKind() == ElementKind.CLASS || element.getKind() == ElementKind.INTERFACE;
return element;
}
return topDeclaringType(enclosing);
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (roundEnv.processingOver()) {
return true;
}
TypeElement optionTypeElement = getTypeElement(OPTION_CLASS_NAME);
optionTypeMirror = optionTypeElement.asType();
optionKeyTypeMirror = getTypeElement(OPTION_KEY_CLASS_NAME).asType();
Map<Element, OptionsInfo> map = new HashMap<>();
for (Element element : roundEnv.getElementsAnnotatedWith(optionTypeElement)) {
if (!processed.contains(element)) {
processed.add(element);
Element topDeclaringType = topDeclaringType(element);
OptionsInfo options = map.get(topDeclaringType);
if (options == null) {
options = new OptionsInfo(topDeclaringType);
map.put(topDeclaringType, options);
}
if (!element.getEnclosingElement().getSimpleName().toString().endsWith("Options")) {
processingEnv.getMessager().printMessage(Kind.ERROR, "Option declaring classes must have a name that ends with 'Options'", element.getEnclosingElement());
}
processElement(element, options);
}
}
boolean ok = true;
Map<String, OptionInfo> uniqueness = new HashMap<>();
for (OptionsInfo info : map.values()) {
for (OptionInfo option : info.options) {
OptionInfo conflict = uniqueness.put(option.name, option);
if (conflict != null) {
processingEnv.getMessager().printMessage(Kind.ERROR, "Duplicate option names for " + option + " and " + conflict, option.field);
ok = false;
}
}
}
if (ok) {
for (OptionsInfo info : map.values()) {
createFiles(info);
}
}
return true;
}
}