Skip to content

Commit c9c1053

Browse files
committed
feat: add @ElementOf annotation
This allows picking from a fixed set of values during mutation.
1 parent d10428a commit c9c1053

File tree

6 files changed

+352
-17
lines changed

6 files changed

+352
-17
lines changed

docs/mutation-framework.md

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -87,22 +87,23 @@ string. This is done using annotations directly on the parameters.
8787
All annotations reside in the `com.code_intelligence.jazzer.mutation.annotation`
8888
package.
8989

90-
| Annotation | Applies To | Notes |
91-
|-------------------|----------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------|
92-
| `@Ascii` | `java.lang.String` | `String` should only contain ASCII characters |
93-
| `@InRange` | `byte`, `Byte`, `char`, `Character`, `short`, `Short`, `int`, `Integer`, `long`, `Long` | Specifies `min` and `max` values of generated integrals |
94-
| `@FloatInRange` | `float`, `Float` | Specifies `min` and `max` values of generated floats |
95-
| `@DoubleInRange` | `double`, `Double` | Specifies `min` and `max` values of generated doubles |
96-
| `@Positive` | `byte`, `Byte`, `short`, `Short`, `int`, `Integer`, `long`, `Long`, `float`, `Float`, `double`, `Double` | Specifies that only positive values are generated |
97-
| `@Negative` | `byte`, `Byte`, `short`, `Short`, `int`, `Integer`, `long`, `Long`, `float`, `Float`, `double`, `Double` | Specifies that only negative values are generated |
98-
| `@NonPositive` | `byte`, `Byte`, `short`, `Short`, `int`, `Integer`, `long`, `Long`, `float`, `Float`, `double`, `Double` | Specifies that only non-positive values are generated |
99-
| `@NonNegative` | `byte`, `Byte`, `short`, `Short`, `int`, `Integer`, `long`, `Long`, `float`, `Float`, `double`, `Double` | Specifies that only non-negative values are generated |
100-
| `@Finite` | `float`, `Float`, `double`, `Double` | Specifies that only finite values are generated |
101-
| `@NotNull` | | Specifies that a reference type should not be `null` |
102-
| `@WithLength` | `byte[]` | Specifies the length of the generated byte array |
103-
| `@WithUtf8Length` | `java.lang.String` | Specifies the length of the generated string in UTF-8 bytes, see annotation Javadoc for further information |
104-
| `@WithSize` | `java.util.List`, `java.util.Map` | Specifies the size of the generated collection |
105-
| `@UrlSegment` | `java.lang.String` | `String` should only contain valid URL segment characters |
90+
| Annotation | Applies To | Notes |
91+
|-------------------|-----------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------|
92+
| `@Ascii` | `java.lang.String` | `String` should only contain ASCII characters |
93+
| `@InRange` | `byte`, `Byte`, `char`, `Character`, `short`, `Short`, `int`, `Integer`, `long`, `Long` | Specifies `min` and `max` values of generated integrals |
94+
| `@FloatInRange` | `float`, `Float` | Specifies `min` and `max` values of generated floats |
95+
| `@DoubleInRange` | `double`, `Double` | Specifies `min` and `max` values of generated doubles |
96+
| `@Positive` | `byte`, `Byte`, `short`, `Short`, `int`, `Integer`, `long`, `Long`, `float`, `Float`, `double`, `Double` | Specifies that only positive values are generated |
97+
| `@Negative` | `byte`, `Byte`, `short`, `Short`, `int`, `Integer`, `long`, `Long`, `float`, `Float`, `double`, `Double` | Specifies that only negative values are generated |
98+
| `@NonPositive` | `byte`, `Byte`, `short`, `Short`, `int`, `Integer`, `long`, `Long`, `float`, `Float`, `double`, `Double` | Specifies that only non-positive values are generated |
99+
| `@NonNegative` | `byte`, `Byte`, `short`, `Short`, `int`, `Integer`, `long`, `Long`, `float`, `Float`, `double`, `Double` | Specifies that only non-negative values are generated |
100+
| `@Finite` | `float`, `Float`, `double`, `Double` | Specifies that only finite values are generated |
101+
| `@ElementOf` | `byte`, `Byte`, `short`, `Short`, `int`, `Integer`, `long`, `Long`, `char`, `Character`, `float`, `Float`, `double`, `Double`, `String` | Restricts the value to a fixed set; populate only the array matching the parameter type with at least two entries |
102+
| `@NotNull` | | Specifies that a reference type should not be `null` |
103+
| `@WithLength` | `byte[]` | Specifies the length of the generated byte array |
104+
| `@WithUtf8Length` | `java.lang.String` | Specifies the length of the generated string in UTF-8 bytes, see annotation Javadoc for further information |
105+
| `@WithSize` | `java.util.List`, `java.util.Map` | Specifies the size of the generated collection |
106+
| `@UrlSegment` | `java.lang.String` | `String` should only contain valid URL segment characters |
106107

