use std::sync::Arc;
use php_ast::owned::{ExprKind, StaticDynMethodCallExpr, StaticMethodCallExpr};
use php_ast::Span;
use mir_issues::{IssueKind, Severity};
use mir_types::{Atomic, Type};
use crate::expr::ExpressionAnalyzer;
use crate::flow_state::FlowState;
use crate::symbol::ReferenceKind;
use super::args::{
check_args, expr_can_be_passed_by_reference_owned, spread_element_type,
substitute_static_in_return, CheckArgsParams,
};
use super::method::resolve_method_from_db;
use super::CallAnalyzer;
fn extract_namespace(fqcn: &str) -> Option<&str> {
if let Some(pos) = fqcn.rfind('\\') {
Some(&fqcn[..pos])
} else {
None
}
}
fn is_valid_class_name_type(ty: &Type) -> bool {
ty.contains(|t| {
matches!(
t,
Atomic::TString | Atomic::TClassString(_) | Atomic::TLiteralString(_) | Atomic::TMixed
)
})
}
fn is_object_atomic(t: &Atomic) -> bool {
matches!(
t,
Atomic::TObject
| Atomic::TNamedObject { .. }
| Atomic::TStaticObject { .. }
| Atomic::TSelf { .. }
| Atomic::TParent { .. }
| Atomic::TIntersection { .. }
| Atomic::TNull
)
}
fn extract_object_fqcn(ty: &Type) -> Option<String> {
let mut result: Option<String> = None;
for atom in ty.types.iter() {
let fqcn_str = match atom {
Atomic::TNamedObject { fqcn, .. }
| Atomic::TStaticObject { fqcn }
| Atomic::TSelf { fqcn }
| Atomic::TParent { fqcn } => fqcn.to_string(),
Atomic::TNull => continue, _ => return None,
};
match &result {
None => result = Some(fqcn_str),
Some(existing) if *existing == fqcn_str => {}
_ => return None,
}
}
result
}
impl CallAnalyzer {
pub fn analyze_static_method_call<'a>(
ea: &mut ExpressionAnalyzer<'a>,
call: &StaticMethodCallExpr,
ctx: &mut FlowState,
span: Span,
) -> Type {
let method_name = match &call.method.kind {
ExprKind::Identifier(name) => name.as_ref(),
_ => return Type::mixed(),
};
let fqcn = match &call.class.kind {
ExprKind::Identifier(name) => crate::db::resolve_name(ea.db, &ea.file, name.as_ref()),
_ => {
let ty = ea.analyze(&call.class, ctx);
if let Some(fqcn) = extract_object_fqcn(&ty) {
if ty.is_nullable() {
ea.emit(
IssueKind::PossiblyNullMethodCall {
method: method_name.to_string(),
},
Severity::Info,
call.class.span,
);
}
fqcn
} else {
if !is_valid_class_name_type(&ty) && !ty.types.iter().all(is_object_atomic) {
ea.emit(
IssueKind::InvalidStringClass {
actual: ty.to_string(),
},
Severity::Warning,
call.class.span,
);
}
return Type::mixed();
}
}
};
let fqcn = resolve_static_class(&fqcn, ctx);
let arg_types: Vec<Type> = call
.args
.iter()
.map(|arg| {
let ty = ea.analyze(&arg.value, ctx);
if arg.unpack {
spread_element_type(&ty)
} else {
ty
}
})
.collect();
let arg_spans: Vec<Span> = call.args.iter().map(|a| a.span).collect();
let fqcn_arc: Arc<str> = Arc::from(fqcn.as_str());
let method_name_lower = method_name.to_lowercase();
if crate::db::class_exists(ea.db, &fqcn) {
let here = crate::db::Fqcn::from_str(ea.db, fqcn_arc.as_ref());
let is_interface = crate::db::find_class_like(ea.db, here)
.map(|c| c.is_interface())
.unwrap_or(false);
if is_interface {
ea.emit(
IssueKind::UndefinedClass { name: fqcn.clone() },
Severity::Error,
call.class.span,
);
return Type::mixed();
}
}
let resolved = resolve_method_from_db(ea, &fqcn_arc, &method_name_lower);
if let Some(resolved) = resolved {
ea.record_ref(
Arc::from(format!("{}::{}", &fqcn, method_name.to_lowercase())),
call.method.span,
);
if let Some(msg) = resolved.deprecated.clone() {
ea.emit(
IssueKind::DeprecatedMethodCall {
class: fqcn.clone(),
method: method_name.to_string(),
message: Some(msg).filter(|m| !m.is_empty()),
},
Severity::Info,
span,
);
}
if resolved.is_internal {
let calling_namespace = ea.db.file_namespace(&ea.file).map(|ns| ns.to_string());
let method_namespace =
extract_namespace(&resolved.owner_fqcn).map(|s| s.to_string());
if calling_namespace != method_namespace {
ea.emit(
IssueKind::InternalMethod {
class: fqcn.clone(),
method: method_name.to_string(),
},
Severity::Warning,
span,
);
}
}
let arg_names: Vec<Option<String>> = call
.args
.iter()
.map(|a| a.name.as_ref().map(crate::parser::name_to_string_owned))
.collect();
let arg_can_be_byref: Vec<bool> = call
.args
.iter()
.map(|a| expr_can_be_passed_by_reference_owned(&a.value))
.collect();
check_args(
ea,
CheckArgsParams {
fn_name: method_name,
params: &resolved.params,
arg_types: &arg_types,
arg_spans: &arg_spans,
arg_names: &arg_names,
arg_can_be_byref: &arg_can_be_byref,
call_span: span,
has_spread: call.args.iter().any(|a| a.unpack),
template_params: &resolved.template_params,
},
);
let ret_raw = resolved.return_ty_raw;
let ret = substitute_static_in_return(ret_raw, &fqcn_arc);
ea.record_symbol(
call.method.span,
ReferenceKind::StaticCall {
class: fqcn_arc,
method: Arc::from(method_name),
},
ret.clone(),
);
ret
} else if crate::db::class_exists(ea.db, &fqcn)
&& !crate::db::has_unknown_ancestor(ea.db, &fqcn)
{
let is_abstract = crate::db::class_kind(ea.db, &fqcn)
.map(|k| k.is_abstract)
.unwrap_or(false);
let has_callstatic_magic = crate::db::has_method_in_chain(ea.db, &fqcn, "__callstatic");
if is_abstract || has_callstatic_magic {
Type::mixed()
} else {
ea.emit(
IssueKind::UndefinedMethod {
class: fqcn,
method: method_name.to_string(),
},
Severity::Error,
span,
);
Type::mixed()
}
} else if !crate::db::class_exists(ea.db, &fqcn)
&& !matches!(fqcn.as_str(), "self" | "static" | "parent")
{
ea.emit(
IssueKind::UndefinedClass { name: fqcn },
Severity::Error,
call.class.span,
);
Type::mixed()
} else {
Type::mixed()
}
}
pub fn analyze_static_dyn_method_call<'a>(
ea: &mut ExpressionAnalyzer<'a>,
call: &StaticDynMethodCallExpr,
ctx: &mut FlowState,
) -> Type {
for arg in call.args.iter() {
ea.analyze(&arg.value, ctx);
}
Type::mixed()
}
}
fn resolve_static_class(name: &str, ctx: &FlowState) -> String {
match name.to_lowercase().as_str() {
"self" => ctx.self_fqcn.as_deref().unwrap_or("self").to_string(),
"parent" => ctx.parent_fqcn.as_deref().unwrap_or("parent").to_string(),
"static" => ctx
.static_fqcn
.as_deref()
.unwrap_or(ctx.self_fqcn.as_deref().unwrap_or("static"))
.to_string(),
_ => name.to_string(),
}
}