package org.mutabilitydetector.unittesting.matchers.reasons; /* * #%L * MutabilityDetector * %% * Copyright (C) 2008 - 2014 Graham Allan * %% * 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. * #L% */ import static com.google.common.collect.ImmutableSet.copyOf; import static org.mutabilitydetector.MutabilityReason.ABSTRACT_COLLECTION_TYPE_TO_FIELD; import static org.mutabilitydetector.MutabilityReason.ABSTRACT_TYPE_TO_FIELD; import static org.mutabilitydetector.MutabilityReason.ARRAY_TYPE_INHERENTLY_MUTABLE; import static org.mutabilitydetector.MutabilityReason.COLLECTION_FIELD_WITH_MUTABLE_ELEMENT_TYPE; import static org.mutabilitydetector.MutabilityReason.FIELD_CAN_BE_REASSIGNED; import static org.mutabilitydetector.MutabilityReason.MUTABLE_TYPE_TO_FIELD; import static org.mutabilitydetector.MutabilityReason.NON_FINAL_FIELD; import java.util.Collections; import java.util.HashSet; import java.util.Set; import org.hamcrest.Description; import org.hamcrest.Matcher; import org.hamcrest.TypeSafeMatcher; import org.mutabilitydetector.MutabilityReason; import org.mutabilitydetector.MutableReasonDetail; import org.mutabilitydetector.locations.CodeLocation.FieldLocation; import org.mutabilitydetector.unittesting.AllowedReason; /** * Allowed reasons for mutability warnings related to fields. * <p> * It is expected that this class will not be used directly. Instead, use the * factory methods provided by {@link AllowedReason} for more fluent unit tests. * <p> * * @see AllowedReason#assumingFields(Iterable) * @see AllowedReason#assumingFields(String, String...) */ public final class FieldAssumptions { private final Set<String> fieldNames; private FieldAssumptions(Set<String> fieldNames) { this.fieldNames = Collections.unmodifiableSet(new HashSet<String>(fieldNames)); } /** * Advice: use the factory method * {@link AllowedReason#assumingFields(String, String...)} for greater * readability. */ public static FieldAssumptions named(Set<String> fieldNames) { return new FieldAssumptions(copyOf(fieldNames)); } /** * Insists fields of collection types are copied and wrapped safely. * <p> * One way to use mutable collection types in an immutable class is to copy * the contents and wrap in an unmodifiable collection. Mutability Detector * has limited support for recognising this pattern, e.g.: * <code>Collections.unmodifiableList(new ArrayList(original));</code>. * However, the methods used for copying and wrapping must be those * available in the JDK. If you are using your own, or third-party * collection types, Mutability Detector will raise a warning. This allowed * reason will permit those warnings. * <p> * Example usage: * * <pre> * <code> * import com.google.common.collect.Lists; * * @Immutable * public final class SafelyCopiesAndWraps { * private final List<String> unmodifiableCopy; * * public SafelyCopiesAndWraps(List<String> original) { * // use Guava method to copy * this.unmodifiableCopy = Collections.unmodifiableList(Lists.newArrayList(original)); * } * * // ... other methods * } * * // a warning will be raised because copy method, Guava's Lists.newArrayList(), is unrecognised * assertInstancesOf(SafelyCopiesAndWraps.class, areImmutable()); * * // use FieldAssumptions to insist the usage is safe * assertInstancesOf(SafelyCopiesAndWraps.class, * areImmutable(), * assumingFields("unmodifiableCopy").areSafelyCopiedUnmodifiableCollectionsWithImmutableElements()); * </code> * </pre> * <p> * This case will also work when the collection is declared (with generics) * to contain a mutable type. * * @see MutabilityReason#ABSTRACT_COLLECTION_TYPE_TO_FIELD * @see MutabilityReason#ABSTRACT_TYPE_TO_FIELD * @see MutabilityReason#COLLECTION_FIELD_WITH_MUTABLE_ELEMENT_TYPE */ public Matcher<MutableReasonDetail> areSafelyCopiedUnmodifiableCollectionsWithImmutableElements() { return new AssumeCopiedIntoUnmodifiable(); } private final class AssumeCopiedIntoUnmodifiable extends BaseMutableReasonDetailMatcher { @Override protected boolean matchesSafely(MutableReasonDetail reasonDetail) { return new FieldLocationWithNameMatcher().matches(reasonDetail.codeLocation()) && reasonDetail.reason().isOneOf(ABSTRACT_COLLECTION_TYPE_TO_FIELD, ABSTRACT_TYPE_TO_FIELD, COLLECTION_FIELD_WITH_MUTABLE_ELEMENT_TYPE); } } /** * Insists that a mutable field is used safely. * <p> * A requirement for immutability is * "If the instance fields include references to mutable objects, don't allow those objects to be changed" * [0]. This necessitates that any mutable fields are not modified (e.g. by * calling a method which mutates it) and their reference is not published * (where client code could invoke a mutating method). While greater care is * needed, it is possible to create immutable objects composed of mutable * fields. * <p> * Example usage: * * <pre> * <code> * import java.util.Date; * * @Immutable * public final class UsesMutableField { * private final Date myDate; * * public UsesMutableField(Date original) { * this.myDate = new Date(original.getTime()); * } * * public Date getDate() { * // if we used 'return myDate;' we would be publishing reference * return new Date(myDate.getTime()); * } * * // ... other methods, which never call myDate.setTime() * // if we called, e.g. setTime() we would be mutating the field * } * * // a warning will be raised because myDate is of a mutable type, java.util.Date * assertInstancesOf(UsesMutableField.class, areImmutable()); * * // use FieldAssumptions to insist the usage is safe * assertInstancesOf(UsesMutableField.class, * areImmutable(), * assumingFields("myDate").areNotModifiedAndDoNotEscape()); * </code> * </pre> * <p> * Note: this allowed reason also assumes the defensive copy of * <code>original</code> into <code>myDate</code>, although there is * currently no support for automatically detecting this. * * <p> * [0] <a href="http://docs.oracle.com/javase/tutorial/essential/concurrency/imstrat"> * A Strategy for Defining Immutable Objects</a> * * @see MutabilityReason#MUTABLE_TYPE_TO_FIELD * @see MutabilityReason#COLLECTION_FIELD_WITH_MUTABLE_ELEMENT_TYPE * @see MutabilityReason#ARRAY_TYPE_INHERENTLY_MUTABLE */ public Matcher<MutableReasonDetail> areNotModifiedAndDoNotEscape() { return new MutableFieldNotModifiedAndDoesntEscapeMatcher(); } private final class MutableFieldNotModifiedAndDoesntEscapeMatcher extends BaseMutableReasonDetailMatcher { @Override protected boolean matchesSafely(MutableReasonDetail reasonDetail) { return new FieldLocationWithNameMatcher().matches(reasonDetail.codeLocation()) && reasonDetail.reason().isOneOf(MUTABLE_TYPE_TO_FIELD, COLLECTION_FIELD_WITH_MUTABLE_ELEMENT_TYPE, ARRAY_TYPE_INHERENTLY_MUTABLE); } } /** * Insists that while a field may have been mutated, changes will not be * observable. * <p> * As described in the documentation for Java Concurrency In Practice's * @Immutable annotation, objects may maintain mutable state, as long * as the mutation is internal, and cannot be observed by callers. This can * be useful for providing caching within the object instance. A classic * example of this is the Open JDK's implementation of * {@link java.lang.String}. Each instance uses the <code>hash</code> field * to cache the result of {@link #hashCode()}. The hash field is computed * lazily, and the field is reassigned (a mutation), however clients of * {@link String} can not observe the mutation as it is internal. * <p> * While this technique is tricky, it can be very useful for performance * reasons. Unfortunately, Mutability Detector cannot tell the difference * between: lazily storing the result of a computation for future lookup; * and a setter method. * <p> * This allowed reason will permit mutable fields, and also reassigning field references. * <p> * Example usage: * <pre> * <code> * import java.util.Date; * * @Immutable * public static final class ReassignsHashCode { * private final String name; * private final Integer age; * private int hash; * * public ReassignsHashCode(String name, Integer age) { * this.name = name; * this.age = age; * } * * @Override * public int hashCode() { * if (hash == 0) { * hash = name.hashCode() + age.hashCode(); * } * return hash; * } * } * * // a warning will be raised because the hash field is reassigned, as with a setter method * assertInstancesOf(ReassignsHashCode.class, areImmutable()); * * // use FieldAssumptions to insist the usage is safe * assertInstancesOf(ReassignsHashCode.class, * areImmutable(), * assumingFields("hash").areModifiedAsPartOfAnUnobservableCachingStrategy()); * </code> * </pre> * * @see MutabilityReason#MUTABLE_TYPE_TO_FIELD * @see MutabilityReason#COLLECTION_FIELD_WITH_MUTABLE_ELEMENT_TYPE * @see MutabilityReason#ARRAY_TYPE_INHERENTLY_MUTABLE * @see MutabilityReason#FIELD_CAN_BE_REASSIGNED * @see MutabilityReason#NON_FINAL_FIELD */ public Matcher<MutableReasonDetail> areModifiedAsPartOfAnUnobservableCachingStrategy() { return new FieldModifiedAsPartOfAnUnobservableCachingStrategy(); } private final class FieldModifiedAsPartOfAnUnobservableCachingStrategy extends BaseMutableReasonDetailMatcher { @Override protected boolean matchesSafely(MutableReasonDetail reasonDetail) { return new FieldLocationWithNameMatcher().matches(reasonDetail.codeLocation()) && reasonDetail.reason().isOneOf(MUTABLE_TYPE_TO_FIELD, COLLECTION_FIELD_WITH_MUTABLE_ELEMENT_TYPE, ARRAY_TYPE_INHERENTLY_MUTABLE, FIELD_CAN_BE_REASSIGNED, NON_FINAL_FIELD); } } private class FieldLocationWithNameMatcher extends TypeSafeMatcher<FieldLocation> { @Override public void describeTo(Description description) { } @Override protected boolean matchesSafely(FieldLocation locationOfMutability) { return fieldNames.contains(locationOfMutability.fieldName()); } } }