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