Skip to main content

mir_analyzer/call/
function.rs

1use php_ast::ast::{ExprKind, FunctionCallExpr};
2use php_ast::Span;
3
4use mir_codebase::storage::AssertionKind;
5use mir_issues::{IssueKind, Severity};
6use mir_types::{Atomic, Union};
7
8use crate::context::Context;
9use crate::expr::ExpressionAnalyzer;
10use crate::generic::{check_template_bounds, infer_template_bindings};
11use crate::symbol::SymbolKind;
12use crate::taint::{classify_sink, is_expr_tainted, SinkKind};
13
14use super::args::{
15    check_args, expr_can_be_passed_by_reference, spread_element_type, CheckArgsParams,
16};
17use super::CallAnalyzer;
18
19impl CallAnalyzer {
20    pub fn analyze_function_call<'a, 'arena, 'src>(
21        ea: &mut ExpressionAnalyzer<'a>,
22        call: &FunctionCallExpr<'arena, 'src>,
23        ctx: &mut Context,
24        span: Span,
25    ) -> Union {
26        let fn_name = match &call.name.kind {
27            ExprKind::Identifier(name) => (*name).to_string(),
28            _ => {
29                let callee_ty = ea.analyze(call.name, ctx);
30                for arg in call.args.iter() {
31                    ea.analyze(&arg.value, ctx);
32                }
33                for atomic in &callee_ty.types {
34                    match atomic {
35                        Atomic::TClosure { return_type, .. } => return *return_type.clone(),
36                        Atomic::TCallable {
37                            return_type: Some(rt),
38                            ..
39                        } => return *rt.clone(),
40                        _ => {}
41                    }
42                }
43                return Union::mixed();
44            }
45        };
46
47        // Taint sink check (M19): before evaluating args so we can inspect raw exprs
48        if let Some(sink_kind) = classify_sink(&fn_name) {
49            for arg in call.args.iter() {
50                if is_expr_tainted(&arg.value, ctx) {
51                    let issue_kind = match sink_kind {
52                        SinkKind::Html => IssueKind::TaintedHtml,
53                        SinkKind::Sql => IssueKind::TaintedSql,
54                        SinkKind::Shell => IssueKind::TaintedShell,
55                    };
56                    ea.emit(issue_kind, Severity::Error, span);
57                    break;
58                }
59            }
60        }
61
62        // PHP resolves `foo()` as `\App\Ns\foo` first, then `\foo` if not found.
63        // A leading `\` means explicit global namespace.
64        let fn_name = fn_name
65            .strip_prefix('\\')
66            .map(|s: &str| s.to_string())
67            .unwrap_or(fn_name);
68        let resolved_fn_name: String = {
69            let qualified = ea.codebase.resolve_class_name(&ea.file, &fn_name);
70            if ea.codebase.functions.contains_key(qualified.as_str()) {
71                qualified
72            } else if ea.codebase.functions.contains_key(fn_name.as_str()) {
73                fn_name.clone()
74            } else {
75                qualified
76            }
77        };
78
79        // Pre-mark by-reference parameter variables as defined BEFORE evaluating args
80        if let Some(func) = ea.codebase.functions.get(resolved_fn_name.as_str()) {
81            for (i, param) in func.params.iter().enumerate() {
82                if param.is_byref {
83                    if param.is_variadic {
84                        for arg in call.args.iter().skip(i) {
85                            if let ExprKind::Variable(name) = &arg.value.kind {
86                                let var_name = name.as_str().trim_start_matches('$');
87                                if !ctx.var_is_defined(var_name) {
88                                    ctx.set_var(var_name, Union::mixed());
89                                }
90                            }
91                        }
92                    } else if let Some(arg) = call.args.get(i) {
93                        if let ExprKind::Variable(name) = &arg.value.kind {
94                            let var_name = name.as_str().trim_start_matches('$');
95                            if !ctx.var_is_defined(var_name) {
96                                ctx.set_var(var_name, Union::mixed());
97                            }
98                        }
99                    }
100                }
101            }
102        }
103
104        let arg_types: Vec<Union> = call
105            .args
106            .iter()
107            .map(|arg| {
108                let ty = ea.analyze(&arg.value, ctx);
109                if arg.unpack {
110                    spread_element_type(&ty)
111                } else {
112                    ty
113                }
114            })
115            .collect();
116
117        // When call_user_func / call_user_func_array is called with a bare string
118        // literal as the callable argument, treat that string as a direct FQN
119        // reference so the named function is not flagged as dead code.
120        // Note: 'helper' always resolves to \helper (global) — no namespace
121        // fallback applies to runtime callable strings.
122        if matches!(
123            resolved_fn_name.as_str(),
124            "call_user_func" | "call_user_func_array"
125        ) {
126            if let Some(arg) = call.args.first() {
127                if let ExprKind::String(name) = &arg.value.kind {
128                    let fqn = name.strip_prefix('\\').unwrap_or(name);
129                    if let Some(func) = ea.codebase.functions.get(fqn) {
130                        if !ea.inference_only {
131                            let (line, col_start, col_end) = ea.span_to_ref_loc(arg.span);
132                            ea.codebase.mark_function_referenced_at(
133                                &func.fqn,
134                                ea.file.clone(),
135                                line,
136                                col_start,
137                                col_end,
138                            );
139                        }
140                    }
141                }
142            }
143        }
144
145        if let Some(func) = ea.codebase.functions.get(resolved_fn_name.as_str()) {
146            if !ea.inference_only {
147                let (line, col_start, col_end) = ea.span_to_ref_loc(call.name.span);
148                ea.codebase.mark_function_referenced_at(
149                    &func.fqn,
150                    ea.file.clone(),
151                    line,
152                    col_start,
153                    col_end,
154                );
155            }
156            let deprecated = func.deprecated.clone();
157            let params = func.params.clone();
158            let template_params = func.template_params.clone();
159            let return_ty_raw = func
160                .effective_return_type()
161                .cloned()
162                .unwrap_or_else(Union::mixed);
163
164            if let Some(msg) = deprecated {
165                ea.emit(
166                    IssueKind::DeprecatedCall {
167                        name: resolved_fn_name.clone(),
168                        message: Some(msg).filter(|m| !m.is_empty()),
169                    },
170                    Severity::Info,
171                    span,
172                );
173            }
174
175            check_args(
176                ea,
177                CheckArgsParams {
178                    fn_name: &fn_name,
179                    params: &params,
180                    arg_types: &arg_types,
181                    arg_spans: &call.args.iter().map(|a| a.span).collect::<Vec<_>>(),
182                    arg_names: &call
183                        .args
184                        .iter()
185                        .map(|a| a.name.as_ref().map(|n| n.to_string_repr().into_owned()))
186                        .collect::<Vec<_>>(),
187                    arg_can_be_byref: &call
188                        .args
189                        .iter()
190                        .map(|a| expr_can_be_passed_by_reference(&a.value))
191                        .collect::<Vec<_>>(),
192                    call_span: span,
193                    has_spread: call.args.iter().any(|a| a.unpack),
194                },
195            );
196
197            for (i, param) in params.iter().enumerate() {
198                if param.is_byref {
199                    if param.is_variadic {
200                        for arg in call.args.iter().skip(i) {
201                            if let ExprKind::Variable(name) = &arg.value.kind {
202                                let var_name = name.as_str().trim_start_matches('$');
203                                ctx.set_var(var_name, Union::mixed());
204                            }
205                        }
206                    } else if let Some(arg) = call.args.get(i) {
207                        if let ExprKind::Variable(name) = &arg.value.kind {
208                            let var_name = name.as_str().trim_start_matches('$');
209                            ctx.set_var(var_name, Union::mixed());
210                        }
211                    }
212                }
213            }
214
215            for assertion in func
216                .assertions
217                .iter()
218                .filter(|a| a.kind == AssertionKind::Assert)
219            {
220                if let Some(index) = params.iter().position(|p| p.name == assertion.param) {
221                    if let Some(arg) = call.args.get(index) {
222                        if let ExprKind::Variable(name) = &arg.value.kind {
223                            ctx.set_var(
224                                name.as_str().trim_start_matches('$'),
225                                assertion.ty.clone(),
226                            );
227                        }
228                    }
229                }
230            }
231
232            let return_ty = if !template_params.is_empty() {
233                let bindings = infer_template_bindings(&template_params, &params, &arg_types);
234                for (name, inferred, bound) in check_template_bounds(&bindings, &template_params) {
235                    ea.emit(
236                        IssueKind::InvalidTemplateParam {
237                            name: name.to_string(),
238                            expected_bound: format!("{bound}"),
239                            actual: format!("{inferred}"),
240                        },
241                        Severity::Error,
242                        span,
243                    );
244                }
245                return_ty_raw.substitute_templates(&bindings)
246            } else {
247                return_ty_raw
248            };
249
250            ea.record_symbol(
251                call.name.span,
252                SymbolKind::FunctionCall(func.fqn.clone()),
253                return_ty.clone(),
254            );
255            return return_ty;
256        }
257
258        ea.emit(
259            IssueKind::UndefinedFunction { name: fn_name },
260            Severity::Error,
261            span,
262        );
263        Union::mixed()
264    }
265}