1use php_ast::ast::{ExprKind, FunctionCallExpr};
2use php_ast::Span;
3
4use std::sync::Arc;
5
6use mir_codebase::storage::{Assertion, AssertionKind, FnParam, TemplateParam};
7use mir_issues::{IssueKind, Severity};
8use mir_types::{Atomic, Union};
9
10use crate::context::Context;
11use crate::expr::ExpressionAnalyzer;
12use crate::generic::{check_template_bounds, infer_template_bindings};
13use crate::symbol::SymbolKind;
14use crate::taint::{classify_sink, is_expr_tainted, SinkKind};
15
16use super::args::{
17 check_args, expr_can_be_passed_by_reference, spread_element_type, CheckArgsParams,
18};
19use super::CallAnalyzer;
20
21struct ResolvedFn {
22 fqn: std::sync::Arc<str>,
23 deprecated: Option<std::sync::Arc<str>>,
24 params: Vec<FnParam>,
25 template_params: Vec<TemplateParam>,
26 assertions: Vec<Assertion>,
27 return_ty_raw: Union,
28 throws: Arc<[Arc<str>]>,
29}
30
31fn resolve_fn(ea: &ExpressionAnalyzer<'_>, fqn: &str) -> Option<ResolvedFn> {
32 let db = ea.db;
33 let node = db.lookup_function_node(fqn).filter(|n| n.active(db))?;
34 let inferred = node.inferred_return_type(db);
40 let return_ty_raw = node
41 .return_type(db)
42 .or(inferred)
43 .map(|t| (*t).clone())
44 .unwrap_or_else(Union::mixed);
45 Some(ResolvedFn {
46 fqn: node.fqn(db),
47 deprecated: node.deprecated(db),
48 params: node.params(db).to_vec(),
49 template_params: node.template_params(db).to_vec(),
50 assertions: node.assertions(db).to_vec(),
51 return_ty_raw,
52 throws: node.throws(db),
53 })
54}
55
56impl CallAnalyzer {
57 pub fn analyze_function_call<'a, 'arena, 'src>(
58 ea: &mut ExpressionAnalyzer<'a>,
59 call: &FunctionCallExpr<'arena, 'src>,
60 ctx: &mut Context,
61 span: Span,
62 ) -> Union {
63 let fn_name = match &call.name.kind {
64 ExprKind::Identifier(name) => (*name).to_string(),
65 _ => {
66 let callee_ty = ea.analyze(call.name, ctx);
67 for arg in call.args.iter() {
68 ea.analyze(&arg.value, ctx);
69 }
70 for atomic in &callee_ty.types {
71 match atomic {
72 Atomic::TClosure { return_type, .. } => return *return_type.clone(),
73 Atomic::TCallable {
74 return_type: Some(rt),
75 ..
76 } => return *rt.clone(),
77 _ => {}
78 }
79 }
80 return Union::mixed();
81 }
82 };
83
84 if let Some(sink_kind) = classify_sink(&fn_name) {
86 for arg in call.args.iter() {
87 if is_expr_tainted(&arg.value, ctx) {
88 let issue_kind = match sink_kind {
89 SinkKind::Html => IssueKind::TaintedHtml,
90 SinkKind::Sql => IssueKind::TaintedSql,
91 SinkKind::Shell => IssueKind::TaintedShell,
92 };
93 ea.emit(issue_kind, Severity::Error, span);
94 break;
95 }
96 }
97 }
98
99 let fn_name = fn_name
102 .strip_prefix('\\')
103 .map(|s: &str| s.to_string())
104 .unwrap_or(fn_name);
105 let resolved_fn_name: String = {
106 let imports = ea.db.file_imports(&ea.file);
107 let qualified = if let Some(imported) = imports.get(fn_name.as_str()) {
108 imported.clone()
109 } else if fn_name.contains('\\') {
110 crate::db::resolve_name_via_db(ea.db, &ea.file, &fn_name)
111 } else if let Some(ns) = ea.db.file_namespace(&ea.file) {
112 format!("{}\\{}", ns, fn_name)
113 } else {
114 fn_name.clone()
115 };
116 let fn_exists = |name: &str| -> bool {
117 let db = ea.db;
118 db.lookup_function_node(name).is_some_and(|n| n.active(db))
119 };
120 if fn_exists(qualified.as_str()) {
121 qualified
122 } else if fn_exists(fn_name.as_str()) {
123 fn_name.clone()
124 } else {
125 qualified
126 }
127 };
128
129 let resolved = resolve_fn(ea, resolved_fn_name.as_str());
131
132 if let Some(ref resolved) = resolved {
134 for (i, param) in resolved.params.iter().enumerate() {
135 if param.is_byref {
136 if param.is_variadic {
137 for arg in call.args.iter().skip(i) {
138 if let ExprKind::Variable(name) = &arg.value.kind {
139 let var_name = name.as_str().trim_start_matches('$');
140 if !ctx.var_is_defined(var_name) {
141 ctx.set_var(var_name, Union::mixed());
142 }
143 }
144 }
145 } else if let Some(arg) = call.args.get(i) {
146 if let ExprKind::Variable(name) = &arg.value.kind {
147 let var_name = name.as_str().trim_start_matches('$');
148 if !ctx.var_is_defined(var_name) {
149 ctx.set_var(var_name, Union::mixed());
150 }
151 }
152 }
153 }
154 }
155 }
156
157 let arg_types: Vec<Union> = call
158 .args
159 .iter()
160 .map(|arg| {
161 let ty = ea.analyze(&arg.value, ctx);
162 if arg.unpack {
163 spread_element_type(&ty)
164 } else {
165 ty
166 }
167 })
168 .collect();
169
170 let arg_spans: Vec<Span> = call.args.iter().map(|a| a.span).collect();
171
172 if matches!(
178 resolved_fn_name.as_str(),
179 "call_user_func" | "call_user_func_array"
180 ) {
181 if let Some(arg) = call.args.first() {
182 if let ExprKind::String(name) = &arg.value.kind {
183 let fqn = name.strip_prefix('\\').unwrap_or(name);
184 if let Some(node) = ea.db.lookup_function_node(fqn).filter(|n| n.active(ea.db))
185 {
186 if !ea.inference_only {
187 let (line, col_start, col_end) = ea.span_to_ref_loc(arg.span);
188 ea.db.record_reference_location(crate::db::RefLoc {
189 symbol_key: Arc::from(node.fqn(ea.db).as_ref()),
190 file: ea.file.clone(),
191 line,
192 col_start,
193 col_end,
194 });
195 }
196 }
197 }
198 }
199 }
200
201 if fn_name == "compact" {
203 for arg in call.args.iter() {
204 if let ExprKind::String(name) = &arg.value.kind {
205 ctx.read_vars.insert((*name).to_string());
206 }
207 }
208 }
209
210 if let Some(resolved) = resolved {
211 if !ea.inference_only {
212 let (line, col_start, col_end) = ea.span_to_ref_loc(call.name.span);
213 ea.db.record_reference_location(crate::db::RefLoc {
214 symbol_key: resolved.fqn.clone(),
215 file: ea.file.clone(),
216 line,
217 col_start,
218 col_end,
219 });
220 }
221 let deprecated = resolved.deprecated;
222 let params = resolved.params;
223 let template_params = resolved.template_params;
224 let return_ty_raw = resolved.return_ty_raw;
225
226 if let Some(msg) = deprecated {
227 ea.emit(
228 IssueKind::DeprecatedCall {
229 name: resolved_fn_name.clone(),
230 message: Some(msg).filter(|m| !m.is_empty()),
231 },
232 Severity::Info,
233 span,
234 );
235 }
236
237 check_args(
238 ea,
239 CheckArgsParams {
240 fn_name: &fn_name,
241 params: ¶ms,
242 arg_types: &arg_types,
243 arg_spans: &arg_spans,
244 arg_names: &call
245 .args
246 .iter()
247 .map(|a| a.name.as_ref().map(|n| n.to_string_repr().into_owned()))
248 .collect::<Vec<_>>(),
249 arg_can_be_byref: &call
250 .args
251 .iter()
252 .map(|a| expr_can_be_passed_by_reference(&a.value))
253 .collect::<Vec<_>>(),
254 call_span: span,
255 has_spread: call.args.iter().any(|a| a.unpack),
256 },
257 );
258
259 match resolved_fn_name.as_str() {
260 "array_map" => {
261 super::callable::check_array_map_callback(ea, &arg_types, &arg_spans)
262 }
263 "array_filter" => {
264 super::callable::check_array_filter_callback(ea, &arg_types, &arg_spans)
265 }
266 "array_reduce" => {
267 super::callable::check_array_reduce_callback(ea, &arg_types, &arg_spans)
268 }
269 "usort" | "uasort" | "uksort" | "array_walk" | "array_walk_recursive" => {
270 super::callable::check_sort_callback(
271 ea,
272 &resolved_fn_name,
273 &arg_types,
274 &arg_spans,
275 )
276 }
277 _ => {}
278 }
279
280 for (i, param) in params.iter().enumerate() {
281 if param.is_byref {
282 if param.is_variadic {
283 for arg in call.args.iter().skip(i) {
284 if let ExprKind::Variable(name) = &arg.value.kind {
285 let var_name = name.as_str().trim_start_matches('$');
286 ctx.set_var(var_name, Union::mixed());
287 }
288 }
289 } else if let Some(arg) = call.args.get(i) {
290 if let ExprKind::Variable(name) = &arg.value.kind {
291 let var_name = name.as_str().trim_start_matches('$');
292 ctx.set_var(var_name, Union::mixed());
293 }
294 }
295 }
296 }
297
298 let template_bindings = if !template_params.is_empty() {
299 let bindings = infer_template_bindings(&template_params, ¶ms, &arg_types);
300 for (name, inferred, bound) in check_template_bounds(&bindings, &template_params) {
301 ea.emit(
302 IssueKind::InvalidTemplateParam {
303 name: name.to_string(),
304 expected_bound: format!("{bound}"),
305 actual: format!("{inferred}"),
306 },
307 Severity::Error,
308 span,
309 );
310 }
311 Some(bindings)
312 } else {
313 None
314 };
315
316 for assertion in resolved
317 .assertions
318 .iter()
319 .filter(|a| a.kind == AssertionKind::Assert)
320 {
321 if let Some(index) = params.iter().position(|p| p.name == assertion.param) {
322 if let Some(arg) = call.args.get(index) {
323 if let ExprKind::Variable(name) = &arg.value.kind {
324 let asserted_ty = match &template_bindings {
325 Some(b) => assertion.ty.substitute_templates(b),
326 None => assertion.ty.clone(),
327 };
328 ctx.set_var(name.as_str().trim_start_matches('$'), asserted_ty);
329 }
330 }
331 }
332 }
333
334 let return_ty = match &template_bindings {
335 Some(bindings) => return_ty_raw.substitute_templates(bindings),
336 None => return_ty_raw,
337 };
338
339 for callee_throw in resolved.throws.iter() {
341 if !ctx.fn_declared_throws.iter().any(|declared| {
342 declared.as_ref() == callee_throw.as_ref()
343 || crate::db::extends_or_implements_via_db(
344 ea.db,
345 callee_throw.as_ref(),
346 declared.as_ref(),
347 )
348 }) {
349 ea.emit(
350 IssueKind::MissingThrowsDocblock {
351 class: callee_throw.to_string(),
352 },
353 Severity::Info,
354 span,
355 );
356 }
357 }
358
359 ea.record_symbol(
360 call.name.span,
361 SymbolKind::FunctionCall(resolved.fqn.clone()),
362 return_ty.clone(),
363 );
364 return return_ty;
365 }
366
367 ea.emit(
368 IssueKind::UndefinedFunction { name: fn_name },
369 Severity::Error,
370 span,
371 );
372 Union::mixed()
373 }
374}