From 48a5bc23a926f2f17631338a63d96d3972838691 Mon Sep 17 00:00:00 2001 From: loki5512344 Date: Sun, 11 Oct 2026 13:02:26 +0200 Subject: [PATCH] classfile,passes: model runtime-visible annotations, rewrite their descriptors --- .../noxis-classfile/src/format/parse/attrs.rs | 18 ++- .../noxis-classfile/src/format/parse/pool.rs | 95 ++++++++++++++ .../src/format/write/annotations.rs | 78 +++++++++++ .../noxis-classfile/src/format/write/attrs.rs | 15 +++ .../noxis-classfile/src/format/write/mod.rs | 1 + crates/noxis-classfile/src/lib.rs | 6 +- .../noxis-classfile/src/model/annotations.rs | 42 ++++++ crates/noxis-classfile/src/model/mod.rs | 7 + .../noxis-classfile/tests/roundtrip/attrs.rs | 121 ++++++++++++++++++ .../src/rename/rewrite/annotations.rs | 41 ++++++ crates/noxis-passes/src/rename/rewrite/mod.rs | 6 + .../src/rename/tests/annotations.rs | 100 +++++++++++++++ crates/noxis-passes/src/rename/tests/mod.rs | 1 + 13 files changed, 527 insertions(+), 4 deletions(-) create mode 100644 crates/noxis-classfile/src/format/write/annotations.rs create mode 100644 crates/noxis-classfile/src/model/annotations.rs create mode 100644 crates/noxis-passes/src/rename/rewrite/annotations.rs create mode 100644 crates/noxis-passes/src/rename/tests/annotations.rs diff --git a/crates/noxis-classfile/src/format/parse/attrs.rs b/crates/noxis-classfile/src/format/parse/attrs.rs index d983318..c99a392 100644 --- a/crates/noxis-classfile/src/format/parse/attrs.rs +++ b/crates/noxis-classfile/src/format/parse/attrs.rs @@ -10,7 +10,7 @@ use ristretto_classfile::ConstantPool; use ristretto_classfile::attributes::Attribute as RtAttr; use super::pool::{ - class_name, const_from, is_interface, kind_code, name_and_type, ref_parts, utf8, + annotation, class_name, const_from, is_interface, kind_code, name_and_type, ref_parts, utf8, }; use crate::error::ClassfileError; use crate::model::{Attribute, BootstrapMethod, InnerClass, MethodParameter, RecordComponent}; @@ -107,6 +107,22 @@ pub(super) fn convert(attr: &RtAttr, pool: &ConstantPool<'_>) -> Result { + Attribute::RuntimeVisibleAnnotations( + annotations + .iter() + .map(|anno| annotation(pool, anno)) + .collect::>()?, + ) + } + RtAttr::RuntimeInvisibleAnnotations { annotations, .. } => { + Attribute::RuntimeInvisibleAnnotations( + annotations + .iter() + .map(|anno| annotation(pool, anno)) + .collect::>()?, + ) + } other => raw(other)?, }) } diff --git a/crates/noxis-classfile/src/format/parse/pool.rs b/crates/noxis-classfile/src/format/parse/pool.rs index 8908611..bda169c 100644 --- a/crates/noxis-classfile/src/format/parse/pool.rs +++ b/crates/noxis-classfile/src/format/parse/pool.rs @@ -4,6 +4,7 @@ use ristretto_classfile::{Constant, ConstantPool, ReferenceKind}; use crate::error::ClassfileError; use crate::insn::{Const, InvokeKind}; +use crate::model::{Annotation, AnnotationValue}; /// The class name from a `CONSTANT_Class`. pub(super) fn class_name(pool: &ConstantPool<'_>, index: u16) -> Result { @@ -182,3 +183,97 @@ pub(super) fn expect_kind( ))) } } + +/// One `annotation` structure → IR (names, not pool indices). +pub(super) fn annotation( + pool: &ConstantPool<'_>, + anno: &ristretto_classfile::attributes::Annotation, +) -> Result { + Ok(Annotation { + descriptor: utf8(pool, anno.type_index)?, + elements: anno + .elements + .iter() + .map(|pair| Ok((utf8(pool, pair.name_index)?, element(pool, &pair.value)?))) + .collect::>()?, + }) +} + +/// One `element_value` → IR, keeping the tag (HotSpot checks it). +fn element( + pool: &ConstantPool<'_>, + value: &ristretto_classfile::attributes::AnnotationElement, +) -> Result { + use ristretto_classfile::attributes::AnnotationElement as E; + Ok(match value { + E::Byte { const_value_index } => { + AnnotationValue::Byte(const_int(pool, *const_value_index)?) + } + E::Char { const_value_index } => { + AnnotationValue::Char(const_int(pool, *const_value_index)?) + } + E::Short { const_value_index } => { + AnnotationValue::Short(const_int(pool, *const_value_index)?) + } + E::Boolean { const_value_index } => { + AnnotationValue::Boolean(const_int(pool, *const_value_index)?) + } + E::Int { const_value_index } => AnnotationValue::Int(const_int(pool, *const_value_index)?), + E::Long { const_value_index } => { + AnnotationValue::Long(const_long(pool, *const_value_index)?) + } + E::Float { const_value_index } => { + AnnotationValue::Float(const_float(pool, *const_value_index)?) + } + E::Double { const_value_index } => { + AnnotationValue::Double(const_double(pool, *const_value_index)?) + } + E::String { const_value_index } => AnnotationValue::String(utf8(pool, *const_value_index)?), + E::Enum { + type_name_index, + const_name_index, + } => AnnotationValue::Enum { + descriptor: utf8(pool, *type_name_index)?, + name: utf8(pool, *const_name_index)?, + }, + E::Class { class_info_index } => AnnotationValue::Class(utf8(pool, *class_info_index)?), + E::Annotation { annotation: nested } => { + AnnotationValue::Nested(Box::new(annotation(pool, nested)?)) + } + E::Array { values } => AnnotationValue::Array( + values + .iter() + .map(|value| element(pool, value)) + .collect::>()?, + ), + }) +} + +/// The `CONSTANT_Integer` behind a `B`/`C`/`S`/`Z`/`I` element. +fn const_int(pool: &ConstantPool<'_>, index: u16) -> Result { + match const_from(pool, index)? { + Const::Int(v) => Ok(v), + _ => Err(ClassfileError::Parse("не Integer".into())), + } +} + +fn const_long(pool: &ConstantPool<'_>, index: u16) -> Result { + match const_from(pool, index)? { + Const::Long(v) => Ok(v), + _ => Err(ClassfileError::Parse("не Long".into())), + } +} + +fn const_float(pool: &ConstantPool<'_>, index: u16) -> Result { + match const_from(pool, index)? { + Const::Float(v) => Ok(v), + _ => Err(ClassfileError::Parse("не Float".into())), + } +} + +fn const_double(pool: &ConstantPool<'_>, index: u16) -> Result { + match const_from(pool, index)? { + Const::Double(v) => Ok(v), + _ => Err(ClassfileError::Parse("не Double".into())), + } +} diff --git a/crates/noxis-classfile/src/format/write/annotations.rs b/crates/noxis-classfile/src/format/write/annotations.rs new file mode 100644 index 0000000..4fab58c --- /dev/null +++ b/crates/noxis-classfile/src/format/write/annotations.rs @@ -0,0 +1,78 @@ +//! Runtime-visible/invisible annotations: IR → backend structures. + +use ristretto_classfile::attributes::{ + Annotation as RtAnno, AnnotationElement as RtElem, AnnotationValuePair as RtPair, +}; + +use super::pool::Pool; +use crate::error::ClassfileError; +use crate::insn::Const; +use crate::model::{Annotation, AnnotationValue}; +/// One IR annotation → backend structure (fresh pool indices). +pub(super) fn annotation(anno: &Annotation, pool: &mut Pool) -> Result { + Ok(RtAnno { + type_index: pool.utf8(&anno.descriptor)?, + elements: anno + .elements + .iter() + .map(|(name, value)| { + Ok(RtPair { + name_index: pool.utf8(name)?, + value: element(value, pool)?, + }) + }) + .collect::>()?, + }) +} + +/// One IR element value → backend structure, tag preserved. +fn element(value: &AnnotationValue, pool: &mut Pool) -> Result { + fn int(v: i32, pool: &mut Pool) -> Result { + pool.constant(&Const::Int(v)) + } + Ok(match value { + AnnotationValue::Byte(v) => RtElem::Byte { + const_value_index: int(*v, pool)?, + }, + AnnotationValue::Char(v) => RtElem::Char { + const_value_index: int(*v, pool)?, + }, + AnnotationValue::Short(v) => RtElem::Short { + const_value_index: int(*v, pool)?, + }, + AnnotationValue::Boolean(v) => RtElem::Boolean { + const_value_index: int(*v, pool)?, + }, + AnnotationValue::Int(v) => RtElem::Int { + const_value_index: int(*v, pool)?, + }, + AnnotationValue::Long(v) => RtElem::Long { + const_value_index: pool.constant(&Const::Long(*v))?, + }, + AnnotationValue::Float(v) => RtElem::Float { + const_value_index: pool.constant(&Const::Float(*v))?, + }, + AnnotationValue::Double(v) => RtElem::Double { + const_value_index: pool.constant(&Const::Double(*v))?, + }, + AnnotationValue::String(v) => RtElem::String { + const_value_index: pool.utf8(v)?, + }, + AnnotationValue::Enum { descriptor, name } => RtElem::Enum { + type_name_index: pool.utf8(descriptor)?, + const_name_index: pool.utf8(name)?, + }, + AnnotationValue::Class(desc) => RtElem::Class { + class_info_index: pool.utf8(desc)?, + }, + AnnotationValue::Nested(nested) => RtElem::Annotation { + annotation: annotation(nested, pool)?, + }, + AnnotationValue::Array(values) => RtElem::Array { + values: values + .iter() + .map(|value| element(value, pool)) + .collect::>()?, + }, + }) +} diff --git a/crates/noxis-classfile/src/format/write/attrs.rs b/crates/noxis-classfile/src/format/write/attrs.rs index 43580b7..246dc15 100644 --- a/crates/noxis-classfile/src/format/write/attrs.rs +++ b/crates/noxis-classfile/src/format/write/attrs.rs @@ -8,6 +8,7 @@ use ristretto_classfile::attributes::{ LocalVariableTypeTable as RtLocalType, Record as RtRecord, }; +use super::annotations::annotation; use super::helpers; use super::pool::Pool; use crate::error::ClassfileError; @@ -178,6 +179,20 @@ pub(super) fn convert(attr: &Attribute, pool: &mut Pool) -> Result>()?, }, + Attribute::RuntimeVisibleAnnotations(annos) => RtAttr::RuntimeVisibleAnnotations { + name_index: pool.utf8("RuntimeVisibleAnnotations")?, + annotations: annos + .iter() + .map(|anno| annotation(anno, pool)) + .collect::>()?, + }, + Attribute::RuntimeInvisibleAnnotations(annos) => RtAttr::RuntimeInvisibleAnnotations { + name_index: pool.utf8("RuntimeInvisibleAnnotations")?, + annotations: annos + .iter() + .map(|anno| annotation(anno, pool)) + .collect::>()?, + }, Attribute::Raw { name, data } => RtAttr::Unknown { name_index: pool.utf8(name)?, info: data.clone(), diff --git a/crates/noxis-classfile/src/format/write/mod.rs b/crates/noxis-classfile/src/format/write/mod.rs index a9ef3fa..a6cdf22 100644 --- a/crates/noxis-classfile/src/format/write/mod.rs +++ b/crates/noxis-classfile/src/format/write/mod.rs @@ -3,6 +3,7 @@ //! The pool is built from scratch by a deterministic walk; the backend recomputes //! instruction numbers into byte offsets and verifies the result (`verify`). +mod annotations; mod attrs; pub(crate) mod code; mod helpers; diff --git a/crates/noxis-classfile/src/lib.rs b/crates/noxis-classfile/src/lib.rs index c5d9cbd..a69879e 100644 --- a/crates/noxis-classfile/src/lib.rs +++ b/crates/noxis-classfile/src/lib.rs @@ -23,9 +23,9 @@ pub use insn::{ }; pub use model::builder::{ClassBuilder, empty_code}; pub use model::{ - Attribute, BootstrapMethod, ClassFile, Code, ExceptionHandler, Field, InnerClass, LineNumber, - LocalVariable, LocalVariableType, Method, MethodParameter, Program, RecordComponent, Resource, - Version, + Annotation, AnnotationValue, Attribute, BootstrapMethod, ClassFile, Code, ExceptionHandler, + Field, InnerClass, LineNumber, LocalVariable, LocalVariableType, Method, MethodParameter, + Program, RecordComponent, Resource, Version, }; /// Safety budget: a method body that would grow past this many instructions diff --git a/crates/noxis-classfile/src/model/annotations.rs b/crates/noxis-classfile/src/model/annotations.rs new file mode 100644 index 0000000..0a5efc2 --- /dev/null +++ b/crates/noxis-classfile/src/model/annotations.rs @@ -0,0 +1,42 @@ +//! Runtime-visible/invisible annotations: names instead of pool indices. +//! +//! Raw `info` blobs go stale the moment the pool is rebuilt on write, and +//! the JVM's reflection parser then dies with `AnnotationFormatError` or +//! reads neighboring attribute bytes as signatures. + +/// One `annotation` structure: the type descriptor + element-value pairs. +#[derive(Debug, Clone, PartialEq)] +pub struct Annotation { + /// The annotation type as a field descriptor (`Lpkg/Ann;`). + pub descriptor: String, + /// `(element name, value)` pairs in class-file order. + pub elements: Vec<(String, AnnotationValue)>, +} + +/// A single `element_value`: one variant per tag, so `B`/`C`/`S`/`Z` +/// keep their identity (the pool stores them all as `CONSTANT_Integer`, +/// but HotSpot checks the tag against the annotation member's type). +#[derive(Debug, Clone, PartialEq)] +pub enum AnnotationValue { + Byte(i32), + Char(i32), + Double(f64), + Float(f32), + Int(i32), + Long(i64), + Short(i32), + Boolean(i32), + /// The `s` tag: a `CONSTANT_Utf8`, not a `CONSTANT_String`. + String(String), + /// The `e` tag: enum type descriptor + constant name. + Enum { + descriptor: String, + name: String, + }, + /// The `c` tag: a class-info descriptor (`Ljava/lang/String;` or `I`). + Class(String), + /// The `@` tag: a nested annotation. + Nested(Box), + /// The `[` tag: array of values. + Array(Vec), +} diff --git a/crates/noxis-classfile/src/model/mod.rs b/crates/noxis-classfile/src/model/mod.rs index 87c2948..4f5548b 100644 --- a/crates/noxis-classfile/src/model/mod.rs +++ b/crates/noxis-classfile/src/model/mod.rs @@ -1,8 +1,10 @@ //! The "program" structure - the unit of work for the pipeline. +pub mod annotations; pub mod builder; pub mod version; +pub use annotations::{Annotation, AnnotationValue}; pub use version::Version; use crate::format::StackMapFrame; @@ -41,6 +43,11 @@ pub enum Attribute { Record(Vec), /// `LocalVariableTypeTable`: generic local signatures (like LVT). LocalVariableTypeTable(Vec), + /// `RuntimeVisibleAnnotations` (JVMS §4.7.16): parsed, not raw bytes - + /// reflection reads them at runtime and raw indices go stale on write. + RuntimeVisibleAnnotations(Vec), + /// `RuntimeInvisibleAnnotations` (JVMS §4.7.17): CLASS-retention ones. + RuntimeInvisibleAnnotations(Vec), /// Everything else: name + raw `info` bytes (careful with pool indices). Raw { name: String, diff --git a/crates/noxis-classfile/tests/roundtrip/attrs.rs b/crates/noxis-classfile/tests/roundtrip/attrs.rs index 1fa776d..2251e84 100644 --- a/crates/noxis-classfile/tests/roundtrip/attrs.rs +++ b/crates/noxis-classfile/tests/roundtrip/attrs.rs @@ -144,3 +144,124 @@ fn permitted_subclasses_round_trip() { let ir = round_trip(&rt); assert_eq!(ir.this_class, "demo/Sealed"); } + +/// Runtime-visible annotations on a method survive the round trip: the +/// pool is rebuilt on write, so raw index blobs would go stale and +/// reflection (`Method.getAnnotation`) would die with +/// `AnnotationFormatError`/`GenericSignatureFormatError`. +#[test] +fn runtime_visible_annotations_round_trip() { + use ristretto_classfile::attributes::{ + Annotation as RtAnno, AnnotationElement as RtElem, AnnotationValuePair as RtPair, + }; + let mut pool = ConstantPool::default(); + let this = pool.add_class("demo/Anno").expect("this"); + let object = pool.add_class("java/lang/Object").expect("object"); + let name = pool.add_utf8("run").expect("name"); + let desc = pool.add_utf8("()V").expect("desc"); + let code_name = pool.add_utf8("Code").expect("code"); + let attr_name = pool.add_utf8("RuntimeVisibleAnnotations").expect("rva"); + let marker = pool.add_utf8("Ldemo/Marker;").expect("marker"); + let value_key = pool.add_utf8("value").expect("value key"); + let hello = pool.add_utf8("hello").expect("hello"); + let klass_key = pool.add_utf8("klass").expect("klass key"); + let target = pool.add_utf8("Ldemo/Target;").expect("target"); + let kind_key = pool.add_utf8("kind").expect("kind key"); + let kind_type = pool.add_utf8("Ldemo/Kind;").expect("kind type"); + let one = pool.add_utf8("ONE").expect("one"); + let nested_key = pool.add_utf8("nested").expect("nested key"); + let nested_type = pool.add_utf8("Ldemo/Nested;").expect("nested type"); + let arr_key = pool.add_utf8("arr").expect("arr key"); + let seven = pool.add_integer(7).expect("seven"); + + let annotation = RtAnno { + type_index: marker, + elements: vec![ + RtPair { + name_index: value_key, + value: RtElem::String { + const_value_index: hello, + }, + }, + RtPair { + name_index: klass_key, + value: RtElem::Class { + class_info_index: target, + }, + }, + RtPair { + name_index: kind_key, + value: RtElem::Enum { + type_name_index: kind_type, + const_name_index: one, + }, + }, + RtPair { + name_index: nested_key, + value: RtElem::Annotation { + annotation: RtAnno { + type_index: nested_type, + elements: vec![], + }, + }, + }, + RtPair { + name_index: arr_key, + value: RtElem::Array { + values: vec![ + RtElem::String { + const_value_index: hello, + }, + RtElem::Int { + const_value_index: seven, + }, + ], + }, + }, + ], + }; + + let rt = RtClass { + version: JAVA_21, + constant_pool: pool, + access_flags: ClassAccessFlags::PUBLIC, + this_class: this, + super_class: object, + interfaces: vec![], + fields: vec![], + methods: vec![RtMethod { + access_flags: MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + name_index: name, + descriptor_index: desc, + attributes: vec![ + RtAttr::Code { + name_index: code_name, + max_stack: 0, + max_locals: 1, + code: vec![Rt::Return], + exception_table: vec![], + attributes: vec![], + }, + RtAttr::RuntimeVisibleAnnotations { + name_index: attr_name, + annotations: vec![annotation], + }, + ], + }], + attributes: vec![], + ..RtClass::default() + }; + + let ir = round_trip(&rt); + let attrs = &ir.methods[0].attributes; + let annos = attrs + .iter() + .find_map(|a| match a { + noxis_classfile::Attribute::RuntimeVisibleAnnotations(list) => Some(list), + _ => None, + }) + .expect("annotations modeled"); + assert_eq!(annos.len(), 1); + assert_eq!(annos[0].descriptor, "Ldemo/Marker;"); + assert_eq!(annos[0].elements.len(), 5); +} diff --git a/crates/noxis-passes/src/rename/rewrite/annotations.rs b/crates/noxis-passes/src/rename/rewrite/annotations.rs new file mode 100644 index 0000000..d5bcefc --- /dev/null +++ b/crates/noxis-passes/src/rename/rewrite/annotations.rs @@ -0,0 +1,41 @@ +//! Annotation rewriting: class descriptors follow the mapping, string +//! values are data and stay put. + +use noxis_classfile::{Annotation, AnnotationValue}; + +use super::super::assign::{RenameMaps, rewrite_descriptor}; + +/// A list of annotations (visible or invisible) - same rewrite for both. +pub(super) fn rewrite_annotations(annotations: &mut [Annotation], maps: &RenameMaps) { + for anno in annotations.iter_mut() { + rewrite_annotation(anno, maps); + } +} + +fn rewrite_annotation(anno: &mut Annotation, maps: &RenameMaps) { + anno.descriptor = rewrite_descriptor(&anno.descriptor, &maps.classes); + for (_, value) in anno.elements.iter_mut() { + rewrite_value(value, maps); + } +} + +fn rewrite_value(value: &mut AnnotationValue, maps: &RenameMaps) { + match value { + // The `'e'` tag: enum type descriptor moves, the constant name does not. + AnnotationValue::Enum { descriptor, .. } => { + *descriptor = rewrite_descriptor(descriptor, &maps.classes); + } + // The `'c'` tag: class-info descriptor (`LFoo;` or a primitive `I`). + AnnotationValue::Class(desc) => { + *desc = rewrite_descriptor(desc, &maps.classes); + } + AnnotationValue::Nested(anno) => rewrite_annotation(anno, maps), + AnnotationValue::Array(values) => { + for value in values.iter_mut() { + rewrite_value(value, maps); + } + } + // Scalars and strings hold no class references. + _ => {} + } +} diff --git a/crates/noxis-passes/src/rename/rewrite/mod.rs b/crates/noxis-passes/src/rename/rewrite/mod.rs index 2287c59..933ca76 100644 --- a/crates/noxis-passes/src/rename/rewrite/mod.rs +++ b/crates/noxis-passes/src/rename/rewrite/mod.rs @@ -1,7 +1,9 @@ //! Переписывание одного класса: имена, дескрипторы, код, атрибуты. +mod annotations; mod code; +use annotations::rewrite_annotations; use noxis_classfile::{Attribute, ClassFile, Const, StackMapFrame, VerificationType}; use noxis_core::{Config, Context}; @@ -141,6 +143,10 @@ fn rewrite_attributes( } } } + Attribute::RuntimeVisibleAnnotations(annos) + | Attribute::RuntimeInvisibleAnnotations(annos) => { + rewrite_annotations(annos, maps); + } Attribute::BootstrapMethods(methods) => { for bsm in methods.iter_mut() { rewrite_method_ref(&mut bsm.owner, &mut bsm.name, &mut bsm.descriptor, maps); diff --git a/crates/noxis-passes/src/rename/tests/annotations.rs b/crates/noxis-passes/src/rename/tests/annotations.rs new file mode 100644 index 0000000..63277b1 --- /dev/null +++ b/crates/noxis-passes/src/rename/tests/annotations.rs @@ -0,0 +1,100 @@ +//! Annotation rewriting: descriptors follow the mapping, values do not. + +#![allow(clippy::expect_used, clippy::unwrap_used)] + +use super::run_rename; +use noxis_classfile::{Annotation, AnnotationValue, Attribute, ClassBuilder, Version}; + +/// Class references inside annotations live as descriptors: a stale one +/// makes the JVM's reflection parser fail (`AnnotationFormatError`) or +/// resolve a ghost type. String *values* are data and stay put. +#[test] +fn annotation_descriptors_follow_the_mapping() { + let mut program = super::sample(); + program.classes.push( + ClassBuilder::new("app/Annotated", Some("java/lang/Object")) + .version(Version::JAVA_21) + .method_with_code(0x000a, "go", "()V", noxis_classfile::empty_code(0)) + .build(), + ); + let annotated = program + .classes + .iter_mut() + .find(|c| c.this_class == "app/Annotated") + .expect("annotated"); + annotated + .attributes + .push(Attribute::RuntimeVisibleAnnotations(vec![Annotation { + descriptor: "Lapp/Helper;".to_string(), + elements: vec![ + ( + "kind".to_string(), + AnnotationValue::Enum { + descriptor: "Lapp/Helper;".to_string(), + name: "ONE".to_string(), + }, + ), + ( + "klass".to_string(), + AnnotationValue::Class("Lapp/Helper;".to_string()), + ), + ( + "nested".to_string(), + AnnotationValue::Nested(Box::new(Annotation { + descriptor: "Lapp/Helper;".to_string(), + elements: vec![( + "note".to_string(), + AnnotationValue::String("app/Helper".to_string()), + )], + })), + ), + ( + "arr".to_string(), + AnnotationValue::Array(vec![AnnotationValue::Class( + "[Lapp/Helper;".to_string(), + )]), + ), + ], + }])); + let ctx = run_rename(&mut program); + let helper = ctx.mapping.class("app/Helper").expect("renamed"); + let annotated_name = ctx.mapping.class("app/Annotated").expect("annotated"); + let class = program.class(annotated_name).expect("class"); + let annos = class + .attributes + .iter() + .find_map(|a| match a { + Attribute::RuntimeVisibleAnnotations(list) => Some(list), + _ => None, + }) + .expect("annotations"); + let anno = &annos[0]; + assert_eq!(anno.descriptor, format!("L{helper};")); + let expect_desc = format!("L{helper};"); + assert_eq!( + anno.elements[0].1, + AnnotationValue::Enum { + descriptor: expect_desc.clone(), + name: "ONE".to_string(), + } + ); + assert_eq!( + anno.elements[1].1, + AnnotationValue::Class(expect_desc.clone()) + ); + match &anno.elements[2].1 { + AnnotationValue::Nested(nested) => { + assert_eq!(nested.descriptor, expect_desc); + assert_eq!( + nested.elements[0].1, + AnnotationValue::String("app/Helper".to_string()), + "string values are data, not references" + ); + } + other => panic!("nested expected, got {other:?}"), + } + assert_eq!( + anno.elements[3].1, + AnnotationValue::Array(vec![AnnotationValue::Class(format!("[L{helper};"))]) + ); +} diff --git a/crates/noxis-passes/src/rename/tests/mod.rs b/crates/noxis-passes/src/rename/tests/mod.rs index 695c972..1a5274f 100644 --- a/crates/noxis-passes/src/rename/tests/mod.rs +++ b/crates/noxis-passes/src/rename/tests/mod.rs @@ -13,6 +13,7 @@ use noxis_core::{Config, Context, Pass, RenameConfig}; use super::RenamePass; use super::assign::{RenameMaps, rewrite_descriptor}; +mod annotations; mod enclosing; mod rewrite;