| # Null Checking | |
| Chromium uses [NullAway] to enforce [JSpecify]-style `@Nullable` annotations. | |
| NullAway is a [Error Prone] plugin and [runs as a static analysis step] for | |
| targets without `chromium_code = false`. | |
| [TOC] | |
| [NullAway]: https://github.com/uber/NullAway | |
| [JSpecify]: https://jspecify.dev/docs/user-guide/ | |
| [Error Prone]: https://errorprone.info/ | |
| [runs as a static analysis step]: /build/android/docs/static_analysis.md#ErrorProne | |
| ## NullAway Configuration | |
| [Chromium's NullAway configuration] is as follows: | |
| * [JSpecify mode] is enabled. | |
| * `@Nullable` is `TYPE_USE`. | |
| * Non-annotated means non-null (no need for `@NonNull`). | |
| * Nullness of local variables is inferred (do not annotate them with `@Nullable`). | |
| * Copies of [supported annotations] exist under | |
| `org.chromium.build.annotations`. | |
| * These are a part of `//build/android:build_java`, which for convenience, | |
| is a default dep of all `android_library` and `java_library` targets. | |
| * Null checking is enabled only for classes annotated with `@NullMarked`. | |
| * For other classes (e.g.: most library & OS APIs), `@Nullable` and | |
| `@NonNull` are respected, but non-annotated types are permissive (return | |
| types are non-null and parameters are nullable). | |
| * Java collections and Guava's `Preconditions` are [modeled directly] in | |
| NullAway. | |
| * Some additional types are modeled via [`ChromeNullAwayLibraryModel`]. | |
| * Android's `onCreate()` (and similar) methods are implicitly marked `@Initializer`. | |
| [Chromium's NullAway configuration]: https://source.chromium.org/search?q=%22XepOpt:NullAway%22%20-f:third_party&sq=&ss=chromium | |
| [JSpecify mode]: https://github.com/uber/NullAway/wiki/JSpecify-Support | |
| [supported annotations]: https://github.com/uber/NullAway/wiki/Supported-Annotations | |
| [`ChromeNullAwayLibraryModel`]: https://source.chromium.org/chromium/chromium/src/+/main:tools/android/errorprone_plugin/src/org/chromium/tools/errorprone/plugin/ChromeNullAwayLibraryModel.java | |
| [modeled directly]: https://github.com/uber/NullAway/blob/HEAD/nullaway/src/main/java/com/uber/nullaway/handlers/LibraryModelsHandler.java | |
| ## Nullness Migration | |
| We are actively opting classes into enforcement. Track progress via [crbug.com/389129271]. | |
| [crbug.com/389129271]: https://crbug.com/389129271 | |
| ## Nullness Primer | |
| ### Type Annotations | |
| ```java | |
| // Plain Objects: | |
| private String mNonNullString; | |
| private @Nullable String mNullableString; | |
| private Outer.@Nullable Inner mNullableNestedType; | |
| // Arrays: | |
| private String @Nullable[] mNullableArrayOfNonNullString; | |
| private @Nullable String[] mNonNullArrayOfNullableString; | |
| // Generics: | |
| private List<@Nullable String> mNonNullListOfNullableString; | |
| private @Nullable Callback<@Nullable String> mNullableCallbackOfNullableString; | |
| // Does not compile (annotation must come immediately before type): | |
| @Nullable | |
| private String mInvalidAnnotation; | |
| ``` | |
| ### Method Annotations | |
| NullAway analyzes code on a per-method basis. These annotations tell it how | |
| about pre/post conditions: | |
| ```java | |
| // Using this with non-private methods never makes sense. | |
| @RequiresNonNull("mNullableString") | |
| private void usesNullableString() { | |
| // No warning: | |
| if (mNullableString.isEmpty()) { ... } | |
| } | |
| @EnsuresNonNull("mNullableString") | |
| private void codeCanCallThisAndThenUseNullableString() { | |
| // This will warn if mNullableString is @Nullable at any egress. | |
| assert mNullableString != null; | |
| } | |
| // If this method returns true, then mThing is non-null. | |
| @EnsuresNonNullIf("mThing") | |
| private boolean isThingEnabled() { | |
| return mThing != null; | |
| } | |
| // Also works with static fields and negated return values. | |
| @EnsuresNonNullIf(value={"sThing1", "sThing2"}, result=false) | |
| private static boolean isDestroyed() { | |
| return sThing1 == null || sThing2 == null; | |
| } | |
| // If foo is null, this method returns false. | |
| // Most other forms of contracts are not supported. | |
| @Contract("null -> false") | |
| private boolean isParamNonNull(@Nullable String foo) { | |
| return foo != null; | |
| } | |
| // Returns null only when defaultValue is null | |
| @Contract("_, !null -> !null") | |
| @Nullable String getOrDefault(String key, @Nullable String defaultValue) { | |
| return defaultValue; | |
| } | |
| ``` | |
| ### Field Annotations | |
| ```java | |
| // Starts as null, but may not be assigned a nullable value. | |
| private @MonotonicNonNull String mSomeValue; | |
| public void doThing(String value) { | |
| // Emits a warning since mSomeValue is nullable: | |
| helper(mSomeValue); | |
| mSomeValue = value; | |
| // No warning about mSomeValue being nullable, even though it's used in a lambda. | |
| PostTask.postTask(TaskTraits.USER_BLOCKING, () -> helper(mSomeValue)); | |
| } | |
| ``` | |
| ### "assert", "assumeNonNull()", "assertNonNull()", and "requireNonNull()" | |
| ```java | |
| // Always use "import static" for assumeNonNull / assertNonNull. | |
| import static org.chromium.build.NullUtil.assumeNonNull; | |
| import static org.chromium.build.NullUtil.assertNonNull; | |
| public String void example() { | |
| // Prefer statements over expressions to keep preconditions separate from usage. | |
| assumeNonNull(mNullableThing); | |
| assert mOtherThing != null; | |
| // It supports nested fields and getters. | |
| assumeNonNull(someObj.nullableField); | |
| assumeNonNull(someObj.getNullableThing()); | |
| // Use its expression form when it is more readable to do so. | |
| someHelper(assumeNonNull(Foo.maybeCreate(true))); | |
| // Use assertNonNull when you need an assert as an expression. | |
| mNonNullField = assertNonNull(dict.get("key")); | |
| String ret = obj.getNullableString(); | |
| if (willJustCrashLaterAnyways) { | |
| // Use "assert" when not locally dereferencing the object. | |
| assert ret != null; | |
| } else { | |
| // Use "requireNonNull()" when returning null might lead to bad things. | |
| // Asserts are enabled only on Canary and are set as "dump without crashing". | |
| Objects.requireNonNull(ret); | |
| } | |
| return ret; | |
| } | |
| // Use "assertNonNull(null)" for unreachable code. | |
| public String describe(@MyIntDef int validity) { | |
| return switch (validity) { | |
| case MyIntDef.VALID -> "okay"; | |
| case MyIntDef.INVALID -> "not okay"; | |
| default -> assertNonNull(null); | |
| }; | |
| } | |
| ``` | |
| ### Object Construction and Destruction | |
| **Construction:** | |
| * NullAway warns if any non-null fields are still nullable at the end of a | |
| constructor. | |
| * When a class uses two-phase initialization (e.g., has an `onCreate()` or | |
| `initialize()`), you can tell NullAway to pretend all such methods have | |
| been called before performing validation. | |
| * `@Initializer` can also be used for `static` methods, which impacts | |
| warnings for `static` fields. | |
| * That `@Initializer` methods are actually called is not checked. | |
| *** note | |
| **Note:** When multiple setters are always called after constructing an object, | |
| prefer to create an single `initialize()` method that sets them instead. | |
| *** | |
| **Destruction:** | |
| For classes with `destroy()` methods that set fields to `null` that would | |
| otherwise be non-null, you can either: | |
| 1) Annotate the fields as `@Nullable` and add `!isDestroyed()` asserts / guards | |
| where necessary (where `isDestroyed()` is annotated with | |
| `@EnsuresNonNullIf(value=..., result=false)`), or | |
| 2) Annotate the `destroy()` method with `@SuppressWarnings("NullAway")`. | |
| **View Binders:** | |
| It might seem appropriate to mark `onBindViewHolder()` with `@Initializer`, | |
| but these are not really "methods that are called immediately after the | |
| constructor". Instead, consider adding an `assertBound()` method. | |
| Example: | |
| ```java | |
| @EnsuresNonNull({"mField1", "mField2", ...}) | |
| private void assertBound() { | |
| assert mField1 != null; | |
| assert mField2 != null; | |
| ... | |
| } | |
| ``` | |
| ### JNI | |
| * Nullness is not checked for `@CalledByNative` methods ([crbug/389192501]). | |
| * Nullness **is checked** via `assert` statements for Java->Native methods | |
| (when `@NullMarked` exists). | |
| [crbug/389192501]: https://crbug.com/389192501 | |
| ### Struct-like Classes | |
| NullAway has no special handling for classes with public fields and will emit | |
| a warning for any non-primitive non-`@Nullable` fields not initialized by a | |
| constructor. | |
| Fix this by: | |
| * Creating a constructor that sets these fields (Android Studio has a | |
| `Generate->Constructor` function that will do this). | |
| * If this makes the call-site less readable, add `/* paramName= */` comments | |
| for the parameters. | |
| * As a bonus, the constructor may also allow you to mark fields as `final`. | |
| ### Effectively Non-Null Return Types | |
| Some methods are technically `@Nullable`, but effectively `@NonNull`. That is, | |
| they are marked as having `@NonNull` return types despite sometimes returning | |
| `null`. Examples: | |
| * [`Activity.findViewById()`] | |
| * `Context.getSystemService()` | |
| * `PreferenceManager.findPreference()` (this one via [`ChromeNullAwayLibraryModel`]) | |
| Enforcing null checks for these would be detrimental to readability. | |
| For Chromium-authored code that falls into this bucket, prefer to add | |
| companion "Checked" methods over mis-annotating nullability. | |
| Example: | |
| ```java | |
| // When you're not sure if the tab exists: | |
| public @Nullable Tab getTabById(String tabId) { | |
| ... | |
| } | |
| // When you know the tab exists: | |
| public Tab getTabByIdChecked(String tabId) { | |
| return assertNonNull(getTabById(key)); | |
| } | |
| ``` | |
| [`Activity.findViewById()`]: https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/app/Activity.java?q=symbol%3A%5Cbandroid.app.Activity.findViewById%5Cb%20case%3Ayes | |
| ### Suppliers | |
| * To pass a `Supplier<T>` to a method that accepts a `Supplier<@Nullable T>`, | |
| you must explicitly cast it to the nullable form. See | |
| https://github.com/uber/NullAway/issues/1356 for why this is. | |
| * To pass a `Supplier<@Nullable T>` to a method that accepts a `Supplier<T>`, | |
| use `SupplierUtils.asNonNull()`. | |
| * If the value might be null at the time of conversion, change the parameter | |
| type a `Supplier<@Nullable T>`, and add `assumeNonNull()` / `assertNonNull()` | |
| to `.get()` calls accordingly. | |
| ## NullAway Shortcomings | |
| Does not work: `boolean isNull = thing == null; if (!isNull) { ... }` | |
| * Feature request: https://github.com/uber/NullAway/issues/98 | |
| Checking that `@Contract` is correct is buggy (we disable it). `@Contract` | |
| still apply to callers though (they are assumed to be true). | |
| * Bug: https://github.com/uber/NullAway/issues/1104 | |
| ## FAQ | |
| **Q: Why not use Checker Framework?** | |
| * Chromium already uses Error Prone, so NullAway was easy to integrate. | |
| **Q: How do `@NullUnmarked` and `@SuppressWarnings("NullAway")` differ?** | |
| * Both suppress warnings on a method. | |
| * `@SuppressWarnings` leaves the method signature `@NullMarked`. | |
| * `@NullUnmarked` causes parameters and return types to have unknown | |
| nullability, and thus also suppress nullness warnings that may exist at a | |
| method's call sites. | |
| **Q: Can I use JSpecify Annotations?** | |
| * Yes. For code that will be mirrored and built in other environments, it is | |
| best to use JSpecify annotations. You'll probably want to set: | |
| ```gn | |
| deps += [ "//third_party/android_deps:org_jspecify_jspecify_java" ] | |
| # Prevent automatic dep on build_java. | |
| chromium_code = false | |
| # Do not let chromium_code = false disable Error Prone. | |
| enable_errorprone = true | |
| ``` |