The Checker Framework Manual: |
For the impatient: Section 1.3 describes how to install and use pluggable type-checkers.
This manual is also available in PDF.
The Checker Framework enhances Java’s type system to make it more powerful and useful. This lets software developers detect and prevent errors in their Java programs.
A “checker” is a compile-time tool that warns you about certain errors or gives you a guarantee that those errors do not occur. The Checker Framework comes with checkers for specific types of errors:
These checkers are easy to use and are invoked as arguments to javac.
The Checker Framework also enables you to write new checkers of your own; see Chapters 29 and 36.
If you wish to get started using some particular type system from the list above, then the most effective way to read this manual is:
Java’s built-in type-checker finds and prevents many errors — but it doesn’t find and prevent enough errors. The Checker Framework lets you define new type systems and run them as a plug-in to the javac compiler. Your code stays completely backward-compatible: your code compiles with any Java compiler, it runs on any JVM, and your coworkers don’t have to use the enhanced type system if they don’t want to. You can check part of your program, or the whole thing. Type inference tools exist to help you annotate your code; see Section 34.
Most programmers will use type systems created by other people, such as those listed at the start of the introduction (Chapter 1). Some people, called “type system designers”, create new type systems (Chapter 36). The Checker Framework is useful both to programmers who wish to write error-free code, and to type system designers who wish to evaluate and deploy their type systems.
This document uses the terms “checker” and “type-checking compiler plugin” as synonyms.
This section describes how to install the Checker Framework.
Requirement: You must have a JDK (version 8 or later) installed.
The installation process has two required steps and one optional step.
Now you are ready to start using the checkers.
We recommend that you work through the Checker Framework tutorial, which demonstrates the Nullness, Regex, and Tainting Checkers.
Section 1.4 walks you through a simple example. More detailed instructions for using a checker appear in Chapter 2.
The Checker Framework is released on a monthly schedule. The minor version (the middle number in the version number) is incremented if there are any incompatibilities with the previous version, including in user-visible behavior or in methods that a checker implementation might call.
This section gives a very simple example of running the Checker Framework. There is also a tutorial that you can work along with.
Let’s consider this very simple Java class. The local variable ref’s type is annotated as @NonNull, indicating that ref must be a reference to a non-null object. Save the file as GetStarted.java.
import org.checkerframework.checker.nullness.qual.*; public class GetStarted { void sample() { @NonNull Object ref = new Object(); } }
If you run the Nullness Checker (Chapter 3), the compilation completes without any errors.
Now, introduce an error. Modify ref’s assignment to:
@NonNull Object ref = null;
If you run the Nullness Checker again, it emits the following error:
GetStarted.java:5: incompatible types. found : @Nullable <nulltype> required: @NonNull Object @NonNull Object ref = null; ^ 1 error
This is a trivially simple example. Even an unsound bug-finding tool like SpotBugs or Error Prone could have detected this bug. The Checker Framework’s analysis is more powerful than those tools and detects more code defects than they do.
Type qualifiers such as @NonNull are permitted anywhere that you can write a type, including generics and casts; see Section 2.1. Here are some examples:
@Interned String intern() { ... } // return value int compareTo(@NonNull String other) { ... } // parameter @NonNull List<@Interned String> messages; // non-null list of interned Strings
A pluggable type-checker enables you to detect certain bugs in your code, or to prove that they are not present. The verification happens at compile time.
Finding bugs, or verifying their absence, with a checker is a two-step process, whose steps are described in Sections 2.1 and 2.2.
This chapter is structured as follows:
Additional topics that apply to all checkers are covered later in the manual:
There is a tutorial that walks you through using the Checker Framework on the command line.
You may write a type annotation immediately before any use of a type, including in generics and casts. Because array levels are types and receivers have types, you can also write type annotations on them. Here are a few examples of type annotations:
@Interned String intern() { ... } // return value int compareTo(@NonNull String other) { ... } // parameter String toString(@Tainted MyClass this) { ... } // receiver ("this" parameter) @NonNull List<@Interned String> messages; // generics: non-null list of interned Strings @Interned String @NonNull [] messages; // arrays: non-null array of interned Strings myDate = (@Initialized Date) beingConstructed; // cast
You only need to write type annotations on method signatures, fields, and some type arguments. Most annotations within method bodies are inferred for you; for more details, see Section 32.7.
The Java Language Specification also defines declaration annotations, such as @Deprecated and @Override, which apply to a class, method, or field but do not apply to the method’s return type or the field’s type. They should be written on their own line in the source code, before the method’s signature.
To run a checker, run the compiler javac as usual, but either pass the -processor plugin_class command-line option, or use auto-discovery as described in Section 2.2.3. (If your project already uses auto-discovery for some annotation processor, such as AutoValue, then you should use auto-discovery.) Two concretes example of using -processor to run the Nullness Checker are
javac -processor nullness MyFile.java javac -processor org.checkerframework.checker.nullness.NullnessChecker MyFile.java
where javac is as specified in Section 38.6.
You can also run a checker from within your favorite IDE or build system. See Chapter 38 for details about build tools such as Ant (Section 38.3), Buck (Section 38.5), Bazel (Section 38.4), Gradle (Section 38.9), Maven (Section 38.13), and sbt (Section 38.15); IDEs such as Eclipse (Section 38.8), IntelliJ IDEA (Section 38.10), NetBeans (Section 38.14), and tIDE (Section 38.16); and about customizing other IDEs and build tools.
The checker is run on only the Java files that javac compiles. This includes all Java files specified on the command line and those created by another annotation processor. It may also include other of your Java files, if they are more recent than the corresponding .class file. Even when the checker does not analyze a class (say, the class was already compiled, or source code is not available), it does check the uses of those classes in the source code being compiled. Type-checking works modularly and intraprocedurally: when verifying a method, it examines only the signature (including annotations) of other methods, not their implementations. When analyzing a variable use, it relies on the type of the variable, not any dataflow outside the current method that produced the value.
After you compile your code while running a checker, the resulting .class and .jar files can be used for pluggable type-checking of client code.
If you compile code without the -processor command-line option, no checking of the type annotations is performed. Furthermore, only explicitly-written annotations are written to the .class file; defaulted annotations are not, and this will interfere with type-checking of clients that use your code. Therefore, to create .class files that will be distributed or compiled against, you should run the type-checkers for all the annotations that you have written.
When your code uses a library that is not currently being compiled, the Checker Framework looks up the library’s annotations in its class files or in a stub file.
Some projects are already distributed with type annotations by their maintainers, so you do not need to do anything special. An example is all the libraries in https://github.com/plume-lib/. Over time, this should become more common.
For some other libraries, the Checker Framework developers have provided an annotated version of the library, either as a stub file or as compiled class files. (If some library is not available in either of these forms, you can contribute by annotating it, which will help you and all other Checker Framework users; see Chapter 35.)
Some stub files are used automatically by a checker, without any action on your part. For others, you must pass the -Astubs=... command-line argument. As a special case, if an .astub file appears in checker/src/main/resources/, then pass the command-line option use -Astubs=checker.jar/stubfilename.astub. The “checker.jar” should be literal — don’t provide a path. This special syntax only works for “checker.jar”.
The annotated libraries that are provided as class files appear in the org.checkerframework.annotatedlib group in the Maven Central Repository. The annotated library has identical behavior to the upstream, unannotated version; the source code is identical other than added annotations. (Some of the annotated libraries are bcel, commons-csv, commons-io, guava, and java-getopt.)
To use an annotated library:
api group: 'org.apache.bcel', name: 'bcel', version: '6.3.1' api group: 'commons-io', name: 'commans-io', version: '2.8'
to
api group: 'org.checkerframework.annotatedlib', name: 'bcel', version: '6.3.1' api group: 'org.checkerframework.annotatedlib', name: 'commons-io', version: '2.8.0.1'
Usually use the same version number. (Sometimes you will use a slightly larger number, if the Checker Framework developers have improved the type annotations since the last release by the upstream maintainers.) If a newer version of the upstream library is available but that version is not available in org.checkerframework.annotatedlib, then open an issue requesting that the org.checkerframework.annotatedlib version be updated.
There is one special case. If an .astub file is shipped with the Checker Framework in checker/src/main/resources/, then you can use -Astubs=checker.jar/stubfilename.astub. The “checker.jar” should be literal — don’t provide a path. (This special syntax only works for “checker.jar”.)
You can pass command-line arguments to a checker via javac’s standard -A option (“A” stands for “annotation”). All of the distributed checkers support the following command-line options. Each checker may support additional command-line options; see the checker’s documentation.
To pass an option to only a particular checker, prefix the option with the canonical or simple name of a checker, followed by an underscore “_”. Such an option will apply only to a checker with that name or any subclass of that checker. For example, you can use
-ANullnessChecker_lint=redundantNullComparison -Aorg.checkerframework.checker.guieffect.GuiEffectChecker_lint=debugSpew
to pass different lint options to the Nullness and GUI Effect Checkers. A downside is that, in this example, the Nullness Checker will issue a “The following options were not recognized by any processor” warning about the second option and the GUI Effect Checker will issue a “The following options were not recognized by any processor” warning about the first option.
Unsound checking: ignore some errors
More sound (strict) checking: enable errors that are disabled by default
Type-checking modes: enable/disable functionality
Partially-annotated libraries
Debugging
Some checkers support additional options, which are described in that checker’s manual section. For example, -Aquals tells the Subtyping Checker (see Chapter 29) and the Fenum Checker (see Chapter 9) which annotations to check.
Here are some standard javac command-line options that you may find useful. Many of them contain “processor” or “proc”, because in javac jargon, a checker is an “annotation processor”.
“Auto-discovery” makes the javac compiler always run an annotation processor, such as a checker plugin, without explicitly passing the -processor command-line option. This can make your command line shorter, and it ensures that your code is checked even if you forget the command-line option.
If the javac command line specifies any -processor command-line option, then auto-discovery is disabled. This means that if your project currently uses auto-discovery, you should use auto-discovery for the Checker Framework too. (Alternately, if you prefer to use a -processor command-line argument, you will need to specify all annotation processors, including ones that used to be auto-discovered.)
To enable auto-discovery, place a configuration file named META-INF/services/javax.annotation.processing.Processor in your classpath. The file contains the names of the checkers to be used, listed one per line. For instance, to run the Nullness Checker and the Interning Checker automatically, the configuration file should contain:
org.checkerframework.checker.nullness.NullnessChecker org.checkerframework.checker.interning.InterningChecker
You can disable this auto-discovery mechanism by passing the -proc:none command-line option to javac, which disables all annotation processing including all pluggable type-checking.
Ordinarily, javac’s -processor flag requires fully-qualified class names. When using the Checker Framework javac wrapper (Section 38.6), you may omit the package name and the Checker suffix. The following three commands are equivalent:
javac -processor org.checkerframework.checker.nullness.NullnessChecker MyFile.java javac -processor NullnessChecker MyFile.java javac -processor nullness MyFile.java
This feature also works when multiple checkers are specified. Their names are separated by commas, with no surrounding space. For example:
javac -processor NullnessChecker,RegexChecker MyFile.java javac -processor nullness,regex MyFile.java
This feature does not apply to javac @argfiles.
A checker guarantees two things: type annotations reflect facts about run-time values, and illegal operations are not performed.
For example, the Nullness Checker (Chapter 3) guarantees lack of null pointer exceptions (Java NullPointerException). More precisely, it guarantees that expressions whose type is annotated with @NonNull never evaluate to null, and it forbids other expressions from being dereferenced.
As another example, the Interning Checker (Chapter 6) guarantees that correct equality tests are performed. More precisely, it guarantees that every expression whose type is an @Interned type evaluates to an interned value, and it forbids == on other expressions.
The guarantee holds only if you run the checker on every part of your program and the checker issues no warnings anywhere in the code. You can also verify just part of your program.
There are some limitations to the guarantee.
In each of these cases, any use of the code is checked — for example, a call to a native method must be compatible with any annotations on the native method’s signature. However, the annotations on the un-checked code are trusted; there is no verification that the implementation of the native method satisfies the annotations.
In order to avoid a flood of unhelpful warnings, many of the checkers avoid issuing the same warning multiple times. For example, consider this code:
@Nullable Object x = ...; x.toString(); // warning x.toString(); // no warning
The second call to toString cannot possibly throw a null pointer warning — x is non-null if control flows to the second statement. In other cases, a checker avoids issuing later warnings with the same cause even when later code in a method might also fail. This does not affect the soundness guarantee, but a user may need to examine more warnings after fixing the first ones identified. (Often, a single fix corrects all the warnings.)
If you find that a checker fails to issue a warning that it should, then please report a bug (see Section 40.2).
Section 35.1 gives additional tips that are specific to annotating a third-party library.
Before you run a checker, annotate the code, based on its documentation. Then, run the checker to uncover bugs in the code or the documentation.
Don’t do the opposite, which is to run the checker and then add annotations according to the warnings issued. This approach is less systematic, so you may overlook some annotations. It often leads to confusion and poor results. It leads users to make changes not for any principled reason, but to “make the type-checker happy”, even when the changes are in conflict with the documentation or the code. Also see “Annotations are a specification”, below.
Annotating an entire existing program may seem like a daunting task. But, if you approach it systematically and do a little bit at a time, you will find that it is manageable.
Start small. Focus on one specific property that matters to you; in other words, run just one checker rather than multiple ones. You may choose a different checker for different programs. Focus on the most mission-critical or error-prone part of your code; don’t try to annotate your whole program at first.
It is easiest to add annotations if you know the code or the code contains documentation. While adding annotations, you will spend most of your time understanding the code, and less time actually writing annotations or running the checker.
Don’t annotate the whole program, but work module by module. Start annotating classes at the leaves of the call tree — that is, start with classes/packages that have few dependencies on other code. Annotate supertypes before you annotate classes that extend or implement them. The reason for this rule is that it is easiest to annotate a class if the code it depends on has already been annotated. Sections 33.4 and 33.5 give ways to skip checking of some files, directories, or packages. Section 2.4.6 gives advice about handling calls from annotated code into unannotated code.
When annotating, be systematic; we recommend annotating an entire class or module at a time (not just some of the methods) so that you don’t lose track of your work or redo work. For example, working class-by-class avoids confusion about whether an unannotated type use means you determined that the default is desirable, or it means you didn’t yet examine that type use.
Don’t overuse pluggable type-checking. If the regular Java type system can verify a property using Java subclasses, then that is a better choice than pluggable type-checking (see Section 39.1.2).
When you write annotations, you are writing a specification, and you should think about them that way. Start out by understanding the program so that you can write an accurate specification. Sections 2.4.3 and 2.4.4 give more tips about writing specifications.
For each class, read its Javadoc. For instance, if you are adding annotations for the Nullness Checker (Section 3), then you can search the documentation for “null” and then add @Nullable anywhere appropriate. Start by annotating signatures and fields, but not method bodies. The only reason to even read the method bodies yet is to determine signature annotations for undocumented methods — for example, if the method returns null, you know its return type should be annotated @Nullable, and a parameter that is compared against null may need to be annotated @Nullable.
The specification should state all facts that are relevant to callees. When checking a method, the checker uses only the specification, not the implementation, of other methods. (Equivalently, type-checking is “modular” or “intraprocedural”.) When analyzing a variable use, the checker relies on the type of the variable, not any dataflow outside the current method that produced the value.
After you have annotated all the signatures, run the checker. Then, fix bugs in code and add/modify annotations as necessary. Don’t get discouraged if you see many type-checker warnings at first. Often, adding just a few missing annotations will eliminate many warnings, and you’ll be surprised how fast the process goes overall (assuming that you understand the code, of course).
It is usually not a good idea to experiment with adding and removing annotations in order to understand their effect. It is better to reason about the desired design. However, to avoid having to manually examine all callees, a more automated approach is to save the checker output before changing an annotation, then compare it to the checker output after changing the annotation.
Chapter 35 tells you how to annotate libraries that your code uses. Section 2.4.5 and Chapter 33 tell you what to do when you are unable to eliminate checker warnings by adding annotations.
Avoid complex code, which is more error-prone. If you write your code to be simple and clear enough for the type-checker to verify, then it will also be easier for programmers to understand. When you verify your code, a side benefit is improving your code’s structure.
Your code should compile cleanly under the regular Java compiler. As a specific example, your code should not use raw types like List; use parameterized types like List<String> instead (Section 31.1.1). If you suppress Java compiler warnings, then the Checker Framework will issue more warnings, and its messages will be more confusing. (Also, if you are not willing to write code that type-checks in Java, then you might not be willing to use an even more powerful type system.)
Do not write unnecessary annotations.
// The below code increments variable i by adding 1 to it. i++;
As you become comfortable with pluggable type-checking, you will find redundant annotations to be distracting clutter, so avoid putting them in your code in the first place.
You should use annotations to specify normal behavior. The annotations indicate all the values that you want to flow to a reference — not every value that might possibly flow there if your program has a bug.
As an example, consider the Nullness Checker. Its goal is to guarantee that your program does not crash due to a null value.
This method crashes if null is passed to it:
/** @throws NullPointerException if arg is null */ void m1(Object arg) { arg.toString(); ... }
Therefore, the type of arg should be @NonNull Object — you can write this as just Object, because @NonNull is the default. The Nullness Checker (Chapter 3) prevents null pointer exceptions by warning you whenever a client passes a value that might cause m1 to crash.
Here is another method:
/** @throws NullPointerException if arg is null */ void m2(Object arg) { Objects.requireNonNull(arg); ... }
Method m2 behaves just like m1 in that it throws NullPointerException if a client passes null. Therefore, their specifications should be identical (the formal parameter type is annotated with @NonNull), so the checker will issue the same warning if a client might pass null.
The same argument applies to any method that is guaranteed to throw an exception if it receives null as an argument. Examples include:
com.google.common.base.Preconditions.checkNotNull(Object) java.lang.Double.valueOf(String) java.lang.Objects.requireNonNull(Object) java.lang.String.contains(CharSequence) org.junit.Assert.assertNotNull(Object) org.junit.jupiter.api.Assert.assertNotNull(Object)
Their formal parameter type is annotated as @NonNull, because otherwise the program might crash. Adding a call to a method like requireNonNull never prevents a crash: your code still crashes, but with a slightly different stack trace. In order to prevent all exceptions in your program caused by null pointers, you need to prevent those thrown by methods including requireNonNull.
(One might argue that the formal parameter should be annotated as @Nullable because passing null has a well-defined semantics (throw an exception) and such an execution may be possible if your program has a bug. However, it is never the programmer’s intent for null to flow there. Preventing such bugs is the purpose of the Nullness Checker.)
A method like requireNonNull is useless for making your code correct, but it does have a benefit: its stack trace may help developers to track down the bug. (For users, the stack trace is scary, confusing, and usually non-actionable.) But if you are using the Checker Framework, you can prevent errors rather than needing extra help in debugging the ones that occur at run time.
Another example is the Optional Checker (Chapter 5) and the orElseThrow() method. The goal of the Optional Checker is to ensure that the program does not crash due to use of a non-present Optional value. Therefore, the receiver of orElseThrow() is annotated as @Present, and the Optional Checker issues an error if the client calls orElseThrow() on a @MaybePresent value. (For details, see Section 5.3.)
You can make a checker ignore crashes in library code, such as assertNotNull(), that occur as a result of misuse by your code. This invalidates the checker’s guarantee that your program will not crash. (Programmers and users typically care about all crashes, no matter which method is at the top of the call stack when the exception is thrown.) The checker will still warn you about crashes in your own code.
As a special case, if you want the Nullness Checker to prevent most null pointer exceptions in your code, but to permit null pointer exceptions at nullness assertion methods, you can pass -Astubs=permit-nullness-assertion-exception.astub.
If a method can possibly throw an exception because its parameter is null, then that parameter’s type should be @NonNull, which guarantees that the type-checker will issue a warning for every client use that has the potential to cause an exception. Don’t write @Nullable on the parameter just because there exist some executions that don’t necessarily throw an exception.
An annotation indicates a guarantee that a client can depend upon. A subclass is not permitted to weaken the contract; for example, if a method accepts null as an argument, then every overriding definition must also accept null. A subclass is permitted to strengthen the contract; for example, if a method does not accept null as an argument, then an overriding definition is permitted to accept null.
As a bad example, consider an erroneous @Nullable annotation in com/google/common/collect/Multiset.java:
101 public interface Multiset<E> extends Collection<E> { ... 122 /** 123 * Adds a number of occurrences of an element to this multiset. ... 129 * @param element the element to add occurrences of; may be {@code null} only 130 * if explicitly allowed by the implementation ... 137 * @throws NullPointerException if {@code element} is null and this 138 * implementation does not permit null elements. Note that if {@code 139 * occurrences} is zero, the implementation may opt to return normally. 140 */ 141 int add(@Nullable E element, int occurrences);
There exist implementations of Multiset that permit null elements, and implementations of Multiset that do not permit null elements. A client with a variable Multiset ms does not know which variety of Multiset ms refers to. However, the @Nullable annotation promises that ms.add(null, 1) is permissible. (Recall from Section 2.4.3 that annotations should indicate normal behavior.)
If parameter element on line 141 were to be annotated, the correct annotation would be @NonNull. Suppose a client has a reference to same Multiset ms. The only way the client can be sure not to throw an exception is to pass only non-null elements to ms.add(). A particular class that implements Multiset could declare add to take a @Nullable parameter. That still satisfies the original contract. It strengthens the contract by promising even more: a client with such a reference can pass any non-null value to add(), and may also pass null.
However, the best annotation for line 141 is no annotation at all. The reason is that each implementation of the Multiset interface should specify its own nullness properties when it specifies the type parameter for Multiset. For example, two clients could be written as
class MyNullPermittingMultiset implements Multiset<@Nullable Object> { ... } class MyNullProhibitingMultiset implements Multiset<@NonNull Object> { ... }
or, more generally, as
class MyNullPermittingMultiset<E extends @Nullable Object> implements Multiset<E> { ... } class MyNullProhibitingMultiset<E extends @NonNull Object> implements Multiset<E> { ... }
Then, the specification is more informative, and the Checker Framework is able to do more precise checking, than if line 141 has an annotation.
It is a pleasant feature of the Checker Framework that in many cases, no annotations at all are needed on type parameters such as E in MultiSet.
When you run a type-checker on your code, it is likely to issue warnings or errors. Don’t panic! If you have trouble understanding a Checker Framework warning message, you can search for its text in this manual. There are three general causes for the warnings:
Usually you need to improve the annotations in your source code. Sometimes you need to improve annotations in a library that your program uses (see Chapter 35).
If possible, rewrite your code to be simpler for the checker to analyze; this is likely to make it easier for people to understand, too. If that is not possible, suppress the warning (see Chapter 33); be sure to include a code comment explaining how you know the code is correct even though the type-checker cannot deduce that fact.
Do not add an if test that can never fail, just to suppress a warning. Adding a gratuitous if clutters the code and confuses readers. A reader should assume that every if condition can evaluate to true or false. There is one exception to this rule: an if test may have a condition that you think will never evaluate to true, if its body just throws a descriptive error message.
For each warning issued by the checker, you need to determine which of the above categories it falls into. Here is an effective methodology to do so. It relies mostly on manual code examination, but you may also find it useful to write test cases for your code or do other kinds of analysis, to verify your reasoning. (Also see Section 40.1.4 and Chapter 40, Troubleshooting. In particular, Section 40.1.4 explains this same methodology in different words.)
Write an explanation of why your code is correct and it never suffers the error at run time. In other words, this is an informal proof that the type-checker’s warning is incorrect. Write it in natural language, such as English.
Don’t skip any steps in your proof. (For example, don’t write an unsubstantiated claim such as “x is non-null here”; instead, give a justification.) Don’t let your reasoning rely on facts that you do not write down explicitly. For example, remember that calling a method might change the values of object fields; your proof might need to state that certain methods have no side effects.
If you cannot write a proof, then there is a bug in your code (you should fix the bug) or your code is too complex for you to understand (you should improve its documentation and/or design).
Here are some examples of the translation.
All of these are examples of correcting weaknesses in the annotations you wrote. The Checker Framework provides many other powerful annotations; you may be surprised how many proofs you can express in annotations. If you need to annotate a method that is defined in a library that your code uses, see Chapter 35.
Don’t omit any parts of your proof. When the Checker Framework analyzes a method, it examines only the signature/specification (not the implementation) of other methods.
If there are complex facts in your proof that cannot be expressed as annotations, then that is a weakness in the type-checker. For example, the Nullness Checker cannot express “in list lst, elements stored at even indices are always non-null, but elements stored at odd elements might be null.” In this case, you have two choices. First, you can suppress the warning (Chapter 33); be sure to write a comment explaining your reasoning for suppressing the warning. You may wish to submit a feature request (Section 40.2) asking for annotations that handle your use case. Second, you can rewrite the code to make the proof simpler; in the above example, it might be better to use a list of pairs rather than a heterogeneous list.
At this point, all the steps in your proof have been formalized as annotations. Re-run the checker and repeat the process for any new or remaining warnings.
If every step of your proof can be expressed in annotations, but the checker cannot make one of the deductions (it cannot follow one of the steps), then that is a weakness in the type-checker. First, double-check your reasoning. Then, suppress the warning, along with a comment explaining your reasoning (Chapter 33). The comment is an excerpt from your informal proof, and the proof guides you to the best place to suppress the warning. Please submit a bug report so that the checker can be improved in the future (Section 40.2).
Sometimes, you wish to type-check only part of your program. You might focus on the most mission-critical or error-prone part of your code. When you start to use a checker, you may not wish to annotate your entire program right away. You may not have enough knowledge to annotate poorly-documented libraries that your program uses. Or, the code you are annotating may call into unannotated libraries.
If annotated code uses unannotated code, then the checker may issue warnings. For example, the Nullness Checker (Chapter 3) will warn whenever an unannotated method result is used in a non-null context:
@NonNull myvar = unannotated_method(); // WARNING: unannotated_method may return null
If the call can return null, you should fix the bug in your program by removing the @NonNull annotation in your own program.
If the call never returns null, you have two choices: annotate the library or suppress warnings.
If the Nullness Checker issues no warnings for a given program, then running that program will never throw a null pointer exception. In other words, the Nullness Checker prevents all NullPointerExceptions. See Section 3.1 for more details about the guarantee and what is checked.
The most important annotations supported by the Nullness Checker are @NonNull and @Nullable. @NonNull is rarely written, because it is the default. All of the annotations are explained in Section 3.2.
To run the Nullness Checker, supply the -processor org.checkerframework.checker.nullness.NullnessChecker command-line option to javac. For examples, see Section 3.5.
The NullnessChecker is actually an ensemble of three pluggable type-checkers that work together: the Nullness Checker proper (which is the main focus of this chapter), the Initialization Checker (Section 3.8), and the Map Key Checker (Chapter 4). Their type hierarchies are completely independent, but they work together to provide precise nullness checking.
If the Nullness Checker type-checks your program without errors, then your program will not crash with a NullPointerException that is caused by misuse of null in checked code. Section 2.3 notes some limitations to guarantees made by the Checker Framework.
The checker issues a warning in these cases:
As a special case of an of @NonNull type becoming null, the checker also warns whenever a field of @NonNull type is not initialized in a constructor.
This example illustrates the programming errors that the checker detects:
@Nullable Object obj; // might be null @NonNull Object nnobj; // never null ... obj.toString() // checker warning: dereference might cause null pointer exception nnobj = obj; // checker warning: nnobj may become null if (nnobj == null) // checker warning: redundant test
Parameter passing and return values are checked analogously to assignments.
The Nullness Checker also checks the correctness, and correct use, of initialization (see Section 3.8) and of map key annotations (see Chapter 4).
The checker performs additional checks if certain -Alint command-line options are provided. (See Section 33.7 for more details about the -Alint command-line option.)
By default, the checker forbids calls to those methods, and also special-cases type-checking of calls to System.getProperty() and System.setProperties(). A call to one of these methods can return null in general, but by default the Nullness Checker treats it as returning non-null if the argument is one of the literal strings listed in the documentation of System.getProperties(). To make this behavior sound, the Nullness Checker forbids calls that might clear any built-in property, as described above.
The Nullness Checker uses three separate type hierarchies: one for nullness, one for initialization (Section 3.8), and one for map keys (Chapter 4) The Nullness Checker has four varieties of annotations: nullness type qualifiers, nullness method annotations, initialization type qualifiers, and map key type qualifiers.
The nullness hierarchy contains these qualifiers:
The @NonNull annotation is rarely written in a program, because it is the default (see Section 3.3.2).
The benefit of @MonotonicNonNull over @Nullable is that after a check of a @MonotonicNonNull field, all subsequent accesses within that method can be assumed to be @NonNull, even after arbitrary external method calls that have access to the given field. By contrast, for a @Nullable field, the Nullness Checker assumes that most method calls might set it to null. (Exceptions are calls to methods that are @SideEffectFree or that have an @EnsuresNonNull or @EnsuresNonNullIf annotation.)
A @MonotonicNonNull field may be initialized to null, but the field may not be assigned to null anywhere else in the program. If you supply the noInitForMonotonicNonNull lint flag (for example, supply -Alint=noInitForMonotonicNonNull on the command line), then @MonotonicNonNull fields are not allowed to have initializers at their declarations.
Use of @MonotonicNonNull on a static field is a code smell: it may indicate poor design. You should consider whether it is possible to make the field a member field that is set in the constructor.
In the type system, @MonotonicNonNull is a supertype of @NonNull and a subtype of @Nullable.
Figure 3.1 shows part of the type hierarchy for the Nullness type system. (The annotations exist only at compile time; at run time, Java has no multiple inheritance.)
![]()
Figure 3.1: Partial type hierarchy for the Nullness type system. Java’s Object is expressed as @Nullable Object. Programmers can omit most type qualifiers, because the default annotation (Section 3.3.2) is usually correct. The Nullness Checker verifies three type hierarchies: this one for nullness, one for initialization (Section 3.8), and one for map keys (Chapter 4).
The Nullness Checker supports several annotations that specify method behavior. These are declaration annotations, not type annotations: they apply to the method itself rather than to some particular type.
The Nullness Checker invokes an Initialization Checker, whose annotations indicate whether an object is fully initialized — that is, whether all of its fields have been assigned.
Use of these annotations can help you to type-check more code. Figure 3.5 shows its type hierarchy. For details, see Section 3.8.
indicates that a value is a key for a given map — that is, indicates whether map.containsKey(value) would evaluate to true.
This annotation is checked by a Map Key Checker (Chapter 4) that the Nullness Checker invokes. The @KeyFor annotation enables the Nullness Checker to treat calls to Map.get precisely rather than assuming it may always return null. In particular, a call mymap.get(mykey) returns a non-null value if two conditions are satisfied:
If either of these two conditions is violated, then mymap.get(mykey) has the possibility of returning null.
The command-line argument -AassumeKeyFor makes the Nullness Checker not run the Map Key Checker. The Nullness Checker will unsoundly assume that the argument to Map.get is a key for the receiver map. That is, the second condition above is always considered to be true.
The Nullness Checker adds implicit qualifiers, reducing the number of annotations that must appear in your code (see Section 32.4). For example, enum types are implicitly non-null, so you never need to write @NonNull MyEnumType.
If you want details about implicitly-added nullness qualifiers, see the implementation of NullnessAnnotatedTypeFactory.
Unannotated references are treated as if they had a default annotation. All types default to @NonNull, except that @Nullable is used for casts, locals, instanceof, and implicit bounds (see Section 32.5.3). A user can choose a different defaulting rule by writing a @DefaultQualifier annotation on a package, class, or method. In the example below, fields are defaulted to @Nullable instead of @NonNull.
@DefaultQualifier(value = Nullable.class, locations = TypeUseLocation.FIELD) class MyClass { Object nullableField = null; @NonNull Object nonNullField = new Object(); }
The Nullness Checker supports a form of conditional nullness types, via the @EnsuresNonNullIf method annotations. The annotation on a method declares that some expressions are non-null, if the method returns true (false, respectively).
Consider java.lang.Class. Method Class.getComponentType() may return null, but it is specified to return a non-null value if Class.isArray() is true. You could declare this relationship in the following way (this particular example is already done for you in the annotated JDK that comes with the Checker Framework):
class Class<T> { @EnsuresNonNullIf(expression="getComponentType()", result=true) public native boolean isArray(); public native @Nullable Class<?> getComponentType(); }
A client that checks that a Class reference is indeed that of an array, can then de-reference the result of Class.getComponentType safely without any nullness check. The Checker Framework source code itself uses such a pattern:
if (clazz.isArray()) { // no possible null dereference on the following line TypeMirror componentType = typeFromClass(clazz.getComponentType()); ... }
Another example is Queue.peek and Queue.poll, which return non-null if isEmpty returns false.
The argument to @EnsuresNonNullIf is a Java expression, including method calls (as shown above), method formal parameters, fields, etc.; for details, see Section 32.8. More examples of the use of these annotations appear in the Javadoc for @EnsuresNonNullIf.
Java programs sometimes contain more complex nullness invariants. When these invariants are more complex than handled by the Nullness Checker, you will need to suppress a warning (see Section 3.4).
Suppose that you declare an array to contain non-null elements:
Object [] oa = new Object[10];
(recall that Object means the same thing as @NonNull Object). By default, the Nullness Checker unsoundly permits this code.
To make the Nullness Checker conservatively reject code that may leave a non-null value in an array, use the command-line option -Alint=soundArrayCreationNullness. The option is currently disabled because it makes the checker issue many false positive errors.
With the option enabled, you can write your code to create a nullable or lazy-nonnull array, initialize each component, and then assign the result to a non-null array:
@MonotonicNonNull Object [] temp = new @MonotonicNonNull Object[10]; for (int i = 0; i < temp.length; ++i) { temp[i] = new Object(); } @SuppressWarnings("nullness") // temp array is now fully initialized @NonNull Object [] oa = temp;
Note that the checker is currently not powerful enough to ensure that each array component was initialized. Therefore, the last assignment needs to be trusted: that is, a programmer must verify that it is safe, then write a @SuppressWarnings annotation.
The nullness semantics of Collection.toArray(T[]) cannot be captured by just the nullness type system, though the Nullness Checker contains special-case code to type-check calls to toArray. Therefore, you will probably have to write @SuppressWarnings("nullness") on any overriding definitions of toArray.
The nullness type of the returned array depends on the size of the passed parameter. In particular, the returned array component is of type @NonNull if the following conditions hold:
Additionally, when you supply the -Alint=trustArrayLenZero command-line option, a call to Collection.toArray will be estimated to return an array with a non-null component type if the argument is a field access where the field declaration has a @ArrayLen(0) annotation. This trusts the @ArrayLen(0) annotation, but does not verify it. Run the Constant Value Checker (see Chapter 23) to verify that annotation.
Note: The nullness of the returned array doesn’t depend on the passed array nullness. This is a fact about Collection.toArray(T[]), not a limitation of this heuristic.
When you perform a run-time check for nullness, such as if (x != null) ..., then the Nullness Checker refines the type of x to @NonNull. The refinement lasts until the end of the scope of the test or until x may be side-effected. For more details, see Section 32.7.
It can be tedious to write annotations in your code. Tools exist that can automatically infer annotations and insert them in your source code. (This is different than type qualifier refinement for local variables (Section 32.7), which infers a more specific type for local variables and uses them during type-checking but does not insert them in your source code. Type qualifier refinement is always enabled, no matter how annotations on signatures got inserted in your source code.)
Your choice of tool depends on what default annotation (see Section 3.3.2) your code uses. You only need one of these tools.
When the Nullness Checker reports a warning, it’s best to change the code or its annotations, to eliminate the warning. Alternately, you can suppress the warning, which does not change the code but prevents the Nullness Checker from reporting this particular warning to you.
The Checker Framework supplies several ways to suppress warnings, most notably the @SuppressWarnings("nullness") annotation (see Chapter 33). An example use is
// might return null @Nullable Object getObject(...) { ... } void myMethod() { @SuppressWarnings("nullness") // with argument x, getObject always returns a non-null value @NonNull Object o2 = getObject(x); }
The Nullness Checker supports an additional warning suppression string, nullness:generic.argument. Use of @SuppressWarnings("nullness:generic.argument") causes the Nullness Checker to suppress warnings related to misuse of generic type arguments. One use for this key is when a class is declared to take only @NonNull type arguments, but you want to instantiate the class with a @Nullable type argument, as in List<@Nullable Object>.
The Nullness Checker also permits you to use assertions or method calls to suppress warnings; see below.
Occasionally, it is inconvenient or verbose to use the @SuppressWarnings annotation. For example, Java does not permit annotations such as @SuppressWarnings to appear on statements, expressions, static initializers, etc. Here are three ways to suppress a warning in such cases:
The rest of this section discusses the castNonNull method. It is useful if you wish to suppress a warning within an expression.
The Nullness Checker considers both the return value, and also the argument, to be non-null after the castNonNull method call. The Nullness Checker issues no warnings in any of the following code:
// One way to use castNonNull as a cast: @NonNull String s = castNonNull(possiblyNull1); // Another way to use castNonNull as a cast: castNonNull(possiblyNull2).toString(); // It is possible, but not recommmended, to use castNonNull as a statement: // (It would be better to write an assert statement with @AssumeAssertion // in its message, instead.) castNonNull(possiblyNull3); possiblyNull3.toString();
The castNonNull method throws AssertionError if Java assertions are enabled and the argument is null. However, it is not intended for general defensive programming; see Section 33.2.1.
To use the castNonNull method, the checker-util.jar file must be on the classpath at run time.
The Nullness Checker introduces a new method, rather than re-using an existing method such as org.junit.Assert.assertNotNull(Object) or com.google.common.base.Preconditions.checkNotNull(Object). Those methods are commonly used for defensive programming, so it is impossible to know the programmer’s intent when writing them. Therefore, it is important to have a method call that is used only for warning suppression. See Section 33.2.1 for a discussion of the distinction between warning suppression and defensive programming.
For collection methods with Object formal parameter type, such as contains, indexOf, and remove, the annotated JDK forbids null as an argument.
The reason is that some implementations (like ConcurrentHashMap) throw NullPointerException if null is passed. It would be unsound to permit null, because it could lead to a false negative: the Checker Framework issuing no warning even though a NullPointerException can occur at run time.
However, many other common implementations permit such calls, so some users may wish to sacrifice soundness for a reduced number of false positive warnings. To permit null as an argument to these methods, pass the command-line argument -Astubs=collection-object-parameters-may-be-null.astub.
The JDK contains nullness annotations that preserve the Nullness Checker’s guarantee (see Section 3.1) that your program will not crash with a NullPointerException. In some cases, a formal parameter may be null in some circumstances, but must be non-null in other circumstances, and those circumstances are not expressible using the Nullness Checker’s annotations.
An example is restrictions on collection arguments (see Section 3.4.2).
Another example is this WeakReference constructor:
... * @param q the queue with which the reference is to be registered, * or {@code null} if registration is not required */ public WeakReference(@Nullable T referent, ReferenceQueue<? super T> q) { ...
For some calls, q must be non-null. Therefore, q is annotated as @NonNull (which is the default and need not be explicitly written).
These JDK annotations reflect a verification philosophy: a verification tool finds all possible errors, but it sometimes issues a false positive warning. An alternate philosophy is a bug-finding philosophy: permit all calls that might be correct at run time, but sometimes miss a real error. If you wish to use the Checker Framework with the bug-finding philosophy (though the Checker Framework is still much more thorough than other bug-finders), you can do so by passing the command-line argument -Astubs=sometimes-nullable.astub.
To try the Nullness Checker on a source file that uses the @NonNull qualifier, use the following command (where javac is the Checker Framework compiler that is distributed with the Checker Framework, see Section 38.6 for details):
javac -processor org.checkerframework.checker.nullness.NullnessChecker docs/examples/NullnessExample.java
Compilation will complete without warnings.
To see the checker warn about incorrect usage of annotations (and therefore the possibility of a null pointer exception at run time), use the following command:
javac -processor org.checkerframework.checker.nullness.NullnessChecker docs/examples/NullnessExampleWithWarnings.java
The compiler will issue two warnings regarding violation of the semantics of @NonNull.
Some libraries that are annotated with nullness qualifiers are:
The papers “Practical pluggable types for Java” [PAC+08] (ISSTA 2008, https://homes.cs.washington.edu/~mernst/pubs/pluggable-checkers-issta2008.pdf) and “Building and using pluggable type-checkers” [DDE+11] (ICSE 2011, https://homes.cs.washington.edu/~mernst/pubs/pluggable-checkers-icse2011.pdf) describe case studies in which the Nullness Checker found previously-unknown errors in real software.
Here are some tips about getting started using the Nullness Checker on a legacy codebase. For more generic advice (not specific to the Nullness Checker), see Section 2.4.2. Also see the Checker Framework tutorial, which includes an example of using the Nullness Checker.
Your goal is to add @Nullable annotations to the types of any variables that can be null. (The default is to assume that a variable is non-null unless it has a @Nullable annotation.) Then, you will run the Nullness Checker. Each of its errors indicates either a possible null pointer exception, or a wrong/missing annotation. When there are no more warnings from the checker, you are done!
We recommend that you start by searching the code for occurrences of null in the following locations; when you find one, write the corresponding annotation:
You should ignore all other occurrences of null within a method body. In particular, you rarely need to annotate local variables (except their type arguments or array element types).
Only after this step should you run the Nullness Checker. The reason is that it is quicker to search for places to change than to repeatedly run the checker and fix the errors it tells you about, one at a time.
Here are some other tips:
assert var != null : "@AssumeAssertion(nullness)";
The Checker Framework’s nullness annotations are similar to annotations used in other tools. You might prefer to use the Checker Framework because it has a more powerful analysis that can warn you about more null pointer errors in your code. Most of the other tools are bug-finding tools rather than verification tools, since they give up precision, soundness, or both in favor of being fast and easy to use. Also see Section 39.10.1 for a comparison to other tools.
If your code is already annotated with a different nullness annotation, the Checker Framework can type-check your code. It treats annotations from other tools as if you had written the corresponding annotation from the Nullness Checker, as described in Figures 3.2, 3.3, and 3.4. If the other annotation is a declaration annotation, it may be moved; see Section 39.6.10.
android.annotation.NonNull android.support.annotation.NonNull android.support.annotation.RecentlyNonNull androidx.annotation.NonNull androidx.annotation.RecentlyNonNull com.android.annotations.NonNull com.google.firebase.database.annotations.NotNull com.google.firebase.internal.NonNull com.mongodb.lang.NonNull com.sun.istack.NotNull com.sun.istack.internal.NotNull com.unboundid.util.NotNull edu.umd.cs.findbugs.annotations.NonNull io.micrometer.core.lang.NonNull io.micronaut.core.annotation.NonNull io.reactivex.annotations.NonNull io.reactivex.rxjava3.annotations.NonNull jakarta.annotation.Nonnull javax.annotation.Nonnull javax.validation.constraints.NotNull libcore.util.NonNull lombok.NonNull net.bytebuddy.agent.utility.nullability.NeverNull net.bytebuddy.utility.nullability.NeverNull org.antlr.v4.runtime.misc.NotNull org.checkerframework.checker.nullness.compatqual.NonNullDecl org.checkerframework.checker.nullness.compatqual.NonNullType org.codehaus.commons.nullanalysis.NotNull org.eclipse.jdt.annotation.NonNull org.eclipse.jgit.annotations.NonNull org.eclipse.lsp4j.jsonrpc.validation.NonNull org.jetbrains.annotations.NotNull org.jmlspecs.annotation.NonNull org.jspecify.annotations.NonNull org.jspecify.nullness.NonNull org.netbeans.api.annotations.common.NonNull org.springframework.lang.NonNull reactor.util.annotation.NonNull ⇒ org.checkerframework.checker.nullness.qual.NonNull
Figure 3.2: Correspondence between other nullness annotations and the Checker Framework’s NonNull annotation.
android.annotation.Nullable android.support.annotation.Nullable android.support.annotation.RecentlyNullable androidx.annotation.Nullable androidx.annotation.RecentlyNullable com.android.annotations.Nullable com.beust.jcommander.internal.Nullable com.google.api.server.spi.config.Nullable com.google.firebase.database.annotations.Nullable com.google.firebase.internal.Nullable com.google.gerrit.common.Nullable com.google.protobuf.Internal.ProtoMethodAcceptsNullParameter com.google.protobuf.Internal.ProtoMethodMayReturnNull com.mongodb.lang.Nullable com.sun.istack.Nullable com.sun.istack.internal.Nullable com.unboundid.util.Nullable edu.umd.cs.findbugs.annotations.CheckForNull edu.umd.cs.findbugs.annotations.Nullable edu.umd.cs.findbugs.annotations.PossiblyNull edu.umd.cs.findbugs.annotations.UnknownNullness io.micrometer.core.lang.Nullable io.micronaut.core.annotation.Nullable io.reactivex.annotations.Nullable io.reactivex.rxjava3.annotations.Nullable io.vertx.codegen.annotations.Nullable jakarta.annotation.Nullable javax.annotation.CheckForNull javax.annotation.Nullable junitparams.converters.Nullable libcore.util.Nullable net.bytebuddy.agent.utility.nullability.AlwaysNull net.bytebuddy.agent.utility.nullability.MaybeNull net.bytebuddy.agent.utility.nullability.UnknownNull net.bytebuddy.utility.nullability.AlwaysNull net.bytebuddy.utility.nullability.MaybeNull net.bytebuddy.utility.nullability.UnknownNull org.apache.avro.reflect.Nullable org.apache.cxf.jaxrs.ext.Nullable org.apache.shindig.common.Nullable org.checkerframework.checker.nullness.compatqual.NullableDecl org.checkerframework.checker.nullness.compatqual.NullableType org.codehaus.commons.nullanalysis.Nullable org.eclipse.jdt.annotation.Nullable org.eclipse.jgit.annotations.Nullable org.jetbrains.annotations.Nullable org.jetbrains.annotations.UnknownNullability org.jmlspecs.annotation.Nullable org.jspecify.annotations.Nullable org.jspecify.nullness.Nullable org.jspecify.nullness.NullnessUnspecified org.netbeans.api.annotations.common.CheckForNull org.netbeans.api.annotations.common.NullAllowed org.netbeans.api.annotations.common.NullUnknown org.springframework.lang.Nullable reactor.util.annotation.Nullable ⇒ org.checkerframework.checker.nullness.qual.Nullable
Figure 3.3: Correspondence between other nullness annotations and the Checker Framework’s Nullable annotation.
com.google.protobuf.Internal.ProtoPassThroughNullness ⇒ org.checkerframework.checker.nullness.qual.PolyNull
Figure 3.4: Correspondence between other nullness annotations and the Checker Framework’s PolyNull annotation.
The Checker Framework may issue more or fewer errors than another tool. This is expected, since each tool uses a different analysis. Remember that the Checker Framework aims at soundness: it aims to never miss a possible null dereference, while at the same time limiting false reports. Also, note SpotBugs’s non-standard meaning for @Nullable (Section 3.7.2).
Java permits you to import at most one annotation of a given name. For example, if you use both android.annotation.NonNull and lombok.NonNull in your source code, then you must write at least one of them in fully-qualified form, as @android.annotation.NonNull rather than as @NonNull.
Different tools are appropriate in different circumstances. Section 39.10.1 compares verification tools like the Checker Framework with bug detectors like SpotBugs and Error Prone. In brief, a bug detector is easier to use because it requires fewer annotations, but it misses lots of real bugs that a verifier finds. You should use whichever tool is appropriate for the importance of your code.
You may also choose to use multiple tools, especially since each tool focuses on different types of errors. If you know that you will eventually want to do verification for some particular task (say, nullness checking), there is little point using the nullness analysis of bug detector such as SpotBugs first. It is easier to go straight to using the Checker Framework.
If some other tool discovers a nullness error that the Checker Framework does not, please report it to us (see Section 40.2) so that we can enhance the Checker Framework. For example, SpotBugs might detect an error that the Nullness Checker does not, if you are using an unannotated library (including an unannotated part of the JDK) and running the Checker Framework in an unsound mode (see Section 2.2.2).
FindBugs and SpotBugs have a non-standard definition of @Nullable. This treatment is not documented in its own Javadoc; it is different from the definition of @Nullable in every other tool for nullness analysis; it means the same thing as @NonNull when applied to a formal parameter; and it invariably surprises programmers. Thus, SpotBugs’s @Nullable is detrimental rather than useful as documentation. In practice, your best bet is to not rely on SpotBugs for nullness analysis, even if you find SpotBugs useful for other purposes.
You can skip the rest of this section unless you wish to learn more details.
SpotBugs suppresses all warnings at uses of a @Nullable variable. (You have to use @CheckForNull to indicate a nullable variable that SpotBugs should check.) For example:
// declare getObject() to possibly return null @Nullable Object getObject() { ... } void myMethod() { @Nullable Object o = getObject(); // SpotBugs issues no warning about calling toString on a possibly-null reference! o.toString(); }
The Checker Framework does not emulate this non-standard behavior of SpotBugs, even if the code uses FindBugs/SpotBugs annotations.
With SpotBugs, you annotate a declaration, which suppresses checking at all client uses, even the places that you want to check. It is better to suppress warnings at only the specific client uses where the value is known to be non-null; the Checker Framework supports this, if you write @SuppressWarnings at the client uses. The Checker Framework also supports suppressing checking at all client uses, by writing a @SuppressWarnings annotation at the declaration site. Thus, the Checker Framework supports both use cases, whereas SpotBugs supports only one and gives the programmer less flexibility.
In general, the Checker Framework will issue more warnings than SpotBugs, and some of them may be about real bugs in your program. See Section 3.4 for information about suppressing nullness warnings.
FindBugs and SpotBugs made a poor choice of names. The choice of names should make a clear distinction between annotations that specify whether a reference is null, and annotations that suppress false warnings. The choice of names should also have been consistent for other tools, and intuitively clear to programmers. The FindBugs/SpotBugs choices make the SpotBugs annotations less helpful to people, and much less useful for other tools.
Another problem is that the SpotBugs @Nullable annotation is a declaration annotation rather than a type annotation. This means that it cannot be written in important locations such as type arguments, and it is misleading when written on a field of array type or a method that returns an array.
Overall, it is best to stay away from the SpotBugs nullness annotations and analysis, and use a tool with a more principled design.
Many null pointer exceptions occur because the programmer forgets to check whether a reference is null before dereferencing it. Java 8’s Optional<T> class provides a partial solution: a programmer must call the get method to access the value, and the designers of Optional hope that the syntactic occurrence of the get method will remind programmers to first check that the value is present. This is still easy to forget, however.
The Checker Framework contains an Optional Checker (see Chapter 5) that guarantees that programmers use Optional correctly, such as calling isPresent before calling get.
There are some limitations to the utility of Optional, which might lead to you choose to use regular Java references rather than Optional. (For more details, see the article “Nothing is better than the Optional type”.)
The Nullness Checker does not suffer these limitations. Furthermore, it works with existing code and types, it ensures that you check for null wherever necessary, and it infers when the check for null is not necessary based on previous statements in the method.
Java’s Optional class provides utility routines to reduce clutter when using Optional. The Nullness Checker provides an Opt class that provides all the same methods, but written for regular possibly-null Java references. To use the Opt class, the checker-util.jar file must be on the classpath at run time.
The Initialization Checker determines whether an object is initialized or not. For any object that is not fully initialized, the Nullness Checker treats its fields as possibly-null — even fields annotated as @NonNull. (The Initialization Checker focuses on @NonNull fields, to detect null pointer exceptions when using them. It does not currently ensure that primitives or @Nullable fields are initialized. Use the Initialized Fields Checker (Chapter 26) to check initialization with respect to properties other than nullness.)
Every object’s fields start out as null. By the time the constructor finishes executing, the @NonNull fields have been set to a different value. Your code can suffer a NullPointerException when using a @NonNull field, if your code uses the field during initialization. The Nullness Checker prevents this problem by warning you anytime that you may be accessing an uninitialized field. This check is useful because it prevents errors in your code. However, the analysis can be confusing to understand. If you wish to disable the initialization checks, see Section 3.8.8.
An object is partially initialized from the time that its constructor starts until its constructor finishes. This is relevant to the Nullness Checker because while the constructor is executing — that is, before initialization completes — a @NonNull field may be observed to be null, until that field is set. In particular, the Nullness Checker issues a warning for code like this:
public class MyClass { private @NonNull Object f; public MyClass(int x) { // Error because constructor contains no assignment to this.f. // By the time the constructor exits, f must be initialized to a non-null value. } public MyClass(int x, int y) { // Error because this.f is accessed before f is initialized. // At the beginning of the constructor's execution, accessing this.f // yields null, even though field f has a non-null type. this.f.toString(); f = new Object(); } public MyClass(int x, int y, int z) { m(); f = new Object(); } public void m() { // Error because this.f is accessed before f is initialized, // even though the access is not in a constructor. // When m is called from the constructor, accessing f yields null, // even though field f has a non-null type. this.f.toString(); }
When a field f is declared with a @NonNull type, then code can depend on the fact that the field is not null. However, this guarantee does not hold for a partially-initialized object.
The Initialization Checker uses three annotations to indicate whether an object is initialized (all its @NonNull fields have been assigned), under initialization (its constructor is currently executing), or its initialization state is unknown.
These distinctions are mostly relevant within the constructor, or for references to this that escape the constructor (say, by being stored in a field or passed to a method before initialization is complete). Use of initialization annotations is rare in most code.
![]()
Figure 3.5: Partial type hierarchy for the Initialization type system. @UnknownInitialization and @UnderInitialization each take an optional parameter indicating how far initialization has proceeded, and the right side of the figure illustrates its type hierarchy in more detail.
The initialization hierarchy is shown in Figure 3.5. The initialization hierarchy contains these qualifiers:
@UnknownInitialization takes a parameter that is the class the object is definitely initialized up to. For instance, the type @UnknownInitialization(Foo.class) denotes an object in which every fields declared in Foo or its superclasses is initialized, but other fields might not be. Just @UnknownInitialization is equivalent to @UnknownInitialization(Object.class).
A partially-initialized object (this in a constructor) may be passed to a helper method or stored in a variable; if so, the method receiver, or the field, would have to be annotated as @UnknownInitialization or as @UnderInitialization.
If a reference has @UnknownInitialization or @UnderInitialization type, then all of its @NonNull fields are treated as @MonotonicNonNull: when read, they are treated as being @Nullable, but when written, they are treated as being @NonNull.
The initialization hierarchy is orthogonal to the nullness hierarchy. It is legal for a reference to be @NonNull @UnderInitialization, @Nullable @UnderInitialization, @NonNull @Initialized, or @Nullable @Initialized. The nullness hierarchy tells you about the reference itself: might the reference be null? The initialization hierarchy tells you about the @NonNull fields in the referred-to object: might those fields be temporarily null in contravention of their type annotation? Figure 3.6 contains some examples.
Declarations Expression Expression’s nullness type, or checker error class C { @NonNull Object f; @Nullable Object g; ... }@NonNull @Initialized C a; a @NonNull a.f @NonNull a.g @Nullable @NonNull @UnderInitialization C b; b @NonNull b.f @MonotonicNonNull b.g @Nullable @Nullable @Initialized C c; c @Nullable c.f error: deref of nullable c.g error: deref of nullable @Nullable @UnderInitialization C d; d @Nullable d.f error: deref of nullable d.g error: deref of nullable
Figure 3.6: Examples of the interaction between nullness and initialization. Declarations are shown at the left for reference, but the focus of the table is the expressions and their nullness type or error.
Within the constructor, this starts out with @UnderInitialization type. As soon as all of the @NonNull fields in class C have been initialized, then this is treated as @UnderInitialization(C). This means that this is still being initialized, but all initialization of C’s fields is complete, including all fields of supertypes. Eventually, when all constructors complete, the type is @Initialized.
The Initialization Checker issues an error if the constructor fails to initialize any @NonNull field. This ensures that the object is in a legal (initialized) state by the time that the constructor exits. This is different than Java’s test for definite assignment (see JLS ch.16), which does not apply to fields (except blank final ones, defined in JLS §4.12.4) because fields have a default value of null.
All @NonNull fields must either have a default in the field declaration, or be assigned in the constructor or in a helper method that the constructor calls. If your code initializes (some) fields in a helper method, you will need to annotate the helper method with an annotation such as @EnsuresNonNull({"field1", "field2"}) for all the fields that the helper method assigns.
The most common use for the @UnderInitialization annotation is for a helper routine that is called by constructor. For example:
class MyClass { Object field1; Object field2; Object field3; public MyClass(String arg1) { this.field1 = arg1; init_other_fields(); } // A helper routine that initializes all the fields other than field1. @EnsuresNonNull({"field2", "field3"}) private void init_other_fields(@UnderInitialization(Object.class) MyClass this) { field2 = new Object(); field3 = new Object(); } public MyClass(String arg1, String arg2, String arg3) { this.field1 = arg1; this.field2 = arg2; this.field3 = arg3; checkRep(); } // Verify that the representation invariants are satisfied. // Works as long as the MyClass fields are initialized, even if the receiver's // class is a subclass of MyClass and not all of the subclass fields are initialized. private void checkRep(@UnderInitialization(MyClass.class) MyClass this) { ... } }
At the end of the constructor, this is not fully initialized. Rather, it is @UnderInitialization(CurrentClass.class). The reason is that there might be subclasses with uninitialized fields. The following example illustrates this:
class A { @NonNull String a; public A() { a = ""; // Now, all fields of A are initialized. // However, if this constructor is invoked as part of 'new B()', then // the fields of B are not yet initialized. // If we would type 'this' as @Initialized, then the following call is valid: doSomething(); } void doSomething() {} } class B extends A { @NonNull String b; @Override void doSomething() { // Dereferencing 'b' is ok, because 'this' is @Initialized and 'b' @NonNull. // However, when executing 'new B()', this line throws a null-pointer exception. b.toString(); } }
So far, we have discussed initialization as if it is an all-or-nothing property: an object is non-initialized until initialization completes, and then it is initialized. The full truth is a bit more complex: during the initialization process an object can be partially initialized, and as the object’s superclass constructors complete, its initialization status is updated. The Initialization Checker lets you express such properties when necessary.
Consider a simple example:
class A { Object aField; A() { aField = new Object(); } } class B extends A { Object bField; B() { super(); bField = new Object(); } }
Consider what happens during execution of new B().
At any moment during initialization, the superclasses of a given class can be divided into those that have completed initialization and those that have not yet completed initialization. More precisely, at any moment there is a point in the class hierarchy such that all the classes above that point are fully initialized, and all those below it are not yet initialized. As initialization proceeds, this dividing line between the initialized and uninitialized classes moves down the type hierarchy.
The Nullness Checker lets you indicate where the dividing line is between the initialized and non-initialized classes. The @UnderInitialization(classliteral) indicates the first class that is known to be fully initialized. When you write @UnderInitialization(OtherClass.class) MyClass x;, that means that variable x is initialized for OtherClass and its superclasses, and x is (possibly) uninitialized for subclasses of OtherClass.
The example above lists 4 moments during construction. At those moments, the type of the object being constructed is:
Note that neither @UnderInitialization(A.class) A nor @UnderInitialization(A.class) B may be used where <@Initialized A> is required. Permitting that would be unsound. For example, consider this code, where all types are @NonNull because @NonNull is the default annotation:
class A { Object aField; A() { aField = new Object(); } Object get() { return aField; } } class B extends A { Object bField; B() { super(); bField = new Object(); } @Override Object get() { return bField; } }
Given these declarations:
@Initialized A w; @Initialized B x; @UnderInitialization(A.class) A y; @UnderInitialization(A.class) B z;
the expressions w.get() and x.get() evaluate to a non-null value, but y.get() and z.get() might evaluate to null. (y.get() might evaluate to null because the run-time type of y might be B. That is, y and z might refer to the same value, just as w and x might refer to the same value.)
Consider the following incorrect program.
class A { Object aField; A() { aField = new Object(); process(5); // illegal call } public void process(int arg) { ... } }
The call to process() is not legal. process() is declared to be called on a fully-initialized receiver, which is the default if you do not write a different initialization annotation. At the call to process(), all the fields of A have been set, but this is not fully initialized because fields in subclasses of A have not yet been set. The type of this is @UnderInitialization(A.class), meaning that this is partially-initialized, with the A part of initialization done but the initialization of subclasses not yet complete.
The Initialization Checker output indicates this problem:
Client.java:7: error: [method.invocation] call to process(int) not allowed on the given receiver. process(5); // illegal call ^ found : @UnderInitialization(A.class) A required: @Initialized A
Here is a subclass and client code that crashes with a NullPointerException.
class B extends A { List<Integer> processed; B() { super(); processed = new ArrayList<Integer>(); } @Override public void process(int arg) { super(); processed.add(arg); } } class Client { public static void main(String[] args) { new B(); } }
You can correct the problem in multiple ways.
One solution is to not call methods that can be overridden from the constructor: move the call to process() to after the constructor has completed.
Another solution is to change the declaration of process():
public void process(@UnderInitialization(A.class) A this, int arg) { ... }
If you choose this solution, you will need to rewrite the definition of B.process() so that it is consistent with the declared receiver type.
A non-solution is to prevent subclasses from overriding process() by using final on the method. This doesn’t work because even if process() is not overridden, it might call other methods that are overridden.
As final classes cannot have subclasses, they can be handled more flexibly: once all fields of the final class have been initialized, this is fully initialized.
There is one final aspect of the initialization type system to be considered: the rules governing reading and writing to objects that are currently under initialization (both reading from fields of objects under initialization, as well as writing objects under initialization to fields). By default, only fully-initialized objects can be stored in a field of another object. If this was the only option, then it would not be possible to create circular data structures (such as a doubly-linked list) where fields have a @NonNull type. However, the annotation @NotOnlyInitialized can be used to indicate that a field can store objects that are currently under initialization. In this case, the rules for reading and writing to that field become a little bit more interesting, to soundly support circular structures.
The rules for reading from a @NotOnlyInitialized field are summarized in Figure 3.7. Essentially, nothing is known about the initialization status of the value returned unless the receiver was @Initialized.
x.f f is @NonNull f is @Nullable x is @Initialized @Initialized @NonNull @Initialized @Nullable x is @UnderInitialization @UnknownInitialization @Nullable @UnknownInitialization @Nullable x is @UnknownInitialization @UnknownInitialization @Nullable @UnknownInitialization @Nullable
Figure 3.7: Initialization rules for reading a @NotOnlyInitialized field f.
Similarly, Figure 3.8 shows under which conditions an assignment x.f = y is allowed for a @NotOnlyInitialized field f. If the receiver x is @UnderInitialization, then any y can be of any initialization state. If y is known to be fully initialized, then any receiver is allowed. All other assignments are disallowed.
x.f = y y is @Initialized y is @UnderInitialization y is @UnknownInitialization x is @Initialized yes no no x is @UnderInitialization yes yes yes x is @UnknownInitialization yes no no
Figure 3.8: Rules for deciding when an assignment x.f = y is allowed for a @NotOnlyInitialized field f.
These rules allow for the safe initialization of circular structures. For instance, consider a doubly linked list:
class List<T> { @NotOnlyInitialized Node<T> sentinel; public List() { this.sentinel = new Node<>(this); } void insert(@Nullable T data) { this.sentinel.insertAfter(data); } public static void main() { List<Integer> l = new List<>(); l.insert(1); l.insert(2); } } class Node<T> { @NotOnlyInitialized Node<T> prev; @NotOnlyInitialized Node<T> next; @NotOnlyInitialized List parent; @Nullable T data; // for sentinel construction Node(@UnderInitialization List parent) { this.parent = parent; this.prev = this; this.next = this; } // for data node construction Node(Node<T> prev, Node<T> next, @Nullable T data) { this.parent = prev.parent; this.prev = prev; this.next = next; this.data = data; } void insertAfter(@Nullable T data) { Node<T> n = new Node<>(this, this.next, data); this.next.prev = n; this.next = n; } }
Like any warning, “error: the constructor does not initialize fields: …” indicates that either your annotations are incorrect or your code is buggy. You can fix either the annotations or the code.
Do not initialize the field to an arbitrary non-null value just to eliminate the warning. Doing so degrades your code: it introduces a value that will confuse other programmers, and it converts a clear NullPointerException into a more obscure error.
If your code calls an instance method from a constructor, you may see a message such as the following:
MyFile.java:123: error: call to initHelper() not allowed on the given receiver. initHelper(); ^ found : @UnderInitialization(com.google.Bar.class) @NonNull MyClass required: @Initialized @NonNull MyClass
The problem is that the current object (this) is under initialization, but the receiver formal parameter (Section 39.6.1) of method initHelper() is implicitly annotated as @Initialized. If initHelper() doesn’t depend on its receiver being initialized — that is, it’s OK to call x.initHelper even if x is not initialized — then you can indicate that by using @UnderInitialization or @UnknownInitialization.
class MyClass { void initHelper(@UnknownInitialization MyClass this, String param1) { ... } }
You are likely to want to annotate initHelper() with @EnsuresNonNull as well; see Section 3.2.2.
You may get the “call to … is not allowed on the given receiver” error even if your constructor has already initialized all the fields. For this code:
public class MyClass { @NonNull Object field; public MyClass() { field = new Object(); helperMethod(); } private void helperMethod() { } }
the Nullness Checker issues the following warning:
MyClass.java:7: error: call to helperMethod() not allowed on the given receiver. helperMethod(); ^ found : @UnderInitialization(MyClass.class) @NonNull MyClass required: @Initialized @NonNull MyClass 1 error
The reason is that even though the object under construction has had all the fields declared in MyClass initialized, there might be a subclass of MyClass. Thus, the receiver of helperMethod should be declared as @UnderInitialization(MyClass.class), which says that initialization has completed for all the MyClass fields but may not have been completed overall. If helperMethod had been a public method that could also be called after initialization was actually complete, then the receiver should have type @UnknownInitialization, which is the supertype of @UnknownInitialization and @UnderInitialization.
To suppress most warnings related to partially-initialized values, use the warning suppression string “initialization”. You can write @SuppressWarnings("initialization") on a field, constructor, or class, or pass the command-line argument -AsuppressWarnings=initialization when running the Nullness Checker. (For more about suppressing warnings, see Chapter 33.) You will no longer get a guarantee of no null pointer exceptions, but you can still use the Nullness Checker to find most of the null-pointer problems in your code.
This suppresses warnings that are specific to the Initialization Checker, but does not suppress warnings issued by the Checker Framework itself, such as about assignments or method overriding.
It is not possible to completely disable initialization checking while retaining nullness checking. That is because of an implementation detail of the Nullness and Initialization Checkers: they are actually the same checker, rather than being two separate checkers that are aggregated together.
A method with a non-initialized receiver may assume that a few fields (but not all of them) are non-null, and it sometimes sets some more fields to non-null values. To express these concepts, use the @RequiresNonNull, @EnsuresNonNull, and @EnsuresNonNullIf method annotations; see Section 3.2.2.
The type system enforced by the Initialization Checker is based on “Freedom Before Commitment” [SM11]. Our implementation changes its initialization modifiers (“committed”, “free”, and “unclassified”) to “initialized”, “unknown initialization”, and “under initialization”. Our implementation also has several enhancements. For example, it supports partial initialization (the argument to the @UnknownInitialization and @UnderInitialization annotations). The benefit (in terms of reduced false positive initialization warnings) from supporting partial initialization is greater than the benefit from adopting the Freedom Before Commitment system.
The Map Key Checker tracks which values are keys for which maps. If variable v has type @KeyFor("m")..., then the value of v is a key in Map m. That is, the expression m.containsKey(v) evaluates to true.
Section 3.2.4 describes how @KeyFor annotations enable the Nullness Checker (Chapter 3) to treat calls to Map.get more precisely by refining its result to @NonNull in some cases.
You will not typically run the Map Key Checker. It is automatically run by other checkers, in particular the Nullness Checker.
You can unsoundly suppress warnings related to map keys with @SuppressWarnings("keyfor"), or everywhere by using command-line option -AsuppressWarnings=keyfor; see Chapter 33.
The command-line argument -AassumeKeyFor makes the Map Key Checker unsoundly assume that the argument to Map.get is a key for the receiver map. This is like declaring the Map.get method as V get(Object key) rather than @Nullable V get(Object key). (Just changing the JDK declaration would not work, however, because the Map Key Checker has special-case logic for Map.get. This is different than suppressing warnings, because it changes a method’s return type. This is not the same as assuming that the return value is @NonNull, because the map’s value type might be @Nullable, as in Map<String, @Nullable Integer>.
These qualifiers are part of the Map Key type system:
The following method annotations can be used to establish a method postcondition that ensures that a certain expression is a key for a map:
![]()
Figure 4.1: The subtyping relationship of the Map Key Checker’s qualifiers. @KeyFor(A) is a supertype of @KeyFor(B) if and only if A is a subset of B. Qualifiers in gray are used internally by the type system but should never be written by a programmer.
The qualifier for the type of the null literal is @UnknownKeyFor. If null were @KeyForBottom, that would mean that null is guaranteed to be a key for every map (which is not necessarily true).
Lower bounds are defaulted to @UnknownKeyFor. However, in java.* packages, the default for lower bounds is @KeyForBottom.
It is challenging to choose a default for lower bounds of type variables and wildcards.
Here is a comparison of two choices for lower bounds:
@KeyForBottom default | @UnknownKeyFor default (current choice) |
class MyClass1<@UnknownKeyFor T> { | class MyClass1<T> { |
T var = null; // OK | T var = null; // OK |
class MyClass2<T> { | |
@UnknownKeyFor T var = null; // OK | |
class MyClass3<T> { | |
T var = null; // ERROR | |
class MySet1<T> implements Set<T> { } | |
MySet1<@KeyFor("m") String> s1; // ERROR | |
class Set<E> { } | class Set<@KeyForBottom E> { } |
class MySet2<T> implements Set<T> { } | class MySet2<@KeyForBottom T> implements Set<T> { } |
MySet2<@KeyFor("m") String> s2; // OK | MySet2<@KeyFor("m") String> s2; // OK |
If lower bounds are defaulted to @KeyForBottom (which is not currently the case), then whenever null is assigned to a variable whose type is a type variable, programmers must write an @UnknownKeyFor annotation on either the type variable declaration or on variable declarations, as shown in MyClass1 and MyClass2. A disadvantage of this default is that the Map Key checker may issue warnings in code that has nothing to do with map keys, and in which no map key annotations are used.
If lower bounds are defaulted to @UnknownKeyFor (which is currently the case), then whenever a client might use a @KeyFor type argument, programmers must write a @KeyForBottom annotation on the type parameter, as in MySet2 (and Set).
Under the current defaulting (lower bounds default to @UnknownKeyFor), suppose you write this code:
public class Graph<N> { Map<N, Integer> nodes = new HashMap<>(); } class Client { @Nullable Graph<@KeyFor("g.nodes") String> g; }
The Nullness Checker issues this error message:
Graph.java:14: error: [type.argument] incompatible types in type argument. @Nullable Graph<@KeyFor("g.nodes") String> g; ^ found : @KeyFor("this.g.nodes") String required: [extends @UnknownKeyFor Object super @UnknownKeyFor null]
Note that the upper and lower bounds are both @UnknownKeyFor. You can make the code type-check by writing a lower bound, which is written before the type variable name (Section 31.1.2):
public class Graph<@KeyForBottom N> { ...
The Map Key Checker keeps track of which variables reference keys to which maps. A variable annotated with @KeyFor(mapSet) can only contain a value that is a key for all the maps in mapSet. For example:
Map<String,Date> m, n; @KeyFor("m") String km; @KeyFor("n") String kn; @KeyFor({"m", "n"}) String kmn; km = kmn; // OK - a key for maps m and n is also a key for map m km = kn; // error: a key for map n is not necessarily a key for map m
As with any annotation, use of the @KeyFor annotation may force you to slightly refactor your code. For example, this would be illegal:
Map<String,Object> m; Collection<@KeyFor("m") String> coll; coll.add(x); // error: element type is @KeyFor("m") String, but x does not have that type m.put(x, ...);
The example type-checks if you reorder the two calls:
Map<String,Object> m; Collection<@KeyFor("m") String> coll; m.put(x, ...); // after this statement, x has type @KeyFor("m") String coll.add(x); // OK
Within a method body, you usually do not have to write @KeyFor explicitly (except sometimes on type arguments), because the checker infers it based on usage patterns. When the Map Key Checker encounters a run-time check for map keys, such as “if (m.containsKey(k)) ...”, then the Map Key Checker refines the type of k to @KeyFor("m") within the scope of the test (or until k is side-effected within that scope). The Map Key Checker also infers @KeyFor annotations based on iteration over a map’s key set or calls to put or containsKey. For more details about type refinement, see Section 32.7.
Suppose we have these declarations:
Map<String,Date> m = new Map<>(); String k = "key"; @KeyFor("m") String km;
Ordinarily, the following assignment does not type-check:
km = k; // Error since k is not known to be a key for map m.
The following examples show cases where the Map Key Checker infers a @KeyFor annotation for variable k based on usage patterns, enabling the km = k assignment to type-check.
m.put(k, ...); // At this point, the type of k is refined to @KeyFor("m") String. km = k; // OK if (m.containsKey(k)) { // At this point, the type of k is refined to @KeyFor("m") String. km = k; // OK ... } else { km = k; // Error since k is not known to be a key for map m. ... }
The following example shows a case where the Map Key Checker resets its assumption about the type of a field used as a key because that field may have been side-effected.
class MyClass { private Map<String,Object> m; private String k; // The type of k defaults to @UnknownKeyFor String private @KeyFor("m") String km; public void myMethod() { if (m.containsKey(k)) { km = k; // OK: the type of k is refined to @KeyFor("m") String sideEffectFreeMethod(); km = k; // OK: the type of k is not affected by the method call // and remains @KeyFor("m") String otherMethod(); km = k; // error: At this point, the type of k is once again // @UnknownKeyFor String, because otherMethod might have // side-effected k such that it is no longer a key for map m. } } @SideEffectFree private void sideEffectFreeMethod() { ... } private void otherMethod() { ... } }
Use of the Optional Checker guarantees that your program will not suffer a NoSuchElementException when calling methods on an expression of Optional type. The Optional Checker also enforces Stuart Marks’s style guidelines (see below).
Java 8 introduced the Optional class, a container that is either empty or contains a non-null value.
Using Optional is intended to help programmers remember to check whether data is present or not. However, Optional itself is prone to misuse. The article Nothing is better than the Optional type gives reasons to use regular nullable references rather than Optional. However, if you do use Optional, then the Optional Checker will help you avoid Optional’s pitfalls. Most notably, the Optional Checker guarantees your code will not suffer a NoSuchElementException due to use of an empty Optional.
Stuart Marks gave 7 rules to avoid problems with Optional:
Rule #1 is guaranteed by the Nullness Checker (Chapter 3). Rules #2–#7 are guaranteed by the Optional Checker, described in this chapter. (Exception: Rule #5 is not yet implemented and will be checked by the Optional Checker in the future.)
javac -processor optional MyFile.java ... javac -processor org.checkerframework.checker.optional.OptionalChecker MyFile.java ...
These qualifiers make up the Optional type system:
The subtyping hierarchy of the Optional Checker’s qualifiers is shown in Figure 5.1.
![]()
Figure 5.1: The subtyping relationship of the Optional Checker’s qualifiers.
The Optional Checker supports several annotations that specify method behavior. These are declaration annotations, not type annotations: they apply to the method itself rather than to some particular type.
The Optional Checker guarantees that your code will not throw a NoSuchElementException exception due to use of an absent Optional where a present Optional is needed. More specifically, the Optional Checker will issue an error if you call get or orElseThrow() on a @MaybePresent Optional receiver, because each of these methods throws a NoSuchElementException exception if the receiver is a possibly-absent Optional.
By contrast, the Optional Checker does not issue an error if you call orElseThrow(Supplier) with a possibly-absent Optional. That method call does not throw NoSuchElementException. The Optional Checker assumes that the programmer has mechanisms in place to handle whatever exception it throws. If you wish for the Optional Checker to warn about calling orElseThrow(Supplier) on a possibly-absent Optional, then you can use a stub file (Section 35.5) to annotate its receiver as @Present.
The Optional Checker does not check nullness properties, such as requiring that the argument to of is non-null or guaranteeing that the result of get is non-null. To obtain such a guarantee, run both the Optional Checker and the Nullness Checker (Chapter 3).
As with any checker, the guarantee is subject to certain limitations (see Section 2.3).
It is often best to change the code or annotations when the Optional Checker reports a warning. Alternatively, you might choose to suppress the warning. This does not change the code but prevents the warning from being presented to you.
The Checker Framework supplies several ways to suppress warnings. The @SuppressWarnings("optional") annotation is specific to warnings raised by the Optional Checker. See Chapter 33 for additional usages. An example use is
// might return a possibly-empty Optional Optional<T> wrapWithOptional(...) { ... } void myMethod() { @SuppressWarnings("optional") // with argument x, wrapWithOptional always returns a present Optional @Present Optional<T> optX = wrapWithOptional(x); }
The Optional Checker also permits the use of method calls and assertions to suppress warnings; see immediately below.
Occasionally, it is inconvenient or verbose to use the @SuppressWarnings annotation. For example, Java does not permit annotations such as @SuppressWarnings to appear on statements, expressions, static initializers, etc. Here are three ways to suppress a warning in such cases:
The rest of this section discusses the castPresent method. It is useful if you wish to suppress a warning within an expression.
The Optional Checker considers both the return value, and also the argument, to be an instance of a present Optional after the castPresent method call. The Optional Checker issues no warnings in any of the following code:
// One way to use castPresent as a cast: @Present Optional<String> optString = castPresent(possiblyEmpty1); // Another way to use castPresent as a cast: castPresent(possiblyEmpty2).toString(); // It is possible, but not recommmended, to use castPresent as a statement: // (It would be better to write an assert statement with @AssumeAssertion // in its message, instead.) castPresent(possiblyEmpty3); possiblyEmpty3.toString();
The castPresent method throws AssertionError if Java assertions are enabled and the argument is an empty Optional. However, it is not intended for general defensive programming; see Section 33.2.1.
To use the castPresent method, the checker-util.jar file must be on the classpath at run time.
If the Interning Checker issues no errors for a given program, then all reference equality tests (i.e., all uses of “==”) are proper; that is, == is not misused where equals() should have been used instead.
Interning is a design pattern in which the same object is used whenever two different objects would be considered equal. Interning is also known as canonicalization or hash-consing, and it is related to the flyweight design pattern. Interning has two benefits: it can save memory, and it can speed up testing for equality by permitting use of ==.
The Interning Checker prevents two types of problems in your code. First, it prevents using == on non-interned values, which can result in subtle bugs. For example:
Integer x = new Integer(22); Integer y = new Integer(22); System.out.println(x == y); // prints false!
Second, the Interning Checker helps to prevent performance problems that result from failure to use interning. (See Section 2.3 for caveats to the checker’s guarantees.)
Interning is such an important design pattern that Java builds it in for these types: String, Boolean, Byte, Character, Integer, Short. Every string literal in the program is guaranteed to be interned (JLS §3.10.5), and the String.intern() method performs interning for strings that are computed at run time. The valueOf methods in wrapper classes always (Boolean, Byte) or sometimes (Character, Integer, Short) return an interned result (JLS §5.1.7). Users can also write their own interning methods for other types.
It is a proper optimization to use ==, rather than equals(), whenever the comparison is guaranteed to produce the same result — that is, whenever the comparison is never provided with two different objects for which equals() would return true. Here are three reasons that this property could hold:
To eliminate Interning Checker errors, you will need to annotate the declarations of any expression used as an argument to ==. Thus, the Interning Checker could also have been called the Reference Equality Checker.
To run the Interning Checker, supply the -processor org.checkerframework.checker.interning.InterningChecker command-line option to javac. For examples, see Section 6.5.
These qualifiers are part of the Interning type system:
Two ways to satisfy this annotation are: (1) neither this class nor any of its superclasses overrides the equals method, or (2) this class defines equals with body return this == o;.
![]()
Figure 6.1: Type hierarchy for the Interning type system.
In order to perform checking, you must annotate your code with the @Interned type annotation. A type annotated with @Interned contains the canonical representation of an object:
String s1 = ...; // type is (uninterned) "String" @Interned String s2 = ...; // type is "@Interned String"
The Interning Checker ensures that only interned values can be assigned to s2.
An interned annotation on a class declaration indicates that all objects of a type are interned except for newly created objects. That means that all uses of such types are @Interned by default and the type @UnknownInterned MyClass is an invalid type.
An exception is constructor results. Constructor results and this within the body of the constructor are @UnknownInterned by default. Although @UnknownInterned InternClass is not a legal type, no “type.invalid” error is issued at constructor declarations. Instead, an “interned.object.creation” error is issued at the invocation of the constructor. The user should inspect this location and suppress the warning if the newly created object is interned.
For example:
@Interned class InternedClass { @UnknownInterned InternedClass() { // error, "this" is @UnknownInterned. @Interned InternedClass that = this; } @SuppressWarnings("intern") // Only creation of an InternedClass object. static final InternedClass ONE = new InternedClass(); }
Some interned classes use an intern() method to look up the interned version of the object. These methods must be annotated with the declaration annotation @InternMethod. This allows the checker to verify that a newly created object is immediately interned and therefore not issue an interned object creation error.
new InternedClass().intern() // no error
Because an intern method is expected to be called on uninterned objects, the type of this in intern is implicitly @UnknownInterned. This will cause an error if this is used someplace where an interned object is expected. Some of these warnings will be false positives that should be suppressed by the user.
@InternMethod public InternedClass intern() { // Type of "this" inside an @InternMethod is @UnknownInterned @Interned InternedClass that = this; // error if (!pool.contains(this)) { @SuppressWarnings("interning:assignment") @Interned InternedClass internedThis = this; pool.add(internedThis); } return pool.get(this); }
Some interned classes do not use an intern method to ensure that every object of that class is interned. For these classes, the user will have to manually inspect every constructor invocation and suppress the “interned.object.creation” error.
If every invocation of a constructor is guaranteed to be interned, then the user should annotate the constructor result with @Interned and suppress a warning at the constructor.
@Interned class AnotherInternedClass { // manually verified that all constructor invocations used such that all // new objects are interned @SuppressWarnings("super.invocation") @Interned AnotherInternedClass() {} }
The Interning Checker adds qualifiers to unannotated types, reducing the number of annotations that must appear in your code (see Section 32.4).
For a complete description of all defaulting rules for interning qualifiers, see the Javadoc for InterningAnnotatedTypeFactory.
The @InternedDistinct annotation represents values that are not equals() to any other value. Suppose expression e has type @InternedDistinct. Then e.equals(x) == (e == x). Therefore, it is legal to use == whenever at least one of the operands has type @InternedDistinct.
@InternedDistinct is stronger (more restrictive) than @Interned. For example, consider these variables:
@Interned String i = "22"; String s = new Integer(22).toString();
The variable i is not @InternedDistinct because i.equals(s) is true.
@InternedDistinct is not as restrictive as stating that all objects of a given Java type are interned.
The @InternedDistinct annotation is rarely used, because it arises from coding paradigms that are tricky to reason about. One use is on static fields that hold canonical values of a type. Given this declaration:
class MyType { final static @InternedDistinct MyType SPECIAL = new MyType(...); ... }
it would be legal to write myValue == MyType.SPECIAL rather than myValue.equals(MyType.SPECIAL).
The @InternedDistinct is trusted (not verified), because it would be too complex to analyze the equals() method to ensure that no other value is equals() to a @InternedDistinct value. You will need to manually verify that it is only written in locations where its contract is satisfied. For example, here is one set of guidelines that you could check manually:
Objects of an @Interned type may be safely compared using the “==” operator.
The checker issues an error in two cases:
This example shows both sorts of problems:
Date date; @Interned Date idate; @InternedDistinct Date ddate; ... if (date == idate) ... // error: reference equality test is unsafe idate = date; // error: idate's referent might no longer be interned ddate = idate; // error: idate's referent might be equals() to some other value
The Interning Checker’s warnings look like
MyFile.java:716: error: [interning:not.interned] attempting to use a non-@Interned comparison operand if (date == idate) ^
To resolve a not.interned error, you should change the argument that is passed to ==, or use .equals() instead of ==, or suppress the warning.
The checker also issues a warning when .equals is used where == could be safely used. You can disable this behavior via the javac -Alint=-dotequals command-line option.
For a complete description of all checks performed by the checker, see the Javadoc for InterningVisitor.
To restrict which types the checker should type-check, pass a canonical name (fully-qualified name) using the -Acheckclass option. For example, to find only the interning errors related to uses of String, you can pass -Acheckclass=java.lang.String. The Interning Checker always checks all subclasses and superclasses of the given class.
The Interning Checker conservatively assumes that the Character, Integer, and Short valueOf methods return a non-interned value. In fact, these methods sometimes return an interned value and sometimes a non-interned value, depending on the run-time argument (JLS §5.1.7). If you know that the run-time argument to valueOf implies that the result is interned, then you will need to suppress an error. (The Interning Checker should make use of the Value Checker to estimate the upper and lower bounds on char, int, and short values so that it can more precisely determine whether the result of a given valueOf call is interned.)
To try the Interning Checker on a source file that uses the @Interned qualifier, use the following command:
javac -processor org.checkerframework.checker.interning.InterningChecker docs/examples/InterningExample.java
Compilation will complete without errors or warnings.
To see the checker warn about incorrect usage of annotations, use the following command:
javac -processor org.checkerframework.checker.interning.InterningChecker docs/examples/InterningExampleWithWarnings.java
The compiler will issue an error regarding violation of the semantics of @Interned.
The Daikon invariant detector (http://plse.cs.washington.edu/daikon/) is also annotated with @Interned. From directory java/, run make check-interning.
The paper “Building and using pluggable type-checkers” [DDE+11] (ICSE 2011, https://homes.cs.washington.edu/~mernst/pubs/pluggable-checkers-icse2011.pdf) describes case studies in which the Interning Checker found previously-unknown errors in real software.
The Checker Framework’s interning annotations are similar to annotations used elsewhere.
If your code is already annotated with a different interning annotation, the Checker Framework can type-check your code. It treats annotations from other tools as if you had written the corresponding annotation from the Interning Checker, as described in Figure 6.2. If the other annotation is a declaration annotation, it may be moved; see Section 39.6.10.
com.sun.istack.internal.Interned ⇒ org.checkerframework.checker.interning.qual.Interned
Figure 6.2: Correspondence between other interning annotations and the Checker Framework’s annotations.
The Called Methods Checker tracks the names of methods that have definitely been called on an object. This checker is useful for checking any property of the form “call method A before method B”. For the purpose of this checker, a method has “definitely been called” if it is invoked: a method that might never return or that might throw an exception has definitely been called on every path after the call, including exceptional paths. The checker also assumes that the program is free of null-pointer dereferences. You can verify that all pointer dereferences are safe by running the Nullness Checker (Section 3).
The Called Methods Checker provides built-in support for one such property: that clients of the builder pattern for object construction always provide all required arguments before calling build(). The builder pattern is a flexible and readable way to construct objects, but it is error-prone. Failing to provide a required argument causes a run-time error that manifests during testing or in the field, instead of at compile time as for regular Java constructors. The Called Methods Checker verifies at compile time that your code correctly uses the builder pattern, never omitting a required argument. The Called Methods Checker has built-in support for Lombok (see the caveats about Lombok in Section 7.2) and AutoValue.
You can verify other builders, or verify other properties of the form “foo() must be called before bar()”, by writing method specifications. Section 7.5 describes another example related to a security property.
If the checker issues no warnings, then you have a guarantee that your code supplies all the required information to the builder. The checker might yield a false positive warning when your code is too tricky for it to verify. Please submit an issue if you discover this.
javac -processor calledmethods MyFile.java ... javac -processor org.checkerframework.checker.calledmethods.CalledMethodsChecker MyFile.java ...
The Called Methods Checker supports the following optional command-line arguments:
The Called Methods Checker supports projects that use Lombok via the io.freefair.lombok Gradle plugin automatically. However, note that the checker’s error messages refer to Lombok’s output, which is a variant of your source code that appears in a delombok directory. To fix issues, you should edit your original source code, not the files in the checker’s error messages.
If you use Lombok with a build system other than Gradle, you must configure it to do two tasks. If either of these is not done, the checker will not issue any errors on Lombok code.
The Called Methods Checker reads method specifications (contracts) that state what a method requires when it is called. It warns if method arguments do not satisfy the method’s specification.
If you use AutoValue or Lombok, most specifications are automatically inferred by the Called Methods Checker, from field annotations such as @Nullable and field types such as Optional. Section 7.4 gives defaulting rules for Lombok and AutoValue.
![]()
Figure 7.1: The type hierarchy for the Called Methods type system, for an object with two methods: a() and b(). Types displayed in gray should rarely be written by the programmer.
In some cases, you may need to specify your code. You do so by writing one of the following type annotations (Figure 7.1):
Suppose that the method build is annotated as
class MyObjectBuilder { MyObject build(@CalledMethods({"setX", "setY"}) MyObjectBuilder this) { ... } }
Then the receiver for any call to build() must have had setX() and setY() called on it.
A typical case for Lombok users for manually writing this annotation is when performing extra builder input validation. Performing validation can be done through subclassing generated builders and overriding the generated build() method as follows.
class MyObjectSubBuilder extends MyObjectBuilder { @Override MyObject build(@CalledMethods({"setX", "setY"}) MyObjectSubBuilder this) { MyObject o = super.build(); if ((o.getX() == null) != (o.getY() == null)) { throw new IllegalArgumentException("Nullness for x and y should be equal"); } return o; } }
For example, the annotation @CalledMethodsPredicate("x && y || z") on a type represents objects such that either both the x() and y() methods have been called on the object, or the z() method has been called on the object.
A note on the not operator (!): the annotation @CalledMethodsPredicate("!m") means “it is not true m was definitely called”; equivalently “there is some path on which m was not called”. The annotation @CalledMethodsPredicate("!m") does not mean “m was not called”.
The Called Methods Checker does not have a way of expressing that a method must never be called. You can do unsound bug-finding for such a property by using the ! operator. The Called Methods Checker will detect if the method was always called, but will silently approve the code if the method is called on some but not all paths.
For Lombok users it is important to (manually) add @This to any custom method that calls any other generated builder method. For instance, in the following example, it is important to annotate setXMod42() with @This, since the added method calls setX() (which returns the current builder instance).
class MyObjectBuilder { @This MyObjectBuilder setXMod42(int x) { return setX(x % 42); } }
There are also method annotations:
For example, this specification:
@EnsuresCalledMethods(value = "#1", methods = {"x", "y"}) void m(Param p) { ... }
guarantees that p.x() and p.y() will always be called before m returns. The body of m must satisfy that property, and clients of m can depend on the property.
Sometimes, you need to provide information to enable the Called Methods Checker to verify the property. Consider this example:
@EnsuresCalledMethods(value="#1", methods="close") public void closeSocket(Socket sock) throws IOException { sock.close(); m(); }
If m() might have side-effects (i.e., it is not annotated as @SideEffectFree or @Pure), then the Called Methods Checker issues an error because it cannot make any assumptions about the call to m(), and therefore assumes the worst: that all information it knows about in-scope variables (including that close() was called on sock) is stale and must be discarded. There are two possible fixes:
For example, this specification:
@EnsuresCalledMethodsIf(expression = "#1", methods = {"x", "y"}, result=true) boolean m(Param p) { ... }
guarantees that p.x() and p.y() will always be called if m returns true. The body of m must satisfy that property, and clients of m can depend on the property.
@EnsuresCalledMethods(expression = "#1", methods = {"close"}) @EnsuresCalledMethodsOnException(expression = "#1", methods = {"close"}) void closeIfNonNull(Closeable p) { if (p != null) { p.close(); } }
@EnsuresCalledMethodsOnException can also be used together with the Resource Leak Checker’s @Owning annotation. See 8.4.1.
This section explains how the Called Methods Checker infers types for code that uses the Lombok and AutoValue frameworks. Most readers can skip these details.
You can disable the builder framework support by specifying them in a
comma-separated lowercase list to the command-line flag
disableBuilderFrameworkSupports. For example, to disable both Lombok
and AutoValue support, use:
-ACalledMethodsChecker_disableBuilderFrameworkSupports=autovalue,lombok
The Called Methods Checker automatically assumes default annotations for code that uses builders generated by Lombok and AutoValue. There are three places annotations are usually assumed:
If your program directly defines any of these methods (for example, by adding your own setters to a Lombok builder), you may need to write the annotations manually.
Minor notes/caveats on these rules:
The Called Methods Checker can be used to verify any property of the form “always call A before B”, even if the property is unrelated to builders.
For example, consider the AWS EC2 describeImages API, which clients use during the process of initializing a new cloud instance. CVE-2018-15869 describes how an improperly-configured request to this API can make the requesting client vulnerable to a “machine-image sniping” attack that would allow a malicious third-party to control the operating system image used to initialize the machine. To prevent this attack, clients must specify some trusted source for the image by calling the withOwners or withImageIds methods on the request prior to sending it to AWS. Using a stub file for the describeImages API (DescribeImages.astub), the Called Methods Checker can prove that a client is not vulnerable to such an attack.
To improve precision, you can specify the -ACalledMethodsChecker_useValueChecker command-line option, which instructs the checker to treat provably-safe calls to the withFilters method of a DescribeImagesRequest as equivalent to the withOwners or withImageIds methods.
The paper “Verifying Object Construction” [KRS+20] (ICSE 2020, https://homes.cs.washington.edu/~mernst/pubs/object-construction-icse2020-abstract.html) gives more information about the Called Methods Checker, such as theoretical underpinnings and results of experiments. (The paper uses an earlier name, “Object Construction Checker”.)
The Resource Leak Checker guarantees that the program fulfills every object’s must-call obligations before the object is de-allocated.
A resource leak occurs when a program does not explicitly dispose of some finite underlying resource, such as a socket, file descriptor, or database connection. To dispose of the resource, the program should call some method on an object. (De-allocating or garbage-collecting the object is not sufficient.) For example, the program must call close() on every object that implements the interface java.io.Closeable.
The Resource Leak Checker can check any property of the form “the programmer must call each method in a set of methods M at least once on object O before O is de-allocated”. For resource leaks, by default M is the set containing close() and O is any object that implements the interface java.io.Closeable. You can extend this guarantee to other types and methods by writing @MustCall or @InheritableMustCall annotations, as described in Section 28.1.
The Resource Leak Checker works in three stages:
The paper “Lightweight and Modular Resource Leak Verification” [KSSE21] (ESEC/FSE 2021, https://homes.cs.washington.edu/~mernst/pubs/resource-leak-esecfse2021-abstract.html) gives more details about the Resource Leak Checker.
Run one of these lines:
javac -processor resourceleak MyFile.java ... javac -processor org.checkerframework.checker.resourceleak.ResourceLeakChecker MyFile.java ...
The Resource Leak Checker supports all the command-line arguments listed in Section 7.1 for the Called Methods Checker, plus three others:
If you are running the Resource Leak Checker, then there is no need to run the Must Call Checker (Chapter 28), because the Resource Leak Checker does so automatically.
The Resource Leak Checker relies on the type qualifiers of two other checkers: the Must Call Checker (Section 28.1) and the Called Methods Checker (Section 7.3). You might need to write qualifiers from either type hierarchy. The most common annotations from these checkers that you might need to write are:
The Resource Leak Checker supports annotations that express aliasing patterns related to resource leaks:
The Resource Leak Checker also supports an annotation to permit re-assigning fields or re-opening resources:
Consider the following example of safe use of a Socket, in which the comments indicate the inferred Must Call and Called Methods type qualifiers for s:
{ Socket s = null; // 1. @MustCall({}) @CalledMethodsBottom try { s = new Socket(myHost, myPort); // 2. @MustCall("close") @CalledMethods({}) } catch (Exception e) { // do nothing } finally { if (s != null) { s.close(); // 3. @MustCall("close") @CalledMethods("close") } else { // do nothing // 4. @MustCall("close") @CalledMethodsBottom } // 5. @MustCall("close") @CalledMethods("close") } // 6. @MustCall("close") @CalledMethods("close") }
At point 1, s’s type qualifiers are the type qualifiers of null: null has no must-call obligations (@MustCall({})), and methods cannot be called on it (@CalledMethodsBottom).
At point 2, s is a new Socket object, which has a must-call obligation (@MustCall("close")) and has had no methods called on it (@CalledMethods({})).
At point 3, close() has definitely been called on s, so s’s Called Methods type is updated. Note that the Must Call type does not change.
At point 4, s is definitely null and its type is adjusted accordingly.
At point 5, s’s type is the least upper bound of the types at points 3 and 4.
At point 6, s goes out of scope. The Resource Leak Checker reports a required.method.not.called error if the Must Call set contains any element that the Called Methods set does not.
Resource leak checking is complicated by aliasing. Multiple expressions may evaluate to the same Java object, but each object only needs to be closed once. (Section 8.5 describes a related situation called “resource aliasing”, when multiple Java objects refer to the same underlying resource.)
For example, consider the following code that safely closes a Socket:
void example(String myHost, int myPort) throws IOException { Socket s = new Socket(myHost, myPort); closeSocket(s); } void closeSocket(@Owning @MustCall("close") Socket t) { try { t.close(); } catch (IOException e) { e.printStackTrace(); } }
There are two aliases for a socket object: s in example() and t in closeSocket(). Ordinarily, the Resource Leak Checker requires that close() is called on every expression of type Socket, but that is not necessary here. The Resource Leak Checker should not warn when s goes out of scope in example(), because closeSocket() takes ownership of the socket — that is, closeSocket() takes responsibility for closing it. The @Owning annotation on t’s declaration expresses this fact; it tells the Resource Leak Checker that t is the reference that must be closed, and its alias s need not be closed.
Constructor returns are always @Owning. Method returns default to @Owning, and parameters and fields default to @NotOwning. This treatment of parameter and return types ensures sound handling of unannotated third-party libraries: any object returned from such a library will be tracked by default, and the checker never assumes that passing an object to an unannotated library will satisfy its obligations.
@Owning and @NotOwning always transfer must-call obligations: must-call obligations are conserved (i.e., neither created nor destroyed) by ownership annotations. Writing @Owning or @NotOwning can never make the checker unsound: a real warning can never be hidden by them. As with any annotation, incorrect or missing annotations can lead to false positive warnings.
When @Owning is written on a method parameter, the method only takes ownership of the parameter when it returns normally. In this example, the Resource Leak Checker will report an error in the example method and allow the definition of closeSocket:
void example(String myHost, int myPort) throws Exception { // Error: `s` is not closed on all paths Socket s = new Socket(myHost, myPort); // `closeSocket` does not have to close `s` when it throws IOException. // Instead, this method has to catch the exception and close `s`. closeSocket(s); } void closeSocket(@Owning Socket t) throws IOException { throw new IOException(); }
Sometimes a method really does promise to call some methods on an @Owning parameter, even if it throws an exception. The annotation @EnsuresCalledMethodsOnException can overcome this limitation. For example, a constructor that throws an exception might choose to close an @Owning parameter instead of letting ownership remain with the caller:
@EnsuresCalledMethodsOnException(value = "#1", methods = "close") public Constructor(@Owning Closeable resource) { this.resource = resource; try { initialize(); } catch (Exception e) { resource.close(); throw e; } }
Unannotated fields are treated as non-owning.
For final, non-static owning fields, the Resource Leak Checker enforces the “resource acquisition is initialization (RAII)” programming idiom. Some destructor-like method d() must satisfy the field’s must-call obligation (and this fact must be expressed via a @EnsuresCalledMethods annotation on d()), and the enclosing class must have a @MustCall("d") obligation to ensure the destructor is called. In addition to the @EnsuresCalledMethods annotation, which guarantees that the field(s) it references have their must-call obligations satisfied on non-exceptional paths, the Resource Leak Checker requires those fields to have their must-call obligations satisfied on all paths in (only) the destructor, and will issue a destructor.exceptional.postcondition error if they are not satisfied. Resolve this error by ensuring that the required methods are called on all exceptional paths.
Non-final, non-static owning fields usually require one or more @CreatesMustCallFor annotations when they might be re-assigned. See Section 8.6 for more details on how to annotate a non-final, non-static owning field.
Owning fields are treated slightly differently in constructors versus normal methods. In normal methods, assigning a value to an owning field always satisfies the object’s must-call obligations. However, within a constructor, those obligations are only satisfied if the constructor returns normally. If the constructor throws an exception, the constructed object will not be accessible afterward, and therefore its fields need to be closed before it exits.
This constructor safely closes the object it allocates before throwing an exception:
private final @Owning Socket socket; public ConstructorThatCanThrow() throws IOException { Socket s = new Socket(myHost, myPort); try { initialize(s); // may throw IOException } catch (Exception e) { s.close(); throw e; } this.socket = s; }
An assignment to a static owning field does not satisfy a must-call obligation; for example,
static @Owning PrintWriter debugLog = new PrintWriter("debug.log");
The Resource Leak Checker issues a warning about every assignment of an object with a must-call annotation into a static owning field, indicating that the obligation of the field’s content might not be satisfied. When those fields are used throughout execution, until the program exits, there is no good place to dispose of them, so these warnings might not be useful. The -ApermitStaticOwning command-line argument suppresses warnings related to static owning fields. This can help in checking legacy code. It permits only a small number of resource retained throughout execution, related to the number of such fields and assignments to them.
A resource alias set is a set of Java objects that correspond to the same underlying system resource. Calling a must-call method on any member of a resource-alias set fulfills that obligation for all members of the set. Members of the set may have different Java types.
Programmers most often encounter resource aliasing when using wrapper types. For example, the Java BufferedOutputStream wrapper adds buffering to a delegate stream. The wrapper’s close() method invokes close() on the delegate. Calling close() on either object has the same effect: it closes the underlying resource.
A resource aliasing relationship is expressed in source code via a pair of @MustCallAlias annotations: one on a parameter of a method or constructor, and another on its return type. For example, the annotated JDK contains this constructor of BufferedOutputStream:
@MustCallAlias BufferedOutputStream(@MustCallAlias OutputStream out);
When a pair of @MustCallAlias annotations is written on a method or constructor m’s return type and its parameter p, the Resource Leak Checker requires one of the following:
Here is a complete example of a type InputStreamWrapper that wraps an InputStream as a resource alias. Defining a wrapper type typically involves combined usage of @InheritableMustCall, @EnsuresCalledMethods, an @Owning field, and @MustCallAlias. The test method shows that the checker is able to verify code that releases an InputStream using either the InputStream directly or a wrapping InputStreamWrapper.
@InheritableMustCall("dispose") class InputStreamWrapper { private final @Owning InputStream stream; @MustCallAlias InputStreamWrapper(@MustCallAlias InputStream stream) { this.stream = stream; } @EnsuresCalledMethods(value = "this.stream", methods = "close") public void dispose() throws IOException { this.stream.close(); } /** Shows that either the stream or the wrapper can be closed. */ static void test(@Owning InputStream stream, boolean b) throws IOException { InputStreamWrapper wrapper = new InputStreamWrapper(stream); if (b) { stream.close(); } else { wrapper.dispose(); } } }
Consider a class that has must-call obligations; that is, the class declaration is annotated with @MustCall(...). Every constructor implicitly creates obligations for the newly-created object. Non-constructor methods may also create obligations when re-assigning non-final owning fields or allocating new system-level resources.
A post-condition annotation, @CreatesMustCallFor, indicates for which expression an obligation is created. If you write @CreatesMustCallFor(T) on a method N that overrides a method M, then M must also be annotated as @CreatesMustCallFor(T). (M may also have other @CreatesMustCallFor annotations that N does not.)
@CreatesMustCallFor allows the Resource Leak Checker to verify uses of non-final fields that contain a resource, even if they are re-assigned. Consider the following example:
@MustCall("close") // default qualifier for uses of SocketContainer class SocketContainer { private @Owning Socket sock; public SocketContainer() { sock = ...; } void close() { sock.close() }; @CreatesMustCallFor("this") void reconnect() { if (!sock.isClosed()) { sock.close(); } sock = ...; } }
In the lifetime of a SocketContainer object, sock might be re-assigned arbitrarily many times: once at each call to reconnect(). This code is safe, however: reconnect() ensures that sock is closed before re-assigning it.
Sections 8.6.1 and 8.6.2 explain how the Resource Leak Checker verifies uses and declarations of methods annotated with @CreatesMustCallFor.
At a call site to a method annotated as @CreatesMustCallFor(expr), the Resource Leak Checker:
Treating the obligation before the call as satisfied is sound: the checker creates a new obligation for calls to @CreatesMustCallFor methods, and the Must Call Checker (Chapter 28) ensures the @MustCall type for the target expression will have a superset of any methods present before the call. Intuitively, calling an @CreatesMustCallFor method “resets” the obligations of the target expression, so whether they were satisfied before the call or not is irrelevant.
If an @CreatesMustCallFor method n is invoked within a method m that has an @CreatesMustCallFor annotation, and the @CreatesMustCallFor annotations on n and m have the same target expression—imposing the obligation produced by calling n on the caller of m—then the newly-created obligation is treated as satisfied immediately at the call-site of n in the body of m (because it is imposed at call-sites of m instead).
Any method that re-assigns a non-final, owning field of some object obj must be annotated @CreatesMustCallFor("obj"). Other methods may also be annotated with @CreatesMustCallFor.
The Resource Leak Checker enforces two rules to ensure that re-assignments to non-final, owning fields (like sock in method reconnect above) are sound:
The first rule ensures that close() is called after the last call to reconnect(), and the second rule ensures that reconnect() safely closes sock before re-assigning it. Because the Called Methods Checker treats calls to an @CreatesMustCallFor method like reconnect() as if the call might cause arbitrary side-effects, after such a call the only method known to have been definitely called is the @CreatesMustCallFor method: previous called methods (including close()) do not appear in the @CalledMethods type qualifier.
The Resource Leak Checker checks that an element’s must-call obligations are fulfilled when that element may go out of scope: at the end of its lexical scope or when control may be transferred to the end of its lexical scope, such as via a break or continue statement or via throwing an exception. As an example of an exception, consider the following method:
void foo() { Socket s = ...; bar(); s.close(); }
If bar is declared to throw an exception, the Resource Leak Checker warns that a Socket may be leaked. If bar throws an exception, the only reference to s is lost, which could lead to a resource leak.
The Resource Leak Checker ignores control flow due to some exceptions.
The set of ignored exception types can be controlled with the option -AresourceLeakIgnoredExceptions=.... The option takes a comma-separated list of fully-qualified exception types. A type can be prefixed with = to ignore exactly that type and not its subclasses. For example, for a very pedantic set of ignored exceptions use:
-AresourceLeakIgnoredExceptions=java.lang.Error, =java.lang.NullPointerException
which ignores java.lang.Error (and all its subclasses) as well as java.lang.NullPointerException (but not its subclasses).
The keyword default will expand to the default set of ignored exceptions. So, to add an additional exception to the set of ignored exceptions, use:
-AresourceLeakIgnoredExceptions=default,package.MyCustomException
The Resource Leak Checker warns about re-assignments to owning fields, because the value that was overwritten might not have had its obligations satisfied. Such a warning is not necessary on the first assignment to a field, since the field had no content before the assignment. Sometimes, the Resource Leak Checker is unable to determine that an assignment is the first one, so it conservatively assumes the assignment is a re-assignment and issues an error.
One way to prevent this false positive warning is to declare the field as final.
Alternately, to suppress all warnings related to field assignments in the constructor and in initializer blocks, pass the -ApermitInitializationLeak command-line argument. This makes the checker unsound: the Resource Leak Checker will not warn if the constructor and initializers set a field more than once. The amount of leakage is limited to how many times the field is set.
The Resource Leak Checker issues a required.method.not.known error when a variable with the type @MustCallUnknown has a must call obligation. @MustCallUnknown rarely occurs, but if you encounter this error usually the right thing to do is to write an explicit @MustCall annotation on the indicated expression (e.g., as a cast), because the Must Call Checker will only use @MustCallUnknown as a default when encountering a language feature that it is unable to reason about.
The Resource Leak Checker cannot verify code that stores a collection of resources in a generic collection (e.g., java.util.List) and then resolves the obligations of each element of the collection at once (e.g., by iterating over the List). In the future, the checker will support this. The remainder of this section explains the implementation issues; most users can skip it.
The first implementation issue is that @Owning and @NotOwning are declaration annotations rather than type qualifiers, so they cannot be written on type arguments. It is possible under the current design to have an @Owning List<Socket>, but not a List<@Owning Socket>. It would be better to make @Owning a type annotation, but this is a challenging design problem.
The second implementation issue is the defaulting rule for @MustCall on type variable upper bounds. Currently, this default is @MustCall({}), which prevents many false positives in code with type variables that makes no use of resources — an important design principle. However, this defaulting rule does have an unfortunate consequence: it is an error to write List<Socket> or any other type with a concrete type argument where the type argument itself isn’t @MustCall({}). A programmer who needs to write such a type while using the Resource Leak Checker has a few choices, all of which have some downsides:
The recommended way to use the Resource Leak Checker in this situation is to rewrite the code to avoid a List of owning resources. If rewriting is not possible, the programmer will probably need to suppress a warning and then verify the code using a method other than the Resource Leak Checker.
The Resource Leak Checker uses a specialized algorithm to infer annotations when whole program inference (WPI, Section 34.2) is enabled. The algorithm is described in the paper “Inference of Resource Management Specifications” [SGT+23] (OOPSLA 2023, https://homes.cs.washington.edu/~mernst/pubs/resource-inference-oopsla2023-abstract.html).
The Fake Enum Checker, or Fenum Checker, enables you to define a type alias or typedef, in which two different sets of values have the same representation (the same Java type) but are not allowed to be used interchangeably. It is also possible to create a typedef using the Subtyping Checker (Chapter 29), and that approach is sometimes more appropriate.
One common use for the Fake Enum Checker is the fake enumeration pattern (Section 9.6). For example, consider this code adapted from Android’s IntDef documentation:
@NavigationMode int NAVIGATION_MODE_STANDARD = 0; @NavigationMode int NAVIGATION_MODE_LIST = 1; @NavigationMode int NAVIGATION_MODE_TABS = 2; @NavigationMode int getNavigationMode(); void setNavigationMode(@NavigationMode int mode);
The Fake Enum Checker can issue a compile-time warning if the programmer ever tries to call setNavigationMode with an int that is not a @NavigationMode int.
The Fake Enum Checker gives the same safety guarantees as a true enumeration type or typedef, but retaining backward-compatibility with interfaces that use existing Java types. You can apply fenum annotations to any Java type, including all primitive types and also reference types. Thus, you could use it (for example) to represent floating-point values between 0 and 1, or Strings with some particular characteristic. (Note that the Fake Enum Checker does not let you create a shorter alias for a long type name, as a real typedef would if Java supported it.)
As explained in Section 9.1, you can either define your own fenum annotations, such as @NavigationMode above, or you can use the @Fenum type qualifier with a string argument. Figure 9.1 shows part of the type hierarchy for the Fenum type system.
![]()
Figure 9.1: Partial type hierarchy for the Fenum type system. There are two forms of fake enumeration annotations — above, illustrated by @Fenum("A") and @FenumC. See Section 9.1 for descriptions of how to introduce both types of fenums. The type qualifiers in gray (@FenumTop, @FenumUnqualified, and @FenumBottom) should never be written in source code; they are used internally by the type system. @FenumUnqualified is the default qualifier for unannotated types, except for upper bounds which default to @FenumTop.
The Fake Enum Checker supports two ways to introduce a new fake enum (fenum):
package myPackage.qual; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.checkerframework.checker.fenum.qual.FenumTop; import org.checkerframework.framework.qual.SubtypeOf; @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) @SubtypeOf(FenumTop.class) public @interface MyFenum {}
You only need to adapt the italicized package, annotation, and file names in the example.
Note that all custom annotations must have the @Target({ElementType.TYPE_USE}) meta-annotation. See section 36.5.1.
The first approach allows you to define a short, meaningful name suitable for your project, whereas the second approach allows quick prototyping.
The Fenum Checker ensures that unrelated types are not mixed. All types with a particular fenum annotation, or @Fenum(...) with a particular String argument, are disjoint from all unannotated types and from all types with a different fenum annotation or String argument.
The checker ensures that only compatible fenum types are used in comparisons and arithmetic operations (if applicable to the annotated type).
It is the programmer’s responsibility to ensure that fields with a fenum type are properly initialized before use. Otherwise, one might observe a null reference or zero value in the field of a fenum type. (The Nullness Checker (Chapter 3) can prevent failure to initialize a reference variable.)
The Fenum Checker can be invoked by running the following commands.
javac -classpath /full/path/to/myProject/bin:/full/path/to/myLibrary/bin \ -processor org.checkerframework.checker.fenum.FenumChecker \ -Aquals=myPackage.qual.MyFenum MyFile.java ...
The annotations listed in -Aquals must be accessible to the compiler during compilation. Before you run the Fenum Checker with javac, they must be compiled and on the same path (the classpath or processorpath) as the Checker Framework. It is not sufficient to supply their source files on the command line.
You can also provide the fully-qualified paths to a set of directories that contain the annotations through the -AqualDirs option, using a colon-no-space-separated notation. For example, if the Checker Framework is on the classpath rather than the processorpath:
javac -classpath /full/path/to/myProject/bin:/full/path/to/myLibrary/bin \ -processor org.checkerframework.checker.fenum.FenumChecker \ -AqualDirs=/full/path/to/myProject/bin:/full/path/to/myLibrary/bin MyFile.java ...
Note that in these two examples, the compiled class file of the myPackage.qual.MyFenum annotation must exist in either the myProject/bin directory or the myLibrary/bin directory. The following placement of the class file will work with the above commands:
.../myProject/bin/myPackage/qual/MyFenum.class
The two options can be used at the same time to provide groups of annotations from directories, and individually named annotations.
javac -processor org.checkerframework.checker.fenum.FenumChecker MyFile.java ...
For an example of running the Fake Enum Checker on Android code, see https://github.com/karlicoss/checker-fenum-android-demo.
One example of when you need to suppress warnings is when you initialize the fenum constants to literal values. To remove this warning message, add a @SuppressWarnings annotation to either the field or class declaration, for example:
@SuppressWarnings("fenum:assignment") // initialization of fake enums class MyConsts { public static final @Fenum("A") int ACONST1 = 1; public static final @Fenum("A") int ACONST2 = 2; }
The following example introduces two fenums in class TestStatic and then performs a few typical operations.
@SuppressWarnings("fenum:assignment") // initialization of fake enums public class TestStatic { public static final @Fenum("A") int ACONST1 = 1; public static final @Fenum("A") int ACONST2 = 2; public static final @Fenum("B") int BCONST1 = 4; public static final @Fenum("B") int BCONST2 = 5; } class FenumUser { @Fenum("A") int state1 = TestStatic.ACONST1; // ok @Fenum("B") int state2 = TestStatic.ACONST1; // Incompatible fenums forbidden! void fenumArg(@Fenum("A") int p) {} void fenumTest() { state1 = 4; // Direct use of value forbidden! state1 = TestStatic.BCONST1; // Incompatible fenums forbidden! state1 = TestStatic.ACONST2; // ok fenumArg(5); // Direct use of value forbidden! fenumArg(TestStatic.BCONST1); // Incompatible fenums forbidden! fenumArg(TestStatic.ACONST1); // ok } }
Also, see the example project in the docs/examples/fenum-extension directory.
The paper “Building and using pluggable type-checkers” [DDE+11] (ICSE 2011, https://homes.cs.washington.edu/~mernst/pubs/pluggable-checkers-icse2011.pdf) describes case studies of the Fake Enum Checker.
Java’s enum keyword lets you define an enumeration type: a finite set of distinct values that are related to one another but are disjoint from all other types, including other enumerations. Before enums were added to Java, there were two ways to encode an enumeration, both of which are error-prone:
Sometimes you need to use the fake enum pattern, rather than a real enum or the typesafe enum pattern. One reason is backward-compatibility. A public API that predates Java’s enum keyword may use int constants; it cannot be changed, because doing so would break existing clients. For example, Java’s JDK still uses int constants in the AWT and Swing frameworks, and Android also uses int constants rather than Java enums. Another reason is performance, especially in environments with limited resources. Use of an int instead of an object can reduce code size, memory requirements, and run time.
In cases when code has to use the fake enum pattern, the Fake Enum Checker, or Fenum Checker, gives the same safety guarantees as a true enumeration type. The developer can introduce new types that are distinct from all values of the base type and from all other fake enums. Fenums can be introduced for primitive types as well as for reference types.
The Lock Checker prevents certain concurrency errors by enforcing a locking discipline. A locking discipline indicates which locks must be held when a given operation occurs. You express the locking discipline by declaring a variable’s type to have the qualifier @GuardedBy("lockexpr"). This indicates that the variable’s value may be dereferenced only if the given lock is held.
To run the Lock Checker, supply the -processor org.checkerframework.checker.lock.LockChecker command-line option to javac. The -AconcurrentSemantics command-line option is always enabled for the Lock Checker (see Section 39.4.6).
The Lock Checker gives the following guarantee. Suppose that expression e has type @GuardedBy({"x", "y.z"}). Then the value computed for e is only dereferenced by a thread when the thread holds locks x and y.z. Dereferencing a value is reading or writing one of its fields. The guarantee about e’s value holds not only if the expression e is dereferenced directly, but also if the value was first copied into a variable, returned as the result of a method call, etc. Copying a reference is always permitted by the Lock Checker, regardless of which locks are held.
A lock is held if it has been acquired but not yet released. Java has two types of locks. A monitor lock is acquired upon entry to a synchronized method or block, and is released on exit from that method or block. An explicit lock is acquired by a method call such as Lock.lock(), and is released by another method call such as Lock.unlock(). The Lock Checker enforces that any expression whose type implements Lock is used as an explicit lock, and all other expressions are used as monitor locks.
Ensuring that your program obeys its locking discipline is an easy and effective way to eliminate a common and important class of errors. If the Lock Checker issues no warnings, then your program obeys its locking discipline. However, your program might still have other types of concurrency errors. For example, you might have specified an inadequate locking discipline because you forgot some @GuardedBy annotations. Your program might release and re-acquire the lock, when correctness requires it to hold it throughout a computation. And, there are other concurrency errors that cannot, or should not, be solved with locks.
This section describes the lock annotations you can write on types and methods.
The @GuardedBy annotation can list multiple expressions, as in @GuardedBy({"expr1", "expr2"}), in which case the dereference is permitted only if the thread holds all the locks.
Section 32.8 explains which expressions the Lock Checker is able to analyze as lock expressions. These include <self>, the value of the annotated reference (non-primitive) variable. For example, @GuardedBy("<self>") Object o indicates that the value referenced by o is guarded by the intrinsic (monitor) lock of the value referenced by o.
@GuardedBy({}), which means the value is always allowed to be dereferenced, is the default type qualifier that is used for all locations where the programmer does not write an explicit locking type qualifier (except all CLIMB-to-top locations other than upper bounds and exception parameters — see Section 32.5.3). (Section 10.5.4 discusses this choice.) It is also the conservative default type qualifier for method parameters in unannotated libraries (see Chapter 35).
Any value can be assigned to a variable of type @GuardedByUnknown. In particular, if it is written on a formal parameter, then any value, including one whose locks are not currently held, may be passed as an argument.
@GuardedByUnknown is the conservative default type qualifier for method receivers in unannotated libraries (see Chapter 35).
![]()
Figure 10.1: The subtyping relationship of the Lock Checker’s qualifiers. @GuardedBy({}) is the default type qualifier for unannotated types (except all CLIMB-to-top locations other than upper bounds and exception parameters — see Section 32.5.3).
Figure 10.1 shows the type hierarchy of these qualifiers. All @GuardedBy annotations are incomparable: if exprSet1 ≠ exprSet2, then @GuardedBy(exprSet1) and @GuardedBy(exprSet2) are siblings in the type hierarchy. You might expect that @GuardedBy({"x", "y"}) T is a subtype of @GuardedBy({"x"}) T. The first type requires two locks to be held, and the second requires only one lock to be held and so could be used in any situation where both locks are held. The type system conservatively prohibits this in order to prevent type-checking loopholes that would result from aliasing and side effects — that is, from having two mutable references, of different types, to the same data. See Section 10.4.2 for an example of a problem that would occur if this rule were relaxed.
As with other qualifier-polymorphism annotations (Section 31.2), the index argument indicates when two values are guarded by the same (unknown) set of locks.
@GuardSatisfied is only allowed in method signatures: on formal parameters (including the receiver) and return types. It may not be written on fields. Also, it is a limitation of the current design that @GuardSatisfied may not be written on array elements or on local variables.
A return type can only be annotated with @GuardSatisfied(index), not @GuardSatisfied.
See Section 10.4.6 for an example of a use of @GuardSatisfied.
The Lock Checker supports several annotations that specify method behavior. These are declaration annotations, not type annotations: they apply to the method itself rather than to some particular type.
Since @SideEffectFree implies @LockingFree, if both are applicable then you only need to write @SideEffectFree.
This is the default for methods being type-checked that have no @LockingFree, @MayReleaseLocks, @SideEffectFree, or @Pure annotation.
In addition to the standard subtyping rules enforcing the subtyping relationship described in Figure 10.1, the Lock Checker enforces the following additional rules.
The overall rules for polymorphic qualifiers are given in Section 31.2.
Here are additional constraints for (pseudo-)assignments:
If a formal parameter type is annotated with @GuardSatisfied without an index, then that formal parameter type is unrelated to every other type in the @GuardedBy hierarchy, including other occurrences of @GuardSatisfied without an index.
@GuardSatisfied may not be used on formal parameters, receivers, or return types of a method annotated with @MayReleaseLocks.
Primitive types, boxed primitive types (such as java.lang.Integer), and type java.lang.String are annotated with @GuardedBy({}). It is an error for the programmer to annotate any of these types with an annotation from the @GuardedBy type hierarchy, including @GuardedBy({}).
@MayReleaseLocks @ReleasesNoLocks @LockingFree @SideEffectFree @Pure
The Lock Checker guarantees that a value that was computed from an expression of @GuardedBy type is dereferenced only when the current thread holds all the expressions in the @GuardedBy annotation.
The following example demonstrates the basic type-checking rules.
class MyClass { final ReentrantLock lock; // Initialized in the constructor @GuardedBy("lock") Object x = new Object(); @GuardedBy("lock") Object y = x; // OK, since dereferences of y will require "lock" to be held. @GuardedBy({}) Object z = x; // ILLEGAL since dereferences of z don't require "lock" to be held. @GuardedBy("lock") Object myMethod() { // myMethod is implicitly annotated with @ReleasesNoLocks. return x; // OK because the return type is annotated with @GuardedBy("lock") } [...] void exampleMethod() { x.toString(); // ILLEGAL because the lock is not known to be held y.toString(); // ILLEGAL because the lock is not known to be held myMethod().toString(); // ILLEGAL because the lock is not known to be held lock.lock(); x.toString(); // OK: the lock is known to be held y.toString(); // OK: the lock is known to be held, and toString() is annotated with @SideEffectFree. myMethod().toString(); // OK: the lock is known to be held, since myMethod // is implicitly annotated with @ReleasesNoLocks. } }
Note that the expression new Object() is inferred to have type @GuardedBy("lock") because it is immediately assigned to a newly-declared variable having type annotation @GuardedBy("lock"). You could explicitly write new @GuardedBy("lock") Object() but it is not required.
The following example demonstrates that using <self> as a lock expression allows a guarded value to be dereferenced even when the original variable name the value was originally assigned to falls out of scope.
class MyClass { private final @GuardedBy("<self>") Object x = new Object(); void method() { x.toString(); // ILLEGAL because x is not known to be held. synchronized(x) { x.toString(); // OK: x is known to be held. } } public @GuardedBy("<self>") Object get_x() { return x; // OK, since the return type is @GuardedBy("<self>"). } } class MyOtherClass { void method() { MyClass m = new MyClass(); final @GuardedBy("<self>") Object o = m.get_x(); o.toString(); // ILLEGAL because o is not known to be held. synchronized(o) { o.toString(); // OK: o is known to be held. } } }
@GuardedBy(exprSet)
The following example demonstrates the reason the Lock Checker enforces the following rule: if exprSet1 ≠ exprSet2, then @GuardedBy(exprSet1) and @GuardedBy(exprSet2) are siblings in the type hierarchy.
class MyClass { final Object lockA = new Object(); final Object lockB = new Object(); @GuardedBy("lockA") Object x = new Object(); @GuardedBy({"lockA", "lockB"}) Object y = new Object(); void myMethod() { y = x; // ILLEGAL; if legal, later statement x.toString() would cause trouble synchronized(lockA) { x.toString(); // dereferences y's value without holding lock lockB } } }
If the Lock Checker permitted the assignment y = x;, then the undesired dereference would be possible.
The following example shows the interaction between @GuardedBy and @Holding:
void helper1(@GuardedBy("myLock") Object a) { a.toString(); // ILLEGAL: the lock is not held synchronized(myLock) { a.toString(); // OK: the lock is held } } @Holding("myLock") void helper2(@GuardedBy("myLock") Object b) { b.toString(); // OK: the lock is held } void helper3(@GuardedBy("myLock") Object d) { d.toString(); // ILLEGAL: the lock is not held } void myMethod2(@GuardedBy("myLock") Object e) { helper1(e); // OK to pass to another routine without holding the lock // (but helper1's body has an error) e.toString(); // ILLEGAL: the lock is not held synchronized (myLock) { helper2(e); // OK: the lock is held helper3(e); // OK, but helper3's body has an error } }
@EnsuresLockHeld and @EnsuresLockHeldIf are primarily intended for annotating JDK locking methods, as in:
package java.util.concurrent.locks; class ReentrantLock { @EnsuresLockHeld("this") public void lock(); @EnsuresLockHeldIf(expression="this", result=true) public boolean tryLock(); ... }
They can also be used to annotate user methods, particularly for higher-level lock constructs such as a Monitor, as in this simplified example:
public class Monitor { private final ReentrantLock lock; // Initialized in the constructor ... @EnsuresLockHeld("lock") public void enter() { lock.lock(); } ... }
@LockingFree is useful when a method does not make any use of synchronization or locks but causes other side effects (hence @SideEffectFree is not appropriate). @SideEffectFree implies @LockingFree, therefore if both are applicable, you should only write @SideEffectFree. @ReleasesNoLocks has a weaker guarantee than @LockingFree, and @MayReleaseLocks provides no guarantees.
private Object myField; private final ReentrantLock lock; // Initialized in the constructor private @GuardedBy("lock") Object x; // Initialized in the constructor [...] // This method does not use locks or synchronization, but it cannot // be annotated as @SideEffectFree since it alters myField. @LockingFree void myMethod() { myField = new Object(); } @SideEffectFree int mySideEffectFreeMethod() { return 0; } @MayReleaseLocks void myUnlockingMethod() { lock.unlock(); } @ReleasesNoLocks void myLockingMethod() { lock.lock(); } @MayReleaseLocks void clientMethod() { if (lock.tryLock()) { x.toString(); // OK: the lock is held myMethod(); x.toString(); // OK: the lock is still held since myMethod is locking-free mySideEffectFreeMethod(); x.toString(); // OK: the lock is still held since mySideEffectFreeMethod is side-effect-free myUnlockingMethod(); x.toString(); // ILLEGAL: myUnlockingMethod may have released a lock } if (lock.tryLock()) { x.toString(); // OK: the lock is held myLockingMethod(); x.toString(); // OK: the lock is held } if (lock.isHeldByCurrentThread()) { x.toString(); // OK: the lock is known to be held } }
The polymorphic @GuardSatisfied type annotation allows a method body to dereference the method’s formal parameters even if the @GuardedBy annotations on the actual parameters are unknown at the method declaration site.
The declaration of StringBuffer.append(String str) is annotated as:
@LockingFree public @GuardSatisfied(1) StringBuffer append(@GuardSatisfied(1) StringBuffer this, @GuardSatisfied(2) String str)
The method manipulates the values of its arguments, so all their locks must be held. However, the declaration does not know what those are and they might not even be in scope at the declaration. Therefore, the declaration cannot use @GuardedBy and must use @GuardSatisfied. The arguments to @GuardSatisfied indicate that the receiver and result (which are the same value) are guarded by the same (unknown, possibly empty) set of locks, and the str parameter may be guarded by a different set of locks.
The @LockingFree annotation indicates that this method makes no use of locks or synchronization.
Given these annotations on append, the following code type-checks:
final ReentrantLock lock1, lock2; // Initialized in the constructor @GuardedBy("lock1") StringBuffer filename; @GuardedBy("lock2") StringBuffer extension; ... lock1.lock(); lock2.lock(); filename = filename.append(extension);
This section gives some details that are helpful for understanding how Java locking and the Lock Checker works.
The paper “Locking discipline inference and checking” [ELM+16] (ICSE 2016, https://homes.cs.washington.edu/~mernst/pubs/locking-inference-checking-icse2016-abstract.html) gives additional details.
Java provides two types of locking: monitor locks and explicit locks.
You should not mix the two varieties of locking, and the Lock Checker enforces this. To prevent an object from being used both as a monitor and an explicit lock, the Lock Checker issues a warning if a synchronized(E) block’s expression E has a type that implements Lock.
Whereas Java locking is defined in terms of values, Java programs are written in terms of expressions. We say that a lock expression is held if the value to which the expression currently evaluates is held.
The Lock Checker conservatively estimates the expressions that are held at each point in a program. The Lock Checker does not track aliasing (different expressions that evaluate to the same value); it only considers the exact expression used to acquire a lock to be held. After any statement that might side-effect a held expression or a lock expression, the Lock Checker conservatively considers the expression to be no longer held.
Section 32.8 explains which Java expressions the Lock Checker is able to analyze as lock expressions.
The @LockHeld and @LockPossiblyHeld type qualifiers are used internally by the Lock Checker and should never be written by the programmer. If you see a warning mentioning @LockHeld or @LockPossiblyHeld, please contact the Checker Framework developers as it is likely to indicate a bug in the Checker Framework.
When you perform a run-time check for locking, such as if (explicitLock.isHeldByCurrentThread()){...} or if (Thread.holdsLock(monitorLock)){...}, then the Lock Checker considers the lock expression to be held within the scope of the test. For more details, see Section 32.7.
The default qualifier for unannotated types is @GuardedBy({}). This default forces you to write explicit @GuardSatisfied in method signatures in the common case that clients ensure that all locks are held.
It might seem that @GuardSatisfied would be a better default for method signatures, but such a default would require even more annotations. The reason is that @GuardSatisfied cannot be used on fields. If @GuardedBy({}) is the default for fields but @GuardSatisfied is the default for parameters and return types, then getters, setters, and many other types of methods do not type-check without explicit lock qualifiers.
A programmer might choose to use the @Holding method annotation in two different ways: to specify correctness constraints for a synchronization protocol, or to summarize intended usage. Both of these approaches are useful, and the Lock Checker supports both.
@Holding can specify a synchronization protocol that is not expressible as locks over the parameters to a method. For example, a global lock or a lock on a different object might need to be held. By requiring locks to be held, you can create protocol primitives without giving up the benefits of the annotations and checking of them.
@Holding can be a method summary that simplifies reasoning. In this case, the @Holding doesn’t necessarily introduce a new correctness constraint; the program might be correct even if the lock were not already acquired.
Rather, here @Holding expresses a fact about execution: when execution reaches this point, the following locks are known to be already held. This fact enables people and tools to reason intra- rather than inter-procedurally.
In Java, it is always legal to re-acquire a lock that is already held, and the re-acquisition always works. Thus, whenever you write
@Holding("myLock") void myMethod() { ... }
it would be equivalent, from the point of view of which locks are held during the body, to write
void myMethod() { synchronized (myLock) { // no-op: re-acquire a lock that is already held ... } }
It is better to write a @Holding annotation rather than writing the extra synchronized block. Here are reasons:
The Checker Framework’s lock annotations are similar to annotations used elsewhere.
If your code is already annotated with a different lock annotation, the Checker Framework can type-check your code. It treats annotations from other tools as if you had written the corresponding annotation from the Lock Checker, as described in Figure 10.2. If the other annotation is a declaration annotation, it may be moved; see Section 39.6.10.
net.jcip.annotations.GuardedBy javax.annotation.concurrent.GuardedBy ⇒ org.checkerframework.checker.lock.qual.GuardedBy (for fields) or …Holding (for methods)
Figure 10.2: Correspondence between other lock annotations and the Checker Framework’s annotations.
The book Java Concurrency in Practice [GPB+06] defines a @GuardedBy annotation that is the inspiration for ours. The book’s @GuardedBy serves two related but distinct purposes:
The Lock Checker renames the method annotation to @Holding, and it generalizes the @GuardedBy annotation into a type annotation that can apply not just to a field but to an arbitrary type (including the type of a parameter, return value, local variable, generic type parameter, etc.). Another important distinction is that the Lock Checker’s annotations express and enforce a locking discipline over values, just like the JLS expresses Java’s locking semantics; by contrast, JCIP’s annotations express a locking discipline that protects variable names and does not prevent race conditions. This makes the annotations more expressive and also more amenable to automated checking. It also accommodates the distinct meanings of the two annotations, and resolves ambiguity when @GuardedBy is written in a location that might apply to either the method or the return type.
(The JCIP book gives some rationales for reusing the annotation name for two purposes. One rationale is that there are fewer annotations to learn. Another rationale is that both variables and methods are “members” that can be “accessed” and @GuardedBy creates preconditions for doing so. Variables can be accessed by reading or writing them (putfield, getfield), and methods can be accessed by calling them (invokevirtual, invokeinterface). This informal intuition is inappropriate for a tool that requires precise semantics.)
The Lock Checker validates some uses of locks, but not all. It would be possible to enrich it with additional annotations. This would increase the programmer annotation burden, but would provide additional guarantees.
Lock ordering: Specify that one lock must be acquired before or after another, or specify a global ordering for all locks. This would prevent deadlock.
Not-holding: Specify that a method must not be called if any of the listed locks are held.
These features are supported by Clang’s thread-safety analysis.
The Index Checker warns about potentially out-of-bounds accesses to sequence data structures, such as arrays and strings.
The Index Checker prevents IndexOutOfBoundsExceptions that result from an index expression that might be negative or might be equal to or larger than the sequence’s length. It also prevents NegativeArraySizeExceptions that result from a negative array dimension in an array creation expression. (A caveat: the Index Checker does not check for arithmetic overflow. If an expression overflows, the Index Checker might fail to warn about a possible exception. This is unlikely to be a problem in practice unless you have an array whose length is Integer.MAX_VALUE.)
The programmer can write annotations that indicate which expressions are indices for which sequences. The Index Checker prohibits any operation that may violate these properties, and the Index Checker takes advantage of these properties when verifying indexing operations. Typically, a programmer writes few annotations, because the Index Checker infers properties of indexes from the code around them. For example, it will infer that x is positive within the then block of an if (x > 0) statement. The programmer does need to write field types and method pre-conditions or post-conditions. For instance, if a method’s formal parameter is used as an index for myArray, the programmer might need to write an @IndexFor("myArray") annotation on the formal parameter’s types.
The Index Checker checks fixed-size data structures, whose size is never changed after creation. A fixed-size data structure has no add or remove operation. Examples are strings and arrays, and you can add support for other fixed-size data structures (see Section 11.9).
To run the Index Checker, run either of these commands:
javac -processor index MyJavaFile.java javac -processor org.checkerframework.checker.index.IndexChecker MyJavaFile.java
Recall that in Java, type annotations are written before the type; in particular, array annotations appear immediately before “[]”. Here is how to declare a length-9 array of positive integers:
@Positive int @ArrayLen(9) []
Multi-dimensional arrays are similar. Here is how to declare a length-2 array of length-4 arrays:
String @ArrayLen(2) [] @ArrayLen(4) []
The paper “Lightweight Verification of Array Indexing” (ISSTA 2018, https://homes.cs.washington.edu/~mernst/pubs/array-indexing-issta2018-abstract.html) gives more details about the Index Checker. “Enforcing correct array indexes with a type system” [San16] (FSE 2016) describes an earlier version.
Internally, the Index Checker computes information about integers that might be indices:
and about sequence lengths:
The Index Checker checks of all these properties at once, but this manual discusses each type system in a different section. There are some annotations that are shorthand for writing multiple annotations, each from a different type system:
class String { char charAt(@IndexFor("this") int index) { ... } }
More generally, a variable declared as @IndexFor("someArray") int i has type @IndexFor("someArray") int and its run-time value is guaranteed to be non-negative and less than the length of someArray. You could also express this as @NonNegative @LTLengthOf("someArray") int i, but @IndexFor("someArray") int i is more concise.
For example, the Arrays.fill method is declared as
class Arrays { void fill(Object[] a, @IndexFor("#1") int fromIndex, @IndexOrHigh("#1") int toIndex, Object val) }
The Index Checker issues an error when a sequence is indexed by an integer that might be negative. The Lower Bound Checker uses a type system (Figure 11.1) with the following qualifiers:
![]()
Figure 11.1: The two type hierarchies for integer types used by the Index Checker. On the left is a type system for lower bounds. On the right is a type system for upper bounds. Qualifiers written in gray should never be written in source code; they are used internally by the type system.
In the Upper Bound type system, subtyping rules depend on both the array name ("myArray", in the figure) and on the offset (which is 0, the default, in the figure). Another qualifier is @UpperBoundLiteral, whose subtyping relationships depend on its argument and on offsets for other qualifiers.
The Index Checker issues an error when a sequence index might be too high. To do this, it maintains information about which expressions are safe indices for which sequences. The length of a sequence is arr.length for arrays and str.length() for strings. It uses a type system (Figure 11.1) with the following qualifiers:
It issues an error when a sequence arr is indexed by an integer that is not of type @LTLengthOf("arr") or @LTOMLengthOf("arr").
@LTLengthOf takes an optional offset element, meaning that the annotated expression plus the offset is less than the length of the given sequence. For example, suppose expression e has type @LTLengthOf(value = {"a", "b"}, offset = {"-1", "x"}). Then e - 1 is less than a.length, and e + x is less than b.length. This helps to make the checker more precise. Programmers rarely need to write the offset element.
@LTEqLengthOf({"a"}) = @LTLengthOf(value={"a"}, offset=-1), and
@LTEqLengthOf(value={"a"}, offset=x) = @LTLengthOf(value={"a"},
offset=x-1) for any x.
This type exists to allow the checker to infer the safety of loops of the form:
for (int i = 0; i < array.length - 1; ++i) { arr[i] = arr[i+1]; }
This annotation should rarely (if ever) be written by the programmer; usually @LTLengthOf(String[] names) should be written instead. @LTOMLengthOf({"a", "b"}) is a subtype of both @LTOMLengthOf("a") and @LTOMLengthOf("b"). (“LTOM” stands for “Less Than One Minus”, because another way of saying “at least 2 less than a.length” is “less than a.length-1”.)
@LTOMLengthOf({"a"}) = @LTLengthOf(value={"a"}, offset=1), and
@LTOMLengthOf(value={"a"}, offset=x) = @LTLengthOf(value={"a"},
offset=x+1) for any x.
The following method annotations can be used to establish a method postcondition that ensures that a certain expression is a valid index for a sequence:
There is one declaration annotation that indicates the relationship between two sequences:
For example, to indicate that shorter is a subsequence of longer:
int start; int end; int[] shorter; @HasSubsequence(value="shorter", from="this.start", to="this.end") int[] longer;
Thus, a valid index into shorter is also a valid index (between start and end-1 inclusive) into longer. More generally, if x is @IndexFor("shorter") in the example above, then start + x is @IndexFor("longer"). If y is @IndexFor("longer") and @LessThan("end"), then y - start is @IndexFor("shorter"). Finally, end - start is @IndexOrHigh("shorter").
This annotation is in part checked and in part trusted. When an array is assigned to longer, three facts are checked: that start is non-negative, that start is less than or equal to end, and that end is less than or equal to the length of longer. This ensures that the indices are valid. The programmer must manually verify that the value of shorter equals the subsequence that they describe.
The Index Checker estimates, for each sequence expression, how long its value might be at run time by computing a minimum length that the sequence is guaranteed to have. This enables the Index Checker to verify indices that are compile-time constants. For example, this code:
String getThirdElement(String[] arr) { return arr[2]; }
is legal if arr has at least three elements, which can be indicated in this way:
String getThirdElement(String @MinLen(3) [] arr) { return arr[2]; }
When the index is not a compile-time constant, as in arr[i], then the Index Checker depends not on a @MinLen annotation but on i being annotated as @LTLengthOf("arr").
The MinLen type qualifier is implemented in practice by the Constant Value Checker, using @ArrayLenRange annotations (see Chapter 23). This means that errors related to the minimum lengths of arrays must be suppressed using the "value" argument to @SuppressWarnings. @ArrayLenRange and @ArrayLen annotations can also be used to establish the minimum length of a sequence, if a more precise estimate of length is known. For example, if arr is known to have exactly three elements:
String getThirdElement(String @ArrayLen(3) [] arr) { return arr[2]; }
The following type qualifiers (from Chapter 23) can establish the minimum length of a sequence:
![]()
Figure 11.2: The type hierarchy for arrays of equal length ("a" and "b" are assumed to be in-scope sequences). Qualifiers written in gray should never be written in source code; they are used internally by the type system.
The following method annotation can be used to establish a method postcondition that ensures that a certain sequence has a minimum length:
The Index Checker determines whether two or more sequences have the same length. This enables it to verify that all the indexing operations are safe in code like the following:
boolean lessThan(double[] arr1, double @SameLen("#1") [] arr2) { for (int i = 0; i < arr1.length; i++) { if (arr1[i] < arr2[i]) { return true; } else if (arr1[i] > arr2[i]) { return false; } } return false; }
When needed, you can specify which sequences have the same length using the following type qualifiers (Figure 11.2):
The JDK’s Arrays.binarySearch method returns either where the value was found, or a negative value indicating where the value could be inserted. The Search Index Checker represents this concept.
![]()
Figure 11.3: The type hierarchy for the Index Checker’s internal type system that captures information about the results of calls to Arrays.binarySearch.
The Search Index Checker’s type hierarchy (Figure 11.3) has four type qualifiers:
~
) to an expression of this type produces an expression
of type @IndexOrHigh.
The methods String.indexOf and String.lastIndexOf return an index of a given substring within a given string, or -1 if no such substring exists. The index i returned from receiver.indexOf(substring) satisfies the following property, which is stated here in three equivalent ways:
i == -1 || ( i >= 0 && i <= receiver.length() - substring.length() ) i == -1 || ( @NonNegative && @LTLengthOf(value="receiver", offset="substring.length()-1") ) @SubstringIndexFor(value="receiver", offset="substring.length()-1")
The return type of methods String.indexOf and String.lastIndexOf has the annotation @SubstringIndexFor(value="this", offset="#1.length()-1")). This allows writing code such as the following with no warnings from the Index Checker:
public static String removeSubstring(String original, String removed) { int i = original.indexOf(removed); if (i != -1) { return original.substring(0, i) + original.substring(i + removed.length()); } return original; }
![]()
Figure 11.4: The type hierarchy for the Substring Index Checker, which captures information about the results of calls to String.indexOf and String.lastIndexOf.
The @SubstringIndexFor annotation is implemented in a Substring Index Checker that runs together with the Index Checker and has its own type hierarchy (Figure 11.4) with three type qualifiers:
No other annotation supported by the Index Checker precisely represents the possible return values of methods String.indexOf and String.lastIndexOf. The reason is the methods’ special cases for empty strings and for failed matches.
Consider the result i of receiver.indexOf(substring):
The last annotation in the list above, @LTLengthOf(value = "receiver", offset = "substring.length()-1"), is the correct and precise upper bound for all values of i except -1. The offset expresses the fact that we can add substring.length() to this index and still get a valid index for receiver. That is useful for type-checking code that adds the length of the substring to the found index, in order to obtain the rest of the string. However, the upper bound applies only after the index is explicitly checked not to be -1:
int i = receiver.indexOf(substring); // i is @GTENegativeOne and @LTEqLengthOf("receiver") // i is not @LTLengthOf(value = "receiver", offset = "substring.length()-1") if (i != -1) { // i is @NonNegative and @LTLengthOf(value = "receiver", offset = "substring.length()-1") int j = i + substring.length(); // j is @IndexOrHigh("receiver") return receiver.substring(j); // this call is safe }
The property of the result of indexOf cannot be expressed by any combination of lower-bound (Section 11.2) and upper-bound (Section 11.3) annotations, because the upper-bound annotations apply independently of the lower-bound annotations, but in this case, the upper bound i <= receiver.length() - substring.length() holds only if i >= 0. Therefore, to express this property and make the example type-check without false positives, a new annotation such as @SubstringIndexFor(value = "receiver", offset = "substring.length()-1") is necessary.
The Index Checker estimates which expression’s values are less than other expressions’ values.
The Index Checker has built-in support for Strings and arrays. You can add support for additional fixed-size data structures by writing annotations. This allows the Index Checker to typecheck the data structure’s implementation and to typecheck uses of the class.
This section gives an example: a fixed-length collection.
/** ArrayWrapper is a fixed-size generic collection. */ public class ArrayWrapper<T> { private final Object @SameLen("this") [] delegate; @SuppressWarnings("index") // constructor creates object of size @SameLen(this) by definition ArrayWrapper(@NonNegative int size) { delegate = new Object[size]; } public @LengthOf("this") int size() { return delegate.length; } public void set(@IndexFor("this") int index, T obj) { delegate[index] = obj; } @SuppressWarnings("unchecked") // required for normal Java compilation due to unchecked cast public T get(@IndexFor("this") int index) { return (T) delegate[index]; } }
The Index Checker treats methods annotated with @LengthOf("this") as the length of a sequence like arr.length for arrays and str.length() for strings.
With these annotations, client code like the following typechecks with no warnings:
public static void clearIndex1(ArrayWrapper<? extends Object> a, @IndexFor("#1") int i) { a.set(i, null); } public static void clearIndex2(ArrayWrapper<? extends Object> a, int i) { if (0 <= i && i < a.size()) { a.set(i, null); } }
The Tainting Checker prevents certain kinds of trust errors. A tainted, or untrusted, value is one that comes from an arbitrary, possibly malicious source, such as user input or unvalidated data. In certain parts of your application, using a tainted value can compromise the application’s integrity, causing it to crash, corrupt data, leak private data, etc.
For example, a user-supplied pointer, handle, or map key should be validated before being dereferenced. As another example, a user-supplied string should not be concatenated into a SQL query, lest the program be subject to a SQL injection attack. A location in your program where malicious data could do damage is called a sensitive sink.
A program must “sanitize” or “untaint” an untrusted value before using it at a sensitive sink. There are two general ways to untaint a value: by checking that it is innocuous/legal (e.g., it contains no characters that can be interpreted as SQL commands when pasted into a string context), or by transforming the value to be legal (e.g., quoting all the characters that can be interpreted as SQL commands). A correct program must use one of these two techniques so that tainted values never flow to a sensitive sink. The Tainting Checker ensures that your program does so.
If the Tainting Checker issues no warning for a given program, then no tainted value ever flows to a sensitive sink. However, your program is not necessarily free from all trust errors. As a simple example, you might have forgotten to annotate a sensitive sink as requiring an untainted type, or you might have forgotten to annotate untrusted data as having a tainted type.
To run the Tainting Checker, supply the -processor TaintingChecker or -processor org.checkerframework.checker.tainting.TaintingChecker command-line option to javac.
The Tainting type system uses the following annotations:
Most programs are designed with a boundary that surrounds sensitive computations, separating them from untrusted values. Outside this boundary, the program may manipulate malicious values, but no malicious values ever pass the boundary to be operated upon by sensitive computations.
In some programs, the area outside the boundary is very small: values are sanitized as soon as they are received from an external source. In other programs, the area inside the boundary is very small: values are sanitized only immediately before being used at a sensitive sink. Either approach can work, so long as every possibly-tainted value is sanitized before it reaches a sensitive sink.
Once you determine the boundary, annotating your program is easy: put @Tainted outside the boundary, @Untainted inside, and @SuppressWarnings("tainting") at the validation or sanitization routines that are used at the boundary.
The Tainting Checker’s standard default qualifier is @Tainted (see Section 32.5 for overriding this default). This is the safest default, and the one that should be used for all code outside the boundary (for example, code that reads user input). You can set the default qualifier to @Untainted in code that may contain sensitive sinks.
The Tainting Checker does not know the intended semantics of your program, so it cannot warn you if you mis-annotate a sensitive sink as taking @Tainted data, or if you mis-annotate external data as @Untainted. So long as you correctly annotate the sensitive sinks and the places that untrusted data is read, the Tainting Checker will ensure that all your other annotations are correct and that no undesired information flows exist.
As an example, suppose that you wish to prevent SQL injection attacks. You would start by annotating the Statement class to indicate that the execute operations may only operate on untainted queries (Chapter 35 describes how to annotate external libraries):
public boolean execute(@Untainted String sql) throws SQLException; public boolean executeUpdate(@Untainted String sql) throws SQLException;
The @Tainted and @Untainted annotations have only minimal built-in semantics. In fact, the Tainting Checker provides only a small amount of functionality beyond the Subtyping Checker (Chapter 29). This lack of hard-coded behavior has two consequences. The first consequence is that the annotations can serve many different purposes, such as:
The second consequence is that the Tainting Checker is not useful unless you annotate the appropriate sources, sinks, and untainting/sanitization routines.
If you want more specialized semantics, or you want to annotate multiple types of tainting (for example, HTML and SQL) in a single program, then you can copy the definition of the Tainting Checker to create a new annotation and checker with a more specific name and semantics. You will change the copy to rename the annotations, and you will annotate libraries and/or your code to identify sources, sinks, and validation/sanitization routines. See Chapter 36 for more details.
Misuse of polymorphism can lead to unsoundness with the Tainting Checker and other similar information flow checkers. To understand the potential problem, consider the append function in java.lang.StringBuffer:
public StringBuffer append(StringBuffer this, String toAppend);
Given these declarations:
@Tainted StringBuffer tsb; @Tainted String ts; @Untainted StringBuffer usb; @Untainted String us;
both of these invocations should be legal:
tsb.append(ts); usb.append(us);
That suggests that perhaps the function should be annotated as polymorphic:
// UNSOUND annotation -- do not do this! public @PolyTainted StringBuffer append(@PolyTainted StringBuffer this, @PolyTainted String toAppend);
The problem with the above annotation is that it permits the undesirable invocation:
usb.append(ts); // illegal invocation
This invocation is permitted because, in the expression, all @PolyTainted annotations on formal parameters are instantiated to @Tainted, the top annotation, and each argument is a subtype of the corresponding formal parameter.
Beware this problem both in code you write, and also in annotated libraries (such as stub files). The correct way to annotate this class is to add a class qualifier parameter; see Section 31.3.
(Side note: if append were purely functional (had no side effects and returned a new StringBuffer) the method call would be acceptable, because the return type is instantiated to @Tainted StringBuffer for the expression usb.append(ts). However, the append method works via side-effect, and only returns a reference to the buffer as a convenience for writing “fluent” client code.)
The SQL Quotes Checker helps prevent SQL injection vulnerabilities. Many SQL injection attacks involve malicious user input containing unescaped special characters such as the single quote, which can change whether subsequent concatenations are interpreted as SQL command code or as SQL query values.
The SQL Quotes Checker helps classifies Strings by the parity of unescaped single quotes. Furthermore, it marks user-supplied Strings as unsafe for SQL query use, as their quoting is unknown; the program must sanitize or validate all such values before concatenating them to a SQL query for execution. Sanitization can be done by quoting user input as necessary to ensure special characters are properly escaped. The SQL Quotes Checker follows ANSI standard and considers doubled-up single quotes as one escaped single quote. (Some databases support the use of the backslash as an escape character for single quotes, but this is not safe for all SQL database implementations.)
The SQL Quotes Checker guarantees that no unchecked values are passed to a SQL query execution statement. It does not to guarantee that SQL queries written by the code author are safe for execution; e.g., the checker will not warn at compile time about syntax errors or improper uses of DELETE or DROP statements.
When possible, it is better to use a prepared statement or other mechanisms that automatically handle quoting without the need for the programmer to do any work. The SQL Quotes Checker is useful for the large quantity of code that still uses string concatenation to create SQL queries.
To run the SQL Quotes Checker, supply the -processor SqlQuotesChecker or -processor org.checkerframework.checker.sqlquotes.SqlQuotesChecker command-line option to javac.
The SQL Quotes type system uses the following annotations:
The subtyping hierarchy of the SQL Quotes Checker’s qualifiers is shown in Figure 13.1.
![]()
Figure 13.1: The subtyping relationship of the SQL Quotes Checker’s qualifiers. The type qualifiers are applicable to Strings. Qualifiers in gray are used internally by the type system but should never be written by a programmer.
Concatenation of @SqlEvenQuotes and @SqlOddQuotes Strings parallels integer parity over addition. In other words, concatenation of @SqlOddQuotes and @SqlEvenQuotes Strings are most narrowly typed as follows:
@SqlOddQuotes + @SqlOddQuotes = @SqlEvenQuotes @SqlOddQuotes + @SqlEvenQuotes = @SqlOddQuotes @SqlEvenQuotes + @SqlOddQuotes = @SqlOddQuotes @SqlEvenQuotes + @SqlEvenQuotes = @SqlEvenQuotes
The programmer needs to write trusted annotations on SQL-related methods. This is already done for you, for commonly-used libraries such as java.sql and org.springframework.security.crypto.bcrypt. (If you annotate other libraries, please contribute them to the Checker Framework project. Thanks!)
A SQL query execution method such as Statement.executeQuery must be annotated to take a @SqlEvenQuotes string as a parameter.
A sanitization or quoting method should be annotated to take a @SqlQuotesUnknown parameter and to return a @SqlEvenQuotes result.
Given such annotations, the type system will issue an error if an un-sanitized value can ever flow into a database query. (You might need to write some annotations in your own code as well.)
The Regex Checker prevents, at compile-time, use of syntactically invalid regular expressions and access of invalid capturing groups.
A regular expression, or regex, is a pattern for matching certain strings of text. In Java, a programmer writes a regular expression as a string. The syntax of regular expressions is complex, so it is easy to make a mistake. It is also easy to accidentally use a regex feature from another language that is not supported by Java (see section “Comparison to Perl 5” in the Pattern Javadoc). These problems cause run-time errors.
Regular expressions in Java also have capturing groups, which are delimited by parentheses and allow for extraction from text. If a programmer uses an incorrect index (larger than the number of capturing groups), an IndexOutOfBoundsException is thrown.
The Regex Checker warns about these problems at compile time, guaranteeing that your program does not crash due to incorrect use of regular expressions.
For further details, including case studies, see the paper “A type system for regular expressions” [SDE12] (FTfJP 2012, https://homes.cs.washington.edu/~mernst/pubs/regex-types-ftfjp2012-abstract.html).
To run the Regex Checker, supply the -processor org.checkerframework.checker.regex.RegexChecker command-line option to javac.
These qualifiers make up the Regex type system:
The subtyping hierarchy of the Regex Checker’s qualifiers is shown in Figure 14.1.
![]()
Figure 14.1: The subtyping relationship of the Regex Checker’s qualifiers. The type qualifiers are applicable to CharSequence and its subtypes. Because the parameter to a @Regex qualifier is at least the number of capturing groups in a regular expression, a @Regex qualifier with more capturing groups is a subtype of a @Regex qualifier with fewer capturing groups. Qualifiers in gray are used internally by the type system but should never be written by a programmer.
The Regex Checker adds implicit qualifiers, reducing the number of annotations that must appear in your code (see Section 32.4). If a String literal is a valid regex, the checker implicitly adds the @Regex qualifier with the argument set to the correct number of capturing groups. The Regex Checker allows the null literal to be assigned to any type qualified with the Regex qualifier.
The Regex Checker validates that a legal capturing group number is passed to Matcher’s group, start and end methods. To do this, the type of Matcher must be qualified with a @Regex annotation with the number of capturing groups in the regular expression. This is handled implicitly by the Regex Checker for local variables (see Section 32.7), but you may need to add @Regex annotations with a capturing group count to Pattern and Matcher fields and parameters.
public @Regex String parenthesize(@Regex String regex) { return "(" + regex + ")"; // Even though the parentheses are not @Regex Strings, // the whole expression is a @Regex String }
Figure 14.2: An example of the Regex Checker’s support for concatenation of non-regular-expression Strings to produce valid regular expression Strings.
In general, concatenating a non-regular-expression String with any other string yields a non-regular-expression String. The Regex Checker can sometimes determine that concatenation of non-regular-expression Strings will produce valid regular expression Strings. For an example see Figure 14.2.
Sometimes, the Regex Checker cannot infer whether a particular expression is a regular expression — and sometimes your code cannot either! In these cases, you can use the isRegex method to perform such a test, and other helper methods to provide useful error messages. A common use is for user-provided regular expressions (such as ones passed on the command-line). Figure 14.3 gives an example of the intended use of the RegexUtil methods.
An additional version of each of these methods is also provided that takes an additional group count parameter. The RegexUtil.isRegex method verifies that the argument has at least the given number of groups. The RegexUtil.regexError and RegexUtil.regexException methods return a String error message and PatternSyntaxException, respectively, detailing why the given String is not a syntactically valid regular expression with at least the given number of capturing groups.
If you detect that a String is not a valid regular expression but would like to report the error higher up the call stack (potentially where you can provide a more detailed error message) you can throw a RegexUtil.CheckedPatternSyntaxException. This exception is functionally the same as a PatternSyntaxException except it is checked to guarantee that the error will be handled up the call stack. For more details, see the Javadoc for RegexUtil.CheckedPatternSyntaxException.
To use the RegexUtil class, the checker-util.jar file must be on the classpath at run time.
String regex = getRegexFromUser(); if (! RegexUtil.isRegex(regex)) { throw new RuntimeException("Error parsing regex " + regex, RegexUtil.regexException(regex)); } Pattern p = Pattern.compile(regex);
Figure 14.3: Example use of RegexUtil methods.
If you are positive that a particular string that is being used as a regular expression is syntactically valid, but the Regex Checker cannot conclude this and issues a warning about possible use of an invalid regular expression, then you can use the RegexUtil.asRegex method to suppress the warning.
You can think of this method as a cast: it returns its argument unchanged, but with the type @Regex String if it is a valid regular expression. It throws an error if its argument is not a valid regular expression, but you should only use it when you are sure it will not throw an error.
There is an additional RegexUtil.asRegex method that takes a capturing group parameter. This method works the same as described above, but returns a @Regex String with the parameter on the annotation set to the value of the capturing group parameter passed to the method.
The use case shown in Figure 14.3 should support most cases so the asRegex method should be used rarely.
The Format String Checker prevents use of incorrect format strings in format methods such as System.out.printf and String.format.
The Format String Checker warns you if you write an invalid format string, and it warns you if the other arguments are not consistent with the format string (in number of arguments or in their types). Here are examples of errors that the Format String Checker detects at compile time. Section 15.3 provides more details.
String.format("%y", 7); // error: invalid format string String.format("%d", "a string"); // error: invalid argument type for %d String.format("%d %s", 7); // error: missing argument for %s String.format("%d", 7, 3); // warning: unused argument 3 String.format("{0}", 7); // warning: unused argument 7, because {0} is wrong syntax
To run the Format String Checker, supply the -processor org.checkerframework.checker.formatter.FormatterChecker command-line option to javac.
The paper “A type system for format strings” [WKSE14] (ISSTA 2014, https://homes.cs.washington.edu/~mernst/pubs/format-string-issta2014-abstract.html) gives more details about the Format String Checker and the Internationalization Format String Checker (Chapter 16);
Printf-style formatting takes as an argument a format string and a list of arguments. It produces a new string in which each format specifier has been replaced by the corresponding argument. The format specifier determines how the format argument is converted to a string. A format specifier is introduced by a % character. For example, String.format("The %s is %d.","answer",42) yields "The answer is 42.". "The %s is %d." is the format string, "%s" and "%d" are the format specifiers; "answer" and 42 are format arguments.
The @Format qualifier on a string type indicates a valid format string. A programmer rarely writes the @Format annotation, as it is inferred for string literals. A programmer may need to write it on fields and on method signatures.
The @Format qualifier is parameterized with a list of conversion categories that impose restrictions on the format arguments. Conversion categories are explained in more detail in Section 15.2.1. The type qualifier for "%d %f" is for example @Format({INT, FLOAT}).
Consider the below printFloatAndInt method. Its parameter must be a format string that can be used in a format method, where the first format argument is “float-like” and the second format argument is “integer-like”. The type of its parameter, @Format({FLOAT, INT}) String, expresses that contract.
void printFloatAndInt(@Format({FLOAT, INT}) String fs) { System.out.printf(fs, 3.1415, 42); } printFloatAndInt("Float %f, Number %d"); // OK printFloatAndInt("Float %f"); // error
![]()
Figure 15.1: The Format String Checker type qualifier hierarchy. The type qualifiers are applicable to CharSequence and its subtypes. The figure does not show the subtyping rules among different @Format(...) qualifiers; see Section 15.2.2.
Figure 15.1 shows all the type qualifiers. The annotations other than @Format are only used internally and cannot be written in your code. @InvalidFormat indicates an invalid format string — that is, a string that cannot be used as a format string. For example, the type of "%y" is @InvalidFormat String. @FormatBottom is the type of the null literal. @UnknownFormat is the default that is applied to strings that are not literals and on which the user has not written a @Format annotation.
There is also a @FormatMethod annotation; see Section 15.5.
Given a format specifier, only certain format arguments are compatible with it, depending on its “conversion” — its last, or last two, characters. For example, in the format specifier "%d", the conversion d restricts the corresponding format argument to be “integer-like”:
String.format("%d", 5); // OK String.format("%d", "hello"); // error
Many conversions enforce the same restrictions. A set of restrictions is represented as a conversion category. The “integer like” restriction is for example the conversion category INT. The following conversion categories are defined in the ConversionCategory enumeration:
All conversion categories accept null. Furthermore, null is always a legal argument array, because it is treated as supplying null to each format specifer. For example, String.format("%d %f %s", (Object[]) null) evaluates to "null null null".
The same format argument may serve as a replacement for multiple format specifiers. Until now, we have assumed that the format specifiers simply consume format arguments left to right. But there are two other ways for a format specifier to select a format argument:
In the following example, the format argument must be compatible with both conversion categories, and can therefore be neither a Character nor a long.
format("Char %1$c, Int %1$d", (int)42); // OK format("Char %1$c, Int %1$d", new Character(42)); // error format("Char %1$c, Int %1$d", (long)42); // error
Only three additional conversion categories are needed represent all possible intersections of previously-mentioned conversion categories:
All other intersections lead to already existing conversion categories. For example, GENERAL ∩ CHAR = CHAR and UNUSED ∩ GENERAL = GENERAL.
Figure 15.2 summarizes the subset relationship among all conversion categories.
![]()
Figure 15.2: The subset relationship among conversion categories.
Here are the subtyping rules among different @Format qualifiers. It is legal to:
The following example shows the subtyping rules in action:
@Format({FLOAT, INT}) String f; f = "%f %d"; // OK f = "%s %d"; // OK, %s is weaker than %f f = "%f"; // warning: last argument is ignored f = "%f %d %s"; // error: too many arguments f = "%d %d"; // error: %d is not weaker than %f String.format(f, 0.8, 42);
If the Format String Checker issues no errors, it provides the following guarantees:
Following are examples for every guarantee:
String.format("%d", 42); // OK String.format(Locale.GERMAN, "%d", 42); // OK String.format(new Object()); // error (1a) String.format("%y"); // error (1a) String.format("%2$s", "unused", "used"); // warning (1b) String.format("%1$d %1$f", 5.5); // error (1c) String.format("%1$d %1$f %d", null, 6); // error (1c) String.format("%s"); // error (2a) String.format("%s", "used", "ignored"); // warning (2b) String.format("%c",4.2); // error (2c) String.format("%c", (String)null); // error (2c) String.format("%1$d %1$f", new Object[]{1}); // warning (3) String.format("%s", new Object[]{"hello"}); // warning (3)
There are three cases in which the Format String Checker may issue a warning or error, even though the code cannot fail at run time. (These are in addition to the general conservatism of a type system: code may be correct because of application invariants that are not captured by the type system.) In each of these cases, you can rewrite the code, or you can manually check it and write a @SuppressWarnings annotation if you can reason that the code is correct.
Case 1(b): Unused format arguments. It is legal to provide more arguments than are required by the format string; Java ignores the extras. However, this is an uncommon case. In practice, a mismatch between the number of format specifiers and the number of format arguments is usually an error.
Case 1(c): Format arguments that can only be null. It is legal to write a format string that permits only null arguments and throws an exception for any other argument. An example is String.format("%1$d %1$f", null). The Format String Checker forbids such a format string. If you should ever need such a format string, simply replace the problematic format specifier with "null". For example, you would replace the call above by String.format("null null").
Case 3: Array format arguments. The Format String Checker performs no analysis of arrays, only of varargs invocations. It is better style to use varargs when possible.
The Format String Checker helps prevent bugs by detecting, at compile time, which invocations of format methods will fail. While the Format String Checker finds most of these invocations, there are cases in which a format method call will fail even though the Format String Checker issued neither errors nor warnings. These cases are:
The following examples illustrate these limitations:
class A { public String toString() { throw new Error(); } } class B implements Formattable { public void formatTo(Formatter fmt, int f, int width, int precision) { throw new Error(); } } // The checker issues no errors or warnings for the // following illegal invocations of format methods. String.format(null); // NullPointerException (1) String.format("%s", new A()); // Error (2) String.format("%s", new B()); // Error (3) String.format("%c", (int)-1); // IllegalFormatCodePointException (4)
The Format String Checker adds implicit qualifiers, reducing the number of annotations that must appear in your code (see Section 32.4). The checker implicitly adds the @Format qualifier with the appropriate conversion categories to any String literal that is a valid format string.
Your project may contain methods that forward their arguments to a format method. Consider for example the following log method:
@FormatMethod void log(String format, Object... args) { if (enabled) { logfile.print(indent_str); logfile.printf(format , args); } }
You should annotate such a method with the @FormatMethod annotation, which indicates that the String argument is a format string for the remaining arguments.
The Format String Checker automatically determines whether each String literal is a valid format string or not. When a string is computed or is obtained from an external resource, then the string must be trusted or tested.
One way to test a string is to call the FormatUtil.asFormat method to check whether the format string is valid and its format specifiers match certain conversion categories. If this is not the case, asFormat raises an exception. Your code should catch this exception and handle it gracefully.
The following code examples may fail at run time, and therefore they do not type check. The type-checking errors are indicated by comments.
Scanner s = new Scanner(System.in); String fs = s.next(); System.out.printf(fs, "hello", 1337); // error: fs is not known to be a format string
Scanner s = new Scanner(System.in); @Format({GENERAL, INT}) String fs = s.next(); // error: fs is not known to have the given type System.out.printf(fs, "hello", 1337); // OK
The following variant does not throw a run-time error, and therefore passes the type-checker:
Scanner s = new Scanner(System.in); String format = s.next() try { format = FormatUtil.asFormat(format, GENERAL, INT); } catch (IllegalFormatException e) { // Replace this by your own error handling. System.err.println("The user entered the following invalid format string: " + format); System.exit(2); } // fs is now known to be of type: @Format({GENERAL, INT}) String System.out.printf(format, "hello", 1337);
To use the FormatUtil class, the checker-util.jar file must be on the classpath at run time.
The Internationalization Format String Checker, or I18n Format String Checker, prevents use of incorrect i18n format strings.
If the I18n Format String Checker issues no warnings or errors, then MessageFormat.format will raise no error at run time. “I18n” is short for “internationalization” because there are 18 characters between the “i” and the “n”.
Here are the examples of errors that the I18n Format Checker detects at compile time.
// Warning: the second argument is missing. MessageFormat.format("{0} {1}", 3.1415); // String argument cannot be formatted as Time type. MessageFormat.format("{0, time}", "my string"); // Invalid format string: unknown format type: thyme. MessageFormat.format("{0, thyme}", new Date()); // Invalid format string: missing the right brace. MessageFormat.format("{0", new Date()); // Invalid format string: the argument index is not an integer. MessageFormat.format("{0.2, time}", new Date()); // Invalid format string: "#.#.#" subformat is invalid. MessageFormat.format("{0, number, #.#.#}", 3.1415);
For instructions on how to run the Internationalization Format String Checker, see Section 16.6.
The Internationalization Checker or I18n Checker (Chapter 17.2) has a different purpose. It verifies that your code is properly internationalized: any user-visible text should be obtained from a localization resource and all keys exist in that resource.
The paper “A type system for format strings” [WKSE14] (ISSTA 2014, https://homes.cs.washington.edu/~mernst/pubs/format-string-issta2014-abstract.html) gives more details about the Internationalization Format String Checker and the Format String Checker (Chapter 15);
![]()
Figure 16.1: The Internationalization Format String Checker type qualifier hierarchy. The type qualifiers are applicable to CharSequence and its subtypes. The figure does not show the subtyping rules among different @I18nFormat(...) qualifiers; see Section 16.2. All @I18nFormatFor annotations are unrelated by subtyping, unless they are identical. The qualifiers in gray are used internally by the checker and should never be written by a programmer.
The MessageFormat documentation specifies the syntax of the i18n format string.
These are the qualifiers that make up the I18n Format String type system. Figure 16.1 shows their subtyping relationships.
static void method(@I18nFormatFor("#2") String format, Object... args) { // the body may use the parameters like this: MessageFormat.format(format, args); } method("{0, number} {1}", 3.1415, "A string"); // OK // error: The string "hello" cannot be formatted as a Number. method("{0, number} {1}", "hello", "goodbye");
In a message string, the optional second element within the curly braces is called a format type and must be one of number, date, time, and choice. These four format types correspond to different conversion categories. date and time correspond to DATE in the conversion categories figure. choice corresponds to NUMBER. The format type restricts what arguments are legal. For example, a date argument is not compatible with the number format type, i.e., MessageFormat.format("{0, number}", new Date()) will throw an exception.
The I18n Checker represents the possible arguments via conversion categories. A conversion category defines a set of restrictions or a subtyping rule.
Figure 16.2 summarizes the subset relationship among all conversion categories.
![]()
Figure 16.2: The subset relationship among i18n conversion categories.
Here are the subtyping rules among different @I18nFormat qualifiers. It is legal to:
The following example shows the subtyping rules in action:
@I18nFormat({NUMBER, DATE}) String f; f = "{0, number, #.#} {1, date}"; // OK f = "{0, number} {1}"; // OK, GENERAL is weaker (less restrictive) than DATE f = "{0} {1, date}"; // OK, GENERAL is weaker (less restrictive) than NUMBER f = "{0, number}"; // warning: last argument is ignored f = "{0}"; // warning: last argument is ignored f = "{0, number} {1, number}"; // error: NUMBER is stronger (more restrictive) than DATE f = "{0} {1} {2}"; // error: too many arguments
The conversion categories are:
The Internationalization Format String Checker checks calls to the i18n formatting method MessageFormat.format and guarantees the following:
MessageFormat.format("{0, number} {1, number}", 3.14); // Output: 3.14 {1}
MessageFormat.format("{0, number}", 1, new Date());
MessageFormat.format("{0, number} {0, number}", 3.14, 3.14);
This does not cause an error at run time, but it often indicates a programmer mistake. If it is intentional, then you should suppress the warning (see Chapter 33).
MessageFormat.format("{0, number} {1}", array);
The checker cannot verify whether the format string is valid, so the checker conservatively issues a warning. This is a limitation of the Internationalization Format String Checker.
MessageFormat.format("{0, time", new Date());
MessageFormat.format("{0.2, time}", new Date());
MessageFormat.format("{-1, time}", new Date());
MessageFormat.format("{0, foo}", 3.14);
MessageFormat.format("{0, choice}", 3.14);
MessageFormat.format("{0, time, number}", 3.14);
MessageFormat.format("{0, number, #.#.#}", 3.14)
MessageFormat.format("{0, number}", new Date());
The Checker also detects illegal assignments: assigning a non-format-string or an incompatible format string to a variable declared as containing a specific type of format string. For example,
@I18nFormat({GENERAL, NUMBER}) String format; // OK. format = "{0} {1, number}"; // OK, GENERAL is weaker (less restrictive) than NUMBER. format = "{0} {1}"; // OK, it is legal to have fewer arguments than required (less restrictive). // But the warning will be issued instead. format = "{0}"; // Error, the format string is stronger (more restrictive) than the specifiers. format = "{0} {1} {2}"; // Error, the format string is more restrictive. NUMBER is a subtype of GENERAL. format = "{0, number} {1, number}";
A programmer rarely writes an i18n format string literally. (The examples in this chapter show that for simplicity.) Rather, the i18n format strings are read from a resource file. The program chooses a resource file at run time depending on the locale (for example, different resource files for English and Spanish users).
For example, suppose that the resource1.properties file contains
key1 = The number is {0, number}.
Then code such as the following:
String formatPattern = ResourceBundle.getBundle("resource1").getString("key1"); System.out.println(MessageFormat.format(formatPattern, 2.2361));
will output “The number is 2.2361.” A different resource file would contain key1 = El número es {0, number}.
When you run the I18n Format String Checker, you need to indicate which resource file it should check. If you change the resource file or use a different resource file, you should re-run the checker to ensure that you did not make an error. The I18n Format String Checker supports two types of resource files: ResourceBundles and property files. The example above shows use of resource bundles. For more about checking property files, see Chapter 17.
The checker can be invoked by running one of the following commands (with the whole command on one line).
javac -processor org.checkerframework.checker.i18nformatter.I18nFormatterChecker -Abundlenames=MyResource MyFile.java
javac -processor org.checkerframework.checker.i18nformatter.I18nFormatterChecker -Apropfiles=MyResource.properties MyFile.java
javac -processor org.checkerframework.checker.i18nformatter.I18nFormatterChecker MyFile.java
In the case that the checker cannot infer the i18n format type of a string, you can use the I18nFormatUtil.hasFormat method to define the type of the string in the scope of a conditional statement.
For an example, see Section 16.8.
To use the I18nFormatUtil class, the checker-util.jar file must be on the classpath at run time.
// suppose the bundle "MyResource" contains: key1={0, number} {1, date} String value = ResourceBundle.getBundle("MyResource").getString("key1"); MessageFormat.format(value, 3.14, new Date()); // OK // error: incompatible types in argument; found String, expected number MessageFormat.format(value, "Text", new Date());
void test1(String format) { if (I18nFormatUtil.hasFormat(format, I18nConversionCategory.GENERAL, I18nConversionCategory.NUMBER)) { MessageFormat.format(format, "Hello", 3.14); // OK // error: incompatible types in argument; found String, expected number MessageFormat.format(format, "Hello", "Bye"); // error: missing arguments; expected 2 but 1 given MessageFormat.format(format, "Bye"); // error: too many arguments; expected 2 but 3 given MessageFormat.format(format, "A String", 3.14, 3.14); } }
static void method(@I18nFormatFor("#2") String f, Object... args) {...} // OK, MessageFormat.format(...) would return "3.14 Hello greater than one" method("{0, number} {1} {2, choice,0#zero|1#one|1<greater than one}", 3.14, "Hello", 100); // error: incompatible types in argument; found String, expected number method("{0, number} {1}", "Bye", "Bye");
@I18nFormat({I18nConversionCategory.DATE}) String; s1 = "{0}"; s1 = "{0, number}"; // error: incompatible types in assignment
The Property File Checker ensures that a property file or resource bundle (both of which act like maps from keys to values) is only accessed with valid keys. Accesses without a valid key either return null or a default value, which can lead to a NullPointerException or hard-to-trace behavior. The Property File Checker (Section 17.1) ensures that the used keys are found in the corresponding property file or resource bundle.
We also provide two specialized checkers. An Internationalization Checker (Section 17.2) verifies that code is properly internationalized. A Compiler Message Key Checker (Section 17.3) verifies that compiler message keys used in the Checker Framework are declared in a property file. This is an example of a simple specialization of the property file checker, and the Checker Framework source code shows how it is used.
It is easy to customize the property key checker for other related purposes. Take a look at the source code of the Compiler Message Key Checker and adapt it for your purposes.
The general Property File Checker ensures that a resource key is located in a specified property file or resource bundle.
The annotation @PropertyKey indicates that the qualified CharSequence is a valid key found in the property file or resource bundle. You do not need to annotate String literals. The checker looks up every String literal in the specified property file or resource bundle, and adds annotations as appropriate.
If you pass a CharSequence (including String) variable to be eventually used as a key, you also need to annotate all these variables with @PropertyKey.
The checker can be invoked by running the following command:
javac -processor org.checkerframework.checker.propkey.PropertyKeyChecker -Abundlenames=MyResource MyFile.java ...
You must specify the resources, which map keys to strings. The checker supports two types of resource: resource bundles and property files. You can specify one or both of the following two command-line options:
resource_name is the name of the resource to be used with ResourceBundle.getBundle(). The checker uses the default Locale and ClassLoader in the compilation system. (For a tutorial about ResourceBundles, see https://docs.oracle.com/javase/tutorial/i18n/resbundle/concept.html.) Multiple resource bundle names are separated by colons ’:’.
prop_file is the name of a properties file that maps keys to values. The file format is described in the Javadoc for Properties.load(). Multiple files are separated by Java’s File.pathSeparator (semicolon “;” on Windows, colon “:” on Linux and Mac).
The Internationalization Checker, or I18n Checker, verifies that your code is properly internationalized. Internationalization is the process of designing software so that it can be adapted to different languages and locales without needing to change the code. Localization is the process of adapting internationalized software to specific languages and locales.
Internationalization is sometimes called i18n, because the word starts with “i”, ends with “n”, and has 18 characters in between. Localization is similarly sometimes abbreviated as l10n.
The checker focuses on one aspect of internationalization: user-visible text should be presented in the user’s own language, such as English, French, or German. This is achieved by looking up keys in a localization resource, which maps keys to user-visible text. For instance, one version of a resource might map "CANCEL_STRING" to "Cancel", and another version of the same resource might map "CANCEL_STRING" to "Abbrechen".
There are other aspects to localization, such as formatting of dates (3/5 vs. 5/3 for March 5), that the checker does not check.
The Internationalization Checker verifies these two properties:
If you use the Internationalization Checker, you may want to also use the Internationalization Format String Checker, or I18n Format String Checker (Chapter 16). It verifies that internationalization format strings are well-formed and used with arguments of the proper type, so that MessageFormat.format does not fail at run time.
The Internationalization Checker supports two type systems:
You may need to add the @Localized annotation to more methods in the JDK or other libraries, or in your own code.
The Internationalization Checker can be invoked by running the following command:
javac -processor org.checkerframework.checker.i18n.I18nChecker -Abundlenames=MyResource MyFile.java ...
You must specify the localization resource, which maps keys to user-visible text. Like the general Property Key Checker, the Internationalization Checker supports two types of localization resource: ResourceBundles using the -Abundlenames=resource_name option or property files using the -Apropfiles=prop_file option.
The Checker Framework uses compiler message keys to output error messages. These keys are substituted by localized strings for user-visible error messages. Using keys instead of the localized strings in the source code enables easier testing, as the expected message keys can stay unchanged while the localized strings can still be modified. We use the Compiler Message Key Checker to ensure that all internal keys are correctly localized. Instead of using the Property File Checker, we use a specialized checker, giving us more precise documentation of the intended use of Strings.
The single annotation used by this checker is @CompilerMessageKey. The Checker Framework is completely annotated; for example, class org.checkerframework.framework.source.Result uses @CompilerMessageKey in methods failure and warning. For most users of the Checker Framework there will be no need to annotate any Strings, as the checker looks up all String literals and adds annotations as appropriate.
The Compiler Message Key Checker can be invoked by running the following command:
javac -processor org.checkerframework.checker.compilermsgs.CompilerMessagesChecker -Apropfiles=messages.properties MyFile.java ...
You must specify the resource, which maps compiler message keys to user-visible text. The checker supports the same options as the general property key checker. Within the Checker Framework we only use property files, so the -Apropfiles=prop_file option should be used.
The Signature String Checker, or Signature Checker for short, verifies that string representations of types and signatures are used correctly.
Java defines multiple different string representations for types (see Section 18.1), and it is easy to misuse them or to miss bugs during testing. Using the wrong string format leads to a run-time exception or an incorrect result. This is a particular problem for fully qualified and binary names, which are nearly the same — they differ only for nested classes and arrays.
The paper “Building and using pluggable type-checkers” [DDE+11] (ICSE 2011, https://homes.cs.washington.edu/~mernst/pubs/pluggable-checkers-icse2011.pdf) describes case studies of the Signature String Checker.
Java defines six formats for the string representation of a type. There is an annotation for each of these representations. Figure 18.1 shows how they are related; examples appear in a table below.
![]()
Figure 18.1: Partial type hierarchy for the Signature type system, showing string representations of a Java type. The type qualifiers are applicable to CharSequence and its subtypes. Programmers usually only need to write the boldfaced qualifiers; other qualifiers are included to improve the internal handling of String literals.
Other type qualifiers are the intersection of two or more qualifiers listed above; for example, a @BinaryNameWithoutPackage is a string that is a valid internal form and a valid binary name. A programmer should never or rarely use these qualifiers, and you can ignore them as implementation details of the Signature Checker, though you might occasionally see them in an error message. These qualifiers exist to give literals sufficiently precise types that they can be used in any appropriate context.
Java also defines other string formats for a type, notably qualified names (JLS §6.2). The Signature Checker does not include annotations for these.
Here are examples of the supported formats:
fully qualified name | Class.getName | field descriptor | binary name | internal form | Class.getSimpleName |
int | int | I | n/a for primitive type | n/a for primitive type | int |
int[][] | [[I | [[I | n/a for array type | n/a for array type | int[][] |
MyClass | MyClass | LMyClass; | MyClass | MyClass | MyClass |
MyClass[] | [LMyClass; | [LMyClass; | n/a for array type | n/a for array type | MyClass[] |
n/a for anonymous class | MyClass$22 | LMyClass$22; | MyClass$22 | MyClass$22 | (empty string) |
n/a for array of anon. class | [LMyClass$22; | [LMyClass$22; | n/a for array type | n/a for array type | [] |
java.lang.Integer | java.lang.Integer | Ljava/lang/Integer; | java.lang.Integer | java/lang/Integer | Integer |
java.lang.Integer[] | [Ljava.lang.Integer; | [Ljava/lang/Integer; | n/a for array type | n/a for array type | Integer[] |
pkg.Outer.Inner | pkg.Outer$Inner | Lpkg/Outer$Inner; | pkg.Outer$Inner | pkg/Outer$Inner | Inner |
pkg.Outer.Inner[] | [Lpkg.Outer$Inner; | [Lpkg/Outer$Inner; | n/a for array type | n/a for array type | Inner[] |
n/a for anonymous class | pkg.Outer$22 | Lpkg/Outer$22; | pkg.Outer$22 | pkg/Outer$22 | (empty string) |
n/a for array of anon. class | [Lpkg.Outer$22; | [Lpkg/Outer$22; | n/a for array type | n/a for array type | [] |
Java defines one format for the string representation of a method signature:
Object mymethod(int i, double d, Thread t)is
(IDLjava/lang/Thread;)Ljava/lang/Object;
Certain methods in the JDK, such as Class.forName, are annotated indicating the type they require. The Signature Checker ensures that clients call them with the proper arguments. The Signature Checker does not reason about string operations such as concatenation, substring, parsing, etc.
To run the Signature Checker, supply the -processor org.checkerframework.checker.signature.SignatureChecker command-line option to javac.
One of the most prevalent GUI-related bugs is invalid UI update or invalid thread access: accessing the UI directly from a background thread.
Most GUI frameworks (including Android, AWT, Swing, and SWT) create a single distinguished thread — the UI event thread — that handles all GUI events and updates. To keep the interface responsive, any expensive computation should be offloaded to background threads (also called worker threads). If a background thread accesses a UI element such as a JPanel (by calling a JPanel method or reading/writing a field of JPanel), the GUI framework raises an exception that terminates the program. To fix the bug, the background thread should send a request to the UI thread to perform the access on its behalf.
It is difficult for a programmer to remember which methods may be called on which thread(s). The GUI Effect Checker solves this problem. The programmer annotates each method to indicate whether:
The GUI Effect Checker verifies these effects and statically enforces that UI methods are only called from the correct thread. A method with the safe effect is prohibited from calling a method with the UI effect.
For example, the effect system can reason about when method calls must be dispatched to the UI thread via a message such as Display.syncExec.
@SafeEffect public void calledFromBackgroundThreads(JLabel l) { l.setText("Foo"); // Error: calling a @UIEffect method from a @SafeEffect method Display.syncExec(new Runnable { @UIEffect // inferred by default public void run() { l.setText("Bar"); // OK: accessing JLabel from code run on the UI thread } }); }
The GUI Effect Checker’s annotations fall into three categories:
There are two primary GUI effect annotations:
@SafeEffect is a sub-effect of @UIEffect, in that it is always safe to call a @SafeEffect method anywhere it is permitted to call a @UIEffect method. We write this relationship as
The GUI Effect Checker ensures that only the UI thread accesses UI objects. This prevents GUI errors such as invalid UI update and invalid thread access.
The GUI Effect Checker issues errors in the following cases:
Additionally, if a method implements or overrides a method in two supertypes (two interfaces, or an interface and parent class), and those supertypes give different effects for the methods, the GUI Effect Checker issues a warning (not an error).
The GUI Effect Checker can be invoked by running the following command:
javac -processor org.checkerframework.checker.guieffect.GuiEffectChecker MyFile.java ...
The default method annotation is @SafeEffect, since most code in most programs is not related to the UI. This also means that typically, code that is unrelated to the UI need not be annotated at all.
The GUI Effect Checker provides three primary ways to change the default method effect for a class or package:
There is one other place where the default annotation is not automatically @SafeEffect: anonymous inner classes. Since anonymous inner classes exist primarily for brevity, it would be unfortunate to spoil that brevity with extra annotations. By default, an anonymous inner class method that overrides or implements a method of the parent type inherits that method’s effect. For example, an anonymous inner class implementing an interface with method @UIEffect void m() need not explicitly annotate its implementation of m(); the implementation will inherit the parent’s effect. Methods of the anonymous inner class that are not inherited from a parent type follow the standard defaulting rules.
Sometimes a type is reused for both UI-specific and background-thread work. A good example is the Runnable interface, which is used both for creating new background threads (in which case the run() method must have the @SafeEffect) and for sending code to the UI thread to execute (in which case the run() method may have the @UIEffect). But the declaration of Runnable.run() may have only one effect annotation in the source code. How do we reconcile these conflicting use cases?
Effect-polymorphism permits a type to be used for both UI and non-UI purposes. It is similar to Java’s generics in that you define, then use, the effect-polymorphic type. Recall that to define a generic type, you write a type parameter such as <T> and use it in the body of the type definition; for example, class List<T> { ... T get() {...} ... }. To instantiate a generic type, you write its name along with a type argument; for example, List<Date> myDates;.
To declare that a class is effect-polymorphic, annotate its definition with @PolyUIType. To use the effect variable in the class body, annotate a method with @PolyUIEffect. It is an error to use @PolyUIEffect in a class that is not effect-polymorphic.
Consider the following example:
@PolyUIType public interface Runnable { @PolyUIEffect void run(); }
This declares that class Runnable is parameterized over one generic effect, and that when Runnable is instantiated, the effect argument will be used as the effect for the run method.
To instantiate an effect-polymorphic type, write one of these three type qualifiers before a use of the type:
As an example:
@AlwaysSafe Runnable s = ...; s.run(); // s.run() is @SafeEffect @PolyUI Runnable p = ...; p.run(); // p.run() is @PolyUIEffect (context-dependent) @UI Runnable u = ...; u.run(); // u.run() is @UIEffect
It is an error to apply an effect instantiation qualifier to a type that is not effect-polymorphic.
Note that no annotation is required on the anonymous class declaration itself (e.g. new Runnable(){...} does not require a type use annotation, although the variable, field, or argument it ends up being assigned to might). Instead, the GUI Effect Checker will infer the effect qualifier based on the method being called from within the members of that specific anonymous class.
Sometimes you may wish to subclass a specific instantiation of an effect-polymorphic type, just as you may extend List<String>.
To do this, simply place the effect instantiation qualifier by the name of the type you are defining, e.g.:
@UI public class UIRunnable extends Runnable {...} @AlwaysSafe public class SafeRunnable extends Runnable {...}
The GUI Effect Checker will automatically apply the qualifier to all classes and interfaces the class being defined extends or implements. (This means you cannot write a class that is a subtype of a @AlwaysSafe Foo and a @UI Bar, but this has not been a problem in our experience.)
With three effect annotations, we must extend the static sub-effecting relationship:
This is the correct sub-effecting relation because it is always safe to call a @SafeEffect method (whether from an effect-polymorphic method or a UI method), and a @UIEffect method may safely call any other method.
This induces a subtyping hierarchy on type qualifiers:
This is sound because a method instantiated according to any qualifier will always be safe to call in place of a method instantiated according to one of its super-qualifiers. This allows clients to pass “safer” instances of some object type to a given method.
Sometimes it is useful to have @PolyUI parameters on a method. As a trivial example, this permits us to specify an identity method that works for both @UI Runnable and @AlwaysSafe Runnable:
public @PolyUI Runnable id(@PolyUI Runnable r) { return r; }
This use of @PolyUI will be handled as is standard for polymorphic qualifiers in the Checker Framework (see Section 31.2).
@PolyUIEffect methods should generally not use @PolyUI arguments: it is permitted by the framework, but its interaction with inheritance is subtle, and may not behave as you would hope.
The shortest explanation is this: @PolyUI arguments may only be overridden by @PolyUI arguments, even though the implicitly @PolyUI receiver may be overridden with a @AlwaysSafe receiver.
As noted earlier (Section 31.1.6), Java’s generics are invariant — A<X> is a subtype of B<Y> only if X is identical to Y. Class-level use of @PolyUI behaves slightly differently. Marking a type declaration @PolyUIType is conceptually equivalent to parameterizing the type by some E extends Effect. But in this view, Runnable<SafeEffect> (really @AlwaysSafe Runnable) would be considered a subtype of Runnable<UIEffect> (really @UI Runnable), as explained earlier in this section. Java’s generics do not permit this, which is called covariant subtyping in the effect parameter. Permitting it for all generics leads to problems where a type system can miss errors. Java solves this by making all generics invariant, which rejects more programs than strictly necessary, but leads to an easy-to-explain limitation. For this checker, covariant subtyping of effect parameters is very important: being able to pass an @AlwaysSafe Runnable in place of a @UI Runnable is extremely useful. Since we need to allow some cases for flexibility, but need to reject other cases to avoid missing errors, the distinction is a bit more subtle for this checker.
Consider this small example (warning: the following is rejected by the GUI Effect Checker):
@PolyUIType public interface Dispatcher { @PolyUIEffect void dispatch(@PolyUI Runnable toRun); } @SafeType public class SafeDispatcher implements Dispatcher { @Override public void dispatch(@AlwaysSafe Runnable toRun) { runOnBackgroundThread(toRun); } }
This may initially seem like harmless code to write, which simply specializes the implicit effect parameter from Dispatcher in the SafeDispatcher implementation. However, the way method effect polymorphism is implemented is by implicitly making the receiver of a @PolyUIEffect method — the object on which the method is invoked — @PolyUI. So if the definitions above were permitted, the following client code would be possible:
@AlwaysSafe SafeDispatcher s = ...; @UI Runnable uitask = ...; s.dispatch(uitask);
At the call to dispatch, the Checker Framework is free to consider s as its supertype, @UI SafeDispatcher. This permits the framework to choose the same qualifier for both the (implicit) receiver use of @PolyUI and the toRun argument to Dispatcher.dispatch, passing the checker. But this code would then pass a UI-thread-only task to a method which should only accept background thread tasks — exactly what the checker should prevent!
To resolve this, the GUI Effect Checker rejects the definitions above, specifically the @AlwaysSafe on SafeDispatcher.dispatch’s parameter, which would need to remain @PolyUI.
A subtlety of the code above is that the receiver for SafeDispatcher.dispatch is also overridden, switching from a @PolyUI receiver to a @Safe receiver. That change is permissible. But when that is done, the polymorphic arguments may no longer be interchangeable with the receiver.
The ECOOP 2013 paper “JavaUI: Effects for controlling UI object access” [GDEG13] (ECOOP 2013, https://homes.cs.washington.edu/~mernst/pubs/gui-thread-ecoop2013-abstract.html) includes some case studies on the checker’s efficacy, including descriptions of the relatively few false warnings we encountered. It also contains a more formal description of the effect system.
For many applications, it is important to use the correct units of measurement for primitive types. For example, NASA’s Mars Climate Orbiter (cost: $327 million) was lost because of a discrepancy between use of the metric unit Newtons and the imperial measure Pound-force.
The Units Checker ensures consistent usage of units. For example, consider the following code:
@m int meters = 5 * UnitsTools.m; @s int secs = 2 * UnitsTools.s; @mPERs int speed = meters / secs;
Due to the annotations @m and @s, the variables meters and secs are guaranteed to contain only values with meters and seconds as units of measurement. The assignment of an unqualified value to meters, as in meters = 99, will be flagged as an error by the Units Checker. Utility class UnitsTools provides constants that you can multiply with unqualified integer are multiplied to get values of the corresponding unit; for example, meters = 99 * UnitsTools.m is legal, or just meters = 99 * m if the file contains import static org.checkerframework.checker.units.util.UnitsTools.*;. To use the UnitsTools class, the checker-util.jar file must be on the classpath at run time.
The division meters/secs takes the types of the two operands into account and determines that the result is of type meters per second, signified by the @mPERs qualifier. We provide an extensible framework to define the result of operations on units.
The checker currently supports three varieties of units annotations: kind annotations (@Length, @Mass, …), the SI units (@m, @kg, …), and polymorphic annotations (@PolyUnit).
Kind annotations can be used to declare what the expected unit of measurement is, without fixing the particular unit used. For example, one could write a method taking a @Length value, without specifying whether it will take meters or kilometers. The following kind annotations are defined:
For each kind of unit, the corresponding SI unit of measurement is defined:
You may specify SI unit prefixes, using enumeration Prefix. The basic SI units (@s, @m, @g, @A, @K, @mol, @cd) take an optional Prefix enum as argument. For example, to use nanoseconds as unit, you could use @s(Prefix.nano) as a unit type. You can sometimes use a different annotation instead of a prefix; for example, @mm is equivalent to @m(Prefix.milli).
Class UnitsTools contains a constant for each SI unit. To create a value of the particular unit, multiply an unqualified value with one of these constants. By using static imports, this allows very natural notation; for example, after statically importing UnitsTools.m, the expression 5 * m represents five meters. As all these unit constants are public, static, and final with value one, the compiler will optimize away these multiplications. To use the UnitsTools class, the checker-util.jar file must be on the classpath at run time.
The polymorphic annotation @PolyUnit enables you to write a method that takes an argument of any unit type and returns a result of that same type. For more about polymorphic qualifiers, see Section 31.2. For an example of its use, see the @PolyUnit Javadoc.
You can create new kind annotations and unit annotations that are specific to the particular needs of your project. An easy way to do this is by copying and adapting an existing annotation. (In addition, search for all uses of the annotation’s name throughout the Units Checker implementation, to find other code to adapt; read on for details.)
Here is an example of a new unit annotation.
@Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) @SubtypeOf({Time.class}) @UnitsMultiple(quantity=s.class, prefix=Prefix.nano) public @interface ns {}
The @SubtypeOf meta-annotation specifies that this annotation introduces an additional unit of time. The @UnitsMultiple meta-annotation specifies that this annotation should be a nano multiple of the basic unit @s: @ns and @s(Prefix.nano) behave equivalently and interchangeably. Most annotation definitions do not have a @UnitsMultiple meta-annotation.
Note that all custom annotations must have the @Target(ElementType.TYPE_USE) meta-annotation. See section 36.5.1.
To take full advantage of the additional unit qualifier, you need to do two additional steps. (1) Provide constants that convert from unqualified types to types that use the new unit. See class UnitsTools for examples (you will need to suppress a checker warning in just those few locations). (2) Put the new unit in relation to existing units. Provide an implementation of the UnitsRelations interface as a meta-annotation to one of the units.
See demonstration docs/examples/units-extension/ for an example extension that defines Hertz (hz) as scalar per second, and defines an implementation of UnitsRelations to enforce it.
The Units Checker ensures that unrelated types are not mixed.
All types with a particular unit annotation are disjoint from all unannotated types, from all types with a different unit annotation, and from all types with the same unit annotation but a different prefix.
Subtyping between the units and the unit kinds is taken into account, as is the @UnitsMultiple meta-annotation.
Multiplying a scalar with a unit type results in the same unit type.
The division of a unit type by the same unit type results in the unqualified type.
Multiplying or dividing different unit types, for which no unit relation is known to the system, will result in a MixedUnits type, which is separate from all other units. If you encounter a MixedUnits annotation in an error message, ensure that your operations are performed on correct units or refine your UnitsRelations implementation.
The Units Checker does not change units based on multiplication; for example, if variable mass has the type @kg double, then mass * 1000 has that same type rather than the type @g double. (The Units Checker has no way of knowing whether you intended a conversion, or you were computing the mass of 1000 items. You need to make all conversions explicit in your code, and it’s good style to minimize the number of conversions.)
The Units Checker can be invoked by running the following commands.
javac -processor org.checkerframework.checker.units.UnitsChecker MyFile.java ...
javac -classpath /full/path/to/myProject/bin:/full/path/to/myLibrary/bin \ -processor org.checkerframework.checker.units.UnitsChecker \ -Aunits=myPackage.qual.MyUnit,myPackage.qual.MyOtherUnit MyFile.java ...
The annotations listed in -Aunits must be accessible to the compiler during compilation. Before you run the Units Checker with javac, they must be compiled and on the same path (the classpath or processorpath) as the Checker Framework. It is not sufficient to supply their source files on the command line.
javac -classpath /full/path/to/myProject/bin:/full/path/to/myLibrary/bin \ -processor org.checkerframework.checker.units.UnitsChecker \ -AunitsDirs=/full/path/to/myProject/bin:/full/path/to/myLibrary/bin MyFile.java ...
Note that in these two examples, the compiled class file of the myPackage.qual.MyUnit and myPackage.qual.MyOtherUnit annotations must exist in either the myProject/bin directory or the myLibrary/bin directory. The following placement of the class files will work with the above commands:
.../myProject/bin/myPackage/qual/MyUnit.class .../myProject/bin/myPackage/qual/MyOtherUnit.class
The two options can be used at the same time to provide groups of annotations from directories, and individually named annotations.
Also, see the example project in the docs/examples/units-extension directory.
One example of when you need to suppress warnings is when you initialize a variable with a unit type by a literal value. To remove this warning message, it is best to introduce a constant that represents the unit and to add a @SuppressWarnings annotation to that constant. For examples, see class UnitsTools.
The Signedness Checker guarantees that signed and unsigned integral values are not mixed together in a computation. In addition, it prohibits meaningless operations, such as division on an unsigned value.
Recall that a computer represents a number as a sequence of bits. Signedness indicates how to interpret the most significant bit. For example, the bits 10000010 ordinarily represent the value -126, but when interpreted as unsigned, those bits represent the value 130. The bits 01111110 represent the value 126 in signed and in unsigned interpretation. The range of signed byte values is -128 to 127. The range of unsigned byte values is 0 to 255.
Signedness is only applicable to the integral types byte, short, int, and long and their boxed variants Byte, Short, Integer, and Long. char and Character are always unsigned. Floating-point types float, double, Float, and Double are always signed.
Signedness is primarily about how the bits of the representation are interpreted, not about the values that it can represent. An unsigned value is always non-negative, but just because a variable’s value is non-negative does not mean that it should be marked as @Unsigned. If variable v will be compared to a signed value, or used in arithmetic operations with a signed value, then v should have signed type. To indicate the range of possible values for a variable, use the @NonNegative annotation of the Index Checker (see Chapter 11) or the @IntRange annotation of the Constant Value Checker (see Chapter 23).
Additional details appear in the paper “Preventing signedness errors in numerical computations in Java” [Mac16] (FSE 2016).
To run the Signedness Checker, run javac with -processor org.checkerframework.checker.signedness.SignednessChecker.
The Signedness Checker uses type annotations to indicate the signedness that the programmer intends an expression to have.
![]()
Figure 21.1: The type qualifier hierarchy of the signedness annotations. Qualifiers in gray are used internally by the type system but should never be written by a programmer.
These are the qualifiers in the signedness type system:
The only type qualifier that the programmer should need to write is @Unsigned. When a programmer leaves an expression unannotated, the Signedness Checker treats it in one of the following ways:
The Signedness Checker prohibits the following uses of operators:
>>
) with an @Unsigned left operand.
>>>
) with a @Signed left operand.
<<
).There are some special cases where these operations are permitted; see Section 21.2.2.
Like every type-checker built with the Checker Framework, the Signedness Checker ensures that assignments and pseudo-assignments have consistent types. For example, it is not permitted to assign a @Signed expression to an @Unsigned variable or vice versa.
The Signedness Checker prevents misuse of unsigned values in Java code. Most Java operations interpret operands as signed. If applied to unsigned values, those operations would produce unexpected, incorrect results.
Consider the following Java code:
public class SignednessManualExample { int s1 = -2; int s2 = -1; @Unsigned int u1 = 2147483646; // unsigned: 2^32 - 2, signed: -2 @Unsigned int u2 = 2147483647; // unsigned: 2^32 - 1, signed: -1 void m() { int w = s1 / s2; // OK: result is 2, which is correct for -2 / -1 int x = u1 / u2; // ERROR: result is 2, which is incorrect for (2^32 - 2) / (2^32 - 1) } int s3 = -1; int s4 = 5; @Unsigned int u3 = 2147483647; // unsigned: 2^32 - 1, signed: -1 @Unsigned int u4 = 5; void m2() { int y = s3 % s4; // OK: result is -1, which is correct for -1 % 5 int z = u3 % u4; // ERROR: result is -1, which is incorrect for (2^32 - 1) % 5 = 2 } }
These examples illustrate why division and modulus with an unsigned operand are illegal. Other uses of operators are prohibited for similar reasons.
As exceptions to the rules given above, the Signedness Checker permits certain right shifts which are immediately followed by a cast or masking operation.
For example, right shift by 8 then mask by 0xFF evaluates to the same value
whether the argument is interpreted as signed or unsigned. Thus, the
Signedness Checker permits both ((myInt >> 8) & 0xFF)
and
((myInt >>> 8) & 0xFF)
, regardless of the qualifier on the type of
myInt.
Likewise, right shift by 8 then cast to byte evaluates to the
same value whether the argument is interpreted as signed or unsigned, so
the Signedness Checker permits both (byte) (myInt >> 8)
and
(byte) (myInt >>> 8)
, regardless of the type of myInt.
Class SignednessUtil provides static utility methods for working with unsigned values. They are properly annotated with @Unsigned where appropriate, so using them may reduce the number of annotations that you need to write. To use the SignednessUtil class, the checker-util.jar file must be on the classpath at run time.
Class SignednessUtilExtra contains more utility methods that reference packages not included in Android. This class is not included in checker-util.jar, so you may want to copy the methods to your code.
Local type refinement/inference (Section 32.7) may be surprising for the Signedness type system. Ordinarily, an expression with unsigned type may not participate in a division, as shown in Sections 21.2 and 21.2.1. However, if a constant is assigned to a variable that was declared with @Unsigned type, then — just like the constant — the variable may be treated as either signed or unsigned, due to local type refinement (Section 32.7). For example, it can participate in division.
void useLocalVariables() { int s1 = -2; int s2 = -1; @Unsigned int u1 = 2147483646; // unsigned: 2^32 - 2, signed: -2 @Unsigned int u2 = 2147483647; // unsigned: 2^32 - 1, signed: -1 int w = s1 / s2; // OK: result is 2, which is correct for -2 / -1 int x = u1 / u2; // OK; computation over constants, interpreted as signed; result is signed }
To prevent local type refinement, use a cast:
@Unsigned int u1 = (@Unsigned int) 2147483646;
Note that type-checking produces a different result for int x = u1 / u2; here than in the similar example in Section 21.2.1. In Section 21.2.1, the method is reading fields, and all it knows is the declared type of the field. In 20.4, the method is reading a local variable, and dataflow (that is, flow-sensitive type refinement) refines the types of local variables.
When calling a method with formal parameters annotated as @PolySigned, all arguments for @PolySigned formal parameters must have comparable types. (One way to do this is for all types to be the same.) This is different than the usual rules for polymorphic qualifiers. If you violate this rule, then the Signedness Checker’s error messages can be obscure, because they are about @SignednessBottom. You can fix the signedness error messages by casting the arguments.
The Checker Framework’s signedness annotations are similar to annotations used elsewhere.
If your code is already annotated with a different annotation, the Checker Framework can type-check your code. It treats annotations from other tools as if you had written the corresponding annotation from the Signedness Checker, as described in Figure 21.2.
jdk.jfr.Unsigned ⇒ org.checkerframework.checker.signedness.qual.Unsigned
Figure 21.2: Correspondence between other signedness annotations and the Checker Framework’s annotations.
The Purity Checker identifies methods that have no side effects, that return the same value each time they are called on the same argument, or both.
Purity analysis aids type refinement (Section 32.7).
All checkers utilize purity annotations on called methods. You do not need to run the Purity Checker directly. However you may run just the Purity Checker by supplying the following command-line options to javac: -processor org.checkerframework.framework.util.PurityChecker.
The Checker Framework can infer purity annotations. If you supply the command-line option -AsuggestPureMethods, then the Checker Framework will suggest methods that can be marked as @SideEffectFree, @Deterministic, or @Pure. In addition, such suggestions are output by -Ainfer and when using whole-program inference.
By default, purity annotations are trusted. Purity annotations on called methods affect type-checking of client code. However, you can make a mistake by writing @SideEffectFree on the declaration of a method that is not actually side-effect-free or by writing @Deterministic on the declaration of a method that is not actually deterministic.
To enable checking of the annotations, supply the command-line option -AcheckPurityAnnotations. It is not enabled by default because of a high false positive rate. In the future, after a new purity-checking analysis is implemented, the Checker Framework will default to checking purity annotations.
If a method in a superclass has a purity annotation, then every overriding definition must also have that purity annotation (or a stronger one).
Here is an example error if this requirement is violated:
MyClass.java:1465: error: int hashCode() in MyClass cannot override int hashCode(Object this) in java.lang.Object; attempting to use an incompatible purity declaration public int hashCode() { ^ found : [] required: [SIDE_EFFECT_FREE, DETERMINISTIC]
The reason for the error is that the Object class is annotated as:
class Object { ... @Pure int hashCode() { ... } }
(where @Pure means both @SideEffectFree and @Deterministic). Every overriding definition, including those in your program, must use be at least as strong a specification. For hashCode, every overriding definition must be annotated as @Pure.
You can fix the definition by adding @Pure to your method definition. Alternately, you can suppress the warning. You can suppress each such warning individually using @SuppressWarnings("purity.overriding"), or you can use the -AsuppressWarnings=purity.overriding command-line argument to suppress all such warnings. In the future, the Checker Framework will support inheriting annotations from superclass definitions.
The command-line options -AassumeSideEffectFree, -AassumeDeterministic, and -AassumePure make the Checker Framework unsoundly assume that every called method is side-effect-free, is deterministic, or is both, respectively.
The command-line option -AassumePureGetters makes the Checker Framework unsoundly assume that every getter method is side-effect-free and deterministic. For the purposes of -AassumePureGetters, a getter method is defined as an instance method with no formal parameters, whose name starts with “get”, “is”, “not”, or “has” followed by an uppercase letter.
This can make flow-sensitive type refinement much more effective, since method calls will not cause the analysis to discard information that it has learned. However, this option can mask real errors. It is most appropriate when one of the following is true:
The Constant Value Checker is a constant propagation analysis: for each variable, it determines whether that variable’s value can be known at compile time.
There are two ways to run the Constant Value Checker.
The Constant Value Checker uses type annotations to indicate the value of an expression (Section 23.1.1), and it uses method annotations to indicate methods that the Constant Value Checker can execute at compile time (Section 23.2.2).
Typically, the programmer does not write any type annotations. Rather, the type annotations are inferred by the Constant Value Checker. The programmer is also permitted to write type annotations. This is only necessary in locations where the Constant Value Checker does not infer annotations: on fields and method signatures.
The main type annotations are @BoolVal, @IntVal, @IntRange, @DoubleVal, @StringVal, @MatchesRegex, @DoesNotMatchRegex, and @EnumVal. Additional type annotations for arrays and strings are @ArrayLen, @ArrayLenRange, and @MinLen. A polymorphic qualifier (@PolyValue) is also supported (see Section 31.2). In addition, there are separate checkers for @ClassVal and @MethodVal annotations (see Section 25.2).
Each *Val type annotation takes as an argument a set of values, and its meaning is that at run time, the expression evaluates to one of the values. For example, an expression of type @StringVal("a", "b") evaluates to one of the values "a", "b", or null. The set is limited to 10 entries; if a variable could be more than 10 different values, the Constant Value Checker gives up and its type becomes @IntRange for integral types, @ArrayLenRange for array types, @MatchesRegex, @DoesNotMatchRegex, @ArrayLen, or @ArrayLenRange for String, and @UnknownVal for all other types. The @ArrayLen annotation means that at run time, the expression evaluates to an array or a string whose length is one of the annotation’s arguments.
In the case of too many strings in @StringVal, the values are forgotten and just the lengths are used in @ArrayLen. If this would result in too many lengths, only the minimum and maximum lengths are used in @ArrayLenRange, giving a range of possible lengths of the string.
The @StringVal, @MatchesRegex, and @DoesNotMatchRegex annotations may be applied to char arrays. Although byte arrays are often converted to/from strings, these annotations may not be applied to them. This is because the conversion depends on the platform’s character set.
The @MatchesRegex and @DoesNotMatchRegex annotations use the standard Java regular expression syntax. @MatchesRegex(A) is only a subtype of @MatchesRegex(B) if the set of regular expressions A is a subset of the set of regular expressions B. An @StringVal annotation is a subtype of an @MatchesRegex annotation if each string matches at least one of the regular expressions. @DoesNotMatchRegex(A) is only a subtype of @MatchesRegex(B) if the set of regular expressions A is a superset of the set of regular expressions B. Matching is done via the java.lang.String#matches method, which matches against the entire string (it does not look for a matching substring).
The @EnumVal annotation’s argument is the names of the enum constants that the type might evaluate to. (Java syntax does not allow the enum constants themselves to be arguments.) @EnumVal is treated identically to @StringVal by the checker internally, so @StringVal may appear in error messages related to enums.
@IntRange takes two arguments — a lower bound and an upper bound. Its meaning is that at run time, the expression evaluates to a value between the bounds (inclusive). For example, an expression of type @IntRange(from=0, to=255) evaluates to 0, 1, 2, …, 254, or 255. An @IntVal and @IntRange annotation that represent the same set of values are semantically identical and interchangeable: they have exactly the same meaning, and using either one has the same effect. @ArrayLenRange has the same relationship to @ArrayLen that @IntRange has to @IntVal. The @MinLen annotation is an alias for @ArrayLenRange (meaning that every @MinLen annotation is automatically converted to an @ArrayLenRange annotation) that only takes one argument, which is the lower bound of the range. The upper bound of the range is the maximum integer value.
Figure 23.1 shows the subtyping relationship among the type annotations. For two annotations of the same type, subtypes have a smaller set of possible values, as also shown in the figure. Because int can be casted to double, an @IntVal annotation is a subtype of a @DoubleVal annotation with the same values.
![]()
Figure 23.1: At the top, the type qualifier hierarchy of the Constant Value Checker annotations. The first four qualifiers are applicable to primitives and their wrappers; the next to Strings (it can also be written as @EnumVal for enumeration constants), and the final two to arrays. Qualifiers in gray are used internally by the type system but should never be written by a programmer. At the bottom are examples of additional subtyping relationships that depend on the annotations’ arguments.
Figure 23.2 illustrates how the Constant Value Checker infers type annotations (using flow-sensitive type qualifier refinement, Section 32.7).
public void flowSensitivityExample(boolean b) { int i = 1; // i has type: @IntVal({1}) int if (b) { i = 2; // i now has type: @IntVal({2}) int } // i now has type: @IntVal({1,2}) int i = i + 1; // i now has type: @IntVal({2,3}) int }
Figure 23.2: The Constant Value Checker infers different types for a variable on different lines of the program.
The Checker Framework’s constant value annotations are similar to annotations used elsewhere.
If your code is already annotated with a different constant value or range annotation, the Checker Framework can type-check your code. It treats annotations from other tools as if you had written the corresponding annotation from the Constant Value Checker, as described in Figure 23.3. If the other annotation is a declaration annotation, it may be moved; see Section 39.6.10.
android.support.annotation.IntRange ⇒ org.checkerframework.checker.common.value.qual.IntRange
Figure 23.3: Correspondence between other constant value and range annotations and the Checker Framework’s annotations.
The Constant Value Checker trusts the @Positive, @NonNegative, and @GTENegativeOne annotations. If your code contains any of these annotations, then in order to guarantee soundness, you must run the Index Checker whenever you run the Constant Value Checker.
Whenever all the operands of an expression are compile-time constants (that is, their types have constant-value type annotations), the Constant Value Checker attempts to execute the expression. This is independent of any optimizations performed by the compiler and does not affect the code that is generated.
The Constant Value Checker statically executes (at compile time) operators that do not throw exceptions (e.g., +, -, <<, !=).
The Constant Value Checker statically executes (at compile time) methods annotated with @StaticallyExecutable.
@StaticallyExecutable @Pure public static int myAdd(int a, int b) { return a + b; } public void bar() { int a = 5; // a has type: @IntVal({5}) int int b = 4; // b has type: @IntVal({4}) int int c = myAdd(a, b); // c has type: @IntVal({9}) int }
Figure 23.4: The @StaticallyExecutable annotation enables constant propagation through method calls.
The static execution feature has some requirements:
This means that @StaticallyExecutable is not applicable to user-written instance methods. It is only applicable to instance methods whose receiver is a compile-time constant, such as a primitive wrapper or an array.
To use @StaticallyExecutable on methods in your own code, you should first compile the code without the Constant Value Checker and then add the location of the resulting .class files to the classpath or processorpath, whichever is appropriate. For example, the command-line arguments to the Checker Framework might include:
-processor org.checkerframework.common.value.ValueChecker -classpath $CLASSPATH:MY_PROJECT/build/
or
-processor org.checkerframework.common.value.ValueChecker -processorpath ${CHECKERFRAMEWORK}/checker/build/libs/checker-3.6.1-SNAPSHOT.jar:MY_PROJECT/build/
If the option -AreportEvalWarns options is used, the Constant Value Checker issues a warning if it cannot load and run, at compile time, a method marked as @StaticallyExecutable. If it issues such a warning, then the return value of the method will be @UnknownVal instead of being able to be resolved to a specific value annotation. Some examples of these:
The checker could not find the class specified for resolving a @StaticallyExecutable method. Typically this means that the path that contains the Checker Framework (the classpath or the processorpath) lacks the given classfile.
The checker could not find the method foo(int) specified for resolving a @StaticallyExecutable method, but could find the class. This is usually due to providing an outdated version of the classfile that does not contain the method that was annotated as @StaticallyExecutable.
An exception was thrown when trying to statically execute (at compile time) the method. In this case it was a divide-by-zero exception. If the arguments to the method each only had one value in their annotations then this exception will always occur when the program is actually run as well. If there are multiple possible values then the exception might not be thrown on every execution, depending on the run-time values.
There are some other situations in which the Constant Value Checker produces a warning message:
The Constant Value Checker only tracks up to 10 possible values for an expression. If you write an annotation with more values than will be tracked, the annotation is replaced with @IntRange, @ArrayLen, @ArrayLenRange, or @UnknownVal.
The Constant Value Checker takes Java’s overflow rules into account when computing the possible values of expressions. The -AignoreRangeOverflow command-line option makes it ignore the possibility of overflow for range annotations @IntRange and @ArrayLenRange. Figure 23.5 gives an example of behavior with and without the -AignoreRangeOverflow command-line option.
... if (i > 5) { // i now has type: @IntRange(from=5, to=Integer.MAX_VALUE) i = i + 1; // If i started out as Integer.MAX_VALUE, then i is now Integer.MIN_VALUE. // i's type is now @IntRange(from=Integer.MIN_VALUE, to=Integer.MAX_VALUE). // When ignoring overflow, i's type is now @IntRange(from=6, to=Integer.MAX_VALUE). }
Figure 23.5: With the -AignoreRangeOverflow command-line option, the Constant Value Checker ignores overflow for range types, which gives smaller ranges to range types.
As with any unsound behavior in the Checker Framework, this option reduces the number of warnings and errors produced, and may reduce the number of @IntRange qualifiers that you need to write in the source code. However, it is possible that at run time, an expression might evaluate to a value that is not in its @IntRange qualifier. You should either accept that possibility, or verify the lack of overflow using some other tool or manual analysis.
By default, the Constant Value Checker is sound with respect to string concatenation and nullness. It assumes that, in a string concatenation, every non-primitive argument might be null, except for String literals and compile-time constants. It ignores Nullness Checker annotations. (This behavior is conservative but sound.)
Consider a variable declared as @StringVal("a", "b") String x;. At run time, x evaluates to one of the values "a", "b", or null. Therefore, the type of “x + "c"” is @StringVal("ac", "bc", "nullc") String.
The -AnonNullStringsConcatenation command-line option makes the Constant Value Checker unsoundly assume that no arguments in a string concatenation are null. With the command-line argument, the type of “x + "c"” is @StringVal("ac", "bc") String.
The Returns Receiver Checker enables documenting and checking that a method returns its receiver (i.e., the this parameter).
There are two ways to run the Returns Receiver Checker.
If the code being checked does not use fluent APIs, you can pass the -AdisableReturnsReceiver command-line option. This disables the Returns Receiver Checker and makes the other checker run faster.
The qualifier @This on the return type of a method indicates that the method returns its receiver. Methods that return their receiver are common in so-called “fluent” APIs. Here is an example:
class MyBuilder { @This MyBuilder setName(String name) { this.name = name; return this; } }
An @This annotation can only be written on a return type, a receiver type, or in a downcast.
As is standard, the Returns Receiver Checker has a top qualifier, @UnknownThis, and a bottom qualifier, @BottomThis. Programmers rarely need to write these annotations.
Here are additional details. @This is a polymorphic qualifier rather than a regular type qualifier (see Section 31.2). Conceptually, a receiver type always has an @This qualifier. When a method return type also has an @This qualifier, the presence of the polymorphic annotation on both the method’s return and receiver type forces their type qualifiers to be equal. Hence, the method will only pass the type checker if it returns its receiver argument, achieving the desired checking.
@AutoValue abstract class Animal { abstract String name(); abstract int numberOfLegs(); static Builder builder() { return new AutoValue_Animal.Builder(); } @AutoValue.Builder abstract static class Builder { abstract Builder setName(String value); // @This is automatically added here abstract Builder setNumberOfLegs(int value); // @This is automatically added here abstract Animal build(); } }
Figure 24.1: User-written code that uses the @AutoValue.Builder annotation. Given this code, (1) AutoValue automatically generates a concrete subclass of Animal.Builder, see Figure 24.2, and (2) the Returns Receiver Checker automatically adds @This annotations on setters in both user-written and automatically-generated code.
class AutoValue_Animal { static final class Builder extends Animal.Builder { private String name; private Integer numberOfLegs; @This Animal.Builder setName(String name) { this.name = name; return this; } @This Animal.Builder setNumberOfLegs(int numberOfLegs) { this.numberOfLegs = numberOfLegs; return this; } @Override Animal build() { return new AutoValue_Animal(this.name, this.numberOfLegs); } } }
Figure 24.2: Code generated by AutoValue for the example of Figure 24.1, including the @This annotations added by the Returns Receiver Checker.
The AutoValue and Lombok projects both support automatic generation of builder classes, which enable flexible object construction. For code using these two frameworks, the Returns Receiver Checker automatically adds @This annotations to setter methods in builder classes. All the @This annotations in Figures 24.1 and 24.2 are automatically added by the Returns Receiver Checker.
A call to Method.invoke might reflectively invoke any method. That method might place requirements on its formal parameters, and it might return any value. To reflect these facts, the annotated JDK contains conservative annotations for Method.invoke. These conservative library annotations often cause a checker to issue false positive warnings when type-checking code that uses reflection.
If you supply the -AresolveReflection command-line option, the Checker Framework attempts to resolve reflection. At each call to Method.invoke or Constructor.newInstance, the Checker Framework first soundly estimates which methods might be invoked at run time. When type-checking the call, the Checker Framework uses the library annotations for the possibly-invoked methods, rather than the imprecise one for Method.invoke.
If the estimate of invoked methods is small, the checker issues fewer false positive warnings. If the estimate of invoked methods is large, these types may be no better than the conservative library annotations.
Reflection resolution is disabled by default, because it increases the time to type-check a program. You should enable reflection resolution with the -AresolveReflection command-line option if, for some call site of Method.invoke or Constructor.newInstance in your program:
Reflection resolution does not change your source code or generated code. In particular, it does not replace the Method.invoke or Constructor.newInstance calls.
The command-line option -AresolveReflection=debug outputs verbose information about the reflection resolution process, which may be useful for debugging.
Section 25.1 gives an example of reflection resolution. Then, Section 25.2 describes the MethodVal and ClassVal Checkers, which reflection resolution uses internally. The paper “Static analysis of implicit control flow: Resolving Java reflection and Android intents” [BJM+15] (ASE 2015, https://homes.cs.washington.edu/~mernst/pubs/implicit-control-flow-ase2015-abstract.html) gives further details.
Consider the following example, in which the Nullness Checker employs reflection resolution to avoid issuing a false positive warning.
public class LocationInfo { @NonNull Location getCurrentLocation() { ... } } public class Example { LocationInfo privateLocation = ... ; String getCurrentCity() throws Exception { Method getCurrentLocationObj = LocationInfo.class.getMethod("getCurrentLocation"); Location currentLocation = (Location) getCurrentLocationObj.invoke(privateLocation); return currentLocation.nameOfCity(); } }
When reflection resolution is not enabled, the Nullness Checker uses conservative annotations on the Method.invoke method signature:
@Nullable Object invoke(@NonNull Object recv, @NonNull Object ... args)
This causes the Nullness Checker to issue the following warning even though currentLocation cannot be null.
error: [dereference.of.nullable] dereference of possibly-null reference currentLocation return currentLocation.nameOfCity(); ^ 1 error
When reflection resolution is enabled, the MethodVal Checker infers that the @MethodVal annotation for getCurrentLocationObj is:
@MethodVal(className="LocationInfo", methodName="getCurrentLocation", params=0)
Based on this @MethodVal annotation, the reflection resolver determines that the reflective method call represents a call to getCurrentLocation in class LocationInfo. The reflection resolver uses this information to provide the following precise procedure summary to the Nullness Checker, for this call site only:
@NonNull Object invoke(@NonNull Object recv, @Nullable Object ... args)
Using this more precise signature, the Nullness Checker does not issue the false positive warning shown above.
The implementation of reflection resolution internally uses the ClassVal Checker (Section 25.2.1) and the MethodVal Checker (Section 25.2.2). They are similar to the Constant Value Checker (Section 23) in that their annotations estimate the run-time value of an expression.
In some cases, you may need to write annotations such as @ClassVal, @MethodVal, @StringVal, and @ArrayLen to aid in reflection resolution. Often, though, these annotations can be inferred (Section 25.2.3).
The ClassVal Checker defines the following annotations:
If multiple arguments are given, then the expression’s run-time value is known to be in that set.
Each argument is a “fully-qualified binary name” (@FqBinaryName): a primitive or binary name (JLS §13.1), possibly followed by array brackets.
If multiple arguments are given, then the run-time value is equal to or a subclass of some class in that set.
Each argument is a “fully-qualified binary name” (@FqBinaryName): a primitive or binary name (JLS §13.1), possibly followed by array brackets.
![]()
Figure 25.1: Partial type hierarchy for the ClassVal type system. The type qualifiers in gray (@UnknownClass and @ClassValBottom) should never be written in source code; they are used internally by the type system.
Figure 25.1 shows part of the type hierarchy of the ClassVal type system. @ClassVal(A) is a subtype of @ClassVal(B) if A is a subset of B. @ClassBound(A) is a subtype of @ClassBound(B) if A is a subset of B. @ClassVal(A) is a subtype of @ClassBound(B) if A is a subset of B.
The MethodVal Checker defines the following annotations:
Consider the following example:
@MethodVal(className={"java.util.HashMap", "java.util.HashMap"}, methodName={"containsKey", "containsValue"}, params={1, 1})
This @MethodVal annotation indicates that the Method is either HashMap.containsKey with 1 formal parameter or HashMap.containsValue with 1 formal parameter.
The @MethodVal type qualifier indicates the number of parameters that the method takes, but not their type. This means that the Checker Framework’s reflection resolution cannot distinguish among overloaded methods.
![]()
Figure 25.2: Partial type hierarchy for the MethodVal type system. The type qualifiers in gray (@UnknownMethod and @MethodValBottom) should never be written in source code; they are used internally by the type system.
Figure 25.2 shows part of the type hierarchy of the MethodVal type system. @MethodVal(classname=CA, methodname=MA, params=PA) is a subtype of @MethodVal(classname=CB, methodname=MB, params=PB) if
∀ indexes i ∃ an index j: CA[i] = CB[j], MA[i] = MA[j], and PA[i] = PB[j] |
where CA, MA, and PA are lists of equal size and CB, MB, and PB are lists of equal size.
The developer rarely has to write @ClassVal or @MethodVal annotations, because the Checker Framework infers them according to Figure 25.3. Most readers can skip this section, which explains the inference rules.
The ClassVal Checker infers the exact class name (@ClassVal) for a Class literal (C.class), and for a static method call (e.g., Class.forName(arg), ClassLoader.loadClass(arg), ...) if the argument is a statically computable expression. In contrast, it infers an upper bound (@ClassBound) for instance method calls (e.g., obj.getClass()).
The MethodVal Checker infers @MethodVal annotations for Method and Constructor types that have been created using a method call to Java’s Reflection API:
Note that an exact class name is necessary to precisely resolve reflectively-invoked constructors since a constructor in a subclass does not override a constructor in its superclass. This means that the MethodVal Checker does not infer a @MethodVal annotation for Class.getConstructor if the type of that class is @ClassBound. In contrast, either an exact class name or a bound is adequate to resolve reflectively-invoked methods because of the subtyping rules for overridden methods.
The Initialized Fields Checker warns if a constructor does not initialize a field.
An example invocation is
javac -processor org.checkerframework.common.initializedfields.InitializedFieldsChecker MyFile.java
If you run it together with other checkers, then it issues warnings only if the default value assigned by Java (0, false, or null) is not consistent with the field’s annotation, for the other checkers. An example invocation is
javac -processor ValueChecker,InitializedFieldsChecker MyFile.java
Without the Initialized Fields Checker, every type system is unsound with respect to fields that are never set. (Exception: The Nullness Checker (Chapter 3) is sound. Also, a type system is sound if every annotation is consistent with 0, false, and null.) Consider the following code:
import org.checkerframework.checker.index.qual.Positive; class MyClass { @Positive int x; MyClass() { // empty body } @Positive int getX() { return x; } }
Method getX is incorrect because it returns 0, which is not positive. However, the code type-checks because there is never an assignment to x whose right-hand side is not positive. If you run the Index Checker together with the Initialized Fields Checker, then the code correctly does not type-check.
Even with the Initialized Fields Checker, every type system (except the Nullness Checker, Chapter 3) is unsound with respect to partially-initialized fields. Consider the following code:
import org.checkerframework.checker.index.qual.Positive; class MyClass { @Positive int x; MyClass() { foo(this); x = 1; } @Positive int foo() { // ... use x, expecting it to be positive ... } }
Within method foo, x can have the value 0 even though the type of x is @Positive int.
As an example, consider the following code:
import org.checkerframework.checker.index.qual.Positive; class MyClass { @Positive int x; @Positive int y; int z; // Warning: field y is not initialized MyClass() { x = 1; } }
When run by itself, the Initialized Fields Checker warns that fields y and field z are not set.
When run together with the Index Checker, the Initialized Fields Checker warns that field y is not set. It does not warn about field z, because its default value (0) is consistent with its annotations.
The Initialized Fields type system uses the following type annotations:
![]()
Figure 26.1: The type qualifier hierarchy of the Initialized Fields Checker. @InitializedFieldsBottom is rarely written by a programmer.
Figure 26.1 shows the subtyping relationships among the type qualifiers.
There is also a method declaration annotation:
The Initialized Fields Checker is a lightweight version of the Initialization Checker (Section 3.8). Here is a comparison between them.
Initialization Checker | Initialized Fields Checker | |
superclasses | tracks initialization of supertype fields | checks one class at a time |
partial initialization | changes the types of fields that are not initialized | unsound treatment of partially-initialized objects (*) |
type systems | works only with the Nullness Checker (**) | works for any type system |
disabling | always runs with the Nullness Checker | can be enabled/disabled per run |
* See Section 26.2 for an example.
** The Initialization Checker could be made to work with any type system, but
doing so would require changing the implementation of both the type system and
the Initialization Checker.
The Aliasing Checker identifies expressions that definitely have no aliases.
Two expressions are aliased when they have the same non-primitive value; that is, they are references to the identical Java object in the heap. Another way of saying this is that two expressions, exprA and exprB, are aliases of each other when exprA == exprB at the same program point.
Assigning to a variable or field typically creates an alias. For example, after the statement a = b;, the variables a and b are aliased.
Knowing that an expression is not aliased permits more accurate reasoning about how side effects modify the expression’s value.
To run the Aliasing Checker, supply the -processor org.checkerframework.common.aliasing.AliasingChecker command-line option to javac. However, a user rarely runs the Aliasing Checker directly. This type system is mainly intended to be used together with other type systems. For example, the SPARTA information flow type-checker (Section 30.20) uses the Aliasing Checker to improve its type refinement — if an expression has no aliases, a more refined type can often be inferred, otherwise the type-checker makes conservative assumptions.
![]()
Figure 27.1: Type hierarchy for the Aliasing type system. These qualifiers are applicable to any reference (non-primitive) type.
There are two possible types for an expression:
The @Unique annotation is only allowed at local variables, method parameters, constructor results, and method returns. A constructor’s result should be annotated with @Unique only if the constructor’s body does not creates an alias to the constructed object.
There are also two annotations, which are currently trusted instead of verified, that can be used on formal parameters (including the receiver parameter, this):
This section lists the expressions that create aliases. These are also called “leaking contexts”.
@Unique Object u = ...; Object o = u; // (not.unique) type-checking error!
If this example type-checked, then u and o would be aliased. For this example to type-check, either the @Unique annotation on the type of u, or the o = u; assignment, must be removed.
Here is an example for argument-passing:
void mightDoAnything(Object o) { ... } @Unique Object u = ...; mightDoAnything(u); // type-checking error, because the callee may create an alias of the passed argument
Passing a non-aliased reference to a method does not necessarily create an alias. However, the body of the method might create an alias or leak the reference. Thus, the Aliasing Checker always treats a method call as creating aliases for each argument unless the corresponding formal parameter is marked as @@NonLeaked or @@LeakedToResult.
Here is an example for a return statement:
Object id(@Unique Object p) { return p; // (not.unique) type-checking error! }
If this code type-checked, then it would be possible for clients to write code like this:
@Unique Object u = ...; Object o = id(u);
after which there is an alias to u even though it is declared as @Unique.
However, it is permitted to write
Object id(@LeakedToResult Object p) { return p; }
after which the following code type-checks:
@Unique Object u = ...; id(u); // method call result is not used Object o1 = ...; Object o2 = id(o1); // argument is not @Unique
void aliasInCatchBlock() { @Unique Exception uex = new Exception(); try { throw uex; // (not.unique) type-checking error! } catch (Exception ex) { // uex and ex refer to the same object here. } }
Array initializers assign the elements in the initializers to corresponding indexes in the array, therefore expressions in an array initializer are leaked.
void aliasInArrayInitializer() { @Unique Object o = new Object(); Object[] ar = new Object[] { o }; // (not.unique) type-checking error! // The expressions o and ar[0] are now aliased. }
The @Unique qualifier may not be written on locations such as fields, array elements, and type parameters.
As an example of why @Unique may not be written on a field’s type, consider the following code:
class MyClass { @Unique Object field; void makesAlias() { MyClass myClass2 = this; // this.field is now an alias of myClass2.field } }
That code must not type-check, because field is declared as @Unique but has an alias. The Aliasing Checker solves the problem by forbidding the @Unique qualifier on subcomponents of a structure, such as fields. Other solutions might be possible; they would be more complicated but would permit more code to type-check.
@Unique may not be written on a type parameter for similar reasons. The assignment
List<@Unique Object> l1 = ...; List<@Unique Object> l2 = l1;
must be forbidden because it would alias l1.get(0) with l2.get(0) even though both have type @Unique. The Aliasing Checker forbids this code by rejecting the type List<@Unique Object>.
Type refinement enables a type checker to treat an expression as a subtype of its declared type. For example, even if you declare a local variable as @MaybeAliased (or don’t write anything, since @MaybeAliased is the default), sometimes the Aliasing Checker can determine that it is actually @Unique. For more details, see Section 32.7.
The Aliasing Checker treats type refinement in the usual way, except that at (pseudo-)assignments the right-hand-side (RHS) may lose its type refinement, before the left-hand-side (LHS) is type-refined. The RHS always loses its type refinement (it is widened to @MaybeAliased, and its declared type must have been @MaybeAliased) except in the following cases:
A consequence of the above rules is that most method calls are treated conservatively. If a variable with declared type @MaybeAliased has been refined to @Unique and is used as an argument of a method call, it usually loses its @Unique refined type.
Figure 27.2 gives an example of the Aliasing Checker’s type refinement rules.
// Annotations on the StringBuffer class, used in the examples below. // class StringBuffer { // @Unique StringBuffer(); // StringBuffer append(@LeakedToResult StringBuffer this, @NonLeaked String s); // } void foo() { StringBuffer sb = new StringBuffer(); // sb is refined to @Unique. StringBuffer sb2 = sb; // sb loses its refinement. // Both sb and sb2 have aliases and because of that have type @MaybeAliased. } void bar() { StringBuffer sb = new StringBuffer(); // sb is refined to @Unique. sb.append("someString"); // sb stays @Unique, as no aliases are created. StringBuffer sb2 = sb.append("someString"); // sb is leaked and becomes @MaybeAliased. // Both sb and sb2 have aliases and because of that have type @MaybeAliased. }
Figure 27.2: Example of the Aliasing Checker’s type refinement rules.
The Must Call Checker conservatively over-approximates the set of methods that an object should call before it is de-allocated. The checker does not enforce any rules other than subtyping; in particular, it does not enforce that the methods are called before objects are de-allocated. The Must Call Checker is intended to be run as a subchecker of another checker. The primary client of the Must Call Checker is the Resource Leak Checker (Section 8), which enforces that every method in a must-call obligation for an expression is called before that expression is de-allocated.
For example, consider a java.io.OutputStream. This OutputStream might have an obligation to call the close() method, to release an underlying file resource. The type of this OutputStream is @MustCall({"close"}). Or, the OutputStream might not have such an obligation, if the underlying resource is a byte array. The type of this OutputStream is @MustCall({}). For an arbitrary OutputStream, the Must Call Checker over-approximates the methods that it should call by assigning it the type @MustCall({"close"}) OutputStream, which can be read as “an OutputStream that might need to call close() (but no other methods) before it is de-allocated”.
If you are running the Resource Leak Checker (Chapter 8), then there is no need to run the Must Call Checker, because the Resource Leak Checker does so automatically. Running both may lead to warning suppressions for the Must Call Checker not working.
The Must Call Checker supports these type qualifiers:
![]()
Figure 28.1: Part of the Must Call Checker’s type qualifier hierarchy. The full hierarchy contains one @MustCall annotation for every combination of methods. Qualifiers in gray are used internally by the type system but should never be written by a programmer.
Here are some facts about the type qualifier hierarchy, which is shown in Figure 28.1. Any expression of type @MustCall({}) Object also has type @MustCall({"foo"}) Object. The type @MustCall({"foo"}) Object contains objects that need to call foo and objects that need to call nothing, but the type does not contain an object that needs to call bar (or both foo and bar). @MustCall({"foo", "bar"}) Object represents all objects that need to call foo, bar, both, or neither; it cannot not represent an object that needs to call some other method.
As explained in Section 32.3, a type annotation on a type declaration means that every use of the type has that annotation by default. If a class A’s declaration has a @MustCall annotation, then A’s subclasses usually should too. To do so, a programmer can either write a @MustCall annotation on every subclass, or can write @InheritableMustCall on only A, which will cause the checker to treat every subclass as if it has an identical @MustCall annotation. The latter is the preferred style.
For example, given the following class annotation:
package java.net; import org.checkerframework.checker.mustcall.qual.InheritableMustCall; @InheritableMustCall({"close"}) class Socket { }
any use of Socket or a subclass of Socket defaults to @MustCall({"close"}) Socket.
The @InheritableMustCall annotation is necessary because type annotations cannot be made inheritable. @InheritableMustCall is a declaration annotation.
The Must Call Checker interacts with the @Owning and @NotOwning annotations of the Resource Leak Checker (Section 8). In particular, any location that the Resource Leak Checker would consider “non-owning” (e.g., unannotated parameters, @NotOwning returns, etc.) is treated as @MustCall() by the Must Call Checker. This can lead to surprising results when running the Must Call Checker alone (which is not recommended). For example, consider the following program:
@MustCall({"toString"}) String foo(@MustCall({"hashCode"}) String arg) { return arg; }
The Must Call Checker will not issue a warning about this code, because arg is non-owning (and therefore treated as @MustCall()).
The Must Call Checker is unsound with respect to reflection: it assumes that objects instantiated reflectively do not have must-call obligations (i.e., that their type is @MustCall({}). If the checker is run with -AresolveReflection, then this assumption applies only to types that cannot be resolved.
In an unannotated program, there may be mismatches between defaulted type qualifiers that lead to type-checking errors. That is, the defaulted annotations at two different locations may be different and in conflict. A specific example is in code that contains a mix of explicit upper bounds with an extends clause and implicit upper bounds without an extends clause.
For example, consider the following example (from plume-util) of an interface with explicit upper bounds and a client with implicit upper bounds:
interface Partition<ELEMENT extends @Nullable Object, CLASS extends @Nullable Object> {} class MultiRandSelector<T> { private Partition<T, T> eq; }
The code contains no @MustCall annotations. Running the Must Call Checker on this code produces an error at each use of T in MultiRandSelector:
error: [type.argument] incompatible type argument for type parameter ELEMENT of Partitioner. private Partitioner<T, T> eq; ^ found : T extends @MustCallUnknown Object required: @MustCall Object error: [type.argument] incompatible type argument for type parameter CLASS of Partitioner. private Partitioner<T, T> eq; ^ found : T extends @MustCallUnknown Object required: @MustCall Object
The defaulted Must Call annotations differ. Partitioner has an explicit bound, which uses the usual default of @MustCall({}). MultiRandSelector has an implicit bound, which defaults to top (that is, @MustCallUnknown), as explained in Section 32.5.3.
In many cases, including this one, you can eliminate the false positive warning without writing any @MustCall annotations. You can either:
The Subtyping Checker enforces only subtyping rules. It operates over annotations specified by a user on the command line. Thus, users can create a simple type-checker without writing any code beyond definitions of the type qualifier annotations.
The Subtyping Checker can accommodate all of the type system enhancements that can be declaratively specified (see Chapter 36). This includes type introduction rules via the @QualifierForLiterals meta-annotation, and other features such as type refinement (Section 32.7) and qualifier polymorphism (Section 31.2).
The Subtyping Checker is also useful to type system designers who wish to experiment with a checker before writing code; the Subtyping Checker demonstrates the functionality that a checker inherits from the Checker Framework.
If you need typestate analysis, then you can extend a typestate checker. For more details (including a definition of “typestate”), see Chapter 30.30. See Section 39.7.1 for a simpler alternative.
For type systems that require special checks (e.g., warning about dereferences of possibly-null values), you will need to write code and extend the framework as discussed in Chapter 36.
The Subtyping Checker is used in the same way as other checkers (using the -processor org.checkerframework.common.subtyping.SubtypingChecker option; see Chapter 2), except that it requires an additional annotation processor argument via the standard “-A” switch. You must provide one or both of the two following command-line arguments:
javac -classpath /full/path/to/myProject/bin:/full/path/to/myLibrary/bin \ -processor org.checkerframework.common.subtyping.SubtypingChecker \ -Aquals=myPackage.qual.MyQual,myPackage.qual.OtherQual MyFile.java ...
javac -classpath /full/path/to/myProject/bin:/full/path/to/myLibrary/bin \ -processor org.checkerframework.common.subtyping.SubtypingChecker \ -AqualDirs=/full/path/to/myProject/bin:/full/path/to/myLibrary/bin MyFile.java
If the Checker Framework is on the processorpath, place the annotations on the processorpath instead of on the classpath.
The annotations listed in -Aquals or -AqualDirs must be accessible to the compiler during compilation. Before you run the Subtyping Checker with javac, they must be compiled and on the same path (the classpath or processorpath) as the Checker Framework. It is not sufficient to supply their source files on the command line.
When suppressing a warning issued by the Subtyping Checker, as the “checker name” you may use the unqualified, uncapitalized name of any of the annotations passed to -Aquals. (See Section 33.1.1 for details about the @SuppressWarnings syntax.) As a matter of style, you should choose one of the annotations as the “checker name” part of the @SuppressWarnings string and use it consistently; this avoids confusion and makes it easier to search for uses.
Consider a hypothetical Encrypted type qualifier, which denotes that the representation of an object (such as a String, CharSequence, or byte[]) is encrypted. To use the Subtyping Checker for the Encrypted type system, follow three steps.
(This example also appears in the Checker Framework Tutorial, in more detail.)
package myPackage.qual;
import org.checkerframework.framework.qual.DefaultFor;
import org.checkerframework.framework.qual.SubtypeOf;
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
/**
* Denotes that the representation of an object is encrypted.
*/
@SubtypeOf(PossiblyUnencrypted.class)
@DefaultFor({TypeUseLocation.LOWER_BOUND})
@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER})
public @interface Encrypted {}
package myPackage.qual;
import org.checkerframework.framework.qual.DefaultQualifierInHierarchy;
import org.checkerframework.framework.qual.SubtypeOf;
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
/**
* Denotes that the representation of an object might not be encrypted.
*/
@DefaultQualifierInHierarchy
@SubtypeOf({})
@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER})
public @interface PossiblyUnencrypted {}
Note that all custom annotations must have the @Target(ElementType.TYPE_USE) meta-annotation. See Section 36.5.1.
Don’t forget to compile these classes:
$ javac myPackage/qual/Encrypted.java myPackage/qual/PossiblyUnencrypted.java
The resulting .class files should either be on the same path (the classpath or processor path) as the Checker Framework.
import myPackage.qual.Encrypted;
...
public @Encrypted String encrypt(String text) {
// ...
}
// Only send encrypted data!
public void sendOverInternet(@Encrypted String msg) {
// ...
}
void sendText() {
// ...
@Encrypted String ciphertext = encrypt(plaintext);
sendOverInternet(ciphertext);
// ...
}
void sendPassword() {
String password = getUserPassword();
sendOverInternet(password);
}
You may also need to add @SuppressWarnings annotations to the encrypt and decrypt methods. Analyzing them is beyond the capability of any realistic type system.
javac -processorpath myqualpath -processor org.checkerframework.common.subtyping.SubtypingChecker -Aquals=myPackage.qual.Encrypted,myPackage.qual.PossiblyUnencrypted YourProgram.java YourProgram.java:42: incompatible types. found : @myPackage.qual.PossiblyUnencrypted java.lang.String required: @myPackage.qual.Encrypted java.lang.String sendOverInternet(password); ^
javac -classpath /full/path/to/myProject/bin:/full/path/to/myLibrary/bin \ -processor org.checkerframework.common.subtyping.SubtypingChecker \ -AqualDirs=/full/path/to/myProject/bin:/full/path/to/myLibrary/bin YourProgram.java
Note that in these two examples, the compiled class file of the myPackage.qual.Encrypted and myPackage.qual.PossiblyUnencrypted annotations must exist in either the myProject/bin directory or the myLibrary/bin directory. The following placement of the class files will work with the above commands:
.../myProject/bin/myPackage/qual/Encrypted.class .../myProject/bin/myPackage/qual/PossiblyUnencrypted.class
Also, see the example project in the docs/examples/subtyping-extension directory.
A type alias or typedef is a type that shares the same representation as another type but is conceptually distinct from it. For example, some strings in your program may be street addresses; others may be passwords; and so on. You wish to indicate, for each string, which one it is, and to avoid mixing up the different types of strings. Likewise, you could distinguish integers that are offsets from those that are absolute values.
Creating a new type makes your code easier to understand by conveying the intended use of each variable. It also prevents errors that come from using the wrong type or from mixing incompatible types in an operation.
If you want to create a type alias or typedef, you have multiple options: a regular Java subtype, the Units Checker (Chapter 20), the Fake Enum Checker (Chapter 9), or the Subtyping Checker.
A Java subtype is easy to create and does not require a tool such as the Checker Framework; for instance, you would declare class Address extends String. There are a number of limitations to this “pseudo-typedef”, however [Goe06]. Primitive types and final types (including String) cannot be extended. Equality and identity tests can return incorrect results when a wrapper object is used. Existing return types in code would need to be changed, which is easy with an annotation but disruptive to change the Java type. Therefore, it is best to avoid the pseudo-typedef antipattern.
The Units Checker (Chapter 20) is useful for the particular case of units of measurement, such as kilometers verses miles.
The Fake Enum Checker (Chapter 9) builds in a set of assumptions. If those fit your use case, then it’s easiest to use the Fake Enum Checker (though you can achieve them using the Subtyping Checker). The Fake Enum Checker forbids mixing of fenums of different types, or fenums and unannotated types. For instance, binary operations other than string concatenations are forbidden, such as NORTH+1, NORTH+MONDAY, and NORTH==MONDAY. However, NORTH+SOUTH is permitted.
By default, the Subtyping Checker does not forbid any operations.
If you choose to use the Subtyping Checker, then you have an additional design choice to make about the type system. In the general case, your type system will look something like Figure 29.1.
References whose type is @MyType are known to store only values from your new type. There is no such guarantee for @MyTypeUnknown and @NotMyType, but those types mean different things. An expression of type @NotMyType is guaranteed never to evaluate to a value of your new type. An expression of type @MyTypeUnknown may evaluate to any value — including values of your new type and values not of your new type. (@MyTypeBottom is the type of null and is also used for dead code and erroneous situations; it can be ignored for this discussion.)
A key choice for the type system designer is which type is the default. That is, if a programmer does not write @MyType on a given type use, should that type use be interpreted as @MyTypeUnknown or as @NotMyType?
A downside of the stronger guarantee that comes from using @NotMyType as the default is the need to write additional annotations. For example, if @NotMyType is the default, this code does not typecheck:
void method(Object o) { ... } <U> void use(List<U> list) { method(list.get(0)); }
Because (implicit) upper bounds are interpreted as the top type (see Section 31.1.2), this is interpreted as
void method(@NotMyType Object o) { ... } <@U extends @MyTypeUnknown Object> void use(List<U> list) { // type error: list.get(0) has type @MyTypeUnknown, method expects @NotMyType method(list.get(0)); }
To make the code type-check, it is necessary to write an explicit annotation, either to restrict use’s argument or to expand method’s parameter type.
The Checker Framework has been used to build other checkers that are not distributed together with the framework. This chapter gives an incomplete list of them. (If you know of others, or if you want this chapter to reference your checker, please send us a link and a short description.) Many of the publications in Section 40.7 provide implementations, which are not necessarily listed here yet.
These tools are externally-maintained, so if you have problems or questions, you should contact their maintainers rather than the Checker Framework maintainers.
This list is in reverse chronological order; newer tools appear first and older ones appear last.
The Determinism Checker ensures that a program is deterministic across executions. A determinismic program is easier to test, and it is easier to debug (such as comparing executions).
The Determinism Checker focuses on sequential programs. It detects determinism due to the iteration order of a hash table (or on any other property of hash codes), default formatting (such as Java’s Object.toString(), which includes a memory address), random, date-and-time functions, and accessing system properties such as the file system or environment variables.
The paper “Verifying determinism in sequential programs” [MWME21] describes the Determinism Checker.
Interval analysis estimates the run-time values of numerical expressions in the source code by computing a lower bound and an upper bound. The Constant Value Inference project is a whole-program inference approach for integral range analysis (interval analysis).
The thesis “Interval Type Inference: Improvements and Evaluations” [Wan21] describes Constant Value Inference.
The Crypto Checker can help you find whether there are any weak or unsupported crypto algorithms and the unsupported algorithm providers being used in your program. If the Crypto Checker issues no warnings for a given program, then you have a guarantee that your program at runtime will never have these issues. This is similar to the earlier AWS Crypto Policy Compliance Checker (Section 30.4).
The paper “Ensuring correct cryptographic algorithm and provider usage at compile time” [XCD21] (FTFjP 2021) and the thesis “Light-weight verification of cryptographic API usage” [Xin20] describe the Crypto Checker.
The AWS crypto policy compliance checker checks that no weak cipher algorithms are used with the Java crypto API.
The paper “Continuous Compliance” [KSTE20] (ASE 2020) describes several custom Checker Framework checkers in the context of a compliance regime at Amazon Web Services.
The AWS KMS compliance checker extends the Constant Value Checker (see Chapter 23) to enforce that calls to Amazon Web Services’ Key Management System only request 256-bit (or longer) data keys. This checker can be used to help enforce a compliance requirement (such as from SOC or PCI-DSS) that customer data is always encrypted with 256-bit keys.
The KMS compliance checker is available in Maven Central. To use it in build.gradle, add the following dependency:
compile group: 'software.amazon.checkerframework', name: 'aws-kms-compliance-checker', version: '1.0.2'
Other build systems are similar.
The paper “Continuous Compliance” [KSTE20] (ASE 2020) describes several custom Checker Framework checkers in the context of a compliance regime at Amazon Web Services.
The PUnits system can check the correctness of a program or annotate a program with units of measurement. It is described in the paper “Precise inference of expressive units of measurement types” (OOPSLA 2020) [XLD20].
JaTyC, Java Typestate Checker, ensures that methods are called in the correct order. The sequences of method calls allowed are specified in a protocol file which is associated with a Java class by adding a @Typestate annotation to the class. It is described in “Behavioural Types for Memory and Method Safety in a Core Object-Oriented Language” [BFG+20], and the implementation is an extension of Mungo (https://www.dcs.gla.ac.uk/research/mungo/index.html).
NullAway is a fast type-checker for only nullness properties. It is built on top of Error Prone and the Checker Framework’s Dataflow Framework. See Section 39.10.3 for a comparison between NullAway and the Nullness Checker. NullAway is described in the paper “NullAway: Practical type-based null safety for Java” [BCS19].
The Nullness Rawness Checker is a variant of the Nullness Checker that uses a different type system for initialization. It was distributed with the Checker Framework through release 2.9.0 (dated 3 July 2019). If you wish to use it, install Checker Framework version 2.9.0.
The paper “Inference of field initialization” [SE11] describes inference for the Rawness Initialization Checker.
The Rx Thread & Effect Checker enforces UI Thread safety properties for stream-based Android applications. The paper “Safe Stream-Based Programming with Refinement Types” [SCSC18] describes the Rx Thread & Effect Checker.
Practical Immutability For Classes And Objects (PICO) is a type system that supports class level and object level immutability based on Checker Framework. The implementation is available at https://github.com/opprop/immutability, and a docker image is available at https://hub.docker.com/repository/docker/lnsun/pico.
The theses “Context Sensitive Typechecking And Inference: Ownership And Immutability” [Ta18] and “An Immutability Type System for Classes and Objects: Improvements, Experiments, and Comparisons” [Sun21] describe PICO.
The InputStream.read() and Reader.read() methods read a byte (or character) from a stream. The methods return an int, using -1 to indicate the end of the stream. It is an error to cast the value to a byte (or character) without first checking the value against -1. This is rule CERT-FIO08-J. The Read Checker and/or Cast Checker enforces it.
The paper “Don’t miss the end: Preventing unsafe end-of-file comparisons” [CD18] and the thesis “Pluggable Properties for Program Understanding: Ontic Type Checking and Inference” [Che18] describes the Read Checker.
The Ontology type system groups related types into coarser concepts. For example, the SEQUENCE concept includes Java’s Array, List, and their subtypes. Other examples are VELOCITY and FORCE.
The thesis “Pluggable Properties for Program Understanding: Ontic Type Checking and Inference” [Che18] describes the Ontic type system.
Glacier enforces transitive class immutability in Java. According to its webpage:
Glacier is described by the paper “Glacier: Transitive class immutability for Java” (ICSE 2017) [CNA+17] and the PhD thesis “User-Centered Design of Principled Programming Languages” [Cob20].
jOOQ is a database API that lets you build typesafe SQL queries. jOOQ version 3.0.9 and later ships with a SQL checker that provides even more safety: it ensures that you don’t use SQL features that are not supported by your database implementation. You can learn about the SQL checker at https://blog.jooq.org/jsr-308-and-the-checker-framework-add-even-more-typesafety-to-jooq-3-9/.
Javari [TE05], IGJ [ZPA+07], and OIGJ [ZPL+10] are type systems that enforce immutability constraints. Type-checkers for all three type systems were distributed with the Checker Framework through release 1.9.13 (dated 1 April 2016). If you wish to use them, install Checker Framework version 1.9.13.
They were removed from the main distribution on June 1, 2016 because the implementations were not being maintained as the Checker Framework evolved. The type systems are valuable, and some people found the type-checkers useful. However, we wanted to focus on distributing checkers that are currently being maintained.
IGJ and OIGJ are described in the papers “Object and reference immutability using Java generics” [ZPA+07] (ESEC/FSE 2007) and “Ownership and immutability in generic Java” [ZPL+10] (OOPSLA 2010). The Javari type system is described in “Javari: Adding reference immutability to Java” [TE05] (OOPSLA 2005); for inference, see “Inference of reference immutability” [QTE08] (ECOOP 2008) and “Parameter reference immutability: Formal definition, inference tool, and comparison” [AQKE09] (J.ASE 2009). The paper “Practical pluggable types for Java” [PAC+08] (ISSTA 2008) describes case studies in which the Javari and IGJ Checkers found previously-unknown errors in real software.
JCrypt is a static program analysis which transforms a Java program into an equivalent one, so that it performs computation over encrypted data and preserves data confidentiality. It is described in the paper “JCrypt: Towards computation over encrypted data” [DMD16a].
DroidInfer determines the Android library sources (e.g., location access, phone state) and sinks (e.g., Internet access) used by a program, and determines whether there is impermissible information flow between them. It is described in the paper “Scalable and Precise Taint Analysis for Android” (ISSTA 2015) [HDMD15].
Error Prone is a linter or bug detector for Java. It reports violations of style rules. It is built on top of the Checker Framework’s Dataflow Framework. See Section 39.10.1 for a comparison between Error Prone and the Nullness Checker.
SPARTA is a security toolset aimed at preventing malware from appearing in an app store. SPARTA provides an information-flow type-checker that is customized to Android but can also be applied to other domains. The paper “Collaborative verification of information flow for a high-assurance app store” [EJM+14] (CCS 2014) describes the SPARTA toolset and information flow type-checker.
SFlow is a context-sensitive type system for secure information flow. It contains two variants, SFlow/Integrity and SFlow/Confidentiality. SFlowInfer is its worst-case-cubic inference analysis.
The paper “Type-based taint analysis for Java web applications” (FASE 2014) and the thesis “An Inference And Checking Framework For Context-Sensitive Pluggable Types” (PhD thesis, 2014) describe SFlow and SFlowInfer.
CheckLT uses taint tracking to detect illegal information flows, such as unsanitized data that could result in a SQL injection attack.
EnerJ is an extension to Java that exposes hardware faults in a safe, principled manner to save energy with only slight sacrifices to the quality of service. More details appear in the paper “EnerJ: Approximate Data Types for Safe and General Low-Power Computation” [SDF+11] (PLDI 2011).
A checker and inference tool for ReIm [HMDE12, Hua14], an immutability type system, is available at https://github.com/proganalysis/type-inference.
The “SFlow × ReIm” system combines information flow and reference immutability. It is described in the paper “Composing polymorphic information flow systems with reference immutability” [MH13].
A checker for Generic Universe Types [DEM11], a lightweight ownership type system, is available from https://ece.uwaterloo.ca/~wdietl/ownership/ and https://github.com/opprop/universe.
The paper “Tunable static inference for Generic Universe Types” [DEM11] (ECOOP 2011) describes inference for the Generic Universe Types type system. Another implementation of Universe Types and ownership types is described in “Inference and checking of object ownership” [HDME12] (ECOOP 2012).
A checker for Safety-Critical Java (SCJ, JSR 302) [TPV10, TPNV11] is available at https://sss.cs.purdue.edu/projects/oscj/checker/checker.html. Developer resources are available at the project page https://code.google.com/archive/p/scj-jsr302/.
Loci is a checker for thread locality. For more details, see the paper “Loci: Simple thread-locality for Java” [WPM+09] (ECOOP 2009) and the thesis “The Design, Implementation and Evaluation of a Pluggable Type Checker for Thread-Locality in Java” [She11].
A checker for units and dimensions is available at https://www.lexspoon.org/expannots/.
Unlike the Units Checker that is distributed with the Checker Framework (see Section 20), this checker includes dynamic checks and permits annotation arguments that are Java expressions. This added flexibility, however, requires that you use a special version both of the Checker Framework and of the javac compiler.
In a regular type system, a variable has the same type throughout its scope. In a typestate system, a variable’s type can change as operations are performed on it.
The most common example of typestate is for a File object. Assume a file can be in two states, @Open and @Closed. Calling the close() method changes the file’s state. Any subsequent attempt to read, write, or close the file will lead to a run-time error. It would be better for the type system to warn about such problems, or guarantee their absence, at compile time.
Accumulation analysis (Chapter 37) is a special case of typestate analysis. One instantiation of it is the Called Methods Checker (Chapter 7), which can check any property of the form “call method A before method B”. It also ensures that builders are used correctly.
JaTyC (Section 30.7) is a Java typestate checker.
The Checker Framework’s flow-sensitive type refinement (Section 32.7) implements a form of typestate analysis. For example, after code that tests a variable against null, the Nullness Checker (Chapter 3) treats the variable’s type as @NonNull T, for some T.
For many type systems, flow-sensitive type refinement is sufficient. But sometimes, you need full typestate analysis. This section compares the two. (Unused variables (Section 32.10) also have similarities with typestate analysis and can occasionally substitute for it. For brevity, this discussion omits them.)
A typestate analysis is easier for a user to create or extend. Flow-sensitive type refinement is built into the Checker Framework and is optionally extended by each checker. Modifying the rules requires writing Java code in your checker. By contrast, it is possible to write a simple typestate checker declaratively, by writing annotations on the methods (such as close()) that change a reference’s typestate.
A typestate analysis can change a reference’s type to something that is not consistent with its original definition. For example, suppose that a programmer decides that the @Open and @Closed qualifiers are incomparable — neither is a subtype of the other. A typestate analysis can specify that the close() operation converts an @Open File into a @Closed File. By contrast, flow-sensitive type refinement can only give a new type that is a subtype of the declared type — for flow-sensitive type refinement to be effective, @Closed would need to be a child of @Open in the qualifier hierarchy (and close() would need to be treated specially by the checker).
Section 31.1 describes support for Java generics (also known as “parametric polymorphism”). Section 31.2 describes polymorphism over type qualifiers for methods. Section 31.3 describes polymorphism over type qualifiers for classes.
The Checker Framework fully supports type-qualified Java generic types and methods (also known as “parametric polymorphism”). When instantiating a generic type, clients supply the qualifier along with the type argument, as in List<@NonNull String>. When using a type variable T within the implementation of a generic type, typically no type qualifier is written (see Section 31.1.3); rather, the instantiation of the type parameter is restricted (see Section 31.1.2).
Before running any pluggable type-checker, we recommend that you eliminate raw types from your code (e.g., your code should use List<...> as opposed to List). Your code should compile without warnings when using the standard Java compiler and the -Xlint:unchecked -Xlint:rawtypes command-line options. Using generics helps prevent type errors just as using a pluggable type-checker does, and makes the Checker Framework’s warnings easier to understand.
If your code uses raw types, then the Checker Framework will do its best to infer the Java type arguments and the type qualifiers. By default these inferred types are ignored in subtyping checks. If you supply the command-line option -AignoreRawTypeArguments=false you will see errors from raw types.
When you define a generic class in Java, the extends clause
of the generic type parameter (known as the “upper bound”) requires that
the corresponding type argument must be a subtype of the bound.
For example, given the definition
class G<T extends Number> {...}
,
the upper bound is Number
and a client can instantiate it as G<Number> or G<Integer>
but not G<Date>.
You can write a type qualifier on the extends clause to make the upper bound a qualified type. For example, you can declare that a generic list class can hold only non-null values:
class MyList<T extends @NonNull Object> {...} MyList<@NonNull String> m1; // OK MyList<@Nullable String> m2; // error
That is, in the above example, all arguments that replace T in MyList<T> must be subtypes of @NonNull Object.
Conceptually, each generic type parameter has two bounds — a lower bound and an upper bound — and at instantiation, the type argument must be within the bounds. Java only allows you to specify the upper bound; the lower bound is implicitly the bottom type void. The Checker Framework gives you more power: you can specify both an upper and lower bound for type parameters. Write the upper bound on the extends clause, and write the lower bound on the type variable.
class MyList<@LowerBound T extends @UpperBound Object> { ... }
You may omit either the upper or the lower bound, and the Checker Framework will use a default.
For a discussion of wildcards, see Section 31.1.4.
For a concrete example, consider the type system of the Regex Checker (see Figure 14.1) in which @Regex(0) :> @Regex(1) :> @Regex(2) :> @Regex(3) :> ….
class MyRegexes<@Regex(5) T extends @Regex(1) String> { ... } MyRegexes<@Regex(0) String> mu; // error - @Regex(0) is not a subtype of @Regex(1) MyRegexes<@Regex(1) String> m1; // OK MyRegexes<@Regex(3) String> m3; // OK MyRegexes<@Regex(5) String> m5; // OK MyRegexes<@Regex(6) String> m6; // error - @Regex(6) is not a supertype of @Regex(5)
The above declaration states that the upper bound of the type variable is @Regex(1) String and the lower bound is @Regex(5) void. That is, arguments that replace T in MyList<T> must be subtypes of @Regex(1) String and supertypes of @Regex(5) void. Since void cannot be used to instantiate a generic class, MyList may be instantiated with @Regex(1) String through @Regex(5) String.
To specify an exact bound, place the same annotation on both bounds. For example:
class MyListOfNonNulls<@NonNull T extends @NonNull Object> { ... } class MyListOfNullables<@Nullable T extends @Nullable Object> { ... } MyListOfNonNulls<@NonNull Number> v1; // OK MyListOfNonNulls<@Nullable Number> v2; // error MyListOfNullables<@NonNull Number> v4; // error MyListOfNullables<@Nullable Number> v3; // OK
It is an error if the lower bound is not a subtype of the upper bound.
class MyClass<@Nullable T extends @NonNull Object> // error: @Nullable is not a subtype of @NonNull
A generic type parameter or wildcard is written as class MyClass<@LowerBound T extends @UpperBound JavaUpperBound> or as MyClass<@UpperBound ? super @LowerBound JavaLowerBound>, where “@LowerBound” and “@UpperBound” are type qualifiers.
For lower bounds: If no type annotation is written in front of ?, then the lower bound defaults to @BottomType void.
For upper bounds:
The upper-bound rules mean that even though in Java the following two declarations are equivalent:
class MyClass<T> class MyClass<T extends Object>
they specify different type qualifiers on the upper bound, if the type system’s default annotation is not its top annotation.
The Nullness type system is an example.
class MyClass<T> == class MyClass<T extends @Nullable Object> class MyClass<T extends Object> == class MyClass<T extends @NonNull Object>
The rationale for this choice is:
Here are some style guidelines:
For further discussion, see Section 39.7.2.
A type annotation on a use of a generic type variable overrides/ignores any type qualifier (in the same type hierarchy) on the corresponding actual type argument. For example, suppose that T is a formal type parameter. Then using @Nullable T within the scope of T applies the type qualifier @Nullable to the (unqualified) Java type of T. This feature is sometimes useful, but more often the implementation of a generic type just uses the type variable T, whose instantiation is restricted (see Section 31.1.2).
Here is an example of applying a type annotation to a generic type variable:
class MyClass2<T> { ... @Nullable T myField = null; ... }
The type annotation does not restrict how MyClass2 may be instantiated. In other words, both MyClass2<@NonNull String> and MyClass2<@Nullable String> are legal, and in both cases @Nullable T means @Nullable String. In MyClass2<@Interned String>, @Nullable T means @Nullable @Interned String.
Defaulting never affects a use of a type variable, even if the type variable use has no explicit annotation. Defaulting helps to choose a single type qualifier for a concrete Java class or interface. By contrast, a type variable use represents a set of possible types.
At an instantiation of a generic type, a Java wildcard indicates that some constraints are known on the type argument, but the type argument is not known exactly. For example, you can indicate that the type parameter for variable ls is some unknown subtype of CharSequence:
List<? extends CharSequence> ls; ls = new ArrayList<String>(); // OK ls = new ArrayList<Integer>(); // error: Integer is not a subtype of CharSequence
For more details about wildcards, see the Java tutorial on wildcards or JLS §4.5.1.
You can write a type annotation on the bound of a wildcard:
List<? extends @NonNull CharSequence> ls; ls = new ArrayList<@NonNull String>(); // OK ls = new ArrayList<@Nullable String>(); // error: @Nullable is not a subtype of @NonNull
Conceptually, every wildcard has two bounds — an upper bound and a lower bound. Java only permits you to write one bound. You can specify the upper bound with <? extends SomeType>, in which case the lower bound is implicitly the bottom type void. You can specify the lower bound (with <? super OtherType>), in which case the upper bound is implicitly the top type Object. The Checker Framework is more flexible: it lets you similarly write annotations on both the upper and lower bound.
To annotate the implicit bound, write the type annotation before the ?. For example:
List<@LowerBound ? extends @UpperBound CharSequence> lo; List<@UpperBound ? super @NonNull Number> ls;
For an unbounded wildcard (<?>, with neither bound specified), the annotation in front of a wildcard applies to both bounds. The following three declarations are equivalent (except that you cannot write the bottom type void; note that Void does not denote the bottom type):
List<@NonNull ?> lnn; List<@NonNull ? extends @NonNull Object> lnn; List<@NonNull ? super @NonNull void> lnn;
Note that the annotation in front of a type parameter always applies to its lower bound, because type parameters can only be written with extends and never super.
The defaulting rules for wildcards also differ from those of type parameters (see Section 32.5.5).
Recall that @Nullable X is a supertype of @NonNull X, for any X. Most of the following types mean different things:
class MyList1<@Nullable T> { ... } class MyList1a<@Nullable T extends @Nullable Object> { ... } // same as MyList1 class MyList2<@NonNull T extends @NonNull Object> { ... } class MyList2a<T extends @NonNull Object> { ... } // same as MyList2 class MyList3<T extends @Nullable Object> { ... }
MyList1 and MyList1a must be instantiated with a nullable type. The implementation of MyList1 must be able to consume (store) a null value and produce (retrieve) a null value.
MyList2 and MyList2a must be instantiated with non-null type. The implementation of MyList2 has to account for only non-null values — it does not have to account for consuming or producing null.
MyList3 may be instantiated either way: with a nullable type or a non-null type. The implementation of MyList3 must consider that it may be instantiated either way — flexible enough to support either instantiation, yet rigorous enough to impose the correct constraints of the specific instantiation. It must also itself comply with the constraints of the potential instantiations.
One way to express the difference among MyList1, MyList2, and MyList3 is by comparing what expressions are legal in the implementation of the list — that is, what expressions may appear in the ellipsis in the declarations above, such as inside a method’s body. Suppose each class has, in the ellipsis, these declarations:
T t; @Nullable T nble; // Section "Type annotations on a use of a generic type variable", below, @NonNull T nn; // further explains the meaning of "@Nullable T" and "@NonNull T". void add(T arg) {} T get(int i) {}
Then the following expressions would be legal, inside a given implementation — that is, also within the ellipses. (Compilable source code appears as file checker-framework/checker/tests/nullness/generics/GenericsExample.java.)
MyList1 | MyList2 | MyList3 | |
t = null; | OK | error | error |
t = nble; | OK | error | error |
nble = null; | OK | OK | OK |
nn = null; | error | error | error |
t = this.get(0); | OK | OK | OK |
nble = this.get(0); | OK | OK | OK |
nn = this.get(0); | error | OK | error |
this.add(t); | OK | OK | OK |
this.add(nble); | OK | error | error |
this.add(nn); | OK | OK | OK |
The differences are more significant when the qualifier hierarchy is more complicated than just @Nullable and @NonNull.
Java types are invariant in their type parameter. This means that A<X> is a subtype of B<Y> only if X is identical to Y. For example, ArrayList<Number> is a subtype of List<Number>, but neither ArrayList<Integer> nor List<Integer> is a subtype of List<Number>. (If they were, there would be a loophole in the Java type system.) For the same reason, type parameter annotations are treated invariantly. For example, List<@Nullable String> is not a subtype of List<String>.
When a type parameter is used in a read-only way — that is, when clients read values of that type from the class but never pass values of that type to the class — then it is safe for the type to be covariant in the type parameter. Use the @Covariant annotation to indicate this. When a type parameter is covariant, two instantiations of the class with different type arguments have the same subtyping relationship as the type arguments do.
For example, consider Iterator. A client can read elements but not write them, so Iterator<@Nullable String> can be a subtype of Iterator<String> without introducing a hole in the type system. Therefore, its type parameter is annotated with @Covariant. The first type parameter of Map.Entry is also covariant. Another example would be the type parameter of a hypothetical class ImmutableList.
The @Covariant annotation is trusted but not checked. If you incorrectly specify as covariant a type parameter that can be written (say, the class supports a set operation or some other mutation on an object of that type), then you have created an unsoundness in the type system. For example, it would be incorrect to annotate the type parameter of ListIterator as covariant, because ListIterator supports a set operation.
Sometimes method type argument inference does not interact well with type qualifiers. In such situations, you might need to provide explicit method type arguments, for which the syntax is as follows:
Collections.<@MyTypeAnnotation Object>sort(l, c);
This uses Java’s existing syntax for specifying a method call’s type arguments.
Many type systems have a *Bottom type that is used only for the null value, dead code, and some erroneous situations. A programmer should rarely write the bottom type.
One use is on a lower bound, to indicate that any type qualifier is permitted. A lower-bounded wildcard indicates that a consumer method can accept a collection containing any Java type above some Java type, and you can add the bottom type qualifier as well:
public static void addNumbers(List<? super @SignednessBottom Integer> list) { ... }
Type qualifier polymorphism permits a single method to have multiple different qualified type signatures.
Here is where a polymorphic qualifier (e.g., @PolyNull) can be used:
Section 36.5.2 explains how to define a polymorphic qualifier.
A method whose signature has a polymorphic qualifier (such as @PolyNull) conceptually has multiple versions, somewhat like the generics feature of Java or a template in C++. In each version, each instance of the polymorphic qualifier has been replaced by the same other qualifier from the hierarchy.
The method body must type-check with all signatures. A method call is type-correct if it type-checks under any one of the signatures. If a call matches multiple signatures, then the compiler uses the most specific matching signature for the purpose of type-checking. This is the same as Java’s rule for resolving overloaded methods.
As an example of the use of @PolyNull, method Class.cast returns null if and only if its argument is null:
@PolyNull T cast(@PolyNull Object obj) { ... }
This is like writing:
@NonNull T cast( @NonNull Object obj) { ... } @Nullable T cast(@Nullable Object obj) { ... }
except that the latter is not legal Java, since it defines two methods with the same Java signature.
As another example, consider
// Returns null if either argument is null. @PolyNull T max(@PolyNull T x, @PolyNull T y);
which is like writing
@NonNull T max( @NonNull T x, @NonNull T y); @Nullable T max(@Nullable T x, @Nullable T y);
At a call site, the most specific applicable signature is selected.
Another way of thinking about which one of the two max variants is selected is that the nullness annotations of (the declared types of) both arguments are unified to a type that is a supertype of both, also known as the least upper bound or lub. If both arguments are @NonNull, their unification (lub) is @NonNull, and the method return type is @NonNull. But if even one of the arguments is @Nullable, then the unification (lub) is @Nullable, and so is the return type.
Qualifier polymorphism has the same purpose and plays the same role as Java’s generics. You use them for the similar reasons, such as:
If a method is written using Java generics, it usually does not need qualifier polymorphism. If you can use Java’s generics, then that is often better. On the other hand, if you have legacy code that is not written generically, and you cannot change it to use generics, then you can use qualifier polymorphism to achieve a similar effect, with respect to type qualifiers only. The Java compiler still treats the base Java types non-generically.
In some cases, you don’t need qualifier polymorphism because subtyping already provides the needed functionality. String is a supertype of @Interned String, so a method toUpperCase that is declared to take a String parameter can also be called on an @Interned String argument.
Usually, it does not make sense to write only a single instance of a polymorphic qualifier in a method definition: if you write one instance of (say) @PolyNull, then you should use at least two. The main benefit of polymorphic qualifiers comes when one is used multiple times in a method, since then each instance turns into the same type qualifier. (Section 31.2.4 describes some exceptions to this rule: times when it makes sense to write a single polymorphic qualifier in a signature.)
Most frequently, the polymorphic qualifier appears on at least one formal parameter and also on the return type.
It can also be useful to have polymorphic qualifiers on (only) multiple formal parameters, especially if the method side-effects one of its arguments. For example, consider
void moveBetweenStacks(Stack<@PolyNull Object> s1, Stack<@PolyNull Object> s2) { s1.push(s2.pop()); }
In this particular example, it would be cleaner to rewrite your code to use Java generics, if you can do so:
<T> void moveBetweenStacks(Stack<T> s1, Stack<T> s2) { s1.push(s2.pop()); }
As explained in Section 31.2.3, you will usually use a polymorphic qualifier multiple times in a signature. This section describes situations when it makes sense to write just one polymorphic qualifier in a method signature. Some of these situations can be avoided by writing a generic method, but in legacy code it may not be possible for you to change a method to be generic.
It is unusual, but permitted, to write just one polymorphic qualifier, on a return type.
This is just like it is unusual, but permitted, to write just one occurrence of a generic type parameter, on a return type. An example of such a method is Collections.emptyList().
It can make sense to use a polymorphic qualifier just once, on an array or generic element type.
For example, consider a routine that returns the index, in an array, of a given element:
public static int indexOf(@PolyNull Object[] a, @Nullable Object elt) { ... }
If @PolyNull were replaced with either @Nullable or @NonNull, then one of these safe client calls would be rejected:
@Nullable Object[] a1; @NonNull Object[] a2; indexOf(a1, someObject); indexOf(a2, someObject);
Of course, it would be better style to use a generic method, as in either of these signatures:
public static <T extends @Nullable Object> int indexOf(T[] a, @Nullable Object elt) { ... } public static <T extends @Nullable Object> int indexOf(T[] a, T elt) { ... }
Another example is a method that writes bytes to a file. It accepts an array of signed or unsigned bytes, and it behaves identically for both:
void write(@PolySigned byte[] b) { ... }
These examples use arrays, but there are similar examples that use collections.
There is no point to writing just one polymorphic qualifier in a method signature, as the main type qualifier on a formal parameter type. Consider this signature:
void m(@PolyNull Object obj)
which expands to
void m(@NonNull Object obj) void m(@Nullable Object obj)
This is no different (in terms of which calls to the method will type-check) than writing just
void m(@Nullable Object obj)
However, it does make sense to write a single polymorphic qualifier within a formal parameter type, as in
void m2a(List<@PolyNull MySubClass> strings)
which is similar to
void m2b(List<? extends @Nullable MySubClass> strings)
Method m2b() can be called on (for example) List<@Nullable MySubClass> and List<@NonNull MySubClass>, which are not legal arguments to method m2a().
Class qualifier parameters permit you to supply a type qualifier (only, without a Java basetype) to any class (generic or not).
When a generic class represents a collection, a user can write a type qualifier on the type argument, as in List<@Tainted Character> versus List<@Untainted Character>. When a non-generic class represents a collection with a hard-coded type (as StringBuffer hard-codes Character), you can use a class qualifier parameter to distinguish StringBuffers that contain different types of characters.
To add a qualifier parameter to a class, annotate its declaration with @HasQualifierParameter and write the class of the top qualifier as its element.
@HasQualifierParameter(Tainted.class) class StringBuffer { ... }
A qualifier on a use of StringBuffer is treated as
appearing both on the StringBuffer and on its conceptual type argument. That
is:
@Tainted StringBuffer ≈ @Tainted Collection<@Tainted Character>
@Untainted StringBuffer ≈ @Untainted Collection<@Untainted Character>
If two types have different qualifier arguments, they have no subtyping relationship. (This is “invariant subtyping”, also used by Java for generic classes.) In particular, @Untainted StringBuffer is not a subtype of @Tainted StringBuffer; an attempt to cast between them, in either direction, will yield an invariant.cast.unsafe error.
@HasQualifierParameter is inherited. If type T has a @HasQualifierParameter annotation, then its subtypes are automatically treated as having the same annotation.
Within a class with a qualifier parameter, the default qualifier for uses of that class is the polymorphic qualifier.
Qualifier polymorphism changes the rules for instantiating polymorphic qualifiers (Section 31.2). If the receiver type has a qualifier parameter and is annotated with a polymorphic qualifier, then at a call site all polymorphic annotations are instantiated to the same qualifier as the type of the receiver expression of the method call. Otherwise, use the rules of Section 31.2
For example, consider
@HasQualifierParameter(Tainted.class) class Buffer { void append(@PolyTainted Buffer this, @PolyTainted String s) { ... } }
Because @PolyTainted applies to a type (Buffer) with a qualifier parameter, all uses of @PolyTainted are instantiated to the qualifiers on the type of the receiver expression at call sites to append. For example,
@Untainted Buffer untaintedBuffer = ...; @Tainted String taintedString = ...; untaintedBuffer.append(taintedString); // error: (argument)
The above append call is illegal because the @PolyTainted is instantiated to @Untainted and the type of the argument is @Tainted which is a supertype of @Untainted. If the type of untaintedBuffer were @Tainted then the call would be legal.
To express that the type of a field should have the same qualifier as the class qualifier parameter, annotate the field type with the polymorphic qualifier for the type system.
@HasQualifierParameter(Tainted.class) class Buffer { @PolyTainted String field; }
At a field access where the declared type of the field has a polymorphic qualifier, that polymorphic qualifier is instantiated to the qualifier on the type of the receiver of the field access (or in the case of type variables, the qualifier on the upper bound). That is, the qualifier on myBuffer.field is that same as that on myBuffer.
Local variables default to the top type (see Section 32.5.3). Type refinement determines if a variable can be treated as a suitable subtype, and annotations on local variables are rarely needed as a result. However, since qualifier parameters add invariant subtyping, type refinement is no longer valid. For example, suppose in the following code that StringBuffer is annotated with @HasQualifierParameter(Tainted.class).
void method(@Untainted StringBuffer buffer) { StringBuffer local = buffer; executeSql(local.toString()); } void executeSql(@Untainted String code) { // ... }
Normally, the framework would determine that local has type @Untainted StringBuffer and the call to executeSql would be valid. However, since by default local has type @Tainted StringBuffer, and @Untainted StringBuffer is not a subtype, no type refinement would be performed, leading to an error. Fixing this would require manually annotating local as an @Untainted StringBuffer, increasing the annotation burden on programmers.
For this reason, local variables with types that have a qualifier parameter use different defaulting rules. When a local variable has an initializer, the type of that initializer is used as the default type of that variable if no other annotations are written. For example, in the above code, the type of local would be @Untainted StringBuffer. This eliminates the need for type refinement.
If many classes in a project should have @HasQualifierParameter, it’s possible to enable it on all classes in a package by default. Writing @HasQualifierParameter on a package is equivalent to writing @HasQualifierParameter on each class in that package and all subpackages with the same arguments.
For example, writing this annotation enables @HasQualifierParameter for all classes in mypackage.
@HasQualifierParameter(Tainted.class) package mypackage;
When using @HasQualifierParameter on a package, it’s possible to disable it for a specific class using @NoQualifierParameter. Writing this on a class indicates it has no class qualifier parameter and @HasQualifierParameter will not be enabled by default. Like @HasQualifierParameter, it takes one or more top annotations. It is illegal to explicitly write both @HasQualifierParameter and @NoQualifierParameter on the same class for the same hierarchy.
Types with qualifier parameters are only allowed as type arguments to type parameters whose upper bound have a qualifier parameter. If they were allowed for as type arguments for any type parameter, then unsound casts would be permitted. For example:
@HasQualifierParameter(Tainted.class) interface Buffer { void append(@PolyTainted String s); } public class ClassQPTypeVarTest { <T> @Tainted T cast(T param) { return param; } void bug(@Untainted Buffer b, @Tainted String s) { cast(b).append(s); // error } }
This chapter describes features that are automatically supported by every checker written with the Checker Framework. You may wish to skim or skip this chapter on first reading. After you have used a checker for a little while and want to be able to express more sophisticated and useful types, or to understand more about how the Checker Framework works, you can return to it.
Java’s type system is unsound with respect to arrays. That is, the Java type-checker approves code that is unsafe and will cause a run-time crash. Technically, the problem is that Java has “covariant array types”, such as treating String[] as a subtype of Object[]. Consider the following example:
String[] strings = new String[] {"hello"}; Object[] objects = strings; objects[0] = new Object(); String myString = strs[0];
The above code puts an Object in the array strings and thence in myString, even though myString = new Object() should be, and is, rejected by the Java type system. Java prevents corruption of the JVM by doing a costly run-time check at every array assignment; nonetheless, it is undesirable to learn about a type error only via a run-time crash rather than at compile time.
When you pass the -AinvariantArrays command-line option, the Checker Framework is stricter than Java, in the sense that it treats arrays invariantly rather than covariantly. This means that a type system built upon the Checker Framework is sound: you get a compile-time guarantee without the need for any run-time checks. But it also means that the Checker Framework rejects code that is similar to what Java unsoundly accepts. The guarantee and the compile-time checks are about your extended type system. The Checker Framework does not reject the example code above, which contains no type annotations.
Java’s covariant array typing is sound if the array is used in a read-only fashion: that is, if the array’s elements are accessed but the array is not modified. However, facts about read-only usage are not built into any of the type-checkers. Therefore, when using type systems along with -AinvariantArrays, you will need to suppress any warnings that are false positives because the array is treated in a read-only way.
When you write an expression, the Checker Framework gives it the most precise possible type, depending on the particular expression or value. For example, when using the Regex Checker (Chapter 14), the string "hello" is given type @Regex String because it is a legal regular expression (whether it is meant to be used as one or not) and the string "(foo" is given the type @RegexBottom String because it is not a legal regular expression.
Array constructors work differently. When you create an array with the array constructor syntax, such as the right-hand side of this assignment:
String[] myStrings = {"hello"};
then the expression does not get the most precise possible type, because doing so could cause inconvenience. Rather, its type is determined by the context in which it is used: the left-hand side if it is in an assignment, the declared formal parameter type if it is in a method call, etc.
In particular, if the expression {"hello"}
were given the type
@Regex String[], then the assignment would be illegal! But the Checker
Framework gives the type String[] based on the assignment context, so the code
type-checks.
If you prefer a specific type for a constructed array, you can indicate
that either in the context (change the declaration of myStrings) or in a
new construct (change the expression to new @Regex String[] {"hello"}
).
The examples in this section use the type qualifier hierarchy @A :> @B :> @C.
A qualifier on a use of a certain type must be a subtype or equal to the upper bound for that type. The upper bound of qualifiers used on a given type is specified by annotating the type declaration with some qualifier — that is, by writing an annotation on a class declaration.
@C class MyClass {}
This means that @B MyClass is an invalid type. (Annotations on class declarations may also specify default annotations for uses of the type; see Section 32.5.1)
If it is not possible to annotate the class’s definition (e.g., for primitives and some library classes), the type-system designer can specify an upper bound by using the meta-annotation @UpperBoundFor.
If no annotation is present on a type declaration and if no @UpperBoundFor mentions the type, then the bound is top. This can be changed by overriding AnnotatedTypeFactory#getTypeDeclarationBounds.
There are two exceptions.
Due to existing type rules, an expression of type @A MyClass can only be used in limited ways.
These operations might refine its type. If a user wishes to annotate a method that does type refinement, its formal parameter must be of illegal type @A MyClass, which requires a warning suppression.
If the framework were to forbid expressions and local variables from having types inconsistent with the class annotation, then important APIs and common coding paradigms would no longer type-check.
Consider the annotation
@NonNull class Optional { ... }
and the client code
Map<String, Optional> m; String key = ...; Optional value = m.get(key); if (value != null) { ... }
The type of m.get(key) is @Nullable Optional, which is an illegal type. However, this is a very common paradigm. Programmers should not need to rewrite the code to test m.containsKey(key) nor suppress a warning in this safe code.
A checker sometimes treats a type as having a slightly different qualifier than what is written on the type — especially if the programmer wrote no qualifier at all. Most readers can skip this section on first reading, because you will probably find the system simply “does what you mean”, without forcing you to write too many qualifiers in your program. particular, programmers rarely write qualifiers in method bodies (except on type arguments and array component types).
The following steps determine the effective qualifier on a type — the qualifier that the checkers treat as being present.
An unannotated Java type is treated as if it had a default annotation. Both the type system designer and an end-user programmer can control the defaulting. Defaulting never applies to uses of type variables, even if they do not have an explicit type annotation. Most of this section is about defaults for source code that is read by the compiler. When the compiler reads a .class file, different defaulting rules apply. See Section 32.5.6 for these rules.
There are several defaulting mechanisms, for convenience and flexibility. When determining the default qualifier for a use of an unannotated type, MyClass, the following rules are used in order, until one applies.
If the unannotated type is the type of a local variable, then the first 5 rules are skipped and only rules 6 and 7 apply. If rule 6 applies, it makes the type of local variables top so they can be refined.
The type declaration annotation @DefaultQualifierForUse indicates that the specified qualifier should be added to all unannotated uses of the type.
For example:
@DefaultQualifierForUse(B.class) class MyClass {}
This means any unannotated use of MyClass is treated as @B MyClass by the checker. (Except for locals, which can be refined.)
Similarly, the meta-annotation @DefaultFor can be used to specify defaults for uses of types, using the types element, or type kinds, using the typeKinds elements.
Interaction between qualifier bounds and DefaultQualifierForUse:
@DefaultQualifierForUse(C.class) @C class MyClass {}
@NoDefaultQualifierForUse @C class MyClass {}This means that unannotated uses of MyClass are defaulted normally.
The end-user programmer specifies a default qualifier by writing the @DefaultQualifier(ClassName, [locations]) annotation on a package, class, method, or variable declaration. The argument to @DefaultQualifier is the Class name of an annotation. The optional second argument indicates where the default applies. If the second argument is omitted, the specified annotation is the default in all locations. See the Javadoc of DefaultQualifier for details.
For example, using the Nullness type system (Chapter 3):
import org.checkerframework.framework.qual.DefaultQualifier; import org.checkerframework.checker.nullness.qual.NonNull; @DefaultQualifier(NonNull.class) class MyClass { public boolean compile(File myFile) { // myFile has type "@NonNull File" if (!myFile.exists()) // no warning: myFile is non-null ... @Nullable File srcPath = ...; // must annotate to specify "@Nullable File" if (srcPath.exists()) // warning: srcPath might be null ... } @DefaultQualifier(Tainted.class) public boolean isJavaFile(File myfile) { // myFile has type "@Tainted File" ... } }
You may write multiple @DefaultQualifier annotations at a single location.
If @DefaultQualifier[s] is placed on a package (via the package-info.java file), then it applies to the given package and all subpackages.
Each type system defines a default qualifier (see Section 36.5.4). For example, the default qualifier for the Nullness Checker is @NonNull. When a user writes an unqualified type such as Date, the Nullness Checker interprets it as @NonNull Date.
The type system applies that default qualifier to most but not all type uses. In particular, unless otherwise stated, every type system uses the CLIMB-to-top rule. This rule states that the top qualifier in the hierarchy is the default for the CLIMB locations: Casts, Locals, and (some) Implicit Bounds. For example, when the user writes an unqualified type such as Date in such a location, the Nullness Checker interprets it as @Nullable Date (because @Nullable is the top qualifier in the hierarchy, see Figure 3.1). (Casts are treated a bit specially; see below.)
The CLIMB-to-top rule is used only for unannotated source code that is being processed by a checker. For unannotated libraries (code read by the compiler in .class or .jar form), see Section 32.5.6.
The rest of this section explains the rationale and implementation of CLIMB-to-top.
Here is the rationale for CLIMB-to-top:
Variables defaulted to top include local variables, resource variables in the try-with-resources construct, variables in for statements, and catch arguments (known as exception parameters in the Java Language Specification).
Exception parameters default to the top type because they might catch an exception thrown anywhere in the program.
An alternate design for exception parameters would be to default exception parameters some other type T (instead of the top type); then the Checker Framework would need to issue a warning at every throw statement whose argument might not be a subtype of T. A checker can implement this alternate design by overriding a few methods. The alternative is not appropriate for all type systems. The alternative is unsound for deep type systems because the JDK’s annotations are trusted rather than checked. A deep type system is one where the type of a field can determine the type of its containing instance, such as tainting, Example: a user passes a secret regex to the JDK, and the JDK throws a format exception that includes the regex. This could be caught by a catch clause in the program whose exception parameter is not annotated as secret. As another example, the user passes a secret integer and the JDK throws a DivideByZeroException that reveals the value.
Suppose a user declares a class as class C<T> { ... }. The Checker Framework assumes that the user intended to allow any instantiation of the class. The Checker Framework interprets the declaration as class C<T extends @Nullable Object> { ... } rather than as class C<T extends @NonNull Object> { ... }. The latter would forbid instantiations such as C<@Nullable String>, or would require rewriting of code. On the other hand, if a user writes an explicit bound such as class C<T extends D> { ... }, then the user intends some restriction on instantiation and can write a qualifier on the upper bound as desired.
This rule means that the upper bound of class C<T> is defaulted differently than the upper bound of class C<T extends Object>. This is a bit unfortunate, but it is the least bad option. The more confusing alternative would be for “Object” to be defaulted differently in class C<T extends Object> and in an instantiation C<Object>, and for the upper bounds to be defaulted differently in class C<T extends Object> and class C<T extends Date>.
A @DefaultQualifier that specifies a CLIMB-to-top location takes precedence over the CLIMB-to-top rule.
Here is how the Nullness Checker overrides part of the CLIMB-to-top rule:
@DefaultQualifierInHierarchy @DefaultFor({ TypeUseLocation.EXCEPTION_PARAMETER }) public @interface NonNull {} public @interface Nullable {}
As mentioned above, the exception parameters are always non-null, so @DefaultFor({ TypeUseLocation.EXCEPTION_PARAMETER }) on @NonNull overrides the CLIMB-to-top rule.
When overriding a method, programmers must fully specify types in the overriding method, which duplicates information on the overridden method. By contrast, declaration annotations that are meta-annotated with @InheritedAnnotation are inherited by overriding methods.
An example for type annotations is that when defining an equals() method, programmers must write the type annotation @Nullable:
public boolean equals(@Nullable Object obj) { ... }
An alternate design would be for every annotation on a superclass member to to be automatically inherited by subclasses that override it.
The alternate design would reduce annotation effort.
The alternate design would reduce program comprehensibility. Currently, a user can determine the annotation on a parameter or return value by looking at a single file. If annotations could be inherited from supertypes, then a user would have to examine all supertypes, and do computations over them, to understand the meaning of an unannotated type in a given file. For declaration annotations, no computation is necessary; that is why they may be inherited. Computation is necessary for type annotations because different annotations might be inherited from a supertype and an interface, or from two interfaces. For return types, the inherited type should be the least upper bound of all annotations on overridden implementations in supertypes. For method parameters, the inherited type should be the greatest lower bound of all annotations on overridden implementations in supertypes. In each case, the Checker Framework would need to issue an error if no such annotations existed.
Because a program is read more often than it is edited/annotated, the Checker Framework does not currently support the alternate design. In the future, this feature may be added.
If a wildcard is unbounded and has no annotation (e.g. List<?>), the annotations on the wildcard’s bounds are copied from the type parameter to which the wildcard is an argument.
For example, the two wildcards in the declarations below are equivalent.
class MyList<@Nullable T extends @Nullable Object> {} MyList<?> listOfNullables; MyList<@Nullable ? extends @Nullable Object> listOfNullables;
The Checker Framework copies these annotations because wildcards must be within the bounds of their corresponding type parameter. By contrast, if the bounds of a wildcard were defaulted differently from the bounds of its corresponding type parameter, then there would be many false positive type.argument warnings.
Here is another example of two equivalent wildcard declarations:
class MyList<@Regex(5) T extends @Regex(1) Object> {} MyList<?> listOfRegexes; MyList<@Regex(5) ? extends @Regex(1) Object> listOfRegexes;
Note, this copying of annotations for a wildcard’s bounds applies only to unbounded wildcards. The two wildcards in the following example are equivalent.
class MyList<@NonNull T extends @Nullable Object> {} MyList<? extends Object> listOfNonNulls; MyList<@NonNull ? extends @NonNull Object> listOfNonNulls2;
Note, the upper bound of the wildcard ? extends Object is defaulted to @NonNull using the CLIMB-to-top rule (see Section 32.5.3). Also note that the MyList class declaration could have been more succinctly written as: class MyList<T extends @Nullable Object> where the lower bound is implicitly the bottom annotation: @NonNull.
(Note: Currently, the conservative library defaults presented in this section are off by default and can be turned on by supplying the -AuseConservativeDefaultsForUncheckedCode=bytecode command-line option. In a future release, they will be turned on by default and it will be possible to turn them off by supplying a -AuseConservativeDefaultsForUncheckedCode=-bytecode command-line option.)
The defaulting rules presented so far apply to source code that is read by the compiler. When the compiler reads a .class file, different defaulting rules apply.
If the checker was run during the compiler execution that created the .class file, then there is no need for defaults: the .class file has an explicit qualifier at each type use. (Furthermore, unless warnings were suppressed, those qualifiers are guaranteed to be correct.) When you are performing pluggable type-checking, it is best to ensure that the compiler only reads such .class files. Section 35.4 discusses how to create annotated libraries.
If the checker was not run during the compiler execution that created the .class file, then the .class file contains only the type qualifiers that the programmer wrote explicitly. (Furthermore, there is no guarantee that these qualifiers are correct, since they have not been checked.) In this case, each checker decides what qualifier to use for the locations where the programmer did not write an annotation. Unless otherwise noted, the choice is:
These choices are conservative. They are likely to cause many false-positive type-checking errors, which will help you to know which library methods need annotations. You can then write those library annotations (see Chapter 35) or alternately suppress the warnings (see Chapter 33).
For example, an unannotated method
String concatenate(String p1, String p2)
in a classfile would be interpreted as
@Top String concatenate(@Bottom String p1, @Bottom String p2)
There is no single possible default that is sound for fields. In the rare circumstance that there is a mutable public field in an unannotated library, the Checker Framework may fail to warn about code that can misbehave at run time.
An annotation on the “return type” of a constructor declaration indicates what the constructor creates. For example,
@B class MyClass { @C MyClass() {} }
means that invoking that constructor creates a @C MyClass.
The Checker Framework cannot verify that the constructor really creates such an object, because the Checker Framework does not know the type-system-specific semantics of the @C annotation. Therefore, if the constructor result type is different than the top annotation in the hierarchy, the Checker Framework will issue a warning. The programmer should check the annotation manually, then suppress the warning.
If a constructor declaration is unannotated, it defaults to the same type as that of its enclosing class (rather than the default qualifier in the hierarchy). For example, the Tainting Checker (Chapter 12) has @Tainted as its default qualifier. Consider the following class:
@Untainted class MyClass { MyClass() {} }
The constructor declaration is equivalent to @Untainted MyClass() {}.
The Checker Framework produces the same error messages for explicitly-written and defaulted annotations.
The type of a method call expression x.myMethod(y, z) is determined by the return type of the declaration of myMethod. There is no way to write an annotation on the call to change its type. However, it is possible to write a cast: (@Anno SomeType) x.myMethod(y, z). The Checker Framework will issue a warning that it cannot verify that the downcast is correct. The programmer should manually determine that the annotation is correct and then suppress the warning.
A constructor invocation new MyClass() is also a call, so its semantics are similar. The type of the expression is determined by the annotation on the result type of the constructor declaration. It is possible to write a cast (@Anno MyClass) new MyClass(). The syntax new @Anno MyClass() is shorthand for the cast. For either syntax, the Checker Framework will issue a warning that it cannot verify that the cast is correct. The programmer may suppress the warning if the code is correct.
A checker can sometimes deduce that an expression’s type is more specific than — that is, a subtype of — its declared or defaulted (Section 32.5). This is called “flow-sensitive type refinement” or “local type inference”.
Due to local type refinement, a programmer typically does not write any qualifiers on local variables within a method body (except on type arguments and array component types). However, the programmer must write type annotations for method signatures (arguments and return values) and fields, unless the default annotations are correct. Local type refinement does not change the source code to insert the inferred annotations on local variables.
Here is an example for the Nullness Checker (Chapter 3). myVar is declared as @Nullable String, but it is treated as @NonNull String within the body of the if test.
@Nullable String myVar; ... // myVar has type @Nullable String here. myVar.hashCode(); // warning: possible dereference of null. ... if (myVar != null) { ... // myVar has type @NonNull String here. myVar.hashCode(); // no warning. }
Here is another example. Note that the same expression may yield a warning or not depending on its context (that is, depending on the current type refinement).
@Nullable String myVar; ... // myVar has type @Nullable String myVar = "hello"; ... // myVar has type @NonNull String myVar.hashCode(); // no warning ... myVar = myMap.get(someKey); ... // myVar has type @Nullable String myVar.hashCode(); // warning: posible dereference of null
Type refinement applies to every checker, including new checkers that you write. Here is an example for the Regex Checker (Chapter 14):
void m2(@Unannotated String s) { s = RegexUtil.asRegex(s, 2); // asRegex throws an exception if its argument is not // a regex with the given number of capturing groups ... // s now has type "@Regex(2) String" }
The checker treats a variable or expression as a subtype of its declared type:
The checker never treats a variable as a supertype of its declared type. For example, an expression with declared type @NonNull type is never treated as possibly-null, and such an assignment is always illegal.
The functionality has a variety of names: type refinement, flow-sensitive type qualifier inference, local type inference, and sometimes just “flow”.
You generally do not need to annotate the top-level type of a local variable. You do need to annotate its type arguments or array element types. (Type refinement does not change them, because doing so would not produce a subtype, as explained in see Section 31.1.6 and Section 32.1.) Type refinement works within a method, so you still need to annotate method signatures (parameter and return type) and field types.
If you find examples where you think a value should be inferred to have (or not have) a given annotation, but the checker does not do so, please submit a bug report (see Section 40.2) that includes a small piece of Java code that reproduces the problem.
Type refinement infers the type of fields in some restricted cases:
public final String protocol = "https";
Such an inferred type may leak to the public interface of the class. If you wish to override such behavior, you can explicitly insert the desired annotation, e.g.,
public final @Nullable String protocol = "https";
class DBObject { @Nullable Date updatedAt; void m() { // updatedAt is @Nullable, so warning about .getTime() ... updatedAt.getTime() ... // warning about possible NullPointerException if (updatedAt == null) { updatedAt = new Date(); } // updatedAt is now @NonNull, so .getTime() call is OK ... updatedAt.getTime() ... } }
A method call may invalidate inferences about field types; see Section 32.7.5.
Some type systems support a run-time test that the Checker Framework can use to refine types within the scope of a conditional such as if, after an assert statement, etc.
Whether a type system supports such a run-time test depends on whether the type system is computing properties of data itself, or properties of provenance (the source of the data). An example of a property about data is whether a string is a regular expression. An example of a property about provenance is units of measure: there is no way to look at the representation of a number and determine whether it is intended to represent kilometers or miles.
Type systems that support a run-time test are:
Type systems that do not currently support a run-time test, but could do so with some additional implementation work, are
Type systems that cannot support a run-time test are:
Calling a method typically causes the checker to discard its knowledge of the refined type, because the method might assign a field. The @SideEffectFree annotation indicates that the method has no side effects, so calling it does not invalidate any dataflow facts.
Calling a method twice might have different results, so facts known about one call cannot be relied upon at another call. The @Deterministic annotation indicates that the method returns the same result every time it is called on the same arguments.
@Pure means both @SideEffectFree and @Deterministic. The @TerminatesExecution annotation indicates that a given method never returns. This can enable the type refinement to be more precise.
Chapter 22 gives more information about these annotations. This section explains how to use them to improve type refinement.
Consider the following declarations and uses:
@Nullable Object myField; int computeValue() { ... } void m() { ... if (myField != null) { // The type of myField is now "@NonNull Object". int result = computeValue(); // The type of myField is now "@Nullable Object", // because computeValue might have set myField to null. myField.toString(); // Warning: possible null pointer exception. } }
There are three ways to express that computeValue does not set myField to null, and thus to prevent the Nullness Checker from issuing a warning about the call myField.toString().
@SideEffectFree int computeValue() { ... }
The Nullness Checker issues no warnings, because it can reason that the second occurrence of myField has the same (non-null) value as the one in the test.
@MonotonicNonNull Object myField;
@EnsuresNonNull("myField") int computeValue() { ... }
If computeValue maintains myField as a non-null value, even if it might have other side effects and even if other methods might set myField to null, declare it as
@RequiresNonNull("myField") @EnsuresNonNull("myField") int computeValue() { ... }
There are two other ways to suppress the warning:
Consider the following declaration and uses:
@Nullable Object getField(Object arg) { ... } void m() { ... if (x.getField(y) != null) { x.getField(y).toString(); // warning: possible null pointer exception } }
The Nullness Checker issues a warning regarding the toString() call, because its receiver x.getField(y) might be null, according to the @Nullable return type in the declaration of getField. The Nullness Checker cannot assume that getField returns non-null on the second call, just based on the fact that it returned non-null on the first call.
To indicate that a method returns the same value each time it is called on the same arguments, use the @Deterministic annotation. Actually, it is necessary to use @Pure which means both @Deterministic and @SideEffectFree, because otherwise the first call might change a value that the method depends on.
If you change the declaration of getField to
@Pure @Nullable Object getField(Object arg) { ... }
then the Nullness Checker issues no warnings. Because getField is @SideEffectFree, the values of x and y are the same at both invocations. Because getField is @Deterministic, the two invocations of x.getField(y) have the same value. Therefore, x.getField(y) is non-null within the then branch of the if statement.
If your code contains an assert statement, then your code could behave in two different ways at run time, depending on whether assertions are enabled or disabled via the -ea, -enableassertions, -da, or <-disableassertions> command-line options to java.
By default, the Checker Framework outputs warnings about any error that could happen at run time, whether assertions are enabled or disabled.
If you supply the -AassumeAssertionsAreEnabled command-line option, then the Checker Framework assumes assertions (that is, Java assert statements) are enabled, as if Java is run with the -ea or -enableassertions command-line argument. If you supply the -AassumeAssertionsAreDisabled command-line option, then the Checker Framework assumes assertions are disabled, as if Java is run with the -da or -disableassertions command-line argument. You may not supply both command-line options. It is uncommon to supply either one.
These command-line arguments have no effect on processing of assert statements whose message contains the text @AssumeAssertion; see Section 33.2.
The initial type of a variable declared with var is exactly the type of the initializer expression at the declaration.
var list1 = new ArrayList<@Tainted String>(); // type of list1 is ArrayList<@Tainted String> var list2 = new ArrayList<@Untainted String>(); // type of list2 is ArrayList<@Untainted String>
Flow-sensitive type refinement applies to these variables as usual. The base type qualifier may change at a later program point due to a subsequent assignment, but the qualifiers on type parameters and array contents remain exactly the same.
var list1 = new ArrayList<@Tainted String>(); // type of list1 is ArrayList<@Tainted String> var list2 = new ArrayList<@Untainted String>(); // type of list2 is ArrayList<@Untainted String> var list3 = list1; // type of list3 is ArrayList<@Tainted String> list2 = list1; // assignment error
Sometimes, it is necessary to write a Java expression as the argument to an annotation. The annotations that take a Java expression as an argument include:
The set of permitted expressions is a subset of all Java expressions, with a few extensions. The extensions are formal parameters like “#1” and (for some type systems) “<self>”.
The formal parameter syntax #1 is less natural in source code than writing the formal parameter name. This syntax is necessary for separate compilation, because no formal parameter name information is available in a .class file. Suppose an annotated method m has already been compiled into a .class file, perhaps by a compilation that did not use the Checker Framework. When a client of m is later compiled, it cannot interpret a formal parameter name, but it can interpret a number.
Within a method body, you may use the formal parameter name. The formal parameter name never works within a method signature or for a contract (pre- or post-condition) annotation; in those locations, an identifier is interpreted as a field name (not a formal parameter).
Currently, the Checker Framework cannot prove all contracts about method calls, so you may need to suppress some warnings.
One unusual feature of the Checker Framework’s Java expressions is that a method call is allowed to have side effects. Other tools forbid methods with side effects (and doing so is necessary if a specification is going to be checked at run time via assertions). The Checker Framework enables you to state more facts. For example, consider the annotation on java.io.BufferedReader.ready():
@EnsuresNonNullIf(expression="readLine()", result=true) @Pure public boolean ready() throws IOException { ... }
This states that if readLine() is called immediately after ready() returns true, then readLine() returns a non-null value.
If you mention a class in a Java expression, then that class must be available to the compiler. For example, if you write @EnsuresNonNull("package1.package2.MyClass.myField.myOtherField"), then you should pass MyClass.java to the compiler. Otherwise, the compiler does not know which components of the dotted name are packages, classes, and field names. In that case, you might get confusing error messages such as “Invalid ’package1.package2’ because could not find class package1 in package package2”.
It is not possible to write a quantification over all array components (e.g., to express that all array elements are non-null). There is no such Java expression, but it would be useful when writing specifications.
Sometimes a field declared in a superclass has a more precise type in a subclass. To express this fact, write @FieldInvariant on the subclass. It specifies the field’s type in the class on which this annotation is written. The field must be declared in a superclass and must be final.
For example,
class Person { final @Nullable String nickname; public Person(@Nullable String nickname) { this.nickname = nickname; } } // A rapper always has a nickname. @FieldInvariant(qualifier = NonNull.class, field = "nickname") class Rapper extends Person { public Rapper(String nickname) { super(nickname); } void method() { ... nickname.length() ... // legal, nickname is non-null in this class. } }
A field invariant annotation can refer to more than one field. For example, @FieldInvariant(qualifier = NonNull.class, field = {fieldA, fieldB}) means that fieldA and fieldB are both non-null in the class upon which the annotation is written. A field invariant annotation can also apply different qualifiers to different fields. For example, @FieldInvariant(qualifier = {NonNull.class, Untainted.class}, field = {fieldA, fieldB}) means that fieldA is non-null and fieldB is untainted.
This annotation is inherited: if a superclass is annotated with @FieldInvariant, its subclasses have the same annotation. If a subclass has its own @FieldInvariant, then it must include the fields in the superclass annotation and those fields’ annotations must be a subtype (or equal) to the annotations for those fields in the superclass @FieldInvariant.
Currently, the @FieldInvariant annotation is trusted rather than checked. In other words, the @FieldInvariant annotation introduces a loophole in the type system, which requires verification by other means such as manual examination.
In an inheritance hierarchy, subclasses often introduce new methods and fields. For example, a Marsupial (and its subclasses such as Kangaroo) might have a variable pouchSize indicating the size of the animal’s pouch. The field does not exist in superclasses such as Mammal and Animal, so Java issues a compile-time error if a program tries to access myMammal.pouchSize.
If you cannot use subtypes in your program, you can enforce similar requirements using type qualifiers. For fields, use the @Unused annotation (Section 32.10.1), which enforces that a field or method may only be accessed from a receiver expression with a given annotation (or one of its subtypes). For methods, annotate the receiver parameter this; then a method call type-checks only if the actual receiver is of the specified type.
Also see the discussion of typestate checkers, in Chapter 30.30.
A Java subtype can have more fields than its supertype. For example:
class Animal {} class Mammal extends Animal { ... } class Marsupial extends Mammal { int pouchSize; // pouch capacity, in cubic centimeters ... }
You can simulate the same effect for type qualifiers: the @Unused annotation on a field declares that the field may not be accessed via a receiver of the given qualified type (or any supertype). For example:
class Animal { @Unused(when=Mammal.class) int pouchSize; // pouch capacity, in cubic centimeters ... } @interface Mammal {} @interface Marsupial {} @Marsupial Animal joey = ...; ... joey.pouchSize ... // OK @Mammal Animal mae = ...; ... mae.pouchSize ... // compile-time error
The above class declaration is like writing
class @Mammal-Animal { ... } class @Marsupial-Animal { int pouchSize; // pouch capacity, in cubic centimeters ... }
When the Checker Framework reports a warning, it’s best to fix the underlying problem, by changing the code or its annotations. For each warning, follow the methodology in Section 2.4.5 to correct the underlying problem.
This section describes what to do if the methodology of Section 2.4.5 indicates that you need to suppress the warning. You won’t change your code, but you will prevent the Checker Framework from reporting this particular warning to you. (Changing the code to fix a bug is another way to prevent the Checker Framework from issuing a warning, but it is not what this chapter is about.)
You may wish to suppress checker warnings because of unannotated libraries or un-annotated portions of your own code, because of application invariants that are beyond the capabilities of the type system, because of checker limitations, because you are interested in only some of the guarantees provided by a checker, or for other reasons. Suppressing a warning is similar to writing a cast in a Java program: the programmer knows more about the type than the type system does and uses the warning suppression or cast to convey that information to the type system.
You can suppress a warning message in a single variable initializer, method, or class by using the following mechanisms:
You can suppress warnings throughout the codebase by using the following mechanisms:
Some type checkers can suppress warnings via
The rest of this chapter explains these mechanisms in turn.
You can use the -AwarnUnneededSuppressions command-line option to issue a warning if a @SuppressWarnings did not suppress any warnings issued by the current checker.
You can suppress specific errors and warnings by use of the @SuppressWarnings annotation, for example @SuppressWarnings("interning") or @SuppressWarnings("nullness"). Section 33.1.1 explains the syntax of the argument string.
A @SuppressWarnings annotation may be placed on program declarations such as a local variable declaration, a method, or a class. It suppresses all warnings within that program element. Section 33.1.2 discusses where the annotation may be written in source code.
Section 33.1.3 gives best practices for writing @SuppressWarnings annotations.
The @SuppressWarnings annotation takes a string argument, in one of the following forms: "checkername:messagekey", "checkername", or "messagekey".
The argument checkername is the checker name, without “Checker”. It is lower case by default, though a checker can choose a different casing. For example, if you invoke a checker as javac -processor MyNiftyChecker ..., then you would suppress its error messages with @SuppressWarnings("mynifty"). (An exception is the Subtyping Checker, for which you use the annotation name; see Section 29.1.) Sometimes, a checker honors multiple checkername arguments; use the -AshowSuppressWarningsStrings command-line option to see them.
The argument messagekey is the message key for the error. Each warning message from the compiler gives the most specific suppression string that can be used to suppress that warning. An example is “dereference.of.nullable” in
MyFile.java:107: error: [dereference.of.nullable] dereference of possibly-null reference myList myList.add(elt); ^
You are allowed to use any substring of a message key, so long as the substring extends at each end to a period or an end of the key. For example, to suppress a warning with message key "assignment", you could use @SuppressWarnings("assignment"), @SuppressWarnings("assignment.type"), @SuppressWarnings("type.incompatible"), or other variants. We recommend using the longest possible message key; a short message might suppress more warnings than you expect.
The checkername "allcheckers" means all checkers. Using this is not recommended, except for messages common to all checkers such as purity-related messages when using -AcheckPurityAnnotations. If you use "allcheckers", you run some checker that does not issue any warnings, and you suply the -AwarnUnneededSuppressions command-line argument, then the Checker Framework will issue an unneeded.suppression warning.
The special messagekey “all” means to suppress all warnings.
If the checkername part is omitted, the @SuppressWarnings applies to all checkers. If the messagekey part is omitted, the @SuppressWarnings applies to all messages (it suppresses all warnings from the given checker).
With the -ArequirePrefixInWarningSuppressions command-line option, the Checker Framework only suppresses warnings when the string is in the "checkername" or "checkername:messagekey" format, as in @SuppressWarnings("nullness") or @SuppressWarnings("nullness:assignment"). For example, @SuppressWarnings("assignment") and @SuppressWarnings("all") have no effect (they are ignored) when -ArequirePrefixInWarningSuppressions is used. You can use @SuppressWarnings("allcheckers") to suppress all Checker Framework warnings.
@SuppressWarnings is a declaration annotation, so it may be placed on program declarations such as a local variable declaration, a method, or a class.
@SuppressWarnings cannot be used on statements, expressions, or types. To work around this, you can
Always write a @SuppressWarnings annotation on the smallest possible scope. To reduce the scope of a @SuppressWarnings annotation, it is sometimes desirable to refactor the code. You might extract an expression into a local variable, so that warnings can be suppressed just for that local variable’s initializer expression. Likewise, you might extract some code into a separate method, so that warnings can be suppressed just for its body. Or, you can use @AssumeAssertion on an assert statement; see Section 33.2.
As an example, consider suppressing a warning at an assignment that you know is safe. Here is an example that uses the Tainting Checker (Section 12). Assume that expr has compile-time (declared) type @Tainted String, but you know that the run-time value of expr is untainted.
@SuppressWarnings("tainting:cast.unsafe") // expr is untainted because ... [explanation goes here] @Untainted String myvar = expr;
Java does not permit annotations (such as @SuppressWarnings) on assignments (or on other statements or expressions), so it would have been illegal to write
@Untainted String myvar; ... @SuppressWarnings("tainting:cast.unsafe") // expr is untainted because ... myvar = expr;
Prefer @SuppressWarnings on a local variable declaration to one on a method, and prefer one on a method to one on a class. @SuppressWarnings on a local variable declaration applies only to the declaration (including its initializer if any), not to all uses of the variable.
You may be able to suppress a warning about a use of an expression by writing @AssumeAssertion for the expression, before the use. See Section 33.2.
Another way to reduce the scope of a @SuppressWarnings is to extract the expression into a new local variable and place a @SuppressWarnings annotation on the variable declaration. See Section 33.1.2.
It is best to use the most specific possible message key to suppress just a specific error that you know to be a false positive. The checker outputs this message key when it issues an error. If you use a broader @SuppressWarnings annotation, then it may mask other errors that you needed to know about.
Any of the following would have suppressed the warning in Section 33.1.2:
@SuppressWarnings("tainting") // suppresses all tainting-related warnings @SuppressWarnings("cast") // suppresses warnings from all checkers about casts @SuppressWarnings("unsafe") // suppresses warnings from all checkers about unsafe code @SuppressWarnings("cast.unsafe") // suppresses warnings from all checkers about unsafe casts @SuppressWarnings("tainting:cast") // suppresses tainting warnings about casts @SuppressWarnings("tainting:unsafe") // suppresses tainting warnings about unsafe code @SuppressWarnings("tainting:cast.unsafe") // suppresses tainting warnings about unsafe casts
The last one is the most specific, and therefore is the best style.
A @SuppressWarnings annotation asserts that the programmer knows that the code is actually correct or safe (that is, no undesired behavior will occur), even though the type system is unable to prove that the code is correct or safe.
Whenever you write a @SuppressWarnings annotation, you should also write, typically on the same line, a code comment explaining why the code is actually correct. In some cases you might also justify why the code cannot be rewritten in a simpler way that would be amenable to type-checking. Also make it clear what error is being suppressed. (This is particularly important when the @SuppressWarnings is on a method declaration and the suppressed warning might be anywhere in the method body.)
This documentation will help you and others to understand the reason for the @SuppressWarnings annotation. It will also help you audit your code to verify all the warning suppressions. (The code is correct only if the checker issues no warnings and each @SuppressWarnings is correct.)
A suppression message like “a.f is not null” is not useful. The fact that you are suppressing the warning means that you believe that a.f is not null. The message should explain why you believe that; for example, “a.f was checked above and no subsequent side effect can affect it”.
Here are some terse examples from libraries in plume-lib:
@SuppressWarnings("cast") // cast is redundant (except when checking nullness) @SuppressWarnings("interning") // FbType.FREE is interned but is not annotated @SuppressWarnings("interning") // equality testing optimization @SuppressWarnings("nullness") // used portion of array is non-null @SuppressWarnings("nullness") // oi.factory is a static method, so null first argument is OK @SuppressWarnings("purity") // side effect to local state of type BitSet
A particularly good (and concise) justification is to reference an issue in the issue tracker, as in these two from Daikon:
@SuppressWarnings("flowexpr.parse.error") // https://tinyurl.com/cfissue/862 @SuppressWarnings("keyfor") // https://tinyurl.com/cfissue/877
Please report false positive warnings, then reference them in your warning suppressions. This permits the Checker Framework maintainers to know about the problem, it helps them with prioritization (by knowing how often in your codebase a particular issue arises), and it enables you to know when an issue has been fixed (though the -AwarnUnneededSuppressions command-line option also serves the latter purpose).
Sometimes, it is too disruptive to refactor your code to create a location where @SuppressWarnings can be written. You can instead suppress a warning by writing an assertion whose message contains the string @AssumeAssertion(checkername).
For example, in this code:
while (c != Object.class) { ... c = c.getSuperclass(); assert c != null : "@AssumeAssertion(nullness): c was not Object, so its superclass is not null"; }
the Nullness Checker assumes that c is non-null from the assert statement forward (including on the next iteration through the loop).
The assert expression must be an expression that would affect flow-sensitive type refinement (Section 32.7), if the expression appeared in a conditional test. Each type system has its own rules about what type refinement it performs.
The value in parentheses is a checker name (typically lowercase), exactly as in the @SuppressWarnings annotation (Section 33.1.1). Any subcheckers will also assume that the assertion is true (e.g., the Map Key Checker will assume that the assertion in the example above cannot fail, when it runs as a subchecker of the Nullness Checker).
The same good practices apply as for @SuppressWarnings annotations, such as writing a comment justifying why the assumption is safe (Section 33.1.3).
The -AassumeAssertionsAreEnabled and -AassumeAssertionsAreDisabled command-line options (Section 32.7.6) do not affect processing of assert statements that have @AssumeAssertion in their message. Writing @AssumeAssertion means that the assertion would succeed if it were executed, and the Checker Framework makes use of that information regardless of the -AassumeAssertionsAreEnabled and -AassumeAssertionsAreDisabled command-line options.
This section explains the distinction between two different uses for assertions: debugging a program (also known as defensive programming) versus specifying a program. The examples use nullness annotations, but the concepts apply to any checker.
The purpose of assertions is to aid debugging by throwing an exception when a program does not work correctly. Sometimes, programmers use assertions for a different purpose: documenting how the program works. By default, the Checker Framework assumes that each assertion is used for its primary purpose of debugging: the assertion might fail at run time, and the programmer wishes to be informed at compile time about such possible run-time errors.
Suppose that a programmer encounters a failing test, adds an assertion to aid debugging, and fixes the test. The programmer leaves the assertion in the program if the programmer is worried that the program might fail in a similar way in the future. The Checker Framework should not assume that the assertion succeeds — doing so would defeat the very purpose of the Checker Framework, which is to detect errors at compile time and prevent them from occurring at run time.
A non-standard use for annotations is to document facts that a programmer has independently verified to be true. The Checker Framework can leverage these assertions in order to avoid issuing false positive warnings. The programmer marks such assertions with the @AssumeAssertion string in the assert message (see Section 33.2). Only do so if you are sure that the assertion always succeeds at run time.
Methods such as Objects.requireNonNull, JUnit’s Assert.assertNotNull, and Guava’s verifyNotNull and checkNotNull are similar to assertions. Just as for assertions, their intended use is as debugging aids, they might fail at run time, and the Checker Framework warns if that might happen. Some programmers may use assert methods as documentation of facts that the programmer has verified in some other manner. If you know that a particular codebase always uses an assertion method not for defensive programming but to indicate facts that are guaranteed to be true (that is, these assertions cannot fail at run time), then there are two approaches to avoid false positive warnings: write specifications or suppress warnings; see below for an explanation of each approach.
The method NullnessUtil.castNonNull is not an assertion method. It is a warning suppression method.
Note that some libraries have an imprecise/incorrect specification of their assertion methods. For example, Guava’s Verify.verifyNotNull is imprecisely/incorrectly specified to have a @Nullable formal parameter. In a correct execution, null never flows there, so its type can and should be annotated as @NonNull. That annotation allows the Nullness Checker to warn about programs that crash due to passing null to verifyNotNull. (A comment in Guava’s Preconditions.java agrees with this reasoning: “the first parameter to checkNotNull should be annotated to require it to be non-null.” The comment goes on to say “I had hoped to take a principled stand on this” (that, is, write the annotation @NonNull), but Guava annotates it as