org.mutabilitydetector.ConfigurationBuilder Maven / Gradle / Ivy
Show all versions of MutabilityDetector Show documentation
package org.mutabilitydetector;
/*
* #%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 com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import org.mutabilitydetector.asmoverride.AsmVerifierFactory.ClassloadingOption;
import org.mutabilitydetector.checkers.CheckerRunner.ExceptionPolicy;
import org.mutabilitydetector.checkers.MethodIs;
import org.mutabilitydetector.checkers.MutabilityAnalysisException;
import org.mutabilitydetector.checkers.MutabilityCheckerFactory.ReassignedFieldAnalysisChoice;
import org.mutabilitydetector.checkers.info.CopyMethod;
import org.mutabilitydetector.config.HardcodedResultsUsage;
import org.mutabilitydetector.locations.ClassNameConverter;
import org.mutabilitydetector.locations.Dotted;
import org.mutabilitydetector.unittesting.MutabilityAssert;
import org.objectweb.asm.Type;
import javax.annotation.concurrent.Immutable;
import javax.annotation.concurrent.NotThreadSafe;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import static org.mutabilitydetector.checkers.CheckerRunner.ExceptionPolicy.FAIL_FAST;
import static org.mutabilitydetector.checkers.MutabilityCheckerFactory.ReassignedFieldAnalysisChoice.NAIVE_PUT_FIELD_ANALYSIS;
import static org.mutabilitydetector.config.HardcodedResultsUsage.LOOKUP_WHEN_REFERENCED;
import static org.mutabilitydetector.locations.ClassNameConverter.CONVERTER;
import static org.mutabilitydetector.locations.Dotted.dotted;
/**
* Builds a {@link Configuration} for customising Mutability Detector's analysis.
*
* The most significant feature of {@link ConfigurationBuilder} is to allow
* defining hardcoded results for particular classes, which should be respected
* during analysis. For more details, see {@link MutabilityAssert}.
*
*
* Users should subclass {@link ConfigurationBuilder} and override the
* {@link #configure()} method, which should then be used to construct a
* MutabilityAsserter instance.
*
* For example:
*
*
*
* MutabilityAsserter myAsserter = MutabilityAsserter.configured(new ConfigurationBuilder() {
* @Override public void configure() {
*
* hardcodeAsDefinitelyImmutable(SomeClass.class);
* setExceptionPolicy(ExceptionPolicy.CARRY_ON);
*
* mergeHardcodedResultsFrom(ConfigurationBuilder.DEFAULT_CONFIGURATION);
* }
* });
*
*
*
* This class also provides an out-of-the-box configuration with hardcoded
* results for common JDK classes. This includes classes where Mutability
* Detector's analysis is incorrect, for example, java.lang.String,
* java.lang.Integer (and other primitive wrapper types) and
* java.math.BigDecimal.
*
* @see Configurations#JDK_CONFIGURATION
*
*/
@NotThreadSafe
public abstract class ConfigurationBuilder {
/**
* Subclasses should override this method to configure analysis.
*
* It is recommended that any custom {@link Configuration}'s merge with the
* {@link Configurations#OUT_OF_THE_BOX_CONFIGURATION} in order to remain consistent with
* {@link MutabilityAssert}, and the command line settings. For example:
*
*
*
* MutabilityAsserter myAsserter = MutabilityAsserter.configured(new ConfigurationBuilder() {
* @Override public void configure() {
* mergeHardcodedResultsFrom(ConfigurationBuilder.OUT_OF_THE_BOX_CONFIGURATION);
* }
* });
*
*
* Similarly for {@link DefaultCachingAnalysisSession#createWithCurrentClassPath(Configuration)}
*
* The available configuration methods are listed below.
*
* @see #hardcodeResult(AnalysisResult)
* @see #hardcodeResults(AnalysisResult...)
* @see #hardcodeResults(Iterable)
* @see #hardcodeAsDefinitelyImmutable(Class)
* @see #hardcodeAsDefinitelyImmutable(String)
*
* @see #setHowToUseHardcodedResults(HardcodedResultsUsage)
*
* @see #mergeHardcodedResultsFrom(Configuration)
* @see #setExceptionPolicy(ExceptionPolicy)
*
* @see Configurations#OUT_OF_THE_BOX_CONFIGURATION
*/
public abstract void configure();
public final Configuration build() {
configure();
return new DefaultConfiguration(
hardcodedResults.build(),
hardcodedImmutableContainerClasses.build(),
exceptionPolicy,
classloadingOption,
reassignedFieldAlgorithm,
validCopyMethods.build(),
howToUseHardcodedResults);
}
private ImmutableSet.Builder hardcodedResults = ImmutableSet.builder();
private ImmutableSet.Builder hardcodedImmutableContainerClasses = ImmutableSet.builder();
private ExceptionPolicy exceptionPolicy = FAIL_FAST;
private ClassloadingOption classloadingOption = ClassloadingOption.ENABLED;
private ReassignedFieldAnalysisChoice reassignedFieldAlgorithm = NAIVE_PUT_FIELD_ANALYSIS;
private ImmutableSetMultimap.Builder validCopyMethods = ImmutableSetMultimap.builder();
private HardcodedResultsUsage howToUseHardcodedResults = LOOKUP_WHEN_REFERENCED;
/**
* Configures how Mutability Detector's analysis should respond to
* exceptions during analysis.
*
* During analysis, an exception may occur which is recoverable. That is,
* Mutability Detector is able to continue it's analysis, and may
* produce valid results.
*
* The default behaviour is to use {@link ExceptionPolicy#FAIL_FAST},
* meaning any unhandled exceptions will propogate up past the assertion,
* and cause a failing test.
*
* Setting this configuration flag to {@link ExceptionPolicy#CARRY_ON} may
* allow unit tests to function where exceptions don't necessarily preclude
* a useful output. For example, consider a class which you wish to make
* immutable; a test for that class fails with an unhandled exception. If
* that test has, say 10 reasons for mutability, and 1 of those causes the
* test to abort with an exception, you have just lost out on 90% of the
* required information. {@link ExceptionPolicy#CARRY_ON} will allow the
* test to report 9 out of 10 reasons. The test may be useful, although it
* won't be comprehensive.
*
*
* If you are unlucky enough to have a test which results in an exception,
* please report it to the Mutability Detector project, at the project
* homepage.
*
*
* @see Configuration#exceptionPolicy()
* @param exceptionPolicy
* - how to respond to exceptions during analysis. Defaults to
* {@link ExceptionPolicy#FAIL_FAST}
*/
protected final void setExceptionPolicy(ExceptionPolicy exceptionPolicy) {
this.exceptionPolicy = exceptionPolicy;
}
/**
* Configures whether Mutability Detector loads classes or not during analysis.
*
* Mutability Detector often needs to analyse classes other than the one
* specified in order to gain a more accurate result. The default behaviour
* is to load these classes from the current classpath. Setting this flag to
* {@link ClassloadingOption#DISABLED} will instruct Mutability Detector not to
* attempt to load classes, and instead use a method of analysing
* all classes, which guarantees not to load classes. This can save on heap requirements
* as Mutability Detector's non classloading approach requires less data than class loading.
*
* For the moment, this option is recommended if you find classloading takes up too much heap,
* and the classes loaded for analysis won't be loaded anyway.
*
* @see Configuration#classloadingOption()
* @param classloadingOption - whether to allow class loading for analysis or not
*/
protected final void setClassloadingPolicy(ClassloadingOption classloadingOption) {
this.classloadingOption = classloadingOption;
}
/**
* Add a predefined result used during analysis.
*
* Hardcoding a result means that information queried about a class will
* honour the result you have set. For example, if during analysis,
* Mutability Detector has to discover whether a field type is mutable or
* not. However, requesting the {@link AnalysisResult} of the class in
* question directly will return the real result from the actual analysis.
* This holds for unit tests, command line runs, and runtime analysis. As
* such, calling this method will have no effect when querying an
* AnalysisResult directly.
*
* @see Configuration#hardcodedResults()
* @see AnalysisResult
* @see AnalysisSession#resultFor(org.mutabilitydetector.locations.Dotted)
*/
protected final void hardcodeResult(AnalysisResult result) {
hardcodedResults.add(result);
}
/**
* Adds all the given AnalysisResults, as if hardcodeResult was called for
* each element in the iterable.
*
* @see AnalysisResult
* @see #hardcodeResult(AnalysisResult)
*/
protected final void hardcodeResults(Iterable result) {
hardcodedResults.addAll(result);
}
/**
* Adds all the given AnalysisResults, as if hardcodeResult was called for
* each element in the var args parameter.
*
* @see AnalysisResult
* @see #hardcodeResult(AnalysisResult)
*/
protected final void hardcodeResults(AnalysisResult... result) {
hardcodeResults(Arrays.asList(result));
}
/**
* Hardcodes a result indicating the given class is Immutable.
*
* @see AnalysisResult#definitelyImmutable(String)
*/
protected final void hardcodeAsDefinitelyImmutable(Class> immutableClass) {
hardcodeResult(AnalysisResult.definitelyImmutable(Dotted.fromClass(immutableClass)));
}
/**
* Hardcodes a result indicating the given class is Immutable.
*
* The most reliable format of class name would be the dotted version of the
* fully qualified name, e.g. java.lang.String. However, there will be a
* "best effort" attempt to accept other formats, e.g. java/lang/String, or
* java.lang.String.class.
*
* @see ClassNameConverter
* @see AnalysisResult#definitelyImmutable(String)
*/
protected final void hardcodeAsDefinitelyImmutable(String immutableClassName) {
hardcodeResult(AnalysisResult.definitelyImmutable(dotted(immutableClassName)));
}
/**
* Specifies how hardcoded results should be used.
*
* The default is {@link HardcodedResultsUsage#LOOKUP_WHEN_REFERENCED} which was the implicit behaviour in
* Mutability Detector prior to the introduction of this configuration setting.
*
* @see HardcodedResultsUsage#LOOKUP_WHEN_REFERENCED
*/
protected final void setHowToUseHardcodedResults(HardcodedResultsUsage usage) {
this.howToUseHardcodedResults = usage;
}
/**
* Returns an immutable snapshot of the hardcoded results as at time of calling.
*
* Note changes the returned Set will not allow modifications, and will not
* reflect changes to the underlying configuration.
*/
protected final Set getCurrentlyHardcodedResults() {
return hardcodedResults.build();
}
/**
* Configures a generic container type as immutable.
*
* Should be used for classes which, while immutable, contain potentially mutable elements. Mutability Detector's
* analysis will warn on classes which use immutable container classes parameterised with mutable types, but allow
* container classes parameterised with immutable types.
*
* For example:
*
* private final ImmutableContainer<java.util.Date> // considered mutable
* private final ImmutableContainer<GENERIC_TYPE_VARIABLE> // considered mutable
* private final ImmutableContainer<?> // considered mutable
* private final ImmutableContainer<String> // considered immutable
*
*
* A good example of an immutable container type is JDK 8's java.util.Optional. It has a single, final field, and
* cannot be subclassed. However, the generic parameter allows an instance of Optional to contain a mutable type,
* which can be modified by any code with a reference. However, if a class has an Optional field containing an
* immutable type, e.g. Optional<String>
then it should not cause the class to be considered
* mutable.
*
* Be default java.util.Optional is considered an immutable container class.
*
* @param immutableContainerClass class that is immutable as long as contained elements are immutable
* @see MutabilityReason#COLLECTION_FIELD_WITH_MUTABLE_ELEMENT_TYPE
*/
protected final void hardcodeAsImmutableContainerType(Class> immutableContainerClass) {
hardcodeAsImmutableContainerType(Dotted.fromClass(immutableContainerClass));
}
protected final void hardcodeAsImmutableContainerType(String immutableContainerClassName) {
hardcodeAsImmutableContainerType(dotted(CONVERTER.dotted(immutableContainerClassName)));
}
private void hardcodeAsImmutableContainerType(Dotted immutableContainerClassName) {
hardcodedImmutableContainerClasses.add(immutableContainerClassName);
}
/**
* Merges the hardcoded results of this Configuration with the given
* Configuration.
*
* The resultant hardcoded results will be the union of the two sets of
* hardcoded results. Where the AnalysisResult for a class is found in both
* Configurations, the result from otherConfiguration will replace the
* existing result in this Configuration. This replacement behaviour will
* occur for subsequent calls to
* {@link #mergeHardcodedResultsFrom(Configuration)}.
*
* @param otherConfiguration - Configuration to merge hardcoded results with.
*/
protected void mergeHardcodedResultsFrom(Configuration otherConfiguration) {
Map resultsMap = hardcodedResults.build().stream()
.collect(Collectors.toMap(r -> r.className, r -> r));
resultsMap.putAll(otherConfiguration.hardcodedResults());
hardcodedResults = ImmutableSet.builder().addAll(resultsMap.values());
}
/**
* Merges the immutable container types of this Configuration with the given
* Configuration.
*
* The resultant immutable container types results will be the union of the two sets of
* immutable container types. Where the type is found in both
* Configurations, the result from otherConfiguration will replace the
* existing result in this Configuration. This replacement behaviour will
* occur for subsequent calls to
* {@link #mergeImmutableContainerTypesFrom(Configuration)} .
*
* @param otherConfiguration - Configuration to merge immutable container types with.
*/
protected void mergeImmutableContainerTypesFrom(Configuration otherConfiguration) {
Set union =
Sets.union(hardcodedImmutableContainerClasses.build(), otherConfiguration.immutableContainerClasses());
hardcodedImmutableContainerClasses = ImmutableSet.builder().addAll(union);
}
/**
* Merge valid copy methods from another configuration.
*
* @param otherConfiguration - configuration to merge harcoded copy methods from.
*/
protected void mergeValidCopyMethodsFrom(Configuration otherConfiguration) {
validCopyMethods.putAll(otherConfiguration.hardcodedCopyMethods());
}
/**
* Merges configuration from another configuration with this one.
*
* As with other merge methods, where the same setting is present in both configurations, the settings from
* other
will be applied.
*
* Merging applies only to the configuration applied to certain classes, such as hardcoded results. It does not
* change this Configuration
's setting for e.g. @{link #setExceptionPolicy}.
*
*
* @param otherConfiguration configuration to merge from
* @see #mergeHardcodedResultsFrom(Configuration)
* @see #mergeImmutableContainerTypesFrom(Configuration)
* @see #mergeValidCopyMethodsFrom(Configuration)
*/
protected void merge(Configuration otherConfiguration) {
mergeHardcodedResultsFrom(otherConfiguration);
mergeImmutableContainerTypesFrom(otherConfiguration);
mergeValidCopyMethodsFrom(otherConfiguration);
}
protected void useAdvancedReassignedFieldAlgorithm() {
this.reassignedFieldAlgorithm = ReassignedFieldAnalysisChoice.LAZY_INITIALISATION_ANALYSIS;
}
/**
* Hardcode a copy method as being valid. This should be used to tell Mutability Detector about
* a method which copies a collection, and when the copy can be wrapped in an immutable wrapper
* we can consider the assignment immutable. Useful for allowing Mutability Detector to correctly
* work with other collections frameworks such as Google Guava. Reflection is used to obtain the
* method's descriptor and to verify the method's existence.
*
* @param fieldType - the type of the field to which the result of the copy is assigned
* @param fullyQualifiedMethodName - the fully qualified method name
* @param argType - the type of the argument passed to the copy method
*
* @throws MutabilityAnalysisException - if the specified class or method does not exist
* @throws IllegalArgumentException - if any of the arguments are null
*/
protected final void hardcodeValidCopyMethod(Class> fieldType, String fullyQualifiedMethodName, Class> argType) {
if (argType==null || fieldType==null || fullyQualifiedMethodName==null) {
throw new IllegalArgumentException("All parameters must be supplied - no nulls");
}
String className = fullyQualifiedMethodName.substring(0, fullyQualifiedMethodName.lastIndexOf("."));
String methodName = fullyQualifiedMethodName.substring(fullyQualifiedMethodName.lastIndexOf(".")+1);
String desc = null;
try {
if (MethodIs.aConstructor(methodName)) {
Constructor> ctor = Class.forName(className).getDeclaredConstructor(argType);
desc = Type.getConstructorDescriptor(ctor);
} else {
Method method = Class.forName(className).getMethod(methodName, argType);
desc = Type.getMethodDescriptor(method);
}
} catch (NoSuchMethodException e) {
rethrow("No such method", e);
} catch (SecurityException e) {
rethrow("Security error", e);
} catch (ClassNotFoundException e) {
rethrow("Class not found", e);
}
CopyMethod copyMethod = new CopyMethod(dotted(className), methodName, desc);
hardcodeValidCopyMethod(fieldType, copyMethod);
}
protected void hardcodeValidCopyMethod(Class> fieldType, CopyMethod copyMethod) {
validCopyMethods.put(fieldType.getCanonicalName(), copyMethod);
}
public Multimap getCopyMethodsAllowed() {
return validCopyMethods.build();
}
private void rethrow(String message, Throwable e) {
throw new MutabilityAnalysisException("Error in configuration: "+message+": "+e.getMessage(), e);
}
@Immutable
private static final class DefaultConfiguration implements Configuration {
private final ImmutableSet hardcodedResults;
private final HardcodedResultsUsage howToUseHardcodedResults;
private final ImmutableMap resultsByClassname;
private final ImmutableSet immutableContainerClasses;
private final ImmutableSetMultimap validCopyMethods;
private final ExceptionPolicy exceptionPolicy;
private final ClassloadingOption classloadingOption;
private final ReassignedFieldAnalysisChoice reassignedFieldAlgorithm;
private DefaultConfiguration(ImmutableSet predefinedResults,
ImmutableSet immutableContainerClasses,
ExceptionPolicy exceptionPolicy,
ClassloadingOption classloadingOption,
ReassignedFieldAnalysisChoice reassignedFieldAlgorithm,
ImmutableSetMultimap validCopyMethods,
HardcodedResultsUsage howToUseHardcodedResults) {
this.immutableContainerClasses = immutableContainerClasses;
this.exceptionPolicy = exceptionPolicy;
this.hardcodedResults = predefinedResults;
this.classloadingOption = classloadingOption;
this.howToUseHardcodedResults = howToUseHardcodedResults;
this.resultsByClassname = ImmutableMap.copyOf(hardcodedResults.stream()
.collect(Collectors.toMap(r -> r.className, r -> r)));
this.reassignedFieldAlgorithm = reassignedFieldAlgorithm;
this.validCopyMethods = validCopyMethods;
}
@Override
public Map hardcodedResults() {
return resultsByClassname;
}
@Override
public HardcodedResultsUsage howToUseHardcodedResults() {
return howToUseHardcodedResults;
}
@Override
public Set immutableContainerClasses() { return immutableContainerClasses; }
@Override
public ExceptionPolicy exceptionPolicy() {
return exceptionPolicy;
}
@Override
public ClassloadingOption classloadingOption() { return classloadingOption; }
@Override
public ReassignedFieldAnalysisChoice reassignedFieldAlgorithm() {
return reassignedFieldAlgorithm;
}
@Override
public ImmutableSetMultimap hardcodedCopyMethods() {
return validCopyMethods;
}
}
}