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        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}