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