blob: c8816ecefe42d1f1be11be29b7f311642703c80a [file] [log] [blame]
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);
}
}