diff --git a/core/src/main/java/org/jboss/jandex/ClassInfo.java b/core/src/main/java/org/jboss/jandex/ClassInfo.java index 119d5b53..d09ae4d6 100644 --- a/core/src/main/java/org/jboss/jandex/ClassInfo.java +++ b/core/src/main/java/org/jboss/jandex/ClassInfo.java @@ -75,6 +75,7 @@ public final class ClassInfo implements Declaration, Descriptor, GenericSignatur private boolean hasNoArgsConstructor; private NestingInfo nestingInfo; private Set memberClasses; + private Set permittedSubclasses; /** Describes the form of nesting used by a class */ public enum NestingType { @@ -267,6 +268,24 @@ public final boolean isInterface() { return Modifier.isInterface(flags); } + /** + * @return {@code true} if this class object represents a {@code final} class; + * an interface is never {@code final} + * @since 3.2.0 + */ + public final boolean isFinal() { + return Modifier.isFinal(flags); + } + + /** + * @return {@code true} if this class object represents an {@code abstract} class; + * an interface is always {@code abstract} + * @since 3.2.0 + */ + public final boolean isAbstract() { + return Modifier.isAbstract(flags); + } + /** * * @return {@code true} if this class object represents an enum type @@ -1096,6 +1115,28 @@ public ModuleInfo module() { return nestingInfo != null ? nestingInfo.module : null; } + /** + * Returns the set of permitted subclasses of this {@code sealed} class (or interface). + * Returns an empty set if this class is not {@code sealed}. + * + * @return immutable set of names of this class's permitted subclasses, never {@code null} + * @since 3.2.0 + */ + public Set permittedSubclasses() { + if (permittedSubclasses == null) { + return Collections.emptySet(); + } + return Collections.unmodifiableSet(permittedSubclasses); + } + + /** + * @return {@code true} if this class object represents a {@code sealed} class (or interface) + * @since 3.2.0 + */ + public boolean isSealed() { + return permittedSubclasses != null && !permittedSubclasses.isEmpty(); + } + /** * Returns whether this class must have a generic signature. That is, whether the Java compiler * when compiling this class had to emit the {@code Signature} bytecode attribute. @@ -1363,4 +1404,8 @@ void setModule(ModuleInfo module) { void setFlags(short flags) { this.flags = flags; } + + void setPermittedSubclasses(Set permittedSubclasses) { + this.permittedSubclasses = permittedSubclasses; + } } diff --git a/core/src/main/java/org/jboss/jandex/IndexReaderV2.java b/core/src/main/java/org/jboss/jandex/IndexReaderV2.java index 12b227f6..3bd6b75d 100644 --- a/core/src/main/java/org/jboss/jandex/IndexReaderV2.java +++ b/core/src/main/java/org/jboss/jandex/IndexReaderV2.java @@ -50,7 +50,7 @@ */ final class IndexReaderV2 extends IndexReaderImpl { static final int MIN_VERSION = 6; - static final int MAX_VERSION = 11; + static final int MAX_VERSION = 12; private static final byte NULL_TARGET_TAG = 0; private static final byte FIELD_TAG = 1; private static final byte METHOD_TAG = 2; @@ -629,6 +629,17 @@ private ClassInfo readClassEntry(PackedDataInputStream stream, } } + Set permittedSubclasses = null; + if (version >= 12) { + int permittedSubclassesCount = stream.readPackedU32(); + if (permittedSubclassesCount > 0) { + permittedSubclasses = new HashSet<>(permittedSubclassesCount); + for (int i = 0; i < permittedSubclassesCount; i++) { + permittedSubclasses.add(nameTable[stream.readPackedU32()]); + } + } + } + int size = stream.readPackedU32(); Map> annotations = size > 0 @@ -647,6 +658,9 @@ private ClassInfo readClassEntry(PackedDataInputStream stream, if (memberClasses != null) { clazz.setMemberClasses(memberClasses); } + if (permittedSubclasses != null) { + clazz.setPermittedSubclasses(permittedSubclasses); + } FieldInternal[] fields = readClassFields(stream, clazz); clazz.setFieldArray(fields); diff --git a/core/src/main/java/org/jboss/jandex/IndexWriterV2.java b/core/src/main/java/org/jboss/jandex/IndexWriterV2.java index 4c496211..9d296d96 100644 --- a/core/src/main/java/org/jboss/jandex/IndexWriterV2.java +++ b/core/src/main/java/org/jboss/jandex/IndexWriterV2.java @@ -54,7 +54,7 @@ */ final class IndexWriterV2 extends IndexWriterImpl { static final int MIN_VERSION = 6; - static final int MAX_VERSION = 11; + static final int MAX_VERSION = 12; // babelfish (no h) private static final int MAGIC = 0xBABE1F15; @@ -606,6 +606,13 @@ private void writeClassEntry(PackedDataOutputStream stream, ClassInfo clazz) thr } } + if (version >= 12) { + stream.writePackedU32(clazz.permittedSubclasses().size()); + for (DotName permittedSubclass : clazz.permittedSubclasses()) { + stream.writePackedU32(positionOf(permittedSubclass)); + } + } + // Annotation length is early to allow eager allocation in reader. stream.writePackedU32(clazz.annotationsMap().size()); @@ -938,6 +945,9 @@ private void addClass(ClassInfo clazz) { for (DotName memberClass : clazz.memberClasses()) { addClassName(memberClass); } + for (DotName permittedSubclass : clazz.permittedSubclasses()) { + addClassName(permittedSubclass); + } addMethodList(clazz.methodArray()); names.intern(clazz.methodPositionArray()); diff --git a/core/src/main/java/org/jboss/jandex/Indexer.java b/core/src/main/java/org/jboss/jandex/Indexer.java index 93d9e625..540af1b3 100644 --- a/core/src/main/java/org/jboss/jandex/Indexer.java +++ b/core/src/main/java/org/jboss/jandex/Indexer.java @@ -209,6 +209,14 @@ public final class Indexer { 0x41, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73 }; + // "PermittedSubclasses" + private final static byte[] PERMITTED_SUBCLASSES = new byte[] { + // P e r m i t t e d + 0x50, 0x65, 0x72, 0x6d, 0x69, 0x74, 0x74, 0x65, 0x64, + // S u b c l a s s e s + 0x53, 0x75, 0x62, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x65, 0x73 + }; + private final static int RUNTIME_ANNOTATIONS_LEN = RUNTIME_ANNOTATIONS.length; private final static int RUNTIME_PARAM_ANNOTATIONS_LEN = RUNTIME_PARAM_ANNOTATIONS.length; private final static int RUNTIME_TYPE_ANNOTATIONS_LEN = RUNTIME_TYPE_ANNOTATIONS.length; @@ -227,6 +235,7 @@ public final class Indexer { private final static int RUNTIME_INVISIBLE_ANNOTATIONS_LEN = RUNTIME_INVISIBLE_ANNOTATIONS.length; private final static int RUNTIME_INVISIBLE_PARAM_ANNOTATIONS_LEN = RUNTIME_INVISIBLE_PARAM_ANNOTATIONS.length; private final static int RUNTIME_INVISIBLE_TYPE_ANNOTATIONS_LEN = RUNTIME_INVISIBLE_TYPE_ANNOTATIONS.length; + private final static int PERMITTED_SUBCLASSES_LEN = PERMITTED_SUBCLASSES.length; private final static int HAS_RUNTIME_ANNOTATION = 1; private final static int HAS_RUNTIME_PARAM_ANNOTATION = 2; @@ -246,6 +255,7 @@ public final class Indexer { private final static int HAS_RUNTIME_INVISIBLE_ANNOTATION = 16; private final static int HAS_RUNTIME_INVISIBLE_PARAM_ANNOTATION = 17; private final static int HAS_RUNTIME_INVISIBLE_TYPE_ANNOTATION = 18; + private final static int HAS_PERMITTED_SUBCLASSES = 19; private static class InnerClassInfo { private InnerClassInfo(DotName innerClass, DotName enclosingClass, String simpleName, int flags) { @@ -528,6 +538,18 @@ private void processRecordComponents(DataInputStream data) throws IOException { this.recordComponents = recordComponents; } + private void processPermittedSubclasses(DataInputStream data, ClassInfo target) throws IOException { + int numPermittedSubclasses = data.readUnsignedShort(); + if (numPermittedSubclasses > 0) { + Set permittedSubclasses = new HashSet<>(numPermittedSubclasses); + for (int i = 0; i < numPermittedSubclasses; i++) { + DotName name = decodeClassEntry(data.readUnsignedShort()); + permittedSubclasses.add(name); + } + target.setPermittedSubclasses(permittedSubclasses); + } + } + private void processAttributes(DataInputStream data, AnnotationTarget target) throws IOException { int numAttrs = data.readUnsignedShort(); byte[] constantPoolAnnoAttrributes = this.constantPoolAnnoAttrributes; @@ -583,6 +605,8 @@ private void processAttributes(DataInputStream data, AnnotationTarget target) th processModuleMainClass(data, (ClassInfo) target); } else if (annotationAttribute == HAS_RECORD && target instanceof ClassInfo) { processRecordComponents(data); + } else if (annotationAttribute == HAS_PERMITTED_SUBCLASSES && target instanceof ClassInfo) { + processPermittedSubclasses(data, (ClassInfo) target); } else { skipFully(data, attributeLen); } @@ -2404,6 +2428,8 @@ && match(buf, offset, RUNTIME_INVISIBLE_PARAM_ANNOTATIONS)) { } else if (len == RUNTIME_INVISIBLE_TYPE_ANNOTATIONS_LEN && match(buf, offset, RUNTIME_INVISIBLE_TYPE_ANNOTATIONS)) { annoAttributes[pos] = HAS_RUNTIME_INVISIBLE_TYPE_ANNOTATION; + } else if (len == PERMITTED_SUBCLASSES_LEN && match(buf, offset, PERMITTED_SUBCLASSES)) { + annoAttributes[pos] = HAS_PERMITTED_SUBCLASSES; } offset += len; break; diff --git a/core/src/test/java/org/jboss/jandex/test/ModuleInfoTestCase.java b/core/src/test/java/org/jboss/jandex/test/ModuleInfoTestCase.java index 0ff4a159..41e4f27f 100644 --- a/core/src/test/java/org/jboss/jandex/test/ModuleInfoTestCase.java +++ b/core/src/test/java/org/jboss/jandex/test/ModuleInfoTestCase.java @@ -63,7 +63,7 @@ public void testModuleAnnotations() throws IOException { public void testModulePackagesListed() throws IOException { ModuleInfo mod = indexModuleInfo(); List expected = Arrays.asList(DotName.createSimple("test"), - DotName.createSimple("test.exec")); + DotName.createSimple("test.exec"), DotName.createSimple("test.expr")); assertEquals(expected.size(), mod.packages().size()); for (DotName e : expected) { assertTrue(mod.packages().contains(e)); @@ -91,11 +91,13 @@ public void testModuleExports() { @Test public void testModuleOpens() { List opens = mod.opens(); - assertEquals(2, opens.size()); + assertEquals(3, opens.size()); assertEquals("test", opens.get(0).source().toString()); assertEquals("java.base", opens.get(0).targets().get(0).toString()); assertEquals("test.exec", opens.get(1).source().toString()); assertEquals("java.base", opens.get(1).targets().get(0).toString()); + assertEquals("test.expr", opens.get(2).source().toString()); + assertEquals("java.base", opens.get(2).targets().get(0).toString()); } @Test diff --git a/core/src/test/java/org/jboss/jandex/test/PermittedSubclassesTest.java b/core/src/test/java/org/jboss/jandex/test/PermittedSubclassesTest.java new file mode 100644 index 00000000..b8ebc3bb --- /dev/null +++ b/core/src/test/java/org/jboss/jandex/test/PermittedSubclassesTest.java @@ -0,0 +1,80 @@ +package org.jboss.jandex.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.DotName; +import org.jboss.jandex.Index; +import org.jboss.jandex.Indexer; +import org.jboss.jandex.test.util.IndexingUtil; +import org.junit.jupiter.api.Test; + +public class PermittedSubclassesTest { + @Test + public void test() throws IOException { + Indexer indexer = new Indexer(); + indexer.index(PermittedSubclassesTest.class.getResourceAsStream("/test/expr/Add.class")); + indexer.index(PermittedSubclassesTest.class.getResourceAsStream("/test/expr/Arith.class")); + indexer.index(PermittedSubclassesTest.class.getResourceAsStream("/test/expr/BestValue.class")); + indexer.index(PermittedSubclassesTest.class.getResourceAsStream("/test/expr/Expr.class")); + indexer.index(PermittedSubclassesTest.class.getResourceAsStream("/test/expr/Mul.class")); + indexer.index(PermittedSubclassesTest.class.getResourceAsStream("/test/expr/Value.class")); + Index index = indexer.complete(); + + doTest(index); + doTest(IndexingUtil.roundtrip(index)); + } + + private void doTest(Index index) { + ClassInfo expr = index.getClassByName("test.expr.Expr"); + assertTrue(expr.isSealed()); + assertFalse(expr.isFinal()); + assertEquals(setOf("test.expr.Value", "test.expr.Arith"), expr.permittedSubclasses()); + + ClassInfo value = index.getClassByName("test.expr.Value"); + assertFalse(value.isSealed()); + assertFalse(value.isFinal()); + assertTrue(value.interfaceNames().contains(DotName.createSimple("test.expr.Expr"))); + assertEquals(Collections.emptySet(), value.permittedSubclasses()); + + ClassInfo bestValue = index.getClassByName("test.expr.BestValue"); + assertFalse(bestValue.isSealed()); + assertFalse(bestValue.isFinal()); + assertEquals(bestValue.superName(), DotName.createSimple("test.expr.Value")); + assertEquals(Collections.emptySet(), bestValue.permittedSubclasses()); + + ClassInfo arith = index.getClassByName("test.expr.Arith"); + assertTrue(arith.isSealed()); + assertFalse(arith.isFinal()); + assertTrue(arith.isAbstract()); + assertTrue(value.interfaceNames().contains(DotName.createSimple("test.expr.Expr"))); + assertEquals(setOf("test.expr.Add", "test.expr.Mul"), arith.permittedSubclasses()); + + ClassInfo add = index.getClassByName("test.expr.Add"); + assertFalse(add.isSealed()); + assertTrue(add.isFinal()); + assertEquals(DotName.createSimple("test.expr.Arith"), add.superName()); + assertEquals(Collections.emptySet(), value.permittedSubclasses()); + + ClassInfo mul = index.getClassByName("test.expr.Mul"); + assertFalse(mul.isSealed()); + assertTrue(mul.isFinal()); + assertEquals(DotName.createSimple("test.expr.Arith"), mul.superName()); + assertEquals(Collections.emptySet(), value.permittedSubclasses()); + } + + private static Set setOf(String... strings) { + Set result = new HashSet<>(); + for (String string : strings) { + result.add(DotName.createSimple(string)); + } + return result; + } +} diff --git a/doc/modules/ROOT/pages/index.adoc b/doc/modules/ROOT/pages/index.adoc index 11665fa0..034baa3d 100644 --- a/doc/modules/ROOT/pages/index.adoc +++ b/doc/modules/ROOT/pages/index.adoc @@ -62,6 +62,9 @@ It is also a maximum persistent index format version the given Jandex version ca |=== |Jandex version |Persistent format version +|Jandex 3.2.x +|12 + |Jandex 3.0.x, 3.1.x |11 diff --git a/test-data/src/main/java/module-info.java b/test-data/src/main/java/module-info.java index a0cbb91e..02c6dcad 100644 --- a/test-data/src/main/java/module-info.java +++ b/test-data/src/main/java/module-info.java @@ -11,6 +11,7 @@ opens test to java.base; opens test.exec to java.base; + opens test.expr to java.base; provides test.ServiceProviderExample with test.ServiceProviderExample.ServiceProviderExampleImpl; diff --git a/test-data/src/main/java/test/expr/Add.java b/test-data/src/main/java/test/expr/Add.java new file mode 100644 index 00000000..fec2936b --- /dev/null +++ b/test-data/src/main/java/test/expr/Add.java @@ -0,0 +1,16 @@ +package test.expr; + +public final class Add extends Arith { + private final Expr left; + private final Expr right; + + public Add(Expr left, Expr right) { + this.left = left; + this.right = right; + } + + @Override + public int eval() { + return left.eval() + right.eval(); + } +} diff --git a/test-data/src/main/java/test/expr/Arith.java b/test-data/src/main/java/test/expr/Arith.java new file mode 100644 index 00000000..c010e309 --- /dev/null +++ b/test-data/src/main/java/test/expr/Arith.java @@ -0,0 +1,4 @@ +package test.expr; + +public sealed abstract class Arith implements Expr permits Add, Mul { +} diff --git a/test-data/src/main/java/test/expr/BestValue.java b/test-data/src/main/java/test/expr/BestValue.java new file mode 100644 index 00000000..4f6079e8 --- /dev/null +++ b/test-data/src/main/java/test/expr/BestValue.java @@ -0,0 +1,7 @@ +package test.expr; + +public class BestValue extends Value { + public BestValue() { + super(42); + } +} diff --git a/test-data/src/main/java/test/expr/Expr.java b/test-data/src/main/java/test/expr/Expr.java new file mode 100644 index 00000000..e1075a39 --- /dev/null +++ b/test-data/src/main/java/test/expr/Expr.java @@ -0,0 +1,5 @@ +package test.expr; + +public sealed interface Expr permits Value, Arith { + int eval(); +} diff --git a/test-data/src/main/java/test/expr/Mul.java b/test-data/src/main/java/test/expr/Mul.java new file mode 100644 index 00000000..7c31f8b2 --- /dev/null +++ b/test-data/src/main/java/test/expr/Mul.java @@ -0,0 +1,16 @@ +package test.expr; + +public final class Mul extends Arith { + private final Expr left; + private final Expr right; + + public Mul(Expr left, Expr right) { + this.left = left; + this.right = right; + } + + @Override + public int eval() { + return left.eval() * right.eval(); + } +} diff --git a/test-data/src/main/java/test/expr/Value.java b/test-data/src/main/java/test/expr/Value.java new file mode 100644 index 00000000..255c4674 --- /dev/null +++ b/test-data/src/main/java/test/expr/Value.java @@ -0,0 +1,14 @@ +package test.expr; + +public non-sealed class Value implements Expr { + private final int value; + + public Value(int value) { + this.value = value; + } + + @Override + public int eval() { + return value; + } +}