Skip to content

Commit 2880e2f

Browse files
fmeumoetr
authored andcommitted
mutation: Add support for sealed classes
1 parent ed5d042 commit 2880e2f

File tree

7 files changed

+371
-25
lines changed

7 files changed

+371
-25
lines changed

src/main/java/com/code_intelligence/jazzer/mutation/combinator/MutatorCombinators.java

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import com.code_intelligence.jazzer.mutation.api.Serializer;
3030
import com.code_intelligence.jazzer.mutation.api.SerializingInPlaceMutator;
3131
import com.code_intelligence.jazzer.mutation.api.SerializingMutator;
32+
import com.code_intelligence.jazzer.mutation.support.Preconditions;
3233
import com.google.errorprone.annotations.ImmutableTypeParameter;
3334
import java.io.DataInputStream;
3435
import java.io.DataOutputStream;
@@ -428,6 +429,102 @@ public String toDebugString(Predicate<Debuggable> isInCycle) {
428429
};
429430
}
430431

432+
/**
433+
* Mutates a sum type (e.g. a sealed interface), preferring to mutate the current state but
434+
* occasionally switching to a different state.
435+
*
436+
* @param getState a function that returns the current state of the sum type as an index into
437+
* {@code perStateMutators}, or -1 if the state is indeterminate.
438+
* @param perStateMutators the mutators for each state
439+
* @return a mutator that mutates the sum type
440+
*/
441+
@SafeVarargs
442+
public static <T> SerializingMutator<?> mutateSum(
443+
ToIntFunction<T> getState, SerializingMutator<T>... perStateMutators) {
444+
Preconditions.require(perStateMutators.length > 0, "At least one mutator must be provided");
445+
if (perStateMutators.length == 1) {
446+
return perStateMutators[0];
447+
}
448+
boolean hasFixedSize = stream(perStateMutators).allMatch(SerializingMutator::hasFixedSize);
449+
final SerializingMutator<T>[] mutators =
450+
Arrays.copyOf(perStateMutators, perStateMutators.length);
451+
return new SerializingMutator<T>() {
452+
@Override
453+
public T init(PseudoRandom prng) {
454+
return mutators[prng.indexIn(mutators)].init(prng);
455+
}
456+
457+
@Override
458+
public T mutate(T value, PseudoRandom prng) {
459+
int currentState = getState.applyAsInt(value);
460+
if (currentState == -1) {
461+
// The value is in an indeterminate state, initialize it.
462+
return init(prng);
463+
}
464+
if (prng.trueInOneOutOf(100)) {
465+
// Initialize to a different state.
466+
return mutators[prng.otherIndexIn(mutators, currentState)].init(prng);
467+
}
468+
// Mutate within the current state.
469+
return mutators[currentState].mutate(value, prng);
470+
}
471+
472+
@Override
473+
public T crossOver(T value, T otherValue, PseudoRandom prng) {
474+
// Try to cross over in current state and leave state changes to the mutate step.
475+
int currentState = getState.applyAsInt(value);
476+
int otherState = getState.applyAsInt(otherValue);
477+
if (currentState == -1) {
478+
// If reference is not initialized to a concrete state yet, try to do so in
479+
// the state of other reference, as that's at least some progress.
480+
if (otherState == -1) {
481+
// If both states are indeterminate, cross over can not be performed.
482+
return value;
483+
}
484+
return mutators[otherState].init(prng);
485+
}
486+
if (currentState == otherState) {
487+
return mutators[currentState].crossOver(value, otherValue, prng);
488+
}
489+
return value;
490+
}
491+
492+
@Override
493+
public T detach(T value) {
494+
int currentState = getState.applyAsInt(value);
495+
if (currentState == -1) {
496+
return value;
497+
}
498+
return mutators[currentState].detach(value);
499+
}
500+
501+
@Override
502+
public T read(DataInputStream in) throws IOException {
503+
int currentState = Math.floorMod(in.readInt(), mutators.length);
504+
return mutators[currentState].read(in);
505+
}
506+
507+
@Override
508+
public void write(T value, DataOutputStream out) throws IOException {
509+
int currentState = getState.applyAsInt(value);
510+
out.writeInt(currentState);
511+
mutators[currentState].write(value, out);
512+
}
513+
514+
@Override
515+
public boolean hasFixedSize() {
516+
return hasFixedSize;
517+
}
518+
519+
@Override
520+
public String toDebugString(Predicate<Debuggable> isInCycle) {
521+
return stream(mutators)
522+
.map(mutator -> mutator.toDebugString(isInCycle))
523+
.collect(joining(" | ", "(", ")"));
524+
}
525+
};
526+
}
527+
431528
/**
432529
* Use {@link #markAsRequiringRecursionBreaking(SerializingMutator)} instead for {@link
433530
* com.code_intelligence.jazzer.mutation.api.ValueMutator}.

src/main/java/com/code_intelligence/jazzer/mutation/mutator/Mutators.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,9 @@ public static ExtendedMutatorFactory newFactory() {
4040
CollectionMutators.newFactories(),
4141
ProtoMutators.newFactories(),
4242
LibFuzzerMutators.newFactories(),
43-
AggregateMutators.newFactories(),
44-
TimeMutators.newFactories());
43+
TimeMutators.newFactories(),
44+
// Keep generic aggregate mutators last in case a concrete type is also an aggregate type.
45+
AggregateMutators.newFactories());
4546
}
4647

4748
// Mutators for which the NullableMutatorFactory

src/main/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/AggregateMutators.java

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -25,26 +25,43 @@ private AggregateMutators() {}
2525

2626
public static Stream<MutatorFactory> newFactories() {
2727
// Register the record mutator first as it is more specific.
28-
return Stream.concat(
29-
newRecordMutatorFactoryIfSupported(),
30-
Stream.of(
31-
new SetterBasedBeanMutatorFactory(),
32-
new ConstructorBasedBeanMutatorFactory(),
33-
new CachedConstructorMutatorFactory()));
28+
return Stream.of(
29+
newRecordMutatorFactoryIfSupported(),
30+
newSealedClassMutatorFactoryIfSupported(),
31+
Stream.of(
32+
new SetterBasedBeanMutatorFactory(),
33+
new ConstructorBasedBeanMutatorFactory(),
34+
new CachedConstructorMutatorFactory()))
35+
.flatMap(s -> s);
3436
}
3537

3638
private static Stream<MutatorFactory> newRecordMutatorFactoryIfSupported() {
37-
if (!supportsRecords()) {
39+
try {
40+
Class.forName("java.lang.Record");
41+
return Stream.of(instantiateMutatorFactory("RecordMutatorFactory"));
42+
} catch (ClassNotFoundException ignored) {
43+
return Stream.empty();
44+
}
45+
}
46+
47+
private static Stream<MutatorFactory> newSealedClassMutatorFactoryIfSupported() {
48+
try {
49+
Class.class.getMethod("getPermittedSubclasses");
50+
return Stream.of(instantiateMutatorFactory("SealedClassMutatorFactory"));
51+
} catch (NoSuchMethodException e) {
3852
return Stream.empty();
3953
}
54+
}
55+
56+
private static MutatorFactory instantiateMutatorFactory(String simpleClassName) {
4057
try {
41-
// Instantiate RecordMutatorFactory via reflection as making it a compile time dependency
42-
// breaks the r8 step in the Android build.
43-
Class<? extends MutatorFactory> recordMutatorFactory;
44-
recordMutatorFactory =
45-
Class.forName(AggregateMutators.class.getPackage().getName() + ".RecordMutatorFactory")
58+
// Instantiate factory via reflection as making it a compile time dependency breaks the r8
59+
// step in the Android build.
60+
Class<? extends MutatorFactory> factory;
61+
factory =
62+
Class.forName(AggregateMutators.class.getPackage().getName() + "." + simpleClassName)
4663
.asSubclass(MutatorFactory.class);
47-
return Stream.of(recordMutatorFactory.getDeclaredConstructor().newInstance());
64+
return factory.getDeclaredConstructor().newInstance();
4865
} catch (ClassNotFoundException
4966
| NoSuchMethodException
5067
| InstantiationException
@@ -53,13 +70,4 @@ private static Stream<MutatorFactory> newRecordMutatorFactoryIfSupported() {
5370
throw new IllegalStateException(e);
5471
}
5572
}
56-
57-
private static boolean supportsRecords() {
58-
try {
59-
Class.forName("java.lang.Record");
60-
return true;
61-
} catch (ClassNotFoundException ignored) {
62-
return false;
63-
}
64-
}
6573
}

src/main/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/BUILD.bazel

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ java_library(
66
"AggregatesHelper.java",
77
"BeanSupport.java",
88
"RecordMutatorFactory.java",
9+
"SealedClassMutatorFactory.java",
910
],
1011
),
1112
visibility = [
@@ -16,6 +17,7 @@ java_library(
1617
"@platforms//os:android": [],
1718
"//conditions:default": [
1819
":record_mutator_factory",
20+
":sealed_class_mutator_factory",
1921
],
2022
}),
2123
deps = [
@@ -40,6 +42,20 @@ java_library(
4042
],
4143
)
4244

45+
java_library(
46+
name = "sealed_class_mutator_factory",
47+
srcs = ["SealedClassMutatorFactory.java"],
48+
javacopts = [
49+
"--release",
50+
"17",
51+
],
52+
deps = [
53+
"//src/main/java/com/code_intelligence/jazzer/mutation/api",
54+
"//src/main/java/com/code_intelligence/jazzer/mutation/combinator",
55+
"//src/main/java/com/code_intelligence/jazzer/mutation/support",
56+
],
57+
)
58+
4359
java_library(
4460
name = "aggregates_helper",
4561
srcs = [
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright 2025 Code Intelligence GmbH
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.code_intelligence.jazzer.mutation.mutator.aggregate;
18+
19+
import static com.code_intelligence.jazzer.mutation.support.StreamSupport.toArrayOrEmpty;
20+
import static java.util.Arrays.stream;
21+
22+
import com.code_intelligence.jazzer.mutation.api.ExtendedMutatorFactory;
23+
import com.code_intelligence.jazzer.mutation.api.MutatorFactory;
24+
import com.code_intelligence.jazzer.mutation.api.SerializingMutator;
25+
import com.code_intelligence.jazzer.mutation.combinator.MutatorCombinators;
26+
import com.code_intelligence.jazzer.mutation.support.TypeSupport;
27+
import java.lang.reflect.AnnotatedType;
28+
import java.util.Optional;
29+
import java.util.function.ToIntFunction;
30+
31+
final class SealedClassMutatorFactory<T> implements MutatorFactory {
32+
@Override
33+
public Optional<SerializingMutator<?>> tryCreate(
34+
AnnotatedType type, ExtendedMutatorFactory factory) {
35+
if (!(type.getType() instanceof Class<?>)) {
36+
return Optional.empty();
37+
}
38+
Class<T>[] permittedSubclasses =
39+
(Class<T>[]) ((Class<T>) type.getType()).getPermittedSubclasses();
40+
if (permittedSubclasses == null) {
41+
return Optional.empty();
42+
}
43+
44+
ToIntFunction<T> getState =
45+
(value) -> {
46+
// We can't use value.getClass() as it might be a subclass of the permitted (direct)
47+
// subclasses.
48+
for (int i = 0; i < permittedSubclasses.length; i++) {
49+
if (permittedSubclasses[i].isInstance(value)) {
50+
return i;
51+
}
52+
}
53+
return -1;
54+
};
55+
return toArrayOrEmpty(
56+
stream(permittedSubclasses)
57+
.map(TypeSupport::asAnnotatedType)
58+
.map(TypeSupport::notNull)
59+
.map(factory::tryCreate),
60+
SerializingMutator<?>[]::new)
61+
.map(
62+
mutators -> MutatorCombinators.mutateSum(getState, (SerializingMutator<T>[]) mutators));
63+
}
64+
}

src/main/java/com/code_intelligence/jazzer/mutation/support/TypeSupport.java

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import com.code_intelligence.jazzer.mutation.annotation.WithLength;
3030
import com.code_intelligence.jazzer.mutation.utils.PropertyConstraint;
3131
import java.lang.annotation.Annotation;
32+
import java.lang.annotation.Inherited;
3233
import java.lang.reflect.AnnotatedArrayType;
3334
import java.lang.reflect.AnnotatedElement;
3435
import java.lang.reflect.AnnotatedParameterizedType;
@@ -94,6 +95,65 @@ public static <T> Optional<Class<? extends T>> asSubclassOrEmpty(
9495
return Optional.of(actualClazz.asSubclass(superclass));
9596
}
9697

98+
/**
99+
* Synthesizes an {@link AnnotatedType} for the given {@link Class}.
100+
*
101+
* <p>Usage of this method should be avoided in favor of obtaining annotated types in a natural
102+
* way if possible (e.g. prefer {@link Class#getAnnotatedSuperclass()} to {@link
103+
* Class#getSuperclass()}.
104+
*/
105+
public static AnnotatedType asAnnotatedType(Class<?> clazz) {
106+
requireNonNull(clazz);
107+
return new AnnotatedType() {
108+
@Override
109+
public Type getType() {
110+
return clazz;
111+
}
112+
113+
@Override
114+
public <T extends Annotation> T getAnnotation(Class<T> annotationClass) {
115+
return annotatedElementGetAnnotation(this, annotationClass);
116+
}
117+
118+
@Override
119+
public Annotation[] getAnnotations() {
120+
// No directly present annotations, look for inheritable present annotations on the
121+
// superclass.
122+
if (clazz.getSuperclass() == null) {
123+
return new Annotation[0];
124+
}
125+
return stream(clazz.getSuperclass().getAnnotations())
126+
.filter(
127+
annotation ->
128+
annotation.annotationType().getDeclaredAnnotation(Inherited.class) != null)
129+
.toArray(Annotation[]::new);
130+
}
131+
132+
@Override
133+
public Annotation[] getDeclaredAnnotations() {
134+
// No directly present annotations.
135+
return new Annotation[0];
136+
}
137+
138+
@Override
139+
public String toString() {
140+
return annotatedTypeToString(this);
141+
}
142+
143+
@Override
144+
public int hashCode() {
145+
throw new UnsupportedOperationException(
146+
"hashCode() is not supported as its behavior isn't specified");
147+
}
148+
149+
@Override
150+
public boolean equals(Object obj) {
151+
throw new UnsupportedOperationException(
152+
"equals() is not supported as its behavior isn't specified");
153+
}
154+
};
155+
}
156+
97157
/**
98158
* Visits the individual classes and their directly present annotations that make up the given
99159
* type.

0 commit comments

Comments
 (0)