Skip to content

Commit 7796cdd

Browse files
committed
fix reproducibility in case type annotations are used
1 parent 359518b commit 7796cdd

File tree

2 files changed

+107
-7
lines changed

2 files changed

+107
-7
lines changed

core/src/main/java/org/jboss/jandex/Indexer.java

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -396,14 +396,21 @@ void returnConstantAnnoAttributes(byte[] attributes) {
396396
private int[] constantPoolOffsets;
397397
private byte[] constantPoolAnnoAttrributes;
398398

399+
// note that for reproducibility, we have to establish a predictable iteration order for all `Map`s
400+
// below that we iterate upon (fortunately, that's not too many)
401+
// this is either by using keys with predictable `equals`/`hashCode` (such as `DotName`),
402+
// or by storing the keys on the side in a list and iterate on that (needed for `IdentityHashMap`s)
403+
399404
private ClassInfo currentClass;
400405
private HashMap<DotName, List<AnnotationInstance>> classAnnotations;
401406
private ArrayList<AnnotationInstance> elementAnnotations;
402407
private IdentityHashMap<AnnotationTarget, Object> signaturePresent;
403408
private List<Object> signatures;
404409
private int classSignatureIndex = -1;
405410
private Map<DotName, InnerClassInfo> innerClasses;
411+
// iteration: `typeAnnotationsKeys` is used for predictable iteration order, we never iterate on `typeAnnotations`
406412
private IdentityHashMap<AnnotationTarget, List<TypeAnnotationState>> typeAnnotations;
413+
private List<AnnotationTarget> typeAnnotationsKeys;
407414
private List<MethodInfo> methods;
408415
private List<FieldInfo> fields;
409416
private List<RecordComponentInfo> recordComponents;
@@ -416,9 +423,12 @@ void returnConstantAnnoAttributes(byte[] attributes) {
416423
private Map<DotName, List<ClassInfo>> subclasses;
417424
private Map<DotName, List<ClassInfo>> subinterfaces;
418425
private Map<DotName, List<ClassInfo>> implementors;
426+
// iteration: `DotName` has predictable `equals`/`hashCode`, which implies predictable iteration order
419427
private Map<DotName, ClassInfo> classes;
420428
private Map<DotName, ModuleInfo> modules;
421-
private Map<DotName, Set<ClassInfo>> users; // must be a linked set for reproducibility
429+
// iteration: `DotName` has predictable `equals`/`hashCode`, which implies predictable iteration order
430+
// iteration: the `Set`s in map values must be linked sets for predictable iteration order
431+
private Map<DotName, Set<ClassInfo>> users;
422432
private NameTable names;
423433
private GenericSignatureParser signatureParser;
424434
private final TmpObjects tmpObjects = new TmpObjects();
@@ -459,6 +469,7 @@ private void initClassFields() {
459469
signaturePresent = new IdentityHashMap<AnnotationTarget, Object>();
460470
signatures = new ArrayList<Object>();
461471
typeAnnotations = new IdentityHashMap<AnnotationTarget, List<TypeAnnotationState>>();
472+
typeAnnotationsKeys = new ArrayList<>();
462473

463474
// in bytecode, record components are stored as class attributes,
464475
// and if the attribute is missing, processRecordComponents isn't called at all
@@ -903,6 +914,7 @@ private void processTypeAnnotations(DataInputStream data, AnnotationTarget targe
903914
typeAnnotations.get(target).addAll(annotations);
904915
} else {
905916
typeAnnotations.put(target, annotations);
917+
typeAnnotationsKeys.add(target);
906918
}
907919
}
908920

@@ -1104,9 +1116,8 @@ private static boolean isEnumConstructor(MethodInfo method) {
11041116
}
11051117

11061118
private void resolveTypeAnnotations() {
1107-
for (Map.Entry<AnnotationTarget, List<TypeAnnotationState>> entry : typeAnnotations.entrySet()) {
1108-
AnnotationTarget key = entry.getKey();
1109-
List<TypeAnnotationState> annotations = entry.getValue();
1119+
for (AnnotationTarget key : typeAnnotationsKeys) {
1120+
List<TypeAnnotationState> annotations = typeAnnotations.get(key);
11101121

11111122
for (TypeAnnotationState annotation : annotations) {
11121123
resolveTypeAnnotation(key, annotation);
@@ -1202,9 +1213,8 @@ private void recordUsedClass(DotName usedClass) {
12021213
}
12031214

12041215
private void updateTypeTargets() {
1205-
for (Map.Entry<AnnotationTarget, List<TypeAnnotationState>> entry : typeAnnotations.entrySet()) {
1206-
AnnotationTarget key = entry.getKey();
1207-
List<TypeAnnotationState> annotations = entry.getValue();
1216+
for (AnnotationTarget key : typeAnnotationsKeys) {
1217+
List<TypeAnnotationState> annotations = typeAnnotations.get(key);
12081218

12091219
for (TypeAnnotationState annotation : annotations) {
12101220
updateTypeTarget(key, annotation);
@@ -2686,6 +2696,7 @@ public ClassSummary indexWithSummary(InputStream stream) throws IOException {
26862696
classSignatureIndex = -1;
26872697
innerClasses = null;
26882698
typeAnnotations = null;
2699+
typeAnnotationsKeys = null;
26892700
methods = null;
26902701
fields = null;
26912702
recordComponents = null;
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package org.jboss.jandex.test;
2+
3+
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
4+
5+
import java.io.ByteArrayOutputStream;
6+
import java.io.File;
7+
import java.io.IOException;
8+
import java.io.InputStream;
9+
import java.net.URI;
10+
import java.net.URISyntaxException;
11+
import java.nio.file.Files;
12+
import java.nio.file.Path;
13+
import java.nio.file.Paths;
14+
import java.util.Arrays;
15+
import java.util.Comparator;
16+
import java.util.List;
17+
import java.util.jar.JarEntry;
18+
import java.util.jar.JarFile;
19+
import java.util.stream.Collectors;
20+
import java.util.stream.Stream;
21+
import java.util.zip.ZipEntry;
22+
23+
import org.jboss.jandex.Index;
24+
import org.jboss.jandex.IndexWriter;
25+
import org.jboss.jandex.Indexer;
26+
import org.junit.jupiter.api.BeforeAll;
27+
import org.junit.jupiter.api.RepeatedTest;
28+
29+
// asserts that the indexing process is reproducible (that is, always produces the same output)
30+
// on the same JVM version with the same sequence of input files, using the test classes from
31+
// packages `org.jboss.jandex.test` (the `core` module) and `test` (the `test-data` module)
32+
//
33+
// there's also a reproducibility integration test in the `maven-plugin` module, which uses
34+
// a different set of inputs
35+
public class ReproducibilityTest {
36+
static byte[] firstIndex;
37+
38+
static byte[] index() throws IOException, URISyntaxException {
39+
Indexer indexer = new Indexer();
40+
ClassLoader cl = ReproducibilityTest.class.getClassLoader();
41+
for (String pkg : Arrays.asList("org/jboss/jandex/test/", "test/")) {
42+
URI uri = cl.getResources(pkg).nextElement().toURI();
43+
String scheme = uri.getScheme();
44+
if ("file".equals(scheme)) {
45+
try (Stream<Path> stream = Files.walk(Paths.get(uri))) {
46+
List<Path> classes = stream
47+
.filter(it -> Files.isRegularFile(it) && it.getFileName().toString().endsWith(".class"))
48+
.sorted()
49+
.collect(Collectors.toList());
50+
for (Path path : classes) {
51+
try (InputStream in = Files.newInputStream(path)) {
52+
indexer.index(in);
53+
}
54+
}
55+
}
56+
}
57+
if ("jar".equals(scheme)) {
58+
// opaque URI of the form `jar:<file URI>!/pkg/`
59+
String part = uri.getSchemeSpecificPart();
60+
uri = new URI(part.substring(0, part.indexOf("!")));
61+
try (JarFile jar = new JarFile(new File(uri))) {
62+
List<JarEntry> classes = jar.stream()
63+
.filter(it -> it.getName().endsWith(".class"))
64+
.sorted(Comparator.comparing(ZipEntry::getName))
65+
.collect(Collectors.toList());
66+
for (JarEntry entry : classes) {
67+
try (InputStream in = jar.getInputStream(entry)) {
68+
indexer.index(in);
69+
}
70+
}
71+
}
72+
}
73+
}
74+
Index index = indexer.complete();
75+
ByteArrayOutputStream out = new ByteArrayOutputStream();
76+
new IndexWriter(out).write(index);
77+
return out.toByteArray();
78+
}
79+
80+
@BeforeAll
81+
public static void setup() throws IOException, URISyntaxException {
82+
firstIndex = index();
83+
}
84+
85+
@RepeatedTest(50)
86+
public void test() throws IOException, URISyntaxException {
87+
assertArrayEquals(firstIndex, index());
88+
}
89+
}

0 commit comments

Comments
 (0)