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