#![allow(clippy::cast_possible_truncation)]
use cafebabe::attributes::AttributeData;
use cafebabe::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags, ParseOptions};
use crate::stub::model::{
AccessFlags, ClassKind, ClassStub, FieldStub, InnerClassEntry, MethodStub, RecordComponent,
};
use crate::{ClasspathError, ClasspathResult};
use super::constants::{
class_name_to_fqn, extract_constant_value, extract_method_parameter_names, extract_source_file,
method_descriptor_to_types,
};
const METHOD_ACC_BRIDGE: u16 = 0x0040;
const METHOD_ACC_SYNTHETIC: u16 = 0x1000;
pub fn parse_class(bytes: &[u8]) -> ClasspathResult<ClassStub> {
let mut opts = ParseOptions::default();
opts.parse_bytecode(false);
let class_file = cafebabe::parse_class_with_options(bytes, &opts).map_err(|e| {
ClasspathError::BytecodeParseError {
class_name: String::from("<unknown>"),
reason: e.to_string(),
}
})?;
let this_class_raw = class_file.this_class.to_string();
if this_class_raw.ends_with("module-info") || this_class_raw.ends_with("package-info") {
return Err(ClasspathError::BytecodeParseError {
class_name: class_name_to_fqn(&this_class_raw),
reason: "module-info and package-info classes are handled by U07b".to_owned(),
});
}
let fqn = class_name_to_fqn(&this_class_raw);
let name = extract_simple_name(&fqn);
let access = convert_class_access_flags(class_file.access_flags);
let kind = determine_class_kind(class_file.access_flags, &class_file.attributes);
let superclass = class_file
.super_class
.as_ref()
.map(|sc| class_name_to_fqn(sc));
let interfaces: Vec<String> = class_file
.interfaces
.iter()
.map(|i| class_name_to_fqn(i))
.collect();
let methods = parse_methods(&class_file.methods, &fqn);
let fields = parse_fields(&class_file.fields, &fqn);
let enum_constants = if kind == ClassKind::Enum {
extract_enum_constants(&class_file.fields, &this_class_raw)
} else {
vec![]
};
let inner_classes = extract_inner_classes(&class_file.attributes);
let record_components = extract_record_components(&class_file.attributes);
let source_file = extract_source_file(&class_file.attributes);
Ok(ClassStub {
fqn,
name,
kind,
access,
superclass,
interfaces,
methods,
fields,
annotations: vec![], generic_signature: None, inner_classes,
lambda_targets: vec![], module: None, record_components,
enum_constants,
source_file,
source_jar: None, kotlin_metadata: None, scala_signature: None, })
}
fn extract_simple_name(fqn: &str) -> String {
fqn.rsplit('.').next().unwrap_or(fqn).to_owned()
}
fn convert_class_access_flags(flags: ClassAccessFlags) -> AccessFlags {
AccessFlags::new(flags.bits())
}
fn convert_method_access_flags(flags: MethodAccessFlags) -> AccessFlags {
AccessFlags::new(flags.bits())
}
fn convert_field_access_flags(flags: FieldAccessFlags) -> AccessFlags {
AccessFlags::new(flags.bits())
}
fn determine_class_kind(
flags: ClassAccessFlags,
attrs: &[cafebabe::attributes::AttributeInfo<'_>],
) -> ClassKind {
if flags.contains(ClassAccessFlags::MODULE) {
return ClassKind::Module;
}
if flags.contains(ClassAccessFlags::ANNOTATION) && flags.contains(ClassAccessFlags::INTERFACE) {
return ClassKind::Annotation;
}
if flags.contains(ClassAccessFlags::ENUM) {
return ClassKind::Enum;
}
if flags.contains(ClassAccessFlags::INTERFACE) {
return ClassKind::Interface;
}
let has_record = attrs
.iter()
.any(|a| matches!(&a.data, AttributeData::Record(_)));
if has_record {
return ClassKind::Record;
}
ClassKind::Class
}
fn parse_methods(methods: &[cafebabe::MethodInfo<'_>], class_fqn: &str) -> Vec<MethodStub> {
let mut result = Vec::with_capacity(methods.len());
for method in methods {
let raw_bits = method.access_flags.bits();
if raw_bits & METHOD_ACC_BRIDGE != 0 || raw_bits & METHOD_ACC_SYNTHETIC != 0 {
continue;
}
match parse_single_method(method) {
Ok(stub) => result.push(stub),
Err(e) => {
log::warn!(
"Skipping method '{}' in class '{}': {}",
method.name,
class_fqn,
e
);
}
}
}
result
}
#[allow(clippy::unnecessary_debug_formatting)] #[allow(clippy::unnecessary_wraps)] fn parse_single_method(method: &cafebabe::MethodInfo<'_>) -> ClasspathResult<MethodStub> {
let name = method.name.to_string();
let access = convert_method_access_flags(method.access_flags);
let descriptor = method.descriptor.to_string();
let (parameter_types, return_type) = method_descriptor_to_types(&method.descriptor);
let parameter_names = extract_method_parameter_names(&method.attributes);
Ok(MethodStub {
name,
access,
descriptor,
generic_signature: None, annotations: vec![], parameter_annotations: vec![], parameter_names,
return_type,
parameter_types,
})
}
fn parse_fields(fields: &[cafebabe::FieldInfo<'_>], class_fqn: &str) -> Vec<FieldStub> {
let mut result = Vec::with_capacity(fields.len());
for field in fields {
match parse_single_field(field) {
Ok(stub) => result.push(stub),
Err(e) => {
log::warn!(
"Skipping field '{}' in class '{}': {}",
field.name,
class_fqn,
e
);
}
}
}
result
}
#[allow(clippy::unnecessary_wraps)] fn parse_single_field(field: &cafebabe::FieldInfo<'_>) -> ClasspathResult<FieldStub> {
let name = field.name.to_string();
let access = convert_field_access_flags(field.access_flags);
let descriptor = field.descriptor.to_string();
let constant_value = if access.is_static() && access.is_final() {
extract_constant_value(&field.attributes)
} else {
None
};
Ok(FieldStub {
name,
access,
descriptor,
generic_signature: None, annotations: vec![], constant_value,
})
}
fn extract_enum_constants(
fields: &[cafebabe::FieldInfo<'_>],
this_class_internal: &str,
) -> Vec<String> {
let expected_descriptor = format!("L{this_class_internal};");
fields
.iter()
.filter(|f| {
let bits = f.access_flags.bits();
bits & FieldAccessFlags::STATIC.bits() != 0
&& bits & FieldAccessFlags::FINAL.bits() != 0
&& bits & FieldAccessFlags::ENUM.bits() != 0
&& f.descriptor.to_string() == expected_descriptor
})
.map(|f| f.name.to_string())
.collect()
}
fn extract_inner_classes(
attrs: &[cafebabe::attributes::AttributeInfo<'_>],
) -> Vec<InnerClassEntry> {
let mut result = Vec::new();
for attr in attrs {
if let AttributeData::InnerClasses(entries) = &attr.data {
for entry in entries {
result.push(InnerClassEntry {
inner_fqn: class_name_to_fqn(&entry.inner_class_info),
outer_fqn: entry
.outer_class_info
.as_ref()
.map(|o| class_name_to_fqn(o)),
inner_name: entry
.inner_name
.as_ref()
.map(std::string::ToString::to_string),
access: AccessFlags::new(entry.access_flags.bits()),
});
}
}
}
result
}
fn extract_record_components(
attrs: &[cafebabe::attributes::AttributeInfo<'_>],
) -> Vec<RecordComponent> {
let mut result = Vec::new();
for attr in attrs {
if let AttributeData::Record(components) = &attr.data {
for comp in components {
result.push(RecordComponent {
name: comp.name.to_string(),
descriptor: comp.descriptor.to_string(),
generic_signature: None, annotations: vec![], });
}
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use crate::stub::model::{BaseType, ConstantValue, TypeSignature};
struct ClassFileBuilder {
major_version: u16,
cp_entries: Vec<Vec<u8>>,
access_flags: u16,
this_class_idx: u16,
super_class_idx: u16,
interface_indices: Vec<u16>,
fields: Vec<Vec<u8>>,
methods: Vec<Vec<u8>>,
attributes: Vec<Vec<u8>>,
}
impl ClassFileBuilder {
fn new(class_name: &str) -> Self {
let mut builder = Self {
major_version: 52,
cp_entries: Vec::new(),
access_flags: 0x0021, this_class_idx: 0,
super_class_idx: 0,
interface_indices: Vec::new(),
fields: Vec::new(),
methods: Vec::new(),
attributes: Vec::new(),
};
let class_name_idx = builder.add_utf8(class_name);
builder.this_class_idx = builder.add_class(class_name_idx);
let object_name_idx = builder.add_utf8("java/lang/Object");
builder.super_class_idx = builder.add_class(object_name_idx);
builder
}
fn add_utf8(&mut self, s: &str) -> u16 {
let mut entry = vec![1u8]; let bytes = s.as_bytes();
entry.extend_from_slice(&(bytes.len() as u16).to_be_bytes());
entry.extend_from_slice(bytes);
self.cp_entries.push(entry);
self.cp_entries.len() as u16
}
fn add_class(&mut self, name_idx: u16) -> u16 {
let mut entry = vec![7u8]; entry.extend_from_slice(&name_idx.to_be_bytes());
self.cp_entries.push(entry);
self.cp_entries.len() as u16
}
fn add_integer(&mut self, value: i32) -> u16 {
let mut entry = vec![3u8]; entry.extend_from_slice(&value.to_be_bytes());
self.cp_entries.push(entry);
self.cp_entries.len() as u16
}
fn add_string(&mut self, utf8_idx: u16) -> u16 {
let mut entry = vec![8u8]; entry.extend_from_slice(&utf8_idx.to_be_bytes());
self.cp_entries.push(entry);
self.cp_entries.len() as u16
}
fn access_flags(mut self, flags: u16) -> Self {
self.access_flags = flags;
self
}
fn no_superclass(mut self) -> Self {
self.super_class_idx = 0;
self
}
fn superclass(mut self, name: &str) -> Self {
let name_idx = self.add_utf8(name);
self.super_class_idx = self.add_class(name_idx);
self
}
fn add_interface(&mut self, name: &str) -> &mut Self {
let name_idx = self.add_utf8(name);
let class_idx = self.add_class(name_idx);
self.interface_indices.push(class_idx);
self
}
fn add_field(
&mut self,
name: &str,
descriptor: &str,
access_flags: u16,
constant_value_cp_idx: Option<u16>,
) -> &mut Self {
let name_idx = self.add_utf8(name);
let desc_idx = self.add_utf8(descriptor);
let mut field_bytes = Vec::new();
field_bytes.extend_from_slice(&access_flags.to_be_bytes());
field_bytes.extend_from_slice(&name_idx.to_be_bytes());
field_bytes.extend_from_slice(&desc_idx.to_be_bytes());
if let Some(cv_idx) = constant_value_cp_idx {
let attr_name_idx = self.add_utf8("ConstantValue");
field_bytes.extend_from_slice(&1u16.to_be_bytes()); field_bytes.extend_from_slice(&attr_name_idx.to_be_bytes());
field_bytes.extend_from_slice(&2u32.to_be_bytes()); field_bytes.extend_from_slice(&cv_idx.to_be_bytes());
} else {
field_bytes.extend_from_slice(&0u16.to_be_bytes()); }
self.fields.push(field_bytes);
self
}
fn add_method(&mut self, name: &str, descriptor: &str, access_flags: u16) -> &mut Self {
let name_idx = self.add_utf8(name);
let desc_idx = self.add_utf8(descriptor);
let mut method_bytes = Vec::new();
method_bytes.extend_from_slice(&access_flags.to_be_bytes());
method_bytes.extend_from_slice(&name_idx.to_be_bytes());
method_bytes.extend_from_slice(&desc_idx.to_be_bytes());
method_bytes.extend_from_slice(&0u16.to_be_bytes());
self.methods.push(method_bytes);
self
}
fn add_method_with_params(
&mut self,
name: &str,
descriptor: &str,
access_flags: u16,
param_names: &[&str],
) -> &mut Self {
let name_idx = self.add_utf8(name);
let desc_idx = self.add_utf8(descriptor);
let mut method_bytes = Vec::new();
method_bytes.extend_from_slice(&access_flags.to_be_bytes());
method_bytes.extend_from_slice(&name_idx.to_be_bytes());
method_bytes.extend_from_slice(&desc_idx.to_be_bytes());
let attr_name_idx = self.add_utf8("MethodParameters");
let param_name_indices: Vec<u16> =
param_names.iter().map(|pn| self.add_utf8(pn)).collect();
method_bytes.extend_from_slice(&1u16.to_be_bytes());
method_bytes.extend_from_slice(&attr_name_idx.to_be_bytes());
let attr_length = 1 + param_name_indices.len() as u32 * 4;
method_bytes.extend_from_slice(&attr_length.to_be_bytes());
method_bytes.push(param_name_indices.len() as u8);
for idx in ¶m_name_indices {
method_bytes.extend_from_slice(&idx.to_be_bytes());
method_bytes.extend_from_slice(&0u16.to_be_bytes()); }
self.methods.push(method_bytes);
self
}
fn add_inner_classes_attribute(
&mut self,
entries: &[(&str, Option<&str>, Option<&str>, u16)],
) -> &mut Self {
let attr_name_idx = self.add_utf8("InnerClasses");
let mut attr_data = Vec::new();
attr_data.extend_from_slice(&(entries.len() as u16).to_be_bytes());
for (inner, outer, inner_name, flags) in entries {
let inner_name_idx = self.add_utf8(inner);
let inner_class_idx = self.add_class(inner_name_idx);
attr_data.extend_from_slice(&inner_class_idx.to_be_bytes());
if let Some(outer_name) = outer {
let outer_name_idx = self.add_utf8(outer_name);
let outer_class_idx = self.add_class(outer_name_idx);
attr_data.extend_from_slice(&outer_class_idx.to_be_bytes());
} else {
attr_data.extend_from_slice(&0u16.to_be_bytes());
}
if let Some(name) = inner_name {
let name_idx = self.add_utf8(name);
attr_data.extend_from_slice(&name_idx.to_be_bytes());
} else {
attr_data.extend_from_slice(&0u16.to_be_bytes());
}
attr_data.extend_from_slice(&flags.to_be_bytes());
}
let mut attr_bytes = Vec::new();
attr_bytes.extend_from_slice(&attr_name_idx.to_be_bytes());
attr_bytes.extend_from_slice(&(attr_data.len() as u32).to_be_bytes());
attr_bytes.extend_from_slice(&attr_data);
self.attributes.push(attr_bytes);
self
}
fn add_source_file_attribute(&mut self, source_file: &str) -> &mut Self {
let attr_name_idx = self.add_utf8("SourceFile");
let source_idx = self.add_utf8(source_file);
let mut attr_bytes = Vec::new();
attr_bytes.extend_from_slice(&attr_name_idx.to_be_bytes());
attr_bytes.extend_from_slice(&2u32.to_be_bytes()); attr_bytes.extend_from_slice(&source_idx.to_be_bytes());
self.attributes.push(attr_bytes);
self
}
fn build(&self) -> Vec<u8> {
let mut bytes = Vec::new();
bytes.extend_from_slice(&0xCAFE_BABEu32.to_be_bytes());
bytes.extend_from_slice(&0u16.to_be_bytes());
bytes.extend_from_slice(&self.major_version.to_be_bytes());
let cp_count = self.cp_entries.len() as u16 + 1;
bytes.extend_from_slice(&cp_count.to_be_bytes());
for entry in &self.cp_entries {
bytes.extend_from_slice(entry);
}
bytes.extend_from_slice(&self.access_flags.to_be_bytes());
bytes.extend_from_slice(&self.this_class_idx.to_be_bytes());
bytes.extend_from_slice(&self.super_class_idx.to_be_bytes());
bytes.extend_from_slice(&(self.interface_indices.len() as u16).to_be_bytes());
for idx in &self.interface_indices {
bytes.extend_from_slice(&idx.to_be_bytes());
}
bytes.extend_from_slice(&(self.fields.len() as u16).to_be_bytes());
for field in &self.fields {
bytes.extend_from_slice(field);
}
bytes.extend_from_slice(&(self.methods.len() as u16).to_be_bytes());
for method in &self.methods {
bytes.extend_from_slice(method);
}
bytes.extend_from_slice(&(self.attributes.len() as u16).to_be_bytes());
for attr in &self.attributes {
bytes.extend_from_slice(attr);
}
bytes
}
}
#[test]
fn test_parse_minimal_class() {
let bytes = ClassFileBuilder::new("com/example/Minimal").build();
let stub = parse_class(&bytes).unwrap();
assert_eq!(stub.fqn, "com.example.Minimal");
assert_eq!(stub.name, "Minimal");
assert_eq!(stub.kind, ClassKind::Class);
assert!(stub.access.is_public());
assert_eq!(stub.superclass.as_deref(), Some("java.lang.Object"));
assert!(stub.interfaces.is_empty());
assert!(stub.methods.is_empty());
assert!(stub.fields.is_empty());
assert!(stub.inner_classes.is_empty());
assert!(stub.enum_constants.is_empty());
assert!(stub.record_components.is_empty());
}
#[test]
fn test_parse_class_with_methods_and_fields() {
let mut builder = ClassFileBuilder::new("com/example/MyClass");
builder.add_method("toString", "()Ljava/lang/String;", 0x0001); builder.add_method("hashCode", "()I", 0x0001); builder.add_field("name", "Ljava/lang/String;", 0x0002, None); builder.add_field("age", "I", 0x0001, None);
let bytes = builder.build();
let stub = parse_class(&bytes).unwrap();
assert_eq!(stub.methods.len(), 2);
assert_eq!(stub.methods[0].name, "toString");
assert_eq!(stub.methods[0].descriptor, "()Ljava/lang/String;");
assert!(stub.methods[0].access.is_public());
assert_eq!(stub.methods[1].name, "hashCode");
assert_eq!(stub.fields.len(), 2);
assert_eq!(stub.fields[0].name, "name");
assert!(stub.fields[0].access.is_private());
assert_eq!(stub.fields[1].name, "age");
assert_eq!(stub.fields[1].descriptor, "I");
}
#[test]
fn test_parse_enum_class() {
let mut builder = ClassFileBuilder::new("com/example/Color");
builder = builder.access_flags(0x0001 | 0x0010 | 0x0020 | 0x4000);
builder = builder.superclass("java/lang/Enum");
builder.add_field("RED", "Lcom/example/Color;", 0x4019, None);
builder.add_field("GREEN", "Lcom/example/Color;", 0x4019, None);
builder.add_field("BLUE", "Lcom/example/Color;", 0x4019, None);
builder.add_field("rgb", "I", 0x0012, None);
let bytes = builder.build();
let stub = parse_class(&bytes).unwrap();
assert_eq!(stub.kind, ClassKind::Enum);
assert_eq!(stub.enum_constants, vec!["RED", "GREEN", "BLUE"]);
assert_eq!(stub.superclass.as_deref(), Some("java.lang.Enum"));
}
#[test]
fn test_parse_interface() {
let builder = ClassFileBuilder::new("com/example/Readable").access_flags(0x0601); let bytes = builder.build();
let stub = parse_class(&bytes).unwrap();
assert_eq!(stub.kind, ClassKind::Interface);
assert!(stub.access.is_interface());
assert!(stub.access.is_abstract());
}
#[test]
fn test_parse_class_with_inner_classes() {
let mut builder = ClassFileBuilder::new("com/example/Outer");
builder.add_inner_classes_attribute(&[
(
"com/example/Outer$Inner",
Some("com/example/Outer"),
Some("Inner"),
0x0001, ),
(
"com/example/Outer$1",
None, None, 0x0000, ),
]);
let bytes = builder.build();
let stub = parse_class(&bytes).unwrap();
assert_eq!(stub.inner_classes.len(), 2);
assert_eq!(stub.inner_classes[0].inner_fqn, "com.example.Outer$Inner");
assert_eq!(
stub.inner_classes[0].outer_fqn.as_deref(),
Some("com.example.Outer")
);
assert_eq!(stub.inner_classes[0].inner_name.as_deref(), Some("Inner"));
assert!(stub.inner_classes[0].access.is_public());
assert_eq!(stub.inner_classes[1].inner_fqn, "com.example.Outer$1");
assert!(stub.inner_classes[1].outer_fqn.is_none());
assert!(stub.inner_classes[1].inner_name.is_none());
}
#[test]
fn test_parse_class_with_constant_fields() {
let mut builder = ClassFileBuilder::new("com/example/Constants");
let int_idx = builder.add_integer(42);
builder.add_field("MAX_SIZE", "I", 0x0019, Some(int_idx));
let str_utf8_idx = builder.add_utf8("hello");
let str_idx = builder.add_string(str_utf8_idx);
builder.add_field("DEFAULT_NAME", "Ljava/lang/String;", 0x0019, Some(str_idx));
builder.add_field("mutable", "I", 0x0001, None);
let bytes = builder.build();
let stub = parse_class(&bytes).unwrap();
assert_eq!(stub.fields.len(), 3);
assert_eq!(stub.fields[0].name, "MAX_SIZE");
assert_eq!(stub.fields[0].constant_value, Some(ConstantValue::Int(42)));
assert_eq!(stub.fields[1].name, "DEFAULT_NAME");
assert_eq!(
stub.fields[1].constant_value,
Some(ConstantValue::String("hello".to_owned()))
);
assert!(stub.fields[2].constant_value.is_none());
}
#[test]
fn test_synthetic_methods_filtered() {
let mut builder = ClassFileBuilder::new("com/example/Filtered");
builder.add_method("realMethod", "()V", 0x0001); builder.add_method("access$000", "()V", METHOD_ACC_SYNTHETIC); builder.add_method("bridge$0", "()V", METHOD_ACC_BRIDGE);
let bytes = builder.build();
let stub = parse_class(&bytes).unwrap();
assert_eq!(stub.methods.len(), 1);
assert_eq!(stub.methods[0].name, "realMethod");
}
#[test]
fn test_bridge_and_synthetic_combined_filtered() {
let mut builder = ClassFileBuilder::new("com/example/BridgeSynthetic");
builder.add_method("realMethod", "()V", 0x0001);
builder.add_method("combined", "()V", METHOD_ACC_BRIDGE | METHOD_ACC_SYNTHETIC);
let bytes = builder.build();
let stub = parse_class(&bytes).unwrap();
assert_eq!(stub.methods.len(), 1);
assert_eq!(stub.methods[0].name, "realMethod");
}
#[test]
fn test_malformed_bytes_returns_error() {
assert!(parse_class(&[]).is_err());
assert!(parse_class(&[0xDE, 0xAD, 0xBE, 0xEF]).is_err());
assert!(parse_class(&[0xCA, 0xFE, 0xBA, 0xBE, 0x00, 0x00, 0x00, 0x34]).is_err());
assert!(parse_class(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]).is_err());
}
#[test]
fn test_method_descriptor_parsing_produces_correct_types() {
let mut builder = ClassFileBuilder::new("com/example/Types");
builder.add_method("process", "(ILjava/lang/String;[D)V", 0x0001);
let bytes = builder.build();
let stub = parse_class(&bytes).unwrap();
assert_eq!(stub.methods.len(), 1);
let method = &stub.methods[0];
assert_eq!(method.parameter_types.len(), 3);
assert!(matches!(
method.parameter_types[0],
TypeSignature::Base(BaseType::Int)
));
match &method.parameter_types[1] {
TypeSignature::Class { fqn, .. } => assert_eq!(fqn, "java.lang.String"),
other => panic!("Expected Class, got {other:?}"),
}
match &method.parameter_types[2] {
TypeSignature::Array(inner) => {
assert!(matches!(
inner.as_ref(),
TypeSignature::Base(BaseType::Double)
));
}
other => panic!("Expected Array, got {other:?}"),
}
assert!(matches!(
method.return_type,
TypeSignature::Base(BaseType::Void)
));
}
#[test]
fn test_access_flags_combinations() {
let builder = ClassFileBuilder::new("com/example/Abstract").access_flags(0x0421); let bytes = builder.build();
let stub = parse_class(&bytes).unwrap();
assert!(stub.access.is_public());
assert!(stub.access.is_abstract());
let builder = ClassFileBuilder::new("com/example/Final").access_flags(0x0031); let bytes = builder.build();
let stub = parse_class(&bytes).unwrap();
assert!(stub.access.is_public());
assert!(stub.access.is_final());
}
#[test]
fn test_class_with_interfaces() {
let mut builder = ClassFileBuilder::new("com/example/MyList");
builder.add_interface("java/io/Serializable");
builder.add_interface("java/lang/Comparable");
let bytes = builder.build();
let stub = parse_class(&bytes).unwrap();
assert_eq!(stub.interfaces.len(), 2);
assert_eq!(stub.interfaces[0], "java.io.Serializable");
assert_eq!(stub.interfaces[1], "java.lang.Comparable");
}
#[test]
fn test_source_file_attribute() {
let mut builder = ClassFileBuilder::new("com/example/WithSource");
builder.add_source_file_attribute("WithSource.java");
let bytes = builder.build();
let stub = parse_class(&bytes).unwrap();
assert_eq!(stub.source_file.as_deref(), Some("WithSource.java"));
}
#[test]
fn test_method_with_parameter_names() {
let mut builder = ClassFileBuilder::new("com/example/Params");
builder.add_method_with_params(
"greet",
"(Ljava/lang/String;I)V",
0x0001, &["name", "count"],
);
let bytes = builder.build();
let stub = parse_class(&bytes).unwrap();
assert_eq!(stub.methods.len(), 1);
assert_eq!(stub.methods[0].parameter_names, vec!["name", "count"]);
}
#[test]
fn test_annotation_type() {
let builder = ClassFileBuilder::new("com/example/MyAnnotation").access_flags(0x2601);
let bytes = builder.build();
let stub = parse_class(&bytes).unwrap();
assert_eq!(stub.kind, ClassKind::Annotation);
}
#[test]
fn test_module_info_skipped() {
let builder = ClassFileBuilder::new("module-info");
let bytes = builder.build();
let result = parse_class(&bytes);
assert!(result.is_err());
}
#[test]
fn test_package_info_skipped() {
let builder = ClassFileBuilder::new("com/example/package-info");
let bytes = builder.build();
let result = parse_class(&bytes);
assert!(result.is_err());
}
#[test]
fn test_simple_name_extraction() {
assert_eq!(extract_simple_name("java.util.HashMap"), "HashMap");
assert_eq!(extract_simple_name("SimpleClass"), "SimpleClass");
assert_eq!(extract_simple_name("java.util.Map.Entry"), "Entry");
}
#[test]
fn test_no_superclass_for_object() {
let builder = ClassFileBuilder::new("java/lang/Object").no_superclass();
let bytes = builder.build();
let stub = parse_class(&bytes).unwrap();
assert!(stub.superclass.is_none());
}
#[test]
fn test_constructor_and_static_init() {
let mut builder = ClassFileBuilder::new("com/example/WithInit");
builder.add_method("<init>", "()V", 0x0001); builder.add_method("<clinit>", "()V", 0x0008);
let bytes = builder.build();
let stub = parse_class(&bytes).unwrap();
assert_eq!(stub.methods.len(), 2);
assert_eq!(stub.methods[0].name, "<init>");
assert_eq!(stub.methods[1].name, "<clinit>");
}
#[test]
fn test_field_method_return_type_object() {
let mut builder = ClassFileBuilder::new("com/example/ReturnTypes");
builder.add_method("getList", "()Ljava/util/List;", 0x0001);
let bytes = builder.build();
let stub = parse_class(&bytes).unwrap();
match &stub.methods[0].return_type {
TypeSignature::Class { fqn, .. } => assert_eq!(fqn, "java.util.List"),
other => panic!("Expected Class return type, got {other:?}"),
}
}
}