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_codebase::storage::{FnParam, TemplateParam, Visibility};
7use mir_issues::{IssueKind, Severity};
8use mir_types::Union;
9
10use crate::context::Context;
11use crate::expr::ExpressionAnalyzer;
12use crate::generic::{build_class_bindings, check_template_bounds, infer_template_bindings};
13use crate::symbol::SymbolKind;
14
15use super::args::{
16    check_args, check_method_visibility, expr_can_be_passed_by_reference, spread_element_type,
17    substitute_static_in_return, CheckArgsParams,
18};
19use super::CallAnalyzer;
20
21pub(super) struct ResolvedMethod {
22    pub(super) owner_fqcn: Arc<str>,
23    pub(super) name: Arc<str>,
24    pub(super) visibility: Visibility,
25    pub(super) deprecated: Option<Arc<str>>,
26    pub(super) params: Vec<FnParam>,
27    pub(super) template_params: Vec<TemplateParam>,
28    pub(super) return_ty_raw: Union,
29}
30
31/// Resolve a method via the Salsa db, walking the class ancestor chain.
32pub(super) fn resolve_method_from_db(
33    ea: &ExpressionAnalyzer<'_>,
34    fqcn: &Arc<str>,
35    method_name_lower: &str,
36) -> Option<ResolvedMethod> {
37    let db = ea.db;
38
39    // Walk own → mixins → traits → ancestors via the canonical chain helper.
40    let node = crate::db::lookup_method_in_chain(db, fqcn, method_name_lower)?;
41    let owner_fqcn = node.fqcn(db);
42    let name = node.name(db);
43
44    // `inferred_return_type` is published on `MethodNode` by the priming
45    // sweep's serial commit phase; see `MirDb::commit_inferred_return_types`.
46    // Every analyzer entry path runs a priming sweep + commit before the
47    // issue-emitting pass, so the read-side codebase fallback is gone.
48    let inferred = node.inferred_return_type(db);
49    let return_ty_raw = node
50        .return_type(db)
51        .or(inferred)
52        .map(|t| (*t).clone())
53        .unwrap_or_else(Union::mixed);
54
55    Some(ResolvedMethod {
56        owner_fqcn,
57        name,
58        visibility: node.visibility(db),
59        deprecated: node.deprecated(db),
60        params: node.params(db).to_vec(),
61        template_params: node.template_params(db).to_vec(),
62        return_ty_raw,
63    })
64}
65
66impl CallAnalyzer {
67    pub fn analyze_method_call<'a, 'arena, 'src>(
68        ea: &mut ExpressionAnalyzer<'a>,
69        call: &MethodCallExpr<'arena, 'src>,
70        ctx: &mut Context,
71        span: Span,
72        nullsafe: bool,
73    ) -> Union {
74        let obj_ty = ea.analyze(call.object, ctx);
75
76        let method_name = match &call.method.kind {
77            ExprKind::Identifier(name) => name.as_str(),
78            _ => return Union::mixed(),
79        };
80
81        // Always analyze arguments — even when the receiver is null/mixed and we
82        // return early — so that variable reads inside args are tracked and side
83        // effects (taint, etc.) are recorded.
84        let arg_types: Vec<Union> = call
85            .args
86            .iter()
87            .map(|arg| {
88                let ty = ea.analyze(&arg.value, ctx);
89                if arg.unpack {
90                    spread_element_type(&ty)
91                } else {
92                    ty
93                }
94            })
95            .collect();
96
97        let arg_spans: Vec<Span> = call.args.iter().map(|a| a.span).collect();
98
99        if obj_ty.contains(|t| matches!(t, mir_types::Atomic::TNull)) {
100            if nullsafe {
101                // ?-> is fine, just returns null on null receiver
102            } else if obj_ty.is_single() {
103                ea.emit(
104                    IssueKind::NullMethodCall {
105                        method: method_name.to_string(),
106                    },
107                    Severity::Error,
108                    span,
109                );
110                return Union::mixed();
111            } else {
112                ea.emit(
113                    IssueKind::PossiblyNullMethodCall {
114                        method: method_name.to_string(),
115                    },
116                    Severity::Info,
117                    span,
118                );
119            }
120        }
121
122        if obj_ty.is_mixed() {
123            ea.emit(
124                IssueKind::MixedMethodCall {
125                    method: method_name.to_string(),
126                },
127                Severity::Info,
128                span,
129            );
130            return Union::mixed();
131        }
132
133        let receiver = obj_ty.remove_null();
134        let mut result = Union::empty();
135
136        for atomic in &receiver.types {
137            match atomic {
138                mir_types::Atomic::TNamedObject {
139                    fqcn,
140                    type_params: receiver_type_params,
141                } => {
142                    let fqcn_resolved = crate::db::resolve_name_via_db(ea.db, &ea.file, fqcn);
143                    let fqcn = &std::sync::Arc::from(fqcn_resolved.as_str());
144                    result = Union::merge(
145                        &result,
146                        &resolve_method_return(
147                            ea,
148                            ctx,
149                            call,
150                            span,
151                            method_name,
152                            fqcn,
153                            receiver_type_params.as_slice(),
154                            &arg_types,
155                            &arg_spans,
156                        ),
157                    );
158                }
159                mir_types::Atomic::TSelf { fqcn }
160                | mir_types::Atomic::TStaticObject { fqcn }
161                | mir_types::Atomic::TParent { fqcn } => {
162                    let fqcn_resolved = crate::db::resolve_name_via_db(ea.db, &ea.file, fqcn);
163                    let fqcn = &std::sync::Arc::from(fqcn_resolved.as_str());
164                    result = Union::merge(
165                        &result,
166                        &resolve_method_return(
167                            ea,
168                            ctx,
169                            call,
170                            span,
171                            method_name,
172                            fqcn,
173                            &[],
174                            &arg_types,
175                            &arg_spans,
176                        ),
177                    );
178                }
179                mir_types::Atomic::TIntersection { parts } => {
180                    let mut intersection_result = Union::empty();
181                    let mut found_method = false;
182                    for part in parts {
183                        for inner_atomic in &part.types {
184                            if let mir_types::Atomic::TNamedObject {
185                                fqcn,
186                                type_params: receiver_type_params,
187                            } = inner_atomic
188                            {
189                                let fqcn_resolved =
190                                    crate::db::resolve_name_via_db(ea.db, &ea.file, fqcn);
191                                let resolved_arc = Arc::from(fqcn_resolved.as_str());
192                                if crate::db::method_exists_via_db(
193                                    ea.db,
194                                    &resolved_arc,
195                                    method_name,
196                                ) {
197                                    found_method = true;
198                                    intersection_result = Union::merge(
199                                        &intersection_result,
200                                        &resolve_method_return(
201                                            ea,
202                                            ctx,
203                                            call,
204                                            span,
205                                            method_name,
206                                            &resolved_arc,
207                                            receiver_type_params.as_slice(),
208                                            &arg_types,
209                                            &arg_spans,
210                                        ),
211                                    );
212                                }
213                            }
214                        }
215                    }
216                    if found_method {
217                        result = Union::merge(&result, &intersection_result);
218                    } else {
219                        result = Union::merge(&result, &Union::mixed());
220                    }
221                }
222                mir_types::Atomic::TObject | mir_types::Atomic::TTemplateParam { .. } => {
223                    result = Union::merge(&result, &Union::mixed());
224                }
225                _ => {
226                    result = Union::merge(&result, &Union::mixed());
227                }
228            }
229        }
230
231        if nullsafe && obj_ty.is_nullable() {
232            result.add_type(mir_types::Atomic::TNull);
233        }
234
235        let final_ty = if result.is_empty() {
236            Union::mixed()
237        } else {
238            result
239        };
240
241        for atomic in &obj_ty.types {
242            if let mir_types::Atomic::TNamedObject { fqcn, .. } = atomic {
243                ea.record_symbol(
244                    call.method.span,
245                    SymbolKind::MethodCall {
246                        class: fqcn.clone(),
247                        method: Arc::from(method_name),
248                    },
249                    final_ty.clone(),
250                );
251                break;
252            }
253        }
254        final_ty
255    }
256}
257
258/// Resolves method return type for a known receiver FQCN, shared between the
259/// `TNamedObject` and `TSelf`/`TStaticObject`/`TParent` branches.
260#[allow(clippy::too_many_arguments)]
261fn resolve_method_return<'a, 'arena, 'src>(
262    ea: &mut ExpressionAnalyzer<'a>,
263    ctx: &Context,
264    call: &MethodCallExpr<'arena, 'src>,
265    span: Span,
266    method_name: &str,
267    fqcn: &Arc<str>,
268    receiver_type_params: &[Union],
269    arg_types: &[Union],
270    arg_spans: &[Span],
271) -> Union {
272    let method_name_lower = method_name.to_lowercase();
273    let resolved = resolve_method_from_db(ea, fqcn, &method_name_lower);
274
275    if let Some(resolved) = resolved {
276        if !ea.inference_only {
277            let (line, col_start, col_end) = ea.span_to_ref_loc(call.method.span);
278            ea.db.record_reference_location(crate::db::RefLoc {
279                symbol_key: Arc::from(format!(
280                    "{}::{}",
281                    &resolved.owner_fqcn,
282                    resolved.name.to_lowercase()
283                )),
284                file: ea.file.clone(),
285                line,
286                col_start,
287                col_end,
288            });
289        }
290        if let Some(msg) = resolved.deprecated.clone() {
291            ea.emit(
292                IssueKind::DeprecatedMethodCall {
293                    class: fqcn.to_string(),
294                    method: method_name.to_string(),
295                    message: Some(msg).filter(|m| !m.is_empty()),
296                },
297                Severity::Info,
298                span,
299            );
300        }
301        check_method_visibility(
302            ea,
303            resolved.visibility,
304            &resolved.owner_fqcn,
305            &resolved.name,
306            ctx,
307            span,
308        );
309
310        let arg_names: Vec<Option<String>> = call
311            .args
312            .iter()
313            .map(|a| a.name.as_ref().map(|n| n.to_string_repr().into_owned()))
314            .collect();
315        let arg_can_be_byref: Vec<bool> = call
316            .args
317            .iter()
318            .map(|a| expr_can_be_passed_by_reference(&a.value))
319            .collect();
320        // Build class-level template bindings before arg-checking so we can substitute
321        // template params (e.g. T → int from Box<int>) into param types.
322        let class_tps = crate::db::class_template_params_via_db(ea.db, fqcn)
323            .map(|tps| tps.to_vec())
324            .unwrap_or_default();
325        let mut bindings = build_class_bindings(&class_tps, receiver_type_params);
326        for (k, v) in crate::db::inherited_template_bindings_via_db(ea.db, fqcn) {
327            bindings.entry(k).or_insert(v);
328        }
329
330        // Substitute class bindings into param types so argument checking resolves T → int etc.
331        let substituted_params: Vec<FnParam>;
332        let effective_params: &[FnParam] = if bindings.is_empty() {
333            &resolved.params
334        } else {
335            substituted_params = resolved
336                .params
337                .iter()
338                .map(|p| FnParam {
339                    ty: mir_codebase::wrap_param_type(
340                        p.ty.as_ref().map(|t| t.substitute_templates(&bindings)),
341                    ),
342                    ..p.clone()
343                })
344                .collect();
345            &substituted_params
346        };
347
348        check_args(
349            ea,
350            CheckArgsParams {
351                fn_name: method_name,
352                params: effective_params,
353                arg_types,
354                arg_spans,
355                arg_names: &arg_names,
356                arg_can_be_byref: &arg_can_be_byref,
357                call_span: span,
358                has_spread: call.args.iter().any(|a| a.unpack),
359            },
360        );
361
362        let ret_raw = substitute_static_in_return(resolved.return_ty_raw, fqcn);
363
364        if !resolved.template_params.is_empty() {
365            let method_bindings =
366                infer_template_bindings(&resolved.template_params, &resolved.params, arg_types);
367            for key in method_bindings.keys() {
368                if bindings.contains_key(key) {
369                    ea.emit(
370                        IssueKind::ShadowedTemplateParam {
371                            name: key.to_string(),
372                        },
373                        Severity::Info,
374                        span,
375                    );
376                }
377            }
378            bindings.extend(method_bindings);
379            for (name, inferred, bound) in
380                check_template_bounds(&bindings, &resolved.template_params)
381            {
382                ea.emit(
383                    IssueKind::InvalidTemplateParam {
384                        name: name.to_string(),
385                        expected_bound: format!("{bound}"),
386                        actual: format!("{inferred}"),
387                    },
388                    Severity::Error,
389                    span,
390                );
391            }
392        }
393
394        if !bindings.is_empty() {
395            ret_raw.substitute_templates(&bindings)
396        } else {
397            ret_raw
398        }
399    } else if crate::db::type_exists_via_db(ea.db, fqcn)
400        && !crate::db::has_unknown_ancestor_via_db(ea.db, fqcn)
401    {
402        let (is_interface, is_abstract) = crate::db::class_kind_via_db(ea.db, fqcn)
403            .map(|k| (k.is_interface, k.is_abstract))
404            .unwrap_or((false, false));
405        if is_interface || is_abstract || crate::db::method_exists_via_db(ea.db, fqcn, "__call") {
406            Union::mixed()
407        } else {
408            ea.emit(
409                IssueKind::UndefinedMethod {
410                    class: fqcn.to_string(),
411                    method: method_name.to_string(),
412                },
413                Severity::Error,
414                span,
415            );
416            Union::mixed()
417        }
418    } else {
419        Union::mixed()
420    }
421}