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.Iterables.transform;
import static java.util.Arrays.asList;
import static java.util.Collections.singleton;
import static org.hamcrest.core.AnyOf.anyOf;
import static org.mutabilitydetector.MutabilityReason.ABSTRACT_TYPE_TO_FIELD;
import static org.mutabilitydetector.MutabilityReason.COLLECTION_FIELD_WITH_MUTABLE_ELEMENT_TYPE;
import static org.mutabilitydetector.MutabilityReason.MUTABLE_TYPE_TO_FIELD;
import org.hamcrest.Matcher;
import org.mutabilitydetector.MutableReasonDetail;
import org.mutabilitydetector.checkers.MutableTypeToFieldChecker;
import org.mutabilitydetector.locations.Dotted;
import com.google.common.collect.Iterables;
public final class ProvidedOtherClass {
private final Iterable<Dotted> dottedClassNames;
private ProvidedOtherClass(Iterable<Dotted> dottedClassName) {
this.dottedClassNames = dottedClassName;
}
public static ProvidedOtherClass provided(Dotted className) {
return provided(singleton(className));
}
public static ProvidedOtherClass provided(Dotted... classNames) {
return provided(asList(classNames));
}
public static ProvidedOtherClass provided(Iterable<Dotted> classNames) {
return new ProvidedOtherClass(classNames);
}
/**
* Assumes that the selected type is immutable, preventing warnings related
* to transitive mutability.
* <p>
* One common way for classes to be rendered mutable is to contain a mutable
* field. Another way is that the class is extensible (non-final). The
* interaction of these rules can occasionally conflict with the notion of
* abstraction. For example, consider the following classes:
*
* <pre>
* </code>
* // implementations MUST be immutable
* public interface Named {
* String getName();
* }
*
* public final class HasSomethingNamed {
* private final Named named;
* public HasSomethingNamed(Named named) {
* this.named = named;
* }
*
* public String getNameOfYourThing() {
* return this.named.getName();
* }
* }
* </code>
* </pre>
*
* In this contrived example, the interface Named is abstracting something.
* It would be preferable to be able to depend on that abstraction, rather
* than a concrete implementation. Unfortunately, any implementation of
* Named <strong>could</strong> violate the condition that it must be
* immutable. If the Named implementation given to the constructor of
* HasSomethingNamed is actually mutable, it causes HasSomething named to be
* mutable as well. Consider this code:
*
* <pre>
* </code>
* SneakyMutableNamed n = new SneakyMutableNamed("Jimmy");
* HasSomethingNamed h = new HasSomethingNamed(n);
*
* String nameOnFirstCall = h.getNameOfYourThing();
* n.myReassignableName = "Bobby";
* String nameOnSecondCall = h.getNameOfYourThing();
* </code>
* </pre>
*
* Here, because a sneaky subclass of Named is mutated, the instance of
* HasSomethingNamed has been observed to change (getNameOfYourThing() first
* returns "Jimmy" then "Bobby").
* <p>
* Despite this limitation, it can still be preferable that the abstract
* class is given as a parameter. Perhaps you are able to trust that all
* implementations <strong>are</strong> immutable. In that case, Mutability
* Detector raising a warning on HasSomethingNamed would be considered a
* false positive. This reason allows the test to pass.
* <p>
* Example usage:
*
* <pre><code>
* assertInstancesOf(HasSomethingNamed.class,
* areImmutable(),
* AllowedReason.provided(Named.class).isAlsoImmutable());
* </pre></code>
*
* Not that this also allows a field which is a collection type, with Named as a generic element type.
*/
public Matcher<MutableReasonDetail> isAlsoImmutable() {
final Matcher<MutableReasonDetail> allowGenericTypes = new AllowedIfOtherClassIsGenericTypeOfCollectionField(dottedClassNames);
return anyOf(allowGenericTypes, anyOf(transform(dottedClassNames, AllowedIfOtherClassIsImmutable::new)));
}
/**
* Assumes that the selected type is immutable, preventing warnings related
* to transitive mutability.
* <p>
* One common way for classes to be rendered mutable is to contain a mutable
* field. Another way is that the class is extensible (non-final). The
* interaction of these rules can occasionally conflict with the notion of
* abstraction. For example, consider the following classes:
*
* <pre>
* </code>
* // implementations MUST be immutable
* public interface Named {
* String getName();
* }
*
* public final class HasSomethingNamed {
* private final Named named;
* public HasSomethingNamed(Named named) {
* this.named = named;
* }
*
* public String getNameOfYourThing() {
* return this.named.getName();
* }
* }
* </code>
* </pre>
*
* In this contrived example, the interface Named is abstracting something.
* It would be preferable to be able to depend on that abstraction, rather
* than a concrete implementation. Unfortunately, any implementation of
* Named <strong>could</strong> violate the condition that it must be
* immutable. If the Named implementation given to the constructor of
* HasSomethingNamed is actually mutable, it causes HasSomething named to be
* mutable as well. Consider this code:
*
* <pre>
* </code>
* SneakyMutableNamed n = new SneakyMutableNamed("Jimmy");
* HasSomethingNamed h = new HasSomethingNamed(n);
*
* String nameOnFirstCall = h.getNameOfYourThing();
* n.myReassignableName = "Bobby";
* String nameOnSecondCall = h.getNameOfYourThing();
* </code>
* </pre>
*
* Here, because a sneaky subclass of Named is mutated, the instance of
* HasSomethingNamed has been observed to change (getNameOfYourThing() first
* returns "Jimmy" then "Bobby)".
* <p>
* Despite this limitation, it can still be preferable that the abstract
* class is given as a parameter. Perhaps you are able to trust that all
* implementations <strong>are</strong> immutable. In that case, Mutability
* Detector raising a warning on HasSomethingNamed would be considered a
* false positive. This reason allows the test to pass.
* <p>
* Example usage:
*
* <pre><code>
* assertInstancesOf(HasSomethingNamed.class,
* areImmutable(),
* AllowedReason.provided(Named.class).areAlsoImmutable());
* </pre></code>
*
* Not that this also allows a field which is a collection type, with Named as a generic element type.
*/
public Matcher<MutableReasonDetail> areAlsoImmutable() {
return isAlsoImmutable();
}
private static final class AllowedIfOtherClassIsImmutable extends BaseMutableReasonDetailMatcher {
private final Dotted className;
public AllowedIfOtherClassIsImmutable(Dotted dottedClassName) {
this.className = dottedClassName;
}
@Override
protected boolean matchesSafely(MutableReasonDetail reasonDetail) {
return isAssignedField(reasonDetail);
}
private boolean isAssignedField(MutableReasonDetail reasonDetail) {
return reasonDetail.reason().isOneOf(ABSTRACT_TYPE_TO_FIELD, MUTABLE_TYPE_TO_FIELD)
&& reasonDetail.message().contains(classNameAsItAppearsInDescription());
}
/**
* This matcher has to check against string created by the checker, which may change.
* @see MutableTypeToFieldChecker
*/
private String classNameAsItAppearsInDescription() {
return "(" + className.asString() + ")";
}
}
private static final class AllowedIfOtherClassIsGenericTypeOfCollectionField extends BaseMutableReasonDetailMatcher {
private final Iterable<Dotted> classNames;
public AllowedIfOtherClassIsGenericTypeOfCollectionField(Iterable<Dotted> classNames) {
this.classNames = classNames;
}
@Override
protected boolean matchesSafely(MutableReasonDetail reasonDetail) {
return allowedIfCollectionTypeWhereAllGenericElementsAreConsideredImmutable(reasonDetail);
}
private boolean allowedIfCollectionTypeWhereAllGenericElementsAreConsideredImmutable(MutableReasonDetail reasonDetail) {
return reasonDetail.reason().isOneOf(COLLECTION_FIELD_WITH_MUTABLE_ELEMENT_TYPE)
&& allElementTypesAreConsideredImmutable(reasonDetail.message());
}
/**
* This matcher has to check against string created by the checker, which may change.
* @see MutableTypeToFieldChecker
*/
private boolean allElementTypesAreConsideredImmutable(String message) {
String fieldTypeDescription = message.substring(message.indexOf("("), message.indexOf(")") + 1);
String generics = fieldTypeDescription.substring(fieldTypeDescription.indexOf("<") + 1, fieldTypeDescription.lastIndexOf(">"));
String[] genericsTypesDescription = generics.contains(", ")
? generics.split(", ")
: new String[] { generics };
for (String genericType : genericsTypesDescription) {
if (!Iterables.contains(classNames, Dotted.dotted(genericType))) {
return false;
}
}
return true;
}
}
}