Skip to main content

sqry_classpath/bytecode/
annotations.rs

1//! Annotation attribute parser for JVM bytecode.
2//!
3//! Converts `cafebabe`'s parsed annotation structures into the sqry stub model
4//! types ([`AnnotationStub`], [`AnnotationElement`], [`AnnotationElementValue`]).
5//!
6//! This module handles the four annotation attribute types defined in JVMS 4.7.16-4.7.19:
7//! - `RuntimeVisibleAnnotations` (`is_runtime_visible = true`)
8//! - `RuntimeInvisibleAnnotations` (`is_runtime_visible = false`)
9//! - `RuntimeVisibleParameterAnnotations` (`is_runtime_visible = true`)
10//! - `RuntimeInvisibleParameterAnnotations` (`is_runtime_visible = false`)
11//!
12//! ## Type Descriptor Conversion
13//!
14//! Annotation type descriptors in JVM bytecode use the internal form
15//! (e.g., `Lorg/springframework/web/bind/annotation/RequestMapping;`).
16//! These are converted to FQN dot-notation
17//! (e.g., `org.springframework.web.bind.annotation.RequestMapping`).
18
19use cafebabe::attributes::{
20    Annotation, AnnotationElement as CafeAnnotationElement,
21    AnnotationElementValue as CafeAnnotationElementValue, AttributeData, ParameterAnnotation,
22};
23use cafebabe::descriptors::FieldType;
24
25use crate::ClasspathError;
26use crate::stub::model::{
27    AnnotationElement, AnnotationElementValue, AnnotationStub, ConstantValue, OrderedFloat,
28};
29
30/// Convert a JVM internal class name (using `/` separators) to a fully qualified
31/// dot-separated name.
32///
33/// For annotation type descriptors, `cafebabe` parses `Lorg/example/Foo;` into a
34/// `FieldDescriptor` with `FieldType::Object(ClassName("org/example/Foo"))`. The
35/// `ClassName` derefs to a `&str` with `/` separators, which we replace with `.`.
36fn internal_name_to_fqn(internal: &str) -> String {
37    internal.replace('/', ".")
38}
39
40/// Extract the fully qualified name from a cafebabe `FieldType`.
41///
42/// Annotation types are always `FieldType::Object(ClassName)`. For non-object
43/// field types (which should never appear as annotation types), we fall back to
44/// a string representation of the primitive type.
45fn field_type_to_fqn(field_type: &FieldType<'_>) -> String {
46    match field_type {
47        FieldType::Object(class_name) => internal_name_to_fqn(class_name),
48        FieldType::Byte => "byte".to_owned(),
49        FieldType::Char => "char".to_owned(),
50        FieldType::Double => "double".to_owned(),
51        FieldType::Float => "float".to_owned(),
52        FieldType::Integer => "int".to_owned(),
53        FieldType::Long => "long".to_owned(),
54        FieldType::Short => "short".to_owned(),
55        FieldType::Boolean => "boolean".to_owned(),
56    }
57}
58
59/// Convert a single cafebabe `AnnotationElementValue` to the sqry model
60/// [`AnnotationElementValue`].
61///
62/// The JVM constant pool types `B` (byte), `C` (char), `S` (short), `Z` (boolean)
63/// are all stored as `i32` in cafebabe; we map them to [`ConstantValue::Int`] since
64/// the JVM representation is the same.
65fn convert_element_value(
66    value: &CafeAnnotationElementValue<'_>,
67    is_runtime_visible: bool,
68) -> AnnotationElementValue {
69    match value {
70        CafeAnnotationElementValue::ByteConstant(v)
71        | CafeAnnotationElementValue::CharConstant(v)
72        | CafeAnnotationElementValue::IntConstant(v)
73        | CafeAnnotationElementValue::ShortConstant(v)
74        | CafeAnnotationElementValue::BooleanConstant(v) => {
75            AnnotationElementValue::Const(ConstantValue::Int(*v))
76        }
77        CafeAnnotationElementValue::LongConstant(v) => {
78            AnnotationElementValue::Const(ConstantValue::Long(*v))
79        }
80        CafeAnnotationElementValue::FloatConstant(v) => {
81            AnnotationElementValue::Const(ConstantValue::Float(OrderedFloat(*v)))
82        }
83        CafeAnnotationElementValue::DoubleConstant(v) => {
84            AnnotationElementValue::Const(ConstantValue::Double(OrderedFloat(*v)))
85        }
86        CafeAnnotationElementValue::StringConstant(v) => {
87            AnnotationElementValue::Const(ConstantValue::String(v.to_string()))
88        }
89        CafeAnnotationElementValue::EnumConstant {
90            type_name,
91            const_name,
92        } => AnnotationElementValue::EnumConst {
93            type_fqn: field_type_to_fqn(&type_name.field_type),
94            const_name: const_name.to_string(),
95        },
96        CafeAnnotationElementValue::ClassLiteral { class_name } => {
97            // class_name is a descriptor like "Ljava/lang/String;" or a primitive.
98            // We store the FQN form (dots, no L/; wrapper).
99            let fqn = class_name.replace('/', ".");
100            // Strip the `L` prefix and `;` suffix if present (object type descriptor).
101            let fqn = fqn
102                .strip_prefix('L')
103                .and_then(|s| s.strip_suffix(';'))
104                .map_or_else(|| fqn.clone(), str::to_owned);
105            AnnotationElementValue::ClassInfo(fqn)
106        }
107        CafeAnnotationElementValue::AnnotationValue(nested) => AnnotationElementValue::Annotation(
108            Box::new(convert_annotation(nested, is_runtime_visible)),
109        ),
110        CafeAnnotationElementValue::ArrayValue(elements) => AnnotationElementValue::Array(
111            elements
112                .iter()
113                .map(|e| convert_element_value(e, is_runtime_visible))
114                .collect(),
115        ),
116    }
117}
118
119/// Convert a single cafebabe [`Annotation`] to an [`AnnotationStub`].
120fn convert_annotation(annotation: &Annotation<'_>, is_runtime_visible: bool) -> AnnotationStub {
121    let type_fqn = field_type_to_fqn(&annotation.type_descriptor.field_type);
122
123    let elements = annotation
124        .elements
125        .iter()
126        .map(|e| convert_element(e, is_runtime_visible))
127        .collect();
128
129    AnnotationStub {
130        type_fqn,
131        elements,
132        is_runtime_visible,
133    }
134}
135
136/// Convert a single cafebabe [`CafeAnnotationElement`] to an [`AnnotationElement`].
137fn convert_element(
138    element: &CafeAnnotationElement<'_>,
139    is_runtime_visible: bool,
140) -> AnnotationElement {
141    AnnotationElement {
142        name: element.name.to_string(),
143        value: convert_element_value(&element.value, is_runtime_visible),
144    }
145}
146
147/// Convert a list of cafebabe [`Annotation`] values into [`AnnotationStub`] records.
148///
149/// This is used for both `RuntimeVisibleAnnotations` and
150/// `RuntimeInvisibleAnnotations` attributes. The caller controls which
151/// variant by passing `is_runtime_visible`.
152///
153/// # Arguments
154///
155/// * `annotations` - Parsed annotation list from cafebabe.
156/// * `is_runtime_visible` - Whether these annotations come from a
157///   `RuntimeVisibleAnnotations` attribute.
158#[must_use]
159pub fn convert_annotations(
160    annotations: &[Annotation<'_>],
161    is_runtime_visible: bool,
162) -> Vec<AnnotationStub> {
163    annotations
164        .iter()
165        .map(|a| convert_annotation(a, is_runtime_visible))
166        .collect()
167}
168
169/// Convert a list of cafebabe [`ParameterAnnotation`] values into nested
170/// `Vec<Vec<AnnotationStub>>` records.
171///
172/// The outer vector is indexed by parameter position; the inner vector
173/// contains the annotations for that parameter.
174///
175/// # Arguments
176///
177/// * `param_annotations` - Parsed parameter annotation list from cafebabe.
178/// * `is_runtime_visible` - Whether these annotations come from a
179///   `RuntimeVisibleParameterAnnotations` attribute.
180#[must_use]
181pub fn convert_parameter_annotations(
182    param_annotations: &[ParameterAnnotation<'_>],
183    is_runtime_visible: bool,
184) -> Vec<Vec<AnnotationStub>> {
185    param_annotations
186        .iter()
187        .map(|pa| convert_annotations(&pa.annotations, is_runtime_visible))
188        .collect()
189}
190
191/// Extract all annotations from a cafebabe [`AttributeData`] value.
192///
193/// Returns `Ok(Some(stubs))` if the attribute is an annotation attribute,
194/// `Ok(None)` if it is a different attribute type. Returns `Err` only on
195/// internal conversion failures (currently infallible, but the signature
196/// allows for future extension).
197///
198/// This handles:
199/// - `RuntimeVisibleAnnotations` → `is_runtime_visible = true`
200/// - `RuntimeInvisibleAnnotations` → `is_runtime_visible = false`
201#[allow(clippy::missing_errors_doc)] // Internal helper function
202pub fn extract_annotations_from_attribute(
203    attr: &AttributeData<'_>,
204) -> Result<Option<Vec<AnnotationStub>>, ClasspathError> {
205    match attr {
206        AttributeData::RuntimeVisibleAnnotations(annotations) => {
207            Ok(Some(convert_annotations(annotations, true)))
208        }
209        AttributeData::RuntimeInvisibleAnnotations(annotations) => {
210            Ok(Some(convert_annotations(annotations, false)))
211        }
212        _ => Ok(None),
213    }
214}
215
216/// Extract parameter annotations from a cafebabe [`AttributeData`] value.
217///
218/// Returns `Ok(Some(stubs))` if the attribute is a parameter annotation attribute,
219/// `Ok(None)` if it is a different attribute type.
220///
221/// This handles:
222/// - `RuntimeVisibleParameterAnnotations` → `is_runtime_visible = true`
223/// - `RuntimeInvisibleParameterAnnotations` → `is_runtime_visible = false`
224#[allow(clippy::missing_errors_doc)] // Internal helper function
225pub fn extract_parameter_annotations_from_attribute(
226    attr: &AttributeData<'_>,
227) -> Result<Option<Vec<Vec<AnnotationStub>>>, ClasspathError> {
228    match attr {
229        AttributeData::RuntimeVisibleParameterAnnotations(param_annotations) => {
230            Ok(Some(convert_parameter_annotations(param_annotations, true)))
231        }
232        AttributeData::RuntimeInvisibleParameterAnnotations(param_annotations) => Ok(Some(
233            convert_parameter_annotations(param_annotations, false),
234        )),
235        _ => Ok(None),
236    }
237}
238
239// ---------------------------------------------------------------------------
240// Tests
241// ---------------------------------------------------------------------------
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246    use cafebabe::attributes::{
247        Annotation, AnnotationElement as CafeAnnotationElement,
248        AnnotationElementValue as CafeAnnotationElementValue, AttributeData, ParameterAnnotation,
249    };
250    use cafebabe::descriptors::{ClassName, FieldDescriptor, FieldType};
251    use std::borrow::Cow;
252
253    /// Helper: build a `FieldDescriptor` for an object type from an internal name.
254    fn object_descriptor(internal_name: &str) -> FieldDescriptor<'_> {
255        FieldDescriptor {
256            dimensions: 0,
257            field_type: FieldType::Object(
258                ClassName::try_from(Cow::Borrowed(internal_name)).expect("valid class name"),
259            ),
260        }
261    }
262
263    /// Helper: build a cafebabe `Annotation` with no elements.
264    fn marker_annotation(internal_name: &str) -> Annotation<'_> {
265        Annotation {
266            type_descriptor: object_descriptor(internal_name),
267            elements: vec![],
268        }
269    }
270
271    // -- Test 1: Simple marker annotation (no elements) ---------------------
272
273    #[test]
274    fn simple_marker_annotation() {
275        let cafe_ann = marker_annotation("java/lang/Override");
276        let stubs = convert_annotations(&[cafe_ann], true);
277
278        assert_eq!(stubs.len(), 1);
279        assert_eq!(stubs[0].type_fqn, "java.lang.Override");
280        assert!(stubs[0].elements.is_empty());
281        assert!(stubs[0].is_runtime_visible);
282    }
283
284    // -- Test 2: Annotation with string value --------------------------------
285
286    #[test]
287    fn annotation_with_string_value() {
288        let cafe_ann = Annotation {
289            type_descriptor: object_descriptor(
290                "org/springframework/web/bind/annotation/RequestMapping",
291            ),
292            elements: vec![CafeAnnotationElement {
293                name: Cow::Borrowed("value"),
294                value: CafeAnnotationElementValue::StringConstant(Cow::Borrowed("/api")),
295            }],
296        };
297
298        let stubs = convert_annotations(&[cafe_ann], true);
299
300        assert_eq!(stubs.len(), 1);
301        assert_eq!(
302            stubs[0].type_fqn,
303            "org.springframework.web.bind.annotation.RequestMapping"
304        );
305        assert_eq!(stubs[0].elements.len(), 1);
306        assert_eq!(stubs[0].elements[0].name, "value");
307        assert!(matches!(
308            &stubs[0].elements[0].value,
309            AnnotationElementValue::Const(ConstantValue::String(s)) if s == "/api"
310        ));
311    }
312
313    // -- Test 3: Annotation with enum constant --------------------------------
314
315    #[test]
316    fn annotation_with_enum_constant() {
317        let cafe_ann = Annotation {
318            type_descriptor: object_descriptor(
319                "org/springframework/web/bind/annotation/RequestMapping",
320            ),
321            elements: vec![CafeAnnotationElement {
322                name: Cow::Borrowed("method"),
323                value: CafeAnnotationElementValue::EnumConstant {
324                    type_name: object_descriptor(
325                        "org/springframework/web/bind/annotation/RequestMethod",
326                    ),
327                    const_name: Cow::Borrowed("GET"),
328                },
329            }],
330        };
331
332        let stubs = convert_annotations(&[cafe_ann], true);
333
334        assert_eq!(stubs.len(), 1);
335        assert_eq!(stubs[0].elements.len(), 1);
336        assert!(matches!(
337            &stubs[0].elements[0].value,
338            AnnotationElementValue::EnumConst { type_fqn, const_name }
339                if type_fqn == "org.springframework.web.bind.annotation.RequestMethod"
340                && const_name == "GET"
341        ));
342    }
343
344    // -- Test 4: Annotation with array value ----------------------------------
345
346    #[test]
347    fn annotation_with_array_value() {
348        let cafe_ann = Annotation {
349            type_descriptor: object_descriptor(
350                "org/springframework/context/annotation/ComponentScan",
351            ),
352            elements: vec![CafeAnnotationElement {
353                name: Cow::Borrowed("basePackages"),
354                value: CafeAnnotationElementValue::ArrayValue(vec![
355                    CafeAnnotationElementValue::StringConstant(Cow::Borrowed("com.example.a")),
356                    CafeAnnotationElementValue::StringConstant(Cow::Borrowed("com.example.b")),
357                ]),
358            }],
359        };
360
361        let stubs = convert_annotations(&[cafe_ann], false);
362
363        assert_eq!(stubs.len(), 1);
364        assert!(!stubs[0].is_runtime_visible);
365        assert_eq!(stubs[0].elements[0].name, "basePackages");
366        match &stubs[0].elements[0].value {
367            AnnotationElementValue::Array(items) => {
368                assert_eq!(items.len(), 2);
369                assert!(matches!(
370                    &items[0],
371                    AnnotationElementValue::Const(ConstantValue::String(s)) if s == "com.example.a"
372                ));
373                assert!(matches!(
374                    &items[1],
375                    AnnotationElementValue::Const(ConstantValue::String(s)) if s == "com.example.b"
376                ));
377            }
378            other => panic!("expected Array, got {other:?}"),
379        }
380    }
381
382    // -- Test 5: Annotation with class literal --------------------------------
383
384    #[test]
385    fn annotation_with_class_literal() {
386        let cafe_ann = Annotation {
387            type_descriptor: object_descriptor("javax/persistence/Type"),
388            elements: vec![CafeAnnotationElement {
389                name: Cow::Borrowed("value"),
390                value: CafeAnnotationElementValue::ClassLiteral {
391                    class_name: Cow::Borrowed("Ljava/lang/String;"),
392                },
393            }],
394        };
395
396        let stubs = convert_annotations(&[cafe_ann], true);
397
398        assert_eq!(stubs[0].elements[0].name, "value");
399        assert!(matches!(
400            &stubs[0].elements[0].value,
401            AnnotationElementValue::ClassInfo(fqn) if fqn == "java.lang.String"
402        ));
403    }
404
405    // -- Test 6: Nested annotation -------------------------------------------
406
407    #[test]
408    fn nested_annotation() {
409        let inner = Annotation {
410            type_descriptor: object_descriptor("javax/validation/constraints/Size"),
411            elements: vec![
412                CafeAnnotationElement {
413                    name: Cow::Borrowed("min"),
414                    value: CafeAnnotationElementValue::IntConstant(1),
415                },
416                CafeAnnotationElement {
417                    name: Cow::Borrowed("max"),
418                    value: CafeAnnotationElementValue::IntConstant(100),
419                },
420            ],
421        };
422
423        let outer = Annotation {
424            type_descriptor: object_descriptor("javax/validation/Valid"),
425            elements: vec![CafeAnnotationElement {
426                name: Cow::Borrowed("payload"),
427                value: CafeAnnotationElementValue::AnnotationValue(inner),
428            }],
429        };
430
431        let stubs = convert_annotations(&[outer], true);
432
433        assert_eq!(stubs.len(), 1);
434        assert_eq!(stubs[0].type_fqn, "javax.validation.Valid");
435        match &stubs[0].elements[0].value {
436            AnnotationElementValue::Annotation(nested) => {
437                assert_eq!(nested.type_fqn, "javax.validation.constraints.Size");
438                assert!(nested.is_runtime_visible);
439                assert_eq!(nested.elements.len(), 2);
440                assert_eq!(nested.elements[0].name, "min");
441                assert!(matches!(
442                    &nested.elements[0].value,
443                    AnnotationElementValue::Const(ConstantValue::Int(1))
444                ));
445                assert_eq!(nested.elements[1].name, "max");
446                assert!(matches!(
447                    &nested.elements[1].value,
448                    AnnotationElementValue::Const(ConstantValue::Int(100))
449                ));
450            }
451            other => panic!("expected Annotation, got {other:?}"),
452        }
453    }
454
455    // -- Test 7: Parameter annotations ----------------------------------------
456
457    #[test]
458    fn parameter_annotations() {
459        let param0_annotations = ParameterAnnotation {
460            annotations: vec![Annotation {
461                type_descriptor: object_descriptor("javax/annotation/Nonnull"),
462                elements: vec![],
463            }],
464        };
465        let param1_annotations = ParameterAnnotation {
466            annotations: vec![
467                Annotation {
468                    type_descriptor: object_descriptor("javax/validation/constraints/NotNull"),
469                    elements: vec![],
470                },
471                Annotation {
472                    type_descriptor: object_descriptor("javax/validation/constraints/Size"),
473                    elements: vec![CafeAnnotationElement {
474                        name: Cow::Borrowed("max"),
475                        value: CafeAnnotationElementValue::IntConstant(255),
476                    }],
477                },
478            ],
479        };
480        let param2_no_annotations = ParameterAnnotation {
481            annotations: vec![],
482        };
483
484        let result = convert_parameter_annotations(
485            &[
486                param0_annotations,
487                param1_annotations,
488                param2_no_annotations,
489            ],
490            true,
491        );
492
493        assert_eq!(result.len(), 3);
494        // Parameter 0: one annotation
495        assert_eq!(result[0].len(), 1);
496        assert_eq!(result[0][0].type_fqn, "javax.annotation.Nonnull");
497        assert!(result[0][0].is_runtime_visible);
498
499        // Parameter 1: two annotations
500        assert_eq!(result[1].len(), 2);
501        assert_eq!(
502            result[1][0].type_fqn,
503            "javax.validation.constraints.NotNull"
504        );
505        assert_eq!(result[1][1].type_fqn, "javax.validation.constraints.Size");
506        assert_eq!(result[1][1].elements.len(), 1);
507        assert_eq!(result[1][1].elements[0].name, "max");
508
509        // Parameter 2: no annotations
510        assert!(result[2].is_empty());
511    }
512
513    // -- Test 8: Extract from AttributeData -----------------------------------
514
515    #[test]
516    fn extract_visible_annotations_from_attribute_data() {
517        let annotations = vec![marker_annotation("java/lang/Deprecated")];
518        let attr = AttributeData::RuntimeVisibleAnnotations(annotations);
519
520        let result = extract_annotations_from_attribute(&attr).unwrap();
521        let stubs = result.expect("should return Some for annotation attribute");
522        assert_eq!(stubs.len(), 1);
523        assert_eq!(stubs[0].type_fqn, "java.lang.Deprecated");
524        assert!(stubs[0].is_runtime_visible);
525    }
526
527    #[test]
528    fn extract_invisible_annotations_from_attribute_data() {
529        let annotations = vec![marker_annotation("javax/annotation/Generated")];
530        let attr = AttributeData::RuntimeInvisibleAnnotations(annotations);
531
532        let result = extract_annotations_from_attribute(&attr).unwrap();
533        let stubs = result.expect("should return Some");
534        assert_eq!(stubs.len(), 1);
535        assert!(!stubs[0].is_runtime_visible);
536    }
537
538    #[test]
539    fn extract_returns_none_for_non_annotation_attribute() {
540        let attr = AttributeData::Deprecated;
541        let result = extract_annotations_from_attribute(&attr).unwrap();
542        assert!(result.is_none());
543    }
544
545    #[test]
546    fn extract_visible_parameter_annotations() {
547        let param_annotations = vec![ParameterAnnotation {
548            annotations: vec![marker_annotation("javax/annotation/Nullable")],
549        }];
550        let attr = AttributeData::RuntimeVisibleParameterAnnotations(param_annotations);
551
552        let result = extract_parameter_annotations_from_attribute(&attr).unwrap();
553        let stubs = result.expect("should return Some");
554        assert_eq!(stubs.len(), 1);
555        assert_eq!(stubs[0].len(), 1);
556        assert_eq!(stubs[0][0].type_fqn, "javax.annotation.Nullable");
557        assert!(stubs[0][0].is_runtime_visible);
558    }
559
560    #[test]
561    fn extract_invisible_parameter_annotations() {
562        let param_annotations = vec![ParameterAnnotation {
563            annotations: vec![marker_annotation("javax/annotation/Nonnull")],
564        }];
565        let attr = AttributeData::RuntimeInvisibleParameterAnnotations(param_annotations);
566
567        let result = extract_parameter_annotations_from_attribute(&attr).unwrap();
568        let stubs = result.expect("should return Some");
569        assert_eq!(stubs[0][0].type_fqn, "javax.annotation.Nonnull");
570        assert!(!stubs[0][0].is_runtime_visible);
571    }
572
573    // -- Test: Numeric constant types -----------------------------------------
574
575    #[test]
576    fn byte_char_short_boolean_constants_map_to_int() {
577        let values = vec![
578            CafeAnnotationElementValue::ByteConstant(42),
579            CafeAnnotationElementValue::CharConstant(65),
580            CafeAnnotationElementValue::ShortConstant(1000),
581            CafeAnnotationElementValue::BooleanConstant(1),
582        ];
583
584        for v in &values {
585            let result = convert_element_value(v, true);
586            assert!(
587                matches!(result, AnnotationElementValue::Const(ConstantValue::Int(_))),
588                "expected Int variant for {v:?}"
589            );
590        }
591    }
592
593    #[test]
594    fn long_constant() {
595        let v = CafeAnnotationElementValue::LongConstant(i64::MAX);
596        let result = convert_element_value(&v, true);
597        assert!(matches!(
598            result,
599            AnnotationElementValue::Const(ConstantValue::Long(i64::MAX))
600        ));
601    }
602
603    #[test]
604    fn float_constant() {
605        let v = CafeAnnotationElementValue::FloatConstant(std::f32::consts::PI);
606        let result = convert_element_value(&v, true);
607        match result {
608            AnnotationElementValue::Const(ConstantValue::Float(f)) => {
609                assert!((f.0 - std::f32::consts::PI).abs() < f32::EPSILON);
610            }
611            other => panic!("expected Float, got {other:?}"),
612        }
613    }
614
615    #[test]
616    fn double_constant() {
617        let v = CafeAnnotationElementValue::DoubleConstant(std::f64::consts::E);
618        let result = convert_element_value(&v, true);
619        match result {
620            AnnotationElementValue::Const(ConstantValue::Double(d)) => {
621                assert!((d.0 - std::f64::consts::E).abs() < f64::EPSILON);
622            }
623            other => panic!("expected Double, got {other:?}"),
624        }
625    }
626
627    // -- Test: Internal name conversion ---------------------------------------
628
629    #[test]
630    fn internal_name_conversion() {
631        assert_eq!(
632            internal_name_to_fqn("org/springframework/web/bind/annotation/RequestMapping"),
633            "org.springframework.web.bind.annotation.RequestMapping"
634        );
635        assert_eq!(internal_name_to_fqn("java/lang/Object"), "java.lang.Object");
636        assert_eq!(internal_name_to_fqn("Foo"), "Foo");
637    }
638
639    // -- Test: Class literal with primitive descriptor -----------------------
640
641    #[test]
642    fn class_literal_primitive_descriptor() {
643        // Primitive class literals like `int.class` use descriptor "I" etc.
644        let v = CafeAnnotationElementValue::ClassLiteral {
645            class_name: Cow::Borrowed("I"),
646        };
647        let result = convert_element_value(&v, true);
648        assert!(matches!(
649            result,
650            AnnotationElementValue::ClassInfo(ref s) if s == "I"
651        ));
652    }
653
654    // -- Test: Multiple annotations in one attribute -------------------------
655
656    #[test]
657    fn multiple_annotations() {
658        let annotations = vec![
659            marker_annotation("java/lang/Override"),
660            marker_annotation("java/lang/Deprecated"),
661            marker_annotation("java/lang/SuppressWarnings"),
662        ];
663
664        let stubs = convert_annotations(&annotations, false);
665        assert_eq!(stubs.len(), 3);
666        assert_eq!(stubs[0].type_fqn, "java.lang.Override");
667        assert_eq!(stubs[1].type_fqn, "java.lang.Deprecated");
668        assert_eq!(stubs[2].type_fqn, "java.lang.SuppressWarnings");
669        for stub in &stubs {
670            assert!(!stub.is_runtime_visible);
671        }
672    }
673
674    // -- Test: Empty annotation list -----------------------------------------
675
676    #[test]
677    fn empty_annotation_list() {
678        let stubs = convert_annotations(&[], true);
679        assert!(stubs.is_empty());
680    }
681
682    // -- Test: Empty parameter annotation list --------------------------------
683
684    #[test]
685    fn empty_parameter_annotation_list() {
686        let stubs = convert_parameter_annotations(&[], true);
687        assert!(stubs.is_empty());
688    }
689
690    // -- Test: Complex annotation with mixed element types --------------------
691
692    #[test]
693    fn complex_annotation_with_mixed_elements() {
694        let ann = Annotation {
695            type_descriptor: object_descriptor(
696                "org/springframework/web/bind/annotation/RequestMapping",
697            ),
698            elements: vec![
699                CafeAnnotationElement {
700                    name: Cow::Borrowed("value"),
701                    value: CafeAnnotationElementValue::ArrayValue(vec![
702                        CafeAnnotationElementValue::StringConstant(Cow::Borrowed("/api/users")),
703                    ]),
704                },
705                CafeAnnotationElement {
706                    name: Cow::Borrowed("method"),
707                    value: CafeAnnotationElementValue::EnumConstant {
708                        type_name: object_descriptor(
709                            "org/springframework/web/bind/annotation/RequestMethod",
710                        ),
711                        const_name: Cow::Borrowed("GET"),
712                    },
713                },
714                CafeAnnotationElement {
715                    name: Cow::Borrowed("produces"),
716                    value: CafeAnnotationElementValue::ClassLiteral {
717                        class_name: Cow::Borrowed("Ljava/lang/String;"),
718                    },
719                },
720                CafeAnnotationElement {
721                    name: Cow::Borrowed("timeout"),
722                    value: CafeAnnotationElementValue::IntConstant(30),
723                },
724            ],
725        };
726
727        let stubs = convert_annotations(&[ann], true);
728        assert_eq!(stubs.len(), 1);
729        let stub = &stubs[0];
730        assert_eq!(stub.elements.len(), 4);
731
732        // Array element
733        assert!(matches!(
734            &stub.elements[0].value,
735            AnnotationElementValue::Array(items) if items.len() == 1
736        ));
737        // Enum element
738        assert!(matches!(
739            &stub.elements[1].value,
740            AnnotationElementValue::EnumConst { const_name, .. } if const_name == "GET"
741        ));
742        // Class element
743        assert!(matches!(
744            &stub.elements[2].value,
745            AnnotationElementValue::ClassInfo(fqn) if fqn == "java.lang.String"
746        ));
747        // Int element
748        assert!(matches!(
749            &stub.elements[3].value,
750            AnnotationElementValue::Const(ConstantValue::Int(30))
751        ));
752    }
753}