use rustc_hash::FxHashMap;
use mir_codebase::storage::{FnParam, TemplateParam};
use mir_types::{atomic::ArrayKey, union::empty_type_params, Atomic, Name, Type};
use crate::db::MirDatabase;
use crate::subtype::is_subtype;
pub fn infer_template_bindings(
template_params: &[TemplateParam],
params: &[FnParam],
arg_types: &[Type],
) -> FxHashMap<Name, Type> {
let mut bindings = infer_arg_template_bindings(template_params, params, arg_types);
for tp in template_params {
bindings
.entry(Name::from(tp.name.as_ref()))
.or_insert_with(|| tp.bound.as_deref().cloned().unwrap_or_else(Type::mixed));
}
bindings
}
pub fn infer_arg_template_bindings(
template_params: &[TemplateParam],
params: &[FnParam],
arg_types: &[Type],
) -> FxHashMap<Name, Type> {
let mut bindings: FxHashMap<Name, Type> = FxHashMap::default();
let template_names: std::collections::HashSet<Name> = template_params
.iter()
.map(|tp| Name::from(tp.name.as_ref()))
.collect();
for (idx, arg_ty) in arg_types.iter().enumerate() {
let param = if idx < params.len() {
¶ms[idx]
} else {
match params.last() {
Some(p) if p.is_variadic => p,
_ => break,
}
};
if let Some(param_ty) = ¶m.ty {
if param.is_variadic {
let elem = variadic_element_type(param_ty);
infer_from_pair(elem, arg_ty, &template_names, &mut bindings);
} else {
infer_from_pair(param_ty, arg_ty, &template_names, &mut bindings);
}
}
}
bindings
}
fn variadic_element_type(ty: &Type) -> &Type {
if ty.types.len() == 1 {
match &ty.types[0] {
Atomic::TArray { value, .. } | Atomic::TNonEmptyArray { value, .. } => value,
Atomic::TList { value } | Atomic::TNonEmptyList { value } => value,
_ => ty,
}
} else {
ty
}
}
pub fn check_template_bounds_with_inheritance<'a>(
db: &dyn MirDatabase,
bindings: &'a FxHashMap<Name, Type>,
template_params: &'a [TemplateParam],
) -> Vec<(&'a Name, &'a Type, &'a Type)> {
let is_unresolved = |ty: &Type| {
ty.types.iter().any(|a| match a {
Atomic::TTemplateParam { .. }
| Atomic::TSelf { .. }
| Atomic::TStaticObject { .. }
| Atomic::TParent { .. } => true,
Atomic::TNamedObject { fqcn, type_params } => {
(type_params.is_empty() && !fqcn.contains('\\') && {
let name = fqcn.as_str();
name.eq_ignore_ascii_case("self")
|| name.eq_ignore_ascii_case("static")
|| name.eq_ignore_ascii_case("parent")
|| template_params.iter().any(|tp| tp.name.as_ref() == name)
}) || type_params.iter().any(is_unresolved_shallow)
}
_ => false,
})
};
let mut violations = Vec::new();
for tp in template_params {
if let Some(bound) = &tp.bound {
if let Some(inferred) = bindings.get(&tp.name) {
let resolved_bound = bound.substitute_templates(bindings);
if !resolved_bound.is_mixed()
&& !inferred.is_mixed()
&& !is_unresolved(inferred)
&& !is_subtype(db, inferred, &resolved_bound)
{
violations.push((&tp.name, inferred, bound.as_ref()));
}
}
}
}
violations
}
fn is_unresolved_shallow(ty: &Type) -> bool {
ty.types.iter().any(|a| match a {
Atomic::TTemplateParam { .. }
| Atomic::TSelf { .. }
| Atomic::TStaticObject { .. }
| Atomic::TParent { .. } => true,
Atomic::TNamedObject { fqcn, type_params } => {
type_params.is_empty()
&& !fqcn.contains('\\')
&& fqcn.chars().next().is_some_and(|c| c.is_ascii_uppercase())
&& fqcn.as_str() != "Closure"
}
_ => false,
})
}
pub fn build_class_bindings(
class_template_params: &[TemplateParam],
receiver_type_params: &[Type],
) -> FxHashMap<Name, Type> {
class_template_params
.iter()
.zip(receiver_type_params.iter())
.map(|(tp, ty)| (Name::from(tp.name.as_ref()), ty.clone()))
.collect()
}
fn compute_template_residual(
param_ty: &Type,
arg_ty: &Type,
template_names: &std::collections::HashSet<Name>,
) -> Option<Type> {
let mut has_template = false;
let mut has_template_class_string = false;
let mut concrete: Vec<&Atomic> = Vec::new();
for a in ¶m_ty.types {
if is_template_atomic(a, template_names) {
has_template = true;
} else if matches!(a, Atomic::TClassString(Some(n)) if template_names.contains(n)) {
has_template_class_string = true;
} else {
concrete.push(a);
}
}
if !has_template || (concrete.is_empty() && !has_template_class_string) {
return None;
}
let mut residual = Type::empty();
residual.from_docblock = arg_ty.from_docblock;
residual.possibly_undefined = arg_ty.possibly_undefined;
let mut class_string_consumed = false;
for a in &arg_ty.types {
let consumed_by_class_string = has_template_class_string
&& matches!(a, Atomic::TClassString(_))
|| matches!(a, Atomic::TLiteralString(s) if has_template_class_string && literal_is_class_like(s));
if consumed_by_class_string {
class_string_consumed = true;
continue;
}
if !concrete.iter().any(|c| atomics_match_for_filter(c, a)) {
residual.add_type(a.clone());
}
}
if residual.types.is_empty() {
return class_string_consumed.then_some(residual);
}
if residual.types.len() == arg_ty.types.len() && !class_string_consumed {
return None;
}
Some(residual)
}
fn literal_is_class_like(s: &str) -> bool {
let t = s.trim_start_matches('\\');
if t.is_empty() {
return false;
}
let shape_ok = t.split('\\').all(|seg| {
!seg.is_empty()
&& seg
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_' || !c.is_ascii())
&& !seg.chars().next().is_some_and(|c| c.is_ascii_digit())
});
shape_ok && (s.contains('\\') || t.chars().next().is_some_and(|c| c.is_ascii_uppercase()))
}
fn is_template_atomic(a: &Atomic, template_names: &std::collections::HashSet<Name>) -> bool {
match a {
Atomic::TTemplateParam { .. } => true,
Atomic::TNamedObject { fqcn, type_params } => {
type_params.is_empty() && !fqcn.contains('\\') && template_names.contains(fqcn)
}
_ => false,
}
}
fn atomics_match_for_filter(concrete: &Atomic, arg: &Atomic) -> bool {
matches!(
(concrete, arg),
(Atomic::TNull, Atomic::TNull)
| (Atomic::TBool, Atomic::TBool)
| (Atomic::TBool, Atomic::TTrue)
| (Atomic::TBool, Atomic::TFalse)
| (Atomic::TTrue, Atomic::TTrue)
| (Atomic::TFalse, Atomic::TFalse)
| (Atomic::TInt, Atomic::TInt)
| (Atomic::TFloat, Atomic::TFloat)
| (Atomic::TString, Atomic::TString)
)
}
fn infer_from_pair(
param_ty: &Type,
arg_ty: &Type,
template_names: &std::collections::HashSet<Name>,
bindings: &mut FxHashMap<Name, Type>,
) {
let template_residual = compute_template_residual(param_ty, arg_ty, template_names);
for p_atomic in ¶m_ty.types {
match p_atomic {
Atomic::TTemplateParam { name, .. } => {
let bind = template_residual.as_ref().unwrap_or(arg_ty);
if bind.types.is_empty() {
continue;
}
let entry = bindings.entry(*name).or_insert_with(Type::empty);
entry.merge_with(bind);
}
Atomic::TNonEmptyArray { key: pk, value: pv } => {
for a_atomic in &arg_ty.types {
match a_atomic {
Atomic::TArray { key: ak, value: av }
| Atomic::TNonEmptyArray { key: ak, value: av } => {
infer_from_pair(pk, ak, template_names, bindings);
infer_from_pair(pv, av, template_names, bindings);
}
Atomic::TKeyedArray { properties, .. } => {
let mut key_union = Type::empty();
let mut val_union = Type::empty();
for (k, prop) in properties {
let key_atomic = match k {
ArrayKey::String(_) => Atomic::TString,
ArrayKey::Int(_) => Atomic::TInt,
};
key_union.add_type(key_atomic);
val_union.merge_with(&prop.ty);
}
if !key_union.types.is_empty() {
infer_from_pair(pk, &key_union, template_names, bindings);
infer_from_pair(pv, &val_union, template_names, bindings);
}
}
_ => {}
}
}
}
Atomic::TArray { key: pk, value: pv } => {
for a_atomic in &arg_ty.types {
match a_atomic {
Atomic::TArray { key: ak, value: av }
| Atomic::TNonEmptyArray { key: ak, value: av } => {
infer_from_pair(pk, ak, template_names, bindings);
infer_from_pair(pv, av, template_names, bindings);
}
Atomic::TKeyedArray { properties, .. } => {
let mut key_union = Type::empty();
let mut val_union = Type::empty();
for (k, prop) in properties {
let key_atomic = match k {
ArrayKey::String(_) => Atomic::TString,
ArrayKey::Int(_) => Atomic::TInt,
};
key_union.add_type(key_atomic);
val_union.merge_with(&prop.ty);
}
if !key_union.types.is_empty() {
infer_from_pair(pk, &key_union, template_names, bindings);
infer_from_pair(pv, &val_union, template_names, bindings);
}
}
_ => {}
}
}
}
Atomic::TList { value: pv } | Atomic::TNonEmptyList { value: pv } => {
for a_atomic in &arg_ty.types {
match a_atomic {
Atomic::TList { value: av } | Atomic::TNonEmptyList { value: av } => {
infer_from_pair(pv, av, template_names, bindings);
}
_ => {}
}
}
}
Atomic::TNamedObject {
fqcn: pfqcn,
type_params: pp,
} => {
if pp.is_empty() && !pfqcn.contains('\\') && template_names.contains(pfqcn) {
let bind = template_residual.as_ref().unwrap_or(arg_ty);
if bind.types.is_empty() {
continue; }
let entry = bindings.entry(*pfqcn).or_insert_with(Type::empty);
entry.merge_with(bind);
continue;
}
for a_atomic in &arg_ty.types {
if let Atomic::TNamedObject {
fqcn: afqcn,
type_params: ap,
} = a_atomic
{
if pfqcn == afqcn {
for (p_param, a_param) in pp.iter().zip(ap.iter()) {
infer_from_pair(p_param, a_param, template_names, bindings);
}
}
}
}
}
Atomic::TClosure {
params: p_params,
return_type: p_ret,
..
} => {
for a_atomic in &arg_ty.types {
match a_atomic {
Atomic::TClosure {
params: a_params,
return_type: a_ret,
..
} => {
for (pp, ap) in p_params.iter().zip(a_params.iter()) {
if let (Some(pt), Some(at)) = (pp.ty.as_ref(), ap.ty.as_ref()) {
infer_from_pair(
&pt.to_union(),
&at.to_union(),
template_names,
bindings,
);
}
}
infer_from_pair(p_ret, a_ret, template_names, bindings);
}
Atomic::TCallable {
params: Some(a_params),
return_type: Some(a_ret),
} => {
for (pp, ap) in p_params.iter().zip(a_params.iter()) {
if let (Some(pt), Some(at)) = (pp.ty.as_ref(), ap.ty.as_ref()) {
infer_from_pair(
&pt.to_union(),
&at.to_union(),
template_names,
bindings,
);
}
}
infer_from_pair(p_ret, a_ret, template_names, bindings);
}
_ => {}
}
}
}
Atomic::TCallable {
params: Some(p_params),
return_type: Some(p_ret),
} => {
for a_atomic in &arg_ty.types {
match a_atomic {
Atomic::TCallable {
params: Some(a_params),
return_type: Some(a_ret),
} => {
for (pp, ap) in p_params.iter().zip(a_params.iter()) {
if let (Some(pt), Some(at)) = (pp.ty.as_ref(), ap.ty.as_ref()) {
infer_from_pair(
&pt.to_union(),
&at.to_union(),
template_names,
bindings,
);
}
}
infer_from_pair(p_ret, a_ret, template_names, bindings);
}
Atomic::TClosure {
params: a_params,
return_type: a_ret,
..
} => {
for (pp, ap) in p_params.iter().zip(a_params.iter()) {
if let (Some(pt), Some(at)) = (pp.ty.as_ref(), ap.ty.as_ref()) {
infer_from_pair(
&pt.to_union(),
&at.to_union(),
template_names,
bindings,
);
}
}
infer_from_pair(p_ret, a_ret, template_names, bindings);
}
_ => {}
}
}
}
Atomic::TIntersection { parts } => {
let arg = template_residual.as_ref().unwrap_or(arg_ty);
if arg.types.is_empty() {
continue;
}
for part in parts.iter() {
infer_from_pair(part, arg, template_names, bindings);
}
}
Atomic::TClassString(Some(param_name)) if template_names.contains(param_name) => {
for a_atomic in &arg_ty.types {
let cls_ty = match a_atomic {
Atomic::TClassString(Some(arg_cls)) => {
Some(Type::single(Atomic::TNamedObject {
fqcn: *arg_cls,
type_params: empty_type_params(),
}))
}
Atomic::TClassString(None) => Some(Type::single(Atomic::TObject)),
Atomic::TLiteralString(s) if literal_is_class_like(s) => {
Some(Type::single(Atomic::TNamedObject {
fqcn: Name::new(s.trim_start_matches('\\')),
type_params: empty_type_params(),
}))
}
_ => None,
};
if let Some(cls_ty) = cls_ty {
let entry = bindings.entry(*param_name).or_insert_with(Type::empty);
entry.merge_with(&cls_ty);
}
}
}
_ => {}
}
}
}