use php_ast::owned::{ExprKind, FunctionCallExpr};
use mir_types::Atomic;
use crate::db::MirDatabase;
use crate::flow_state::FlowState;
use super::super::class_introspection::{
extract_class_implements_or_parents_arg, extract_class_implements_or_parents_static_prop_arg,
};
use super::super::core::{
apply_prop_narrowed, extract_any_prop_access, extract_static_prop_access, extract_var_name,
narrow_receiver_non_null_on_prop_match, resolve_prop_current_type,
resolve_static_prop_current_type, set_narrowed, ScalarArgTarget,
};
use super::super::instanceof_core::{
filter_out_instanceof_match, narrow_instanceof_preserving_subtypes, narrow_prop_instanceof,
narrow_prop_is_subclass_of, narrow_static_prop_instanceof, narrow_static_prop_is_subclass_of,
narrow_strict_subclass_of,
};
use super::shapes::{
collect_array_access_path, narrow_shape_path_key_exists, narrow_shape_path_key_exists_false,
resolve_shape_base_current_type, set_shape_base_narrowed,
};
pub(crate) fn narrow_static_prop_array_key_exists(
ctx: &mut FlowState,
fqcn: &str,
prop: &str,
key: &mir_types::atomic::ArrayKey,
db: &dyn MirDatabase,
) {
let current = resolve_static_prop_current_type(ctx, fqcn, prop, db);
if current.is_mixed() {
return;
}
let non_null = current.remove_null();
let narrowed = add_key_to_sealed_shapes(&non_null, key);
if narrowed != current {
ctx.set_prop_refined(fqcn, prop, narrowed);
}
}
pub(crate) fn narrow_prop_array_key_exists(
ctx: &mut FlowState,
obj_var: &str,
prop: &str,
key: &mir_types::atomic::ArrayKey,
db: &dyn MirDatabase,
file: &str,
) {
let current = resolve_prop_current_type(ctx, obj_var, prop, db, file);
if current.is_mixed() {
return;
}
let non_null = current.remove_null();
let narrowed = add_key_to_sealed_shapes(&non_null, key);
if narrowed != current {
ctx.set_prop_refined(obj_var, prop, narrowed);
}
}
pub(crate) fn add_key_to_sealed_shapes(
ty: &mir_types::Type,
key: &mir_types::atomic::ArrayKey,
) -> mir_types::Type {
use mir_types::atomic::{ArrayKey, KeyedProperty};
let is_real_union = ty.types.len() > 1;
let mut changed = false;
let mut result = mir_types::Type::empty();
for a in &ty.types {
if let Atomic::TKeyedArray {
properties,
is_open,
is_list,
} = a
{
if !properties.contains_key(key) {
changed = true;
if *is_open || !is_real_union {
let mut new_props = properties.clone();
let stays_list = *is_list
&& matches!(key, ArrayKey::Int(n) if *n == properties.len() as i64);
new_props.insert(
key.clone(),
KeyedProperty {
ty: mir_types::Type::mixed(),
optional: false,
},
);
result.add_type(Atomic::TKeyedArray {
properties: new_props,
is_open: *is_open,
is_list: stays_list,
});
}
continue;
}
if let Some(prop) = properties.get(key) {
if prop.optional {
changed = true;
let mut new_props = properties.clone();
if let Some(new_prop) = new_props.get_mut(key) {
new_prop.optional = false;
}
result.add_type(Atomic::TKeyedArray {
properties: new_props,
is_open: *is_open,
is_list: *is_list,
});
continue;
}
}
}
result.add_type(a.clone());
}
if !changed {
return ty.clone();
}
if result.types.is_empty() {
return ty.clone();
}
result.from_docblock = ty.from_docblock;
result
}
pub(crate) fn remove_key_from_sealed_shapes(
ty: &mir_types::Type,
key: &mir_types::atomic::ArrayKey,
) -> mir_types::Type {
if ty.types.len() <= 1 {
return ty.clone();
}
let mut changed = false;
let mut result = mir_types::Type::empty();
for a in &ty.types {
if let Atomic::TKeyedArray { properties, .. } = a {
if let Some(prop) = properties.get(key) {
if !prop.optional {
changed = true;
continue;
}
}
}
result.add_type(a.clone());
}
if !changed {
return ty.clone();
}
if result.types.is_empty() {
return ty.clone();
}
result.from_docblock = ty.from_docblock;
result
}
pub(crate) fn narrow_array_key_exists_condition(
ctx: &mut FlowState,
call: &FunctionCallExpr,
is_true: bool,
db: &dyn MirDatabase,
file: &str,
) {
if let (Some(key_arg), Some(arr_arg)) = (call.args.first(), call.args.get(1)) {
let literal_key = match &key_arg.value.kind {
ExprKind::String(s) => Some(mir_types::atomic::ArrayKey::String(std::sync::Arc::from(
s.as_ref(),
))),
ExprKind::Int(i) => Some(mir_types::atomic::ArrayKey::Int(*i)),
_ => {
let key_ty = if let Some(name) = extract_var_name(&key_arg.value) {
Some(ctx.get_var(&name))
} else if let Some((obj, prop)) = extract_any_prop_access(&key_arg.value) {
Some(resolve_prop_current_type(ctx, &obj, &prop, db, file))
} else {
extract_static_prop_access(&key_arg.value, ctx, db, file)
.map(|(fqcn, prop)| resolve_static_prop_current_type(ctx, &fqcn, &prop, db))
};
key_ty.and_then(|ty| match ty.types.as_slice() {
[Atomic::TLiteralString(s)] => {
Some(mir_types::atomic::ArrayKey::String(s.clone()))
}
[Atomic::TLiteralInt(i)] => Some(mir_types::atomic::ArrayKey::Int(*i)),
_ => None,
})
}
};
if let Some(key) = literal_key {
if is_true {
if let Some(var_name) = extract_var_name(&arr_arg.value) {
let current = ctx.get_var(&var_name);
let non_null = current.remove_null();
let narrowed = add_key_to_sealed_shapes(&non_null, &key);
if narrowed != current {
ctx.set_var(&var_name, narrowed);
}
} else if let Some((obj, prop)) = extract_any_prop_access(&arr_arg.value) {
narrow_prop_array_key_exists(ctx, &obj, &prop, &key, db, file);
narrow_receiver_non_null_on_prop_match(ctx, &obj, true);
} else if let Some((fqcn, prop)) =
extract_static_prop_access(&arr_arg.value, ctx, db, file)
{
narrow_static_prop_array_key_exists(ctx, &fqcn, &prop, &key, db);
} else if let Some((base, path)) =
collect_array_access_path(&arr_arg.value, ctx, db, file)
{
let current = resolve_shape_base_current_type(ctx, &base, db, file);
if let Some(narrowed) = narrow_shape_path_key_exists(¤t, &path, &key) {
set_shape_base_narrowed(ctx, &base, current, narrowed);
}
} else if let (
mir_types::atomic::ArrayKey::String(iface_name),
Some((target, is_parents)),
) = (
&key,
extract_class_implements_or_parents_arg(&arr_arg.value),
) {
let fqcn = crate::db::resolve_name(db, file, iface_name);
match &target {
ScalarArgTarget::Var(var_name) => {
let current = ctx.get_var(var_name);
let narrowed = if is_parents {
narrow_strict_subclass_of(
¤t,
&fqcn,
db,
&ctx.template_param_names,
)
} else {
narrow_instanceof_preserving_subtypes(
¤t,
&fqcn,
db,
&ctx.template_param_names,
)
};
set_narrowed(ctx, var_name, ¤t, narrowed, true);
}
ScalarArgTarget::Prop(obj, prop) => {
if is_parents {
narrow_prop_is_subclass_of(ctx, obj, prop, &fqcn, db, file, true);
} else {
narrow_prop_instanceof(ctx, obj, prop, &fqcn, db, file, true);
}
narrow_receiver_non_null_on_prop_match(ctx, obj, true);
}
}
} else if let (
mir_types::atomic::ArrayKey::String(iface_name),
Some(((static_fqcn, prop), is_parents)),
) = (
&key,
extract_class_implements_or_parents_static_prop_arg(
&arr_arg.value,
ctx,
db,
file,
),
) {
let fqcn = crate::db::resolve_name(db, file, iface_name);
if is_parents {
narrow_static_prop_is_subclass_of(
ctx,
&static_fqcn,
&prop,
&fqcn,
db,
true,
);
} else {
narrow_static_prop_instanceof(ctx, &static_fqcn, &prop, &fqcn, db, true);
}
}
} else {
if let Some(var_name) = extract_var_name(&arr_arg.value) {
let current = ctx.get_var(&var_name);
let non_null = current.remove_null();
let narrowed = remove_key_from_sealed_shapes(&non_null, &key);
set_narrowed(ctx, &var_name, ¤t, narrowed, true);
} else if let Some((obj, prop)) = extract_any_prop_access(&arr_arg.value) {
let current = resolve_prop_current_type(ctx, &obj, &prop, db, file);
if !current.is_mixed() {
let non_null = current.remove_null();
let narrowed = remove_key_from_sealed_shapes(&non_null, &key);
apply_prop_narrowed(ctx, &obj, &prop, current, narrowed, true);
}
narrow_receiver_non_null_on_prop_match(ctx, &obj, true);
} else if let Some((fqcn, prop)) =
extract_static_prop_access(&arr_arg.value, ctx, db, file)
{
let current = resolve_static_prop_current_type(ctx, &fqcn, &prop, db);
if !current.is_mixed() {
let non_null = current.remove_null();
let narrowed = remove_key_from_sealed_shapes(&non_null, &key);
apply_prop_narrowed(ctx, &fqcn, &prop, current, narrowed, true);
}
} else if let Some((base, path)) =
collect_array_access_path(&arr_arg.value, ctx, db, file)
{
let current = resolve_shape_base_current_type(ctx, &base, db, file);
if let Some(narrowed) =
narrow_shape_path_key_exists_false(¤t, &path, &key)
{
set_shape_base_narrowed(ctx, &base, current, narrowed);
}
} else if let (
mir_types::atomic::ArrayKey::String(iface_name),
Some((target, is_parents)),
) = (
&key,
extract_class_implements_or_parents_arg(&arr_arg.value),
) {
if !is_parents {
let fqcn = crate::db::resolve_name(db, file, iface_name);
match &target {
ScalarArgTarget::Var(var_name) => {
let current = ctx.get_var(var_name);
let narrowed = filter_out_instanceof_match(¤t, &fqcn, db);
set_narrowed(ctx, var_name, ¤t, narrowed, true);
}
ScalarArgTarget::Prop(obj, prop) => {
narrow_prop_instanceof(ctx, obj, prop, &fqcn, db, file, false);
}
}
}
} else if let (
mir_types::atomic::ArrayKey::String(iface_name),
Some(((static_fqcn, prop), is_parents)),
) = (
&key,
extract_class_implements_or_parents_static_prop_arg(
&arr_arg.value,
ctx,
db,
file,
),
) {
if !is_parents {
let fqcn = crate::db::resolve_name(db, file, iface_name);
narrow_static_prop_instanceof(ctx, &static_fqcn, &prop, &fqcn, db, false);
}
}
}
}
}
}