use mago_allocator::Arena;
use mago_codex::identifier::function_like::FunctionLikeIdentifier;
use mago_span::HasSpan;
use mago_syntax::cst::ClassLikeMemberSelector;
use mago_syntax::cst::Expression;
use mago_syntax::cst::StaticMethodCall;
use crate::analyzable::Analyzable;
use crate::artifacts::AnalysisArtifacts;
use crate::context::Context;
use crate::context::block::BlockContext;
use crate::error::AnalysisError;
use crate::expression::call::analyze_invocation_targets;
use crate::expression::call::method_call::analyze_undocumented_method_return_type;
use crate::expression::call::method_call::prepare_unresolved_method_targets;
use crate::expression::call::record_external_method_call;
use crate::expression::call::record_external_method_call_targets;
use crate::invocation::InvocationArgumentsSource;
use crate::invocation::InvocationTarget;
use crate::invocation::MethodInvocationKind;
use crate::invocation::MethodTargetContext;
use crate::plugin::ExpressionHookResult;
use crate::plugin::context::HookContext;
use crate::resolver::static_method::resolve_static_method_targets;
use crate::utils::expression::expression_is_nullsafe;
impl<'ast, 'arena> Analyzable<'ast, 'arena> for StaticMethodCall<'arena> {
#[allow(clippy::expect_used)]
fn analyze<'ctx, A>(
&'ast self,
context: &mut Context<'ctx, 'arena, A>,
block_context: &mut BlockContext<'ctx>,
artifacts: &mut AnalysisArtifacts,
) -> Result<(), AnalysisError>
where
A: Arena,
{
if context.plugin_registry.has_static_method_call_hooks() {
let mut hook_context = HookContext::new(context.codebase, context.source_file, block_context, artifacts);
let result = context.plugin_registry.before_static_method_call(self, &mut hook_context)?;
for reported in hook_context.take_issues() {
context.collector.report_with_code(reported.code, reported.issue);
}
match result {
ExpressionHookResult::Continue => {}
ExpressionHookResult::Skip => {
return Ok(());
}
ExpressionHookResult::SkipWithType(ty) => {
artifacts.set_expression_type(&self.span(), ty);
return Ok(());
}
}
}
if block_context.flags.collect_initializations()
&& let Expression::Parent(_) = self.class
&& let ClassLikeMemberSelector::Identifier(method_ident) = &self.method
{
if method_ident.value.eq_ignore_ascii_case(b"__construct") {
block_context.flags.set_calls_parent_constructor(true);
} else {
let method_name = mago_word::ascii_lowercase_word(method_ident.value);
let parent_meta = block_context
.scope
.get_class_like()
.and_then(|m| m.direct_parent_class)
.and_then(|p| context.codebase.get_class_like(p.as_bytes()));
if let Some(parent_meta) = parent_meta
&& context.is_class_initializer_for(parent_meta, method_name)
{
block_context.calls_parent_initializer = Some(method_name);
}
}
}
let mut method_resolution =
resolve_static_method_targets(context, block_context, artifacts, self.class, &self.method, self.span())?;
let undocumented_template_result =
(!method_resolution.undocumented_methods.is_empty()).then(|| method_resolution.template_result.clone());
let mut invocation_targets = vec![];
for resolved_method in method_resolution.resolved_methods {
let metadata = context
.codebase
.get_class_like(resolved_method.classname.as_bytes())
.expect("class-like metadata should exist for resolved method");
let method_metadata = context
.codebase
.get_method_by_id(&resolved_method.method_identifier)
.expect("method metadata should exist for resolved method");
let method_display = format!(
"{}::{}",
resolved_method.method_identifier.get_class_name(),
resolved_method.method_identifier.get_method_name(),
);
crate::utils::availability::check_method_availability(
context,
method_metadata,
&method_display,
self.span(),
);
let method_target_context = MethodTargetContext {
invocation_kind: MethodInvocationKind::Static,
declaring_method_id: Some(resolved_method.method_identifier),
class_like_metadata: metadata,
class_type: resolved_method.static_class_type,
declaring_object_type: resolved_method.declaring_object,
};
invocation_targets.push(InvocationTarget::FunctionLike {
identifier: FunctionLikeIdentifier::Method(
resolved_method.method_identifier.get_class_name(),
resolved_method.method_identifier.get_method_name(),
),
metadata: method_metadata,
inferred_return_type: None,
effective_signature: None,
method_context: Some(method_target_context),
span: self.span(),
});
}
if !method_resolution.unresolved_methods.is_empty() {
method_resolution.has_invalid_target |= prepare_unresolved_method_targets(
context,
artifacts,
std::mem::take(&mut method_resolution.unresolved_methods),
InvocationArgumentsSource::ArgumentList(&self.argument_list),
self.span(),
MethodInvocationKind::Static,
&mut invocation_targets,
)?;
}
let has_resolved_methods = !invocation_targets.is_empty();
record_external_method_call(context, artifacts, &invocation_targets, self.span());
if !method_resolution.undocumented_methods.is_empty() {
record_external_method_call_targets(
context,
artifacts,
method_resolution.undocumented_methods.iter().map(|method| (method.classname, method.method_name)),
self.span(),
);
}
let class_has_nullsafe_null = artifacts.get_expression_type(self.class).is_some_and(|t| t.has_nullsafe_null());
if has_resolved_methods || method_resolution.undocumented_methods.is_empty() {
analyze_invocation_targets(
context,
block_context,
artifacts,
method_resolution.template_result,
invocation_targets,
InvocationArgumentsSource::ArgumentList(&self.argument_list),
self.span(),
None,
method_resolution.has_invalid_target,
method_resolution.encountered_mixed,
expression_is_nullsafe(self.class) || method_resolution.encountered_null,
class_has_nullsafe_null,
)?;
}
if !method_resolution.undocumented_methods.is_empty() {
analyze_undocumented_method_return_type(
context,
block_context,
artifacts,
method_resolution.undocumented_methods,
undocumented_template_result.as_ref().expect("undocumented calls have a template result"),
&self.argument_list,
self.span(),
MethodInvocationKind::Static,
!has_resolved_methods,
)?;
}
if context.plugin_registry.has_static_method_call_hooks() {
let mut hook_context = HookContext::new(context.codebase, context.source_file, block_context, artifacts);
context.plugin_registry.after_static_method_call(self, &mut hook_context)?;
for reported in hook_context.take_issues() {
context.collector.report_with_code(reported.code, reported.issue);
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use indoc::indoc;
use crate::code::IssueCode;
use crate::test_analysis;
test_analysis! {
name = calling_non_static_method_statically_is_ok,
code = indoc! {"
<?php
class Example {
private string $value = '';
function doWork(): void {
$something = self::getSomething(); // Ok
$something .= $this->getSomething(); // Ok
$something .= Example::getSomething(); // Ok
$something .= static::getSomething(); // Ok
echo 'Doing work with: ' . $something;
}
function getSomething(): string {
return $this->value;
}
}
class SubExample extends Example {
function doWork(): void {
$something = self::getSomething(); // Ok
$something .= $this->getSomething(); // Ok
$something .= Example::getSomething(); // Ok
$something .= SubExample::getSomething(); // Ok
$something .= static::getSomething(); // Ok
$something .= parent::getSomething(); // Ok
echo 'Doing work with: ' . $something;
}
}
trait TraitExample {
function doWork(): void {
$something = self::getSomething(); // Ok
$something .= $this->getSomething(); // Ok
$something .= static::getSomething(); // Ok
echo 'Doing work with: ' . $something;
}
function getSomething(): string {
return 'Trait value';
}
}
class TraitUser {
use TraitExample;
function doWorkToo(): void {
$something = self::getSomething(); // Ok
$something .= $this->getSomething(); // Ok
$something .= TraitUser::getSomething(); // Ok
$something .= static::getSomething(); // Ok
echo 'Doing work with: ' . $something;
}
}
$e = new Example();
$s = new SubExample();
$t = new TraitUser();
$e->doWork();
$s->doWork();
$t->doWork();
$t->doWorkToo();
"}
}
test_analysis! {
name = calling_static_method_on_interface_string,
code = indoc! {"
<?php
interface Example {
public static function doTheThing(): void;
public static function getSomeValue(): int;
}
/**
* @param array<class-string<Example>> $examples
*
* @return array<string, int>
*/
function process(array $examples): array {
$result = [];
foreach ($examples as $example) {
$example::doTheThing();
$value = $example::getSomeValue();
$result[$example] = $value;
}
return $result;
}
"},
issues = [
IssueCode::PossiblyStaticAccessOnInterface,
IssueCode::PossiblyStaticAccessOnInterface,
]
}
test_analysis! {
name = calling_static_method_on_interface_name,
code = indoc! {"
<?php
interface Example {
public static function doTheThing(): void;
public static function getSomeValue(): int;
}
Example::doTheThing();
echo Example::getSomeValue();
"},
issues = [
IssueCode::StaticAccessOnInterface,
IssueCode::StaticAccessOnInterface,
IssueCode::MixedArgument,
]
}
}