| // Copyright 2017 The Bazel Authors. All rights reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| package com.google.devtools.common.options.processor; |
| |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.devtools.common.options.Converter; |
| import com.google.devtools.common.options.Converters; |
| import com.google.devtools.common.options.ExpansionFunction; |
| import com.google.devtools.common.options.Option; |
| import com.google.devtools.common.options.OptionDocumentationCategory; |
| import com.google.devtools.common.options.OptionEffectTag; |
| import com.google.devtools.common.options.OptionMetadataTag; |
| import com.google.devtools.common.options.OptionsBase; |
| import com.google.devtools.common.options.OptionsParser; |
| import com.google.devtools.common.options.OptionsParsingException; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.regex.Pattern; |
| import java.util.stream.Collectors; |
| import javax.annotation.processing.AbstractProcessor; |
| import javax.annotation.processing.Messager; |
| import javax.annotation.processing.ProcessingEnvironment; |
| 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.ExecutableElement; |
| import javax.lang.model.element.Modifier; |
| import javax.lang.model.element.TypeElement; |
| import javax.lang.model.element.VariableElement; |
| import javax.lang.model.type.DeclaredType; |
| import javax.lang.model.type.ExecutableType; |
| import javax.lang.model.type.PrimitiveType; |
| import javax.lang.model.type.TypeKind; |
| import javax.lang.model.type.TypeMirror; |
| import javax.lang.model.util.Elements; |
| import javax.lang.model.util.Types; |
| import javax.tools.Diagnostic; |
| |
| /** |
| * Annotation processor for {@link Option}. |
| * |
| * <p>Checks the following invariants about {@link Option}-annotated fields ("options"): |
| * <ul> |
| * <li>The {@link OptionsParser} only accepts options in {@link OptionsBase}-inheriting classes |
| * <li>All options must be declared publicly and be neither static nor final. |
| * <li>All options that must be used on the command line must have sensible names without |
| * whitespace or other confusing characters, such as equal signs. |
| * <li>The type of the option must match the converter that will convert the unparsed string value |
| * into the option type. For options that do not specify a converter, check that there is a |
| * valid match in the {@link Converters#DEFAULT_CONVERTERS} list. |
| * <li>Options must list valid combinations of tags and documentation categories. |
| * <li>Expansion options and options with implicit requirements cannot expand in more than one way, |
| * how multiple expansions would interact is not defined and should not be necessary. |
| * </ul> |
| * |
| * <p>These properties can be relied upon at runtime without additional checks. |
| */ |
| @SupportedAnnotationTypes({"com.google.devtools.common.options.Option"}) |
| public final class OptionProcessor extends AbstractProcessor { |
| |
| private Types typeUtils; |
| private Elements elementUtils; |
| private Messager messager; |
| private ImmutableMap<TypeMirror, Converter<?>> defaultConverters; |
| private ImmutableMap<Class<?>, PrimitiveType> primitiveTypeMap; |
| |
| @Override |
| public SourceVersion getSupportedSourceVersion() { |
| return SourceVersion.latestSupported(); |
| } |
| |
| @Override |
| public synchronized void init(ProcessingEnvironment processingEnv) { |
| super.init(processingEnv); |
| typeUtils = processingEnv.getTypeUtils(); |
| elementUtils = processingEnv.getElementUtils(); |
| messager = processingEnv.getMessager(); |
| |
| // Because of the discrepancies between the java.lang and javax.lang type models, we can't |
| // directly use the get() method for the default converter map. Instead, we'll convert it once, |
| // to be more usable, and with the boxed type return values of convert() as the keys. |
| ImmutableMap.Builder<TypeMirror, Converter<?>> converterMapBuilder = |
| new ImmutableMap.Builder<>(); |
| |
| // Create a link from the primitive Classes to their primitive types. This intentionally |
| // only contains the types in the DEFAULT_CONVERTERS map. |
| ImmutableMap.Builder<Class<?>, PrimitiveType> builder = new ImmutableMap.Builder<>(); |
| builder.put(int.class, typeUtils.getPrimitiveType(TypeKind.INT)); |
| builder.put(double.class, typeUtils.getPrimitiveType(TypeKind.DOUBLE)); |
| builder.put(boolean.class, typeUtils.getPrimitiveType(TypeKind.BOOLEAN)); |
| builder.put(long.class, typeUtils.getPrimitiveType(TypeKind.LONG)); |
| primitiveTypeMap = builder.build(); |
| |
| for (Map.Entry<Class<?>, Converter<?>> entry : Converters.DEFAULT_CONVERTERS.entrySet()) { |
| Class<?> converterClass = entry.getKey(); |
| String typeName = converterClass.getCanonicalName(); |
| TypeElement typeElement = elementUtils.getTypeElement(typeName); |
| // Check that we can get a type mirror, either through the type element or the primitive type. |
| if (typeElement != null) { |
| converterMapBuilder.put(typeElement.asType(), entry.getValue()); |
| } else { |
| if (!primitiveTypeMap.containsKey(converterClass)) { |
| messager.printMessage( |
| Diagnostic.Kind.ERROR, |
| String.format("Can't get a TypeElement for Type %s", typeName)); |
| continue; |
| } |
| // Add the primitive types to the map, both in primitive TypeMirror form, and the boxed |
| // classes, such as java.lang.Integer, because primitives must be boxed in collections, |
| // such as allowMultiple options, which have type List<singleOptionType>. |
| PrimitiveType primitiveType = primitiveTypeMap.get(converterClass); |
| converterMapBuilder.put(primitiveType, entry.getValue()); |
| converterMapBuilder.put(typeUtils.boxedClass(primitiveType).asType(), entry.getValue()); |
| } |
| } |
| defaultConverters = converterMapBuilder.build(); |
| } |
| |
| /** Check that the Option variables only occur in OptionBase-inheriting classes. */ |
| private void checkInOptionBase(VariableElement optionField) throws OptionProcessorException { |
| if (optionField.getEnclosingElement().getKind() != ElementKind.CLASS) { |
| throw new OptionProcessorException(optionField, "The field should belong to a class."); |
| } |
| TypeMirror thisOptionClass = optionField.getEnclosingElement().asType(); |
| TypeMirror optionsBase = |
| elementUtils.getTypeElement("com.google.devtools.common.options.OptionsBase").asType(); |
| if (!typeUtils.isAssignable(thisOptionClass, optionsBase)) { |
| throw new OptionProcessorException( |
| optionField, |
| "@Option annotated fields can only be in classes that inherit from OptionsBase."); |
| } |
| } |
| |
| /** |
| * Checks that the Option variables is public and neither final nor static. |
| * |
| * <p>Private or protected fields would prevent the options parser from having full access to the |
| * fields it's expected to read, and {@link OptionsBase} equality would not work as intended. |
| * |
| * <p>Static or final fields would cause issue with correct value assigning at the end of parsing. |
| */ |
| private void checkModifiers(VariableElement optionField) throws OptionProcessorException { |
| if (!optionField.getModifiers().contains(Modifier.PUBLIC)) { |
| throw new OptionProcessorException(optionField, "@Option annotated fields should be public."); |
| } |
| if (optionField.getModifiers().contains(Modifier.STATIC)) { |
| throw new OptionProcessorException( |
| optionField, "@Option annotated fields should not be static."); |
| } |
| if (optionField.getModifiers().contains(Modifier.FINAL)) { |
| throw new OptionProcessorException( |
| optionField, "@Option annotated fields should not be final."); |
| } |
| } |
| |
| private ImmutableList<TypeMirror> getAcceptedConverterReturnTypes(VariableElement optionField) |
| throws OptionProcessorException { |
| TypeMirror optionType = optionField.asType(); |
| Option annotation = optionField.getAnnotation(Option.class); |
| TypeMirror listType = elementUtils.getTypeElement(List.class.getCanonicalName()).asType(); |
| // Options that accumulate multiple mentions in an arglist must have type List<T>, where each |
| // individual mention has type T. Identify type T to use it for checking the converter's return |
| // type. |
| if (annotation.allowMultiple()) { |
| // Check that the option type is in fact a list. |
| if (optionType.getKind() != TypeKind.DECLARED) { |
| throw new OptionProcessorException( |
| optionField, |
| "Option that allows multiple occurrences must be of type %s, but is of type %s", |
| listType, |
| optionType); |
| } |
| DeclaredType optionDeclaredType = (DeclaredType) optionType; |
| // optionDeclaredType.asElement().asType() gets us from List<actualType> to List<E>, so this |
| // is unfortunately necessary. |
| if (!typeUtils.isAssignable(optionDeclaredType.asElement().asType(), listType)) { |
| throw new OptionProcessorException( |
| optionField, |
| "Option that allows multiple occurrences must be of type %s, but is of type %s", |
| listType, |
| optionType); |
| } |
| |
| // Check that there is only one generic parameter, and store it as the singular option type. |
| List<? extends TypeMirror> genericParameters = optionDeclaredType.getTypeArguments(); |
| if (genericParameters.size() != 1) { |
| throw new OptionProcessorException( |
| optionField, |
| "Option that allows multiple occurrences must be of type %s, " |
| + "where E is the type of an individual command-line mention of this option, " |
| + "but is of type %s", |
| listType, |
| optionType); |
| } |
| |
| // For repeated options, we also accept cases where each option itself contains a list, which |
| // are then concatenated into the final single list type. For this reason, we will accept both |
| // converters that return the type of a single option, and List<singleOption>, which, |
| // incidentally, is the original optionType. |
| // Example: --foo=a,b,c --foo=d,e,f could have a final value of type List<Char>, |
| // value {a,b,c,e,d,f}, instead of requiring a final value of type List<List<Char>> |
| // value {{a,b,c},{d,e,f}} |
| TypeMirror singularOptionType = genericParameters.get(0); |
| |
| return ImmutableList.of(singularOptionType, optionType); |
| } else { |
| return ImmutableList.of(optionField.asType()); |
| } |
| } |
| |
| private void checkForDefaultConverter( |
| VariableElement optionField, |
| List<TypeMirror> acceptedConverterReturnTypes, |
| String defaultValue) |
| throws OptionProcessorException { |
| for (TypeMirror acceptedConverterReturnType : acceptedConverterReturnTypes) { |
| Converter<?> converterInstance = defaultConverters.get(acceptedConverterReturnType); |
| if (converterInstance == null) { |
| // This return type isn't a match, move on to the next one in case. |
| continue; |
| } |
| TypeElement converter = |
| elementUtils.getTypeElement(converterInstance.getClass().getCanonicalName()); |
| try { |
| // For the default converters, it so happens we have access to the convert methods |
| // at compile time, since we already have the OptionsParser source. Take advantage of |
| // this to test that the provided defaultValue is valid. |
| converterInstance.convert(defaultValue); |
| } catch (OptionsParsingException e) { |
| throw new OptionProcessorException( |
| optionField, |
| /* throwable = */ e, |
| "Option lists a default value (%s) that is not parsable by the option's converter " |
| + "(s)", |
| defaultValue, |
| converter); |
| } |
| return; // This one passes the test. |
| } |
| |
| // We didn't find a default converter. |
| throw new OptionProcessorException( |
| optionField, |
| "Cannot find valid converter for option of type %s", |
| acceptedConverterReturnTypes.get(0)); |
| } |
| |
| private void checkProvidedConverter( |
| VariableElement optionField, |
| ImmutableList<TypeMirror> acceptedConverterReturnTypes, |
| TypeElement converterElement) |
| throws OptionProcessorException { |
| if (converterElement.getModifiers().contains(Modifier.ABSTRACT)) { |
| throw new OptionProcessorException( |
| optionField, "The converter type %s must be a concrete type", converterElement.asType()); |
| } |
| |
| DeclaredType converterType = (DeclaredType) converterElement.asType(); |
| |
| // Unfortunately, for provided classes, we do not have access to the compiled convert |
| // method at this time, and cannot check that the default value is parseable. We will |
| // instead check that T of Converter<T> matches the option's type, but this is all we can |
| // do. |
| List<ExecutableElement> methodList = |
| elementUtils |
| .getAllMembers(converterElement) |
| .stream() |
| .filter(element -> element.getKind() == ElementKind.METHOD) |
| .map(methodElement -> (ExecutableElement) methodElement) |
| .filter(methodElement -> methodElement.getSimpleName().contentEquals("convert")) |
| .filter( |
| methodElement -> |
| methodElement.getParameters().size() == 1 |
| && typeUtils.isSameType( |
| methodElement.getParameters().get(0).asType(), |
| elementUtils.getTypeElement(String.class.getCanonicalName()).asType())) |
| .collect(Collectors.toList()); |
| // Check that there is just the one method |
| if (methodList.size() != 1) { |
| throw new OptionProcessorException( |
| optionField, |
| "Converter %s has methods 'convert(String)': %s", |
| converterElement, |
| methodList.stream().map(Object::toString).collect(Collectors.joining(", "))); |
| } |
| |
| ExecutableType convertMethodType = |
| (ExecutableType) typeUtils.asMemberOf(converterType, methodList.get(0)); |
| TypeMirror convertMethodResultType = convertMethodType.getReturnType(); |
| // Check that the converter's return type is in the accepted list. |
| for (TypeMirror acceptedConverterReturnType : acceptedConverterReturnTypes) { |
| if (typeUtils.isAssignable(convertMethodResultType, acceptedConverterReturnType)) { |
| return; // This one passes the test. |
| } |
| } |
| throw new OptionProcessorException( |
| optionField, |
| "Type of field (%s) must be assignable from the converter's return type (%s)", |
| acceptedConverterReturnTypes.get(0), |
| convertMethodResultType); |
| } |
| |
| private void checkConverter(VariableElement optionField) throws OptionProcessorException { |
| TypeMirror optionType = optionField.asType(); |
| Option annotation = optionField.getAnnotation(Option.class); |
| ImmutableList<TypeMirror> acceptedConverterReturnTypes = |
| getAcceptedConverterReturnTypes(optionField); |
| |
| // For simple, static expansions, don't accept non-Void types. |
| if (annotation.expansion().length != 0 |
| && !typeUtils.isSameType( |
| optionType, elementUtils.getTypeElement(Void.class.getCanonicalName()).asType())) { |
| throw new OptionProcessorException( |
| optionField, |
| "Option is an expansion flag with a static expansion, but does not have Void type."); |
| } |
| |
| // Obtain the converter for this option. |
| AnnotationMirror optionMirror = |
| ProcessorUtils.getAnnotation(elementUtils, typeUtils, optionField, Option.class); |
| TypeElement defaultConverterElement = |
| elementUtils.getTypeElement(Converter.class.getCanonicalName()); |
| TypeElement converterElement = |
| ProcessorUtils.getClassTypeFromAnnotationField(elementUtils, optionMirror, "converter"); |
| if (converterElement == null) { |
| throw new OptionProcessorException(optionField, "Null converter found."); |
| } |
| |
| if (typeUtils.isSameType(converterElement.asType(), defaultConverterElement.asType())) { |
| // Find a matching converter in the default converter list, and check that it successfully |
| // parses the default value for this option. |
| checkForDefaultConverter( |
| optionField, acceptedConverterReturnTypes, annotation.defaultValue()); |
| } else { |
| // Check that the provided converter has an accepted return type. |
| checkProvidedConverter(optionField, acceptedConverterReturnTypes, converterElement); |
| } |
| } |
| |
| /** |
| * Check that the option lists at least one effect, and that no nonsensical combinations are |
| * listed, such as having a known effect listed with UNKNOWN. |
| */ |
| private void checkEffectTagRationality(VariableElement optionField) |
| throws OptionProcessorException { |
| Option annotation = optionField.getAnnotation(Option.class); |
| OptionEffectTag[] effectTags = annotation.effectTags(); |
| // Check that there is at least one OptionEffectTag listed. |
| if (effectTags.length < 1) { |
| throw new OptionProcessorException( |
| optionField, |
| "Option does not list at least one OptionEffectTag. If the option has no effect, " |
| + "please be explicit and add NO_OP. Otherwise, add a tag representing its effect."); |
| } else if (effectTags.length > 1) { |
| // If there are more than 1 tag, make sure that NO_OP and UNKNOWN is not one of them. |
| // These don't make sense if other effects are listed. |
| ImmutableList<OptionEffectTag> tags = ImmutableList.copyOf(effectTags); |
| if (tags.contains(OptionEffectTag.UNKNOWN)) { |
| throw new OptionProcessorException( |
| optionField, |
| "Option includes UNKNOWN with other, known, effects. Please remove UNKNOWN from " |
| + "the list."); |
| } |
| if (tags.contains(OptionEffectTag.NO_OP)) { |
| throw new OptionProcessorException( |
| optionField, |
| "Option includes NO_OP with other effects. This doesn't make much sense. Please " |
| + "remove NO_OP or the actual effects from the list, whichever is correct."); |
| } |
| } |
| } |
| |
| /** |
| * Check that if the metadata tags listed by an option require the option to be unknown by the |
| * average user, the same option will be omitted from documentation. |
| */ |
| private void checkMetadataTagAndCategoryRationality(VariableElement optionField) |
| throws OptionProcessorException { |
| Option annotation = optionField.getAnnotation(Option.class); |
| OptionMetadataTag[] metadataTags = annotation.metadataTags(); |
| OptionDocumentationCategory category = annotation.documentationCategory(); |
| |
| for (OptionMetadataTag tag : metadataTags) { |
| if (tag == OptionMetadataTag.HIDDEN || tag == OptionMetadataTag.INTERNAL) { |
| if (category != OptionDocumentationCategory.UNDOCUMENTED) { |
| throw new OptionProcessorException( |
| optionField, |
| "Option has metadata tag %s but does not have category UNDOCUMENTED. Please fix.", |
| tag); |
| } |
| } |
| } |
| } |
| |
| /** These categories used to indicate whether a flag was documented, but no longer. */ |
| private static final ImmutableList<String> DEPRECATED_CATEGORIES = |
| ImmutableList.of("undocumented", "hidden", "internal"); |
| |
| private void checkOldCategoriesAreNotUsed(VariableElement optionField) |
| throws OptionProcessorException { |
| Option annotation = optionField.getAnnotation(Option.class); |
| if (DEPRECATED_CATEGORIES.contains(annotation.category())) { |
| throw new OptionProcessorException( |
| optionField, |
| "Documentation level is no longer read from the option category. Category \"" |
| + annotation.category() |
| + "\" is disallowed, see OptionMetadataTags for the relevant tags."); |
| } |
| } |
| |
| private void checkOptionName(VariableElement optionField) throws OptionProcessorException { |
| Option annotation = optionField.getAnnotation(Option.class); |
| String optionName = annotation.name(); |
| if (optionName.isEmpty()) { |
| throw new OptionProcessorException(optionField, "Option must have an actual name."); |
| } |
| |
| // Specifically for non-internal options, which are flags intended to be used on the command |
| // line, check that there are no weird characters or whitespace. |
| if (!ImmutableList.copyOf(annotation.metadataTags()).contains(OptionMetadataTag.INTERNAL)) { |
| if (!Pattern.matches("([\\w:-])*", optionName)) { |
| // Ideally, this would be just \w, but - and : are needed for legacy options. We can lie in |
| // the error though, no harm in encouraging good behavior. |
| throw new OptionProcessorException( |
| optionField, |
| "Options that are used on the command line as flags must have names made from word " |
| + "characters only."); |
| } |
| } |
| } |
| |
| /** |
| * Some flags expand to other flags, either in place, or with "implicit requirements" that get |
| * added on top of the flag's value. Don't let these flags do too many crazy things, dealing with |
| * this is enough. |
| */ |
| private void checkExpansionOptions(VariableElement optionField) throws OptionProcessorException { |
| Option annotation = optionField.getAnnotation(Option.class); |
| boolean isStaticExpansion = annotation.expansion().length > 0; |
| boolean hasImplicitRequirements = annotation.implicitRequirements().length > 0; |
| |
| AnnotationMirror annotationMirror = |
| ProcessorUtils.getAnnotation(elementUtils, typeUtils, optionField, Option.class); |
| TypeElement expansionFunction = |
| ProcessorUtils.getClassTypeFromAnnotationField( |
| elementUtils, annotationMirror, "expansionFunction"); |
| TypeElement defaultExpansionFunction = |
| elementUtils.getTypeElement(ExpansionFunction.class.getCanonicalName()); |
| boolean isFunctionalExpansion = |
| !typeUtils.isSameType(expansionFunction.asType(), defaultExpansionFunction.asType()); |
| |
| if (isStaticExpansion && isFunctionalExpansion) { |
| throw new OptionProcessorException( |
| optionField, |
| "Options cannot expand using both a static expansion list and an expansion function."); |
| } |
| boolean isExpansion = isStaticExpansion || isFunctionalExpansion; |
| |
| if (isExpansion && hasImplicitRequirements) { |
| throw new OptionProcessorException( |
| optionField, |
| "Can't set an option to be both an expansion option and have implicit requirements."); |
| } |
| |
| if (isExpansion || hasImplicitRequirements) { |
| if (annotation.allowMultiple()) { |
| throw new OptionProcessorException( |
| optionField, |
| "Can't set an option to accumulate multiple values and let it expand to other flags."); |
| } |
| } |
| } |
| |
| @Override |
| public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { |
| for (Element annotatedElement : roundEnv.getElementsAnnotatedWith(Option.class)) { |
| try { |
| // Only fields are annotated with Option, this should already be checked by the |
| // @Target(ElementType.FIELD) annotation. |
| VariableElement optionField = (VariableElement) annotatedElement; |
| |
| checkModifiers(optionField); |
| checkInOptionBase(optionField); |
| checkOptionName(optionField); |
| checkOldCategoriesAreNotUsed(optionField); |
| checkExpansionOptions(optionField); |
| checkConverter(optionField); |
| checkEffectTagRationality(optionField); |
| checkMetadataTagAndCategoryRationality(optionField); |
| } catch (OptionProcessorException e) { |
| error(e.getElementInError(), e.getMessage()); |
| } |
| } |
| // Claim all Option annotated fields. |
| return true; |
| } |
| |
| /** |
| * Prints an error message & fails the compilation. |
| * |
| * @param e The element which has caused the error. Can be null |
| * @param msg The error message |
| */ |
| public void error(Element e, String msg) { |
| messager.printMessage(Diagnostic.Kind.ERROR, msg, e); |
| } |
| } |