Skip to main content

mir_analyzer/call/
method.rs

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        // Always analyze arguments — even when the receiver is null/mixed and we
36        // return early — so that variable reads inside args are tracked and side
37        // effects (taint, etc.) are recorded.
38        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                // ?-> is fine, just returns null on null receiver
56            } 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/// Resolves method return type for a known receiver FQCN, shared between the
208/// `TNamedObject` and `TSelf`/`TStaticObject`/`TParent` branches.
209#[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}