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        if let Some(func) = ea.codebase.functions.get(resolved_fn_name.as_str()) {
108            let name_span = call.name.span;
109            ea.codebase.mark_function_referenced_at(
110                &func.fqn,
111                ea.file.clone(),
112                name_span.start,
113                name_span.end,
114            );
115            let deprecated = func.deprecated.clone();
116            let params = func.params.clone();
117            let template_params = func.template_params.clone();
118            let return_ty_raw = func
119                .effective_return_type()
120                .cloned()
121                .unwrap_or_else(Union::mixed);
122
123            if let Some(msg) = deprecated {
124                ea.emit(
125                    IssueKind::DeprecatedCall {
126                        name: resolved_fn_name.clone(),
127                        message: Some(msg).filter(|m| !m.is_empty()),
128                    },
129                    Severity::Info,
130                    span,
131                );
132            }
133
134            check_args(
135                ea,
136                CheckArgsParams {
137                    fn_name: &fn_name,
138                    params: &params,
139                    arg_types: &arg_types,
140                    arg_spans: &call.args.iter().map(|a| a.span).collect::<Vec<_>>(),
141                    arg_names: &call
142                        .args
143                        .iter()
144                        .map(|a| a.name.as_ref().map(|n| n.to_string_repr().into_owned()))
145                        .collect::<Vec<_>>(),
146                    arg_can_be_byref: &call
147                        .args
148                        .iter()
149                        .map(|a| expr_can_be_passed_by_reference(&a.value))
150                        .collect::<Vec<_>>(),
151                    call_span: span,
152                    has_spread: call.args.iter().any(|a| a.unpack),
153                },
154            );
155
156            for (i, param) in params.iter().enumerate() {
157                if param.is_byref {
158                    if param.is_variadic {
159                        for arg in call.args.iter().skip(i) {
160                            if let ExprKind::Variable(name) = &arg.value.kind {
161                                let var_name = name.as_str().trim_start_matches('$');
162                                ctx.set_var(var_name, Union::mixed());
163                            }
164                        }
165                    } else if let Some(arg) = call.args.get(i) {
166                        if let ExprKind::Variable(name) = &arg.value.kind {
167                            let var_name = name.as_str().trim_start_matches('$');
168                            ctx.set_var(var_name, Union::mixed());
169                        }
170                    }
171                }
172            }
173
174            for assertion in func
175                .assertions
176                .iter()
177                .filter(|a| a.kind == AssertionKind::Assert)
178            {
179                if let Some(index) = params.iter().position(|p| p.name == assertion.param) {
180                    if let Some(arg) = call.args.get(index) {
181                        if let ExprKind::Variable(name) = &arg.value.kind {
182                            ctx.set_var(
183                                name.as_str().trim_start_matches('$'),
184                                assertion.ty.clone(),
185                            );
186                        }
187                    }
188                }
189            }
190
191            let return_ty = if !template_params.is_empty() {
192                let bindings = infer_template_bindings(&template_params, &params, &arg_types);
193                for (name, inferred, bound) in check_template_bounds(&bindings, &template_params) {
194                    ea.emit(
195                        IssueKind::InvalidTemplateParam {
196                            name: name.to_string(),
197                            expected_bound: format!("{bound}"),
198                            actual: format!("{inferred}"),
199                        },
200                        Severity::Error,
201                        span,
202                    );
203                }
204                return_ty_raw.substitute_templates(&bindings)
205            } else {
206                return_ty_raw
207            };
208
209            ea.record_symbol(
210                call.name.span,
211                SymbolKind::FunctionCall(func.fqn.clone()),
212                return_ty.clone(),
213            );
214            return return_ty;
215        }
216
217        ea.emit(
218            IssueKind::UndefinedFunction { name: fn_name },
219            Severity::Error,
220            span,
221        );
222        Union::mixed()
223    }
224}