use mir_types::{Atomic, Type};
use crate::db::MirDatabase;
use crate::flow_state::FlowState;
use super::core::{
apply_prop_narrowed, resolve_prop_current_type, resolve_static_prop_current_type,
UnionNarrowExt,
};
pub(super) fn project_type_params_onto_subclass(
db: &dyn MirDatabase,
atom_fqcn: &str,
atom_type_params: &[Type],
class_name: &str,
) -> std::sync::Arc<[Type]> {
let Some(class_own_tps) = crate::db::class_template_params(db, class_name) else {
return mir_types::union::empty_type_params();
};
if class_own_tps.is_empty() {
return mir_types::union::empty_type_params();
}
let Some(atom_own_tps) = crate::db::class_template_params(db, atom_fqcn) else {
return mir_types::union::empty_type_params();
};
let here = crate::db::Fqcn::from_str(db, class_name);
let Some(class) = crate::db::find_class_like(db, here) else {
return mir_types::union::empty_type_params();
};
let explicit_args: Option<&[Type]> = if class
.parent()
.is_some_and(|p| p.as_ref().eq_ignore_ascii_case(atom_fqcn))
{
Some(class.extends_type_args())
} else {
class
.implements_type_args()
.iter()
.chain(class.interface_extends_type_args())
.find(|(iface, _)| iface.as_ref().eq_ignore_ascii_case(atom_fqcn))
.map(|(_, args)| args.as_slice())
};
let mut result = vec![Type::mixed(); class_own_tps.len()];
let mut any_bound = false;
if let Some(args) = explicit_args.filter(|a| !a.is_empty()) {
for (idx, given_ty) in atom_type_params.iter().enumerate() {
let Some(arg_expr) = args.get(idx) else {
continue;
};
let Some(bare_name) = bare_named_type(arg_expr) else {
continue;
};
if let Some(pos) = class_own_tps
.iter()
.position(|tp| tp.name.as_str() == bare_name)
{
result[pos] = given_ty.clone();
any_bound = true;
}
}
} else if class_own_tps.len() == atom_own_tps.len() {
result = atom_type_params.to_vec();
any_bound = !result.is_empty();
}
if any_bound {
mir_types::union::vec_to_type_params(result)
} else {
mir_types::union::empty_type_params()
}
}
fn bare_named_type(ty: &Type) -> Option<&str> {
if ty.types.len() != 1 {
return None;
}
match &ty.types[0] {
Atomic::TNamedObject { fqcn, type_params }
if type_params.is_empty() && !fqcn.contains('\\') =>
{
Some(fqcn.as_ref())
}
Atomic::TTemplateParam { name, .. } => Some(name.as_ref()),
_ => None,
}
}
pub(super) fn narrow_instanceof_preserving_subtypes(
current: &Type,
class_name: &str,
db: &dyn MirDatabase,
template_param_names: &rustc_hash::FxHashSet<mir_types::Name>,
) -> Type {
let narrowed_ty = Atomic::TNamedObject {
fqcn: class_name.into(),
type_params: mir_types::union::empty_type_params(),
};
if current.is_empty() || current.is_mixed_not_template() {
return Type::single(narrowed_ty);
}
let mut result = Type::empty();
result.possibly_undefined = current.possibly_undefined;
result.from_docblock = current.from_docblock;
for atomic in ¤t.types {
match atomic {
Atomic::TNamedObject { fqcn, .. }
| Atomic::TSelf { fqcn }
| Atomic::TStaticObject { fqcn }
| Atomic::TParent { fqcn }
if named_object_matches_instanceof(fqcn, class_name, db) =>
{
result.add_type(atomic.clone());
}
Atomic::TNamedObject { fqcn, type_params }
if type_params.is_empty()
&& !fqcn.contains('\\')
&& template_param_names.contains(fqcn) =>
{
result.add_type(Atomic::TIntersection {
parts: std::sync::Arc::from(vec![
Type::single(atomic.clone()),
Type::single(narrowed_ty.clone()),
]),
});
}
Atomic::TTemplateParam { .. } => {
result.add_type(Atomic::TIntersection {
parts: std::sync::Arc::from(vec![
Type::single(atomic.clone()),
Type::single(narrowed_ty.clone()),
]),
});
}
Atomic::TObject | Atomic::TMixed => result.add_type(narrowed_ty.clone()),
Atomic::TIntersection { parts } => {
let already_covered = parts.iter().any(|p| {
p.types.iter().any(|a| {
matches!(a, Atomic::TNamedObject { fqcn, .. }
if named_object_matches_instanceof(fqcn, class_name, db))
})
});
if already_covered {
result.add_type(atomic.clone());
} else {
let projected_atom = parts.iter().find_map(|p| {
p.types.iter().find_map(|a| match a {
Atomic::TNamedObject { fqcn, type_params }
if !type_params.is_empty()
&& named_object_matches_instanceof(class_name, fqcn, db) =>
{
Some(Atomic::TNamedObject {
fqcn: class_name.into(),
type_params: project_type_params_onto_subclass(
db,
fqcn,
type_params,
class_name,
),
})
}
_ => None,
})
});
let mut new_parts: Vec<Type> = parts.iter().cloned().collect();
new_parts.push(Type::single(
projected_atom.unwrap_or_else(|| narrowed_ty.clone()),
));
result.add_type(Atomic::TIntersection {
parts: std::sync::Arc::from(new_parts),
});
}
}
Atomic::TNamedObject { fqcn, type_params }
if !type_params.is_empty()
&& named_object_matches_instanceof(class_name, fqcn, db) =>
{
let projected =
project_type_params_onto_subclass(db, fqcn, type_params, class_name);
result.add_type(Atomic::TNamedObject {
fqcn: class_name.into(),
type_params: projected,
});
}
Atomic::TNamedObject { fqcn, .. }
| Atomic::TSelf { fqcn }
| Atomic::TStaticObject { fqcn }
| Atomic::TParent { fqcn }
if named_object_matches_instanceof(class_name, fqcn, db) =>
{
result.add_type(narrowed_ty.clone());
}
Atomic::TNamedObject { fqcn, .. }
| Atomic::TSelf { fqcn }
| Atomic::TStaticObject { fqcn }
| Atomic::TParent { fqcn }
if classes_can_coexist(fqcn, class_name, db) =>
{
result.add_type(Atomic::TIntersection {
parts: std::sync::Arc::from(vec![
Type::single(atomic.clone()),
Type::single(narrowed_ty.clone()),
]),
});
}
Atomic::TClosure { .. } if class_name.eq_ignore_ascii_case("Closure") => {
result.add_type(atomic.clone());
}
_ => {}
}
}
result
}
pub(super) fn narrow_or_instanceof_union(
current: &Type,
class_names: &[String],
db: &dyn MirDatabase,
template_param_names: &rustc_hash::FxHashSet<mir_types::Name>,
) -> Type {
let class_atom = |cn: &str| Atomic::TNamedObject {
fqcn: cn.into(),
type_params: mir_types::union::empty_type_params(),
};
if current.is_empty() || current.is_mixed_not_template() {
let mut out = Type::empty();
for cn in class_names {
out.add_type(class_atom(cn));
}
return out;
}
let mut result = Type::empty();
result.possibly_undefined = current.possibly_undefined;
result.from_docblock = current.from_docblock;
for atomic in ¤t.types {
match atomic {
Atomic::TNamedObject { fqcn, .. }
| Atomic::TSelf { fqcn }
| Atomic::TStaticObject { fqcn }
| Atomic::TParent { fqcn }
if class_names
.iter()
.any(|cn| named_object_matches_instanceof(fqcn, cn, db)) =>
{
result.add_type(atomic.clone());
}
Atomic::TNamedObject { fqcn, type_params }
if type_params.is_empty()
&& !fqcn.contains('\\')
&& template_param_names.contains(fqcn) =>
{
let mut classes = Type::empty();
for cn in class_names {
classes.add_type(class_atom(cn));
}
result.add_type(Atomic::TIntersection {
parts: std::sync::Arc::from(vec![Type::single(atomic.clone()), classes]),
});
}
Atomic::TTemplateParam { .. } => {
let mut classes = Type::empty();
for cn in class_names {
classes.add_type(class_atom(cn));
}
result.add_type(Atomic::TIntersection {
parts: std::sync::Arc::from(vec![Type::single(atomic.clone()), classes]),
});
}
Atomic::TObject | Atomic::TMixed => {
for cn in class_names {
result.add_type(class_atom(cn));
}
}
Atomic::TClosure { .. }
if class_names
.iter()
.any(|cn| cn.eq_ignore_ascii_case("Closure")) =>
{
result.add_type(atomic.clone());
}
Atomic::TIntersection { parts } => {
let mut remaining = Type::empty();
for cn in class_names {
let already_covered = parts.iter().any(|p| {
p.types.iter().any(|a| {
matches!(a, Atomic::TNamedObject { fqcn, .. }
if named_object_matches_instanceof(fqcn, cn, db))
})
});
if !already_covered {
let projected_atom = parts.iter().find_map(|p| {
p.types.iter().find_map(|a| match a {
Atomic::TNamedObject { fqcn, type_params }
if !type_params.is_empty()
&& named_object_matches_instanceof(cn, fqcn, db) =>
{
Some(Atomic::TNamedObject {
fqcn: cn.as_str().into(),
type_params: project_type_params_onto_subclass(
db,
fqcn,
type_params,
cn,
),
})
}
_ => None,
})
});
remaining.add_type(projected_atom.unwrap_or_else(|| class_atom(cn)));
}
}
if remaining.is_empty() {
result.add_type(atomic.clone());
} else {
let mut new_parts: Vec<Type> = parts.iter().cloned().collect();
new_parts.push(remaining);
result.add_type(Atomic::TIntersection {
parts: std::sync::Arc::from(new_parts),
});
}
}
Atomic::TNamedObject { fqcn, type_params }
if !type_params.is_empty()
&& class_names
.iter()
.any(|cn| named_object_matches_instanceof(cn, fqcn, db)) =>
{
for cn in class_names {
if named_object_matches_instanceof(cn, fqcn, db) {
let projected =
project_type_params_onto_subclass(db, fqcn, type_params, cn);
result.add_type(Atomic::TNamedObject {
fqcn: cn.as_str().into(),
type_params: projected,
});
}
}
}
Atomic::TNamedObject { fqcn, .. }
| Atomic::TSelf { fqcn }
| Atomic::TStaticObject { fqcn }
| Atomic::TParent { fqcn }
if class_names
.iter()
.any(|cn| named_object_matches_instanceof(cn, fqcn, db)) =>
{
for cn in class_names {
if named_object_matches_instanceof(cn, fqcn, db) {
result.add_type(class_atom(cn));
}
}
}
Atomic::TNamedObject { fqcn, .. }
| Atomic::TSelf { fqcn }
| Atomic::TStaticObject { fqcn }
| Atomic::TParent { fqcn } => {
let mut classes = Type::empty();
for cn in class_names {
if classes_can_coexist(fqcn, cn, db) {
classes.add_type(class_atom(cn));
}
}
if !classes.is_empty() {
result.add_type(Atomic::TIntersection {
parts: std::sync::Arc::from(vec![Type::single(atomic.clone()), classes]),
});
}
}
_ => {}
}
}
result
}
fn classes_can_coexist(a: &str, b: &str, db: &dyn MirDatabase) -> bool {
crate::db::class_kind(db, a).is_some_and(|k| k.is_interface)
|| crate::db::class_kind(db, b).is_some_and(|k| k.is_interface)
}
pub(super) fn filter_out_instanceof_match(
current: &Type,
class_name: &str,
db: &dyn MirDatabase,
) -> Type {
current.filter(|t| match t {
Atomic::TNamedObject { fqcn, .. }
| Atomic::TSelf { fqcn }
| Atomic::TStaticObject { fqcn }
| Atomic::TParent { fqcn } => !named_object_matches_instanceof(fqcn, class_name, db),
Atomic::TClosure { .. } => !class_name.eq_ignore_ascii_case("Closure"),
Atomic::TIntersection { parts } => !parts.iter().any(|part| {
part.types.iter().any(|inner| match inner {
Atomic::TNamedObject { fqcn, .. }
| Atomic::TSelf { fqcn }
| Atomic::TStaticObject { fqcn }
| Atomic::TParent { fqcn } => named_object_matches_instanceof(fqcn, class_name, db),
_ => false,
})
}),
_ => true,
})
}
pub(super) fn filter_out_intersection_match(
current: &Type,
parts: &[Type],
db: &dyn MirDatabase,
) -> Type {
current.filter(|t| {
let atom_fqcns: Vec<&str> = match t {
Atomic::TNamedObject { fqcn, .. }
| Atomic::TSelf { fqcn }
| Atomic::TStaticObject { fqcn }
| Atomic::TParent { fqcn } => vec![fqcn.as_ref()],
Atomic::TIntersection { parts: cur_parts } => cur_parts
.iter()
.flat_map(|p| p.types.iter())
.filter_map(|a| match a {
Atomic::TNamedObject { fqcn, .. }
| Atomic::TSelf { fqcn }
| Atomic::TStaticObject { fqcn }
| Atomic::TParent { fqcn } => Some(fqcn.as_ref()),
_ => None,
})
.collect(),
_ => return true,
};
!parts.iter().all(|part| {
part.types.iter().any(|inner| match inner {
Atomic::TNamedObject { fqcn: target, .. }
| Atomic::TSelf { fqcn: target }
| Atomic::TStaticObject { fqcn: target }
| Atomic::TParent { fqcn: target } => atom_fqcns
.iter()
.any(|fqcn| named_object_matches_instanceof(fqcn, target, db)),
_ => false,
})
})
})
}
pub(super) fn named_object_matches_instanceof(
fqcn: &str,
class_name: &str,
db: &dyn MirDatabase,
) -> bool {
fqcn == class_name || crate::db::extends_or_implements(db, fqcn, class_name)
}
pub(super) fn partition_is_a_string_like(
current: &Type,
class_name: &str,
db: &dyn MirDatabase,
) -> (Type, Type) {
let mut string_part = Type::empty();
string_part.possibly_undefined = current.possibly_undefined;
string_part.from_docblock = current.from_docblock;
let mut obj_part = Type::empty();
for atom in ¤t.types {
if let Atomic::TClassString(Some(name)) = atom {
if named_object_matches_instanceof(name, class_name, db)
|| classes_can_coexist(name, class_name, db)
{
string_part.add_type(atom.clone());
}
} else if atom.is_string() {
string_part.add_type(atom.clone());
} else {
obj_part.add_type(atom.clone());
}
}
(string_part, obj_part)
}
pub(super) fn filter_out_is_a_string_match(
current: &Type,
class_name: &str,
db: &dyn MirDatabase,
) -> Type {
filter_out_instanceof_match(current, class_name, db).filter(|t| {
!matches!(t, Atomic::TClassString(Some(name)) if named_object_matches_instanceof(name, class_name, db))
})
}
pub(super) fn narrow_strict_subclass_of(
current: &Type,
class_name: &str,
db: &dyn MirDatabase,
template_param_names: &rustc_hash::FxHashSet<mir_types::Name>,
) -> Type {
let narrowed_ty = Atomic::TNamedObject {
fqcn: class_name.into(),
type_params: mir_types::union::empty_type_params(),
};
if current.is_empty() || current.is_mixed_not_template() {
return Type::single(narrowed_ty);
}
let mut result = Type::empty();
result.possibly_undefined = current.possibly_undefined;
result.from_docblock = current.from_docblock;
for atomic in ¤t.types {
match atomic {
Atomic::TNamedObject { fqcn, .. }
| Atomic::TSelf { fqcn }
| Atomic::TStaticObject { fqcn }
| Atomic::TParent { fqcn }
if crate::db::extends_or_implements(db, fqcn.as_ref(), class_name)
&& fqcn.as_ref() != class_name =>
{
result.add_type(atomic.clone());
}
Atomic::TNamedObject { fqcn, type_params }
if type_params.is_empty()
&& !fqcn.contains('\\')
&& template_param_names.contains(fqcn) =>
{
result.add_type(Atomic::TIntersection {
parts: std::sync::Arc::from(vec![
Type::single(atomic.clone()),
Type::single(narrowed_ty.clone()),
]),
});
}
Atomic::TTemplateParam { .. } => {
result.add_type(Atomic::TIntersection {
parts: std::sync::Arc::from(vec![
Type::single(atomic.clone()),
Type::single(narrowed_ty.clone()),
]),
});
}
Atomic::TObject | Atomic::TMixed => result.add_type(narrowed_ty.clone()),
Atomic::TIntersection { parts } => {
let already_covered = parts.iter().any(|p| {
p.types.iter().any(|a| {
matches!(a, Atomic::TNamedObject { fqcn, .. }
if crate::db::extends_or_implements(db, fqcn.as_ref(), class_name)
&& fqcn.as_ref() != class_name)
})
});
if already_covered {
result.add_type(atomic.clone());
} else {
let mut new_parts: Vec<Type> = parts.iter().cloned().collect();
new_parts.push(Type::single(narrowed_ty.clone()));
result.add_type(Atomic::TIntersection {
parts: std::sync::Arc::from(new_parts),
});
}
}
Atomic::TNamedObject { fqcn, .. }
| Atomic::TSelf { fqcn }
| Atomic::TStaticObject { fqcn }
| Atomic::TParent { fqcn }
if classes_can_coexist(fqcn, class_name, db) =>
{
result.add_type(Atomic::TIntersection {
parts: std::sync::Arc::from(vec![
Type::single(atomic.clone()),
Type::single(narrowed_ty.clone()),
]),
});
}
_ => {}
}
}
result
}
pub(super) fn narrow_static_prop_instanceof(
ctx: &mut FlowState,
fqcn: &str,
prop: &str,
class_name: &str,
db: &dyn MirDatabase,
is_true: bool,
) {
let current = if let Some(refined) = ctx.get_prop_refined(fqcn, prop) {
refined.clone()
} else {
let here = crate::db::Fqcn::from_str(db, fqcn);
crate::db::find_property_in_chain(db, here, prop)
.and_then(|(_, p)| p.ty.as_deref().cloned())
.unwrap_or_else(mir_types::Type::mixed)
};
if current.is_mixed_not_template() {
return;
}
let narrowed = if is_true {
narrow_instanceof_preserving_subtypes(¤t, class_name, db, &ctx.template_param_names)
} else {
filter_out_instanceof_match(¤t, class_name, db)
};
if !narrowed.is_empty() {
if narrowed != current {
ctx.set_prop_refined(fqcn, prop, narrowed);
}
} else if !current.is_empty() && !current.is_mixed() {
ctx.diverges = true;
}
}
pub(super) fn narrow_prop_instanceof(
ctx: &mut FlowState,
obj_var: &str,
prop: &str,
class_name: &str,
db: &dyn MirDatabase,
file: &str,
is_true: bool,
) {
let current = resolve_prop_current_type(ctx, obj_var, prop, db, file);
if current.is_mixed_not_template() {
return;
}
let narrowed = if is_true {
narrow_instanceof_preserving_subtypes(¤t, class_name, db, &ctx.template_param_names)
} else {
filter_out_instanceof_match(¤t, class_name, db)
};
let mark_diverges = is_true || !ctx.get_var(obj_var).is_nullable();
apply_prop_narrowed(ctx, obj_var, prop, current, narrowed, mark_diverges);
}
pub(super) fn narrow_static_prop_is_a(
ctx: &mut FlowState,
fqcn: &str,
prop: &str,
class_name: &str,
allow_string: bool,
db: &dyn MirDatabase,
is_true: bool,
) {
let current = resolve_static_prop_current_type(ctx, fqcn, prop, db);
if current.is_mixed_not_template() {
return;
}
if allow_string {
let narrowed = if is_true {
let (mut result, obj_part) = partition_is_a_string_like(¤t, class_name, db);
if !obj_part.is_empty() || current.is_mixed() {
let obj_src = if obj_part.is_empty() {
¤t
} else {
&obj_part
};
let obj_narrowed = narrow_instanceof_preserving_subtypes(
obj_src,
class_name,
db,
&ctx.template_param_names,
);
for atom in obj_narrowed.types.iter() {
result.add_type(atom.clone());
}
}
result
} else {
filter_out_is_a_string_match(¤t, class_name, db)
};
apply_prop_narrowed(ctx, fqcn, prop, current, narrowed, false);
} else {
let narrowed = if is_true {
narrow_instanceof_preserving_subtypes(
¤t,
class_name,
db,
&ctx.template_param_names,
)
} else {
filter_out_instanceof_match(¤t, class_name, db)
};
apply_prop_narrowed(ctx, fqcn, prop, current, narrowed, true);
}
}
#[allow(clippy::too_many_arguments)]
pub(super) fn narrow_prop_is_a(
ctx: &mut FlowState,
obj_var: &str,
prop: &str,
class_name: &str,
allow_string: bool,
db: &dyn MirDatabase,
file: &str,
is_true: bool,
) {
let current = resolve_prop_current_type(ctx, obj_var, prop, db, file);
if current.is_mixed_not_template() {
return;
}
if allow_string {
let narrowed = if is_true {
let (mut result, obj_part) = partition_is_a_string_like(¤t, class_name, db);
if !obj_part.is_empty() || current.is_mixed() {
let obj_src = if obj_part.is_empty() {
¤t
} else {
&obj_part
};
let obj_narrowed = narrow_instanceof_preserving_subtypes(
obj_src,
class_name,
db,
&ctx.template_param_names,
);
for atom in obj_narrowed.types.iter() {
result.add_type(atom.clone());
}
}
result
} else {
filter_out_is_a_string_match(¤t, class_name, db)
};
if !narrowed.is_empty() && narrowed != current {
ctx.set_prop_refined(obj_var, prop, narrowed);
}
} else {
let narrowed = if is_true {
narrow_instanceof_preserving_subtypes(
¤t,
class_name,
db,
&ctx.template_param_names,
)
} else {
filter_out_instanceof_match(¤t, class_name, db)
};
let mark_diverges = is_true || !ctx.get_var(obj_var).is_nullable();
apply_prop_narrowed(ctx, obj_var, prop, current, narrowed, mark_diverges);
}
}
pub(super) fn narrow_static_prop_is_subclass_of(
ctx: &mut FlowState,
fqcn: &str,
prop: &str,
class_name: &str,
db: &dyn MirDatabase,
is_true: bool,
) {
if !is_true {
return;
}
let current = resolve_static_prop_current_type(ctx, fqcn, prop, db);
if current.is_mixed_not_template() {
return;
}
let narrowed = narrow_strict_subclass_of(¤t, class_name, db, &ctx.template_param_names);
apply_prop_narrowed(ctx, fqcn, prop, current, narrowed, false);
}
pub(super) fn narrow_prop_is_subclass_of(
ctx: &mut FlowState,
obj_var: &str,
prop: &str,
class_name: &str,
db: &dyn MirDatabase,
file: &str,
is_true: bool,
) {
if !is_true {
return;
}
let current = resolve_prop_current_type(ctx, obj_var, prop, db, file);
if current.is_mixed_not_template() {
return;
}
let narrowed = narrow_strict_subclass_of(¤t, class_name, db, &ctx.template_param_names);
if !narrowed.is_empty() && narrowed != current {
ctx.set_prop_refined(obj_var, prop, narrowed);
}
}