1use 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
30fn internal_name_to_fqn(internal: &str) -> String {
37 internal.replace('/', ".")
38}
39
40fn 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
59fn 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 let fqn = class_name.replace('/', ".");
100 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
119fn 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
136fn 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#[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#[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
191pub 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
215pub 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#[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 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 fn marker_annotation(internal_name: &str) -> Annotation<'_> {
263 Annotation {
264 type_descriptor: object_descriptor(internal_name),
265 elements: vec![],
266 }
267 }
268
269 #[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]
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]
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]
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]
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]
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]
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 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 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 assert!(result[2].is_empty());
509 }
510
511 #[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]
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]
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]
640 fn class_literal_primitive_descriptor() {
641 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]
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]
675 fn empty_annotation_list() {
676 let stubs = convert_annotations(&[], true);
677 assert!(stubs.is_empty());
678 }
679
680 #[test]
683 fn empty_parameter_annotation_list() {
684 let stubs = convert_parameter_annotations(&[], true);
685 assert!(stubs.is_empty());
686 }
687
688 #[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 assert!(matches!(
732 &stub.elements[0].value,
733 AnnotationElementValue::Array(items) if items.len() == 1
734 ));
735 assert!(matches!(
737 &stub.elements[1].value,
738 AnnotationElementValue::EnumConst { const_name, .. } if const_name == "GET"
739 ));
740 assert!(matches!(
742 &stub.elements[2].value,
743 AnnotationElementValue::ClassInfo(fqn) if fqn == "java.lang.String"
744 ));
745 assert!(matches!(
747 &stub.elements[3].value,
748 AnnotationElementValue::Const(ConstantValue::Int(30))
749 ));
750 }
751}