blob: 1c3cb9a3aaa9b3666ab18c8ce1e74971b5b0964f [file] [log] [blame]
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.
*
* <p>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",
// 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. <E extends F, F extends Object>
// 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.
*
* <p>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.
*
* <p>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.
*
* <p>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.
*
* <p>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.
*
* <p>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<String> supportedLints;
/** The enabled lint options. */
private Set<String> 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
* &rarr; 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 &rarr; value.
* If the option is of the form "-Akey=value", then add key &rarr; value.
*
* <p>Both the simple and the canonical name of the checker can be used. Superclasses of the
* current checker are also considered.
*/
private Map<String, String> 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) {
throw new UserError(
"Use JDK 8 or JDK 11 to run the Checker Framework. You are using version %d.",
jreVersion);
} else if (jreVersion > 12) {
throw new UserError(
String.format(
"Use JDK 8 or JDK 11 to run the Checker Framework. You are using version %d.",
jreVersion));
} else if (jreVersion != 8 && jreVersion != 11) {
message(
Kind.WARNING,
"Use JDK 8 or JDK 11 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.
*
* <p>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<Class<?>> 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<String, String> 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<String, String> options) {
// default matches everything
return getPattern(patternName, options, ".");
}
private Pattern getPattern(
String patternName, Map<String, String> 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<String, String> options) {
return getSkipPattern("skipUses", options);
}
private Pattern getOnlyUsesPattern(Map<String, String> options) {
return getOnlyPattern("onlyUses", options);
}
private Pattern getSkipDefsPattern(Map<String, String> options) {
return getSkipPattern("skipDefs", options);
}
private Pattern getOnlyDefsPattern(Map<String, String> options) {
return getOnlyPattern("onlyDefs", options);
}
///////////////////////////////////////////////////////////////////////////
/// Type-checking
///
/**
* {@inheritDoc}
*
* <p>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 && SystemPlume.gcPercentage() > .25) {
messager.printMessage(
Kind.WARNING, "Garbage collection consumed over 25% of CPU during the past minute.");
messager.printMessage(
Kind.WARNING,
String.format(
"Perhaps increase max heap size"
+ " (max memory = %d, total memory = %d, free memory = %d).",
Runtime.getRuntime().maxMemory(),
Runtime.getRuntime().totalMemory(),
Runtime.getRuntime().freeMemory()));
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.
*
* <p>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.
*
* <p>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.
*
* <p>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.
*
* <p>This implementation expands the argument if it is exactly a message key.
*
* <p>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<String> prefixes = this.getSuppressWarningsPrefixes();
prefixes.remove(SUPPRESS_ALL_PREFIX);
if (hasOption("showSuppressWarningsStrings")) {
List<String> 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<String> createActiveLints(Map<String, String> options) {
if (!options.containsKey("lint")) {
return Collections.emptySet();
}
String lintString = options.get("lint");
if (lintString == null) {
return Collections.singleton("all");
}
Set<String> activeLint = new HashSet<>();
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 <a
* href="https://docs.oracle.com/javase/7/docs/technotes/guides/javac/index.html">javac</a> uses
* "-Xlint:xxx" to enable and "-Xlint:-xxx" to disable option xxx, annotation-related lint options
* are enabled with "-Alint:xxx" and disabled with "-Alint:-xxx".
*
* @throws IllegalArgumentException if the option name is not recognized via the {@link
* SupportedLintOptions} annotation or the {@link SourceChecker#getSupportedLintOptions}
* method
* @param name the name of the lint option to check for
* @return true if the lint option was given, false if it was not given or was given prepended
* with a "-"
* @see SourceChecker#getLintOption(String, boolean)
*/
public final boolean getLintOption(String name) {
return getLintOption(name, false);
}
/**
* Determines the value of the lint option with the given name. Just as <a
* href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javac.html">javac</a> uses
* "-Xlint:xxx" to enable and "-Xlint:-xxx" to disable option xxx, annotation-related lint options
* are enabled with "-Alint=xxx" and disabled with "-Alint=-xxx".
*
* @throws IllegalArgumentException if the option name is not recognized via the {@link
* SupportedLintOptions} annotation or the {@link SourceChecker#getSupportedLintOptions}
* method
* @param name the name of the lint option to check for
* @param def the default option value, returned if the option was not given
* @return true if the lint option was given, false if it was given prepended with a "-", or
* {@code def} if it was not given at all
* @see SourceChecker#getLintOption(String)
* @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 <a
* href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javac.html">javac</a> uses
* "-Xlint:xxx" to enable and "-Xlint:-xxx" to disable option xxx, annotation-related lint options
* are enabled with "-Alint=xxx" and disabled with "-Alint=-xxx". 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<String> 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.
*
* <pre>
* Example
* cast:unsafe &rarr; cast
* cast &rarr; all
* all &rarr; {@code null}
* </pre>
*
* @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<String> getSupportedLintOptions() {
if (supportedLints == null) {
supportedLints = createSupportedLintOptions();
}
return supportedLints;
}
/** Compute the set of supported lint options. */
protected Set<String> 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<String> 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<String> 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<String, String> createActiveOptions(Map<String, String> options) {
if (options.isEmpty()) {
return Collections.emptyMap();
}
Map<String, String> activeOpts = new HashMap<>(CollectionsPlume.mapCapacity(options));
for (Map.Entry<String, String> 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<String, String> moreOpts) {
Map<String, String> activeOpts = new HashMap<>(getOptions());
activeOpts.putAll(moreOpts);
activeOptions = Collections.unmodifiableMap(activeOpts);
}
/**
* Check whether the given option is provided.
*
* <p>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<String, String> 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<String> getSupportedOptions() {
Set<String> 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<Class<?>> 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<String> expandCFOptions(
List<? extends Class<?>> clazzPrefixes, String[] options) {
Set<String> 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 "*".
*
* <p>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.
*
* <p>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<String> 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<String, String> 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<Element> elementsSuppress = new HashSet<>(this.elementsWithSuppressedWarnings);
this.elementsWithSuppressedWarnings.clear();
Set<String> prefixes = new HashSet<>(getSuppressWarningsPrefixes());
Set<String> 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<Element> elementsSuppress, Set<String> prefixes, Set<String> 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<? extends AnnotationTree> 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<String> 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)) {
return true;
}
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);
@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<Element> 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.
*
* <p>A SuppressWarnings string may be of the following pattern:
*
* <ol>
* <li>{@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.
* <li>{@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".
* <li>{@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".
* </ol>
*
* {@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.
*
* <p>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<String> 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<String> 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.
*
* <p>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<String> 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<String> getStandardSuppressWarningsPrefixes() {
NavigableSet<String> 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;
}
@SuppressWarnings("deprecation") // SuppressWarningsKeys was renamed to SuppressWarningsPrefix
SuppressWarningsKeys annotation = this.getClass().getAnnotation(SuppressWarningsKeys.class);
if (annotation != null) {
for (String prefix : annotation.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.
*
* <p>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.
*
* <p>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<MemoryPoolMXBean> 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);
}
}