use foldhash::HashSet;
use mago_allocator::Arena;
use mago_codex::identifier::method::MethodIdentifier;
use mago_codex::metadata::CodebaseMetadata;
use mago_codex::metadata::class_like::ClassLikeMetadata;
use mago_codex::metadata::function_like::FunctionLikeMetadata;
use mago_codex::misc::GenericParent;
use mago_codex::ttype::TType;
use mago_codex::ttype::atomic::TAtomic;
use mago_codex::ttype::atomic::generic::TGenericParameter;
use mago_codex::ttype::atomic::mixed::TMixed;
use mago_codex::ttype::atomic::object::TObject;
use mago_codex::ttype::atomic::object::named::TNamedObject;
use mago_codex::ttype::comparator::ComparisonResult;
use mago_codex::ttype::comparator::union_comparator::is_contained_by;
use mago_codex::ttype::expander::StaticClassType;
use mago_codex::ttype::get_mixed;
use mago_codex::ttype::get_specialized_template_type;
use mago_codex::ttype::template::GenericTemplate;
use mago_codex::ttype::template::TemplateResult;
use mago_codex::ttype::union::TUnion;
use mago_reporting::Annotation;
use mago_reporting::Issue;
use mago_span::HasSpan;
use mago_span::Span;
use mago_syntax::cst::ClassLikeMemberSelector;
use mago_syntax::cst::Expression;
use mago_word::Word;
use mago_word::ascii_lowercase_word;
use mago_word::word;
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::resolver::class_name::report_non_existent_class_like;
use crate::resolver::selector::resolve_member_selector;
use crate::utils::names::display_class_like_name;
use crate::utils::names::display_method_name;
use crate::visibility::check_method_visibility;
use crate::visibility::is_method_visible;
#[derive(Debug)]
pub struct ResolvedMethod {
pub classname: Word,
pub method_identifier: MethodIdentifier,
pub static_class_type: StaticClassType,
pub declaring_object: Option<TNamedObject>,
pub is_static: bool,
pub mixin_without_magic_method: Option<MixinWithoutMagicMethod>,
}
#[derive(Debug, Clone)]
pub struct MixinWithoutMagicMethod {
pub mixin_class_name: Word,
pub target_is_final: bool,
}
pub struct MethodCandidate<'ctx> {
pub metadata: &'ctx ClassLikeMetadata,
pub method_identifier: MethodIdentifier,
pub object: TObject,
pub classname: Word,
pub mixin_without_magic_method: Option<MixinWithoutMagicMethod>,
pub receiver_object: Option<TObject>,
}
#[derive(Default, Debug)]
pub struct MethodResolutionResult {
pub template_result: TemplateResult,
pub resolved_methods: Vec<ResolvedMethod>,
pub magic_call_methods: Vec<ResolvedMethod>,
pub has_dynamic_selector: bool,
pub has_ambiguous_target: bool,
pub has_invalid_target: bool,
pub encountered_mixed: bool,
pub encountered_null: bool,
pub all_methods_non_nullable_return: bool,
}
pub fn resolve_method_targets<'ctx, 'ast, 'arena, A>(
context: &mut Context<'ctx, 'arena, A>,
block_context: &mut BlockContext<'ctx>,
artifacts: &mut AnalysisArtifacts,
object: &'ast Expression<'arena>,
selector: &'ast ClassLikeMemberSelector<'arena>,
is_null_safe: bool,
access_span: Span,
) -> Result<MethodResolutionResult, AnalysisError>
where
A: Arena,
{
let mut result = MethodResolutionResult::default();
let mut asserted_descendant_method_references = Vec::new();
let was_inside_general_use = block_context.flags.inside_general_use();
block_context.flags.set_inside_general_use(true);
object.analyze(context, block_context, artifacts)?;
block_context.flags.set_inside_general_use(was_inside_general_use);
let resolved_selectors = resolve_member_selector(context, block_context, artifacts, selector)?;
let mut method_names = Vec::new();
for resolved_selector in resolved_selectors {
if resolved_selector.is_dynamic() {
result.has_dynamic_selector = true;
}
if let Some(name) = resolved_selector.name() {
method_names.push(ascii_lowercase_word(name.as_bytes()));
} else {
result.has_invalid_target = true;
}
}
if let Some(object_type) = artifacts.get_expression_type(object) {
let mut object_atomics = object_type.types.iter().collect::<Vec<_>>();
while let Some(object_atomic) = object_atomics.pop() {
if let TAtomic::GenericParameter(TGenericParameter { constraint, .. }) = object_atomic {
object_atomics.extend(constraint.types.iter());
continue;
}
if object_atomic.is_never() {
continue;
}
if object_atomic.is_null() {
result.encountered_null = true;
if !object_type.ignore_nullable_issues() && !is_null_safe && !object_type.has_nullsafe_null() {
result.has_invalid_target = true;
context.collector.report_with_code(
if object_type.is_null() {
IssueCode::MethodAccessOnNull
} else {
IssueCode::PossibleMethodAccessOnNull
},
Issue::error("Attempting to call a method on `null`.")
.with_annotation(
Annotation::primary(object.span()).with_message("This expression can be `null`"),
)
.with_help("Use the nullsafe operator (`?->`) if `null` is an expected value."),
);
}
continue;
}
let TAtomic::Object(obj_type) = object_atomic else {
if object_atomic.is_mixed() {
result.encountered_mixed = true;
} else {
result.has_invalid_target = true;
}
report_call_on_non_object(context, object_atomic, object.span(), selector.span());
continue;
};
let resolved_magic_call_method = resolve_method_from_object(
context,
block_context,
object,
selector,
obj_type,
word(b"__call"),
access_span,
true,
&mut result,
);
let mut had_undocumented_magic_call = false;
for &method_name in &method_names {
let resolved_methods = resolve_method_from_object(
context,
block_context,
object,
selector,
obj_type,
method_name,
access_span,
!resolved_magic_call_method.is_empty(),
&mut result,
);
if resolved_methods.is_empty() {
collect_asserted_descendant_method_references(
context,
block_context,
obj_type,
method_name,
&mut asserted_descendant_method_references,
);
}
if resolved_methods.is_empty() {
if let Some(classname) = obj_type.get_name() {
let method_name_bytes: &[u8] = method_name.as_ref();
let has_method_assertion = type_has_method_assertion(obj_type, method_name_bytes);
if !has_method_assertion {
if resolved_magic_call_method.is_empty() {
report_non_existent_method(
context,
object.span(),
selector.span(),
classname,
method_name,
);
result.has_invalid_target = true;
} else {
report_non_documented_method(
context,
object.span(),
selector.span(),
classname,
method_name,
);
had_undocumented_magic_call = true;
}
}
} else {
}
} else {
for resolved_method in &resolved_methods {
if let Some(mixin_info) = &resolved_method.mixin_without_magic_method
&& let Some(classname) = obj_type.get_name()
{
if mixin_info.target_is_final {
report_non_existent_mixin_method(
context,
object.span(),
selector.span(),
classname,
method_name,
mixin_info.mixin_class_name,
);
result.has_invalid_target = true;
} else {
report_possibly_non_existent_mixin_method(
context,
object.span(),
selector.span(),
classname,
method_name,
mixin_info.mixin_class_name,
);
}
}
}
}
result.resolved_methods.extend(resolved_methods);
}
if had_undocumented_magic_call {
result.magic_call_methods.extend(resolved_magic_call_method);
}
}
} else {
result.has_invalid_target = true;
result.encountered_mixed = true;
report_call_on_non_object(context, &TAtomic::Mixed(TMixed::new()), object.span(), selector.span());
}
for method_id in asserted_descendant_method_references {
artifacts.symbol_references.add_reference_for_method_call(&block_context.scope, &method_id);
}
result.all_methods_non_nullable_return = !result.resolved_methods.is_empty()
&& result.resolved_methods.iter().all(|resolved_method| {
context
.codebase
.get_method_by_id(&resolved_method.method_identifier)
.and_then(|method| method.return_type_metadata.as_ref())
.is_some_and(|return_type| !return_type.type_union.is_nullable())
});
Ok(result)
}
fn collect_asserted_descendant_method_references<'ctx, A>(
context: &Context<'ctx, '_, A>,
block_context: &BlockContext<'ctx>,
object_type: &TObject,
method_name: Word,
references: &mut Vec<MethodIdentifier>,
) where
A: Arena,
{
if !type_has_method_assertion(object_type, method_name.as_bytes()) {
return;
}
let Some(class_name) = object_type.get_name() else {
return;
};
for descendant in context.codebase.get_all_descendants(class_name.as_bytes()) {
if !context.codebase.method_exists(descendant.as_bytes(), method_name.as_bytes())
|| !is_method_visible(context, block_context, descendant.as_bytes(), method_name.as_bytes())
{
continue;
}
let method_id =
context.codebase.get_declaring_method_identifier(&MethodIdentifier::new(descendant, method_name));
if !references.contains(&method_id) {
references.push(method_id);
}
}
}
pub fn resolve_method_from_object<'ctx, 'ast, 'arena, A>(
context: &mut Context<'ctx, 'arena, A>,
block_context: &BlockContext<'ctx>,
object: &'ast Expression<'arena>,
selector: &'ast ClassLikeMemberSelector<'arena>,
object_type: &TObject,
method_name: Word,
access_span: Span,
has_magic_call: bool,
result: &mut MethodResolutionResult,
) -> Vec<ResolvedMethod>
where
A: Arena,
{
let mut resolved_methods = vec![];
let candidates = get_method_candidates_from_object(
context,
block_context,
object,
selector,
object_type,
object_type,
method_name,
access_span,
has_magic_call,
result,
);
for candidate in candidates {
let MethodCandidate {
metadata,
method_identifier: declaring_method_id,
object,
classname,
mixin_without_magic_method,
receiver_object,
} = candidate;
let declaring_class_metadata =
context.codebase.get_class_like(declaring_method_id.get_class_name().as_bytes()).unwrap_or(metadata);
let class_template_parameters = super::class_template_type_collector::collect(
context.codebase,
metadata,
declaring_class_metadata,
Some(&object),
);
if let Some(class_template_parameters) = class_template_parameters {
result.template_result.add_lower_bounds(class_template_parameters);
}
for (index, parameter) in object.get_type_parameters().unwrap_or_default().iter().enumerate() {
let Some(template_name) = metadata.get_template_name_for_index(index) else {
continue;
};
result
.template_result
.template_types
.entry(template_name)
.or_default()
.push(GenericTemplate::new(GenericParent::ClassLike(metadata.name), parameter.clone()));
}
let (static_class_type, declaring_object) = match receiver_object {
Some(receiver) => {
let declaring_object = match object {
TObject::Named(named) => Some(named),
other => other.get_name().map(TNamedObject::new),
};
(StaticClassType::Object(receiver), declaring_object)
}
None => (StaticClassType::Object(object), None),
};
resolved_methods.push(ResolvedMethod {
method_identifier: declaring_method_id,
static_class_type,
declaring_object,
classname,
is_static: false,
mixin_without_magic_method,
});
}
resolved_methods
}
pub fn get_method_candidates_from_object<'ctx, 'ast, 'arena, 'object, A>(
context: &mut Context<'ctx, 'arena, A>,
block_context: &BlockContext<'ctx>,
object: &'ast Expression<'arena>,
selector: &'ast ClassLikeMemberSelector<'arena>,
object_type: &'object TObject,
outer_object: &'object TObject,
method_name: Word,
access_span: Span,
has_magic_call: bool,
result: &mut MethodResolutionResult,
) -> Vec<MethodCandidate<'ctx>>
where
A: Arena,
{
let mut candidates = vec![];
let Some(name) = object_type.get_name() else {
if std::ptr::eq(object_type, outer_object) {
result.has_ambiguous_target = true;
if !has_magic_call {
let method_name_bytes: &[u8] = method_name.as_ref();
let has_method_assertion = type_has_method_assertion(object_type, method_name_bytes);
if !has_method_assertion {
report_call_on_ambiguous_object(context, object.span(), selector.span());
}
}
}
return candidates;
};
let Some(class_metadata) = context.codebase.get_class_like(name.as_bytes()) else {
result.has_invalid_target = true;
report_non_existent_class_like(context, object.span(), name);
return candidates;
};
let mut method_id = MethodIdentifier::new(class_metadata.original_name, method_name);
if !context.codebase.method_identifier_exists(&method_id) {
method_id = context.codebase.get_declaring_method_identifier(&method_id);
}
if let Some(function_like_metadata) = context.codebase.get_method_by_id(&method_id) {
if !check_method_visibility(
context,
block_context,
class_metadata.original_name.as_bytes(),
method_name.as_bytes(),
access_span,
Some(selector.span()),
) {
result.has_invalid_target = true;
}
if !check_where_method_constraints(
context,
object_type,
object,
selector,
class_metadata,
function_like_metadata,
class_metadata.original_name,
) {
result.has_invalid_target = true;
}
if function_like_metadata.flags.is_magic_method() {
let lowercase_method = ascii_lowercase_word(method_name.as_bytes());
let is_pseudo = class_metadata.pseudo_methods.contains(&lowercase_method)
|| class_metadata.all_parent_classes.iter().any(|parent_name| {
context
.codebase
.get_class_like(parent_name.as_bytes())
.is_some_and(|parent| parent.pseudo_methods.contains(&lowercase_method))
});
let mut is_inherited = false;
if is_pseudo {
for parent_class_name in &class_metadata.all_parent_classes {
if let Some(parent_metadata) = context.codebase.get_class_like(parent_class_name.as_bytes())
&& parent_metadata.methods.contains(&lowercase_method)
&& !parent_metadata.pseudo_methods.contains(&lowercase_method)
{
is_inherited = true;
break;
}
}
}
if function_like_metadata.flags.is_static() {
result.has_invalid_target = true;
report_dynamic_static_method_call(
context,
object.span(),
selector.span(),
class_metadata.original_name,
method_name,
has_magic_call,
);
} else if !has_magic_call && !is_inherited && !class_metadata.kind.is_interface() {
if class_metadata.flags.is_final() && !class_metadata.flags.is_abstract() {
report_magic_call_without_call_method(
context,
object.span(),
selector.span(),
class_metadata.original_name,
method_name,
false,
);
} else {
report_possibly_missing_magic_call(
context,
object.span(),
selector.span(),
class_metadata.original_name,
method_name,
false,
);
}
} else {
}
}
candidates.push(MethodCandidate {
metadata: class_metadata,
method_identifier: method_id,
object: outer_object.clone(),
classname: name,
mixin_without_magic_method: None,
receiver_object: None,
});
} else if !class_metadata.require_extends.is_empty() || !class_metadata.require_implements.is_empty() {
for required_class in class_metadata.require_extends.iter().chain(class_metadata.require_implements.iter()) {
let Some(required_metadata) = context.codebase.get_class_like(required_class.as_bytes()) else {
continue;
};
let mut required_method_id = MethodIdentifier::new(required_metadata.original_name, method_name);
if !context.codebase.method_identifier_exists(&required_method_id) {
required_method_id = context.codebase.get_declaring_method_identifier(&required_method_id);
}
if context.codebase.get_method_by_id(&required_method_id).is_some() {
candidates.push(MethodCandidate {
metadata: required_metadata,
method_identifier: required_method_id,
object: outer_object.clone(),
classname: *required_class,
mixin_without_magic_method: None,
receiver_object: None,
});
break;
}
}
} else if !class_metadata.mixins.is_empty() {
let mixin_types = collect_mixin_types(context.codebase, class_metadata, outer_object, &class_metadata.mixins);
for (mixin_class_name, mixin_object) in mixin_types {
let Some(mixin_metadata) = context.codebase.get_class_like(mixin_class_name.as_bytes()) else {
continue;
};
let mut mixin_method_id = MethodIdentifier::new(mixin_metadata.original_name, method_name);
if !context.codebase.method_identifier_exists(&mixin_method_id) {
mixin_method_id = context.codebase.get_declaring_method_identifier(&mixin_method_id);
}
if let Some(function_like_metadata) = context.codebase.get_method_by_id(&mixin_method_id) {
if !check_method_visibility(
context,
block_context,
mixin_metadata.original_name.as_bytes(),
method_name.as_bytes(),
access_span,
Some(selector.span()),
) {
result.has_invalid_target = true;
continue;
}
if let Some(method_metadata) = &function_like_metadata.method_metadata
&& !method_metadata.visibility.is_public()
{
continue;
}
let mixin_info = if has_magic_call {
None
} else {
Some(MixinWithoutMagicMethod { mixin_class_name, target_is_final: class_metadata.flags.is_final() })
};
candidates.push(MethodCandidate {
metadata: mixin_metadata,
method_identifier: mixin_method_id,
object: mixin_object.clone(),
classname: mixin_class_name,
mixin_without_magic_method: mixin_info,
receiver_object: Some(outer_object.clone()),
});
}
}
} else {
}
if let Some(intersection_types) = object_type.get_intersection_types() {
for intersected_atomic in intersection_types {
match intersected_atomic {
TAtomic::Object(intersected_object) => {
candidates.extend(get_method_candidates_from_object(
context,
block_context,
object,
selector,
intersected_object,
object_type,
method_name,
access_span,
has_magic_call,
result,
));
}
TAtomic::GenericParameter(generic_parameter) => {
for constraint_atomic in generic_parameter.constraint.types.as_ref() {
if let TAtomic::Object(intersected_object) = constraint_atomic {
candidates.extend(get_method_candidates_from_object(
context,
block_context,
object,
selector,
intersected_object,
object_type,
method_name,
access_span,
has_magic_call,
result,
));
}
}
}
_ => {
}
}
}
}
let mut seen = HashSet::default();
candidates.retain(|candidate| seen.insert(candidate.method_identifier));
candidates
}
fn check_where_method_constraints<A>(
context: &mut Context<'_, '_, A>,
object_type: &TObject,
object: &Expression,
selector: &ClassLikeMemberSelector,
class_like_metadata: &ClassLikeMetadata,
function_like_metadata: &FunctionLikeMetadata,
defining_class_id: Word,
) -> bool
where
A: Arena,
{
let Some(method_metadata) = function_like_metadata.method_metadata.as_ref() else {
return true;
};
if method_metadata.where_constraints.is_empty() {
return true;
}
for (&template_name, constraint) in &method_metadata.where_constraints {
let actual_template_type = get_specialized_template_type(
context.codebase,
template_name,
defining_class_id,
class_like_metadata,
object_type.get_type_parameters(),
)
.unwrap_or_else(get_mixed);
if is_contained_by(
context.codebase,
&actual_template_type,
&constraint.type_union,
false,
false,
false,
&mut ComparisonResult::default(),
) {
continue;
}
let required_constraint_str = constraint.type_union.get_id();
let actual_template_type_str = actual_template_type.get_id();
context.collector.report_with_code(
IssueCode::WhereConstraintViolation,
Issue::error(format!(
"Method call violates `@where` constraint for template `{template_name}`.",
))
.with_annotation(
Annotation::primary(selector.span())
.with_message("This method cannot be called here..."),
)
.with_annotation(
Annotation::secondary(object.span())
.with_message(format!(
"...because this object's template parameter `{template_name}` is type `{actual_template_type_str}`...",
)),
)
.with_annotation(
Annotation::secondary(constraint.span)
.with_message(format!(
"...but this `@where` clause requires it to be `{required_constraint_str}`.",
)),
)
.with_note(
"The `@where` tag on a method adds a constraint that must be satisfied by the object's generic types at the time of the call."
)
.with_help(
format!("Ensure the object's template parameter `{template_name}` satisfies the `{required_constraint_str}` constraint before calling this method.")
),
);
return false;
}
true
}
fn report_call_on_non_object<A>(
context: &mut Context<'_, '_, A>,
atomic_type: &TAtomic,
obj_span: Span,
selector_span: Span,
) where
A: Arena,
{
let type_str = atomic_type.get_id();
context.collector.report_with_code(
if atomic_type.is_mixed() { IssueCode::MixedMethodAccess } else { IssueCode::InvalidMethodAccess },
Issue::error(format!("Attempting to access a method on a non-object type (`{type_str}`)."))
.with_annotation(Annotation::primary(selector_span).with_message("Cannot call method here"))
.with_annotation(
Annotation::secondary(obj_span).with_message(format!("This expression has type `{type_str}`")),
),
);
}
fn report_call_on_ambiguous_object<A>(context: &mut Context<'_, '_, A>, obj_span: Span, selector_span: Span)
where
A: Arena,
{
context.collector.report_with_code(
IssueCode::AmbiguousObjectMethodAccess,
Issue::warning("Cannot statically verify method call on a generic `object` type.")
.with_annotation(Annotation::primary(selector_span).with_message("Cannot verify this method call"))
.with_annotation(
Annotation::secondary(obj_span).with_message("This expression has the general type `object`"),
)
.with_help("Provide a more specific type hint for the object for robust analysis."),
);
}
pub(super) fn report_non_existent_method<A>(
context: &mut Context<'_, '_, A>,
obj_span: Span,
selector_span: Span,
classname: Word,
method_name: Word,
) where
A: Arena,
{
let classname = display_class_like_name(context, classname);
let method_name = display_method_name(context, classname, method_name);
context.collector.report_with_code(
IssueCode::NonExistentMethod,
Issue::error(format!("Method `{method_name}` does not exist on type `{classname}`."))
.with_annotation(Annotation::primary(selector_span).with_message("This method selection is invalid"))
.with_annotation(
Annotation::secondary(obj_span).with_message(format!("This expression has type `{classname}`")),
)
.with_help(format!("Ensure the `{method_name}` method is defined in the `{classname}` class-like.")),
);
}
pub(super) fn report_non_documented_method<A>(
context: &mut Context<'_, '_, A>,
obj_span: Span,
selector_span: Span,
classname: Word,
method_name: Word,
) where
A: Arena,
{
let classname = display_class_like_name(context, classname);
let method_name = display_method_name(context, classname, method_name);
context.collector.report_with_code(
IssueCode::NonDocumentedMethod,
Issue::warning(format!(
"Ambiguous method call to `{method_name}` on class `{classname}`."
))
.with_annotation(
Annotation::primary(selector_span).with_message("This method is not explicitly defined"),
)
.with_annotation(
Annotation::secondary(obj_span).with_message(format!("On an object of type `{classname}`")),
)
.with_note(
"While this call might be handled by `__call()` or `__callStatic()`, Mago cannot verify its arguments or return type without a corresponding `@method` docblock tag.",
)
.with_help(format!(
"To enable full analysis, add a `@method` tag to the docblock of the `{classname}` class. For example: `/** @method returnType {method_name}(argType $argName) */`"
)),
);
}
fn report_possibly_non_existent_mixin_method<A>(
context: &mut Context<'_, '_, A>,
obj_span: Span,
selector_span: Span,
classname: Word,
method_name: Word,
mixin_classname: Word,
) where
A: Arena,
{
let mixin_classname = display_class_like_name(context, mixin_classname);
let method_name = display_method_name(context, classname, method_name);
let classname = display_class_like_name(context, classname);
context.collector.report_with_code(
IssueCode::PossiblyNonExistentMethod,
Issue::warning(format!(
"Method `{method_name}` might not exist on type `{classname}` at runtime."
))
.with_annotation(
Annotation::primary(selector_span).with_message("Method might not exist"),
)
.with_annotation(
Annotation::secondary(obj_span).with_message(format!("On an instance of `{classname}`")),
)
.with_note(format!(
"The method `{method_name}` is defined in mixin class `{mixin_classname}`, but `{classname}` does not have a `__call` method to forward the call."
))
.with_note(
"A subclass of this class could implement `__call` to handle this, so the call might succeed at runtime."
)
.with_help(format!(
"Add a `__call` method to `{classname}`, or make `{classname}` final if this should be an error."
)),
);
}
fn report_non_existent_mixin_method<A>(
context: &mut Context<'_, '_, A>,
obj_span: Span,
selector_span: Span,
classname: Word,
method_name: Word,
mixin_classname: Word,
) where
A: Arena,
{
let mixin_classname = display_class_like_name(context, mixin_classname);
let method_name = display_method_name(context, classname, method_name);
let classname = display_class_like_name(context, classname);
context.collector.report_with_code(
IssueCode::NonExistentMethod,
Issue::error(format!(
"Method `{method_name}` does not exist on final type `{classname}`."
))
.with_annotation(
Annotation::primary(selector_span).with_message("Method does not exist"),
)
.with_annotation(
Annotation::secondary(obj_span).with_message(format!("On an instance of final class `{classname}`")),
)
.with_note(format!(
"The method `{method_name}` is defined in mixin class `{mixin_classname}`, but `{classname}` is final and does not have a `__call` method to forward the call."
))
.with_help(format!(
"Add a `__call` method to `{classname}` to handle mixin method calls."
)),
);
}
pub(super) fn report_possibly_missing_magic_call<A>(
context: &mut Context<'_, '_, A>,
obj_span: Span,
selector_span: Span,
classname: Word,
method_name: Word,
is_static: bool,
) where
A: Arena,
{
let magic_method_name = if is_static { "__callStatic" } else { "__call" };
let classname = display_class_like_name(context, classname);
let method_name = display_method_name(context, classname, method_name);
context.collector.report_with_code(
IssueCode::PossiblyNonExistentMethod,
Issue::warning(format!(
"Call to documented magic method `{method_name}()` on a class that may not handle it."
))
.with_annotation(
Annotation::primary(selector_span).with_message("This magic method is documented but may not be callable"),
)
.with_annotation(
Annotation::secondary(obj_span)
.with_message(format!("Class `{classname}` is missing the `{magic_method_name}` method")),
)
.with_note(format!(
"The class `{classname}` has a `@method` tag for `{method_name}` but does not have a `{magic_method_name}` method to handle the call. A subclass could provide `{magic_method_name}` at runtime, so this is only a warning; if `{classname}` were final, this would be a hard error."
))
.with_help(format!(
"Add a `{magic_method_name}` method to `{classname}`, or make `{classname}` final if calls to `{method_name}` should be rejected outright."
)),
);
}
pub(super) fn report_magic_call_without_call_method<A>(
context: &mut Context<'_, '_, A>,
obj_span: Span,
selector_span: Span,
classname: Word,
method_name: Word,
is_static: bool,
) where
A: Arena,
{
let magic_method_name = if is_static { "__callStatic" } else { "__call" };
let classname = display_class_like_name(context, classname);
let method_name = display_method_name(context, classname, method_name);
context.collector.report_with_code(
IssueCode::MissingMagicMethod,
Issue::error(format!(
"Call to documented magic method `{method_name}()` on a class that cannot handle it."
))
.with_annotation(
Annotation::primary(selector_span)
.with_message("This magic method is documented but cannot be called"),
)
.with_annotation(
Annotation::secondary(obj_span).with_message(format!("Class `{classname}` is missing the `{magic_method_name}` method")),
)
.with_note(
format!("The class `{classname}` has a `@method` tag for `{method_name}` but does not have a `{magic_method_name}` method to handle the call. This will cause a fatal `Error` at runtime.")
)
.with_help(
format!("Add a `{magic_method_name}` method to the `{classname}` class to handle calls to magic methods.")
),
);
}
pub(super) fn report_dynamic_static_method_call<A>(
context: &mut Context<'_, '_, A>,
obj_span: Span,
selector_span: Span,
classname: Word,
method_name: Word,
has_magic_call: bool,
) where
A: Arena,
{
let classname = display_class_like_name(context, classname);
let method_name = display_method_name(context, classname, method_name);
let mut issue =
Issue::error(format!("Cannot call magic static method `{classname}::{method_name}` on an instance."))
.with_annotation(
Annotation::primary(selector_span)
.with_message("This magic method is static and must be called statically"),
)
.with_annotation(
Annotation::secondary(obj_span).with_message(format!("Called on an instance of `{classname}`")),
);
if has_magic_call {
issue = issue
.with_note(format!(
"The magic method `{method_name}` is documented as `static` and is intended to be handled by `__callStatic()`."
))
.with_note(
"However, because it's being called on an instance (`->`), the call will be routed to the existing `__call()` method instead."
)
.with_note(
"This is likely not the intended behavior and may lead to unexpected errors."
);
} else {
issue = issue
.with_note(
"Magic methods defined with `@method static` are handled by `__callStatic()`."
)
.with_note(
"When called on an instance (`->`), PHP attempts to route the call to a `__call()` method."
)
.with_note(format!(
"Since the class `{classname}` is missing a `__call()` method, this will cause a fatal `Error` at runtime."
));
}
context.collector.report_with_code(
IssueCode::DynamicStaticMethodCall,
issue.with_help(format!("Call this method statically instead: `{classname}::{method_name}`.")),
);
}
fn type_has_method_assertion(object_type: &TObject, method_name: &[u8]) -> bool {
match object_type {
TObject::HasMethod(has_method) => {
if has_method.has_method(method_name) {
return true;
}
has_method.intersection_types.as_ref().is_some_and(|types| {
types.iter().any(|atomic| {
if let TAtomic::Object(obj) = atomic { type_has_method_assertion(obj, method_name) } else { false }
})
})
}
TObject::HasProperty(has_property) => has_property.intersection_types.as_ref().is_some_and(|types| {
types.iter().any(|atomic| {
if let TAtomic::Object(obj) = atomic { type_has_method_assertion(obj, method_name) } else { false }
})
}),
TObject::Named(named_object) => named_object.get_intersection_types().is_some_and(|intersection_types| {
intersection_types.iter().any(|atomic| {
if let TAtomic::Object(obj) = atomic { type_has_method_assertion(obj, method_name) } else { false }
})
}),
_ => false,
}
}
fn collect_mixin_types(
codebase: &CodebaseMetadata,
class_metadata: &ClassLikeMetadata,
outer_object: &TObject,
mixins: &[TUnion],
) -> Vec<(Word, TObject)> {
let mut results = Vec::new();
let mut visited = HashSet::default();
collect_mixin_types_into(codebase, class_metadata, outer_object, mixins, &mut results, &mut visited);
results
}
fn collect_mixin_types_into(
codebase: &CodebaseMetadata,
class_metadata: &ClassLikeMetadata,
outer_object: &TObject,
mixins: &[TUnion],
results: &mut Vec<(Word, TObject)>,
visited: &mut HashSet<Word>,
) {
let mut direct: Vec<(Word, &TObject)> = Vec::new();
for mixin_type in mixins {
for mixin_atomic in mixin_type.types.as_ref() {
match mixin_atomic {
TAtomic::Object(obj @ TObject::Named(named)) => {
direct.push((ascii_lowercase_word(named.name.as_bytes()), obj));
}
TAtomic::Object(obj @ TObject::Enum(enum_type)) => {
direct.push((ascii_lowercase_word(enum_type.name.as_bytes()), obj));
}
TAtomic::GenericParameter(TGenericParameter {
parameter_name, constraint, defining_entity, ..
}) => {
let mut resolved = false;
if let TObject::Named(named_object) = outer_object
&& let Some(type_params) = named_object.get_type_parameters()
&& let GenericParent::ClassLike(defining_class) = defining_entity
&& named_object.name.as_bytes().eq_ignore_ascii_case(defining_class.as_bytes())
&& let Some(index) = class_metadata.get_template_index_for_name(*parameter_name)
&& let Some(concrete_type) = type_params.get(index)
{
for atomic in concrete_type.types.as_ref() {
match atomic {
TAtomic::Object(obj @ TObject::Named(named)) => {
direct.push((ascii_lowercase_word(named.name.as_bytes()), obj));
resolved = true;
}
TAtomic::Object(obj @ TObject::Enum(enum_type)) => {
direct.push((ascii_lowercase_word(enum_type.name.as_bytes()), obj));
resolved = true;
}
_ => {}
}
}
}
if !resolved {
for constraint_atomic in constraint.types.as_ref() {
match constraint_atomic {
TAtomic::Object(obj @ TObject::Named(named)) => {
direct.push((ascii_lowercase_word(named.name.as_bytes()), obj));
}
TAtomic::Object(obj @ TObject::Enum(enum_type)) => {
direct.push((ascii_lowercase_word(enum_type.name.as_bytes()), obj));
}
_ => {}
}
}
}
}
_ => {}
}
}
}
for (name, obj) in direct {
if !visited.insert(name) {
continue;
}
let specialized_obj = specialize_mixin_object(codebase, class_metadata, outer_object, obj);
let obj = specialized_obj.as_ref().unwrap_or(obj);
results.push((name, obj.clone()));
if let Some(mixin_metadata) = codebase.get_class_like(name.as_bytes())
&& !mixin_metadata.mixins.is_empty()
{
collect_mixin_types_into(codebase, mixin_metadata, obj, &mixin_metadata.mixins, results, visited);
}
}
}
fn specialize_mixin_object(
codebase: &CodebaseMetadata,
carrier_metadata: &ClassLikeMetadata,
carrier_object: &TObject,
mixin_object: &TObject,
) -> Option<TObject> {
let TObject::Named(named) = mixin_object else {
return None;
};
let parameters = named.type_parameters.as_deref()?;
if !parameters.iter().any(|parameter| parameter.has_template_types()) {
return None;
}
let TObject::Named(carrier) = carrier_object else {
return None;
};
let carrier_parameters = carrier.type_parameters.as_ref()?;
let mut named = named.clone();
named.type_parameters = Some(
parameters
.iter()
.map(|parameter| {
super::class_template_type_collector::resolve_template_parameter(
codebase,
parameter,
carrier_metadata,
carrier_parameters,
)
.unwrap_or_else(|| parameter.clone())
})
.collect(),
);
Some(TObject::Named(named))
}