package org.checkerframework.checker.formatter; /*>>> import org.checkerframework.checker.compilermsgs.qual.CompilerMessageKey; */ 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 java.util.Locale; import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; import javax.lang.model.type.ArrayType; import javax.lang.model.type.DeclaredType; import javax.lang.model.type.NullType; import javax.lang.model.type.PrimitiveType; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.SimpleElementVisitor7; import javax.lang.model.util.SimpleTypeVisitor7; 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.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.framework.util.AnnotationBuilder; import org.checkerframework.javacutil.AnnotationUtils; import org.checkerframework.javacutil.TreeUtils; /** * This class provides a collection of utilities to ease working with syntax trees that have * something to do with Formatters. * * @author Konstantin Weitz */ public class FormatterTreeUtil { public final BaseTypeChecker checker; public final ProcessingEnvironment processingEnv; // private final ExecutableElement formatArgTypesElement; public FormatterTreeUtil(BaseTypeChecker checker) { this.checker = checker; this.processingEnv = checker.getProcessingEnvironment(); /* this.formatArgTypesElement = TreeUtils.getMethod( org.checkerframework.checker.formatter.qual.Format.class.getCanonicalName(), "value", 0, processingEnv); */ } /** Describes the ways a format method may be invoked. */ public 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; } public interface Result<E> { E value(); } private static class ResultImpl<E> implements Result<E> { private final E value; public final ExpressionTree location; public ResultImpl(E value, ExpressionTree location) { this.value = value; this.location = location; } @Override public E value() { return value; } } 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) { 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 = typeMirrorToClass(((FieldAccessNode) conv).getType()); if (clazz == ConversionCategory.class) { res[i] = ConversionCategory.valueOf(((FieldAccessNode) conv).getFieldName()); continue; /* avoid returning null */ } } return null; } return res; } return null; } public Result<ConversionCategory[]> asFormatCallCategories(MethodInvocationNode node) { // TODO make sure the method signature looks good return new ResultImpl<ConversionCategory[]>( asFormatCallCategoriesLowLevel(node), node.getTree()); } public boolean isFormatCall(MethodInvocationTree node, AnnotatedTypeFactory atypeFactory) { ExecutableElement method = TreeUtils.elementFromUse(node); AnnotationMirror anno = atypeFactory.getDeclAnnotation(method, FormatMethod.class); return anno != null; } /** Represents a format method invocation in the syntax tree. */ public class FormatCall { private final AnnotatedTypeMirror formatAnno; private final List<? extends ExpressionTree> args; private final MethodInvocationTree node; private final ExpressionTree formatArg; private final AnnotatedTypeFactory atypeFactory; public FormatCall(MethodInvocationTree node, AnnotatedTypeFactory atypeFactory) { this.node = node; // TODO figure out how to make passing of environment // objects such as atypeFactory, processingEnv, ... nicer this.atypeFactory = atypeFactory; List<? extends ExpressionTree> theargs; theargs = node.getArguments(); if (typeMirrorToClass(atypeFactory.getAnnotatedType(theargs.get(0)).getUnderlyingType()) == Locale.class) { // call with Locale as first argument theargs = theargs.subList(1, theargs.size()); } // TODO check that the first parameter exists and is a string formatArg = theargs.get(0); formatAnno = atypeFactory.getAnnotatedType(formatArg); this.args = theargs.subList(1, theargs.size()); } /** * Returns an error description if the format string cannot be satisfied. Returns null if * the format string does not contain syntactic errors. */ public final Result<String> isIllegalFormat() { String res = null; if (!formatAnno.hasAnnotation(Format.class)) { res = "(is a @Format annotation missing?)"; AnnotationMirror inv = formatAnno.getAnnotation(InvalidFormat.class); if (inv != null) { res = invalidFormatAnnotationToErrorMessage(inv); } } return new ResultImpl<String>(res, formatArg); } /** * 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 = node.getMethodSelect(); if (type != InvocationType.VARARG && args.size() > 0) { loc = args.get(0); } return new ResultImpl<InvocationType>(type, loc); } /** * Returns the conversion category for every parameter. * * @see ConversionCategory */ public final ConversionCategory[] getFormatCategories() { AnnotationMirror anno = formatAnno.getAnnotation(Format.class); return formatAnnotationToCategories(anno); } /** * Returns the type of the function's parameters. Use {@link * #isValidParameter(ConversionCategory, TypeMirror) isValidParameter} and {@link * #isParameterNull(TypeMirror) isParameterNull} to work with the result. */ public final Result<TypeMirror>[] getParamTypes() { // One to make javac happy, the other to make Eclipse happy... @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 = atypeFactory.getAnnotatedType(arg).getUnderlyingType(); res[i] = new ResultImpl<TypeMirror>(argType, arg); } return res; } /** * Checks if the type of a parameter returned from {@link #getParamTypes()} is valid for the * passed ConversionCategory. */ public final boolean isValidParameter(ConversionCategory formatCat, TypeMirror paramType) { Class<? extends Object> type = typeMirrorToClass(paramType); if (type == null) { // we did not recognize the parameter type return false; } for (Class<? extends Object> c : formatCat.types) { if (c.isAssignableFrom(type)) { return true; } } return false; } /** * Checks if the parameter returned from {@link #getParamTypes()} is a {@code null} * expression. */ public final boolean isParameterNull(TypeMirror type) { // 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); } } /** Reports an error. Takes a {@link Result} to report the location. */ public final <E> void failure( Result<E> res, /*@CompilerMessageKey*/ String msg, Object... args) { ResultImpl<E> impl = (ResultImpl<E>) res; checker.report( org.checkerframework.framework.source.Result.failure(msg, args), impl.location); } /** Reports an warning. Takes a {@link Result} to report the location. */ public final <E> void warning( Result<E> res, /*@CompilerMessageKey*/ String msg, Object... args) { ResultImpl<E> impl = (ResultImpl<E>) res; checker.report( org.checkerframework.framework.source.Result.warning(msg, args), impl.location); } /** * 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()); } /** * Takes an invalid formatter string and, returns a syntax trees element that represents a * {@link InvalidFormat} annotation with the invalid formatter string as value. */ // package-private AnnotationMirror stringToInvalidFormatAnnotation(String invalidFormatString) { AnnotationBuilder builder = new AnnotationBuilder(processingEnv, InvalidFormat.class.getCanonicalName()); builder.setValue("value", invalidFormatString); return builder.build(); } /** * Takes a syntax tree element that represents a {@link InvalidFormat} annotation, and returns * its value. */ public String invalidFormatAnnotationToErrorMessage(AnnotationMirror anno) { return "\"" + AnnotationUtils.getElementValue(anno, "value", String.class, true) + "\""; } /** * Takes a list of ConversionCategory elements, and returns a syntax tree element that * represents a {@link Format} annotation with the list as value. */ public AnnotationMirror categoriesToFormatAnnotation(ConversionCategory[] args) { AnnotationBuilder builder = new AnnotationBuilder(processingEnv, Format.class.getCanonicalName()); builder.setValue("value", args); return builder.build(); } /** * Takes a syntax tree element that represents a {@link Format} annotation, and returns its * value. */ public ConversionCategory[] formatAnnotationToCategories(AnnotationMirror anno) { List<ConversionCategory> list = AnnotationUtils.getElementValueEnumArray( anno, "value", ConversionCategory.class, false); return list.toArray(new ConversionCategory[] {}); } private final Class<? extends Object> typeMirrorToClass(final TypeMirror type) { return type.accept( new SimpleTypeVisitor7<Class<? extends Object>, Class<Void>>() { @Override public Class<? extends Object> visitPrimitive(PrimitiveType t, Class<Void> v) { switch (t.getKind()) { case BOOLEAN: return Boolean.class; case BYTE: return Byte.class; case CHAR: return Character.class; case SHORT: return Short.class; case INT: return Integer.class; case LONG: return Long.class; case FLOAT: return Float.class; case DOUBLE: return Double.class; default: return null; } } @Override public Class<? extends Object> visitDeclared(DeclaredType dt, Class<Void> v) { return dt.asElement() .accept( new SimpleElementVisitor7< Class<? extends Object>, Class<Void>>() { @Override public Class<? extends Object> visitType( TypeElement e, Class<Void> v) { try { return Class.forName( e.getQualifiedName().toString()); } catch (ClassNotFoundException e1) { return null; // the lookup should work for all the classes we care about } } }, Void.TYPE); } }, Void.TYPE); } }