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 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 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 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 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: ¶ms,
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, ¶ms, &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}