blob: 50f928571bb6463038ef18a6ced1a2d93cd33cd6 [file] [log] [blame]
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.util.Collections;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.ResourceBundle;
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.checker.i18nformatter.util.I18nFormatUtil;
import org.checkerframework.checker.signature.qual.CanonicalName;
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.MostlyNoElementQualifierHierarchy;
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.QualifierKind;
import org.checkerframework.javacutil.AnnotationBuilder;
import org.checkerframework.javacutil.AnnotationUtils;
import org.checkerframework.javacutil.BugInCF;
import org.plumelib.reflection.Signatures;
/**
* 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
*/
public class I18nFormatterAnnotatedTypeFactory extends BaseAnnotatedTypeFactory {
/** The @{@link I18nUnknownFormat} annotation. */
protected final AnnotationMirror I18NUNKNOWNFORMAT =
AnnotationBuilder.fromClass(elements, I18nUnknownFormat.class);
/** The @{@link I18nFormatBottom} annotation. */
protected final AnnotationMirror I18NFORMATBOTTOM =
AnnotationBuilder.fromClass(elements, I18nFormatBottom.class);
/** The fully-qualified name of {@link I18nFormat}. */
protected static final @CanonicalName String I18NFORMAT_NAME =
I18nFormat.class.getCanonicalName();
/** The fully-qualified name of {@link I18nInvalidFormat}. */
protected static final @CanonicalName String I18NINVALIDFORMAT_NAME =
I18nInvalidFormat.class.getCanonicalName();
/** The fully-qualified name of {@link I18nFormatFor}. */
protected static final @CanonicalName String I18NFORMATFOR_NAME =
I18nFormatFor.class.getCanonicalName();
/** Map from a translation file key to its value in the file. */
public final Map<String, String> translations = Collections.unmodifiableMap(buildLookup());
/** Syntax tree utilities. */
protected final I18nFormatterTreeUtil treeUtil = new I18nFormatterTreeUtil(checker);
/** Create a new I18nFormatterAnnotatedTypeFactory. */
public I18nFormatterAnnotatedTypeFactory(BaseTypeChecker checker) {
super(checker);
this.postInit();
}
/**
* Builds a map from a translation file key to its value in the file.
*
* @return a map from a translation file key to its value in the file
*/
private Map<String, String> buildLookup() {
Map<String, String> result = new HashMap<>();
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();
ClassLoader cl = this.getClass().getClassLoader();
if (cl == null) {
// The class loader is null if the system class loader was used.
cl = ClassLoader.getSystemClassLoader();
}
InputStream 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(null, "propertykeychecker.filenotfound", name);
// 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) {
if (!Signatures.isBinaryName(bundleName)) {
System.err.println(
"Malformed resource bundle: <" + bundleName + "> should be a binary name.");
continue;
}
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
protected QualifierHierarchy createQualifierHierarchy() {
return new I18nFormatterQualifierHierarchy();
}
@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(I18NUNKNOWNFORMAT)) {
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);
}
}
/** I18nFormatterQualifierHierarchy. */
class I18nFormatterQualifierHierarchy extends MostlyNoElementQualifierHierarchy {
/** Qualifier kind for the @{@link I18nFormat} annotation. */
private final QualifierKind I18NFORMAT_KIND;
/** Qualifier kind for the @{@link I18nFormatFor} annotation. */
private final QualifierKind I18NFORMATFOR_KIND;
/** Qualifier kind for the @{@link I18nInvalidFormat} annotation. */
private final QualifierKind I18NINVALIDFORMAT_KIND;
/** Creates I18nFormatterQualifierHierarchy. */
public I18nFormatterQualifierHierarchy() {
super(I18nFormatterAnnotatedTypeFactory.this.getSupportedTypeQualifiers(), elements);
this.I18NFORMAT_KIND = this.getQualifierKind(I18NFORMAT_NAME);
this.I18NFORMATFOR_KIND = this.getQualifierKind(I18NFORMATFOR_NAME);
this.I18NINVALIDFORMAT_KIND = this.getQualifierKind(I18NINVALIDFORMAT_NAME);
}
@Override
protected boolean isSubtypeWithElements(
AnnotationMirror subAnno,
QualifierKind subKind,
AnnotationMirror superAnno,
QualifierKind superKind) {
if (subKind == I18NFORMAT_KIND && superKind == I18NFORMAT_KIND) {
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;
} else if ((subKind == I18NINVALIDFORMAT_KIND && superKind == I18NINVALIDFORMAT_KIND)
|| (subKind == I18NFORMATFOR_KIND && superKind == I18NFORMATFOR_KIND)) {
return Objects.equals(
treeUtil.getI18nInvalidFormatValue(subAnno),
treeUtil.getI18nInvalidFormatValue(superAnno));
}
throw new BugInCF("Unexpected QualifierKinds: %s %s", subKind, superKind);
}
@Override
protected AnnotationMirror leastUpperBoundWithElements(
AnnotationMirror anno1,
QualifierKind qualifierKind1,
AnnotationMirror anno2,
QualifierKind qualifierKind2,
QualifierKind lubKind) {
if (qualifierKind1.isBottom()) {
return anno2;
} else if (qualifierKind2.isBottom()) {
return anno1;
} else if (qualifierKind1 == I18NFORMAT_KIND && qualifierKind2 == I18NFORMAT_KIND) {
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);
} else if (qualifierKind1 == I18NINVALIDFORMAT_KIND
&& qualifierKind2 == I18NINVALIDFORMAT_KIND) {
assert !anno1.getElementValues().isEmpty();
assert !anno1.getElementValues().isEmpty();
if (AnnotationUtils.areSame(anno1, anno2)) {
return anno1;
}
return treeUtil.stringToInvalidFormatAnnotation(
"("
+ treeUtil.invalidFormatAnnotationToErrorMessage(anno1)
+ " or "
+ treeUtil.invalidFormatAnnotationToErrorMessage(anno2)
+ ")");
} else if (qualifierKind1 == I18NFORMATFOR_KIND && AnnotationUtils.areSame(anno1, anno2)) {
// @I18nFormatFor annotations are unrelated by subtyping, unless they are identical.
return anno1;
}
return I18NUNKNOWNFORMAT;
}
@Override
protected AnnotationMirror greatestLowerBoundWithElements(
AnnotationMirror anno1,
QualifierKind qualifierKind1,
AnnotationMirror anno2,
QualifierKind qualifierKind2,
QualifierKind glbKind) {
if (qualifierKind1.isTop()) {
return anno2;
} else if (qualifierKind2.isTop()) {
return anno1;
} else if (qualifierKind1 == I18NFORMAT_KIND && qualifierKind2 == I18NFORMAT_KIND) {
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);
} else if (qualifierKind1 == I18NINVALIDFORMAT_KIND
&& qualifierKind2 == I18NINVALIDFORMAT_KIND) {
assert !anno2.getElementValues().isEmpty();
if (AnnotationUtils.areSame(anno1, anno2)) {
return anno1;
}
return treeUtil.stringToInvalidFormatAnnotation(
"("
+ treeUtil.invalidFormatAnnotationToErrorMessage(anno1)
+ " and "
+ treeUtil.invalidFormatAnnotationToErrorMessage(anno2)
+ ")");
} else if (qualifierKind1 == I18NFORMATFOR_KIND && AnnotationUtils.areSame(anno1, anno2)) {
// @I18nFormatFor annotations are unrelated by subtyping, unless they are identical.
return anno1;
}
return I18NFORMATBOTTOM;
}
}
}