All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.checkerframework.framework.source.SourceChecker Maven / Gradle / Ivy

Go to download

The Checker Framework enhances Java's type system to make it more powerful and useful. This lets software developers detect and prevent errors in their Java programs. The Checker Framework includes compiler plug-ins ("checkers") that find bugs or verify their absence. It also permits you to write your own compiler plug-ins.

There is a newer version: 3.44.0
Show newest version
package org.checkerframework.framework.source;

import com.sun.source.tree.AnnotationTree;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.VariableTree;
import com.sun.source.util.SourcePositions;
import com.sun.source.util.TreePath;
import com.sun.source.util.Trees;
import com.sun.tools.javac.code.Source;
import com.sun.tools.javac.processing.JavacProcessingEnvironment;
import com.sun.tools.javac.util.Context;
import com.sun.tools.javac.util.DiagnosticSource;
import com.sun.tools.javac.util.JCDiagnostic.DiagnosticPosition;
import com.sun.tools.javac.util.Log;
import io.github.classgraph.ClassGraph;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryPoolMXBean;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.net.URI;
import java.time.Instant;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.NavigableSet;
import java.util.Objects;
import java.util.Properties;
import java.util.Set;
import java.util.SortedSet;
import java.util.StringJoiner;
import java.util.TreeSet;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic;
import javax.tools.Diagnostic.Kind;
import org.checkerframework.checker.compilermsgs.qual.CompilerMessageKey;
import org.checkerframework.checker.formatter.qual.FormatMethod;
import org.checkerframework.checker.interning.qual.InternedDistinct;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.checker.signature.qual.CanonicalName;
import org.checkerframework.checker.signature.qual.FullyQualifiedName;
import org.checkerframework.common.basetype.BaseTypeChecker;
import org.checkerframework.framework.qual.AnnotatedFor;
import org.checkerframework.framework.type.AnnotatedTypeFactory;
import org.checkerframework.framework.util.CheckerMain;
import org.checkerframework.framework.util.OptionConfiguration;
import org.checkerframework.javacutil.AbstractTypeProcessor;
import org.checkerframework.javacutil.AnnotationProvider;
import org.checkerframework.javacutil.AnnotationUtils;
import org.checkerframework.javacutil.BugInCF;
import org.checkerframework.javacutil.ElementUtils;
import org.checkerframework.javacutil.SystemUtil;
import org.checkerframework.javacutil.TreePathUtil;
import org.checkerframework.javacutil.TreeUtils;
import org.checkerframework.javacutil.TypeSystemError;
import org.checkerframework.javacutil.UserError;
import org.plumelib.util.CollectionsPlume;
import org.plumelib.util.SystemPlume;
import org.plumelib.util.UtilPlume;

