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