| 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; |
| } |
| } |