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