package org.checkerframework.checker.i18nformatter;
import com.sun.source.tree.LiteralTree;
import com.sun.source.tree.Tree;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.util.Collections;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.ResourceBundle;
import java.util.Set;
import javax.lang.model.element.AnnotationMirror;
import org.checkerframework.checker.i18nformatter.qual.I18nConversionCategory;
import org.checkerframework.checker.i18nformatter.qual.I18nFormat;
import org.checkerframework.checker.i18nformatter.qual.I18nFormatBottom;
import org.checkerframework.checker.i18nformatter.qual.I18nFormatFor;
import org.checkerframework.checker.i18nformatter.qual.I18nInvalidFormat;
import org.checkerframework.checker.i18nformatter.qual.I18nUnknownFormat;
import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory;
import org.checkerframework.common.basetype.BaseTypeChecker;
import org.checkerframework.framework.type.AnnotatedTypeFactory;
import org.checkerframework.framework.type.AnnotatedTypeMirror;
import org.checkerframework.framework.type.QualifierHierarchy;
import org.checkerframework.framework.type.treeannotator.ListTreeAnnotator;
import org.checkerframework.framework.type.treeannotator.TreeAnnotator;
import org.checkerframework.framework.util.GraphQualifierHierarchy;
import org.checkerframework.framework.util.MultiGraphQualifierHierarchy.MultiGraphFactory;
import org.checkerframework.javacutil.AnnotationUtils;
/**
* Adds {@link I18nFormat} to the type of tree, if it is a {@code String} or {@code char} literal
* that represents a satisfiable format. The annotation's value is set to be a list of appropriate
* {@link I18nConversionCategory} values for every parameter of the format.
*
* <p>It also creates a map from the provided translation file if exists. This map will be used to
* get the corresponding value of a key when {@link java.util.ResourceBundle#getString} method is
* invoked.
*
* @checker_framework.manual #i18n-formatter-checker Internationalization Format String Checker
* @author Siwakorn Srisakaokul
*/
public class I18nFormatterAnnotatedTypeFactory extends BaseAnnotatedTypeFactory {
protected final AnnotationMirror I18NUNKNOWNFORMAT;
protected final AnnotationMirror I18NFORMAT;
protected final AnnotationMirror I18NINVALIDFORMAT;
protected final AnnotationMirror I18NFORMATBOTTOM;
protected final AnnotationMirror I18NFORMATFOR;
public final Map<String, String> translations;
protected final I18nFormatterTreeUtil treeUtil;
public I18nFormatterAnnotatedTypeFactory(BaseTypeChecker checker) {
super(checker);
I18NUNKNOWNFORMAT = AnnotationUtils.fromClass(elements, I18nUnknownFormat.class);
I18NFORMAT = AnnotationUtils.fromClass(elements, I18nFormat.class);
I18NINVALIDFORMAT = AnnotationUtils.fromClass(elements, I18nInvalidFormat.class);
I18NFORMATBOTTOM = AnnotationUtils.fromClass(elements, I18nFormatBottom.class);
I18NFORMATFOR = AnnotationUtils.fromClass(elements, I18nFormatFor.class);
this.translations = Collections.unmodifiableMap(buildLookup());
this.treeUtil = new I18nFormatterTreeUtil(checker);
this.postInit();
}
@Override
protected Set<Class<? extends Annotation>> createSupportedTypeQualifiers() {
return getBundledTypeQualifiersWithoutPolyAll(
I18nUnknownFormat.class, I18nFormatBottom.class);
}
private Map<String, String> buildLookup() {
Map<String, String> result = new HashMap<String, String>();
if (checker.hasOption("propfiles")) {
String names = checker.getOption("propfiles");
String[] namesArr = names.split(":");
if (namesArr == null) {
System.err.println("Couldn't parse the properties files: <" + names + ">");
} else {
for (String name : namesArr) {
try {
Properties prop = new Properties();
InputStream in = null;
ClassLoader cl = this.getClass().getClassLoader();
if (cl == null) {
// the class loader is null if the system class
// loader was
// used
cl = ClassLoader.getSystemClassLoader();
}
in = cl.getResourceAsStream(name);
if (in == null) {
// if the classloader didn't manage to load the
// file, try
// whether a FileInputStream works. For absolute
// paths this
// might help.
try {
in = new FileInputStream(name);
} catch (FileNotFoundException e) {
// ignore
}
}
if (in == null) {
System.err.println("Couldn't find the properties file: " + name);
// report(Result.failure("propertykeychecker.filenotfound",
// name), null);
// return Collections.emptySet();
continue;
}
prop.load(in);
for (String key : prop.stringPropertyNames()) {
result.put(key, prop.getProperty(key));
}
} catch (Exception e) {
// TODO: is there a nicer way to report messages, that
// are not
// connected to an AST node?
// One cannot use report, because it needs a node.
System.err.println(
"Exception in PropertyKeyChecker.keysOfPropertyFile: " + e);
e.printStackTrace();
}
}
}
}
if (checker.hasOption("bundlenames")) {
String bundleNames = checker.getOption("bundlenames");
String[] namesArr = bundleNames.split(":");
if (namesArr == null) {
System.err.println("Couldn't parse the resource bundles: <" + bundleNames + ">");
} else {
for (String bundleName : namesArr) {
ResourceBundle bundle = ResourceBundle.getBundle(bundleName);
if (bundle == null) {
System.err.println(
"Couldn't find the resource bundle: <"
+ bundleName
+ "> for locale <"
+ Locale.getDefault()
+ ">");
continue;
}
for (String key : bundle.keySet()) {
result.put(key, bundle.getString(key));
}
}
}
}
return result;
}
@Override
public QualifierHierarchy createQualifierHierarchy(MultiGraphFactory factory) {
return new I18nFormatterQualifierHierarchy(factory);
}
@Override
public TreeAnnotator createTreeAnnotator() {
return new ListTreeAnnotator(
super.createTreeAnnotator(), new I18nFormatterTreeAnnotator(this));
}
private class I18nFormatterTreeAnnotator extends TreeAnnotator {
public I18nFormatterTreeAnnotator(AnnotatedTypeFactory atypeFactory) {
super(atypeFactory);
}
@Override
public Void visitLiteral(LiteralTree tree, AnnotatedTypeMirror type) {
if (!type.isAnnotatedInHierarchy(I18NFORMAT)) {
String format = null;
if (tree.getKind() == Tree.Kind.STRING_LITERAL) {
format = (String) tree.getValue();
} else if (tree.getKind() == Tree.Kind.CHAR_LITERAL) {
format = Character.toString((Character) tree.getValue());
}
if (format != null) {
AnnotationMirror anno;
try {
I18nConversionCategory[] cs =
I18nFormatUtil.formatParameterCategories(format);
anno =
I18nFormatterAnnotatedTypeFactory.this.treeUtil
.categoriesToFormatAnnotation(cs);
} catch (IllegalArgumentException e) {
anno =
I18nFormatterAnnotatedTypeFactory.this.treeUtil
.exceptionToInvalidFormatAnnotation(e);
}
type.addAnnotation(anno);
}
}
return super.visitLiteral(tree, type);
}
}
class I18nFormatterQualifierHierarchy extends GraphQualifierHierarchy {
public I18nFormatterQualifierHierarchy(MultiGraphFactory f) {
super(f, I18NFORMATBOTTOM);
}
@Override
public boolean isSubtype(AnnotationMirror subAnno, AnnotationMirror superAnno) {
if (AnnotationUtils.areSameIgnoringValues(subAnno, I18NFORMAT)
&& AnnotationUtils.areSameIgnoringValues(superAnno, I18NFORMAT)) {
I18nConversionCategory[] rhsArgTypes =
treeUtil.formatAnnotationToCategories(subAnno);
I18nConversionCategory[] lhsArgTypes =
treeUtil.formatAnnotationToCategories(superAnno);
if (rhsArgTypes.length > lhsArgTypes.length) {
return false;
}
for (int i = 0; i < rhsArgTypes.length; ++i) {
if (!I18nConversionCategory.isSubsetOf(lhsArgTypes[i], rhsArgTypes[i])) {
return false;
}
}
return true;
}
if (AnnotationUtils.areSameIgnoringValues(superAnno, I18NINVALIDFORMAT)
&& AnnotationUtils.areSameIgnoringValues(subAnno, I18NINVALIDFORMAT)) {
return (AnnotationUtils.getElementValue(subAnno, "value", String.class, true))
.equals(
AnnotationUtils.getElementValue(
superAnno, "value", String.class, true));
}
if (AnnotationUtils.areSameIgnoringValues(superAnno, I18NFORMAT)) {
superAnno = I18NFORMAT;
}
if (AnnotationUtils.areSameIgnoringValues(subAnno, I18NFORMAT)) {
subAnno = I18NFORMAT;
}
if (AnnotationUtils.areSameIgnoringValues(superAnno, I18NINVALIDFORMAT)) {
superAnno = I18NINVALIDFORMAT;
}
if (AnnotationUtils.areSameIgnoringValues(subAnno, I18NINVALIDFORMAT)) {
subAnno = I18NINVALIDFORMAT;
}
if (AnnotationUtils.areSameIgnoringValues(superAnno, I18NFORMATFOR)) {
superAnno = I18NFORMATFOR;
}
if (AnnotationUtils.areSameIgnoringValues(subAnno, I18NFORMATFOR)) {
subAnno = I18NFORMATFOR;
}
return super.isSubtype(subAnno, superAnno);
}
@Override
public AnnotationMirror leastUpperBound(AnnotationMirror anno1, AnnotationMirror anno2) {
if (AnnotationUtils.areSameIgnoringValues(anno1, I18NFORMATBOTTOM)) {
return anno2;
}
if (AnnotationUtils.areSameIgnoringValues(anno2, I18NFORMATBOTTOM)) {
return anno1;
}
if (AnnotationUtils.areSameIgnoringValues(anno1, I18NFORMAT)
&& AnnotationUtils.areSameIgnoringValues(anno2, I18NFORMAT)) {
I18nConversionCategory[] shorterArgTypesList =
treeUtil.formatAnnotationToCategories(anno1);
I18nConversionCategory[] longerArgTypesList =
treeUtil.formatAnnotationToCategories(anno2);
if (shorterArgTypesList.length > longerArgTypesList.length) {
I18nConversionCategory[] temp = longerArgTypesList;
longerArgTypesList = shorterArgTypesList;
shorterArgTypesList = temp;
}
// From the manual:
// It is legal to use a format string with fewer format specifiers
// than required, but a warning is issued.
I18nConversionCategory[] resultArgTypes =
new I18nConversionCategory[longerArgTypesList.length];
for (int i = 0; i < shorterArgTypesList.length; ++i) {
resultArgTypes[i] =
I18nConversionCategory.intersect(
shorterArgTypesList[i], longerArgTypesList[i]);
}
for (int i = shorterArgTypesList.length; i < longerArgTypesList.length; ++i) {
resultArgTypes[i] = longerArgTypesList[i];
}
return treeUtil.categoriesToFormatAnnotation(resultArgTypes);
}
if (AnnotationUtils.areSameIgnoringValues(anno1, I18NINVALIDFORMAT)
&& AnnotationUtils.areSameIgnoringValues(anno2, I18NINVALIDFORMAT)) {
assert !anno1.getElementValues().isEmpty();
assert !anno2.getElementValues().isEmpty();
if (AnnotationUtils.areSame(anno1, anno2)) {
return anno1;
}
return treeUtil.stringToInvalidFormatAnnotation(
"("
+ treeUtil.invalidFormatAnnotationToErrorMessage(anno1)
+ " or "
+ treeUtil.invalidFormatAnnotationToErrorMessage(anno2)
+ ")");
}
// All @I18nFormatFor annotations are unrelated by subtyping.
if (AnnotationUtils.areSameIgnoringValues(anno1, I18NFORMATFOR)
&& AnnotationUtils.areSame(anno1, anno2)) {
return anno1;
}
return I18NUNKNOWNFORMAT;
}
@Override
public AnnotationMirror greatestLowerBound(AnnotationMirror anno1, AnnotationMirror anno2) {
if (AnnotationUtils.areSameIgnoringValues(anno1, I18NUNKNOWNFORMAT)) {
return anno2;
}
if (AnnotationUtils.areSameIgnoringValues(anno2, I18NUNKNOWNFORMAT)) {
return anno1;
}
if (AnnotationUtils.areSameIgnoringValues(anno1, I18NFORMAT)
&& AnnotationUtils.areSameIgnoringValues(anno2, I18NFORMAT)) {
I18nConversionCategory[] anno1ArgTypes =
treeUtil.formatAnnotationToCategories(anno1);
I18nConversionCategory[] anno2ArgTypes =
treeUtil.formatAnnotationToCategories(anno2);
// From the manual:
// It is legal to use a format string with fewer format specifiers
// than required, but a warning is issued.
int length = anno1ArgTypes.length;
if (anno2ArgTypes.length < length) {
length = anno2ArgTypes.length;
}
I18nConversionCategory[] anno3ArgTypes = new I18nConversionCategory[length];
for (int i = 0; i < length; ++i) {
anno3ArgTypes[i] =
I18nConversionCategory.union(anno1ArgTypes[i], anno2ArgTypes[i]);
}
return treeUtil.categoriesToFormatAnnotation(anno3ArgTypes);
}
if (AnnotationUtils.areSameIgnoringValues(anno1, I18NINVALIDFORMAT)
&& AnnotationUtils.areSameIgnoringValues(anno2, I18NINVALIDFORMAT)) {
assert !anno1.getElementValues().isEmpty();
assert !anno2.getElementValues().isEmpty();
if (AnnotationUtils.areSame(anno1, anno2)) {
return anno1;
}
return treeUtil.stringToInvalidFormatAnnotation(
"("
+ treeUtil.invalidFormatAnnotationToErrorMessage(anno1)
+ " and "
+ treeUtil.invalidFormatAnnotationToErrorMessage(anno2)
+ ")");
}
// All @I18nFormatFor annotations are unrelated by subtyping.
if (AnnotationUtils.areSameIgnoringValues(anno1, I18NFORMATFOR)
&& AnnotationUtils.areSame(anno1, anno2)) {
return anno1;
}
return I18NFORMATBOTTOM;
}
}
}