blob: 46d809e506e1fd64847ea9e07973f481b1f88000 [file] [log] [blame]
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.AnnotationBuilder;
import org.plumelib.reflection.Signatures;
/**
* This AnnotatedTypeFactory adds PropertyKey annotations to String literals that contain values
* from lookupKeys.
*/
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 = AnnotationBuilder.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.
*
* @return the valid keys that can be used
*/
public Set<String> getLookupKeys() {
return this.lookupKeys;
}
private Set<String> buildLookupKeys() {
Set<String> result = new HashSet<>();
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<>(namesArr.length);
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) {
checker.message(Kind.WARNING, "Couldn't find the properties file: " + name);
// report(null, "propertykeychecker.filenotfound", name);
// 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<>(namesArr.length);
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) {
checker.message(
Kind.WARNING,
"Couldn't find the resource bundle: <"
+ bundleName
+ "> for locale <"
+ Locale.getDefault()
+ ">");
continue;
}
result.addAll(bundle.keySet());
}
return result;
}
}