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
.
Chromium's NullAway configuration is as follows:
@Nullable
is TYPE_USE
.@NonNull
).org.chromium.build.annotations
.//build/android:build_java
, which for convenience, is a default dep of all android_library
and java_library
targets.@NullMarked
.@Nullable
and @NonNull
are respected, but non-annotated types are permissive (return types are non-null and parameters are nullable).Preconditions
are modeled directly in NullAway.ChromeNullAwayLibraryModel
.onCreate()
(and similar) methods are implicitly marked @Initializer
.We are actively opting classes into enforcement. Track progress via crbug.com/389129271.
// 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;
NullAway analyzes code on a per-method basis. These annotations tell it how about pre/post conditions:
// 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; }
// 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)); }
// 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); }; }
Construction:
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.@Initializer
methods are actually called is not checked.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:
@Nullable
and add !isDestroyed()
asserts / guards where necessary (where isDestroyed()
is annotated with @EnsuresNonNullIf(value=..., result=false)
), ordestroy()
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:
@EnsuresNonNull({"mField1", "mField2", ...}) private void assertBound() { assert mField1 != null; assert mField2 != null; ... }
@CalledByNative
methods (crbug/389192501).assert
statements for Java->Native methods (when @NullMarked
exists).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:
Generate->Constructor
function that will do this)./* paramName= */
comments for the parameters.final
.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:
// 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)); }
Does not work: boolean isNull = thing == null; if (!isNull) { ... }
It does not infer nullness of inferred generics.
Validation of (but not use of) @Contract
is buggy.
Q: Why not use Checker Framework?
Q: How do @NullUnmarked
and @SuppressWarnings("NullAway")
differ?
@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?
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