diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/combinator/MutatorCombinators.java b/src/main/java/com/code_intelligence/jazzer/mutation/combinator/MutatorCombinators.java index 567194435..7e8a95305 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/combinator/MutatorCombinators.java +++ b/src/main/java/com/code_intelligence/jazzer/mutation/combinator/MutatorCombinators.java @@ -29,6 +29,7 @@ import com.code_intelligence.jazzer.mutation.api.Serializer; import com.code_intelligence.jazzer.mutation.api.SerializingInPlaceMutator; import com.code_intelligence.jazzer.mutation.api.SerializingMutator; +import com.code_intelligence.jazzer.mutation.support.Preconditions; import com.google.errorprone.annotations.ImmutableTypeParameter; import java.io.DataInputStream; import java.io.DataOutputStream; @@ -428,6 +429,102 @@ public String toDebugString(Predicate isInCycle) { }; } + /** + * Mutates a sum type (e.g. a sealed interface), preferring to mutate the current state but + * occasionally switching to a different state. + * + * @param getState a function that returns the current state of the sum type as an index into + * {@code perStateMutators}, or -1 if the state is indeterminate. + * @param perStateMutators the mutators for each state + * @return a mutator that mutates the sum type + */ + @SafeVarargs + public static SerializingMutator mutateSum( + ToIntFunction getState, SerializingMutator... perStateMutators) { + Preconditions.require(perStateMutators.length > 0, "At least one mutator must be provided"); + if (perStateMutators.length == 1) { + return perStateMutators[0]; + } + boolean hasFixedSize = stream(perStateMutators).allMatch(SerializingMutator::hasFixedSize); + final SerializingMutator[] mutators = + Arrays.copyOf(perStateMutators, perStateMutators.length); + return new SerializingMutator() { + @Override + public T init(PseudoRandom prng) { + return mutators[prng.indexIn(mutators)].init(prng); + } + + @Override + public T mutate(T value, PseudoRandom prng) { + int currentState = getState.applyAsInt(value); + if (currentState == -1) { + // The value is in an indeterminate state, initialize it. + return init(prng); + } + if (prng.trueInOneOutOf(100)) { + // Initialize to a different state. + return mutators[prng.otherIndexIn(mutators, currentState)].init(prng); + } + // Mutate within the current state. + return mutators[currentState].mutate(value, prng); + } + + @Override + public T crossOver(T value, T otherValue, PseudoRandom prng) { + // Try to cross over in current state and leave state changes to the mutate step. + int currentState = getState.applyAsInt(value); + int otherState = getState.applyAsInt(otherValue); + if (currentState == -1) { + // If reference is not initialized to a concrete state yet, try to do so in + // the state of other reference, as that's at least some progress. + if (otherState == -1) { + // If both states are indeterminate, cross over can not be performed. + return value; + } + return mutators[otherState].init(prng); + } + if (currentState == otherState) { + return mutators[currentState].crossOver(value, otherValue, prng); + } + return value; + } + + @Override + public T detach(T value) { + int currentState = getState.applyAsInt(value); + if (currentState == -1) { + return value; + } + return mutators[currentState].detach(value); + } + + @Override + public T read(DataInputStream in) throws IOException { + int currentState = Math.floorMod(in.readInt(), mutators.length); + return mutators[currentState].read(in); + } + + @Override + public void write(T value, DataOutputStream out) throws IOException { + int currentState = getState.applyAsInt(value); + out.writeInt(currentState); + mutators[currentState].write(value, out); + } + + @Override + public boolean hasFixedSize() { + return hasFixedSize; + } + + @Override + public String toDebugString(Predicate isInCycle) { + return stream(mutators) + .map(mutator -> mutator.toDebugString(isInCycle)) + .collect(joining(" | ", "(", ")")); + } + }; + } + /** * Use {@link #markAsRequiringRecursionBreaking(SerializingMutator)} instead for {@link * com.code_intelligence.jazzer.mutation.api.ValueMutator}. diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/Mutators.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/Mutators.java index 381e47f95..c0b810395 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/Mutators.java +++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/Mutators.java @@ -40,8 +40,9 @@ public static ExtendedMutatorFactory newFactory() { CollectionMutators.newFactories(), ProtoMutators.newFactories(), LibFuzzerMutators.newFactories(), - AggregateMutators.newFactories(), - TimeMutators.newFactories()); + TimeMutators.newFactories(), + // Keep generic aggregate mutators last in case a concrete type is also an aggregate type. + AggregateMutators.newFactories()); } // Mutators for which the NullableMutatorFactory diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/AggregateMutators.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/AggregateMutators.java index bd65b35c1..854d4a04d 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/AggregateMutators.java +++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/AggregateMutators.java @@ -25,26 +25,43 @@ private AggregateMutators() {} public static Stream newFactories() { // Register the record mutator first as it is more specific. - return Stream.concat( - newRecordMutatorFactoryIfSupported(), - Stream.of( - new SetterBasedBeanMutatorFactory(), - new ConstructorBasedBeanMutatorFactory(), - new CachedConstructorMutatorFactory())); + return Stream.of( + newRecordMutatorFactoryIfSupported(), + newSealedClassMutatorFactoryIfSupported(), + Stream.of( + new SetterBasedBeanMutatorFactory(), + new ConstructorBasedBeanMutatorFactory(), + new CachedConstructorMutatorFactory())) + .flatMap(s -> s); } private static Stream newRecordMutatorFactoryIfSupported() { - if (!supportsRecords()) { + try { + Class.forName("java.lang.Record"); + return Stream.of(instantiateMutatorFactory("RecordMutatorFactory")); + } catch (ClassNotFoundException ignored) { + return Stream.empty(); + } + } + + private static Stream newSealedClassMutatorFactoryIfSupported() { + try { + Class.class.getMethod("getPermittedSubclasses"); + return Stream.of(instantiateMutatorFactory("SealedClassMutatorFactory")); + } catch (NoSuchMethodException e) { return Stream.empty(); } + } + + private static MutatorFactory instantiateMutatorFactory(String simpleClassName) { try { - // Instantiate RecordMutatorFactory via reflection as making it a compile time dependency - // breaks the r8 step in the Android build. - Class recordMutatorFactory; - recordMutatorFactory = - Class.forName(AggregateMutators.class.getPackage().getName() + ".RecordMutatorFactory") + // Instantiate factory via reflection as making it a compile time dependency breaks the r8 + // step in the Android build. + Class factory; + factory = + Class.forName(AggregateMutators.class.getPackage().getName() + "." + simpleClassName) .asSubclass(MutatorFactory.class); - return Stream.of(recordMutatorFactory.getDeclaredConstructor().newInstance()); + return factory.getDeclaredConstructor().newInstance(); } catch (ClassNotFoundException | NoSuchMethodException | InstantiationException @@ -53,13 +70,4 @@ private static Stream newRecordMutatorFactoryIfSupported() { throw new IllegalStateException(e); } } - - private static boolean supportsRecords() { - try { - Class.forName("java.lang.Record"); - return true; - } catch (ClassNotFoundException ignored) { - return false; - } - } } diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/BUILD.bazel index 81d18d401..436b8244c 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/BUILD.bazel +++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/BUILD.bazel @@ -6,6 +6,7 @@ java_library( "AggregatesHelper.java", "BeanSupport.java", "RecordMutatorFactory.java", + "SealedClassMutatorFactory.java", ], ), visibility = [ @@ -16,6 +17,7 @@ java_library( "@platforms//os:android": [], "//conditions:default": [ ":record_mutator_factory", + ":sealed_class_mutator_factory", ], }), deps = [ @@ -40,6 +42,20 @@ java_library( ], ) +java_library( + name = "sealed_class_mutator_factory", + srcs = ["SealedClassMutatorFactory.java"], + javacopts = [ + "--release", + "17", + ], + deps = [ + "//src/main/java/com/code_intelligence/jazzer/mutation/api", + "//src/main/java/com/code_intelligence/jazzer/mutation/combinator", + "//src/main/java/com/code_intelligence/jazzer/mutation/support", + ], +) + java_library( name = "aggregates_helper", srcs = [ diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/SealedClassMutatorFactory.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/SealedClassMutatorFactory.java new file mode 100644 index 000000000..6c45e3891 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/SealedClassMutatorFactory.java @@ -0,0 +1,64 @@ +/* + * Copyright 2025 Code Intelligence GmbH + * + * 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.code_intelligence.jazzer.mutation.mutator.aggregate; + +import static com.code_intelligence.jazzer.mutation.support.StreamSupport.toArrayOrEmpty; +import static java.util.Arrays.stream; + +import com.code_intelligence.jazzer.mutation.api.ExtendedMutatorFactory; +import com.code_intelligence.jazzer.mutation.api.MutatorFactory; +import com.code_intelligence.jazzer.mutation.api.SerializingMutator; +import com.code_intelligence.jazzer.mutation.combinator.MutatorCombinators; +import com.code_intelligence.jazzer.mutation.support.TypeSupport; +import java.lang.reflect.AnnotatedType; +import java.util.Optional; +import java.util.function.ToIntFunction; + +final class SealedClassMutatorFactory implements MutatorFactory { + @Override + public Optional> tryCreate( + AnnotatedType type, ExtendedMutatorFactory factory) { + if (!(type.getType() instanceof Class)) { + return Optional.empty(); + } + Class[] permittedSubclasses = + (Class[]) ((Class) type.getType()).getPermittedSubclasses(); + if (permittedSubclasses == null) { + return Optional.empty(); + } + + ToIntFunction getState = + (value) -> { + // We can't use value.getClass() as it might be a subclass of the permitted (direct) + // subclasses. + for (int i = 0; i < permittedSubclasses.length; i++) { + if (permittedSubclasses[i].isInstance(value)) { + return i; + } + } + return -1; + }; + return toArrayOrEmpty( + stream(permittedSubclasses) + .map(TypeSupport::asAnnotatedType) + .map(TypeSupport::notNull) + .map(factory::tryCreate), + SerializingMutator[]::new) + .map( + mutators -> MutatorCombinators.mutateSum(getState, (SerializingMutator[]) mutators)); + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/support/TypeSupport.java b/src/main/java/com/code_intelligence/jazzer/mutation/support/TypeSupport.java index 5bdb8aa13..2480d4e93 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/support/TypeSupport.java +++ b/src/main/java/com/code_intelligence/jazzer/mutation/support/TypeSupport.java @@ -29,6 +29,7 @@ import com.code_intelligence.jazzer.mutation.annotation.WithLength; import com.code_intelligence.jazzer.mutation.utils.PropertyConstraint; import java.lang.annotation.Annotation; +import java.lang.annotation.Inherited; import java.lang.reflect.AnnotatedArrayType; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.AnnotatedParameterizedType; @@ -94,6 +95,65 @@ public static Optional> asSubclassOrEmpty( return Optional.of(actualClazz.asSubclass(superclass)); } + /** + * Synthesizes an {@link AnnotatedType} for the given {@link Class}. + * + *

Usage of this method should be avoided in favor of obtaining annotated types in a natural + * way if possible (e.g. prefer {@link Class#getAnnotatedSuperclass()} to {@link + * Class#getSuperclass()}. + */ + public static AnnotatedType asAnnotatedType(Class clazz) { + requireNonNull(clazz); + return new AnnotatedType() { + @Override + public Type getType() { + return clazz; + } + + @Override + public T getAnnotation(Class annotationClass) { + return annotatedElementGetAnnotation(this, annotationClass); + } + + @Override + public Annotation[] getAnnotations() { + // No directly present annotations, look for inheritable present annotations on the + // superclass. + if (clazz.getSuperclass() == null) { + return new Annotation[0]; + } + return stream(clazz.getSuperclass().getAnnotations()) + .filter( + annotation -> + annotation.annotationType().getDeclaredAnnotation(Inherited.class) != null) + .toArray(Annotation[]::new); + } + + @Override + public Annotation[] getDeclaredAnnotations() { + // No directly present annotations. + return new Annotation[0]; + } + + @Override + public String toString() { + return annotatedTypeToString(this); + } + + @Override + public int hashCode() { + throw new UnsupportedOperationException( + "hashCode() is not supported as its behavior isn't specified"); + } + + @Override + public boolean equals(Object obj) { + throw new UnsupportedOperationException( + "equals() is not supported as its behavior isn't specified"); + } + }; + } + /** * Visits the individual classes and their directly present annotations that make up the given * type. diff --git a/src/test/java/com/code_intelligence/jazzer/mutation/mutator/StressTest.java b/src/test/java/com/code_intelligence/jazzer/mutation/mutator/StressTest.java index cb01c1e27..5c59ed8ad 100644 --- a/src/test/java/com/code_intelligence/jazzer/mutation/mutator/StressTest.java +++ b/src/test/java/com/code_intelligence/jazzer/mutation/mutator/StressTest.java @@ -124,6 +124,82 @@ private record RepeatedRecord(SimpleRecord first, SimpleRecord second) {} private record LinkedListNode(SimpleRecord value, LinkedListNode next) {} + private sealed interface Sealed { + sealed interface A extends Sealed { + record A1(@NotNull boolean b) implements A {} + } + + abstract sealed class B implements Sealed { + static final class B1 extends B { + private final boolean b; + + B1(boolean b) { + this.b = b; + } + + @NotNull + public boolean b() { + return b; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + B1 b1 = (B1) o; + return b == b1.b; + } + + @Override + public int hashCode() { + return Objects.hash(b); + } + + @Override + public String toString() { + return "B1{" + "b=" + b + '}'; + } + } + + static final class B2 extends B { + private final boolean b; + + B2(boolean b) { + this.b = b; + } + + @NotNull + public boolean b() { + return b; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + B2 b1 = (B2) o; + return b == b1.b; + } + + @Override + public int hashCode() { + return Objects.hash(b); + } + + @Override + public String toString() { + return "B2{" + "b=" + b + '}'; + } + } + } + + sealed interface C extends Sealed { + record C1(@NotNull boolean b) implements C {} + + record C2(@NotNull int i) implements C {} + } + } + public static class SomeSetterBasedBean { protected long quz; @@ -809,7 +885,31 @@ void singleParam(int parameter) {} "Nullable<[[Nullable] -> SuperBuilderTargetBuilder] -> SuperBuilderTarget>", false, distinctElementsRatio(0.4), - distinctElementsRatio(0.4))); + distinctElementsRatio(0.4)), + arguments( + new TypeHolder() {}.annotatedType(), + "Nullable<([Boolean] -> A1 | ([Boolean] -> B1 | [Boolean] -> B2) | ([Boolean] -> C1 |" + + " [Integer] -> C2))>", + true, + contains( + null, + new Sealed.A.A1(false), + new Sealed.B.B1(false), + new Sealed.B.B2(false), + new Sealed.C.C1(false), + new Sealed.C.C2(0)), + contains( + null, + new Sealed.A.A1(false), + new Sealed.B.B1(false), + new Sealed.B.B2(false), + new Sealed.C.C1(false), + new Sealed.C.C2(0), + new Sealed.A.A1(true), + new Sealed.B.B1(true), + new Sealed.B.B2(true), + new Sealed.C.C1(true), + new Sealed.C.C2(1)))); } public static Stream protoStressTestCases() {