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}
29
30fn resolve_fn(ea: &ExpressionAnalyzer<'_>, fqn: &str) -> Option<ResolvedFn> {
31 let db = ea.db;
32 let node = db.lookup_function_node(fqn).filter(|n| n.active(db))?;
33 let inferred = node.inferred_return_type(db);
39 let return_ty_raw = node
40 .return_type(db)
41 .or(inferred)
42 .map(|t| (*t).clone())
43 .unwrap_or_else(Union::mixed);
44 Some(ResolvedFn {
45 fqn: node.fqn(db),
46 deprecated: node.deprecated(db),
47 params: node.params(db).to_vec(),
48 template_params: node.template_params(db).to_vec(),
49 assertions: node.assertions(db).to_vec(),
50 return_ty_raw,
51 })
52}
53
54impl CallAnalyzer {
55 pub fn analyze_function_call<'a, 'arena, 'src>(
56 ea: &mut ExpressionAnalyzer<'a>,
57 call: &FunctionCallExpr<'arena, 'src>,
58 ctx: &mut Context,
59 span: Span,
60 ) -> Union {
61 let fn_name = match &call.name.kind {
62 ExprKind::Identifier(name) => (*name).to_string(),
63 _ => {
64 let callee_ty = ea.analyze(call.name, ctx);
65 for arg in call.args.iter() {
66 ea.analyze(&arg.value, ctx);
67 }
68 for atomic in &callee_ty.types {
69 match atomic {
70 Atomic::TClosure { return_type, .. } => return *return_type.clone(),
71 Atomic::TCallable {
72 return_type: Some(rt),
73 ..
74 } => return *rt.clone(),
75 _ => {}
76 }
77 }
78 return Union::mixed();
79 }
80 };
81
82 if let Some(sink_kind) = classify_sink(&fn_name) {
84 for arg in call.args.iter() {
85 if is_expr_tainted(&arg.value, ctx) {
86 let issue_kind = match sink_kind {
87 SinkKind::Html => IssueKind::TaintedHtml,
88 SinkKind::Sql => IssueKind::TaintedSql,
89 SinkKind::Shell => IssueKind::TaintedShell,
90 };
91 ea.emit(issue_kind, Severity::Error, span);
92 break;
93 }
94 }
95 }
96
97 let fn_name = fn_name
100 .strip_prefix('\\')
101 .map(|s: &str| s.to_string())
102 .unwrap_or(fn_name);
103 let resolved_fn_name: String = {
104 let imports = ea.db.file_imports(&ea.file);
105 let qualified = if let Some(imported) = imports.get(fn_name.as_str()) {
106 imported.clone()
107 } else if fn_name.contains('\\') {
108 crate::db::resolve_name_via_db(ea.db, &ea.file, &fn_name)
109 } else if let Some(ns) = ea.db.file_namespace(&ea.file) {
110 format!("{}\\{}", ns, fn_name)
111 } else {
112 fn_name.clone()
113 };
114 let fn_exists = |name: &str| -> bool {
115 let db = ea.db;
116 db.lookup_function_node(name).is_some_and(|n| n.active(db))
117 };
118 if fn_exists(qualified.as_str()) {
119 qualified
120 } else if fn_exists(fn_name.as_str()) {
121 fn_name.clone()
122 } else {
123 qualified
124 }
125 };
126
127 let resolved = resolve_fn(ea, resolved_fn_name.as_str());
129
130 if let Some(ref resolved) = resolved {
132 for (i, param) in resolved.params.iter().enumerate() {
133 if param.is_byref {
134 if param.is_variadic {
135 for arg in call.args.iter().skip(i) {
136 if let ExprKind::Variable(name) = &arg.value.kind {
137 let var_name = name.as_str().trim_start_matches('$');
138 if !ctx.var_is_defined(var_name) {
139 ctx.set_var(var_name, Union::mixed());
140 }
141 }
142 }
143 } else if let Some(arg) = call.args.get(i) {
144 if let ExprKind::Variable(name) = &arg.value.kind {
145 let var_name = name.as_str().trim_start_matches('$');
146 if !ctx.var_is_defined(var_name) {
147 ctx.set_var(var_name, Union::mixed());
148 }
149 }
150 }
151 }
152 }
153 }
154
155 let arg_types: Vec<Union> = call
156 .args
157 .iter()
158 .map(|arg| {
159 let ty = ea.analyze(&arg.value, ctx);
160 if arg.unpack {
161 spread_element_type(&ty)
162 } else {
163 ty
164 }
165 })
166 .collect();
167
168 if matches!(
174 resolved_fn_name.as_str(),
175 "call_user_func" | "call_user_func_array"
176 ) {
177 if let Some(arg) = call.args.first() {
178 if let ExprKind::String(name) = &arg.value.kind {
179 let fqn = name.strip_prefix('\\').unwrap_or(name);
180 if let Some(node) = ea.db.lookup_function_node(fqn).filter(|n| n.active(ea.db))
181 {
182 if !ea.inference_only {
183 let (line, col_start, col_end) = ea.span_to_ref_loc(arg.span);
184 ea.db.record_reference_location(crate::db::RefLoc {
185 symbol_key: Arc::from(node.fqn(ea.db).as_ref()),
186 file: ea.file.clone(),
187 line,
188 col_start,
189 col_end,
190 });
191 }
192 }
193 }
194 }
195 }
196
197 if fn_name == "compact" {
199 for arg in call.args.iter() {
200 if let ExprKind::String(name) = &arg.value.kind {
201 ctx.read_vars.insert((*name).to_string());
202 }
203 }
204 }
205
206 if let Some(resolved) = resolved {
207 if !ea.inference_only {
208 let (line, col_start, col_end) = ea.span_to_ref_loc(call.name.span);
209 ea.db.record_reference_location(crate::db::RefLoc {
210 symbol_key: resolved.fqn.clone(),
211 file: ea.file.clone(),
212 line,
213 col_start,
214 col_end,
215 });
216 }
217 let deprecated = resolved.deprecated;
218 let params = resolved.params;
219 let template_params = resolved.template_params;
220 let return_ty_raw = resolved.return_ty_raw;
221
222 if let Some(msg) = deprecated {
223 ea.emit(
224 IssueKind::DeprecatedCall {
225 name: resolved_fn_name.clone(),
226 message: Some(msg).filter(|m| !m.is_empty()),
227 },
228 Severity::Info,
229 span,
230 );
231 }
232
233 check_args(
234 ea,
235 CheckArgsParams {
236 fn_name: &fn_name,
237 params: ¶ms,
238 arg_types: &arg_types,
239 arg_spans: &call.args.iter().map(|a| a.span).collect::<Vec<_>>(),
240 arg_names: &call
241 .args
242 .iter()
243 .map(|a| a.name.as_ref().map(|n| n.to_string_repr().into_owned()))
244 .collect::<Vec<_>>(),
245 arg_can_be_byref: &call
246 .args
247 .iter()
248 .map(|a| expr_can_be_passed_by_reference(&a.value))
249 .collect::<Vec<_>>(),
250 call_span: span,
251 has_spread: call.args.iter().any(|a| a.unpack),
252 },
253 );
254
255 for (i, param) in params.iter().enumerate() {
256 if param.is_byref {
257 if param.is_variadic {
258 for arg in call.args.iter().skip(i) {
259 if let ExprKind::Variable(name) = &arg.value.kind {
260 let var_name = name.as_str().trim_start_matches('$');
261 ctx.set_var(var_name, Union::mixed());
262 }
263 }
264 } else if let Some(arg) = call.args.get(i) {
265 if let ExprKind::Variable(name) = &arg.value.kind {
266 let var_name = name.as_str().trim_start_matches('$');
267 ctx.set_var(var_name, Union::mixed());
268 }
269 }
270 }
271 }
272
273 let template_bindings = if !template_params.is_empty() {
274 let bindings = infer_template_bindings(&template_params, ¶ms, &arg_types);
275 for (name, inferred, bound) in check_template_bounds(&bindings, &template_params) {
276 ea.emit(
277 IssueKind::InvalidTemplateParam {
278 name: name.to_string(),
279 expected_bound: format!("{bound}"),
280 actual: format!("{inferred}"),
281 },
282 Severity::Error,
283 span,
284 );
285 }
286 Some(bindings)
287 } else {
288 None
289 };
290
291 for assertion in resolved
292 .assertions
293 .iter()
294 .filter(|a| a.kind == AssertionKind::Assert)
295 {
296 if let Some(index) = params.iter().position(|p| p.name == assertion.param) {
297 if let Some(arg) = call.args.get(index) {
298 if let ExprKind::Variable(name) = &arg.value.kind {
299 let asserted_ty = match &template_bindings {
300 Some(b) => assertion.ty.substitute_templates(b),
301 None => assertion.ty.clone(),
302 };
303 ctx.set_var(name.as_str().trim_start_matches('$'), asserted_ty);
304 }
305 }
306 }
307 }
308
309 let return_ty = match &template_bindings {
310 Some(bindings) => return_ty_raw.substitute_templates(bindings),
311 None => return_ty_raw,
312 };
313
314 ea.record_symbol(
315 call.name.span,
316 SymbolKind::FunctionCall(resolved.fqn.clone()),
317 return_ty.clone(),
318 );
319 return return_ty;
320 }
321
322 ea.emit(
323 IssueKind::UndefinedFunction { name: fn_name },
324 Severity::Error,
325 span,
326 );
327 Union::mixed()
328 }
329}