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`
201pub fn extract_annotations_from_attribute(
202    attr: &AttributeData<'_>,
203) -> Result<Option<Vec<AnnotationStub>>, ClasspathError> {
204    match attr {
205        AttributeData::RuntimeVisibleAnnotations(annotations) => {
206            Ok(Some(convert_annotations(annotations, true)))
207        }
208        AttributeData::RuntimeInvisibleAnnotations(annotations) => {
209            Ok(Some(convert_annotations(annotations, false)))
210        }
211        _ => Ok(None),
212    }
213}
214
215/// Extract parameter annotations from a cafebabe [`AttributeData`] value.
216///
217/// Returns `Ok(Some(stubs))` if the attribute is a parameter annotation attribute,
218/// `Ok(None)` if it is a different attribute type.
219///
220/// This handles:
221/// - `RuntimeVisibleParameterAnnotations` → `is_runtime_visible = true`
222/// - `RuntimeInvisibleParameterAnnotations` → `is_runtime_visible = false`
223pub fn extract_parameter_annotations_from_attribute(
224    attr: &AttributeData<'_>,
225) -> Result<Option<Vec<Vec<AnnotationStub>>>, ClasspathError> {
226    match attr {
227        AttributeData::RuntimeVisibleParameterAnnotations(param_annotations) => {
228            Ok(Some(convert_parameter_annotations(param_annotations, true)))
229        }
230        AttributeData::RuntimeInvisibleParameterAnnotations(param_annotations) => Ok(Some(
231            convert_parameter_annotations(param_annotations, false),
232        )),
233        _ => Ok(None),
234    }
235}
236
237// ---------------------------------------------------------------------------
238// Tests
239// ---------------------------------------------------------------------------
240
241#[cfg(test)]
242mod tests {
243    use super::*;
244    use cafebabe::attributes::{
245        Annotation, AnnotationElement as CafeAnnotationElement,
246        AnnotationElementValue as CafeAnnotationElementValue, AttributeData, ParameterAnnotation,
247    };
248    use cafebabe::descriptors::{ClassName, FieldDescriptor, FieldType};
249    use std::borrow::Cow;
250
251    /// Helper: build a `FieldDescriptor` for an object type from an internal name.
252    fn object_descriptor(internal_name: &str) -> FieldDescriptor<'_> {
253        FieldDescriptor {
254            dimensions: 0,
255            field_type: FieldType::Object(
256                ClassName::try_from(Cow::Borrowed(internal_name)).expect("valid class name"),
257            ),
258        }
259    }
260
261    /// Helper: build a cafebabe `Annotation` with no elements.
262    fn marker_annotation(internal_name: &str) -> Annotation<'_> {
263        Annotation {
264            type_descriptor: object_descriptor(internal_name),
265            elements: vec![],
266        }
267    }
268
269    // -- Test 1: Simple marker annotation (no elements) ---------------------
270
271    #[test]
272    fn simple_marker_annotation() {
273        let cafe_ann = marker_annotation("java/lang/Override");
274        let stubs = convert_annotations(&[cafe_ann], true);
275
276        assert_eq!(stubs.len(), 1);
277        assert_eq!(stubs[0].type_fqn, "java.lang.Override");
278        assert!(stubs[0].elements.is_empty());
279        assert!(stubs[0].is_runtime_visible);
280    }
281
282    // -- Test 2: Annotation with string value --------------------------------
283
284    #[test]
285    fn annotation_with_string_value() {
286        let cafe_ann = Annotation {
287            type_descriptor: object_descriptor(
288                "org/springframework/web/bind/annotation/RequestMapping",
289            ),
290            elements: vec![CafeAnnotationElement {
291                name: Cow::Borrowed("value"),
292                value: CafeAnnotationElementValue::StringConstant(Cow::Borrowed("/api")),
293            }],
294        };
295
296        let stubs = convert_annotations(&[cafe_ann], true);
297
298        assert_eq!(stubs.len(), 1);
299        assert_eq!(
300            stubs[0].type_fqn,
301            "org.springframework.web.bind.annotation.RequestMapping"
302        );
303        assert_eq!(stubs[0].elements.len(), 1);
304        assert_eq!(stubs[0].elements[0].name, "value");
305        assert!(matches!(
306            &stubs[0].elements[0].value,
307            AnnotationElementValue::Const(ConstantValue::String(s)) if s == "/api"
308        ));
309    }
310
311    // -- Test 3: Annotation with enum constant --------------------------------
312
313    #[test]
314    fn annotation_with_enum_constant() {
315        let cafe_ann = Annotation {
316            type_descriptor: object_descriptor(
317                "org/springframework/web/bind/annotation/RequestMapping",
318            ),
319            elements: vec![CafeAnnotationElement {
320                name: Cow::Borrowed("method"),
321                value: CafeAnnotationElementValue::EnumConstant {
322                    type_name: object_descriptor(
323                        "org/springframework/web/bind/annotation/RequestMethod",
324                    ),
325                    const_name: Cow::Borrowed("GET"),
326                },
327            }],
328        };
329
330        let stubs = convert_annotations(&[cafe_ann], true);
331
332        assert_eq!(stubs.len(), 1);
333        assert_eq!(stubs[0].elements.len(), 1);
334        assert!(matches!(
335            &stubs[0].elements[0].value,
336            AnnotationElementValue::EnumConst { type_fqn, const_name }
337                if type_fqn == "org.springframework.web.bind.annotation.RequestMethod"
338                && const_name == "GET"
339        ));
340    }
341
342    // -- Test 4: Annotation with array value ----------------------------------
343
344    #[test]
345    fn annotation_with_array_value() {
346        let cafe_ann = Annotation {
347            type_descriptor: object_descriptor(
348                "org/springframework/context/annotation/ComponentScan",
349            ),
350            elements: vec![CafeAnnotationElement {
351                name: Cow::Borrowed("basePackages"),
352                value: CafeAnnotationElementValue::ArrayValue(vec![
353                    CafeAnnotationElementValue::StringConstant(Cow::Borrowed("com.example.a")),
354                    CafeAnnotationElementValue::StringConstant(Cow::Borrowed("com.example.b")),
355                ]),
356            }],
357        };
358
359        let stubs = convert_annotations(&[cafe_ann], false);
360
361        assert_eq!(stubs.len(), 1);
362        assert!(!stubs[0].is_runtime_visible);
363        assert_eq!(stubs[0].elements[0].name, "basePackages");
364        match &stubs[0].elements[0].value {
365            AnnotationElementValue::Array(items) => {
366                assert_eq!(items.len(), 2);
367                assert!(matches!(
368                    &items[0],
369                    AnnotationElementValue::Const(ConstantValue::String(s)) if s == "com.example.a"
370                ));
371                assert!(matches!(
372                    &items[1],
373                    AnnotationElementValue::Const(ConstantValue::String(s)) if s == "com.example.b"
374                ));
375            }
376            other => panic!("expected Array, got {other:?}"),
377        }
378    }
379
380    // -- Test 5: Annotation with class literal --------------------------------
381
382    #[test]
383    fn annotation_with_class_literal() {
384        let cafe_ann = Annotation {
385            type_descriptor: object_descriptor("javax/persistence/Type"),
386            elements: vec![CafeAnnotationElement {
387                name: Cow::Borrowed("value"),
388                value: CafeAnnotationElementValue::ClassLiteral {
389                    class_name: Cow::Borrowed("Ljava/lang/String;"),
390                },
391            }],
392        };
393
394        let stubs = convert_annotations(&[cafe_ann], true);
395
396        assert_eq!(stubs[0].elements[0].name, "value");
397        assert!(matches!(
398            &stubs[0].elements[0].value,
399            AnnotationElementValue::ClassInfo(fqn) if fqn == "java.lang.String"
400        ));
401    }
402
403    // -- Test 6: Nested annotation -------------------------------------------
404
405    #[test]
406    fn nested_annotation() {
407        let inner = Annotation {
408            type_descriptor: object_descriptor("javax/validation/constraints/Size"),
409            elements: vec![
410                CafeAnnotationElement {
411                    name: Cow::Borrowed("min"),
412                    value: CafeAnnotationElementValue::IntConstant(1),
413                },
414                CafeAnnotationElement {
415                    name: Cow::Borrowed("max"),
416                    value: CafeAnnotationElementValue::IntConstant(100),
417                },
418            ],
419        };
420
421        let outer = Annotation {
422            type_descriptor: object_descriptor("javax/validation/Valid"),
423            elements: vec![CafeAnnotationElement {
424                name: Cow::Borrowed("payload"),
425                value: CafeAnnotationElementValue::AnnotationValue(inner),
426            }],
427        };
428
429        let stubs = convert_annotations(&[outer], true);
430
431        assert_eq!(stubs.len(), 1);
432        assert_eq!(stubs[0].type_fqn, "javax.validation.Valid");
433        match &stubs[0].elements[0].value {
434            AnnotationElementValue::Annotation(nested) => {
435                assert_eq!(nested.type_fqn, "javax.validation.constraints.Size");
436                assert!(nested.is_runtime_visible);
437                assert_eq!(nested.elements.len(), 2);
438                assert_eq!(nested.elements[0].name, "min");
439                assert!(matches!(
440                    &nested.elements[0].value,
441                    AnnotationElementValue::Const(ConstantValue::Int(1))
442                ));
443                assert_eq!(nested.elements[1].name, "max");
444                assert!(matches!(
445                    &nested.elements[1].value,
446                    AnnotationElementValue::Const(ConstantValue::Int(100))
447                ));
448            }
449            other => panic!("expected Annotation, got {other:?}"),
450        }
451    }
452
453    // -- Test 7: Parameter annotations ----------------------------------------
454
455    #[test]
456    fn parameter_annotations() {
457        let param0_annotations = ParameterAnnotation {
458            annotations: vec![Annotation {
459                type_descriptor: object_descriptor("javax/annotation/Nonnull"),
460                elements: vec![],
461            }],
462        };
463        let param1_annotations = ParameterAnnotation {
464            annotations: vec![
465                Annotation {
466                    type_descriptor: object_descriptor("javax/validation/constraints/NotNull"),
467                    elements: vec![],
468                },
469                Annotation {
470                    type_descriptor: object_descriptor("javax/validation/constraints/Size"),
471                    elements: vec![CafeAnnotationElement {
472                        name: Cow::Borrowed("max"),
473                        value: CafeAnnotationElementValue::IntConstant(255),
474                    }],
475                },
476            ],
477        };
478        let param2_no_annotations = ParameterAnnotation {
479            annotations: vec![],
480        };
481
482        let result = convert_parameter_annotations(
483            &[
484                param0_annotations,
485                param1_annotations,
486                param2_no_annotations,
487            ],
488            true,
489        );
490
491        assert_eq!(result.len(), 3);
492        // Parameter 0: one annotation
493        assert_eq!(result[0].len(), 1);
494        assert_eq!(result[0][0].type_fqn, "javax.annotation.Nonnull");
495        assert!(result[0][0].is_runtime_visible);
496
497        // Parameter 1: two annotations
498        assert_eq!(result[1].len(), 2);
499        assert_eq!(
500            result[1][0].type_fqn,
501            "javax.validation.constraints.NotNull"
502        );
503        assert_eq!(result[1][1].type_fqn, "javax.validation.constraints.Size");
504        assert_eq!(result[1][1].elements.len(), 1);
505        assert_eq!(result[1][1].elements[0].name, "max");
506
507        // Parameter 2: no annotations
508        assert!(result[2].is_empty());
509    }
510
511    // -- Test 8: Extract from AttributeData -----------------------------------
512
513    #[test]
514    fn extract_visible_annotations_from_attribute_data() {
515        let annotations = vec![marker_annotation("java/lang/Deprecated")];
516        let attr = AttributeData::RuntimeVisibleAnnotations(annotations);
517
518        let result = extract_annotations_from_attribute(&attr).unwrap();
519        let stubs = result.expect("should return Some for annotation attribute");
520        assert_eq!(stubs.len(), 1);
521        assert_eq!(stubs[0].type_fqn, "java.lang.Deprecated");
522        assert!(stubs[0].is_runtime_visible);
523    }
524
525    #[test]
526    fn extract_invisible_annotations_from_attribute_data() {
527        let annotations = vec![marker_annotation("javax/annotation/Generated")];
528        let attr = AttributeData::RuntimeInvisibleAnnotations(annotations);
529
530        let result = extract_annotations_from_attribute(&attr).unwrap();
531        let stubs = result.expect("should return Some");
532        assert_eq!(stubs.len(), 1);
533        assert!(!stubs[0].is_runtime_visible);
534    }
535
536    #[test]
537    fn extract_returns_none_for_non_annotation_attribute() {
538        let attr = AttributeData::Deprecated;
539        let result = extract_annotations_from_attribute(&attr).unwrap();
540        assert!(result.is_none());
541    }
542
543    #[test]
544    fn extract_visible_parameter_annotations() {
545        let param_annotations = vec![ParameterAnnotation {
546            annotations: vec![marker_annotation("javax/annotation/Nullable")],
547        }];
548        let attr = AttributeData::RuntimeVisibleParameterAnnotations(param_annotations);
549
550        let result = extract_parameter_annotations_from_attribute(&attr).unwrap();
551        let stubs = result.expect("should return Some");
552        assert_eq!(stubs.len(), 1);
553        assert_eq!(stubs[0].len(), 1);
554        assert_eq!(stubs[0][0].type_fqn, "javax.annotation.Nullable");
555        assert!(stubs[0][0].is_runtime_visible);
556    }
557
558    #[test]
559    fn extract_invisible_parameter_annotations() {
560        let param_annotations = vec![ParameterAnnotation {
561            annotations: vec![marker_annotation("javax/annotation/Nonnull")],
562        }];
563        let attr = AttributeData::RuntimeInvisibleParameterAnnotations(param_annotations);
564
565        let result = extract_parameter_annotations_from_attribute(&attr).unwrap();
566        let stubs = result.expect("should return Some");
567        assert_eq!(stubs[0][0].type_fqn, "javax.annotation.Nonnull");
568        assert!(!stubs[0][0].is_runtime_visible);
569    }
570
571    // -- Test: Numeric constant types -----------------------------------------
572
573    #[test]
574    fn byte_char_short_boolean_constants_map_to_int() {
575        let values = vec![
576            CafeAnnotationElementValue::ByteConstant(42),
577            CafeAnnotationElementValue::CharConstant(65),
578            CafeAnnotationElementValue::ShortConstant(1000),
579            CafeAnnotationElementValue::BooleanConstant(1),
580        ];
581
582        for v in &values {
583            let result = convert_element_value(v, true);
584            assert!(
585                matches!(result, AnnotationElementValue::Const(ConstantValue::Int(_))),
586                "expected Int variant for {v:?}"
587            );
588        }
589    }
590
591    #[test]
592    fn long_constant() {
593        let v = CafeAnnotationElementValue::LongConstant(i64::MAX);
594        let result = convert_element_value(&v, true);
595        assert!(matches!(
596            result,
597            AnnotationElementValue::Const(ConstantValue::Long(i64::MAX))
598        ));
599    }
600
601    #[test]
602    fn float_constant() {
603        let v = CafeAnnotationElementValue::FloatConstant(std::f32::consts::PI);
604        let result = convert_element_value(&v, true);
605        match result {
606            AnnotationElementValue::Const(ConstantValue::Float(f)) => {
607                assert!((f.0 - std::f32::consts::PI).abs() < f32::EPSILON);
608            }
609            other => panic!("expected Float, got {other:?}"),
610        }
611    }
612
613    #[test]
614    fn double_constant() {
615        let v = CafeAnnotationElementValue::DoubleConstant(std::f64::consts::E);
616        let result = convert_element_value(&v, true);
617        match result {
618            AnnotationElementValue::Const(ConstantValue::Double(d)) => {
619                assert!((d.0 - std::f64::consts::E).abs() < f64::EPSILON);
620            }
621            other => panic!("expected Double, got {other:?}"),
622        }
623    }
624
625    // -- Test: Internal name conversion ---------------------------------------
626
627    #[test]
628    fn internal_name_conversion() {
629        assert_eq!(
630            internal_name_to_fqn("org/springframework/web/bind/annotation/RequestMapping"),
631            "org.springframework.web.bind.annotation.RequestMapping"
632        );
633        assert_eq!(internal_name_to_fqn("java/lang/Object"), "java.lang.Object");
634        assert_eq!(internal_name_to_fqn("Foo"), "Foo");
635    }
636
637    // -- Test: Class literal with primitive descriptor -----------------------
638
639    #[test]
640    fn class_literal_primitive_descriptor() {
641        // Primitive class literals like `int.class` use descriptor "I" etc.
642        let v = CafeAnnotationElementValue::ClassLiteral {
643            class_name: Cow::Borrowed("I"),
644        };
645        let result = convert_element_value(&v, true);
646        assert!(matches!(
647            result,
648            AnnotationElementValue::ClassInfo(ref s) if s == "I"
649        ));
650    }
651
652    // -- Test: Multiple annotations in one attribute -------------------------
653
654    #[test]
655    fn multiple_annotations() {
656        let annotations = vec![
657            marker_annotation("java/lang/Override"),
658            marker_annotation("java/lang/Deprecated"),
659            marker_annotation("java/lang/SuppressWarnings"),
660        ];
661
662        let stubs = convert_annotations(&annotations, false);
663        assert_eq!(stubs.len(), 3);
664        assert_eq!(stubs[0].type_fqn, "java.lang.Override");
665        assert_eq!(stubs[1].type_fqn, "java.lang.Deprecated");
666        assert_eq!(stubs[2].type_fqn, "java.lang.SuppressWarnings");
667        for stub in &stubs {
668            assert!(!stub.is_runtime_visible);
669        }
670    }
671
672    // -- Test: Empty annotation list -----------------------------------------
673
674    #[test]
675    fn empty_annotation_list() {
676        let stubs = convert_annotations(&[], true);
677        assert!(stubs.is_empty());
678    }
679
680    // -- Test: Empty parameter annotation list --------------------------------
681
682    #[test]
683    fn empty_parameter_annotation_list() {
684        let stubs = convert_parameter_annotations(&[], true);
685        assert!(stubs.is_empty());
686    }
687
688    // -- Test: Complex annotation with mixed element types --------------------
689
690    #[test]
691    fn complex_annotation_with_mixed_elements() {
692        let ann = Annotation {
693            type_descriptor: object_descriptor(
694                "org/springframework/web/bind/annotation/RequestMapping",
695            ),
696            elements: vec![
697                CafeAnnotationElement {
698                    name: Cow::Borrowed("value"),
699                    value: CafeAnnotationElementValue::ArrayValue(vec![
700                        CafeAnnotationElementValue::StringConstant(Cow::Borrowed("/api/users")),
701                    ]),
702                },
703                CafeAnnotationElement {
704                    name: Cow::Borrowed("method"),
705                    value: CafeAnnotationElementValue::EnumConstant {
706                        type_name: object_descriptor(
707                            "org/springframework/web/bind/annotation/RequestMethod",
708                        ),
709                        const_name: Cow::Borrowed("GET"),
710                    },
711                },
712                CafeAnnotationElement {
713                    name: Cow::Borrowed("produces"),
714                    value: CafeAnnotationElementValue::ClassLiteral {
715                        class_name: Cow::Borrowed("Ljava/lang/String;"),
716                    },
717                },
718                CafeAnnotationElement {
719                    name: Cow::Borrowed("timeout"),
720                    value: CafeAnnotationElementValue::IntConstant(30),
721                },
722            ],
723        };
724
725        let stubs = convert_annotations(&[ann], true);
726        assert_eq!(stubs.len(), 1);
727        let stub = &stubs[0];
728        assert_eq!(stub.elements.len(), 4);
729
730        // Array element
731        assert!(matches!(
732            &stub.elements[0].value,
733            AnnotationElementValue::Array(items) if items.len() == 1
734        ));
735        // Enum element
736        assert!(matches!(
737            &stub.elements[1].value,
738            AnnotationElementValue::EnumConst { const_name, .. } if const_name == "GET"
739        ));
740        // Class element
741        assert!(matches!(
742            &stub.elements[2].value,
743            AnnotationElementValue::ClassInfo(fqn) if fqn == "java.lang.String"
744        ));
745        // Int element
746        assert!(matches!(
747            &stub.elements[3].value,
748            AnnotationElementValue::Const(ConstantValue::Int(30))
749        ));
750    }
751}