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::TObject | Atomic::TTemplateParam { .. } => {
134                    result = Union::merge(&result, &Union::mixed());
135                }
136                _ => {
137                    result = Union::merge(&result, &Union::mixed());
138                }
139            }
140        }
141
142        if nullsafe && obj_ty.is_nullable() {
143            result.add_type(Atomic::TNull);
144        }
145
146        let final_ty = if result.is_empty() {
147            Union::mixed()
148        } else {
149            result
150        };
151
152        for atomic in &obj_ty.types {
153            if let Atomic::TNamedObject { fqcn, .. } = atomic {
154                ea.record_symbol(
155                    call.method.span,
156                    SymbolKind::MethodCall {
157                        class: fqcn.clone(),
158                        method: Arc::from(method_name),
159                    },
160                    final_ty.clone(),
161                );
162                break;
163            }
164        }
165        final_ty
166    }
167}
168
169/// Resolves method return type for a known receiver FQCN, shared between the
170/// `TNamedObject` and `TSelf`/`TStaticObject`/`TParent` branches.
171#[allow(clippy::too_many_arguments)]
172fn resolve_method_return<'a, 'arena, 'src>(
173    ea: &mut ExpressionAnalyzer<'a>,
174    ctx: &Context,
175    call: &MethodCallExpr<'arena, 'src>,
176    span: Span,
177    method_name: &str,
178    fqcn: &Arc<str>,
179    receiver_type_params: &[Union],
180    arg_types: &[Union],
181    arg_spans: &[Span],
182) -> Union {
183    if let Some(method) = ea.codebase.get_method(fqcn, method_name) {
184        ea.codebase.mark_method_referenced_at(
185            fqcn,
186            method_name,
187            ea.file.clone(),
188            call.method.span.start,
189            call.method.span.end,
190        );
191        if let Some(msg) = method.deprecated.clone() {
192            ea.emit(
193                IssueKind::DeprecatedMethodCall {
194                    class: fqcn.to_string(),
195                    method: method_name.to_string(),
196                    message: Some(msg).filter(|m| !m.is_empty()),
197                },
198                Severity::Info,
199                span,
200            );
201        }
202        check_method_visibility(ea, &method, ctx, span);
203
204        let arg_names: Vec<Option<String>> = call
205            .args
206            .iter()
207            .map(|a| a.name.as_ref().map(|n| n.to_string_repr().into_owned()))
208            .collect();
209        let arg_can_be_byref: Vec<bool> = call
210            .args
211            .iter()
212            .map(|a| expr_can_be_passed_by_reference(&a.value))
213            .collect();
214        check_args(
215            ea,
216            CheckArgsParams {
217                fn_name: method_name,
218                params: &method.params,
219                arg_types,
220                arg_spans,
221                arg_names: &arg_names,
222                arg_can_be_byref: &arg_can_be_byref,
223                call_span: span,
224                has_spread: call.args.iter().any(|a| a.unpack),
225            },
226        );
227
228        let ret_raw = method
229            .effective_return_type()
230            .cloned()
231            .unwrap_or_else(Union::mixed);
232        let ret_raw = substitute_static_in_return(ret_raw, fqcn);
233
234        let class_tps = ea.codebase.get_class_template_params(fqcn);
235        let mut bindings = build_class_bindings(&class_tps, receiver_type_params);
236        for (k, v) in ea.codebase.get_inherited_template_bindings(fqcn) {
237            bindings.entry(k).or_insert(v);
238        }
239
240        if !method.template_params.is_empty() {
241            let method_bindings =
242                infer_template_bindings(&method.template_params, &method.params, arg_types);
243            for key in method_bindings.keys() {
244                if bindings.contains_key(key) {
245                    ea.emit(
246                        IssueKind::ShadowedTemplateParam {
247                            name: key.to_string(),
248                        },
249                        Severity::Info,
250                        span,
251                    );
252                }
253            }
254            bindings.extend(method_bindings);
255            for (name, inferred, bound) in check_template_bounds(&bindings, &method.template_params)
256            {
257                ea.emit(
258                    IssueKind::InvalidTemplateParam {
259                        name: name.to_string(),
260                        expected_bound: format!("{bound}"),
261                        actual: format!("{inferred}"),
262                    },
263                    Severity::Error,
264                    span,
265                );
266            }
267        }
268
269        if !bindings.is_empty() {
270            ret_raw.substitute_templates(&bindings)
271        } else {
272            ret_raw
273        }
274    } else if ea.codebase.type_exists(fqcn) && !ea.codebase.has_unknown_ancestor(fqcn) {
275        let is_interface = ea.codebase.interfaces.contains_key(fqcn.as_ref());
276        let is_abstract = ea.codebase.is_abstract_class(fqcn.as_ref());
277        if is_interface || is_abstract || ea.codebase.get_method(fqcn, "__call").is_some() {
278            Union::mixed()
279        } else {
280            ea.emit(
281                IssueKind::UndefinedMethod {
282                    class: fqcn.to_string(),
283                    method: method_name.to_string(),
284                },
285                Severity::Error,
286                span,
287            );
288            Union::mixed()
289        }
290    } else {
291        Union::mixed()
292    }
293}