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 */
23package org.graalvm.compiler.options.processor;
24
25import java.io.IOException;
26import java.io.PrintWriter;
27import java.util.ArrayList;
28import java.util.Collections;
29import java.util.HashMap;
30import java.util.HashSet;
31import java.util.List;
32import java.util.Map;
33import java.util.Set;
34
35import javax.annotation.processing.AbstractProcessor;
36import javax.annotation.processing.Filer;
37import javax.annotation.processing.RoundEnvironment;
38import javax.annotation.processing.SupportedAnnotationTypes;
39import javax.lang.model.SourceVersion;
40import javax.lang.model.element.Element;
41import javax.lang.model.element.ElementKind;
42import javax.lang.model.element.Modifier;
43import javax.lang.model.element.Name;
44import javax.lang.model.element.PackageElement;
45import javax.lang.model.element.TypeElement;
46import javax.lang.model.element.VariableElement;
47import javax.lang.model.type.DeclaredType;
48import javax.lang.model.type.TypeKind;
49import javax.lang.model.type.TypeMirror;
50import javax.lang.model.util.Elements;
51import javax.lang.model.util.Types;
52import javax.tools.Diagnostic.Kind;
53import javax.tools.JavaFileObject;
54
55import org.graalvm.compiler.options.Option;
56import org.graalvm.compiler.options.OptionDescriptor;
57import org.graalvm.compiler.options.OptionDescriptors;
58import org.graalvm.compiler.options.OptionValue;
59
60/**
61 * Processes static fields annotated with {@link Option}. An {@link OptionDescriptors}
62 * implementation is generated for each top level class containing at least one such field. The name
63 * of the generated class for top level class {@code com.foo.Bar} is
64 * {@code com.foo.Bar_OptionDescriptors}.
65 */
66@SupportedAnnotationTypes({"org.graalvm.compiler.options.Option"})
67public class OptionProcessor extends AbstractProcessor {
68
69    @Override
70    public SourceVersion getSupportedSourceVersion() {
71        return SourceVersion.latest();
72    }
73
74    private final Set<Element> processed = new HashSet<>();
75
76    private void processElement(Element element, OptionsInfo info) {
77
78        if (!element.getModifiers().contains(Modifier.STATIC)) {
79            processingEnv.getMessager().printMessage(Kind.ERROR, "Option field must be static", element);
80            return;
81        }
82        if (element.getModifiers().contains(Modifier.PRIVATE)) {
83            processingEnv.getMessager().printMessage(Kind.ERROR, "Option field cannot be private", element);
84            return;
85        }
86
87        Option annotation = element.getAnnotation(Option.class);
88        assert annotation != null;
89        assert element instanceof VariableElement;
90        assert element.getKind() == ElementKind.FIELD;
91        VariableElement field = (VariableElement) element;
92        String fieldName = field.getSimpleName().toString();
93
94        Elements elements = processingEnv.getElementUtils();
95        Types types = processingEnv.getTypeUtils();
96
97        TypeMirror fieldType = field.asType();
98        if (fieldType.getKind() != TypeKind.DECLARED) {
99            processingEnv.getMessager().printMessage(Kind.ERROR, "Option field must be of type " + OptionValue.class.getName(), element);
100            return;
101        }
102        DeclaredType declaredFieldType = (DeclaredType) fieldType;
103
104        TypeMirror optionValueType = elements.getTypeElement(OptionValue.class.getName()).asType();
105        if (!types.isSubtype(fieldType, types.erasure(optionValueType))) {
106            String msg = String.format("Option field type %s is not a subclass of %s", fieldType, optionValueType);
107            processingEnv.getMessager().printMessage(Kind.ERROR, msg, element);
108            return;
109        }
110
111        if (!field.getModifiers().contains(Modifier.STATIC)) {
112            processingEnv.getMessager().printMessage(Kind.ERROR, "Option field must be static", element);
113            return;
114        }
115        if (field.getModifiers().contains(Modifier.PRIVATE)) {
116            processingEnv.getMessager().printMessage(Kind.ERROR, "Option field cannot be private", element);
117            return;
118        }
119
120        String help = annotation.help();
121        if (help.length() != 0) {
122            char firstChar = help.charAt(0);
123            if (!Character.isUpperCase(firstChar)) {
124                processingEnv.getMessager().printMessage(Kind.ERROR, "Option help text must start with upper case letter", element);
125                return;
126            }
127        }
128
129        String optionName = annotation.name();
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 capital letter", element);
136            return;
137        }
138
139        DeclaredType declaredOptionValueType = declaredFieldType;
140        while (!types.isSameType(types.erasure(declaredOptionValueType), types.erasure(optionValueType))) {
141            List<? extends TypeMirror> directSupertypes = types.directSupertypes(declaredFieldType);
142            assert !directSupertypes.isEmpty();
143            declaredOptionValueType = (DeclaredType) directSupertypes.get(0);
144        }
145
146        assert !declaredOptionValueType.getTypeArguments().isEmpty();
147        String optionType = declaredOptionValueType.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        while (enclosing != null) {
158            if (enclosing.getKind() == ElementKind.CLASS || enclosing.getKind() == ElementKind.INTERFACE) {
159                if (enclosing.getModifiers().contains(Modifier.PRIVATE)) {
160                    String msg = String.format("Option field cannot be declared in a private %s %s", enclosing.getKind().name().toLowerCase(), enclosing);
161                    processingEnv.getMessager().printMessage(Kind.ERROR, msg, element);
162                    return;
163                }
164                originatingElementsList.add(enclosing);
165                declaringClass = enclosing.getSimpleName() + separator + declaringClass;
166                separator = ".";
167            } else {
168                assert enclosing.getKind() == ElementKind.PACKAGE;
169            }
170            enclosing = enclosing.getEnclosingElement();
171        }
172
173        info.options.add(new OptionInfo(optionName, help, optionType, declaringClass, field));
174    }
175
176    private void createFiles(OptionsInfo info) {
177        String pkg = ((PackageElement) info.topDeclaringType.getEnclosingElement()).getQualifiedName().toString();
178        Name topDeclaringClass = info.topDeclaringType.getSimpleName();
179        Element[] originatingElements = info.originatingElements.toArray(new Element[info.originatingElements.size()]);
180
181        createOptionsDescriptorsFile(info, pkg, topDeclaringClass, originatingElements);
182    }
183
184    private void createOptionsDescriptorsFile(OptionsInfo info, String pkg, Name topDeclaringClass, Element[] originatingElements) {
185        String optionsClassName = topDeclaringClass + "_" + OptionDescriptors.class.getSimpleName();
186
187        Filer filer = processingEnv.getFiler();
188        try (PrintWriter out = createSourceFile(pkg, optionsClassName, filer, originatingElements)) {
189
190            out.println("// CheckStyle: stop header check");
191            out.println("// CheckStyle: stop line length check");
192            out.println("// GENERATED CONTENT - DO NOT EDIT");
193            out.println("// Source: " + topDeclaringClass + ".java");
194            out.println("package " + pkg + ";");
195            out.println("");
196            out.println("import java.util.*;");
197            out.println("import " + OptionDescriptors.class.getPackage().getName() + ".*;");
198            out.println("");
199            out.println("public class " + optionsClassName + " implements " + OptionDescriptors.class.getSimpleName() + " {");
200
201            String desc = OptionDescriptor.class.getSimpleName();
202
203            int i = 0;
204            Collections.sort(info.options);
205
206            out.println("    @Override");
207            out.println("    public OptionDescriptor get(String value) {");
208            out.println("        // CheckStyle: stop line length check");
209            if (info.options.size() == 1) {
210                out.println("        if (value.equals(\"" + info.options.get(0).name + "\")) {");
211            } else {
212                out.println("        switch (value) {");
213            }
214            for (OptionInfo option : info.options) {
215                String name = option.name;
216                String optionValue;
217                if (option.field.getModifiers().contains(Modifier.PRIVATE)) {
218                    throw new InternalError();
219                } else {
220                    optionValue = option.declaringClass + "." + option.field.getSimpleName();
221                }
222                String type = option.type;
223                String help = option.help;
224                String declaringClass = option.declaringClass;
225                Name fieldName = option.field.getSimpleName();
226                if (info.options.size() == 1) {
227                    out.printf("            return %s.create(\"%s\", %s.class, \"%s\", %s.class, \"%s\", %s);\n", desc, name, type, help, declaringClass, fieldName, optionValue);
228                } else {
229                    out.printf("            case \"" + name + "\": return %s.create(\"%s\", %s.class, \"%s\", %s.class, \"%s\", %s);\n", desc, name, type, help, declaringClass, fieldName,
230                                    optionValue);
231                }
232            }
233            out.println("        }");
234            out.println("        // CheckStyle: resume line length check");
235            out.println("        return null;");
236            out.println("    }");
237            out.println();
238            out.println("    @Override");
239            out.println("    public Iterator<" + desc + "> iterator() {");
240            out.println("        // CheckStyle: stop line length check");
241            out.println("        List<" + desc + "> options = Arrays.asList(");
242            for (OptionInfo option : info.options) {
243                String optionValue;
244                if (option.field.getModifiers().contains(Modifier.PRIVATE)) {
245                    throw new InternalError();
246                } else {
247                    optionValue = option.declaringClass + "." + option.field.getSimpleName();
248                }
249                String name = option.name;
250                String type = option.type;
251                String help = option.help;
252                String declaringClass = option.declaringClass;
253                Name fieldName = option.field.getSimpleName();
254                String comma = i == info.options.size() - 1 ? "" : ",";
255                out.printf("            %s.create(\"%s\", %s.class, \"%s\", %s.class, \"%s\", %s)%s\n", desc, name, type, help, declaringClass, fieldName, optionValue, comma);
256                i++;
257            }
258            out.println("        );");
259            out.println("        // CheckStyle: resume line length check");
260            out.println("        return options.iterator();");
261            out.println("    }");
262            out.println("}");
263        }
264    }
265
266    protected PrintWriter createSourceFile(String pkg, String relativeName, Filer filer, Element... originatingElements) {
267        try {
268            // Ensure Unix line endings to comply with code style guide checked by Checkstyle
269            JavaFileObject sourceFile = filer.createSourceFile(pkg + "." + relativeName, originatingElements);
270            return new PrintWriter(sourceFile.openWriter()) {
271
272                @Override
273                public void println() {
274                    print("\n");
275                }
276            };
277        } catch (IOException e) {
278            throw new RuntimeException(e);
279        }
280    }
281
282    static class OptionInfo implements Comparable<OptionInfo> {
283
284        final String name;
285        final String help;
286        final String type;
287        final String declaringClass;
288        final VariableElement field;
289
290        OptionInfo(String name, String help, String type, String declaringClass, VariableElement field) {
291            this.name = name;
292            this.help = help;
293            this.type = type;
294            this.declaringClass = declaringClass;
295            this.field = field;
296        }
297
298        @Override
299        public int compareTo(OptionInfo other) {
300            return name.compareTo(other.name);
301        }
302
303        @Override
304        public String toString() {
305            return declaringClass + "." + field;
306        }
307    }
308
309    static class OptionsInfo {
310
311        final Element topDeclaringType;
312        final List<OptionInfo> options = new ArrayList<>();
313        final Set<Element> originatingElements = new HashSet<>();
314
315        OptionsInfo(Element topDeclaringType) {
316            this.topDeclaringType = topDeclaringType;
317        }
318    }
319
320    private static Element topDeclaringType(Element element) {
321        Element enclosing = element.getEnclosingElement();
322        if (enclosing == null || enclosing.getKind() == ElementKind.PACKAGE) {
323            assert element.getKind() == ElementKind.CLASS || element.getKind() == ElementKind.INTERFACE;
324            return element;
325        }
326        return topDeclaringType(enclosing);
327    }
328
329    @Override
330    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
331        if (roundEnv.processingOver()) {
332            return true;
333        }
334
335        Map<Element, OptionsInfo> map = new HashMap<>();
336        for (Element element : roundEnv.getElementsAnnotatedWith(Option.class)) {
337            if (!processed.contains(element)) {
338                processed.add(element);
339                Element topDeclaringType = topDeclaringType(element);
340                OptionsInfo options = map.get(topDeclaringType);
341                if (options == null) {
342                    options = new OptionsInfo(topDeclaringType);
343                    map.put(topDeclaringType, options);
344                }
345                processElement(element, options);
346            }
347        }
348
349        boolean ok = true;
350        Map<String, OptionInfo> uniqueness = new HashMap<>();
351        for (OptionsInfo info : map.values()) {
352            for (OptionInfo option : info.options) {
353                OptionInfo conflict = uniqueness.put(option.name, option);
354                if (conflict != null) {
355                    processingEnv.getMessager().printMessage(Kind.ERROR, "Duplicate option names for " + option + " and " + conflict, option.field);
356                    ok = false;
357                }
358            }
359        }
360
361        if (ok) {
362            for (OptionsInfo info : map.values()) {
363                createFiles(info);
364            }
365        }
366
367        return true;
368    }
369}
370