use std::borrow::Cow;
use std::hash::Hash;
use std::hash::Hasher;
use std::sync::Arc;
use mago_word::Word;
use mago_word::concat_word;
use mago_word::empty_word;
use mago_word::join_words;
use mago_word::word;
use crate::metadata::CodebaseMetadata;
use crate::reference::ReferenceSource;
use crate::reference::SymbolReferences;
use crate::symbol::Symbols;
use crate::ttype::TType;
use crate::ttype::TypeRef;
use crate::ttype::atomic::TAtomic;
use crate::ttype::atomic::array::TArray;
use crate::ttype::atomic::array::key::ArrayKey;
use crate::ttype::atomic::generic::TGenericParameter;
use crate::ttype::atomic::mixed::truthiness::TMixedTruthiness;
use crate::ttype::atomic::object::TObject;
use crate::ttype::atomic::object::named::TNamedObject;
use crate::ttype::atomic::populate_atomic_type;
use crate::ttype::atomic::scalar::TScalar;
use crate::ttype::atomic::scalar::bool::TBool;
use crate::ttype::atomic::scalar::class_like_string::TClassLikeString;
use crate::ttype::atomic::scalar::float::TFloat;
use crate::ttype::atomic::scalar::int::TInteger;
use crate::ttype::atomic::scalar::string::TString;
use crate::ttype::atomic::scalar::string::TStringCasing;
use crate::ttype::flags::UnionFlags;
use crate::ttype::get_mixed;
#[derive(Debug, Clone, Eq, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TUnion {
pub types: Cow<'static, [TAtomic]>,
pub flags: UnionFlags,
}
impl Hash for TUnion {
fn hash<H>(&self, state: &mut H)
where
H: Hasher,
{
for t in self.types.as_ref() {
t.hash(state);
}
}
}
impl TUnion {
#[must_use]
pub fn new(types: Cow<'static, [TAtomic]>) -> TUnion {
TUnion { types, flags: UnionFlags::empty() }
}
#[must_use]
pub fn from_vec(mut types: Vec<TAtomic>) -> TUnion {
if cfg!(debug_assertions) {
assert!(
!types.is_empty(),
"TUnion::from_vec() received an empty Vec. This indicates a logic error \
in type construction - unions must contain at least one type. \
Consider using TAtomic::Never for empty/impossible types."
);
}
if types.len() > 1 {
types.retain(|atomic| {
!atomic.is_never() && !atomic.map_generic_parameter_constraint(TUnion::is_never).unwrap_or(false)
});
}
if types.is_empty() {
types.push(TAtomic::Never);
}
if types.len() > 1 {
types.sort_unstable();
}
Self::new(Cow::Owned(types))
}
#[must_use]
pub fn from_single(atomic: Cow<'static, TAtomic>) -> TUnion {
let types_cow = match atomic {
Cow::Borrowed(borrowed_atomic) => Cow::Borrowed(std::slice::from_ref(borrowed_atomic)),
Cow::Owned(owned_atomic) => Cow::Owned(vec![owned_atomic]),
};
TUnion::new(types_cow)
}
#[must_use]
pub fn from_atomic(atomic: TAtomic) -> TUnion {
TUnion::new(Cow::Owned(vec![atomic]))
}
#[inline]
pub fn set_possibly_undefined(&mut self, possibly_undefined: bool, from_try: Option<bool>) {
let from_try = from_try.unwrap_or(self.flags.contains(UnionFlags::POSSIBLY_UNDEFINED_FROM_TRY));
self.flags.set(UnionFlags::POSSIBLY_UNDEFINED, possibly_undefined);
self.flags.set(UnionFlags::POSSIBLY_UNDEFINED_FROM_TRY, from_try);
}
#[inline]
#[must_use]
pub const fn had_template(&self) -> bool {
self.flags.contains(UnionFlags::HAD_TEMPLATE)
}
#[inline]
#[must_use]
pub const fn by_reference(&self) -> bool {
self.flags.contains(UnionFlags::BY_REFERENCE)
}
#[inline]
#[must_use]
pub const fn reference_free(&self) -> bool {
self.flags.contains(UnionFlags::REFERENCE_FREE)
}
#[inline]
#[must_use]
pub const fn possibly_undefined_from_try(&self) -> bool {
self.flags.contains(UnionFlags::POSSIBLY_UNDEFINED_FROM_TRY)
}
#[inline]
#[must_use]
pub const fn possibly_undefined(&self) -> bool {
self.flags.contains(UnionFlags::POSSIBLY_UNDEFINED)
}
#[inline]
#[must_use]
pub const fn ignore_nullable_issues(&self) -> bool {
self.flags.contains(UnionFlags::IGNORE_NULLABLE_ISSUES)
}
#[inline]
#[must_use]
pub const fn ignore_falsable_issues(&self) -> bool {
self.flags.contains(UnionFlags::IGNORE_FALSABLE_ISSUES)
}
#[inline]
#[must_use]
pub const fn from_template_default(&self) -> bool {
self.flags.contains(UnionFlags::FROM_TEMPLATE_DEFAULT)
}
#[inline]
#[must_use]
pub const fn from_unspecified_template(&self) -> bool {
self.flags.contains(UnionFlags::FROM_UNSPECIFIED_TEMPLATE)
}
#[inline]
#[must_use]
pub const fn from_template_fallback(&self) -> bool {
self.from_template_default() || self.from_unspecified_template()
}
#[must_use]
pub fn contains_unspecified_template_arguments(&self) -> bool {
self.from_unspecified_template()
|| self
.get_all_child_nodes()
.into_iter()
.any(|node| matches!(node, TypeRef::Union(union) if union.from_unspecified_template()))
}
#[inline]
#[must_use]
pub const fn populated(&self) -> bool {
self.flags.contains(UnionFlags::POPULATED)
}
#[inline]
#[must_use]
pub const fn has_nullsafe_null(&self) -> bool {
self.flags.contains(UnionFlags::NULLSAFE_NULL)
}
#[inline]
pub fn set_had_template(&mut self, value: bool) {
self.flags.set(UnionFlags::HAD_TEMPLATE, value);
}
#[inline]
pub fn set_by_reference(&mut self, value: bool) {
self.flags.set(UnionFlags::BY_REFERENCE, value);
}
#[inline]
pub fn set_reference_free(&mut self, value: bool) {
self.flags.set(UnionFlags::REFERENCE_FREE, value);
}
#[inline]
pub fn set_possibly_undefined_from_try(&mut self, value: bool) {
self.flags.set(UnionFlags::POSSIBLY_UNDEFINED_FROM_TRY, value);
}
#[inline]
pub fn set_ignore_nullable_issues(&mut self, value: bool) {
self.flags.set(UnionFlags::IGNORE_NULLABLE_ISSUES, value);
}
#[inline]
pub fn set_ignore_falsable_issues(&mut self, value: bool) {
self.flags.set(UnionFlags::IGNORE_FALSABLE_ISSUES, value);
}
#[inline]
pub fn set_from_template_default(&mut self, value: bool) {
self.flags.set(UnionFlags::FROM_TEMPLATE_DEFAULT, value);
}
#[inline]
pub fn set_from_unspecified_template(&mut self, value: bool) {
self.flags.set(UnionFlags::FROM_UNSPECIFIED_TEMPLATE, value);
}
#[inline]
pub fn set_populated(&mut self, value: bool) {
self.flags.set(UnionFlags::POPULATED, value);
}
#[inline]
pub fn set_nullsafe_null(&mut self, value: bool) {
self.flags.set(UnionFlags::NULLSAFE_NULL, value);
}
#[must_use]
pub fn clone_with_types(&self, types: Vec<TAtomic>) -> TUnion {
TUnion { types: Cow::Owned(ensure_non_empty_types(types)), flags: self.flags }
}
#[must_use]
pub fn to_non_nullable(&self) -> TUnion {
TUnion {
types: Cow::Owned(ensure_non_empty_types(self.get_non_nullable_types())),
flags: self.flags & !UnionFlags::NULLSAFE_NULL,
}
}
#[must_use]
pub fn to_truthy(&self) -> TUnion {
TUnion { types: Cow::Owned(ensure_non_empty_types(self.get_truthy_types())), flags: self.flags }
}
#[must_use]
pub fn get_non_nullable_types(&self) -> Vec<TAtomic> {
self.types
.iter()
.filter_map(|t| match t {
TAtomic::Null | TAtomic::Void => None,
TAtomic::GenericParameter(parameter) => Some(TAtomic::GenericParameter(TGenericParameter {
parameter_name: parameter.parameter_name,
defining_entity: parameter.defining_entity,
intersection_types: parameter.intersection_types.clone(),
constraint: Arc::new(parameter.constraint.to_non_nullable()),
})),
TAtomic::Mixed(mixed) => Some(TAtomic::Mixed(mixed.with_is_non_null(true))),
atomic => Some(atomic.clone()),
})
.collect()
}
#[must_use]
pub fn get_truthy_types(&self) -> Vec<TAtomic> {
self.types
.iter()
.filter_map(|t| match t {
TAtomic::GenericParameter(parameter) => Some(TAtomic::GenericParameter(TGenericParameter {
parameter_name: parameter.parameter_name,
defining_entity: parameter.defining_entity,
intersection_types: parameter.intersection_types.clone(),
constraint: Arc::new(parameter.constraint.to_truthy()),
})),
TAtomic::Mixed(mixed) => Some(TAtomic::Mixed(mixed.with_truthiness(TMixedTruthiness::Truthy))),
atomic => {
if atomic.is_falsy() {
None
} else {
Some(atomic.clone())
}
}
})
.collect()
}
pub fn widen_scalars(&mut self) {
self.widen(WidenKind::Scalars);
}
pub fn widen_literals(&mut self) {
self.widen(WidenKind::Literals);
}
fn widen(&mut self, kind: WidenKind) {
for atomic in self.types.to_mut() {
widen_atomic(atomic, kind);
}
}
#[must_use]
pub fn as_nullable(mut self) -> TUnion {
let types = self.types.to_mut();
for atomic in types.iter_mut() {
if let TAtomic::Mixed(mixed) = atomic {
*mixed = mixed.with_is_non_null(false);
}
}
if !types.iter().any(|atomic| atomic.is_null() || atomic.is_mixed()) {
types.push(TAtomic::Null);
}
self
}
pub fn remove_type(&mut self, bad_type: &TAtomic) {
self.types.to_mut().retain(|t| t != bad_type);
}
pub fn replace_type(&mut self, remove_type: &TAtomic, add_type: TAtomic) {
let types = self.types.to_mut();
if let Some(index) = types.iter().position(|t| t == remove_type) {
types[index] = add_type;
} else {
types.push(add_type);
}
}
#[must_use]
pub fn is_int(&self) -> bool {
for atomic in self.types.as_ref() {
if !atomic.is_int() {
return false;
}
}
true
}
#[must_use]
pub fn has_int_and_string(&self) -> bool {
let mut has_int = false;
let mut has_string = false;
for atomic in self.types.as_ref() {
if atomic.is_int() {
has_int = true;
} else if atomic.is_string() {
has_string = true;
} else if atomic.is_array_key() {
has_int = true;
has_string = true;
}
if has_int && has_string {
return true;
}
}
false
}
#[must_use]
pub fn has_int(&self) -> bool {
for atomic in self.types.as_ref() {
if atomic.is_int() || atomic.is_array_key() || atomic.is_numeric() {
return true;
}
}
false
}
#[must_use]
pub fn has_float(&self) -> bool {
for atomic in self.types.as_ref() {
if atomic.is_float() {
return true;
}
}
false
}
#[must_use]
pub fn is_array_key(&self) -> bool {
for atomic in self.types.as_ref() {
if atomic.is_array_key() {
continue;
}
return false;
}
true
}
#[must_use]
pub fn is_any_string(&self) -> bool {
for atomic in self.types.as_ref() {
if !atomic.is_any_string() {
return false;
}
}
true
}
pub fn is_string(&self) -> bool {
self.types.iter().all(TAtomic::is_string) && !self.types.is_empty()
}
#[must_use]
pub fn is_always_array_key(&self, ignore_never: bool) -> bool {
self.types.iter().all(|atomic| match atomic {
TAtomic::Never => ignore_never,
TAtomic::Scalar(scalar) => matches!(
scalar,
TScalar::ArrayKey | TScalar::Integer(_) | TScalar::String(_) | TScalar::ClassLikeString(_)
),
TAtomic::GenericParameter(generic_parameter) => {
generic_parameter.constraint.is_always_array_key(ignore_never)
}
_ => false,
})
}
pub fn is_non_empty_string(&self) -> bool {
self.types.iter().all(TAtomic::is_non_empty_string) && !self.types.is_empty()
}
pub fn is_empty_array(&self) -> bool {
self.types.iter().all(TAtomic::is_empty_array) && !self.types.is_empty()
}
pub fn has_string(&self) -> bool {
self.types.iter().any(TAtomic::is_string) && !self.types.is_empty()
}
pub fn is_float(&self) -> bool {
self.types.iter().all(TAtomic::is_float) && !self.types.is_empty()
}
pub fn is_bool(&self) -> bool {
self.types.iter().all(TAtomic::is_bool) && !self.types.is_empty()
}
pub fn is_never(&self) -> bool {
self.types.iter().all(TAtomic::is_never) || self.types.is_empty()
}
pub fn is_never_template(&self) -> bool {
self.types.iter().all(TAtomic::is_templated_as_never) && !self.types.is_empty()
}
#[must_use]
pub fn is_placeholder(&self) -> bool {
self.types.iter().all(|t| matches!(t, TAtomic::Placeholder)) && !self.types.is_empty()
}
#[must_use]
pub fn contains_placeholder(&self) -> bool {
self.types.iter().any(|t| t.contains_placeholder())
}
pub fn is_true(&self) -> bool {
self.types.iter().all(TAtomic::is_true) && !self.types.is_empty()
}
pub fn is_false(&self) -> bool {
self.types.iter().all(TAtomic::is_false) && !self.types.is_empty()
}
pub fn is_numeric(&self) -> bool {
self.types.iter().all(TAtomic::is_numeric) && !self.types.is_empty()
}
pub fn is_int_or_float(&self) -> bool {
self.types.iter().all(TAtomic::is_int_or_float) && !self.types.is_empty()
}
#[must_use]
pub fn effective_int_or_float(&self) -> Option<bool> {
let mut result: Option<bool> = None;
for atomic in self.types.as_ref() {
{
let is_int = atomic.effective_int_or_float()?;
if let Some(prev) = result {
if prev != is_int {
return None;
}
} else {
result = Some(is_int);
}
}
}
result
}
#[must_use]
pub fn is_mixed(&self) -> bool {
self.types.iter().all(|t| matches!(t, TAtomic::Mixed(_))) && !self.types.is_empty()
}
#[must_use]
pub fn has_mixed(&self) -> bool {
self.types.iter().any(|t| matches!(t, TAtomic::Mixed(_))) && !self.types.is_empty()
}
pub fn has_mixed_template(&self) -> bool {
self.types.iter().any(TAtomic::is_templated_as_mixed) && !self.types.is_empty()
}
#[must_use]
pub fn has_nullable_mixed(&self) -> bool {
self.types.iter().any(|t| matches!(t, TAtomic::Mixed(mixed) if !mixed.is_non_null())) && !self.types.is_empty()
}
#[must_use]
pub fn has_null(&self) -> bool {
self.types.iter().any(|t| matches!(t, TAtomic::Null)) && !self.types.is_empty()
}
#[must_use]
pub fn has_nullish(&self) -> bool {
self.types.iter().any(|t| match t {
TAtomic::Null | TAtomic::Void => true,
TAtomic::Mixed(mixed) => !mixed.is_non_null(),
TAtomic::GenericParameter(parameter) => parameter.constraint.has_nullish(),
_ => false,
}) && !self.types.is_empty()
}
#[must_use]
pub fn is_falsy_mixed(&self) -> bool {
if self.types.len() != 1 {
return false;
}
matches!(&self.types[0], &TAtomic::Mixed(mixed) if mixed.is_falsy())
}
#[must_use]
pub fn is_vanilla_mixed(&self) -> bool {
if self.types.len() != 1 {
return false;
}
self.types[0].is_vanilla_mixed()
}
#[must_use]
pub fn is_templated_as_vanilla_mixed(&self) -> bool {
if self.types.len() != 1 {
return false;
}
self.types[0].is_templated_as_vanilla_mixed()
}
#[must_use]
pub fn has_template_or_static(&self) -> bool {
for atomic in self.types.as_ref() {
if let TAtomic::GenericParameter(_) = atomic {
return true;
}
if let TAtomic::Object(TObject::Named(named_object)) = atomic {
if named_object.is_static {
return true;
}
if let Some(intersections) = named_object.get_intersection_types() {
for intersection in intersections {
if let TAtomic::GenericParameter(_) = intersection {
return true;
}
}
}
}
}
false
}
#[must_use]
pub fn has_template(&self) -> bool {
for atomic in self.types.as_ref() {
if let TAtomic::GenericParameter(_) = atomic {
return true;
}
if let Some(intersections) = atomic.get_intersection_types() {
for intersection in intersections {
if let TAtomic::GenericParameter(_) = intersection {
return true;
}
}
}
}
false
}
#[must_use]
pub fn has_template_types(&self) -> bool {
let all_child_nodes = self.get_all_child_nodes();
for child_node in all_child_nodes {
if let TypeRef::Atomic(
TAtomic::GenericParameter(_)
| TAtomic::Scalar(TScalar::ClassLikeString(TClassLikeString::Generic { .. })),
) = child_node
{
return true;
}
}
false
}
#[must_use]
pub fn get_template_types(&self) -> Vec<&TAtomic> {
let all_child_nodes = self.get_all_child_nodes();
let mut template_types = Vec::new();
for child_node in all_child_nodes {
if let TypeRef::Atomic(inner) = child_node {
match inner {
TAtomic::GenericParameter(_) => {
template_types.push(inner);
}
TAtomic::Scalar(TScalar::ClassLikeString(TClassLikeString::Generic { .. })) => {
template_types.push(inner);
}
_ => {}
}
}
}
template_types
}
#[must_use]
pub fn is_objecty(&self) -> bool {
for atomic in self.types.as_ref() {
if let &TAtomic::Object(_) = atomic {
continue;
}
if let TAtomic::Callable(callable) = atomic
&& callable.is_closure()
{
continue;
}
return false;
}
true
}
#[must_use]
pub fn is_generator(&self) -> bool {
for atomic in self.types.as_ref() {
if atomic.is_generator() {
continue;
}
return false;
}
true
}
#[must_use]
pub fn extends_or_implements(&self, codebase: &CodebaseMetadata, interface: &[u8]) -> bool {
for atomic in self.types.as_ref() {
if !atomic.extends_or_implements(codebase, interface) {
return false;
}
}
true
}
#[must_use]
pub fn is_generic_parameter(&self) -> bool {
self.types.len() == 1 && matches!(self.types[0], TAtomic::GenericParameter(_))
}
#[must_use]
pub fn is_null(&self) -> bool {
self.types.iter().all(|t| matches!(t, TAtomic::Null)) && !self.types.is_empty()
}
#[must_use]
pub fn is_nullable(&self) -> bool {
self.types.iter().any(|t| match t {
TAtomic::Null => self.types.len() >= 2,
TAtomic::GenericParameter(param) => param.constraint.is_nullable(),
_ => false,
})
}
#[must_use]
pub fn can_be_null(&self) -> bool {
self.types.iter().any(|t| match t {
TAtomic::Null => true,
TAtomic::Void => true,
TAtomic::Mixed(mixed) if !mixed.is_non_null() => true,
TAtomic::GenericParameter(param) => param.constraint.can_be_null(),
_ => false,
})
}
#[must_use]
pub fn is_void(&self) -> bool {
self.types.iter().all(|t| matches!(t, TAtomic::Void)) && !self.types.is_empty()
}
pub fn is_resource(&self) -> bool {
self.types.iter().all(TAtomic::is_resource) && !self.types.is_empty()
}
pub fn is_array(&self) -> bool {
self.types.iter().all(TAtomic::is_array) && !self.types.is_empty()
}
pub fn is_list(&self) -> bool {
self.types.iter().all(TAtomic::is_list) && !self.types.is_empty()
}
pub fn is_vanilla_array(&self) -> bool {
self.types.iter().all(TAtomic::is_vanilla_array) && !self.types.is_empty()
}
pub fn is_keyed_array(&self) -> bool {
self.types.iter().all(TAtomic::is_keyed_array) && !self.types.is_empty()
}
pub fn is_falsable(&self) -> bool {
self.types.len() >= 2 && self.types.iter().any(TAtomic::is_false)
}
#[must_use]
pub fn has_bool(&self) -> bool {
if self.types.is_empty() {
return false;
}
self.types.iter().any(|atomic| {
atomic.is_bool()
|| atomic.is_generic_scalar()
|| atomic.map_generic_parameter_constraint(TUnion::has_bool).unwrap_or(false)
})
}
pub fn has_scalar(&self) -> bool {
self.types.iter().any(TAtomic::is_generic_scalar)
}
#[must_use]
pub fn has_scalar_combination(&self) -> bool {
const HAS_INT: u8 = 1 << 0;
const HAS_FLOAT: u8 = 1 << 1;
const HAS_BOOL: u8 = 1 << 2;
const HAS_STRING: u8 = 1 << 3;
const ALL_SCALARS: u8 = HAS_INT | HAS_FLOAT | HAS_BOOL | HAS_STRING;
let mut flags = 0u8;
for atomic in self.types.as_ref() {
if atomic.is_int() {
flags |= HAS_INT;
} else if atomic.is_float() {
flags |= HAS_FLOAT;
} else if atomic.is_bool() {
flags |= HAS_BOOL;
} else if atomic.is_string() {
flags |= HAS_STRING;
} else if atomic.is_array_key() {
flags |= HAS_INT | HAS_STRING;
} else if atomic.is_numeric() {
flags |= HAS_INT | HAS_FLOAT;
} else if atomic.is_generic_scalar() {
return true;
}
if flags == ALL_SCALARS {
return true;
}
}
flags == ALL_SCALARS
}
pub fn has_array_key(&self) -> bool {
self.types.iter().any(TAtomic::is_array_key)
}
pub fn has_iterable(&self) -> bool {
self.types.iter().any(TAtomic::is_iterable) && !self.types.is_empty()
}
pub fn has_array(&self) -> bool {
self.types.iter().any(TAtomic::is_array) && !self.types.is_empty()
}
#[must_use]
pub fn has_traversable(&self, codebase: &CodebaseMetadata) -> bool {
self.types.iter().any(|atomic| atomic.is_traversable(codebase)) && !self.types.is_empty()
}
#[must_use]
pub fn has_array_key_like(&self) -> bool {
self.types.iter().any(|atomic| atomic.is_array_key() || atomic.is_int() || atomic.is_string())
}
pub fn has_numeric(&self) -> bool {
self.types.iter().any(TAtomic::is_numeric) && !self.types.is_empty()
}
pub fn is_always_truthy(&self) -> bool {
self.types.iter().all(TAtomic::is_truthy) && !self.types.is_empty()
}
pub fn is_always_falsy(&self) -> bool {
self.types.iter().all(TAtomic::is_falsy) && !self.types.is_empty()
}
#[must_use]
pub fn is_literal_of(&self, other: &TUnion) -> bool {
let is_matching_literal: fn(&TAtomic) -> bool = match other.types.first() {
Some(TAtomic::Scalar(TScalar::String(_))) => TAtomic::is_string_of_literal_origin,
Some(TAtomic::Scalar(TScalar::Integer(_))) => TAtomic::is_literal_int,
Some(TAtomic::Scalar(TScalar::Float(_))) => TAtomic::is_literal_float,
_ => return false,
};
self.types.iter().all(is_matching_literal)
}
#[must_use]
pub fn all_literals(&self) -> bool {
self.types
.iter()
.all(|atomic| atomic.is_string_of_literal_origin() || atomic.is_literal_int() || atomic.is_literal_float())
}
#[inline]
#[must_use]
pub fn is_single(&self) -> bool {
self.types.len() == 1
}
#[inline]
#[must_use]
pub fn get_single_string(&self) -> Option<&TString> {
if self.is_single()
&& let TAtomic::Scalar(TScalar::String(string)) = &self.types[0]
{
Some(string)
} else {
None
}
}
#[inline]
#[must_use]
pub fn get_single_array(&self) -> Option<&TArray> {
if self.is_single()
&& let TAtomic::Array(array) = &self.types[0]
{
Some(array)
} else {
None
}
}
#[inline]
#[must_use]
pub fn get_single_bool(&self) -> Option<&TBool> {
if self.is_single()
&& let TAtomic::Scalar(TScalar::Bool(bool)) = &self.types[0]
{
Some(bool)
} else {
None
}
}
#[inline]
#[must_use]
pub fn get_single_named_object(&self) -> Option<&TNamedObject> {
if self.is_single()
&& let TAtomic::Object(TObject::Named(named_object)) = &self.types[0]
{
Some(named_object)
} else {
None
}
}
#[inline]
#[must_use]
pub fn get_single(&self) -> &TAtomic {
&self.types[0]
}
#[inline]
#[must_use]
pub fn get_single_owned(self) -> TAtomic {
self.types[0].clone()
}
#[inline]
#[must_use]
pub fn is_named_object(&self) -> bool {
self.types.iter().all(|t| matches!(t, TAtomic::Object(TObject::Named(_))))
}
#[must_use]
pub fn is_enum(&self) -> bool {
self.types.iter().all(|t| matches!(t, TAtomic::Object(TObject::Enum(_))))
}
#[must_use]
pub fn is_enum_case(&self) -> bool {
self.types.iter().all(|t| matches!(t, TAtomic::Object(TObject::Enum(r#enum)) if r#enum.case.is_some()))
}
#[must_use]
pub fn is_single_enum_case(&self) -> bool {
self.is_single()
&& self.types.iter().all(|t| matches!(t, TAtomic::Object(TObject::Enum(r#enum)) if r#enum.case.is_some()))
}
#[inline]
#[must_use]
pub fn has_callable(&self) -> bool {
self.types.iter().any(|t| matches!(t, TAtomic::Callable(_)))
}
#[inline]
#[must_use]
pub fn is_callable(&self) -> bool {
self.types.iter().all(|t| matches!(t, TAtomic::Callable(_)))
}
#[inline]
#[must_use]
pub fn has_object_type(&self) -> bool {
self.types.iter().any(|t| matches!(t, TAtomic::Object(_)))
}
#[must_use]
pub fn get_enum_cases(&self) -> Vec<(Word, Option<Word>)> {
self.types
.iter()
.filter_map(|t| match t {
TAtomic::Object(TObject::Enum(enum_object)) => Some((enum_object.name, enum_object.case)),
_ => None,
})
.collect()
}
#[must_use]
pub fn get_single_int(&self) -> Option<TInteger> {
if self.is_single() { self.get_single().get_integer() } else { None }
}
#[must_use]
pub fn get_single_literal_int_value(&self) -> Option<i64> {
if self.is_single() { self.get_single().get_literal_int_value() } else { None }
}
#[must_use]
pub fn get_maximum_int_value(&self) -> Option<i64> {
self.types.iter().map(TAtomic::get_maximum_int_value).reduce(|first, second| Some(first?.max(second?)))?
}
#[must_use]
pub fn get_minimum_int_value(&self) -> Option<i64> {
self.types.iter().map(TAtomic::get_minimum_int_value).reduce(|first, second| Some(first?.min(second?)))?
}
#[must_use]
pub fn get_single_literal_float_value(&self) -> Option<f64> {
if self.is_single() { self.get_single().get_literal_float_value() } else { None }
}
#[must_use]
pub fn get_single_literal_string_value(&self) -> Option<&[u8]> {
if self.is_single() { self.get_single().get_literal_string_value() } else { None }
}
#[must_use]
pub fn get_single_class_string_value(&self) -> Option<Word> {
if self.is_single() { self.get_single().get_class_string_value() } else { None }
}
#[must_use]
pub fn get_single_array_key(&self) -> Option<ArrayKey> {
if self.is_single() { self.get_single().to_array_key() } else { None }
}
#[must_use]
pub fn get_single_value_of_array_like(&self) -> Option<Cow<'_, TUnion>> {
if !self.is_single() {
return None;
}
match self.get_single() {
TAtomic::Array(array) => match array {
TArray::List(list) => Some(Cow::Borrowed(&list.element_type)),
TArray::Keyed(keyed_array) => match &keyed_array.parameters {
Some((_, v)) => Some(Cow::Borrowed(v)),
None => Some(Cow::Owned(get_mixed())),
},
},
_ => None,
}
}
fn any_scalar(&self, is_matching: fn(&TScalar) -> bool) -> bool {
self.types.iter().any(|atomic| match atomic {
TAtomic::Scalar(scalar) => is_matching(scalar),
_ => false,
})
}
#[must_use]
pub fn has_literal_float(&self) -> bool {
self.any_scalar(TScalar::is_literal_float)
}
#[must_use]
pub fn has_literal_int(&self) -> bool {
self.any_scalar(TScalar::is_literal_int)
}
#[must_use]
pub fn has_literal_string(&self) -> bool {
self.any_scalar(TScalar::is_known_literal_string)
}
#[must_use]
pub fn has_literal_value(&self) -> bool {
self.any_scalar(TScalar::is_literal_value)
}
#[must_use]
pub fn accepts_false(&self) -> bool {
self.types.iter().any(|t| match t {
TAtomic::GenericParameter(parameter) => parameter.constraint.accepts_false(),
TAtomic::Mixed(mixed) if !mixed.is_truthy() => true,
TAtomic::Scalar(TScalar::Generic | TScalar::Bool(TBool { value: None | Some(false) })) => true,
_ => false,
})
}
#[must_use]
pub fn accepts_null(&self) -> bool {
self.types.iter().any(|t| match t {
TAtomic::GenericParameter(generic_parameter) => generic_parameter.constraint.accepts_null(),
TAtomic::Mixed(mixed) if !mixed.is_non_null() => true,
TAtomic::Null | TAtomic::Placeholder => true,
_ => false,
})
}
}
impl TType for TUnion {
fn get_child_nodes(&self) -> Vec<TypeRef<'_>> {
self.types.iter().map(TypeRef::Atomic).collect()
}
fn needs_population(&self) -> bool {
!self.flags.contains(UnionFlags::POPULATED) && self.types.iter().any(super::TType::needs_population)
}
#[inline]
fn is_expandable(&self) -> bool {
if self.types.is_empty() {
return true;
}
self.types.iter().any(super::TType::is_expandable)
}
fn is_complex(&self) -> bool {
self.types.len() > 3 || self.types.iter().any(super::TType::is_complex)
}
fn get_id(&self) -> Word {
let len = self.types.len();
let mut atomic_ids: Vec<Word> = self
.types
.as_ref()
.iter()
.map(|atomic| {
let id = atomic.get_id();
if atomic.is_generic_parameter() || atomic.has_intersection_types() && len > 1 {
concat_word!(b"(", id.as_bytes(), b")")
} else {
id
}
})
.collect();
if len <= 1 {
return atomic_ids.pop().unwrap_or_else(empty_word);
}
atomic_ids.sort_unstable();
join_words(&atomic_ids, b"|")
}
fn get_pretty_id_with_indent(&self, indent: usize) -> Word {
let len = self.types.len();
if len <= 1 {
return self.types.first().map_or_else(empty_word, |atomic| atomic.get_pretty_id_with_indent(indent));
}
if len > 3 {
let mut atomic_ids: Vec<Word> = self
.types
.as_ref()
.iter()
.map(|atomic| {
let id = atomic.get_pretty_id_with_indent(indent + 2);
if atomic.has_intersection_types() { concat_word!(b"(", id.as_bytes(), b")") } else { id }
})
.collect();
atomic_ids.sort_unstable();
let mut result: Vec<u8> = Vec::new();
result.extend_from_slice(atomic_ids[0].as_bytes());
for id in &atomic_ids[1..] {
result.extend_from_slice(b"\n");
result.resize(result.len() + indent, b' ');
result.extend_from_slice(b"| ");
result.extend_from_slice(id.as_bytes());
}
word(&result)
} else {
let mut atomic_ids: Vec<Word> = self
.types
.as_ref()
.iter()
.map(|atomic| {
let id = atomic.get_pretty_id_with_indent(indent);
if atomic.has_intersection_types() && len > 1 {
concat_word!(b"(", id.as_bytes(), b")")
} else {
id
}
})
.collect();
atomic_ids.sort_unstable();
let mut result = atomic_ids[0];
for id in &atomic_ids[1..] {
result = concat_word!(result.as_bytes(), b" | ", id.as_bytes());
}
result
}
}
}
impl PartialEq for TUnion {
fn eq(&self, other: &TUnion) -> bool {
if std::ptr::eq(self, other) {
return true;
}
const SEMANTIC_FLAGS: UnionFlags = UnionFlags::HAD_TEMPLATE
.union(UnionFlags::BY_REFERENCE)
.union(UnionFlags::REFERENCE_FREE)
.union(UnionFlags::POSSIBLY_UNDEFINED_FROM_TRY)
.union(UnionFlags::POSSIBLY_UNDEFINED)
.union(UnionFlags::IGNORE_NULLABLE_ISSUES)
.union(UnionFlags::IGNORE_FALSABLE_ISSUES)
.union(UnionFlags::FROM_TEMPLATE_DEFAULT)
.union(UnionFlags::FROM_UNSPECIFIED_TEMPLATE);
if self.flags.intersection(SEMANTIC_FLAGS) != other.flags.intersection(SEMANTIC_FLAGS) {
return false;
}
let len = self.types.len();
if len != other.types.len() {
return false;
}
if self.types == other.types {
return true;
}
fn is_subset(first: &[TAtomic], second: &[TAtomic]) -> bool {
first.iter().all(|atomic| second.contains(atomic))
}
is_subset(&self.types, &other.types) && is_subset(&other.types, &self.types)
}
}
#[inline]
fn ensure_non_empty_types(mut types: Vec<TAtomic>) -> Vec<TAtomic> {
if types.is_empty() {
types.push(TAtomic::Never);
}
types
}
pub fn populate_union_type(
unpopulated_union: &mut TUnion,
codebase_symbols: &Symbols,
reference_source: Option<&ReferenceSource>,
symbol_references: &mut SymbolReferences,
force: bool,
) {
if unpopulated_union.flags.contains(UnionFlags::POPULATED) && !force {
return;
}
if !force && !unpopulated_union.needs_population() {
return;
}
unpopulated_union.flags.insert(UnionFlags::POPULATED);
let unpopulated_atomics = unpopulated_union.types.to_mut();
for unpopulated_atomic in unpopulated_atomics {
match unpopulated_atomic {
TAtomic::Scalar(TScalar::ClassLikeString(
TClassLikeString::Generic { constraint, .. } | TClassLikeString::OfType { constraint, .. },
)) => {
populate_atomic_type(
Arc::make_mut(constraint),
codebase_symbols,
reference_source,
symbol_references,
force,
);
}
_ => {
populate_atomic_type(unpopulated_atomic, codebase_symbols, reference_source, symbol_references, force);
}
}
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum WidenKind {
Scalars,
Literals,
}
fn widen_atomic(atomic: &mut TAtomic, kind: WidenKind) {
match atomic {
TAtomic::Scalar(scalar) => {
if let Some(widened) = widened_scalar(scalar, kind) {
*scalar = widened;
}
}
TAtomic::Array(array) => match array {
TArray::List(list) => {
widen_arc_union(&mut list.element_type, kind);
if let Some(known) = list.known_elements.as_mut() {
for (_, ty) in known.values_mut() {
ty.widen(kind);
}
}
}
TArray::Keyed(keyed) => {
if let Some((key, value)) = keyed.parameters.as_mut() {
widen_arc_union(key, kind);
widen_arc_union(value, kind);
}
if let Some(known) = keyed.known_items.as_mut() {
for (_, ty) in known.values_mut() {
ty.widen(kind);
}
}
}
},
TAtomic::Iterable(iterable) => {
widen_arc_union(&mut iterable.key_type, kind);
widen_arc_union(&mut iterable.value_type, kind);
if let Some(intersections) = iterable.intersection_types.as_mut() {
for inner in intersections.iter_mut() {
widen_atomic(inner, kind);
}
}
}
TAtomic::Object(TObject::Named(named)) => {
if let Some(params) = named.type_parameters.as_mut() {
for ty in params.iter_mut() {
ty.widen(kind);
}
}
}
TAtomic::Object(TObject::WithProperties(with_props)) => {
for (_, ty) in with_props.known_properties.values_mut() {
ty.widen(kind);
}
}
TAtomic::GenericParameter(generic) => {
widen_arc_union(&mut generic.constraint, kind);
if let Some(intersections) = generic.intersection_types.as_mut() {
for inner in intersections.iter_mut() {
widen_atomic(inner, kind);
}
}
}
TAtomic::Conditional(conditional) => {
widen_arc_union(&mut conditional.subject, kind);
widen_arc_union(&mut conditional.target, kind);
widen_arc_union(&mut conditional.then, kind);
widen_arc_union(&mut conditional.otherwise, kind);
}
_ => {}
}
}
#[inline]
fn widen_arc_union(union: &mut Arc<TUnion>, kind: WidenKind) {
if union_has_widenable(union, kind) {
Arc::make_mut(union).widen(kind);
}
}
fn widened_scalar(scalar: &TScalar, kind: WidenKind) -> Option<TScalar> {
match (scalar, kind) {
(TScalar::String(string), WidenKind::Scalars) if !is_string_fully_general(string) => {
Some(TScalar::String(TString::general()))
}
(TScalar::String(string), WidenKind::Literals) if string.literal.is_some() => {
Some(TScalar::String(string.without_literal()))
}
(TScalar::Integer(integer), WidenKind::Scalars) if !matches!(integer, TInteger::Unspecified) => {
Some(TScalar::Integer(TInteger::Unspecified))
}
(TScalar::Integer(TInteger::Literal(_) | TInteger::UnspecifiedLiteral), WidenKind::Literals) => {
Some(TScalar::Integer(TInteger::Unspecified))
}
(TScalar::Float(float), WidenKind::Scalars) if !matches!(float, TFloat::Float) => {
Some(TScalar::Float(TFloat::Float))
}
(TScalar::Float(TFloat::Literal(_) | TFloat::UnspecifiedLiteral), WidenKind::Literals) => {
Some(TScalar::Float(TFloat::Float))
}
(TScalar::Bool(boolean), _) if !boolean.is_general() => Some(TScalar::Bool(TBool::general())),
_ => None,
}
}
#[inline]
fn is_string_fully_general(string: &TString) -> bool {
string.literal.is_none()
&& !string.is_numeric
&& !string.is_truthy
&& !string.is_non_empty
&& !string.is_callable
&& matches!(string.casing, TStringCasing::Unspecified)
}
fn union_has_widenable(union: &TUnion, kind: WidenKind) -> bool {
union.types.iter().any(|atomic| atomic_has_widenable(atomic, kind))
}
fn atomic_has_widenable(atomic: &TAtomic, kind: WidenKind) -> bool {
match atomic {
TAtomic::Scalar(scalar) => widened_scalar(scalar, kind).is_some(),
TAtomic::Array(TArray::List(list)) => {
union_has_widenable(&list.element_type, kind)
|| list.known_elements.as_ref().is_some_and(|m| m.values().any(|(_, t)| union_has_widenable(t, kind)))
}
TAtomic::Array(TArray::Keyed(keyed)) => {
keyed.parameters.as_ref().is_some_and(|(k, v)| union_has_widenable(k, kind) || union_has_widenable(v, kind))
|| keyed.known_items.as_ref().is_some_and(|m| m.values().any(|(_, t)| union_has_widenable(t, kind)))
}
TAtomic::Iterable(iterable) => {
union_has_widenable(&iterable.key_type, kind)
|| union_has_widenable(&iterable.value_type, kind)
|| iterable.intersection_types.as_ref().is_some_and(|v| v.iter().any(|t| atomic_has_widenable(t, kind)))
}
TAtomic::Object(TObject::Named(named)) => {
named.type_parameters.as_ref().is_some_and(|p| p.iter().any(|t| union_has_widenable(t, kind)))
}
TAtomic::Object(TObject::WithProperties(with_props)) => {
with_props.known_properties.values().any(|(_, t)| union_has_widenable(t, kind))
}
TAtomic::GenericParameter(generic) => {
union_has_widenable(&generic.constraint, kind)
|| generic.intersection_types.as_ref().is_some_and(|v| v.iter().any(|t| atomic_has_widenable(t, kind)))
}
TAtomic::Conditional(conditional) => {
union_has_widenable(&conditional.subject, kind)
|| union_has_widenable(&conditional.target, kind)
|| union_has_widenable(&conditional.then, kind)
|| union_has_widenable(&conditional.otherwise, kind)
}
_ => false,
}
}