107108
The example below shows how Fuzz Test parameters can be annotated to provide
108109
additional information to the mutation framework.
@@ -116,6 +117,15 @@ public void testSimpleTypeRecord(@NotNull @WithSize(min = 3, max = 100) List<Sim
116117
}
117118
```
118119

120+
Use `@ElementOf` when a parameter should only take one of a few constant values.
121+
122+
```java title="Example" showLineNumbers
123+
@FuzzTest
124+
void fuzz(@ElementOf(strings = {"one", "two", "three"}) String value) {
125+
// value is always "one", "two", "three" or null
126+
}
127+
```
128+
119129
### Annotation constraints
120130

121131
Often, annotations should be applied to a type and all it's nested component
@@ -347,4 +357,3 @@ class ParserTests {
347357
}
348358
}
349359
```
350-
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
* Copyright 2024 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.annotation;
18+
19+
import static java.lang.annotation.ElementType.TYPE_USE;
20+
import static java.lang.annotation.RetentionPolicy.RUNTIME;
21+
22+
import com.code_intelligence.jazzer.mutation.utils.AppliesTo;
23+
import java.lang.annotation.Retention;
24+
import java.lang.annotation.Target;
25+
26+
/**
27+
* Restricts generated values to a fixed set.
28+
*
29+
* <p>Populate exactly one of the type-specific arrays with at least two values. Only the array that
30+
* matches the annotated parameter type is used; all others are ignored. For {@link String} and
31+
* wrapper types, the mutator may still emit {@code null}; add {@link NotNull} (the only supported
32+
* annotation in combination with {@code @ElementOf}) to prevent that.
33+
*
34+
* <p>Example usage:
35+
*
36+
* <pre>{@code
37+
* @FuzzTest
38+
* void fuzz(
39+
* @ElementOf(integers = {1, 2, 3}) int option,
40+
* @ElementOf(strings = {"one", "two"}) String label) {
41+
* // option is always 1, 2, or 3; label is always "one" or "two".
42+
* }
43+
* }</pre>
44+
*/
45+
@Target(TYPE_USE)
46+
@Retention(RUNTIME)
47+
@AppliesTo({
48+
byte.class,
49+
Byte.class,
50+
short.class,
51+
Short.class,
52+
int.class,
53+
Integer.class,
54+
long.class,
55+
Long.class,
56+
char.class,
57+
Character.class,
58+
float.class,
59+
Float.class,
60+
double.class,
61+
Double.class,
62+
String.class
63+
})
64+
public @interface ElementOf {
65+
byte[] bytes() default {};
66+
67+
short[] shorts() default {};
68+
69+
int[] integers() default {};
70+
71+
long[] longs() default {};
72+
73+
char[] chars() default {};
74+
75+
float[] floats() default {};
76+
77+
double[] doubles() default {};
78+
79+
String[] strings() default {};
80+
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
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.lang;
18+
19+
import static com.code_intelligence.jazzer.mutation.combinator.MutatorCombinators.mutateIndices;
20+
import static com.code_intelligence.jazzer.mutation.combinator.MutatorCombinators.mutateThenMap;
21+
import static com.code_intelligence.jazzer.mutation.support.Preconditions.require;
22+
import static java.lang.String.format;
23+
import static java.util.Arrays.stream;
24+
import static java.util.stream.Collectors.toList;
25+
26+
import com.code_intelligence.jazzer.mutation.annotation.ElementOf;
27+
import com.code_intelligence.jazzer.mutation.api.ExtendedMutatorFactory;
28+
import com.code_intelligence.jazzer.mutation.api.MutatorFactory;
29+
import com.code_intelligence.jazzer.mutation.api.SerializingMutator;
30+
import java.lang.reflect.AnnotatedType;
31+
import java.util.ArrayList;
32+
import java.util.Arrays;
33+
import java.util.List;
34+
import java.util.Map;
35+
import java.util.Optional;
36+
import java.util.stream.Collectors;
37+
import java.util.stream.IntStream;
38+
39+
final class ElementOfMutatorFactory implements MutatorFactory {
40+
@Override
41+
public Optional<SerializingMutator<?>> tryCreate(
42+
AnnotatedType type, ExtendedMutatorFactory factory) {
43+
ElementOf elementOf = type.getAnnotation(ElementOf.class);
44+
if (elementOf == null) {
45+
return Optional.empty();
46+
}
47+
if (!(type.getType() instanceof Class<?>)) {
48+
return Optional.empty();
49+
}
50+
Class<?> rawType = (Class<?>) type.getType();
51+
52+
if (rawType == byte.class || rawType == Byte.class) {
53+
return Optional.of(
54+
elementOfMutator(boxBytes(elementOf.bytes()), "bytes", rawType.getSimpleName()));
55+
} else if (rawType == short.class || rawType == Short.class) {
56+
return Optional.of(
57+
elementOfMutator(boxShorts(elementOf.shorts()), "shorts", rawType.getSimpleName()));
58+
} else if (rawType == int.class || rawType == Integer.class) {
59+
return Optional.of(
60+
elementOfMutator(boxInts(elementOf.integers()), "integers", rawType.getSimpleName()));
61+
} else if (rawType == long.class || rawType == Long.class) {
62+
return Optional.of(
63+
elementOfMutator(boxLongs(elementOf.longs()), "longs", rawType.getSimpleName()));
64+
} else if (rawType == char.class || rawType == Character.class) {
65+
return Optional.of(
66+
elementOfMutator(boxChars(elementOf.chars()), "chars", rawType.getSimpleName()));
67+
} else if (rawType == float.class || rawType == Float.class) {
68+
return Optional.of(
69+
elementOfMutator(boxFloats(elementOf.floats()), "floats", rawType.getSimpleName()));
70+
} else if (rawType == double.class || rawType == Double.class) {
71+
return Optional.of(
72+
elementOfMutator(boxDoubles(elementOf.doubles()), "doubles", rawType.getSimpleName()));
73+
} else if (rawType == String.class) {
74+
return Optional.of(
75+
elementOfMutator(Arrays.asList(elementOf.strings()), "strings", rawType.getSimpleName()));
76+
}
77+
return Optional.empty();
78+
}
79+
80+
private static <T> SerializingMutator<T> elementOfMutator(
81+
List<T> values, String fieldName, String targetTypeName) {
82+
require(
83+
values.size() > 1,
84+
format(
85+
"@ElementOf %s array must contain at least two values for %s",
86+
fieldName, targetTypeName));
87+
Map<T, Integer> inverse =
88+
IntStream.range(0, values.size()).boxed().collect(Collectors.toMap(values::get, i -> i));
89+
return mutateThenMap(
90+
mutateIndices(values.size()),
91+
values::get,
92+
v -> inverse.getOrDefault(v, 0),
93+
isInCycle -> format("@ElementOf<%s>[%d]", targetTypeName, values.size()));
94+
}
95+
96+
private static List<Byte> boxBytes(byte[] values) {
97+
List<Byte> result = new ArrayList<>(values.length);
98+
for (byte value : values) {
99+
result.add(value);
100+
}
101+
return result;
102+
}
103+
104+
private static List<Short> boxShorts(short[] values) {
105+
List<Short> result = new ArrayList<>(values.length);
106+
for (short value : values) {
107+
result.add(value);
108+
}
109+
return result;
110+
}
111+
112+
private static List<Integer> boxInts(int[] values) {
113+
return stream(values).boxed().collect(toList());
114+
}
115+
116+
private static List<Long> boxLongs(long[] values) {
117+
return Arrays.stream(values).boxed().collect(toList());
118+
}
119+
120+
private static List<Character> boxChars(char[] values) {
121+
List<Character> result = new ArrayList<>(values.length);
122+
for (char value : values) {
123+
result.add(value);
124+
}
125+
return result;
126+
}
127+
128+
private static List<Float> boxFloats(float[] values) {
129+
List<Float> result = new ArrayList<>(values.length);
130+
for (float value : values) {
131+
result.add(value);
132+
}
133+
return result;
134+
}
135+
136+
private static List<Double> boxDoubles(double[] values) {
137+
return Arrays.stream(values).boxed().collect(toList());
138+
}
139+
}

src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/LangMutators.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ public static Stream<MutatorFactory> newFactories(ValuePoolRegistry valuePoolReg
3131
return Stream.of(
3232
// DON'T EVER SORT THESE! The order is important for the mutator engine to work correctly.
3333
new NullableMutatorFactory(),
34+
new ElementOfMutatorFactory(),
3435
new ValuePoolMutatorFactory(valuePoolRegistry),
3536
new BooleanMutatorFactory(),
3637
new FloatingPointMutatorFactory(),

src/test/java/com/code_intelligence/jazzer/mutation/mutator/StressTest.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import com.code_intelligence.jazzer.api.FuzzedDataProvider;
4040
import com.code_intelligence.jazzer.driver.FuzzedDataProviderImpl;
4141
import com.code_intelligence.jazzer.mutation.annotation.DoubleInRange;
42+
import com.code_intelligence.jazzer.mutation.annotation.ElementOf;
4243
import com.code_intelligence.jazzer.mutation.annotation.FloatInRange;
4344
import com.code_intelligence.jazzer.mutation.annotation.InRange;
4445
import com.code_intelligence.jazzer.mutation.annotation.NotNull;
@@ -868,6 +869,13 @@ void singleParam(int parameter) {}
868869
true,
869870
exactly(null, TestEnumThree.A, TestEnumThree.B, TestEnumThree.C),
870871
exactly(null, TestEnumThree.A, TestEnumThree.B, TestEnumThree.C)),
872+
arguments(
873+
new TypeHolder<
874+
@ElementOf(strings = {"one", "two", "three"}) String>() {}.annotatedType(),
875+
"Nullable<@ElementOf(strings, size=3) -> String>",
876+
true,
877+
exactly(null, "one", "two", "three"),
878+
exactly(null, "one", "two", "three")),
871879
arguments(
872880
new TypeHolder<@NotNull @FloatInRange(min = 0f) Float>() {}.annotatedType(),
873881
"Float",

0 commit comments

Comments
 (0)