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                        let (line, col_start, col_end) = ea.span_to_ref_loc(arg.span);
131                        ea.codebase.mark_function_referenced_at(
132                            &func.fqn,
133                            ea.file.clone(),
134                            line,
135                            col_start,
136                            col_end,
137                        );
138                    }
139                }
140            }
141        }
142
143        if let Some(func) = ea.codebase.functions.get(resolved_fn_name.as_str()) {
144            let (line, col_start, col_end) = ea.span_to_ref_loc(call.name.span);
145            ea.codebase.mark_function_referenced_at(
146                &func.fqn,
147                ea.file.clone(),
148                line,
149                col_start,
150                col_end,
151            );
152            let deprecated = func.deprecated.clone();
153            let params = func.params.clone();
154            let template_params = func.template_params.clone();
155            let return_ty_raw = func
156                .effective_return_type()
157                .cloned()
158                .unwrap_or_else(Union::mixed);
159
160            if let Some(msg) = deprecated {
161                ea.emit(
162                    IssueKind::DeprecatedCall {
163                        name: resolved_fn_name.clone(),
164                        message: Some(msg).filter(|m| !m.is_empty()),
165                    },
166                    Severity::Info,
167                    span,
168                );
169            }
170
171            check_args(
172                ea,
173                CheckArgsParams {
174                    fn_name: &fn_name,
175                    params: &params,
176                    arg_types: &arg_types,
177                    arg_spans: &call.args.iter().map(|a| a.span).collect::<Vec<_>>(),
178                    arg_names: &call
179                        .args
180                        .iter()
181                        .map(|a| a.name.as_ref().map(|n| n.to_string_repr().into_owned()))
182                        .collect::<Vec<_>>(),
183                    arg_can_be_byref: &call
184                        .args
185                        .iter()
186                        .map(|a| expr_can_be_passed_by_reference(&a.value))
187                        .collect::<Vec<_>>(),
188                    call_span: span,
189                    has_spread: call.args.iter().any(|a| a.unpack),
190                },
191            );
192
193            for (i, param) in params.iter().enumerate() {
194                if param.is_byref {
195                    if param.is_variadic {
196                        for arg in call.args.iter().skip(i) {
197                            if let ExprKind::Variable(name) = &arg.value.kind {
198                                let var_name = name.as_str().trim_start_matches('$');
199                                ctx.set_var(var_name, Union::mixed());
200                            }
201                        }
202                    } else if let Some(arg) = call.args.get(i) {
203                        if let ExprKind::Variable(name) = &arg.value.kind {
204                            let var_name = name.as_str().trim_start_matches('$');
205                            ctx.set_var(var_name, Union::mixed());
206                        }
207                    }
208                }
209            }
210
211            for assertion in func
212                .assertions
213                .iter()
214                .filter(|a| a.kind == AssertionKind::Assert)
215            {
216                if let Some(index) = params.iter().position(|p| p.name == assertion.param) {
217                    if let Some(arg) = call.args.get(index) {
218                        if let ExprKind::Variable(name) = &arg.value.kind {
219                            ctx.set_var(
220                                name.as_str().trim_start_matches('$'),
221                                assertion.ty.clone(),
222                            );
223                        }
224                    }
225                }
226            }
227
228            let return_ty = if !template_params.is_empty() {
229                let bindings = infer_template_bindings(&template_params, &params, &arg_types);
230                for (name, inferred, bound) in check_template_bounds(&bindings, &template_params) {
231                    ea.emit(
232                        IssueKind::InvalidTemplateParam {
233                            name: name.to_string(),
234                            expected_bound: format!("{bound}"),
235                            actual: format!("{inferred}"),
236                        },
237                        Severity::Error,
238                        span,
239                    );
240                }
241                return_ty_raw.substitute_templates(&bindings)
242            } else {
243                return_ty_raw
244            };
245
246            ea.record_symbol(
247                call.name.span,
248                SymbolKind::FunctionCall(func.fqn.clone()),
249                return_ty.clone(),
250            );
251            return return_ty;
252        }
253
254        ea.emit(
255            IssueKind::UndefinedFunction { name: fn_name },
256            Severity::Error,
257            span,
258        );
259        Union::mixed()
260    }
261}