1use std::sync::Arc;
2
3use php_ast::ast::{ExprKind, MethodCallExpr};
4use php_ast::Span;
5
6use mir_issues::{IssueKind, Severity};
7use mir_types::{Atomic, Union};
8
9use crate::context::Context;
10use crate::expr::ExpressionAnalyzer;
11use crate::generic::{build_class_bindings, check_template_bounds, infer_template_bindings};
12use crate::symbol::SymbolKind;
13
14use super::args::{
15 check_args, check_method_visibility, expr_can_be_passed_by_reference, spread_element_type,
16 substitute_static_in_return, CheckArgsParams,
17};
18use super::CallAnalyzer;
19
20impl CallAnalyzer {
21 pub fn analyze_method_call<'a, 'arena, 'src>(
22 ea: &mut ExpressionAnalyzer<'a>,
23 call: &MethodCallExpr<'arena, 'src>,
24 ctx: &mut Context,
25 span: Span,
26 nullsafe: bool,
27 ) -> Union {
28 let obj_ty = ea.analyze(call.object, ctx);
29
30 let method_name = match &call.method.kind {
31 ExprKind::Identifier(name) => name.as_str(),
32 _ => return Union::mixed(),
33 };
34
35 let arg_types: Vec<Union> = call
39 .args
40 .iter()
41 .map(|arg| {
42 let ty = ea.analyze(&arg.value, ctx);
43 if arg.unpack {
44 spread_element_type(&ty)
45 } else {
46 ty
47 }
48 })
49 .collect();
50
51 let arg_spans: Vec<Span> = call.args.iter().map(|a| a.span).collect();
52
53 if obj_ty.contains(|t| matches!(t, Atomic::TNull)) {
54 if nullsafe {
55 } else if obj_ty.is_single() {
57 ea.emit(
58 IssueKind::NullMethodCall {
59 method: method_name.to_string(),
60 },
61 Severity::Error,
62 span,
63 );
64 return Union::mixed();
65 } else {
66 ea.emit(
67 IssueKind::PossiblyNullMethodCall {
68 method: method_name.to_string(),
69 },
70 Severity::Info,
71 span,
72 );
73 }
74 }
75
76 if obj_ty.is_mixed() {
77 ea.emit(
78 IssueKind::MixedMethodCall {
79 method: method_name.to_string(),
80 },
81 Severity::Info,
82 span,
83 );
84 return Union::mixed();
85 }
86
87 let receiver = obj_ty.remove_null();
88 let mut result = Union::empty();
89
90 for atomic in &receiver.types {
91 match atomic {
92 Atomic::TNamedObject {
93 fqcn,
94 type_params: receiver_type_params,
95 } => {
96 let fqcn_resolved = ea.codebase.resolve_class_name(&ea.file, fqcn);
97 let fqcn = &std::sync::Arc::from(fqcn_resolved.as_str());
98 result = Union::merge(
99 &result,
100 &resolve_method_return(
101 ea,
102 ctx,
103 call,
104 span,
105 method_name,
106 fqcn,
107 receiver_type_params.as_slice(),
108 &arg_types,
109 &arg_spans,
110 ),
111 );
112 }
113 Atomic::TSelf { fqcn }
114 | Atomic::TStaticObject { fqcn }
115 | Atomic::TParent { fqcn } => {
116 let fqcn_resolved = ea.codebase.resolve_class_name(&ea.file, fqcn);
117 let fqcn = &std::sync::Arc::from(fqcn_resolved.as_str());
118 result = Union::merge(
119 &result,
120 &resolve_method_return(
121 ea,
122 ctx,
123 call,
124 span,
125 method_name,
126 fqcn,
127 &[],
128 &arg_types,
129 &arg_spans,
130 ),
131 );
132 }
133 Atomic::TIntersection { parts } => {
134 let mut intersection_result = Union::empty();
135 let mut found_method = false;
136 for part in parts {
137 for inner_atomic in &part.types {
138 if let Atomic::TNamedObject {
139 fqcn,
140 type_params: receiver_type_params,
141 } = inner_atomic
142 {
143 let fqcn_resolved = ea.codebase.resolve_class_name(&ea.file, fqcn);
144 let resolved_arc = Arc::from(fqcn_resolved.as_str());
145 if ea.codebase.get_method(&resolved_arc, method_name).is_some() {
146 found_method = true;
147 intersection_result = Union::merge(
148 &intersection_result,
149 &resolve_method_return(
150 ea,
151 ctx,
152 call,
153 span,
154 method_name,
155 &resolved_arc,
156 receiver_type_params.as_slice(),
157 &arg_types,
158 &arg_spans,
159 ),
160 );
161 }
162 }
163 }
164 }
165 if found_method {
166 result = Union::merge(&result, &intersection_result);
167 } else {
168 result = Union::merge(&result, &Union::mixed());
169 }
170 }
171 Atomic::TObject | Atomic::TTemplateParam { .. } => {
172 result = Union::merge(&result, &Union::mixed());
173 }
174 _ => {
175 result = Union::merge(&result, &Union::mixed());
176 }
177 }
178 }
179
180 if nullsafe && obj_ty.is_nullable() {
181 result.add_type(Atomic::TNull);
182 }
183
184 let final_ty = if result.is_empty() {
185 Union::mixed()
186 } else {
187 result
188 };
189
190 for atomic in &obj_ty.types {
191 if let Atomic::TNamedObject { fqcn, .. } = atomic {
192 ea.record_symbol(
193 call.method.span,
194 SymbolKind::MethodCall {
195 class: fqcn.clone(),
196 method: Arc::from(method_name),
197 },
198 final_ty.clone(),
199 );
200 break;
201 }
202 }
203 final_ty
204 }
205}
206
207#[allow(clippy::too_many_arguments)]
210fn resolve_method_return<'a, 'arena, 'src>(
211 ea: &mut ExpressionAnalyzer<'a>,
212 ctx: &Context,
213 call: &MethodCallExpr<'arena, 'src>,
214 span: Span,
215 method_name: &str,
216 fqcn: &Arc<str>,
217 receiver_type_params: &[Union],
218 arg_types: &[Union],
219 arg_spans: &[Span],
220) -> Union {
221 if let Some(method) = ea.codebase.get_method(fqcn, method_name) {
222 let (line, col_start, col_end) = ea.span_to_ref_loc(call.method.span);
223 ea.codebase.mark_method_referenced_at(
224 fqcn,
225 method_name,
226 ea.file.clone(),
227 line,
228 col_start,
229 col_end,
230 );
231 if let Some(msg) = method.deprecated.clone() {
232 ea.emit(
233 IssueKind::DeprecatedMethodCall {
234 class: fqcn.to_string(),
235 method: method_name.to_string(),
236 message: Some(msg).filter(|m| !m.is_empty()),
237 },
238 Severity::Info,
239 span,
240 );
241 }
242 check_method_visibility(ea, &method, ctx, span);
243
244 let arg_names: Vec<Option<String>> = call
245 .args
246 .iter()
247 .map(|a| a.name.as_ref().map(|n| n.to_string_repr().into_owned()))
248 .collect();
249 let arg_can_be_byref: Vec<bool> = call
250 .args
251 .iter()
252 .map(|a| expr_can_be_passed_by_reference(&a.value))
253 .collect();
254 check_args(
255 ea,
256 CheckArgsParams {
257 fn_name: method_name,
258 params: &method.params,
259 arg_types,
260 arg_spans,
261 arg_names: &arg_names,
262 arg_can_be_byref: &arg_can_be_byref,
263 call_span: span,
264 has_spread: call.args.iter().any(|a| a.unpack),
265 },
266 );
267
268 let ret_raw = method
269 .effective_return_type()
270 .cloned()
271 .unwrap_or_else(Union::mixed);
272 let ret_raw = substitute_static_in_return(ret_raw, fqcn);
273
274 let class_tps = ea.codebase.get_class_template_params(fqcn);
275 let mut bindings = build_class_bindings(&class_tps, receiver_type_params);
276 for (k, v) in ea.codebase.get_inherited_template_bindings(fqcn) {
277 bindings.entry(k).or_insert(v);
278 }
279
280 if !method.template_params.is_empty() {
281 let method_bindings =
282 infer_template_bindings(&method.template_params, &method.params, arg_types);
283 for key in method_bindings.keys() {
284 if bindings.contains_key(key) {
285 ea.emit(
286 IssueKind::ShadowedTemplateParam {
287 name: key.to_string(),
288 },
289 Severity::Info,
290 span,
291 );
292 }
293 }
294 bindings.extend(method_bindings);
295 for (name, inferred, bound) in check_template_bounds(&bindings, &method.template_params)
296 {
297 ea.emit(
298 IssueKind::InvalidTemplateParam {
299 name: name.to_string(),
300 expected_bound: format!("{bound}"),
301 actual: format!("{inferred}"),
302 },
303 Severity::Error,
304 span,
305 );
306 }
307 }
308
309 if !bindings.is_empty() {
310 ret_raw.substitute_templates(&bindings)
311 } else {
312 ret_raw
313 }
314 } else if ea.codebase.type_exists(fqcn) && !ea.codebase.has_unknown_ancestor(fqcn) {
315 let is_interface = ea.codebase.interfaces.contains_key(fqcn.as_ref());
316 let is_abstract = ea.codebase.is_abstract_class(fqcn.as_ref());
317 if is_interface || is_abstract || ea.codebase.get_method(fqcn, "__call").is_some() {
318 Union::mixed()
319 } else {
320 ea.emit(
321 IssueKind::UndefinedMethod {
322 class: fqcn.to_string(),
323 method: method_name.to_string(),
324 },
325 Severity::Error,
326 span,
327 );
328 Union::mixed()
329 }
330 } else {
331 Union::mixed()
332 }
333}