1use std::sync::Arc;
2
3use php_ast::ast::{ExprKind, MethodCallExpr};
4use php_ast::Span;
5
6use mir_codebase::storage::{FnParam, TemplateParam, Visibility};
7use mir_issues::{IssueKind, Severity};
8use mir_types::Union;
9
10use crate::context::Context;
11use crate::expr::ExpressionAnalyzer;
12use crate::generic::{build_class_bindings, check_template_bounds, infer_template_bindings};
13use crate::symbol::SymbolKind;
14
15use super::args::{
16 check_args, check_method_visibility, expr_can_be_passed_by_reference, spread_element_type,
17 substitute_static_in_return, CheckArgsParams,
18};
19use super::CallAnalyzer;
20
21pub(super) struct ResolvedMethod {
22 pub(super) owner_fqcn: Arc<str>,
23 pub(super) name: Arc<str>,
24 pub(super) visibility: Visibility,
25 pub(super) deprecated: Option<Arc<str>>,
26 pub(super) params: Vec<FnParam>,
27 pub(super) template_params: Vec<TemplateParam>,
28 pub(super) return_ty_raw: Union,
29}
30
31pub(super) fn resolve_method_from_db(
33 ea: &ExpressionAnalyzer<'_>,
34 fqcn: &Arc<str>,
35 method_name_lower: &str,
36) -> Option<ResolvedMethod> {
37 let db = ea.db;
38
39 let node = crate::db::lookup_method_in_chain(db, fqcn, method_name_lower)?;
41 let owner_fqcn = node.fqcn(db);
42 let name = node.name(db);
43
44 let inferred = node.inferred_return_type(db);
49 let return_ty_raw = node
50 .return_type(db)
51 .or(inferred)
52 .map(|t| (*t).clone())
53 .unwrap_or_else(Union::mixed);
54
55 Some(ResolvedMethod {
56 owner_fqcn,
57 name,
58 visibility: node.visibility(db),
59 deprecated: node.deprecated(db),
60 params: node.params(db).to_vec(),
61 template_params: node.template_params(db).to_vec(),
62 return_ty_raw,
63 })
64}
65
66impl CallAnalyzer {
67 pub fn analyze_method_call<'a, 'arena, 'src>(
68 ea: &mut ExpressionAnalyzer<'a>,
69 call: &MethodCallExpr<'arena, 'src>,
70 ctx: &mut Context,
71 span: Span,
72 nullsafe: bool,
73 ) -> Union {
74 let obj_ty = ea.analyze(call.object, ctx);
75
76 let method_name = match &call.method.kind {
77 ExprKind::Identifier(name) => name.as_str(),
78 _ => return Union::mixed(),
79 };
80
81 let arg_types: Vec<Union> = call
85 .args
86 .iter()
87 .map(|arg| {
88 let ty = ea.analyze(&arg.value, ctx);
89 if arg.unpack {
90 spread_element_type(&ty)
91 } else {
92 ty
93 }
94 })
95 .collect();
96
97 let arg_spans: Vec<Span> = call.args.iter().map(|a| a.span).collect();
98
99 if obj_ty.contains(|t| matches!(t, mir_types::Atomic::TNull)) {
100 if nullsafe {
101 } else if obj_ty.is_single() {
103 ea.emit(
104 IssueKind::NullMethodCall {
105 method: method_name.to_string(),
106 },
107 Severity::Error,
108 span,
109 );
110 return Union::mixed();
111 } else {
112 ea.emit(
113 IssueKind::PossiblyNullMethodCall {
114 method: method_name.to_string(),
115 },
116 Severity::Info,
117 span,
118 );
119 }
120 }
121
122 if obj_ty.is_mixed() {
123 ea.emit(
124 IssueKind::MixedMethodCall {
125 method: method_name.to_string(),
126 },
127 Severity::Info,
128 span,
129 );
130 return Union::mixed();
131 }
132
133 let receiver = obj_ty.remove_null();
134 let mut result = Union::empty();
135
136 for atomic in &receiver.types {
137 match atomic {
138 mir_types::Atomic::TNamedObject {
139 fqcn,
140 type_params: receiver_type_params,
141 } => {
142 let fqcn_resolved = crate::db::resolve_name_via_db(ea.db, &ea.file, fqcn);
143 let fqcn = &std::sync::Arc::from(fqcn_resolved.as_str());
144 result = Union::merge(
145 &result,
146 &resolve_method_return(
147 ea,
148 ctx,
149 call,
150 span,
151 method_name,
152 fqcn,
153 receiver_type_params.as_slice(),
154 &arg_types,
155 &arg_spans,
156 ),
157 );
158 }
159 mir_types::Atomic::TSelf { fqcn }
160 | mir_types::Atomic::TStaticObject { fqcn }
161 | mir_types::Atomic::TParent { fqcn } => {
162 let fqcn_resolved = crate::db::resolve_name_via_db(ea.db, &ea.file, fqcn);
163 let fqcn = &std::sync::Arc::from(fqcn_resolved.as_str());
164 result = Union::merge(
165 &result,
166 &resolve_method_return(
167 ea,
168 ctx,
169 call,
170 span,
171 method_name,
172 fqcn,
173 &[],
174 &arg_types,
175 &arg_spans,
176 ),
177 );
178 }
179 mir_types::Atomic::TIntersection { parts } => {
180 let mut intersection_result = Union::empty();
181 let mut found_method = false;
182 for part in parts {
183 for inner_atomic in &part.types {
184 if let mir_types::Atomic::TNamedObject {
185 fqcn,
186 type_params: receiver_type_params,
187 } = inner_atomic
188 {
189 let fqcn_resolved =
190 crate::db::resolve_name_via_db(ea.db, &ea.file, fqcn);
191 let resolved_arc = Arc::from(fqcn_resolved.as_str());
192 if crate::db::method_exists_via_db(
193 ea.db,
194 &resolved_arc,
195 method_name,
196 ) {
197 found_method = true;
198 intersection_result = Union::merge(
199 &intersection_result,
200 &resolve_method_return(
201 ea,
202 ctx,
203 call,
204 span,
205 method_name,
206 &resolved_arc,
207 receiver_type_params.as_slice(),
208 &arg_types,
209 &arg_spans,
210 ),
211 );
212 }
213 }
214 }
215 }
216 if found_method {
217 result = Union::merge(&result, &intersection_result);
218 } else {
219 result = Union::merge(&result, &Union::mixed());
220 }
221 }
222 mir_types::Atomic::TObject | mir_types::Atomic::TTemplateParam { .. } => {
223 result = Union::merge(&result, &Union::mixed());
224 }
225 _ => {
226 result = Union::merge(&result, &Union::mixed());
227 }
228 }
229 }
230
231 if nullsafe && obj_ty.is_nullable() {
232 result.add_type(mir_types::Atomic::TNull);
233 }
234
235 let final_ty = if result.is_empty() {
236 Union::mixed()
237 } else {
238 result
239 };
240
241 for atomic in &obj_ty.types {
242 if let mir_types::Atomic::TNamedObject { fqcn, .. } = atomic {
243 ea.record_symbol(
244 call.method.span,
245 SymbolKind::MethodCall {
246 class: fqcn.clone(),
247 method: Arc::from(method_name),
248 },
249 final_ty.clone(),
250 );
251 break;
252 }
253 }
254 final_ty
255 }
256}
257
258#[allow(clippy::too_many_arguments)]
261fn resolve_method_return<'a, 'arena, 'src>(
262 ea: &mut ExpressionAnalyzer<'a>,
263 ctx: &Context,
264 call: &MethodCallExpr<'arena, 'src>,
265 span: Span,
266 method_name: &str,
267 fqcn: &Arc<str>,
268 receiver_type_params: &[Union],
269 arg_types: &[Union],
270 arg_spans: &[Span],
271) -> Union {
272 let method_name_lower = method_name.to_lowercase();
273 let resolved = resolve_method_from_db(ea, fqcn, &method_name_lower);
274
275 if let Some(resolved) = resolved {
276 if !ea.inference_only {
277 let (line, col_start, col_end) = ea.span_to_ref_loc(call.method.span);
278 ea.db.record_reference_location(crate::db::RefLoc {
279 symbol_key: Arc::from(format!(
280 "{}::{}",
281 &resolved.owner_fqcn,
282 resolved.name.to_lowercase()
283 )),
284 file: ea.file.clone(),
285 line,
286 col_start,
287 col_end,
288 });
289 }
290 if let Some(msg) = resolved.deprecated.clone() {
291 ea.emit(
292 IssueKind::DeprecatedMethodCall {
293 class: fqcn.to_string(),
294 method: method_name.to_string(),
295 message: Some(msg).filter(|m| !m.is_empty()),
296 },
297 Severity::Info,
298 span,
299 );
300 }
301 check_method_visibility(
302 ea,
303 resolved.visibility,
304 &resolved.owner_fqcn,
305 &resolved.name,
306 ctx,
307 span,
308 );
309
310 let arg_names: Vec<Option<String>> = call
311 .args
312 .iter()
313 .map(|a| a.name.as_ref().map(|n| n.to_string_repr().into_owned()))
314 .collect();
315 let arg_can_be_byref: Vec<bool> = call
316 .args
317 .iter()
318 .map(|a| expr_can_be_passed_by_reference(&a.value))
319 .collect();
320 let class_tps = crate::db::class_template_params_via_db(ea.db, fqcn)
323 .map(|tps| tps.to_vec())
324 .unwrap_or_default();
325 let mut bindings = build_class_bindings(&class_tps, receiver_type_params);
326 for (k, v) in crate::db::inherited_template_bindings_via_db(ea.db, fqcn) {
327 bindings.entry(k).or_insert(v);
328 }
329
330 let substituted_params: Vec<FnParam>;
332 let effective_params: &[FnParam] = if bindings.is_empty() {
333 &resolved.params
334 } else {
335 substituted_params = resolved
336 .params
337 .iter()
338 .map(|p| FnParam {
339 ty: mir_codebase::wrap_param_type(
340 p.ty.as_ref().map(|t| t.substitute_templates(&bindings)),
341 ),
342 ..p.clone()
343 })
344 .collect();
345 &substituted_params
346 };
347
348 check_args(
349 ea,
350 CheckArgsParams {
351 fn_name: method_name,
352 params: effective_params,
353 arg_types,
354 arg_spans,
355 arg_names: &arg_names,
356 arg_can_be_byref: &arg_can_be_byref,
357 call_span: span,
358 has_spread: call.args.iter().any(|a| a.unpack),
359 },
360 );
361
362 let ret_raw = substitute_static_in_return(resolved.return_ty_raw, fqcn);
363
364 if !resolved.template_params.is_empty() {
365 let method_bindings =
366 infer_template_bindings(&resolved.template_params, &resolved.params, arg_types);
367 for key in method_bindings.keys() {
368 if bindings.contains_key(key) {
369 ea.emit(
370 IssueKind::ShadowedTemplateParam {
371 name: key.to_string(),
372 },
373 Severity::Info,
374 span,
375 );
376 }
377 }
378 bindings.extend(method_bindings);
379 for (name, inferred, bound) in
380 check_template_bounds(&bindings, &resolved.template_params)
381 {
382 ea.emit(
383 IssueKind::InvalidTemplateParam {
384 name: name.to_string(),
385 expected_bound: format!("{bound}"),
386 actual: format!("{inferred}"),
387 },
388 Severity::Error,
389 span,
390 );
391 }
392 }
393
394 if !bindings.is_empty() {
395 ret_raw.substitute_templates(&bindings)
396 } else {
397 ret_raw
398 }
399 } else if crate::db::type_exists_via_db(ea.db, fqcn)
400 && !crate::db::has_unknown_ancestor_via_db(ea.db, fqcn)
401 {
402 let (is_interface, is_abstract) = crate::db::class_kind_via_db(ea.db, fqcn)
403 .map(|k| (k.is_interface, k.is_abstract))
404 .unwrap_or((false, false));
405 if is_interface || is_abstract || crate::db::method_exists_via_db(ea.db, fqcn, "__call") {
406 Union::mixed()
407 } else {
408 ea.emit(
409 IssueKind::UndefinedMethod {
410 class: fqcn.to_string(),
411 method: method_name.to_string(),
412 },
413 Severity::Error,
414 span,
415 );
416 Union::mixed()
417 }
418 } else {
419 Union::mixed()
420 }
421}