use mago_atom::Atom;
use mago_atom::ascii_lowercase_atom;
use mago_atom::atom;
use mago_codex::metadata::CodebaseMetadata;
use mago_codex::metadata::class_like::ClassLikeMetadata;
use mago_codex::ttype::TType;
use mago_codex::ttype::atomic::TAtomic;
use mago_codex::ttype::atomic::object::TObject;
use mago_codex::ttype::atomic::object::r#enum::TEnum;
use mago_codex::ttype::atomic::object::named::TNamedObject;
use mago_codex::ttype::atomic::scalar::TScalar;
use mago_codex::ttype::atomic::scalar::class_like_string::TClassLikeString;
use mago_reporting::Annotation;
use mago_reporting::Issue;
use mago_span::HasSpan;
use mago_span::Span;
use mago_syntax::ast::Access;
use mago_syntax::ast::Call;
use mago_syntax::ast::Expression;
use crate::analyzable::Analyzable;
use crate::artifacts::AnalysisArtifacts;
use crate::code::IssueCode;
use crate::context::Context;
use crate::context::block::BlockContext;
use crate::error::AnalysisError;
use crate::utils::expression::expression_is_nullsafe;
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum ResolutionOrigin {
Invalid,
Named { is_parent: bool, is_self: bool },
Static { can_extend: bool },
Object { is_this: bool },
LiteralClassString,
AnyClassString,
AnyString,
SpecificClassLikeString(TClassLikeString),
AnyObject,
Mixed,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ResolvedClassname {
pub fqcn: Option<Atom>,
pub origin: ResolutionOrigin,
pub intersections: Vec<ResolvedClassname>,
pub is_final: bool,
}
impl ResolvedClassname {
#[inline]
const fn new(fq_class_id: Option<Atom>, origin: ResolutionOrigin, is_final: bool) -> Self {
Self { fqcn: fq_class_id, origin, intersections: Vec::new(), is_final }
}
#[inline]
const fn invalid() -> Self {
Self { fqcn: None, origin: ResolutionOrigin::Invalid, intersections: Vec::new(), is_final: false }
}
#[inline]
pub const fn is_invalid(&self) -> bool {
matches!(self.origin, ResolutionOrigin::Invalid)
}
pub const fn is_possibly_invalid(&self) -> bool {
matches!(self.origin, ResolutionOrigin::Mixed | ResolutionOrigin::Invalid)
}
#[inline]
pub const fn is_from_mixed(&self) -> bool {
matches!(self.origin, ResolutionOrigin::Mixed)
}
pub const fn is_static(&self) -> bool {
matches!(self.origin, ResolutionOrigin::Static { .. })
}
pub const fn is_self(&self) -> bool {
matches!(self.origin, ResolutionOrigin::Named { is_self: true, .. })
}
pub const fn can_extend_static(&self) -> bool {
matches!(self.origin, ResolutionOrigin::Static { can_extend: true })
}
pub const fn is_from_class_string(&self) -> bool {
matches!(
self.origin,
ResolutionOrigin::AnyClassString
| ResolutionOrigin::LiteralClassString
| ResolutionOrigin::SpecificClassLikeString(_)
)
}
pub const fn is_from_literal_class_string(&self) -> bool {
matches!(self.origin, ResolutionOrigin::LiteralClassString)
}
pub const fn is_from_any_object(&self) -> bool {
matches!(self.origin, ResolutionOrigin::AnyObject)
}
pub const fn is_object_instance(&self) -> bool {
matches!(self.origin, ResolutionOrigin::Object { .. })
}
#[inline]
pub const fn is_named(&self) -> bool {
matches!(self.origin, ResolutionOrigin::Named { .. })
}
#[inline]
pub const fn is_parent(&self) -> bool {
matches!(self.origin, ResolutionOrigin::Named { is_parent: true, .. })
}
#[inline]
pub const fn is_relative(&self) -> bool {
matches!(
self.origin,
ResolutionOrigin::Named { is_self: true, .. }
| ResolutionOrigin::Named { is_parent: true, .. }
| ResolutionOrigin::Static { .. }
)
}
#[inline]
pub fn get_object_type(&self, codebase: &CodebaseMetadata) -> TAtomic {
if let ResolutionOrigin::SpecificClassLikeString(class_string) = &self.origin {
return class_string.get_object_type(codebase);
}
let mut object_atomic = TAtomic::Object(match self.fqcn {
Some(fqcn) => {
let lowercase_fqcn = ascii_lowercase_atom(&fqcn);
if codebase.symbols.contains_enum(lowercase_fqcn) {
TObject::Enum(TEnum::new(fqcn))
} else {
TObject::Named(TNamedObject::new(fqcn))
}
}
None => TObject::Any,
});
for intersection_class in &self.intersections {
object_atomic.add_intersection_type(intersection_class.get_object_type(codebase));
}
object_atomic
}
}
pub fn resolve_classnames_from_expression<'ctx, 'arena>(
context: &mut Context<'ctx, 'arena>,
block_context: &mut BlockContext<'ctx>,
artifacts: &mut AnalysisArtifacts,
class_expression: &Expression<'arena>,
class_is_analyzed: bool,
) -> Result<Vec<ResolvedClassname>, AnalysisError> {
let mut possible_types = vec![];
match class_expression.unparenthesized() {
Expression::Identifier(name_node) => {
let fqcn = atom(context.resolved_names.get(name_node));
crate::utils::casing::check_class_like_casing(context, fqcn, name_node.span());
crate::utils::experimental::check_experimental_class_like(context, block_context, fqcn, name_node.span());
possible_types.push(ResolvedClassname::new(
Some(fqcn),
ResolutionOrigin::Named { is_parent: false, is_self: false },
context.codebase.is_enum_or_final_class(&fqcn),
));
}
Expression::Self_(self_keyword) => {
if let Some(self_class) = block_context.scope.get_class_like() {
let origin = ResolutionOrigin::Named { is_parent: false, is_self: true };
let mut class_name = ResolvedClassname::new(
Some(self_class.original_name),
origin,
self_class.kind.is_enum() || self_class.flags.is_final(),
);
class_name.intersections = get_intersections_from_metadata(context, self_class);
possible_types.push(class_name);
} else {
possible_types.push(ResolvedClassname::invalid());
context.collector.report_with_code(
IssueCode::SelfOutsideClassScope,
Issue::error("Cannot use `self` keyword outside of a class context.")
.with_annotation(Annotation::primary(self_keyword.span()).with_message("`self` used here"))
.with_note("The `self` keyword refers to the current class and can only be used within a class method.")
);
}
}
Expression::Static(static_keyword) => {
if let Some(self_class) = block_context.scope.get_class_like() {
let origin = ResolutionOrigin::Static { can_extend: !self_class.flags.is_final() };
let mut classname = ResolvedClassname::new(
Some(self_class.original_name),
origin,
self_class.kind.is_enum() || self_class.flags.is_final(),
);
classname.intersections = get_intersections_from_metadata(context, self_class);
possible_types.push(classname);
} else {
possible_types.push(ResolvedClassname::invalid());
context.collector.report_with_code(
IssueCode::StaticOutsideClassScope,
Issue::error("Cannot use `static` keyword outside of a class scope.")
.with_annotation(Annotation::primary(static_keyword.span()).with_message("`static` used here"))
.with_note(
"The `static` keyword refers to the called class at runtime and requires a class scope.",
),
);
}
}
Expression::Parent(parent_keyword) => {
if let Some(self_meta) = block_context.scope.get_class_like() {
let mut found_parent = false;
if let Some(parent_metadata) =
self_meta.direct_parent_class.as_ref().and_then(|id| context.codebase.get_class_like(id))
{
let origin = ResolutionOrigin::Named { is_parent: true, is_self: false };
let mut classname = ResolvedClassname::new(Some(parent_metadata.original_name), origin, false);
classname.intersections = get_intersections_from_metadata(context, self_meta);
possible_types.push(classname);
found_parent = true;
}
if !found_parent && self_meta.kind.is_trait() && !self_meta.require_extends.is_empty() {
let mut intersections = get_intersections_from_metadata(context, self_meta);
let mut parent_classname = unsafe { intersections.pop().unwrap_unchecked() };
parent_classname.intersections = intersections;
possible_types.push(parent_classname);
found_parent = true;
}
if !found_parent {
context.collector.report_with_code(
IssueCode::InvalidParentType,
Issue::error(format!(
"Cannot use `parent` as the current type (`{}`) does not have a parent class.",
self_meta.original_name
))
.with_annotation(Annotation::primary(parent_keyword.span()).with_message("`parent` used here"))
.with_annotation(
Annotation::secondary(self_meta.name_span.unwrap_or(self_meta.span))
.with_message(format!("Class `{}` has no parent", self_meta.original_name)),
),
);
possible_types.push(ResolvedClassname::invalid());
}
} else {
context.collector.report_with_code(
IssueCode::ParentOutsideClassScope,
Issue::error("Cannot use `parent` keyword outside of a class context.")
.with_annotation(Annotation::primary(parent_keyword.span()).with_message("`parent` used here"))
.with_note("The `parent` keyword refers to the parent class and must be used inside a class."),
);
possible_types.push(ResolvedClassname::invalid());
}
}
expression => {
if !class_is_analyzed {
let was_inside_call = block_context.flags.inside_call();
block_context.flags.set_inside_call(true);
expression.analyze(context, block_context, artifacts)?;
block_context.flags.set_inside_call(was_inside_call);
}
let expression_type = artifacts.get_expression_type(expression);
let is_directly_nullsafe = matches!(
expression.unparenthesized(),
Expression::Access(Access::NullSafeProperty(_)) | Expression::Call(Call::NullSafeMethod(_))
);
for atomic in expression_type.map(|u| u.types.iter()).unwrap_or_default() {
if let Some(resolved_classname) = get_class_name_from_atomic(context.codebase, atomic) {
possible_types.push(resolved_classname);
} else if atomic.is_null() && is_directly_nullsafe {
context.collector.report_with_code(
IssueCode::PossiblyNullPropertyAccess,
Issue::error("Attempting static access on a possibly `null` value.")
.with_annotation(
Annotation::primary(expression.span())
.with_message("This expression can be `null` here"),
)
.with_note("PHP's nullsafe operator (`?->`) does not short-circuit static access (`::`).")
.with_help(
"Add a null check before the static access, or ensure the expression is never null.",
),
);
} else if atomic.is_null() && expression_is_nullsafe(expression) {
} else if atomic.is_null() {
context.collector.report_with_code(
IssueCode::PossiblyNullPropertyAccess,
Issue::error("Attempting static access on a possibly `null` value.")
.with_annotation(
Annotation::primary(expression.span())
.with_message("This expression can be `null` here"),
)
.with_help(
"Add a null check before the static access, or ensure the expression is never null.",
),
);
} else {
possible_types.push(ResolvedClassname::invalid());
context.collector.report_with_code(
IssueCode::InvalidClassStringExpression,
Issue::error(format!(
"Expression of type `{}` cannot be used as a class name.",
atomic.get_id()
))
.with_annotation(Annotation::primary(expression.span()).with_message("This expression is used as a class name"))
.with_note("To use an expression as a class name, it must evaluate to a string that is a valid class name (e.g., a `class-string` type).")
);
}
}
}
}
Ok(possible_types)
}
pub fn get_class_name_from_atomic(codebase: &CodebaseMetadata, atomic: &TAtomic) -> Option<ResolvedClassname> {
#[inline]
fn get_class_name_from_atomic_impl(
codebase: &CodebaseMetadata,
atomic: &TAtomic,
active_class_string: Option<&TClassLikeString>,
) -> Option<ResolvedClassname> {
let mut class_name = match atomic {
TAtomic::GenericParameter(parameter) => parameter
.constraint
.types
.iter()
.filter_map(|constraint_atomic| {
get_class_name_from_atomic_impl(codebase, constraint_atomic, active_class_string)
})
.next()
.unwrap_or_else(ResolvedClassname::invalid),
TAtomic::Object(object) => match object {
TObject::Any => {
let origin = if let Some(class_string) = active_class_string {
ResolutionOrigin::SpecificClassLikeString(class_string.clone())
} else {
ResolutionOrigin::AnyObject
};
ResolvedClassname::new(None, origin, false)
}
TObject::Enum(enum_object) => {
let origin = if let Some(class_string) = active_class_string {
ResolutionOrigin::SpecificClassLikeString(class_string.clone())
} else {
ResolutionOrigin::Object { is_this: atomic.is_this() }
};
ResolvedClassname::new(Some(enum_object.name), origin, true)
}
TObject::Named(named_object) => {
let origin = if let Some(class_string) = active_class_string {
ResolutionOrigin::SpecificClassLikeString(class_string.clone())
} else {
ResolutionOrigin::Object { is_this: atomic.is_this() }
};
ResolvedClassname::new(
Some(named_object.name),
origin,
codebase.is_enum_or_final_class(&named_object.name),
)
}
TObject::WithProperties(_) | TObject::HasMethod(_) | TObject::HasProperty(_) => {
let origin = if let Some(class_string) = active_class_string {
ResolutionOrigin::SpecificClassLikeString(class_string.clone())
} else {
ResolutionOrigin::AnyObject
};
ResolvedClassname::new(None, origin, false)
}
},
TAtomic::Scalar(TScalar::ClassLikeString(class_string)) => {
match class_string {
TClassLikeString::Any { .. } => {
ResolvedClassname::new(None, ResolutionOrigin::AnyClassString, false)
}
TClassLikeString::OfType { constraint, .. } | TClassLikeString::Generic { constraint, .. } => {
get_class_name_from_atomic_impl(codebase, constraint.as_ref(), Some(class_string))?
}
TClassLikeString::Literal { value } => ResolvedClassname::new(
Some(*value),
ResolutionOrigin::LiteralClassString,
codebase.is_enum_or_final_class(value),
),
}
}
TAtomic::Scalar(scalar) => {
if let Some(literal_string) = atomic.get_literal_string_value() {
let class_id = atom(literal_string);
ResolvedClassname::new(Some(class_id), ResolutionOrigin::AnyString, false)
} else if scalar.is_string() {
ResolvedClassname::new(None, ResolutionOrigin::AnyString, false)
} else {
return None; }
}
TAtomic::Mixed(_) => ResolvedClassname::new(None, ResolutionOrigin::Mixed, false),
_ => {
return None;
}
};
if let Some(intersections) = atomic.get_intersection_types() {
let intersection_class_names = intersections
.iter()
.filter_map(|intersection| get_class_name_from_atomic_impl(codebase, intersection, None))
.collect::<Vec<_>>();
class_name.intersections = intersection_class_names;
}
Some(class_name)
}
get_class_name_from_atomic_impl(codebase, atomic, None)
}
fn get_intersections_from_metadata(context: &Context<'_, '_>, metadata: &ClassLikeMetadata) -> Vec<ResolvedClassname> {
if metadata.kind.is_enum() {
return vec![];
}
let mut intersections = vec![];
for required_interface in &metadata.require_implements {
let Some(interface_metadata) = context.codebase.get_interface(required_interface) else {
continue;
};
intersections.extend(get_intersections_from_metadata(context, interface_metadata));
intersections.push(ResolvedClassname::new(
Some(interface_metadata.original_name),
ResolutionOrigin::Named { is_parent: false, is_self: false },
false,
));
}
for required_class in &metadata.require_extends {
let Some(parent_class_metadata) = context.codebase.get_class_like(required_class) else {
continue;
};
intersections.extend(get_intersections_from_metadata(context, parent_class_metadata));
intersections.push(ResolvedClassname::new(
Some(parent_class_metadata.original_name),
ResolutionOrigin::Named { is_parent: true, is_self: false },
false,
));
}
intersections
}
pub fn report_non_existent_class_like(context: &mut Context, span: Span, classname: Atom) {
context.collector.report_with_code(
IssueCode::NonExistentClassLike,
Issue::error(format!("Class, Interface, or Trait `{classname}` does not exist."))
.with_annotation(
Annotation::primary(span).with_message("This expression refers to a non-existent class-like type"),
)
.with_help(format!("Ensure the `{classname}` is defined in the codebase.")),
);
}