This is an automated email from the ASF dual-hosted git repository.
garydgregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-bcel.git
The following commit(s) were added to refs/heads/master by this push:
new fa058187 Strengthen type annotation tests and clarify nested type paths
fa058187 is described below
commit fa058187e077ef9d97ef2b4582c500ba5443e96e
Author: Gary Gregory <[email protected]>
AuthorDate: Thu Oct 22 17:29:59 2026 -0400
Strengthen type annotation tests and clarify nested type paths
Cover all 22 target layouts, unsigned boundaries, truncated input,
deep-copy independence, visitor context, and attributes within Code and
record components. Verify complete class-file round trips and
distinguish inner classes from static nested classes in Javadoc and
tests.
---
src/changes/changes.xml | 2 +-
.../apache/bcel/classfile/TypeAnnotationEntry.java | 10 +-
.../classfile/TypeAnnotationAttributesTest.java | 245 +++++++++++++++++
.../bcel/classfile/TypeAnnotationEntryTest.java | 289 +++++++++++++++++++++
.../apache/bcel/classfile/TypeAnnotationsTest.java | 116 +++++++++
5 files changed, 658 insertions(+), 4 deletions(-)
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 49ba1c0f..83ded8a6 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -68,7 +68,7 @@ The <action> type attribute can be add,update,fix,remove.
<action type="fix" dev="ggregory" due-to="Gary
Gregory">Update ECJ versions and generate the SWAP fixture with BCEL
(#537).</action>
<!-- ADD -->
<action type="add" dev="ggregory" due-to="Gary
Gregory">Add and use ClassFormatException.ClassFormatException(String,
Object...).</action>
- <action type="add" dev="ggregory" due-to="Nicolas
Baumann">Add support for the RuntimeVisibleTypeAnnotations and
RuntimeInvisibleTypeAnnotations attributes.</action>
+ <action type="add" dev="ggregory" due-to="Nicolas
Baumann, Gary Gregory">Add support for the RuntimeVisibleTypeAnnotations and
RuntimeInvisibleTypeAnnotations attributes.</action>
<!-- UPDATE -->
<action type="update" dev="ggregory" due-to="Gary
Gregory">Bump org.apache.commons:commons-parent from 104 to 106.</action>
<action type="update" dev="ggregory" due-to="Gary
Gregory">Bump org.apache.commons:commons-lang3 from 3.20.0 to 3.21.0
#531.</action>
diff --git a/src/main/java/org/apache/bcel/classfile/TypeAnnotationEntry.java
b/src/main/java/org/apache/bcel/classfile/TypeAnnotationEntry.java
index f4145b3d..c076a964 100644
--- a/src/main/java/org/apache/bcel/classfile/TypeAnnotationEntry.java
+++ b/src/main/java/org/apache/bcel/classfile/TypeAnnotationEntry.java
@@ -291,9 +291,13 @@ public class TypeAnnotationEntry {
}
/**
- * Gets the number of steps of the type path, which leads from the type of
the target to the part of it that is annotated. The path is empty when the
- * annotation applies to the outermost part of that type where an
annotation is allowed: for a nested type, the outermost class ({@code @A
Outer.Inner}),
- * while {@code Outer.@A Inner} needs a step.
+ * Gets the number of steps in the type path.
+ *
+ * <p>
+ * The path leads from the type of the target to the part of it that is
annotated. An empty path identifies the outermost part of the type where an
+ * annotation is allowed. For a non-static inner class, {@code @A
Outer.Inner} has an empty path, while {@code Outer.@A Inner} requires one step.
+ * For a static nested class, {@code Outer.@A Nested} has an empty path.
+ * </p>
*
* @return The number of steps of the type path.
*/
diff --git
a/src/test/java/org/apache/bcel/classfile/TypeAnnotationAttributesTest.java
b/src/test/java/org/apache/bcel/classfile/TypeAnnotationAttributesTest.java
new file mode 100644
index 00000000..8dfad67b
--- /dev/null
+++ b/src/test/java/org/apache/bcel/classfile/TypeAnnotationAttributesTest.java
@@ -0,0 +1,245 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.bcel.classfile;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.EOFException;
+import java.io.IOException;
+import java.util.Arrays;
+
+import org.apache.bcel.Const;
+import org.apache.bcel.generic.ConstantPoolGen;
+import org.apache.bcel.verifier.statics.StringRepresentation;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+/** Tests attribute dispatch, copying, and nesting without requiring a newer
source language level. */
+class TypeAnnotationAttributesTest {
+
+ private static final class Fixture {
+ private final ConstantPool pool;
+ private final int nameIndex;
+ private final int typeIndex;
+ private final int codeIndex;
+ private final int recordIndex;
+ private final int componentNameIndex;
+ private final int descriptorIndex;
+
+ Fixture(final boolean visible) {
+ final ConstantPoolGen gen = new ConstantPoolGen();
+ nameIndex = gen.addUtf8(visible ? "RuntimeVisibleTypeAnnotations"
: "RuntimeInvisibleTypeAnnotations");
+ typeIndex = gen.addUtf8("Lexample/Annotation;");
+ codeIndex = gen.addUtf8("Code");
+ recordIndex = gen.addUtf8("Record");
+ componentNameIndex = gen.addUtf8("value");
+ descriptorIndex = gen.addUtf8("Ljava/lang/String;");
+ pool = gen.getFinalConstantPool();
+ }
+
+ byte[] annotationAttribute(final int target, final byte[] info, final
int count) throws IOException {
+ final ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ try (DataOutputStream out = new DataOutputStream(bytes)) {
+ out.writeShort(count);
+ for (int i = 0; i < count; i++) {
+ out.writeByte(target);
+ out.write(info);
+ out.writeByte(0); // empty type path
+ out.writeShort(typeIndex);
+ out.writeShort(0); // no element-value pairs
+ }
+ }
+ return attribute(nameIndex, bytes.toByteArray());
+ }
+
+ TypeAnnotations annotations(final int count) throws IOException {
+ return (TypeAnnotations) parse(annotationAttribute(0x13, new
byte[0], count), pool);
+ }
+ }
+
+ private static byte[] attribute(final int nameIndex, final byte[]
contents) throws IOException {
+ final ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ try (DataOutputStream out = new DataOutputStream(bytes)) {
+ out.writeShort(nameIndex);
+ out.writeInt(contents.length);
+ out.write(contents);
+ }
+ return bytes.toByteArray();
+ }
+
+ private static byte[] dump(final Attribute attribute) throws IOException {
+ final ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ try (DataOutputStream out = new DataOutputStream(bytes)) {
+ attribute.dump(out);
+ }
+ return bytes.toByteArray();
+ }
+
+ private static Attribute parse(final byte[] bytes, final ConstantPool
pool) throws IOException {
+ try (DataInputStream input = new DataInputStream(new
ByteArrayInputStream(bytes))) {
+ final Attribute attribute = Attribute.readAttribute(input, pool);
+ assertEquals(0, input.available());
+ return attribute;
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ void dispatchesAndCopiesBothAttributes(final boolean visible) throws
IOException {
+ final Fixture fixture = new Fixture(visible);
+ final byte[] encoded = fixture.annotationAttribute(0x13, new byte[0],
2);
+ final TypeAnnotations original = (TypeAnnotations) parse(encoded,
fixture.pool);
+ final String name = visible ? "RuntimeVisibleTypeAnnotations" :
"RuntimeInvisibleTypeAnnotations";
+ assertEquals(visible ? Const.ATTR_RUNTIME_VISIBLE_TYPE_ANNOTATIONS :
Const.ATTR_RUNTIME_INVISIBLE_TYPE_ANNOTATIONS, original.getTag());
+ assertEquals(name, Const.getAttributeName(original.getTag()));
+ assertEquals(visible ? RuntimeVisibleTypeAnnotations.class :
RuntimeInvisibleTypeAnnotations.class, original.getClass());
+ assertEquals(visible, original.isRuntimeVisible());
+ assertEquals(2, original.getNumTypeAnnotations());
+ assertEquals(name + ":\n " + original.getTypeAnnotationEntries()[0] +
"\n " + original.getTypeAnnotationEntries()[1], original.toString());
+ assertEquals(original.toString(), new
StringRepresentation(original).toString());
+ final ConstantPool otherPool = fixture.pool.copy();
+ final TypeAnnotations copy = (TypeAnnotations)
original.copy(otherPool);
+ assertSame(otherPool, copy.getConstantPool());
+ assertNotSame(original.getTypeAnnotationEntries(),
copy.getTypeAnnotationEntries());
+ int index = 0;
+ for (final TypeAnnotationEntry entry : copy) {
+ assertSame(entry, copy.getTypeAnnotationEntries()[index]);
+ assertNotSame(original.getTypeAnnotationEntries()[index], entry);
+
assertNotSame(original.getTypeAnnotationEntries()[index].getAnnotationEntry(),
entry.getAnnotationEntry());
+ assertSame(otherPool,
entry.getAnnotationEntry().getConstantPool());
+ assertEquals(visible,
entry.getAnnotationEntry().isRuntimeVisible());
+ index++;
+ }
+ assertEquals(2, index);
+ assertArrayEquals(encoded, dump(original));
+ assertArrayEquals(encoded, dump(copy));
+ copy.getTypeAnnotationEntries()[0] =
copy.getTypeAnnotationEntries()[1];
+ assertNotSame(original.getTypeAnnotationEntries()[0],
original.getTypeAnnotationEntries()[1]);
+ copy.setTypeAnnotationTable(null);
+ copy.setLength(2);
+ assertEquals(0, copy.getNumTypeAnnotations());
+ assertFalse(copy.iterator().hasNext());
+ assertEquals(name + ":\n", copy.toString());
+ assertArrayEquals(fixture.annotationAttribute(0x13, new byte[0], 0),
dump(copy));
+ assertEquals(2, original.getNumTypeAnnotations());
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ void readsEmptyAttributes(final boolean visible) throws IOException {
+ final Fixture fixture = new Fixture(visible);
+ final byte[] encoded = fixture.annotationAttribute(0x13, new byte[0],
0);
+ final TypeAnnotations annotations = (TypeAnnotations) parse(encoded,
fixture.pool);
+ assertEquals(0, annotations.getNumTypeAnnotations());
+ assertFalse(annotations.iterator().hasNext());
+ assertArrayEquals(encoded, dump(annotations));
+ assertArrayEquals(encoded,
dump(annotations.copy(fixture.pool.copy())));
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ void rejectsTruncatedAttributes(final boolean visible) throws IOException {
+ final Fixture fixture = new Fixture(visible);
+ final byte[] encoded = fixture.annotationAttribute(0x13, new byte[0],
2);
+ for (int length = 0; length < encoded.length; length++) {
+ final byte[] truncated = Arrays.copyOf(encoded, length);
+ assertThrows(EOFException.class, () -> parse(truncated,
fixture.pool), "length " + length);
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ void enforcesTheUnsignedAnnotationCount(final boolean visible) throws
IOException {
+ final Fixture fixture = new Fixture(visible);
+ final TypeAnnotations annotations = fixture.annotations(0);
+ final TypeAnnotationEntry entry = new TypeAnnotationEntry(0x13, new
byte[0], new byte[0],
+ new AnnotationEntry(fixture.typeIndex, fixture.pool, visible));
+ final TypeAnnotationEntry[] entries = new TypeAnnotationEntry[65535];
+ Arrays.fill(entries, entry);
+ annotations.setTypeAnnotationTable(entries);
+ annotations.setLength(2 + entries.length * 6);
+ final byte[] encoded = dump(annotations);
+ assertEquals(65535, ((TypeAnnotations) parse(encoded,
fixture.pool)).getNumTypeAnnotations());
+ annotations.setTypeAnnotationTable(new TypeAnnotationEntry[65536]);
+ assertThrows(ClassFormatException.class, () -> dump(annotations));
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ void readsTypeAnnotationsInsideCode(final boolean visible) throws
IOException {
+ final Fixture fixture = new Fixture(visible);
+ final byte[] nested = fixture.annotationAttribute(0x40, new byte[] {0,
1, 0, 0, 0, 1, 0, 0}, 1);
+ final ByteArrayOutputStream body = new ByteArrayOutputStream();
+ try (DataOutputStream out = new DataOutputStream(body)) {
+ out.writeShort(0); // max_stack
+ out.writeShort(1); // max_locals
+ out.writeInt(1); // code_length
+ out.writeByte(Const.RETURN);
+ out.writeShort(0); // exception_table_length
+ out.writeShort(1); // attributes_count
+ out.write(nested);
+ }
+ final byte[] encoded = attribute(fixture.codeIndex,
body.toByteArray());
+ final Code code = (Code) parse(encoded, fixture.pool);
+ assertEquals(1, code.getAttributes().length);
+ final TypeAnnotations annotations = (TypeAnnotations)
code.getAttributes()[0];
+ assertEquals(visible, annotations.isRuntimeVisible());
+ assertEquals(0x40,
annotations.getTypeAnnotationEntries()[0].getTargetType());
+ assertArrayEquals(new byte[] {0, 1, 0, 0, 0, 1, 0, 0},
annotations.getTypeAnnotationEntries()[0].getTargetInfo());
+ assertArrayEquals(encoded, dump(code));
+ assertArrayEquals(encoded, dump(code.copy(fixture.pool.copy())));
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ void readsTypeAnnotationsInsideRecordComponents(final boolean visible)
throws IOException {
+ final Fixture fixture = new Fixture(visible);
+ final byte[] nested = fixture.annotationAttribute(0x13, new byte[0],
1);
+ final ByteArrayOutputStream body = new ByteArrayOutputStream();
+ try (DataOutputStream out = new DataOutputStream(body)) {
+ out.writeShort(1); // components_count
+ out.writeShort(fixture.componentNameIndex);
+ out.writeShort(fixture.descriptorIndex);
+ out.writeShort(1); // attributes_count
+ out.write(nested);
+ }
+ final byte[] encoded = attribute(fixture.recordIndex,
body.toByteArray());
+ final Record record = (Record) parse(encoded, fixture.pool);
+ assertEquals(1, record.getComponents().length);
+ final RecordComponentInfo component = record.getComponents()[0];
+ assertEquals(1, component.getAttributes().length);
+ final TypeAnnotations annotations = (TypeAnnotations)
component.getAttributes()[0];
+ assertSame(annotations, component.getAttribute(visible ?
Const.ATTR_RUNTIME_VISIBLE_TYPE_ANNOTATIONS :
Const.ATTR_RUNTIME_INVISIBLE_TYPE_ANNOTATIONS));
+ assertEquals(visible, annotations.isRuntimeVisible());
+ assertTrue(annotations.iterator().hasNext());
+ assertEquals(0x13,
annotations.getTypeAnnotationEntries()[0].getTargetType());
+ assertArrayEquals(encoded, dump(record));
+ }
+}
diff --git
a/src/test/java/org/apache/bcel/classfile/TypeAnnotationEntryTest.java
b/src/test/java/org/apache/bcel/classfile/TypeAnnotationEntryTest.java
new file mode 100644
index 00000000..b729beae
--- /dev/null
+++ b/src/test/java/org/apache/bcel/classfile/TypeAnnotationEntryTest.java
@@ -0,0 +1,289 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.bcel.classfile;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.util.Arrays;
+import java.util.stream.Stream;
+
+import org.apache.bcel.generic.ConstantPoolGen;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+/** Tests the target layouts and type paths defined by JVMS 4.7.20,
independently of BCEL's writer. */
+class TypeAnnotationEntryTest {
+
+ // Array component, inner type, type argument 255, then its wildcard bound
(JVMS 4.7.20.2).
+ private static final byte[] TYPE_PATH = {0, 0, 1, 0, 3, (byte) 255, 2, 0};
+
+ private static ConstantPool constantPool() {
+ return new ConstantPool(new Constant[] {null, new
ConstantUtf8("Lexample/Annotation;")});
+ }
+
+ private static byte[] dump(final TypeAnnotationEntry entry) throws
IOException {
+ final ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ try (DataOutputStream out = new DataOutputStream(bytes)) {
+ entry.dump(out);
+ }
+ return bytes.toByteArray();
+ }
+
+ private static byte[] encodedEntry(final int target, final byte[]
targetInfo, final byte[] path) throws IOException {
+ final ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ try (DataOutputStream out = new DataOutputStream(bytes)) {
+ out.writeByte(target);
+ out.write(targetInfo);
+ out.writeByte(path.length / 2);
+ out.write(path);
+ out.writeShort(1); // annotation descriptor in the constant pool
+ out.writeShort(0); // no element-value pairs
+ }
+ return bytes.toByteArray();
+ }
+
+ private static TypeAnnotationEntry read(final byte[] bytes, final boolean
visible) throws IOException {
+ try (DataInputStream input = new DataInputStream(new
ByteArrayInputStream(bytes))) {
+ final TypeAnnotationEntry entry = TypeAnnotationEntry.read(input,
constantPool(), visible);
+ assertEquals(0, input.available(), "The entry must consume exactly
its encoded bytes");
+ return entry;
+ }
+ }
+
+ // The numeric tags and payloads are specified by JVMS tables 4.7.20-A/B
and section 4.7.20.1.
+ static Stream<Arguments> targetLayouts() {
+ return Stream.of(
+ Arguments.of(TypeAnnotationEntry.TARGET_CLASS_TYPE_PARAMETER,
0x00, new byte[] {(byte) 254}),
+ Arguments.of(TypeAnnotationEntry.TARGET_METHOD_TYPE_PARAMETER,
0x01, new byte[] {(byte) 253}),
+ Arguments.of(TypeAnnotationEntry.TARGET_CLASS_EXTENDS, 0x10,
new byte[] {(byte) 255, (byte) 255}),
+
Arguments.of(TypeAnnotationEntry.TARGET_CLASS_TYPE_PARAMETER_BOUND, 0x11, new
byte[] {1, 2}),
+
Arguments.of(TypeAnnotationEntry.TARGET_METHOD_TYPE_PARAMETER_BOUND, 0x12, new
byte[] {3, 4}),
+ Arguments.of(TypeAnnotationEntry.TARGET_FIELD, 0x13, new
byte[0]),
+ Arguments.of(TypeAnnotationEntry.TARGET_METHOD_RETURN, 0x14,
new byte[0]),
+ Arguments.of(TypeAnnotationEntry.TARGET_METHOD_RECEIVER, 0x15,
new byte[0]),
+
Arguments.of(TypeAnnotationEntry.TARGET_METHOD_FORMAL_PARAMETER, 0x16, new
byte[] {(byte) 252}),
+ Arguments.of(TypeAnnotationEntry.TARGET_THROWS, 0x17, new
byte[] {(byte) 128, 1}),
+ Arguments.of(TypeAnnotationEntry.TARGET_LOCAL_VARIABLE, 0x40,
new byte[] {0, 1, 0, 2, 0, 3, 0, (byte) 254}),
+ Arguments.of(TypeAnnotationEntry.TARGET_RESOURCE_VARIABLE,
0x41, new byte[] {0, 1, 0, 4, 0, 5, 0, (byte) 253}),
+ Arguments.of(TypeAnnotationEntry.TARGET_EXCEPTION_PARAMETER,
0x42, new byte[] {1, 2}),
+ Arguments.of(TypeAnnotationEntry.TARGET_INSTANCEOF, 0x43, new
byte[] {3, 4}),
+ Arguments.of(TypeAnnotationEntry.TARGET_NEW, 0x44, new byte[]
{5, 6}),
+ Arguments.of(TypeAnnotationEntry.TARGET_CONSTRUCTOR_REFERENCE,
0x45, new byte[] {7, 8}),
+ Arguments.of(TypeAnnotationEntry.TARGET_METHOD_REFERENCE,
0x46, new byte[] {9, 10}),
+ Arguments.of(TypeAnnotationEntry.TARGET_CAST, 0x47, new byte[]
{11, 12, 2}),
+
Arguments.of(TypeAnnotationEntry.TARGET_CONSTRUCTOR_INVOCATION_TYPE_ARGUMENT,
0x48, new byte[] {13, 14, 3}),
+
Arguments.of(TypeAnnotationEntry.TARGET_METHOD_INVOCATION_TYPE_ARGUMENT, 0x49,
new byte[] {15, 16, 4}),
+
Arguments.of(TypeAnnotationEntry.TARGET_CONSTRUCTOR_REFERENCE_TYPE_ARGUMENT,
0x4A, new byte[] {17, 18, 5}),
+
Arguments.of(TypeAnnotationEntry.TARGET_METHOD_REFERENCE_TYPE_ARGUMENT, 0x4B,
new byte[] {19, 20, (byte) 255}));
+ }
+
+ @ParameterizedTest
+ @MethodSource("targetLayouts")
+ void readsAndDumpsEveryTargetLayout(final int targetConstant, final int
tag, final byte[] info) throws IOException {
+ assertEquals(tag, targetConstant, "The public constant must match the
JVMS tag");
+ final byte[] expected = encodedEntry(tag, info, new byte[0]);
+ for (final boolean visible : new boolean[] {false, true}) {
+ final TypeAnnotationEntry entry = read(expected, visible);
+ assertEquals(tag, entry.getTargetType());
+ assertArrayEquals(info, entry.getTargetInfo());
+ assertEquals(0, entry.getTypePathLength());
+ assertEquals("Lexample/Annotation;",
entry.getAnnotationEntry().getAnnotationType());
+ assertEquals(visible,
entry.getAnnotationEntry().isRuntimeVisible());
+ assertArrayEquals(expected, dump(entry));
+ assertArrayEquals(expected, dump(new
TypeAnnotationEntry(targetConstant, info, new byte[0],
entry.getAnnotationEntry())));
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ void readsEveryPathKindAndUnsignedArgumentIndex(final boolean visible)
throws IOException {
+ final byte[] expected = encodedEntry(0x13, new byte[0], TYPE_PATH);
+ final TypeAnnotationEntry entry = read(expected, visible);
+ assertEquals(4, entry.getTypePathLength());
+ assertEquals(0, entry.getTypePathKind(0));
+ assertEquals(1, entry.getTypePathKind(1));
+ assertEquals(3, entry.getTypePathKind(2));
+ assertEquals(2, entry.getTypePathKind(3));
+ assertEquals(0, entry.getTypePathArgumentIndex(0));
+ assertEquals(0, entry.getTypePathArgumentIndex(1));
+ assertEquals(255, entry.getTypePathArgumentIndex(2));
+ assertEquals(0, entry.getTypePathArgumentIndex(3));
+ assertEquals(visible, entry.getAnnotationEntry().isRuntimeVisible());
+ assertArrayEquals(expected, dump(entry));
+ }
+
+ @ParameterizedTest
+ @MethodSource("targetLayouts")
+ void rejectsTruncatedEntries(final int targetConstant, final int tag,
final byte[] info) throws IOException {
+ final byte[] bytes = encodedEntry(tag, info, TYPE_PATH);
+ for (int length = 0; length < bytes.length; length++) {
+ final byte[] truncated = Arrays.copyOf(bytes, length);
+ assertThrows(EOFException.class, () -> read(truncated, true),
"target " + targetConstant + ", length " + length);
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("targetLayouts")
+ void rejectsIncorrectTargetInfoLengths(final int targetConstant, final int
tag, final byte[] info) {
+ final AnnotationEntry annotation = new AnnotationEntry(1,
constantPool(), true);
+ assertEquals(tag, targetConstant);
+ assertThrows(IllegalArgumentException.class,
+ () -> new TypeAnnotationEntry(targetConstant,
Arrays.copyOf(info, info.length + 1), new byte[0], annotation));
+ if (info.length > 0) {
+ assertThrows(IllegalArgumentException.class,
+ () -> new TypeAnnotationEntry(targetConstant,
Arrays.copyOf(info, info.length - 1), new byte[0], annotation));
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = {0, 1, 255, 256, 65535})
+ void readsUnsignedLocalVariableTableLengths(final int ranges) throws
IOException {
+ final byte[] info = new byte[2 + ranges * 6];
+ info[0] = (byte) (ranges >>> 8);
+ info[1] = (byte) ranges;
+ if (ranges > 0) {
+ info[info.length - 1] = (byte) 255;
+ }
+ for (final int target : new int[] {0x40, 0x41}) {
+ final byte[] bytes = encodedEntry(target, info, new byte[0]);
+ final TypeAnnotationEntry entry = read(bytes, true);
+ assertArrayEquals(info, entry.getTargetInfo());
+ assertArrayEquals(bytes, dump(entry));
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = {0, 1, 127, 128, 255})
+ void readsUnsignedPathLengths(final int steps) throws IOException {
+ final byte[] bytes = encodedEntry(0x13, new byte[0], new byte[steps *
2]);
+ final TypeAnnotationEntry entry = read(bytes, true);
+ assertEquals(steps, entry.getTypePathLength());
+ assertArrayEquals(bytes, dump(entry));
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = {-1, 2, 15, 24, 63, 76, 127, 255, 256})
+ void rejectsUndefinedConstructorTargets(final int target) {
+ assertThrows(ClassFormatException.class, () -> new
TypeAnnotationEntry(target, new byte[0], new byte[0], null));
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = {2, 15, 24, 63, 76, 127, 255})
+ void rejectsUndefinedEncodedTargets(final int target) throws IOException {
+ final byte[] bytes = encodedEntry(target, new byte[0], new byte[0]);
+ assertThrows(ClassFormatException.class, () -> read(bytes, true));
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = {0, 1})
+ void rejectsLocalVariableTablesWithoutACompleteCount(final int length) {
+ for (final int target : new int[] {0x40, 0x41}) {
+ assertThrows(IllegalArgumentException.class,
+ () -> new TypeAnnotationEntry(target, new byte[length],
new byte[0], new AnnotationEntry(1, constantPool(), true)));
+ }
+ }
+
+ @Test
+ void protectsTargetInfoAndPathFromCallerMutation() throws IOException {
+ final byte[] info = {0, 2};
+ final byte[] path = {3, 1};
+ final TypeAnnotationEntry entry = new TypeAnnotationEntry(0x10, info,
path, new AnnotationEntry(1, constantPool(), true));
+ info[0] = 99;
+ path[0] = 99;
+ path[1] = 99;
+ final byte[] returnedInfo = entry.getTargetInfo();
+ returnedInfo[0] = 88;
+ assertArrayEquals(new byte[] {0, 2}, entry.getTargetInfo());
+ assertEquals(3, entry.getTypePathKind(0));
+ assertEquals(1, entry.getTypePathArgumentIndex(0));
+ assertArrayEquals(encodedEntry(0x10, new byte[] {0, 2}, new byte[] {3,
1}), dump(entry));
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ void deeplyCopiesNestedAnnotationValues(final boolean visible) throws
IOException {
+ final ConstantPoolGen gen = new ConstantPoolGen();
+ final int type = gen.addUtf8("Lexample/Annotation;");
+ final int nestedType = gen.addUtf8("Lexample/Nested;");
+ final int arrayName = gen.addUtf8("items");
+ final int nestedName = gen.addUtf8("nested");
+ final int valueName = gen.addUtf8("value");
+ final int first = gen.addUtf8("first");
+ final int second = gen.addUtf8("second");
+ final ConstantPool pool = gen.getFinalConstantPool();
+ final AnnotationEntry nested = new AnnotationEntry(nestedType, pool,
visible);
+ nested.addElementNameValuePair(new ElementValuePair(valueName, new
SimpleElementValue(ElementValue.STRING, first, pool), pool));
+ final AnnotationEntry annotation = new AnnotationEntry(type, pool,
visible);
+ annotation.addElementNameValuePair(new ElementValuePair(arrayName, new
ArrayElementValue(ElementValue.ARRAY,
+ new ElementValue[] {new
SimpleElementValue(ElementValue.STRING, first, pool)}, pool), pool));
+ annotation.addElementNameValuePair(new ElementValuePair(nestedName,
new AnnotationElementValue(ElementValue.ANNOTATION, nested, pool), pool));
+ final TypeAnnotationEntry original = new TypeAnnotationEntry(0x13, new
byte[0], TYPE_PATH, annotation);
+ final byte[] expected = dump(original);
+ final ConstantPool otherPool = pool.copy();
+ final TypeAnnotationEntry copy = original.copy(otherPool);
+ assertArrayEquals(expected, dump(copy));
+ assertNotSame(original, copy);
+ assertNotSame(annotation, copy.getAnnotationEntry());
+ assertSame(otherPool, copy.getAnnotationEntry().getConstantPool());
+ assertEquals(visible, copy.getAnnotationEntry().isRuntimeVisible());
+ final ElementValuePair[] copiedPairs =
copy.getAnnotationEntry().getElementValuePairs();
+ final ArrayElementValue array = (ArrayElementValue)
copiedPairs[0].getValue();
+ final AnnotationEntry copiedNested = ((AnnotationElementValue)
copiedPairs[1].getValue()).getAnnotationEntry();
+ assertNotSame(annotation.getElementValuePairs()[0], copiedPairs[0]);
+ assertNotSame(((ArrayElementValue)
annotation.getElementValuePairs()[0].getValue()).getElementValuesArray(),
array.getElementValuesArray());
+ assertNotSame(nested, copiedNested);
+ assertSame(otherPool, array.getConstantPool());
+ assertSame(otherPool,
array.getElementValuesArray()[0].getConstantPool());
+ assertSame(otherPool, copiedNested.getConstantPool());
+ assertSame(otherPool,
copiedNested.getElementValuePairs()[0].getValue().getConstantPool());
+ assertEquals(visible, copiedNested.isRuntimeVisible());
+ ((SimpleElementValue)
array.getElementValuesArray()[0]).setIndex(second);
+ ((SimpleElementValue)
copiedNested.getElementValuePairs()[0].getValue()).setIndex(second);
+ assertEquals("second",
array.getElementValuesArray()[0].stringifyValue());
+ assertEquals("second",
copiedNested.getElementValuePairs()[0].getValue().stringifyValue());
+ assertArrayEquals(expected, dump(original), "Mutating nested copied
values must not change the original");
+ }
+
+ @Test
+ void wrapsCopyIoFailures() {
+ final IOException failure = new IOException("Cannot encode the entry");
+ final TypeAnnotationEntry entry = new TypeAnnotationEntry(0x13, new
byte[0], new byte[0], null) {
+ @Override
+ public void dump(final DataOutputStream out) throws IOException {
+ throw failure;
+ }
+ };
+ assertSame(failure, assertThrows(UncheckedIOException.class, () ->
entry.copy(constantPool())).getCause());
+ }
+}
diff --git a/src/test/java/org/apache/bcel/classfile/TypeAnnotationsTest.java
b/src/test/java/org/apache/bcel/classfile/TypeAnnotationsTest.java
index 53f3031c..bda0a3ce 100644
--- a/src/test/java/org/apache/bcel/classfile/TypeAnnotationsTest.java
+++ b/src/test/java/org/apache/bcel/classfile/TypeAnnotationsTest.java
@@ -34,11 +34,19 @@ import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
+import java.io.InputStream;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.nio.file.Files;
+import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
import org.apache.bcel.Const;
+import org.apache.bcel.generic.InstructionList;
import org.apache.bcel.verifier.statics.StringRepresentation;
import org.junit.jupiter.api.Test;
@@ -47,8 +55,41 @@ import org.junit.jupiter.api.Test;
*/
class TypeAnnotationsTest {
+ @Target(ElementType.TYPE_USE)
+ @Retention(RetentionPolicy.RUNTIME)
+ @interface TypeUse {
+ }
+
+ static class Outer {
+ class Inner {
+ }
+
+ static class Nested {
+ }
+ }
+
+ static class AnnotatedTypes {
+ @TypeUse Outer.Inner outer;
+ Outer.@TypeUse Inner inner;
+ Outer.@TypeUse Nested nested;
+ List<? extends @TypeUse Number[]> wildcard;
+
+ String cast(final Object value) {
+ @TypeUse final String local = (@TypeUse String) value;
+ return local;
+ }
+ }
+
private static final String CLASS_FILE =
"src/test/resources/typeannotations/TypeAnnotated.class";
+ private static JavaClass annotatedTypes() throws IOException {
+ final String resource = "TypeAnnotationsTest$AnnotatedTypes.class";
+ try (InputStream input =
TypeAnnotationsTest.class.getResourceAsStream(resource)) {
+ assertNotNull(input);
+ return new ClassParser(input, resource).parse();
+ }
+ }
+
private static JavaClass parse() throws IOException {
return new ClassParser(CLASS_FILE).parse();
}
@@ -134,6 +175,7 @@ class TypeAnnotationsTest {
try (DataOutputStream dos = new DataOutputStream(out)) {
clazz.dump(dos);
}
+ assertArrayEquals(Files.readAllBytes(Paths.get(CLASS_FILE)),
out.toByteArray());
final JavaClass reread = new ClassParser(new
ByteArrayInputStream(out.toByteArray()), CLASS_FILE).parse();
for (final String name : new String[] {"field", "items", "hidden"}) {
final Field original = field(clazz, name);
@@ -275,4 +317,78 @@ class TypeAnnotationsTest {
() -> new
TypeAnnotationEntry(TypeAnnotationEntry.TARGET_LOCAL_VARIABLE,
twoRangesAnnouncedAsOne, new byte[0], annotation));
assertEquals(2, new
TypeAnnotationEntry(TypeAnnotationEntry.TARGET_LOCAL_VARIABLE, new byte[2], new
byte[0], annotation).getTargetInfo().length);
}
+
+ @Test
+ void distinguishesInnerClassesFromStaticNestedClasses() throws IOException
{
+ final JavaClass clazz = annotatedTypes();
+ final TypeAnnotationEntry outer = typeAnnotationsOf(field(clazz,
"outer"),
Const.ATTR_RUNTIME_VISIBLE_TYPE_ANNOTATIONS).getTypeAnnotationEntries()[0];
+ final TypeAnnotationEntry inner = typeAnnotationsOf(field(clazz,
"inner"),
Const.ATTR_RUNTIME_VISIBLE_TYPE_ANNOTATIONS).getTypeAnnotationEntries()[0];
+ final TypeAnnotationEntry nested = typeAnnotationsOf(field(clazz,
"nested"),
Const.ATTR_RUNTIME_VISIBLE_TYPE_ANNOTATIONS).getTypeAnnotationEntries()[0];
+ assertEquals(0, outer.getTypePathLength());
+ assertEquals(1, inner.getTypePathLength());
+ assertEquals(1, inner.getTypePathKind(0));
+ assertEquals(0, inner.getTypePathArgumentIndex(0));
+ assertEquals(0, nested.getTypePathLength());
+ }
+
+ @Test
+ void followsTypeArgumentsWildcardBoundsAndArrayElements() throws
IOException {
+ final TypeAnnotationEntry entry =
typeAnnotationsOf(field(annotatedTypes(), "wildcard"),
Const.ATTR_RUNTIME_VISIBLE_TYPE_ANNOTATIONS)
+ .getTypeAnnotationEntries()[0];
+ assertEquals(3, entry.getTypePathLength());
+ assertEquals(3, entry.getTypePathKind(0));
+ assertEquals(2, entry.getTypePathKind(1));
+ assertEquals(0, entry.getTypePathKind(2));
+ for (int step = 0; step < 3; step++) {
+ assertEquals(0, entry.getTypePathArgumentIndex(step));
+ }
+ }
+
+ @Test
+ void visitsCompilerGeneratedCodeAnnotations() throws IOException {
+ final JavaClass clazz = annotatedTypes();
+ final Code code = method(clazz, "cast").getCode();
+ final TypeAnnotations annotations = (TypeAnnotations)
Stream.of(code.getAttributes()).filter(TypeAnnotations.class::isInstance).findFirst()
+ .orElseThrow(AssertionError::new);
+ assertEquals(2, annotations.getNumTypeAnnotations());
+ final List<Integer> targets = new ArrayList<>();
+ annotations.forEach(entry -> targets.add(entry.getTargetType()));
+
assertTrue(targets.contains(TypeAnnotationEntry.TARGET_LOCAL_VARIABLE));
+ assertTrue(targets.contains(TypeAnnotationEntry.TARGET_CAST));
+ for (final TypeAnnotationEntry entry : annotations) {
+ try (DataInputStream info = new DataInputStream(new
ByteArrayInputStream(entry.getTargetInfo()))) {
+ if (entry.getTargetType() == TypeAnnotationEntry.TARGET_CAST) {
+ final int offset = info.readUnsignedShort();
+ assertTrue(offset < code.getCode().length);
+ // javac 8 points to the operand load here, while newer
compilers can point to checkcast.
+ assertNotNull(new
InstructionList(code.getCode()).findHandle(offset), "The cast annotation must
point to an instruction boundary");
+ assertEquals(0, info.readUnsignedByte()); // first type in
the cast
+ } else {
+ final int ranges = info.readUnsignedShort();
+ assertTrue(ranges > 0);
+ for (int range = 0; range < ranges; range++) {
+ final int start = info.readUnsignedShort();
+ final int length = info.readUnsignedShort();
+ assertTrue(start < code.getCode().length);
+ assertTrue(start + length <= code.getCode().length);
+ assertTrue(info.readUnsignedShort() <
code.getMaxLocals());
+ }
+ }
+ assertEquals(0, info.available());
+ }
+ }
+ final int[] entries = {0};
+ final DescendingVisitor[] descending = new DescendingVisitor[1];
+ descending[0] = new DescendingVisitor(clazz, new EmptyVisitor() {
+ @Override
+ public void visitAnnotationEntry(final AnnotationEntry entry) {
+ if (descending[0].predecessor() == annotations) {
+ assertSame(code, descending[0].predecessor(1));
+ entries[0]++;
+ }
+ }
+ });
+ descending[0].visit();
+ assertEquals(2, entries[0]);
+ }
}