use php_ast::ast::{AssignOp, BinaryOp};
use php_ast::owned::ExprKind;
use mir_types::{Atomic, Type};
use crate::db::MirDatabase;
use crate::flow_state::FlowState;
use super::arrays::{
extract_count_arg, extract_count_static_prop_arg, narrow_array_count_comparison,
narrow_prop_array_count_comparison, narrow_static_prop_array_count_comparison,
};
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.set_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();
}
let obj_ty = ctx.get_var(obj_var);
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);
}
}
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)?;
let prop = match &pa.property.kind {
ExprKind::Identifier(s) => s.as_ref().to_string(),
_ => return None,
};
Some((obj, prop))
}
ExprKind::Parenthesized(inner) => extract_prop_access(inner),
_ => None,
}
}
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)?;
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::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_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 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()))
}
_ => None,
}
}
pub(crate) enum MatchSubject {
Var(String),
Prop(String, String),
Static(std::sync::Arc<str>, String),
}
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));
}
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(super) 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.set_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 here = crate::db::Fqcn::from_str(db, &resolved);
if let Some(func) = crate::db::find_function(db, here) {
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.set_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::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"));
}
}