use super::encoding::{ObjCMethodSignature, ObjCQualifiedType, ObjCType, TypeQualifier};
#[derive(Debug, Clone)]
pub struct SignatureCompat {
pub compatible: bool,
pub findings: Vec<SignatureIssue>,
}
#[derive(Debug, Clone)]
pub struct SignatureIssue {
pub severity: IssueSeverity,
pub message: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum IssueSeverity {
Error,
Warning,
}
pub fn compare_method_signatures(
target: &ObjCMethodSignature,
provider: &ObjCMethodSignature,
) -> SignatureCompat {
let mut findings = Vec::new();
if target.arguments.len() != provider.arguments.len() {
findings.push(SignatureIssue {
severity: IssueSeverity::Error,
message: format!(
"argument count mismatch: target has {}, provider has {}",
target.arguments.len(),
provider.arguments.len(),
),
});
} else {
for (i, (t_arg, p_arg)) in target.arguments.iter().zip(&provider.arguments).enumerate() {
if !types_compatible(&t_arg.ty, &p_arg.ty) {
let sev = if types_width_compatible(&t_arg.ty, &p_arg.ty) {
IssueSeverity::Warning
} else {
IssueSeverity::Error
};
findings.push(SignatureIssue {
severity: sev,
message: format!(
"argument {i} type mismatch: target '{}', provider '{}'",
t_arg.ty.render(),
p_arg.ty.render(),
),
});
}
let qual_diff = qualifier_diff(&t_arg.ty.qualifiers, &p_arg.ty.qualifiers);
if !qual_diff.is_empty() {
findings.push(SignatureIssue {
severity: IssueSeverity::Warning,
message: format!("argument {i} qualifier difference: {qual_diff}"),
});
}
}
}
if !types_compatible(&target.return_type, &provider.return_type) {
let sev = if types_width_compatible(&target.return_type, &provider.return_type) {
IssueSeverity::Warning
} else {
IssueSeverity::Error
};
findings.push(SignatureIssue {
severity: sev,
message: format!(
"return type mismatch: target '{}', provider '{}'",
target.return_type.render(),
provider.return_type.render(),
),
});
}
let compatible = !findings.iter().any(|f| f.severity == IssueSeverity::Error);
SignatureCompat {
compatible,
findings,
}
}
pub fn types_compatible(a: &ObjCQualifiedType, b: &ObjCQualifiedType) -> bool {
type_cores_compatible(&a.ty, &b.ty)
}
fn type_cores_compatible(a: &ObjCType, b: &ObjCType) -> bool {
match (a, b) {
(ObjCType::Void, ObjCType::Void)
| (ObjCType::Bool, ObjCType::Bool)
| (ObjCType::Char, ObjCType::Char)
| (ObjCType::UnsignedChar, ObjCType::UnsignedChar)
| (ObjCType::Short, ObjCType::Short)
| (ObjCType::UnsignedShort, ObjCType::UnsignedShort)
| (ObjCType::Int, ObjCType::Int)
| (ObjCType::UnsignedInt, ObjCType::UnsignedInt)
| (ObjCType::Long, ObjCType::Long)
| (ObjCType::UnsignedLong, ObjCType::UnsignedLong)
| (ObjCType::LongLong, ObjCType::LongLong)
| (ObjCType::UnsignedLongLong, ObjCType::UnsignedLongLong)
| (ObjCType::Float, ObjCType::Float)
| (ObjCType::Double, ObjCType::Double)
| (ObjCType::CString, ObjCType::CString)
| (ObjCType::CharPtr, ObjCType::CharPtr)
| (ObjCType::Selector, ObjCType::Selector)
| (ObjCType::Class, ObjCType::Class) => true,
(
ObjCType::Object {
class_name: a_cls,
is_block: a_blk,
..
},
ObjCType::Object {
class_name: b_cls,
is_block: b_blk,
..
},
) => {
if *a_blk && *b_blk {
return true;
}
if a_cls.is_none() || b_cls.is_none() {
return true;
}
a_cls == b_cls
}
(ObjCType::Pointer(a_inner), ObjCType::Pointer(b_inner)) => {
types_compatible(a_inner, b_inner)
}
(ObjCType::Struct { name: a_name, .. }, ObjCType::Struct { name: b_name, .. }) => {
a_name == b_name
}
(ObjCType::Union { name: a_name, .. }, ObjCType::Union { name: b_name, .. }) => {
a_name == b_name
}
(
ObjCType::Array {
len: a_len,
element: a_el,
},
ObjCType::Array {
len: b_len,
element: b_el,
},
) => a_len == b_len && types_compatible(a_el, b_el),
(ObjCType::BitField(a_bits), ObjCType::BitField(b_bits)) => a_bits == b_bits,
(ObjCType::CString, ObjCType::CharPtr) | (ObjCType::CharPtr, ObjCType::CString) => true,
_ => false,
}
}
fn types_width_compatible(a: &ObjCQualifiedType, b: &ObjCQualifiedType) -> bool {
if is_pointer_family(&a.ty) != is_pointer_family(&b.ty) {
return false;
}
if is_pointer_family(&a.ty) && is_pointer_family(&b.ty) {
return false;
}
let a_w = type_width(&a.ty);
let b_w = type_width(&b.ty);
match (a_w, b_w) {
(Some(aw), Some(bw)) => aw == bw,
_ => false,
}
}
fn is_pointer_family(ty: &ObjCType) -> bool {
matches!(
ty,
ObjCType::Pointer(_)
| ObjCType::Object { .. }
| ObjCType::CharPtr
| ObjCType::CString
| ObjCType::Selector
| ObjCType::Class
)
}
fn type_width(ty: &ObjCType) -> Option<usize> {
match ty {
ObjCType::Void => Some(0),
ObjCType::Bool | ObjCType::Char | ObjCType::UnsignedChar => Some(1),
ObjCType::Short | ObjCType::UnsignedShort => Some(2),
ObjCType::Int | ObjCType::UnsignedInt | ObjCType::Float => Some(4),
ObjCType::Long
| ObjCType::UnsignedLong
| ObjCType::LongLong
| ObjCType::UnsignedLongLong
| ObjCType::Double => Some(8),
ObjCType::Pointer(_)
| ObjCType::Object { .. }
| ObjCType::CharPtr
| ObjCType::CString
| ObjCType::Selector
| ObjCType::Class => Some(8),
_ => None,
}
}
fn qualifier_diff(a: &[TypeQualifier], b: &[TypeQualifier]) -> String {
let mut diffs = Vec::new();
for q in a {
if !b.contains(q) {
diffs.push(format!("target has {q:?}"));
}
}
for q in b {
if !a.contains(q) {
diffs.push(format!("provider has {q:?}"));
}
}
diffs.join(", ")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::objc::encoding::ObjCMethodArg;
fn make_sig(ret: ObjCType, args: Vec<ObjCType>) -> ObjCMethodSignature {
ObjCMethodSignature {
return_type: ObjCQualifiedType {
qualifiers: vec![],
ty: ret,
},
return_offset: None,
self_type: None,
cmd_type: None,
arguments: args
.into_iter()
.map(|ty| ObjCMethodArg {
ty: ObjCQualifiedType {
qualifiers: vec![],
ty,
},
stack_offset: None,
})
.collect(),
}
}
#[test]
fn identical_signatures_compatible() {
let sig = make_sig(ObjCType::Void, vec![ObjCType::Int, ObjCType::Bool]);
let result = compare_method_signatures(&sig, &sig);
assert!(result.compatible);
assert!(result.findings.is_empty());
}
#[test]
fn arg_count_mismatch() {
let target = make_sig(ObjCType::Void, vec![ObjCType::Int]);
let provider = make_sig(ObjCType::Void, vec![ObjCType::Int, ObjCType::Bool]);
let result = compare_method_signatures(&target, &provider);
assert!(!result.compatible);
assert!(
result
.findings
.iter()
.any(|f| f.severity == IssueSeverity::Error)
);
}
#[test]
fn return_type_mismatch_is_error() {
let target = make_sig(ObjCType::Void, vec![]);
let provider = make_sig(ObjCType::Int, vec![]);
let result = compare_method_signatures(&target, &provider);
assert!(!result.compatible);
}
#[test]
fn same_width_mismatch_is_warning() {
let target = make_sig(ObjCType::Int, vec![]);
let provider = make_sig(ObjCType::UnsignedInt, vec![]);
let result = compare_method_signatures(&target, &provider);
assert!(result.compatible); assert!(!result.findings.is_empty());
}
#[test]
fn id_compatible_with_named_object() {
let id_type = ObjCType::Object {
class_name: None,
protocols: vec![],
is_block: false,
};
let named_type = ObjCType::Object {
class_name: Some("NSString".into()),
protocols: vec![],
is_block: false,
};
let target = make_sig(id_type.clone(), vec![named_type.clone()]);
let provider = make_sig(named_type, vec![id_type]);
let result = compare_method_signatures(&target, &provider);
assert!(result.compatible);
}
#[test]
fn pointer_depth_mismatch() {
let ptr = ObjCType::Pointer(Box::new(ObjCQualifiedType {
qualifiers: vec![],
ty: ObjCType::Int,
}));
let double_ptr = ObjCType::Pointer(Box::new(ObjCQualifiedType {
qualifiers: vec![],
ty: ptr.clone(),
}));
let target = make_sig(ObjCType::Void, vec![ptr]);
let provider = make_sig(ObjCType::Void, vec![double_ptr]);
let result = compare_method_signatures(&target, &provider);
assert!(!result.compatible);
}
#[test]
fn struct_name_mismatch() {
let a = ObjCType::Struct {
name: "CGRect".into(),
fields: vec![],
};
let b = ObjCType::Struct {
name: "CGSize".into(),
fields: vec![],
};
let target = make_sig(ObjCType::Void, vec![a]);
let provider = make_sig(ObjCType::Void, vec![b]);
let result = compare_method_signatures(&target, &provider);
assert!(!result.compatible);
}
#[test]
fn cstring_and_charptr_compatible() {
let target = make_sig(ObjCType::CString, vec![]);
let provider = make_sig(ObjCType::CharPtr, vec![]);
let result = compare_method_signatures(&target, &provider);
assert!(result.compatible);
}
}