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