Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -634,8 +634,8 @@ private static String keyToServeAsHeader(String label, DuplicateGroupedAndTyped
* an associated value, so it won't factor into alignment.
*/
String key = keyToGoWithElementsString(label, elements);
if (elements.homogeneousTypeToDisplay != null) {
key += " (" + elements.homogeneousTypeToDisplay + ")";
if (elements.getHomogeneousTypeToDisplay() != null) {
key += " (" + elements.getHomogeneousTypeToDisplay() + ")";
}
return key;
}
Expand Down
54 changes: 54 additions & 0 deletions core/src/main/java/com/google/common/truth/NullnessCasts.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright (C) 2021 Google, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/

package com.google.common.truth;

import org.jspecify.annotations.Nullable;

/** A utility method to perform unchecked casts to suppress errors produced by nullness analyses. */
final class NullnessCasts {
/**
* Accepts a {@code @Nullable T} and returns a plain {@code T}, without performing any check that
* that conversion is safe.
*
* <p>This method is intended to help with usages of type parameters that have parametric
* nullness. If a type parameter instead ranges over only non-null types (or if the type is a
* non-variable type, like {@code String}), then code should almost never use this method,
* preferring instead to call {@code requireNonNull} so as to benefit from its runtime check.
*
* <p>An example use case for this method is in implementing an {@code Iterator<T>} whose {@code
* next} field is lazily initialized. The type of that field would be {@code @Nullable T}, and the
* code would be responsible for populating a "real" {@code T} (which might still be the value
* {@code null}!) before returning it to callers. Depending on how the code is structured, a
* nullness analysis might not understand that the field has been populated. To avoid that problem
* without having to add {@code @SuppressWarnings}, the code can call this method.
*
* <p>Why <i>not</i> just add {@code SuppressWarnings}? The problem is that this method is
* typically useful for {@code return} statements. That leaves the code with two options: Either
* add the suppression to the whole method (which turns off checking for a large section of code),
* or extract a variable, and put the suppression on that. However, a local variable typically
* doesn't work: Because nullness analyses typically infer the nullness of local variables,
* there's no way to assign a {@code @Nullable T} to a field {@code T foo;} and instruct the
* analysis that that means "plain {@code T}" rather than the inferred type {@code @Nullable T}.
* (Even if supported added {@code @NonNull}, that would not help, since the problem case
* addressed by this method is the case in which {@code T} has parametric nullness -- and thus its
* value may be legitimately {@code null}.)
*/
@SuppressWarnings("nullness")
static <T extends @Nullable Object> T uncheckedCastNullableTToT(@Nullable T t) {
return t;
}

private NullnessCasts() {}
}
26 changes: 18 additions & 8 deletions core/src/main/java/com/google/common/truth/SubjectUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import static com.google.common.collect.Iterables.isEmpty;
import static com.google.common.collect.Iterables.transform;
import static com.google.common.collect.Multisets.immutableEntry;
import static com.google.common.truth.NullnessCasts.uncheckedCastNullableTToT;

import com.google.common.base.Equivalence;
import com.google.common.base.Equivalence.Wrapper;
Expand Down Expand Up @@ -49,15 +50,20 @@ private SubjectUtils() {}

static final String HUMAN_UNDERSTANDABLE_EMPTY_STRING = "\"\" (empty String)";

static <T extends @Nullable Object> List<T> accumulate(T first, T second, T @Nullable ... rest) {
static <T extends @Nullable Object> List<T> accumulate(T first, T second, T @Nullable [] rest) {
// rest should never be deliberately null, so assume that the caller passed null
// in the third position but intended it to be the third element in the array of values.
// Javac makes the opposite inference, so handle that here.
List<T> items = new ArrayList<>(2 + ((rest == null) ? 1 : rest.length));
items.add(first);
items.add(second);
if (rest == null) {
items.add((T) null);
/*
* This cast is probably not actually safe as used in IterableSubject.UsingCorrespondence. But
* that whole API is stuck being type-unsafe unless we re-generify IterableSubject:
* b/145689657#comment1.
*/
items.add(uncheckedCastNullableTToT(null));
} else {
items.addAll(asList(rest));
}
Expand Down Expand Up @@ -195,15 +201,19 @@ protected int doHash(Object o) {
* elements and even to output different elements on different lines.
*/
static final class DuplicateGroupedAndTyped {
final NonHashingMultiset<?> valuesAndMaybeTypes;
final @Nullable String homogeneousTypeToDisplay;
private final NonHashingMultiset<?> valuesAndMaybeTypes;
private final @Nullable String homogeneousTypeToDisplay;

DuplicateGroupedAndTyped(
NonHashingMultiset<?> valuesAndMaybeTypes, @Nullable String homogeneousTypeToDisplay) {
this.valuesAndMaybeTypes = valuesAndMaybeTypes;
this.homogeneousTypeToDisplay = homogeneousTypeToDisplay;
}

@Nullable String getHomogeneousTypeToDisplay() {
return homogeneousTypeToDisplay;
}

int totalCopies() {
return valuesAndMaybeTypes.totalCopies();
}
Expand Down Expand Up @@ -257,10 +267,10 @@ public String toString() {
* <p>Example: {@code hasMatchingToStringPair([1L, 2L], [1]) == true}
*/
static boolean hasMatchingToStringPair(Iterable<?> items1, Iterable<?> items2) {
if (isEmpty(items1) || isEmpty(items2)) {
return false; // Bail early to avoid calling hashCode() on the elements unnecessarily.
}
return !retainMatchingToString(items1, items2).isEmpty();
// Bail early for empty iterables to avoid calling hashCode() on the elements unnecessarily.
return !isEmpty(items1)
&& !isEmpty(items2)
&& !retainMatchingToString(items1, items2).isEmpty();
}

static String objectToTypeName(@Nullable Object item) {
Expand Down
Loading