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
191#[allow(clippy::missing_errors_doc)] pub 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#[allow(clippy::missing_errors_doc)] pub 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#[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 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 fn marker_annotation(internal_name: &str) -> Annotation<'_> {
265 Annotation {
266 type_descriptor: object_descriptor(internal_name),
267 elements: vec![],
268 }
269 }
270
271 #[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]
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]
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]
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]
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]
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]
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 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 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 assert!(result[2].is_empty());
511 }
512
513 #[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]
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]
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]
642 fn class_literal_primitive_descriptor() {
643 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]
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]
677 fn empty_annotation_list() {
678 let stubs = convert_annotations(&[], true);
679 assert!(stubs.is_empty());
680 }
681
682 #[test]
685 fn empty_parameter_annotation_list() {
686 let stubs = convert_parameter_annotations(&[], true);
687 assert!(stubs.is_empty());
688 }
689
690 #[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 assert!(matches!(
734 &stub.elements[0].value,
735 AnnotationElementValue::Array(items) if items.len() == 1
736 ));
737 assert!(matches!(
739 &stub.elements[1].value,
740 AnnotationElementValue::EnumConst { const_name, .. } if const_name == "GET"
741 ));
742 assert!(matches!(
744 &stub.elements[2].value,
745 AnnotationElementValue::ClassInfo(fqn) if fqn == "java.lang.String"
746 ));
747 assert!(matches!(
749 &stub.elements[3].value,
750 AnnotationElementValue::Const(ConstantValue::Int(30))
751 ));
752 }
753}