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::{check_args, spread_element_type, CheckArgsParams};
15use super::CallAnalyzer;
16
17impl CallAnalyzer {
18    pub fn analyze_function_call<'a, 'arena, 'src>(
19        ea: &mut ExpressionAnalyzer<'a>,
20        call: &FunctionCallExpr<'arena, 'src>,
21        ctx: &mut Context,
22        span: Span,
23    ) -> Union {
24        let fn_name = match &call.name.kind {
25            ExprKind::Identifier(name) => (*name).to_string(),
26            _ => {
27                ea.analyze(call.name, ctx);
28                for arg in call.args.iter() {
29                    ea.analyze(&arg.value, ctx);
30                }
31                return Union::mixed();
32            }
33        };
34
35        // Taint sink check (M19): before evaluating args so we can inspect raw exprs
36        if let Some(sink_kind) = classify_sink(&fn_name) {
37            for arg in call.args.iter() {
38                if is_expr_tainted(&arg.value, ctx) {
39                    let issue_kind = match sink_kind {
40                        SinkKind::Html => IssueKind::TaintedHtml,
41                        SinkKind::Sql => IssueKind::TaintedSql,
42                        SinkKind::Shell => IssueKind::TaintedShell,
43                    };
44                    ea.emit(issue_kind, Severity::Error, span);
45                    break;
46                }
47            }
48        }
49
50        // PHP resolves `foo()` as `\App\Ns\foo` first, then `\foo` if not found.
51        // A leading `\` means explicit global namespace.
52        let fn_name = fn_name
53            .strip_prefix('\\')
54            .map(|s: &str| s.to_string())
55            .unwrap_or(fn_name);
56        let resolved_fn_name: String = {
57            let qualified = ea.codebase.resolve_class_name(&ea.file, &fn_name);
58            if ea.codebase.functions.contains_key(qualified.as_str()) {
59                qualified
60            } else if ea.codebase.functions.contains_key(fn_name.as_str()) {
61                fn_name.clone()
62            } else {
63                qualified
64            }
65        };
66
67        // Pre-mark by-reference parameter variables as defined BEFORE evaluating args
68        if let Some(func) = ea.codebase.functions.get(resolved_fn_name.as_str()) {
69            for (i, param) in func.params.iter().enumerate() {
70                if param.is_byref {
71                    if param.is_variadic {
72                        for arg in call.args.iter().skip(i) {
73                            if let ExprKind::Variable(name) = &arg.value.kind {
74                                let var_name = name.as_str().trim_start_matches('$');
75                                if !ctx.var_is_defined(var_name) {
76                                    ctx.set_var(var_name, Union::mixed());
77                                }
78                            }
79                        }
80                    } else if let Some(arg) = call.args.get(i) {
81                        if let ExprKind::Variable(name) = &arg.value.kind {
82                            let var_name = name.as_str().trim_start_matches('$');
83                            if !ctx.var_is_defined(var_name) {
84                                ctx.set_var(var_name, Union::mixed());
85                            }
86                        }
87                    }
88                }
89            }
90        }
91
92        let arg_types: Vec<Union> = call
93            .args
94            .iter()
95            .map(|arg| {
96                let ty = ea.analyze(&arg.value, ctx);
97                if arg.unpack {
98                    spread_element_type(&ty)
99                } else {
100                    ty
101                }
102            })
103            .collect();
104
105        if let Some(func) = ea.codebase.functions.get(resolved_fn_name.as_str()) {
106            let name_span = call.name.span;
107            ea.codebase.mark_function_referenced_at(
108                &func.fqn,
109                ea.file.clone(),
110                name_span.start,
111                name_span.end,
112            );
113            let deprecated = func.deprecated.clone();
114            let params = func.params.clone();
115            let template_params = func.template_params.clone();
116            let return_ty_raw = func
117                .effective_return_type()
118                .cloned()
119                .unwrap_or_else(Union::mixed);
120
121            if let Some(msg) = deprecated {
122                ea.emit(
123                    IssueKind::DeprecatedCall {
124                        name: resolved_fn_name.clone(),
125                        message: Some(msg).filter(|m| !m.is_empty()),
126                    },
127                    Severity::Info,
128                    span,
129                );
130            }
131
132            check_args(
133                ea,
134                CheckArgsParams {
135                    fn_name: &fn_name,
136                    params: &params,
137                    arg_types: &arg_types,
138                    arg_spans: &call.args.iter().map(|a| a.span).collect::<Vec<_>>(),
139                    arg_names: &call
140                        .args
141                        .iter()
142                        .map(|a| a.name.as_ref().map(|n| n.to_string_repr().into_owned()))
143                        .collect::<Vec<_>>(),
144                    call_span: span,
145                    has_spread: call.args.iter().any(|a| a.unpack),
146                },
147            );
148
149            for (i, param) in params.iter().enumerate() {
150                if param.is_byref {
151                    if param.is_variadic {
152                        for arg in call.args.iter().skip(i) {
153                            if let ExprKind::Variable(name) = &arg.value.kind {
154                                let var_name = name.as_str().trim_start_matches('$');
155                                ctx.set_var(var_name, Union::mixed());
156                            }
157                        }
158                    } else if let Some(arg) = call.args.get(i) {
159                        if let ExprKind::Variable(name) = &arg.value.kind {
160                            let var_name = name.as_str().trim_start_matches('$');
161                            ctx.set_var(var_name, Union::mixed());
162                        }
163                    }
164                }
165            }
166
167            for assertion in func
168                .assertions
169                .iter()
170                .filter(|a| a.kind == AssertionKind::Assert)
171            {
172                if let Some(index) = params.iter().position(|p| p.name == assertion.param) {
173                    if let Some(arg) = call.args.get(index) {
174                        if let ExprKind::Variable(name) = &arg.value.kind {
175                            ctx.set_var(
176                                name.as_str().trim_start_matches('$'),
177                                assertion.ty.clone(),
178                            );
179                        }
180                    }
181                }
182            }
183
184            let return_ty = if !template_params.is_empty() {
185                let bindings = infer_template_bindings(&template_params, &params, &arg_types);
186                for (name, inferred, bound) in check_template_bounds(&bindings, &template_params) {
187                    ea.emit(
188                        IssueKind::InvalidTemplateParam {
189                            name: name.to_string(),
190                            expected_bound: format!("{}", bound),
191                            actual: format!("{}", inferred),
192                        },
193                        Severity::Error,
194                        span,
195                    );
196                }
197                return_ty_raw.substitute_templates(&bindings)
198            } else {
199                return_ty_raw
200            };
201
202            ea.record_symbol(
203                call.name.span,
204                SymbolKind::FunctionCall(func.fqn.clone()),
205                return_ty.clone(),
206            );
207            return return_ty;
208        }
209
210        ea.emit(
211            IssueKind::UndefinedFunction { name: fn_name },
212            Severity::Error,
213            span,
214        );
215        Union::mixed()
216    }
217}