use php_ast::ast::{AssignOp, BinaryOp, UnaryPrefixOp};
use php_ast::owned::ExprKind;
use mir_types::{Atomic, Name, Type};
use crate::db::MirDatabase;
use crate::flow_state::{FlagValue, FlowState, Guard, GuardKind};
use super::arrays::{
collect_array_access_path, extract_count_arg, extract_count_static_prop_arg,
narrow_array_count_comparison, narrow_prop_array_count_comparison,
narrow_static_prop_array_count_comparison, ShapeBase,
};
use super::literals::{extract_int_literal, narrow_var_null};
use super::strings::{
extract_strlen_arg, extract_strlen_static_prop_arg, narrow_prop_string_strlen_comparison,
narrow_static_prop_string_strlen_comparison, narrow_string_strlen_comparison,
};
pub(super) fn set_narrowed(
ctx: &mut FlowState,
name: &str,
current: &Type,
narrowed: Type,
mark_diverges: bool,
) {
if !narrowed.is_empty() {
ctx.narrow_var(name, narrowed);
} else if mark_diverges && !current.is_empty() && !current.is_mixed() {
ctx.diverges = true;
}
}
pub(crate) fn resolve_prop_current_type(
ctx: &FlowState,
obj_var: &str,
prop: &str,
db: &dyn MirDatabase,
file: &str,
) -> Type {
if let Some(refined) = ctx.get_prop_refined(obj_var, prop) {
return refined.clone();
}
if prop.ends_with("()") {
return resolve_method_call_current_type(ctx, obj_var, prop, db);
}
let obj_ty = match split_prop_hop(obj_var) {
Some((base, mid)) => resolve_prop_current_type(ctx, base, mid, db, file),
None => receiver_type(ctx, obj_var, db),
};
let mut prop_ty = mir_types::Type::mixed();
'outer: for atomic in &obj_ty.types {
if let mir_types::Atomic::TNamedObject { fqcn, .. } = atomic {
let here = crate::db::Fqcn::from_str(db, fqcn.as_ref());
if let Some((_, p_def)) = crate::db::find_property_in_chain(db, here, prop) {
if let Some(ty) = p_def.ty.as_deref() {
prop_ty = ty.clone();
break 'outer;
}
}
} else if let mir_types::Atomic::TSelf { fqcn }
| mir_types::Atomic::TStaticObject { fqcn }
| mir_types::Atomic::TParent { fqcn } = atomic
{
let here = crate::db::Fqcn::from_str(db, fqcn.as_ref());
if let Some((_, p_def)) = crate::db::find_property_in_chain(db, here, prop) {
if let Some(ty) = p_def.ty.as_deref() {
prop_ty = ty.clone();
break 'outer;
}
}
}
}
if prop_ty.is_mixed() && obj_var == "this" {
if let Some(fqcn) = ctx.self_fqcn.as_ref() {
let resolved = crate::db::resolve_name(db, file, fqcn.as_ref());
let here = crate::db::Fqcn::from_str(db, &resolved);
if let Some((_, p_def)) = crate::db::find_property_in_chain(db, here, prop) {
if let Some(ty) = p_def.ty.as_deref() {
prop_ty = ty.clone();
}
}
}
}
prop_ty
}
pub(crate) fn resolve_static_prop_current_type(
ctx: &FlowState,
fqcn: &str,
prop: &str,
db: &dyn MirDatabase,
) -> Type {
if let Some(refined) = ctx.get_prop_refined(fqcn, prop) {
return refined.clone();
}
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)
}
pub(crate) fn apply_prop_narrowed(
ctx: &mut FlowState,
obj_var: &str,
prop: &str,
current: Type,
narrowed: Type,
mark_diverges: bool,
) {
if !narrowed.is_empty() {
if narrowed != current {
ctx.set_prop_refined(obj_var, prop, narrowed);
}
} else if mark_diverges && !current.is_empty() && !current.is_mixed() {
ctx.diverges = true;
}
}
pub(crate) fn narrow_receiver_non_null_on_prop_match(
ctx: &mut FlowState,
obj_var: &str,
proved_match: bool,
) {
if proved_match {
narrow_var_null(ctx, obj_var, false);
}
}
fn split_prop_hop(key: &str) -> Option<(&str, &str)> {
key.rsplit_once("->")
.filter(|(_, prop)| !prop.ends_with("()"))
}
pub(crate) fn prop_receiver_type(
ctx: &FlowState,
key: &str,
db: &dyn MirDatabase,
file: &str,
) -> Type {
match split_prop_hop(key) {
Some((base, prop)) => resolve_prop_current_type(ctx, base, prop, db, file),
None => ctx.get_var(key),
}
}
pub(crate) fn narrow_receiver_non_null(
ctx: &mut FlowState,
key: &str,
db: &dyn MirDatabase,
file: &str,
) {
let Some((base, prop)) = split_prop_hop(key) else {
narrow_var_null(ctx, key, false);
return;
};
let current = resolve_prop_current_type(ctx, base, prop, db, file);
if !current.is_mixed() {
let narrowed = current.remove_null();
apply_prop_narrowed(ctx, base, prop, current, narrowed, false);
}
}
pub(crate) fn extract_class_fqcn_from_expr(
expr: &php_ast::owned::Expr,
self_fqcn: Option<&str>,
static_fqcn: Option<&str>,
parent_fqcn: Option<&str>,
db: &dyn MirDatabase,
file: &str,
) -> Option<std::sync::Arc<str>> {
let expr = peel_parens(expr);
match &expr.kind {
ExprKind::ClassConstAccess(cca) => {
if let ExprKind::Identifier(id) = &cca.class.kind {
let member = match &cca.member.kind {
ExprKind::Identifier(s) => s.as_ref(),
_ => return None,
};
if member.eq_ignore_ascii_case("class") {
match id.to_ascii_lowercase().as_str() {
"self" | "static" => {
let fqcn = if id.eq_ignore_ascii_case("static") {
static_fqcn.or(self_fqcn)
} else {
self_fqcn
};
return fqcn.map(std::sync::Arc::from);
}
"parent" => return parent_fqcn.map(std::sync::Arc::from),
_ => {
let resolved = crate::db::resolve_name(db, file, id.as_ref());
return Some(std::sync::Arc::from(resolved.as_str()));
}
}
}
}
None
}
ExprKind::String(s) => {
let name = s.as_ref().trim_start_matches('\\');
if !name.is_empty() {
Some(std::sync::Arc::from(name))
} else {
None
}
}
_ => None,
}
}
pub(crate) fn extract_prop_access(expr: &php_ast::owned::Expr) -> Option<(String, String)> {
match &expr.kind {
ExprKind::PropertyAccess(pa) => {
let obj = extract_var_name(&pa.object).or_else(|| method_chain_key(&pa.object))?;
let prop = match &pa.property.kind {
ExprKind::Identifier(s) => s.as_ref().to_string(),
_ => return None,
};
Some((obj, prop))
}
ExprKind::MethodCall(call) if call.args.is_empty() => {
let obj = extract_var_name(&call.object).or_else(|| method_chain_key(&call.object))?;
let ExprKind::Identifier(name) = &call.method.kind else {
return None;
};
Some((obj, method_call_key(name)))
}
ExprKind::Parenthesized(inner) => extract_prop_access(inner),
_ => None,
}
}
pub(crate) fn method_call_key(method: &str) -> String {
format!("{}()", method.to_ascii_lowercase())
}
pub(crate) fn method_chain_key(expr: &php_ast::owned::Expr) -> Option<String> {
match &expr.kind {
ExprKind::MethodCall(call) if call.args.is_empty() => {
let ExprKind::Identifier(name) = &call.method.kind else {
return None;
};
let base = extract_var_name(&call.object).or_else(|| method_chain_key(&call.object))?;
Some(format!("{base}->{}", method_call_key(name)))
}
ExprKind::Parenthesized(inner) => method_chain_key(inner),
_ => None,
}
}
pub(crate) fn memoized_receiver_key(expr: &php_ast::owned::Expr) -> Option<String> {
match &expr.kind {
ExprKind::Variable(name) => Some(name.trim_start_matches('$').to_string()),
_ => method_chain_key(expr),
}
}
pub(crate) fn receiver_key_is_stable(ctx: &FlowState, key: &str, db: &dyn MirDatabase) -> bool {
match key.rsplit_once("->") {
Some((base, call)) if call.ends_with("()") => {
stable_method_return(ctx, base, call, db).is_some()
&& receiver_key_is_stable(ctx, base, db)
}
_ => true,
}
}
fn receiver_type(ctx: &FlowState, key: &str, db: &dyn MirDatabase) -> Type {
match key.rsplit_once("->") {
Some((base, call)) if call.ends_with("()") => ctx
.get_prop_refined(base, call)
.cloned()
.unwrap_or_else(|| resolve_method_call_current_type(ctx, base, call, db)),
_ => ctx.get_var(key),
}
}
fn resolve_method_call_current_type(
ctx: &FlowState,
obj_var: &str,
key: &str,
db: &dyn MirDatabase,
) -> Type {
stable_method_return(ctx, obj_var, key, db).unwrap_or_else(Type::mixed)
}
fn stable_method_return(
ctx: &FlowState,
obj_var: &str,
key: &str,
db: &dyn MirDatabase,
) -> Option<Type> {
let method = key.trim_end_matches("()");
let obj_ty = receiver_type(ctx, obj_var, db);
let [Atomic::TNamedObject { fqcn, .. }] = obj_ty.types.as_slice() else {
return None;
};
let resolved = crate::call::method::resolve_method_from_db(
db,
&std::sync::Arc::from(fqcn.as_ref()),
method,
)?;
let receiver_independent = resolved.template_params.is_empty()
&& resolved.return_ty_raw.types.iter().all(|a| match a {
Atomic::TNamedObject { type_params, .. } => type_params.is_empty(),
Atomic::TSelf { .. }
| Atomic::TStaticObject { .. }
| Atomic::TParent { .. }
| Atomic::TTemplateParam { .. }
| Atomic::TConditional { .. } => false,
_ => true,
});
if resolved.is_static
|| !(resolved.is_pure || resolved.is_mutation_free || db.memoize_method_call_results())
|| !receiver_independent
{
return None;
}
Some(resolved.return_ty_raw)
}
pub(super) fn extract_nullsafe_prop_access(
expr: &php_ast::owned::Expr,
) -> Option<(String, String)> {
match &expr.kind {
ExprKind::NullsafePropertyAccess(pa) => {
let obj = extract_var_name(&pa.object).or_else(|| method_chain_key(&pa.object))?;
let prop = match &pa.property.kind {
ExprKind::Identifier(s) => s.as_ref().to_string(),
_ => return None,
};
Some((obj, prop))
}
ExprKind::Parenthesized(inner) => extract_nullsafe_prop_access(inner),
_ => None,
}
}
pub(crate) fn extract_any_prop_access(expr: &php_ast::owned::Expr) -> Option<(String, String)> {
extract_nullsafe_prop_access(expr).or_else(|| extract_prop_access(expr))
}
pub(crate) fn chained_prop_receiver_key(object_expr: &php_ast::owned::Expr) -> Option<String> {
match &object_expr.kind {
ExprKind::Variable(name) => Some(name.trim_start_matches('$').to_string()),
ExprKind::MethodCall(_) => method_chain_key(object_expr),
ExprKind::PropertyAccess(inner_pa) | ExprKind::NullsafePropertyAccess(inner_pa) => {
let base = extract_var_name(&inner_pa.object)?;
let mid_prop = match &inner_pa.property.kind {
ExprKind::Identifier(s) => s.as_ref(),
_ => return None,
};
Some(format!("{base}->{mid_prop}"))
}
_ => None,
}
}
pub(crate) fn extract_prop_path_access(expr: &php_ast::owned::Expr) -> Option<(String, String)> {
extract_any_prop_access(expr).or_else(|| extract_chained_prop_access(expr))
}
pub(crate) fn extract_chained_prop_access(expr: &php_ast::owned::Expr) -> Option<(String, String)> {
let pa = match &expr.kind {
ExprKind::PropertyAccess(pa) | ExprKind::NullsafePropertyAccess(pa) => pa,
ExprKind::Parenthesized(inner) => return extract_chained_prop_access(inner),
_ => return None,
};
let prop = match &pa.property.kind {
ExprKind::Identifier(s) => s.as_ref().to_string(),
_ => return None,
};
let obj_key = chained_prop_receiver_key(&pa.object)?;
Some((obj_key, prop))
}
pub(crate) fn extract_static_prop_access(
expr: &php_ast::owned::Expr,
ctx: &FlowState,
db: &dyn MirDatabase,
file: &str,
) -> Option<(std::sync::Arc<str>, String)> {
extract_static_prop_access_parts(
expr,
db,
file,
ctx.self_fqcn.as_deref(),
ctx.static_fqcn.as_deref(),
ctx.parent_fqcn.as_deref(),
)
}
pub(super) fn extract_static_prop_access_parts(
expr: &php_ast::owned::Expr,
db: &dyn MirDatabase,
file: &str,
self_fqcn: Option<&str>,
static_fqcn: Option<&str>,
parent_fqcn: Option<&str>,
) -> Option<(std::sync::Arc<str>, String)> {
match &expr.kind {
ExprKind::StaticPropertyAccess(spa) => {
let id = match &spa.class.kind {
ExprKind::Identifier(id) => id,
_ => return None,
};
let resolved = crate::db::resolve_name(db, file, id.as_ref());
let fqcn = match resolved.as_str() {
"self" | "static" => std::sync::Arc::from(self_fqcn.or(static_fqcn)?),
"parent" => std::sync::Arc::from(parent_fqcn?),
s => std::sync::Arc::from(s),
};
let prop = match &spa.member.kind {
ExprKind::Variable(name) | ExprKind::Identifier(name) => {
name.trim_start_matches('$').to_string()
}
_ => return None,
};
Some((fqcn, prop))
}
ExprKind::Parenthesized(inner) => {
extract_static_prop_access_parts(inner, db, file, self_fqcn, static_fqcn, parent_fqcn)
}
_ => None,
}
}
pub(super) fn array_access_under_prop_hops(
expr: &php_ast::owned::Expr,
) -> Option<&php_ast::owned::Expr> {
let mut cur = expr;
let mut hopped = false;
loop {
match &cur.kind {
ExprKind::PropertyAccess(pa) | ExprKind::NullsafePropertyAccess(pa) => {
hopped = true;
cur = &pa.object;
}
ExprKind::Parenthesized(inner) => cur = inner,
ExprKind::ArrayAccess(_) if hopped => return Some(cur),
_ => return None,
}
}
}
pub(crate) fn established_guard(expr: &php_ast::owned::Expr, is_true: bool) -> Option<Guard> {
let plain_var = |e: &php_ast::owned::Expr| match &strip_parens(e).kind {
ExprKind::Variable(v) => Some(Name::from(v.trim_start_matches('$'))),
_ => None,
};
let (var, kind) = match &expr.kind {
ExprKind::Parenthesized(inner) => return established_guard(inner, is_true),
ExprKind::UnaryPrefix(u) if u.op == UnaryPrefixOp::BooleanNot => {
return established_guard(&u.operand, !is_true)
}
ExprKind::Variable(_) if is_true => (plain_var(expr)?, GuardKind::Truthy),
ExprKind::Empty(inner) if !is_true => (plain_var(inner)?, GuardKind::Truthy),
ExprKind::Isset(vars) if is_true && vars.len() == 1 => {
(plain_var(&vars[0])?, GuardKind::NotNull)
}
ExprKind::Binary(b) if matches!(b.op, BinaryOp::Identical | BinaryOp::NotIdentical) => {
let holds = (b.op == BinaryOp::NotIdentical) != is_true;
let (var_side, lit_side) =
match (&strip_parens(&b.left).kind, &strip_parens(&b.right).kind) {
(ExprKind::Variable(_), _) => (&b.left, &b.right),
(_, ExprKind::Variable(_)) => (&b.right, &b.left),
_ => return None,
};
let kind = match &strip_parens(lit_side).kind {
ExprKind::Null if !holds => GuardKind::NotNull,
ExprKind::String(s) if holds => {
GuardKind::Identical(FlagValue::Str(Name::from(&**s)))
}
ExprKind::Int(n) if holds => GuardKind::Identical(FlagValue::Int(*n)),
ExprKind::Bool(true) if holds => GuardKind::Identical(FlagValue::True),
_ => return None,
};
(plain_var(var_side)?, kind)
}
_ => return None,
};
Some(Guard {
var,
kind,
from_flag: false,
})
}
fn strip_parens(mut e: &php_ast::owned::Expr) -> &php_ast::owned::Expr {
while let ExprKind::Parenthesized(inner) = &e.kind {
e = inner;
}
e
}
pub(super) fn extract_var_name(expr: &php_ast::owned::Expr) -> Option<String> {
match &expr.kind {
ExprKind::Variable(name) => Some(name.trim_start_matches('$').to_string()),
ExprKind::Parenthesized(inner) => extract_var_name(inner),
ExprKind::Assign(a) if matches!(a.op, AssignOp::Assign) => extract_var_name(&a.target),
_ => None,
}
}
pub(crate) fn extract_expr_guard_key(
expr: &php_ast::owned::Expr,
ctx: &FlowState,
db: &dyn MirDatabase,
file: &str,
) -> Option<std::sync::Arc<str>> {
match &expr.kind {
ExprKind::Variable(name) => Some(std::sync::Arc::from(name.trim_start_matches('$'))),
ExprKind::Parenthesized(inner) => extract_expr_guard_key(inner, ctx, db, file),
ExprKind::PropertyAccess(pa) => {
let base = extract_var_name(&pa.object)?;
let prop = match &pa.property.kind {
ExprKind::Identifier(s) => s.as_ref(),
ExprKind::Variable(s) => s.trim_start_matches('$'),
_ => return None,
};
Some(std::sync::Arc::from(format!("{base}->{prop}").as_str()))
}
ExprKind::StaticPropertyAccess(_) => {
let (fqcn, prop) = extract_static_prop_access(expr, ctx, db, file)?;
Some(std::sync::Arc::from(
format!("static:{fqcn}::{prop}").as_str(),
))
}
ExprKind::ClassConstAccess(cca) => {
let ExprKind::Identifier(member) = &cca.member.kind else {
return None;
};
if !member.eq_ignore_ascii_case("class") {
return None;
}
let ExprKind::Identifier(class_name) = &cca.class.kind else {
return None;
};
let resolved = crate::db::resolve_name(db, file, class_name.as_ref());
Some(std::sync::Arc::from(format!("cls:{resolved}").as_str()))
}
ExprKind::String(s) => {
let resolved = crate::db::resolve_name(db, file, s.as_ref());
Some(std::sync::Arc::from(format!("cls:{resolved}").as_str()))
}
_ => None,
}
}
pub(crate) enum MatchSubject {
Var(String),
Prop(String, String),
Static(std::sync::Arc<str>, String),
ShapePath(ShapeBase, Vec<mir_types::atomic::ArrayKey>),
}
impl MatchSubject {
pub(crate) fn extract(
expr: &php_ast::owned::Expr,
ctx: &FlowState,
db: &dyn MirDatabase,
file: &str,
) -> Option<Self> {
if let Some(name) = extract_var_name(expr) {
return Some(MatchSubject::Var(name));
}
if let Some((obj, prop)) = extract_prop_access(expr) {
return Some(MatchSubject::Prop(obj, prop));
}
if let Some((base, path)) = collect_array_access_path(expr, ctx, db, file) {
return Some(MatchSubject::ShapePath(base, path));
}
extract_static_prop_access(expr, ctx, db, file)
.map(|(fqcn, prop)| MatchSubject::Static(fqcn, prop))
}
}
pub(super) fn extract_null_coalesce(
expr: &php_ast::owned::Expr,
) -> Option<&php_ast::owned::NullCoalesceExpr> {
match &expr.kind {
ExprKind::NullCoalesce(nc) => Some(nc),
ExprKind::Parenthesized(inner) => extract_null_coalesce(inner),
_ => None,
}
}
pub(super) fn same_literal(a: &php_ast::owned::Expr, b: &php_ast::owned::Expr) -> bool {
let a = peel_parens(a);
let b = peel_parens(b);
match (&a.kind, &b.kind) {
(ExprKind::Null, ExprKind::Null) => true,
(ExprKind::Bool(a), ExprKind::Bool(b)) => a == b,
(ExprKind::Int(a), ExprKind::Int(b)) => a == b,
(ExprKind::String(a), ExprKind::String(b)) => a == b,
_ => false,
}
}
pub(super) fn peel_parens(expr: &php_ast::owned::Expr) -> &php_ast::owned::Expr {
match &expr.kind {
ExprKind::Parenthesized(inner) => peel_parens(inner),
_ => expr,
}
}
pub(super) fn extract_class_name(
expr: &php_ast::owned::Expr,
self_fqcn: Option<&str>,
parent_fqcn: Option<&str>,
) -> Option<String> {
match &expr.kind {
ExprKind::Identifier(name) => match name.to_ascii_lowercase().as_str() {
"self" | "static" => self_fqcn.map(|s| s.to_string()),
"parent" => parent_fqcn.map(|s| s.to_string()),
_ => Some(name.to_string()),
},
ExprKind::Variable(name) if name.trim_start_matches('$') == "this" => {
self_fqcn.map(|s| s.to_string())
}
ExprKind::Variable(_) => None, _ => None,
}
}
pub(crate) fn promote_assignment_effects(
expr: &php_ast::owned::Expr,
ctx: &mut FlowState,
db: &dyn crate::db::MirDatabase,
file: &str,
) {
match &expr.kind {
ExprKind::Assign(a) => {
if let Some(var_name) = extract_var_name(&a.target) {
let sym = mir_types::Name::from(var_name.as_str());
if ctx.possibly_assigned_vars.contains(&sym) {
let ty = ctx.get_var(&var_name);
ctx.narrow_var(&var_name, ty);
std::sync::Arc::make_mut(&mut ctx.possibly_assigned_vars).remove(&sym);
}
}
promote_assignment_effects(&a.value, ctx, db, file);
}
ExprKind::UnaryPrefix(u) => {
promote_assignment_effects(&u.operand, ctx, db, file);
}
ExprKind::FunctionCall(call) => {
if let ExprKind::Identifier(fn_name) = &call.name.kind {
let resolved = crate::db::resolve_name(db, file, fn_name.as_ref());
let find =
|name: &str| crate::db::find_function(db, crate::db::Fqcn::from_str(db, name));
let func = find(&resolved).or_else(|| {
(!fn_name.starts_with('\\') && !fn_name.contains('\\'))
.then(|| find(fn_name.as_ref()))
.flatten()
});
if let Some(func) = func {
for (i, param) in func.params.iter().enumerate() {
if param.is_byref {
let arg = call.args.get(i);
if let Some(arg) = arg {
if let Some(value) = &arg.value {
if let ExprKind::Variable(name) = &value.kind {
let var_name = name.as_ref().trim_start_matches('$');
let sym = mir_types::Name::from(var_name);
if ctx.possibly_assigned_vars.contains(&sym) {
let ty = ctx.get_var(var_name);
ctx.narrow_var(var_name, ty);
std::sync::Arc::make_mut(
&mut ctx.possibly_assigned_vars,
)
.remove(&sym);
}
}
}
}
}
}
}
}
for arg in call.args.iter() {
if let Some(value) = &arg.value {
promote_assignment_effects(value, ctx, db, file);
}
}
}
ExprKind::MethodCall(mc) | ExprKind::NullsafeMethodCall(mc) => {
promote_assignment_effects(&mc.object, ctx, db, file);
for arg in mc.args.iter() {
if let Some(value) = &arg.value {
promote_assignment_effects(value, ctx, db, file);
}
}
}
ExprKind::StaticMethodCall(smc) => {
for arg in smc.args.iter() {
if let Some(value) = &arg.value {
promote_assignment_effects(value, ctx, db, file);
}
}
}
ExprKind::Binary(b) if b.op == BinaryOp::BooleanAnd || b.op == BinaryOp::LogicalAnd => {
promote_assignment_effects(&b.left, ctx, db, file);
}
ExprKind::Binary(b) if b.op == BinaryOp::BooleanOr || b.op == BinaryOp::LogicalOr => {
promote_assignment_effects(&b.left, ctx, db, file);
}
ExprKind::Binary(b) => {
promote_assignment_effects(&b.left, ctx, db, file);
promote_assignment_effects(&b.right, ctx, db, file);
}
ExprKind::Parenthesized(inner) => {
promote_assignment_effects(inner, ctx, db, file);
}
ExprKind::Ternary(t) => {
promote_assignment_effects(&t.condition, ctx, db, file);
}
ExprKind::Cast(_, inner) => {
promote_assignment_effects(inner, ctx, db, file);
}
ExprKind::Isset(args) => {
if let Some(first) = args.first() {
promote_assignment_effects(first, ctx, db, file);
}
}
ExprKind::Array(elements) => {
for element in elements.iter() {
if let Some(key) = &element.key {
promote_assignment_effects(key, ctx, db, file);
}
promote_assignment_effects(&element.value, ctx, db, file);
}
}
ExprKind::NullCoalesce(nc) => {
promote_assignment_effects(&nc.left, ctx, db, file);
}
ExprKind::ArrayAccess(aa) => {
promote_assignment_effects(&aa.array, ctx, db, file);
if let Some(idx) = &aa.index {
promote_assignment_effects(idx, ctx, db, file);
}
}
_ => {}
}
}
pub(super) enum ScalarArgTarget {
Var(String),
Prop(String, String),
}
impl ScalarArgTarget {
pub(super) fn extract(expr: &php_ast::owned::Expr) -> Option<Self> {
if let Some(name) = extract_var_name(expr) {
return Some(ScalarArgTarget::Var(name));
}
extract_any_prop_access(expr).map(|(obj, prop)| ScalarArgTarget::Prop(obj, prop))
}
}
pub(super) trait UnionNarrowExt {
fn filter<F: Fn(&Atomic) -> bool>(&self, f: F) -> Type;
}
impl UnionNarrowExt for Type {
fn filter<F: Fn(&Atomic) -> bool>(&self, f: F) -> Type {
let mut result = Type::empty();
result.possibly_undefined = self.possibly_undefined;
result.from_docblock = self.from_docblock;
for atomic in &self.types {
if f(atomic) {
result.types.push(atomic.clone());
}
}
result
}
}
pub(crate) fn is_numeric_string(s: &str) -> bool {
let t = s.trim();
if t.is_empty() {
return false;
}
t.parse::<i64>().is_ok() || t.parse::<f64>().is_ok_and(f64::is_finite)
}
pub(super) fn narrow_count_or_strlen_equality(
ctx: &mut FlowState,
db: &dyn MirDatabase,
file: &str,
left: &php_ast::owned::Expr,
right: &php_ast::owned::Expr,
op: BinaryOp,
is_true: bool,
) {
let count_on_left = extract_count_arg(left).is_some()
|| extract_count_static_prop_arg(left, ctx, db, file).is_some();
let (count_expr, count_lit) = if count_on_left {
(left, right)
} else {
(right, left)
};
if let (Some(target), Some(n)) = (
extract_count_arg(count_expr),
extract_int_literal(count_lit),
) {
match target {
ScalarArgTarget::Var(arr_var) => {
narrow_array_count_comparison(ctx, &arr_var, op, n, is_true)
}
ScalarArgTarget::Prop(obj, prop) => {
narrow_prop_array_count_comparison(ctx, &obj, &prop, db, file, op, n, is_true)
}
}
return;
} else if let (Some((fqcn, prop)), Some(n)) = (
extract_count_static_prop_arg(count_expr, ctx, db, file),
extract_int_literal(count_lit),
) {
narrow_static_prop_array_count_comparison(ctx, &fqcn, &prop, db, op, n, is_true);
return;
}
let strlen_on_left = extract_strlen_arg(left).is_some()
|| extract_strlen_static_prop_arg(left, ctx, db, file).is_some();
let (strlen_expr, strlen_lit) = if strlen_on_left {
(left, right)
} else {
(right, left)
};
if let (Some(target), Some(n)) = (
extract_strlen_arg(strlen_expr),
extract_int_literal(strlen_lit),
) {
match target {
ScalarArgTarget::Var(str_var) => {
narrow_string_strlen_comparison(ctx, &str_var, op, n, is_true)
}
ScalarArgTarget::Prop(obj, prop) => {
narrow_prop_string_strlen_comparison(ctx, &obj, &prop, db, file, op, n, is_true)
}
}
} else if let (Some((fqcn, prop)), Some(n)) = (
extract_strlen_static_prop_arg(strlen_expr, ctx, db, file),
extract_int_literal(strlen_lit),
) {
narrow_static_prop_string_strlen_comparison(ctx, &fqcn, &prop, db, op, n, is_true);
}
}
pub(super) fn count_or_strlen_emptiness(op: BinaryOp, n: i64, is_true: bool) -> Option<bool> {
match (op, is_true) {
(BinaryOp::Greater, true) if n >= 0 => Some(true), (BinaryOp::GreaterOrEqual, true) if n >= 1 => Some(true), (BinaryOp::Less, false) if n >= 1 => Some(true), (BinaryOp::LessOrEqual, false) if n >= 0 => Some(true), (BinaryOp::Identical | BinaryOp::Equal, true) if n >= 1 => Some(true),
(BinaryOp::Identical | BinaryOp::Equal, false) if n == 0 => Some(true),
(BinaryOp::NotIdentical | BinaryOp::NotEqual, true) if n == 0 => Some(true),
(BinaryOp::NotIdentical | BinaryOp::NotEqual, false) if n >= 1 => Some(true),
(BinaryOp::Less, true) if n <= 1 => Some(false),
(BinaryOp::LessOrEqual, true) if n <= 0 => Some(false),
(BinaryOp::GreaterOrEqual, false) if n <= 1 => Some(false),
(BinaryOp::Greater, false) if n <= 0 => Some(false),
(BinaryOp::Identical | BinaryOp::Equal, true) if n == 0 => Some(false),
(BinaryOp::NotIdentical | BinaryOp::NotEqual, false) if n == 0 => Some(false),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::is_numeric_string;
#[test]
fn numeric_strings_are_recognized() {
assert!(is_numeric_string("42"));
assert!(is_numeric_string("-42"));
assert!(is_numeric_string("+42"));
assert!(is_numeric_string("3.14"));
assert!(is_numeric_string(".5"));
assert!(is_numeric_string("1e10"));
assert!(is_numeric_string(" 123 "));
}
#[test]
fn non_numeric_strings_are_rejected() {
assert!(!is_numeric_string(""));
assert!(!is_numeric_string(" "));
assert!(!is_numeric_string("hello"));
assert!(!is_numeric_string("0x1A"));
assert!(!is_numeric_string("12abc"));
}
#[test]
fn nan_and_infinity_keywords_are_not_numeric() {
assert!(!is_numeric_string("NAN"));
assert!(!is_numeric_string("nan"));
assert!(!is_numeric_string("INF"));
assert!(!is_numeric_string("-INF"));
assert!(!is_numeric_string("Infinity"));
assert!(!is_numeric_string("-Infinity"));
}
}