use php_ast::owned::ExprKind;
use mir_types::{Atomic, Type};
use crate::db::MirDatabase;
use crate::flow_state::FlowState;
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, ScalarArgTarget,
};
use super::key_exists::{add_key_to_sealed_shapes, remove_key_from_sealed_shapes};
pub(crate) fn narrow_prop_array_empty(
ctx: &mut FlowState,
obj_var: &str,
prop: &str,
db: &dyn MirDatabase,
file: &str,
is_empty: bool,
) {
let current = resolve_prop_current_type(ctx, obj_var, prop, db, file);
let narrowed = if is_empty {
current.narrow_to_empty_collection()
} else {
current.narrow_to_non_empty_collection()
};
apply_prop_narrowed(ctx, obj_var, prop, current, narrowed, false);
}
pub(crate) fn narrow_static_prop_array_empty(
ctx: &mut FlowState,
fqcn: &str,
prop: &str,
db: &dyn MirDatabase,
is_empty: bool,
) {
let current = resolve_static_prop_current_type(ctx, fqcn, prop, db);
let narrowed = if is_empty {
current.narrow_to_empty_collection()
} else {
current.narrow_to_non_empty_collection()
};
apply_prop_narrowed(ctx, fqcn, prop, current, narrowed, false);
}
pub(crate) enum ShapeBase {
Var(String),
Prop(String, String),
Static(std::sync::Arc<str>, String),
}
impl ShapeBase {
fn extract(
expr: &php_ast::owned::Expr,
ctx: &FlowState,
db: &dyn MirDatabase,
file: &str,
) -> Option<Self> {
match ScalarArgTarget::extract(expr) {
Some(ScalarArgTarget::Var(name)) => Some(ShapeBase::Var(name)),
Some(ScalarArgTarget::Prop(obj, prop)) => Some(ShapeBase::Prop(obj, prop)),
None => extract_static_prop_access(expr, ctx, db, file)
.map(|(fqcn, prop)| ShapeBase::Static(fqcn, prop)),
}
}
}
pub(crate) fn array_access_base_target(
expr: &php_ast::owned::Expr,
ctx: &FlowState,
db: &dyn MirDatabase,
file: &str,
) -> Option<ShapeBase> {
match &expr.kind {
ExprKind::ArrayAccess(aa) => array_access_base_target(&aa.array, ctx, db, file),
ExprKind::Parenthesized(inner) => array_access_base_target(inner, ctx, db, file),
_ => ShapeBase::extract(expr, ctx, db, file),
}
}
pub(crate) fn narrow_container_non_null_non_false(
ctx: &mut FlowState,
target: &ShapeBase,
db: &dyn MirDatabase,
file: &str,
) {
match target {
ShapeBase::Var(name) => {
let current = ctx.get_var(name);
ctx.set_var(name, current.remove_null().remove_false());
}
ShapeBase::Prop(obj, prop) => {
let current = resolve_prop_current_type(ctx, obj, prop, db, file);
if !current.is_mixed() {
let narrowed = current.remove_null().remove_false();
apply_prop_narrowed(ctx, obj, prop, current, narrowed, true);
}
}
ShapeBase::Static(fqcn, prop) => {
let current = resolve_static_prop_current_type(ctx, fqcn, prop, db);
if !current.is_mixed() {
let narrowed = current.remove_null().remove_false();
apply_prop_narrowed(ctx, fqcn, prop, current, narrowed, true);
}
}
}
}
pub(crate) fn narrow_isset_shape_key(
var_expr: &php_ast::owned::Expr,
ctx: &mut FlowState,
db: &dyn MirDatabase,
file: &str,
) {
let Some((base, path)) = collect_array_access_path(var_expr, ctx, db, file) else {
return;
};
let current = resolve_shape_base_current_type(ctx, &base, db, file);
if let Some(narrowed) = narrow_shape_path(¤t, &path) {
set_shape_base_narrowed(ctx, &base, current, narrowed);
}
}
pub(crate) fn collect_array_access_path(
expr: &php_ast::owned::Expr,
ctx: &FlowState,
db: &dyn MirDatabase,
file: &str,
) -> Option<(ShapeBase, Vec<mir_types::atomic::ArrayKey>)> {
let ExprKind::ArrayAccess(aa) = &expr.kind else {
return None;
};
let idx = aa.index.as_ref()?;
let key = match &idx.kind {
ExprKind::String(s) => {
mir_types::atomic::ArrayKey::String(std::sync::Arc::from(s.as_ref()))
}
ExprKind::Int(i) => mir_types::atomic::ArrayKey::Int(*i),
_ => return None,
};
if let Some(base) = ShapeBase::extract(&aa.array, ctx, db, file) {
Some((base, vec![key]))
} else {
let (base, mut path) = collect_array_access_path(&aa.array, ctx, db, file)?;
path.push(key);
Some((base, path))
}
}
pub(crate) fn resolve_shape_base_current_type(
ctx: &mut FlowState,
base: &ShapeBase,
db: &dyn MirDatabase,
file: &str,
) -> Type {
match base {
ShapeBase::Var(name) => ctx.get_var(name),
ShapeBase::Prop(obj, prop) => resolve_prop_current_type(ctx, obj, prop, db, file),
ShapeBase::Static(fqcn, prop) => resolve_static_prop_current_type(ctx, fqcn, prop, db),
}
}
pub(crate) fn set_shape_base_narrowed(
ctx: &mut FlowState,
base: &ShapeBase,
current: Type,
narrowed: Type,
) {
match base {
ShapeBase::Var(name) => ctx.set_var(name, narrowed),
ShapeBase::Prop(obj, prop) => apply_prop_narrowed(ctx, obj, prop, current, narrowed, false),
ShapeBase::Static(fqcn, prop) => {
apply_prop_narrowed(ctx, fqcn, prop, current, narrowed, false)
}
}
}
pub(crate) fn set_shape_path(
ty: &Type,
path: &[mir_types::atomic::ArrayKey],
asserted: &Type,
) -> Type {
let Some((key, rest)) = path.split_first() else {
return asserted.clone();
};
let mut touched = false;
let mut result = Type::empty();
for atomic in &ty.types {
match atomic {
Atomic::TKeyedArray {
properties,
is_open,
is_list,
} => {
touched = true;
let mut new_props = properties.clone();
let value_ty = if rest.is_empty() {
asserted.clone()
} else {
let existing = new_props
.get(key)
.map(|p| p.ty.clone())
.unwrap_or_else(Type::mixed);
set_shape_path(&existing, rest, asserted)
};
new_props.insert(
key.clone(),
mir_types::atomic::KeyedProperty {
ty: value_ty,
optional: false,
},
);
result.add_type(Atomic::TKeyedArray {
properties: new_props,
is_open: *is_open,
is_list: *is_list,
});
}
other => result.add_type(other.clone()),
}
}
if touched {
result
} else {
ty.clone()
}
}
pub(crate) fn get_shape_path_type(ty: &Type, path: &[mir_types::atomic::ArrayKey]) -> Type {
let Some((key, rest)) = path.split_first() else {
return ty.clone();
};
let mut result = Type::empty();
let mut found = false;
for atomic in &ty.types {
if let Atomic::TKeyedArray { properties, .. } = atomic {
if let Some(prop) = properties.get(key) {
found = true;
if rest.is_empty() {
result.merge_with(&prop.ty);
} else {
result.merge_with(&get_shape_path_type(&prop.ty, rest));
}
}
}
}
if found {
result
} else {
Type::mixed()
}
}
fn narrow_shape_path(ty: &Type, path: &[mir_types::atomic::ArrayKey]) -> Option<Type> {
let (key, rest) = path.split_first()?;
let mut changed = false;
let mut result = Type::empty();
for atomic in &ty.types {
match atomic {
Atomic::TKeyedArray {
properties,
is_open,
is_list,
} => {
if properties.contains_key(key) {
let mut new_props = properties.clone();
if let Some(prop) = new_props.get_mut(key) {
let mut narrowed_ty = prop.ty.remove_null();
if !rest.is_empty() {
if let Some(deeper) = narrow_shape_path(&narrowed_ty, rest) {
narrowed_ty = deeper;
}
}
if !narrowed_ty.is_empty() {
prop.ty = narrowed_ty;
}
prop.optional = false;
}
changed = true;
result.add_type(Atomic::TKeyedArray {
properties: new_props,
is_open: *is_open,
is_list: *is_list,
});
} else if *is_open {
result.add_type(atomic.clone());
} else {
changed = true;
}
}
_ => result.add_type(atomic.clone()),
}
}
if changed && !result.types.is_empty() {
Some(result)
} else {
None
}
}
pub(crate) fn narrow_isset_shape_key_false(
var_expr: &php_ast::owned::Expr,
ctx: &mut FlowState,
db: &dyn MirDatabase,
file: &str,
) {
let Some((base, path)) = collect_array_access_path(var_expr, ctx, db, file) else {
return;
};
if path.len() != 1 {
return;
}
let key = &path[0];
let current = resolve_shape_base_current_type(ctx, &base, db, file);
let mut changed = false;
let mut result = Type::empty();
for atomic in ¤t.types {
if let Atomic::TKeyedArray { properties, .. } = atomic {
if let Some(prop) = properties.get(key) {
let definitely_present_non_null =
!prop.optional && !prop.ty.types.iter().any(|a| matches!(a, Atomic::TNull));
if definitely_present_non_null {
changed = true;
continue;
}
}
}
result.add_type(atomic.clone());
}
if changed && !result.types.is_empty() {
set_shape_base_narrowed(ctx, &base, current, result);
}
}
pub(crate) fn narrow_shape_path_key_exists(
ty: &Type,
path: &[mir_types::atomic::ArrayKey],
key: &mir_types::atomic::ArrayKey,
) -> Option<Type> {
let Some((head, rest)) = path.split_first() else {
let narrowed = add_key_to_sealed_shapes(ty, key);
return if narrowed != *ty {
Some(narrowed)
} else {
None
};
};
let mut changed = false;
let mut result = Type::empty();
for atomic in &ty.types {
match atomic {
Atomic::TKeyedArray {
properties,
is_open,
is_list,
} => {
if properties.contains_key(head) {
let mut new_props = properties.clone();
if let Some(prop) = new_props.get_mut(head) {
if let Some(deeper) = narrow_shape_path_key_exists(&prop.ty, rest, key) {
prop.ty = deeper;
changed = true;
}
if prop.optional {
prop.optional = false;
changed = true;
}
}
result.add_type(Atomic::TKeyedArray {
properties: new_props,
is_open: *is_open,
is_list: *is_list,
});
} else {
result.add_type(atomic.clone());
}
}
_ => result.add_type(atomic.clone()),
}
}
if changed {
Some(result)
} else {
None
}
}
pub(crate) fn narrow_shape_path_key_exists_false(
ty: &Type,
path: &[mir_types::atomic::ArrayKey],
key: &mir_types::atomic::ArrayKey,
) -> Option<Type> {
let Some((head, rest)) = path.split_first() else {
let narrowed = remove_key_from_sealed_shapes(ty, key);
return if narrowed != *ty {
Some(narrowed)
} else {
None
};
};
let mut changed = false;
let mut result = Type::empty();
for atomic in &ty.types {
match atomic {
Atomic::TKeyedArray {
properties,
is_open,
is_list,
} => {
if properties.contains_key(head) {
let mut new_props = properties.clone();
if let Some(prop) = new_props.get_mut(head) {
if let Some(deeper) =
narrow_shape_path_key_exists_false(&prop.ty, rest, key)
{
prop.ty = deeper;
changed = true;
}
if prop.optional {
prop.optional = false;
changed = true;
}
}
result.add_type(Atomic::TKeyedArray {
properties: new_props,
is_open: *is_open,
is_list: *is_list,
});
} else {
result.add_type(atomic.clone());
}
}
_ => result.add_type(atomic.clone()),
}
}
if changed {
Some(result)
} else {
None
}
}
pub(crate) fn narrow_array_emptiness_condition(
ctx: &mut FlowState,
db: &dyn MirDatabase,
file: &str,
left: &php_ast::owned::Expr,
right: &php_ast::owned::Expr,
effective_true: bool,
) -> bool {
if let ExprKind::Array(elems) = &right.kind {
if elems.is_empty() {
if let Some(var_name) = extract_var_name(left) {
let current = ctx.get_var(&var_name);
let narrowed = if effective_true {
current.narrow_to_empty_collection()
} else {
current.narrow_to_non_empty_collection()
};
if !narrowed.is_empty() && narrowed != current {
ctx.set_var(&var_name, narrowed);
}
} else if let Some((obj, prop)) = extract_any_prop_access(left) {
narrow_prop_array_empty(ctx, &obj, &prop, db, file, effective_true);
narrow_receiver_non_null_on_prop_match(ctx, &obj, effective_true);
} else if let Some((fqcn, prop)) = extract_static_prop_access(left, ctx, db, file) {
narrow_static_prop_array_empty(ctx, &fqcn, &prop, db, effective_true);
}
}
true
} else if let ExprKind::Array(elems) = &left.kind {
if elems.is_empty() {
if let Some(var_name) = extract_var_name(right) {
let current = ctx.get_var(&var_name);
let narrowed = if effective_true {
current.narrow_to_empty_collection()
} else {
current.narrow_to_non_empty_collection()
};
if !narrowed.is_empty() && narrowed != current {
ctx.set_var(&var_name, narrowed);
}
} else if let Some((obj, prop)) = extract_any_prop_access(right) {
narrow_prop_array_empty(ctx, &obj, &prop, db, file, effective_true);
narrow_receiver_non_null_on_prop_match(ctx, &obj, effective_true);
} else if let Some((fqcn, prop)) = extract_static_prop_access(right, ctx, db, file) {
narrow_static_prop_array_empty(ctx, &fqcn, &prop, db, effective_true);
}
}
true
} else {
false
}
}
pub(crate) fn narrow_empty_shape_key(
var_expr: &php_ast::owned::Expr,
ctx: &mut FlowState,
is_true: bool,
db: &dyn MirDatabase,
file: &str,
) {
let Some((base, path)) = collect_array_access_path(var_expr, ctx, db, file) else {
return;
};
if path.len() > 1 {
if !is_true {
let current = resolve_shape_base_current_type(ctx, &base, db, file);
if let Some(narrowed) = narrow_not_empty_shape_path(¤t, &path) {
set_shape_base_narrowed(ctx, &base, current, narrowed);
}
}
return;
}
let key = path
.into_iter()
.next()
.expect("path.len() == 1 checked above");
let current = resolve_shape_base_current_type(ctx, &base, db, file);
let mut changed = false;
let mut result = Type::empty();
for atomic in ¤t.types {
match atomic {
Atomic::TKeyedArray {
properties,
is_open,
is_list,
} => {
if properties.contains_key(&key) {
let mut new_props = properties.clone();
if let Some(prop) = new_props.get_mut(&key) {
if is_true {
let narrowed_ty = prop.ty.narrow_to_falsy();
if !narrowed_ty.is_empty() {
prop.ty = narrowed_ty;
}
} else {
let narrowed_ty = prop.ty.narrow_to_truthy();
if !narrowed_ty.is_empty() {
prop.ty = narrowed_ty;
}
prop.optional = false;
}
}
changed = true;
result.add_type(Atomic::TKeyedArray {
properties: new_props,
is_open: *is_open,
is_list: *is_list,
});
} else if *is_open || is_true {
result.add_type(atomic.clone());
} else {
changed = true;
}
}
_ => result.add_type(atomic.clone()),
}
}
if changed && !result.types.is_empty() {
set_shape_base_narrowed(ctx, &base, current, result);
}
}
fn narrow_not_empty_shape_path(ty: &Type, path: &[mir_types::atomic::ArrayKey]) -> Option<Type> {
let (key, rest) = path.split_first()?;
let is_last = rest.is_empty();
let mut changed = false;
let mut result = Type::empty();
for atomic in &ty.types {
match atomic {
Atomic::TKeyedArray {
properties,
is_open,
is_list,
} => {
if properties.contains_key(key) {
let mut new_props = properties.clone();
if let Some(prop) = new_props.get_mut(key) {
if is_last {
let narrowed_ty = prop.ty.narrow_to_truthy();
if !narrowed_ty.is_empty() {
prop.ty = narrowed_ty;
}
} else {
let mut narrowed_ty = prop.ty.remove_null();
if let Some(deeper) = narrow_not_empty_shape_path(&narrowed_ty, rest) {
narrowed_ty = deeper;
}
if !narrowed_ty.is_empty() {
prop.ty = narrowed_ty;
}
}
prop.optional = false;
}
changed = true;
result.add_type(Atomic::TKeyedArray {
properties: new_props,
is_open: *is_open,
is_list: *is_list,
});
} else if *is_open {
result.add_type(atomic.clone());
} else {
changed = true;
}
}
_ => result.add_type(atomic.clone()),
}
}
if changed && !result.types.is_empty() {
Some(result)
} else {
None
}
}