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 ea.emit(
136 IssueKind::MixedMethodCall {
137 method: method_name.to_string(),
138 },
139 Severity::Info,
140 span,
141 );
142 return Union::mixed();
143 }
144
145 let receiver = obj_ty.remove_null();
146 let mut result = Union::empty();
147
148 for atomic in &receiver.types {
149 match atomic {
150 mir_types::Atomic::TNamedObject {
151 fqcn,
152 type_params: receiver_type_params,
153 } => {
154 let fqcn_resolved = crate::db::resolve_name_via_db(ea.db, &ea.file, fqcn);
155 let fqcn = &std::sync::Arc::from(fqcn_resolved.as_str());
156 result = Union::merge(
157 &result,
158 &resolve_method_return(
159 ea,
160 ctx,
161 call,
162 span,
163 method_name,
164 fqcn,
165 receiver_type_params.as_slice(),
166 &arg_types,
167 &arg_spans,
168 ),
169 );
170 }
171 mir_types::Atomic::TSelf { fqcn }
172 | mir_types::Atomic::TStaticObject { fqcn }
173 | mir_types::Atomic::TParent { fqcn } => {
174 let fqcn_resolved = crate::db::resolve_name_via_db(ea.db, &ea.file, fqcn);
175 let fqcn = &std::sync::Arc::from(fqcn_resolved.as_str());
176 result = Union::merge(
177 &result,
178 &resolve_method_return(
179 ea,
180 ctx,
181 call,
182 span,
183 method_name,
184 fqcn,
185 &[],
186 &arg_types,
187 &arg_spans,
188 ),
189 );
190 }
191 mir_types::Atomic::TIntersection { parts } => {
192 let mut intersection_result = Union::empty();
193 let mut found_method = false;
194 for part in parts {
195 for inner_atomic in &part.types {
196 if let mir_types::Atomic::TNamedObject {
197 fqcn,
198 type_params: receiver_type_params,
199 } = inner_atomic
200 {
201 let fqcn_resolved =
202 crate::db::resolve_name_via_db(ea.db, &ea.file, fqcn);
203 let resolved_arc = Arc::from(fqcn_resolved.as_str());
204 if crate::db::lookup_method_in_chain(
205 ea.db,
206 &resolved_arc,
207 method_name,
208 )
209 .is_some()
210 {
211 found_method = true;
212 intersection_result = Union::merge(
213 &intersection_result,
214 &resolve_method_return(
215 ea,
216 ctx,
217 call,
218 span,
219 method_name,
220 &resolved_arc,
221 receiver_type_params.as_slice(),
222 &arg_types,
223 &arg_spans,
224 ),
225 );
226 }
227 }
228 }
229 }
230 if found_method {
231 result = Union::merge(&result, &intersection_result);
232 } else {
233 result = Union::merge(&result, &Union::mixed());
234 }
235 }
236 mir_types::Atomic::TObject | mir_types::Atomic::TTemplateParam { .. } => {
237 result = Union::merge(&result, &Union::mixed());
238 }
239 _ => {
240 result = Union::merge(&result, &Union::mixed());
241 }
242 }
243 }
244
245 if nullsafe && obj_ty.is_nullable() {
246 result.add_type(mir_types::Atomic::TNull);
247 }
248
249 let final_ty = if result.is_empty() {
250 Union::mixed()
251 } else {
252 result
253 };
254
255 for atomic in &obj_ty.types {
256 if let mir_types::Atomic::TNamedObject { fqcn, .. } = atomic {
257 ea.record_symbol(
258 call.method.span,
259 SymbolKind::MethodCall {
260 class: fqcn.clone(),
261 method: Arc::from(method_name),
262 },
263 final_ty.clone(),
264 );
265 break;
266 }
267 }
268 final_ty
269 }
270}
271
272#[allow(clippy::too_many_arguments)]
275fn resolve_method_return<'a, 'arena, 'src>(
276 ea: &mut ExpressionAnalyzer<'a>,
277 ctx: &Context,
278 call: &MethodCallExpr<'arena, 'src>,
279 span: Span,
280 method_name: &str,
281 fqcn: &Arc<str>,
282 receiver_type_params: &[Union],
283 arg_types: &[Union],
284 arg_spans: &[Span],
285) -> Union {
286 let method_name_lower = method_name.to_lowercase();
287 let resolved = resolve_method_from_db(ea, fqcn, &method_name_lower);
288
289 if let Some(resolved) = resolved {
290 if !ea.inference_only {
291 let (line, col_start, col_end) = ea.span_to_ref_loc(call.method.span);
292 ea.db.record_reference_location(crate::db::RefLoc {
293 symbol_key: Arc::from(format!(
294 "{}::{}",
295 &resolved.owner_fqcn,
296 resolved.name.to_lowercase()
297 )),
298 file: ea.file.clone(),
299 line,
300 col_start,
301 col_end,
302 });
303 }
304 if let Some(msg) = resolved.deprecated.clone() {
305 ea.emit(
306 IssueKind::DeprecatedMethodCall {
307 class: fqcn.to_string(),
308 method: method_name.to_string(),
309 message: Some(msg).filter(|m| !m.is_empty()),
310 },
311 Severity::Info,
312 span,
313 );
314 }
315 if resolved.is_internal {
316 let calling_namespace = ea.db.file_namespace(&ea.file).map(|ns| ns.to_string());
317 let method_namespace = extract_namespace(&resolved.owner_fqcn).map(|s| s.to_string());
318 if calling_namespace != method_namespace {
319 ea.emit(
320 IssueKind::InternalMethod {
321 class: fqcn.to_string(),
322 method: method_name.to_string(),
323 },
324 Severity::Warning,
325 span,
326 );
327 }
328 }
329 check_method_visibility(
330 ea,
331 resolved.visibility,
332 &resolved.owner_fqcn,
333 &resolved.name,
334 ctx,
335 span,
336 );
337
338 let arg_names: Vec<Option<String>> = call
339 .args
340 .iter()
341 .map(|a| a.name.as_ref().map(|n| n.to_string_repr().into_owned()))
342 .collect();
343 let arg_can_be_byref: Vec<bool> = call
344 .args
345 .iter()
346 .map(|a| expr_can_be_passed_by_reference(&a.value))
347 .collect();
348 let class_tps = crate::db::class_template_params_via_db(ea.db, fqcn)
351 .map(|tps| tps.to_vec())
352 .unwrap_or_default();
353 let mut bindings = build_class_bindings(&class_tps, receiver_type_params);
354 for (k, v) in crate::db::inherited_template_bindings_via_db(ea.db, fqcn) {
355 bindings.entry(k).or_insert(v);
356 }
357
358 let substituted_params: Vec<FnParam>;
360 let effective_params: &[FnParam] = if bindings.is_empty() {
361 &resolved.params
362 } else {
363 substituted_params = resolved
364 .params
365 .iter()
366 .map(|p| FnParam {
367 ty: mir_codebase::wrap_param_type(
368 p.ty.as_ref().map(|t| t.substitute_templates(&bindings)),
369 ),
370 ..p.clone()
371 })
372 .collect();
373 &substituted_params
374 };
375
376 check_args(
377 ea,
378 CheckArgsParams {
379 fn_name: method_name,
380 params: effective_params,
381 arg_types,
382 arg_spans,
383 arg_names: &arg_names,
384 arg_can_be_byref: &arg_can_be_byref,
385 call_span: span,
386 has_spread: call.args.iter().any(|a| a.unpack),
387 },
388 );
389
390 let ret_raw = substitute_static_in_return(resolved.return_ty_raw, fqcn);
391
392 if !resolved.template_params.is_empty() {
393 let method_bindings =
394 infer_template_bindings(&resolved.template_params, &resolved.params, arg_types);
395 for key in method_bindings.keys() {
396 if bindings.contains_key(key) {
397 ea.emit(
398 IssueKind::ShadowedTemplateParam {
399 name: key.to_string(),
400 },
401 Severity::Info,
402 span,
403 );
404 }
405 }
406 bindings.extend(method_bindings);
407 for (name, inferred, bound) in
408 check_template_bounds(&bindings, &resolved.template_params)
409 {
410 ea.emit(
411 IssueKind::InvalidTemplateParam {
412 name: name.to_string(),
413 expected_bound: format!("{bound}"),
414 actual: format!("{inferred}"),
415 },
416 Severity::Error,
417 span,
418 );
419 }
420 }
421
422 for callee_throw in resolved.throws.iter() {
424 if !ctx.fn_declared_throws.iter().any(|declared| {
425 declared.as_ref() == callee_throw.as_ref()
426 || crate::db::extends_or_implements_via_db(
427 ea.db,
428 callee_throw.as_ref(),
429 declared.as_ref(),
430 )
431 }) {
432 ea.emit(
433 IssueKind::MissingThrowsDocblock {
434 class: callee_throw.to_string(),
435 },
436 Severity::Info,
437 span,
438 );
439 }
440 }
441
442 if !bindings.is_empty() {
443 ret_raw.substitute_templates(&bindings)
444 } else {
445 ret_raw
446 }
447 } else if crate::db::type_exists_via_db(ea.db, fqcn)
448 && !crate::db::has_unknown_ancestor_via_db(ea.db, fqcn)
449 {
450 let (is_interface, is_abstract) = crate::db::class_kind_via_db(ea.db, fqcn)
451 .map(|k| (k.is_interface, k.is_abstract))
452 .unwrap_or((false, false));
453 let has_call_magic = crate::db::lookup_method_in_chain(ea.db, fqcn, "__call").is_some();
455 if is_interface || is_abstract || has_call_magic {
456 Union::mixed()
457 } else {
458 ea.emit(
459 IssueKind::UndefinedMethod {
460 class: fqcn.to_string(),
461 method: method_name.to_string(),
462 },
463 Severity::Error,
464 span,
465 );
466 Union::mixed()
467 }
468 } else {
469 Union::mixed()
470 }
471}