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