Skip to content

Commit

Permalink
Merge pull request #348 from Ladicek/permitted-subclasses
Browse files Browse the repository at this point in the history
Add support for sealed classes
  • Loading branch information
Ladicek authored May 13, 2024
2 parents 5a892b0 + 929d1c7 commit 913dc3c
Show file tree
Hide file tree
Showing 13 changed files with 250 additions and 2 deletions.
53 changes: 53 additions & 0 deletions core/src/main/java/org/jboss/jandex/ClassInfo.java
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ private static final class ExtraInfo {
private RecordComponentInternal[] recordComponents;
private byte[] recordComponentPositions = EMPTY_POSITIONS;
private Set<DotName> memberClasses;
private Set<DotName> permittedSubclasses;
private ModuleInfo module;
}

Expand Down Expand Up @@ -266,6 +267,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
Expand Down Expand Up @@ -1133,6 +1152,28 @@ public ModuleInfo module() {
return extra != null ? extra.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<DotName> permittedSubclasses() {
if (extra == null || extra.permittedSubclasses == null) {
return Collections.emptySet();
}
return Collections.unmodifiableSet(extra.permittedSubclasses);
}

/**
* @return {@code true} if this class object represents a {@code sealed} class (or interface)
* @since 3.2.0
*/
public boolean isSealed() {
return extra != null && extra.permittedSubclasses != null && !extra.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.
Expand Down Expand Up @@ -1435,4 +1476,16 @@ void setModule(ModuleInfo module) {
void setFlags(short flags) {
this.flags = flags;
}

void setPermittedSubclasses(Set<DotName> permittedSubclasses) {
if (permittedSubclasses == null || permittedSubclasses.isEmpty()) {
return;
}

if (extra == null) {
extra = new ExtraInfo();
}

extra.permittedSubclasses = permittedSubclasses;
}
}
14 changes: 14 additions & 0 deletions core/src/main/java/org/jboss/jandex/IndexReaderV2.java
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,17 @@ private ClassInfo readClassEntry(PackedDataInputStream stream,
}
}

Set<DotName> 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<DotName, List<AnnotationInstance>> annotations = size > 0
Expand All @@ -651,6 +662,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);
Expand Down
10 changes: 10 additions & 0 deletions core/src/main/java/org/jboss/jandex/IndexWriterV2.java
Original file line number Diff line number Diff line change
Expand Up @@ -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());

Expand Down Expand Up @@ -941,6 +948,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());
Expand Down
26 changes: 26 additions & 0 deletions core/src/main/java/org/jboss/jandex/Indexer.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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) {
Expand Down Expand Up @@ -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<DotName> 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;
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public void testModuleAnnotations() throws IOException {
public void testModulePackagesListed() throws IOException {
ModuleInfo mod = indexModuleInfo();
List<DotName> 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));
Expand Down Expand Up @@ -91,11 +91,13 @@ public void testModuleExports() {
@Test
public void testModuleOpens() {
List<OpenedPackageInfo> 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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<DotName> setOf(String... strings) {
Set<DotName> result = new HashSet<>();
for (String string : strings) {
result.add(DotName.createSimple(string));
}
return result;
}
}
1 change: 1 addition & 0 deletions test-data/src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
16 changes: 16 additions & 0 deletions test-data/src/main/java/test/expr/Add.java
Original file line number Diff line number Diff line change
@@ -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();
}
}
4 changes: 4 additions & 0 deletions test-data/src/main/java/test/expr/Arith.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package test.expr;

public sealed abstract class Arith implements Expr permits Add, Mul {
}
7 changes: 7 additions & 0 deletions test-data/src/main/java/test/expr/BestValue.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package test.expr;

public class BestValue extends Value {
public BestValue() {
super(42);
}
}
5 changes: 5 additions & 0 deletions test-data/src/main/java/test/expr/Expr.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package test.expr;

public sealed interface Expr permits Value, Arith {
int eval();
}
16 changes: 16 additions & 0 deletions test-data/src/main/java/test/expr/Mul.java
Original file line number Diff line number Diff line change
@@ -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();
}
}
14 changes: 14 additions & 0 deletions test-data/src/main/java/test/expr/Value.java
Original file line number Diff line number Diff line change
@@ -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;
}
}

0 comments on commit 913dc3c

Please sign in to comment.