package checkers.source; import java.io.IOException; import java.io.InputStream; import java.util.*; import java.util.regex.*; import javax.annotation.processing.*; import javax.lang.model.SourceVersion; import javax.lang.model.element.*; import javax.tools.Diagnostic; import checkers.basetype.BaseTypeChecker; import checkers.compilermsgs.quals.CompilerMessageKey; import checkers.nullness.quals.*; import checkers.quals.TypeQualifiers; import checkers.types.*; import checkers.util.*; import com.sun.source.tree.*; import com.sun.source.util.*; import com.sun.tools.javac.processing.*; /** * An abstract annotation processor designed for implementing a * source-file checker for a JSR-308 conforming compiler plug-in. It provides an * interface to {@code javac}'s annotation processing API, routines for error * reporting via the JSR 199 compiler API, and an implementation for using a * {@link SourceVisitor} to perform the type-checking. * * <p> * * Subclasses must implement the following methods: * * <ul> * <li>{@link SourceChecker#getMessages} (for type-qualifier specific error messages) * <li>{@link SourceChecker#createSourceVisitor(CompilationUnitTree)} (for a custom {@link SourceVisitor}) * <li>{@link SourceChecker#createFactory} (for a custom {@link AnnotatedTypeFactory}) * <li>{@link SourceChecker#getSuppressWarningsKey} (for honoring * {@link SuppressWarnings} annotations) * </ul> * * Most type-checker plug-ins will want to extend {@link BaseTypeChecker}, * instead of this class. Only checkers which require annotated types but not * subtype checking (e.g. for testing purposes) should extend this. * Non-type checkers (e.g. for enforcing coding styles) should extend * {@link AbstractProcessor} (or even this class) as the Checker Framework is * not designed for such checkers. */ public abstract class SourceChecker extends AbstractTypeProcessor { // TODO checkers should export themselves through a separate interface, // and maybe have an interface for all the methods for which it's safe // to override /** Provides access to compiler helpers/internals. */ protected ProcessingEnvironment env; /** file name of the localized messages */ private static final String MSGS_FILE = "messages.properties"; /** Maps error keys to localized/custom error messages. */ protected Properties messages; /** Used to report error messages and warnings via the compiler. */ protected JavacMessager messager; /** Used as a helper for the {@link SourceVisitor}. */ protected Trees trees; /** The source tree that's being scanned. */ protected CompilationUnitTree currentRoot; public TreePath currentPath; /** issue errors as warnings */ private boolean warns; /** A regular expression for classes that should be skipped. */ private Pattern skipPattern; /** The chosent lint options that have been enabled by programmer */ private Set<String> activeLints; /** The line separator */ private final static String LINE_SEPARATOR = System.getProperty("line.separator").intern(); /** * @return the {@link ProcessingEnvironment} that was supplied to this * checker */ public ProcessingEnvironment getProcessingEnvironment() { return this.env; } /** * @param root the AST root for the factory * @return an {@link AnnotatedTypeFactory} for use by typecheckers */ public AnnotatedTypeFactory createFactory(CompilationUnitTree root) { return new AnnotatedTypeFactory(this, root); } /** * Provides the {@link SourceVisitor} that the checker should use to scan * input source trees. * * @param root * the AST root * @return a {@link SourceVisitor} to use to scan source trees */ protected abstract SourceVisitor<?, ?> createSourceVisitor(CompilationUnitTree root); /** * Provides a mapping of error keys to custom error messages. * * As a default, this implementation builds a {@link Properties} out of * file {@code messages.properties}. It accumulates all the properties files * in the Java class hierarchy from the checker up to {@code SourceChecker}. * This permits subclasses to inherit default messages while being able to * override them. * * @return a {@link Properties} that maps error keys to error message text */ public Properties getMessages() { if (this.messages != null) return this.messages; this.messages = new Properties(); Stack<Class<?>> checkers = new Stack<Class<?>>(); Class<?> currClass = this.getClass(); while (currClass != SourceChecker.class) { checkers.push(currClass); currClass = currClass.getSuperclass(); } checkers.push(SourceChecker.class); while (!checkers.empty()) messages.putAll(getProperties(checkers.pop(), MSGS_FILE)); return this.messages; } private Pattern getSkipPattern(Map<String, String> options) { String pattern = ""; if (options.containsKey("skipClasses")) pattern = options.get("skipClasses"); else if (System.getProperty("checkers.skipClasses") != null) pattern = System.getProperty("checkers.skipClasses"); else if (System.getenv("skipClasses") != null) pattern = System.getenv("skipClasses"); // return a pattern of an illegal Java identifier character if (pattern.equals("")) pattern = "\\("; return Pattern.compile(pattern); } private Set<String> createActiveLints(Map<String, String> options) { if (!options.containsKey("lint")) return Collections.emptySet(); String lintString = options.get("lint"); if (lintString == null) { return Collections.singleton("all"); } Set<String> activeLint = new HashSet<String>(); for (String s : lintString.split(",")) { activeLint.add(s); if (s.equals("none")) activeLint.add("-all"); } return activeLint; } /** * {@inheritDoc} * * @see AbstractProcessor#init(ProcessingEnvironment) */ @Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); this.env = processingEnv; this.skipPattern = getSkipPattern(processingEnv.getOptions()); // Grab the Trees and Messager instances now; other utilities // (like Types and Elements) can be retrieved by subclasses. @Nullable Trees trees = Trees.instance(processingEnv); assert trees != null; /*nninvariant*/ this.trees = trees; this.messager = (JavacMessager) processingEnv.getMessager(); this.messages = getMessages(); this.warns = processingEnv.getOptions().containsKey("warns"); this.activeLints = createActiveLints(processingEnv.getOptions()); } /** * Type-check the code with Java specifications and then runs the Checker * Rule Checking visitor on the processed source. * * @see Processor#process(Set, RoundEnvironment) */ @Override public void typeProcess(TypeElement e, TreePath p) { currentRoot = p.getCompilationUnit(); currentPath = p; // Visit the attributed tree. try { SourceVisitor<?,?> visitor = createSourceVisitor(currentRoot); visitor.scan(p, null); } catch (Throwable exception) { String message = getClass().getSimpleName().replaceAll("Checker", "") + " processor threw unexpected exception when processing " + currentRoot.getSourceFile().getName(); Error err = new Error(message, exception); err.printStackTrace(); throw err; } } // Uses private fields, need to rewrite. // public void dumpState() { // System.out.printf("SourceChecker = %s%n", this); // System.out.printf(" env = %s%n", env); // System.out.printf(" env.elementUtils = %s%n", ((JavacProcessingEnvironment) env).elementUtils); // System.out.printf(" env.elementUtils.types = %s%n", ((JavacProcessingEnvironment) env).elementUtils.types); // System.out.printf(" env.elementUtils.enter = %s%n", ((JavacProcessingEnvironment) env).elementUtils.enter); // System.out.printf(" env.typeUtils = %s%n", ((JavacProcessingEnvironment) env).typeUtils); // System.out.printf(" trees = %s%n", trees); // System.out.printf(" trees.enter = %s%n", ((com.sun.tools.javac.api.JavacTrees) trees).enter); // System.out.printf(" trees.elements = %s%n", ((com.sun.tools.javac.api.JavacTrees) trees).elements); // System.out.printf(" trees.elements.types = %s%n", ((com.sun.tools.javac.api.JavacTrees) trees).elements.types); // System.out.printf(" trees.elements.enter = %s%n", ((com.sun.tools.javac.api.JavacTrees) trees).elements.enter); // } /** * Returns the localized long message corresponding for this key, and * returns the defValue if no localized message is found. * */ protected String fullMessageOf(String messageKey, String defValue) { String key = messageKey; do { if (messages.containsKey(key)) { return messages.getProperty(key); } int dot = key.indexOf('.'); if (dot < 0) return defValue; key = key.substring(dot + 1); } while (true); } /** * Prints a message (error, warning, note, etc.) via JSR-269. * * @param kind * the type of message to print * @param source * the object from which to obtain source position information * @param msgKey * the message key to print * @param args * arguments for interpolation in the string corresponding to the * given message key * @see Diagnostic * @throws IllegalArgumentException * if {@code source} is neither a {@link Tree} nor an * {@link Element} */ protected void message(Diagnostic.Kind kind, Object source, @CompilerMessageKey String msgKey, Object... args) { assert messages != null : "null messages"; if (args != null) { // look whether we can expand the arguments, too. for (int i = 0; i < args.length; ++i) { args[i] = (args[i] == null) ? null : messages.getProperty(args[i].toString(), args[i].toString()); } } if (kind == Diagnostic.Kind.NOTE) { System.err.println("(NOTE) " + String.format(msgKey, args)); return; } final String defaultFormat = String.format("(%s)", msgKey); String fmtString; if (this.env.getOptions() != null /*nnbug*/ && this.env.getOptions().containsKey("nomsgtext")) fmtString = defaultFormat; else fmtString = fullMessageOf(msgKey, defaultFormat); String messageText = String.format(fmtString, args); // Replace '\n' with the proper line separator if (LINE_SEPARATOR != "\n") // interned messageText = messageText.replaceAll("\n", LINE_SEPARATOR); if (source instanceof Element) messager.printMessage(kind, messageText, (Element) source); else if (source instanceof Tree) Trees.instance(env).printMessage(kind, messageText, (Tree) source, currentRoot); else throw new IllegalArgumentException("invalid position source: " + source.getClass().getName()); } /** * Determines if an error (whose error key is {@code err}), should * be suppressed according to the user explicitly written * {@code anno} Suppress annotation. * * A suppress warnings value may be of the following pattern: * * <ol> * <li>{@code "suppress-key"}, where suppress-key is a supported warnings key, as * specified by {@link #getSuppressWarningsKey()}, * e.g. {@code "nullness"} for nullness, {@code "igj"} for igj * test</li> * * <li>{@code "suppress-key:error-key}, where the suppress-key * is as above, and error-key being the prefix of the errors * that it may suppress. So "nullness:generic.argument", would * suppress any errors in nullness checker related to * generic.argument. * * @param annos the annotations to search * @param err the error key the checker is emitting * @return true if one of {@code annos} is a {@link SuppressWarnings} * annotation with the key returned by {@link * SourceChecker#getSuppressWarningsKey} */ private boolean checkSuppressWarnings(SuppressWarnings anno, String err) { if (anno == null) return false; Collection<String> swkeys = this.getSuppressWarningsKey(); // For all the method's annotations, check for a @SuppressWarnings // annotation. If one is found, check its values for this checker's // SuppressWarnings key. for (String suppressWarningValue : anno.value()) { for (String swKey : swkeys) { if (suppressWarningValue.equals(swKey)) return true; String expected = swKey + ":" + err; if (expected.contains(suppressWarningValue)) return true; } } return false; } /** * Determines whether the warnings pertaining to a given tree should be * suppressed (namely, if its containing method has a @SuppressWarnings * annotation for which one of the values is the key provided by the {@link * SourceChecker#getSuppressWarningsKey} method). * * @param tree the tree that might be a source of a warning * @return true if no warning should be emitted for the given tree because * it is contained by a method with an appropriately-valued * @SuppressWarnings annotation; false otherwise */ private boolean shouldSuppressWarnings(Tree tree, String err) { // Don't suppress warnings if there's no key. Collection<String> swKeys = this.getSuppressWarningsKey(); if (swKeys.isEmpty()) return false; @Nullable TreePath path = trees.getPath(this.currentRoot, tree); if (path == null) return false; @Nullable VariableTree var = TreeUtils.enclosingVariable(path); if (var != null && shouldSuppressWarnings(InternalUtils.symbol(var), err)) return true; @Nullable MethodTree method = TreeUtils.enclosingMethod(path); if (method != null && shouldSuppressWarnings(InternalUtils.symbol(method), err)) return true; @Nullable ClassTree cls = TreeUtils.enclosingClass(path); if (cls != null && shouldSuppressWarnings(InternalUtils.symbol(cls), err)) return true; return false; } private boolean shouldSuppressWarnings(@Nullable Element elt, String err) { if (elt == null) return false; return checkSuppressWarnings(elt.getAnnotation(SuppressWarnings.class), err) || shouldSuppressWarnings(elt.getEnclosingElement(), err); } /** * Reports a result. By default, it prints it to the screen via the * compiler's internal messenger if the result is non-success; otherwise, * the method returns with no side-effects. * * @param r * the result to report * @param src * the position object associated with the result */ public void report(final Result r, final Object src) { String err = r.getMessageKeys().iterator().next(); // TODO: SuppressWarnings checking for Elements if (src instanceof Tree && shouldSuppressWarnings((Tree)src, err)) return; if (src instanceof Element && shouldSuppressWarnings((Element)src, err)) return; if (r.isSuccess()) return; for (Result.DiagMessage msg : r.getDiagMessages()) { if (r.isFailure()) this.message(warns ? Diagnostic.Kind.MANDATORY_WARNING : Diagnostic.Kind.ERROR, src, msg.getMessageKey(), msg.getArgs()); else if (r.isWarning()) this.message(Diagnostic.Kind.MANDATORY_WARNING, src, msg.getMessageKey(), msg.getArgs()); else this.message(Diagnostic.Kind.NOTE, src, msg.getMessageKey(), msg.getArgs()); } } /** * Determines the value of the lint option with the given name. Just * as <a * href="http://java.sun.com/j2se/1.5.0/docs/tooldocs/solaris/javac.html">javac</a> * uses "-Xlint:xxx" to enable and "-Xlint:-xxx" to disable option xxx, * annotation-related lint options are enabled with "-Alint:xxx" and * disabled with "-Alint:-xxx". * * @throws IllegalArgumentException if the option name is not recognized * via the {@link SupportedLintOptions} annotation or the {@link * SourceChecker#getSupportedLintOptions} method * @param name the name of the lint option to check for * @return true if the lint option was given, false if it was not given or * was given prepended with a "-" * * @see SourceChecker#getLintOption(String,boolean) */ public final boolean getLintOption(String name) { return getLintOption(name, false); } /** * Determines the value of the lint option with the given name. Just * as <a * href="http://java.sun.com/j2se/1.5.0/docs/tooldocs/solaris/javac.html">javac</a> * uses "-Xlint:xxx" to enable and "-Xlint:-xxx" to disable option xxx, * annotation-related lint options are enabled with "-Alint=xxx" and * disabled with "-Alint=-xxx". * * @throws IllegalArgumentException if the option name is not recognized * via the {@link SupportedLintOptions} annotation or the {@link * SourceChecker#getSupportedLintOptions} method * @param name the name of the lint option to check for * @param def the default option value, returned if the option was not given * @return true if the lint option was given, false if it was given * prepended with a "-", or {@code def} if it was not given at all * * @see SourceChecker#getLintOption(String) */ public final boolean getLintOption(String name, boolean def) { if (!this.getSupportedLintOptions().contains(name)) throw new IllegalArgumentException("illegal lint option: " + name); if (activeLints.isEmpty()) return def; String tofind = name; while (tofind != null) { if (activeLints.contains(tofind)) return true; else if (activeLints.contains(String.format("-%s", tofind))) return false; tofind = parentOfOption(tofind); } return def; } /** * Helper method to find the parent of a lint key. The lint hierarchy * level is donated by a color ':'. 'all' is the root for all hierarchy. * * Example * cast:unsafe --> cast * cast --> all * all --> {@code null} */ private String parentOfOption(String name) { if (name.equals("all")) return null; else if (name.contains(":")) { return name.substring(0, name.lastIndexOf(':')); } else { return "all"; } } /** * Returns the lint options recognized by this checker. Lint options are * those which can be checked for via {@link SourceChecker#getLintOption}. * * @return an unmodifiable {@link Set} of the lint options recognized by * this checker */ public Set<String> getSupportedLintOptions() { @Nullable SupportedLintOptions sl = this.getClass().getAnnotation(SupportedLintOptions.class); if (sl == null) return Collections.</*@NonNull*/ String>emptySet(); @Nullable String /*@Nullable*/ [] slValue = sl.value(); assert slValue != null; /*nninvariant*/ @Nullable String [] lintArray = slValue; Set<String> lintSet = new HashSet<String>(lintArray.length); for (String s : lintArray) lintSet.add(s); return Collections.</*@NonNull*/ String>unmodifiableSet(lintSet); } /* * Force "-Alint" as a recognized processor option for all subtypes of * SourceChecker. * * [This method is provided here, rather than via the @SupportedOptions * annotation, so that it may be inherited by subclasses.] */ @Override public Set<String> getSupportedOptions() { Set<String> options = new HashSet<String>(); options.add("skipClasses"); options.add("lint"); options.add("nomsgtext"); options.add("filenames"); options.add("showchecks"); options.add("stubs"); options.add("warns"); options.add("annotatedTypeParams"); options.addAll(super.getSupportedOptions()); return Collections.</*@NonNull*/ String>unmodifiableSet(options); } /** * Always returns a singleton set containing only "*". * * This method returns the argument to the {@link * SupportedAnnotationTypes} annotation, so the effect of returning "*" * is as if the checker were annotated by * {@code @SupportedAnnotationTypes("*")}: * javac runs the checker on every * class mentioned on the javac command line. This method also checks * that subclasses do not contain a {@link SupportedAnnotationTypes} * annotation. <p> * * To specify the annotations that a checker recognizes as type qualifiers, * use the {@link TypeQualifiers} annotation on the declaration of * subclasses of this class or override the * {@link BaseTypeChecker#getSupportedTypeQualifiers()} method. * * @throws Error if a subclass is annotated with * {@link SupportedAnnotationTypes} * * @see TypeQualifiers * @see BaseTypeChecker#getSupportedAnnotationTypes() */ @Override public final Set<String> getSupportedAnnotationTypes() { SupportedAnnotationTypes supported = this.getClass().getAnnotation( SupportedAnnotationTypes.class); if (supported != null) throw new Error("@SupportedAnnotationTypes should not be written on any checker;" + " supported annotation types are inherited from SourceChecker"); return Collections.singleton("*"); } /** * @return String keys that a checker honors for suppressing warnings * and errors that it issues * * @see SuppressWarningsKey */ public Collection<String> getSuppressWarningsKey() { SuppressWarningsKey annotation = this.getClass().getAnnotation(SuppressWarningsKey.class); if (annotation != null) return Collections.singleton(annotation.value()); // Inferring key from class name String className = this.getClass().getSimpleName(); int indexOfChecker = className.lastIndexOf("Checker"); if (indexOfChecker == -1) indexOfChecker = className.lastIndexOf("Subchecker"); String key = (indexOfChecker == -1) ? className : className.substring(0, indexOfChecker); return Collections.singleton(key.trim().toLowerCase()); } /** * Returns a regular expression pattern to specify java classes that are not * annotated, and thus whose warnings and should be surpressed. * * It returns the pattern specified by the user, through the option * {@code checkers.skipClasses}; otherwise it returns a pattern that can * match no class. * * @return pattern of un-annotated classes that should be skipped */ public Pattern getShouldSkip() { return this.skipPattern; } /** * A helper function to parse a Properties file * * @param cls the class whose location is the base of the file path * @param filePath the name/path of the file to be read * @return the properties */ private Properties getProperties(Class<?> cls, String filePath) { Properties prop = new Properties(); try { InputStream base = cls.getResourceAsStream(filePath); if (base == null) // No message customization file was given return prop; prop.load(base); } catch (IOException e) { System.err.println("Couldn't parse " + filePath + " file"); e.printStackTrace(); // ignore the possible customization file } return prop; } @Override public final SourceVersion getSupportedSourceVersion() { try { return SourceVersion.RELEASE_7; } catch (NoSuchFieldError e) { // Running in JDK 6 return SourceVersion.latest(); } } }