/**
 * An abstract annotation processor designed for implementing a source-file checker as an annotation
 * processor (a 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.
 *
 * 

Most type-checker plug-ins should extend {@link BaseTypeChecker}, instead of this class. Only * checkers that require annotated types but not subtype checking (e.g. for testing purposes) should * extend this. Non-type checkers (e.g. for enforcing coding styles) may extend {@link * AbstractProcessor} (or even this class). */ @SupportedOptions({ // When adding a new standard option: // 1. Add a brief blurb here about the use case // and a pointer to one prominent use of the option. // 2. Update the Checker Framework manual: // * docs/manual/introduction.tex contains a list of all options, // which should be in the same order as this source code file. // * a specific section should contain a detailed discussion. /// /// Unsound checking: ignore some errors /// // A comma-separated list of warnings to suppress // org.checkerframework.framework.source.SourceChecker.createSuppressWarnings "suppressWarnings", // Set inclusion/exclusion of type uses or definitions // org.checkerframework.framework.source.SourceChecker.shouldSkipUses and similar "skipUses", "onlyUses", "skipDefs", "onlyDefs", // Unsoundly assume all methods have no side effects, are deterministic, or both. "assumeSideEffectFree", "assumeDeterministic", "assumePure", // Whether to assume that assertions are enabled or disabled // org.checkerframework.framework.flow.CFCFGBuilder.CFCFGBuilder "assumeAssertionsAreEnabled", "assumeAssertionsAreDisabled", // Treat checker errors as warnings // org.checkerframework.framework.source.SourceChecker.report "warns", /// /// More sound (strict checking): enable errors that are disabled by default /// // The next ones *increase* rather than *decrease* soundness. They will eventually be replaced by // their complements (except -AconcurrentSemantics) and moved into the above section. // TODO: Checking of bodies of @SideEffectFree, @Deterministic, and // @Pure methods is temporarily disabled unless -AcheckPurityAnnotations is // supplied on the command line. // Re-enable it after making the analysis more precise. // org.checkerframework.common.basetype.BaseTypeVisitor.visitMethod(MethodTree, Void) "checkPurityAnnotations", // TODO: Temporary option to make array subtyping invariant, // which will be the new default soon. "invariantArrays", // TODO: Temporary option to make casts stricter, in particular when // casting to an array or generic type. This will be the new default soon. "checkCastElementType", // Whether to use conservative defaults for bytecode and/or source code. // This option takes arguments "source" and/or "bytecode". // The default is "-source,-bytecode" (eventually this will be changed to "-source,bytecode"). // Note, in source code, conservative defaults are never // applied to code in the scope of an @AnnotatedFor. // See the "Compiling partially-annotated libraries" and // "Default qualifiers for \<.class> files (conservative library defaults)" // sections in the manual for more details // org.checkerframework.framework.source.SourceChecker.useConservativeDefault "useConservativeDefaultsForUncheckedCode", // Temporary, for backward compatibility "useDefaultsForUncheckedCode", // Whether to assume sound concurrent semantics or // simplified sequential semantics // org.checkerframework.framework.flow.CFAbstractTransfer.sequentialSemantics "concurrentSemantics", // Whether to use a conservative value for type arguments that could not be inferred. // See Issue 979. "conservativeUninferredTypeArguments", // Whether to ignore all subtype tests for type arguments that // were inferred for a raw type. Defaults to true. // org.checkerframework.framework.type.TypeHierarchy.isSubtypeTypeArguments "ignoreRawTypeArguments", /// /// Type-checking modes: enable/disable functionality /// // Lint options // org.checkerframework.framework.source.SourceChecker.getSupportedLintOptions() and similar "lint", // Whether to suggest methods that could be marked @SideEffectFree, // @Deterministic, or @Pure // org.checkerframework.common.basetype.BaseTypeVisitor.visitMethod(MethodTree, Void) "suggestPureMethods", // Whether to resolve reflective method invocations. // "-AresolveReflection=debug" causes debugging information // to be output. "resolveReflection", // Whether to use whole-program inference. Takes an argument to specify the output format: // "-Ainfer=stubs" or "-Ainfer=jaifs". "infer", // With each warning, in addition to the concrete error key, // output the SuppressWarnings strings that can be used to // suppress that warning. "showSuppressWarningsStrings", // Warn about @SuppressWarnings annotations that do not suppress any warnings. // org.checkerframework.common.basetype.BaseTypeChecker.warnUnneededSuppressions // org.checkerframework.framework.source.SourceChecker.warnUnneededSuppressions // org.checkerframework.framework.source.SourceChecker.shouldSuppressWarnings(javax.lang.model.element.Element, java.lang.String) // org.checkerframework.framework.source.SourceVisitor.checkForSuppressWarningsAnno "warnUnneededSuppressions", // Exceptions to -AwarnUnneededSuppressions. "warnUnneededSuppressionsExceptions", // Require that warning suppression annotations contain a checker key as a prefix in order for // the warning to be suppressed. // org.checkerframework.framework.source.SourceChecker.checkSuppressWarnings(java.lang.String[], // java.lang.String) "requirePrefixInWarningSuppressions", // Permit running under JDKs other than those the Checker Framework officially supports. "permitUnsupportedJdkVersion", // Ignore annotations in bytecode that have invalid annotation locations. // See https://github.com/typetools/checker-framework/issues/2173 // org.checkerframework.framework.type.ElementAnnotationApplier.apply "ignoreInvalidAnnotationLocations", /// /// Partially-annotated libraries /// // Additional stub files to use // org.checkerframework.framework.type.AnnotatedTypeFactory.parseStubFiles() "stubs", // Additional ajava files to use // org.checkerframework.framework.type.AnnotatedTypeFactory.parserAjavaFiles() "ajava", // Whether to print warnings about types/members in a stub file // that were not found on the class path // org.checkerframework.framework.stub.AnnotationFileParser.warnIfNotFound "stubWarnIfNotFound", // Whether to ignore missing classes even when warnIfNotFound is set to true and other classes // from the same package are present (useful if a package spans more than one jar). // org.checkerframework.framework.stub.AnnotationFileParser.warnIfNotFoundIgnoresClasses "stubWarnIfNotFoundIgnoresClasses", // Whether to print warnings about stub files that overwrite annotations from bytecode. "stubWarnIfOverwritesBytecode", // Whether to print warnings about stub files that are redundant with the annotations from // bytecode. "stubWarnIfRedundantWithBytecode", // Whether to issue a NOTE rather than a WARNING for -AstubWarn* command-line options "stubWarnNote", // With this option, annotations in stub files are used EVEN IF THE SOURCE FILE IS // PRESENT. Only use this option when you intend to store types in stub files rather than // directly in source code, such as during whole-program inference. The annotations in the // stub files will be glb'd with those in the source code before local inference begins. "mergeStubsWithSource", // Already listed above, but worth noting again in this section: // "useConservativeDefaultsForUncheckedCode" /// /// Debugging /// /// Amount of detail in messages // Print the version of the Checker Framework "version", // Print info about git repository from which the Checker Framework was compiled "printGitProperties", // Whether to print @InvisibleQualifier marked annotations // org.checkerframework.framework.type.AnnotatedTypeMirror.toString() "printAllQualifiers", // Whether to print [] around a set of type parameters in order to clearly see where they end // e.g. // without this option the E is printed: E extends F extends Object // with this option: E [ extends F [ extends Object super Void ] super Void ] // when multiple type variables are used this becomes useful very quickly "printVerboseGenerics", // Whether to NOT output a stack trace for each framework error. // org.checkerframework.framework.source.SourceChecker.logBugInCF "noPrintErrorStack", // Only output error code, useful for testing framework // org.checkerframework.framework.source.SourceChecker.message(Kind, Object, String, Object...) "nomsgtext", /// Format of messages // Output detailed message in simple-to-parse format, useful // for tools parsing Checker Framework output. // org.checkerframework.framework.source.SourceChecker.message(Kind, Object, String, Object...) "detailedmsgtext", /// Stub and JDK libraries // Ignore the standard jdk.astub file; primarily for testing or debugging. // org.checkerframework.framework.type.AnnotatedTypeFactory.parseStubFiles() "ignorejdkastub", // Whether to check that the annotated JDK is correctly provided // org.checkerframework.common.basetype.BaseTypeVisitor.checkForAnnotatedJdk() "permitMissingJdk", "nocheckjdk", // temporary, for backward compatibility // Parse all JDK files at startup rather than as needed. // org.checkerframework.framework.stub.AnnotationFileElementTypes.AnnotationFileElementTypes "parseAllJdk", // Whether to print debugging messages while processing the stub files // org.checkerframework.framework.stub.AnnotationFileParser.debugAnnotationFileParser "stubDebug", /// Progress tracing // Output file names before checking // org.checkerframework.framework.source.SourceChecker.typeProcess() "filenames", // Output all subtyping checks // org.checkerframework.common.basetype.BaseTypeVisitor "showchecks", // Output information about intermediate steps in method type argument inference // org.checkerframework.framework.util.typeinference.DefaultTypeArgumentInference "showInferenceSteps", // Output a stack trace when reporting errors or warnings // org.checkerframework.common.basetype.SourceChecker.printStackTrace() "dumpOnErrors", /// Visualizing the CFG // Implemented in the wrapper rather than this file, but worth noting here. // -AoutputArgsToFile // Mechanism to visualize the control flow graph (CFG). // The argument is a sequence of values or key-value pairs. // The first argument has to be the fully-qualified name of the // org.checkerframework.dataflow.cfg.CFGVisualizer implementation that should be used. The // remaining values or key-value pairs are passed to CFGVisualizer.init. // For example: // -Acfgviz=MyViz,a,b=c,d // instantiates class MyViz and calls CFGVisualizer.init // with {"a" -> true, "b" -> "c", "d" -> true}. "cfgviz", // Directory for .dot files generated from the CFG visualization in // org.checkerframework.dataflow.cfg.DOTCFGVisualizer // as initialized by // org.checkerframework.framework.type.GenericAnnotatedTypeFactory.createCFGVisualizer() // -Aflowdotdir=xyz // is short-hand for // -Acfgviz=org.checkerframework.dataflow.cfg.DOTCFGVisualizer,outdir=xyz "flowdotdir", // Enable additional output in the CFG visualization. // -Averbosecfg // is short-hand for // -Acfgviz=MyClass,verbose "verbosecfg", /// Caches // Set the cache size for caches in AnnotatedTypeFactory "atfCacheSize", // Sets AnnotatedTypeFactory shouldCache to false "atfDoNotCache", /// Miscellaneous debugging options // Whether to output resource statistics at JVM shutdown // org.checkerframework.framework.source.SourceChecker.shutdownHook() "resourceStats", // Parse all JDK files at startup rather than as needed. "parseAllJdk", // Run checks that test ajava files. // // Whenever processing a source file, parse it with JavaParser and check that the AST can be // matched with javac's tree. Crash if not. For testing the class JointJavacJavaParserVisitor. // // Also checks that annotations can be inserted. For each Java file, clears all annotations and // reinserts them, then checks if the original and modified ASTs are equivalent. "ajavaChecks", }) public abstract class SourceChecker extends AbstractTypeProcessor implements OptionConfiguration { // TODO A checker should export itself through a separate interface, and maybe have an interface // for all the methods for which it's safe to override. /** The line separator. */ private static final String LINE_SEPARATOR = System.lineSeparator().intern(); /** The message key that will suppress all warnings (it matches any message key). */ public static final String SUPPRESS_ALL_MESSAGE_KEY = "all"; /** The SuppressWarnings prefix that will suppress warnings for all checkers. */ public static final String SUPPRESS_ALL_PREFIX = "allcheckers"; /** The message key emitted when an unused warning suppression is found. */ public static final @CompilerMessageKey String UNNEEDED_SUPPRESSION_KEY = "unneeded.suppression"; /** File name of the localized messages. */ protected static final String MSGS_FILE = "messages.properties"; /** * Maps error keys to localized/custom error messages. Do not use directly; call {@link * #fullMessageOf} or {@link #processArg}. */ protected Properties messagesProperties; /** Used to report error messages and warnings via the compiler. */ protected Messager messager; /** Used as a helper for the {@link SourceVisitor}. */ protected Trees trees; /** The source tree that is being scanned. */ protected @InternedDistinct CompilationUnitTree currentRoot; /** The visitor to use. */ protected SourceVisitor visitor; /** * Exceptions to -AwarnUnneededSuppressions processing. No warning about unneeded suppressions is * issued if the SuppressWarnings string matches this pattern. */ private @Nullable Pattern warnUnneededSuppressionsExceptions; /** * SuppressWarnings strings supplied via the -AsuppressWarnings option. Do not use directly, call * {@link #getSuppressWarningsStringsFromOption()}. */ private String @Nullable [] suppressWarningsStringsFromOption; /** * If true, use the "allcheckers:" warning string prefix. * *

Checkers that never issue any error messages should set this to false. That prevents {@code * -AwarnUnneededSuppressions} from issuing warnings about * {@code @SuppressWarnings("allcheckers:...")}. */ protected boolean useAllcheckersPrefix = true; /** * Regular expression pattern to specify Java classes that are not annotated, so warnings about * uses of them should be suppressed. * *

It contains the pattern specified by the user, through the option {@code checkers.skipUses}; * otherwise it contains a pattern that can match no class. */ private Pattern skipUsesPattern; /** * Regular expression pattern to specify Java classes that are annotated, so warnings about them * should be issued but warnings about all other classes should be suppressed. * *

It contains the pattern specified by the user, through the option {@code checkers.onlyUses}; * otherwise it contains a pattern that matches every class. */ private Pattern onlyUsesPattern; /** * Regular expression pattern to specify Java classes whose definition should not be checked. * *

It contains the pattern specified by the user, through the option {@code checkers.skipDefs}; * otherwise it contains a pattern that can match no class. */ private Pattern skipDefsPattern; /** * Regular expression pattern to specify Java classes whose definition should be checked. * *

It contains the pattern specified by the user, through the option {@code checkers.onlyDefs}; * otherwise it contains a pattern that matches every class. */ private Pattern onlyDefsPattern; /** The supported lint options. */ private Set supportedLints; /** The enabled lint options. */ private Set activeLints; /** * The active options for this checker. This is a processed version of {@link * ProcessingEnvironment#getOptions()}: If the option is of the form "-ACheckerName_key=value" and * the current checker class, or one of its superclasses, is named "CheckerName", then add key * → value. If the option is of the form "-ACheckerName_key=value" and the current checker * class, and none of its superclasses, is named "CheckerName", then do not add key → value. * If the option is of the form "-Akey=value", then add key → value. * *

Both the simple and the canonical name of the checker can be used. Superclasses of the * current checker are also considered. */ private Map activeOptions; /** * The string that separates the checker name from the option name in a "-A" command-line * argument. This string may only consist of valid Java identifier part characters, because it * will be used within the key of an option. */ protected static final String OPTION_SEPARATOR = "_"; /** * The checker that called this one, whether that be a BaseTypeChecker (used as a compound * checker) or an AggregateChecker. Null if this is the checker that calls all others. Note that * in the case of a compound checker, the compound checker is the parent, not the checker that was * run prior to this one by the compound checker. */ protected @Nullable SourceChecker parentChecker; /** List of upstream checker names. Includes the current checker. */ protected List<@FullyQualifiedName String> upstreamCheckerNames; @Override public final synchronized void init(ProcessingEnvironment env) { ProcessingEnvironment unwrappedEnv = unwrapProcessingEnvironment(env); super.init(unwrappedEnv); // The processingEnvironment field will be set by the superclass's init method. // This is used to trigger AggregateChecker's setProcessingEnvironment. setProcessingEnvironment(unwrappedEnv); // Keep in sync with check in checker-framework/build.gradle and text in installation // section of manual. int jreVersion = SystemUtil.getJreVersion(); if (jreVersion != 8 && jreVersion != 11 && jreVersion != 17) { message( (hasOption("permitUnsupportedJdkVersion") ? Kind.NOTE : Kind.WARNING), "Use JDK 8, 11, or 17 to run the Checker Framework. You are using version %d.", jreVersion); } if (!hasOption("warnUnneededSuppressionsExceptions")) { warnUnneededSuppressionsExceptions = null; } else { String warnUnneededSuppressionsExceptionsString = getOption("warnUnneededSuppressionsExceptions"); if (warnUnneededSuppressionsExceptionsString == null) { throw new UserError("Must supply an argument to -AwarnUnneededSuppressionsExceptions"); } try { warnUnneededSuppressionsExceptions = Pattern.compile(warnUnneededSuppressionsExceptionsString); } catch (PatternSyntaxException e) { throw new UserError( "Argument to -AwarnUnneededSuppressionsExceptions is not a regular expression: " + e.getMessage()); } } if (hasOption("printGitProperties")) { printGitProperties(); } } /////////////////////////////////////////////////////////////////////////// /// Getters and setters /// /** * Returns the {@link ProcessingEnvironment} that was supplied to this checker. * * @return the {@link ProcessingEnvironment} that was supplied to this checker */ public ProcessingEnvironment getProcessingEnvironment() { return this.processingEnv; } /** Set the processing environment of the current checker. */ /* This method is protected only to allow the AggregateChecker and BaseTypeChecker to call it. */ protected void setProcessingEnvironment(ProcessingEnvironment env) { this.processingEnv = env; } /** Set the parent checker of the current checker. */ protected void setParentChecker(SourceChecker parentChecker) { this.parentChecker = parentChecker; } /** * Returns the immediate parent checker of the current checker. * * @return the immediate parent checker of the current checker, or null if there is none */ public @Nullable SourceChecker getParentChecker() { return this.parentChecker; } /** * Invoked when the current compilation unit root changes. * * @param newRoot the new compilation unit root */ @SuppressWarnings("interning:assignment") // used in == tests protected void setRoot(CompilationUnitTree newRoot) { this.currentRoot = newRoot; visitor.setRoot(currentRoot); } /** * Returns a list containing this checker name and all checkers it is a part of (that is, checkers * that called it). * * @return a list containing this checker name and all checkers it is a part of (that is, checkers * that called it) */ public List<@FullyQualifiedName String> getUpstreamCheckerNames() { if (upstreamCheckerNames == null) { upstreamCheckerNames = new ArrayList<>(); SourceChecker checker = this; while (checker != null) { upstreamCheckerNames.add(checker.getClass().getCanonicalName()); checker = checker.parentChecker; } } return upstreamCheckerNames; } /** * Returns the OptionConfiguration associated with this. * * @return the OptionConfiguration associated with this */ public OptionConfiguration getOptionConfiguration() { return this; } /** * Returns the element utilities associated with this. * * @return the element utilities associated with this */ public Elements getElementUtils() { return getProcessingEnvironment().getElementUtils(); } /** * Returns the type utilities associated with this. * * @return the type utilities associated with this */ public Types getTypeUtils() { return getProcessingEnvironment().getTypeUtils(); } /** * Returns the tree utilities associated with this. * * @return the tree utilities associated with this */ public Trees getTreeUtils() { return Trees.instance(getProcessingEnvironment()); } /** * Returns the SourceVisitor associated with this. * * @return the SourceVisitor associated with this */ public SourceVisitor getVisitor() { return this.visitor; } /** * Provides the {@link SourceVisitor} that the checker should use to scan input source trees. * * @return a {@link SourceVisitor} to use to scan source trees */ protected abstract SourceVisitor createSourceVisitor(); /** * Returns the AnnotationProvider (the type factory) associated with this. * * @return the AnnotationProvider (the type factory) associated with this */ public AnnotationProvider getAnnotationProvider() { throw new UnsupportedOperationException( "getAnnotationProvider is not implemented for this class."); } /** * 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 getMessagesProperties() { if (messagesProperties != null) { return messagesProperties; } messagesProperties = new Properties(); ArrayDeque> checkers = new ArrayDeque<>(); Class currClass = this.getClass(); while (currClass != AbstractTypeProcessor.class) { checkers.addFirst(currClass); currClass = currClass.getSuperclass(); } for (Class checker : checkers) { messagesProperties.putAll(getProperties(checker, MSGS_FILE, true)); } return messagesProperties; } /** * Return the given skip pattern if supplied by the user, or else a pattern that matches nothing. * * @param patternName "skipUses" or "skipDefs" * @param options the command-line options * @return the user-supplied regex for the given pattern, or a regex that matches nothing */ private Pattern getSkipPattern(String patternName, Map options) { // Default is an illegal Java identifier substring // so that it won't match anything. // Note that AnnotatedType's toString output format contains characters such as "():{}". return getPattern(patternName, options, "\\]'\"\\]"); } /** * Return the given only pattern if supplied by the user, or else a pattern that matches * everything. * * @param patternName "onlyUses" or "onlyDefs" * @param options the command-line options * @return the user-supplied regex for the given pattern, or a regex that matches everything */ private Pattern getOnlyPattern(String patternName, Map options) { // default matches everything return getPattern(patternName, options, "."); } private Pattern getPattern( String patternName, Map options, String defaultPattern) { String pattern = ""; if (options.containsKey(patternName)) { pattern = options.get(patternName); if (pattern == null) { message( Kind.WARNING, "The " + patternName + " property is empty; please fix your command line"); pattern = ""; } } else if (System.getProperty("checkers." + patternName) != null) { pattern = System.getProperty("checkers." + patternName); } else if (System.getenv(patternName) != null) { pattern = System.getenv(patternName); } if (pattern.indexOf("/") != -1) { message( Kind.WARNING, "The " + patternName + " property contains \"/\", which will never match a class name: " + pattern); } if (pattern.equals("")) { pattern = defaultPattern; } return Pattern.compile(pattern); } private Pattern getSkipUsesPattern(Map options) { return getSkipPattern("skipUses", options); } private Pattern getOnlyUsesPattern(Map options) { return getOnlyPattern("onlyUses", options); } private Pattern getSkipDefsPattern(Map options) { return getSkipPattern("skipDefs", options); } private Pattern getOnlyDefsPattern(Map options) { return getOnlyPattern("onlyDefs", options); } /////////////////////////////////////////////////////////////////////////// /// Type-checking /// /** * {@inheritDoc} * *

Type-checkers are not supposed to override this. Instead override initChecker. This allows * us to handle BugInCF only here and doesn't require all overriding implementations to be aware * of BugInCF. * * @see AbstractProcessor#init(ProcessingEnvironment) * @see SourceChecker#initChecker() */ @Override public void typeProcessingStart() { try { super.typeProcessingStart(); initChecker(); if (this.messager == null) { messager = processingEnv.getMessager(); messager.printMessage( Kind.WARNING, "You have forgotten to call super.initChecker in your " + "subclass of SourceChecker, " + this.getClass() + "! Please ensure your checker is properly initialized."); } if (shouldAddShutdownHook()) { Runtime.getRuntime() .addShutdownHook( new Thread() { @Override public void run() { shutdownHook(); } }); } if (hasOption("version")) { messager.printMessage(Kind.NOTE, "Checker Framework " + getCheckerVersion()); } } catch (UserError ce) { logUserError(ce); } catch (TypeSystemError ce) { logTypeSystemError(ce); } catch (BugInCF ce) { logBugInCF(ce); } catch (Throwable t) { logBugInCF(wrapThrowableAsBugInCF("SourceChecker.typeProcessingStart", t, null)); } } /** * Initialize the checker. * * @see AbstractProcessor#init(ProcessingEnvironment) */ public void initChecker() { // 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; this.trees = trees; this.messager = processingEnv.getMessager(); this.messagesProperties = getMessagesProperties(); this.visitor = createSourceVisitor(); // Validate the lint flags, if they haven't been used already. if (this.activeLints == null) { this.activeLints = createActiveLints(getOptions()); } } /** Output the warning about source level at most once. */ private boolean warnedAboutSourceLevel = false; /** * If true, javac failed to compile the code or a previously-run annotation processor issued an * error. */ protected boolean javacErrored = false; /** Output the warning about memory at most once. */ private boolean warnedAboutGarbageCollection = false; /** * The number of errors at the last exit of the type processor. At entry to the type processor we * check whether the current error count is higher and then don't process the file, as it contains * some Java errors. Needs to be protected to allow access from AggregateChecker and * BaseTypeChecker. */ protected int errsOnLastExit = 0; /** * Report "type.checking.not.run" error. * * @param p error is reported at the leaf of the path */ @SuppressWarnings("interning:assignment") // used in == tests protected void reportJavacError(TreePath p) { // If javac issued any errors, do not type check any file, so that the Checker Framework // does not have to deal with error types. currentRoot = p.getCompilationUnit(); reportError(p.getLeaf(), "type.checking.not.run", getClass().getSimpleName()); } /** * Type-check the code using this checker's visitor. * * @see Processor#process(Set, RoundEnvironment) */ @Override public void typeProcess(TypeElement e, TreePath p) { if (javacErrored) { reportJavacError(p); return; } // Cannot use BugInCF here because it is outside of the try/catch for BugInCF. if (e == null) { messager.printMessage(Kind.ERROR, "Refusing to process empty TypeElement"); return; } if (p == null) { messager.printMessage(Kind.ERROR, "Refusing to process empty TreePath in TypeElement: " + e); return; } if (!warnedAboutGarbageCollection) { String gcUsageMessage = SystemPlume.gcUsageMessage(.25, 60); if (gcUsageMessage != null) { messager.printMessage(Kind.WARNING, gcUsageMessage); warnedAboutGarbageCollection = true; } } Context context = ((JavacProcessingEnvironment) processingEnv).getContext(); Source source = Source.instance(context); // Don't use source.allowTypeAnnotations() because that API changed after 9. // Also the enum constant Source.JDK1_8 was renamed at some point... if (!warnedAboutSourceLevel && source.compareTo(Source.lookup("8")) < 0) { messager.printMessage( Kind.WARNING, "-source " + source.name + " does not support type annotations"); warnedAboutSourceLevel = true; } Log log = Log.instance(context); if (log.nerrors > this.errsOnLastExit) { this.errsOnLastExit = log.nerrors; javacErrored = true; reportJavacError(p); return; } if (visitor == null) { // typeProcessingStart invokes initChecker, which should have set the visitor. If the field is // still null, an exception occurred during initialization, which was already logged // there. Don't also cause a NPE here. return; } if (p.getCompilationUnit() != currentRoot) { setRoot(p.getCompilationUnit()); if (hasOption("filenames")) { // TODO: Have a command-line option to turn the timestamps on/off too, because // they are nondeterministic across runs. // Add timestamp to indicate how long operations are taking. // Duplicate messages are suppressed, so this might not appear in front of every " // is type-checking " message (when a file takes less than a second to type-check). message(Kind.NOTE, Instant.now().toString()); message( Kind.NOTE, "%s is type-checking %s", (Object) this.getClass().getSimpleName(), currentRoot.getSourceFile().getName()); } } // Visit the attributed tree. try { visitor.visit(p); warnUnneededSuppressions(); } catch (UserError ce) { logUserError(ce); } catch (TypeSystemError ce) { logTypeSystemError(ce); } catch (BugInCF ce) { logBugInCF(ce); } catch (Throwable t) { logBugInCF(wrapThrowableAsBugInCF("SourceChecker.typeProcess", t, p)); } finally { // Also add possibly deferred diagnostics, which will get published back in // AbstractTypeProcessor. this.errsOnLastExit = log.nerrors; } } /////////////////////////////////////////////////////////////////////////// /// Reporting type-checking errors; most clients use reportError() or reportWarning() /// /** * Reports an error. By default, prints it to the screen via the compiler's internal messager. * * @param source the source position information; may be an Element, a Tree, or null * @param messageKey the message key * @param args arguments for interpolation in the string corresponding to the given message key */ public void reportError(Object source, @CompilerMessageKey String messageKey, Object... args) { report(source, Kind.ERROR, messageKey, args); } /** * Reports a warning. By default, prints it to the screen via the compiler's internal messager. * * @param source the source position information; may be an Element, a Tree, or null * @param messageKey the message key * @param args arguments for interpolation in the string corresponding to the given message key */ public void reportWarning(Object source, @CompilerMessageKey String messageKey, Object... args) { report(source, Kind.MANDATORY_WARNING, messageKey, args); } /** * Reports a diagnostic message. By default, prints it to the screen via the compiler's internal * messager. * *

Most clients should use {@link #reportError} or {@link #reportWarning}. * * @param source the source position information; may be an Element, a Tree, or null * @param d the diagnostic message */ public void report(Object source, DiagMessage d) { report(source, d.getKind(), d.getMessageKey(), d.getArgs()); } /** * Reports a diagnostic message. By default, it prints it to the screen via the compiler's * internal messager; however, it might also store it for later output. * * @param source the source position information; may be an Element, a Tree, or null * @param kind the type of message * @param messageKey the message key * @param args arguments for interpolation in the string corresponding to the given message key */ // Not a format method. However, messageKey should be either a format string for `args`, or a // property key that maps to a format string for `args`. // @FormatMethod @SuppressWarnings("formatter:format.string") // arg is a format string or a property key private void report( Object source, javax.tools.Diagnostic.Kind kind, @CompilerMessageKey String messageKey, Object... args) { assert messagesProperties != null : "null messagesProperties"; if (shouldSuppressWarnings(source, messageKey)) { return; } if (args != null) { for (int i = 0; i < args.length; ++i) { args[i] = processArg(args[i]); } } if (kind == Kind.NOTE) { System.err.println("(NOTE) " + String.format(messageKey, args)); return; } final String defaultFormat = "(" + messageKey + ")"; String fmtString; if (this.processingEnv.getOptions() != null /*nnbug*/ && this.processingEnv.getOptions().containsKey("nomsgtext")) { fmtString = defaultFormat; } else if (this.processingEnv.getOptions() != null /*nnbug*/ && this.processingEnv.getOptions().containsKey("detailedmsgtext")) { // The -Adetailedmsgtext command-line option was given, so output // a stylized error message for easy parsing by a tool. fmtString = detailedMsgTextPrefix(source, defaultFormat, args) + fullMessageOf(messageKey, defaultFormat); } else { fmtString = "[" + suppressWarningsString(messageKey) + "] " + fullMessageOf(messageKey, defaultFormat); } String messageText; try { messageText = String.format(fmtString, args); } catch (Exception e) { throw new BugInCF( "Invalid format string: \"" + fmtString + "\" args: " + Arrays.toString(args), e); } if (kind == Kind.ERROR && hasOption("warns")) { kind = Kind.MANDATORY_WARNING; } if (source instanceof Element) { messager.printMessage(kind, messageText, (Element) source); } else if (source instanceof Tree) { printOrStoreMessage(kind, messageText, (Tree) source, currentRoot); } else { throw new BugInCF("invalid position source, class=" + source.getClass()); } } /** * Print a non-localized message using the javac messager. This is preferable to using System.out * or System.err, but should only be used for exceptional cases that don't happen in correct * usage. Localized messages should be raised using {@link #reportError}, {@link #reportWarning}, * etc. * * @param kind the kind of message to print * @param msg the message text * @param args optional arguments to substitute in the message * @see SourceChecker#report(Object, DiagMessage) */ @FormatMethod public void message(javax.tools.Diagnostic.Kind kind, String msg, Object... args) { message(kind, String.format(msg, args)); } /** * Print a non-localized message using the javac messager. This is preferable to using System.out * or System.err, but should only be used for exceptional cases that don't happen in correct * usage. Localized messages should be raised using {@link #reportError}, {@link #reportWarning}, * etc. * * @param kind the kind of message to print * @param msg the message text * @see SourceChecker#report(Object, DiagMessage) */ public void message(javax.tools.Diagnostic.Kind kind, String msg) { if (messager == null) { // If this method is called before initChecker() sets the field messager = processingEnv.getMessager(); } messager.printMessage(kind, msg); } /** * Print the given message. * * @param msg the message to print x */ private void printMessage(String msg) { if (messager == null) { // If this method is called before initChecker() sets the field messager = processingEnv.getMessager(); } messager.printMessage(Kind.ERROR, msg); } /** * Do not call this method. Call {@link #reportError} or {@link #reportWarning} instead. * *

This method exists so that the BaseTypeChecker can override it. For compound checkers, it * stores all messages and sorts them by location before outputting them. * * @param kind the kind of message to print * @param message the message text * @param source the source code position of the diagnostic message * @param root the compilation unit */ protected void printOrStoreMessage( javax.tools.Diagnostic.Kind kind, String message, Tree source, CompilationUnitTree root) { StackTraceElement[] trace = Thread.currentThread().getStackTrace(); printOrStoreMessage(kind, message, source, root, trace); } /** * Do not call this method. Call {@link #reportError} or {@link #reportWarning} instead. * *

This method exists so that the BaseTypeChecker can override it. For compound checkers, it * stores all messages and sorts them by location before outputting them. * * @param kind the kind of message to print * @param message the message text * @param source the source code position of the diagnostic message * @param root the compilation unit * @param trace the stack trace where the checker encountered an error. It is printed when the * dumpOnErrors option is enabled. */ protected void printOrStoreMessage( javax.tools.Diagnostic.Kind kind, String message, Tree source, CompilationUnitTree root, StackTraceElement[] trace) { Trees.instance(processingEnv).printMessage(kind, message, source, root); printStackTrace(trace); } /** * Output the given stack trace if the "dumpOnErrors" option is enabled. * * @param trace stack trace when the checker encountered a warning/error */ private void printStackTrace(StackTraceElement[] trace) { if (hasOption("dumpOnErrors")) { StringBuilder msg = new StringBuilder(); for (StackTraceElement elem : trace) { msg.append("\tat " + elem + "\n"); } message(Diagnostic.Kind.NOTE, msg.toString()); } } /////////////////////////////////////////////////////////////////////////// /// Diagnostic message formatting /// /** * Returns the localized long message corresponding to this key. If not found, tries suffixes of * this key, stripping off dot-separated prefixes. If still not found, returns {@code * defaultValue}. * * @param messageKey a message key * @param defaultValue a default value to use if {@code messageKey} is not a message key * @return the localized long message corresponding to this key or a suffix, or {@code * defaultValue} */ protected String fullMessageOf(String messageKey, String defaultValue) { String key = messageKey; do { if (messagesProperties.containsKey(key)) { return messagesProperties.getProperty(key); } int dot = key.indexOf('.'); if (dot < 0) { return defaultValue; } key = key.substring(dot + 1); } while (true); } /** * Process an argument to an error message before it is passed to String.format. * *

This implementation expands the argument if it is exactly a message key. * *

By contrast, {@link #fullMessageOf} processes the message key itself but not the arguments, * and tries suffixes. * * @param arg the argument * @return the result after processing */ protected Object processArg(Object arg) { // Check to see if the argument itself is a property to be expanded if (arg instanceof String) { return messagesProperties.getProperty((String) arg, (String) arg); } else { return arg; } } /** Separates parts of a "detailed message", to permit easier parsing. */ public static final String DETAILS_SEPARATOR = " $$ "; /** * Returns all but the message key part of the message format output by -Adetailedmsgtext. * * @param source the object from which to obtain source position information; may be an Element, a * Tree, or null * @param defaultFormat the message key, in parentheses * @param args arguments for interpolation in the string corresponding to the given message key * @return the first part of the message format output by -Adetailedmsgtext */ private String detailedMsgTextPrefix(Object source, String defaultFormat, Object[] args) { StringJoiner sj = new StringJoiner(DETAILS_SEPARATOR); // The parts, separated by " $$ " (DETAILS_SEPARATOR), are: // (1) error key sj.add(defaultFormat); // (2) number of additional tokens, and those tokens; this depends on the error message, and an // example is the found and expected types if (args != null) { sj.add(Integer.toString(args.length)); for (Object arg : args) { sj.add(Objects.toString(arg)); } } else { // Output 0 for null arguments. sj.add(Integer.toString(0)); } // (3) The error position, as starting and ending characters in the source file. sj.add(detailedMsgTextPositionString(sourceToTree(source), currentRoot)); // (4) The human-readable error message will be added by the caller. sj.add(""); // Add DETAILS_SEPARATOR at the end. return sj.toString(); } /** * Returns the most specific warning suppression string for the warning/error being printed. This * is {@code msg} prefixed by a checker name (or "allcheckers") and a colon. * * @param messageKey the simple, checker-specific error message key * @return the most specific SuppressWarnings string for the warning/error being printed */ private String suppressWarningsString(String messageKey) { Collection prefixes = this.getSuppressWarningsPrefixes(); prefixes.remove(SUPPRESS_ALL_PREFIX); if (hasOption("showSuppressWarningsStrings")) { List list = new ArrayList<>(prefixes); // Make sure "allcheckers" is at the end of the list. if (useAllcheckersPrefix) { list.add(SUPPRESS_ALL_PREFIX); } return list + ":" + messageKey; } else if (hasOption("requirePrefixInWarningSuppressions")) { // If the warning key must be prefixed with a prefix (a checker name), then add that to // the SuppressWarnings string that is printed. String defaultPrefix = getDefaultSuppressWarningsPrefix(); if (prefixes.contains(defaultPrefix)) { return defaultPrefix + ":" + messageKey; } else { String firstKey = prefixes.iterator().next(); return firstKey + ":" + messageKey; } } else { return messageKey; } } /** * Convert a Tree, Element, or null, into a Tree or null. * * @param source the object from which to obtain source position information; may be an Element, a * Tree, or null * @return the tree associated with the given source object, or null if none */ private @Nullable Tree sourceToTree(@Nullable Object source) { if (source instanceof Element) { return trees.getTree((Element) source); } else if (source instanceof Tree) { return (Tree) source; } else if (source == null) { return null; } else { throw new BugInCF("Unexpected source %s [%s]", source, source.getClass()); } } /** * For the given tree, compute the source positions for that tree. Return a "tuple"-like string * (e.g. "( 1, 200 )" ) that contains the start and end position of the tree in the current * compilation unit. Used only by the -Adetailedmsgtext output format. * * @param tree tree to locate within the current compilation unit * @param currentRoot the current compilation unit * @return a tuple string representing the range of characters that tree occupies in the source * file, or the empty string if {@code tree} is null */ private String detailedMsgTextPositionString(Tree tree, CompilationUnitTree currentRoot) { if (tree == null) { return ""; } SourcePositions sourcePositions = trees.getSourcePositions(); long start = sourcePositions.getStartPosition(currentRoot, tree); long end = sourcePositions.getEndPosition(currentRoot, tree); return "( " + start + ", " + end + " )"; } /////////////////////////////////////////////////////////////////////////// /// Lint options ("-Alint:xxxx" and "-Alint:-xxxx") /// /** * Determine which lint options are artive. * * @param options the command-line options * @return the active lint options */ private Set createActiveLints(Map options) { if (!options.containsKey("lint")) { return Collections.emptySet(); } String lintString = options.get("lint"); if (lintString == null) { return Collections.singleton("all"); } Set activeLint = new HashSet<>(); for (String s : lintString.split(",")) { if (!this.getSupportedLintOptions().contains(s) && !(s.charAt(0) == '-' && this.getSupportedLintOptions().contains(s.substring(1))) && !s.equals("all") && !s.equals("none")) { this.messager.printMessage( Kind.WARNING, "Unsupported lint option: " + s + "; All options: " + this.getSupportedLintOptions()); } activeLint.add(s); if (s.equals("none")) { activeLint.add("-all"); } } return Collections.unmodifiableSet(activeLint); } /** * Determines the value of the lint option with the given name. Just as javac 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 javac 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) * @see SourceChecker#getOption(String) */ public final boolean getLintOption(String name, boolean def) { if (!this.getSupportedLintOptions().contains(name)) { throw new UserError("Illegal lint option: " + name); } if (activeLints == null) { activeLints = createActiveLints(getOptions()); } 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; } /** * Set the value of the lint option with the given name. Just as javac 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". This method can be used by * subclasses to enforce having certain lint options enabled/disabled. * * @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 set * @param val the option value * @see SourceChecker#getLintOption(String) * @see SourceChecker#getLintOption(String,boolean) */ protected final void setLintOption(String name, boolean val) { if (!this.getSupportedLintOptions().contains(name)) { throw new UserError("Illegal lint option: " + name); } /* TODO: warn if the option is also provided on the command line(?) boolean exists = false; if (!activeLints.isEmpty()) { String tofind = name; while (tofind != null) { if (activeLints.contains(tofind) || // direct activeLints.contains(String.format("-%s", tofind)) || // negation activeLints.contains(tofind.substring(1))) { // name was negation exists = true; } tofind = parentOfOption(tofind); } } if (exists) { // TODO: Issue warning? } TODO: assert that name doesn't start with '-' */ Set newlints = new HashSet<>(); newlints.addAll(activeLints); if (val) { newlints.add(name); } else { newlints.add(String.format("-%s", name)); } activeLints = Collections.unmodifiableSet(newlints); } /** * Helper method to find the parent of a lint key. The lint hierarchy level is denoted by a colon * ':'. 'all' is the root for all hierarchy. * *

   * Example
   *    cast:unsafe → cast
   *    cast        → all
   *    all         → {@code null}
   * 
* * @param name the lint key whose parest to find * @return the parent of the lint key */ private String parentOfOption(String name) { if (name.equals("all")) { return null; } int colonIndex = name.lastIndexOf(':'); if (colonIndex != -1) { return name.substring(0, colonIndex); } 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 getSupportedLintOptions() { if (supportedLints == null) { supportedLints = createSupportedLintOptions(); } return supportedLints; } /** Compute the set of supported lint options. */ protected Set createSupportedLintOptions() { @Nullable SupportedLintOptions sl = this.getClass().getAnnotation(SupportedLintOptions.class); if (sl == null) { return Collections.emptySet(); } @Nullable String @Nullable [] slValue = sl.value(); assert slValue != null; @Nullable String[] lintArray = slValue; Set lintSet = new HashSet<>(lintArray.length); for (String s : lintArray) { lintSet.add(s); } return Collections.unmodifiableSet(lintSet); } /** * Set the supported lint options. Use of this method should be limited to the AggregateChecker, * who needs to set the lint options to the union of all subcheckers. Also, e.g. the * NullnessSubchecker need to use this method, as one is created by the other. * * @param newLints the new supported lint options, which replace any existing ones */ protected void setSupportedLintOptions(Set newLints) { supportedLints = newLints; } /////////////////////////////////////////////////////////////////////////// /// Regular (non-lint) options ("-Axxxx") /// /** * Determine which options are active. * * @param options all provided options * @return a value for {@link #activeOptions} */ private Map createActiveOptions(Map options) { if (options.isEmpty()) { return Collections.emptyMap(); } Map activeOpts = new HashMap<>(CollectionsPlume.mapCapacity(options)); for (Map.Entry opt : options.entrySet()) { String key = opt.getKey(); String value = opt.getValue(); String[] split = key.split(OPTION_SEPARATOR); splitlengthswitch: switch (split.length) { case 1: // No separator, option always active. activeOpts.put(key, value); break; case 2: Class clazz = this.getClass(); do { if (clazz.getCanonicalName().equals(split[0]) || clazz.getSimpleName().equals(split[0])) { // Valid class-option pair. activeOpts.put(split[1], value); break splitlengthswitch; } clazz = clazz.getSuperclass(); } while (clazz != null && !clazz.getName().equals(AbstractTypeProcessor.class.getCanonicalName())); // Didn't find a matching class. Option might be for another processor. Add // option anyways. javac will warn if no processor supports the option. activeOpts.put(key, value); break; default: // Too many separators. Option might be for another processor. Add option // anyways. javac will warn if no processor supports the option. activeOpts.put(key, value); } } return Collections.unmodifiableMap(activeOpts); } /** * Add additional active options. Use of this method should be limited to the AggregateChecker, * who needs to set the active options to the union of all subcheckers. * * @param moreOpts the active options to add */ protected void addOptions(Map moreOpts) { Map activeOpts = new HashMap<>(getOptions()); activeOpts.putAll(moreOpts); activeOptions = Collections.unmodifiableMap(activeOpts); } /** * Check whether the given option is provided. * *

Note that {@link #getOption(String)} can still return null even if {@code hasOption} returns * true: this happens e.g. for {@code -Amyopt} * * @param name the name of the option to check * @return true if the option name was provided, false otherwise */ @Override public final boolean hasOption(String name) { return getOptions().containsKey(name); } /** * Determines the value of the option with the given name. * * @param name the name of the option to check * @see SourceChecker#getLintOption(String,boolean) */ @Override public final String getOption(String name) { return getOption(name, null); } /** * Determines the boolean value of the option with the given name. Returns false if the option is * not set. * * @param name the name of the option to check * @see SourceChecker#getLintOption(String,boolean) */ @Override public final boolean getBooleanOption(String name) { return getBooleanOption(name, false); } /** * Determines the boolean value of the option with the given name. Returns the given default value * if the option is not set. * * @param name the name of the option to check * @param defaultValue the default value to use if the option is not set * @see SourceChecker#getLintOption(String,boolean) */ @Override public final boolean getBooleanOption(String name, boolean defaultValue) { String value = getOption(name); if (value == null) { return defaultValue; } if (value.equals("true")) { return true; } if (value.equals("false")) { return false; } throw new UserError( String.format("Value of %s option should be a boolean, but is \"%s\".", name, value)); } /** * Return all active options for this checker. * * @return all active options for this checker */ @Override public Map getOptions() { if (activeOptions == null) { activeOptions = createActiveOptions(processingEnv.getOptions()); } return activeOptions; } /** * Determines the value of the lint option with the given name and returns the default value if * nothing is specified. * * @param name the name of the option to check * @param defaultValue the default value to use if the option is not set * @see SourceChecker#getOption(String) * @see SourceChecker#getLintOption(String) */ @Override public final String getOption(String name, String defaultValue) { if (!this.getSupportedOptions().contains(name)) { throw new UserError("Illegal option: " + name); } if (activeOptions == null) { activeOptions = createActiveOptions(processingEnv.getOptions()); } if (activeOptions.isEmpty()) { return defaultValue; } if (activeOptions.containsKey(name)) { return activeOptions.get(name); } else { return defaultValue; } } /** * Map the Checker Framework version of {@link SupportedOptions} to the standard annotation * provided version {@link javax.annotation.processing.SupportedOptions}. */ @Override public Set getSupportedOptions() { Set options = new HashSet<>(); // Support all options provided with the standard {@link // javax.annotation.processing.SupportedOptions} annotation. options.addAll(super.getSupportedOptions()); // For the Checker Framework annotation // {@link org.checkerframework.framework.source.SupportedOptions} // we additionally add Class clazz = this.getClass(); List> clazzPrefixes = new ArrayList<>(); do { clazzPrefixes.add(clazz); SupportedOptions so = clazz.getAnnotation(SupportedOptions.class); if (so != null) { options.addAll(expandCFOptions(clazzPrefixes, so.value())); } clazz = clazz.getSuperclass(); } while (clazz != null && !clazz.getName().equals(AbstractTypeProcessor.class.getCanonicalName())); return Collections.unmodifiableSet(options); } /** * Generate the possible command-line option names by prefixing each class name from {@code * classPrefixes} to {@code options}, separated by {@link #OPTION_SEPARATOR}. * * @param clazzPrefixes the classes to prefix * @param options the option names * @return the possible combinations that should be supported */ protected Collection expandCFOptions( List> clazzPrefixes, String[] options) { Set res = new HashSet<>(); for (String option : options) { res.add(option); for (Class clazz : clazzPrefixes) { res.add(clazz.getCanonicalName() + OPTION_SEPARATOR + option); res.add(clazz.getSimpleName() + OPTION_SEPARATOR + option); } } return res; } /** * Overrides the default implementation to always return a singleton set containing only "*". * *

javac uses this list to determine which classes process; javac only runs an annotation * processor on classes that contain at least one of the mentioned annotations. Thus, 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. * *

To specify the annotations that a checker recognizes as type qualifiers, see {@link * AnnotatedTypeFactory#createSupportedTypeQualifiers()}. * * @throws Error if a subclass is annotated with {@link SupportedAnnotationTypes} */ @Override public final Set getSupportedAnnotationTypes() { SupportedAnnotationTypes supported = this.getClass().getAnnotation(SupportedAnnotationTypes.class); if (supported != null) { throw new BugInCF( "@SupportedAnnotationTypes should not be written on any checker;" + " supported annotation types are inherited from SourceChecker."); } return Collections.singleton("*"); } /////////////////////////////////////////////////////////////////////////// /// Warning suppression and unneeded warnings /// /** * Returns the argument to -AsuppressWarnings, split on commas, or null if no such argument. Only * ever called once; the value is cached in field {@link #suppressWarningsStringsFromOption}. * * @return the argument to -AsuppressWarnings, split on commas, or null if no such argument */ private String @Nullable [] getSuppressWarningsStringsFromOption() { Map options = getOptions(); if (this.suppressWarningsStringsFromOption == null) { if (!options.containsKey("suppressWarnings")) { return null; } String swStrings = options.get("suppressWarnings"); if (swStrings == null) { return null; } this.suppressWarningsStringsFromOption = swStrings.split(","); } return this.suppressWarningsStringsFromOption; } /** * Issues a warning about any {@code @SuppressWarnings} that didn't suppress a warning, but starts * with this checker name or "allcheckers". */ protected void warnUnneededSuppressions() { if (!hasOption("warnUnneededSuppressions")) { return; } Set elementsSuppress = new HashSet<>(this.elementsWithSuppressedWarnings); this.elementsWithSuppressedWarnings.clear(); Set prefixes = new HashSet<>(getSuppressWarningsPrefixes()); Set errorKeys = new HashSet<>(messagesProperties.stringPropertyNames()); warnUnneededSuppressions(elementsSuppress, prefixes, errorKeys); getVisitor().treesWithSuppressWarnings.clear(); } /** * Issues a warning about any {@code @SuppressWarnings} string that didn't suppress a warning, but * starts with one of the given prefixes (checker names). * * @param elementsSuppress elements with a {@code @SuppressWarnings} that actually suppressed a * warning * @param prefixes the SuppressWarnings prefixes that suppress all warnings from this checker * @param allErrorKeys all error keys that can be issued by this checker */ protected void warnUnneededSuppressions( Set elementsSuppress, Set prefixes, Set allErrorKeys) { for (Tree tree : getVisitor().treesWithSuppressWarnings) { Element elt = TreeUtils.elementFromTree(tree); // TODO: This test is too coarse. The fact that this @SuppressWarnings suppressed // *some* warning doesn't mean that every value in it did so. if (elementsSuppress.contains(elt)) { continue; } // tree has a @SuppressWarnings annotation that didn't suppress any warnings. SuppressWarnings suppressAnno = elt.getAnnotation(SuppressWarnings.class); String[] suppressWarningsStrings = suppressAnno.value(); for (String suppressWarningsString : suppressWarningsStrings) { if (warnUnneededSuppressionsExceptions != null && warnUnneededSuppressionsExceptions.matcher(suppressWarningsString).find(0)) { continue; } for (String prefix : prefixes) { if (suppressWarningsString.equals(prefix) || suppressWarningsString.startsWith(prefix + ":")) { reportUnneededSuppression(tree, suppressWarningsString); break; // Don't report the same warning string more than once. } } } } } /** * Issues a warning that the string in a {@code @SuppressWarnings} on {@code tree} isn't needed. * * @param tree has unneeded {@code @SuppressWarnings} * @param suppressWarningsString the SuppressWarnings string that isn't needed */ private void reportUnneededSuppression(Tree tree, String suppressWarningsString) { Tree swTree = findSuppressWarningsTree(tree); report( swTree, Kind.MANDATORY_WARNING, SourceChecker.UNNEEDED_SUPPRESSION_KEY, "\"" + suppressWarningsString + "\"", getClass().getSimpleName()); } /** The name of the @SuppressWarnings annotation. */ private final @CanonicalName String suppressWarningsClassName = SuppressWarnings.class.getCanonicalName(); /** * Finds the tree that is a {@code @SuppressWarnings} annotation. * * @param tree a class, method, or variable tree annotated with {@code @SuppressWarnings} * @return tree for {@code @SuppressWarnings} or {@code default} if one isn't found */ private Tree findSuppressWarningsTree(Tree tree) { List annotations; if (TreeUtils.isClassTree(tree)) { annotations = ((ClassTree) tree).getModifiers().getAnnotations(); } else if (tree.getKind() == Tree.Kind.METHOD) { annotations = ((MethodTree) tree).getModifiers().getAnnotations(); } else { annotations = ((VariableTree) tree).getModifiers().getAnnotations(); } for (AnnotationTree annotationTree : annotations) { if (AnnotationUtils.areSameByName( TreeUtils.annotationFromAnnotationTree(annotationTree), suppressWarningsClassName)) { return annotationTree; } } throw new BugInCF("Did not find @SuppressWarnings: " + tree); } /** * Returns true if all the warnings pertaining to the given source should be suppressed. This * implementation just that delegates to an overloaded, more specific version of {@code * shouldSuppressWarnings()}. * * @param src the position object to test; may be an Element, a Tree, or null * @param errKey the error key the checker is emitting * @return true if all warnings pertaining to the given source should be suppressed * @see #shouldSuppressWarnings(Element, String) * @see #shouldSuppressWarnings(Tree, String) */ private boolean shouldSuppressWarnings(@Nullable Object src, String errKey) { if (src instanceof Element) { return shouldSuppressWarnings((Element) src, errKey); } else if (src instanceof Tree) { return shouldSuppressWarnings((Tree) src, errKey); } else if (src == null) { return false; } else { throw new BugInCF("Unexpected source " + src); } } /** * Determines whether all the warnings pertaining to a given tree should be suppressed. Returns * true if the tree is within the scope of a @SuppressWarnings annotation, one of whose values * suppresses the checker's warnings. Also, returns true if the {@code errKey} matches a string in * {@code -AsuppressWarnings}. * * @param tree the tree that might be a source of a warning * @param errKey the error key the checker is emitting * @return true if no warning should be emitted for the given tree because it is contained by a * declaration with an appropriately-valued {@literal @}SuppressWarnings annotation; false * otherwise */ public boolean shouldSuppressWarnings(Tree tree, String errKey) { Collection prefixes = getSuppressWarningsPrefixes(); if (prefixes.isEmpty() || (prefixes.contains(SUPPRESS_ALL_PREFIX) && prefixes.size() == 1)) { throw new BugInCF( "Checker must provide a SuppressWarnings prefix." + " SourceChecker#getSuppressWarningsPrefixes was not overridden correctly."); } if (shouldSuppress(getSuppressWarningsStringsFromOption(), errKey)) { // If the error key matches a warning string in the -AsuppressWarnings, then suppress // the warning. return true; } // trees.getPath might be slow, but this is only used in error reporting @Nullable TreePath path = trees.getPath(this.currentRoot, tree); return shouldSuppressWarnings(path, errKey); } /** * Determines whether all the warnings pertaining to a given tree path should be suppressed. * Returns true if the path is within the scope of a @SuppressWarnings annotation, one of whose * values suppresses all the checker's warnings. * * @param path the TreePath that might be a source of, or related to, a warning * @param errKey the error key the checker is emitting * @return true if no warning should be emitted for the given path because it is contained by a * declaration with an appropriately-valued {@code @SuppressWarnings} annotation; false * otherwise */ public boolean shouldSuppressWarnings(@Nullable TreePath path, String errKey) { if (path == null) { return false; } @Nullable VariableTree var = TreePathUtil.enclosingVariable(path); if (var != null && shouldSuppressWarnings(TreeUtils.elementFromTree(var), errKey)) { return true; } @Nullable MethodTree method = TreePathUtil.enclosingMethod(path); if (method != null) { @Nullable Element elt = TreeUtils.elementFromTree(method); if (shouldSuppressWarnings(elt, errKey)) { return true; } if (isAnnotatedForThisCheckerOrUpstreamChecker(elt)) { // Return false immediately. Do NOT check for AnnotatedFor in the enclosing elements, // because they may not have an @AnnotatedFor. return false; } } @Nullable ClassTree cls = TreePathUtil.enclosingClass(path); if (cls != null) { @Nullable Element elt = TreeUtils.elementFromTree(cls); if (shouldSuppressWarnings(elt, errKey)) { return true; } if (isAnnotatedForThisCheckerOrUpstreamChecker(elt)) { // Return false immediately. Do NOT check for AnnotatedFor in the enclosing elements, // because they may not have an @AnnotatedFor. return false; } } if (useConservativeDefault("source")) { // If we got this far without hitting an @AnnotatedFor and returning // false, we DO suppress the warning. return true; } return false; } /** * Should conservative defaults be used for the kind of unchecked code indicated by the parameter? * * @param kindOfCode source or bytecode * @return whether conservative defaults should be used */ public boolean useConservativeDefault(String kindOfCode) { final boolean useUncheckedDefaultsForSource = false; final boolean useUncheckedDefaultsForByteCode = false; String option = this.getOption("useConservativeDefaultsForUncheckedCode"); // Temporary, for backward compatibility. if (option == null) { this.getOption("useDefaultsForUncheckedCode"); } String[] args = option != null ? option.split(",") : new String[0]; for (String arg : args) { boolean value = arg.indexOf("-") != 0; arg = value ? arg : arg.substring(1); if (arg.equals(kindOfCode)) { return value; } } if (kindOfCode.equals("source")) { return useUncheckedDefaultsForSource; } else if (kindOfCode.equals("bytecode")) { return useUncheckedDefaultsForByteCode; } else { throw new UserError( "SourceChecker: unexpected argument to useConservativeDefault: " + kindOfCode); } } /** * Elements with a {@code @SuppressWarnings} that actually suppressed a warning for this checker. */ protected final Set elementsWithSuppressedWarnings = new HashSet<>(); /** * Determines whether all the warnings pertaining to a given element should be suppressed. Returns * true if the element is within the scope of a @SuppressWarnings annotation, one of whose values * suppresses all the checker's warnings. * * @param elt the Element that might be a source of, or related to, a warning * @param errKey the error key the checker is emitting * @return true if no warning should be emitted for the given Element because it is contained by a * declaration with an appropriately-valued {@code @SuppressWarnings} annotation; false * otherwise */ public boolean shouldSuppressWarnings(@Nullable Element elt, String errKey) { if (UNNEEDED_SUPPRESSION_KEY.equals(errKey)) { // Never suppress an "unneeded.suppression" warning. // TODO: This choice is questionable, because these warnings should be suppressable just // like any others. The reason for the choice is that if a user writes // `@SuppressWarnings("nullness")` that isn't needed, then that annotation would // suppress the unneeded suppression warning. It would take extra work to permit more // desirable behavior in that case. return false; } if (shouldSuppress(getSuppressWarningsStringsFromOption(), errKey)) { return true; } while (elt != null) { SuppressWarnings suppressWarningsAnno = elt.getAnnotation(SuppressWarnings.class); if (suppressWarningsAnno != null) { String[] suppressWarningsStrings = suppressWarningsAnno.value(); if (shouldSuppress(suppressWarningsStrings, errKey)) { if (hasOption("warnUnneededSuppressions")) { elementsWithSuppressedWarnings.add(elt); } return true; } } if (isAnnotatedForThisCheckerOrUpstreamChecker(elt)) { // Return false immediately. Do NOT check for AnnotatedFor in the // enclosing elements, because they may not have an @AnnotatedFor. return false; } elt = elt.getEnclosingElement(); } return false; } /** * Determines whether an error (whose message key is {@code messageKey}) should be suppressed. It * is suppressed if any of the given SuppressWarnings strings suppresses it. * *

A SuppressWarnings string may be of the following pattern: * *

    *
  1. {@code "prefix"}, where prefix is a SuppressWarnings prefix, as specified by {@link * #getSuppressWarningsPrefixes()}. For example, {@code "nullness"} and {@code * "initialization"} for the Nullness Checker, {@code "regex"} for the Regex Checker. *
  2. {@code "partial-message-key"}, where partial-message-key is a prefix or suffix of the * message key that it may suppress. So "generic.argument" would suppress any errors whose * message key contains "generic.argument". *
  3. {@code "prefix:partial-message-key}, where the prefix and partial-message-key is as * above. So "nullness:generic.argument", would suppress any errors in the Nullness Checker * with a message key that contains "generic.argument". *
* * {@code "allcheckers"} is a prefix that suppresses a warning from any checker. {@code "all"} is * a partial-message-key that suppresses a warning with any message key. * *

If the {@code -ArequirePrefixInWarningSuppressions} command-line argument was supplied, then * {@code "partial-message-key"} has no effect; {@code "prefix"} and {@code * "prefix:partial-message-key"} are the only SuppressWarnings strings that have an effect. * * @param suppressWarningsStrings the SuppressWarnings strings that are in effect. May be null, in * which case this method returns false. * @param messageKey the message key of the error the checker is emitting; a lowercase string, * without any "checkername:" prefix * @return true if an element of {@code suppressWarningsStrings} suppresses the error */ private boolean shouldSuppress(String[] suppressWarningsStrings, String messageKey) { Set prefixes = this.getSuppressWarningsPrefixes(); return shouldSuppress(prefixes, suppressWarningsStrings, messageKey); } /** * Helper method for {@link #shouldSuppress(String[], String)}. * * @param prefixes the SuppressWarnings prefixes used by this checker * @param suppressWarningsStrings the SuppressWarnings strings that are in effect. May be null, in * which case this method returns false. * @param messageKey the message key of the error the checker is emitting; a lowercase string, * without any "checkername:" prefix * @return true if one of the {@code suppressWarningsStrings} suppresses the error */ private boolean shouldSuppress( Set prefixes, String[] suppressWarningsStrings, String messageKey) { if (suppressWarningsStrings == null) { return false; } // Is the name of the checker required to suppress a warning? boolean requirePrefix = hasOption("requirePrefixInWarningSuppressions"); for (String suppressWarningsString : suppressWarningsStrings) { int colonPos = suppressWarningsString.indexOf(":"); String messageKeyInSuppressWarningsString; if (colonPos == -1) { // The SuppressWarnings string is not of the form prefix:partial-message-key if (prefixes.contains(suppressWarningsString)) { // The value in the @SuppressWarnings is exactly a prefix. Suppress the warning // no matter its message key. return true; } else if (requirePrefix) { // A prefix is required, but this SuppressWarnings string does not have a // prefix; check the next SuppressWarnings string. continue; } else if (suppressWarningsString.equals(SUPPRESS_ALL_MESSAGE_KEY)) { // Prefixes aren't required and the SuppressWarnings string is "all". Suppress // the warning no matter its message key. return true; } // The suppressWarningsString is not a prefix or a prefix:message-key, so it might // be a message key. messageKeyInSuppressWarningsString = suppressWarningsString; } else { // The SuppressWarnings string has a prefix. String suppressWarningsPrefix = suppressWarningsString.substring(0, colonPos); if (!prefixes.contains(suppressWarningsPrefix)) { // The prefix of this SuppressWarnings string is a not a prefix supported by // this checker. Proceed to the next SuppressWarnings string. continue; } messageKeyInSuppressWarningsString = suppressWarningsString.substring(colonPos + 1); } // Check if the message key in the warning suppression is part of the message key that // the checker is emiting. if (messageKey.equals(messageKeyInSuppressWarningsString) || messageKey.startsWith(messageKeyInSuppressWarningsString + ".") || messageKey.endsWith("." + messageKeyInSuppressWarningsString) || messageKey.contains("." + messageKeyInSuppressWarningsString + ".")) { return true; } } // None of the SuppressWarnings strings suppress this error. return false; } /** * Return true if the element has an {@code @AnnotatedFor} annotation, for this checker or an * upstream checker that called this one. * * @param elt the source code element to check, or null * @return true if the element is annotated for this checker or an upstream checker */ private boolean isAnnotatedForThisCheckerOrUpstreamChecker(@Nullable Element elt) { if (elt == null || !useConservativeDefault("source")) { return false; } @Nullable AnnotatedFor anno = elt.getAnnotation(AnnotatedFor.class); String[] userAnnotatedFors = (anno == null ? null : anno.value()); if (userAnnotatedFors != null) { List<@FullyQualifiedName String> upstreamCheckerNames = getUpstreamCheckerNames(); for (String userAnnotatedFor : userAnnotatedFors) { if (CheckerMain.matchesCheckerOrSubcheckerFromList( userAnnotatedFor, upstreamCheckerNames)) { return true; } } } return false; } /** * Returns a modifiable set of lower-case strings that are prefixes for SuppressWarnings strings. * *

The collection must not be empty and must not contain only {@link #SUPPRESS_ALL_PREFIX}. * * @return non-empty modifiable set of lower-case prefixes for SuppressWarnings strings */ public SortedSet getSuppressWarningsPrefixes() { return getStandardSuppressWarningsPrefixes(); } /** * Returns a sorted set of SuppressWarnings prefixes read from the {@link SuppressWarningsPrefix} * meta-annotation on the checker class. Or if no {@link SuppressWarningsPrefix} is used, the * checker name is used. {@link #SUPPRESS_ALL_PREFIX} is also added, at the end, unless {@link * #useAllcheckersPrefix} is false. * * @return a sorted set of SuppressWarnings prefixes */ protected final NavigableSet getStandardSuppressWarningsPrefixes() { NavigableSet prefixes = new TreeSet<>(); if (useAllcheckersPrefix) { prefixes.add(SUPPRESS_ALL_PREFIX); } SuppressWarningsPrefix prefixMetaAnno = this.getClass().getAnnotation(SuppressWarningsPrefix.class); if (prefixMetaAnno != null) { for (String prefix : prefixMetaAnno.value()) { prefixes.add(prefix); } return prefixes; } // No @SuppressWarningsPrefixes annotation, by default infer key from class name. String defaultPrefix = getDefaultSuppressWarningsPrefix(); prefixes.add(defaultPrefix); return prefixes; } /** * Returns the default SuppressWarnings prefix for this checker based on the checker name. * * @return the default SuppressWarnings prefix for this checker based on the checker name */ private String getDefaultSuppressWarningsPrefix() { String className = this.getClass().getSimpleName(); int indexOfChecker = className.lastIndexOf("Checker"); if (indexOfChecker == -1) { indexOfChecker = className.lastIndexOf("Subchecker"); } String result = (indexOfChecker == -1) ? className : className.substring(0, indexOfChecker); return result.toLowerCase(); } /////////////////////////////////////////////////////////////////////////// /// Skipping uses and defs /// /** * Tests whether the class owner of the passed element is an unannotated class and matches the * pattern specified in the {@code checker.skipUses} property. * * @param element an element * @return true iff the enclosing class of element should be skipped */ public final boolean shouldSkipUses(Element element) { if (element == null) { return false; } TypeElement typeElement = ElementUtils.enclosingTypeElement(element); if (typeElement == null) { throw new BugInCF("enclosingTypeElement(%s [%s]) => null%n", element, element.getClass()); } @SuppressWarnings("signature:assignment") // TypeElement.toString(): @FullyQualifiedName @FullyQualifiedName String name = typeElement.toString(); return shouldSkipUses(name); } /** * Tests whether the class owner of the passed type matches the pattern specified in the {@code * checker.skipUses} property. In contrast to {@link #shouldSkipUses(Element)} this version can * also be used from primitive types, which don't have an element. * *

Checkers that require their annotations not to be checked on certain JDK classes may * override this method to skip them. They shall call {@code super.shouldSkipUses(typeName)} to * also skip the classes matching the pattern. * * @param typeName the fully-qualified name of a type * @return true iff the enclosing class of element should be skipped */ public boolean shouldSkipUses(@FullyQualifiedName String typeName) { // System.out.printf("shouldSkipUses(%s) %s%nskipUses %s%nonlyUses %s%nresult %s%n", // element, // name, // skipUsesPattern.matcher(name).find(), // onlyUsesPattern.matcher(name).find(), // (skipUsesPattern.matcher(name).find() // || ! onlyUsesPattern.matcher(name).find())); // StackTraceElement[] stea = new Throwable().getStackTrace(); // for (int i=0; i<3; i++) { // System.out.println(" " + stea[i]); // } // System.out.println(); if (skipUsesPattern == null) { skipUsesPattern = getSkipUsesPattern(getOptions()); } if (onlyUsesPattern == null) { onlyUsesPattern = getOnlyUsesPattern(getOptions()); } return skipUsesPattern.matcher(typeName).find() || !onlyUsesPattern.matcher(typeName).find(); } /** * Tests whether the class definition should not be checked because it matches the {@code * checker.skipDefs} property. * * @param node class to potentially skip * @return true if checker should not test node */ public final boolean shouldSkipDefs(ClassTree node) { String qualifiedName = TreeUtils.typeOf(node).toString(); // System.out.printf("shouldSkipDefs(%s) %s%nskipDefs %s%nonlyDefs %s%nresult %s%n%n", // node, // qualifiedName, // skipDefsPattern.matcher(qualifiedName).find(), // onlyDefsPattern.matcher(qualifiedName).find(), // (skipDefsPattern.matcher(qualifiedName).find() // || ! onlyDefsPattern.matcher(qualifiedName).find())); if (skipDefsPattern == null) { skipDefsPattern = getSkipDefsPattern(getOptions()); } if (onlyDefsPattern == null) { onlyDefsPattern = getOnlyDefsPattern(getOptions()); } return skipDefsPattern.matcher(qualifiedName).find() || !onlyDefsPattern.matcher(qualifiedName).find(); } /** * Tests whether the method definition should not be checked because it matches the {@code * checker.skipDefs} property. * *

TODO: currently only uses the class definition. Refine pattern. Same for skipUses. * * @param cls class to potentially skip * @param meth method to potentially skip * @return true if checker should not test node */ public final boolean shouldSkipDefs(ClassTree cls, MethodTree meth) { return shouldSkipDefs(cls); } /////////////////////////////////////////////////////////////////////////// /// Errors other than type-checking errors /// /** * Log (that is, print) a user error. * * @param ce the user error to output */ private void logUserError(UserError ce) { String msg = ce.getMessage(); printMessage(msg); } /** * Log (that is, print) a type system error. * * @param ce the type system error to output */ private void logTypeSystemError(TypeSystemError ce) { String msg = ce.getMessage(); printMessage(msg); } /** * Log (that is, print) an internal error in the framework or a checker. * * @param ce the internal error to output */ private void logBugInCF(BugInCF ce) { StringJoiner msg = new StringJoiner(LINE_SEPARATOR); if (ce.getCause() != null && ce.getCause() instanceof OutOfMemoryError) { msg.add( String.format( "OutOfMemoryError (max memory = %d, total memory = %d, free memory = %d)", Runtime.getRuntime().maxMemory(), Runtime.getRuntime().totalMemory(), Runtime.getRuntime().freeMemory())); } else { msg.add(ce.getMessage()); boolean noPrintErrorStack = (processingEnv != null && processingEnv.getOptions() != null && processingEnv.getOptions().containsKey("noPrintErrorStack")); msg.add("; The Checker Framework crashed. Please report the crash."); if (noPrintErrorStack) { msg.add(" To see the full stack trace, don't invoke the compiler with -AnoPrintErrorStack"); } else { if (this.currentRoot != null && this.currentRoot.getSourceFile() != null) { msg.add("Compilation unit: " + this.currentRoot.getSourceFile().getName()); } if (this.visitor != null) { DiagnosticPosition pos = (DiagnosticPosition) this.visitor.lastVisited; if (pos != null) { DiagnosticSource source = new DiagnosticSource(this.currentRoot.getSourceFile(), null); int linenr = source.getLineNumber(pos.getStartPosition()); int col = source.getColumnNumber(pos.getStartPosition(), true); String line = source.getLine(pos.getStartPosition()); msg.add("Last visited tree at line " + linenr + " column " + col + ":"); msg.add(line); } } } } msg.add("Exception: " + ce.getCause() + "; " + UtilPlume.stackTraceToString(ce.getCause())); boolean printClasspath = ce.getCause() instanceof NoClassDefFoundError; Throwable cause = ce.getCause().getCause(); while (cause != null) { msg.add("Underlying Exception: " + cause + "; " + UtilPlume.stackTraceToString(cause)); printClasspath |= cause instanceof NoClassDefFoundError; cause = cause.getCause(); } if (printClasspath) { msg.add("Classpath:"); for (URI uri : new ClassGraph().getClasspathURIs()) { msg.add(uri.toString()); } } printMessage(msg.toString()); } /** * Converts a throwable to a BugInCF. * * @param methodName the method that caught the exception (redundant with stack trace) * @param t the throwable to be converted to a BugInCF * @param p what source code was being processed * @return a BugInCF that wraps the given throwable */ private BugInCF wrapThrowableAsBugInCF(String methodName, Throwable t, @Nullable TreePath p) { return new BugInCF( methodName + ": unexpected Throwable (" + t.getClass().getSimpleName() + ")" + ((p == null) ? "" : " while processing " + p.getCompilationUnit().getSourceFile().getName()) + (t.getMessage() == null ? "" : "; message: " + t.getMessage()), t); } /////////////////////////////////////////////////////////////////////////// /// Shutdown /// /** * Return true to indicate that method {@link #shutdownHook} should be added as a shutdownHook of * the JVM. * * @return true to add {@link #shutdownHook} as a shutdown hook of the JVM */ protected boolean shouldAddShutdownHook() { return hasOption("resourceStats"); } /** * Method that gets called exactly once at shutdown time of the JVM. Checkers can override this * method to customize the behavior. */ protected void shutdownHook() { if (hasOption("resourceStats")) { // Check for the "resourceStats" option and don't call shouldAddShutdownHook // to allow subclasses to override shouldXXX and shutdownHook and simply // call the super implementations. printStats(); } } /** Print resource usage statistics. */ protected void printStats() { List memoryPools = ManagementFactory.getMemoryPoolMXBeans(); for (MemoryPoolMXBean memoryPool : memoryPools) { System.out.println("Memory pool " + memoryPool.getName() + " statistics"); System.out.println(" Pool type: " + memoryPool.getType()); System.out.println(" Peak usage: " + memoryPool.getPeakUsage()); } } /////////////////////////////////////////////////////////////////////////// /// Miscellaneous /// /** * 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 * @param permitNonExisting if true, return an empty Properties if the file does not exist or * cannot be parsed; if false, issue an error * @return the properties */ protected Properties getProperties(Class cls, String filePath, boolean permitNonExisting) { Properties prop = new Properties(); try { InputStream base = cls.getResourceAsStream(filePath); if (base == null) { // The property file was not found. if (permitNonExisting) { return prop; } else { throw new BugInCF("Couldn't locate properties file " + filePath); } } prop.load(base); } catch (IOException e) { throw new BugInCF("Couldn't parse properties file: " + filePath, e); } return prop; } @Override public final SourceVersion getSupportedSourceVersion() { return SourceVersion.latest(); } /** True if the git.properties file has been printed. */ private static boolean gitPropertiesPrinted = false; /** Print information about the git repository from which the Checker Framework was compiled. */ private void printGitProperties() { if (gitPropertiesPrinted) { return; } gitPropertiesPrinted = true; try (InputStream in = getClass().getResourceAsStream("/git.properties"); BufferedReader reader = new BufferedReader(new InputStreamReader(in)); ) { String line; while ((line = reader.readLine()) != null) { System.out.println(line); } } catch (IOException e) { System.out.println("IOException while reading git.properties: " + e.getMessage()); } } /** * Returns the version of the Checker Framework. * * @return the Checker Framework version */ private String getCheckerVersion() { Properties gitProperties = getProperties(getClass(), "/git.properties", false); String version = gitProperties.getProperty("git.build.version"); if (version != null) { return version; } throw new BugInCF("Could not find the version in git.properties"); } /** * Gradle and IntelliJ wrap the processing environment to gather information about modifications * done by annotation processor during incremental compilation. But the Checker Framework calls * methods from javac that require the processing environment to be {@code * com.sun.tools.javac.processing.JavacProcessingEnvironment}. They fail if given a proxy. This * method unwraps a proxy if one is used. * * @param env a processing environment * @return unwrapped environment if the argument is a proxy created by IntelliJ or Gradle; * original value (the argument) if the argument is a javac processing environment * @throws BugInCF if method fails to retrieve {@code * com.sun.tools.javac.processing.JavacProcessingEnvironment} */ private static ProcessingEnvironment unwrapProcessingEnvironment(ProcessingEnvironment env) { if (env.getClass().getName() == "com.sun.tools.javac.processing.JavacProcessingEnvironment") { // interned return env; } // IntelliJ >2020.3 wraps the processing environment in a dynamic proxy. ProcessingEnvironment unwrappedIntelliJ = unwrapIntelliJ(env); if (unwrappedIntelliJ != null) { return unwrapProcessingEnvironment(unwrappedIntelliJ); } // Gradle incremental build also wraps the processing environment. for (Class envClass = env.getClass(); envClass != null; envClass = envClass.getSuperclass()) { ProcessingEnvironment unwrappedGradle = unwrapGradle(envClass, env); if (unwrappedGradle != null) { return unwrapProcessingEnvironment(unwrappedGradle); } } throw new BugInCF("Unexpected processing environment: %s %s", env, env.getClass()); } /** * Tries to unwrap ProcessingEnvironment from proxy in IntelliJ 2020.3 or later. * * @param env possibly a dynamic proxy wrapping processing environment * @return unwrapped processing environment, null if not successful */ private static @Nullable ProcessingEnvironment unwrapIntelliJ(ProcessingEnvironment env) { if (!Proxy.isProxyClass(env.getClass())) { return null; } InvocationHandler handler = Proxy.getInvocationHandler(env); try { Field field = handler.getClass().getDeclaredField("val$delegateTo"); field.setAccessible(true); Object o = field.get(handler); if (o instanceof ProcessingEnvironment) { return (ProcessingEnvironment) o; } return null; } catch (NoSuchFieldException | IllegalAccessException e) { return null; } } /** * Tries to unwrap processing environment in Gradle incremental processing. Inspired by project * Lombok. * * @param delegateClass a class in which to find a {@code delegate} field * @param env a processing environment wrapper * @return unwrapped processing environment, null if not successful */ private static @Nullable ProcessingEnvironment unwrapGradle( Class delegateClass, ProcessingEnvironment env) { try { Field field = delegateClass.getDeclaredField("delegate"); field.setAccessible(true); Object o = field.get(env); if (o instanceof ProcessingEnvironment) { return (ProcessingEnvironment) o; } return null; } catch (NoSuchFieldException | IllegalAccessException e) { return null; } } /** * Return the path to the current compilation unit. * * @return path to the current compilation unit */ public TreePath getPathToCompilationUnit() { return TreePath.getPath(currentRoot, currentRoot); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy