use php_ast::ast::BinaryOp;
use php_ast::owned::ExprKind;
use crate::db::MirDatabase;
use crate::flow_state::FlowState;
use super::core::{
apply_prop_narrowed, count_or_strlen_emptiness, extract_static_prop_access,
narrow_receiver_non_null_on_prop_match, resolve_prop_current_type,
resolve_static_prop_current_type, ScalarArgTarget,
};
use super::literals::{
expr_is_nonempty_string_literal, narrow_string_to_empty, narrow_string_to_non_empty,
};
pub(super) fn extract_strlen_arg(expr: &php_ast::owned::Expr) -> Option<ScalarArgTarget> {
if let ExprKind::FunctionCall(call) = &expr.kind {
let name = match &call.name.kind {
ExprKind::Identifier(n) => n.as_ref(),
_ => return None,
};
let bare = name.trim_start_matches('\\');
if bare.eq_ignore_ascii_case("strlen")
|| bare.eq_ignore_ascii_case("mb_strlen")
|| bare.eq_ignore_ascii_case("iconv_strlen")
{
if let Some(arg) = call.args.first() {
let value = arg.value.as_ref()?;
return ScalarArgTarget::extract(value);
}
}
}
None
}
pub(super) fn extract_strlen_static_prop_arg(
expr: &php_ast::owned::Expr,
ctx: &FlowState,
db: &dyn MirDatabase,
file: &str,
) -> Option<(std::sync::Arc<str>, String)> {
if let ExprKind::FunctionCall(call) = &expr.kind {
let name = match &call.name.kind {
ExprKind::Identifier(n) => n.as_ref(),
_ => return None,
};
let bare = name.trim_start_matches('\\');
if bare.eq_ignore_ascii_case("strlen")
|| bare.eq_ignore_ascii_case("mb_strlen")
|| bare.eq_ignore_ascii_case("iconv_strlen")
{
if let Some(arg) = call.args.first() {
let value = arg.value.as_ref()?;
return extract_static_prop_access(value, ctx, db, file);
}
}
}
None
}
pub(super) fn narrow_string_strlen_comparison(
ctx: &mut FlowState,
str_var: &str,
op: BinaryOp,
n: i64,
is_true: bool,
) {
let Some(non_empty) = count_or_strlen_emptiness(op, n, is_true) else {
return;
};
let current = ctx.get_var(str_var);
if current.is_mixed() {
return;
}
let base = if non_empty {
current.remove_null()
} else {
current.clone()
};
let narrowed = if non_empty {
narrow_string_to_non_empty(&base)
} else {
narrow_string_to_empty(&base)
};
if !narrowed.is_empty() && narrowed != current {
ctx.set_var(str_var, narrowed);
}
}
#[allow(clippy::too_many_arguments)]
pub(super) fn narrow_prop_string_strlen_comparison(
ctx: &mut FlowState,
obj_var: &str,
prop: &str,
db: &dyn MirDatabase,
file: &str,
op: BinaryOp,
n: i64,
is_true: bool,
) {
let Some(non_empty) = count_or_strlen_emptiness(op, n, is_true) else {
return;
};
if non_empty {
narrow_receiver_non_null_on_prop_match(ctx, obj_var, true);
}
let current = resolve_prop_current_type(ctx, obj_var, prop, db, file);
if current.is_mixed() {
return;
}
let base = if non_empty {
current.remove_null()
} else {
current.clone()
};
let narrowed = if non_empty {
narrow_string_to_non_empty(&base)
} else {
narrow_string_to_empty(&base)
};
apply_prop_narrowed(ctx, obj_var, prop, current, narrowed, false);
}
pub(super) fn narrow_static_prop_string_strlen_comparison(
ctx: &mut FlowState,
fqcn: &str,
prop: &str,
db: &dyn MirDatabase,
op: BinaryOp,
n: i64,
is_true: bool,
) {
let Some(non_empty) = count_or_strlen_emptiness(op, n, is_true) else {
return;
};
let current = resolve_static_prop_current_type(ctx, fqcn, prop, db);
if current.is_mixed() {
return;
}
let base = if non_empty {
current.remove_null()
} else {
current.clone()
};
let narrowed = if non_empty {
narrow_string_to_non_empty(&base)
} else {
narrow_string_to_empty(&base)
};
apply_prop_narrowed(ctx, fqcn, prop, current, narrowed, false);
}
pub(super) fn narrow_string_false_comparable_condition(
call: &php_ast::owned::FunctionCallExpr,
ctx: &mut FlowState,
db: &dyn MirDatabase,
file: &str,
is_false: bool,
) {
if is_false {
return;
}
let (Some(haystack_arg), Some(needle_arg)) = (call.args.first(), call.args.get(1)) else {
return;
};
let (Some(haystack_value), Some(needle_value)) = (&haystack_arg.value, &needle_arg.value)
else {
return;
};
let needle_non_empty = expr_is_nonempty_string_literal(needle_value, ctx, db, file);
if !needle_non_empty {
return;
}
match ScalarArgTarget::extract(haystack_value) {
Some(ScalarArgTarget::Var(var_name)) => {
let current = ctx.get_var(&var_name);
if !current.is_mixed() {
let narrowed = narrow_string_to_non_empty(¤t);
if narrowed != current {
ctx.set_var(&var_name, narrowed);
}
}
}
Some(ScalarArgTarget::Prop(obj, prop)) => {
narrow_receiver_non_null_on_prop_match(ctx, &obj, true);
let current = resolve_prop_current_type(ctx, &obj, &prop, db, file);
if !current.is_mixed() {
let narrowed = narrow_string_to_non_empty(¤t);
apply_prop_narrowed(ctx, &obj, &prop, current, narrowed, false);
}
}
None => {
if let Some((fqcn, prop)) = extract_static_prop_access(haystack_value, ctx, db, file) {
let current = resolve_static_prop_current_type(ctx, &fqcn, &prop, db);
if !current.is_mixed() {
let narrowed = narrow_string_to_non_empty(¤t);
apply_prop_narrowed(ctx, &fqcn, &prop, current, narrowed, false);
}
}
}
}
}