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 bee93773 Add support for the RuntimeVisibleTypeAnnotations and 
RuntimeInvisibleTypeAnnotations attributes (#539)
bee93773 is described below

commit bee9377326ab54c97a9e40504b2c0eada8abc23b
Author: Nicolas Baumann <[email protected]>
AuthorDate: Thu Oct 22 23:01:23 2026 +0200

    Add support for the RuntimeVisibleTypeAnnotations and 
RuntimeInvisibleTypeAnnotations attributes (#539)
    
    * Add support for the RuntimeVisibleTypeAnnotations and 
RuntimeInvisibleTypeAnnotations attributes
    
    Both attributes (JVMS 4.7.20) were returned as Unknown, so a caller had to 
parse
    the raw bytes (target_type/target_info, type_path, annotation) to know about
    annotations such as JSpecify's @Nullable, which are type annotations.
    
    - Const: ATTR_RUNTIME_VISIBLE_TYPE_ANNOTATIONS, 
ATTR_RUNTIME_INVISIBLE_TYPE_ANNOTATIONS.
    - TypeAnnotations (base class), RuntimeVisibleTypeAnnotations and
      RuntimeInvisibleTypeAnnotations, TypeAnnotationEntry (target type and 
info,
      type path, AnnotationEntry); read and dump round trip.
    - Visitor.visitTypeAnnotations (default method), DescendingVisitor,
      EmptyVisitor, StringRepresentation.
    
    Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
    
    * Deep copy of the type annotation attributes, traversal of their 
annotations, validation of TypeAnnotationEntry, Javadoc
    
    - copy(ConstantPool) of RuntimeVisibleTypeAnnotations and 
RuntimeInvisibleTypeAnnotations
      copies the entries and binds them and the attribute to the given constant 
pool.
    - DescendingVisitor.visitTypeAnnotations visits the AnnotationEntry of each 
entry.
    - The TypeAnnotationEntry constructor rejects a type path which is not a 
sequence of
      pairs of bytes of at most 255 steps, and a target info whose size does 
not match
      its target type.
    - Javadoc: empty type path, TARGET_CLASS_EXTENDS, scope of the attributes.
    
    Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
    
    ---------
    
    Co-authored-by: Nicolas Baumann 
<[email protected]>
    Co-authored-by: Claude Sonnet 5.5 <[email protected]>
---
 src/changes/changes.xml                            |   1 +
 src/main/java/org/apache/bcel/Const.java           |  18 +-
 .../java/org/apache/bcel/classfile/Attribute.java  |   4 +
 .../apache/bcel/classfile/DescendingVisitor.java   |   9 +
 .../org/apache/bcel/classfile/EmptyVisitor.java    |   5 +
 .../classfile/RuntimeInvisibleTypeAnnotations.java |  66 +++++
 .../classfile/RuntimeVisibleTypeAnnotations.java   |  66 +++++
 .../apache/bcel/classfile/TypeAnnotationEntry.java | 330 +++++++++++++++++++++
 .../org/apache/bcel/classfile/TypeAnnotations.java | 169 +++++++++++
 .../java/org/apache/bcel/classfile/Visitor.java    |  10 +
 .../verifier/statics/StringRepresentation.java     |  11 +
 .../apache/bcel/classfile/TypeAnnotationsTest.java | 278 +++++++++++++++++
 .../resources/typeannotations/TypeAnnotated.class  | Bin 0 -> 709 bytes
 .../resources/typeannotations/TypeAnnotated.java   |  34 +++
 14 files changed, 999 insertions(+), 2 deletions(-)

diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 26394405..49ba1c0f 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -68,6 +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>
       <!-- 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/Const.java 
b/src/main/java/org/apache/bcel/Const.java
index 8eb6f492..5b7ad009 100644
--- a/src/main/java/org/apache/bcel/Const.java
+++ b/src/main/java/org/apache/bcel/Const.java
@@ -3207,14 +3207,28 @@ public final class Const {
      */
     public static final byte ATTR_PERMITTED_SUBCLASSES = 28;
 
+    /**
+     * Attribute constant for RuntimeVisibleTypeAnnotations.
+     *
+     * @since 6.14.0
+     */
+    public static final byte ATTR_RUNTIME_VISIBLE_TYPE_ANNOTATIONS = 29;
+
+    /**
+     * Attribute constant for RuntimeInvisibleTypeAnnotations.
+     *
+     * @since 6.14.0
+     */
+    public static final byte ATTR_RUNTIME_INVISIBLE_TYPE_ANNOTATIONS = 30;
+
     /** Count of known attributes. */
-    public static final short KNOWN_ATTRIBUTES = 29; // count of attributes
+    public static final short KNOWN_ATTRIBUTES = 31; // count of attributes
 
     private static final String[] ATTRIBUTE_NAMES = { "SourceFile", 
"ConstantValue", "Code", "Exceptions", "LineNumberTable", "LocalVariableTable",
             "InnerClasses", "Synthetic", "Deprecated", "PMGClass", 
"Signature", "StackMap", "RuntimeVisibleAnnotations", 
"RuntimeInvisibleAnnotations",
             "RuntimeVisibleParameterAnnotations", 
"RuntimeInvisibleParameterAnnotations", "AnnotationDefault", 
"LocalVariableTypeTable", "EnclosingMethod",
             "StackMapTable", "BootstrapMethods", "MethodParameters", "Module", 
"ModulePackages", "ModuleMainClass", "NestHost", "NestMembers", "Record",
-            "PermittedSubclasses" };
+            "PermittedSubclasses", "RuntimeVisibleTypeAnnotations", 
"RuntimeInvisibleTypeAnnotations" };
 
     /**
      * Constants used in the StackMap attribute.
diff --git a/src/main/java/org/apache/bcel/classfile/Attribute.java 
b/src/main/java/org/apache/bcel/classfile/Attribute.java
index 8fb93646..526a923c 100644
--- a/src/main/java/org/apache/bcel/classfile/Attribute.java
+++ b/src/main/java/org/apache/bcel/classfile/Attribute.java
@@ -259,6 +259,10 @@ public abstract class Attribute implements Cloneable, Node 
{
             return new Record(nameIndex, length, dataInput, constantPool);
         case Const.ATTR_PERMITTED_SUBCLASSES:
             return new PermittedSubclasses(nameIndex, length, dataInput, 
constantPool);
+        case Const.ATTR_RUNTIME_VISIBLE_TYPE_ANNOTATIONS:
+            return new RuntimeVisibleTypeAnnotations(nameIndex, length, 
dataInput, constantPool);
+        case Const.ATTR_RUNTIME_INVISIBLE_TYPE_ANNOTATIONS:
+            return new RuntimeInvisibleTypeAnnotations(nameIndex, length, 
dataInput, constantPool);
         default:
             // Never reached
             throw new IllegalStateException("Unrecognized attribute type tag 
parsed: " + tag);
diff --git a/src/main/java/org/apache/bcel/classfile/DescendingVisitor.java 
b/src/main/java/org/apache/bcel/classfile/DescendingVisitor.java
index 816bf091..b1e49a48 100644
--- a/src/main/java/org/apache/bcel/classfile/DescendingVisitor.java
+++ b/src/main/java/org/apache/bcel/classfile/DescendingVisitor.java
@@ -537,6 +537,15 @@ public class DescendingVisitor implements Visitor {
         stack.pop();
     }
 
+    /** @since 6.14.0 */
+    @Override
+    public void visitTypeAnnotations(final TypeAnnotations obj) {
+        stack.push(obj);
+        obj.accept(visitor);
+        
accept(Stream.of(obj.getTypeAnnotationEntries()).map(TypeAnnotationEntry::getAnnotationEntry).toArray(AnnotationEntry[]::new));
+        stack.pop();
+    }
+
     @Override
     public void visitSignature(final Signature attribute) {
         stack.push(attribute);
diff --git a/src/main/java/org/apache/bcel/classfile/EmptyVisitor.java 
b/src/main/java/org/apache/bcel/classfile/EmptyVisitor.java
index d8f315cf..a34449e3 100644
--- a/src/main/java/org/apache/bcel/classfile/EmptyVisitor.java
+++ b/src/main/java/org/apache/bcel/classfile/EmptyVisitor.java
@@ -303,6 +303,11 @@ public class EmptyVisitor implements Visitor {
     public void visitPermittedSubclasses(final PermittedSubclasses obj) {
     }
 
+    /** @since 6.14.0 */
+    @Override
+    public void visitTypeAnnotations(final TypeAnnotations obj) {
+    }
+
     @Override
     public void visitSignature(final Signature obj) {
     }
diff --git 
a/src/main/java/org/apache/bcel/classfile/RuntimeInvisibleTypeAnnotations.java 
b/src/main/java/org/apache/bcel/classfile/RuntimeInvisibleTypeAnnotations.java
new file mode 100644
index 00000000..f772ae41
--- /dev/null
+++ 
b/src/main/java/org/apache/bcel/classfile/RuntimeInvisibleTypeAnnotations.java
@@ -0,0 +1,66 @@
+/*
+ * 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 java.io.DataInput;
+import java.io.DataOutputStream;
+import java.io.IOException;
+
+import org.apache.bcel.Const;
+
+/**
+ * The type annotations of a class, field, method, record component or {@link 
Code} attribute, recorded in the class file but not visible at runtime.
+ *
+ * @since 6.14.0
+ */
+public class RuntimeInvisibleTypeAnnotations extends TypeAnnotations {
+
+    /**
+     * Constructs a new instance.
+     *
+     * @param nameIndex Index pointing to the name of the attribute.
+     * @param length Content length in bytes.
+     * @param input Input stream.
+     * @param constantPool Array of constants.
+     * @throws IOException Thrown when an I/O exception of some sort has 
occurred.
+     */
+    public RuntimeInvisibleTypeAnnotations(final int nameIndex, final int 
length, final DataInput input, final ConstantPool constantPool) throws 
IOException {
+        super(Const.ATTR_RUNTIME_INVISIBLE_TYPE_ANNOTATIONS, nameIndex, 
length, input, constantPool, false);
+    }
+
+    /**
+     * Creates a deep copy of this attribute, whose type annotations are 
independent of this one's and refer to the given constant pool.
+     *
+     * @param constantPool The constant pool of the copy.
+     * @return deep copy of this attribute.
+     */
+    @Override
+    public Attribute copy(final ConstantPool constantPool) {
+        final RuntimeInvisibleTypeAnnotations c = 
(RuntimeInvisibleTypeAnnotations) clone();
+        c.setConstantPool(constantPool);
+        c.setTypeAnnotationTable(copyTypeAnnotations(constantPool));
+        return c;
+    }
+
+    @Override
+    public final void dump(final DataOutputStream dos) throws IOException {
+        super.dump(dos);
+        writeTypeAnnotations(dos);
+    }
+}
diff --git 
a/src/main/java/org/apache/bcel/classfile/RuntimeVisibleTypeAnnotations.java 
b/src/main/java/org/apache/bcel/classfile/RuntimeVisibleTypeAnnotations.java
new file mode 100644
index 00000000..7db33f01
--- /dev/null
+++ b/src/main/java/org/apache/bcel/classfile/RuntimeVisibleTypeAnnotations.java
@@ -0,0 +1,66 @@
+/*
+ * 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 java.io.DataInput;
+import java.io.DataOutputStream;
+import java.io.IOException;
+
+import org.apache.bcel.Const;
+
+/**
+ * The type annotations of a class, field, method, record component or {@link 
Code} attribute, provided to the JVM and visible at runtime.
+ *
+ * @since 6.14.0
+ */
+public class RuntimeVisibleTypeAnnotations extends TypeAnnotations {
+
+    /**
+     * Constructs a new instance.
+     *
+     * @param nameIndex Index pointing to the name of the attribute.
+     * @param length Content length in bytes.
+     * @param input Input stream.
+     * @param constantPool Array of constants.
+     * @throws IOException Thrown when an I/O exception of some sort has 
occurred.
+     */
+    public RuntimeVisibleTypeAnnotations(final int nameIndex, final int 
length, final DataInput input, final ConstantPool constantPool) throws 
IOException {
+        super(Const.ATTR_RUNTIME_VISIBLE_TYPE_ANNOTATIONS, nameIndex, length, 
input, constantPool, true);
+    }
+
+    /**
+     * Creates a deep copy of this attribute, whose type annotations are 
independent of this one's and refer to the given constant pool.
+     *
+     * @param constantPool The constant pool of the copy.
+     * @return deep copy of this attribute.
+     */
+    @Override
+    public Attribute copy(final ConstantPool constantPool) {
+        final RuntimeVisibleTypeAnnotations c = 
(RuntimeVisibleTypeAnnotations) clone();
+        c.setConstantPool(constantPool);
+        c.setTypeAnnotationTable(copyTypeAnnotations(constantPool));
+        return c;
+    }
+
+    @Override
+    public final void dump(final DataOutputStream dos) throws IOException {
+        super.dump(dos);
+        writeTypeAnnotations(dos);
+    }
+}
diff --git a/src/main/java/org/apache/bcel/classfile/TypeAnnotationEntry.java 
b/src/main/java/org/apache/bcel/classfile/TypeAnnotationEntry.java
new file mode 100644
index 00000000..f4145b3d
--- /dev/null
+++ b/src/main/java/org/apache/bcel/classfile/TypeAnnotationEntry.java
@@ -0,0 +1,330 @@
+/*
+ * 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 java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInput;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.util.Arrays;
+
+import org.apache.bcel.util.Args;
+
+/**
+ * Represents one type annotation of a {@link TypeAnnotations} attribute (JVMS 
4.7.20): an annotation, the kind of program element it applies to
+ * (the target type and its target info), and the path to the annotated type 
inside that element.
+ *
+ * @see <a 
href="https://docs.oracle.com/javase/specs/jvms/se21/html/jvms-4.html#jvms-4.7.20";>JVMS
 4.7.20 The RuntimeVisibleTypeAnnotations attribute</a>
+ * @since 6.14.0
+ */
+public class TypeAnnotationEntry {
+
+    /** Target type: type parameter declaration of a generic class or 
interface. */
+    public static final int TARGET_CLASS_TYPE_PARAMETER = 0x00;
+
+    /** Target type: type parameter declaration of a generic method or 
constructor. */
+    public static final int TARGET_METHOD_TYPE_PARAMETER = 0x01;
+
+    /** Target type: type in an extends or implements clause of a class or 
interface declaration. */
+    public static final int TARGET_CLASS_EXTENDS = 0x10;
+
+    /** Target type: bound of a type parameter of a generic class or 
interface. */
+    public static final int TARGET_CLASS_TYPE_PARAMETER_BOUND = 0x11;
+
+    /** Target type: bound of a type parameter of a generic method or 
constructor. */
+    public static final int TARGET_METHOD_TYPE_PARAMETER_BOUND = 0x12;
+
+    /** Target type: type in a field or record component declaration. */
+    public static final int TARGET_FIELD = 0x13;
+
+    /** Target type: return type of a method, or type of a newly constructed 
object. */
+    public static final int TARGET_METHOD_RETURN = 0x14;
+
+    /** Target type: receiver type of a method or constructor. */
+    public static final int TARGET_METHOD_RECEIVER = 0x15;
+
+    /** Target type: type in a formal parameter declaration of a method, 
constructor, or lambda expression. */
+    public static final int TARGET_METHOD_FORMAL_PARAMETER = 0x16;
+
+    /** Target type: type in a throws clause of a method or constructor. */
+    public static final int TARGET_THROWS = 0x17;
+
+    /** Target type: type in a local variable declaration. */
+    public static final int TARGET_LOCAL_VARIABLE = 0x40;
+
+    /** Target type: type in a resource variable declaration. */
+    public static final int TARGET_RESOURCE_VARIABLE = 0x41;
+
+    /** Target type: type in an exception parameter declaration. */
+    public static final int TARGET_EXCEPTION_PARAMETER = 0x42;
+
+    /** Target type: type in an instanceof expression. */
+    public static final int TARGET_INSTANCEOF = 0x43;
+
+    /** Target type: type in a new expression. */
+    public static final int TARGET_NEW = 0x44;
+
+    /** Target type: type in a method reference expression using ::new. */
+    public static final int TARGET_CONSTRUCTOR_REFERENCE = 0x45;
+
+    /** Target type: type in a method reference expression using ::Identifier. 
*/
+    public static final int TARGET_METHOD_REFERENCE = 0x46;
+
+    /** Target type: type in a cast expression. */
+    public static final int TARGET_CAST = 0x47;
+
+    /** Target type: type argument for a generic constructor in a new 
expression or an explicit constructor invocation. */
+    public static final int TARGET_CONSTRUCTOR_INVOCATION_TYPE_ARGUMENT = 0x48;
+
+    /** Target type: type argument for a generic method in a method invocation 
expression. */
+    public static final int TARGET_METHOD_INVOCATION_TYPE_ARGUMENT = 0x49;
+
+    /** Target type: type argument for a generic constructor in a method 
reference expression using ::new. */
+    public static final int TARGET_CONSTRUCTOR_REFERENCE_TYPE_ARGUMENT = 0x4A;
+
+    /** Target type: type argument for a generic method in a method reference 
expression using ::Identifier. */
+    public static final int TARGET_METHOD_REFERENCE_TYPE_ARGUMENT = 0x4B;
+
+    /**
+     * Empty array of TypeAnnotationEntry objects.
+     */
+    public static final TypeAnnotationEntry[] EMPTY_ARRAY = {};
+
+    /** Maximum number of steps of a type path, which its length of one byte 
limits. */
+    private static final int MAX_TYPE_PATH_LENGTH = 255;
+
+    /** Number of bytes of a {@code localvar_target} entry: start_pc, length 
and index. */
+    private static final int LOCAL_VARIABLE_RANGE_SIZE = 6;
+
+    /**
+     * Gets the number of bytes of the target info that follows a target type.
+     *
+     * @param targetType The target type.
+     * @return the size of the target info, or -1 if it depends on its content 
(the table of a {@code localvar_target}).
+     * @throws ClassFormatException if the target type is not defined.
+     */
+    private static int targetInfoSize(final int targetType) {
+        switch (targetType) {
+        case TARGET_CLASS_TYPE_PARAMETER:
+        case TARGET_METHOD_TYPE_PARAMETER:
+        case TARGET_METHOD_FORMAL_PARAMETER:
+            return 1;
+        case TARGET_CLASS_EXTENDS:
+        case TARGET_CLASS_TYPE_PARAMETER_BOUND:
+        case TARGET_METHOD_TYPE_PARAMETER_BOUND:
+        case TARGET_THROWS:
+        case TARGET_EXCEPTION_PARAMETER:
+        case TARGET_INSTANCEOF:
+        case TARGET_NEW:
+        case TARGET_CONSTRUCTOR_REFERENCE:
+        case TARGET_METHOD_REFERENCE:
+            return 2;
+        case TARGET_FIELD:
+        case TARGET_METHOD_RETURN:
+        case TARGET_METHOD_RECEIVER:
+            return 0;
+        case TARGET_LOCAL_VARIABLE:
+        case TARGET_RESOURCE_VARIABLE:
+            return -1;
+        case TARGET_CAST:
+        case TARGET_CONSTRUCTOR_INVOCATION_TYPE_ARGUMENT:
+        case TARGET_METHOD_INVOCATION_TYPE_ARGUMENT:
+        case TARGET_CONSTRUCTOR_REFERENCE_TYPE_ARGUMENT:
+        case TARGET_METHOD_REFERENCE_TYPE_ARGUMENT:
+            return 3;
+        default:
+            throw new ClassFormatException("Invalid type annotation target 
type: 0x" + Integer.toHexString(targetType));
+        }
+    }
+
+    private static void checkTargetInfo(final int targetType, final byte[] 
targetInfo) {
+        final int size = targetInfoSize(targetType);
+        if (size >= 0) {
+            if (targetInfo.length != size) {
+                throw new IllegalArgumentException("The target info of the 
target type 0x" + Integer.toHexString(targetType) + " must have " + size
+                        + " bytes, not " + targetInfo.length);
+            }
+        } else if (targetInfo.length < 2 || targetInfo.length != 2 + 
((targetInfo[0] & 0xFF) << 8 | targetInfo[1] & 0xFF) * 
LOCAL_VARIABLE_RANGE_SIZE) {
+            throw new IllegalArgumentException("The target info of the target 
type 0x" + Integer.toHexString(targetType)
+                    + " must be a length followed by that many ranges of 6 
bytes");
+        }
+    }
+
+    /**
+     * Factory method to create a TypeAnnotationEntry from a DataInput.
+     *
+     * @param input The input stream.
+     * @param constantPool The constant pool.
+     * @param isRuntimeVisible whether the annotation is runtime visible.
+     * @return The entry.
+     * @throws IOException Thrown if an I/O error occurs.
+     */
+    public static TypeAnnotationEntry read(final DataInput input, final 
ConstantPool constantPool, final boolean isRuntimeVisible) throws IOException {
+        final int targetType = input.readUnsignedByte();
+        final int size = targetInfoSize(targetType);
+        final byte[] targetInfo;
+        if (size >= 0) {
+            targetInfo = new byte[size];
+            input.readFully(targetInfo);
+        } else {
+            final int tableLength = input.readUnsignedShort();
+            targetInfo = new byte[2 + tableLength * LOCAL_VARIABLE_RANGE_SIZE];
+            targetInfo[0] = (byte) (tableLength >>> 8);
+            targetInfo[1] = (byte) tableLength;
+            input.readFully(targetInfo, 2, targetInfo.length - 2);
+        }
+        final byte[] typePath = new byte[input.readUnsignedByte() * 2];
+        input.readFully(typePath);
+        return new TypeAnnotationEntry(targetType, targetInfo, typePath, 
AnnotationEntry.read(input, constantPool, isRuntimeVisible));
+    }
+
+    private final int targetType;
+
+    private final byte[] targetInfo;
+
+    private final byte[] typePath;
+
+    private final AnnotationEntry annotationEntry;
+
+    /**
+     * Constructs a TypeAnnotationEntry.
+     *
+     * @param targetType The target type, one of the {@code TARGET_*} 
constants.
+     * @param targetInfo The target info, as it is stored in the class file.
+     * @param typePath The type path, as it is stored in the class file 
without its length: a pair of bytes (path kind, type argument index) per step.
+     * @param annotationEntry The annotation.
+     * @throws ClassFormatException if the target type is not defined.
+     * @throws IllegalArgumentException if the target info does not have the 
size its target type requires, or if the type path is not a sequence of pairs
+     *         of bytes of at most 255 steps.
+     */
+    public TypeAnnotationEntry(final int targetType, final byte[] targetInfo, 
final byte[] typePath, final AnnotationEntry annotationEntry) {
+        checkTargetInfo(targetType, targetInfo);
+        if (typePath.length % 2 != 0 || typePath.length > MAX_TYPE_PATH_LENGTH 
* 2) {
+            throw new IllegalArgumentException("The type path must be at most 
" + MAX_TYPE_PATH_LENGTH + " pairs of bytes (kind, argument index), not "
+                    + typePath.length + " bytes");
+        }
+        this.targetType = targetType;
+        this.targetInfo = targetInfo.clone();
+        this.typePath = typePath.clone();
+        this.annotationEntry = annotationEntry;
+    }
+
+    /**
+     * Creates a deep copy of this entry, whose annotation is independent of 
this one and refers to the given constant pool. The indexes it holds are kept, 
as they
+     * are in {@link Attribute#copy(ConstantPool)}: the constant pool must 
have the same constants as the one of this entry at these indexes.
+     *
+     * @param constantPool The constant pool of the copy.
+     * @return A deep copy of this entry.
+     */
+    public TypeAnnotationEntry copy(final ConstantPool constantPool) {
+        final ByteArrayOutputStream out = new ByteArrayOutputStream();
+        try {
+            try (DataOutputStream dos = new DataOutputStream(out)) {
+                dump(dos);
+            }
+            return read(new DataInputStream(new 
ByteArrayInputStream(out.toByteArray())), constantPool, 
annotationEntry.isRuntimeVisible());
+        } catch (final IOException e) {
+            throw new UncheckedIOException(e);
+        }
+    }
+
+    /**
+     * Writes this entry to a DataOutputStream.
+     *
+     * @param dos The data output stream.
+     * @throws IOException Thrown if an I/O error occurs.
+     */
+    public void dump(final DataOutputStream dos) throws IOException {
+        dos.writeByte(targetType);
+        dos.write(targetInfo);
+        dos.writeByte(Args.requireU1(typePath.length / 2, "typePath.length"));
+        dos.write(typePath);
+        annotationEntry.dump(dos);
+    }
+
+    /**
+     * Gets the annotation.
+     *
+     * @return The annotation.
+     */
+    public AnnotationEntry getAnnotationEntry() {
+        return annotationEntry;
+    }
+
+    /**
+     * Gets the target info, which depends on the target type (JVMS table 
4.7.20-A): for instance the index of the type parameter for
+     * {@link #TARGET_CLASS_TYPE_PARAMETER}, the index of the type parameter 
then the index of the bound for {@link #TARGET_CLASS_TYPE_PARAMETER_BOUND}, or 
the
+     * index of the parameter for {@link #TARGET_METHOD_FORMAL_PARAMETER}.
+     *
+     * @return A copy of the target info, as it is stored in the class file.
+     */
+    public byte[] getTargetInfo() {
+        return targetInfo.clone();
+    }
+
+    /**
+     * Gets the kind of program element the annotation applies to.
+     *
+     * @return One of the {@code TARGET_*} constants.
+     */
+    public int getTargetType() {
+        return targetType;
+    }
+
+    /**
+     * 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.
+     *
+     * @return The number of steps of the type path.
+     */
+    public int getTypePathLength() {
+        return typePath.length / 2;
+    }
+
+    /**
+     * Gets the kind of a step of the type path: 0 deeper in an array type, 1 
deeper in a nested type, 2 on the bound of a wildcard type argument, 3 on a type
+     * argument of a parameterized type.
+     *
+     * @param step Index of the step, from 0 to {@link #getTypePathLength()} 
excluded.
+     * @return The kind of the step.
+     */
+    public int getTypePathKind(final int step) {
+        return typePath[step * 2] & 0xFF;
+    }
+
+    /**
+     * Gets the index of the type argument for a step of the type path of kind 
3, else 0.
+     *
+     * @param step Index of the step, from 0 to {@link #getTypePathLength()} 
excluded.
+     * @return The index of the type argument.
+     */
+    public int getTypePathArgumentIndex(final int step) {
+        return typePath[step * 2 + 1] & 0xFF;
+    }
+
+    @Override
+    public String toString() {
+        return "TypeAnnotationEntry(target=0x" + 
Integer.toHexString(targetType) + ", info=" + Arrays.toString(targetInfo) + ", 
path=" + Arrays.toString(typePath)
+                + ", " + annotationEntry + ")";
+    }
+}
diff --git a/src/main/java/org/apache/bcel/classfile/TypeAnnotations.java 
b/src/main/java/org/apache/bcel/classfile/TypeAnnotations.java
new file mode 100644
index 00000000..aa13deaf
--- /dev/null
+++ b/src/main/java/org/apache/bcel/classfile/TypeAnnotations.java
@@ -0,0 +1,169 @@
+/*
+ * 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 java.io.DataInput;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.util.Iterator;
+import java.util.stream.Stream;
+
+import org.apache.bcel.Const;
+import org.apache.bcel.util.Args;
+
+/**
+ * Base class for the type annotation attributes, {@link 
RuntimeVisibleTypeAnnotations} and {@link RuntimeInvisibleTypeAnnotations}: the 
annotations written on
+ * the types used in a declaration (such as {@code List<@Nullable String>}, 
the supertypes of a class, a {@code throws} clause, or the declaration and the
+ * bounds of a type parameter as in {@code <T extends @Nullable Object>}) or 
in the code of a method (such as a cast, an {@code instanceof}, a {@code new}
+ * expression or a local variable), which the attributes of the declaration 
annotations do not hold. They are attributes of a class, a field, a method, a
+ * record component or a {@link Code} attribute.
+ *
+ * @see <a 
href="https://docs.oracle.com/javase/specs/jvms/se21/html/jvms-4.html#jvms-4.7.20";>JVMS
 4.7.20 The RuntimeVisibleTypeAnnotations attribute</a>
+ * @since 6.14.0
+ */
+public abstract class TypeAnnotations extends Attribute implements 
Iterable<TypeAnnotationEntry> {
+
+    private TypeAnnotationEntry[] typeAnnotationTable;
+    private final boolean isRuntimeVisible;
+
+    /**
+     * Constructs an instance.
+     *
+     * @param annotationType The subclass type of the attribute.
+     * @param nameIndex Index pointing to the name of the attribute.
+     * @param length Content length in bytes.
+     * @param typeAnnotationTable The actual type annotations.
+     * @param constantPool Array of constants.
+     * @param isRuntimeVisible whether the annotations are visible at runtime.
+     */
+    public TypeAnnotations(final byte annotationType, final int nameIndex, 
final int length, final TypeAnnotationEntry[] typeAnnotationTable,
+            final ConstantPool constantPool, final boolean isRuntimeVisible) {
+        super(annotationType, nameIndex, length, constantPool);
+        setTypeAnnotationTable(typeAnnotationTable);
+        this.isRuntimeVisible = isRuntimeVisible;
+    }
+
+    /**
+     * Constructs an instance.
+     *
+     * @param annotationType The subclass type of the attribute.
+     * @param nameIndex Index pointing to the name of the attribute.
+     * @param length Content length in bytes.
+     * @param input Input stream.
+     * @param constantPool Array of constants.
+     * @param isRuntimeVisible whether the annotations are visible at runtime.
+     * @throws IOException Thrown if an I/O error occurs.
+     */
+    TypeAnnotations(final byte annotationType, final int nameIndex, final int 
length, final DataInput input, final ConstantPool constantPool,
+            final boolean isRuntimeVisible) throws IOException {
+        this(annotationType, nameIndex, length, (TypeAnnotationEntry[]) null, 
constantPool, isRuntimeVisible);
+        final int tableLength = input.readUnsignedShort();
+        typeAnnotationTable = new TypeAnnotationEntry[tableLength];
+        for (int i = 0; i < tableLength; i++) {
+            typeAnnotationTable[i] = TypeAnnotationEntry.read(input, 
constantPool, isRuntimeVisible);
+        }
+    }
+
+    @Override
+    public void accept(final Visitor v) {
+        v.visitTypeAnnotations(this);
+    }
+
+    /**
+     * Creates deep copies of the type annotations, which refer to the given 
constant pool.
+     *
+     * @param constantPool The constant pool of the copies.
+     * @return The copies.
+     */
+    protected final TypeAnnotationEntry[] copyTypeAnnotations(final 
ConstantPool constantPool) {
+        return Stream.of(typeAnnotationTable).map(e -> 
e.copy(constantPool)).toArray(TypeAnnotationEntry[]::new);
+    }
+
+    /**
+     * Gets the number of type annotations.
+     *
+     * @return The number of type annotations.
+     */
+    public final int getNumTypeAnnotations() {
+        return typeAnnotationTable.length;
+    }
+
+    /**
+     * Gets the type annotations.
+     *
+     * @return The type annotations.
+     */
+    public TypeAnnotationEntry[] getTypeAnnotationEntries() {
+        return typeAnnotationTable;
+    }
+
+    /**
+     * Tests whether the annotations are visible at runtime.
+     *
+     * @return true if the annotations are runtime visible.
+     */
+    public boolean isRuntimeVisible() {
+        return isRuntimeVisible;
+    }
+
+    @Override
+    public Iterator<TypeAnnotationEntry> iterator() {
+        return Stream.of(typeAnnotationTable).iterator();
+    }
+
+    /**
+     * Sets the type annotations.
+     *
+     * @param typeAnnotationTable The type annotations.
+     */
+    public final void setTypeAnnotationTable(final TypeAnnotationEntry[] 
typeAnnotationTable) {
+        this.typeAnnotationTable = typeAnnotationTable != null ? 
typeAnnotationTable : TypeAnnotationEntry.EMPTY_ARRAY;
+    }
+
+    /**
+     * Converts to a String representation.
+     *
+     * @return String representation.
+     */
+    @Override
+    public final String toString() {
+        final StringBuilder buf = new 
StringBuilder(Const.getAttributeName(getTag()));
+        buf.append(":\n");
+        for (int i = 0; i < typeAnnotationTable.length; i++) {
+            buf.append("  ").append(typeAnnotationTable[i]);
+            if (i < typeAnnotationTable.length - 1) {
+                buf.append('\n');
+            }
+        }
+        return buf.toString();
+    }
+
+    /**
+     * Writes the type annotations to a DataOutputStream.
+     *
+     * @param dos The data output stream.
+     * @throws IOException Thrown if an I/O error occurs.
+     */
+    protected void writeTypeAnnotations(final DataOutputStream dos) throws 
IOException {
+        dos.writeShort(Args.requireU2(typeAnnotationTable.length, 
"typeAnnotationTable.length"));
+        for (final TypeAnnotationEntry element : typeAnnotationTable) {
+            element.dump(dos);
+        }
+    }
+}
diff --git a/src/main/java/org/apache/bcel/classfile/Visitor.java 
b/src/main/java/org/apache/bcel/classfile/Visitor.java
index 555bbfc3..2e644120 100644
--- a/src/main/java/org/apache/bcel/classfile/Visitor.java
+++ b/src/main/java/org/apache/bcel/classfile/Visitor.java
@@ -437,6 +437,16 @@ public interface Visitor {
         // empty
     }
 
+    /**
+     * Visits a RuntimeVisibleTypeAnnotations or 
RuntimeInvisibleTypeAnnotations attribute.
+     *
+     * @param obj The attribute.
+     * @since 6.14.0
+     */
+    default void visitTypeAnnotations(final TypeAnnotations obj) {
+        // empty
+    }
+
     /**
      * Visits a {@link Record} object.
      *
diff --git 
a/src/main/java/org/apache/bcel/verifier/statics/StringRepresentation.java 
b/src/main/java/org/apache/bcel/verifier/statics/StringRepresentation.java
index 0ba7801d..8819b4f0 100644
--- a/src/main/java/org/apache/bcel/verifier/statics/StringRepresentation.java
+++ b/src/main/java/org/apache/bcel/verifier/statics/StringRepresentation.java
@@ -69,6 +69,7 @@ import org.apache.bcel.classfile.SourceFile;
 import org.apache.bcel.classfile.StackMap;
 import org.apache.bcel.classfile.StackMapEntry;
 import org.apache.bcel.classfile.Synthetic;
+import org.apache.bcel.classfile.TypeAnnotations;
 import org.apache.bcel.classfile.Unknown;
 import org.apache.bcel.verifier.exc.AssertionViolatedException;
 
@@ -412,6 +413,16 @@ public class StringRepresentation extends 
org.apache.bcel.classfile.EmptyVisitor
         tostring = toString(obj);
     }
 
+    /**
+     * Visits RuntimeVisibleTypeAnnotations and 
RuntimeInvisibleTypeAnnotations attributes.
+     *
+     * @since 6.14.0
+     */
+    @Override
+    public void visitTypeAnnotations(final TypeAnnotations obj) {
+        tostring = toString(obj);
+    }
+
     @Override
     public void visitRecord(final Record obj) {
         tostring = toString(obj);
diff --git a/src/test/java/org/apache/bcel/classfile/TypeAnnotationsTest.java 
b/src/test/java/org/apache/bcel/classfile/TypeAnnotationsTest.java
new file mode 100644
index 00000000..53f3031c
--- /dev/null
+++ b/src/test/java/org/apache/bcel/classfile/TypeAnnotationsTest.java
@@ -0,0 +1,278 @@
+/*
+ * 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.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertNull;
+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.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Stream;
+
+import org.apache.bcel.Const;
+import org.apache.bcel.verifier.statics.StringRepresentation;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests the type annotation attributes, read from {@code TypeAnnotated.class} 
(see {@code TypeAnnotated.java}).
+ */
+class TypeAnnotationsTest {
+
+    private static final String CLASS_FILE = 
"src/test/resources/typeannotations/TypeAnnotated.class";
+
+    private static JavaClass parse() throws IOException {
+        return new ClassParser(CLASS_FILE).parse();
+    }
+
+    private static TypeAnnotations typeAnnotationsOf(final FieldOrMethod 
member, final byte tag) {
+        return (TypeAnnotations) Stream.of(member.getAttributes()).filter(a -> 
a.getTag() == tag).findFirst().orElse(null);
+    }
+
+    private static Method method(final JavaClass clazz, final String name) {
+        return Stream.of(clazz.getMethods()).filter(m -> 
m.getName().equals(name)).findFirst().orElseThrow(AssertionError::new);
+    }
+
+    private static Field field(final JavaClass clazz, final String name) {
+        return Stream.of(clazz.getFields()).filter(f -> 
f.getName().equals(name)).findFirst().orElseThrow(AssertionError::new);
+    }
+
+    @Test
+    void readsTheTypeAnnotationOfAField() throws IOException {
+        final TypeAnnotations annotations = typeAnnotationsOf(field(parse(), 
"field"), Const.ATTR_RUNTIME_VISIBLE_TYPE_ANNOTATIONS);
+        assertNotNull(annotations);
+        assertTrue(annotations instanceof RuntimeVisibleTypeAnnotations);
+        assertTrue(annotations.isRuntimeVisible());
+        assertEquals(1, annotations.getNumTypeAnnotations());
+        final TypeAnnotationEntry entry = 
annotations.getTypeAnnotationEntries()[0];
+        assertEquals(TypeAnnotationEntry.TARGET_FIELD, entry.getTargetType());
+        assertEquals(0, entry.getTargetInfo().length);
+        assertEquals(0, entry.getTypePathLength());
+        assertEquals("Ltypeannotations/Visible;", 
entry.getAnnotationEntry().getAnnotationType());
+    }
+
+    @Test
+    void readsTheTypePathOfATypeArgument() throws IOException {
+        final TypeAnnotations annotations = typeAnnotationsOf(field(parse(), 
"items"), Const.ATTR_RUNTIME_VISIBLE_TYPE_ANNOTATIONS);
+        final TypeAnnotationEntry entry = 
annotations.getTypeAnnotationEntries()[0];
+        assertEquals(TypeAnnotationEntry.TARGET_FIELD, entry.getTargetType());
+        assertEquals(1, entry.getTypePathLength());
+        // a type argument of the parameterized type, the first one
+        assertEquals(3, entry.getTypePathKind(0));
+        assertEquals(0, entry.getTypePathArgumentIndex(0));
+    }
+
+    @Test
+    void readsTheInvisibleAttribute() throws IOException {
+        final Field hidden = field(parse(), "hidden");
+        assertNull(typeAnnotationsOf(hidden, 
Const.ATTR_RUNTIME_VISIBLE_TYPE_ANNOTATIONS));
+        final TypeAnnotations annotations = typeAnnotationsOf(hidden, 
Const.ATTR_RUNTIME_INVISIBLE_TYPE_ANNOTATIONS);
+        assertTrue(annotations instanceof RuntimeInvisibleTypeAnnotations);
+        assertFalse(annotations.isRuntimeVisible());
+        assertEquals("Ltypeannotations/Invisible;", 
annotations.getTypeAnnotationEntries()[0].getAnnotationEntry().getAnnotationType());
+    }
+
+    @Test
+    void readsTheTargetsOfAMethod() throws IOException {
+        final TypeAnnotations annotations = typeAnnotationsOf(method(parse(), 
"find"), Const.ATTR_RUNTIME_VISIBLE_TYPE_ANNOTATIONS);
+        final List<Integer> targets = new ArrayList<>();
+        for (final TypeAnnotationEntry entry : annotations) {
+            targets.add(entry.getTargetType());
+            if (entry.getTargetType() == 
TypeAnnotationEntry.TARGET_METHOD_FORMAL_PARAMETER) {
+                // the index of the parameter
+                assertArrayEquals(new byte[] {0}, entry.getTargetInfo());
+            }
+        }
+        assertTrue(targets.contains(TypeAnnotationEntry.TARGET_METHOD_RETURN));
+        
assertTrue(targets.contains(TypeAnnotationEntry.TARGET_METHOD_FORMAL_PARAMETER));
+    }
+
+    @Test
+    void readsTheBoundOfATypeParameterOfTheClass() throws IOException {
+        final JavaClass clazz = parse();
+        final TypeAnnotations annotations = (TypeAnnotations) 
Stream.of(clazz.getAttributes())
+                .filter(a -> a.getTag() == 
Const.ATTR_RUNTIME_VISIBLE_TYPE_ANNOTATIONS).findFirst().orElse(null);
+        assertNotNull(annotations);
+        final TypeAnnotationEntry entry = 
annotations.getTypeAnnotationEntries()[0];
+        assertEquals(TypeAnnotationEntry.TARGET_CLASS_TYPE_PARAMETER_BOUND, 
entry.getTargetType());
+        // the first type parameter, its first bound
+        assertArrayEquals(new byte[] {0, 0}, entry.getTargetInfo());
+    }
+
+    @Test
+    void dumpsWhatItReads() throws IOException {
+        final JavaClass clazz = parse();
+        final ByteArrayOutputStream out = new ByteArrayOutputStream();
+        try (DataOutputStream dos = new DataOutputStream(out)) {
+            clazz.dump(dos);
+        }
+        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);
+            final Field copy = field(reread, name);
+            assertEquals(original.getAttributes().length, 
copy.getAttributes().length);
+            for (int i = 0; i < original.getAttributes().length; i++) {
+                assertEquals(original.getAttributes()[i].toString(), 
copy.getAttributes()[i].toString());
+            }
+        }
+    }
+
+    @Test
+    void copiesTheAttributeWithItsOwnAnnotationsAndTheGivenConstantPool() 
throws IOException {
+        final JavaClass clazz = parse();
+        final ConstantPool otherPool = clazz.getConstantPool().copy();
+        final TypeAnnotations original = typeAnnotationsOf(field(clazz, 
"field"), Const.ATTR_RUNTIME_VISIBLE_TYPE_ANNOTATIONS);
+        final TypeAnnotations copy = (TypeAnnotations) 
original.copy(otherPool);
+        assertEquals(original.toString(), copy.toString());
+        assertTrue(copy instanceof RuntimeVisibleTypeAnnotations);
+        assertTrue(copy.isRuntimeVisible());
+        // the copy is bound to the given pool, the original to its own
+        assertSame(otherPool, copy.getConstantPool());
+        assertSame(otherPool, 
copy.getTypeAnnotationEntries()[0].getAnnotationEntry().getConstantPool());
+        assertSame(clazz.getConstantPool(), original.getConstantPool());
+        assertSame(clazz.getConstantPool(), 
original.getTypeAnnotationEntries()[0].getAnnotationEntry().getConstantPool());
+        // nothing is shared: changing the copy leaves the original unchanged
+        assertNotSame(original.getTypeAnnotationEntries(), 
copy.getTypeAnnotationEntries());
+        assertNotSame(original.getTypeAnnotationEntries()[0], 
copy.getTypeAnnotationEntries()[0]);
+        
assertNotSame(original.getTypeAnnotationEntries()[0].getAnnotationEntry(), 
copy.getTypeAnnotationEntries()[0].getAnnotationEntry());
+        
copy.getTypeAnnotationEntries()[0].getAnnotationEntry().addElementNameValuePair(
+                new ElementValuePair(1, new 
SimpleElementValue(ElementValue.PRIMITIVE_INT, 1, otherPool), otherPool));
+        copy.setTypeAnnotationTable(null);
+        assertEquals(0, 
original.getTypeAnnotationEntries()[0].getAnnotationEntry().getNumElementValuePairs());
+        assertEquals(1, original.getNumTypeAnnotations());
+    }
+
+    @Test
+    void copiesTheInvisibleAttribute() throws IOException {
+        final JavaClass clazz = parse();
+        final TypeAnnotations original = typeAnnotationsOf(field(clazz, 
"hidden"), Const.ATTR_RUNTIME_INVISIBLE_TYPE_ANNOTATIONS);
+        final TypeAnnotations copy = (TypeAnnotations) 
original.copy(clazz.getConstantPool().copy());
+        assertTrue(copy instanceof RuntimeInvisibleTypeAnnotations);
+        assertFalse(copy.isRuntimeVisible());
+        
assertFalse(copy.getTypeAnnotationEntries()[0].getAnnotationEntry().isRuntimeVisible());
+        assertEquals(original.toString(), copy.toString());
+    }
+
+    @Test
+    void isVisitedWithItsAnnotations() throws IOException {
+        final JavaClass clazz = parse();
+        final List<String> events = new ArrayList<>();
+        final DescendingVisitor[] descending = new DescendingVisitor[1];
+        descending[0] = new DescendingVisitor(clazz, new EmptyVisitor() {
+            @Override
+            public void visitAnnotationEntry(final AnnotationEntry obj) {
+                // the annotation is visited within its attribute
+                events.add("entry " + obj.getAnnotationType() + " in " + 
descending[0].predecessor().getClass().getSimpleName());
+            }
+
+            @Override
+            public void visitTypeAnnotations(final TypeAnnotations obj) {
+                events.add("attribute " + obj.getClass().getSimpleName());
+            }
+        });
+        descending[0].visit();
+        // the class, three fields and one method have an attribute, which 
holds six annotations in all
+        assertEquals(5, events.stream().filter(e -> 
e.startsWith("attribute")).count());
+        assertEquals(6, events.stream().filter(e -> 
e.startsWith("entry")).count());
+        assertEquals(1, events.stream().filter(e -> e.equals("attribute 
RuntimeInvisibleTypeAnnotations")).count());
+        assertEquals(1, events.stream().filter(e -> e.equals("entry 
Ltypeannotations/Invisible; in RuntimeInvisibleTypeAnnotations")).count());
+        assertEquals(5, events.stream().filter(e -> e.startsWith("entry 
Ltypeannotations/Visible; in RuntimeVisibleTypeAnnotations")).count());
+    }
+
+    @Test
+    void isRepresented() throws IOException {
+        final JavaClass clazz = parse();
+        final StringRepresentation representation = new 
StringRepresentation(typeAnnotationsOf(field(clazz, "field"), 
Const.ATTR_RUNTIME_VISIBLE_TYPE_ANNOTATIONS));
+        
assertTrue(representation.toString().contains("RuntimeVisibleTypeAnnotations"), 
representation.toString());
+    }
+
+    @Test
+    void readsTheTableOfALocalVariableTarget() throws IOException {
+        final ByteArrayOutputStream out = new ByteArrayOutputStream();
+        try (DataOutputStream dos = new DataOutputStream(out)) {
+            dos.writeByte(TypeAnnotationEntry.TARGET_LOCAL_VARIABLE);
+            dos.writeShort(2);
+            for (int i = 0; i < 2; i++) {
+                dos.writeShort(1 + i);
+                dos.writeShort(5);
+                dos.writeShort(i);
+            }
+            dos.writeByte(0);
+            dos.writeShort(1);
+            dos.writeShort(0);
+        }
+        final ConstantPool constantPool = parse().getConstantPool();
+        final TypeAnnotationEntry entry = TypeAnnotationEntry.read(new 
DataInputStream(new ByteArrayInputStream(out.toByteArray())), constantPool, 
true);
+        assertEquals(TypeAnnotationEntry.TARGET_LOCAL_VARIABLE, 
entry.getTargetType());
+        assertEquals(2 + 2 * 6, entry.getTargetInfo().length);
+        final ByteArrayOutputStream dumped = new ByteArrayOutputStream();
+        try (DataOutputStream dos = new DataOutputStream(dumped)) {
+            entry.dump(dos);
+        }
+        assertArrayEquals(out.toByteArray(), dumped.toByteArray());
+    }
+
+    @Test
+    void rejectsAnUndefinedTargetType() throws IOException {
+        final ConstantPool constantPool = parse().getConstantPool();
+        final DataInputStream input = new DataInputStream(new 
ByteArrayInputStream(new byte[] {0x7F, 0, 0, 0}));
+        assertThrows(ClassFormatException.class, () -> 
TypeAnnotationEntry.read(input, constantPool, true));
+    }
+
+    @Test
+    void rejectsAMalformedTypePath() throws IOException {
+        final AnnotationEntry annotation = typeAnnotationsOf(field(parse(), 
"field"), 
Const.ATTR_RUNTIME_VISIBLE_TYPE_ANNOTATIONS).getTypeAnnotationEntries()[0]
+                .getAnnotationEntry();
+        // a path is made of pairs of bytes
+        assertThrows(IllegalArgumentException.class, () -> new 
TypeAnnotationEntry(TypeAnnotationEntry.TARGET_FIELD, new byte[0], new byte[1], 
annotation));
+        assertThrows(IllegalArgumentException.class, () -> new 
TypeAnnotationEntry(TypeAnnotationEntry.TARGET_FIELD, new byte[0], new byte[3], 
annotation));
+        // and has at most 255 steps
+        assertThrows(IllegalArgumentException.class, () -> new 
TypeAnnotationEntry(TypeAnnotationEntry.TARGET_FIELD, new byte[0], new byte[256 
* 2], annotation));
+        assertEquals(255, new 
TypeAnnotationEntry(TypeAnnotationEntry.TARGET_FIELD, new byte[0], new byte[255 
* 2], annotation).getTypePathLength());
+    }
+
+    @Test
+    void rejectsATargetInfoOfTheWrongSize() throws IOException {
+        final AnnotationEntry annotation = typeAnnotationsOf(field(parse(), 
"field"), 
Const.ATTR_RUNTIME_VISIBLE_TYPE_ANNOTATIONS).getTypeAnnotationEntries()[0]
+                .getAnnotationEntry();
+        assertThrows(IllegalArgumentException.class, () -> new 
TypeAnnotationEntry(TypeAnnotationEntry.TARGET_FIELD, new byte[1], new byte[0], 
annotation));
+        assertThrows(IllegalArgumentException.class,
+                () -> new 
TypeAnnotationEntry(TypeAnnotationEntry.TARGET_METHOD_FORMAL_PARAMETER, new 
byte[0], new byte[0], annotation));
+        assertThrows(IllegalArgumentException.class,
+                () -> new 
TypeAnnotationEntry(TypeAnnotationEntry.TARGET_CLASS_TYPE_PARAMETER_BOUND, new 
byte[3], new byte[0], annotation));
+        assertThrows(ClassFormatException.class, () -> new 
TypeAnnotationEntry(0x7F, new byte[0], new byte[0], annotation));
+        // a table of two ranges of 6 bytes, announced as one
+        final byte[] twoRangesAnnouncedAsOne = {0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 
0, 0, 0, 0};
+        assertThrows(IllegalArgumentException.class,
+                () -> 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);
+    }
+}
diff --git a/src/test/resources/typeannotations/TypeAnnotated.class 
b/src/test/resources/typeannotations/TypeAnnotated.class
new file mode 100644
index 00000000..f28e8121
Binary files /dev/null and 
b/src/test/resources/typeannotations/TypeAnnotated.class differ
diff --git a/src/test/resources/typeannotations/TypeAnnotated.java 
b/src/test/resources/typeannotations/TypeAnnotated.java
new file mode 100644
index 00000000..07c19507
--- /dev/null
+++ b/src/test/resources/typeannotations/TypeAnnotated.java
@@ -0,0 +1,34 @@
+package typeannotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.List;
+
+// Compiled with: javac --release 8 -d . TypeAnnotated.java (only 
TypeAnnotated.class is used by the tests)
+@Target(ElementType.TYPE_USE)
+@Retention(RetentionPolicy.RUNTIME)
+@interface Visible {
+}
+
+@Target(ElementType.TYPE_USE)
+@Retention(RetentionPolicy.CLASS)
+@interface Invisible {
+}
+
+public class TypeAnnotated<T extends @Visible Object> {
+
+    @Visible
+    String field;
+
+    List<@Visible String> items;
+
+    @Invisible
+    String hidden;
+
+    @Visible
+    String find(@Visible Object key, int count) {
+        return null;
+    }
+}

Reply via email to