Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for sealed classes #348

Merged
merged 1 commit into from
May 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
}
}
Loading