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 if !ea.inference_only {
223 let (line, col_start, col_end) = ea.span_to_ref_loc(call.method.span);
224 ea.codebase.mark_method_referenced_at(
225 fqcn,
226 method_name,
227 ea.file.clone(),
228 line,
229 col_start,
230 col_end,
231 );
232 }
233 if let Some(msg) = method.deprecated.clone() {
234 ea.emit(
235 IssueKind::DeprecatedMethodCall {
236 class: fqcn.to_string(),
237 method: method_name.to_string(),
238 message: Some(msg).filter(|m| !m.is_empty()),
239 },
240 Severity::Info,
241 span,
242 );
243 }
244 check_method_visibility(ea, &method, ctx, span);
245
246 let arg_names: Vec<Option<String>> = call
247 .args
248 .iter()
249 .map(|a| a.name.as_ref().map(|n| n.to_string_repr().into_owned()))
250 .collect();
251 let arg_can_be_byref: Vec<bool> = call
252 .args
253 .iter()
254 .map(|a| expr_can_be_passed_by_reference(&a.value))
255 .collect();
256 check_args(
257 ea,
258 CheckArgsParams {
259 fn_name: method_name,
260 params: &method.params,
261 arg_types,
262 arg_spans,
263 arg_names: &arg_names,
264 arg_can_be_byref: &arg_can_be_byref,
265 call_span: span,
266 has_spread: call.args.iter().any(|a| a.unpack),
267 },
268 );
269
270 let ret_raw = method
271 .effective_return_type()
272 .cloned()
273 .unwrap_or_else(Union::mixed);
274 let ret_raw = substitute_static_in_return(ret_raw, fqcn);
275
276 let class_tps = ea.codebase.get_class_template_params(fqcn);
277 let mut bindings = build_class_bindings(&class_tps, receiver_type_params);
278 for (k, v) in ea.codebase.get_inherited_template_bindings(fqcn) {
279 bindings.entry(k).or_insert(v);
280 }
281
282 if !method.template_params.is_empty() {
283 let method_bindings =
284 infer_template_bindings(&method.template_params, &method.params, arg_types);
285 for key in method_bindings.keys() {
286 if bindings.contains_key(key) {
287 ea.emit(
288 IssueKind::ShadowedTemplateParam {
289 name: key.to_string(),
290 },
291 Severity::Info,
292 span,
293 );
294 }
295 }
296 bindings.extend(method_bindings);
297 for (name, inferred, bound) in check_template_bounds(&bindings, &method.template_params)
298 {
299 ea.emit(
300 IssueKind::InvalidTemplateParam {
301 name: name.to_string(),
302 expected_bound: format!("{bound}"),
303 actual: format!("{inferred}"),
304 },
305 Severity::Error,
306 span,
307 );
308 }
309 }
310
311 if !bindings.is_empty() {
312 ret_raw.substitute_templates(&bindings)
313 } else {
314 ret_raw
315 }
316 } else if ea.codebase.type_exists(fqcn) && !ea.codebase.has_unknown_ancestor(fqcn) {
317 let is_interface = ea.codebase.interfaces.contains_key(fqcn.as_ref());
318 let is_abstract = ea.codebase.is_abstract_class(fqcn.as_ref());
319 if is_interface || is_abstract || ea.codebase.get_method(fqcn, "__call").is_some() {
320 Union::mixed()
321 } else {
322 ea.emit(
323 IssueKind::UndefinedMethod {
324 class: fqcn.to_string(),
325 method: method_name.to_string(),
326 },
327 Severity::Error,
328 span,
329 );
330 Union::mixed()
331 }
332 } else {
333 Union::mixed()
334 }
335}