use mir_types::{ArrayKey, Atomic, Name, Type};
use php_ast::ast::BinaryOp;
use php_ast::owned::{Expr, ExprKind};
use rustc_hash::FxHashSet;
use crate::subtype::is_subtype;
pub fn canonical_int_array_key(s: &str) -> Option<i64> {
let bytes = s.as_bytes();
if bytes.is_empty() {
return None;
}
let (neg, digits) = if bytes[0] == b'-' {
(true, &bytes[1..])
} else {
(false, bytes)
};
if digits.is_empty() || !digits.iter().all(u8::is_ascii_digit) {
return None;
}
if digits.len() > 1 && digits[0] == b'0' {
return None;
}
if neg && digits == b"0" {
return None;
}
std::str::from_utf8(digits)
.ok()?
.parse::<i64>()
.ok()
.map(|v| if neg { -v } else { v })
}
pub fn literal_array_key_of_kind(kind: &ExprKind) -> Option<ArrayKey> {
match kind {
ExprKind::String(s) => Some(match canonical_int_array_key(s) {
Some(i) => ArrayKey::Int(i),
None => ArrayKey::String(std::sync::Arc::from(s.as_ref())),
}),
ExprKind::Int(i) => Some(ArrayKey::Int(*i)),
ExprKind::Bool(b) => Some(ArrayKey::Int(if *b { 1 } else { 0 })),
ExprKind::Float(f) => Some(ArrayKey::Int(*f as i64)),
ExprKind::Null => Some(ArrayKey::String(std::sync::Arc::from(""))),
_ => None,
}
}
pub fn coerce_array_key_type(ty: &Type) -> Type {
let mut changed = false;
let mut out = Type::empty();
for a in &ty.types {
match a {
Atomic::TTrue => {
changed = true;
out.add_type(Atomic::TLiteralInt(1));
}
Atomic::TFalse => {
changed = true;
out.add_type(Atomic::TLiteralInt(0));
}
Atomic::TBool => {
changed = true;
out.add_type(Atomic::TInt);
}
Atomic::TNull => {
changed = true;
out.add_type(Atomic::TLiteralString(std::sync::Arc::from("")));
}
Atomic::TFloat | Atomic::TIntegralFloat => {
changed = true;
out.add_type(Atomic::TInt);
}
Atomic::TLiteralFloat(hi, lo) => {
changed = true;
let bits = ((*hi as u64) << 32) | (*lo as u32 as u64);
out.add_type(Atomic::TLiteralInt(f64::from_bits(bits) as i64));
}
Atomic::TLiteralString(s) => match canonical_int_array_key(s) {
Some(i) => {
changed = true;
out.add_type(Atomic::TLiteralInt(i));
}
None => out.add_type(a.clone()),
},
_ => out.add_type(a.clone()),
}
}
if !changed {
return ty.clone();
}
out.possibly_undefined = ty.possibly_undefined;
out.from_docblock = ty.from_docblock;
out
}
fn widen_key_to_base(key: &ArrayKey) -> Atomic {
match key {
ArrayKey::String(_) => Atomic::TString,
ArrayKey::Int(_) => Atomic::TInt,
}
}
fn widen_key_type(ty: &Type) -> Type {
let mut out = Type::empty();
for a in &ty.types {
match a {
Atomic::TLiteralString(_) => out.add_type(Atomic::TString),
Atomic::TLiteralInt(_) => out.add_type(Atomic::TInt),
other => out.add_type(other.clone()),
}
}
out
}
pub fn set_nested_keyed_value(
current: &Type,
path: &[ArrayKey],
leaf_value: &Type,
) -> Option<Type> {
let (key, rest) = path.split_first()?;
if current.types.is_empty() {
return None;
}
let all_shapes_have_key = current.types.iter().all(
|a| matches!(a, Atomic::TKeyedArray { properties, .. } if properties.contains_key(key)),
);
if !all_shapes_have_key {
return None;
}
let mut result = Type::empty();
result.possibly_undefined = current.possibly_undefined;
result.from_docblock = current.from_docblock;
for atomic in ¤t.types {
let Atomic::TKeyedArray {
properties,
is_open,
is_list,
} = atomic
else {
unreachable!("filtered to TKeyedArray above")
};
let mut new_properties = properties.clone();
let existing = properties.get(key).expect("checked by all_shapes_have_key");
let new_inner = if rest.is_empty() {
leaf_value.clone()
} else {
set_nested_keyed_value(&existing.ty, rest, leaf_value)?
};
new_properties.insert(
key.clone(),
mir_types::atomic::KeyedProperty {
ty: new_inner,
optional: false,
},
);
result.add_type(Atomic::TKeyedArray {
properties: new_properties,
is_open: *is_open,
is_list: *is_list,
});
}
Some(result)
}
pub fn remove_key_from_shapes(ty: &Type, key: &ArrayKey) -> Type {
let mut changed = false;
let mut result = Type::empty();
for a in &ty.types {
if let Atomic::TKeyedArray {
properties,
is_open,
is_list,
} = a
{
if properties.contains_key(key) {
changed = true;
let mut new_props = properties.clone();
new_props.shift_remove(key);
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();
}
result.from_docblock = ty.from_docblock;
result
}
pub enum DefiniteKeyState {
Absent,
Present(Type),
}
pub fn definite_key_state(current: &Type, key: &ArrayKey) -> Option<DefiniteKeyState> {
if current.types.is_empty() {
return None;
}
let mut any_absent = false;
let mut any_present = false;
let mut present_ty: Option<Type> = None;
for a in ¤t.types {
let Atomic::TKeyedArray {
properties,
is_open,
..
} = a
else {
return None;
};
match properties.get(key) {
Some(prop) if !prop.optional && !prop.ty.is_nullable() => {
any_present = true;
fold_into(&mut present_ty, prop.ty.clone());
}
Some(_) => return None,
None if !*is_open => any_absent = true,
None => return None,
}
}
match (any_absent, any_present) {
(true, false) => Some(DefiniteKeyState::Absent),
(false, true) => present_ty.map(DefiniteKeyState::Present),
_ => None,
}
}
const MAX_SHAPE_KEYS: usize = 8;
fn try_insert_new_shape_key(current: &Type, key: &ArrayKey, new_value: &Type) -> Option<Type> {
if current.types.is_empty() {
return None;
}
let all_growable = current.types.iter().all(|a| {
matches!(a, Atomic::TKeyedArray { properties, .. }
if !properties.contains_key(key) && properties.len() < MAX_SHAPE_KEYS)
});
if !all_growable {
return None;
}
let mut result = Type::empty();
result.possibly_undefined = current.possibly_undefined;
result.from_docblock = current.from_docblock;
for atomic in ¤t.types {
let Atomic::TKeyedArray {
properties,
is_open,
is_list,
} = atomic
else {
unreachable!("filtered to growable TKeyedArray above")
};
let next_is_list =
*is_list && matches!(key, ArrayKey::Int(i) if *i == properties.len() as i64);
let mut new_properties = properties.clone();
new_properties.insert(
key.clone(),
mir_types::atomic::KeyedProperty {
ty: new_value.clone(),
optional: false,
},
);
result.add_type(Atomic::TKeyedArray {
properties: new_properties,
is_open: *is_open,
is_list: next_is_list,
});
}
Some(result)
}
fn try_push_new_shape_key(current: &Type, new_value: &Type) -> Option<Type> {
if current.types.is_empty() {
return None;
}
let all_growable = current.types.iter().all(|a| {
matches!(a, Atomic::TKeyedArray { properties, is_list, .. }
if *is_list && properties.len() < MAX_SHAPE_KEYS)
});
if !all_growable {
return None;
}
let mut result = Type::empty();
result.possibly_undefined = current.possibly_undefined;
result.from_docblock = current.from_docblock;
for atomic in ¤t.types {
let Atomic::TKeyedArray {
properties,
is_open,
..
} = atomic
else {
unreachable!("filtered to growable TKeyedArray above")
};
let mut new_properties = properties.clone();
new_properties.insert(
ArrayKey::Int(properties.len() as i64),
mir_types::atomic::KeyedProperty {
ty: new_value.clone(),
optional: false,
},
);
result.add_type(Atomic::TKeyedArray {
properties: new_properties,
is_open: *is_open,
is_list: true,
});
}
Some(result)
}
fn declared_array_key_value(declared: Option<&Type>) -> Option<(Type, Type)> {
declared?.types.iter().find_map(|a| match a {
Atomic::TArray { key, value } | Atomic::TNonEmptyArray { key, value } => {
Some(((**key).clone(), (**value).clone()))
}
Atomic::TList { value } | Atomic::TNonEmptyList { value } => {
Some((Type::single(Atomic::TInt), (**value).clone()))
}
_ => None,
})
}
pub fn widen_array_with_value_and_key(
current: &Type,
new_value: &Type,
new_key: &Type,
literal_key: Option<&mir_types::ArrayKey>,
inside_loop: bool,
declared_ceiling: Option<&Type>,
) -> Type {
if let Some(key) = literal_key {
let all_shapes_have_key = !current.types.is_empty()
&& current.types.iter().all(|a| match a {
Atomic::TKeyedArray { properties, .. } => properties.contains_key(key),
_ => false,
});
if all_shapes_have_key {
let mut result = Type::empty();
result.possibly_undefined = current.possibly_undefined;
result.from_docblock = current.from_docblock;
for atomic in ¤t.types {
let Atomic::TKeyedArray {
properties,
is_open,
is_list,
} = atomic
else {
unreachable!("filtered to TKeyedArray above")
};
let mut new_properties = properties.clone();
new_properties.insert(
key.clone(),
mir_types::atomic::KeyedProperty {
ty: new_value.clone(),
optional: false,
},
);
result.add_type(Atomic::TKeyedArray {
properties: new_properties,
is_open: *is_open,
is_list: *is_list,
});
}
return result;
}
if !inside_loop {
if let Some(grown) = try_insert_new_shape_key(current, key, new_value) {
return grown;
}
}
}
let mut result = Type::empty();
result.possibly_undefined = current.possibly_undefined;
result.from_docblock = current.from_docblock;
let mut found_array = false;
let mut acc_key: Option<Type> = None;
let mut acc_value: Option<Type> = None;
let mut acc_list: Option<Type> = None;
for atomic in ¤t.types {
match atomic {
Atomic::TKeyedArray { properties, .. } => {
let mut all_values = new_value.clone();
let mut all_keys = widen_key_type(new_key);
for prop in properties.values() {
all_values.merge_with(&prop.ty);
}
for k in properties.keys() {
all_keys.add_type(widen_key_to_base(k));
}
fold_into(&mut acc_key, all_keys);
fold_into(&mut acc_value, all_values);
found_array = true;
}
Atomic::TArray { key, value } => {
fold_into(&mut acc_key, Type::merge(key, new_key));
fold_into(&mut acc_value, Type::merge(value, new_value));
found_array = true;
}
Atomic::TList { value } | Atomic::TNonEmptyList { value } => {
fold_into(&mut acc_list, Type::merge(value, new_value));
found_array = true;
}
Atomic::TNonEmptyArray { key, value } => {
fold_into(&mut acc_key, Type::merge(key, new_key));
fold_into(&mut acc_value, Type::merge(value, new_value));
found_array = true;
}
Atomic::TMixed => {
return Type::mixed();
}
Atomic::TNull => {}
other => {
result.add_type(other.clone());
}
}
}
if let (Some(mut key), Some(mut value)) = (acc_key, acc_value) {
if let Some((declared_key, declared_value)) = declared_array_key_value(declared_ceiling) {
key.merge_with(&declared_key);
value.merge_with(&declared_value);
}
result.add_type(Atomic::TArray {
key: Box::new(key),
value: Box::new(value),
});
}
if let Some(v) = acc_list {
result.add_type(Atomic::TList { value: Box::new(v) });
}
if !found_array {
return current.clone();
}
result
}
pub fn widen_array_as_list(
current: &Type,
new_value: &Type,
inside_loop: bool,
declared_ceiling: Option<&Type>,
) -> Type {
if !inside_loop {
if let Some(grown) = try_push_new_shape_key(current, new_value) {
return grown;
}
}
let mut result = Type::empty();
result.possibly_undefined = current.possibly_undefined;
result.from_docblock = current.from_docblock;
let mut acc: Option<Type> = Some(new_value.clone());
let mut found_array = false;
for atomic in ¤t.types {
match atomic {
Atomic::TKeyedArray { properties, .. } => {
for prop in properties.values() {
fold_into(&mut acc, prop.ty.clone());
}
found_array = true;
}
Atomic::TArray { value, .. }
| Atomic::TNonEmptyArray { value, .. }
| Atomic::TList { value }
| Atomic::TNonEmptyList { value } => {
fold_into(&mut acc, *value.clone());
found_array = true;
}
Atomic::TMixed => return Type::mixed(),
Atomic::TNull => {}
other => result.add_type(other.clone()),
}
}
if !found_array {
return current.clone();
}
if let Some(mut v) = acc {
if let Some((_, declared_value)) = declared_array_key_value(declared_ceiling) {
v.merge_with(&declared_value);
}
result.add_type(Atomic::TList { value: Box::new(v) });
}
result
}
fn fold_into(acc: &mut Option<Type>, new: Type) {
match acc {
None => *acc = Some(new),
Some(existing) => existing.merge_with(&new),
}
}
fn int_bounds(ty: &Type) -> Option<(Option<i64>, Option<i64>)> {
if ty.types.is_empty() {
return None;
}
let mut min: Option<i64> = Some(i64::MAX);
let mut max: Option<i64> = Some(i64::MIN);
for a in &ty.types {
let (lo, hi) = match a {
Atomic::TLiteralInt(n) => (Some(*n), Some(*n)),
Atomic::TIntRange { min, max } => (*min, *max),
Atomic::TPositiveInt => (Some(1), None),
Atomic::TNonNegativeInt => (Some(0), None),
Atomic::TNegativeInt => (None, Some(-1)),
Atomic::TInt => (None, None),
_ => return None,
};
min = match (min, lo) {
(Some(m), Some(l)) => Some(m.min(l)),
_ => None,
};
max = match (max, hi) {
(Some(m), Some(h)) => Some(m.max(h)),
_ => None,
};
}
Some((min, max))
}
fn contains_int_range(ty: &Type) -> bool {
ty.types.iter().any(|a| {
matches!(
a,
Atomic::TIntRange { .. }
| Atomic::TPositiveInt
| Atomic::TNonNegativeInt
| Atomic::TNegativeInt
)
})
}
fn as_single_literal_int(ty: &Type) -> Option<i64> {
if ty.types.len() == 1 {
if let Atomic::TLiteralInt(n) = &ty.types[0] {
return Some(*n);
}
}
None
}
pub fn infer_int_range_arithmetic(left: &Type, right: &Type, op: BinaryOp) -> Option<Type> {
if let (Some(l), Some(r)) = (as_single_literal_int(left), as_single_literal_int(right)) {
let result = match op {
BinaryOp::Add => l.checked_add(r),
BinaryOp::Sub => l.checked_sub(r),
BinaryOp::Mul => l.checked_mul(r),
BinaryOp::Div if r != 0 && l % r == 0 => Some(l / r),
BinaryOp::Mod if r != 0 => Some(l % r),
_ => None,
};
if let Some(n) = result {
return Some(Type::single(Atomic::TLiteralInt(n)));
}
if op == BinaryOp::Div && r != 0 {
return Some(Type::single(Atomic::TFloat));
}
}
if !contains_int_range(left) && !contains_int_range(right) {
return None;
}
let (lmin, lmax) = int_bounds(left)?;
let (rmin, rmax) = int_bounds(right)?;
let add = |a: Option<i64>, b: Option<i64>| match (a, b) {
(Some(a), Some(b)) => a.checked_add(b),
_ => None,
};
let sub = |a: Option<i64>, b: Option<i64>| match (a, b) {
(Some(a), Some(b)) => a.checked_sub(b),
_ => None,
};
let mul_opt = |a: Option<i64>, b: Option<i64>| match (a, b) {
(Some(a), Some(b)) => a.checked_mul(b),
_ => None,
};
let (min, max) = match op {
BinaryOp::Add => (add(lmin, rmin), add(lmax, rmax)),
BinaryOp::Sub => (sub(lmin, rmax), sub(lmax, rmin)),
BinaryOp::Mul if lmin.is_some_and(|m| m >= 0) && rmin.is_some_and(|m| m >= 0) => {
(mul_opt(lmin, rmin), mul_opt(lmax, rmax))
}
BinaryOp::Mod if rmin == rmax && rmin.is_some_and(|r| r > 0) => {
let divisor = rmin.unwrap();
if lmin.is_some_and(|m| m >= 0) {
(Some(0), Some(divisor - 1))
} else {
(Some(-(divisor - 1)), Some(divisor - 1))
}
}
_ => return None,
};
Some(Type::single(Atomic::TIntRange { min, max }))
}
fn coerces_to_int_in_arithmetic(t: &Atomic) -> bool {
t.is_int()
|| matches!(
t,
Atomic::TBool | Atomic::TTrue | Atomic::TFalse | Atomic::TNull
)
}
pub fn infer_arithmetic(left: &Type, right: &Type) -> Type {
if left.is_mixed() || right.is_mixed() {
return Type::mixed();
}
let left_is_array = left.contains(|t| {
matches!(
t,
Atomic::TArray { .. }
| Atomic::TNonEmptyArray { .. }
| Atomic::TList { .. }
| Atomic::TNonEmptyList { .. }
| Atomic::TKeyedArray { .. }
)
});
let right_is_array = right.contains(|t| {
matches!(
t,
Atomic::TArray { .. }
| Atomic::TNonEmptyArray { .. }
| Atomic::TList { .. }
| Atomic::TNonEmptyList { .. }
| Atomic::TKeyedArray { .. }
)
});
if left_is_array || right_is_array {
let merged_left = if left_is_array {
left.clone()
} else {
Type::single(Atomic::TArray {
key: Box::new(Type::single(Atomic::TMixed)),
value: Box::new(Type::mixed()),
})
};
return merged_left;
}
let left_is_float = left.contains(|t| {
matches!(
t,
Atomic::TFloat | Atomic::TIntegralFloat | Atomic::TLiteralFloat(..)
)
});
let right_is_float = right.contains(|t| {
matches!(
t,
Atomic::TFloat | Atomic::TIntegralFloat | Atomic::TLiteralFloat(..)
)
});
if left_is_float || right_is_float {
Type::single(Atomic::TFloat)
} else if left.contains(coerces_to_int_in_arithmetic)
&& right.contains(coerces_to_int_in_arithmetic)
{
Type::single(Atomic::TInt)
} else {
let mut u = Type::empty();
u.add_type(Atomic::TInt);
u.add_type(Atomic::TFloat);
u
}
}
pub fn infer_div(left: &Type, right: &Type) -> Type {
if left.is_mixed() || right.is_mixed() {
return Type::mixed();
}
let left_is_float = left.contains(|t| {
matches!(
t,
Atomic::TFloat | Atomic::TIntegralFloat | Atomic::TLiteralFloat(..)
)
});
let right_is_float = right.contains(|t| {
matches!(
t,
Atomic::TFloat | Atomic::TIntegralFloat | Atomic::TLiteralFloat(..)
)
});
if left_is_float || right_is_float {
return Type::single(Atomic::TFloat);
}
let mut u = Type::empty();
u.add_type(Atomic::TInt);
u.add_type(Atomic::TFloat);
u
}
pub fn is_non_empty_when_concat(ty: &Type) -> bool {
!ty.types.is_empty()
&& ty.types.iter().all(|a| match a {
Atomic::TNonEmptyString
| Atomic::TNumericString
| Atomic::TCallableString
| Atomic::TClassString(_)
| Atomic::TInterfaceString(_)
| Atomic::TEnumString
| Atomic::TTraitString => true,
Atomic::TLiteralString(s) => !s.is_empty(),
Atomic::TLiteralInt(_)
| Atomic::TInt
| Atomic::TPositiveInt
| Atomic::TNegativeInt
| Atomic::TNonNegativeInt
| Atomic::TIntRange { .. } => true,
Atomic::TFloat | Atomic::TIntegralFloat | Atomic::TLiteralFloat(..) => true,
Atomic::TTrue => true,
_ => false,
})
}
pub fn as_concat_str(ty: &Type) -> Option<String> {
if ty.types.len() != 1 {
return None;
}
match &ty.types[0] {
Atomic::TLiteralString(s) => Some(s.as_ref().to_string()),
Atomic::TLiteralInt(n) => Some(n.to_string()),
Atomic::TTrue => Some("1".to_string()),
Atomic::TFalse => Some(String::new()),
_ => None,
}
}
pub fn extract_simple_var(expr: &Expr) -> Option<String> {
match &expr.kind {
ExprKind::Variable(name) => Some(name.trim_start_matches('$').to_string()),
ExprKind::Parenthesized(inner) => extract_simple_var(inner),
_ => None,
}
}
pub(crate) fn ast_params_to_fn_params_resolved(
params: &[php_ast::owned::Param],
self_fqcn: Option<&str>,
db: &dyn crate::db::MirDatabase,
file: &str,
) -> Vec<mir_codebase::DeclaredParam> {
params
.iter()
.map(|p| {
let name_str = p.name.as_deref().unwrap_or("").trim_start_matches('$');
let ty = p
.type_hint
.as_ref()
.map(|h| crate::parser::type_from_hint_owned(h, self_fqcn))
.map(|u| resolve_named_objects_in_union(u, db, file));
mir_codebase::DeclaredParam {
name: Name::new(name_str),
ty: mir_codebase::wrap_param_type(ty),
out_ty: None,
has_default: p.default.is_some(),
is_variadic: p.variadic,
is_byref: p.by_ref,
is_optional: p.default.is_some() || p.variadic,
}
})
.collect()
}
pub(crate) fn apply_doc_param_types(
params: &mut [mir_codebase::DeclaredParam],
ast_params: &[php_ast::owned::Param],
doc_params: &[(String, Type)],
db: &dyn crate::db::MirDatabase,
file: &str,
) {
if doc_params.is_empty() {
return;
}
for (param, ast_param) in params.iter_mut().zip(ast_params.iter()) {
let name = ast_param
.name
.as_deref()
.unwrap_or("")
.trim_start_matches('$');
let Some((_, doc_ty)) = doc_params.iter().find(|(n, _)| n == name) else {
continue;
};
let mut doc_ty = resolve_named_objects_in_union(doc_ty.clone(), db, file);
if let Some(native_ty) = param.ty.as_deref() {
if crate::collector::native_hint_wins_over_docblock_scalar(native_ty, &doc_ty) {
continue;
}
doc_ty = crate::collector::resolve_docblock_scalar_conflict(native_ty, doc_ty);
}
doc_ty.from_docblock = true;
param.ty = mir_codebase::wrap_param_type(Some(doc_ty));
}
}
pub(crate) fn resolve_named_objects_in_union(
union: Type,
db: &dyn crate::db::MirDatabase,
file: &str,
) -> Type {
let from_docblock = union.from_docblock;
let possibly_undefined = union.possibly_undefined;
let types: Vec<Atomic> = union
.types
.into_iter()
.map(|a| resolve_named_objects_in_atomic(a, db, file))
.collect();
let mut result = Type::from_vec(types);
result.from_docblock = from_docblock;
result.possibly_undefined = possibly_undefined;
result
}
fn resolve_named_objects_in_atomic(
atomic: Atomic,
db: &dyn crate::db::MirDatabase,
file: &str,
) -> Atomic {
match atomic {
Atomic::TNamedObject { fqcn, type_params } => {
let resolved = crate::db::resolve_name(db, file, fqcn.as_ref());
let type_params = type_params
.iter()
.cloned()
.map(|tp| resolve_named_objects_in_union(tp, db, file))
.collect();
Atomic::TNamedObject {
fqcn: resolved.into(),
type_params,
}
}
Atomic::TArray { key, value } => Atomic::TArray {
key: Box::new(resolve_named_objects_in_union(*key, db, file)),
value: Box::new(resolve_named_objects_in_union(*value, db, file)),
},
Atomic::TList { value } => Atomic::TList {
value: Box::new(resolve_named_objects_in_union(*value, db, file)),
},
Atomic::TIntersection { parts } => Atomic::TIntersection {
parts: parts
.iter()
.cloned()
.map(|p| resolve_named_objects_in_union(p, db, file))
.collect(),
},
other => other,
}
}
pub(crate) fn extract_string_from_expr(expr: &Expr) -> Option<String> {
match &expr.kind {
ExprKind::Identifier(s) => Some(s.trim_start_matches('$').to_string()),
ExprKind::Variable(_) => None,
ExprKind::String(s) => Some(s.to_string()),
_ => None,
}
}
fn literal_condition_key(expr: &Expr) -> Option<(String, String)> {
match &expr.kind {
ExprKind::Int(n) => Some((format!("int:{n}"), n.to_string())),
ExprKind::String(s) => Some((format!("str:{s}"), format!("\"{s}\""))),
ExprKind::Bool(b) => Some((format!("bool:{b}"), b.to_string())),
ExprKind::Null => Some(("null".to_string(), "null".to_string())),
_ => None,
}
}
pub fn duplicate_literal_conditions<'e>(
conditions: impl Iterator<Item = &'e Expr>,
) -> Vec<(php_ast::Span, String)> {
let mut seen = FxHashSet::default();
let mut duplicates = Vec::new();
for cond in conditions {
if let Some((key, display)) = literal_condition_key(cond) {
if !seen.insert(key) {
duplicates.push((cond.span, display));
}
}
}
duplicates
}
pub(crate) fn type_refs_any_template(ty: &Type, names: &FxHashSet<Name>) -> bool {
fn check_atomic(a: &Atomic, names: &FxHashSet<Name>) -> bool {
match a {
Atomic::TTemplateParam { name, .. } => names.contains(name),
Atomic::TNamedObject { fqcn, type_params } => {
if type_params.is_empty() && !fqcn.contains('\\') && names.contains(fqcn) {
return true;
}
type_params
.iter()
.any(|tp| tp.types.iter().any(|a| check_atomic(a, names)))
}
Atomic::TClassString(Some(inner)) => !inner.contains('\\') && names.contains(inner),
_ => false,
}
}
ty.types.iter().any(|a| check_atomic(a, names))
}
fn scalar_types_compatible(value_ty: &Type, prop_ty: &Type) -> bool {
value_ty.is_subtype_structural(prop_ty)
}
pub(crate) fn property_assign_compatible(
value_ty: &Type,
prop_ty: &Type,
db: &dyn crate::db::MirDatabase,
) -> bool {
if scalar_types_compatible(value_ty, prop_ty) {
return true;
}
if is_subtype(db, value_ty, prop_ty) {
return true;
}
value_ty.types.iter().all(|a| match a {
Atomic::TTemplateParam { .. } => true,
Atomic::TClosure { .. } | Atomic::TCallable { .. } => prop_ty.types.iter().any(|p| {
matches!(p, Atomic::TClosure { .. } | Atomic::TCallable { .. })
|| matches!(p, Atomic::TNamedObject { fqcn, .. } if fqcn.as_ref() == "Closure")
}),
Atomic::TNever => true,
Atomic::TNull => prop_ty.is_nullable(),
_ => false,
})
}
pub(crate) fn is_property_type_coercion(
value_ty: &Type,
prop_ty: &Type,
db: &dyn crate::db::MirDatabase,
) -> bool {
if value_ty.is_mixed() || prop_ty.is_mixed() {
return false;
}
let value_core = value_ty.core_type();
if value_core.types.is_empty() || !value_core.is_single() {
return false;
}
let val_fqcn = match value_core.types.first().unwrap() {
Atomic::TNamedObject { fqcn, type_params } if type_params.is_empty() => *fqcn,
_ => return false,
};
prop_ty.types.iter().any(|p| {
let prop_fqcn = match p {
Atomic::TNamedObject { fqcn, type_params } if type_params.is_empty() => fqcn,
_ => return false,
};
crate::db::extends_or_implements(db, prop_fqcn.as_ref(), val_fqcn.as_ref())
})
}
#[cfg(test)]
mod range_arithmetic_tests {
use super::*;
fn range(min: Option<i64>, max: Option<i64>) -> Type {
Type::single(Atomic::TIntRange { min, max })
}
fn lit(n: i64) -> Type {
Type::single(Atomic::TLiteralInt(n))
}
#[test]
fn add_shifts_both_bounds() {
let r =
infer_int_range_arithmetic(&range(Some(0), Some(4)), &lit(5), BinaryOp::Add).unwrap();
assert_eq!(r.to_string(), "int<5, 9>");
}
#[test]
fn add_keeps_unbounded_upper() {
let r = infer_int_range_arithmetic(&range(Some(0), None), &lit(5), BinaryOp::Add).unwrap();
assert_eq!(r.to_string(), "int<5, max>");
}
#[test]
fn sub_lowers_min_to_negative() {
let r = infer_int_range_arithmetic(&range(Some(0), None), &lit(1), BinaryOp::Sub).unwrap();
assert_eq!(r.to_string(), "int<-1, max>");
}
#[test]
fn add_overflow_saturates_to_unbounded() {
let r = infer_int_range_arithmetic(
&range(Some(i64::MAX), Some(i64::MAX)),
&lit(1),
BinaryOp::Add,
)
.unwrap();
assert_eq!(r.to_string(), "int");
}
#[test]
fn no_range_operand_returns_none() {
assert!(
infer_int_range_arithmetic(&Type::single(Atomic::TInt), &lit(3), BinaryOp::Add)
.is_none()
);
}
#[test]
fn non_integer_operand_returns_none() {
assert!(infer_int_range_arithmetic(
&range(Some(0), None),
&Type::single(Atomic::TString),
BinaryOp::Add
)
.is_none());
}
#[test]
fn mul_non_negative_ranges() {
let r = infer_int_range_arithmetic(&range(Some(0), None), &lit(2), BinaryOp::Mul).unwrap();
assert_eq!(r, range(Some(0), None));
let r = infer_int_range_arithmetic(
&range(Some(2), Some(4)),
&range(Some(3), Some(6)),
BinaryOp::Mul,
)
.unwrap();
assert_eq!(r, range(Some(6), Some(24)));
assert!(
infer_int_range_arithmetic(&range(None, Some(-1)), &lit(2), BinaryOp::Mul).is_none()
);
}
#[test]
fn mod_non_negative_ranges() {
let r = infer_int_range_arithmetic(&range(Some(0), None), &lit(5), BinaryOp::Mod).unwrap();
assert_eq!(r, range(Some(0), Some(4)));
let r = infer_int_range_arithmetic(&range(Some(0), Some(100)), &lit(10), BinaryOp::Mod)
.unwrap();
assert_eq!(r, range(Some(0), Some(9)));
assert!(
infer_int_range_arithmetic(&range(Some(0), None), &lit(-5), BinaryOp::Mod).is_none()
);
}
}