| package org.checkerframework.checker.formatter; |
| |
| import com.sun.source.tree.ExpressionTree; |
| import com.sun.source.tree.MethodInvocationTree; |
| import com.sun.source.tree.Tree; |
| import com.sun.source.tree.TypeCastTree; |
| import com.sun.source.util.SimpleTreeVisitor; |
| import java.util.IllegalFormatException; |
| import java.util.List; |
| import javax.annotation.processing.ProcessingEnvironment; |
| import javax.lang.model.element.AnnotationMirror; |
| import javax.lang.model.element.ExecutableElement; |
| import javax.lang.model.type.ArrayType; |
| import javax.lang.model.type.NullType; |
| import javax.lang.model.type.TypeKind; |
| import javax.lang.model.type.TypeMirror; |
| import javax.lang.model.util.SimpleTypeVisitor7; |
| import org.checkerframework.checker.compilermsgs.qual.CompilerMessageKey; |
| import org.checkerframework.checker.formatter.qual.ConversionCategory; |
| import org.checkerframework.checker.formatter.qual.Format; |
| import org.checkerframework.checker.formatter.qual.FormatMethod; |
| import org.checkerframework.checker.formatter.qual.InvalidFormat; |
| import org.checkerframework.checker.formatter.qual.ReturnsFormat; |
| import org.checkerframework.checker.nullness.qual.Nullable; |
| import org.checkerframework.common.basetype.BaseTypeChecker; |
| import org.checkerframework.dataflow.cfg.node.ArrayCreationNode; |
| import org.checkerframework.dataflow.cfg.node.FieldAccessNode; |
| import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; |
| import org.checkerframework.dataflow.cfg.node.Node; |
| import org.checkerframework.framework.type.AnnotatedTypeFactory; |
| import org.checkerframework.framework.type.AnnotatedTypeMirror; |
| import org.checkerframework.javacutil.AnnotationBuilder; |
| import org.checkerframework.javacutil.AnnotationUtils; |
| import org.checkerframework.javacutil.TreeUtils; |
| import org.checkerframework.javacutil.TypesUtils; |
| |
| /** |
| * This class provides a collection of utilities to ease working with syntax trees that have |
| * something to do with Formatters. |
| */ |
| public class FormatterTreeUtil { |
| /** The checker. */ |
| public final BaseTypeChecker checker; |
| /** The processing environment. */ |
| public final ProcessingEnvironment processingEnv; |
| |
| /** The value() element/field of an @Format annotation. */ |
| protected final ExecutableElement formatValueElement; |
| /** The value() element/field of an @InvalidFormat annotation. */ |
| protected final ExecutableElement invalidFormatValueElement; |
| // private final ExecutableElement formatArgTypesElement; |
| |
| public FormatterTreeUtil(BaseTypeChecker checker) { |
| this.checker = checker; |
| this.processingEnv = checker.getProcessingEnvironment(); |
| formatValueElement = TreeUtils.getMethod(Format.class, "value", 0, processingEnv); |
| invalidFormatValueElement = TreeUtils.getMethod(InvalidFormat.class, "value", 0, processingEnv); |
| /* |
| this.formatArgTypesElement = |
| TreeUtils.getMethod( |
| Format.class, |
| "value", |
| 0, |
| processingEnv); |
| */ |
| } |
| |
| /** Describes the ways a format method may be invoked. */ |
| public static enum InvocationType { |
| /** |
| * The parameters are passed as varargs. For example: |
| * |
| * <blockquote> |
| * |
| * <pre> |
| * String.format("%s %d", "Example", 7); |
| * </pre> |
| * |
| * </blockquote> |
| */ |
| VARARG, |
| |
| /** |
| * The parameters are passed as array. For example: |
| * |
| * <blockquote> |
| * |
| * <pre> |
| * Object[] a = new Object[]{"Example",7}; |
| * String.format("%s %d", a); |
| * </pre> |
| * |
| * </blockquote> |
| */ |
| ARRAY, |
| |
| /** |
| * A null array is passed to the format method. This happens seldomly. |
| * |
| * <blockquote> |
| * |
| * <pre> |
| * String.format("%s %d", (Object[])null); |
| * </pre> |
| * |
| * </blockquote> |
| */ |
| NULLARRAY; |
| } |
| |
| /** A wrapper around a value of type E, plus an ExpressionTree location. */ |
| public static class Result<E> { |
| private final E value; |
| public final ExpressionTree location; |
| |
| public Result(E value, ExpressionTree location) { |
| this.value = value; |
| this.location = location; |
| } |
| |
| public E value() { |
| return value; |
| } |
| } |
| |
| /** |
| * Returns true if the call is to a method with the @ReturnsFormat annotation. An example of such |
| * a method is FormatUtil.asFormat. |
| */ |
| public boolean isAsFormatCall(MethodInvocationNode node, AnnotatedTypeFactory atypeFactory) { |
| ExecutableElement method = node.getTarget().getMethod(); |
| AnnotationMirror anno = atypeFactory.getDeclAnnotation(method, ReturnsFormat.class); |
| return anno != null; |
| } |
| |
| private ConversionCategory[] asFormatCallCategoriesLowLevel(MethodInvocationNode node) { |
| Node vararg = node.getArgument(1); |
| if (!(vararg instanceof ArrayCreationNode)) { |
| return null; |
| } |
| List<Node> convs = ((ArrayCreationNode) vararg).getInitializers(); |
| ConversionCategory[] res = new ConversionCategory[convs.size()]; |
| for (int i = 0; i < convs.size(); ++i) { |
| Node conv = convs.get(i); |
| if (conv instanceof FieldAccessNode) { |
| Class<? extends Object> clazz = |
| TypesUtils.getClassFromType(((FieldAccessNode) conv).getType()); |
| if (clazz == ConversionCategory.class) { |
| res[i] = ConversionCategory.valueOf(((FieldAccessNode) conv).getFieldName()); |
| continue; /* avoid returning null */ |
| } |
| } |
| return null; |
| } |
| return res; |
| } |
| |
| public Result<ConversionCategory[]> asFormatCallCategories(MethodInvocationNode node) { |
| // TODO make sure the method signature looks good |
| return new Result<>(asFormatCallCategoriesLowLevel(node), node.getTree()); |
| } |
| |
| /** |
| * Returns true if {@code node} is a call to a method annotated with {@code @FormatMethod}. |
| * |
| * @param node a method call |
| * @param atypeFactory a type factory |
| * @return true if {@code node} is a call to a method annotated with {@code @FormatMethod} |
| */ |
| public boolean isFormatMethodCall(MethodInvocationTree node, AnnotatedTypeFactory atypeFactory) { |
| ExecutableElement method = TreeUtils.elementFromUse(node); |
| AnnotationMirror anno = atypeFactory.getDeclAnnotation(method, FormatMethod.class); |
| return anno != null; |
| } |
| |
| /** |
| * Creates a new FormatCall, or returns null. |
| * |
| * @param invocationTree a method invocation, where the method is annotated @FormatMethod |
| * @param atypeFactory the type factory |
| * @return a new FormatCall, or null if the invocation is of a method that is improperly |
| * annotated @FormatMethod |
| */ |
| public @Nullable FormatCall create( |
| MethodInvocationTree invocationTree, AnnotatedTypeFactory atypeFactory) { |
| FormatterTreeUtil ftu = ((FormatterAnnotatedTypeFactory) atypeFactory).treeUtil; |
| if (!ftu.isFormatMethodCall(invocationTree, atypeFactory)) { |
| return null; |
| } |
| |
| ExecutableElement methodElement = TreeUtils.elementFromUse(invocationTree); |
| int formatStringIndex = FormatterVisitor.formatStringIndex(methodElement); |
| if (formatStringIndex == -1) { |
| // Reporting the error is redundant if the method was declared in source code, because the |
| // visitor will have reported it; but it is necessary if the method was declared in byte code. |
| atypeFactory |
| .getChecker() |
| .reportError(invocationTree, "format.method", methodElement.getSimpleName()); |
| return null; |
| } |
| ExpressionTree formatStringTree = invocationTree.getArguments().get(formatStringIndex); |
| AnnotatedTypeMirror formatStringType = atypeFactory.getAnnotatedType(formatStringTree); |
| List<? extends ExpressionTree> allArgs = invocationTree.getArguments(); |
| List<? extends ExpressionTree> args = allArgs.subList(formatStringIndex + 1, allArgs.size()); |
| |
| return new FormatCall(invocationTree, formatStringTree, formatStringType, args, atypeFactory); |
| } |
| |
| /** Represents a format method invocation in the syntax tree. */ |
| public class FormatCall { |
| /** The call itself. */ |
| final MethodInvocationTree invocationTree; |
| /** The format string argument. */ |
| private final ExpressionTree formatStringTree; |
| /** The type of the format string argument. */ |
| private final AnnotatedTypeMirror formatStringType; |
| /** The arguments that follow the format string argument. */ |
| private final List<? extends ExpressionTree> args; |
| /** The type factory. */ |
| private final AnnotatedTypeFactory atypeFactory; |
| |
| /** |
| * Create a new FormatCall object. |
| * |
| * @param invocationTree the call itself |
| * @param formatStringTree the format string argument |
| * @param formatStringType the type of the format string argument |
| * @param args the arguments that follow the format string argument |
| * @param atypeFactory the type factory |
| */ |
| private FormatCall( |
| MethodInvocationTree invocationTree, |
| ExpressionTree formatStringTree, |
| AnnotatedTypeMirror formatStringType, |
| List<? extends ExpressionTree> args, |
| AnnotatedTypeFactory atypeFactory) { |
| this.invocationTree = invocationTree; |
| this.formatStringTree = formatStringTree; |
| this.formatStringType = formatStringType; |
| this.args = args; |
| this.atypeFactory = atypeFactory; |
| } |
| |
| /** |
| * Returns an error description if the format-string argument's type is <em>not</em> annotated |
| * as {@code @Format}. Returns null if it is annotated. |
| * |
| * @return an error description if the format string is not annotated as {@code @Format}, or |
| * null if it is |
| */ |
| public final Result<String> errMissingFormatAnnotation() { |
| if (!formatStringType.hasAnnotation(Format.class)) { |
| String msg = "(is a @Format annotation missing?)"; |
| AnnotationMirror inv = formatStringType.getAnnotation(InvalidFormat.class); |
| if (inv != null) { |
| msg = invalidFormatAnnotationToErrorMessage(inv); |
| } |
| return new Result<>(msg, formatStringTree); |
| } |
| return null; |
| } |
| |
| /** |
| * Returns the type of method invocation. |
| * |
| * @see InvocationType |
| */ |
| public final Result<InvocationType> getInvocationType() { |
| InvocationType type = InvocationType.VARARG; |
| |
| if (args.size() == 1) { |
| final ExpressionTree first = args.get(0); |
| TypeMirror argType = atypeFactory.getAnnotatedType(first).getUnderlyingType(); |
| // figure out if argType is an array |
| type = |
| argType.accept( |
| new SimpleTypeVisitor7<InvocationType, Class<Void>>() { |
| @Override |
| protected InvocationType defaultAction(TypeMirror e, Class<Void> p) { |
| // not an array |
| return InvocationType.VARARG; |
| } |
| |
| @Override |
| public InvocationType visitArray(ArrayType t, Class<Void> p) { |
| // it's an array, now figure out if it's a (Object[])null |
| // array |
| return first.accept( |
| new SimpleTreeVisitor<InvocationType, Class<Void>>() { |
| @Override |
| protected InvocationType defaultAction(Tree node, Class<Void> p) { |
| // just a normal array |
| return InvocationType.ARRAY; |
| } |
| |
| @Override |
| public InvocationType visitTypeCast(TypeCastTree node, Class<Void> p) { |
| // it's a (Object[])null |
| return atypeFactory |
| .getAnnotatedType(node.getExpression()) |
| .getUnderlyingType() |
| .getKind() |
| == TypeKind.NULL |
| ? InvocationType.NULLARRAY |
| : InvocationType.ARRAY; |
| } |
| }, |
| p); |
| } |
| |
| @Override |
| public InvocationType visitNull(NullType t, Class<Void> p) { |
| return InvocationType.NULLARRAY; |
| } |
| }, |
| Void.TYPE); |
| } |
| |
| ExpressionTree loc = invocationTree.getMethodSelect(); |
| if (type != InvocationType.VARARG && !args.isEmpty()) { |
| loc = args.get(0); |
| } |
| return new Result<>(type, loc); |
| } |
| |
| /** |
| * Returns the conversion category for every parameter. |
| * |
| * @return the conversion categories of all the parameters |
| * @see ConversionCategory |
| */ |
| public final ConversionCategory[] getFormatCategories() { |
| AnnotationMirror anno = formatStringType.getAnnotation(Format.class); |
| return formatAnnotationToCategories(anno); |
| } |
| |
| /** |
| * Returns the types of the arguments to the call. Use {@link #isValidArgument} and {@link |
| * #isArgumentNull} to work with the result. |
| * |
| * @return the types of the arguments to the call |
| */ |
| public final Result<TypeMirror>[] getArgTypes() { |
| // One to suppress warning in javac, the other to suppress warning in Eclipse... |
| @SuppressWarnings({"rawtypes", "unchecked"}) |
| Result<TypeMirror>[] res = new Result[args.size()]; |
| for (int i = 0; i < res.length; ++i) { |
| ExpressionTree arg = args.get(i); |
| TypeMirror argType; |
| if (TreeUtils.isNullExpression(arg)) { |
| argType = atypeFactory.getProcessingEnv().getTypeUtils().getNullType(); |
| } else { |
| argType = atypeFactory.getAnnotatedType(arg).getUnderlyingType(); |
| } |
| res[i] = new Result<>(argType, arg); |
| } |
| return res; |
| } |
| |
| /** |
| * Checks if the type of an argument returned from {@link #getArgTypes()} is valid for the |
| * passed ConversionCategory. |
| * |
| * @param formatCat a format specifier |
| * @param argType an argument type |
| * @return true if the argument can be passed to the format specifier |
| */ |
| public final boolean isValidArgument(ConversionCategory formatCat, TypeMirror argType) { |
| if (argType.getKind() == TypeKind.NULL || isArgumentNull(argType)) { |
| return true; |
| } |
| Class<? extends Object> type = TypesUtils.getClassFromType(argType); |
| return formatCat.isAssignableFrom(type); |
| } |
| |
| /** |
| * Checks if the argument returned from {@link #getArgTypes()} is a {@code null} expression. |
| * |
| * @param type a type |
| * @return true if the argument is a {@code null} expression |
| */ |
| public final boolean isArgumentNull(TypeMirror type) { |
| // TODO: Just check whether it is the VOID TypeMirror. |
| |
| // is it the null literal |
| return type.accept( |
| new SimpleTypeVisitor7<Boolean, Class<Void>>() { |
| @Override |
| protected Boolean defaultAction(TypeMirror e, Class<Void> p) { |
| // it's not the null literal |
| return false; |
| } |
| |
| @Override |
| public Boolean visitNull(NullType t, Class<Void> p) { |
| // it's the null literal |
| return true; |
| } |
| }, |
| Void.TYPE); |
| } |
| } |
| |
| // The failure() method is required so that FormatterTransfer, which has no access to the |
| // FormatterChecker, can report errors. |
| /** |
| * Reports an error. |
| * |
| * @param res used for source location information |
| * @param msgKey the diagnostic message key |
| * @param args arguments to the diagnostic message |
| */ |
| public final void failure(Result<?> res, @CompilerMessageKey String msgKey, Object... args) { |
| checker.reportError(res.location, msgKey, args); |
| } |
| |
| /** |
| * Reports a warning. |
| * |
| * @param res used for source location information |
| * @param msgKey the diagnostic message key |
| * @param args arguments to the diagnostic message |
| */ |
| public final void warning(Result<?> res, @CompilerMessageKey String msgKey, Object... args) { |
| checker.reportWarning(res.location, msgKey, args); |
| } |
| |
| /** |
| * Takes an exception that describes an invalid formatter string and, returns a syntax trees |
| * element that represents a {@link InvalidFormat} annotation with the exception's error message |
| * as value. |
| */ |
| public AnnotationMirror exceptionToInvalidFormatAnnotation(IllegalFormatException ex) { |
| return stringToInvalidFormatAnnotation(ex.getMessage()); |
| } |
| |
| /** |
| * Creates an {@link InvalidFormat} annotation with the given string as its value. |
| * |
| * @param invalidFormatString an invalid formatter string |
| * @return an {@link InvalidFormat} annotation with the given string as its value |
| */ |
| // package-private |
| AnnotationMirror stringToInvalidFormatAnnotation(String invalidFormatString) { |
| AnnotationBuilder builder = new AnnotationBuilder(processingEnv, InvalidFormat.class); |
| builder.setValue("value", invalidFormatString); |
| return builder.build(); |
| } |
| |
| /** |
| * Gets the value() element/field out of an InvalidFormat annotation. |
| * |
| * @param anno an InvalidFormat annotation |
| * @return its value() element/field |
| */ |
| private String getInvalidFormatValue(AnnotationMirror anno) { |
| return (String) anno.getElementValues().get(invalidFormatValueElement).getValue(); |
| } |
| |
| /** |
| * Takes a syntax tree element that represents a {@link InvalidFormat} annotation, and returns its |
| * value. |
| * |
| * @param anno an InvalidFormat annotation |
| * @return its value() element/field |
| */ |
| public String invalidFormatAnnotationToErrorMessage(AnnotationMirror anno) { |
| return "\"" + getInvalidFormatValue(anno) + "\""; |
| } |
| |
| /** |
| * Creates a {@code @}{@link Format} annotation with the given list as its value. |
| * |
| * @param args conversion categories for the {@code @Format} annotation |
| * @return a {@code @}{@link Format} annotation with the given list as its value |
| */ |
| public AnnotationMirror categoriesToFormatAnnotation(ConversionCategory[] args) { |
| AnnotationBuilder builder = new AnnotationBuilder(processingEnv, Format.class); |
| builder.setValue("value", args); |
| return builder.build(); |
| } |
| |
| /** |
| * Returns the value of a {@code @}{@link Format} annotation. |
| * |
| * @param anno a {@code @}{@link Format} annotation |
| * @return the annotation's {@code value} element |
| */ |
| @SuppressWarnings("GetClassOnEnum") |
| public ConversionCategory[] formatAnnotationToCategories(AnnotationMirror anno) { |
| return AnnotationUtils.getElementValueEnumArray( |
| anno, formatValueElement, ConversionCategory.class); |
| } |
| } |