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::{check_args, spread_element_type, CheckArgsParams};
15use super::CallAnalyzer;
16
17impl CallAnalyzer {
18 pub fn analyze_function_call<'a, 'arena, 'src>(
19 ea: &mut ExpressionAnalyzer<'a>,
20 call: &FunctionCallExpr<'arena, 'src>,
21 ctx: &mut Context,
22 span: Span,
23 ) -> Union {
24 let fn_name = match &call.name.kind {
25 ExprKind::Identifier(name) => (*name).to_string(),
26 _ => {
27 ea.analyze(call.name, ctx);
28 for arg in call.args.iter() {
29 ea.analyze(&arg.value, ctx);
30 }
31 return Union::mixed();
32 }
33 };
34
35 if let Some(sink_kind) = classify_sink(&fn_name) {
37 for arg in call.args.iter() {
38 if is_expr_tainted(&arg.value, ctx) {
39 let issue_kind = match sink_kind {
40 SinkKind::Html => IssueKind::TaintedHtml,
41 SinkKind::Sql => IssueKind::TaintedSql,
42 SinkKind::Shell => IssueKind::TaintedShell,
43 };
44 ea.emit(issue_kind, Severity::Error, span);
45 break;
46 }
47 }
48 }
49
50 let fn_name = fn_name
53 .strip_prefix('\\')
54 .map(|s: &str| s.to_string())
55 .unwrap_or(fn_name);
56 let resolved_fn_name: String = {
57 let qualified = ea.codebase.resolve_class_name(&ea.file, &fn_name);
58 if ea.codebase.functions.contains_key(qualified.as_str()) {
59 qualified
60 } else if ea.codebase.functions.contains_key(fn_name.as_str()) {
61 fn_name.clone()
62 } else {
63 qualified
64 }
65 };
66
67 if let Some(func) = ea.codebase.functions.get(resolved_fn_name.as_str()) {
69 for (i, param) in func.params.iter().enumerate() {
70 if param.is_byref {
71 if param.is_variadic {
72 for arg in call.args.iter().skip(i) {
73 if let ExprKind::Variable(name) = &arg.value.kind {
74 let var_name = name.as_str().trim_start_matches('$');
75 if !ctx.var_is_defined(var_name) {
76 ctx.set_var(var_name, Union::mixed());
77 }
78 }
79 }
80 } else if let Some(arg) = call.args.get(i) {
81 if let ExprKind::Variable(name) = &arg.value.kind {
82 let var_name = name.as_str().trim_start_matches('$');
83 if !ctx.var_is_defined(var_name) {
84 ctx.set_var(var_name, Union::mixed());
85 }
86 }
87 }
88 }
89 }
90 }
91
92 let arg_types: Vec<Union> = call
93 .args
94 .iter()
95 .map(|arg| {
96 let ty = ea.analyze(&arg.value, ctx);
97 if arg.unpack {
98 spread_element_type(&ty)
99 } else {
100 ty
101 }
102 })
103 .collect();
104
105 if let Some(func) = ea.codebase.functions.get(resolved_fn_name.as_str()) {
106 let name_span = call.name.span;
107 ea.codebase.mark_function_referenced_at(
108 &func.fqn,
109 ea.file.clone(),
110 name_span.start,
111 name_span.end,
112 );
113 let deprecated = func.deprecated.clone();
114 let params = func.params.clone();
115 let template_params = func.template_params.clone();
116 let return_ty_raw = func
117 .effective_return_type()
118 .cloned()
119 .unwrap_or_else(Union::mixed);
120
121 if let Some(msg) = deprecated {
122 ea.emit(
123 IssueKind::DeprecatedCall {
124 name: resolved_fn_name.clone(),
125 message: Some(msg).filter(|m| !m.is_empty()),
126 },
127 Severity::Info,
128 span,
129 );
130 }
131
132 check_args(
133 ea,
134 CheckArgsParams {
135 fn_name: &fn_name,
136 params: ¶ms,
137 arg_types: &arg_types,
138 arg_spans: &call.args.iter().map(|a| a.span).collect::<Vec<_>>(),
139 arg_names: &call
140 .args
141 .iter()
142 .map(|a| a.name.as_ref().map(|n| n.to_string_repr().into_owned()))
143 .collect::<Vec<_>>(),
144 call_span: span,
145 has_spread: call.args.iter().any(|a| a.unpack),
146 },
147 );
148
149 for (i, param) in params.iter().enumerate() {
150 if param.is_byref {
151 if param.is_variadic {
152 for arg in call.args.iter().skip(i) {
153 if let ExprKind::Variable(name) = &arg.value.kind {
154 let var_name = name.as_str().trim_start_matches('$');
155 ctx.set_var(var_name, Union::mixed());
156 }
157 }
158 } else if let Some(arg) = call.args.get(i) {
159 if let ExprKind::Variable(name) = &arg.value.kind {
160 let var_name = name.as_str().trim_start_matches('$');
161 ctx.set_var(var_name, Union::mixed());
162 }
163 }
164 }
165 }
166
167 for assertion in func
168 .assertions
169 .iter()
170 .filter(|a| a.kind == AssertionKind::Assert)
171 {
172 if let Some(index) = params.iter().position(|p| p.name == assertion.param) {
173 if let Some(arg) = call.args.get(index) {
174 if let ExprKind::Variable(name) = &arg.value.kind {
175 ctx.set_var(
176 name.as_str().trim_start_matches('$'),
177 assertion.ty.clone(),
178 );
179 }
180 }
181 }
182 }
183
184 let return_ty = if !template_params.is_empty() {
185 let bindings = infer_template_bindings(&template_params, ¶ms, &arg_types);
186 for (name, inferred, bound) in check_template_bounds(&bindings, &template_params) {
187 ea.emit(
188 IssueKind::InvalidTemplateParam {
189 name: name.to_string(),
190 expected_bound: format!("{}", bound),
191 actual: format!("{}", inferred),
192 },
193 Severity::Error,
194 span,
195 );
196 }
197 return_ty_raw.substitute_templates(&bindings)
198 } else {
199 return_ty_raw
200 };
201
202 ea.record_symbol(
203 call.name.span,
204 SymbolKind::FunctionCall(func.fqn.clone()),
205 return_ty.clone(),
206 );
207 return return_ty;
208 }
209
210 ea.emit(
211 IssueKind::UndefinedFunction { name: fn_name },
212 Severity::Error,
213 span,
214 );
215 Union::mixed()
216 }
217}