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, spread_element_type, substitute_static_in_return,
16    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        check_args(
210            ea,
211            CheckArgsParams {
212                fn_name: method_name,
213                params: &method.params,
214                arg_types,
215                arg_spans,
216                arg_names: &arg_names,
217                call_span: span,
218                has_spread: call.args.iter().any(|a| a.unpack),
219            },
220        );
221
222        let ret_raw = method
223            .effective_return_type()
224            .cloned()
225            .unwrap_or_else(Union::mixed);
226        let ret_raw = substitute_static_in_return(ret_raw, fqcn);
227
228        let class_tps = ea.codebase.get_class_template_params(fqcn);
229        let mut bindings = build_class_bindings(&class_tps, receiver_type_params);
230        for (k, v) in ea.codebase.get_inherited_template_bindings(fqcn) {
231            bindings.entry(k).or_insert(v);
232        }
233
234        if !method.template_params.is_empty() {
235            let method_bindings =
236                infer_template_bindings(&method.template_params, &method.params, arg_types);
237            for key in method_bindings.keys() {
238                if bindings.contains_key(key) {
239                    ea.emit(
240                        IssueKind::ShadowedTemplateParam {
241                            name: key.to_string(),
242                        },
243                        Severity::Info,
244                        span,
245                    );
246                }
247            }
248            bindings.extend(method_bindings);
249            for (name, inferred, bound) in check_template_bounds(&bindings, &method.template_params)
250            {
251                ea.emit(
252                    IssueKind::InvalidTemplateParam {
253                        name: name.to_string(),
254                        expected_bound: format!("{}", bound),
255                        actual: format!("{}", inferred),
256                    },
257                    Severity::Error,
258                    span,
259                );
260            }
261        }
262
263        if !bindings.is_empty() {
264            ret_raw.substitute_templates(&bindings)
265        } else {
266            ret_raw
267        }
268    } else if ea.codebase.type_exists(fqcn) && !ea.codebase.has_unknown_ancestor(fqcn) {
269        let is_interface = ea.codebase.interfaces.contains_key(fqcn.as_ref());
270        let is_abstract = ea.codebase.is_abstract_class(fqcn.as_ref());
271        if is_interface || is_abstract || ea.codebase.get_method(fqcn, "__call").is_some() {
272            Union::mixed()
273        } else {
274            ea.emit(
275                IssueKind::UndefinedMethod {
276                    class: fqcn.to_string(),
277                    method: method_name.to_string(),
278                },
279                Severity::Error,
280                span,
281            );
282            Union::mixed()
283        }
284    } else {
285        Union::mixed()
286    }
287}