use php_ast::owned::ExprKind;
use mir_types::{Atomic, Type};
use crate::db::MirDatabase;
use crate::flow_state::FlowState;
use super::core::{
apply_prop_narrowed, extract_any_prop_access, extract_class_name, resolve_prop_current_type,
resolve_static_prop_current_type, set_narrowed, UnionNarrowExt,
};
use super::instanceof_core::{named_object_matches_instanceof, project_type_params_onto_subclass};
pub(super) fn expand_enum_to_cases(db: &dyn MirDatabase, ty: &Type, enum_fqcn: &str) -> Type {
if !ty
.types
.iter()
.any(|a| matches!(a, Atomic::TNamedObject { fqcn, .. } if fqcn.as_ref() == enum_fqcn))
{
return ty.clone();
}
let Some(crate::db::ClassLike::Enum(e)) =
crate::db::find_class_like(db, crate::db::Fqcn::from_str(db, enum_fqcn))
else {
return ty.clone();
};
let mut result = Type::empty();
result.possibly_undefined = ty.possibly_undefined;
result.from_docblock = ty.from_docblock;
for atomic in &ty.types {
match atomic {
Atomic::TNamedObject { fqcn, .. } if fqcn.as_ref() == enum_fqcn => {
for case_name in e.cases.keys() {
result.add_type(Atomic::TLiteralEnumCase {
enum_fqcn: enum_fqcn.into(),
case_name: case_name.as_ref().into(),
});
}
}
other => result.add_type(other.clone()),
}
}
result
}
pub(super) fn extract_enum_value_case(
prop_expr: &php_ast::owned::Expr,
literal_expr: &php_ast::owned::Expr,
ctx: &FlowState,
db: &dyn MirDatabase,
) -> Option<(String, String, String)> {
let (var_name, prop) = extract_any_prop_access(prop_expr)?;
if prop != "value" {
return None;
}
let literal = match &literal_expr.kind {
ExprKind::String(s) => Atomic::TLiteralString(std::sync::Arc::from(s.as_ref())),
ExprKind::Int(n) => Atomic::TLiteralInt(*n),
_ => return None,
};
let current = ctx.get_var(&var_name);
let enum_fqcn = current.types.iter().find_map(|a| match a {
Atomic::TNamedObject { fqcn, .. } => Some(fqcn.as_ref().to_string()),
Atomic::TLiteralEnumCase { enum_fqcn, .. } => Some(enum_fqcn.to_string()),
_ => None,
})?;
let crate::db::ClassLike::Enum(e) =
crate::db::find_class_like(db, crate::db::Fqcn::from_str(db, &enum_fqcn))?
else {
return None;
};
e.scalar_type.as_ref()?; let mut matched: Option<String> = None;
for (name, case) in e.cases.iter() {
let is_match = case
.value
.as_ref()
.is_some_and(|v| v.types.contains(&literal));
if is_match {
if matched.is_some() {
return None;
}
matched = Some(name.to_string());
}
}
matched.map(|case_name| (var_name, enum_fqcn, case_name))
}
pub(super) fn narrow_var_to_literal_enum_case(
db: &dyn MirDatabase,
ctx: &mut FlowState,
name: &str,
enum_fqcn: &str,
case_name: &str,
is_case: bool,
) {
let current = ctx.get_var(name);
let narrowed = if is_case {
Type::single(Atomic::TLiteralEnumCase {
enum_fqcn: enum_fqcn.into(),
case_name: case_name.into(),
})
} else {
expand_enum_to_cases(db, ¤t, enum_fqcn).filter(|t| {
!matches!(t, Atomic::TLiteralEnumCase { enum_fqcn: fqcn, case_name: c }
if fqcn.as_ref() == enum_fqcn && c.as_ref() == case_name)
})
};
set_narrowed(ctx, name, ¤t, narrowed, true);
}
pub(super) fn narrow_prop_to_literal_enum_case(
db: &dyn MirDatabase,
ctx: &mut FlowState,
obj_var: &str,
prop: &str,
file: &str,
(enum_fqcn, case_name): (&str, &str),
is_case: bool,
) {
let current = resolve_prop_current_type(ctx, obj_var, prop, db, file);
let narrowed = if is_case {
Type::single(Atomic::TLiteralEnumCase {
enum_fqcn: enum_fqcn.into(),
case_name: case_name.into(),
})
} else {
expand_enum_to_cases(db, ¤t, enum_fqcn).filter(|t| {
!matches!(t, Atomic::TLiteralEnumCase { enum_fqcn: fqcn, case_name: c }
if fqcn.as_ref() == enum_fqcn && c.as_ref() == case_name)
})
};
let mark_diverges = is_case || !ctx.get_var(obj_var).is_nullable();
apply_prop_narrowed(ctx, obj_var, prop, current, narrowed, mark_diverges);
}
pub(super) fn narrow_static_prop_to_literal_enum_case(
db: &dyn MirDatabase,
ctx: &mut FlowState,
fqcn: &str,
prop: &str,
(enum_fqcn, case_name): (&str, &str),
is_case: bool,
) {
let current = resolve_static_prop_current_type(ctx, fqcn, prop, db);
let narrowed = if is_case {
Type::single(Atomic::TLiteralEnumCase {
enum_fqcn: enum_fqcn.into(),
case_name: case_name.into(),
})
} else {
expand_enum_to_cases(db, ¤t, enum_fqcn).filter(|t| {
!matches!(t, Atomic::TLiteralEnumCase { enum_fqcn: fqcn, case_name: c }
if fqcn.as_ref() == enum_fqcn && c.as_ref() == case_name)
})
};
apply_prop_narrowed(ctx, fqcn, prop, current, narrowed, true);
}
pub(super) fn narrow_static_prop_to_class_string(
ctx: &mut FlowState,
fqcn_key: &str,
prop: &str,
fqcn: &str,
is_class: bool,
db: &dyn MirDatabase,
) {
let current = resolve_static_prop_current_type(ctx, fqcn_key, prop, db);
let narrowed = if is_class {
Type::single(Atomic::TClassString(Some(mir_types::Name::from(fqcn))))
} else {
current.filter(|t| {
!matches!(t, Atomic::TClassString(Some(f)) if f.as_ref() == fqcn && crate::db::is_final(db, fqcn))
})
};
apply_prop_narrowed(ctx, fqcn_key, prop, current, narrowed, true);
}
pub(super) fn narrow_var_to_class_string(
ctx: &mut FlowState,
name: &str,
fqcn: &str,
is_class: bool,
db: &dyn MirDatabase,
) {
let current = ctx.get_var(name);
let narrowed = if is_class {
Type::single(Atomic::TClassString(Some(mir_types::Name::from(fqcn))))
} else {
current.filter(|t| {
!matches!(t, Atomic::TClassString(Some(f)) if f.as_ref() == fqcn && crate::db::is_final(db, fqcn))
})
};
set_narrowed(ctx, name, ¤t, narrowed, true);
}
pub(super) fn narrow_prop_to_class_string(
ctx: &mut FlowState,
obj_var: &str,
prop: &str,
fqcn: &str,
is_class: bool,
db: &dyn MirDatabase,
file: &str,
) {
let current = resolve_prop_current_type(ctx, obj_var, prop, db, file);
let narrowed = if is_class {
Type::single(Atomic::TClassString(Some(mir_types::Name::from(fqcn))))
} else {
current.filter(|t| {
!matches!(t, Atomic::TClassString(Some(f)) if f.as_ref() == fqcn && crate::db::is_final(db, fqcn))
})
};
let mark_diverges = is_class || !ctx.get_var(obj_var).is_nullable();
apply_prop_narrowed(ctx, obj_var, prop, current, narrowed, mark_diverges);
}
pub(super) fn type_params_for_exact_class(
current: &Type,
target_fqcn: &str,
db: &dyn MirDatabase,
) -> std::sync::Arc<[Type]> {
for t in ¤t.types {
if let Atomic::TNamedObject { fqcn, type_params } = t {
if type_params.is_empty() {
continue;
}
if fqcn.as_ref() == target_fqcn {
return type_params.clone();
}
if named_object_matches_instanceof(target_fqcn, fqcn, db) {
return project_type_params_onto_subclass(db, fqcn, type_params, target_fqcn);
}
}
}
mir_types::union::empty_type_params()
}
pub(super) fn narrow_var_to_specific_class(
ctx: &mut FlowState,
name: &str,
fqcn: &str,
is_exact_class: bool,
db: &dyn MirDatabase,
) {
let current = ctx.get_var(name);
let narrowed = if is_exact_class {
Type::single(Atomic::TNamedObject {
fqcn: fqcn.into(),
type_params: type_params_for_exact_class(¤t, fqcn, db),
})
} else {
current.filter(|t| match t {
Atomic::TNamedObject { fqcn: obj_fqcn, .. }
| Atomic::TSelf { fqcn: obj_fqcn }
| Atomic::TStaticObject { fqcn: obj_fqcn }
| Atomic::TParent { fqcn: obj_fqcn } => {
obj_fqcn.as_ref() != fqcn || !crate::db::is_final(db, fqcn)
}
_ => true,
})
};
set_narrowed(ctx, name, ¤t, narrowed, true);
}
pub(super) fn narrow_prop_to_specific_class(
ctx: &mut FlowState,
obj_var: &str,
prop: &str,
fqcn: &str,
is_exact_class: bool,
db: &dyn MirDatabase,
file: &str,
) {
let current = resolve_prop_current_type(ctx, obj_var, prop, db, file);
let narrowed = if is_exact_class {
Type::single(Atomic::TNamedObject {
fqcn: fqcn.into(),
type_params: type_params_for_exact_class(¤t, fqcn, db),
})
} else {
current.filter(|t| match t {
Atomic::TNamedObject { fqcn: obj_fqcn, .. }
| Atomic::TSelf { fqcn: obj_fqcn }
| Atomic::TStaticObject { fqcn: obj_fqcn }
| Atomic::TParent { fqcn: obj_fqcn } => {
obj_fqcn.as_ref() != fqcn || !crate::db::is_final(db, fqcn)
}
_ => true,
})
};
let mark_diverges = is_exact_class || !ctx.get_var(obj_var).is_nullable();
apply_prop_narrowed(ctx, obj_var, prop, current, narrowed, mark_diverges);
}
pub(super) fn narrow_static_prop_to_specific_class(
ctx: &mut FlowState,
fqcn_receiver: &str,
prop: &str,
fqcn: &str,
is_exact_class: bool,
db: &dyn MirDatabase,
) {
let current = resolve_static_prop_current_type(ctx, fqcn_receiver, prop, db);
let narrowed = if is_exact_class {
Type::single(Atomic::TNamedObject {
fqcn: fqcn.into(),
type_params: type_params_for_exact_class(¤t, fqcn, db),
})
} else {
current.filter(|t| match t {
Atomic::TNamedObject { fqcn: obj_fqcn, .. }
| Atomic::TSelf { fqcn: obj_fqcn }
| Atomic::TStaticObject { fqcn: obj_fqcn }
| Atomic::TParent { fqcn: obj_fqcn } => {
obj_fqcn.as_ref() != fqcn || !crate::db::is_final(db, fqcn)
}
_ => true,
})
};
apply_prop_narrowed(ctx, fqcn_receiver, prop, current, narrowed, true);
}
pub(super) fn narrow_var_to_specific_classes(
ctx: &mut FlowState,
name: &str,
fqcns: &[std::sync::Arc<str>],
db: &dyn MirDatabase,
) {
let current = ctx.get_var(name);
let mut narrowed = Type::empty();
for fqcn in fqcns {
narrowed.merge_with(&Type::single(Atomic::TNamedObject {
fqcn: fqcn.clone().into(),
type_params: type_params_for_exact_class(¤t, fqcn.as_ref(), db),
}));
}
set_narrowed(ctx, name, ¤t, narrowed, true);
}
pub(super) fn narrow_prop_to_specific_classes(
ctx: &mut FlowState,
obj_var: &str,
prop: &str,
fqcns: &[std::sync::Arc<str>],
db: &dyn MirDatabase,
file: &str,
) {
let current = resolve_prop_current_type(ctx, obj_var, prop, db, file);
let mut narrowed = Type::empty();
for fqcn in fqcns {
narrowed.merge_with(&Type::single(Atomic::TNamedObject {
fqcn: fqcn.clone().into(),
type_params: type_params_for_exact_class(¤t, fqcn.as_ref(), db),
}));
}
apply_prop_narrowed(ctx, obj_var, prop, current, narrowed, true);
}
pub(super) fn narrow_static_prop_to_specific_classes(
ctx: &mut FlowState,
fqcn_receiver: &str,
prop: &str,
fqcns: &[std::sync::Arc<str>],
db: &dyn MirDatabase,
) {
let current = resolve_static_prop_current_type(ctx, fqcn_receiver, prop, db);
let mut narrowed = Type::empty();
for fqcn in fqcns {
narrowed.merge_with(&Type::single(Atomic::TNamedObject {
fqcn: fqcn.clone().into(),
type_params: type_params_for_exact_class(¤t, fqcn.as_ref(), db),
}));
}
apply_prop_narrowed(ctx, fqcn_receiver, prop, current, narrowed, true);
}
pub(super) fn extract_enum_case(
expr: &php_ast::owned::Expr,
self_fqcn: Option<&str>,
parent_fqcn: Option<&str>,
db: &dyn MirDatabase,
file: &str,
) -> Option<(String, String)> {
let spa = match &expr.kind {
ExprKind::StaticPropertyAccess(spa) => spa,
ExprKind::ClassConstAccess(cca) => cca,
_ => return None,
};
let enum_short_name = extract_class_name(&spa.class, self_fqcn, parent_fqcn)?;
let enum_fqcn = crate::db::resolve_name(db, file, &enum_short_name);
let ExprKind::Identifier(case_name) = &spa.member.kind else {
return None;
};
let is_declared_case = matches!(
crate::db::find_class_like(db, crate::db::Fqcn::from_str(db, &enum_fqcn)),
Some(crate::db::ClassLike::Enum(e)) if e.cases.contains_key(case_name.as_ref())
);
if !is_declared_case {
return None;
}
Some((enum_fqcn, case_name.to_string()))
}
pub(super) fn extract_class_const_fqcn(
cca: &php_ast::owned::StaticAccessExpr,
self_fqcn: Option<&str>,
parent_fqcn: Option<&str>,
db: &dyn MirDatabase,
file: &str,
) -> Option<String> {
let is_class = matches!(&cca.member.kind, ExprKind::Identifier(n) if n.as_ref() == "class");
if !is_class {
return None;
}
let short = extract_class_name(&cca.class, self_fqcn, parent_fqcn)?;
Some(crate::db::resolve_name(db, file, &short))
}