blob: fd91640ec50e2b44f90f0e98f5d3230d50f321d4 [file] [log] [blame]
package org.checkerframework.checker.i18nformatter;
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.List;
import java.util.Map;
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.element.VariableElement;
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.compilermsgs.qual.CompilerMessageKey;
import org.checkerframework.checker.formatter.FormatterTreeUtil.InvocationType;
import org.checkerframework.checker.formatter.FormatterTreeUtil.Result;
import org.checkerframework.checker.i18nformatter.qual.I18nChecksFormat;
import org.checkerframework.checker.i18nformatter.qual.I18nConversionCategory;
import org.checkerframework.checker.i18nformatter.qual.I18nFormat;
import org.checkerframework.checker.i18nformatter.qual.I18nFormatFor;
import org.checkerframework.checker.i18nformatter.qual.I18nInvalidFormat;
import org.checkerframework.checker.i18nformatter.qual.I18nMakeFormat;
import org.checkerframework.checker.i18nformatter.qual.I18nValidFormat;
import org.checkerframework.checker.i18nformatter.util.I18nFormatUtil;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.checker.signature.qual.BinaryName;
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.dataflow.cfg.node.StringLiteralNode;
import org.checkerframework.framework.type.AnnotatedTypeFactory;
import org.checkerframework.framework.type.AnnotatedTypeMirror;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType;
import org.checkerframework.framework.util.JavaExpressionParseUtil;
import org.checkerframework.javacutil.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 I18nFormatters.
*
* @checker_framework.manual #i18n-formatter-checker Internationalization Format String Checker
*/
public class I18nFormatterTreeUtil {
/** The checker. */
public final BaseTypeChecker checker;
/** The processing environment. */
public final ProcessingEnvironment processingEnv;
/** The value() element/field of an @I18nFormat annotation. */
protected final ExecutableElement i18nFormatValueElement;
/** The value() element/field of an @I18nFormatFor annotation. */
protected final ExecutableElement i18nFormatForValueElement;
/** The value() element/field of an @I18nInvalidFormat annotation. */
protected final ExecutableElement i18nInvalidFormatValueElement;
/**
* Creates a new I18nFormatterTreeUtil.
*
* @param checker the checker
*/
public I18nFormatterTreeUtil(BaseTypeChecker checker) {
this.checker = checker;
this.processingEnv = checker.getProcessingEnvironment();
i18nFormatValueElement = TreeUtils.getMethod(I18nFormat.class, "value", 0, processingEnv);
i18nFormatForValueElement = TreeUtils.getMethod(I18nFormatFor.class, "value", 0, processingEnv);
i18nInvalidFormatValueElement =
TreeUtils.getMethod(I18nInvalidFormat.class, "value", 0, processingEnv);
}
/** Describe the format annotation type. */
public enum FormatType {
I18NINVALID,
I18NFORMAT,
I18NFORMATFOR
}
/**
* Takes an exception that describes an invalid formatter string and returns a syntax trees
* element that represents a {@link I18nInvalidFormat} annotation with the exception's error
* message as value.
*/
public AnnotationMirror exceptionToInvalidFormatAnnotation(IllegalArgumentException ex) {
return stringToInvalidFormatAnnotation(ex.getMessage());
}
/**
* Creates an {@link I18nInvalidFormat} annotation with the given string as its value.
*
* @param invalidFormatString an invalid formatter string
* @return an {@link I18nInvalidFormat} annotation with the given string as its value
*/
// package-private
AnnotationMirror stringToInvalidFormatAnnotation(String invalidFormatString) {
AnnotationBuilder builder = new AnnotationBuilder(processingEnv, I18nInvalidFormat.class);
builder.setValue("value", invalidFormatString);
return builder.build();
}
/**
* Gets the value() element/field out of an I18nInvalidFormat annotation.
*
* @param anno an I18nInvalidFormat annotation
* @return its value() element/field, or null if it does not have one
*/
/*package-visible*/
@Nullable String getI18nInvalidFormatValue(AnnotationMirror anno) {
return AnnotationUtils.getElementValue(anno, i18nInvalidFormatValueElement, String.class, null);
}
/**
* Gets the value() element/field out of an I18NFormatFor annotation.
*
* @param anno an I18NFormatFor annotation
* @return its value() element/field
*/
/*package-visible*/ String getI18nFormatForValue(AnnotationMirror anno) {
return AnnotationUtils.getElementValue(anno, i18nFormatForValueElement, String.class);
}
/**
* Takes a syntax tree element that represents a {@link I18nInvalidFormat} annotation, and returns
* its value.
*
* @param anno an I18nInvalidFormat annotation
* @return its value() element/field, within double-quotes
*/
public String invalidFormatAnnotationToErrorMessage(AnnotationMirror anno) {
return "\"" + getI18nInvalidFormatValue(anno) + "\"";
}
/**
* Creates a {@code @}{@link I18nFormat} annotation with the given list as its value.
*
* @param args conversion categories for the {@code @Format} annotation
* @return a {@code @}{@link I18nFormat} annotation with the given list as its value
*/
public AnnotationMirror categoriesToFormatAnnotation(I18nConversionCategory[] args) {
AnnotationBuilder builder = new AnnotationBuilder(processingEnv, I18nFormat.class);
builder.setValue("value", args);
return builder.build();
}
/**
* Takes an {@code @}{@link I18nFormat} annotation, and returns its {@code value} element
*
* @param anno an {@code @}{@link I18nFormat} annotation
* @return the {@code @}{@link I18nFormat} annotation's {@code value} element
*/
public I18nConversionCategory[] formatAnnotationToCategories(AnnotationMirror anno) {
return AnnotationUtils.getElementValueEnumArray(
anno, i18nFormatValueElement, I18nConversionCategory.class);
}
/**
* Returns true if the call is to a method with the @I18nChecksFormat annotation. An example of
* such a method is I18nFormatUtil.hasFormat.
*/
public boolean isHasFormatCall(MethodInvocationNode node, AnnotatedTypeFactory atypeFactory) {
ExecutableElement method = node.getTarget().getMethod();
AnnotationMirror anno = atypeFactory.getDeclAnnotation(method, I18nChecksFormat.class);
return anno != null;
}
/**
* Returns true if the call is to a method with the @I18nValidFormat annotation. An example of
* such a method is I18nFormatUtil.isFormat.
*/
public boolean isIsFormatCall(MethodInvocationNode node, AnnotatedTypeFactory atypeFactory) {
ExecutableElement method = node.getTarget().getMethod();
AnnotationMirror anno = atypeFactory.getDeclAnnotation(method, I18nValidFormat.class);
return anno != null;
}
/**
* Returns true if the call is to a method with the @I18nMakeFormat annotation. An example of such
* a method is ResourceBundle.getString.
*/
public boolean isMakeFormatCall(MethodInvocationNode node, AnnotatedTypeFactory atypeFactory) {
ExecutableElement method = node.getTarget().getMethod();
AnnotationMirror anno = atypeFactory.getDeclAnnotation(method, I18nMakeFormat.class);
return anno != null;
}
/**
* 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);
}
private I18nConversionCategory[] asFormatCallCategoriesLowLevel(MethodInvocationNode node) {
Node vararg = node.getArgument(1);
if (vararg instanceof ArrayCreationNode) {
List<Node> convs = ((ArrayCreationNode) vararg).getInitializers();
I18nConversionCategory[] res = new I18nConversionCategory[convs.size()];
for (int i = 0; i < convs.size(); i++) {
Node conv = convs.get(i);
if (conv instanceof FieldAccessNode) {
if (typeMirrorToClass(((FieldAccessNode) conv).getType())
== I18nConversionCategory.class) {
res[i] = I18nConversionCategory.valueOf(((FieldAccessNode) conv).getFieldName());
continue; /* avoid returning null */
}
}
return null;
}
return res;
}
return null;
}
public Result<I18nConversionCategory[]> getHasFormatCallCategories(MethodInvocationNode node) {
return new Result<>(asFormatCallCategoriesLowLevel(node), node.getTree());
}
public Result<I18nConversionCategory[]> makeFormatCallCategories(
MethodInvocationNode node, I18nFormatterAnnotatedTypeFactory atypeFactory) {
Map<String, String> translations = atypeFactory.translations;
Node firstParam = node.getArgument(0);
Result<I18nConversionCategory[]> ret = new Result<>(null, node.getTree());
// Now only work with a literal string
if (firstParam instanceof StringLiteralNode) {
String s = ((StringLiteralNode) firstParam).getValue();
if (translations.containsKey(s)) {
String value = translations.get(s);
ret = new Result<>(I18nFormatUtil.formatParameterCategories(value), node.getTree());
}
}
return ret;
}
/**
* Returns an I18nFormatCall instance, only if there is an {@code @I18nFormatFor} annotation.
* Otherwise, returns null.
*
* @param tree method invocation tree
* @param atypeFactory type factory
* @return an I18nFormatCall instance, only if there is an {@code @I18nFormatFor} annotation.
* Otherwise, returns null.
*/
public @Nullable I18nFormatCall createFormatForCall(
MethodInvocationTree tree, I18nFormatterAnnotatedTypeFactory atypeFactory) {
ExecutableElement method = TreeUtils.elementFromUse(tree);
AnnotatedExecutableType methodAnno = atypeFactory.getAnnotatedType(method);
for (AnnotatedTypeMirror paramType : methodAnno.getParameterTypes()) {
// find @FormatFor
if (paramType.getAnnotation(I18nFormatFor.class) != null) {
return atypeFactory.treeUtil.new I18nFormatCall(tree, atypeFactory);
}
}
return null;
}
/**
* Represents a format method invocation in the syntax tree.
*
* <p>An I18nFormatCall instance can only be instantiated by the createFormatForCall method.
*/
public class I18nFormatCall {
/** The AST node for the call. */
private final MethodInvocationTree tree;
/** The format string argument. */
private ExpressionTree formatArg;
/** The type factory. */
private final AnnotatedTypeFactory atypeFactory;
/** The arguments to the format string. */
private List<? extends ExpressionTree> args;
/** Extra description for error messages. */
private String invalidMessage;
/** The type of the format string formal parameter. */
private AnnotatedTypeMirror formatAnno;
/**
* Creates an {@code I18nFormatCall} for the given method invocation tree.
*
* @param tree method invocation tree
* @param atypeFactory type factory
*/
public I18nFormatCall(MethodInvocationTree tree, AnnotatedTypeFactory atypeFactory) {
this.tree = tree;
this.atypeFactory = atypeFactory;
List<? extends ExpressionTree> theargs = tree.getArguments();
this.args = null;
ExecutableElement method = TreeUtils.elementFromUse(tree);
AnnotatedExecutableType methodAnno = atypeFactory.getAnnotatedType(method);
initialCheck(theargs, method, methodAnno);
}
/**
* Returns the AST node for the call.
*
* @return the AST node for the call
*/
public MethodInvocationTree getTree() {
return tree;
}
@Override
public String toString() {
return this.tree.toString();
}
/**
* This method checks the validity of the FormatFor. If it is valid, this.args will be set to
* the correct parameter arguments. Otherwise, it will be still null.
*
* @param theargs arguments to the format method call
* @param method the ExecutableElement of the format method
* @param methodAnno annotated type of {@code method}
*/
private void initialCheck(
List<? extends ExpressionTree> theargs,
ExecutableElement method,
AnnotatedExecutableType methodAnno) {
// paramIndex is a 0-based index
int paramIndex = -1;
int i = 0;
for (AnnotatedTypeMirror paramType : methodAnno.getParameterTypes()) {
if (paramType.getAnnotation(I18nFormatFor.class) != null) {
this.formatArg = theargs.get(i);
this.formatAnno = atypeFactory.getAnnotatedType(formatArg);
if (typeMirrorToClass(paramType.getUnderlyingType()) != String.class) {
// Invalid FormatFor invocation
return;
}
String formatforArg = getI18nFormatForValue(paramType.getAnnotation(I18nFormatFor.class));
paramIndex = JavaExpressionParseUtil.parameterIndex(formatforArg);
if (paramIndex == -1) {
// report errors here
checker.reportError(tree, "i18nformat.formatfor");
} else {
paramIndex--;
}
break;
}
i++;
}
if (paramIndex != -1) {
VariableElement param = method.getParameters().get(paramIndex);
if (param.asType().getKind() == TypeKind.ARRAY) {
this.args = theargs.subList(paramIndex, theargs.size());
} else {
this.args = theargs.subList(paramIndex, paramIndex + 1);
}
}
}
public Result<FormatType> getFormatType() {
FormatType type;
if (isValidFormatForInvocation()) {
if (formatAnno.hasAnnotation(I18nFormat.class)) {
type = FormatType.I18NFORMAT;
} else if (formatAnno.hasAnnotation(I18nFormatFor.class)) {
type = FormatType.I18NFORMATFOR;
} else {
type = FormatType.I18NINVALID;
invalidMessage = "(is a @I18nFormat annotation missing?)";
AnnotationMirror inv = formatAnno.getAnnotation(I18nInvalidFormat.class);
if (inv != null) {
invalidMessage = getI18nInvalidFormatValue(inv);
}
}
} else {
// If the FormatFor is invalid, it's still I18nFormatFor type but invalid,
// and we can't do anything else
type = FormatType.I18NFORMATFOR;
}
return new Result<>(type, formatArg);
}
public final String getInvalidError() {
return invalidMessage;
}
public boolean isValidFormatForInvocation() {
return this.args != 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;
loc = tree.getMethodSelect();
if (type != InvocationType.VARARG && !args.isEmpty()) {
loc = args.get(0);
}
return new Result<>(type, loc);
}
public Result<FormatType> getInvalidInvocationType() {
return new Result<>(FormatType.I18NFORMATFOR, formatArg);
}
/**
* Returns the conversion category for every parameter.
*
* @see I18nConversionCategory
*/
public final I18nConversionCategory[] getFormatCategories() {
AnnotationMirror anno = formatAnno.getAnnotation(I18nFormat.class);
return formatAnnotationToCategories(anno);
}
public final Result<TypeMirror>[] getParamTypes() {
// 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 = atypeFactory.getAnnotatedType(arg).getUnderlyingType();
res[i] = new Result<>(argType, arg);
}
return res;
}
public boolean isValidParameter(I18nConversionCategory formatCat, TypeMirror paramType) {
Class<? extends Object> type = typeMirrorToClass(paramType);
if (type == null) {
// we did not recognize the parameter type
return false;
}
return formatCat.isAssignableFrom(type);
}
}
/** Converts a TypeMirror to a Class. */
private static class TypeMirrorToClassVisitor
extends 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 {
@SuppressWarnings("signature") // https://tinyurl.com/cfissue/658:
// Name.toString should be @PolySignature
@BinaryName String cname = e.getQualifiedName().toString();
return Class.forName(cname);
} catch (ClassNotFoundException e1) {
return null; // the lookup should work for all
// the classes we care about
}
}
},
Void.TYPE);
}
}
/** The singleton instance of TypeMirrorToClassVisitor. */
private static TypeMirrorToClassVisitor typeMirrorToClassVisitor = new TypeMirrorToClassVisitor();
/**
* Converts a TypeMirror to a Class.
*
* @param type a TypeMirror
* @return the class corresponding to the argument
*/
private static final Class<? extends Object> typeMirrorToClass(final TypeMirror type) {
return type.accept(typeMirrorToClassVisitor, Void.TYPE);
}
}