package org.checkerframework.checker.propkey;
import com.sun.source.tree.BinaryTree;
import com.sun.source.tree.CompoundAssignmentTree;
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.HashSet;
import java.util.Locale;
import java.util.Properties;
import java.util.ResourceBundle;
import java.util.Set;
import javax.lang.model.element.AnnotationMirror;
import javax.tools.Diagnostic.Kind;
import org.checkerframework.checker.propkey.qual.PropertyKey;
import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory;
import org.checkerframework.common.basetype.BaseTypeChecker;
import org.checkerframework.framework.type.AnnotatedTypeMirror;
import org.checkerframework.framework.type.treeannotator.ListTreeAnnotator;
import org.checkerframework.framework.type.treeannotator.TreeAnnotator;
import org.checkerframework.javacutil.AnnotationUtils;
/**
* This AnnotatedTypeFactory adds PropertyKey annotations to String literals that contain values
* from lookupKeys.
*
* @author wmdietl
*/
public class PropertyKeyAnnotatedTypeFactory extends BaseAnnotatedTypeFactory {
private final Set<String> lookupKeys;
public PropertyKeyAnnotatedTypeFactory(BaseTypeChecker checker) {
super(checker);
this.lookupKeys = Collections.unmodifiableSet(buildLookupKeys());
this.postInit();
}
@Override
public TreeAnnotator createTreeAnnotator() {
return new ListTreeAnnotator(
super.createTreeAnnotator(), new KeyLookupTreeAnnotator(this, PropertyKey.class));
}
// To allow subclasses access to createTreeAnnotator from the BATF.
protected TreeAnnotator createBasicTreeAnnotator() {
return super.createTreeAnnotator();
}
/**
* This TreeAnnotator checks for every String literal whether it is included in the lookup keys.
* If it is, the given annotation is added to the literal; otherwise, nothing happens.
* Subclasses of this AnnotatedTypeFactory can directly reuse this class and use a different
* annotation as parameter.
*/
protected class KeyLookupTreeAnnotator extends TreeAnnotator {
AnnotationMirror theAnnot;
public KeyLookupTreeAnnotator(
BaseAnnotatedTypeFactory atf, Class<? extends Annotation> annot) {
super(atf);
theAnnot = AnnotationUtils.fromClass(elements, annot);
}
@Override
public Void visitLiteral(LiteralTree tree, AnnotatedTypeMirror type) {
if (!type.isAnnotatedInHierarchy(theAnnot)
&& tree.getKind() == Tree.Kind.STRING_LITERAL
&& strContains(lookupKeys, tree.getValue().toString())) {
type.addAnnotation(theAnnot);
}
// A possible extension is to record all the keys that have been used and
// in the end output a list of keys that were not used in the program,
// possibly pointing to the opposite problem, keys that were supposed to
// be used somewhere, but have not been, maybe because of copy-and-paste errors.
return super.visitLiteral(tree, type);
}
// Result of binary op might not be a property key.
@Override
public Void visitBinary(BinaryTree node, AnnotatedTypeMirror type) {
type.removeAnnotation(theAnnot);
return null; // super.visitBinary(node, type);
}
// Result of unary op might not be a property key.
@Override
public Void visitCompoundAssignment(CompoundAssignmentTree node, AnnotatedTypeMirror type) {
type.removeAnnotation(theAnnot);
return null; // super.visitCompoundAssignment(node, type);
}
}
/**
* Instead of a precise comparison, we incrementally remove leading dot-separated strings until
* we find a match. For example if messages contains "y.z" and we look for "x.y.z" we find a
* match after removing the first "x.".
*
* <p>Compare to SourceChecker.fullMessageOf.
*/
private static boolean strContains(Set<String> messages, String messageKey) {
String key = messageKey;
do {
if (messages.contains(key)) {
return true;
}
int dot = key.indexOf('.');
if (dot < 0) return false;
key = key.substring(dot + 1);
} while (true);
}
/** Returns a set of the valid keys that can be used. */
public Set<String> getLookupKeys() {
return this.lookupKeys;
}
private Set<String> buildLookupKeys() {
Set<String> result = new HashSet<String>();
if (checker.hasOption("propfiles")) {
result.addAll(keysOfPropertyFiles(checker.getOption("propfiles")));
}
if (checker.hasOption("bundlenames")) {
result.addAll(keysOfResourceBundle(checker.getOption("bundlenames")));
}
return result;
}
private Set<String> keysOfPropertyFiles(String names) {
String[] namesArr = names.split(":");
if (namesArr == null) {
checker.message(Kind.WARNING, "Couldn't parse the properties files: <" + names + ">");
return Collections.emptySet();
}
Set<String> result = new HashSet<String>();
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) {
checker.message(Kind.WARNING, "Couldn't find the properties file: " + name);
// report(Result.failure("propertykeychecker.filenotfound",
// name), null);
// return Collections.emptySet();
continue;
}
prop.load(in);
result.addAll(prop.stringPropertyNames());
} 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.
checker.message(
Kind.WARNING, "Exception in PropertyKeyChecker.keysOfPropertyFile: " + e);
e.printStackTrace();
}
}
return result;
}
private Set<String> keysOfResourceBundle(String bundleNames) {
String[] namesArr = bundleNames.split(":");
if (namesArr == null) {
checker.message(
Kind.WARNING, "Couldn't parse the resource bundles: <" + bundleNames + ">");
return Collections.emptySet();
}
Set<String> result = new HashSet<String>();
for (String bundleName : namesArr) {
ResourceBundle bundle = ResourceBundle.getBundle(bundleName);
if (bundle == null) {
checker.message(
Kind.WARNING,
"Couldn't find the resource bundle: <"
+ bundleName
+ "> for locale <"
+ Locale.getDefault()
+ ">");
continue;
}
result.addAll(bundle.keySet());
}
return result;
}
}