use crate::assignability::{AssignabilityConfig, Assignable, is_assignable};
use crate::qos::{TypeConsistencyEnforcement, TypeConsistencyKind};
use crate::resolve::TypeRegistry;
use crate::type_identifier::TypeIdentifier;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TypeMatchResult {
Matches,
Incompatible {
reason: &'static str,
},
}
impl TypeMatchResult {
#[must_use]
pub const fn is_match(&self) -> bool {
matches!(self, Self::Matches)
}
fn from_assignable(a: Assignable) -> Self {
match a {
Assignable::Yes => Self::Matches,
Assignable::No(reason) => Self::Incompatible { reason },
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct TypeMatcher<'a> {
tce: &'a TypeConsistencyEnforcement,
}
impl<'a> TypeMatcher<'a> {
#[must_use]
pub const fn new(tce: &'a TypeConsistencyEnforcement) -> Self {
Self { tce }
}
#[must_use]
pub fn match_types(
&self,
writer: &TypeIdentifier,
reader: &TypeIdentifier,
registry: &TypeRegistry,
) -> TypeMatchResult {
let cfg = self.build_config();
TypeMatchResult::from_assignable(is_assignable(writer, reader, registry, &cfg))
}
fn build_config(&self) -> AssignabilityConfig {
let coerce = matches!(self.tce.kind, TypeConsistencyKind::AllowTypeCoercion)
&& !self.tce.prevent_type_widening;
AssignabilityConfig {
allow_type_coercion: if self.tce.force_type_validation {
false
} else {
coerce
},
ignore_sequence_bounds: self.tce.ignore_sequence_bounds,
ignore_string_bounds: self.tce.ignore_string_bounds,
ignore_member_names: self.tce.ignore_member_names,
ignore_literal_names: false,
max_depth: crate::resolve::DEFAULT_MAX_RESOLVE_DEPTH,
}
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::panic,
clippy::field_reassign_with_default
)]
mod tests {
use super::*;
use crate::type_identifier::PrimitiveKind;
fn reg() -> TypeRegistry {
TypeRegistry::new()
}
#[test]
fn identical_primitive_matches() {
let tce = TypeConsistencyEnforcement::default();
let m = TypeMatcher::new(&tce);
let w = TypeIdentifier::Primitive(PrimitiveKind::Int32);
assert_eq!(m.match_types(&w, &w, ®()), TypeMatchResult::Matches);
}
#[test]
fn widening_allowed_by_default_tce() {
let tce = TypeConsistencyEnforcement::default();
let m = TypeMatcher::new(&tce);
let w = TypeIdentifier::Primitive(PrimitiveKind::Int16);
let r = TypeIdentifier::Primitive(PrimitiveKind::Int32);
assert!(m.match_types(&w, &r, ®()).is_match());
}
#[test]
fn widening_blocked_by_prevent_type_widening() {
let mut tce = TypeConsistencyEnforcement::default();
tce.prevent_type_widening = true;
let m = TypeMatcher::new(&tce);
let w = TypeIdentifier::Primitive(PrimitiveKind::Int16);
let r = TypeIdentifier::Primitive(PrimitiveKind::Int32);
assert!(!m.match_types(&w, &r, ®()).is_match());
}
#[test]
fn force_type_validation_blocks_coercion() {
let mut tce = TypeConsistencyEnforcement::default();
tce.force_type_validation = true;
let m = TypeMatcher::new(&tce);
let w = TypeIdentifier::Primitive(PrimitiveKind::Int16);
let r = TypeIdentifier::Primitive(PrimitiveKind::Int32);
assert!(!m.match_types(&w, &r, ®()).is_match());
}
#[test]
fn disallow_type_coercion_blocks_widening() {
let mut tce = TypeConsistencyEnforcement::default();
tce.kind = TypeConsistencyKind::DisallowTypeCoercion;
let m = TypeMatcher::new(&tce);
let w = TypeIdentifier::Primitive(PrimitiveKind::Int16);
let r = TypeIdentifier::Primitive(PrimitiveKind::Int32);
assert!(!m.match_types(&w, &r, ®()).is_match());
}
#[test]
fn incompatible_reports_reason() {
let tce = TypeConsistencyEnforcement::default();
let m = TypeMatcher::new(&tce);
let w = TypeIdentifier::Primitive(PrimitiveKind::Int64);
let r = TypeIdentifier::Primitive(PrimitiveKind::Int16);
match m.match_types(&w, &r, ®()) {
TypeMatchResult::Incompatible { reason } => {
assert!(!reason.is_empty(), "reason must be non-empty");
}
TypeMatchResult::Matches => {
panic!("narrowing i64→i16 must not match");
}
}
}
}