1use std::sync::Arc;
2
3use php_ast::ast::{ExprKind, StaticDynMethodCallExpr, StaticMethodCallExpr};
4use php_ast::Span;
5
6use mir_issues::{IssueKind, Severity};
7use mir_types::Union;
8
9use crate::context::Context;
10use crate::expr::ExpressionAnalyzer;
11use crate::symbol::SymbolKind;
12
13use super::args::{
14 check_args, expr_can_be_passed_by_reference, spread_element_type, substitute_static_in_return,
15 CheckArgsParams,
16};
17use super::method::resolve_method_from_db;
18use super::CallAnalyzer;
19
20fn extract_namespace(fqcn: &str) -> Option<&str> {
21 if let Some(pos) = fqcn.rfind('\\') {
22 Some(&fqcn[..pos])
23 } else {
24 None
25 }
26}
27
28impl CallAnalyzer {
29 pub fn analyze_static_method_call<'a, 'arena, 'src>(
30 ea: &mut ExpressionAnalyzer<'a>,
31 call: &StaticMethodCallExpr<'arena, 'src>,
32 ctx: &mut Context,
33 span: Span,
34 ) -> Union {
35 let method_name = match &call.method.kind {
36 ExprKind::Identifier(name) => name.as_str(),
37 _ => return Union::mixed(),
38 };
39
40 let fqcn = match &call.class.kind {
41 ExprKind::Identifier(name) => {
42 crate::db::resolve_name_via_db(ea.db, &ea.file, name.as_ref())
43 }
44 _ => return Union::mixed(),
45 };
46
47 let fqcn = resolve_static_class(&fqcn, ctx);
48
49 let arg_types: Vec<Union> = call
50 .args
51 .iter()
52 .map(|arg| {
53 let ty = ea.analyze(&arg.value, ctx);
54 if arg.unpack {
55 spread_element_type(&ty)
56 } else {
57 ty
58 }
59 })
60 .collect();
61 let arg_spans: Vec<Span> = call.args.iter().map(|a| a.span).collect();
62
63 let fqcn_arc: Arc<str> = Arc::from(fqcn.as_str());
64 let method_name_lower = method_name.to_lowercase();
65
66 let resolved = resolve_method_from_db(ea, &fqcn_arc, &method_name_lower);
67
68 if let Some(resolved) = resolved {
69 if !ea.inference_only {
70 let (line, col_start, col_end) = ea.span_to_ref_loc(call.method.span);
71 ea.db.record_reference_location(crate::db::RefLoc {
72 symbol_key: Arc::from(format!("{}::{}", &fqcn, method_name.to_lowercase())),
73 file: ea.file.clone(),
74 line,
75 col_start,
76 col_end,
77 });
78 }
79 if let Some(msg) = resolved.deprecated.clone() {
80 ea.emit(
81 IssueKind::DeprecatedMethodCall {
82 class: fqcn.clone(),
83 method: method_name.to_string(),
84 message: Some(msg).filter(|m| !m.is_empty()),
85 },
86 Severity::Info,
87 span,
88 );
89 }
90 if resolved.is_internal {
91 let calling_namespace = ea.db.file_namespace(&ea.file).map(|ns| ns.to_string());
92 let method_namespace =
93 extract_namespace(&resolved.owner_fqcn).map(|s| s.to_string());
94 if calling_namespace != method_namespace {
95 ea.emit(
96 IssueKind::InternalMethod {
97 class: fqcn.clone(),
98 method: method_name.to_string(),
99 },
100 Severity::Warning,
101 span,
102 );
103 }
104 }
105 let arg_names: Vec<Option<String>> = call
106 .args
107 .iter()
108 .map(|a| a.name.as_ref().map(|n| n.to_string_repr().into_owned()))
109 .collect();
110 let arg_can_be_byref: Vec<bool> = call
111 .args
112 .iter()
113 .map(|a| expr_can_be_passed_by_reference(&a.value))
114 .collect();
115 check_args(
116 ea,
117 CheckArgsParams {
118 fn_name: method_name,
119 params: &resolved.params,
120 arg_types: &arg_types,
121 arg_spans: &arg_spans,
122 arg_names: &arg_names,
123 arg_can_be_byref: &arg_can_be_byref,
124 call_span: span,
125 has_spread: call.args.iter().any(|a| a.unpack),
126 },
127 );
128 let ret_raw = resolved.return_ty_raw;
129 let ret = substitute_static_in_return(ret_raw, &fqcn_arc);
130 ea.record_symbol(
131 call.method.span,
132 SymbolKind::StaticCall {
133 class: fqcn_arc,
134 method: Arc::from(method_name),
135 },
136 ret.clone(),
137 );
138 ret
139 } else if crate::db::type_exists_via_db(ea.db, &fqcn)
140 && !crate::db::has_unknown_ancestor_via_db(ea.db, &fqcn)
141 {
142 let (is_interface, is_abstract) = crate::db::class_kind_via_db(ea.db, &fqcn)
143 .map(|k| (k.is_interface, k.is_abstract))
144 .unwrap_or((false, false));
145 let has_callstatic_magic =
147 crate::db::lookup_method_in_chain(ea.db, &fqcn, "__callstatic").is_some();
148 if is_interface || is_abstract || has_callstatic_magic {
149 Union::mixed()
150 } else {
151 ea.emit(
152 IssueKind::UndefinedMethod {
153 class: fqcn,
154 method: method_name.to_string(),
155 },
156 Severity::Error,
157 span,
158 );
159 Union::mixed()
160 }
161 } else if !crate::db::type_exists_via_db(ea.db, &fqcn)
162 && !matches!(fqcn.as_str(), "self" | "static" | "parent")
163 {
164 ea.emit(
165 IssueKind::UndefinedClass { name: fqcn },
166 Severity::Error,
167 call.class.span,
168 );
169 Union::mixed()
170 } else {
171 Union::mixed()
172 }
173 }
174
175 pub fn analyze_static_dyn_method_call<'a, 'arena, 'src>(
176 ea: &mut ExpressionAnalyzer<'a>,
177 call: &StaticDynMethodCallExpr<'arena, 'src>,
178 ctx: &mut Context,
179 ) -> Union {
180 for arg in call.args.iter() {
181 ea.analyze(&arg.value, ctx);
182 }
183 Union::mixed()
184 }
185}
186
187fn resolve_static_class(name: &str, ctx: &Context) -> String {
188 match name.to_lowercase().as_str() {
189 "self" => ctx.self_fqcn.as_deref().unwrap_or("self").to_string(),
190 "parent" => ctx.parent_fqcn.as_deref().unwrap_or("parent").to_string(),
191 "static" => ctx
192 .static_fqcn
193 .as_deref()
194 .unwrap_or(ctx.self_fqcn.as_deref().unwrap_or("static"))
195 .to_string(),
196 _ => name.to_string(),
197 }
198}